207 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2022 New Vector Ltd.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
import * as fs from "fs";
 | 
						|
import * as childProcess from "child_process";
 | 
						|
import * as semver from "semver";
 | 
						|
 | 
						|
import { BuildConfig } from "./BuildConfig";
 | 
						|
 | 
						|
// This expects to be run from ./scripts/install.ts
 | 
						|
 | 
						|
const moduleApiDepName = "@matrix-org/react-sdk-module-api";
 | 
						|
 | 
						|
const MODULES_TS_HEADER = `
 | 
						|
/*
 | 
						|
 * THIS FILE IS AUTO-GENERATED
 | 
						|
 * You can edit it you like, but your changes will be overwritten,
 | 
						|
 * so you'd just be trying to swim upstream like a salmon.
 | 
						|
 * You are not a salmon.
 | 
						|
 */
 | 
						|
 | 
						|
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
 | 
						|
`;
 | 
						|
const MODULES_TS_DEFINITIONS = `
 | 
						|
export const INSTALLED_MODULES: RuntimeModule[] = [];
 | 
						|
`;
 | 
						|
 | 
						|
export function installer(config: BuildConfig): void {
 | 
						|
    if (!config.modules?.length) {
 | 
						|
        // nothing to do
 | 
						|
        writeModulesTs(MODULES_TS_HEADER + MODULES_TS_DEFINITIONS);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    let exitCode = 0;
 | 
						|
 | 
						|
    // We cheat a bit and store the current package.json and lockfile so we can safely
 | 
						|
    // run `yarn add` without creating extra committed files for people. We restore
 | 
						|
    // these files by simply overwriting them when we're done.
 | 
						|
    const packageDeps = readCurrentPackageDetails();
 | 
						|
 | 
						|
    // Record which optional dependencies there are currently, if any, so we can exclude
 | 
						|
    // them from our "must be a module" assumption later on.
 | 
						|
    const currentOptDeps = getOptionalDepNames(packageDeps.packageJson);
 | 
						|
 | 
						|
    try {
 | 
						|
        // Install the modules with yarn
 | 
						|
        const yarnAddRef = config.modules.join(" ");
 | 
						|
        callYarnAdd(yarnAddRef); // install them all at once
 | 
						|
 | 
						|
        // Grab the optional dependencies again and exclude what was there already. Everything
 | 
						|
        // else must be a module, we assume.
 | 
						|
        const pkgJsonStr = fs.readFileSync("./package.json", "utf-8");
 | 
						|
        const optionalDepNames = getOptionalDepNames(pkgJsonStr);
 | 
						|
        const installedModules = optionalDepNames.filter((d) => !currentOptDeps.includes(d));
 | 
						|
 | 
						|
        // Ensure all the modules are compatible. We check them all and report at the end to
 | 
						|
        // try and save the user some time debugging this sort of failure.
 | 
						|
        const ourApiVersion = getTopLevelDependencyVersion(moduleApiDepName);
 | 
						|
        const incompatibleNames: string[] = [];
 | 
						|
        for (const moduleName of installedModules) {
 | 
						|
            const modApiVersion = getModuleApiVersionFor(moduleName);
 | 
						|
            if (!isModuleVersionCompatible(ourApiVersion, modApiVersion)) {
 | 
						|
                incompatibleNames.push(moduleName);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (incompatibleNames.length > 0) {
 | 
						|
            console.error(
 | 
						|
                "The following modules are not compatible with this version of element-web. Please update the module " +
 | 
						|
                    "references and try again.",
 | 
						|
                JSON.stringify(incompatibleNames, null, 4), // stringify to get prettier/complete output
 | 
						|
            );
 | 
						|
            exitCode = 1;
 | 
						|
            return; // hit the finally{} block before exiting
 | 
						|
        }
 | 
						|
 | 
						|
        // If we reach here, everything seems fine. Write modules.ts and log some output
 | 
						|
        // Note: we compile modules.ts in two parts for developer friendliness if they
 | 
						|
        // happen to look at it.
 | 
						|
        console.log("The following modules have been installed: ", installedModules);
 | 
						|
        let modulesTsHeader = MODULES_TS_HEADER;
 | 
						|
        let modulesTsDefs = MODULES_TS_DEFINITIONS;
 | 
						|
        let index = 0;
 | 
						|
        for (const moduleName of installedModules) {
 | 
						|
            const importName = `Module${++index}`;
 | 
						|
            modulesTsHeader += `import ${importName} from "${moduleName}";\n`;
 | 
						|
            modulesTsDefs += `INSTALLED_MODULES.push(${importName});\n`;
 | 
						|
        }
 | 
						|
        writeModulesTs(modulesTsHeader + modulesTsDefs);
 | 
						|
        console.log("Done installing modules");
 | 
						|
    } finally {
 | 
						|
        // Always restore package details (or at least try to)
 | 
						|
        writePackageDetails(packageDeps);
 | 
						|
 | 
						|
        if (exitCode > 0) {
 | 
						|
            process.exit(exitCode);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
type RawDependencies = {
 | 
						|
    lockfile: string;
 | 
						|
    packageJson: string;
 | 
						|
};
 | 
						|
 | 
						|
function readCurrentPackageDetails(): RawDependencies {
 | 
						|
    return {
 | 
						|
        lockfile: fs.readFileSync("./yarn.lock", "utf-8"),
 | 
						|
        packageJson: fs.readFileSync("./package.json", "utf-8"),
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function writePackageDetails(deps: RawDependencies): void {
 | 
						|
    fs.writeFileSync("./yarn.lock", deps.lockfile, "utf-8");
 | 
						|
    fs.writeFileSync("./package.json", deps.packageJson, "utf-8");
 | 
						|
}
 | 
						|
 | 
						|
function callYarnAdd(dep: string): void {
 | 
						|
    // Add the module to the optional dependencies section just in case something
 | 
						|
    // goes wrong in restoring the original package details.
 | 
						|
    childProcess.execSync(`yarn add -O ${dep}`, {
 | 
						|
        env: process.env,
 | 
						|
        stdio: ["inherit", "inherit", "inherit"],
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
function getOptionalDepNames(pkgJsonStr: string): string[] {
 | 
						|
    return Object.keys(JSON.parse(pkgJsonStr)?.["optionalDependencies"] ?? {});
 | 
						|
}
 | 
						|
 | 
						|
function findDepVersionInPackageJson(dep: string, pkgJsonStr: string): string {
 | 
						|
    const pkgJson = JSON.parse(pkgJsonStr);
 | 
						|
    const packages = {
 | 
						|
        ...(pkgJson["optionalDependencies"] ?? {}),
 | 
						|
        ...(pkgJson["devDependencies"] ?? {}),
 | 
						|
        ...(pkgJson["dependencies"] ?? {}),
 | 
						|
    };
 | 
						|
    return packages[dep];
 | 
						|
}
 | 
						|
 | 
						|
function getTopLevelDependencyVersion(dep: string): string {
 | 
						|
    const dependencyTree = JSON.parse(
 | 
						|
        childProcess
 | 
						|
            .execSync(`npm list ${dep} --depth=0 --json`, {
 | 
						|
                env: process.env,
 | 
						|
                stdio: ["inherit", "pipe", "pipe"],
 | 
						|
            })
 | 
						|
            .toString("utf-8"),
 | 
						|
    );
 | 
						|
 | 
						|
    /*
 | 
						|
        What a dependency tree looks like:
 | 
						|
        {
 | 
						|
          "version": "1.10.13",
 | 
						|
          "name": "element-web",
 | 
						|
          "dependencies": {
 | 
						|
            "@matrix-org/react-sdk-module-api": {
 | 
						|
              "version": "0.0.1",
 | 
						|
              "resolved": "file:../../../matrix-react-sdk-module-api"
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
     */
 | 
						|
 | 
						|
    return dependencyTree["dependencies"][dep]["version"];
 | 
						|
}
 | 
						|
 | 
						|
function getModuleApiVersionFor(moduleName: string): string {
 | 
						|
    // We'll just pretend that this isn't highly problematic...
 | 
						|
    // Yarn is fairly stable in putting modules in a flat hierarchy, at least.
 | 
						|
    const pkgJsonStr = fs.readFileSync(`./node_modules/${moduleName}/package.json`, "utf-8");
 | 
						|
    return findDepVersionInPackageJson(moduleApiDepName, pkgJsonStr);
 | 
						|
}
 | 
						|
 | 
						|
// A list of Module API versions that are supported in addition to the currently installed one
 | 
						|
// defined in the package.json. This is necessary because semantic versioning is applied to both
 | 
						|
// the Module-side surface of the API and the Client-side surface of the API. So breaking changes
 | 
						|
// in the Client-side surface lead to a major bump even though the Module-side surface stays
 | 
						|
// compatible. We aim to not break the Module-side surface so we maintain a list of compatible
 | 
						|
// older versions.
 | 
						|
const backwardsCompatibleMajorVersions = ["1.0.0"];
 | 
						|
 | 
						|
function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: string): boolean {
 | 
						|
    if (!moduleApiVersion) return false;
 | 
						|
    return (
 | 
						|
        semver.satisfies(ourApiVersion, moduleApiVersion) ||
 | 
						|
        backwardsCompatibleMajorVersions.some((version) => semver.satisfies(version, moduleApiVersion))
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function writeModulesTs(content: string): void {
 | 
						|
    fs.writeFileSync("./src/modules.ts", content, "utf-8");
 | 
						|
}
 |