@warlock.js/core 5.13.0 → 5.14.0
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 +13 -0
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +2 -0
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +6 -3
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/errors/esbuild-binary-missing-error.mjs +20 -0
- package/esm/errors/esbuild-binary-missing-error.mjs.map +1 -0
- package/esm/generations/features/bull-board.feature.mjs +65 -0
- package/esm/generations/features/bull-board.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/queue.feature.mjs +4 -1
- package/esm/generations/features/queue.feature.mjs.map +1 -1
- package/esm/generations/features/shared/insert-connector-entry.mjs +68 -0
- package/esm/generations/features/shared/insert-connector-entry.mjs.map +1 -0
- package/esm/generations/features/shared/insert-queue-dashboard-block.mjs +55 -0
- package/esm/generations/features/shared/insert-queue-dashboard-block.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +4 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +4 -4
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/middleware/cache-response-middleware.d.mts +12 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +15 -3
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/production/esbuild-preflight.mjs +23 -13
- package/esm/production/esbuild-preflight.mjs.map +1 -1
- package/llms-full.txt +32 -3
- package/llms.txt +2 -2
- package/package.json +11 -11
- package/skills/run-app/SKILL.md +6 -2
- package/skills/use-middleware/SKILL.md +24 -0
- package/skills/write-cli-command/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.14.0 - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `warlock add bull-board` installs the Bull Board packages and writes a `dashboard` block to `src/config/queue.ts`; it adds the queue feature first when it is missing. The generated block enables the dashboard outside production only, since an unguarded dashboard refuses to mount in production.
|
|
14
|
+
- `middleware.cache({ tags })`: cached API responses can be tagged and are evicted by `cache.tags([...]).invalidate()`, like cached pages.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `warlock dev` checks esbuild's native binary before starting and fails with `EsbuildBinaryMissingError` naming the fix (`warlock build` already did).
|
|
19
|
+
- `devServer.timings`: the `watcher` phase always reported 0ms on Windows and Linux; it now measures the real settle time.
|
|
20
|
+
- `warlock add` writes connector arrays formatted like Prettier (`[queueConnector(), webConnector()]`), including empty and multi-line arrays.
|
|
21
|
+
|
|
9
22
|
## 5.13.0 - 2026-09-17
|
|
10
23
|
|
|
11
24
|
### Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/build.command.ts"],"sourcesContent":["import { buildAppProduction } from \"../../production/build-app-production\";\
|
|
1
|
+
{"version":3,"file":"build.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/build.command.ts"],"sourcesContent":["import { buildAppProduction } from \"../../production/build-app-production\";\nimport { command } from \"../../commands/cli-command\";\n\nexport const buildCommand = command({\n name: \"build\",\n description: \"Build the project for production\",\n action: buildAppProduction,\n preload: {\n warlockConfig: true,\n },\n});\n"],"mappings":";;;;AAGA,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,EACP,eAAe,KACjB;AACF,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { command } from "../../commands/cli-command.mjs";
|
|
2
2
|
import { isDevWorker, superviseDevServer } from "../../dev-server/supervisor.mjs";
|
|
3
3
|
import { displayStartupBanner } from "../cli-commands.utils.mjs";
|
|
4
|
+
import { assertEsbuildBinaryIsLinked } from "../../production/esbuild-preflight.mjs";
|
|
4
5
|
import { checkForFrameworkUpdate } from "../../dev-server/check-for-updates.mjs";
|
|
5
6
|
import { startDevelopmentServer } from "../../dev-server/start-development-server.mjs";
|
|
6
7
|
|
|
@@ -18,6 +19,7 @@ const devServerCommand = command({
|
|
|
18
19
|
connectors: true
|
|
19
20
|
},
|
|
20
21
|
preAction: async () => {
|
|
22
|
+
assertEsbuildBinaryIsLinked();
|
|
21
23
|
if (!isDevWorker()) return superviseDevServer();
|
|
22
24
|
await displayStartupBanner({ environment: "development" });
|
|
23
25
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-server.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/dev-server.command.ts"],"sourcesContent":["import { checkForFrameworkUpdate } from \"../../dev-server/check-for-updates\";\r\nimport { startDevelopmentServer } from \"../../dev-server/start-development-server\";\r\nimport { isDevWorker, superviseDevServer } from \"../../dev-server/supervisor\";\r\nimport { command } from \"../../commands/cli-command\";\r\nimport { displayStartupBanner } from \"../cli-commands.utils\";\r\n\r\nexport const devServerCommand = command({\r\n name: \"dev\",\r\n description: \"Start development server (HMR, type-gen, health checks)\",\r\n persistent: true,\r\n preload: {\r\n runtimeStrategy: \"development\",\r\n // Overrides whatever NODE_ENV the shell exported. A shell with\r\n // NODE_ENV=production made Vite's dep optimiser pre-bundle the PRODUCTION\r\n // build of React, which silently suppresses hydration-mismatch warnings —\r\n // the dev server was hiding the exact class of bug it exists to surface.\r\n // Set here, in preload, because preloaders run before the action, and the\r\n // action is what eventually reaches Vite's `createServer`.\r\n environment: \"development\",\r\n config: true, // load all config\r\n bootstrap: true,\r\n prestart: true, // load prestart file (if exists)\r\n // Only the Early lifecycle phase starts here; the Late phase\r\n // (http, socket) starts after app modules load — see\r\n // development-server.ts STEP 8.5.\r\n connectors: true,\r\n },\r\n preAction: async () => {\r\n // Two roles share this command. The first `warlock dev` becomes a thin\r\n // supervisor that owns the terminal and (re)spawns the real server; the\r\n // process it spawns carries WARLOCK_DEV_WORKER and falls through to the\r\n // server itself. See supervisor.ts for why the split exists.\r\n //\r\n // This branch has to sit in `preAction`, which runs BEFORE the command's\r\n // preloaders: the supervisor must never load config or start connectors,\r\n // or it would hold a second database connection open for the whole\r\n // session. `superviseDevServer()` never resolves, so nothing below it —\r\n // preload included — runs in the supervisor.\r\n if (!isDevWorker()) {\r\n return superviseDevServer();\r\n }\r\n\r\n await displayStartupBanner({ environment: \"development\" });\r\n },\r\n action: async (data) => {\r\n const devServer = await startDevelopmentServer({\r\n fresh: Boolean(data.options.fresh),\r\n // Pass `false` only when the CLI flag is explicitly set; `undefined`\r\n // lets `warlock.config.ts > devServer.*` defaults apply.\r\n generateTypings: data.options.skipTypings ? false : undefined,\r\n healthCheckers: data.options.skipHealth ? false : undefined,\r\n });\r\n\r\n // Fire-and-forget: once the server is ready, surface a one-line notice if\r\n // a newer @warlock.js/core has been published, and arm the `u` shortcut\r\n // that updates + restarts. Fully self-guarded — it never blocks startup\r\n // nor breaks dev if the registry is unreachable.\r\n void checkForFrameworkUpdate(devServer);\r\n },\r\n options: [\r\n {\r\n text: \"--fresh, -f\",\r\n description: \"Delete .warlock/manifest.json before start (force full re-parse from disk)\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-typings, -st\",\r\n description: \"Skip background type generation for this run\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-health, -sh\",\r\n description: \"Skip file health checkers for this run\",\r\n type: \"boolean\",\r\n },\r\n ],\r\n});\r\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-server.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/dev-server.command.ts"],"sourcesContent":["import { checkForFrameworkUpdate } from \"../../dev-server/check-for-updates\";\r\nimport { startDevelopmentServer } from \"../../dev-server/start-development-server\";\r\nimport { isDevWorker, superviseDevServer } from \"../../dev-server/supervisor\";\r\nimport { assertEsbuildBinaryIsLinked } from \"../../production/esbuild-preflight\";\r\nimport { command } from \"../../commands/cli-command\";\r\nimport { displayStartupBanner } from \"../cli-commands.utils\";\r\n\r\nexport const devServerCommand = command({\r\n name: \"dev\",\r\n description: \"Start development server (HMR, type-gen, health checks)\",\r\n persistent: true,\r\n preload: {\r\n runtimeStrategy: \"development\",\r\n // Overrides whatever NODE_ENV the shell exported. A shell with\r\n // NODE_ENV=production made Vite's dep optimiser pre-bundle the PRODUCTION\r\n // build of React, which silently suppresses hydration-mismatch warnings —\r\n // the dev server was hiding the exact class of bug it exists to surface.\r\n // Set here, in preload, because preloaders run before the action, and the\r\n // action is what eventually reaches Vite's `createServer`.\r\n environment: \"development\",\r\n config: true, // load all config\r\n bootstrap: true,\r\n prestart: true, // load prestart file (if exists)\r\n // Only the Early lifecycle phase starts here; the Late phase\r\n // (http, socket) starts after app modules load — see\r\n // development-server.ts STEP 8.5.\r\n connectors: true,\r\n },\r\n preAction: async () => {\r\n // Fail before either role (supervisor or worker) does any other work —\r\n // a missing/unlinked esbuild binary would otherwise surface much later\r\n // as an opaque low-level error from deep inside the transpile path.\r\n assertEsbuildBinaryIsLinked();\r\n\r\n // Two roles share this command. The first `warlock dev` becomes a thin\r\n // supervisor that owns the terminal and (re)spawns the real server; the\r\n // process it spawns carries WARLOCK_DEV_WORKER and falls through to the\r\n // server itself. See supervisor.ts for why the split exists.\r\n //\r\n // This branch has to sit in `preAction`, which runs BEFORE the command's\r\n // preloaders: the supervisor must never load config or start connectors,\r\n // or it would hold a second database connection open for the whole\r\n // session. `superviseDevServer()` never resolves, so nothing below it —\r\n // preload included — runs in the supervisor.\r\n if (!isDevWorker()) {\r\n return superviseDevServer();\r\n }\r\n\r\n await displayStartupBanner({ environment: \"development\" });\r\n },\r\n action: async (data) => {\r\n const devServer = await startDevelopmentServer({\r\n fresh: Boolean(data.options.fresh),\r\n // Pass `false` only when the CLI flag is explicitly set; `undefined`\r\n // lets `warlock.config.ts > devServer.*` defaults apply.\r\n generateTypings: data.options.skipTypings ? false : undefined,\r\n healthCheckers: data.options.skipHealth ? false : undefined,\r\n });\r\n\r\n // Fire-and-forget: once the server is ready, surface a one-line notice if\r\n // a newer @warlock.js/core has been published, and arm the `u` shortcut\r\n // that updates + restarts. Fully self-guarded — it never blocks startup\r\n // nor breaks dev if the registry is unreachable.\r\n void checkForFrameworkUpdate(devServer);\r\n },\r\n options: [\r\n {\r\n text: \"--fresh, -f\",\r\n description: \"Delete .warlock/manifest.json before start (force full re-parse from disk)\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-typings, -st\",\r\n description: \"Skip background type generation for this run\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-health, -sh\",\r\n description: \"Skip file health checkers for this run\",\r\n type: \"boolean\",\r\n },\r\n ],\r\n});\r\n"],"mappings":";;;;;;;;AAOA,MAAa,mBAAmB,QAAQ;CACtC,MAAM;CACN,aAAa;CACb,YAAY;CACZ,SAAS;EACP,iBAAiB;EAOjB,aAAa;EACb,QAAQ;EACR,WAAW;EACX,UAAU;EAIV,YAAY;CACd;CACA,WAAW,YAAY;EAIrB,4BAA4B;EAY5B,IAAI,CAAC,YAAY,GACf,OAAO,mBAAmB;EAG5B,MAAM,qBAAqB,EAAE,aAAa,cAAc,CAAC;CAC3D;CACA,QAAQ,OAAO,SAAS;EAatB,AAAK,wBAAwB,MAZL,uBAAuB;GAC7C,OAAO,QAAQ,KAAK,QAAQ,KAAK;GAGjC,iBAAiB,KAAK,QAAQ,cAAc,QAAQ;GACpD,gBAAgB,KAAK,QAAQ,aAAa,QAAQ;EACpD,CAAC,CAMqC;CACxC;CACA,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { Path } from "../utils/normalized-path.mjs";
|
|
|
2
2
|
import { rootPath, srcPath } from "../utils/paths.mjs";
|
|
3
3
|
import "../utils/index.mjs";
|
|
4
4
|
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
import { Random } from "@mongez/reinforcements";
|
|
6
7
|
import events from "@mongez/events";
|
|
7
8
|
import chokidar from "chokidar";
|
|
@@ -67,8 +68,10 @@ var FilesWatcher = class {
|
|
|
67
68
|
},
|
|
68
69
|
depth: 99
|
|
69
70
|
});
|
|
70
|
-
if (timingsEnabled) watcher.on("raw", (_event,
|
|
71
|
-
const
|
|
71
|
+
if (timingsEnabled) watcher.on("raw", (_event, evPath, details) => {
|
|
72
|
+
const watchedPath = details?.watchedPath;
|
|
73
|
+
const absolutePath = watchedPath ? path.resolve(watchedPath, evPath) : evPath;
|
|
74
|
+
const normalized = Path.normalize(absolutePath);
|
|
72
75
|
if (!this.rawEventTimestamps.has(normalized)) this.rawEventTimestamps.set(normalized, performance.now());
|
|
73
76
|
});
|
|
74
77
|
watcher.on("add", (filePath) => this.triggerEvent("add", filePath));
|
|
@@ -92,7 +95,7 @@ var FilesWatcher = class {
|
|
|
92
95
|
settleMs = performance.now() - rawEventAt;
|
|
93
96
|
this.rawEventTimestamps.delete(normalized);
|
|
94
97
|
}
|
|
95
|
-
events.trigger(`file-watcher.${this.id}.${event}`, normalized, error
|
|
98
|
+
events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);
|
|
96
99
|
}
|
|
97
100
|
/**
|
|
98
101
|
* On file change event
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-watcher.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n const envPaths = ENV_FILES.map((file) => rootPath(file));\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = userWatchConfig?.include || config?.include || [];\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = [\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ];\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, path) => {\n const normalized = Path.normalize(path);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error, settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":";;;;;;;;;;;;AAwCA,MAAM,kBAAkB;CAAC;CAAsB;CAAc;CAAkB;AAAY;;;;AAK3F,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAa,eAAb,MAA0B;;YAIX,OAAO,OAAO;4CAUW,IAAI,IAAoB;;;;;;CAM9D,MAAa,MAAM,QAAsB;EAEvC,MAAM,kBAAkB,MAAM,qBAAqB,QAAQ,WAAW;EACtE,MAAM,kBAAkB,iBAAiB;EAQzC,MAAM,YAAY;GAAC,GADF,UAAU,KAAK,SAAS,SAAS,IAAI,CACzB;GAAG,SAAS,mBAAmB;GAAG,QAAQ;EAAC;EACxE,MAAM,kBAAkB,iBAAiB,WAAW,QAAQ,WAAW,CAAC;EAExE,MAAM,QAAQ,CAAC,GAAG,WAAW,GAAG,eAAe,CAAC,CAAC,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC;EAGnF,MAAM,UAAU;GACd,GAAG;GACH,GAAI,iBAAiB,WAAW,CAAC;GACjC,GAAI,QAAQ,WAAW,CAAC;EAC1B;EAEA,MAAM,iBAAiB,iBAAiB,YAAY;EAEpD,MAAM,UAAU,SAAS,MAAM,OAAO;GACpC,eAAe;GACf;GACA,YAAY;GACZ,YAAY;GACZ,UAAU;GACV,gBAAgB;GAChB,kBAAkB;IAChB,oBAAoB;IACpB,cAAc;GAChB;GAEA,OAAO;EACT,CAAC;EAED,IAAI,gBACF,QAAQ,GAAG,QAAQ,QAAQ,SAAS;GAClC,MAAM,aAAa,KAAK,UAAU,IAAI;GACtC,IAAI,CAAC,KAAK,mBAAmB,IAAI,UAAU,GACzC,KAAK,mBAAmB,IAAI,YAAY,YAAY,IAAI,CAAC;EAE7D,CAAC;EAGH,QAAQ,GAAG,QAAQ,aAAa,KAAK,aAAa,OAAO,QAAQ,CAAC;EAClE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,WAAW,aAAa,KAAK,aAAa,UAAU,QAAQ,CAAC;EACxE,QAAQ,GAAG,cAAc,aAAa,KAAK,aAAa,aAAa,QAAQ,CAAC;EAM9E,QAAQ,GAAG,UAAU,YAAY;GAC/B,MAAM,QAAQ,MAAM;EACtB,CAAC;CACH;;;;;CAMA,AAAQ,aAAa,OAAyB,UAAkB,OAAe;EAC7E,MAAM,aAAa,KAAK,UAAU,QAAQ;EAC1C,MAAM,aAAa,KAAK,mBAAmB,IAAI,UAAU;EACzD,IAAI;EAEJ,IAAI,eAAe,QAAW;GAC5B,WAAW,YAAY,IAAI,IAAI;GAC/B,KAAK,mBAAmB,OAAO,UAAU;EAC3C;EAEA,OAAO,QAAQ,gBAAgB,KAAK,GAAG,GAAG,SAAS,YAAY,OAAO,QAAQ;CAChF;;;;CAKA,AAAO,aAAa,UAA8B;EAChD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,aAAa,UAA8B;EAChD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,UAAU,UAA2B;EAC1C,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,AAAO,YAAY,UAA6B;EAC9C,OAAO,KAAK,GAAG,SAAS,QAAQ;CAClC;;;;CAKA,AAAO,eAAe,UAA8B;EAClD,OAAO,KAAK,GAAG,UAAU,QAAQ;CACnC;;;;CAKA,AAAO,kBAAkB,UAAiC;EACxD,OAAO,KAAK,GAAG,aAAa,QAAQ;CACtC;;;;CAKA,AAAO,GAAG,OAAyB,UAA+B;EAChE,OAAO,OAAO,UAAU,gBAAgB,KAAK,GAAG,GAAG,SAAS,QAAQ;CACtE;AACF"}
|
|
1
|
+
{"version":3,"file":"files-watcher.mjs","names":["nodePath"],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport nodePath from \"node:path\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n const envPaths = ENV_FILES.map((file) => rootPath(file));\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = userWatchConfig?.include || config?.include || [];\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = [\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ];\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, evPath, details) => {\n // On the native (non-fsevents) handler chokidar uses on Windows/Linux,\n // `evPath` is only the changed entry's path *relative to the watched\n // directory* (see chokidar's `handler.js` `handleEvent`/`emitRaw`),\n // not the absolute path the later `add`/`change` event carries. Only\n // fsevents (macOS) hands back an already-absolute path. Reconstruct\n // the absolute path from `details.watchedPath` so the timestamp is\n // keyed the same way the stabilised event looks it up below —\n // otherwise the lookup always misses and the phase silently reads 0.\n const watchedPath = (details as { watchedPath?: string } | undefined)?.watchedPath;\n const absolutePath = watchedPath ? nodePath.resolve(watchedPath, evPath) : evPath;\n const normalized = Path.normalize(absolutePath);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n // `error` and `settleMs` share the same second-argument slot: an error\n // event's subscriber reads it as `Error`, every other event's subscriber\n // reads it as the watcher-settle duration (see the `On*Callback` types\n // above). Passing both positionally (`normalized, error, settleMs`) used\n // to leave `settleMs` in an unread third slot, so every change/add/delete\n // subscriber always received `undefined` in its `settleMs` parameter —\n // the watcher phase's timing was silently discarded here even when the\n // raw-event timestamp above resolved correctly.\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region ../core/src/errors/esbuild-binary-missing-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Raised when `warlock dev` / `warlock build` cannot use esbuild because its
|
|
4
|
+
* platform-native binary was never installed or linked.
|
|
5
|
+
*
|
|
6
|
+
* The message names what is missing and the exact fix — reinstall with the
|
|
7
|
+
* package manager without skipping install scripts — so the failure is
|
|
8
|
+
* actionable at the point it is thrown, instead of surfacing later as an
|
|
9
|
+
* opaque low-level error from deep inside the bundler.
|
|
10
|
+
*/
|
|
11
|
+
var EsbuildBinaryMissingError = class extends Error {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super("esbuild's native binary is not installed or linked for this platform, so `warlock dev` / `warlock build` cannot run.\n\nThis usually happens when a platform package such as `@esbuild/win32-x64` was never installed, or the package manager skipped esbuild's postinstall script that links it. Fix it with:\n\n pnpm approve-builds\n\nthen reinstall dependencies (do not skip install scripts), or reinstall with your package manager's equivalent of allowing build scripts.", options);
|
|
14
|
+
this.name = "EsbuildBinaryMissingError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { EsbuildBinaryMissingError };
|
|
20
|
+
//# sourceMappingURL=esbuild-binary-missing-error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"esbuild-binary-missing-error.mjs","names":[],"sources":["../../../../../../../core/src/errors/esbuild-binary-missing-error.ts"],"sourcesContent":["/**\n * Raised when `warlock dev` / `warlock build` cannot use esbuild because its\n * platform-native binary was never installed or linked.\n *\n * The message names what is missing and the exact fix — reinstall with the\n * package manager without skipping install scripts — so the failure is\n * actionable at the point it is thrown, instead of surfacing later as an\n * opaque low-level error from deep inside the bundler.\n */\nexport class EsbuildBinaryMissingError extends Error {\n public constructor(options?: { cause?: unknown }) {\n super(\n \"esbuild's native binary is not installed or linked for this platform, so \" +\n \"`warlock dev` / `warlock build` cannot run.\\n\\n\" +\n \"This usually happens when a platform package such as `@esbuild/win32-x64` \" +\n \"was never installed, or the package manager skipped esbuild's postinstall \" +\n \"script that links it. Fix it with:\\n\\n\" +\n \" pnpm approve-builds\\n\\n\" +\n \"then reinstall dependencies (do not skip install scripts), \" +\n \"or reinstall with your package manager's equivalent of allowing build scripts.\",\n options,\n );\n\n this.name = \"EsbuildBinaryMissingError\";\n }\n}\n"],"mappings":";;;;;;;;;;AASA,IAAa,4BAAb,cAA+C,MAAM;CACnD,AAAO,YAAY,SAA+B;EAChD,MACE,wdAQA,OACF;EAEA,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { insertQueueDashboardBlock } from "./shared/insert-queue-dashboard-block.mjs";
|
|
4
|
+
import { colors } from "@mongez/copper";
|
|
5
|
+
import { fileExistsAsync, getFileAsync, getJsonFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
6
|
+
|
|
7
|
+
//#region ../core/src/generations/features/bull-board.feature.ts
|
|
8
|
+
/**
|
|
9
|
+
* Defensive floor: `requires: ["queue"]` normally installs queue first, so
|
|
10
|
+
* this only fires when the dashboard step runs without it (e.g. called
|
|
11
|
+
* directly), and then fails loudly instead of writing a half-wired config.
|
|
12
|
+
*/
|
|
13
|
+
async function isQueueInstalled() {
|
|
14
|
+
const packageJson = await getJsonFileAsync(rootPath("package.json"));
|
|
15
|
+
return Boolean(packageJson.dependencies?.["@warlock.js/queue"] || packageJson.devDependencies?.["@warlock.js/queue"]);
|
|
16
|
+
}
|
|
17
|
+
/** Insert the `dashboard` block into `src/config/queue.ts`, without reformatting it. */
|
|
18
|
+
async function addDashboardConfigBlock() {
|
|
19
|
+
const configPath = srcPath("config/queue.ts");
|
|
20
|
+
if (!await fileExistsAsync(configPath)) {
|
|
21
|
+
console.log(`${colors.yellowBright("src/config/queue.ts")} not found — add this to your queue config yourself:\n dashboard: { enabled: true, path: "/admin/queues", middleware: [] },
|
|
22
|
+
`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const insertion = insertQueueDashboardBlock(await getFileAsync(configPath));
|
|
26
|
+
if (insertion.status === "already-present") {
|
|
27
|
+
console.log(`${colors.yellowBright("dashboard")} config already present in src/config/queue.ts, skipping...`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (insertion.status === "unrecognised") {
|
|
31
|
+
console.log(`${colors.yellowBright("!")} src/config/queue.ts has no recognisable ${colors.yellowBright("queueConfig")} object — add the dashboard block yourself:
|
|
32
|
+
dashboard: { enabled: true, path: "/admin/queues", middleware: [] },
|
|
33
|
+
`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await putFileAsync(configPath, insertion.next);
|
|
37
|
+
console.log(`${colors.green("✓")} Added dashboard config to src/config/queue.ts`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Wire the dashboard into the app: verify queue is installed, then add the
|
|
41
|
+
* `dashboard` block to its config.
|
|
42
|
+
*/
|
|
43
|
+
async function completeBullBoardInstallation(_options) {
|
|
44
|
+
if (!await isQueueInstalled()) {
|
|
45
|
+
console.log(`${colors.redBright("✗")} @warlock.js/queue is not installed — run ${colors.yellowBright("warlock add queue")} first, then ${colors.yellowBright("warlock add bull-board")}.`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await addDashboardConfigBlock();
|
|
50
|
+
console.log("\nNext: guard the dashboard with a middleware before it ships to production — an empty middleware list throws QueueDashboardUnguardedError at boot when NODE_ENV is \"production\".");
|
|
51
|
+
}
|
|
52
|
+
/** `warlock add bull-board` — a config-driven bull-board dashboard for @warlock.js/queue. */
|
|
53
|
+
const bullBoardFeature = {
|
|
54
|
+
description: "Installs @bull-board/api and @bull-board/fastify and adds a dashboard block to src/config/queue.ts. Adds the queue feature first when it is missing.",
|
|
55
|
+
requires: ["queue"],
|
|
56
|
+
dependencies: {
|
|
57
|
+
"@bull-board/api": "^9.10.1",
|
|
58
|
+
"@bull-board/fastify": "^9.10.1"
|
|
59
|
+
},
|
|
60
|
+
onExecuting: completeBullBoardInstallation
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { bullBoardFeature };
|
|
65
|
+
//# sourceMappingURL=bull-board.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bull-board.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/bull-board.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, getJsonFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { insertQueueDashboardBlock } from \"./shared/insert-queue-dashboard-block\";\nimport type { FeatureDefinition } from \"./types\";\n\n/** The parts of `package.json` this feature reads to check for its prerequisite. */\ntype ProjectPackageJson = {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n};\n\n/**\n * Defensive floor: `requires: [\"queue\"]` normally installs queue first, so\n * this only fires when the dashboard step runs without it (e.g. called\n * directly), and then fails loudly instead of writing a half-wired config.\n */\nasync function isQueueInstalled(): Promise<boolean> {\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\n\n return Boolean(\n packageJson.dependencies?.[\"@warlock.js/queue\"] ||\n packageJson.devDependencies?.[\"@warlock.js/queue\"],\n );\n}\n\n/** Insert the `dashboard` block into `src/config/queue.ts`, without reformatting it. */\nasync function addDashboardConfigBlock(): Promise<void> {\n const configPath = srcPath(\"config/queue.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"src/config/queue.ts\")} not found — add this to your queue config yourself:\\n` +\n ' dashboard: { enabled: true, path: \"/admin/queues\", middleware: [] },\\n',\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n const insertion = insertQueueDashboardBlock(current);\n\n if (insertion.status === \"already-present\") {\n console.log(\n `${colors.yellowBright(\"dashboard\")} config already present in src/config/queue.ts, skipping...`,\n );\n\n return;\n }\n\n if (insertion.status === \"unrecognised\") {\n console.log(\n `${colors.yellowBright(\"!\")} src/config/queue.ts has no recognisable ${colors.yellowBright(\"queueConfig\")} object — ` +\n \"add the dashboard block yourself:\\n\" +\n ' dashboard: { enabled: true, path: \"/admin/queues\", middleware: [] },\\n',\n );\n\n return;\n }\n\n await putFileAsync(configPath, insertion.next);\n console.log(`${colors.green(\"✓\")} Added dashboard config to src/config/queue.ts`);\n}\n\n/**\n * Wire the dashboard into the app: verify queue is installed, then add the\n * `dashboard` block to its config.\n */\nasync function completeBullBoardInstallation(_options: CommandActionData): Promise<void> {\n if (!(await isQueueInstalled())) {\n console.log(\n `${colors.redBright(\"✗\")} @warlock.js/queue is not installed — run ` +\n `${colors.yellowBright(\"warlock add queue\")} first, then ${colors.yellowBright(\"warlock add bull-board\")}.`,\n );\n process.exitCode = 1;\n\n return;\n }\n\n await addDashboardConfigBlock();\n\n console.log(\n \"\\nNext: guard the dashboard with a middleware before it ships to production — an empty \" +\n \"middleware list throws QueueDashboardUnguardedError at boot when NODE_ENV is \\\"production\\\".\",\n );\n}\n\n/** `warlock add bull-board` — a config-driven bull-board dashboard for @warlock.js/queue. */\nexport const bullBoardFeature: FeatureDefinition = {\n description:\n \"Installs @bull-board/api and @bull-board/fastify and adds a dashboard block to src/config/queue.ts. Adds the queue feature first when it is missing.\",\n requires: [\"queue\"],\n dependencies: {\n \"@bull-board/api\": \"^9.10.1\",\n \"@bull-board/fastify\": \"^9.10.1\",\n },\n onExecuting: completeBullBoardInstallation,\n};\n"],"mappings":";;;;;;;;;;;;AAkBA,eAAe,mBAAqC;CAClD,MAAM,cAAc,MAAM,iBAAqC,SAAS,cAAc,CAAC;CAEvF,OAAO,QACL,YAAY,eAAe,wBACzB,YAAY,kBAAkB,oBAClC;AACF;;AAGA,eAAe,0BAAyC;CACtD,MAAM,aAAa,QAAQ,iBAAiB;CAE5C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,qBAAqB,EAAE;CAEhD;EAEA;CACF;CAGA,MAAM,YAAY,0BAA0B,MADtB,aAAa,UAAU,CACM;CAEnD,IAAI,UAAU,WAAW,mBAAmB;EAC1C,QAAQ,IACN,GAAG,OAAO,aAAa,WAAW,EAAE,4DACtC;EAEA;CACF;CAEA,IAAI,UAAU,WAAW,gBAAgB;EACvC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,2CAA2C,OAAO,aAAa,aAAa,EAAE;;CAG5G;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,UAAU,IAAI;CAC7C,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,+CAA+C;AAClF;;;;;AAMA,eAAe,8BAA8B,UAA4C;CACvF,IAAI,CAAE,MAAM,iBAAiB,GAAI;EAC/B,QAAQ,IACN,GAAG,OAAO,UAAU,GAAG,EAAE,4CACpB,OAAO,aAAa,mBAAmB,EAAE,eAAe,OAAO,aAAa,wBAAwB,EAAE,EAC7G;EACA,QAAQ,WAAW;EAEnB;CACF;CAEA,MAAM,wBAAwB;CAE9B,QAAQ,IACN,qLAEF;AACF;;AAGA,MAAa,mBAAsC;CACjD,aACE;CACF,UAAU,CAAC,OAAO;CAClB,cAAc;EACZ,mBAAmB;EACnB,uBAAuB;CACzB;CACA,aAAa;AACf"}
|
|
@@ -10,6 +10,7 @@ import { aiWorkspaceFeature } from "./ai-workspace.feature.mjs";
|
|
|
10
10
|
import { aiFeature } from "./ai.feature.mjs";
|
|
11
11
|
import { authGoogleFeature } from "./auth-google.feature.mjs";
|
|
12
12
|
import { authPasskeysFeature } from "./auth-passkeys.feature.mjs";
|
|
13
|
+
import { bullBoardFeature } from "./bull-board.feature.mjs";
|
|
13
14
|
import { heraldFeature } from "./herald.feature.mjs";
|
|
14
15
|
import { imageFeature } from "./image.feature.mjs";
|
|
15
16
|
import { mailFeature } from "./mail.feature.mjs";
|
|
@@ -57,6 +58,7 @@ const featuresMap = {
|
|
|
57
58
|
shadcn: shadcnFeature,
|
|
58
59
|
herald: heraldFeature,
|
|
59
60
|
queue: queueFeature,
|
|
61
|
+
"bull-board": bullBoardFeature,
|
|
60
62
|
socket: socketFeature,
|
|
61
63
|
notifications: notificationsFeature,
|
|
62
64
|
access: accessFeature,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { bullBoardFeature } from \"./bull-board.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\r\n // needs queue already installed and configured, and a reader scanning\r\n // `--list` for job-queue features should meet the two together.\r\n \"bull-board\": bullBoardFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CACR,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rootPath } from "../../utils/paths.mjs";
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
4
|
+
import { insertConnectorEntry } from "./shared/insert-connector-entry.mjs";
|
|
4
5
|
import { colors } from "@mongez/copper";
|
|
5
6
|
import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
6
7
|
|
|
@@ -41,7 +42,9 @@ async function registerQueueConnector() {
|
|
|
41
42
|
}
|
|
42
43
|
const importLine = "import { queueConnector } from \"@warlock.js/queue\";";
|
|
43
44
|
let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
|
|
44
|
-
|
|
45
|
+
const insertion = insertConnectorEntry(next, "queueConnector()");
|
|
46
|
+
if (insertion.status === "already-present") return;
|
|
47
|
+
if (insertion.status === "added") next = insertion.next;
|
|
45
48
|
else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [queueConnector()],\n");
|
|
46
49
|
else {
|
|
47
50
|
console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [queueConnector()]\` yourself.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/queue.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../../utils\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst queueConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { QueueConfig } from \"@warlock.js/queue\";\n\n/** Durable job queue configuration. Redis is required by BullMQ. */\nconst queueConfig: QueueConfig = {\n connection: {\n host: env(\"REDIS_HOST\", \"127.0.0.1\"),\n port: env(\"REDIS_PORT\", 6379),\n },\n defaultJobOptions: {\n attempts: 3,\n backoff: { type: \"exponential\", delay: 1000 },\n },\n workers: {\n enabled: true,\n concurrency: 5,\n shutdownTimeout: 30_000,\n },\n};\n\nexport default queueConfig;\n`;\n\n/** Register the queue connector in the app-owned configuration without reformatting it. */\nasync function registerQueueConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { queueConnector } from \"@warlock.js/queue\";\\n` +\n ` export default defineConfig({ connectors: [queueConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"queueConnector\")) {\n console.log(`${colors.yellowBright(\"queueConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { queueConnector } from \"@warlock.js/queue\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n
|
|
1
|
+
{"version":3,"file":"queue.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/queue.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../../utils\";\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst queueConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { QueueConfig } from \"@warlock.js/queue\";\n\n/** Durable job queue configuration. Redis is required by BullMQ. */\nconst queueConfig: QueueConfig = {\n connection: {\n host: env(\"REDIS_HOST\", \"127.0.0.1\"),\n port: env(\"REDIS_PORT\", 6379),\n },\n defaultJobOptions: {\n attempts: 3,\n backoff: { type: \"exponential\", delay: 1000 },\n },\n workers: {\n enabled: true,\n concurrency: 5,\n shutdownTimeout: 30_000,\n },\n};\n\nexport default queueConfig;\n`;\n\n/** Register the queue connector in the app-owned configuration without reformatting it. */\nasync function registerQueueConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { queueConnector } from \"@warlock.js/queue\";\\n` +\n ` export default defineConfig({ connectors: [queueConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"queueConnector\")) {\n console.log(`${colors.yellowBright(\"queueConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { queueConnector } from \"@warlock.js/queue\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n const insertion = insertConnectorEntry(next, \"queueConnector()\");\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"added\") {\n next = insertion.next;\n } else if (next.includes(\"defineConfig({\")) {\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [queueConnector()],\\n\");\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\n \"add `connectors: [queueConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered queueConnector in warlock.config.ts`);\n console.log(\n \"Next: start Redis, then define jobs with defineJob() in an application module before boot.\",\n );\n}\n\n/** `warlock add queue` — durable BullMQ jobs backed by the app's Redis server. */\nexport const queueFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/queue — durable BullMQ jobs backed by Redis. Creates src/config/queue.ts and registers queueConnector() in warlock.config.ts.\",\n dependencies: {\n \"@warlock.js/queue\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: queueConfigStub,\n name: \"queue\",\n },\n onExecuting: registerQueueConnector,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;AAwBxB,eAAe,yBAAwC;CACrD,MAAM,aAAa,SAAS,mBAAmB;CAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,2JAG9C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,UAAU;CAE7C,IAAI,QAAQ,SAAS,gBAAgB,GAAG;EACtC,QAAQ,IAAI,GAAG,OAAO,aAAa,gBAAgB,EAAE,iCAAiC;EAEtF;CACF;CAEA,MAAM,aAAa;CACnB,IAAI,OAAO,QAAQ,SAAS,UAAU,IAAI,UAAU,GAAG,WAAW,IAAI;CAEtE,MAAM,YAAY,qBAAqB,MAAM,kBAAkB;CAE/D,IAAI,UAAU,WAAW,mBACvB;CAGF,IAAI,UAAU,WAAW,SACvB,OAAO,UAAU;MACZ,IAAI,KAAK,SAAS,gBAAgB,GACvC,OAAO,KAAK,QAAQ,kBAAkB,qDAAqD;MACtF;EACL,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,4FAE9C;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,IAAI;CACnC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,gDAAgD;CACjF,QAAQ,IACN,4FACF;AACF;;AAGA,MAAa,eAAkC;CAC7C,aACE;CACF,cAAc,EACZ,qBAAqB,0BACvB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region ../core/src/generations/features/shared/insert-connector-entry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Insert one connector call into the `connectors` array of `warlock.config.ts`
|
|
4
|
+
* SOURCE TEXT.
|
|
5
|
+
*
|
|
6
|
+
* **String surgery, never parse-and-print** — same rationale as
|
|
7
|
+
* `insertIncludeEntry`: `warlock.config.ts` is app-owned and may carry any
|
|
8
|
+
* formatting, and a parse-and-print would reformat parts this call never came
|
|
9
|
+
* to touch.
|
|
10
|
+
*
|
|
11
|
+
* Shared by every feature that registers a connector (`queue`, `web`, …) so the
|
|
12
|
+
* formatting rules below live in exactly one place:
|
|
13
|
+
*
|
|
14
|
+
* - An **empty** array (`[]`) gets the bare call: `[queueConnector()]`.
|
|
15
|
+
* - A **single-line, non-empty** array gets `entry, ` prepended right after
|
|
16
|
+
* `[`, so `[webConnector()]` becomes `[queueConnector(), webConnector()]` —
|
|
17
|
+
* WITH the space after the comma.
|
|
18
|
+
* - A **multi-line** array (one connector per line) gets the new entry on its
|
|
19
|
+
* own line, indented to match the array's existing entries, inserted before
|
|
20
|
+
* the first one. Whether the last existing entry already carries a trailing
|
|
21
|
+
* comma is left untouched — both are valid.
|
|
22
|
+
* - A connector already present anywhere in the source (by name, so a call
|
|
23
|
+
* already registered is found however it is formatted) is left unchanged.
|
|
24
|
+
*
|
|
25
|
+
* @param source The config file's current text.
|
|
26
|
+
* @param connectorCall The full call to insert, e.g. `"queueConnector()"`.
|
|
27
|
+
* @returns What happened, and the new text when there is any.
|
|
28
|
+
*/
|
|
29
|
+
function insertConnectorEntry(source, connectorCall) {
|
|
30
|
+
const connectorName = connectorCall.slice(0, connectorCall.indexOf("("));
|
|
31
|
+
if (new RegExp(`\\b${escapeForRegExp(connectorName)}\\s*\\(`).test(source)) return { status: "already-present" };
|
|
32
|
+
const arrayOpen = /connectors:\s*\[/.exec(source);
|
|
33
|
+
if (!arrayOpen) return { status: "unrecognised" };
|
|
34
|
+
const afterBracket = arrayOpen.index + arrayOpen[0].length;
|
|
35
|
+
const closeBracketOffset = source.slice(afterBracket).indexOf("]");
|
|
36
|
+
if (closeBracketOffset === -1) return { status: "unrecognised" };
|
|
37
|
+
const inner = source.slice(afterBracket, afterBracket + closeBracketOffset);
|
|
38
|
+
if (inner.trim() === "") return {
|
|
39
|
+
status: "added",
|
|
40
|
+
next: `${source.slice(0, afterBracket)}${connectorCall}${source.slice(afterBracket)}`
|
|
41
|
+
};
|
|
42
|
+
const multilineEntry = /\n([ \t]*)\S/.exec(inner);
|
|
43
|
+
if (multilineEntry) {
|
|
44
|
+
const indent = multilineEntry[1];
|
|
45
|
+
const insertAt = afterBracket + inner.indexOf("\n") + 1;
|
|
46
|
+
return {
|
|
47
|
+
status: "added",
|
|
48
|
+
next: `${source.slice(0, insertAt)}${indent}${connectorCall},\n${source.slice(insertAt)}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
status: "added",
|
|
53
|
+
next: `${source.slice(0, afterBracket)}${connectorCall}, ${source.slice(afterBracket)}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Escape a literal string for embedding in a regular expression.
|
|
58
|
+
*
|
|
59
|
+
* @param value The literal to escape.
|
|
60
|
+
* @returns The escaped literal.
|
|
61
|
+
*/
|
|
62
|
+
function escapeForRegExp(value) {
|
|
63
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { insertConnectorEntry };
|
|
68
|
+
//# sourceMappingURL=insert-connector-entry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insert-connector-entry.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/insert-connector-entry.ts"],"sourcesContent":["/** What {@link insertConnectorEntry} did, or could not do, to the source it was given. */\nexport type ConnectorInsertion =\n | { status: \"added\"; next: string }\n | { status: \"already-present\" }\n | { status: \"unrecognised\" };\n\n/**\n * Insert one connector call into the `connectors` array of `warlock.config.ts`\n * SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertIncludeEntry`: `warlock.config.ts` is app-owned and may carry any\n * formatting, and a parse-and-print would reformat parts this call never came\n * to touch.\n *\n * Shared by every feature that registers a connector (`queue`, `web`, …) so the\n * formatting rules below live in exactly one place:\n *\n * - An **empty** array (`[]`) gets the bare call: `[queueConnector()]`.\n * - A **single-line, non-empty** array gets `entry, ` prepended right after\n * `[`, so `[webConnector()]` becomes `[queueConnector(), webConnector()]` —\n * WITH the space after the comma.\n * - A **multi-line** array (one connector per line) gets the new entry on its\n * own line, indented to match the array's existing entries, inserted before\n * the first one. Whether the last existing entry already carries a trailing\n * comma is left untouched — both are valid.\n * - A connector already present anywhere in the source (by name, so a call\n * already registered is found however it is formatted) is left unchanged.\n *\n * @param source The config file's current text.\n * @param connectorCall The full call to insert, e.g. `\"queueConnector()\"`.\n * @returns What happened, and the new text when there is any.\n */\nexport function insertConnectorEntry(source: string, connectorCall: string): ConnectorInsertion {\n const connectorName = connectorCall.slice(0, connectorCall.indexOf(\"(\"));\n\n if (new RegExp(`\\\\b${escapeForRegExp(connectorName)}\\\\s*\\\\(`).test(source)) {\n return { status: \"already-present\" };\n }\n\n const arrayOpen = /connectors:\\s*\\[/.exec(source);\n\n if (!arrayOpen) {\n return { status: \"unrecognised\" };\n }\n\n const afterBracket = arrayOpen.index + arrayOpen[0].length;\n const closeBracketOffset = source.slice(afterBracket).indexOf(\"]\");\n\n if (closeBracketOffset === -1) {\n return { status: \"unrecognised\" };\n }\n\n const inner = source.slice(afterBracket, afterBracket + closeBracketOffset);\n\n if (inner.trim() === \"\") {\n return {\n status: \"added\",\n next: `${source.slice(0, afterBracket)}${connectorCall}${source.slice(afterBracket)}`,\n };\n }\n\n const multilineEntry = /\\n([ \\t]*)\\S/.exec(inner);\n\n if (multilineEntry) {\n const indent = multilineEntry[1];\n const insertAt = afterBracket + inner.indexOf(\"\\n\") + 1;\n\n return {\n status: \"added\",\n next:\n `${source.slice(0, insertAt)}${indent}${connectorCall},\\n` + `${source.slice(insertAt)}`,\n };\n }\n\n return {\n status: \"added\",\n next: `${source.slice(0, afterBracket)}${connectorCall}, ${source.slice(afterBracket)}`,\n };\n}\n\n/**\n * Escape a literal string for embedding in a regular expression.\n *\n * @param value The literal to escape.\n * @returns The escaped literal.\n */\nfunction escapeForRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,qBAAqB,QAAgB,eAA2C;CAC9F,MAAM,gBAAgB,cAAc,MAAM,GAAG,cAAc,QAAQ,GAAG,CAAC;CAEvE,IAAI,IAAI,OAAO,MAAM,gBAAgB,aAAa,EAAE,QAAQ,CAAC,CAAC,KAAK,MAAM,GACvE,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,YAAY,mBAAmB,KAAK,MAAM;CAEhD,IAAI,CAAC,WACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,MAAM,eAAe,UAAU,QAAQ,UAAU,EAAE,CAAC;CACpD,MAAM,qBAAqB,OAAO,MAAM,YAAY,CAAC,CAAC,QAAQ,GAAG;CAEjE,IAAI,uBAAuB,IACzB,OAAO,EAAE,QAAQ,eAAe;CAGlC,MAAM,QAAQ,OAAO,MAAM,cAAc,eAAe,kBAAkB;CAE1E,IAAI,MAAM,KAAK,MAAM,IACnB,OAAO;EACL,QAAQ;EACR,MAAM,GAAG,OAAO,MAAM,GAAG,YAAY,IAAI,gBAAgB,OAAO,MAAM,YAAY;CACpF;CAGF,MAAM,iBAAiB,eAAe,KAAK,KAAK;CAEhD,IAAI,gBAAgB;EAClB,MAAM,SAAS,eAAe;EAC9B,MAAM,WAAW,eAAe,MAAM,QAAQ,IAAI,IAAI;EAEtD,OAAO;GACL,QAAQ;GACR,MACE,GAAG,OAAO,MAAM,GAAG,QAAQ,IAAI,SAAS,cAAc,KAAU,OAAO,MAAM,QAAQ;EACzF;CACF;CAEA,OAAO;EACL,QAAQ;EACR,MAAM,GAAG,OAAO,MAAM,GAAG,YAAY,IAAI,cAAc,IAAI,OAAO,MAAM,YAAY;CACtF;AACF;;;;;;;AAQA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region ../core/src/generations/features/shared/insert-queue-dashboard-block.ts
|
|
2
|
+
/** The property this module inserts into `src/config/queue.ts`'s `queueConfig` object. */
|
|
3
|
+
/**
|
|
4
|
+
* Enabled outside production only, because the dashboard can retry and delete
|
|
5
|
+
* jobs: `queueConnector()` refuses to mount it in production with an empty
|
|
6
|
+
* `middleware` list. A flat `enabled: true` would therefore stop a freshly
|
|
7
|
+
* generated app from starting in production at all — add a guard middleware,
|
|
8
|
+
* then enable it everywhere.
|
|
9
|
+
*/
|
|
10
|
+
const DASHBOARD_BLOCK = " // Add a guard middleware, then enable this in production too.\n dashboard: { enabled: process.env.NODE_ENV !== \"production\", path: \"/admin/queues\", middleware: [] },\n";
|
|
11
|
+
/**
|
|
12
|
+
* Insert the `dashboard` property into the `queueConfig` object literal of
|
|
13
|
+
* `src/config/queue.ts` SOURCE TEXT.
|
|
14
|
+
*
|
|
15
|
+
* **String surgery, never parse-and-print** — same rationale as
|
|
16
|
+
* `insertConnectorEntry`: the config is app-owned and may carry formatting or
|
|
17
|
+
* comments a parse-and-print would discard.
|
|
18
|
+
*
|
|
19
|
+
* A `dashboard` property already present anywhere in the source (by key, so
|
|
20
|
+
* one a human has since hand-edited is still found) is left unchanged — that
|
|
21
|
+
* is what makes a second `warlock add bull-board` a no-op instead of a
|
|
22
|
+
* duplicate block.
|
|
23
|
+
*
|
|
24
|
+
* @param source The config file's current text.
|
|
25
|
+
* @returns What happened, and the new text when there is any.
|
|
26
|
+
*/
|
|
27
|
+
function insertQueueDashboardBlock(source) {
|
|
28
|
+
if (/\bdashboard\s*:/.test(source)) return { status: "already-present" };
|
|
29
|
+
const declaration = /const\s+queueConfig\s*:\s*QueueConfig\s*=\s*\{/.exec(source);
|
|
30
|
+
if (!declaration) return { status: "unrecognised" };
|
|
31
|
+
const closingBraceIndex = findMatchingBraceIndex(source, declaration.index + declaration[0].length);
|
|
32
|
+
if (closingBraceIndex === -1) return { status: "unrecognised" };
|
|
33
|
+
return {
|
|
34
|
+
status: "added",
|
|
35
|
+
next: `${source.slice(0, closingBraceIndex)}${DASHBOARD_BLOCK}${source.slice(closingBraceIndex)}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find the index of the `}` that closes the object literal whose `{` sits
|
|
40
|
+
* right before `bodyStart`, accounting for nested `{ }` pairs (`connection:
|
|
41
|
+
* {...}`, `workers: {...}`, …).
|
|
42
|
+
*/
|
|
43
|
+
function findMatchingBraceIndex(source, bodyStart) {
|
|
44
|
+
let depth = 1;
|
|
45
|
+
for (let index = bodyStart; index < source.length; index++) if (source[index] === "{") depth++;
|
|
46
|
+
else if (source[index] === "}") {
|
|
47
|
+
depth--;
|
|
48
|
+
if (depth === 0) return index;
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { insertQueueDashboardBlock };
|
|
55
|
+
//# sourceMappingURL=insert-queue-dashboard-block.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"insert-queue-dashboard-block.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/insert-queue-dashboard-block.ts"],"sourcesContent":["/** What {@link insertQueueDashboardBlock} did, or could not do, to the source it was given. */\nexport type DashboardBlockInsertion =\n | { status: \"added\"; next: string }\n | { status: \"already-present\" }\n | { status: \"unrecognised\" };\n\n/** The property this module inserts into `src/config/queue.ts`'s `queueConfig` object. */\n/**\n * Enabled outside production only, because the dashboard can retry and delete\n * jobs: `queueConnector()` refuses to mount it in production with an empty\n * `middleware` list. A flat `enabled: true` would therefore stop a freshly\n * generated app from starting in production at all — add a guard middleware,\n * then enable it everywhere.\n */\nconst DASHBOARD_BLOCK =\n \" // Add a guard middleware, then enable this in production too.\\n\" +\n ' dashboard: { enabled: process.env.NODE_ENV !== \"production\", path: \"/admin/queues\", middleware: [] },\\n';\n\n/**\n * Insert the `dashboard` property into the `queueConfig` object literal of\n * `src/config/queue.ts` SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertConnectorEntry`: the config is app-owned and may carry formatting or\n * comments a parse-and-print would discard.\n *\n * A `dashboard` property already present anywhere in the source (by key, so\n * one a human has since hand-edited is still found) is left unchanged — that\n * is what makes a second `warlock add bull-board` a no-op instead of a\n * duplicate block.\n *\n * @param source The config file's current text.\n * @returns What happened, and the new text when there is any.\n */\nexport function insertQueueDashboardBlock(source: string): DashboardBlockInsertion {\n if (/\\bdashboard\\s*:/.test(source)) {\n return { status: \"already-present\" };\n }\n\n const declaration = /const\\s+queueConfig\\s*:\\s*QueueConfig\\s*=\\s*\\{/.exec(source);\n\n if (!declaration) {\n return { status: \"unrecognised\" };\n }\n\n const bodyStart = declaration.index + declaration[0].length;\n const closingBraceIndex = findMatchingBraceIndex(source, bodyStart);\n\n if (closingBraceIndex === -1) {\n return { status: \"unrecognised\" };\n }\n\n return {\n status: \"added\",\n next: `${source.slice(0, closingBraceIndex)}${DASHBOARD_BLOCK}${source.slice(closingBraceIndex)}`,\n };\n}\n\n/**\n * Find the index of the `}` that closes the object literal whose `{` sits\n * right before `bodyStart`, accounting for nested `{ }` pairs (`connection:\n * {...}`, `workers: {...}`, …).\n */\nfunction findMatchingBraceIndex(source: string, bodyStart: number): number {\n let depth = 1;\n\n for (let index = bodyStart; index < source.length; index++) {\n if (source[index] === \"{\") {\n depth++;\n } else if (source[index] === \"}\") {\n depth--;\n\n if (depth === 0) {\n return index;\n }\n }\n }\n\n return -1;\n}\n"],"mappings":";;;;;;;;;AAcA,MAAM,kBACJ;;;;;;;;;;;;;;;;;AAmBF,SAAgB,0BAA0B,QAAyC;CACjF,IAAI,kBAAkB,KAAK,MAAM,GAC/B,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,cAAc,iDAAiD,KAAK,MAAM;CAEhF,IAAI,CAAC,aACH,OAAO,EAAE,QAAQ,eAAe;CAIlC,MAAM,oBAAoB,uBAAuB,QAD/B,YAAY,QAAQ,YAAY,EAAE,CAAC,MACa;CAElE,IAAI,sBAAsB,IACxB,OAAO,EAAE,QAAQ,eAAe;CAGlC,OAAO;EACL,QAAQ;EACR,MAAM,GAAG,OAAO,MAAM,GAAG,iBAAiB,IAAI,kBAAkB,OAAO,MAAM,iBAAiB;CAChG;AACF;;;;;;AAOA,SAAS,uBAAuB,QAAgB,WAA2B;CACzE,IAAI,QAAQ;CAEZ,KAAK,IAAI,QAAQ,WAAW,QAAQ,OAAO,QAAQ,SACjD,IAAI,OAAO,WAAW,KACpB;MACK,IAAI,OAAO,WAAW,KAAK;EAChC;EAEA,IAAI,UAAU,GACZ,OAAO;CAEX;CAGF,OAAO;AACT"}
|
|
@@ -2,6 +2,7 @@ import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeRegisterStub, webRootStub } from "../stubs.mjs";
|
|
4
4
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
5
|
+
import { insertConnectorEntry } from "./shared/insert-connector-entry.mjs";
|
|
5
6
|
import { relocateConflictingHomeRoute } from "./shared/relocate-conflicting-home-route.mjs";
|
|
6
7
|
import { resolveContactScaffold } from "./shared/resolve-contact-scaffold.mjs";
|
|
7
8
|
import { colors } from "@mongez/copper";
|
|
@@ -39,7 +40,9 @@ async function registerWebConnector() {
|
|
|
39
40
|
}
|
|
40
41
|
const importLine = "import { webConnector } from \"@warlock.js/web/connector\";";
|
|
41
42
|
let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
|
|
42
|
-
|
|
43
|
+
const insertion = insertConnectorEntry(next, "webConnector()");
|
|
44
|
+
if (insertion.status === "already-present") return;
|
|
45
|
+
if (insertion.status === "added") next = insertion.next;
|
|
43
46
|
else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [webConnector()],");
|
|
44
47
|
else {
|
|
45
48
|
console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [webConnector()]\` yourself.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb =\r\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n const insertion = insertConnectorEntry(next, \"webConnector()\");\r\n\r\n if (insertion.status === \"already-present\") {\r\n return;\r\n }\r\n\r\n if (insertion.status === \"added\") {\r\n next = insertion.next;\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb =\r\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":"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"}
|
|
@@ -618,16 +618,16 @@ export default function App({ children }: AppProps) {
|
|
|
618
618
|
{/*
|
|
619
619
|
REQUIRED — this is the hydration mount point, not a styling wrapper.
|
|
620
620
|
|
|
621
|
-
The browser runtime looks up \`#
|
|
621
|
+
The browser runtime looks up \`#vessel\` and hydrates that element only.
|
|
622
622
|
Remove this div, or rename the id, and the page still renders from the
|
|
623
623
|
server but never becomes interactive: the runtime throws in the console
|
|
624
624
|
and nothing on screen changes.
|
|
625
625
|
|
|
626
626
|
Wrap it in your own markup freely, and put anything that must live
|
|
627
627
|
outside the hydrated tree (a static footer, a portal target) outside
|
|
628
|
-
it — just keep an element with \`id="
|
|
628
|
+
it — just keep an element with \`id="vessel"\` around {children}.
|
|
629
629
|
*/}
|
|
630
|
-
<div id="
|
|
630
|
+
<div id="vessel">{children}</div>
|
|
631
631
|
{/*
|
|
632
632
|
The hydration payload and module tags. Written explicitly because
|
|
633
633
|
placement occasionally matters — a CSP nonce, or ordering against
|
|
@@ -769,7 +769,7 @@ function TextInput({ label, ...controlProps }: FormControlProps & { label: strin
|
|
|
769
769
|
*/
|
|
770
770
|
export default function HomePage(_props: PageProps) {
|
|
771
771
|
// Live state. If the button below does nothing, the page rendered on the
|
|
772
|
-
// server but never hydrated — the runtime never mounted at \`#
|
|
772
|
+
// server but never hydrated — the runtime never mounted at \`#vessel\`. This is
|
|
773
773
|
// deliberately here so that failure is impossible to miss.
|
|
774
774
|
const [count, setCount] = useState(0);
|
|
775
775
|
const [locale, setLocale] = useState<"en" | "ar">("en");
|