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
JavaScript
// 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