From c423e4caccb8ccf3222193184779dedd649735ce Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 03:24:41 +0100 Subject: [PATCH] Adapt the npmInstall script so that it works when packages are linked --- scripts/link-in-app.ts | 65 ++- .../initializeAccountTheme_singlePage.ts | 18 +- src/bin/tools/npmInstall.ts | 389 +++++++++++++++++- 3 files changed, 454 insertions(+), 18 deletions(-) diff --git a/scripts/link-in-app.ts b/scripts/link-in-app.ts index 3dfbbd57..0f304862 100644 --- a/scripts/link-in-app.ts +++ b/scripts/link-in-app.ts @@ -125,7 +125,54 @@ if (testAppPaths.length === 0) { process.exit(-1); } -testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath })); +testAppPaths.forEach(testAppPath => { + const packageJsonFilePath = pathJoin(testAppPath, "package.json"); + + const packageJsonContent = fs.readFileSync(packageJsonFilePath); + + const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as { + scripts?: Record; + }; + + let hasPostInstallOrPrepareScript = false; + + if (parsedPackageJson.scripts !== undefined) { + for (const scriptName of ["postinstall", "prepare"]) { + if (parsedPackageJson.scripts[scriptName] === undefined) { + continue; + } + + hasPostInstallOrPrepareScript = true; + + delete parsedPackageJson.scripts[scriptName]; + } + } + + if (hasPostInstallOrPrepareScript) { + fs.writeFileSync( + packageJsonFilePath, + Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8") + ); + } + + const restorePackageJson = () => { + if (!hasPostInstallOrPrepareScript) { + return; + } + + fs.writeFileSync(packageJsonFilePath, packageJsonContent); + }; + + try { + execSync("yarn install", { cwd: testAppPath }); + } catch (error) { + restorePackageJson(); + + throw error; + } + + restorePackageJson(); +}); console.log("=== Linking common dependencies ==="); @@ -172,4 +219,20 @@ testAppPaths.forEach(testAppPath => }) ); +testAppPaths.forEach(testAppPath => { + const { scripts = {} } = JSON.parse( + fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8") + ) as { + scripts?: Record; + }; + + for (const scriptName of ["postinstall", "prepare"]) { + if (scripts[scriptName] === undefined) { + continue; + } + + execSync(`yarn run ${scriptName}`, { cwd: testAppPath }); + } +}); + export {}; diff --git a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts b/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts index 6c6a1714..77984d3b 100644 --- a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts +++ b/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts @@ -1,4 +1,4 @@ -import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; +import { relative as pathRelative, dirname as pathDirname } from "path"; import type { BuildContext } from "../shared/buildContext"; import * as fs from "fs"; import chalk from "chalk"; @@ -14,7 +14,6 @@ import { is } from "tsafe/is"; import { id } from "tsafe/id"; import { npmInstall } from "../tools/npmInstall"; import { copyBoilerplate } from "./copyBoilerplate"; -import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & { fetchOptions: BuildContext["fetchOptions"]; @@ -121,20 +120,7 @@ export async function initializeAccountTheme_singlePage(params: { JSON.stringify(parsedPackageJson, undefined, 4) ); - run_npm_install: { - if ( - JSON.parse( - fs - .readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")) - .toString("utf8") - )["version"] === "0.0.0" - ) { - //NOTE: Linked version - break run_npm_install; - } - - npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); - } + npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); copyBoilerplate({ accountThemeType: "Single-Page", diff --git a/src/bin/tools/npmInstall.ts b/src/bin/tools/npmInstall.ts index 2d62e129..c1995351 100644 --- a/src/bin/tools/npmInstall.ts +++ b/src/bin/tools/npmInstall.ts @@ -1,7 +1,14 @@ import * as fs from "fs"; -import { join as pathJoin } from "path"; +import { join as pathJoin, dirname as pathDirname } from "path"; import * as child_process from "child_process"; import chalk from "chalk"; +import { z } from "zod"; +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { is } from "tsafe/is"; +import { objectKeys } from "tsafe/objectKeys"; +import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath"; +import { exclude } from "tsafe/exclude"; export function npmInstall(params: { packageJsonDirPath: string }) { const { packageJsonDirPath } = params; @@ -48,6 +55,27 @@ export function npmInstall(params: { packageJsonDirPath: string }) { console.log(`Installing the new dependencies...`); + install_without_breaking_links: { + if (packageManagerBinName !== "yarn") { + break install_without_breaking_links; + } + + const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath }); + + if (garronejLinkInfos === undefined) { + break install_without_breaking_links; + } + + console.log(chalk.green("Installing in a way that won't break the links...")); + + installWithoutBreakingLinks({ + packageJsonDirPath, + garronejLinkInfos + }); + + return; + } + try { child_process.execSync(`${packageManagerBinName} install`, { cwd: packageJsonDirPath, @@ -61,3 +89,362 @@ export function npmInstall(params: { packageJsonDirPath: string }) { ); } } + +function getGarronejLinkInfos(params: { + packageJsonDirPath: string; +}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined { + const { packageJsonDirPath } = params; + + const nodeModuleDirPath = pathJoin(packageJsonDirPath, "node_modules"); + + if (!fs.existsSync(nodeModuleDirPath)) { + return undefined; + } + + const linkedModuleNames: string[] = []; + + let yarnHomeDirPath: string | undefined = undefined; + + const getIsLinkedByGarronejScript = (path: string) => { + let realPath: string; + + try { + realPath = fs.readlinkSync(path); + } catch { + return false; + } + + const doesIncludeYarnHome = realPath.includes(".yarn_home"); + + if (!doesIncludeYarnHome) { + return false; + } + + set_yarnHomeDirPath: { + if (yarnHomeDirPath !== undefined) { + break set_yarnHomeDirPath; + } + + const [firstElement] = getAbsoluteAndInOsFormatPath({ + pathIsh: realPath, + cwd: pathDirname(path) + }).split(".yarn_home"); + + yarnHomeDirPath = pathJoin(firstElement, ".yarn_home"); + } + + return true; + }; + + for (const basename of fs.readdirSync(nodeModuleDirPath)) { + const path = pathJoin(nodeModuleDirPath, basename); + + if (fs.lstatSync(path).isSymbolicLink()) { + if (basename.startsWith("@")) { + return undefined; + } + + if (!getIsLinkedByGarronejScript(path)) { + return undefined; + } + + linkedModuleNames.push(basename); + continue; + } + + if (!fs.lstatSync(path).isDirectory()) { + continue; + } + + if (basename.startsWith("@")) { + for (const subBasename of fs.readdirSync(path)) { + const subPath = pathJoin(path, subBasename); + + if (!fs.lstatSync(subPath).isSymbolicLink()) { + continue; + } + + if (!getIsLinkedByGarronejScript(subPath)) { + return undefined; + } + + linkedModuleNames.push(`${basename}/${subBasename}`); + } + } + } + + if (yarnHomeDirPath === undefined) { + return undefined; + } + + return { linkedModuleNames, yarnHomeDirPath }; +} + +function installWithoutBreakingLinks(params: { + packageJsonDirPath: string; + garronejLinkInfos: Exclude, undefined>; +}) { + const { + packageJsonDirPath, + garronejLinkInfos: { linkedModuleNames, yarnHomeDirPath } + } = params; + + const parsedPackageJson = (() => { + const packageJsonFilePath = pathJoin(packageJsonDirPath, "package.json"); + + type ParsedPackageJson = { + scripts?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + scripts: z.record(z.string()).optional() + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); + })(); + + const parsedPackageJson = JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ) as unknown; + + zParsedPackageJson.parse(parsedPackageJson); + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + const isImplementedScriptByName = { + postinstall: false, + prepare: false + }; + + delete_postinstall_script: { + if (parsedPackageJson.scripts === undefined) { + break delete_postinstall_script; + } + + for (const scriptName of objectKeys(isImplementedScriptByName)) { + if (parsedPackageJson.scripts[scriptName] === undefined) { + continue; + } + + isImplementedScriptByName[scriptName] = true; + + delete parsedPackageJson.scripts[scriptName]; + } + } + + const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject"); + + if (fs.existsSync(tmpProjectDirPath)) { + fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + } + + fs.mkdirSync(tmpProjectDirPath, { recursive: true }); + + fs.writeFileSync( + pathJoin(tmpProjectDirPath, "package.json"), + JSON.stringify(parsedPackageJson, undefined, 4) + ); + + const YARN_LOCK = "yarn.lock"; + + fs.copyFileSync( + pathJoin(packageJsonDirPath, YARN_LOCK), + pathJoin(tmpProjectDirPath, YARN_LOCK) + ); + + child_process.execSync(`yarn install`, { + cwd: tmpProjectDirPath, + stdio: "inherit" + }); + + // NOTE: Moving the modules from the tmp project to the actual project + // without messing up the links. + { + const { getAreSameVersions } = (() => { + type ParsedPackageJson = { + version: string; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + version: z.string() + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); + })(); + + function readVersion(params: { moduleDirPath: string }): string { + const { moduleDirPath } = params; + + const packageJsonFilePath = pathJoin(moduleDirPath, "package.json"); + + const packageJson = JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(packageJson); + assert(is(packageJson)); + + return packageJson.version; + } + + function getAreSameVersions(params: { + moduleDirPath_a: string; + moduleDirPath_b: string; + }): boolean { + const { moduleDirPath_a, moduleDirPath_b } = params; + + return ( + readVersion({ moduleDirPath: moduleDirPath_a }) === + readVersion({ moduleDirPath: moduleDirPath_b }) + ); + } + + return { getAreSameVersions }; + })(); + + const nodeModulesDirPath_tmpProject = pathJoin(tmpProjectDirPath, "node_modules"); + const nodeModulesDirPath = pathJoin(packageJsonDirPath, "node_modules"); + + const modulePaths = fs + .readdirSync(nodeModulesDirPath_tmpProject) + .map(basename => { + if (basename.startsWith(".")) { + return undefined; + } + + const path = pathJoin(nodeModulesDirPath_tmpProject, basename); + + if (basename.startsWith("@")) { + return fs + .readdirSync(path) + .map(subBasename => { + if (subBasename.startsWith(".")) { + return undefined; + } + + const subPath = pathJoin(path, subBasename); + + if (!fs.lstatSync(subPath).isDirectory()) { + return undefined; + } + + return { + moduleName: `${basename}/${subBasename}`, + moduleDirPath_tmpProject: subPath, + moduleDirPath: pathJoin( + nodeModulesDirPath, + basename, + subBasename + ) + }; + }) + .filter(exclude(undefined)); + } + + if (!fs.lstatSync(path).isDirectory()) { + return undefined; + } + + return [ + { + moduleName: basename, + moduleDirPath_tmpProject: path, + moduleDirPath: pathJoin(nodeModulesDirPath, basename) + } + ]; + }) + .filter(exclude(undefined)) + .flat(); + + for (const { + moduleName, + moduleDirPath, + moduleDirPath_tmpProject + } of modulePaths) { + if (linkedModuleNames.includes(moduleName)) { + continue; + } + + let doesTargetModuleExist = false; + + skip_condition: { + if (!fs.existsSync(moduleDirPath)) { + break skip_condition; + } + + doesTargetModuleExist = true; + + const areSameVersions = getAreSameVersions({ + moduleDirPath_a: moduleDirPath, + moduleDirPath_b: moduleDirPath_tmpProject + }); + + if (!areSameVersions) { + break skip_condition; + } + + continue; + } + + if (doesTargetModuleExist) { + fs.rmdirSync(moduleDirPath, { recursive: true }); + } + + fs.renameSync(moduleDirPath_tmpProject, moduleDirPath); + } + + move_bin: { + const binDirPath_tmpProject = pathJoin(nodeModulesDirPath_tmpProject, ".bin"); + const binDirPath = pathJoin(nodeModulesDirPath, ".bin"); + + if (!fs.existsSync(binDirPath_tmpProject)) { + break move_bin; + } + + for (const basename of fs.readdirSync(binDirPath_tmpProject)) { + const path_tmpProject = pathJoin(binDirPath_tmpProject, basename); + const path = pathJoin(binDirPath, basename); + + if (fs.existsSync(path)) { + continue; + } + + fs.renameSync(path_tmpProject, path); + } + } + } + + fs.cpSync( + pathJoin(tmpProjectDirPath, YARN_LOCK), + pathJoin(packageJsonDirPath, YARN_LOCK) + ); + + fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + + for (const scriptName of objectKeys(isImplementedScriptByName)) { + if (!isImplementedScriptByName[scriptName]) { + continue; + } + + child_process.execSync(`yarn run ${scriptName}`, { + cwd: packageJsonDirPath, + stdio: "inherit" + }); + } +}