@warlock.js/core 5.12.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +86 -54
  2. package/esm/cli/commands/build.command.mjs.map +1 -1
  3. package/esm/cli/commands/dev-server.command.mjs +2 -0
  4. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  5. package/esm/database/utils.d.mts +5 -1
  6. package/esm/database/utils.d.mts.map +1 -1
  7. package/esm/database/utils.mjs +7 -3
  8. package/esm/database/utils.mjs.map +1 -1
  9. package/esm/dev-server/file-event-handler.mjs +23 -5
  10. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  11. package/esm/dev-server/files-watcher.mjs +6 -3
  12. package/esm/dev-server/files-watcher.mjs.map +1 -1
  13. package/esm/dev-server/translation-type-generator.mjs +28 -0
  14. package/esm/dev-server/translation-type-generator.mjs.map +1 -0
  15. package/esm/dev-server/tsconfig-manager.mjs +1 -0
  16. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  17. package/esm/dev-server/type-generator.mjs +41 -5
  18. package/esm/dev-server/type-generator.mjs.map +1 -1
  19. package/esm/encryption/index.mjs +1 -1
  20. package/esm/errors/esbuild-binary-missing-error.mjs +20 -0
  21. package/esm/errors/esbuild-binary-missing-error.mjs.map +1 -0
  22. package/esm/generations/features/auth-google.feature.mjs +18 -0
  23. package/esm/generations/features/auth-google.feature.mjs.map +1 -0
  24. package/esm/generations/features/auth-passkeys.feature.mjs +19 -0
  25. package/esm/generations/features/auth-passkeys.feature.mjs.map +1 -0
  26. package/esm/generations/features/bull-board.feature.mjs +65 -0
  27. package/esm/generations/features/bull-board.feature.mjs.map +1 -0
  28. package/esm/generations/features/index.mjs +8 -0
  29. package/esm/generations/features/index.mjs.map +1 -1
  30. package/esm/generations/features/queue.feature.mjs +70 -0
  31. package/esm/generations/features/queue.feature.mjs.map +1 -0
  32. package/esm/generations/features/shared/insert-connector-entry.mjs +68 -0
  33. package/esm/generations/features/shared/insert-connector-entry.mjs.map +1 -0
  34. package/esm/generations/features/shared/insert-queue-dashboard-block.mjs +55 -0
  35. package/esm/generations/features/shared/insert-queue-dashboard-block.mjs.map +1 -0
  36. package/esm/generations/features/web.feature.mjs +4 -1
  37. package/esm/generations/features/web.feature.mjs.map +1 -1
  38. package/esm/generations/stubs.mjs +4 -4
  39. package/esm/generations/stubs.mjs.map +1 -1
  40. package/esm/http/middleware/cache-response-middleware.d.mts +12 -0
  41. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  42. package/esm/http/middleware/cache-response-middleware.mjs +15 -3
  43. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  44. package/esm/index.mjs +1 -1
  45. package/esm/production/esbuild-preflight.mjs +23 -13
  46. package/esm/production/esbuild-preflight.mjs.map +1 -1
  47. package/llms-full.txt +56 -24
  48. package/llms.txt +2 -2
  49. package/package.json +11 -12
  50. package/skills/run-app/SKILL.md +6 -2
  51. package/skills/use-localization/SKILL.md +24 -21
  52. package/skills/use-middleware/SKILL.md +24 -0
  53. package/skills/write-cli-command/SKILL.md +2 -1
@@ -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"}
@@ -8,6 +8,9 @@ import { aiPanopticFeature } from "./ai-panoptic.feature.mjs";
8
8
  import { aiToolsFeature } from "./ai-tools.feature.mjs";
9
9
  import { aiWorkspaceFeature } from "./ai-workspace.feature.mjs";
10
10
  import { aiFeature } from "./ai.feature.mjs";
11
+ import { authGoogleFeature } from "./auth-google.feature.mjs";
12
+ import { authPasskeysFeature } from "./auth-passkeys.feature.mjs";
13
+ import { bullBoardFeature } from "./bull-board.feature.mjs";
11
14
  import { heraldFeature } from "./herald.feature.mjs";
12
15
  import { imageFeature } from "./image.feature.mjs";
13
16
  import { mailFeature } from "./mail.feature.mjs";
@@ -15,6 +18,7 @@ import { mongodbFeature } from "./mongodb.feature.mjs";
15
18
  import { mysqlFeature } from "./mysql.feature.mjs";
16
19
  import { notificationsFeature } from "./notifications.feature.mjs";
17
20
  import { postgresFeature } from "./postgres.feature.mjs";
21
+ import { queueFeature } from "./queue.feature.mjs";
18
22
  import { reactEmailFeature } from "./react-email.feature.mjs";
19
23
  import { reactFeature } from "./react.feature.mjs";
20
24
  import { redisFeature } from "./redis.feature.mjs";
@@ -53,9 +57,13 @@ const featuresMap = {
53
57
  tailwind: tailwindFeature,
54
58
  shadcn: shadcnFeature,
55
59
  herald: heraldFeature,
60
+ queue: queueFeature,
61
+ "bull-board": bullBoardFeature,
56
62
  socket: socketFeature,
57
63
  notifications: notificationsFeature,
58
64
  access: accessFeature,
65
+ "auth-google": authGoogleFeature,
66
+ "auth-passkeys": authPasskeysFeature,
59
67
  ai: aiFeature,
60
68
  "ai-openai": aiOpenaiFeature,
61
69
  "ai-google": aiGoogleFeature,
@@ -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 { 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 { 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 socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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,QAAQ;CACR,eAAe;CACf,QAAQ;CACR,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
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"}
@@ -0,0 +1,70 @@
1
+ import { rootPath } from "../../utils/paths.mjs";
2
+ import "../../utils/index.mjs";
3
+ import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
4
+ import { insertConnectorEntry } from "./shared/insert-connector-entry.mjs";
5
+ import { colors } from "@mongez/copper";
6
+ import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
7
+
8
+ //#region ../core/src/generations/features/queue.feature.ts
9
+ const queueConfigStub = `import { env } from "@warlock.js/core";
10
+ import type { QueueConfig } from "@warlock.js/queue";
11
+
12
+ /** Durable job queue configuration. Redis is required by BullMQ. */
13
+ const queueConfig: QueueConfig = {
14
+ connection: {
15
+ host: env("REDIS_HOST", "127.0.0.1"),
16
+ port: env("REDIS_PORT", 6379),
17
+ },
18
+ defaultJobOptions: {
19
+ attempts: 3,
20
+ backoff: { type: "exponential", delay: 1000 },
21
+ },
22
+ workers: {
23
+ enabled: true,
24
+ concurrency: 5,
25
+ shutdownTimeout: 30_000,
26
+ },
27
+ };
28
+
29
+ export default queueConfig;
30
+ `;
31
+ /** Register the queue connector in the app-owned configuration without reformatting it. */
32
+ async function registerQueueConnector() {
33
+ const configPath = rootPath("warlock.config.ts");
34
+ if (!await fileExistsAsync(configPath)) {
35
+ console.log(`${colors.yellowBright("warlock.config.ts")} not found — add this yourself:\n import { queueConnector } from "@warlock.js/queue";\n export default defineConfig({ connectors: [queueConnector()] });`);
36
+ return;
37
+ }
38
+ const current = await getFileAsync(configPath);
39
+ if (current.includes("queueConnector")) {
40
+ console.log(`${colors.yellowBright("queueConnector")} already registered, skipping...`);
41
+ return;
42
+ }
43
+ const importLine = "import { queueConnector } from \"@warlock.js/queue\";";
44
+ let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
45
+ const insertion = insertConnectorEntry(next, "queueConnector()");
46
+ if (insertion.status === "already-present") return;
47
+ if (insertion.status === "added") next = insertion.next;
48
+ else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [queueConnector()],\n");
49
+ else {
50
+ console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [queueConnector()]\` yourself.`);
51
+ return;
52
+ }
53
+ await putFileAsync(configPath, next);
54
+ console.log(`${colors.green("✓")} Registered queueConnector in warlock.config.ts`);
55
+ console.log("Next: start Redis, then define jobs with defineJob() in an application module before boot.");
56
+ }
57
+ /** `warlock add queue` — durable BullMQ jobs backed by the app's Redis server. */
58
+ const queueFeature = {
59
+ description: "Installs @warlock.js/queue — durable BullMQ jobs backed by Redis. Creates src/config/queue.ts and registers queueConnector() in warlock.config.ts.",
60
+ dependencies: { "@warlock.js/queue": INSTALLED_WARLOCK_VERSION },
61
+ ejectConfig: {
62
+ content: queueConfigStub,
63
+ name: "queue"
64
+ },
65
+ onExecuting: registerQueueConnector
66
+ };
67
+
68
+ //#endregion
69
+ export { queueFeature };
70
+ //# sourceMappingURL=queue.feature.mjs.map
@@ -0,0 +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 { 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
- if (/connectors:\s*\[/.test(next)) next = next.replace(/connectors:\s*\[/, "connectors: [webConnector(),");
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 \`#root\` and hydrates that element only.
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="root"\` around {children}.
628
+ it — just keep an element with \`id="vessel"\` around {children}.
629
629
  */}
630
- <div id="root">{children}</div>
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 \`#root\`. This is
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");