plgg-bundle 0.0.2 → 0.0.5

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/hook.d.mts ADDED
@@ -0,0 +1,20 @@
1
+ // Hand-written types for the JS loader hook (the hook must
2
+ // stay a plain `.mjs` — it runs as Node's ESM resolver
3
+ // before any type-stripping is active — so it carries no
4
+ // inferable types). Declared here so a `.ts` spec can drive
5
+ // `resolve` type-safely under `allowJs: false`.
6
+
7
+ /** A resolved module, as returned to Node's loader chain. */
8
+ export type Resolved = {
9
+ url: string;
10
+ shortCircuit?: boolean;
11
+ };
12
+
13
+ export const resolve: (
14
+ specifier: string,
15
+ context: { parentURL?: string | undefined },
16
+ nextResolve: (
17
+ specifier: string,
18
+ context: { parentURL?: string | undefined },
19
+ ) => Promise<Resolved>,
20
+ ) => Promise<Resolved>;
package/bin/hook.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  //
20
20
  // Everything else (typescript, node:*) falls through to
21
21
  // Node's default resolution.
22
- import { existsSync } from "node:fs";
22
+ import { existsSync, statSync } from "node:fs";
23
23
  import { fileURLToPath, pathToFileURL } from "node:url";
24
24
  import { dirname, join } from "node:path";
25
25
 
@@ -27,13 +27,22 @@ const here = dirname(fileURLToPath(import.meta.url));
27
27
  const srcRoot = join(here, "..", "src");
28
28
  const prefix = "plgg-bundle/";
29
29
 
30
+ // Resolve to a FILE, never a directory: a bare-directory
31
+ // self-alias (e.g. `plgg-bundle/foo`) must land on its
32
+ // `index.ts`, not the directory itself — Node would `read`
33
+ // the directory and throw EISDIR. So the raw `base` matches
34
+ // only when it is a real file; a directory falls through to
35
+ // `base/index.ts`.
36
+ const isFile = (c) =>
37
+ existsSync(c) && statSync(c).isFile();
38
+
30
39
  const pick = (base) => {
31
40
  const candidates = [
32
41
  base,
33
42
  `${base}.ts`,
34
43
  join(base, "index.ts"),
35
44
  ];
36
- return candidates.find((c) => existsSync(c));
45
+ return candidates.find(isFile);
37
46
  };
38
47
 
39
48
  // The `?v=<n>` dev version carried by an importer URL, or
@@ -7,6 +7,15 @@
7
7
  import { register } from "node:module";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { dirname, join } from "node:path";
10
+ import { relocateOutOfNodeModules } from "./relocate.mjs";
11
+
12
+ // Node 24 refuses to strip types from `.ts` under `node_modules`. When this
13
+ // tool is installed from the registry, relocate a copy OUTSIDE `node_modules`
14
+ // and re-exec there; a no-op on a monorepo `file:` link.
15
+ relocateOutOfNodeModules(
16
+ import.meta.url,
17
+ "plgg-bundle.mjs",
18
+ );
10
19
 
11
20
  register("./hook.mjs", import.meta.url);
12
21
 
@@ -0,0 +1,117 @@
1
+ // Shared launcher preamble for the run-from-source plgg CLIs.
2
+ //
3
+ // These tools run their `src/**/*.ts` entry directly and rely on Node stripping
4
+ // types on load. Node refuses to strip types for `.ts` files under
5
+ // `node_modules`, so a registry-installed tool cannot run in place. This helper
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
+ //
11
+ // Plain `.mjs`, Node built-ins only — it runs at process entry, before any
12
+ // resolver hook is registered.
13
+ import { spawnSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
+ import {
16
+ cpSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ readFileSync,
20
+ realpathSync,
21
+ rmSync,
22
+ symlinkSync,
23
+ writeFileSync,
24
+ } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import {
27
+ dirname,
28
+ join,
29
+ sep,
30
+ } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+
33
+ const isUnderNodeModules = (p) =>
34
+ p.split(sep).includes("node_modules");
35
+
36
+ // Relocate the calling launcher's package out of node_modules and re-exec, or
37
+ // return (no-op) when already outside it. Exits the process on re-exec.
38
+ export const relocateOutOfNodeModules = (
39
+ launcherUrl,
40
+ launcherBinName,
41
+ ) => {
42
+ const binDir = dirname(
43
+ fileURLToPath(launcherUrl),
44
+ );
45
+ const pkgRoot = realpathSync(
46
+ join(binDir, ".."),
47
+ );
48
+ if (!isUnderNodeModules(pkgRoot)) {
49
+ return;
50
+ }
51
+
52
+ const pkg = JSON.parse(
53
+ readFileSync(
54
+ join(pkgRoot, "package.json"),
55
+ "utf8",
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);
68
+ const dest = join(
69
+ tmpdir(),
70
+ `plgg-relocate-${pkg.name}-${pkg.version}-${tag}`,
71
+ );
72
+ const ready = join(
73
+ dest,
74
+ ".plgg-relocate-ready",
75
+ );
76
+ const link = join(dest, "node_modules");
77
+
78
+ if (!existsSync(ready)) {
79
+ rmSync(dest, {
80
+ recursive: true,
81
+ force: true,
82
+ });
83
+ mkdirSync(dest, { recursive: true });
84
+ for (const dir of ["src", "bin"]) {
85
+ const from = join(pkgRoot, dir);
86
+ if (existsSync(from)) {
87
+ cpSync(from, join(dest, dir), {
88
+ recursive: true,
89
+ });
90
+ }
91
+ }
92
+ cpSync(
93
+ join(pkgRoot, "package.json"),
94
+ join(dest, "package.json"),
95
+ );
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.
106
+ }
107
+
108
+ const child = spawnSync(
109
+ process.execPath,
110
+ [
111
+ join(dest, "bin", launcherBinName),
112
+ ...process.argv.slice(2),
113
+ ],
114
+ { stdio: "inherit" },
115
+ );
116
+ process.exit(child.status ?? 1);
117
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plgg-bundle",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
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.2"
26
+ "plgg-test": "^0.0.5"
27
27
  }
28
28
  }
@@ -0,0 +1,6 @@
1
+ // Fixture: a directory whose only module is `index.ts`, so a
2
+ // bare-directory self-alias (`plgg-bundle/Dev/fixtures/aliasDir`)
3
+ // has an index to resolve to. Exercised by
4
+ // ../../usecase/selfAliasResolve.spec.ts to guard the loader
5
+ // hook against resolving to the directory itself (EISDIR).
6
+ export const marker = "aliasDir/index.ts";
@@ -2,11 +2,11 @@ import {
2
2
  resolve,
3
3
  isAbsolute,
4
4
  join,
5
- } from "node:path";
6
- import { existsSync } from "node:fs";
7
- import { pathToFileURL } from "node:url";
8
- import { register } from "node:module";
9
- import { type Server } from "node:http";
5
+ } from "plgg-bundle/vendors/nodePath";
6
+ import { existsSync } from "plgg-bundle/vendors/nodeFs";
7
+ import { pathToFileURL } from "plgg-bundle/vendors/nodeUrl";
8
+ import { register } from "plgg-bundle/vendors/nodeProc";
9
+ import { type Server } from "plgg-bundle/vendors/nodeHttp";
10
10
  import { type Fetch } from "plgg-bundle/Dev/model/Fetch";
11
11
  import { type ModuleGraph } from "plgg-bundle/Dev/model/ModuleGraph";
12
12
  import {
@@ -3,7 +3,7 @@ import {
3
3
  type IncomingMessage,
4
4
  type ServerResponse,
5
5
  type Server,
6
- } from "node:http";
6
+ } from "plgg-bundle/vendors/nodeHttp";
7
7
  import { type Fetch } from "plgg-bundle/Dev/model/Fetch";
8
8
 
9
9
  // The node:http ⇄ Web-standard bridge for the dev server.
@@ -2,12 +2,12 @@ import {
2
2
  readdirSync,
3
3
  readFileSync,
4
4
  statSync,
5
- } from "node:fs";
5
+ } from "plgg-bundle/vendors/nodeFs";
6
6
  import {
7
7
  resolve,
8
8
  dirname,
9
9
  join,
10
- } from "node:path";
10
+ } from "plgg-bundle/vendors/nodePath";
11
11
  import { parseImports } from "plgg-bundle/Dev/usecase/parseImports";
12
12
  import { buildGraph } from "plgg-bundle/Dev/usecase/buildGraph";
13
13
  import { type ModuleGraph } from "plgg-bundle/Dev/model/ModuleGraph";
@@ -1,4 +1,4 @@
1
- import { watch, type FSWatcher } from "node:fs";
1
+ import { watch, type FSWatcher } from "plgg-bundle/vendors/nodeFs";
2
2
 
3
3
  // The fs.watch seam: a debounced, recursive watcher over
4
4
  // several source roots. One save can emit multiple events,
@@ -0,0 +1,68 @@
1
+ import {
2
+ test,
3
+ check,
4
+ all,
5
+ toBe,
6
+ } from "plgg-test";
7
+ import {
8
+ resolve,
9
+ type Resolved,
10
+ } from "../../../bin/hook.mjs";
11
+
12
+ // The loader hook's `resolve` rewrites `plgg-bundle/<sub>`
13
+ // self-alias specifiers to on-disk `src/<sub>` files. These
14
+ // drive it directly (the alias branch short-circuits before
15
+ // `nextResolve`, so a stub suffices).
16
+
17
+ const ctx = { parentURL: undefined };
18
+ const fallthrough = async (): Promise<Resolved> => ({
19
+ url: "file:///fallthrough",
20
+ });
21
+
22
+ test(
23
+ "bare-directory self-alias resolves to its index.ts, not the directory (EISDIR guard)",
24
+ async () => {
25
+ const r = await resolve(
26
+ "plgg-bundle/Dev/fixtures/aliasDir",
27
+ ctx,
28
+ fallthrough,
29
+ );
30
+ return all([
31
+ check(
32
+ r.url.endsWith("/aliasDir/index.ts"),
33
+ toBe(true),
34
+ ),
35
+ check(r.shortCircuit ?? false, toBe(true)),
36
+ ]);
37
+ },
38
+ );
39
+
40
+ test(
41
+ "file self-alias still resolves to the .ts file (no regression)",
42
+ async () => {
43
+ const r = await resolve(
44
+ "plgg-bundle/index",
45
+ ctx,
46
+ fallthrough,
47
+ );
48
+ return check(
49
+ r.url.endsWith("/src/index.ts"),
50
+ toBe(true),
51
+ );
52
+ },
53
+ );
54
+
55
+ test(
56
+ "a self-alias with no matching file falls through to nextResolve",
57
+ async () => {
58
+ const r = await resolve(
59
+ "plgg-bundle/does/not/exist",
60
+ ctx,
61
+ fallthrough,
62
+ );
63
+ return check(
64
+ r.url,
65
+ toBe("file:///fallthrough"),
66
+ );
67
+ },
68
+ );
@@ -4,8 +4,8 @@ import {
4
4
  rmSync,
5
5
  renameSync,
6
6
  existsSync,
7
- } from "node:fs";
8
- import { join, dirname } from "node:path";
7
+ } from "plgg-bundle/vendors/nodeFs";
8
+ import { join, dirname } from "plgg-bundle/vendors/nodePath";
9
9
  import {
10
10
  type BundleConfig,
11
11
  type Entry,
@@ -0,0 +1,283 @@
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
+
17
+ test("collectModules records external specifiers without walking them", () => {
18
+ const root = mkdtempSync(
19
+ join(tmpdir(), "plgg-bundle-collect-ext-"),
20
+ );
21
+ try {
22
+ const entry = join(root, "src", "main.ts");
23
+ mkdirSync(join(root, "src"), { recursive: true });
24
+ writeFileSync(
25
+ entry,
26
+ 'import { readFileSync } from "node:fs";\nexport const read = readFileSync;\n',
27
+ );
28
+ const graph = collectModules({
29
+ entryFile: entry,
30
+ root,
31
+ aliasPrefix: "app",
32
+ aliasSrcRoot: join(root, "src"),
33
+ external: /^node:/,
34
+ });
35
+ return all([
36
+ check(graph.modules.length, toBe(1)),
37
+ check(
38
+ graph.modules[0]?.externals[0],
39
+ toBe("node:fs"),
40
+ ),
41
+ ]);
42
+ } finally {
43
+ rmSync(root, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ test("collectModules throws on an unresolvable non-external specifier", () => {
48
+ const root = mkdtempSync(
49
+ join(tmpdir(), "plgg-bundle-collect-missing-"),
50
+ );
51
+ try {
52
+ const entry = join(root, "src", "main.ts");
53
+ mkdirSync(join(root, "src"), { recursive: true });
54
+ writeFileSync(
55
+ entry,
56
+ 'import "missing";\n',
57
+ );
58
+ return check(
59
+ rejects(() =>
60
+ collectModules({
61
+ entryFile: entry,
62
+ root,
63
+ aliasPrefix: "app",
64
+ aliasSrcRoot: join(root, "src"),
65
+ external: /^node:/,
66
+ }),
67
+ ),
68
+ toBe(true),
69
+ );
70
+ } finally {
71
+ rmSync(root, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ test("collectModules inlines an installed prebundled dist entry without resolving its internal registry requires", () => {
76
+ const root = mkdtempSync(
77
+ join(tmpdir(), "plgg-bundle-collect-"),
78
+ );
79
+ try {
80
+ const entry = join(root, "src", "main.ts");
81
+ const dep = join(
82
+ root,
83
+ "node_modules",
84
+ "dep",
85
+ "dist",
86
+ "index.es.js",
87
+ );
88
+ const plgg = join(
89
+ root,
90
+ "node_modules",
91
+ "plgg",
92
+ "dist",
93
+ "index.es.js",
94
+ );
95
+ mkdirSync(join(root, "src"), { recursive: true });
96
+ mkdirSync(join(root, "node_modules", "dep", "dist"), {
97
+ recursive: true,
98
+ });
99
+ mkdirSync(join(root, "node_modules", "plgg", "dist"), {
100
+ recursive: true,
101
+ });
102
+ writeFileSync(
103
+ entry,
104
+ 'import dep from "dep";\nexport const value = dep.value;\n',
105
+ );
106
+ writeFileSync(
107
+ dep,
108
+ [
109
+ 'import * as __ext0 from "plgg";',
110
+ 'const __externals = { "plgg": __ext0 };',
111
+ "const __modules = {",
112
+ '"src/index.ts": function (module, exports, require) {',
113
+ ' exports.value = require("src/internal.ts").value;',
114
+ "},",
115
+ '"src/internal.ts": function (module, exports, require) {',
116
+ " exports.value = __externals.plgg.external;",
117
+ "}",
118
+ "};",
119
+ 'const __entry = __modules["src/index.ts"];',
120
+ "export default { value: __entry };",
121
+ ].join("\n"),
122
+ );
123
+ writeFileSync(
124
+ plgg,
125
+ "export const external = 1;\n",
126
+ );
127
+
128
+ const graph = collectModules({
129
+ entryFile: entry,
130
+ root,
131
+ aliasPrefix: "app",
132
+ aliasSrcRoot: join(root, "src"),
133
+ external: /^node:/,
134
+ resolve: (specifier, _fromFile) =>
135
+ specifier === "dep"
136
+ ? dep
137
+ : specifier === "plgg"
138
+ ? plgg
139
+ : undefined,
140
+ });
141
+ const depModule = graph.modules.find(
142
+ (m) =>
143
+ m.id ===
144
+ "node_modules/dep/dist/index.es.js",
145
+ );
146
+ return all([
147
+ check(graph.modules.length, toBe(3)),
148
+ check(
149
+ depModule?.code.includes(
150
+ 'require("src/internal.ts")',
151
+ ),
152
+ toBe(true),
153
+ ),
154
+ check(
155
+ depModule?.code.includes(
156
+ 'require("node_modules/plgg/dist/index.es.js")',
157
+ ),
158
+ toBe(true),
159
+ ),
160
+ ]);
161
+ } finally {
162
+ rmSync(root, { recursive: true, force: true });
163
+ }
164
+ });
165
+
166
+ test("collectModules resolves node_modules source files without dist-registry filtering", () => {
167
+ const root = mkdtempSync(
168
+ join(tmpdir(), "plgg-bundle-collect-src-"),
169
+ );
170
+ try {
171
+ const entry = join(root, "src", "main.ts");
172
+ const dep = join(
173
+ root,
174
+ "node_modules",
175
+ "dep",
176
+ "src",
177
+ "index.ts",
178
+ );
179
+ const internal = join(
180
+ root,
181
+ "node_modules",
182
+ "dep",
183
+ "src",
184
+ "internal.ts",
185
+ );
186
+ writeImportFixture(entry, dep, internal);
187
+ const graph = collectModules({
188
+ entryFile: entry,
189
+ root,
190
+ aliasPrefix: "app",
191
+ aliasSrcRoot: join(root, "src"),
192
+ external: /^node:/,
193
+ resolve: importFixtureResolver(dep, internal),
194
+ });
195
+ return check(graph.modules.length, toBe(3));
196
+ } finally {
197
+ rmSync(root, { recursive: true, force: true });
198
+ }
199
+ });
200
+
201
+ test("collectModules resolves non-js dist source files without dist-registry filtering", () => {
202
+ const root = mkdtempSync(
203
+ join(tmpdir(), "plgg-bundle-collect-mjs-"),
204
+ );
205
+ try {
206
+ const entry = join(root, "src", "main.ts");
207
+ const dep = join(
208
+ root,
209
+ "node_modules",
210
+ "dep",
211
+ "dist",
212
+ "index.ts",
213
+ );
214
+ const internal = join(
215
+ root,
216
+ "node_modules",
217
+ "dep",
218
+ "dist",
219
+ "internal.ts",
220
+ );
221
+ writeImportFixture(entry, dep, internal);
222
+ const graph = collectModules({
223
+ entryFile: entry,
224
+ root,
225
+ aliasPrefix: "app",
226
+ aliasSrcRoot: join(root, "src"),
227
+ external: /^node:/,
228
+ resolve: importFixtureResolver(dep, internal),
229
+ });
230
+ return check(graph.modules.length, toBe(3));
231
+ } finally {
232
+ rmSync(root, { recursive: true, force: true });
233
+ }
234
+ });
235
+
236
+ const writeImportFixture = (
237
+ entry: string,
238
+ dep: string,
239
+ internal: string,
240
+ ): void => {
241
+ mkdirSync(join(entry, ".."), { recursive: true });
242
+ mkdirSync(join(dep, ".."), { recursive: true });
243
+ mkdirSync(join(internal, ".."), { recursive: true });
244
+ writeFileSync(
245
+ entry,
246
+ 'import { value } from "dep";\nexport const out = value;\n',
247
+ );
248
+ writeFileSync(
249
+ dep,
250
+ 'import { value } from "src/internal.ts";\nexport { value };\n',
251
+ );
252
+ writeFileSync(
253
+ internal,
254
+ "export const value = 1;\n",
255
+ );
256
+ };
257
+
258
+ const importFixtureResolver =
259
+ (
260
+ dep: string,
261
+ internal: string,
262
+ ): ((
263
+ specifier: string,
264
+ fromFile: string,
265
+ ) => string | undefined) =>
266
+ (specifier, _fromFile) =>
267
+ specifier === "dep"
268
+ ? dep
269
+ : specifier === "src/internal.ts"
270
+ ? internal
271
+ : undefined;
272
+
273
+ const rejects = (f: () => unknown): boolean => {
274
+ try {
275
+ f();
276
+ return false;
277
+ } catch (e) {
278
+ return (
279
+ e instanceof Error &&
280
+ e.message.startsWith("ResolveError")
281
+ );
282
+ }
283
+ };