Skip to content

Commit 56263e5

Browse files
authored
Add support for Rush monorepos (#65)
1 parent 109da77 commit 56263e5

File tree

9 files changed

+146
-51
lines changed

9 files changed

+146
-51
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "isolate-package",
3-
"version": "1.12.2",
3+
"version": "1.13.0-1",
44
"description": "Isolate a monorepo package with its shared dependencies to form a self-contained directory, compatible with Firebase deploy",
55
"author": "Thijs Koerselman",
66
"license": "MIT",

src/isolate.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "fs-extra";
22
import assert from "node:assert";
33
import path from "node:path";
4+
import { unique } from "remeda";
45
import type { IsolateConfig } from "./lib/config";
56
import { resolveConfig, setUserConfig } from "./lib/config";
67
import { processLockfile } from "./lib/lockfile";
@@ -22,7 +23,13 @@ import { detectPackageManager } from "./lib/package-manager";
2223
import { getVersion } from "./lib/package-manager/helpers/infer-from-files";
2324
import { createPackagesRegistry, listInternalPackages } from "./lib/registry";
2425
import type { PackageManifest } from "./lib/types";
25-
import { getDirname, getRootRelativePath, readTypedJson } from "./lib/utils";
26+
import {
27+
getDirname,
28+
getRootRelativePath,
29+
isRushWorkspace,
30+
readTypedJson,
31+
writeTypedYamlSync,
32+
} from "./lib/utils";
2633

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

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

61-
/**
62-
* We want a trailing slash here. Functionally it doesn't matter, but it makes
63-
* the relative paths more correct in the debug output.
64-
*/
6568
const workspaceRootDir = config.targetPackagePath
6669
? process.cwd()
6770
: path.join(targetPackageDir, config.workspaceRoot);
@@ -193,11 +196,30 @@ export async function isolate(
193196
* PNPM doesn't install dependencies of packages that are linked via link:
194197
* or file: specifiers. It requires the directory to be configured as a
195198
* workspace, so we copy the workspace config file to the isolate output.
199+
*
200+
* Rush doesn't have a pnpm-workspace.yaml file, so we generate one.
196201
*/
197-
fs.copyFileSync(
198-
path.join(workspaceRootDir, "pnpm-workspace.yaml"),
199-
path.join(isolateDir, "pnpm-workspace.yaml")
200-
);
202+
if (isRushWorkspace(workspaceRootDir)) {
203+
const packagesFolderNames = unique(
204+
internalPackageNames.map(
205+
(name) => path.parse(packagesRegistry[name].rootRelativeDir).dir
206+
)
207+
);
208+
209+
log.debug("Generating pnpm-workspace.yaml for Rush workspace");
210+
log.debug("Packages folder names:", packagesFolderNames);
211+
212+
const packages = packagesFolderNames.map((x) => x + "/*");
213+
214+
await writeTypedYamlSync(path.join(isolateDir, "pnpm-workspace.yaml"), {
215+
packages,
216+
});
217+
} else {
218+
fs.copyFileSync(
219+
path.join(workspaceRootDir, "pnpm-workspace.yaml"),
220+
path.join(isolateDir, "pnpm-workspace.yaml")
221+
);
222+
}
201223
}
202224
/**
203225
* If there is an .npmrc file in the workspace root, copy it to the isolate

src/lib/lockfile/helpers/generate-pnpm-lockfile.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { pick } from "remeda";
1010
import { useConfig } from "~/lib/config";
1111
import { useLogger } from "~/lib/logger";
1212
import type { PackageManifest, PackagesRegistry } from "~/lib/types";
13-
import { getErrorMessage } from "~/lib/utils";
13+
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
1414
import { pnpmMapImporter } from "./pnpm-map-importer";
1515

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

3636
try {
37-
const lockfile = await readWantedLockfile(workspaceRootDir, {
38-
ignoreIncompatible: false,
39-
});
37+
const isRush = isRushWorkspace(workspaceRootDir);
38+
39+
const lockfile = await readWantedLockfile(
40+
isRush
41+
? path.join(workspaceRootDir, "common/config/rush")
42+
: workspaceRootDir,
43+
{
44+
ignoreIncompatible: false,
45+
}
46+
);
4047

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

@@ -49,6 +56,8 @@ export async function generatePnpmLockfile({
4956
internalDepPackageNames.map((name) => {
5057
const pkg = packagesRegistry[name];
5158
assert(pkg, `Package ${name} not found in packages registry`);
59+
60+
console.log("pkg.rootRelativeDir", pkg.rootRelativeDir);
5261
return [name, pkg.rootRelativeDir];
5362
})
5463
);
@@ -64,34 +73,48 @@ export async function generatePnpmLockfile({
6473

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

76+
/**
77+
* In a Rush workspace the original lockfile is not in the root, so the
78+
* importerIds have to be prefixed with `../../`, but that's not how they
79+
* should be stored in the isolated lockfile, so we use the prefixed ids
80+
* only for parsing.
81+
*/
82+
const relevantImporterIdsWithPrefix = relevantImporterIds.map((x) =>
83+
isRush ? `../../${x}` : x
84+
);
85+
6786
lockfile.importers = Object.fromEntries(
68-
Object.entries(pick(lockfile.importers, relevantImporterIds)).map(
69-
([importerId, importer]) => {
70-
if (importerId === targetImporterId) {
71-
log.debug("Setting target package importer on root");
72-
73-
return [
74-
".",
75-
pnpmMapImporter(importer, {
76-
includeDevDependencies,
77-
includePatchedDependencies,
78-
directoryByPackageName,
79-
}),
80-
];
81-
}
82-
83-
log.debug("Setting internal package importer:", importerId);
87+
Object.entries(
88+
pick(lockfile.importers, relevantImporterIdsWithPrefix)
89+
).map(([prefixedImporterId, importer]) => {
90+
const importerId = isRush
91+
? prefixedImporterId.replace("../../", "")
92+
: prefixedImporterId;
93+
94+
if (importerId === targetImporterId) {
95+
log.debug("Setting target package importer on root");
8496

8597
return [
86-
importerId,
98+
".",
8799
pnpmMapImporter(importer, {
88100
includeDevDependencies,
89101
includePatchedDependencies,
90102
directoryByPackageName,
91103
}),
92104
];
93105
}
94-
)
106+
107+
log.debug("Setting internal package importer:", importerId);
108+
109+
return [
110+
importerId,
111+
pnpmMapImporter(importer, {
112+
includeDevDependencies,
113+
includePatchedDependencies,
114+
directoryByPackageName,
115+
}),
116+
];
117+
})
95118
);
96119

97120
log.debug("Pruning the lockfile");

src/lib/lockfile/helpers/generate-yarn-lockfile.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from "fs-extra";
22
import { execSync } from "node:child_process";
33
import path from "node:path";
44
import { useLogger } from "~/lib/logger";
5-
import { getErrorMessage } from "~/lib/utils";
5+
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
66

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

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

23-
const origLockfilePath = path.join(workspaceRootDir, "yarn.lock");
23+
const origLockfilePath = isRushWorkspace(workspaceRootDir)
24+
? path.join(workspaceRootDir, "common/config/rush", "yarn.lock")
25+
: path.join(workspaceRootDir, "yarn.lock");
26+
2427
const newLockfilePath = path.join(isolateDir, "yarn.lock");
2528

2629
if (!fs.existsSync(origLockfilePath)) {

src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ProjectManifest } from "@pnpm/types";
22
import path from "path";
33
import type { PackageManifest } from "~/lib/types";
4-
import { readTypedJson } from "~/lib/utils";
4+
import { isRushWorkspace, readTypedJson } from "~/lib/utils";
55

66
/**
77
* Adopts the `pnpm` fields from the root package manifest. Currently it only
@@ -12,6 +12,10 @@ export async function adoptPnpmFieldsFromRoot(
1212
targetPackageManifest: PackageManifest,
1313
workspaceRootDir: string
1414
) {
15+
if (isRushWorkspace(workspaceRootDir)) {
16+
return targetPackageManifest;
17+
}
18+
1519
const rootPackageManifest = await readTypedJson<ProjectManifest>(
1620
path.join(workspaceRootDir, "package.json")
1721
);

src/lib/package-manager/index.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import path from "node:path";
2+
import { isRushWorkspace } from "../utils/is-rush-workspace";
13
import { inferFromFiles, inferFromManifest } from "./helpers";
24
import type { PackageManager } from "./names";
35

46
export * from "./names";
57

6-
export let packageManager: PackageManager | undefined;
8+
let packageManager: PackageManager | undefined;
79

810
export function usePackageManager() {
911
if (!packageManager) {
@@ -20,13 +22,19 @@ export function usePackageManager() {
2022
* we get the name and version from there. Otherwise we'll search for the
2123
* different lockfiles and ask the OS to report the installed version.
2224
*/
23-
export function detectPackageManager(workspaceRoot: string): PackageManager {
24-
/**
25-
* Disable infer from manifest for now. I doubt it is useful after all but
26-
* I'll keep the code as a reminder.
27-
*/
28-
packageManager =
29-
inferFromManifest(workspaceRoot) ?? inferFromFiles(workspaceRoot);
25+
export function detectPackageManager(workspaceRootDir: string): PackageManager {
26+
if (isRushWorkspace(workspaceRootDir)) {
27+
packageManager = inferFromFiles(
28+
path.join(workspaceRootDir, "common/config/rush")
29+
);
30+
} else {
31+
/**
32+
* Disable infer from manifest for now. I doubt it is useful after all but
33+
* I'll keep the code as a reminder.
34+
*/
35+
packageManager =
36+
inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir);
37+
}
3038

3139
return packageManager;
3240
}

src/lib/registry/create-packages-registry.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { globSync } from "glob";
33
import path from "node:path";
44
import { useLogger } from "../logger";
55
import type { PackageManifest, PackagesRegistry } from "../types";
6-
import { readTypedJson } from "../utils";
6+
import { isRushWorkspace, readTypedJson, readTypedJsonSync } from "../utils";
77
import { findPackagesGlobs } from "./helpers";
88

99
/**
@@ -23,17 +23,14 @@ export async function createPackagesRegistry(
2323
);
2424
}
2525

26-
const packagesGlobs =
27-
workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir);
26+
const allPackages = listWorkspacePackages(
27+
workspacePackagesOverride,
28+
workspaceRootDir
29+
);
2830

2931
const cwd = process.cwd();
3032
process.chdir(workspaceRootDir);
3133

32-
const allPackages = packagesGlobs
33-
.flatMap((glob) => globSync(glob))
34-
/** Make sure to filter any loose files that might hang around. */
35-
.filter((dir) => fs.lstatSync(dir).isDirectory());
36-
3734
const registry: PackagesRegistry = (
3835
await Promise.all(
3936
allPackages.map(async (rootRelativeDir) => {
@@ -70,3 +67,30 @@ export async function createPackagesRegistry(
7067

7168
return registry;
7269
}
70+
71+
type RushConfig = {
72+
projects: { packageName: string; projectFolder: string }[];
73+
};
74+
75+
function listWorkspacePackages(
76+
workspacePackagesOverride: string[] | undefined,
77+
workspaceRootDir: string
78+
) {
79+
if (isRushWorkspace(workspaceRootDir)) {
80+
const rushConfig = readTypedJsonSync<RushConfig>(
81+
path.join(workspaceRootDir, "rush.json")
82+
);
83+
84+
return rushConfig.projects.map(({ projectFolder }) => projectFolder);
85+
} else {
86+
const packagesGlobs =
87+
workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir);
88+
89+
const allPackages = packagesGlobs
90+
.flatMap((glob) => globSync(glob))
91+
/** Make sure to filter any loose files that might hang around. */
92+
.filter((dir) => fs.lstatSync(dir).isDirectory());
93+
94+
return allPackages;
95+
}
96+
}

src/lib/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./get-error-message";
44
export * from "./get-relative-path";
55
export * from "./inspect-value";
66
export * from "./is-present";
7+
export * from "./is-rush-workspace";
78
export * from "./json";
89
export * from "./pack";
910
export * from "./unpack";

src/lib/utils/is-rush-workspace.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
/**
5+
* Detect if this is a Rush monorepo. They use a very different structure so
6+
* there are multiple places where we need to make exceptions based on this.
7+
*/
8+
export function isRushWorkspace(workspaceRootDir: string) {
9+
return fs.existsSync(path.join(workspaceRootDir, "rush.json"));
10+
}

0 commit comments

Comments
 (0)