plgg-bundle 0.0.2 → 0.0.3

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,108 @@
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 version-stamped dir OUTSIDE `node_modules` (with the
7
+ // tool's own deps reachable via a `node_modules` symlink) and re-execs the copy
8
+ // there. On a monorepo `file:` link the package realpath is already outside
9
+ // `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 {
15
+ cpSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readFileSync,
19
+ realpathSync,
20
+ rmSync,
21
+ symlinkSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import {
26
+ dirname,
27
+ join,
28
+ sep,
29
+ } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const isUnderNodeModules = (p) =>
33
+ p.split(sep).includes("node_modules");
34
+
35
+ // Relocate the calling launcher's package out of node_modules and re-exec, or
36
+ // return (no-op) when already outside it. Exits the process on re-exec.
37
+ export const relocateOutOfNodeModules = (
38
+ launcherUrl,
39
+ launcherBinName,
40
+ ) => {
41
+ const binDir = dirname(
42
+ fileURLToPath(launcherUrl),
43
+ );
44
+ const pkgRoot = realpathSync(
45
+ join(binDir, ".."),
46
+ );
47
+ if (!isUnderNodeModules(pkgRoot)) {
48
+ return;
49
+ }
50
+
51
+ const pkg = JSON.parse(
52
+ readFileSync(
53
+ join(pkgRoot, "package.json"),
54
+ "utf8",
55
+ ),
56
+ );
57
+ const dest = join(
58
+ tmpdir(),
59
+ `plgg-relocate-${pkg.name}-${pkg.version}`,
60
+ );
61
+ const ready = join(
62
+ dest,
63
+ ".plgg-relocate-ready",
64
+ );
65
+
66
+ if (!existsSync(ready)) {
67
+ rmSync(dest, {
68
+ recursive: true,
69
+ force: true,
70
+ });
71
+ mkdirSync(dest, { recursive: true });
72
+ for (const dir of ["src", "bin"]) {
73
+ const from = join(pkgRoot, dir);
74
+ if (existsSync(from)) {
75
+ cpSync(from, join(dest, dir), {
76
+ recursive: true,
77
+ });
78
+ }
79
+ }
80
+ cpSync(
81
+ join(pkgRoot, "package.json"),
82
+ join(dest, "package.json"),
83
+ );
84
+ // The tool's own deps (typescript; plgg for plgg-test) live in the
85
+ // node_modules that CONTAINS this package (npm hoists there). Symlink it so
86
+ // resolution from the relocated copy finds them.
87
+ try {
88
+ symlinkSync(
89
+ dirname(pkgRoot),
90
+ join(dest, "node_modules"),
91
+ "dir",
92
+ );
93
+ } catch {
94
+ // A pre-existing symlink from a concurrent run is fine.
95
+ }
96
+ writeFileSync(ready, "");
97
+ }
98
+
99
+ const child = spawnSync(
100
+ process.execPath,
101
+ [
102
+ join(dest, "bin", launcherBinName),
103
+ ...process.argv.slice(2),
104
+ ],
105
+ { stdio: "inherit" },
106
+ );
107
+ process.exit(child.status ?? 1);
108
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plgg-bundle",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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.4"
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,
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
- import { relative } from "node:path";
1
+ import { readFileSync } from "plgg-bundle/vendors/nodeFs";
2
+ import { relative } from "plgg-bundle/vendors/nodePath";
3
3
  import { type External } from "plgg-bundle/domain/model/BundleConfig";
4
4
  import { resolveSpecifier } from "plgg-bundle/domain/usecase/resolveSpecifier";
5
5
  import { isExternal } from "plgg-bundle/domain/usecase/isExternal";
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { readFileSync } from "plgg-bundle/vendors/nodeFs";
2
+ import { join } from "plgg-bundle/vendors/nodePath";
3
3
  import { type External } from "plgg-bundle/domain/model/BundleConfig";
4
4
 
5
5
  /**
@@ -3,8 +3,8 @@ import {
3
3
  readFileSync,
4
4
  existsSync,
5
5
  statSync,
6
- } from "node:fs";
7
- import { join, dirname } from "node:path";
6
+ } from "plgg-bundle/vendors/nodeFs";
7
+ import { join, dirname } from "plgg-bundle/vendors/nodePath";
8
8
 
9
9
  /**
10
10
  * A workspace sibling the app bundler can INLINE from
@@ -1,16 +1,16 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawnSync } from "plgg-bundle/vendors/nodeProc";
2
2
  import {
3
3
  join,
4
4
  dirname,
5
5
  basename,
6
6
  relative,
7
- } from "node:path";
7
+ } from "plgg-bundle/vendors/nodePath";
8
8
  import {
9
9
  existsSync,
10
10
  writeFileSync,
11
11
  rmSync,
12
- } from "node:fs";
13
- import { createRequire } from "node:module";
12
+ } from "plgg-bundle/vendors/nodeFs";
13
+ import { createRequire } from "plgg-bundle/vendors/nodeProc";
14
14
  import { rewriteDtsAliases } from "plgg-bundle/domain/usecase/rewriteDtsAliases";
15
15
 
16
16
  /**
@@ -1,9 +1,9 @@
1
- import { existsSync, statSync } from "node:fs";
1
+ import { existsSync, statSync } from "plgg-bundle/vendors/nodeFs";
2
2
  import {
3
3
  dirname,
4
4
  resolve,
5
5
  join,
6
- } from "node:path";
6
+ } from "plgg-bundle/vendors/nodePath";
7
7
 
8
8
  /**
9
9
  * Resolve a bare import specifier to an absolute source
@@ -1,4 +1,4 @@
1
- import { basename, join } from "node:path";
1
+ import { basename, join } from "plgg-bundle/vendors/nodePath";
2
2
  import {
3
3
  resolveSpecifier,
4
4
  isRelative,
@@ -3,12 +3,12 @@ import {
3
3
  statSync,
4
4
  readFileSync,
5
5
  writeFileSync,
6
- } from "node:fs";
6
+ } from "plgg-bundle/vendors/nodeFs";
7
7
  import {
8
8
  join,
9
9
  dirname,
10
10
  relative,
11
- } from "node:path";
11
+ } from "plgg-bundle/vendors/nodePath";
12
12
 
13
13
  /**
14
14
  * Rewrite the package self-alias (`<prefix>/<sub>`) in
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The filesystem anti-corruption layer for plgg-bundle — the
3
+ * ONLY place the bundler's build usecases reach `node:fs`, kept
4
+ * under `vendors/` so the domain speaks these named operations,
5
+ * not the driver. Re-exported (not re-wrapped) to preserve the
6
+ * exact node types; plgg-bundle's throw-at-the-boundary error
7
+ * model (see BundleConfig) means a failed fs call surfaces as a
8
+ * thrown Error the entrypoint catches — the seam is the import
9
+ * chokepoint, so the vendor-boundary gate holds unexempted.
10
+ */
11
+ export {
12
+ writeFileSync,
13
+ mkdirSync,
14
+ rmSync,
15
+ renameSync,
16
+ existsSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ statSync,
20
+ watch,
21
+ type FSWatcher,
22
+ } from "node:fs";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The HTTP anti-corruption layer for plgg-bundle's DEV server —
3
+ * the ONLY place the node:http ⇄ Web-standard bridge reaches
4
+ * `node:http`. Confined here so the dev orchestration stays
5
+ * `node:`-free (the vendor-boundary gate); a plgg-free
6
+ * reimplementation of `serve` (the toolchain must not import a
7
+ * library it builds).
8
+ */
9
+ export {
10
+ createServer,
11
+ type IncomingMessage,
12
+ type ServerResponse,
13
+ type Server,
14
+ } from "node:http";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The path anti-corruption layer for plgg-bundle — the ONLY
3
+ * place the build usecases reach `node:path`. Re-exported to
4
+ * preserve exact node types; confining the import here is what
5
+ * keeps the domain free of `node:` specifiers (the
6
+ * vendor-boundary gate).
7
+ */
8
+ export {
9
+ join,
10
+ dirname,
11
+ relative,
12
+ basename,
13
+ resolve,
14
+ isAbsolute,
15
+ } from "node:path";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The process/module anti-corruption layer for plgg-bundle —
3
+ * the ONLY place the `.d.ts` emitter reaches `node:child_process`
4
+ * (to spawn the target's own `tsc`) and `node:module` (to
5
+ * resolve it against the target's node_modules). Confined here so
6
+ * the domain stays `node:`-free.
7
+ */
8
+ export { spawnSync } from "node:child_process";
9
+ export {
10
+ createRequire,
11
+ register,
12
+ } from "node:module";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The URL anti-corruption layer for plgg-bundle's dev server —
3
+ * the ONLY place `node:url` is reached (turning a filesystem
4
+ * path into a `file://` URL for the module runner).
5
+ */
6
+ export { pathToFileURL } from "node:url";