@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.
- package/CHANGELOG.md +24 -0
- package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +1 -0
- package/esm/dev-server/health-checker/workers/ts-health.worker.d.mts +1 -0
- package/esm/dev-server/loader/hook-thread.d.mts +48 -0
- package/esm/dev-server/loader/hook-thread.d.mts.map +1 -0
- package/esm/dev-server/loader/hook-thread.mjs +47 -0
- package/esm/dev-server/loader/hook-thread.mjs.map +1 -0
- package/esm/dev-server/loader/load-hook.d.mts +58 -0
- package/esm/dev-server/loader/load-hook.d.mts.map +1 -0
- package/esm/dev-server/loader/load-hook.mjs +86 -0
- package/esm/dev-server/loader/load-hook.mjs.map +1 -0
- package/esm/dev-server/loader/own-resolver.mjs +69 -0
- package/esm/dev-server/loader/own-resolver.mjs.map +1 -0
- package/esm/dev-server/loader/resolve-capture.mjs +43 -0
- package/esm/dev-server/loader/resolve-capture.mjs.map +1 -0
- package/esm/dev-server/loader/resolve-hook.d.mts +41 -0
- package/esm/dev-server/loader/resolve-hook.d.mts.map +1 -0
- package/esm/dev-server/loader/resolve-hook.mjs +87 -0
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -0
- package/esm/dev-server/loader/source-slug.mjs +22 -0
- package/esm/dev-server/loader/source-slug.mjs.map +1 -0
- package/esm/dev-server/loader/transpile-cache.mjs +163 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/loader/version-registry.mjs +45 -0
- package/esm/dev-server/loader/version-registry.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +10 -3
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +115 -1
- package/esm/generations/stubs.mjs.map +1 -1
- 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 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -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"}
|