isolate-package 1.27.0 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -420
- package/dist/index.d.mts +13 -5
- package/dist/index.mjs +21 -2
- package/dist/index.mjs.map +1 -0
- package/dist/{isolate-B11ztsrI.mjs → isolate-D-Qd5BJJ.mjs} +114 -25
- package/dist/isolate-D-Qd5BJJ.mjs.map +1 -0
- package/dist/isolate-bin.mjs +1 -1
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +37 -32
- package/src/get-internal-package-names.test.ts +213 -0
- package/src/get-internal-package-names.ts +38 -0
- package/src/index.ts +3 -5
- package/src/isolate-bin.ts +1 -1
- package/src/isolate.ts +21 -31
- package/src/lib/cli.test.ts +3 -3
- package/src/lib/cli.ts +4 -4
- package/src/lib/config.test.ts +163 -0
- package/src/lib/config.ts +134 -8
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +6 -6
- package/src/lib/lockfile/helpers/pnpm-map-importer.ts +5 -5
- package/src/lib/lockfile/process-lockfile.ts +3 -3
- package/src/lib/manifest/adapt-target-package-manifest.ts +2 -2
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +3 -3
- package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +2 -2
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +1 -1
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +2 -2
- package/src/lib/manifest/helpers/patch-internal-entries.ts +2 -2
- package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +3 -3
- package/src/lib/manifest/io.ts +2 -2
- package/src/lib/manifest/validate-manifest.test.ts +10 -10
- package/src/lib/manifest/validate-manifest.ts +1 -1
- package/src/lib/output/unpack-dependencies.ts +4 -4
- package/src/lib/package-manager/helpers/infer-from-files.ts +1 -1
- package/src/lib/package-manager/helpers/infer-from-manifest.ts +3 -3
- package/src/lib/package-manager/index.ts +2 -2
- package/src/lib/patches/copy-patches.test.ts +7 -7
- package/src/lib/patches/copy-patches.ts +5 -5
- package/src/lib/registry/create-packages-registry.ts +12 -14
- package/src/lib/registry/helpers/find-packages-globs.ts +7 -7
- package/src/lib/registry/list-internal-packages.test.ts +291 -0
- package/src/lib/registry/list-internal-packages.ts +70 -18
- package/src/lib/utils/filter-object-undefined.test.ts +1 -1
- package/src/lib/utils/filter-object-undefined.ts +1 -1
- package/src/lib/utils/filter-patched-dependencies.ts +2 -2
- package/src/lib/utils/json.ts +4 -4
- package/src/lib/utils/pack.ts +3 -3
- package/src/lib/utils/yaml.ts +1 -1
- package/dist/isolate-B11ztsrI.mjs.map +0 -1
- package/docs/firebase.md +0 -144
|
@@ -28,11 +28,11 @@ export async function copyPatches({
|
|
|
28
28
|
let workspaceRootManifest: PackageManifest;
|
|
29
29
|
try {
|
|
30
30
|
workspaceRootManifest = await readTypedJson<PackageManifest>(
|
|
31
|
-
path.join(workspaceRootDir, "package.json")
|
|
31
|
+
path.join(workspaceRootDir, "package.json"),
|
|
32
32
|
);
|
|
33
33
|
} catch (error) {
|
|
34
34
|
log.warn(
|
|
35
|
-
`Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}
|
|
35
|
+
`Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
36
36
|
);
|
|
37
37
|
return {};
|
|
38
38
|
}
|
|
@@ -45,7 +45,7 @@ export async function copyPatches({
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
log.debug(
|
|
48
|
-
`Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace
|
|
48
|
+
`Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`,
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
const filteredPatches = filterPatchedDependencies({
|
|
@@ -69,7 +69,7 @@ export async function copyPatches({
|
|
|
69
69
|
|
|
70
70
|
if (!fs.existsSync(sourcePatchPath)) {
|
|
71
71
|
log.warn(
|
|
72
|
-
`Patch file not found: ${getRootRelativeLogPath(sourcePatchPath, workspaceRootDir)}
|
|
72
|
+
`Patch file not found: ${getRootRelativeLogPath(sourcePatchPath, workspaceRootDir)}`,
|
|
73
73
|
);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
@@ -106,7 +106,7 @@ export async function copyPatches({
|
|
|
106
106
|
* Since the file content is the same after copying, the hash remains valid.
|
|
107
107
|
*/
|
|
108
108
|
async function readLockfilePatchedDependencies(
|
|
109
|
-
workspaceRootDir: string
|
|
109
|
+
workspaceRootDir: string,
|
|
110
110
|
): Promise<Record<string, PatchFile> | undefined> {
|
|
111
111
|
try {
|
|
112
112
|
const { majorVersion } = usePackageManager();
|
|
@@ -13,19 +13,19 @@ import { findPackagesGlobs } from "./helpers";
|
|
|
13
13
|
*/
|
|
14
14
|
export async function createPackagesRegistry(
|
|
15
15
|
workspaceRootDir: string,
|
|
16
|
-
workspacePackagesOverride: string[] | undefined
|
|
16
|
+
workspacePackagesOverride: string[] | undefined,
|
|
17
17
|
): Promise<PackagesRegistry> {
|
|
18
18
|
const log = useLogger();
|
|
19
19
|
|
|
20
20
|
if (workspacePackagesOverride) {
|
|
21
21
|
log.debug(
|
|
22
|
-
`Override workspace packages via config: ${workspacePackagesOverride.join(", ")}
|
|
22
|
+
`Override workspace packages via config: ${workspacePackagesOverride.join(", ")}`,
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const allPackages = listWorkspacePackages(
|
|
27
27
|
workspacePackagesOverride,
|
|
28
|
-
workspaceRootDir
|
|
28
|
+
workspaceRootDir,
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
const registry: PackagesRegistry = (
|
|
@@ -36,14 +36,14 @@ export async function createPackagesRegistry(
|
|
|
36
36
|
|
|
37
37
|
if (!fs.existsSync(manifestPath)) {
|
|
38
38
|
log.warn(
|
|
39
|
-
`Ignoring directory ${rootRelativeDir} because it does not contain a package.json file
|
|
39
|
+
`Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`,
|
|
40
40
|
);
|
|
41
41
|
return;
|
|
42
42
|
} else {
|
|
43
43
|
log.debug(`Registering package ${rootRelativeDir}`);
|
|
44
44
|
|
|
45
45
|
const manifest = await readTypedJson<PackageManifest>(
|
|
46
|
-
path.join(absoluteDir, "package.json")
|
|
46
|
+
path.join(absoluteDir, "package.json"),
|
|
47
47
|
);
|
|
48
48
|
|
|
49
49
|
return {
|
|
@@ -52,7 +52,7 @@ export async function createPackagesRegistry(
|
|
|
52
52
|
absoluteDir,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
})
|
|
55
|
+
}),
|
|
56
56
|
)
|
|
57
57
|
).reduce<PackagesRegistry>((acc, info) => {
|
|
58
58
|
if (info) {
|
|
@@ -70,27 +70,25 @@ type RushConfig = {
|
|
|
70
70
|
|
|
71
71
|
function listWorkspacePackages(
|
|
72
72
|
workspacePackagesOverride: string[] | undefined,
|
|
73
|
-
workspaceRootDir: string
|
|
73
|
+
workspaceRootDir: string,
|
|
74
74
|
) {
|
|
75
75
|
if (isRushWorkspace(workspaceRootDir)) {
|
|
76
76
|
const rushConfig = readTypedJsonSync<RushConfig>(
|
|
77
|
-
path.join(workspaceRootDir, "rush.json")
|
|
77
|
+
path.join(workspaceRootDir, "rush.json"),
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
return rushConfig.projects.map(({ projectFolder }) => projectFolder);
|
|
81
81
|
} else {
|
|
82
|
-
const currentDir = process.cwd();
|
|
83
|
-
process.chdir(workspaceRootDir);
|
|
84
|
-
|
|
85
82
|
const packagesGlobs =
|
|
86
83
|
workspacePackagesOverride ?? findPackagesGlobs(workspaceRootDir);
|
|
87
84
|
|
|
88
85
|
const allPackages = packagesGlobs
|
|
89
|
-
.flatMap((glob) => globSync(glob))
|
|
86
|
+
.flatMap((glob) => globSync(glob, { cwd: workspaceRootDir }))
|
|
90
87
|
/** Make sure to filter any loose files that might hang around. */
|
|
91
|
-
.filter((dir) =>
|
|
88
|
+
.filter((dir) =>
|
|
89
|
+
fs.lstatSync(path.join(workspaceRootDir, dir)).isDirectory(),
|
|
90
|
+
);
|
|
92
91
|
|
|
93
|
-
process.chdir(currentDir);
|
|
94
92
|
return allPackages;
|
|
95
93
|
}
|
|
96
94
|
}
|
|
@@ -21,18 +21,18 @@ export function findPackagesGlobs(workspaceRootDir: string) {
|
|
|
21
21
|
switch (packageManager.name) {
|
|
22
22
|
case "pnpm": {
|
|
23
23
|
const workspaceConfig = readTypedYamlSync<{ packages: string[] }>(
|
|
24
|
-
path.join(workspaceRootDir, "pnpm-workspace.yaml")
|
|
24
|
+
path.join(workspaceRootDir, "pnpm-workspace.yaml"),
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
if (!workspaceConfig) {
|
|
28
28
|
throw new Error(
|
|
29
|
-
"pnpm-workspace.yaml file is empty. Please specify packages configuration."
|
|
29
|
+
"pnpm-workspace.yaml file is empty. Please specify packages configuration.",
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
assert(
|
|
34
34
|
workspaceConfig.packages,
|
|
35
|
-
"packages property must be defined in pnpm-workspace.yaml"
|
|
35
|
+
"packages property must be defined in pnpm-workspace.yaml",
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
const { packages: globs } = workspaceConfig;
|
|
@@ -45,16 +45,16 @@ export function findPackagesGlobs(workspaceRootDir: string) {
|
|
|
45
45
|
case "npm": {
|
|
46
46
|
const workspaceRootManifestPath = path.join(
|
|
47
47
|
workspaceRootDir,
|
|
48
|
-
"package.json"
|
|
48
|
+
"package.json",
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
const { workspaces } = readTypedJsonSync<{ workspaces: string[] }>(
|
|
52
|
-
workspaceRootManifestPath
|
|
52
|
+
workspaceRootManifestPath,
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
if (!workspaces) {
|
|
56
56
|
throw new Error(
|
|
57
|
-
`No workspaces field found in ${workspaceRootManifestPath}
|
|
57
|
+
`No workspaces field found in ${workspaceRootManifestPath}`,
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -70,7 +70,7 @@ export function findPackagesGlobs(workspaceRootDir: string) {
|
|
|
70
70
|
|
|
71
71
|
assert(
|
|
72
72
|
workspacesObject.packages,
|
|
73
|
-
"workspaces.packages must be an array"
|
|
73
|
+
"workspaces.packages must be an array",
|
|
74
74
|
);
|
|
75
75
|
|
|
76
76
|
return workspacesObject.packages;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PackageManifest, PackagesRegistry } from "~/lib/types";
|
|
3
|
+
import { listInternalPackages } from "./list-internal-packages";
|
|
4
|
+
|
|
5
|
+
const mockWarn = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock("~/lib/logger", () => ({
|
|
8
|
+
useLogger: () => ({
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: mockWarn,
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
/** Helper to create a minimal WorkspacePackageInfo entry */
|
|
17
|
+
function entry(manifest: PackageManifest) {
|
|
18
|
+
return {
|
|
19
|
+
absoluteDir: `/workspace/packages/${manifest.name}`,
|
|
20
|
+
rootRelativeDir: `packages/${manifest.name}`,
|
|
21
|
+
manifest,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("listInternalPackages", () => {
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
mockWarn.mockClear();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return an empty array when there are no internal dependencies", () => {
|
|
31
|
+
const manifest: PackageManifest = {
|
|
32
|
+
name: "app",
|
|
33
|
+
version: "1.0.0",
|
|
34
|
+
dependencies: { lodash: "^4.0.0" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const registry: PackagesRegistry = {
|
|
38
|
+
app: entry(manifest),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = listInternalPackages(manifest, registry);
|
|
42
|
+
expect(result).toEqual([]);
|
|
43
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should resolve a simple internal dependency", () => {
|
|
47
|
+
const utilsManifest: PackageManifest = {
|
|
48
|
+
name: "utils",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const appManifest: PackageManifest = {
|
|
53
|
+
name: "app",
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
dependencies: { utils: "workspace:*", lodash: "^4.0.0" },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const registry: PackagesRegistry = {
|
|
59
|
+
app: entry(appManifest),
|
|
60
|
+
utils: entry(utilsManifest),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = listInternalPackages(appManifest, registry);
|
|
64
|
+
expect(result).toEqual(["utils"]);
|
|
65
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should recursively resolve transitive internal dependencies", () => {
|
|
69
|
+
const coreManifest: PackageManifest = {
|
|
70
|
+
name: "core",
|
|
71
|
+
version: "1.0.0",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const utilsManifest: PackageManifest = {
|
|
75
|
+
name: "utils",
|
|
76
|
+
version: "1.0.0",
|
|
77
|
+
dependencies: { core: "workspace:*" },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const appManifest: PackageManifest = {
|
|
81
|
+
name: "app",
|
|
82
|
+
version: "1.0.0",
|
|
83
|
+
dependencies: { utils: "workspace:*" },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const registry: PackagesRegistry = {
|
|
87
|
+
app: entry(appManifest),
|
|
88
|
+
utils: entry(utilsManifest),
|
|
89
|
+
core: entry(coreManifest),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = listInternalPackages(appManifest, registry);
|
|
93
|
+
expect(result).toEqual(expect.arrayContaining(["utils", "core"]));
|
|
94
|
+
expect(result).toHaveLength(2);
|
|
95
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should deduplicate diamond dependencies without warning", () => {
|
|
99
|
+
const coreManifest: PackageManifest = {
|
|
100
|
+
name: "core",
|
|
101
|
+
version: "1.0.0",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const utilsManifest: PackageManifest = {
|
|
105
|
+
name: "utils",
|
|
106
|
+
version: "1.0.0",
|
|
107
|
+
dependencies: { core: "workspace:*" },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const helpersManifest: PackageManifest = {
|
|
111
|
+
name: "helpers",
|
|
112
|
+
version: "1.0.0",
|
|
113
|
+
dependencies: { core: "workspace:*" },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const appManifest: PackageManifest = {
|
|
117
|
+
name: "app",
|
|
118
|
+
version: "1.0.0",
|
|
119
|
+
dependencies: { utils: "workspace:*", helpers: "workspace:*" },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const registry: PackagesRegistry = {
|
|
123
|
+
app: entry(appManifest),
|
|
124
|
+
utils: entry(utilsManifest),
|
|
125
|
+
helpers: entry(helpersManifest),
|
|
126
|
+
core: entry(coreManifest),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = listInternalPackages(appManifest, registry);
|
|
130
|
+
expect(result).toEqual(
|
|
131
|
+
expect.arrayContaining(["utils", "helpers", "core"]),
|
|
132
|
+
);
|
|
133
|
+
expect(result).toHaveLength(3);
|
|
134
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should detect a two-node cycle and log a warning", () => {
|
|
138
|
+
/** A depends on B, B depends on A */
|
|
139
|
+
const bManifest: PackageManifest = {
|
|
140
|
+
name: "b",
|
|
141
|
+
version: "1.0.0",
|
|
142
|
+
dependencies: { a: "workspace:*" },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const aManifest: PackageManifest = {
|
|
146
|
+
name: "a",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
dependencies: { b: "workspace:*" },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const appManifest: PackageManifest = {
|
|
152
|
+
name: "app",
|
|
153
|
+
version: "1.0.0",
|
|
154
|
+
dependencies: { a: "workspace:*" },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const registry: PackagesRegistry = {
|
|
158
|
+
app: entry(appManifest),
|
|
159
|
+
a: entry(aManifest),
|
|
160
|
+
b: entry(bManifest),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = listInternalPackages(appManifest, registry);
|
|
164
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b"]));
|
|
165
|
+
expect(result).toHaveLength(2);
|
|
166
|
+
/** Chain: app → a → b → a */
|
|
167
|
+
expect(mockWarn).toHaveBeenCalledWith(
|
|
168
|
+
expect.stringContaining("app → a → b → a"),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should detect a cycle in nested dependencies and log a warning", () => {
|
|
173
|
+
/** App depends on A, A depends on B, B depends on C, C depends on B */
|
|
174
|
+
const cManifest: PackageManifest = {
|
|
175
|
+
name: "c",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
dependencies: { b: "workspace:*" },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const bManifest: PackageManifest = {
|
|
181
|
+
name: "b",
|
|
182
|
+
version: "1.0.0",
|
|
183
|
+
dependencies: { c: "workspace:*" },
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const aManifest: PackageManifest = {
|
|
187
|
+
name: "a",
|
|
188
|
+
version: "1.0.0",
|
|
189
|
+
dependencies: { b: "workspace:*" },
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const appManifest: PackageManifest = {
|
|
193
|
+
name: "app",
|
|
194
|
+
version: "1.0.0",
|
|
195
|
+
dependencies: { a: "workspace:*" },
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const registry: PackagesRegistry = {
|
|
199
|
+
app: entry(appManifest),
|
|
200
|
+
a: entry(aManifest),
|
|
201
|
+
b: entry(bManifest),
|
|
202
|
+
c: entry(cManifest),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const result = listInternalPackages(appManifest, registry);
|
|
206
|
+
expect(result).toEqual(expect.arrayContaining(["a", "b", "c"]));
|
|
207
|
+
expect(result).toHaveLength(3);
|
|
208
|
+
/** Chain: app → a → b → c → b */
|
|
209
|
+
expect(mockWarn).toHaveBeenCalledWith(
|
|
210
|
+
expect.stringContaining("app → a → b → c → b"),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should include devDependencies and handle cycles in them", () => {
|
|
215
|
+
const devLibManifest: PackageManifest = {
|
|
216
|
+
name: "dev-lib",
|
|
217
|
+
version: "1.0.0",
|
|
218
|
+
dependencies: { app: "workspace:*" },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const appManifest: PackageManifest = {
|
|
222
|
+
name: "app",
|
|
223
|
+
version: "1.0.0",
|
|
224
|
+
dependencies: { lodash: "^4.0.0" },
|
|
225
|
+
devDependencies: { "dev-lib": "workspace:*" },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const registry: PackagesRegistry = {
|
|
229
|
+
app: entry(appManifest),
|
|
230
|
+
"dev-lib": entry(devLibManifest),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Without devDependencies — should not find dev-lib */
|
|
234
|
+
const withoutDev = listInternalPackages(appManifest, registry);
|
|
235
|
+
expect(withoutDev).toEqual([]);
|
|
236
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
|
237
|
+
|
|
238
|
+
/** With devDependencies — should find dev-lib and detect the cycle back to app */
|
|
239
|
+
const withDev = listInternalPackages(appManifest, registry, {
|
|
240
|
+
includeDevDependencies: true,
|
|
241
|
+
});
|
|
242
|
+
expect(withDev).toEqual(["dev-lib"]);
|
|
243
|
+
/** Chain: app → dev-lib → app */
|
|
244
|
+
expect(mockWarn).toHaveBeenCalledWith(
|
|
245
|
+
expect.stringContaining("app → dev-lib → app"),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should handle name clash that creates a false cycle (issue #138)", () => {
|
|
250
|
+
/**
|
|
251
|
+
* Reproduces the crash from issue #138: internal "config" depends on
|
|
252
|
+
* "server", and "server" depends on the npm "config" package. Because
|
|
253
|
+
* both the internal and external package share the name "config", the
|
|
254
|
+
* tool misidentifies the external reference as internal, creating a
|
|
255
|
+
* cycle: config → server → config. Without cycle detection this causes
|
|
256
|
+
* "Maximum call stack size exceeded".
|
|
257
|
+
*/
|
|
258
|
+
const configManifest: PackageManifest = {
|
|
259
|
+
name: "config",
|
|
260
|
+
version: "1.0.0",
|
|
261
|
+
dependencies: { server: "workspace:*" },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const serverManifest: PackageManifest = {
|
|
265
|
+
name: "server",
|
|
266
|
+
version: "1.0.0",
|
|
267
|
+
/** Intended as npm "config", but misidentified as the internal one */
|
|
268
|
+
dependencies: { config: "^3.0.0" },
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const appManifest: PackageManifest = {
|
|
272
|
+
name: "app",
|
|
273
|
+
version: "1.0.0",
|
|
274
|
+
dependencies: { config: "workspace:*" },
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const registry: PackagesRegistry = {
|
|
278
|
+
app: entry(appManifest),
|
|
279
|
+
server: entry(serverManifest),
|
|
280
|
+
config: entry(configManifest),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const result = listInternalPackages(appManifest, registry);
|
|
284
|
+
expect(result).toEqual(expect.arrayContaining(["config", "server"]));
|
|
285
|
+
expect(result).toHaveLength(2);
|
|
286
|
+
/** The false cycle config → server → config is detected and warned about */
|
|
287
|
+
expect(mockWarn).toHaveBeenCalledWith(
|
|
288
|
+
expect.stringContaining("app → config → server → config"),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { got } from "get-or-throw";
|
|
2
|
-
import {
|
|
2
|
+
import { useLogger } from "../logger";
|
|
3
3
|
import type { PackageManifest, PackagesRegistry } from "../types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Recursively
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the package manifest. We can simply compare the package names with the list
|
|
11
|
-
* of packages that were found via the workspace glob patterns and add them to
|
|
12
|
-
* the registry.
|
|
6
|
+
* Recursively collect internal packages, tracking visited nodes and the current
|
|
7
|
+
* ancestor chain to detect cycles. When a cycle is detected, the cyclic
|
|
8
|
+
* reference is not followed, preventing infinite recursion, and a warning is
|
|
9
|
+
* logged.
|
|
13
10
|
*/
|
|
14
|
-
|
|
11
|
+
function collectInternalPackages(
|
|
15
12
|
manifest: PackageManifest,
|
|
16
13
|
packagesRegistry: PackagesRegistry,
|
|
17
|
-
|
|
14
|
+
includeDevDependencies: boolean,
|
|
15
|
+
visited: Set<string>,
|
|
16
|
+
ancestors: Set<string>,
|
|
18
17
|
): string[] {
|
|
19
18
|
const allWorkspacePackageNames = Object.keys(packagesRegistry);
|
|
20
19
|
|
|
@@ -27,14 +26,67 @@ export function listInternalPackages(
|
|
|
27
26
|
: Object.keys(manifest.dependencies ?? {})
|
|
28
27
|
).filter((name) => allWorkspacePackageNames.includes(name));
|
|
29
28
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
29
|
+
const result: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const packageName of internalPackageNames) {
|
|
32
|
+
if (ancestors.has(packageName)) {
|
|
33
|
+
/** Cycle detected — log a warning, skip adding and recursion */
|
|
34
|
+
const chain = [...ancestors, packageName].join(" → ");
|
|
35
|
+
const log = useLogger();
|
|
36
|
+
log.warn(
|
|
37
|
+
`Circular dependency detected: ${chain}. This is likely caused by a workspace package name clashing with an external npm dependency.`,
|
|
38
|
+
);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (visited.has(packageName)) {
|
|
43
|
+
/** Already fully processed (diamond dependency) — skip silently */
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
result.push(packageName);
|
|
48
|
+
|
|
49
|
+
ancestors.add(packageName);
|
|
50
|
+
const nested = collectInternalPackages(
|
|
51
|
+
got(packagesRegistry, packageName).manifest,
|
|
52
|
+
packagesRegistry,
|
|
53
|
+
includeDevDependencies,
|
|
54
|
+
visited,
|
|
55
|
+
ancestors,
|
|
56
|
+
);
|
|
57
|
+
ancestors.delete(packageName);
|
|
58
|
+
visited.add(packageName);
|
|
59
|
+
|
|
60
|
+
result.push(...nested);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursively list all the packages from dependencies (and optionally
|
|
68
|
+
* devDependencies) that are found in the monorepo.
|
|
69
|
+
*
|
|
70
|
+
* Here we do not need to rely on packages being declared with "workspace:" in
|
|
71
|
+
* the package manifest. We can simply compare the package names with the list
|
|
72
|
+
* of packages that were found via the workspace glob patterns and add them to
|
|
73
|
+
* the registry.
|
|
74
|
+
*/
|
|
75
|
+
export function listInternalPackages(
|
|
76
|
+
manifest: PackageManifest,
|
|
77
|
+
packagesRegistry: PackagesRegistry,
|
|
78
|
+
{ includeDevDependencies = false } = {},
|
|
79
|
+
): string[] {
|
|
80
|
+
const visited = new Set<string>();
|
|
81
|
+
const ancestors = new Set<string>(manifest.name ? [manifest.name] : []);
|
|
82
|
+
|
|
83
|
+
const result = collectInternalPackages(
|
|
84
|
+
manifest,
|
|
85
|
+
packagesRegistry,
|
|
86
|
+
includeDevDependencies,
|
|
87
|
+
visited,
|
|
88
|
+
ancestors,
|
|
37
89
|
);
|
|
38
90
|
|
|
39
|
-
return
|
|
91
|
+
return [...new Set(result)];
|
|
40
92
|
}
|
|
@@ -50,13 +50,13 @@ export function filterPatchedDependencies<T>({
|
|
|
50
50
|
|
|
51
51
|
/** Package not found in dependencies or devDependencies */
|
|
52
52
|
log.debug(
|
|
53
|
-
`Excluding patch: ${packageSpec} (package "${packageName}" not in target dependencies)
|
|
53
|
+
`Excluding patch: ${packageSpec} (package "${packageName}" not in target dependencies)`,
|
|
54
54
|
);
|
|
55
55
|
excludedCount++;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
log.debug(
|
|
59
|
-
`Filtered patches: ${includedCount} included, ${excludedCount} excluded
|
|
59
|
+
`Filtered patches: ${includedCount} included, ${excludedCount} excluded`,
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
return Object.keys(filteredPatches).length > 0 ? filteredPatches : undefined;
|
package/src/lib/utils/json.ts
CHANGED
|
@@ -7,13 +7,13 @@ export function readTypedJsonSync<T>(filePath: string) {
|
|
|
7
7
|
try {
|
|
8
8
|
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
9
9
|
const data = JSON.parse(
|
|
10
|
-
stripJsonComments(rawContent, { trailingCommas: true })
|
|
10
|
+
stripJsonComments(rawContent, { trailingCommas: true }),
|
|
11
11
|
) as T;
|
|
12
12
|
return data;
|
|
13
13
|
} catch (err) {
|
|
14
14
|
throw new Error(
|
|
15
15
|
`Failed to read JSON from ${filePath}: ${getErrorMessage(err)}`,
|
|
16
|
-
{ cause: err }
|
|
16
|
+
{ cause: err },
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -22,13 +22,13 @@ export async function readTypedJson<T>(filePath: string) {
|
|
|
22
22
|
try {
|
|
23
23
|
const rawContent = await fs.readFile(filePath, "utf-8");
|
|
24
24
|
const data = JSON.parse(
|
|
25
|
-
stripJsonComments(rawContent, { trailingCommas: true })
|
|
25
|
+
stripJsonComments(rawContent, { trailingCommas: true }),
|
|
26
26
|
) as T;
|
|
27
27
|
return data;
|
|
28
28
|
} catch (err) {
|
|
29
29
|
throw new Error(
|
|
30
30
|
`Failed to read JSON from ${filePath}: ${getErrorMessage(err)}`,
|
|
31
|
-
{ cause: err }
|
|
31
|
+
{ cause: err },
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/lib/utils/pack.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function pack(srcDir: string, dstDir: string) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
resolve(stdout);
|
|
35
|
-
}
|
|
35
|
+
},
|
|
36
36
|
);
|
|
37
37
|
})
|
|
38
38
|
: await new Promise<string>((resolve, reject) => {
|
|
@@ -45,7 +45,7 @@ export async function pack(srcDir: string, dstDir: string) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
resolve(stdout);
|
|
48
|
-
}
|
|
48
|
+
},
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -61,7 +61,7 @@ export async function pack(srcDir: string, dstDir: string) {
|
|
|
61
61
|
|
|
62
62
|
if (!fs.existsSync(filePath)) {
|
|
63
63
|
log.error(
|
|
64
|
-
`The response from pack could not be resolved to an existing file: ${filePath}
|
|
64
|
+
`The response from pack could not be resolved to an existing file: ${filePath}`,
|
|
65
65
|
);
|
|
66
66
|
} else {
|
|
67
67
|
log.debug(`Packed (temp)/${fileName}`);
|