@warlock.js/core 5.13.0 → 5.15.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 +24 -0
- package/esm/application/application-config-types.d.mts +9 -0
- package/esm/application/application-config-types.d.mts.map +1 -1
- package/esm/application/index.d.mts +2 -1
- package/esm/application/index.mjs +1 -0
- package/esm/application/public-url.d.mts +20 -0
- package/esm/application/public-url.d.mts.map +1 -0
- package/esm/application/public-url.mjs +26 -0
- package/esm/application/public-url.mjs.map +1 -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 +4 -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/sitemap.feature.mjs +74 -0
- package/esm/generations/features/sitemap.feature.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/errors/errors.d.mts +16 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +19 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -1
- package/esm/http/index.mjs +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/http/request.d.mts +8 -0
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +13 -1
- package/esm/http/request.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +3 -2
- package/esm/production/esbuild-preflight.mjs +23 -13
- package/esm/production/esbuild-preflight.mjs.map +1 -1
- package/llms-full.txt +82 -6
- package/llms.txt +4 -4
- package/package.json +11 -11
- package/skills/configure-app/SKILL.md +29 -2
- package/skills/run-app/SKILL.md +6 -2
- package/skills/send-response/SKILL.md +15 -1
- package/skills/use-localization/SKILL.md +6 -0
- package/skills/use-middleware/SKILL.md +24 -0
- package/skills/write-cli-command/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ 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.15.0 - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `warlock add sitemap` installs `@warlock.js/sitemap`, writes `src/config/sitemap.ts`, and registers `sitemapConnector()` in `warlock.config.ts`. The generated config ships **disabled**: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
|
|
14
|
+
- `app.publicUrl` config key, with a `PUBLIC_APP_URL` environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **BREAKING (fail-loud):** `request.cookie(name)` and `request.hasCookie(name)` now throw `CookieJarUnavailableError` when `@fastify/cookie` is not registered on the Fastify instance, instead of returning `undefined` / `false`. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under `authMiddleware([], "cookie:token")` it surfaced as a permanent, unexplained 401. Apps built on `createHttpApplication` are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. `request.cookies` stays lenient and still returns `{}`, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
|
|
19
|
+
|
|
20
|
+
## 5.14.0 - 2026-09-17
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `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.
|
|
25
|
+
- `middleware.cache({ tags })`: cached API responses can be tagged and are evicted by `cache.tags([...]).invalidate()`, like cached pages.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `warlock dev` checks esbuild's native binary before starting and fails with `EsbuildBinaryMissingError` naming the fix (`warlock build` already did).
|
|
30
|
+
- `devServer.timings`: the `watcher` phase always reported 0ms on Windows and Linux; it now measures the real settle time.
|
|
31
|
+
- `warlock add` writes connector arrays formatted like Prettier (`[queueConnector(), webConnector()]`), including empty and multi-line arrays.
|
|
32
|
+
|
|
9
33
|
## 5.13.0 - 2026-09-17
|
|
10
34
|
|
|
11
35
|
### Added
|
|
@@ -16,6 +16,15 @@ type AppConfigurations = {
|
|
|
16
16
|
* @default localhost:
|
|
17
17
|
*/
|
|
18
18
|
baseUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The application's public origin — the absolute URL other consumers (the
|
|
21
|
+
* sitemap route, canonical links, OG tags, absolute URLs in mail) build
|
|
22
|
+
* links against. Optional in general; falls back to the `PUBLIC_APP_URL`
|
|
23
|
+
* env var (see {@link getPublicUrl}). A consumer that requires it (e.g. the
|
|
24
|
+
* sitemap route) fails boot loudly, naming both, when neither is set — it
|
|
25
|
+
* never falls back to a request-derived origin.
|
|
26
|
+
*/
|
|
27
|
+
publicUrl?: string;
|
|
19
28
|
/**
|
|
20
29
|
* Application timezone
|
|
21
30
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;
|
|
1
|
+
{"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;EAuBW;AAAA;;;;EAjBX,OAAA;;;;;;;;;EASA,SAAA;;;;EAIA,QAAA;;;;EAIA,WAAA;AAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { app } from "./app.mjs";
|
|
2
2
|
import { Application, BootContext, BootListener, BootValidator, ShutdownListener } from "./application.mjs";
|
|
3
3
|
import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BootSignal, BootSignalType, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
|
|
4
|
-
import { AppConfigurations } from "./application-config-types.mjs";
|
|
4
|
+
import { AppConfigurations } from "./application-config-types.mjs";
|
|
5
|
+
import { getPublicUrl } from "./public-url.mjs";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region ../core/src/application/public-url.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The application's public origin — the ONE absolute-URL source every
|
|
4
|
+
* consumer that needs one (the sitemap route, canonical links, OG tags,
|
|
5
|
+
* absolute URLs in mail) reads instead of each keeping its own copy.
|
|
6
|
+
*
|
|
7
|
+
* `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
|
|
8
|
+
* does not fall back further to a request-derived origin: a sitemap (or any
|
|
9
|
+
* other absolute URL) served from the wrong host is worse than a boot that
|
|
10
|
+
* refuses to start, because nothing downstream ever tells you it was wrong.
|
|
11
|
+
*
|
|
12
|
+
* Optional in general — most apps have no consumer that needs it yet.
|
|
13
|
+
* Returns `undefined` rather than throwing; a consumer that requires the
|
|
14
|
+
* value (e.g. the sitemap route, at boot) is responsible for failing loudly
|
|
15
|
+
* itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
|
|
16
|
+
*/
|
|
17
|
+
declare function getPublicUrl(): string | undefined;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { getPublicUrl };
|
|
20
|
+
//# sourceMappingURL=public-url.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-url.d.mts","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"mappings":";;AAiBA;;;;AAA4B;;;;;;;;;;iBAAZ,YAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { config } from "../config/config-getter.mjs";
|
|
2
|
+
import "../config/index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/application/public-url.ts
|
|
5
|
+
/**
|
|
6
|
+
* The application's public origin — the ONE absolute-URL source every
|
|
7
|
+
* consumer that needs one (the sitemap route, canonical links, OG tags,
|
|
8
|
+
* absolute URLs in mail) reads instead of each keeping its own copy.
|
|
9
|
+
*
|
|
10
|
+
* `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
|
|
11
|
+
* does not fall back further to a request-derived origin: a sitemap (or any
|
|
12
|
+
* other absolute URL) served from the wrong host is worse than a boot that
|
|
13
|
+
* refuses to start, because nothing downstream ever tells you it was wrong.
|
|
14
|
+
*
|
|
15
|
+
* Optional in general — most apps have no consumer that needs it yet.
|
|
16
|
+
* Returns `undefined` rather than throwing; a consumer that requires the
|
|
17
|
+
* value (e.g. the sitemap route, at boot) is responsible for failing loudly
|
|
18
|
+
* itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
|
|
19
|
+
*/
|
|
20
|
+
function getPublicUrl() {
|
|
21
|
+
return config.key("app.publicUrl") || process.env.PUBLIC_APP_URL || void 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { getPublicUrl };
|
|
26
|
+
//# sourceMappingURL=public-url.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-url.mjs","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"sourcesContent":["import { config } from \"../config\";\n\n/**\n * The application's public origin — the ONE absolute-URL source every\n * consumer that needs one (the sitemap route, canonical links, OG tags,\n * absolute URLs in mail) reads instead of each keeping its own copy.\n *\n * `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately\n * does not fall back further to a request-derived origin: a sitemap (or any\n * other absolute URL) served from the wrong host is worse than a boot that\n * refuses to start, because nothing downstream ever tells you it was wrong.\n *\n * Optional in general — most apps have no consumer that needs it yet.\n * Returns `undefined` rather than throwing; a consumer that requires the\n * value (e.g. the sitemap route, at boot) is responsible for failing loudly\n * itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.\n */\nexport function getPublicUrl(): string | undefined {\n return config.key<string | undefined>(\"app.publicUrl\") || process.env.PUBLIC_APP_URL || undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAmC;CACjD,OAAO,OAAO,IAAwB,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAC1F"}
|
|
@@ -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":";;;;;;;;;;;;;AAyCA,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,QAAQ,YAAY;GAS7C,MAAM,cAAe,SAAkD;GACvE,MAAM,eAAe,cAAcA,KAAS,QAAQ,aAAa,MAAM,IAAI;GAC3E,MAAM,aAAa,KAAK,UAAU,YAAY;GAC9C,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;EAUA,OAAO,QAAQ,gBAAgB,KAAK,GAAG,GAAG,SAAS,YAAY,SAAS,QAAQ;CAClF;;;;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"}
|
|
@@ -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";
|
|
@@ -25,6 +26,7 @@ import { s3Feature } from "./s3.feature.mjs";
|
|
|
25
26
|
import { schedulerFeature } from "./scheduler.feature.mjs";
|
|
26
27
|
import { sesFeature } from "./ses.feature.mjs";
|
|
27
28
|
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
29
|
+
import { sitemapFeature } from "./sitemap.feature.mjs";
|
|
28
30
|
import { socketFeature } from "./socket.feature.mjs";
|
|
29
31
|
import { tailwindFeature } from "./tailwind.feature.mjs";
|
|
30
32
|
import { testFeature } from "./test.feature.mjs";
|
|
@@ -55,8 +57,10 @@ const featuresMap = {
|
|
|
55
57
|
web: webFeature,
|
|
56
58
|
tailwind: tailwindFeature,
|
|
57
59
|
shadcn: shadcnFeature,
|
|
60
|
+
sitemap: sitemapFeature,
|
|
58
61
|
herald: heraldFeature,
|
|
59
62
|
queue: queueFeature,
|
|
63
|
+
"bull-board": bullBoardFeature,
|
|
60
64
|
socket: socketFeature,
|
|
61
65
|
notifications: notificationsFeature,
|
|
62
66
|
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 { sitemapFeature } from \"./sitemap.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 // Directly after the web/tailwind/shadcn stack, for the same reason: it\r\n // `requires` web (it reads the page registry `listRoutablePages()`\r\n // exposes) and a reader scanning `--list` for the page stack should meet it\r\n // there rather than filed between queues and sockets.\r\n sitemap: sitemapFeature,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,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;CAKR,SAAS;CACT,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
|