@warlock.js/core 4.2.2 → 4.2.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +1 -0
  3. package/esm/dev-server/health-checker/workers/ts-health.worker.d.mts +1 -0
  4. package/esm/dev-server/loader/hook-thread.d.mts +48 -0
  5. package/esm/dev-server/loader/hook-thread.d.mts.map +1 -0
  6. package/esm/dev-server/loader/hook-thread.mjs +47 -0
  7. package/esm/dev-server/loader/hook-thread.mjs.map +1 -0
  8. package/esm/dev-server/loader/load-hook.d.mts +58 -0
  9. package/esm/dev-server/loader/load-hook.d.mts.map +1 -0
  10. package/esm/dev-server/loader/load-hook.mjs +86 -0
  11. package/esm/dev-server/loader/load-hook.mjs.map +1 -0
  12. package/esm/dev-server/loader/own-resolver.mjs +69 -0
  13. package/esm/dev-server/loader/own-resolver.mjs.map +1 -0
  14. package/esm/dev-server/loader/resolve-capture.mjs +43 -0
  15. package/esm/dev-server/loader/resolve-capture.mjs.map +1 -0
  16. package/esm/dev-server/loader/resolve-hook.d.mts +41 -0
  17. package/esm/dev-server/loader/resolve-hook.d.mts.map +1 -0
  18. package/esm/dev-server/loader/resolve-hook.mjs +87 -0
  19. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -0
  20. package/esm/dev-server/loader/source-slug.mjs +22 -0
  21. package/esm/dev-server/loader/source-slug.mjs.map +1 -0
  22. package/esm/dev-server/loader/transpile-cache.mjs +163 -1
  23. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  24. package/esm/dev-server/loader/version-registry.mjs +45 -0
  25. package/esm/dev-server/loader/version-registry.mjs.map +1 -0
  26. package/esm/generations/add-command.action.mjs +10 -3
  27. package/esm/generations/add-command.action.mjs.map +1 -1
  28. package/esm/generations/stubs.mjs +115 -1
  29. package/esm/generations/stubs.mjs.map +1 -1
  30. package/package.json +27 -9
package/CHANGELOG.md CHANGED
@@ -13,6 +13,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
13
13
  - `warlock add notifications` — new feature in the `add` command: installs `@warlock.js/notifications` (and pulls the `mail` feature for the default mail channel), ejects `config/notifications.ts` (mail + in-app channels wired), and scaffolds the app-owned `Notification` model + a timestamped migration into `src/app/notifications/`. Re-running is idempotent (the model file is the sentinel — no duplicate migration). The async queue stays opt-in (commented in the config; enable with `warlock add herald` + `heraldQueue()`).
14
14
  - **Notifications connector** — a built-in connector (priority `8`, early phase) reads `config/notifications.ts` at boot and hands its default export to `setNotificationConfig`. `@warlock.js/notifications` is lazy-imported (gated on the config's presence), so core keeps no hard dependency on it — the same pattern as the herald connector. Config files stay declarative (`export default config`); the side-effect moves out of the config file.
15
15
 
16
+ ## 4.2.5
17
+
18
+ ### Added
19
+
20
+ - `warlock add notifications` now scaffolds the in-app read/dismiss HTTP surface — `routes.ts` + a `notifications.controller.ts` (list / unread-count / mark-read / mark-all-read / clear / delete), gated by `authMiddleware` and recipient-scoped via `inApp`. Pulls `@warlock.js/auth`.
21
+
22
+ ## 4.2.4
23
+
24
+ ### Fixed
25
+
26
+ - Fix the worker-loader path in the build entry points — a wrong path in 4.2.3 left the worker entry broken (and blocked the 4.2.3 publish for some packages).
27
+
28
+ ## 4.2.3
29
+
30
+ ### Fixed
31
+
32
+ - Add the worker scripts as build entry points so they ship in the published package.
33
+
34
+ ## 4.2.2
35
+
36
+ ### Fixed
37
+
38
+ - Add `cli/start` to the build entry points so the `warlock` CLI entry ships in the published package.
39
+
16
40
  ## 4.2.1
17
41
 
18
42
  ### Fixed
@@ -0,0 +1,48 @@
1
+ import { TranspileInit, load } from "./load-hook.mjs";
2
+ import { resolve } from "./resolve-hook.mjs";
3
+ import { MessagePort } from "node:worker_threads";
4
+
5
+ //#region ../@warlock.js/core/src/dev-server/loader/hook-thread.d.ts
6
+ type InitializeData = {
7
+ port: MessagePort;
8
+ srcRoot: string; /** Transpile config (always present — esbuild transpile is unconditional). */
9
+ transpile: TranspileInit;
10
+ };
11
+ /**
12
+ * ESM loader hook entry-point.
13
+ *
14
+ * Node's `module.register()` runs this file in a dedicated worker thread.
15
+ * The three exported functions (`initialize`, `resolve`, `load`) are the
16
+ * official hook lifecycle callbacks:
17
+ *
18
+ * - `initialize` — runs once when the hook worker starts; receives the data
19
+ * passed as `{ data }` to `module.register()`. We use it to configure the
20
+ * source-root filter and to wire the `MessageChannel` port that the main
21
+ * thread uses to push version-bump notifications.
22
+ *
23
+ * - `resolve` — intercepts every `import` specifier resolution. The
24
+ * framework's own resolver handles tsconfig `paths` + TS extension/index
25
+ * probing; npm/`node:` fall through to Node default. Stamps a `?v=<N>`
26
+ * version token onto tracked `src/` source-file URLs.
27
+ *
28
+ * - `load` — transpiles every `.ts`/`.tsx` with esbuild (content-hash
29
+ * cached); non-TypeScript falls through to the Node default loader.
30
+ *
31
+ * @example
32
+ * // Main thread (register-loader.ts):
33
+ * const { port1, port2 } = new MessageChannel();
34
+ * module.register(hookBundleUrl, import.meta.url, {
35
+ * data: { port: port2, srcRoot: "/abs/path/to/src" },
36
+ * transferList: [port2],
37
+ * });
38
+ * // File watcher later:
39
+ * port1.postMessage({ type: "bump", absolutePath: "/abs/path/to/user.model.ts" });
40
+ */
41
+ declare function initialize({
42
+ port,
43
+ srcRoot,
44
+ transpile
45
+ }: InitializeData): void;
46
+ //#endregion
47
+ export { initialize, load, resolve };
48
+ //# sourceMappingURL=hook-thread.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-thread.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/hook-thread.ts"],"mappings":";;;;;KAKK,cAAA;EACH,IAAA,EAAM,WAAA;EACN,OAAA,UAFiB;EAIjB,SAAA,EAAW,aAAa;AAAA;;;;;;;AAAA;AAiC1B;;;;;;;;;;;;;;;;;AAAuE;;;;;;iBAAvD,UAAA;EAAa,IAAA;EAAM,OAAA;EAAS;AAAA,GAAa,cAAA"}
@@ -0,0 +1,47 @@
1
+ import { configureTranspile, load } from "./load-hook.mjs";
2
+ import { bumpVersion } from "./version-registry.mjs";
3
+ import { resolve, setSrcRoot } from "./resolve-hook.mjs";
4
+
5
+ //#region ../@warlock.js/core/src/dev-server/loader/hook-thread.ts
6
+ /**
7
+ * ESM loader hook entry-point.
8
+ *
9
+ * Node's `module.register()` runs this file in a dedicated worker thread.
10
+ * The three exported functions (`initialize`, `resolve`, `load`) are the
11
+ * official hook lifecycle callbacks:
12
+ *
13
+ * - `initialize` — runs once when the hook worker starts; receives the data
14
+ * passed as `{ data }` to `module.register()`. We use it to configure the
15
+ * source-root filter and to wire the `MessageChannel` port that the main
16
+ * thread uses to push version-bump notifications.
17
+ *
18
+ * - `resolve` — intercepts every `import` specifier resolution. The
19
+ * framework's own resolver handles tsconfig `paths` + TS extension/index
20
+ * probing; npm/`node:` fall through to Node default. Stamps a `?v=<N>`
21
+ * version token onto tracked `src/` source-file URLs.
22
+ *
23
+ * - `load` — transpiles every `.ts`/`.tsx` with esbuild (content-hash
24
+ * cached); non-TypeScript falls through to the Node default loader.
25
+ *
26
+ * @example
27
+ * // Main thread (register-loader.ts):
28
+ * const { port1, port2 } = new MessageChannel();
29
+ * module.register(hookBundleUrl, import.meta.url, {
30
+ * data: { port: port2, srcRoot: "/abs/path/to/src" },
31
+ * transferList: [port2],
32
+ * });
33
+ * // File watcher later:
34
+ * port1.postMessage({ type: "bump", absolutePath: "/abs/path/to/user.model.ts" });
35
+ */
36
+ function initialize({ port, srcRoot, transpile }) {
37
+ setSrcRoot(srcRoot);
38
+ configureTranspile(transpile);
39
+ port.on("message", ({ type, absolutePath }) => {
40
+ if (type === "bump") bumpVersion(absolutePath);
41
+ else if (type === "sync") port.postMessage({ type: "sync-ack" });
42
+ });
43
+ }
44
+
45
+ //#endregion
46
+ export { initialize, load, resolve };
47
+ //# sourceMappingURL=hook-thread.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-thread.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/hook-thread.ts"],"sourcesContent":["import type { MessagePort } from \"node:worker_threads\";\nimport { configureTranspile, load, type TranspileInit } from \"./load-hook.js\";\nimport { resolve, setSrcRoot } from \"./resolve-hook.js\";\nimport { bumpVersion } from \"./version-registry.js\";\n\ntype InitializeData = {\n port: MessagePort;\n srcRoot: string;\n /** Transpile config (always present — esbuild transpile is unconditional). */\n transpile: TranspileInit;\n};\n\n/**\n * ESM loader hook entry-point.\n *\n * Node's `module.register()` runs this file in a dedicated worker thread.\n * The three exported functions (`initialize`, `resolve`, `load`) are the\n * official hook lifecycle callbacks:\n *\n * - `initialize` — runs once when the hook worker starts; receives the data\n * passed as `{ data }` to `module.register()`. We use it to configure the\n * source-root filter and to wire the `MessageChannel` port that the main\n * thread uses to push version-bump notifications.\n *\n * - `resolve` — intercepts every `import` specifier resolution. The\n * framework's own resolver handles tsconfig `paths` + TS extension/index\n * probing; npm/`node:` fall through to Node default. Stamps a `?v=<N>`\n * version token onto tracked `src/` source-file URLs.\n *\n * - `load` — transpiles every `.ts`/`.tsx` with esbuild (content-hash\n * cached); non-TypeScript falls through to the Node default loader.\n *\n * @example\n * // Main thread (register-loader.ts):\n * const { port1, port2 } = new MessageChannel();\n * module.register(hookBundleUrl, import.meta.url, {\n * data: { port: port2, srcRoot: \"/abs/path/to/src\" },\n * transferList: [port2],\n * });\n * // File watcher later:\n * port1.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport function initialize({ port, srcRoot, transpile }: InitializeData): void {\n setSrcRoot(srcRoot);\n configureTranspile(transpile);\n\n port.on(\"message\", ({ type, absolutePath }: { type: string; absolutePath: string }) => {\n if (type === \"bump\") {\n bumpVersion(absolutePath);\n } else if (type === \"sync\") {\n port.postMessage({ type: \"sync-ack\" });\n }\n });\n}\n\nexport { load, resolve };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,WAAW,EAAE,MAAM,SAAS,aAAmC;CAC7E,WAAW,OAAO;CAClB,mBAAmB,SAAS;CAE5B,KAAK,GAAG,YAAY,EAAE,MAAM,mBAA2D;EACrF,IAAI,SAAS,QACX,YAAY,YAAY;OACnB,IAAI,SAAS,QAClB,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;CAEzC,CAAC;AACH"}
@@ -0,0 +1,58 @@
1
+ //#region ../@warlock.js/core/src/dev-server/loader/load-hook.d.ts
2
+ type LoadContext = {
3
+ format?: string;
4
+ importAttributes?: Record<string, string>;
5
+ };
6
+ type LoadResult = {
7
+ format: string;
8
+ source: string | Buffer;
9
+ shortCircuit?: boolean;
10
+ };
11
+ type NextLoad = (url: string, context?: LoadContext) => Promise<LoadResult>;
12
+ /**
13
+ * Everything the transpile path needs, computed once on the main thread and
14
+ * shipped into the hook worker via `initialize()`. Always present — esbuild
15
+ * transpile is unconditional now (tsx is no longer in the TS path).
16
+ */
17
+ type TranspileInit = {
18
+ /** Absolute `.warlock/transpile` directory. */cacheDir: string; /** Source-content key salt: esbuild version + epoch + tsconfig blob. */
19
+ fingerprint: string; /** esbuild `target` (e.g. `"node22"` — must match the prod builder). */
20
+ target: string; /** JSON string of `{ compilerOptions }` handed to esbuild as tsconfigRaw. */
21
+ tsconfigRaw: string;
22
+ /**
23
+ * Debug-only: name cache files `<slug>.<hash>.js` and append a trailing
24
+ * `// @source <path>` marker so entries are eyeball-identifiable. Off by
25
+ * default (opaque `<hash>.js`). Cosmetic — never affects keys or lookup.
26
+ */
27
+ debugNames: boolean;
28
+ };
29
+ /**
30
+ * ESM loader `load()` hook.
31
+ *
32
+ * Owns the transpile for **every** TypeScript file — project `src/`,
33
+ * `@warlock.js/*` framework source, and `warlock.config.ts` alike. There
34
+ * is no tsx in the TypeScript path any more: we read the source, key it by
35
+ * a content hash + options fingerprint, serve the cached JS on a hit, and
36
+ * otherwise transform it with esbuild directly and persist. Non-TypeScript
37
+ * URLs (npm `.js`/`.cjs`/`.mjs`, `node:` builtins, JSON) are not ours —
38
+ * they fall through to the next loader (Node's default).
39
+ *
40
+ * The `?v=<N>` query drives Node's module identity (HMR) for `src/` files
41
+ * and is **never** part of the cache key — the cache is keyed purely by
42
+ * source content, so the two mechanisms are fully orthogonal. Framework
43
+ * and config `.ts` carry no `?v` (no HMR) but are transpiled and cached
44
+ * exactly the same way.
45
+ *
46
+ * The source map esbuild emits is inline base64 with `sourcefile` set to
47
+ * the clean absolute `.ts` path, so V8 (run with source maps enabled)
48
+ * reports stack frames at the original TypeScript location.
49
+ *
50
+ * @example
51
+ * // load("file:///src/user.model.ts?v=3") → esbuild transpile + cache
52
+ * // load("file:///@warlock.js/core/src/x.ts") → esbuild transpile + cache
53
+ * // load("file:///node_modules/ms/index.js") → nextLoad (Node default)
54
+ */
55
+ declare function load(url: string, context: LoadContext, nextLoad: NextLoad): Promise<LoadResult>;
56
+ //#endregion
57
+ export { TranspileInit, load };
58
+ //# sourceMappingURL=load-hook.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-hook.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/load-hook.ts"],"mappings":";KAMK,WAAA;EACH,MAAA;EACA,gBAAA,GAAmB,MAAM;AAAA;AAAA,KAGtB,UAAA;EACH,MAAA;EACA,MAAA,WAAiB,MAAM;EACvB,YAAA;AAAA;AAAA,KAGG,QAAA,IAAY,GAAA,UAAa,OAAA,GAAU,WAAA,KAAgB,OAAA,CAAQ,UAAA;AATrC;;;;;AAAA,KAgBf,aAAA;EAXO,+CAajB,QAAA,UAZY;EAcZ,WAAA,UAXG;EAaH,MAAA;EAEA,WAAA;EAf8D;;;;;EAqB9D,UAAA;AAAA;;;;;;;;;AAAU;AAqDZ;;;;;;;;;;;;;;;;;iBAAsB,IAAA,CACpB,GAAA,UACA,OAAA,EAAS,WAAA,EACT,QAAA,EAAU,QAAA,GACT,OAAA,CAAQ,UAAA"}
@@ -0,0 +1,86 @@
1
+ import { TranspileCache, cacheKey } from "./transpile-cache.mjs";
2
+ import { sourceSlug } from "./source-slug.mjs";
3
+ import { transformSync } from "esbuild";
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ //#region ../@warlock.js/core/src/dev-server/loader/load-hook.ts
8
+ const VERSION_QUERY = /\?v=\d+$/;
9
+ const TS_FILE = /\.(ts|tsx|mts|cts)$/;
10
+ let transpile;
11
+ let cache;
12
+ /**
13
+ * Called once from the hook worker's `initialize()`. Wires the persisted
14
+ * transpile cache and runs one opportunistic GC pass at boot — cheap, off
15
+ * the hot path, single directory scan.
16
+ */
17
+ function configureTranspile(init) {
18
+ transpile = init;
19
+ cache = new TranspileCache(init.cacheDir);
20
+ cache.gc({
21
+ maxBytes: 256 * 1024 * 1024,
22
+ maxAgeMs: 336 * 60 * 60 * 1e3
23
+ });
24
+ }
25
+ /**
26
+ * ESM loader `load()` hook.
27
+ *
28
+ * Owns the transpile for **every** TypeScript file — project `src/`,
29
+ * `@warlock.js/*` framework source, and `warlock.config.ts` alike. There
30
+ * is no tsx in the TypeScript path any more: we read the source, key it by
31
+ * a content hash + options fingerprint, serve the cached JS on a hit, and
32
+ * otherwise transform it with esbuild directly and persist. Non-TypeScript
33
+ * URLs (npm `.js`/`.cjs`/`.mjs`, `node:` builtins, JSON) are not ours —
34
+ * they fall through to the next loader (Node's default).
35
+ *
36
+ * The `?v=<N>` query drives Node's module identity (HMR) for `src/` files
37
+ * and is **never** part of the cache key — the cache is keyed purely by
38
+ * source content, so the two mechanisms are fully orthogonal. Framework
39
+ * and config `.ts` carry no `?v` (no HMR) but are transpiled and cached
40
+ * exactly the same way.
41
+ *
42
+ * The source map esbuild emits is inline base64 with `sourcefile` set to
43
+ * the clean absolute `.ts` path, so V8 (run with source maps enabled)
44
+ * reports stack frames at the original TypeScript location.
45
+ *
46
+ * @example
47
+ * // load("file:///src/user.model.ts?v=3") → esbuild transpile + cache
48
+ * // load("file:///@warlock.js/core/src/x.ts") → esbuild transpile + cache
49
+ * // load("file:///node_modules/ms/index.js") → nextLoad (Node default)
50
+ */
51
+ async function load(url, context, nextLoad) {
52
+ const cleanUrl = url.replace(VERSION_QUERY, "");
53
+ if (!TS_FILE.test(cleanUrl)) return nextLoad(url, context);
54
+ const absolutePath = fileURLToPath(cleanUrl);
55
+ const source = readFileSync(absolutePath, "utf8");
56
+ const key = cacheKey(source, transpile.fingerprint);
57
+ const label = transpile.debugNames ? sourceSlug(absolutePath) : void 0;
58
+ const hit = cache.get(key, label);
59
+ if (hit) return {
60
+ format: "module",
61
+ source: hit.code,
62
+ shortCircuit: true
63
+ };
64
+ const result = transformSync(source, {
65
+ loader: absolutePath.endsWith(".tsx") ? "tsx" : "ts",
66
+ format: "esm",
67
+ target: transpile.target,
68
+ sourcefile: absolutePath,
69
+ sourcemap: "inline",
70
+ tsconfigRaw: transpile.tsconfigRaw
71
+ });
72
+ const code = label ? `${result.code}\n// @source ${absolutePath}\n` : result.code;
73
+ cache.put(key, {
74
+ code,
75
+ map: ""
76
+ }, label);
77
+ return {
78
+ format: "module",
79
+ source: code,
80
+ shortCircuit: true
81
+ };
82
+ }
83
+
84
+ //#endregion
85
+ export { configureTranspile, load };
86
+ //# sourceMappingURL=load-hook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-hook.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/load-hook.ts"],"sourcesContent":["import { transformSync } from \"esbuild\";\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { sourceSlug } from \"./source-slug.js\";\nimport { cacheKey, TranspileCache } from \"./transpile-cache.js\";\n\ntype LoadContext = {\n format?: string;\n importAttributes?: Record<string, string>;\n};\n\ntype LoadResult = {\n format: string;\n source: string | Buffer;\n shortCircuit?: boolean;\n};\n\ntype NextLoad = (url: string, context?: LoadContext) => Promise<LoadResult>;\n\n/**\n * Everything the transpile path needs, computed once on the main thread and\n * shipped into the hook worker via `initialize()`. Always present — esbuild\n * transpile is unconditional now (tsx is no longer in the TS path).\n */\nexport type TranspileInit = {\n /** Absolute `.warlock/transpile` directory. */\n cacheDir: string;\n /** Source-content key salt: esbuild version + epoch + tsconfig blob. */\n fingerprint: string;\n /** esbuild `target` (e.g. `\"node22\"` — must match the prod builder). */\n target: string;\n /** JSON string of `{ compilerOptions }` handed to esbuild as tsconfigRaw. */\n tsconfigRaw: string;\n /**\n * Debug-only: name cache files `<slug>.<hash>.js` and append a trailing\n * `// @source <path>` marker so entries are eyeball-identifiable. Off by\n * default (opaque `<hash>.js`). Cosmetic — never affects keys or lookup.\n */\n debugNames: boolean;\n};\n\nconst VERSION_QUERY = /\\?v=\\d+$/;\nconst TS_FILE = /\\.(ts|tsx|mts|cts)$/;\n\nlet transpile: TranspileInit;\nlet cache: TranspileCache;\n\n/**\n * Called once from the hook worker's `initialize()`. Wires the persisted\n * transpile cache and runs one opportunistic GC pass at boot — cheap, off\n * the hot path, single directory scan.\n */\nexport function configureTranspile(init: TranspileInit): void {\n transpile = init;\n cache = new TranspileCache(init.cacheDir);\n\n // Boot-time eviction so the cache can't grow unbounded across weeks of\n // dev. Bounds are generous; correctness never depends on GC (content\n // hashing already guarantees that) — this is purely disk hygiene.\n cache.gc({\n maxBytes: 256 * 1024 * 1024,\n maxAgeMs: 14 * 24 * 60 * 60 * 1000,\n });\n}\n\n/**\n * ESM loader `load()` hook.\n *\n * Owns the transpile for **every** TypeScript file — project `src/`,\n * `@warlock.js/*` framework source, and `warlock.config.ts` alike. There\n * is no tsx in the TypeScript path any more: we read the source, key it by\n * a content hash + options fingerprint, serve the cached JS on a hit, and\n * otherwise transform it with esbuild directly and persist. Non-TypeScript\n * URLs (npm `.js`/`.cjs`/`.mjs`, `node:` builtins, JSON) are not ours —\n * they fall through to the next loader (Node's default).\n *\n * The `?v=<N>` query drives Node's module identity (HMR) for `src/` files\n * and is **never** part of the cache key — the cache is keyed purely by\n * source content, so the two mechanisms are fully orthogonal. Framework\n * and config `.ts` carry no `?v` (no HMR) but are transpiled and cached\n * exactly the same way.\n *\n * The source map esbuild emits is inline base64 with `sourcefile` set to\n * the clean absolute `.ts` path, so V8 (run with source maps enabled)\n * reports stack frames at the original TypeScript location.\n *\n * @example\n * // load(\"file:///src/user.model.ts?v=3\") → esbuild transpile + cache\n * // load(\"file:///@warlock.js/core/src/x.ts\") → esbuild transpile + cache\n * // load(\"file:///node_modules/ms/index.js\") → nextLoad (Node default)\n */\nexport async function load(\n url: string,\n context: LoadContext,\n nextLoad: NextLoad,\n): Promise<LoadResult> {\n const cleanUrl = url.replace(VERSION_QUERY, \"\");\n\n // Not TypeScript → not ours. npm packages, node: builtins, JSON, etc.\n if (!TS_FILE.test(cleanUrl)) {\n return nextLoad(url, context);\n }\n\n const absolutePath = fileURLToPath(cleanUrl);\n const source = readFileSync(absolutePath, \"utf8\");\n const key = cacheKey(source, transpile.fingerprint);\n\n // Cosmetic filename label; undefined keeps opaque <hash>.js names.\n const label = transpile.debugNames ? sourceSlug(absolutePath) : undefined;\n\n const hit = cache.get(key, label);\n if (hit) {\n return { format: \"module\", source: hit.code, shortCircuit: true };\n }\n\n const loader = absolutePath.endsWith(\".tsx\") ? \"tsx\" : \"ts\";\n\n const result = transformSync(source, {\n loader,\n format: \"esm\",\n target: transpile.target,\n sourcefile: absolutePath,\n // Inline base64 map, self-contained (sourcesContent on by default), so\n // it survives the ?v=N URL with no extra resolution step.\n sourcemap: \"inline\",\n tsconfigRaw: transpile.tsconfigRaw,\n });\n\n // result.code already carries the inline //# sourceMappingURL. The\n // marker goes AFTER that line: a trailing comment can't shift earlier\n // generated lines, so the inline map (and therefore stack-frame line\n // numbers) stays exact. A leading marker would desync it by one line.\n const code = label\n ? `${result.code}\\n// @source ${absolutePath}\\n`\n : result.code;\n\n cache.put(key, { code, map: \"\" }, label);\n\n return { format: \"module\", source: code, shortCircuit: true };\n}\n"],"mappings":";;;;;;;AAyCA,MAAM,gBAAgB;AACtB,MAAM,UAAU;AAEhB,IAAI;AACJ,IAAI;;;;;;AAOJ,SAAgB,mBAAmB,MAA2B;CAC5D,YAAY;CACZ,QAAQ,IAAI,eAAe,KAAK,QAAQ;CAKxC,MAAM,GAAG;EACP,UAAU,MAAM,OAAO;EACvB,UAAU,MAAU,KAAK,KAAK;CAChC,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,KACpB,KACA,SACA,UACqB;CACrB,MAAM,WAAW,IAAI,QAAQ,eAAe,EAAE;CAG9C,IAAI,CAAC,QAAQ,KAAK,QAAQ,GACxB,OAAO,SAAS,KAAK,OAAO;CAG9B,MAAM,eAAe,cAAc,QAAQ;CAC3C,MAAM,SAAS,aAAa,cAAc,MAAM;CAChD,MAAM,MAAM,SAAS,QAAQ,UAAU,WAAW;CAGlD,MAAM,QAAQ,UAAU,aAAa,WAAW,YAAY,IAAI;CAEhE,MAAM,MAAM,MAAM,IAAI,KAAK,KAAK;CAChC,IAAI,KACF,OAAO;EAAE,QAAQ;EAAU,QAAQ,IAAI;EAAM,cAAc;CAAK;CAKlE,MAAM,SAAS,cAAc,QAAQ;EACnC,QAHa,aAAa,SAAS,MAAM,IAAI,QAAQ;EAIrD,QAAQ;EACR,QAAQ,UAAU;EAClB,YAAY;EAGZ,WAAW;EACX,aAAa,UAAU;CACzB,CAAC;CAMD,MAAM,OAAO,QACT,GAAG,OAAO,KAAK,eAAe,aAAa,MAC3C,OAAO;CAEX,MAAM,IAAI,KAAK;EAAE;EAAM,KAAK;CAAG,GAAG,KAAK;CAEvC,OAAO;EAAE,QAAQ;EAAU,QAAQ;EAAM,cAAc;CAAK;AAC9D"}
@@ -0,0 +1,69 @@
1
+ import { URL, fileURLToPath, pathToFileURL } from "node:url";
2
+
3
+ //#region ../@warlock.js/core/src/dev-server/loader/own-resolver.ts
4
+ /**
5
+ * Extension/`index` probing for one candidate base path, mirroring tsx's
6
+ * TypeScript-aware order:
7
+ *
8
+ * 1. exact (if it already has a real extension that exists)
9
+ * 2. `.js`/`.jsx`/`.mjs`/`.cjs` specifier → the TS source it compiles from
10
+ * (`.ts`/`.tsx`) — the standard "import the .js, ship the .ts" ESM style
11
+ * 3. append `.ts`, `.tsx`, `.js`, `.jsx`, `.json`
12
+ * 4. directory index: `<dir>/index.{ts,tsx,js,jsx}`
13
+ *
14
+ * Returns the resolved absolute path, or `null` if nothing exists.
15
+ */
16
+ function probeFile(basePath, fileExists) {
17
+ const jsRewrite = basePath.match(/\.(js|jsx|mjs|cjs)$/);
18
+ if (jsRewrite) {
19
+ const stem = basePath.slice(0, -jsRewrite[0].length);
20
+ for (const ext of [".ts", ".tsx"]) if (fileExists(stem + ext)) return stem + ext;
21
+ }
22
+ if (/\.[a-z]+$/i.test(basePath) && fileExists(basePath)) return basePath;
23
+ for (const ext of [
24
+ ".ts",
25
+ ".tsx",
26
+ ".js",
27
+ ".jsx",
28
+ ".json"
29
+ ]) if (fileExists(basePath + ext)) return basePath + ext;
30
+ for (const ext of [
31
+ ".ts",
32
+ ".tsx",
33
+ ".js",
34
+ ".jsx"
35
+ ]) {
36
+ const candidate = `${basePath}/index${ext}`;
37
+ if (fileExists(candidate)) return candidate;
38
+ }
39
+ return null;
40
+ }
41
+ function isRelative(specifier) {
42
+ return specifier.startsWith("./") || specifier.startsWith("../");
43
+ }
44
+ /**
45
+ * Resolve `specifier` to a `file://` URL we own, or `null` to fall through
46
+ * to the next resolver.
47
+ *
48
+ * @param specifier - The import specifier.
49
+ * @param parentURL - Importing module's URL (already `?v=N`-stripped).
50
+ * @param pathsMatcher - `get-tsconfig` paths matcher (alias → candidates).
51
+ * @param fileExists - Existence probe (injected for testing).
52
+ */
53
+ function ownResolve(specifier, parentURL, pathsMatcher, fileExists) {
54
+ if (specifier.startsWith("node:") || specifier.startsWith("file:") || specifier.startsWith("data:")) return null;
55
+ if (isRelative(specifier) || specifier.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(specifier)) {
56
+ if (!parentURL) return null;
57
+ const hit = probeFile(fileURLToPath(new URL(specifier, parentURL)).replace(/\\/g, "/"), fileExists);
58
+ return hit ? pathToFileURL(hit).href : null;
59
+ }
60
+ if (pathsMatcher) for (const candidate of pathsMatcher(specifier)) {
61
+ const hit = probeFile(candidate.replace(/\\/g, "/"), fileExists);
62
+ if (hit) return pathToFileURL(hit).href;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ //#endregion
68
+ export { ownResolve };
69
+ //# sourceMappingURL=own-resolver.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-resolver.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/own-resolver.ts"],"sourcesContent":["import { fileURLToPath, pathToFileURL, URL } from \"node:url\";\n\n/**\n * The framework's own ESM `resolve()` core — the Phase B replacement for\n * delegating resolution to tsx.\n *\n * Scope by design: it owns exactly what Node's default resolver cannot do\n * (TypeScript extension/`index` probing and tsconfig `paths` aliases). For\n * anything else — bare npm packages, `node:` builtins, `file:`/`data:`\n * specifiers — it returns `null`, meaning \"not mine, fall through to the\n * next resolver\" (Node default, which already honours `node_modules` and\n * package `exports`). That keeps the risky surface minimal: we only have\n * to reproduce tsx's TS-path behaviour, not re-implement npm resolution.\n *\n * Pure and dependency-injected (`pathsMatcher`, `fileExists`) so the whole\n * thing is unit-testable and verifiable against the captured tsx-4.21\n * golden fixture without booting.\n */\n\nexport type PathsMatcher = (specifier: string) => string[];\nexport type FileExists = (absolutePath: string) => boolean;\n\n/**\n * Extension/`index` probing for one candidate base path, mirroring tsx's\n * TypeScript-aware order:\n *\n * 1. exact (if it already has a real extension that exists)\n * 2. `.js`/`.jsx`/`.mjs`/`.cjs` specifier → the TS source it compiles from\n * (`.ts`/`.tsx`) — the standard \"import the .js, ship the .ts\" ESM style\n * 3. append `.ts`, `.tsx`, `.js`, `.jsx`, `.json`\n * 4. directory index: `<dir>/index.{ts,tsx,js,jsx}`\n *\n * Returns the resolved absolute path, or `null` if nothing exists.\n */\nexport function probeFile(basePath: string, fileExists: FileExists): string | null {\n // 2. `.js` → `.ts`/`.tsx` rewrite (checked before the literal .js so a\n // co-located source wins, matching tsx).\n const jsRewrite = basePath.match(/\\.(js|jsx|mjs|cjs)$/);\n if (jsRewrite) {\n const stem = basePath.slice(0, -jsRewrite[0].length);\n for (const ext of [\".ts\", \".tsx\"]) {\n if (fileExists(stem + ext)) return stem + ext;\n }\n }\n\n // 1. exact (real extension already present and on disk)\n if (/\\.[a-z]+$/i.test(basePath) && fileExists(basePath)) {\n return basePath;\n }\n\n // 3. append source/asset extensions\n for (const ext of [\".ts\", \".tsx\", \".js\", \".jsx\", \".json\"]) {\n if (fileExists(basePath + ext)) return basePath + ext;\n }\n\n // 4. directory index\n for (const ext of [\".ts\", \".tsx\", \".js\", \".jsx\"]) {\n const candidate = `${basePath}/index${ext}`;\n if (fileExists(candidate)) return candidate;\n }\n\n return null;\n}\n\nfunction isRelative(specifier: string): boolean {\n return specifier.startsWith(\"./\") || specifier.startsWith(\"../\");\n}\n\n/**\n * Resolve `specifier` to a `file://` URL we own, or `null` to fall through\n * to the next resolver.\n *\n * @param specifier - The import specifier.\n * @param parentURL - Importing module's URL (already `?v=N`-stripped).\n * @param pathsMatcher - `get-tsconfig` paths matcher (alias → candidates).\n * @param fileExists - Existence probe (injected for testing).\n */\nexport function ownResolve(\n specifier: string,\n parentURL: string | undefined,\n pathsMatcher: PathsMatcher | null,\n fileExists: FileExists,\n): string | null {\n // Not ours: builtins and already-formed URLs (config-loader imports by\n // file: URL). Node default handles these.\n if (\n specifier.startsWith(\"node:\") ||\n specifier.startsWith(\"file:\") ||\n specifier.startsWith(\"data:\")\n ) {\n return null;\n }\n\n // Relative / absolute path imports → probe against the parent's dir.\n if (isRelative(specifier) || specifier.startsWith(\"/\") || /^[a-zA-Z]:[\\\\/]/.test(specifier)) {\n if (!parentURL) return null;\n const base = fileURLToPath(new URL(specifier, parentURL));\n const hit = probeFile(base.replace(/\\\\/g, \"/\"), fileExists);\n return hit ? pathToFileURL(hit).href : null;\n }\n\n // Bare / aliased: try tsconfig `paths`. Each candidate is probed; the\n // first that exists on disk wins. If none do (e.g. a real npm package\n // whose name happened to match a baseUrl guess), return null so Node\n // default resolves it from node_modules with full `exports` support.\n if (pathsMatcher) {\n for (const candidate of pathsMatcher(specifier)) {\n const hit = probeFile(candidate.replace(/\\\\/g, \"/\"), fileExists);\n if (hit) return pathToFileURL(hit).href;\n }\n }\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkCA,SAAgB,UAAU,UAAkB,YAAuC;CAGjF,MAAM,YAAY,SAAS,MAAM,qBAAqB;CACtD,IAAI,WAAW;EACb,MAAM,OAAO,SAAS,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,MAAM;EACnD,KAAK,MAAM,OAAO,CAAC,OAAO,MAAM,GAC9B,IAAI,WAAW,OAAO,GAAG,GAAG,OAAO,OAAO;CAE9C;CAGA,IAAI,aAAa,KAAK,QAAQ,KAAK,WAAW,QAAQ,GACpD,OAAO;CAIT,KAAK,MAAM,OAAO;EAAC;EAAO;EAAQ;EAAO;EAAQ;CAAO,GACtD,IAAI,WAAW,WAAW,GAAG,GAAG,OAAO,WAAW;CAIpD,KAAK,MAAM,OAAO;EAAC;EAAO;EAAQ;EAAO;CAAM,GAAG;EAChD,MAAM,YAAY,GAAG,SAAS,QAAQ;EACtC,IAAI,WAAW,SAAS,GAAG,OAAO;CACpC;CAEA,OAAO;AACT;AAEA,SAAS,WAAW,WAA4B;CAC9C,OAAO,UAAU,WAAW,IAAI,KAAK,UAAU,WAAW,KAAK;AACjE;;;;;;;;;;AAWA,SAAgB,WACd,WACA,WACA,cACA,YACe;CAGf,IACE,UAAU,WAAW,OAAO,KAC5B,UAAU,WAAW,OAAO,KAC5B,UAAU,WAAW,OAAO,GAE5B,OAAO;CAIT,IAAI,WAAW,SAAS,KAAK,UAAU,WAAW,GAAG,KAAK,kBAAkB,KAAK,SAAS,GAAG;EAC3F,IAAI,CAAC,WAAW,OAAO;EAEvB,MAAM,MAAM,UADC,cAAc,IAAI,IAAI,WAAW,SAAS,CAC9B,CAAC,CAAC,QAAQ,OAAO,GAAG,GAAG,UAAU;EAC1D,OAAO,MAAM,cAAc,GAAG,CAAC,CAAC,OAAO;CACzC;CAMA,IAAI,cACF,KAAK,MAAM,aAAa,aAAa,SAAS,GAAG;EAC/C,MAAM,MAAM,UAAU,UAAU,QAAQ,OAAO,GAAG,GAAG,UAAU;EAC/D,IAAI,KAAK,OAAO,cAAc,GAAG,CAAC,CAAC;CACrC;CAGF,OAAO;AACT"}
@@ -0,0 +1,43 @@
1
+ import { appendFileSync } from "node:fs";
2
+
3
+ //#region ../@warlock.js/core/src/dev-server/loader/resolve-capture.ts
4
+ /**
5
+ * Golden-fixture recorder for the Phase B resolver work.
6
+ *
7
+ * When `WARLOCK_RESOLVE_CAPTURE` points at a file path, every resolution
8
+ * the *current* (tsx-backed) hook performs is appended there as one JSON
9
+ * object per line: `{ specifier, parentURL, url, format }`, all with the
10
+ * `?v=N` HMR token stripped so the record is stable across runs.
11
+ *
12
+ * A full `yarn start` boot then yields the real import graph's resolutions
13
+ * — tsconfig paths, workspace `@warlock.js/*`, relative/extensionless,
14
+ * package `exports`, bare npm, `node:` builtins — as the ground truth the
15
+ * replacement resolver must reproduce exactly.
16
+ *
17
+ * Disabled (the default) it is a single env-var read and an immediate
18
+ * return: no measurable cost on the resolve hot path.
19
+ */
20
+ const VERSION_QUERY = /\?v=\d+$/;
21
+ let resolvedPath;
22
+ function targetFile() {
23
+ if (resolvedPath !== void 0) return resolvedPath;
24
+ resolvedPath = process.env.WARLOCK_RESOLVE_CAPTURE || null;
25
+ return resolvedPath;
26
+ }
27
+ function captureResolution(record) {
28
+ const file = targetFile();
29
+ if (!file) return;
30
+ const line = JSON.stringify({
31
+ specifier: record.specifier,
32
+ parentURL: record.parentURL?.replace(VERSION_QUERY, ""),
33
+ url: record.url.replace(VERSION_QUERY, ""),
34
+ format: record.format
35
+ }) + "\n";
36
+ try {
37
+ appendFileSync(file, line);
38
+ } catch {}
39
+ }
40
+
41
+ //#endregion
42
+ export { captureResolution };
43
+ //# sourceMappingURL=resolve-capture.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-capture.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/resolve-capture.ts"],"sourcesContent":["import { appendFileSync } from \"node:fs\";\n\n/**\n * Golden-fixture recorder for the Phase B resolver work.\n *\n * When `WARLOCK_RESOLVE_CAPTURE` points at a file path, every resolution\n * the *current* (tsx-backed) hook performs is appended there as one JSON\n * object per line: `{ specifier, parentURL, url, format }`, all with the\n * `?v=N` HMR token stripped so the record is stable across runs.\n *\n * A full `yarn start` boot then yields the real import graph's resolutions\n * — tsconfig paths, workspace `@warlock.js/*`, relative/extensionless,\n * package `exports`, bare npm, `node:` builtins — as the ground truth the\n * replacement resolver must reproduce exactly.\n *\n * Disabled (the default) it is a single env-var read and an immediate\n * return: no measurable cost on the resolve hot path.\n */\n\nconst VERSION_QUERY = /\\?v=\\d+$/;\n\nlet resolvedPath: string | null | undefined;\n\nfunction targetFile(): string | null {\n if (resolvedPath !== undefined) return resolvedPath;\n resolvedPath = process.env.WARLOCK_RESOLVE_CAPTURE || null;\n return resolvedPath;\n}\n\nexport type ResolutionRecord = {\n specifier: string;\n parentURL: string | undefined;\n url: string;\n format: string | undefined;\n};\n\nexport function captureResolution(record: ResolutionRecord): void {\n const file = targetFile();\n if (!file) return;\n\n const line =\n JSON.stringify({\n specifier: record.specifier,\n parentURL: record.parentURL?.replace(VERSION_QUERY, \"\"),\n url: record.url.replace(VERSION_QUERY, \"\"),\n format: record.format,\n }) + \"\\n\";\n\n try {\n appendFileSync(file, line);\n } catch {\n // Capture is a diagnostic aid — never let it break resolution.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAM,gBAAgB;AAEtB,IAAI;AAEJ,SAAS,aAA4B;CACnC,IAAI,iBAAiB,QAAW,OAAO;CACvC,eAAe,QAAQ,IAAI,2BAA2B;CACtD,OAAO;AACT;AASA,SAAgB,kBAAkB,QAAgC;CAChE,MAAM,OAAO,WAAW;CACxB,IAAI,CAAC,MAAM;CAEX,MAAM,OACJ,KAAK,UAAU;EACb,WAAW,OAAO;EAClB,WAAW,OAAO,WAAW,QAAQ,eAAe,EAAE;EACtD,KAAK,OAAO,IAAI,QAAQ,eAAe,EAAE;EACzC,QAAQ,OAAO;CACjB,CAAC,IAAI;CAEP,IAAI;EACF,eAAe,MAAM,IAAI;CAC3B,QAAQ,CAER;AACF"}
@@ -0,0 +1,41 @@
1
+ //#region ../@warlock.js/core/src/dev-server/loader/resolve-hook.d.ts
2
+ type ResolveContext = {
3
+ parentURL?: string;
4
+ conditions?: string[];
5
+ importAttributes?: Record<string, string>;
6
+ };
7
+ type ResolveResult = {
8
+ url: string;
9
+ format?: string;
10
+ shortCircuit?: boolean;
11
+ };
12
+ type NextResolve = (specifier: string, context?: ResolveContext) => Promise<ResolveResult>;
13
+ /**
14
+ * ESM loader `resolve()` hook.
15
+ *
16
+ * The framework's own resolver (`ownResolve`) handles tsconfig `paths`,
17
+ * aliases, and TS extension/`index` probing. Specifiers it doesn't own
18
+ * (bare npm, `node:`, `file:`) fall through to `nextResolve` (Node
19
+ * default). After resolution, stamps a `?v=<N>` version token onto any
20
+ * `.ts` / `.tsx` file that lives under the configured `src/` root.
21
+ *
22
+ * The version token is the cache-bust mechanism: when a file changes the main
23
+ * thread bumps its counter, the next `import()` produces a new URL, Node sees
24
+ * a cache miss, and loads fresh content — native ESM cycles are preserved
25
+ * because static imports remain static throughout.
26
+ *
27
+ * **parentURL cleaning**: the resolved URL carries `?v=N` as a suffix. If a
28
+ * module loaded this way statically imports a relative sibling, Node uses that
29
+ * `?v=N` URL as the `parentURL` for the next `resolve()` call. Passing it
30
+ * unmodified to `nextResolve()` causes tsx to produce paths like
31
+ * `some-sibling.ts?v=0` (the query leaks into URL joining). We strip `?v=N`
32
+ * from `parentURL` before forwarding to prevent this.
33
+ *
34
+ * @example
35
+ * // First import of user.model.ts → ?v=0
36
+ * // File watcher bumps → next import → ?v=1 → fresh module
37
+ */
38
+ declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): Promise<ResolveResult>;
39
+ //#endregion
40
+ export { resolve };
41
+ //# sourceMappingURL=resolve-hook.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-hook.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/resolve-hook.ts"],"mappings":";KAqCK,cAAA;EACH,SAAA;EACA,UAAA;EACA,gBAAA,GAAmB,MAAM;AAAA;AAAA,KAGtB,aAAA;EACH,GAAA;EACA,MAAA;EACA,YAAA;AAAA;AAAA,KAGG,WAAA,IAAe,SAAA,UAAmB,OAAA,GAAU,cAAA,KAAmB,OAAA,CAAQ,aAAA;;AAH9D;AAAA;;;;;;;;;;;;;;AAG2E;AA2BzF;;;;;;;;iBAAsB,OAAA,CACpB,SAAA,UACA,OAAA,EAAS,cAAA,EACT,WAAA,EAAa,WAAA,GACZ,OAAA,CAAQ,aAAA"}
@@ -0,0 +1,87 @@
1
+ import { ownResolve } from "./own-resolver.mjs";
2
+ import { captureResolution } from "./resolve-capture.mjs";
3
+ import { getVersion } from "./version-registry.mjs";
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { getTsconfig, resolvePathAlias } from "get-tsconfig";
7
+
8
+ //#region ../@warlock.js/core/src/dev-server/loader/resolve-hook.ts
9
+ /**
10
+ * The `file://` URL prefix for the project's `src/` root, set once during
11
+ * `initialize()`. Used to identify which resolved URLs belong to user code.
12
+ */
13
+ let srcRootUrl = "";
14
+ /**
15
+ * Configure the source root used by the resolve hook.
16
+ * Must be called from `initialize()` before any imports are resolved.
17
+ *
18
+ * @param srcRoot - Absolute filesystem path to the project `src/` directory.
19
+ */
20
+ function setSrcRoot(srcRoot) {
21
+ srcRootUrl = pathToFileURL(srcRoot).href + "/";
22
+ }
23
+ /** Lazily-built tsconfig `paths` matcher (worker has no startup hook). */
24
+ let pathsMatcher = null;
25
+ let pathsMatcherBuilt = false;
26
+ function getPathsMatcher() {
27
+ if (pathsMatcherBuilt) return pathsMatcher;
28
+ pathsMatcherBuilt = true;
29
+ const tsconfig = getTsconfig(process.cwd());
30
+ pathsMatcher = tsconfig ? (specifier) => resolvePathAlias(tsconfig, specifier) : null;
31
+ return pathsMatcher;
32
+ }
33
+ /**
34
+ * ESM loader `resolve()` hook.
35
+ *
36
+ * The framework's own resolver (`ownResolve`) handles tsconfig `paths`,
37
+ * aliases, and TS extension/`index` probing. Specifiers it doesn't own
38
+ * (bare npm, `node:`, `file:`) fall through to `nextResolve` (Node
39
+ * default). After resolution, stamps a `?v=<N>` version token onto any
40
+ * `.ts` / `.tsx` file that lives under the configured `src/` root.
41
+ *
42
+ * The version token is the cache-bust mechanism: when a file changes the main
43
+ * thread bumps its counter, the next `import()` produces a new URL, Node sees
44
+ * a cache miss, and loads fresh content — native ESM cycles are preserved
45
+ * because static imports remain static throughout.
46
+ *
47
+ * **parentURL cleaning**: the resolved URL carries `?v=N` as a suffix. If a
48
+ * module loaded this way statically imports a relative sibling, Node uses that
49
+ * `?v=N` URL as the `parentURL` for the next `resolve()` call. Passing it
50
+ * unmodified to `nextResolve()` causes tsx to produce paths like
51
+ * `some-sibling.ts?v=0` (the query leaks into URL joining). We strip `?v=N`
52
+ * from `parentURL` before forwarding to prevent this.
53
+ *
54
+ * @example
55
+ * // First import of user.model.ts → ?v=0
56
+ * // File watcher bumps → next import → ?v=1 → fresh module
57
+ */
58
+ async function resolve(specifier, context, nextResolve) {
59
+ const cleanContext = context.parentURL?.includes("?v=") ? {
60
+ ...context,
61
+ parentURL: context.parentURL.replace(/\?v=\d+$/, "")
62
+ } : context;
63
+ const owned = ownResolve(specifier, cleanContext.parentURL, getPathsMatcher(), existsSync);
64
+ const result = owned ? {
65
+ url: owned,
66
+ shortCircuit: true
67
+ } : await nextResolve(specifier, cleanContext);
68
+ const { url } = result;
69
+ captureResolution({
70
+ specifier,
71
+ parentURL: cleanContext.parentURL,
72
+ url,
73
+ format: result.format
74
+ });
75
+ const cleanUrl = url.replace(/\?v=\d+$/, "");
76
+ if (!cleanUrl.startsWith(srcRootUrl) || !cleanUrl.endsWith(".ts") && !cleanUrl.endsWith(".tsx")) return result;
77
+ const version = getVersion(fileURLToPath(cleanUrl));
78
+ return {
79
+ ...result,
80
+ url: `${cleanUrl}?v=${version}`,
81
+ shortCircuit: true
82
+ };
83
+ }
84
+
85
+ //#endregion
86
+ export { resolve, setSrcRoot };
87
+ //# sourceMappingURL=resolve-hook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-hook.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/resolve-hook.ts"],"sourcesContent":["import { getTsconfig, resolvePathAlias } from \"get-tsconfig\";\nimport { existsSync } from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { ownResolve, type PathsMatcher } from \"./own-resolver.js\";\nimport { captureResolution } from \"./resolve-capture.js\";\nimport { getVersion } from \"./version-registry.js\";\n\n/**\n * The `file://` URL prefix for the project's `src/` root, set once during\n * `initialize()`. Used to identify which resolved URLs belong to user code.\n */\nlet srcRootUrl = \"\";\n\n/**\n * Configure the source root used by the resolve hook.\n * Must be called from `initialize()` before any imports are resolved.\n *\n * @param srcRoot - Absolute filesystem path to the project `src/` directory.\n */\nexport function setSrcRoot(srcRoot: string): void {\n srcRootUrl = pathToFileURL(srcRoot).href + \"/\";\n}\n\n/** Lazily-built tsconfig `paths` matcher (worker has no startup hook). */\nlet pathsMatcher: PathsMatcher | null = null;\nlet pathsMatcherBuilt = false;\n\nfunction getPathsMatcher(): PathsMatcher | null {\n if (pathsMatcherBuilt) return pathsMatcher;\n pathsMatcherBuilt = true;\n const tsconfig = getTsconfig(process.cwd());\n // get-tsconfig v5 renamed the curried `createPathsMatcher(t)(spec)` to a direct\n // `resolvePathAlias(t, spec)`. Wrap it to keep the PathsMatcher closure shape.\n pathsMatcher = tsconfig ? (specifier: string) => resolvePathAlias(tsconfig, specifier) : null;\n return pathsMatcher;\n}\n\ntype ResolveContext = {\n parentURL?: string;\n conditions?: string[];\n importAttributes?: Record<string, string>;\n};\n\ntype ResolveResult = {\n url: string;\n format?: string;\n shortCircuit?: boolean;\n};\n\ntype NextResolve = (specifier: string, context?: ResolveContext) => Promise<ResolveResult>;\n\n/**\n * ESM loader `resolve()` hook.\n *\n * The framework's own resolver (`ownResolve`) handles tsconfig `paths`,\n * aliases, and TS extension/`index` probing. Specifiers it doesn't own\n * (bare npm, `node:`, `file:`) fall through to `nextResolve` (Node\n * default). After resolution, stamps a `?v=<N>` version token onto any\n * `.ts` / `.tsx` file that lives under the configured `src/` root.\n *\n * The version token is the cache-bust mechanism: when a file changes the main\n * thread bumps its counter, the next `import()` produces a new URL, Node sees\n * a cache miss, and loads fresh content — native ESM cycles are preserved\n * because static imports remain static throughout.\n *\n * **parentURL cleaning**: the resolved URL carries `?v=N` as a suffix. If a\n * module loaded this way statically imports a relative sibling, Node uses that\n * `?v=N` URL as the `parentURL` for the next `resolve()` call. Passing it\n * unmodified to `nextResolve()` causes tsx to produce paths like\n * `some-sibling.ts?v=0` (the query leaks into URL joining). We strip `?v=N`\n * from `parentURL` before forwarding to prevent this.\n *\n * @example\n * // First import of user.model.ts → ?v=0\n * // File watcher bumps → next import → ?v=1 → fresh module\n */\nexport async function resolve(\n specifier: string,\n context: ResolveContext,\n nextResolve: NextResolve,\n): Promise<ResolveResult> {\n const cleanContext =\n context.parentURL?.includes(\"?v=\")\n ? { ...context, parentURL: context.parentURL.replace(/\\?v=\\d+$/, \"\") }\n : context;\n\n // The framework's own resolver owns tsconfig `paths` + TS\n // extension/index probing. Anything it returns `null` for (bare npm,\n // `node:`, `file:`) falls through to Node default via `nextResolve`.\n const owned = ownResolve(\n specifier,\n cleanContext.parentURL,\n getPathsMatcher(),\n existsSync,\n );\n // When we produced the URL ourselves we did NOT call nextResolve, so the\n // result must short-circuit the loader chain or Node throws\n // ERR_LOADER_CHAIN_INCOMPLETE (e.g. for @warlock.js/* .ts that resolve\n // outside the project src root and hit the early return below).\n const result: ResolveResult = owned\n ? { url: owned, shortCircuit: true }\n : await nextResolve(specifier, cleanContext);\n\n const { url } = result;\n\n // Ground-truth recorder (no-op unless WARLOCK_RESOLVE_CAPTURE is set):\n // logs the answer for this (specifier, parentURL) so the Phase B\n // resolver can be proven to reproduce tsx exactly.\n captureResolution({\n specifier,\n parentURL: cleanContext.parentURL,\n url,\n format: result.format,\n });\n\n const cleanUrl = url.replace(/\\?v=\\d+$/, \"\");\n\n if (\n !cleanUrl.startsWith(srcRootUrl) ||\n (!cleanUrl.endsWith(\".ts\") && !cleanUrl.endsWith(\".tsx\"))\n ) {\n return result;\n }\n\n const absolutePath = fileURLToPath(cleanUrl);\n const version = getVersion(absolutePath);\n\n // Spread the full result from nextResolve (preserves `format` and any other\n // fields tsx sets) and only override `url`. Dropping `format` would cause\n // tsx's load hook to see an undefined format and fall through to Node's\n // default loader, which rejects unknown extensions like .tsx.\n return { ...result, url: `${cleanUrl}?v=${version}`, shortCircuit: true };\n}\n"],"mappings":";;;;;;;;;;;;AAWA,IAAI,aAAa;;;;;;;AAQjB,SAAgB,WAAW,SAAuB;CAChD,aAAa,cAAc,OAAO,CAAC,CAAC,OAAO;AAC7C;;AAGA,IAAI,eAAoC;AACxC,IAAI,oBAAoB;AAExB,SAAS,kBAAuC;CAC9C,IAAI,mBAAmB,OAAO;CAC9B,oBAAoB;CACpB,MAAM,WAAW,YAAY,QAAQ,IAAI,CAAC;CAG1C,eAAe,YAAY,cAAsB,iBAAiB,UAAU,SAAS,IAAI;CACzF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,eAAsB,QACpB,WACA,SACA,aACwB;CACxB,MAAM,eACJ,QAAQ,WAAW,SAAS,KAAK,IAC7B;EAAE,GAAG;EAAS,WAAW,QAAQ,UAAU,QAAQ,YAAY,EAAE;CAAE,IACnE;CAKN,MAAM,QAAQ,WACZ,WACA,aAAa,WACb,gBAAgB,GAChB,UACF;CAKA,MAAM,SAAwB,QAC1B;EAAE,KAAK;EAAO,cAAc;CAAK,IACjC,MAAM,YAAY,WAAW,YAAY;CAE7C,MAAM,EAAE,QAAQ;CAKhB,kBAAkB;EAChB;EACA,WAAW,aAAa;EACxB;EACA,QAAQ,OAAO;CACjB,CAAC;CAED,MAAM,WAAW,IAAI,QAAQ,YAAY,EAAE;CAE3C,IACE,CAAC,SAAS,WAAW,UAAU,KAC9B,CAAC,SAAS,SAAS,KAAK,KAAK,CAAC,SAAS,SAAS,MAAM,GAEvD,OAAO;CAIT,MAAM,UAAU,WADK,cAAc,QACG,CAAC;CAMvC,OAAO;EAAE,GAAG;EAAQ,KAAK,GAAG,SAAS,KAAK;EAAW,cAAc;CAAK;AAC1E"}
@@ -0,0 +1,22 @@
1
+ //#region ../@warlock.js/core/src/dev-server/loader/source-slug.ts
2
+ /**
3
+ * Human-readable label for a cached transpile file, derived from the last
4
+ * three path segments of the source file.
5
+ *
6
+ * Purely cosmetic: it rides in front of the content hash in the cache
7
+ * filename so `ls .warlock/transpile/<shard>/` is browsable when debugging.
8
+ * It is **never** used as a cache key, module URL, or source-map
9
+ * `sourcefile` — correctness still rests entirely on the content hash, so
10
+ * the slug is allowed to be lossy (sanitised, capped).
11
+ *
12
+ * @example
13
+ * sourceSlug("D:/proj/src/app/vectors/utils/locales.ts")
14
+ * // → "vectors-utils-locales"
15
+ */
16
+ function sourceSlug(absolutePath) {
17
+ return absolutePath.replace(/\\/g, "/").replace(/\.[cm]?[jt]sx?$/, "").split("/").filter(Boolean).slice(-3).join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "src";
18
+ }
19
+
20
+ //#endregion
21
+ export { sourceSlug };
22
+ //# sourceMappingURL=source-slug.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-slug.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/source-slug.ts"],"sourcesContent":["/**\n * Human-readable label for a cached transpile file, derived from the last\n * three path segments of the source file.\n *\n * Purely cosmetic: it rides in front of the content hash in the cache\n * filename so `ls .warlock/transpile/<shard>/` is browsable when debugging.\n * It is **never** used as a cache key, module URL, or source-map\n * `sourcefile` — correctness still rests entirely on the content hash, so\n * the slug is allowed to be lossy (sanitised, capped).\n *\n * @example\n * sourceSlug(\"D:/proj/src/app/vectors/utils/locales.ts\")\n * // → \"vectors-utils-locales\"\n */\nexport function sourceSlug(absolutePath: string): string {\n const noExt = absolutePath.replace(/\\\\/g, \"/\").replace(/\\.[cm]?[jt]sx?$/, \"\");\n\n const slug = noExt\n .split(\"/\")\n .filter(Boolean)\n .slice(-3)\n .join(\"-\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\n // The hash guarantees uniqueness; the slug is just a label, so capping it\n // is safe and keeps total path length sane on deep trees / Windows.\n return slug.slice(0, 60) || \"src\";\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,SAAgB,WAAW,cAA8B;CAcvD,OAbc,aAAa,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,mBAAmB,EAEzD,CAAC,CACf,MAAM,GAAG,CAAC,CACV,OAAO,OAAO,CAAC,CACf,MAAM,EAAE,CAAC,CACT,KAAK,GAAG,CAAC,CACT,YAAY,CAAC,CACb,QAAQ,eAAe,GAAG,CAAC,CAC3B,QAAQ,YAAY,EAIb,CAAC,CAAC,MAAM,GAAG,EAAE,KAAK;AAC9B"}