plgg-bundle 0.0.3 → 0.0.6
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/bin/relocate.mjs +27 -18
- package/package.json +2 -2
- package/src/domain/usecase/collectModules.spec.ts +441 -0
- package/src/domain/usecase/collectModules.ts +91 -1
- package/src/domain/usecase/discoverWorkspace.spec.ts +111 -0
- package/src/domain/usecase/discoverWorkspace.ts +139 -24
- package/src/domain/usecase/resolveWorkspaceSpecifier.spec.ts +97 -0
- package/src/domain/usecase/resolveWorkspaceSpecifier.ts +39 -13
package/bin/relocate.mjs
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// These tools run their `src/**/*.ts` entry directly and rely on Node stripping
|
|
4
4
|
// types on load. Node refuses to strip types for `.ts` files under
|
|
5
5
|
// `node_modules`, so a registry-installed tool cannot run in place. This helper
|
|
6
|
-
// copies the package to a version-
|
|
7
|
-
// tool's own deps reachable via a `node_modules`
|
|
8
|
-
// there. On a monorepo `file:` link the package
|
|
9
|
-
// `node_modules`, so it is a no-op.
|
|
6
|
+
// copies the package to a per-(version, install-location) dir OUTSIDE
|
|
7
|
+
// `node_modules` (with the tool's own deps reachable via a `node_modules`
|
|
8
|
+
// symlink) and re-execs the copy there. On a monorepo `file:` link the package
|
|
9
|
+
// realpath is already outside `node_modules`, so it is a no-op.
|
|
10
10
|
//
|
|
11
11
|
// Plain `.mjs`, Node built-ins only — it runs at process entry, before any
|
|
12
12
|
// resolver hook is registered.
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import {
|
|
15
16
|
cpSync,
|
|
16
17
|
existsSync,
|
|
@@ -54,14 +55,25 @@ export const relocateOutOfNodeModules = (
|
|
|
54
55
|
"utf8",
|
|
55
56
|
),
|
|
56
57
|
);
|
|
58
|
+
// The node_modules that CONTAINS this package holds its deps (npm hoists
|
|
59
|
+
// there). Key the relocation dir by that location too — two installs of the
|
|
60
|
+
// same version from different trees (e.g. a publish smoke's scratch install
|
|
61
|
+
// and a real consumer) must NOT share a copy, or one inherits the other's
|
|
62
|
+
// stale (possibly deleted) deps symlink.
|
|
63
|
+
const depsNodeModules = dirname(pkgRoot);
|
|
64
|
+
const tag = createHash("sha1")
|
|
65
|
+
.update(depsNodeModules)
|
|
66
|
+
.digest("hex")
|
|
67
|
+
.slice(0, 12);
|
|
57
68
|
const dest = join(
|
|
58
69
|
tmpdir(),
|
|
59
|
-
`plgg-relocate-${pkg.name}-${pkg.version}`,
|
|
70
|
+
`plgg-relocate-${pkg.name}-${pkg.version}-${tag}`,
|
|
60
71
|
);
|
|
61
72
|
const ready = join(
|
|
62
73
|
dest,
|
|
63
74
|
".plgg-relocate-ready",
|
|
64
75
|
);
|
|
76
|
+
const link = join(dest, "node_modules");
|
|
65
77
|
|
|
66
78
|
if (!existsSync(ready)) {
|
|
67
79
|
rmSync(dest, {
|
|
@@ -81,19 +93,16 @@ export const relocateOutOfNodeModules = (
|
|
|
81
93
|
join(pkgRoot, "package.json"),
|
|
82
94
|
join(dest, "package.json"),
|
|
83
95
|
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// A pre-existing symlink from a concurrent run is fine.
|
|
95
|
-
}
|
|
96
|
-
writeFileSync(ready, "");
|
|
96
|
+
writeFileSync(ready, depsNodeModules + "\n");
|
|
97
|
+
}
|
|
98
|
+
// (Re)create the deps symlink every run so it always points at the CURRENT
|
|
99
|
+
// node_modules: a cached copy from a prior run may hold a symlink to a tree
|
|
100
|
+
// that has since been removed (a publish smoke's scratch install) or moved.
|
|
101
|
+
rmSync(link, { force: true });
|
|
102
|
+
try {
|
|
103
|
+
symlinkSync(depsNodeModules, link, "dir");
|
|
104
|
+
} catch {
|
|
105
|
+
// A concurrent run created it first; its target is identical.
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
const child = spawnSync(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plgg-bundle",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "In-house minimal library bundler for the plgg monorepo (ESM+CJS dual output, per-file .d.ts tree), zero new dependencies — reuses the project's own TypeScript, no native binding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^25.6.0",
|
|
26
|
-
"plgg-test": "^0.0.
|
|
26
|
+
"plgg-test": "^0.0.5"
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
test,
|
|
3
|
+
check,
|
|
4
|
+
all,
|
|
5
|
+
toBe,
|
|
6
|
+
} from "plgg-test";
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { collectModules } from "plgg-bundle/domain/usecase/collectModules";
|
|
16
|
+
import {
|
|
17
|
+
emitCjsBundle,
|
|
18
|
+
emitEsmBundle,
|
|
19
|
+
} from "plgg-bundle/domain/usecase/emitBundle";
|
|
20
|
+
|
|
21
|
+
test("collectModules records external specifiers without walking them", () => {
|
|
22
|
+
const root = mkdtempSync(
|
|
23
|
+
join(tmpdir(), "plgg-bundle-collect-ext-"),
|
|
24
|
+
);
|
|
25
|
+
try {
|
|
26
|
+
const entry = join(root, "src", "main.ts");
|
|
27
|
+
mkdirSync(join(root, "src"), {
|
|
28
|
+
recursive: true,
|
|
29
|
+
});
|
|
30
|
+
writeFileSync(
|
|
31
|
+
entry,
|
|
32
|
+
'import { readFileSync } from "node:fs";\nexport const read = readFileSync;\n',
|
|
33
|
+
);
|
|
34
|
+
const graph = collectModules({
|
|
35
|
+
entryFile: entry,
|
|
36
|
+
root,
|
|
37
|
+
aliasPrefix: "app",
|
|
38
|
+
aliasSrcRoot: join(root, "src"),
|
|
39
|
+
external: /^node:/,
|
|
40
|
+
});
|
|
41
|
+
return all([
|
|
42
|
+
check(graph.modules.length, toBe(1)),
|
|
43
|
+
check(
|
|
44
|
+
graph.modules[0]?.externals[0],
|
|
45
|
+
toBe("node:fs"),
|
|
46
|
+
),
|
|
47
|
+
]);
|
|
48
|
+
} finally {
|
|
49
|
+
rmSync(root, {
|
|
50
|
+
recursive: true,
|
|
51
|
+
force: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("collectModules throws on an unresolvable non-external specifier", () => {
|
|
57
|
+
const root = mkdtempSync(
|
|
58
|
+
join(
|
|
59
|
+
tmpdir(),
|
|
60
|
+
"plgg-bundle-collect-missing-",
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
try {
|
|
64
|
+
const entry = join(root, "src", "main.ts");
|
|
65
|
+
mkdirSync(join(root, "src"), {
|
|
66
|
+
recursive: true,
|
|
67
|
+
});
|
|
68
|
+
writeFileSync(entry, 'import "missing";\n');
|
|
69
|
+
return check(
|
|
70
|
+
rejects(() =>
|
|
71
|
+
collectModules({
|
|
72
|
+
entryFile: entry,
|
|
73
|
+
root,
|
|
74
|
+
aliasPrefix: "app",
|
|
75
|
+
aliasSrcRoot: join(root, "src"),
|
|
76
|
+
external: /^node:/,
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
toBe(true),
|
|
80
|
+
);
|
|
81
|
+
} finally {
|
|
82
|
+
rmSync(root, {
|
|
83
|
+
recursive: true,
|
|
84
|
+
force: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("collectModules inlines an installed prebundled dist entry without resolving its internal registry requires", () => {
|
|
90
|
+
const root = mkdtempSync(
|
|
91
|
+
join(tmpdir(), "plgg-bundle-collect-"),
|
|
92
|
+
);
|
|
93
|
+
try {
|
|
94
|
+
const entry = join(root, "src", "main.ts");
|
|
95
|
+
const dep = join(
|
|
96
|
+
root,
|
|
97
|
+
"node_modules",
|
|
98
|
+
"dep",
|
|
99
|
+
"dist",
|
|
100
|
+
"index.es.js",
|
|
101
|
+
);
|
|
102
|
+
const plgg = join(
|
|
103
|
+
root,
|
|
104
|
+
"node_modules",
|
|
105
|
+
"plgg",
|
|
106
|
+
"dist",
|
|
107
|
+
"index.es.js",
|
|
108
|
+
);
|
|
109
|
+
mkdirSync(join(root, "src"), {
|
|
110
|
+
recursive: true,
|
|
111
|
+
});
|
|
112
|
+
mkdirSync(
|
|
113
|
+
join(root, "node_modules", "dep", "dist"),
|
|
114
|
+
{
|
|
115
|
+
recursive: true,
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
mkdirSync(
|
|
119
|
+
join(root, "node_modules", "plgg", "dist"),
|
|
120
|
+
{
|
|
121
|
+
recursive: true,
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
writeFileSync(
|
|
125
|
+
entry,
|
|
126
|
+
'import dep from "dep";\nexport const value = dep.value;\n',
|
|
127
|
+
);
|
|
128
|
+
writeFileSync(
|
|
129
|
+
dep,
|
|
130
|
+
[
|
|
131
|
+
'import * as __ext0 from "plgg";',
|
|
132
|
+
'const __externals = { "plgg": __ext0 };',
|
|
133
|
+
"const __modules = {",
|
|
134
|
+
'"src/index.ts": function (module, exports, require) {',
|
|
135
|
+
' exports.value = require("src/internal.ts").value;',
|
|
136
|
+
"},",
|
|
137
|
+
'"src/internal.ts": function (module, exports, require) {',
|
|
138
|
+
" exports.value = __externals.plgg.external;",
|
|
139
|
+
"}",
|
|
140
|
+
"};",
|
|
141
|
+
'const __entry = __modules["src/index.ts"];',
|
|
142
|
+
"export default { value: __entry };",
|
|
143
|
+
].join("\n"),
|
|
144
|
+
);
|
|
145
|
+
writeFileSync(
|
|
146
|
+
plgg,
|
|
147
|
+
"export const external = 1;\n",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const graph = collectModules({
|
|
151
|
+
entryFile: entry,
|
|
152
|
+
root,
|
|
153
|
+
aliasPrefix: "app",
|
|
154
|
+
aliasSrcRoot: join(root, "src"),
|
|
155
|
+
external: /^node:/,
|
|
156
|
+
resolve: (specifier, _fromFile) =>
|
|
157
|
+
specifier === "dep"
|
|
158
|
+
? dep
|
|
159
|
+
: specifier === "plgg"
|
|
160
|
+
? plgg
|
|
161
|
+
: undefined,
|
|
162
|
+
});
|
|
163
|
+
const depModule = graph.modules.find(
|
|
164
|
+
(m) =>
|
|
165
|
+
m.id ===
|
|
166
|
+
"node_modules/dep/dist/index.es.js",
|
|
167
|
+
);
|
|
168
|
+
return all([
|
|
169
|
+
check(graph.modules.length, toBe(3)),
|
|
170
|
+
check(
|
|
171
|
+
depModule?.code.includes(
|
|
172
|
+
'require("src/internal.ts")',
|
|
173
|
+
),
|
|
174
|
+
toBe(true),
|
|
175
|
+
),
|
|
176
|
+
check(
|
|
177
|
+
depModule?.code.includes(
|
|
178
|
+
'require("node_modules/plgg/dist/index.es.js")',
|
|
179
|
+
),
|
|
180
|
+
toBe(true),
|
|
181
|
+
),
|
|
182
|
+
]);
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(root, {
|
|
185
|
+
recursive: true,
|
|
186
|
+
force: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("collectModules resolves node_modules source files without dist-registry filtering", () => {
|
|
192
|
+
const root = mkdtempSync(
|
|
193
|
+
join(tmpdir(), "plgg-bundle-collect-src-"),
|
|
194
|
+
);
|
|
195
|
+
try {
|
|
196
|
+
const entry = join(root, "src", "main.ts");
|
|
197
|
+
const dep = join(
|
|
198
|
+
root,
|
|
199
|
+
"node_modules",
|
|
200
|
+
"dep",
|
|
201
|
+
"src",
|
|
202
|
+
"index.ts",
|
|
203
|
+
);
|
|
204
|
+
const internal = join(
|
|
205
|
+
root,
|
|
206
|
+
"node_modules",
|
|
207
|
+
"dep",
|
|
208
|
+
"src",
|
|
209
|
+
"internal.ts",
|
|
210
|
+
);
|
|
211
|
+
writeImportFixture(entry, dep, internal);
|
|
212
|
+
const graph = collectModules({
|
|
213
|
+
entryFile: entry,
|
|
214
|
+
root,
|
|
215
|
+
aliasPrefix: "app",
|
|
216
|
+
aliasSrcRoot: join(root, "src"),
|
|
217
|
+
external: /^node:/,
|
|
218
|
+
resolve: importFixtureResolver(
|
|
219
|
+
dep,
|
|
220
|
+
internal,
|
|
221
|
+
),
|
|
222
|
+
});
|
|
223
|
+
return check(graph.modules.length, toBe(3));
|
|
224
|
+
} finally {
|
|
225
|
+
rmSync(root, {
|
|
226
|
+
recursive: true,
|
|
227
|
+
force: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("collectModules resolves non-js dist source files without dist-registry filtering", () => {
|
|
233
|
+
const root = mkdtempSync(
|
|
234
|
+
join(tmpdir(), "plgg-bundle-collect-mjs-"),
|
|
235
|
+
);
|
|
236
|
+
try {
|
|
237
|
+
const entry = join(root, "src", "main.ts");
|
|
238
|
+
const dep = join(
|
|
239
|
+
root,
|
|
240
|
+
"node_modules",
|
|
241
|
+
"dep",
|
|
242
|
+
"dist",
|
|
243
|
+
"index.ts",
|
|
244
|
+
);
|
|
245
|
+
const internal = join(
|
|
246
|
+
root,
|
|
247
|
+
"node_modules",
|
|
248
|
+
"dep",
|
|
249
|
+
"dist",
|
|
250
|
+
"internal.ts",
|
|
251
|
+
);
|
|
252
|
+
writeImportFixture(entry, dep, internal);
|
|
253
|
+
const graph = collectModules({
|
|
254
|
+
entryFile: entry,
|
|
255
|
+
root,
|
|
256
|
+
aliasPrefix: "app",
|
|
257
|
+
aliasSrcRoot: join(root, "src"),
|
|
258
|
+
external: /^node:/,
|
|
259
|
+
resolve: importFixtureResolver(
|
|
260
|
+
dep,
|
|
261
|
+
internal,
|
|
262
|
+
),
|
|
263
|
+
});
|
|
264
|
+
return check(graph.modules.length, toBe(3));
|
|
265
|
+
} finally {
|
|
266
|
+
rmSync(root, {
|
|
267
|
+
recursive: true,
|
|
268
|
+
force: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("an app bundling two dists with identical inner module paths keeps each dist's externals resolvable", () => {
|
|
274
|
+
// Regression for the plggmatic blank-page defect:
|
|
275
|
+
// pkg-a and pkg-b are plgg-bundle ESM dists whose
|
|
276
|
+
// inner registries both key "src/index.ts", and
|
|
277
|
+
// pkg-b requires pkg-a through its inner
|
|
278
|
+
// __externals table. The outer rewrite must keep
|
|
279
|
+
// that table's key in step with the rewritten
|
|
280
|
+
// require, or pkg-b's inner lookup falls into the
|
|
281
|
+
// async import fallback and yields a Promise where
|
|
282
|
+
// a namespace is expected.
|
|
283
|
+
const root = mkdtempSync(
|
|
284
|
+
join(tmpdir(), "plgg-bundle-collect-nested-"),
|
|
285
|
+
);
|
|
286
|
+
try {
|
|
287
|
+
const entry = join(root, "src", "main.ts");
|
|
288
|
+
const pkgA = join(
|
|
289
|
+
root,
|
|
290
|
+
"node_modules",
|
|
291
|
+
"pkg-a",
|
|
292
|
+
"dist",
|
|
293
|
+
"index.es.js",
|
|
294
|
+
);
|
|
295
|
+
const pkgB = join(
|
|
296
|
+
root,
|
|
297
|
+
"node_modules",
|
|
298
|
+
"pkg-b",
|
|
299
|
+
"dist",
|
|
300
|
+
"index.es.js",
|
|
301
|
+
);
|
|
302
|
+
mkdirSync(join(root, "src"), {
|
|
303
|
+
recursive: true,
|
|
304
|
+
});
|
|
305
|
+
mkdirSync(join(pkgA, ".."), {
|
|
306
|
+
recursive: true,
|
|
307
|
+
});
|
|
308
|
+
mkdirSync(join(pkgB, ".."), {
|
|
309
|
+
recursive: true,
|
|
310
|
+
});
|
|
311
|
+
writeFileSync(
|
|
312
|
+
entry,
|
|
313
|
+
[
|
|
314
|
+
'import { box } from "pkg-a";',
|
|
315
|
+
'import { useBox } from "pkg-b";',
|
|
316
|
+
"export const direct = box(1);",
|
|
317
|
+
"export const viaB = useBox();",
|
|
318
|
+
].join("\n"),
|
|
319
|
+
);
|
|
320
|
+
// Both dists come from the real emitter, so the
|
|
321
|
+
// fixture cannot drift from the emitted shape.
|
|
322
|
+
writeFileSync(
|
|
323
|
+
pkgA,
|
|
324
|
+
emitEsmBundle(
|
|
325
|
+
{
|
|
326
|
+
entryId: "src/index.ts",
|
|
327
|
+
modules: [
|
|
328
|
+
{
|
|
329
|
+
id: "src/index.ts",
|
|
330
|
+
code: "exports.box = (v) => v * 2;",
|
|
331
|
+
externals: [],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
["box"],
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
writeFileSync(
|
|
339
|
+
pkgB,
|
|
340
|
+
emitEsmBundle(
|
|
341
|
+
{
|
|
342
|
+
entryId: "src/index.ts",
|
|
343
|
+
modules: [
|
|
344
|
+
{
|
|
345
|
+
id: "src/index.ts",
|
|
346
|
+
code: 'exports.useBox = () => require("pkg-a").box(21);',
|
|
347
|
+
externals: ["pkg-a"],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
["useBox"],
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
const graph = collectModules({
|
|
355
|
+
entryFile: entry,
|
|
356
|
+
root,
|
|
357
|
+
aliasPrefix: "app",
|
|
358
|
+
aliasSrcRoot: join(root, "src"),
|
|
359
|
+
external: /^node:/,
|
|
360
|
+
resolve: (specifier, _fromFile) =>
|
|
361
|
+
specifier === "pkg-a"
|
|
362
|
+
? pkgA
|
|
363
|
+
: specifier === "pkg-b"
|
|
364
|
+
? pkgB
|
|
365
|
+
: undefined,
|
|
366
|
+
});
|
|
367
|
+
const mod: {
|
|
368
|
+
exports: Record<string, unknown>;
|
|
369
|
+
} = {
|
|
370
|
+
exports: {},
|
|
371
|
+
};
|
|
372
|
+
new Function(
|
|
373
|
+
"module",
|
|
374
|
+
"exports",
|
|
375
|
+
"require",
|
|
376
|
+
emitCjsBundle(graph),
|
|
377
|
+
)(mod, mod.exports, () => ({}));
|
|
378
|
+
return all([
|
|
379
|
+
check(mod.exports.direct, toBe(2)),
|
|
380
|
+
check(mod.exports.viaB, toBe(42)),
|
|
381
|
+
]);
|
|
382
|
+
} finally {
|
|
383
|
+
rmSync(root, {
|
|
384
|
+
recursive: true,
|
|
385
|
+
force: true,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const writeImportFixture = (
|
|
391
|
+
entry: string,
|
|
392
|
+
dep: string,
|
|
393
|
+
internal: string,
|
|
394
|
+
): void => {
|
|
395
|
+
mkdirSync(join(entry, ".."), {
|
|
396
|
+
recursive: true,
|
|
397
|
+
});
|
|
398
|
+
mkdirSync(join(dep, ".."), { recursive: true });
|
|
399
|
+
mkdirSync(join(internal, ".."), {
|
|
400
|
+
recursive: true,
|
|
401
|
+
});
|
|
402
|
+
writeFileSync(
|
|
403
|
+
entry,
|
|
404
|
+
'import { value } from "dep";\nexport const out = value;\n',
|
|
405
|
+
);
|
|
406
|
+
writeFileSync(
|
|
407
|
+
dep,
|
|
408
|
+
'import { value } from "src/internal.ts";\nexport { value };\n',
|
|
409
|
+
);
|
|
410
|
+
writeFileSync(
|
|
411
|
+
internal,
|
|
412
|
+
"export const value = 1;\n",
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const importFixtureResolver =
|
|
417
|
+
(
|
|
418
|
+
dep: string,
|
|
419
|
+
internal: string,
|
|
420
|
+
): ((
|
|
421
|
+
specifier: string,
|
|
422
|
+
fromFile: string,
|
|
423
|
+
) => string | undefined) =>
|
|
424
|
+
(specifier, _fromFile) =>
|
|
425
|
+
specifier === "dep"
|
|
426
|
+
? dep
|
|
427
|
+
: specifier === "src/internal.ts"
|
|
428
|
+
? internal
|
|
429
|
+
: undefined;
|
|
430
|
+
|
|
431
|
+
const rejects = (f: () => unknown): boolean => {
|
|
432
|
+
try {
|
|
433
|
+
f();
|
|
434
|
+
return false;
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return (
|
|
437
|
+
e instanceof Error &&
|
|
438
|
+
e.message.startsWith("ResolveError")
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
@@ -135,7 +135,10 @@ const linkModule = (
|
|
|
135
135
|
cjs: string,
|
|
136
136
|
acc: Map<string, Module>,
|
|
137
137
|
): ReadonlyArray<string> => {
|
|
138
|
-
const specifiers =
|
|
138
|
+
const specifiers = inlineRequireSpecifiers(
|
|
139
|
+
file,
|
|
140
|
+
cjs,
|
|
141
|
+
);
|
|
139
142
|
const externals: string[] = [];
|
|
140
143
|
const deps: string[] = [];
|
|
141
144
|
let rewritten = cjs;
|
|
@@ -156,6 +159,13 @@ const linkModule = (
|
|
|
156
159
|
spec,
|
|
157
160
|
idOf(ctx.root, resolved),
|
|
158
161
|
);
|
|
162
|
+
if (isInstalledDist(file)) {
|
|
163
|
+
rewritten = replaceExternalKey(
|
|
164
|
+
rewritten,
|
|
165
|
+
spec,
|
|
166
|
+
idOf(ctx.root, resolved),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
159
169
|
}
|
|
160
170
|
acc.set(id, {
|
|
161
171
|
id,
|
|
@@ -165,6 +175,63 @@ const linkModule = (
|
|
|
165
175
|
return deps;
|
|
166
176
|
};
|
|
167
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Literal requires that belong to the OUTER graph walk.
|
|
180
|
+
* Registry-installed plgg-family packages ship
|
|
181
|
+
* pre-bundled dist entries whose internal module table
|
|
182
|
+
* still contains strings like `require("src/index.ts")`.
|
|
183
|
+
* Those are parameters of the INNER registry runtime and
|
|
184
|
+
* must not be resolved or rewritten by this app bundle.
|
|
185
|
+
* Real top-level externals in that dist file remain, e.g.
|
|
186
|
+
* `require("plgg")`, and are still inlined by the app
|
|
187
|
+
* graph.
|
|
188
|
+
*/
|
|
189
|
+
const inlineRequireSpecifiers = (
|
|
190
|
+
file: string,
|
|
191
|
+
cjs: string,
|
|
192
|
+
): ReadonlyArray<string> => {
|
|
193
|
+
const specifiers = requireSpecifiers(cjs);
|
|
194
|
+
if (!isInstalledDist(file)) {
|
|
195
|
+
return specifiers;
|
|
196
|
+
}
|
|
197
|
+
const internal = bundledModuleIds(cjs);
|
|
198
|
+
return specifiers.filter(
|
|
199
|
+
(spec) => !internal.has(spec),
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether a file is a registry-installed built JS entry.
|
|
205
|
+
* Source packages under the monorepo are never matched.
|
|
206
|
+
*/
|
|
207
|
+
const isInstalledDist = (
|
|
208
|
+
file: string,
|
|
209
|
+
): boolean => {
|
|
210
|
+
const normalized = file.split("\\").join("/");
|
|
211
|
+
return (
|
|
212
|
+
normalized.includes("/node_modules/") &&
|
|
213
|
+
normalized.includes("/dist/") &&
|
|
214
|
+
normalized.endsWith(".js")
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Module ids declared inside a plgg-bundle emitted
|
|
220
|
+
* registry object: `"src/x.ts": function (...) { ... }`.
|
|
221
|
+
*/
|
|
222
|
+
const bundledModuleIds = (
|
|
223
|
+
cjs: string,
|
|
224
|
+
): ReadonlySet<string> =>
|
|
225
|
+
new Set(
|
|
226
|
+
[
|
|
227
|
+
...cjs.matchAll(
|
|
228
|
+
/["']([^"']+)["']\s*:\s*function\s*\(/g,
|
|
229
|
+
),
|
|
230
|
+
].flatMap((m) =>
|
|
231
|
+
m[1] === undefined ? [] : [m[1]],
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
|
|
168
235
|
/**
|
|
169
236
|
* Read a source file, re-throwing with context.
|
|
170
237
|
*/
|
|
@@ -209,6 +276,29 @@ const requireSpecifiers = (
|
|
|
209
276
|
m[1] === undefined ? [] : [m[1]],
|
|
210
277
|
);
|
|
211
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Rewrite an inlined dist's inner `__externals` table
|
|
281
|
+
* key from the original specifier to the outer module
|
|
282
|
+
* id. A plgg-bundle ESM dist resolves an external
|
|
283
|
+
* required inside its inner module bodies through this
|
|
284
|
+
* table; {@link replaceRequire} rewrites those bodies'
|
|
285
|
+
* `require("<spec>")` calls to the outer id, so the key
|
|
286
|
+
* must follow or the inner lookup misses and falls into
|
|
287
|
+
* the transpiled dynamic-import fallback — a Promise
|
|
288
|
+
* where the consumer expects a namespace ("plgg_1.box
|
|
289
|
+
* is not a function"). Matches the exact
|
|
290
|
+
* `"<spec>": __extN` shape the TS printer emits for the
|
|
291
|
+
* table, so ordinary code never collides with it.
|
|
292
|
+
*/
|
|
293
|
+
const replaceExternalKey = (
|
|
294
|
+
cjs: string,
|
|
295
|
+
spec: string,
|
|
296
|
+
id: string,
|
|
297
|
+
): string =>
|
|
298
|
+
cjs
|
|
299
|
+
.split(`${JSON.stringify(spec)}: __ext`)
|
|
300
|
+
.join(`${JSON.stringify(id)}: __ext`);
|
|
301
|
+
|
|
212
302
|
/**
|
|
213
303
|
* Rewrite every `require("spec")` occurrence to
|
|
214
304
|
* `require("id")`. Operates on the exact specifier
|
|
@@ -4,6 +4,13 @@ import {
|
|
|
4
4
|
all,
|
|
5
5
|
toBe,
|
|
6
6
|
} from "plgg-test";
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
7
14
|
import { join } from "node:path";
|
|
8
15
|
import { discoverWorkspace } from "plgg-bundle/domain/usecase/discoverWorkspace";
|
|
9
16
|
|
|
@@ -61,3 +68,107 @@ test("discoverWorkspace sorts longest-name first (so plgg-view beats plgg)", ()
|
|
|
61
68
|
);
|
|
62
69
|
return check(iView < iCore, toBe(true));
|
|
63
70
|
});
|
|
71
|
+
|
|
72
|
+
test("discoverWorkspace finds dist-only packages installed under node_modules", () => {
|
|
73
|
+
const root = fixtureRoot();
|
|
74
|
+
try {
|
|
75
|
+
const app = join(root, "packages", "app");
|
|
76
|
+
writePackage(app, "app", true);
|
|
77
|
+
writePackage(
|
|
78
|
+
join(app, "node_modules", "plgg"),
|
|
79
|
+
"plgg",
|
|
80
|
+
false,
|
|
81
|
+
);
|
|
82
|
+
const found = discoverWorkspace(app).find(
|
|
83
|
+
(p) => p.name === "plgg",
|
|
84
|
+
);
|
|
85
|
+
return all([
|
|
86
|
+
check(found?.kind, toBe("dist")),
|
|
87
|
+
check(
|
|
88
|
+
found?.exports.get("."),
|
|
89
|
+
toBe("./dist/index.es.js"),
|
|
90
|
+
),
|
|
91
|
+
]);
|
|
92
|
+
} finally {
|
|
93
|
+
rmSync(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("discoverWorkspace keeps a sibling source package before an installed duplicate", () => {
|
|
98
|
+
const root = fixtureRoot();
|
|
99
|
+
try {
|
|
100
|
+
const app = join(root, "packages", "app");
|
|
101
|
+
writePackage(app, "app", true);
|
|
102
|
+
writePackage(
|
|
103
|
+
join(root, "packages", "plgg"),
|
|
104
|
+
"plgg",
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
writePackage(
|
|
108
|
+
join(app, "node_modules", "plgg"),
|
|
109
|
+
"plgg",
|
|
110
|
+
false,
|
|
111
|
+
);
|
|
112
|
+
const found = discoverWorkspace(app).find(
|
|
113
|
+
(p) => p.name === "plgg",
|
|
114
|
+
);
|
|
115
|
+
return check(found?.kind, toBe("source"));
|
|
116
|
+
} finally {
|
|
117
|
+
rmSync(root, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("discoverWorkspace finds installed deps of a sibling source package", () => {
|
|
122
|
+
const root = fixtureRoot();
|
|
123
|
+
try {
|
|
124
|
+
const app = join(root, "packages", "app");
|
|
125
|
+
writePackage(app, "app", true);
|
|
126
|
+
const local = join(root, "packages", "local");
|
|
127
|
+
writePackage(local, "local", true);
|
|
128
|
+
writePackage(
|
|
129
|
+
join(local, "node_modules", "plgg-cms"),
|
|
130
|
+
"plgg-cms",
|
|
131
|
+
false,
|
|
132
|
+
);
|
|
133
|
+
const found = discoverWorkspace(app).find(
|
|
134
|
+
(p) => p.name === "plgg-cms",
|
|
135
|
+
);
|
|
136
|
+
return check(found?.kind, toBe("dist"));
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(root, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const fixtureRoot = (): string =>
|
|
143
|
+
mkdtempSync(
|
|
144
|
+
join(tmpdir(), "plgg-bundle-discover-"),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const writePackage = (
|
|
148
|
+
dir: string,
|
|
149
|
+
name: string,
|
|
150
|
+
source: boolean,
|
|
151
|
+
): void => {
|
|
152
|
+
mkdirSync(source ? join(dir, "src") : join(dir, "dist"), {
|
|
153
|
+
recursive: true,
|
|
154
|
+
});
|
|
155
|
+
writeFileSync(
|
|
156
|
+
join(dir, "package.json"),
|
|
157
|
+
JSON.stringify(
|
|
158
|
+
{
|
|
159
|
+
name,
|
|
160
|
+
type: "module",
|
|
161
|
+
exports: {
|
|
162
|
+
import: {
|
|
163
|
+
default: "./dist/index.es.js",
|
|
164
|
+
},
|
|
165
|
+
require: {
|
|
166
|
+
default: "./dist/index.cjs.js",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
null,
|
|
171
|
+
2,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
};
|
|
@@ -7,38 +7,65 @@ import {
|
|
|
7
7
|
import { join, dirname } from "plgg-bundle/vendors/nodePath";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* A
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
10
|
+
* A package the app bundler can INLINE. Monorepo
|
|
11
|
+
* siblings are inlined from `src`; registry-installed
|
|
12
|
+
* packages usually ship only `dist`, so the resolver
|
|
13
|
+
* inlines their built ESM entry instead.
|
|
14
|
+
*/
|
|
15
|
+
export type WorkspacePackage =
|
|
16
|
+
| SourceWorkspacePackage
|
|
17
|
+
| DistWorkspacePackage;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A source package, usually a sibling under `packages/`.
|
|
21
|
+
* Its public export subpaths are reversed from
|
|
22
|
+
* `exports` dist paths back to `src` entries.
|
|
23
|
+
*/
|
|
24
|
+
export type SourceWorkspacePackage = Readonly<{
|
|
25
|
+
kind: "source";
|
|
26
|
+
name: string;
|
|
27
|
+
dir: string;
|
|
28
|
+
srcDir: string;
|
|
29
|
+
/** export subpath (`"."` | `"./x"`) → dist default. */
|
|
30
|
+
exports: ReadonlyMap<string, string>;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A dist package, usually installed under the app's
|
|
35
|
+
* `node_modules`. It has no source in the consumer tree,
|
|
36
|
+
* so public export subpaths resolve to built ESM files.
|
|
37
|
+
*/
|
|
38
|
+
export type DistWorkspacePackage = Readonly<{
|
|
39
|
+
kind: "dist";
|
|
22
40
|
name: string;
|
|
23
41
|
dir: string;
|
|
42
|
+
distDir: string;
|
|
24
43
|
/** export subpath (`"."` | `"./x"`) → dist default. */
|
|
25
44
|
exports: ReadonlyMap<string, string>;
|
|
26
45
|
}>;
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
|
-
* Discover
|
|
30
|
-
*
|
|
31
|
-
*
|
|
48
|
+
* Discover inlineable packages visible from `packageRoot`:
|
|
49
|
+
* sibling packages under the directory that holds
|
|
50
|
+
* `packageRoot`, plus packages installed in this package's
|
|
51
|
+
* own `node_modules` and in sibling source packages'
|
|
52
|
+
* `node_modules`. Siblings are listed first and win on
|
|
53
|
+
* duplicate names so monorepo builds keep reading source.
|
|
32
54
|
* Sorted longest-name-first so a later prefix match picks
|
|
33
|
-
* `plgg-view` over `plgg`.
|
|
55
|
+
* `plgg-view` over `plgg`.
|
|
34
56
|
*/
|
|
35
57
|
export const discoverWorkspace = (
|
|
36
58
|
packageRoot: string,
|
|
37
59
|
): ReadonlyArray<WorkspacePackage> => {
|
|
38
60
|
const siblingsDir = dirname(packageRoot);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
const siblings = packageDirsIn(siblingsDir);
|
|
62
|
+
return uniqueByName([
|
|
63
|
+
...siblings,
|
|
64
|
+
...packageDirsIn(join(packageRoot, "node_modules")),
|
|
65
|
+
...siblings.flatMap((dir) =>
|
|
66
|
+
packageDirsIn(join(dir, "node_modules")),
|
|
67
|
+
),
|
|
68
|
+
])
|
|
42
69
|
.map(readPackage)
|
|
43
70
|
.flatMap((p) => (p === undefined ? [] : [p]))
|
|
44
71
|
.sort(
|
|
@@ -47,8 +74,61 @@ export const discoverWorkspace = (
|
|
|
47
74
|
};
|
|
48
75
|
|
|
49
76
|
/**
|
|
50
|
-
*
|
|
51
|
-
* `
|
|
77
|
+
* Direct package dirs under `dir`, including scoped
|
|
78
|
+
* `@scope/name` packages. Missing directories simply
|
|
79
|
+
* contribute no packages.
|
|
80
|
+
*/
|
|
81
|
+
const packageDirsIn = (
|
|
82
|
+
dir: string,
|
|
83
|
+
): ReadonlyArray<string> =>
|
|
84
|
+
existsSync(dir)
|
|
85
|
+
? readdirSync(dir).flatMap((name) =>
|
|
86
|
+
childPackageDirs(join(dir, name)),
|
|
87
|
+
)
|
|
88
|
+
: [];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The package dirs represented by one child of a package
|
|
92
|
+
* collection directory. A scope directory fans out one
|
|
93
|
+
* level; ordinary package directories are returned as-is.
|
|
94
|
+
*/
|
|
95
|
+
const childPackageDirs = (
|
|
96
|
+
dir: string,
|
|
97
|
+
): ReadonlyArray<string> => {
|
|
98
|
+
if (!statSync(dir).isDirectory()) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
if (isPackageDir(dir)) {
|
|
102
|
+
return [dir];
|
|
103
|
+
}
|
|
104
|
+
return readdirSync(dir)
|
|
105
|
+
.map((name) => join(dir, name))
|
|
106
|
+
.filter(isPackageDir);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keep the first directory for each package name. The
|
|
111
|
+
* caller supplies siblings before node_modules, so a local
|
|
112
|
+
* source package beats an installed copy.
|
|
113
|
+
*/
|
|
114
|
+
const uniqueByName = (
|
|
115
|
+
dirs: ReadonlyArray<string>,
|
|
116
|
+
): ReadonlyArray<string> => {
|
|
117
|
+
const seen = new Set<string>();
|
|
118
|
+
const out: string[] = [];
|
|
119
|
+
for (const dir of dirs) {
|
|
120
|
+
const name = packageName(dir);
|
|
121
|
+
if (name === undefined || seen.has(name)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
seen.add(name);
|
|
125
|
+
out.push(dir);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether a path is a directory holding a package.json.
|
|
52
132
|
*/
|
|
53
133
|
const isPackageDir = (dir: string): boolean =>
|
|
54
134
|
statSync(dir).isDirectory() &&
|
|
@@ -66,9 +146,44 @@ const readPackage = (
|
|
|
66
146
|
join(dir, "package.json"),
|
|
67
147
|
);
|
|
68
148
|
const name = pkg["name"];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
149
|
+
if (typeof name !== "string") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const srcDir = join(dir, "src");
|
|
153
|
+
if (existsSync(srcDir)) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "source",
|
|
156
|
+
name,
|
|
157
|
+
dir,
|
|
158
|
+
srcDir,
|
|
159
|
+
exports: exportMap(pkg),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const distDir = join(dir, "dist");
|
|
163
|
+
if (existsSync(distDir)) {
|
|
164
|
+
return {
|
|
165
|
+
kind: "dist",
|
|
166
|
+
name,
|
|
167
|
+
dir,
|
|
168
|
+
distDir,
|
|
169
|
+
exports: exportMap(pkg),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read only the package name for de-duplication.
|
|
177
|
+
*/
|
|
178
|
+
const packageName = (
|
|
179
|
+
dir: string,
|
|
180
|
+
): string | undefined => {
|
|
181
|
+
if (!isPackageDir(dir)) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const pkg = parseJson(join(dir, "package.json"));
|
|
185
|
+
const name = pkg["name"];
|
|
186
|
+
return typeof name === "string" ? name : undefined;
|
|
72
187
|
};
|
|
73
188
|
|
|
74
189
|
/**
|
|
@@ -4,6 +4,13 @@ import {
|
|
|
4
4
|
all,
|
|
5
5
|
toBe,
|
|
6
6
|
} from "plgg-test";
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
7
14
|
import { join } from "node:path";
|
|
8
15
|
import { discoverWorkspace } from "plgg-bundle/domain/usecase/discoverWorkspace";
|
|
9
16
|
import { resolveWorkspaceSpecifier } from "plgg-bundle/domain/usecase/resolveWorkspaceSpecifier";
|
|
@@ -77,3 +84,93 @@ test("returns undefined for a non-workspace specifier", () =>
|
|
|
77
84
|
check(resolve("react"), toBe(undefined)),
|
|
78
85
|
check(resolve("node:fs"), toBe(undefined)),
|
|
79
86
|
]));
|
|
87
|
+
|
|
88
|
+
test("resolves an installed dist-only package export to its built ESM file", () => {
|
|
89
|
+
const root = fixtureRoot();
|
|
90
|
+
try {
|
|
91
|
+
const app = join(root, "packages", "app");
|
|
92
|
+
writePackage(app, "app", true);
|
|
93
|
+
writePackage(
|
|
94
|
+
join(app, "node_modules", "plgg"),
|
|
95
|
+
"plgg",
|
|
96
|
+
false,
|
|
97
|
+
);
|
|
98
|
+
const installed = discoverWorkspace(app);
|
|
99
|
+
return check(
|
|
100
|
+
resolveWorkspaceSpecifier({
|
|
101
|
+
specifier: "plgg",
|
|
102
|
+
fromFile: join(app, "src", "main.ts"),
|
|
103
|
+
packages: installed,
|
|
104
|
+
}),
|
|
105
|
+
toBe(
|
|
106
|
+
join(
|
|
107
|
+
app,
|
|
108
|
+
"node_modules",
|
|
109
|
+
"plgg",
|
|
110
|
+
"dist",
|
|
111
|
+
"index.es.js",
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(root, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("does not resolve internal paths from a dist-only installed package", () => {
|
|
121
|
+
const root = fixtureRoot();
|
|
122
|
+
try {
|
|
123
|
+
const app = join(root, "packages", "app");
|
|
124
|
+
writePackage(app, "app", true);
|
|
125
|
+
writePackage(
|
|
126
|
+
join(app, "node_modules", "plgg"),
|
|
127
|
+
"plgg",
|
|
128
|
+
false,
|
|
129
|
+
);
|
|
130
|
+
const installed = discoverWorkspace(app);
|
|
131
|
+
return check(
|
|
132
|
+
resolveWorkspaceSpecifier({
|
|
133
|
+
specifier: "plgg/Atomics/Num",
|
|
134
|
+
fromFile: join(app, "src", "main.ts"),
|
|
135
|
+
packages: installed,
|
|
136
|
+
}),
|
|
137
|
+
toBe(undefined),
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
rmSync(root, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const fixtureRoot = (): string =>
|
|
145
|
+
mkdtempSync(join(tmpdir(), "plgg-bundle-resolve-"));
|
|
146
|
+
|
|
147
|
+
const writePackage = (
|
|
148
|
+
dir: string,
|
|
149
|
+
name: string,
|
|
150
|
+
source: boolean,
|
|
151
|
+
): void => {
|
|
152
|
+
const codeDir = source
|
|
153
|
+
? join(dir, "src")
|
|
154
|
+
: join(dir, "dist");
|
|
155
|
+
mkdirSync(codeDir, { recursive: true });
|
|
156
|
+
writeFileSync(join(codeDir, "index.es.js"), "");
|
|
157
|
+
writeFileSync(
|
|
158
|
+
join(dir, "package.json"),
|
|
159
|
+
JSON.stringify(
|
|
160
|
+
{
|
|
161
|
+
name,
|
|
162
|
+
type: "module",
|
|
163
|
+
exports: {
|
|
164
|
+
import: {
|
|
165
|
+
default: "./dist/index.es.js",
|
|
166
|
+
},
|
|
167
|
+
require: {
|
|
168
|
+
default: "./dist/index.cjs.js",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
null,
|
|
173
|
+
2,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -8,11 +8,12 @@ import { type WorkspacePackage } from "plgg-bundle/domain/usecase/discoverWorksp
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* App-mode resolver: resolve an import specifier to an
|
|
11
|
-
* absolute
|
|
12
|
-
* (`plgg`, `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* absolute inlineable file. Source packages
|
|
12
|
+
* (`packages/plgg`, local `file:` siblings) resolve to
|
|
13
|
+
* `src`; published packages installed in `node_modules`
|
|
14
|
+
* resolve to their built ESM `dist` export. This is the
|
|
15
|
+
* mirror of the library externalization — the leaf app is
|
|
16
|
+
* where bundling deps is correct.
|
|
16
17
|
*
|
|
17
18
|
* Order:
|
|
18
19
|
* 1. relative (`./x`) → resolve against the importer,
|
|
@@ -75,8 +76,8 @@ const matchPackage = (
|
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Resolve a specifier already known to belong to `pkg`:
|
|
78
|
-
* a declared export subpath
|
|
79
|
-
* else
|
|
79
|
+
* a declared export subpath through the package kind,
|
|
80
|
+
* else a source package's internal self-alias path.
|
|
80
81
|
*/
|
|
81
82
|
const resolveInPackage = (
|
|
82
83
|
pkg: WorkspacePackage,
|
|
@@ -89,17 +90,42 @@ const resolveInPackage = (
|
|
|
89
90
|
pkg.name.length + 1,
|
|
90
91
|
)}`;
|
|
91
92
|
const dist = pkg.exports.get(subpath);
|
|
92
|
-
return
|
|
93
|
+
return pkg.kind === "source"
|
|
94
|
+
? resolveInSourcePackage(pkg, specifier, dist)
|
|
95
|
+
: resolveInDistPackage(pkg, dist);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Source packages keep the previous monorepo behavior:
|
|
100
|
+
* public exports reverse their emitted dist stem back to
|
|
101
|
+
* source; non-export self-alias paths resolve under `src`.
|
|
102
|
+
*/
|
|
103
|
+
const resolveInSourcePackage = (
|
|
104
|
+
pkg: WorkspacePackage & { kind: "source" },
|
|
105
|
+
specifier: string,
|
|
106
|
+
dist: string | undefined,
|
|
107
|
+
): string | undefined =>
|
|
108
|
+
dist === undefined
|
|
93
109
|
? resolveSpecifier({
|
|
94
110
|
specifier,
|
|
95
111
|
fromFile: pkg.dir,
|
|
96
112
|
aliasPrefix: pkg.name,
|
|
97
|
-
aliasSrcRoot:
|
|
113
|
+
aliasSrcRoot: pkg.srcDir,
|
|
98
114
|
})
|
|
99
|
-
: pickExisting(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
: pickExisting(join(pkg.srcDir, srcStem(dist)));
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Dist-only packages can safely resolve only declared
|
|
119
|
+
* public exports. Internal self-alias paths are not
|
|
120
|
+
* available in a consumer install.
|
|
121
|
+
*/
|
|
122
|
+
const resolveInDistPackage = (
|
|
123
|
+
pkg: WorkspacePackage & { kind: "dist" },
|
|
124
|
+
dist: string | undefined,
|
|
125
|
+
): string | undefined =>
|
|
126
|
+
dist === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: pickExisting(join(pkg.dir, dist));
|
|
103
129
|
|
|
104
130
|
/**
|
|
105
131
|
* The source stem implied by an export's dist default —
|