@warlock.js/core 4.9.1 → 4.9.2

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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@warlock.js/core` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 4.9.2
8
+
9
+ ### Fixed
10
+
11
+ - the dev server's generated `.warlock/loader-hook.mjs` no longer ships bare `esbuild` / `get-tsconfig` imports. That file is written into the **consuming app's** directory, so a bare specifier resolves from the app — but both packages are core's own dependencies. npm and yarn hoist flat so it worked by accident; under pnpm's strict layout the dev server died with `ERR_MODULE_NOT_FOUND` for a package the app never imported. Each npm specifier is now rewritten at generation time to an absolute path resolved from core's own install, so no consumer has to declare a phantom dependency
12
+
7
13
  ## 4.9.0 - 2026-08-06
8
14
 
9
15
  ### Added
@@ -7,6 +7,42 @@ import { MessageChannel } from "node:worker_threads";
7
7
 
8
8
  //#region ../core/src/dev-server/loader/register-loader.ts
9
9
  /**
10
+ * Rewrite the hook bundle's remaining npm imports to absolute paths resolved
11
+ * from **core's own** location.
12
+ *
13
+ * The bundle is written into the consuming app's `.warlock/` directory, so a
14
+ * bare `import "esbuild"` in it resolves starting from the *app*, walking up
15
+ * the app's `node_modules`. `esbuild` and `get-tsconfig` are core's
16
+ * dependencies, not the app's — under npm/yarn's flat hoisting they happen to
17
+ * be reachable anyway, but under pnpm's strict layout they are not, and the
18
+ * dev server dies with `ERR_MODULE_NOT_FOUND` for a package the app never
19
+ * imported. That is a phantom dependency baked into generated code.
20
+ *
21
+ * Bundling them in instead is not an option for `esbuild`: its JS API is a
22
+ * thin wrapper that spawns a platform-specific native binary, so inlining the
23
+ * JavaScript would not remove the need for the package on disk. Resolving to
24
+ * an absolute `file://` URL at generation time does — the generated file then
25
+ * points straight at the copy core itself is using, whatever the installer's
26
+ * layout.
27
+ */
28
+ const resolveExternalsFromCore = {
29
+ name: "warlock-resolve-externals-from-core",
30
+ setup(build) {
31
+ build.onResolve({ filter: /^[^./]/ }, (args) => {
32
+ if (args.kind === "entry-point" || path.isAbsolute(args.path)) return null;
33
+ if (args.path.startsWith("node:")) return { external: true };
34
+ try {
35
+ return {
36
+ path: import.meta.resolve(args.path),
37
+ external: true
38
+ };
39
+ } catch {
40
+ return { external: true };
41
+ }
42
+ });
43
+ }
44
+ };
45
+ /**
10
46
  * Bundle, write, and register the ESM loader hook.
11
47
  *
12
48
  * **Why bundle?**
@@ -48,7 +84,8 @@ async function registerLoader(transpile) {
48
84
  write: false,
49
85
  platform: "node",
50
86
  target: "node20",
51
- packages: "external"
87
+ packages: "external",
88
+ plugins: [resolveExternalsFromCore]
52
89
  })).outputFiles[0].text;
53
90
  const hookBundlePath = path.join(process.cwd(), ".warlock", "loader-hook.mjs");
54
91
  await putFileAsync(hookBundlePath, bundledCode);
@@ -1 +1 @@
1
- {"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook.js\";\n\n/**\n * Bundle, write, and register the ESM loader hook.\n *\n * **Why bundle?**\n * `module.register()` runs the hook file in a fresh Node.js worker thread that\n * has no tsx hook of its own. The hook source is TypeScript, so we must produce\n * a plain ESM bundle before registering. esbuild inline-bundles all three hook\n * modules (`hook-thread`, `resolve-hook`, `load-hook`, `version-registry`)\n * into a single `.mjs` file written to `.warlock/`. External npm packages\n * (`esbuild`, `node:*`) are kept external — the hook thread can resolve those\n * from `node_modules` normally.\n *\n * **Why a file and not a `data:` URL?**\n * Some Node versions have issues resolving `import.meta.url` inside `data:`\n * modules. A real file under `.warlock/` is simpler and debuggable.\n *\n * **Timing**\n * Called from `FilesOrchestrator.init()` before any user `src/` module is\n * dynamically imported. `module.register()` takes effect for all subsequent\n * `import()` calls, which is exactly the window we need.\n *\n * @param transpile - Transpile-cache config to ship into the hook worker,\n * or `null` to keep the hook in tsx-passthrough mode.\n *\n * @returns The main-thread side of the MessageChannel. Callers post\n * `{ type: \"bump\", absolutePath }` messages on it to invalidate modules.\n *\n * @example\n * const port = await registerLoader(transpileInit);\n * // Later, when a file changes:\n * port.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport async function registerLoader(\n transpile: TranspileInit,\n): Promise<MessagePort> {\n const { port1, port2 } = new MessageChannel();\n\n // hook-thread is a sibling module, so it shares THIS file's extension:\n // `.ts` when core runs from source (tsx), `.mjs` when published.\n const selfPath = fileURLToPath(import.meta.url);\n const hookThreadPath = path.join(\n path.dirname(selfPath),\n `hook-thread${path.extname(selfPath)}`,\n );\n\n const bundleResult = await build({\n entryPoints: [hookThreadPath],\n bundle: true,\n format: \"esm\",\n write: false,\n platform: \"node\",\n target: \"node20\",\n // Keep npm packages and Node built-ins external — the hook thread resolves\n // them normally from node_modules at runtime.\n packages: \"external\",\n });\n\n const bundledCode = bundleResult.outputFiles[0].text;\n // Caller (filesOrchestrator.init) guarantees .warlock/ exists before this runs.\n const hookBundlePath = path.join(process.cwd(), \".warlock\", \"loader-hook.mjs\");\n await putFileAsync(hookBundlePath, bundledCode);\n\n const srcRoot = path.join(process.cwd(), \"src\");\n\n // No tsx registration: our hook owns resolution (own-resolver) and the\n // transpile of every `.ts`/`.tsx` (esbuild, in the load hook). The chain\n // is simply [our hook] → [Node default] for non-TS (npm/.js, node:).\n // In this monorepo tsx is still the *launcher* (`tsx start.ts`) so its\n // loader is present anyway, but it is never consulted for TypeScript —\n // our hook short-circuits first. A released `node bin/warlock.js` has no\n // tsx at all and relies entirely on this hook.\n\n register(pathToFileURL(hookBundlePath).href, import.meta.url, {\n data: { port: port2, srcRoot, transpile },\n transferList: [port2],\n });\n\n return port1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,eACpB,WACsB;CACtB,MAAM,EAAE,OAAO,UAAU,IAAI,eAAe;CAI5C,MAAM,WAAW,cAAc,OAAO,KAAK,GAAG;CAkB9C,MAAM,eAAc,MAZO,MAAM;EAC/B,aAAa,CANQ,KAAK,KAC1B,KAAK,QAAQ,QAAQ,GACrB,cAAc,KAAK,QAAQ,QAAQ,GAIR,CAAC;EAC5B,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,UAAU;EACV,QAAQ;EAGR,UAAU;CACZ,CAAC,EAE+B,CAAC,YAAY,EAAE,CAAC;CAEhD,MAAM,iBAAiB,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY,iBAAiB;CAC7E,MAAM,aAAa,gBAAgB,WAAW;CAE9C,MAAM,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,KAAK;CAU9C,SAAS,cAAc,cAAc,CAAC,CAAC,MAAM,OAAO,KAAK,KAAK;EAC5D,MAAM;GAAE,MAAM;GAAO;GAAS;EAAU;EACxC,cAAc,CAAC,KAAK;CACtB,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"register-loader.mjs","names":[],"sources":["../../../../../../../../core/src/dev-server/loader/register-loader.ts"],"sourcesContent":["import { putFileAsync } from \"@warlock.js/fs\";\nimport { build, type Plugin } from \"esbuild\";\nimport { register } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { type MessagePort, MessageChannel } from \"node:worker_threads\";\nimport type { TranspileInit } from \"./load-hook.js\";\n\n/**\n * Rewrite the hook bundle's remaining npm imports to absolute paths resolved\n * from **core's own** location.\n *\n * The bundle is written into the consuming app's `.warlock/` directory, so a\n * bare `import \"esbuild\"` in it resolves starting from the *app*, walking up\n * the app's `node_modules`. `esbuild` and `get-tsconfig` are core's\n * dependencies, not the app's — under npm/yarn's flat hoisting they happen to\n * be reachable anyway, but under pnpm's strict layout they are not, and the\n * dev server dies with `ERR_MODULE_NOT_FOUND` for a package the app never\n * imported. That is a phantom dependency baked into generated code.\n *\n * Bundling them in instead is not an option for `esbuild`: its JS API is a\n * thin wrapper that spawns a platform-specific native binary, so inlining the\n * JavaScript would not remove the need for the package on disk. Resolving to\n * an absolute `file://` URL at generation time does — the generated file then\n * points straight at the copy core itself is using, whatever the installer's\n * layout.\n */\nexport const resolveExternalsFromCore: Plugin = {\n name: \"warlock-resolve-externals-from-core\",\n setup(build) {\n // The filter is deliberately loose — anything not starting `.` or `/` —\n // so the guards below carry the real logic.\n build.onResolve({ filter: /^[^./]/ }, args => {\n // The entry point comes through here too, and on Windows its absolute\n // path (\"D:\\\\…\") is not `.` or `/`. Marking it external fails the build.\n if (args.kind === \"entry-point\" || path.isAbsolute(args.path)) {\n return null;\n }\n\n if (args.path.startsWith(\"node:\")) {\n return { external: true };\n }\n\n try {\n return { path: import.meta.resolve(args.path), external: true };\n } catch {\n // Unresolvable from here — leave it bare and let Node try, which is\n // exactly the previous behaviour rather than a hard failure.\n return { external: true };\n }\n });\n },\n};\n\n/**\n * Bundle, write, and register the ESM loader hook.\n *\n * **Why bundle?**\n * `module.register()` runs the hook file in a fresh Node.js worker thread that\n * has no tsx hook of its own. The hook source is TypeScript, so we must produce\n * a plain ESM bundle before registering. esbuild inline-bundles all three hook\n * modules (`hook-thread`, `resolve-hook`, `load-hook`, `version-registry`)\n * into a single `.mjs` file written to `.warlock/`. External npm packages\n * (`esbuild`, `node:*`) are kept external — the hook thread can resolve those\n * from `node_modules` normally.\n *\n * **Why a file and not a `data:` URL?**\n * Some Node versions have issues resolving `import.meta.url` inside `data:`\n * modules. A real file under `.warlock/` is simpler and debuggable.\n *\n * **Timing**\n * Called from `FilesOrchestrator.init()` before any user `src/` module is\n * dynamically imported. `module.register()` takes effect for all subsequent\n * `import()` calls, which is exactly the window we need.\n *\n * @param transpile - Transpile-cache config to ship into the hook worker,\n * or `null` to keep the hook in tsx-passthrough mode.\n *\n * @returns The main-thread side of the MessageChannel. Callers post\n * `{ type: \"bump\", absolutePath }` messages on it to invalidate modules.\n *\n * @example\n * const port = await registerLoader(transpileInit);\n * // Later, when a file changes:\n * port.postMessage({ type: \"bump\", absolutePath: \"/abs/path/to/user.model.ts\" });\n */\nexport async function registerLoader(\n transpile: TranspileInit,\n): Promise<MessagePort> {\n const { port1, port2 } = new MessageChannel();\n\n // hook-thread is a sibling module, so it shares THIS file's extension:\n // `.ts` when core runs from source (tsx), `.mjs` when published.\n const selfPath = fileURLToPath(import.meta.url);\n const hookThreadPath = path.join(\n path.dirname(selfPath),\n `hook-thread${path.extname(selfPath)}`,\n );\n\n const bundleResult = await build({\n entryPoints: [hookThreadPath],\n bundle: true,\n format: \"esm\",\n write: false,\n platform: \"node\",\n target: \"node20\",\n // npm packages and Node built-ins stay external, but the plugin rewrites\n // each remaining npm specifier to an absolute path resolved from core's\n // own install — see `resolveExternalsFromCore` for why bare imports break\n // under pnpm's strict layout.\n packages: \"external\",\n plugins: [resolveExternalsFromCore],\n });\n\n const bundledCode = bundleResult.outputFiles[0].text;\n // Caller (filesOrchestrator.init) guarantees .warlock/ exists before this runs.\n const hookBundlePath = path.join(process.cwd(), \".warlock\", \"loader-hook.mjs\");\n await putFileAsync(hookBundlePath, bundledCode);\n\n const srcRoot = path.join(process.cwd(), \"src\");\n\n // No tsx registration: our hook owns resolution (own-resolver) and the\n // transpile of every `.ts`/`.tsx` (esbuild, in the load hook). The chain\n // is simply [our hook] → [Node default] for non-TS (npm/.js, node:).\n // In this monorepo tsx is still the *launcher* (`tsx start.ts`) so its\n // loader is present anyway, but it is never consulted for TypeScript —\n // our hook short-circuits first. A released `node bin/warlock.js` has no\n // tsx at all and relies entirely on this hook.\n\n register(pathToFileURL(hookBundlePath).href, import.meta.url, {\n data: { port: port2, srcRoot, transpile },\n transferList: [port2],\n });\n\n return port1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,2BAAmC;CAC9C,MAAM;CACN,MAAM,OAAO;EAGX,MAAM,UAAU,EAAE,QAAQ,SAAS,IAAG,SAAQ;GAG5C,IAAI,KAAK,SAAS,iBAAiB,KAAK,WAAW,KAAK,IAAI,GAC1D,OAAO;GAGT,IAAI,KAAK,KAAK,WAAW,OAAO,GAC9B,OAAO,EAAE,UAAU,KAAK;GAG1B,IAAI;IACF,OAAO;KAAE,MAAM,OAAO,KAAK,QAAQ,KAAK,IAAI;KAAG,UAAU;IAAK;GAChE,QAAQ;IAGN,OAAO,EAAE,UAAU,KAAK;GAC1B;EACF,CAAC;CACH;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,eACpB,WACsB;CACtB,MAAM,EAAE,OAAO,UAAU,IAAI,eAAe;CAI5C,MAAM,WAAW,cAAc,OAAO,KAAK,GAAG;CAqB9C,MAAM,eAAc,MAfO,MAAM;EAC/B,aAAa,CANQ,KAAK,KAC1B,KAAK,QAAQ,QAAQ,GACrB,cAAc,KAAK,QAAQ,QAAQ,GAIR,CAAC;EAC5B,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,UAAU;EACV,QAAQ;EAKR,UAAU;EACV,SAAS,CAAC,wBAAwB;CACpC,CAAC,EAE+B,CAAC,YAAY,EAAE,CAAC;CAEhD,MAAM,iBAAiB,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY,iBAAiB;CAC7E,MAAM,aAAa,gBAAgB,WAAW;CAE9C,MAAM,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,KAAK;CAU9C,SAAS,cAAc,cAAc,CAAC,CAAC,MAAM,OAAO,KAAK,KAAK;EAC5D,MAAM;GAAE,MAAM;GAAO;GAAS;EAAU;EACxC,cAAc,CAAC,KAAK;CACtB,CAAC;CAED,OAAO;AACT"}
package/llms-full.txt CHANGED
@@ -3217,6 +3217,16 @@ An explicit restart (`r`, `u`, a config change) is a *request*, not a crash, so
3217
3217
 
3218
3218
  Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warning telling you to restart yourself). The same warning is printed if a restart is declined or isn't possible, and any ordinary code files that shared the batch still hot-reload normally.
3219
3219
 
3220
+ ### The generated loader hook
3221
+
3222
+ On first boot `warlock dev` bundles its ESM loader hook and writes it to **your project's** `.warlock/loader-hook.mjs` — the hook runs in a fresh Node worker thread with no TypeScript loader of its own, so it has to be plain, pre-bundled ESM.
3223
+
3224
+ Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from *your* `node_modules`, and `esbuild` / `get-tsconfig` are core's dependencies, not yours.
3225
+
3226
+ :::note[Fixed in 4.9.2 — pnpm users]
3227
+ Before 4.9.2 those imports were left bare. npm and yarn hoist every transitive dependency into one flat tree, so they resolved by accident; pnpm's strict layout does not, and the dev server failed with `ERR_MODULE_NOT_FOUND: Cannot find package 'esbuild'`. The workaround was declaring `esbuild` and `get-tsconfig` in your own `package.json` — no longer needed, and you can drop them.
3228
+ :::
3229
+
3220
3230
  ### What it preloads
3221
3231
 
3222
3232
  ```ts
package/package.json CHANGED
@@ -36,13 +36,13 @@
36
36
  "@mongez/slug": "^1.0.7",
37
37
  "@mongez/supportive-is": "^2.1.3",
38
38
  "@mongez/time-wizard": "^1.0.6",
39
- "@warlock.js/auth": "4.9.1",
40
- "@warlock.js/cache": "4.9.1",
41
- "@warlock.js/cascade": "4.9.1",
42
- "@warlock.js/context": "4.9.1",
43
- "@warlock.js/logger": "4.9.1",
44
- "@warlock.js/seal": "4.9.1",
45
- "@warlock.js/fs": "4.9.1",
39
+ "@warlock.js/auth": "4.9.2",
40
+ "@warlock.js/cache": "4.9.2",
41
+ "@warlock.js/cascade": "4.9.2",
42
+ "@warlock.js/context": "4.9.2",
43
+ "@warlock.js/logger": "4.9.2",
44
+ "@warlock.js/seal": "4.9.2",
45
+ "@warlock.js/fs": "4.9.2",
46
46
  "chokidar": "^5.0.0",
47
47
  "dayjs": "^1.11.19",
48
48
  "es-module-lexer": "^2.0.0",
@@ -68,15 +68,15 @@
68
68
  "react": "^19.2.3",
69
69
  "react-dom": "^19.2.3",
70
70
  "@react-email/render": "^2.0.5",
71
- "@warlock.js/herald": "4.9.1",
72
- "@warlock.js/ai": "4.9.1",
73
- "@warlock.js/access": "4.9.1",
74
- "@warlock.js/notifications": "4.9.1"
71
+ "@warlock.js/herald": "4.9.2",
72
+ "@warlock.js/ai": "4.9.2",
73
+ "@warlock.js/access": "4.9.2",
74
+ "@warlock.js/notifications": "4.9.2"
75
75
  },
76
76
  "bin": {
77
77
  "warlock": "bin/warlock.js"
78
78
  },
79
- "version": "4.9.1",
79
+ "version": "4.9.2",
80
80
  "type": "module",
81
81
  "main": "./esm/index.mjs",
82
82
  "module": "./esm/index.mjs",
@@ -97,6 +97,16 @@ An explicit restart (`r`, `u`, a config change) is a *request*, not a crash, so
97
97
 
98
98
  Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warning telling you to restart yourself). The same warning is printed if a restart is declined or isn't possible, and any ordinary code files that shared the batch still hot-reload normally.
99
99
 
100
+ ### The generated loader hook
101
+
102
+ On first boot `warlock dev` bundles its ESM loader hook and writes it to **your project's** `.warlock/loader-hook.mjs` — the hook runs in a fresh Node worker thread with no TypeScript loader of its own, so it has to be plain, pre-bundled ESM.
103
+
104
+ Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from *your* `node_modules`, and `esbuild` / `get-tsconfig` are core's dependencies, not yours.
105
+
106
+ :::note[Fixed in 4.9.2 — pnpm users]
107
+ Before 4.9.2 those imports were left bare. npm and yarn hoist every transitive dependency into one flat tree, so they resolved by accident; pnpm's strict layout does not, and the dev server failed with `ERR_MODULE_NOT_FOUND: Cannot find package 'esbuild'`. The workaround was declaring `esbuild` and `get-tsconfig` in your own `package.json` — no longer needed, and you can drop them.
108
+ :::
109
+
100
110
  ### What it preloads
101
111
 
102
112
  ```ts