UNPKG

isolate-package

Version:

Isolate a monorepo package with its shared dependencies to form a self-contained directory, compatible with Firebase deploy

1,242 lines (1,197 loc) 39.5 kB
// src/isolate.ts import fs15 from "fs-extra"; import assert6 from "node:assert"; import path20 from "node:path"; import { unique as unique2 } from "remeda"; // src/lib/config.ts import fs8 from "fs-extra"; import path6 from "node:path"; import { isEmpty } from "remeda"; // src/lib/logger.ts import chalk from "chalk"; var _loggerHandlers = { debug(...args) { console.log(chalk.blue("debug"), ...args); }, info(...args) { console.log(chalk.green("info"), ...args); }, warn(...args) { console.log(chalk.yellow("warning"), ...args); }, error(...args) { console.log(chalk.red("error"), ...args); } }; var _logger = { debug(...args) { if (_logLevel === "debug") { _loggerHandlers.debug(...args); } }, info(...args) { if (_logLevel === "debug" || _logLevel === "info") { _loggerHandlers.info(...args); } }, warn(...args) { if (_logLevel === "debug" || _logLevel === "info" || _logLevel === "warn") { _loggerHandlers.warn(...args); } }, error(...args) { _loggerHandlers.error(...args); } }; var _logLevel = "info"; function setLogLevel(logLevel) { _logLevel = logLevel; return _logger; } function useLogger() { return _logger; } // src/lib/utils/get-dirname.ts import { fileURLToPath } from "url"; function getDirname(importMetaUrl) { return fileURLToPath(new URL(".", importMetaUrl)); } // src/lib/utils/get-error-message.ts function getErrorMessage(error) { return toErrorWithMessage(error).message; } function isErrorWithMessage(error) { return typeof error === "object" && error !== null && "message" in error; } function toErrorWithMessage(maybeError) { if (isErrorWithMessage(maybeError)) return maybeError; try { return new Error(JSON.stringify(maybeError)); } catch { return new Error(String(maybeError)); } } // src/lib/utils/inspect-value.ts import { inspect } from "node:util"; function inspectValue(value) { return inspect(value, false, 16, true); } // src/lib/utils/is-rush-workspace.ts import fs from "node:fs"; import path from "node:path"; function isRushWorkspace(workspaceRootDir) { return fs.existsSync(path.join(workspaceRootDir, "rush.json")); } // src/lib/utils/json.ts import fs2 from "fs-extra"; import stripJsonComments from "strip-json-comments"; function readTypedJsonSync(filePath) { try { const rawContent = fs2.readFileSync(filePath, "utf-8"); const data = JSON.parse( stripJsonComments(rawContent, { trailingCommas: true }) ); return data; } catch (err) { throw new Error( `Failed to read JSON from ${filePath}: ${getErrorMessage(err)}` ); } } async function readTypedJson(filePath) { try { const rawContent = await fs2.readFile(filePath, "utf-8"); const data = JSON.parse( stripJsonComments(rawContent, { trailingCommas: true }) ); return data; } catch (err) { throw new Error( `Failed to read JSON from ${filePath}: ${getErrorMessage(err)}` ); } } // src/lib/utils/log-paths.ts import { join } from "node:path"; function getRootRelativeLogPath(path21, rootPath) { const strippedPath = path21.replace(rootPath, ""); return join("(root)", strippedPath); } function getIsolateRelativeLogPath(path21, isolatePath) { const strippedPath = path21.replace(isolatePath, ""); return join("(isolate)", strippedPath); } // src/lib/utils/pack.ts import assert2 from "node:assert"; import { exec } from "node:child_process"; import fs5 from "node:fs"; import path5 from "node:path"; // src/lib/package-manager/index.ts import path4 from "node:path"; // src/lib/package-manager/helpers/infer-from-files.ts import fs3 from "fs-extra"; import { execSync } from "node:child_process"; import path2 from "node:path"; // src/lib/utils/get-major-version.ts function getMajorVersion(version) { return parseInt(version.split(".")[0], 10); } // src/lib/package-manager/names.ts var supportedPackageManagerNames = [ "pnpm", "yarn", "npm", "bun" ]; function getLockfileFileName(name) { switch (name) { case "bun": return "bun.lockb"; case "pnpm": return "pnpm-lock.yaml"; case "yarn": return "yarn.lock"; case "npm": return "package-lock.json"; } } // src/lib/package-manager/helpers/infer-from-files.ts function inferFromFiles(workspaceRoot) { for (const name of supportedPackageManagerNames) { const lockfileName = getLockfileFileName(name); if (fs3.existsSync(path2.join(workspaceRoot, lockfileName))) { try { const version = getVersion(name); return { name, version, majorVersion: getMajorVersion(version) }; } catch (err) { throw new Error( `Failed to find package manager version for ${name}: ${getErrorMessage(err)}` ); } } } if (fs3.existsSync(path2.join(workspaceRoot, "npm-shrinkwrap.json"))) { const version = getVersion("npm"); return { name: "npm", version, majorVersion: getMajorVersion(version) }; } throw new Error(`Failed to detect package manager`); } function getVersion(packageManagerName) { const buffer = execSync(`${packageManagerName} --version`); return buffer.toString().trim(); } // src/lib/package-manager/helpers/infer-from-manifest.ts import fs4 from "fs-extra"; import assert from "node:assert"; import path3 from "node:path"; function inferFromManifest(workspaceRoot) { const log = useLogger(); const { packageManager: packageManagerString } = readTypedJsonSync( path3.join(workspaceRoot, "package.json") ); if (!packageManagerString) { log.debug("No packageManager field found in root manifest"); return; } const [name, version = "*"] = packageManagerString.split("@"); assert( supportedPackageManagerNames.includes(name), `Package manager "${name}" is not currently supported` ); const lockfileName = getLockfileFileName(name); assert( fs4.existsSync(path3.join(workspaceRoot, lockfileName)), `Manifest declares ${name} to be the packageManager, but failed to find ${lockfileName} in workspace root` ); return { name, version, majorVersion: getMajorVersion(version), packageManagerString }; } // src/lib/package-manager/index.ts var packageManager; function usePackageManager() { if (!packageManager) { throw Error( "No package manager detected. Make sure to call detectPackageManager() before usePackageManager()" ); } return packageManager; } function detectPackageManager(workspaceRootDir) { if (isRushWorkspace(workspaceRootDir)) { packageManager = inferFromFiles( path4.join(workspaceRootDir, "common/config/rush") ); } else { packageManager = inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir); } return packageManager; } function shouldUsePnpmPack() { const { name, majorVersion } = usePackageManager(); return name === "pnpm" && majorVersion >= 8; } // src/lib/utils/pack.ts async function pack(srcDir, dstDir) { const log = useLogger(); const execOptions = { maxBuffer: 10 * 1024 * 1024 }; const previousCwd = process.cwd(); process.chdir(srcDir); const stdout = shouldUsePnpmPack() ? await new Promise((resolve, reject) => { exec( `pnpm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout2) => { if (err) { log.error(getErrorMessage(err)); return reject(err); } resolve(stdout2); } ); }) : await new Promise((resolve, reject) => { exec( `npm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout2) => { if (err) { return reject(err); } resolve(stdout2); } ); }); const lastLine = stdout.trim().split("\n").at(-1); assert2(lastLine, `Failed to parse last line from stdout: ${stdout.trim()}`); const fileName = path5.basename(lastLine); assert2(fileName, `Failed to parse file name from: ${lastLine}`); const filePath = path5.join(dstDir, fileName); if (!fs5.existsSync(filePath)) { log.error( `The response from pack could not be resolved to an existing file: ${filePath}` ); } else { log.debug(`Packed (temp)/${fileName}`); } process.chdir(previousCwd); return filePath; } // src/lib/utils/unpack.ts import fs6 from "fs-extra"; import tar from "tar-fs"; import { createGunzip } from "zlib"; async function unpack(filePath, unpackDir) { await new Promise((resolve, reject) => { fs6.createReadStream(filePath).pipe(createGunzip()).pipe(tar.extract(unpackDir)).on("finish", () => resolve()).on("error", (err) => reject(err)); }); } // src/lib/utils/yaml.ts import fs7 from "fs-extra"; import yaml from "yaml"; function readTypedYamlSync(filePath) { try { const rawContent = fs7.readFileSync(filePath, "utf-8"); const data = yaml.parse(rawContent); return data; } catch (err) { throw new Error( `Failed to read YAML from ${filePath}: ${getErrorMessage(err)}` ); } } function writeTypedYamlSync(filePath, content) { fs7.writeFileSync(filePath, yaml.stringify(content), "utf-8"); } // src/lib/config.ts var configDefaults = { buildDirName: void 0, includeDevDependencies: false, includePatchedDependencies: false, isolateDirName: "isolate", logLevel: "info", targetPackagePath: void 0, tsconfigPath: "./tsconfig.json", workspacePackages: void 0, workspaceRoot: "../..", forceNpm: false, pickFromScripts: void 0, omitFromScripts: void 0, omitPackageManager: false }; var validConfigKeys = Object.keys(configDefaults); var CONFIG_FILE_NAME = "isolate.config.json"; function loadConfigFromFile() { const configFilePath = path6.join(process.cwd(), CONFIG_FILE_NAME); return fs8.existsSync(configFilePath) ? readTypedJsonSync(configFilePath) : {}; } function validateConfig(config) { const log = useLogger(); const foreignKeys = Object.keys(config).filter( (key) => !validConfigKeys.includes(key) ); if (!isEmpty(foreignKeys)) { log.warn(`Found invalid config settings:`, foreignKeys.join(", ")); } } function resolveConfig(initialConfig) { setLogLevel(process.env.DEBUG_ISOLATE_CONFIG ? "debug" : "info"); const log = useLogger(); const userConfig = initialConfig ?? loadConfigFromFile(); if (initialConfig) { log.debug(`Using user defined config:`, inspectValue(initialConfig)); } else { log.debug(`Loaded config from ${CONFIG_FILE_NAME}`); } validateConfig(userConfig); if (userConfig.logLevel) { setLogLevel(userConfig.logLevel); } const config = { ...configDefaults, ...userConfig }; log.debug("Using configuration:", inspectValue(config)); return config; } // src/lib/lockfile/helpers/generate-npm-lockfile.ts import Arborist from "@npmcli/arborist"; import fs9 from "fs-extra"; import path7 from "node:path"; // src/lib/lockfile/helpers/load-npm-config.ts import Config from "@npmcli/config"; import defaults from "@npmcli/config/lib/definitions/index.js"; async function loadNpmConfig({ npmPath }) { const config = new Config({ npmPath, definitions: defaults.definitions, shorthands: defaults.shorthands, flatten: defaults.flatten }); await config.load(); return config; } // src/lib/lockfile/helpers/generate-npm-lockfile.ts async function generateNpmLockfile({ workspaceRootDir, isolateDir }) { const log = useLogger(); log.debug("Generating NPM lockfile..."); const nodeModulesPath = path7.join(workspaceRootDir, "node_modules"); try { if (!fs9.existsSync(nodeModulesPath)) { throw new Error(`Failed to find node_modules at ${nodeModulesPath}`); } const config = await loadNpmConfig({ npmPath: workspaceRootDir }); const arborist = new Arborist({ path: isolateDir, ...config.flat }); const { meta } = await arborist.buildIdealTree(); meta?.commit(); const lockfilePath = path7.join(isolateDir, "package-lock.json"); await fs9.writeFile(lockfilePath, String(meta)); log.debug("Created lockfile at", lockfilePath); } catch (err) { log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`); throw err; } } // src/lib/lockfile/helpers/generate-pnpm-lockfile.ts import assert3 from "node:assert"; import path9 from "node:path"; import { getLockfileImporterId as getLockfileImporterId_v8, readWantedLockfile as readWantedLockfile_v8, writeWantedLockfile as writeWantedLockfile_v8 } from "pnpm_lockfile_file_v8"; import { getLockfileImporterId as getLockfileImporterId_v9, readWantedLockfile as readWantedLockfile_v9, writeWantedLockfile as writeWantedLockfile_v9 } from "pnpm_lockfile_file_v9"; import { pruneLockfile as pruneLockfile_v8 } from "pnpm_prune_lockfile_v8"; import { pruneLockfile as pruneLockfile_v9 } from "pnpm_prune_lockfile_v9"; import { pick } from "remeda"; // src/lib/lockfile/helpers/pnpm-map-importer.ts import path8 from "node:path"; import { mapValues } from "remeda"; function pnpmMapImporter(importerPath, { dependencies, devDependencies, ...rest }, { includeDevDependencies, directoryByPackageName }) { return { dependencies: dependencies ? pnpmMapDependenciesLinks( importerPath, dependencies, directoryByPackageName ) : void 0, devDependencies: includeDevDependencies && devDependencies ? pnpmMapDependenciesLinks( importerPath, devDependencies, directoryByPackageName ) : void 0, ...rest }; } function pnpmMapDependenciesLinks(importerPath, def, directoryByPackageName) { return mapValues(def, (value, key) => { if (!value.startsWith("link:")) { return value; } const relativePath = path8.relative(importerPath, directoryByPackageName[key]).replace(path8.sep, path8.posix.sep); return relativePath.startsWith(".") ? `link:${relativePath}` : `link:./${relativePath}`; }); } // src/lib/lockfile/helpers/generate-pnpm-lockfile.ts async function generatePnpmLockfile({ workspaceRootDir, targetPackageDir, isolateDir, internalDepPackageNames, packagesRegistry, targetPackageManifest, majorVersion, includeDevDependencies, includePatchedDependencies }) { const useVersion9 = majorVersion >= 9; const log = useLogger(); log.debug("Generating PNPM lockfile..."); try { const isRush = isRushWorkspace(workspaceRootDir); const lockfile = useVersion9 ? await readWantedLockfile_v9( isRush ? path9.join(workspaceRootDir, "common/config/rush") : workspaceRootDir, { ignoreIncompatible: false } ) : await readWantedLockfile_v8( isRush ? path9.join(workspaceRootDir, "common/config/rush") : workspaceRootDir, { ignoreIncompatible: false } ); assert3(lockfile, `No input lockfile found at ${workspaceRootDir}`); const targetImporterId = useVersion9 ? getLockfileImporterId_v9(workspaceRootDir, targetPackageDir) : getLockfileImporterId_v8(workspaceRootDir, targetPackageDir); const directoryByPackageName = Object.fromEntries( internalDepPackageNames.map((name) => { const pkg = packagesRegistry[name]; assert3(pkg, `Package ${name} not found in packages registry`); return [name, pkg.rootRelativeDir]; }) ); const relevantImporterIds = [ targetImporterId, /** * The directory paths happen to correspond with what PNPM calls the * importer ids in the context of a lockfile. */ ...Object.values(directoryByPackageName) /** * Split the path by the OS separator and join it back with the POSIX * separator. * * The importerIds are built from directory names, so Windows Git Bash * environments will have double backslashes in their ids: * "packages\common" vs. "packages/common". Without this split & join, any * packages not on the top-level will have ill-formatted importerIds and * their entries will be missing from the lockfile.importers list. */ ].map((x) => x.split(path9.sep).join(path9.posix.sep)); log.debug("Relevant importer ids:", relevantImporterIds); const relevantImporterIdsWithPrefix = relevantImporterIds.map( (x) => isRush ? `../../${x}` : x ); lockfile.importers = Object.fromEntries( Object.entries( pick(lockfile.importers, relevantImporterIdsWithPrefix) ).map(([prefixedImporterId, importer]) => { const importerId = isRush ? prefixedImporterId.replace("../../", "") : prefixedImporterId; if (importerId === targetImporterId) { log.debug("Setting target package importer on root"); return [ ".", pnpmMapImporter(".", importer, { includeDevDependencies, includePatchedDependencies, directoryByPackageName }) ]; } log.debug("Setting internal package importer:", importerId); return [ importerId, pnpmMapImporter(importerId, importer, { includeDevDependencies, includePatchedDependencies, directoryByPackageName }) ]; }) ); log.debug("Pruning the lockfile"); const prunedLockfile = useVersion9 ? await pruneLockfile_v9(lockfile, targetPackageManifest, ".") : await pruneLockfile_v8(lockfile, targetPackageManifest, "."); if (lockfile.overrides) { prunedLockfile.overrides = lockfile.overrides; } const patchedDependencies = includePatchedDependencies ? lockfile.patchedDependencies : void 0; useVersion9 ? await writeWantedLockfile_v9(isolateDir, { ...prunedLockfile, patchedDependencies }) : await writeWantedLockfile_v8(isolateDir, { ...prunedLockfile, patchedDependencies }); log.debug("Created lockfile at", path9.join(isolateDir, "pnpm-lock.yaml")); } catch (err) { log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`); throw err; } } // src/lib/lockfile/helpers/generate-yarn-lockfile.ts import fs10 from "fs-extra"; import { execSync as execSync2 } from "node:child_process"; import path10 from "node:path"; async function generateYarnLockfile({ workspaceRootDir, isolateDir }) { const log = useLogger(); log.debug("Generating Yarn lockfile..."); const origLockfilePath = isRushWorkspace(workspaceRootDir) ? path10.join(workspaceRootDir, "common/config/rush", "yarn.lock") : path10.join(workspaceRootDir, "yarn.lock"); const newLockfilePath = path10.join(isolateDir, "yarn.lock"); if (!fs10.existsSync(origLockfilePath)) { throw new Error(`Failed to find lockfile at ${origLockfilePath}`); } log.debug(`Copy original yarn.lock to the isolate output`); try { await fs10.copyFile(origLockfilePath, newLockfilePath); log.debug(`Running local install`); execSync2(`yarn install --cwd ${isolateDir}`); log.debug("Generated lockfile at", newLockfilePath); } catch (err) { log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`); throw err; } } // src/lib/lockfile/process-lockfile.ts async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir, internalDepPackageNames, targetPackageDir, targetPackageManifest, config }) { const log = useLogger(); if (config.forceNpm) { log.debug("Forcing to use NPM for isolate output"); await generateNpmLockfile({ workspaceRootDir, isolateDir }); return true; } const { name, majorVersion } = usePackageManager(); let usedFallbackToNpm = false; switch (name) { case "npm": { await generateNpmLockfile({ workspaceRootDir, isolateDir }); break; } case "yarn": { if (majorVersion === 1) { await generateYarnLockfile({ workspaceRootDir, isolateDir }); } else { log.warn( "Detected modern version of Yarn. Using NPM lockfile fallback." ); await generateNpmLockfile({ workspaceRootDir, isolateDir }); usedFallbackToNpm = true; } break; } case "pnpm": { await generatePnpmLockfile({ workspaceRootDir, targetPackageDir, isolateDir, internalDepPackageNames, packagesRegistry, targetPackageManifest, majorVersion, includeDevDependencies: config.includeDevDependencies, includePatchedDependencies: config.includePatchedDependencies }); break; } case "bun": { log.warn( `Ouput lockfiles for Bun are not yet supported. Using NPM for output` ); await generateNpmLockfile({ workspaceRootDir, isolateDir }); usedFallbackToNpm = true; break; } default: log.warn(`Unexpected package manager ${name}. Using NPM for output`); await generateNpmLockfile({ workspaceRootDir, isolateDir }); usedFallbackToNpm = true; } return usedFallbackToNpm; } // src/lib/manifest/adapt-target-package-manifest.ts import { omit as omit2, pick as pick2 } from "remeda"; // src/lib/manifest/helpers/adapt-internal-package-manifests.ts import path13 from "node:path"; import { omit } from "remeda"; // src/lib/manifest/io.ts import fs11 from "fs-extra"; import path11 from "node:path"; async function readManifest(packageDir) { return readTypedJson(path11.join(packageDir, "package.json")); } async function writeManifest(outputDir, manifest) { await fs11.writeFile( path11.join(outputDir, "package.json"), JSON.stringify(manifest, null, 2) ); } // src/lib/manifest/helpers/patch-internal-entries.ts import path12 from "node:path"; function patchInternalEntries(dependencies, packagesRegistry, parentRootRelativeDir) { const log = useLogger(); const allWorkspacePackageNames = Object.keys(packagesRegistry); return Object.fromEntries( Object.entries(dependencies).map(([key, value]) => { if (allWorkspacePackageNames.includes(key)) { const def = packagesRegistry[key]; const relativePath = parentRootRelativeDir ? path12.relative(parentRootRelativeDir, `./${def.rootRelativeDir}`) : `./${def.rootRelativeDir}`; const linkPath = `file:${relativePath}`; log.debug(`Linking dependency ${key} to ${linkPath}`); return [key, linkPath]; } else { return [key, value]; } }) ); } // src/lib/manifest/helpers/adapt-manifest-internal-deps.ts function adaptManifestInternalDeps({ manifest, packagesRegistry, parentRootRelativeDir }) { const { dependencies, devDependencies } = manifest; return { ...manifest, dependencies: dependencies ? patchInternalEntries( dependencies, packagesRegistry, parentRootRelativeDir ) : void 0, devDependencies: devDependencies ? patchInternalEntries( devDependencies, packagesRegistry, parentRootRelativeDir ) : void 0 }; } // src/lib/manifest/helpers/adapt-internal-package-manifests.ts async function adaptInternalPackageManifests({ internalPackageNames, packagesRegistry, isolateDir, forceNpm }) { const packageManager2 = usePackageManager(); await Promise.all( internalPackageNames.map(async (packageName) => { const { manifest, rootRelativeDir } = packagesRegistry[packageName]; const strippedManifest = omit(manifest, ["scripts", "devDependencies"]); const outputManifest = packageManager2.name === "pnpm" && !forceNpm ? ( /** * For PNPM the output itself is a workspace so we can preserve the specifiers * with "workspace:*" in the output manifest. */ strippedManifest ) : ( /** For other package managers we replace the links to internal dependencies */ adaptManifestInternalDeps({ manifest: strippedManifest, packagesRegistry, parentRootRelativeDir: rootRelativeDir }) ); await writeManifest( path13.join(isolateDir, rootRelativeDir), outputManifest ); }) ); } // src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts import path14 from "path"; async function adoptPnpmFieldsFromRoot(targetPackageManifest, workspaceRootDir) { if (isRushWorkspace(workspaceRootDir)) { return targetPackageManifest; } const rootPackageManifest = await readTypedJson( path14.join(workspaceRootDir, "package.json") ); const overrides = rootPackageManifest.pnpm?.overrides; if (!overrides) { return targetPackageManifest; } return { ...targetPackageManifest, pnpm: { overrides } }; } // src/lib/manifest/adapt-target-package-manifest.ts async function adaptTargetPackageManifest({ manifest, packagesRegistry, workspaceRootDir, config }) { const packageManager2 = usePackageManager(); const { includeDevDependencies, pickFromScripts, omitFromScripts, omitPackageManager, forceNpm } = config; const inputManifest = includeDevDependencies ? manifest : omit2(manifest, ["devDependencies"]); const adaptedManifest = packageManager2.name === "pnpm" && !forceNpm ? ( /** * For PNPM the output itself is a workspace so we can preserve the specifiers * with "workspace:*" in the output manifest, but we do want to adopt the * pnpm.overrides field from the root package.json. */ await adoptPnpmFieldsFromRoot(inputManifest, workspaceRootDir) ) : ( /** For other package managers we replace the links to internal dependencies */ adaptManifestInternalDeps({ manifest: inputManifest, packagesRegistry }) ); return { ...adaptedManifest, /** * Adopt the package manager definition from the root manifest if available. * The option to omit is there because some platforms might not handle it * properly (Cloud Run, April 24th 2024, does not handle pnpm v9) */ packageManager: omitPackageManager ? void 0 : packageManager2.packageManagerString, /** * Scripts are removed by default if not explicitly picked or omitted via * config. */ scripts: pickFromScripts ? pick2(manifest.scripts ?? {}, pickFromScripts) : omitFromScripts ? omit2(manifest.scripts ?? {}, omitFromScripts) : {} }; } // src/lib/output/get-build-output-dir.ts import { getTsconfig } from "get-tsconfig"; import path15 from "node:path"; import outdent from "outdent"; async function getBuildOutputDir({ targetPackageDir, buildDirName, tsconfigPath }) { const log = useLogger(); if (buildDirName) { log.debug("Using buildDirName from config:", buildDirName); return path15.join(targetPackageDir, buildDirName); } const fullTsconfigPath = path15.join(targetPackageDir, tsconfigPath); const tsconfig = getTsconfig(fullTsconfigPath); if (tsconfig) { log.debug("Found tsconfig at:", tsconfig.path); const outDir = tsconfig.config.compilerOptions?.outDir; if (outDir) { return path15.join(targetPackageDir, outDir); } else { throw new Error(outdent` Failed to find outDir in tsconfig. If you are executing isolate from the root of a monorepo you should specify the buildDirName in isolate.config.json. `); } } else { log.warn("Failed to find tsconfig at:", fullTsconfigPath); throw new Error(outdent` Failed to infer the build output directory from either the isolate config buildDirName or a Typescript config file. See the documentation on how to configure one of these options. `); } } // src/lib/output/pack-dependencies.ts import assert4 from "node:assert"; async function packDependencies({ /** All packages found in the monorepo by workspaces declaration */ packagesRegistry, /** The dependencies that appear to be internal packages */ internalPackageNames, /** * The directory where the isolated package and all its dependencies will end * up. This is also the directory from where the package will be deployed. By * default it is a subfolder in targetPackageDir called "isolate" but you can * configure it. */ packDestinationDir }) { const log = useLogger(); const packedFileByName = {}; for (const dependency of internalPackageNames) { const def = packagesRegistry[dependency]; assert4(dependency, `Failed to find package definition for ${dependency}`); const { name } = def.manifest; if (packedFileByName[name]) { log.debug(`Skipping ${name} because it has already been packed`); continue; } packedFileByName[name] = await pack(def.absoluteDir, packDestinationDir); } return packedFileByName; } // src/lib/output/process-build-output-files.ts import fs12 from "fs-extra"; import path16 from "node:path"; var TIMEOUT_MS = 5e3; async function processBuildOutputFiles({ targetPackageDir, tmpDir, isolateDir }) { const log = useLogger(); const packedFilePath = await pack(targetPackageDir, tmpDir); const unpackDir = path16.join(tmpDir, "target"); const now = Date.now(); let isWaitingYet = false; while (!fs12.existsSync(packedFilePath) && Date.now() - now < TIMEOUT_MS) { if (!isWaitingYet) { log.debug(`Waiting for ${packedFilePath} to become available...`); } isWaitingYet = true; await new Promise((resolve) => setTimeout(resolve, 100)); } await unpack(packedFilePath, unpackDir); await fs12.copy(path16.join(unpackDir, "package"), isolateDir); } // src/lib/output/unpack-dependencies.ts import fs13 from "fs-extra"; import path17, { join as join2 } from "node:path"; async function unpackDependencies(packedFilesByName, packagesRegistry, tmpDir, isolateDir) { const log = useLogger(); await Promise.all( Object.entries(packedFilesByName).map(async ([packageName, filePath]) => { const dir = packagesRegistry[packageName].rootRelativeDir; const unpackDir = join2(tmpDir, dir); log.debug("Unpacking", `(temp)/${path17.basename(filePath)}`); await unpack(filePath, unpackDir); const destinationDir = join2(isolateDir, dir); await fs13.ensureDir(destinationDir); await fs13.move(join2(unpackDir, "package"), destinationDir, { overwrite: true }); log.debug( `Moved package files to ${getIsolateRelativeLogPath( destinationDir, isolateDir )}` ); }) ); } // src/lib/registry/create-packages-registry.ts import fs14 from "fs-extra"; import { globSync } from "glob"; import path19 from "node:path"; // src/lib/registry/helpers/find-packages-globs.ts import assert5 from "node:assert"; import path18 from "node:path"; function findPackagesGlobs(workspaceRootDir) { const log = useLogger(); const packageManager2 = usePackageManager(); switch (packageManager2.name) { case "pnpm": { const { packages: globs } = readTypedYamlSync( path18.join(workspaceRootDir, "pnpm-workspace.yaml") ); log.debug("Detected pnpm packages globs:", inspectValue(globs)); return globs; } case "bun": case "yarn": case "npm": { const workspaceRootManifestPath = path18.join( workspaceRootDir, "package.json" ); const { workspaces } = readTypedJsonSync( workspaceRootManifestPath ); if (!workspaces) { throw new Error( `No workspaces field found in ${workspaceRootManifestPath}` ); } if (Array.isArray(workspaces)) { return workspaces; } else { const workspacesObject = workspaces; assert5( workspacesObject.packages, "workspaces.packages must be an array" ); return workspacesObject.packages; } } } } // src/lib/registry/create-packages-registry.ts async function createPackagesRegistry(workspaceRootDir, workspacePackagesOverride) { const log = useLogger(); if (workspacePackagesOverride) { log.debug( `Override workspace packages via config: ${workspacePackagesOverride}` ); } const allPackages = listWorkspacePackages( workspacePackagesOverride, workspaceRootDir ); const registry = (await Promise.all( allPackages.map(async (rootRelativeDir) => { const absoluteDir = path19.join(workspaceRootDir, rootRelativeDir); const manifestPath = path19.join(absoluteDir, "package.json"); if (!fs14.existsSync(manifestPath)) { log.warn( `Ignoring directory ${rootRelativeDir} because it does not contain a package.json file` ); return; } else { log.debug(`Registering package ${rootRelativeDir}`); const manifest = await readTypedJson( path19.join(absoluteDir, "package.json") ); return { manifest, rootRelativeDir, absoluteDir }; } }) )).reduce((acc, info) => { if (info) { acc[info.manifest.name] = info; } return acc; }, {}); return registry; } function listWorkspacePackages(workspacePackagesOverride, workspaceRootDir) { if (isRushWorkspace(workspaceRootDir)) { const rushConfig = readTypedJsonSync( path19.join(workspaceRootDir, "rush.json") ); return rushConfig.projects.map(({ projectFolder }) => projectFolder); } else { const currentDir = process.cwd(); process.chdir(workspaceRootDir); const packagesGlobs = workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir); const allPackages = packagesGlobs.flatMap((glob) => globSync(glob)).filter((dir) => fs14.lstatSync(dir).isDirectory()); process.chdir(currentDir); return allPackages; } } // src/lib/registry/list-internal-packages.ts import { unique } from "remeda"; function listInternalPackages(manifest, packagesRegistry, { includeDevDependencies = false } = {}) { const allWorkspacePackageNames = Object.keys(packagesRegistry); const internalPackageNames = (includeDevDependencies ? [ ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {}) ] : Object.keys(manifest.dependencies ?? {})).filter((name) => allWorkspacePackageNames.includes(name)); const nestedInternalPackageNames = internalPackageNames.flatMap( (packageName) => listInternalPackages( packagesRegistry[packageName].manifest, packagesRegistry, { includeDevDependencies } ) ); return unique(internalPackageNames.concat(nestedInternalPackageNames)); } // src/isolate.ts var __dirname = getDirname(import.meta.url); function createIsolator(config) { const resolvedConfig = resolveConfig(config); return async function isolate2() { const config2 = resolvedConfig; setLogLevel(config2.logLevel); const log = useLogger(); const { version: libraryVersion } = await readTypedJson( path20.join(path20.join(__dirname, "..", "package.json")) ); log.debug("Using isolate-package version", libraryVersion); const targetPackageDir = config2.targetPackagePath ? path20.join(process.cwd(), config2.targetPackagePath) : process.cwd(); const workspaceRootDir = config2.targetPackagePath ? process.cwd() : path20.join(targetPackageDir, config2.workspaceRoot); const buildOutputDir = await getBuildOutputDir({ targetPackageDir, buildDirName: config2.buildDirName, tsconfigPath: config2.tsconfigPath }); assert6( fs15.existsSync(buildOutputDir), `Failed to find build output path at ${buildOutputDir}. Please make sure you build the source before isolating it.` ); log.debug("Workspace root resolved to", workspaceRootDir); log.debug( "Isolate target package", getRootRelativeLogPath(targetPackageDir, workspaceRootDir) ); const isolateDir = path20.join(targetPackageDir, config2.isolateDirName); log.debug( "Isolate output directory", getRootRelativeLogPath(isolateDir, workspaceRootDir) ); if (fs15.existsSync(isolateDir)) { await fs15.remove(isolateDir); log.debug("Cleaned the existing isolate output directory"); } await fs15.ensureDir(isolateDir); const tmpDir = path20.join(isolateDir, "__tmp"); await fs15.ensureDir(tmpDir); const targetPackageManifest = await readTypedJson( path20.join(targetPackageDir, "package.json") ); const packageManager2 = detectPackageManager(workspaceRootDir); log.debug( "Detected package manager", packageManager2.name, packageManager2.version ); if (shouldUsePnpmPack()) { log.debug("Use PNPM pack instead of NPM pack"); } const packagesRegistry = await createPackagesRegistry( workspaceRootDir, config2.workspacePackages ); const internalPackageNames = listInternalPackages( targetPackageManifest, packagesRegistry, { includeDevDependencies: config2.includeDevDependencies } ); const packedFilesByName = await packDependencies({ internalPackageNames, packagesRegistry, packDestinationDir: tmpDir }); await unpackDependencies( packedFilesByName, packagesRegistry, tmpDir, isolateDir ); await adaptInternalPackageManifests({ internalPackageNames, packagesRegistry, isolateDir, forceNpm: config2.forceNpm }); await processBuildOutputFiles({ targetPackageDir, tmpDir, isolateDir }); const outputManifest = await adaptTargetPackageManifest({ manifest: targetPackageManifest, packagesRegistry, workspaceRootDir, config: config2 }); await writeManifest(isolateDir, outputManifest); const usedFallbackToNpm = await processLockfile({ workspaceRootDir, isolateDir, packagesRegistry, internalDepPackageNames: internalPackageNames, targetPackageDir, targetPackageName: targetPackageManifest.name, targetPackageManifest: outputManifest, config: config2 }); if (usedFallbackToNpm) { const manifest = await readManifest(isolateDir); const npmVersion = getVersion("npm"); manifest.packageManager = `npm@${npmVersion}`; await writeManifest(isolateDir, manifest); } if (packageManager2.name === "pnpm" && !config2.forceNpm) { if (isRushWorkspace(workspaceRootDir)) { const packagesFolderNames = unique2( internalPackageNames.map( (name) => path20.parse(packagesRegistry[name].rootRelativeDir).dir ) ); log.debug("Generating pnpm-workspace.yaml for Rush workspace"); log.debug("Packages folder names:", packagesFolderNames); const packages = packagesFolderNames.map((x) => path20.join(x, "/*")); await writeTypedYamlSync(path20.join(isolateDir, "pnpm-workspace.yaml"), { packages }); } else { fs15.copyFileSync( path20.join(workspaceRootDir, "pnpm-workspace.yaml"), path20.join(isolateDir, "pnpm-workspace.yaml") ); } } const npmrcPath = path20.join(workspaceRootDir, ".npmrc"); if (fs15.existsSync(npmrcPath)) { fs15.copyFileSync(npmrcPath, path20.join(isolateDir, ".npmrc")); log.debug("Copied .npmrc file to the isolate output"); } log.debug( "Deleting temp directory", getRootRelativeLogPath(tmpDir, workspaceRootDir) ); await fs15.remove(tmpDir); log.debug("Isolate completed at", isolateDir); return isolateDir; }; } async function isolate(config) { return createIsolator(config)(); } export { isolate }; //# sourceMappingURL=index.mjs.map