Skip to content

Commit

Permalink
Add support for Rush monorepos (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x80 authored Mar 31, 2024
1 parent 109da77 commit 56263e5
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 51 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "isolate-package",
"version": "1.12.2",
"version": "1.13.0-1",
"description": "Isolate a monorepo package with its shared dependencies to form a self-contained directory, compatible with Firebase deploy",
"author": "Thijs Koerselman",
"license": "MIT",
Expand Down
40 changes: 31 additions & 9 deletions src/isolate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs-extra";
import assert from "node:assert";
import path from "node:path";
import { unique } from "remeda";
import type { IsolateConfig } from "./lib/config";
import { resolveConfig, setUserConfig } from "./lib/config";
import { processLockfile } from "./lib/lockfile";
Expand All @@ -22,7 +23,13 @@ import { detectPackageManager } from "./lib/package-manager";
import { getVersion } from "./lib/package-manager/helpers/infer-from-files";
import { createPackagesRegistry, listInternalPackages } from "./lib/registry";
import type { PackageManifest } from "./lib/types";
import { getDirname, getRootRelativePath, readTypedJson } from "./lib/utils";
import {
getDirname,
getRootRelativePath,
isRushWorkspace,
readTypedJson,
writeTypedYamlSync,
} from "./lib/utils";

const __dirname = getDirname(import.meta.url);

Expand Down Expand Up @@ -58,10 +65,6 @@ export async function isolate(
? path.join(process.cwd(), config.targetPackagePath)
: process.cwd();

/**
* We want a trailing slash here. Functionally it doesn't matter, but it makes
* the relative paths more correct in the debug output.
*/
const workspaceRootDir = config.targetPackagePath
? process.cwd()
: path.join(targetPackageDir, config.workspaceRoot);
Expand Down Expand Up @@ -193,11 +196,30 @@ export async function isolate(
* PNPM doesn't install dependencies of packages that are linked via link:
* or file: specifiers. It requires the directory to be configured as a
* workspace, so we copy the workspace config file to the isolate output.
*
* Rush doesn't have a pnpm-workspace.yaml file, so we generate one.
*/
fs.copyFileSync(
path.join(workspaceRootDir, "pnpm-workspace.yaml"),
path.join(isolateDir, "pnpm-workspace.yaml")
);
if (isRushWorkspace(workspaceRootDir)) {
const packagesFolderNames = unique(
internalPackageNames.map(
(name) => path.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) => x + "/*");

await writeTypedYamlSync(path.join(isolateDir, "pnpm-workspace.yaml"), {
packages,
});
} else {
fs.copyFileSync(
path.join(workspaceRootDir, "pnpm-workspace.yaml"),
path.join(isolateDir, "pnpm-workspace.yaml")
);
}
}
/**
* If there is an .npmrc file in the workspace root, copy it to the isolate
Expand Down
67 changes: 45 additions & 22 deletions src/lib/lockfile/helpers/generate-pnpm-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { pick } from "remeda";
import { useConfig } from "~/lib/config";
import { useLogger } from "~/lib/logger";
import type { PackageManifest, PackagesRegistry } from "~/lib/types";
import { getErrorMessage } from "~/lib/utils";
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
import { pnpmMapImporter } from "./pnpm-map-importer";

export async function generatePnpmLockfile({
Expand All @@ -34,9 +34,16 @@ export async function generatePnpmLockfile({
log.info("Generating PNPM lockfile...");

try {
const lockfile = await readWantedLockfile(workspaceRootDir, {
ignoreIncompatible: false,
});
const isRush = isRushWorkspace(workspaceRootDir);

const lockfile = await readWantedLockfile(
isRush
? path.join(workspaceRootDir, "common/config/rush")
: workspaceRootDir,
{
ignoreIncompatible: false,
}
);

assert(lockfile, `No input lockfile found at ${workspaceRootDir}`);

Expand All @@ -49,6 +56,8 @@ export async function generatePnpmLockfile({
internalDepPackageNames.map((name) => {
const pkg = packagesRegistry[name];
assert(pkg, `Package ${name} not found in packages registry`);

console.log("pkg.rootRelativeDir", pkg.rootRelativeDir);
return [name, pkg.rootRelativeDir];
})
);
Expand All @@ -64,34 +73,48 @@ export async function generatePnpmLockfile({

log.debug("Relevant importer ids:", relevantImporterIds);

/**
* In a Rush workspace the original lockfile is not in the root, so the
* importerIds have to be prefixed with `../../`, but that's not how they
* should be stored in the isolated lockfile, so we use the prefixed ids
* only for parsing.
*/
const relevantImporterIdsWithPrefix = relevantImporterIds.map((x) =>
isRush ? `../../${x}` : x
);

lockfile.importers = Object.fromEntries(
Object.entries(pick(lockfile.importers, relevantImporterIds)).map(
([importerId, importer]) => {
if (importerId === targetImporterId) {
log.debug("Setting target package importer on root");

return [
".",
pnpmMapImporter(importer, {
includeDevDependencies,
includePatchedDependencies,
directoryByPackageName,
}),
];
}

log.debug("Setting internal package importer:", importerId);
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 [
importerId,
".",
pnpmMapImporter(importer, {
includeDevDependencies,
includePatchedDependencies,
directoryByPackageName,
}),
];
}
)

log.debug("Setting internal package importer:", importerId);

return [
importerId,
pnpmMapImporter(importer, {
includeDevDependencies,
includePatchedDependencies,
directoryByPackageName,
}),
];
})
);

log.debug("Pruning the lockfile");
Expand Down
7 changes: 5 additions & 2 deletions src/lib/lockfile/helpers/generate-yarn-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "fs-extra";
import { execSync } from "node:child_process";
import path from "node:path";
import { useLogger } from "~/lib/logger";
import { getErrorMessage } from "~/lib/utils";
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";

/**
* Generate an isolated / pruned lockfile, based on the existing lockfile from
Expand All @@ -20,7 +20,10 @@ export async function generateYarnLockfile({

log.info("Generating Yarn lockfile...");

const origLockfilePath = path.join(workspaceRootDir, "yarn.lock");
const origLockfilePath = isRushWorkspace(workspaceRootDir)
? path.join(workspaceRootDir, "common/config/rush", "yarn.lock")
: path.join(workspaceRootDir, "yarn.lock");

const newLockfilePath = path.join(isolateDir, "yarn.lock");

if (!fs.existsSync(origLockfilePath)) {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ProjectManifest } from "@pnpm/types";
import path from "path";
import type { PackageManifest } from "~/lib/types";
import { readTypedJson } from "~/lib/utils";
import { isRushWorkspace, readTypedJson } from "~/lib/utils";

/**
* Adopts the `pnpm` fields from the root package manifest. Currently it only
Expand All @@ -12,6 +12,10 @@ export async function adoptPnpmFieldsFromRoot(
targetPackageManifest: PackageManifest,
workspaceRootDir: string
) {
if (isRushWorkspace(workspaceRootDir)) {
return targetPackageManifest;
}

const rootPackageManifest = await readTypedJson<ProjectManifest>(
path.join(workspaceRootDir, "package.json")
);
Expand Down
24 changes: 16 additions & 8 deletions src/lib/package-manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import path from "node:path";
import { isRushWorkspace } from "../utils/is-rush-workspace";
import { inferFromFiles, inferFromManifest } from "./helpers";
import type { PackageManager } from "./names";

export * from "./names";

export let packageManager: PackageManager | undefined;
let packageManager: PackageManager | undefined;

export function usePackageManager() {
if (!packageManager) {
Expand All @@ -20,13 +22,19 @@ export function usePackageManager() {
* we get the name and version from there. Otherwise we'll search for the
* different lockfiles and ask the OS to report the installed version.
*/
export function detectPackageManager(workspaceRoot: string): PackageManager {
/**
* Disable infer from manifest for now. I doubt it is useful after all but
* I'll keep the code as a reminder.
*/
packageManager =
inferFromManifest(workspaceRoot) ?? inferFromFiles(workspaceRoot);
export function detectPackageManager(workspaceRootDir: string): PackageManager {
if (isRushWorkspace(workspaceRootDir)) {
packageManager = inferFromFiles(
path.join(workspaceRootDir, "common/config/rush")
);
} else {
/**
* Disable infer from manifest for now. I doubt it is useful after all but
* I'll keep the code as a reminder.
*/
packageManager =
inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir);
}

return packageManager;
}
40 changes: 32 additions & 8 deletions src/lib/registry/create-packages-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { globSync } from "glob";
import path from "node:path";
import { useLogger } from "../logger";
import type { PackageManifest, PackagesRegistry } from "../types";
import { readTypedJson } from "../utils";
import { isRushWorkspace, readTypedJson, readTypedJsonSync } from "../utils";
import { findPackagesGlobs } from "./helpers";

/**
Expand All @@ -23,17 +23,14 @@ export async function createPackagesRegistry(
);
}

const packagesGlobs =
workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir);
const allPackages = listWorkspacePackages(
workspacePackagesOverride,
workspaceRootDir
);

const cwd = process.cwd();
process.chdir(workspaceRootDir);

const allPackages = packagesGlobs
.flatMap((glob) => globSync(glob))
/** Make sure to filter any loose files that might hang around. */
.filter((dir) => fs.lstatSync(dir).isDirectory());

const registry: PackagesRegistry = (
await Promise.all(
allPackages.map(async (rootRelativeDir) => {
Expand Down Expand Up @@ -70,3 +67,30 @@ export async function createPackagesRegistry(

return registry;
}

type RushConfig = {
projects: { packageName: string; projectFolder: string }[];
};

function listWorkspacePackages(
workspacePackagesOverride: string[] | undefined,
workspaceRootDir: string
) {
if (isRushWorkspace(workspaceRootDir)) {
const rushConfig = readTypedJsonSync<RushConfig>(
path.join(workspaceRootDir, "rush.json")
);

return rushConfig.projects.map(({ projectFolder }) => projectFolder);
} else {
const packagesGlobs =
workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir);

const allPackages = packagesGlobs
.flatMap((glob) => globSync(glob))
/** Make sure to filter any loose files that might hang around. */
.filter((dir) => fs.lstatSync(dir).isDirectory());

return allPackages;
}
}
1 change: 1 addition & 0 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./get-error-message";
export * from "./get-relative-path";
export * from "./inspect-value";
export * from "./is-present";
export * from "./is-rush-workspace";
export * from "./json";
export * from "./pack";
export * from "./unpack";
Expand Down
10 changes: 10 additions & 0 deletions src/lib/utils/is-rush-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import fs from "node:fs";
import path from "node:path";

/**
* Detect if this is a Rush monorepo. They use a very different structure so
* there are multiple places where we need to make exceptions based on this.
*/
export function isRushWorkspace(workspaceRootDir: string) {
return fs.existsSync(path.join(workspaceRootDir, "rush.json"));
}

0 comments on commit 56263e5

Please sign in to comment.