@warlock.js/core 5.15.0 → 5.17.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 (143) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/esm/cli/commands/migrate.command.mjs +5 -1
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/seed.command.mjs +2 -1
  5. package/esm/cli/commands/seed.command.mjs.map +1 -1
  6. package/esm/cli/commands/typings-generator.command.mjs +16 -23
  7. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  8. package/esm/config/locale-configuration.mjs +7 -1
  9. package/esm/config/locale-configuration.mjs.map +1 -1
  10. package/esm/connectors/http-connector.d.mts.map +1 -1
  11. package/esm/connectors/http-connector.mjs +1 -18
  12. package/esm/connectors/http-connector.mjs.map +1 -1
  13. package/esm/connectors/read-bound-port.mjs +32 -0
  14. package/esm/connectors/read-bound-port.mjs.map +1 -0
  15. package/esm/dev-server/development-server.mjs +5 -1
  16. package/esm/dev-server/development-server.mjs.map +1 -1
  17. package/esm/dev-server/extract-translation-keys.mjs +68 -0
  18. package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
  19. package/esm/dev-server/translation-keys-sources.mjs +19 -0
  20. package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
  21. package/esm/dev-server/type-generator.mjs +64 -11
  22. package/esm/dev-server/type-generator.mjs.map +1 -1
  23. package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
  24. package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
  25. package/esm/generations/features/sitemap.feature.mjs +50 -47
  26. package/esm/generations/features/sitemap.feature.mjs.map +1 -1
  27. package/esm/generations/stubs.mjs +7 -6
  28. package/esm/generations/stubs.mjs.map +1 -1
  29. package/esm/http/csrf-default-guard.d.mts +32 -0
  30. package/esm/http/csrf-default-guard.d.mts.map +1 -0
  31. package/esm/http/csrf-default-guard.mjs +156 -0
  32. package/esm/http/csrf-default-guard.mjs.map +1 -0
  33. package/esm/http/csrf-origin-policy.d.mts +53 -0
  34. package/esm/http/csrf-origin-policy.d.mts.map +1 -0
  35. package/esm/http/csrf-origin-policy.mjs +104 -0
  36. package/esm/http/csrf-origin-policy.mjs.map +1 -0
  37. package/esm/http/error-codes.d.mts +14 -1
  38. package/esm/http/error-codes.d.mts.map +1 -1
  39. package/esm/http/error-codes.mjs +13 -0
  40. package/esm/http/error-codes.mjs.map +1 -1
  41. package/esm/http/index.d.mts +8 -1
  42. package/esm/http/index.mjs +7 -1
  43. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  44. package/esm/http/middleware/idempotency.middleware.mjs +1 -1
  45. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  46. package/esm/http/middleware/inject-request-context.mjs +6 -0
  47. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  48. package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
  49. package/esm/http/middleware/maintenance.middleware.mjs +1 -1
  50. package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
  51. package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
  52. package/esm/http/parse-urlencoded-body.mjs +27 -0
  53. package/esm/http/parse-urlencoded-body.mjs.map +1 -0
  54. package/esm/http/plugins.d.mts.map +1 -1
  55. package/esm/http/plugins.mjs +8 -0
  56. package/esm/http/plugins.mjs.map +1 -1
  57. package/esm/http/request.d.mts +2 -2
  58. package/esm/http/request.d.mts.map +1 -1
  59. package/esm/http/request.mjs +7 -3
  60. package/esm/http/request.mjs.map +1 -1
  61. package/esm/http/response.d.mts +9 -2
  62. package/esm/http/response.d.mts.map +1 -1
  63. package/esm/http/response.mjs +18 -4
  64. package/esm/http/response.mjs.map +1 -1
  65. package/esm/http/uploads/detect-image-format.mjs +59 -0
  66. package/esm/http/uploads/detect-image-format.mjs.map +1 -0
  67. package/esm/http/uploads/generate-image-variant.mjs +46 -0
  68. package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
  69. package/esm/http/uploads/generate-image-variants.d.mts +49 -0
  70. package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
  71. package/esm/http/uploads/generate-image-variants.mjs +141 -0
  72. package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
  73. package/esm/http/uploads/image-variant-types.d.mts +93 -0
  74. package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
  75. package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
  76. package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
  77. package/esm/http/uploads/image-variants-config-error.mjs +17 -0
  78. package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
  79. package/esm/http/uploads/index.d.mts +4 -0
  80. package/esm/http/uploads/index.mjs +5 -0
  81. package/esm/http/uploads/is-path-inside.mjs +26 -0
  82. package/esm/http/uploads/is-path-inside.mjs.map +1 -0
  83. package/esm/http/uploads/load-variant-source.mjs +42 -0
  84. package/esm/http/uploads/load-variant-source.mjs.map +1 -0
  85. package/esm/http/uploads/matches-if-none-match.mjs +13 -0
  86. package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
  87. package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
  88. package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
  89. package/esm/http/uploads/read-file-head.mjs +20 -0
  90. package/esm/http/uploads/read-file-head.mjs.map +1 -0
  91. package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
  92. package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
  93. package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
  94. package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
  95. package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
  96. package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
  97. package/esm/http/uploads/resolve-upload-path.mjs +56 -0
  98. package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
  99. package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
  100. package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
  101. package/esm/http/uploads/single-flight.mjs +40 -0
  102. package/esm/http/uploads/single-flight.mjs.map +1 -0
  103. package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
  104. package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
  105. package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
  106. package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
  107. package/esm/http/uploads/variant-cache-key.mjs +45 -0
  108. package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
  109. package/esm/http/uploads/variant-generations.mjs +16 -0
  110. package/esm/http/uploads/variant-generations.mjs.map +1 -0
  111. package/esm/http/uploads-types.d.mts +102 -1
  112. package/esm/http/uploads-types.d.mts.map +1 -1
  113. package/esm/http/xmlable.d.mts +14 -0
  114. package/esm/http/xmlable.d.mts.map +1 -0
  115. package/esm/index.d.mts +10 -2
  116. package/esm/index.mjs +8 -2
  117. package/esm/router/types.d.mts +23 -0
  118. package/esm/router/types.d.mts.map +1 -1
  119. package/esm/storage/index.d.mts +1 -0
  120. package/esm/storage/index.mjs +1 -0
  121. package/esm/storage/scoped-storage.d.mts +1 -0
  122. package/esm/storage/scoped-storage.d.mts.map +1 -1
  123. package/esm/storage/scoped-storage.mjs +3 -0
  124. package/esm/storage/scoped-storage.mjs.map +1 -1
  125. package/esm/storage/storage.d.mts +2 -0
  126. package/esm/storage/storage.d.mts.map +1 -1
  127. package/esm/storage/storage.mjs +4 -0
  128. package/esm/storage/storage.mjs.map +1 -1
  129. package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
  130. package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
  131. package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
  132. package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
  133. package/llms-full.txt +47 -7
  134. package/llms.txt +1 -1
  135. package/package.json +22 -21
  136. package/skills/configure-app/SKILL.md +6 -6
  137. package/skills/create-controller/SKILL.md +14 -0
  138. package/skills/health-checks/SKILL.md +8 -0
  139. package/skills/send-response/SKILL.md +2 -0
  140. package/skills/use-localization/SKILL.md +16 -0
  141. package/skills/use-middleware/SKILL.md +1 -1
  142. package/esm/dev-server/translation-type-generator.mjs +0 -28
  143. package/esm/dev-server/translation-type-generator.mjs.map +0 -1
@@ -5,14 +5,41 @@ import { devLogError, devLogInfo, devLogSuccess, devServeLog } from "./dev-logge
5
5
  import { filesOrchestrator } from "./files-orchestrator.mjs";
6
6
  import { readConfigAst } from "./read-config-ast.mjs";
7
7
  import { runTypingsGeneration } from "./run-typings-generation.mjs";
8
- import { extractTranslationKeys } from "./translation-type-generator.mjs";
8
+ import { extractTranslationKeys } from "./extract-translation-keys.mjs";
9
+ import { isTranslationRegisteringSource } from "./translation-keys-sources.mjs";
9
10
  import { join, resolve } from "path";
10
11
  import { ensureDirectoryAsync } from "@warlock.js/fs";
11
12
  import { constants } from "fs";
12
13
  import { access, readFile, writeFile } from "fs/promises";
13
14
  import ts from "typescript";
15
+ import { pathToFileURL } from "node:url";
16
+ import { moduleResolve } from "import-meta-resolve";
14
17
 
15
18
  //#region ../core/src/dev-server/type-generator.ts
19
+ const WEB_PACKAGE = "@warlock.js/web";
20
+ const WEB_BUILD_PACKAGE = "@warlock.js/web/build";
21
+ function isAbsentOptionalWebPackage(thrown) {
22
+ return typeof thrown === "object" && thrown !== null && "code" in thrown && thrown.code === "ERR_MODULE_NOT_FOUND" && thrown instanceof Error && thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`);
23
+ }
24
+ /** Load the build-only web key lister without creating a core-to-web static edge. */
25
+ async function listRouteLocaleKeysForTypeGeneration(appRoot = process.cwd()) {
26
+ const projectUrl = pathToFileURL(resolve(appRoot, "package.json"));
27
+ try {
28
+ moduleResolve(WEB_PACKAGE, projectUrl);
29
+ } catch (thrown) {
30
+ if (isAbsentOptionalWebPackage(thrown)) return [];
31
+ throw thrown;
32
+ }
33
+ const buildModule = await import(moduleResolve(WEB_BUILD_PACKAGE, projectUrl).href);
34
+ if (typeof buildModule.listRouteLocaleKeys !== "function") throw new Error(`${WEB_BUILD_PACKAGE} does not export listRouteLocaleKeys. Upgrade @warlock.js/web to a compatible version.`);
35
+ return buildModule.listRouteLocaleKeys({ appRoot });
36
+ }
37
+ /** True only for the route-locales convention under the canonical web root. */
38
+ function isRouteLocalesJsonPath(path) {
39
+ const normalized = Path.normalize(path);
40
+ const relativePath = normalized.startsWith("src/") ? normalized : Path.toRelative(normalized);
41
+ return /^src\/web(?:\/.*)?\/locales\.json$/.test(relativePath);
42
+ }
16
43
  /**
17
44
  * TypeGenerator - Generates TypeScript type definitions from config files
18
45
  *
@@ -27,6 +54,7 @@ var TypeGenerator = class {
27
54
  this.manifestPath = join(this.outputDir, "typings-manifest.json");
28
55
  this.manifest = null;
29
56
  this.configCache = /* @__PURE__ */ new Map();
57
+ this.translationSourceFiles = /* @__PURE__ */ new Set();
30
58
  }
31
59
  /**
32
60
  * Generate all framework type definitions
@@ -134,7 +162,7 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
134
162
  * Check if a file change should trigger type regeneration
135
163
  */
136
164
  shouldRegenerateTypes(changedPath) {
137
- return changedPath.includes("src/config/") || changedPath.includes("config/") || this.isLocalesFile(changedPath);
165
+ return changedPath.includes("src/config/") || changedPath.includes("config/") || this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath);
138
166
  }
139
167
  /**
140
168
  * Handle file change - uses incremental update via cache
@@ -146,7 +174,7 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
146
174
  await this.saveManifest();
147
175
  return;
148
176
  }
149
- if (this.isLocalesFile(changedPath)) {
177
+ if (this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath)) {
150
178
  await this.generateTranslationTypes();
151
179
  return;
152
180
  }
@@ -191,16 +219,30 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
191
219
  devServeLog(`⚠️ Failed to generate config types: ${error}`);
192
220
  }
193
221
  }
194
- /** Generate web's app-augmented translation-key registry. */
222
+ /**
223
+ * Generate web's app-augmented translation-key registry.
224
+ *
225
+ * Scans every discovered app source file — not only dedicated
226
+ * `utils/locales.*` files — for `groupedTranslations` calls, both the
227
+ * named-group and object forms, at any nesting depth. Dynamic group names,
228
+ * dictionaries, or keys cannot be resolved statically and are skipped; an
229
+ * app relying on those must document them itself.
230
+ */
195
231
  async generateTranslationTypes() {
196
232
  const keys = /* @__PURE__ */ new Set();
233
+ const sourceFiles = /* @__PURE__ */ new Set();
197
234
  for (const [path, fileManager] of filesOrchestrator.getFiles()) {
198
- if (!this.isLocalesFile(path)) continue;
235
+ if (!isTranslationRegisteringSource(fileManager.source)) continue;
236
+ sourceFiles.add(path);
199
237
  const sourceFile = await readConfigAst(fileManager.absolutePath);
200
238
  if (sourceFile) for (const key of extractTranslationKeys(sourceFile)) keys.add(key);
201
239
  }
240
+ for (const key of await listRouteLocaleKeysForTypeGeneration()) keys.add(key);
241
+ this.translationSourceFiles = sourceFiles;
202
242
  const content = `// Auto-generated by Warlock.js - DO NOT EDIT
203
- // Generated from groupedTranslations calls in app locale files
243
+ // Generated from groupedTranslations calls and route-locales JSON in discovered
244
+ // app source. Dynamic group names/keys cannot be resolved statically and are
245
+ // not included here — document those yourself.
204
246
 
205
247
  import "@warlock.js/web";
206
248
 
@@ -459,14 +501,25 @@ ${allKeys.map((key) => ` "${key}": true;`).join("\n")}
459
501
  await runTypingsGeneration(this.typingsGenerationPorts);
460
502
  }
461
503
  /**
462
- * Regenerate typings after a batch reload — but only when the batch actually
463
- * touched a config file, since nothing else contributes to them.
504
+ * Regenerate typings after a batch reload — but only when the batch
505
+ * plausibly touched something the typings depend on: a config file, a file
506
+ * that registers translation keys, or a registering file that was deleted.
507
+ *
508
+ * A deleted file is already gone from `filesOrchestrator` by the time this
509
+ * runs (deletes are processed before `dev-server:batch-complete` fires), so
510
+ * its content can't be re-checked here — `translationSourceFiles` is the
511
+ * set from the last generation, kept for exactly this comparison.
464
512
  */
465
513
  async executeTypingsGenerator(upcomingFiles) {
466
- if (!Array.from(new Set(upcomingFiles)).some((file) => {
514
+ const files = filesOrchestrator.getFiles();
515
+ if (!(Array.from(this.translationSourceFiles).some((path) => !files.has(path)) || Array.from(new Set(upcomingFiles)).some((file) => {
467
516
  const normalizedPath = Path.normalize(file);
468
- return normalizedPath.includes("src/config/") || this.isLocalesFile(normalizedPath);
469
- })) return;
517
+ if (normalizedPath.includes("src/config/")) return true;
518
+ if (isRouteLocalesJsonPath(normalizedPath)) return true;
519
+ if (this.translationSourceFiles.has(normalizedPath)) return true;
520
+ const fileManager = files.get(normalizedPath);
521
+ return fileManager !== void 0 && isTranslationRegisteringSource(fileManager.source);
522
+ }))) return;
470
523
  await runTypingsGeneration(this.typingsGenerationPorts);
471
524
  }
472
525
  };
@@ -1 +1 @@
1
- {"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport { constants } from \"fs\";\nimport { access, readFile, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport ts from \"typescript\";\nimport { warlockPath } from \"../utils\";\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\nimport { readConfigAst } from \"./read-config-ast\";\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { extractTranslationKeys } from \"./translation-type-generator\";\n\n/**\n * Typings manifest structure for tracking file hashes\n */\ntype TypingsManifest = {\n version: string;\n lastBuildTime: number;\n storage: {\n sourceHash: string;\n drivers: string[];\n } | null;\n config: Record<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >;\n};\n\n/**\n * TypeGenerator - Generates TypeScript type definitions from config files\n *\n * Parses source config files using the TypeScript Compiler API to extract\n * keys and generate module augmentation types for IDE autocomplete.\n *\n * Uses manifest-based reconciliation to only regenerate when source files change.\n */\nexport class TypeGenerator {\n /**\n * Output directory for generated typings\n */\n private outputDir = warlockPath(\"typings\");\n\n /**\n * Path to typings manifest file\n */\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\n\n /**\n * Cached manifest data\n */\n private manifest: TypingsManifest | null = null;\n\n /**\n * Cache for config type info and keys\n */\n private configCache = new Map<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >();\n\n /**\n * Generate all framework type definitions\n *\n * Uses manifest-based reconciliation:\n * - If output files don't exist: full regeneration\n * - If files exist: only regenerate changed configs\n */\n public async generateAll(): Promise<void> {\n await this.ensureOutputDir();\n\n const storageFile = join(this.outputDir, \"storage.d.ts\");\n const configFile = join(this.outputDir, \"config.d.ts\");\n const translationsFile = join(this.outputDir, \"translations.d.ts\");\n\n const [manifestExists, storageExists, configExists, translationsExist] = await Promise.all([\n this.exists(this.manifestPath),\n this.exists(storageFile),\n this.exists(configFile),\n this.exists(translationsFile),\n ]);\n\n if (!manifestExists || !storageExists || !configExists || !translationsExist) {\n // Full regeneration (first run or files deleted)\n await this.fullGeneration();\n } else {\n // Load manifest for hash comparison\n await this.loadManifest();\n // Reconciliation: only regenerate changed files\n await this.reconcile();\n }\n\n await this.generateTranslationTypes();\n\n await this.saveManifest();\n }\n\n /**\n * Full regeneration of all type files\n */\n private async fullGeneration(): Promise<void> {\n // Generate storage types\n const storageConfigPath = await this.findConfigFile(\"storage\");\n if (storageConfigPath) {\n this.generateStorageTypes(storageConfigPath);\n }\n\n // Generate config types\n await this.generateConfigTypes();\n }\n\n /**\n * Reconcile: only regenerate changed files\n */\n private async reconcile(): Promise<void> {\n const files = filesOrchestrator.getFiles();\n let storageChanged = false;\n let configChanged = false;\n let unchangedCount = 0;\n\n // Check storage config\n for (const [path, fileManager] of files) {\n if (path.startsWith(\"src/config/storage\")) {\n const manifestEntry = this.manifest?.storage;\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n storageChanged = true;\n await this.generateStorageTypes(path);\n }\n break;\n }\n }\n\n // Check config files\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name (remove dir prefix and extension)\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n const manifestEntry = this.manifest?.config[configName];\n\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n // File changed or new - regenerate\n configChanged = true;\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n } else {\n // Unchanged - load from manifest\n unchangedCount++;\n this.configCache.set(configName, manifestEntry);\n }\n }\n\n if (configChanged) {\n await this.writeConfigTypesFromCache();\n } else {\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\n }\n }\n\n /**\n * Generate storage driver name types\n */\n public async generateStorageTypes(configPath: string): Promise<void> {\n try {\n const driverKeys = await this.extractStorageDriverKeys(configPath);\n\n if (driverKeys.length === 0) {\n devServeLog(\"⚠️ No storage drivers found in config\");\n return;\n }\n\n // Get file hash from filesOrchestrator\n const fileManager = filesOrchestrator.getFiles().get(configPath);\n const sourceHash = fileManager?.hash || \"\";\n\n // Update manifest\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.storage = {\n sourceHash,\n drivers: driverKeys,\n };\n\n const interfaceContent = driverKeys\n .map((k) => ` ${this.toInterfaceKey(k)}: true;`)\n .join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from: ${configPath}\n// Regenerates on dev-server start and when storage config changes\n\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface StorageDriverRegistry {\n${interfaceContent}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"storage.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\n } catch (error) {\n devServeLog(`⚠️ Failed to generate storage types: ${error}`);\n }\n }\n\n /**\n * Check if a file change should trigger type regeneration\n */\n public shouldRegenerateTypes(changedPath: string): boolean {\n return (\n changedPath.includes(\"src/config/\") ||\n changedPath.includes(\"config/\") ||\n this.isLocalesFile(changedPath)\n );\n }\n\n /**\n * Handle file change - uses incremental update via cache\n */\n public async handleFileChange(changedPath: string): Promise<void> {\n if (!this.shouldRegenerateTypes(changedPath)) {\n return;\n }\n\n // Regenerate storage types if storage config changed\n if (changedPath.includes(\"config/storage\")) {\n await this.generateStorageTypes(changedPath);\n await this.saveManifest();\n return;\n }\n\n if (this.isLocalesFile(changedPath)) {\n await this.generateTranslationTypes();\n return;\n }\n\n // Extract config name from path\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\n if (!match) {\n return;\n }\n\n const configName = match[1];\n if (configName === \"index\") return;\n\n devLogInfo(`Config changed: ${configName}, updating...`);\n\n // Get file manager for hash\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\n const sourceHash = fileManager?.hash || Date.now().toString();\n\n // Update only the changed config in cache (use optimized combined extraction)\n const configDir = join(process.cwd(), \"src/config\");\n const configPath = join(configDir, `${configName}.ts`);\n\n const info = await this.extractConfigInfo(configPath, configName);\n\n this.configCache.set(configName, {\n sourceHash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n\n // Regenerate config.d.ts from cache\n await this.writeConfigTypesFromCache();\n await this.saveManifest();\n }\n\n /**\n * Generate config types - populates cache and writes file\n */\n public async generateConfigTypes(): Promise<void> {\n try {\n const files = filesOrchestrator.getFiles();\n\n // Clear and repopulate cache\n this.configCache.clear();\n\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n }\n\n await this.writeConfigTypesFromCache();\n } catch (error) {\n devServeLog(`⚠️ Failed to generate config types: ${error}`);\n }\n }\n\n /** Generate web's app-augmented translation-key registry. */\n private async generateTranslationTypes(): Promise<void> {\n const keys = new Set<string>();\n\n for (const [path, fileManager] of filesOrchestrator.getFiles()) {\n if (!this.isLocalesFile(path)) {\n continue;\n }\n\n const sourceFile = await readConfigAst(fileManager.absolutePath);\n if (sourceFile) {\n for (const key of extractTranslationKeys(sourceFile)) {\n keys.add(key);\n }\n }\n }\n\n const entries = Array.from(keys)\n .sort()\n .map((key) => ` ${JSON.stringify(key)}: true;`)\n .join(\"\\n\");\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from groupedTranslations calls in app locale files\n\nimport \"@warlock.js/web\";\n\ndeclare module \"@warlock.js/web\" {\n interface TranslationKeyRegistry {\n${entries}\n }\n}\n`;\n\n await writeFile(join(this.outputDir, \"translations.d.ts\"), content, \"utf-8\");\n devLogSuccess(`Generated translation types: ${keys.size} keys`);\n }\n\n private isLocalesFile(path: string): boolean {\n return Path.normalize(path).includes(\"/utils/locales.\");\n }\n\n /**\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\n * named `use-cases` or a storage driver `do-spaces`) so the generated\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\n * to keep the output clean.\n */\n private toInterfaceKey(name: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n }\n\n /**\n * Write config.d.ts from cached data\n */\n private async writeConfigTypesFromCache(): Promise<void> {\n const configTypeInfos: Array<{\n name: string;\n typeName: string | null;\n importSource: string | null;\n }> = [];\n const allKeys: string[] = [];\n\n // Update manifest config section\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.config = {};\n\n for (const [name, data] of this.configCache) {\n configTypeInfos.push({\n name,\n typeName: data.typeName,\n importSource: data.importSource,\n });\n allKeys.push(...data.keys);\n\n // Store in manifest\n this.manifest.config[name] = data;\n }\n\n // Group imports by source\n const imports = new Map<string, Set<string>>();\n for (const info of configTypeInfos) {\n if (info.typeName && info.importSource) {\n if (!imports.has(info.importSource)) {\n imports.set(info.importSource, new Set());\n }\n imports.get(info.importSource)!.add(info.typeName);\n }\n }\n\n const importStatements = Array.from(imports.entries())\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\n .join(\"\\n\");\n\n const configEntries = configTypeInfos\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\n .join(\"\\n\");\n\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Regenerates on dev-server start and when config files change\n\n${importStatements}\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface ConfigRegistry {\n${configEntries}\n }\n\n interface ConfigKeyRegistry {\n${keyEntries}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"config.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\n );\n }\n\n // ============================================================\n // Manifest Management\n // ============================================================\n\n /**\n * Load manifest from disk\n */\n private async loadManifest(): Promise<boolean> {\n try {\n if (await this.exists(this.manifestPath)) {\n const content = await readFile(this.manifestPath, \"utf-8\");\n this.manifest = JSON.parse(content);\n return true;\n }\n } catch {\n // Manifest corrupted or missing\n }\n this.manifest = null;\n return false;\n }\n\n /**\n * Save manifest to disk\n */\n private async saveManifest(): Promise<void> {\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.lastBuildTime = Date.now();\n\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\n }\n\n /**\n * Create empty manifest structure\n */\n private createEmptyManifest(): TypingsManifest {\n return {\n version: \"1.0.0\",\n lastBuildTime: Date.now(),\n storage: null,\n config: {},\n };\n }\n\n // ============================================================\n // Type Extraction Methods\n // ============================================================\n\n /**\n * Extract BOTH type info AND keys in a single pass\n *\n * One parse serves both extractions. It used to be one whole TypeScript\n * Program per file — halved from two by an earlier pass, which optimised\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\n * numbers.\n *\n * @param configPath Absolute path to the config file\n * @param configName Config name (e.g., \"auth\", \"notifications\")\n * @returns Combined result with type info and keys\n */\n private async extractConfigInfo(\n configPath: string,\n configName: string,\n ): Promise<{\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }> {\n if (!(await this.exists(configPath))) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\n const sourceFile = await readConfigAst(configPath);\n\n if (!sourceFile) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // === Type Info Extraction ===\n const importedTypes = new Map<string, string>();\n const localExportedTypes = new Set<string>();\n let foundTypeName: string | null = null;\n\n // === Keys Extraction ===\n const keys: string[] = [];\n\n const visitForTypes = (node: ts.Node): void => {\n // Collect imported types\n if (ts.isImportDeclaration(node)) {\n const moduleSpecifier = node.moduleSpecifier;\n if (ts.isStringLiteral(moduleSpecifier)) {\n const source = moduleSpecifier.text;\n const importClause = node.importClause;\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\n for (const element of importClause.namedBindings.elements) {\n importedTypes.set(element.name.text, source);\n }\n }\n }\n }\n\n // Collect locally exported types\n if (ts.isTypeAliasDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Collect locally exported interfaces\n if (ts.isInterfaceDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Find type used on config variable + extract keys\n if (ts.isVariableDeclaration(node)) {\n // Type info\n if (node.type && ts.isTypeReferenceNode(node.type)) {\n foundTypeName = node.type.typeName.getText(sourceFile);\n }\n\n // Keys extraction\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\n for (const prop of objNode.properties) {\n if (ts.isPropertyAssignment(prop) && prop.name) {\n const keyName = prop.name.getText(sourceFile);\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\n keys.push(fullKey);\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n visitKeys(prop.initializer, fullKey);\n }\n }\n }\n };\n visitKeys(node.initializer, configName);\n }\n }\n\n ts.forEachChild(node, visitForTypes);\n };\n\n ts.forEachChild(sourceFile, visitForTypes);\n\n // Resolve type info\n let typeName: string | null = null;\n let importSource: string | null = null;\n\n if (foundTypeName) {\n if (importedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n importSource = importedTypes.get(foundTypeName)!;\n } else if (localExportedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\n importSource = `../../${relativePath}`;\n }\n }\n\n return { typeName, importSource, keys };\n }\n\n /**\n * Extract driver keys from storage config\n */\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\n const absolutePath = resolve(configPath);\n\n if (!(await this.exists(absolutePath))) {\n devServeLog(`⚠️ Storage config not found: ${absolutePath}`);\n return [];\n }\n\n const sourceFile = await readConfigAst(absolutePath);\n\n if (!sourceFile) {\n devServeLog(`⚠️ Could not parse storage config: ${absolutePath}`);\n return [];\n }\n\n const keys: string[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isPropertyAssignment(node)) {\n const propName = node.name.getText(sourceFile);\n\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\n for (const prop of node.initializer.properties) {\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\n const keyName = prop.name?.getText(sourceFile);\n\n if (keyName) {\n keys.push(keyName);\n }\n }\n }\n }\n }\n\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return keys;\n }\n\n /**\n * Find a config file by name\n */\n private async findConfigFile(configName: string): Promise<string | undefined> {\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\n\n for (const path of possiblePaths) {\n const fullPath = join(process.cwd(), path);\n\n if (await this.exists(fullPath)) {\n return path;\n }\n }\n\n try {\n const files = filesOrchestrator.getFiles();\n\n for (const [filePath] of files) {\n if (filePath.includes(`config/${configName}`)) {\n return filePath;\n }\n }\n } catch {\n // Files orchestrator not initialized yet\n }\n\n return undefined;\n }\n\n /**\n * Ensure output directory exists\n */\n private async ensureOutputDir(): Promise<void> {\n await ensureDirectoryAsync(this.outputDir);\n }\n\n /**\n * Check if a path exists (async wrapper)\n */\n private async exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * The ports `runTypingsGeneration` needs, bound to this instance and this\n * package's dev logger.\n */\n private get typingsGenerationPorts(): TypingsGenerationPorts {\n return {\n generate: () => this.generateAll(),\n info: (message) => devLogInfo(message),\n success: (message) => devLogSuccess(message),\n error: (message) => devLogError(message),\n };\n }\n\n /**\n * Generate every config's typings.\n *\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\n * which could not resolve in a source checkout — see\n * `run-typings-generation.ts` for the whole story.\n */\n public async executeGenerateAllCommand(): Promise<void> {\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n\n /**\n * Regenerate typings after a batch reload — but only when the batch actually\n * touched a config file, since nothing else contributes to them.\n */\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\n const touchedAConfig = Array.from(new Set(upcomingFiles)).some((file) => {\n const normalizedPath = Path.normalize(file);\n return normalizedPath.includes(\"src/config/\") || this.isLocalesFile(normalizedPath);\n });\n\n if (!touchedAConfig) return;\n\n /*\n The changed paths are deliberately NOT passed along. The previous version\n built a `files` array here and then threw it away, spawning the same\n whole-project command as the branch above — so \"incremental\" was a name,\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\n reload has already updated, so the full pass is both correct and the only\n pass that ever actually ran.\n */\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n}\n\n/**\n * Singleton instance for use throughout dev-server\n */\nexport const typeGenerator = new TypeGenerator();\n"],"mappings":"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"}
1
+ {"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport { constants } from \"fs\";\r\nimport { access, readFile, writeFile } from \"fs/promises\";\r\nimport { moduleResolve } from \"import-meta-resolve\";\r\nimport { join, resolve } from \"path\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport ts from \"typescript\";\r\nimport { warlockPath } from \"../utils\";\r\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\r\nimport { readConfigAst } from \"./read-config-ast\";\r\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { extractTranslationKeys } from \"./extract-translation-keys\";\r\nimport { isTranslationRegisteringSource } from \"./translation-keys-sources\";\r\n\r\ntype RouteLocaleBuildModule = {\r\n listRouteLocaleKeys?: (options: {\r\n appRoot?: string;\r\n }) => readonly string[] | Promise<readonly string[]>;\r\n};\r\n\r\nconst WEB_PACKAGE = \"@warlock.js/web\";\r\nconst WEB_BUILD_PACKAGE = \"@warlock.js/web/build\";\r\n\r\nfunction isAbsentOptionalWebPackage(thrown: unknown): boolean {\r\n return (\r\n typeof thrown === \"object\" &&\r\n thrown !== null &&\r\n \"code\" in thrown &&\r\n (thrown as { code?: unknown }).code === \"ERR_MODULE_NOT_FOUND\" &&\r\n thrown instanceof Error &&\r\n thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`)\r\n );\r\n}\r\n\r\n/** Load the build-only web key lister without creating a core-to-web static edge. */\r\nexport async function listRouteLocaleKeysForTypeGeneration(\r\n appRoot = process.cwd(),\r\n): Promise<readonly string[]> {\r\n // Published web entries are import-only. Resolve with ESM conditions from\r\n // the consuming project; require.resolve rejects those installed exports.\r\n const projectUrl = pathToFileURL(resolve(appRoot, \"package.json\"));\r\n\r\n try {\r\n moduleResolve(WEB_PACKAGE, projectUrl);\r\n } catch (thrown) {\r\n if (isAbsentOptionalWebPackage(thrown)) return [];\r\n throw thrown;\r\n }\r\n\r\n const buildModuleUrl = moduleResolve(WEB_BUILD_PACKAGE, projectUrl);\r\n const buildModule = (await import(buildModuleUrl.href)) as RouteLocaleBuildModule;\r\n\r\n if (typeof buildModule.listRouteLocaleKeys !== \"function\") {\r\n throw new Error(\r\n `${WEB_BUILD_PACKAGE} does not export listRouteLocaleKeys. Upgrade @warlock.js/web to a compatible version.`,\r\n );\r\n }\r\n\r\n return buildModule.listRouteLocaleKeys({ appRoot });\r\n}\r\n\r\n/** True only for the route-locales convention under the canonical web root. */\r\nexport function isRouteLocalesJsonPath(path: string): boolean {\r\n const normalized = Path.normalize(path);\r\n const relativePath = normalized.startsWith(\"src/\") ? normalized : Path.toRelative(normalized);\r\n return /^src\\/web(?:\\/.*)?\\/locales\\.json$/.test(relativePath);\r\n}\r\n\r\n/**\r\n * Typings manifest structure for tracking file hashes\r\n */\r\ntype TypingsManifest = {\r\n version: string;\r\n lastBuildTime: number;\r\n storage: {\r\n sourceHash: string;\r\n drivers: string[];\r\n } | null;\r\n config: Record<\r\n string,\r\n {\r\n sourceHash: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }\r\n >;\r\n};\r\n\r\n/**\r\n * TypeGenerator - Generates TypeScript type definitions from config files\r\n *\r\n * Parses source config files using the TypeScript Compiler API to extract\r\n * keys and generate module augmentation types for IDE autocomplete.\r\n *\r\n * Uses manifest-based reconciliation to only regenerate when source files change.\r\n */\r\nexport class TypeGenerator {\r\n /**\r\n * Output directory for generated typings\r\n */\r\n private outputDir = warlockPath(\"typings\");\r\n\r\n /**\r\n * Path to typings manifest file\r\n */\r\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\r\n\r\n /**\r\n * Cached manifest data\r\n */\r\n private manifest: TypingsManifest | null = null;\r\n\r\n /**\r\n * Cache for config type info and keys\r\n */\r\n private configCache = new Map<\r\n string,\r\n {\r\n sourceHash: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }\r\n >();\r\n\r\n /**\r\n * Generate all framework type definitions\r\n *\r\n * Uses manifest-based reconciliation:\r\n * - If output files don't exist: full regeneration\r\n * - If files exist: only regenerate changed configs\r\n */\r\n public async generateAll(): Promise<void> {\r\n await this.ensureOutputDir();\r\n\r\n const storageFile = join(this.outputDir, \"storage.d.ts\");\r\n const configFile = join(this.outputDir, \"config.d.ts\");\r\n const translationsFile = join(this.outputDir, \"translations.d.ts\");\r\n\r\n const [manifestExists, storageExists, configExists, translationsExist] = await Promise.all([\r\n this.exists(this.manifestPath),\r\n this.exists(storageFile),\r\n this.exists(configFile),\r\n this.exists(translationsFile),\r\n ]);\r\n\r\n if (!manifestExists || !storageExists || !configExists || !translationsExist) {\r\n // Full regeneration (first run or files deleted)\r\n await this.fullGeneration();\r\n } else {\r\n // Load manifest for hash comparison\r\n await this.loadManifest();\r\n // Reconciliation: only regenerate changed files\r\n await this.reconcile();\r\n }\r\n\r\n await this.generateTranslationTypes();\r\n\r\n await this.saveManifest();\r\n }\r\n\r\n /**\r\n * Full regeneration of all type files\r\n */\r\n private async fullGeneration(): Promise<void> {\r\n // Generate storage types\r\n const storageConfigPath = await this.findConfigFile(\"storage\");\r\n if (storageConfigPath) {\r\n this.generateStorageTypes(storageConfigPath);\r\n }\r\n\r\n // Generate config types\r\n await this.generateConfigTypes();\r\n }\r\n\r\n /**\r\n * Reconcile: only regenerate changed files\r\n */\r\n private async reconcile(): Promise<void> {\r\n const files = filesOrchestrator.getFiles();\r\n let storageChanged = false;\r\n let configChanged = false;\r\n let unchangedCount = 0;\r\n\r\n // Check storage config\r\n for (const [path, fileManager] of files) {\r\n if (path.startsWith(\"src/config/storage\")) {\r\n const manifestEntry = this.manifest?.storage;\r\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\r\n storageChanged = true;\r\n await this.generateStorageTypes(path);\r\n }\r\n break;\r\n }\r\n }\r\n\r\n // Check config files\r\n for (const [path, fileManager] of files) {\r\n if (!path.startsWith(\"src/config/\")) continue;\r\n if (path.includes(\"index\")) continue;\r\n\r\n // Extract config name (remove dir prefix and extension)\r\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\r\n\r\n const manifestEntry = this.manifest?.config[configName];\r\n\r\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\r\n // File changed or new - regenerate\r\n configChanged = true;\r\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash: fileManager.hash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n } else {\r\n // Unchanged - load from manifest\r\n unchangedCount++;\r\n this.configCache.set(configName, manifestEntry);\r\n }\r\n }\r\n\r\n if (configChanged) {\r\n await this.writeConfigTypesFromCache();\r\n } else {\r\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\r\n }\r\n }\r\n\r\n /**\r\n * Generate storage driver name types\r\n */\r\n public async generateStorageTypes(configPath: string): Promise<void> {\r\n try {\r\n const driverKeys = await this.extractStorageDriverKeys(configPath);\r\n\r\n if (driverKeys.length === 0) {\r\n devServeLog(\"⚠️ No storage drivers found in config\");\r\n return;\r\n }\r\n\r\n // Get file hash from filesOrchestrator\r\n const fileManager = filesOrchestrator.getFiles().get(configPath);\r\n const sourceHash = fileManager?.hash || \"\";\r\n\r\n // Update manifest\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.storage = {\r\n sourceHash,\r\n drivers: driverKeys,\r\n };\r\n\r\n const interfaceContent = driverKeys\r\n .map((k) => ` ${this.toInterfaceKey(k)}: true;`)\r\n .join(\"\\n\");\r\n\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Generated from: ${configPath}\r\n// Regenerates on dev-server start and when storage config changes\r\n\r\nimport \"@warlock.js/core\";\r\n\r\ndeclare module \"@warlock.js/core\" {\r\n interface StorageDriverRegistry {\r\n${interfaceContent}\r\n }\r\n}\r\n`;\r\n\r\n const outputPath = join(this.outputDir, \"storage.d.ts\");\r\n await writeFile(outputPath, content, \"utf-8\");\r\n\r\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\r\n } catch (error) {\r\n devServeLog(`⚠️ Failed to generate storage types: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Check if a file change should trigger type regeneration\r\n */\r\n public shouldRegenerateTypes(changedPath: string): boolean {\r\n return (\r\n changedPath.includes(\"src/config/\") ||\r\n changedPath.includes(\"config/\") ||\r\n this.isLocalesFile(changedPath) ||\r\n isRouteLocalesJsonPath(changedPath)\r\n );\r\n }\r\n\r\n /**\r\n * Handle file change - uses incremental update via cache\r\n */\r\n public async handleFileChange(changedPath: string): Promise<void> {\r\n if (!this.shouldRegenerateTypes(changedPath)) {\r\n return;\r\n }\r\n\r\n // Regenerate storage types if storage config changed\r\n if (changedPath.includes(\"config/storage\")) {\r\n await this.generateStorageTypes(changedPath);\r\n await this.saveManifest();\r\n return;\r\n }\r\n\r\n if (this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath)) {\r\n await this.generateTranslationTypes();\r\n return;\r\n }\r\n\r\n // Extract config name from path\r\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\r\n if (!match) {\r\n return;\r\n }\r\n\r\n const configName = match[1];\r\n if (configName === \"index\") return;\r\n\r\n devLogInfo(`Config changed: ${configName}, updating...`);\r\n\r\n // Get file manager for hash\r\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\r\n const sourceHash = fileManager?.hash || Date.now().toString();\r\n\r\n // Update only the changed config in cache (use optimized combined extraction)\r\n const configDir = join(process.cwd(), \"src/config\");\r\n const configPath = join(configDir, `${configName}.ts`);\r\n\r\n const info = await this.extractConfigInfo(configPath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n\r\n // Regenerate config.d.ts from cache\r\n await this.writeConfigTypesFromCache();\r\n await this.saveManifest();\r\n }\r\n\r\n /**\r\n * Generate config types - populates cache and writes file\r\n */\r\n public async generateConfigTypes(): Promise<void> {\r\n try {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n // Clear and repopulate cache\r\n this.configCache.clear();\r\n\r\n for (const [path, fileManager] of files) {\r\n if (!path.startsWith(\"src/config/\")) continue;\r\n if (path.includes(\"index\")) continue;\r\n\r\n // Extract config name\r\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\r\n\r\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\r\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash: fileManager.hash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n }\r\n\r\n await this.writeConfigTypesFromCache();\r\n } catch (error) {\r\n devServeLog(`⚠️ Failed to generate config types: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Files (relative paths) that contributed to the last generated registry —\r\n * used to notice a registering file being deleted, since by the time a\r\n * delete event reaches {@link executeTypingsGenerator} the file is already\r\n * gone from `filesOrchestrator`'s map and its content can no longer be\r\n * inspected.\r\n */\r\n private translationSourceFiles = new Set<string>();\r\n\r\n /**\r\n * Generate web's app-augmented translation-key registry.\r\n *\r\n * Scans every discovered app source file — not only dedicated\r\n * `utils/locales.*` files — for `groupedTranslations` calls, both the\r\n * named-group and object forms, at any nesting depth. Dynamic group names,\r\n * dictionaries, or keys cannot be resolved statically and are skipped; an\r\n * app relying on those must document them itself.\r\n */\r\n private async generateTranslationTypes(): Promise<void> {\r\n const keys = new Set<string>();\r\n const sourceFiles = new Set<string>();\r\n\r\n for (const [path, fileManager] of filesOrchestrator.getFiles()) {\r\n if (!isTranslationRegisteringSource(fileManager.source)) {\r\n continue;\r\n }\r\n\r\n sourceFiles.add(path);\r\n\r\n const sourceFile = await readConfigAst(fileManager.absolutePath);\r\n if (sourceFile) {\r\n for (const key of extractTranslationKeys(sourceFile)) {\r\n keys.add(key);\r\n }\r\n }\r\n }\r\n\r\n for (const key of await listRouteLocaleKeysForTypeGeneration()) {\r\n keys.add(key);\r\n }\r\n\r\n this.translationSourceFiles = sourceFiles;\r\n\r\n const entries = Array.from(keys)\r\n .sort()\r\n .map((key) => ` ${JSON.stringify(key)}: true;`)\r\n .join(\"\\n\");\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Generated from groupedTranslations calls and route-locales JSON in discovered\r\n// app source. Dynamic group names/keys cannot be resolved statically and are\r\n// not included here — document those yourself.\r\n\r\nimport \"@warlock.js/web\";\r\n\r\ndeclare module \"@warlock.js/web\" {\r\n interface TranslationKeyRegistry {\r\n${entries}\r\n }\r\n}\r\n`;\r\n\r\n await writeFile(join(this.outputDir, \"translations.d.ts\"), content, \"utf-8\");\r\n devLogSuccess(`Generated translation types: ${keys.size} keys`);\r\n }\r\n\r\n private isLocalesFile(path: string): boolean {\r\n return Path.normalize(path).includes(\"/utils/locales.\");\r\n }\r\n\r\n /**\r\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\r\n * named `use-cases` or a storage driver `do-spaces`) so the generated\r\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\r\n * to keep the output clean.\r\n */\r\n private toInterfaceKey(name: string): string {\r\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\r\n }\r\n\r\n /**\r\n * Write config.d.ts from cached data\r\n */\r\n private async writeConfigTypesFromCache(): Promise<void> {\r\n const configTypeInfos: Array<{\r\n name: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n }> = [];\r\n const allKeys: string[] = [];\r\n\r\n // Update manifest config section\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.config = {};\r\n\r\n for (const [name, data] of this.configCache) {\r\n configTypeInfos.push({\r\n name,\r\n typeName: data.typeName,\r\n importSource: data.importSource,\r\n });\r\n allKeys.push(...data.keys);\r\n\r\n // Store in manifest\r\n this.manifest.config[name] = data;\r\n }\r\n\r\n // Group imports by source\r\n const imports = new Map<string, Set<string>>();\r\n for (const info of configTypeInfos) {\r\n if (info.typeName && info.importSource) {\r\n if (!imports.has(info.importSource)) {\r\n imports.set(info.importSource, new Set());\r\n }\r\n imports.get(info.importSource)!.add(info.typeName);\r\n }\r\n }\r\n\r\n const importStatements = Array.from(imports.entries())\r\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\r\n .join(\"\\n\");\r\n\r\n const configEntries = configTypeInfos\r\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\r\n .join(\"\\n\");\r\n\r\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\r\n\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Regenerates on dev-server start and when config files change\r\n\r\n${importStatements}\r\nimport \"@warlock.js/core\";\r\n\r\ndeclare module \"@warlock.js/core\" {\r\n interface ConfigRegistry {\r\n${configEntries}\r\n }\r\n\r\n interface ConfigKeyRegistry {\r\n${keyEntries}\r\n }\r\n}\r\n`;\r\n\r\n const outputPath = join(this.outputDir, \"config.d.ts\");\r\n await writeFile(outputPath, content, \"utf-8\");\r\n\r\n devLogSuccess(\r\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\r\n );\r\n }\r\n\r\n // ============================================================\r\n // Manifest Management\r\n // ============================================================\r\n\r\n /**\r\n * Load manifest from disk\r\n */\r\n private async loadManifest(): Promise<boolean> {\r\n try {\r\n if (await this.exists(this.manifestPath)) {\r\n const content = await readFile(this.manifestPath, \"utf-8\");\r\n this.manifest = JSON.parse(content);\r\n return true;\r\n }\r\n } catch {\r\n // Manifest corrupted or missing\r\n }\r\n this.manifest = null;\r\n return false;\r\n }\r\n\r\n /**\r\n * Save manifest to disk\r\n */\r\n private async saveManifest(): Promise<void> {\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.lastBuildTime = Date.now();\r\n\r\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\r\n }\r\n\r\n /**\r\n * Create empty manifest structure\r\n */\r\n private createEmptyManifest(): TypingsManifest {\r\n return {\r\n version: \"1.0.0\",\r\n lastBuildTime: Date.now(),\r\n storage: null,\r\n config: {},\r\n };\r\n }\r\n\r\n // ============================================================\r\n // Type Extraction Methods\r\n // ============================================================\r\n\r\n /**\r\n * Extract BOTH type info AND keys in a single pass\r\n *\r\n * One parse serves both extractions. It used to be one whole TypeScript\r\n * Program per file — halved from two by an earlier pass, which optimised\r\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\r\n * numbers.\r\n *\r\n * @param configPath Absolute path to the config file\r\n * @param configName Config name (e.g., \"auth\", \"notifications\")\r\n * @returns Combined result with type info and keys\r\n */\r\n private async extractConfigInfo(\r\n configPath: string,\r\n configName: string,\r\n ): Promise<{\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }> {\r\n if (!(await this.exists(configPath))) {\r\n return { typeName: null, importSource: null, keys: [] };\r\n }\r\n\r\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\r\n const sourceFile = await readConfigAst(configPath);\r\n\r\n if (!sourceFile) {\r\n return { typeName: null, importSource: null, keys: [] };\r\n }\r\n\r\n // === Type Info Extraction ===\r\n const importedTypes = new Map<string, string>();\r\n const localExportedTypes = new Set<string>();\r\n let foundTypeName: string | null = null;\r\n\r\n // === Keys Extraction ===\r\n const keys: string[] = [];\r\n\r\n const visitForTypes = (node: ts.Node): void => {\r\n // Collect imported types\r\n if (ts.isImportDeclaration(node)) {\r\n const moduleSpecifier = node.moduleSpecifier;\r\n if (ts.isStringLiteral(moduleSpecifier)) {\r\n const source = moduleSpecifier.text;\r\n const importClause = node.importClause;\r\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\r\n for (const element of importClause.namedBindings.elements) {\r\n importedTypes.set(element.name.text, source);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Collect locally exported types\r\n if (ts.isTypeAliasDeclaration(node)) {\r\n const modifiers = ts.getModifiers(node);\r\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\r\n localExportedTypes.add(node.name.text);\r\n }\r\n }\r\n\r\n // Collect locally exported interfaces\r\n if (ts.isInterfaceDeclaration(node)) {\r\n const modifiers = ts.getModifiers(node);\r\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\r\n localExportedTypes.add(node.name.text);\r\n }\r\n }\r\n\r\n // Find type used on config variable + extract keys\r\n if (ts.isVariableDeclaration(node)) {\r\n // Type info\r\n if (node.type && ts.isTypeReferenceNode(node.type)) {\r\n foundTypeName = node.type.typeName.getText(sourceFile);\r\n }\r\n\r\n // Keys extraction\r\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\r\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\r\n for (const prop of objNode.properties) {\r\n if (ts.isPropertyAssignment(prop) && prop.name) {\r\n const keyName = prop.name.getText(sourceFile);\r\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\r\n keys.push(fullKey);\r\n if (ts.isObjectLiteralExpression(prop.initializer)) {\r\n visitKeys(prop.initializer, fullKey);\r\n }\r\n }\r\n }\r\n };\r\n visitKeys(node.initializer, configName);\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visitForTypes);\r\n };\r\n\r\n ts.forEachChild(sourceFile, visitForTypes);\r\n\r\n // Resolve type info\r\n let typeName: string | null = null;\r\n let importSource: string | null = null;\r\n\r\n if (foundTypeName) {\r\n if (importedTypes.has(foundTypeName)) {\r\n typeName = foundTypeName;\r\n importSource = importedTypes.get(foundTypeName)!;\r\n } else if (localExportedTypes.has(foundTypeName)) {\r\n typeName = foundTypeName;\r\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\r\n importSource = `../../${relativePath}`;\r\n }\r\n }\r\n\r\n return { typeName, importSource, keys };\r\n }\r\n\r\n /**\r\n * Extract driver keys from storage config\r\n */\r\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\r\n const absolutePath = resolve(configPath);\r\n\r\n if (!(await this.exists(absolutePath))) {\r\n devServeLog(`⚠️ Storage config not found: ${absolutePath}`);\r\n return [];\r\n }\r\n\r\n const sourceFile = await readConfigAst(absolutePath);\r\n\r\n if (!sourceFile) {\r\n devServeLog(`⚠️ Could not parse storage config: ${absolutePath}`);\r\n return [];\r\n }\r\n\r\n const keys: string[] = [];\r\n\r\n const visit = (node: ts.Node): void => {\r\n if (ts.isPropertyAssignment(node)) {\r\n const propName = node.name.getText(sourceFile);\r\n\r\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\r\n for (const prop of node.initializer.properties) {\r\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\r\n const keyName = prop.name?.getText(sourceFile);\r\n\r\n if (keyName) {\r\n keys.push(keyName);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n };\r\n\r\n ts.forEachChild(sourceFile, visit);\r\n\r\n return keys;\r\n }\r\n\r\n /**\r\n * Find a config file by name\r\n */\r\n private async findConfigFile(configName: string): Promise<string | undefined> {\r\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\r\n\r\n for (const path of possiblePaths) {\r\n const fullPath = join(process.cwd(), path);\r\n\r\n if (await this.exists(fullPath)) {\r\n return path;\r\n }\r\n }\r\n\r\n try {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n for (const [filePath] of files) {\r\n if (filePath.includes(`config/${configName}`)) {\r\n return filePath;\r\n }\r\n }\r\n } catch {\r\n // Files orchestrator not initialized yet\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Ensure output directory exists\r\n */\r\n private async ensureOutputDir(): Promise<void> {\r\n await ensureDirectoryAsync(this.outputDir);\r\n }\r\n\r\n /**\r\n * Check if a path exists (async wrapper)\r\n */\r\n private async exists(path: string): Promise<boolean> {\r\n try {\r\n await access(path, constants.F_OK);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * The ports `runTypingsGeneration` needs, bound to this instance and this\r\n * package's dev logger.\r\n */\r\n private get typingsGenerationPorts(): TypingsGenerationPorts {\r\n return {\r\n generate: () => this.generateAll(),\r\n info: (message) => devLogInfo(message),\r\n success: (message) => devLogSuccess(message),\r\n error: (message) => devLogError(message),\r\n };\r\n }\r\n\r\n /**\r\n * Generate every config's typings.\r\n *\r\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\r\n * which could not resolve in a source checkout — see\r\n * `run-typings-generation.ts` for the whole story.\r\n */\r\n public async executeGenerateAllCommand(): Promise<void> {\r\n await runTypingsGeneration(this.typingsGenerationPorts);\r\n }\r\n\r\n /**\r\n * Regenerate typings after a batch reload — but only when the batch\r\n * plausibly touched something the typings depend on: a config file, a file\r\n * that registers translation keys, or a registering file that was deleted.\r\n *\r\n * A deleted file is already gone from `filesOrchestrator` by the time this\r\n * runs (deletes are processed before `dev-server:batch-complete` fires), so\r\n * its content can't be re-checked here — `translationSourceFiles` is the\r\n * set from the last generation, kept for exactly this comparison.\r\n */\r\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n const aKnownTranslationSourceVanished = Array.from(this.translationSourceFiles).some(\r\n (path) => !files.has(path),\r\n );\r\n\r\n const touchedAConfig =\r\n aKnownTranslationSourceVanished ||\r\n Array.from(new Set(upcomingFiles)).some((file) => {\r\n const normalizedPath = Path.normalize(file);\r\n if (normalizedPath.includes(\"src/config/\")) return true;\r\n if (isRouteLocalesJsonPath(normalizedPath)) return true;\r\n if (this.translationSourceFiles.has(normalizedPath)) return true;\r\n\r\n const fileManager = files.get(normalizedPath);\r\n return fileManager !== undefined && isTranslationRegisteringSource(fileManager.source);\r\n });\r\n\r\n if (!touchedAConfig) return;\r\n\r\n /*\r\n The changed paths are deliberately NOT passed along. The previous version\r\n built a `files` array here and then threw it away, spawning the same\r\n whole-project command as the branch above — so \"incremental\" was a name,\r\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\r\n reload has already updated, so the full pass is both correct and the only\r\n pass that ever actually ran.\r\n */\r\n await runTypingsGeneration(this.typingsGenerationPorts);\r\n }\r\n}\r\n\r\n/**\r\n * Singleton instance for use throughout dev-server\r\n */\r\nexport const typeGenerator = new TypeGenerator();\r\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsBA,MAAM,cAAc;AACpB,MAAM,oBAAoB;AAE1B,SAAS,2BAA2B,QAA0B;CAC5D,OACE,OAAO,WAAW,YAClB,WAAW,QACX,UAAU,UACT,OAA8B,SAAS,0BACxC,kBAAkB,SAClB,OAAO,QAAQ,SAAS,wBAAwB,YAAY,EAAE;AAElE;;AAGA,eAAsB,qCACpB,UAAU,QAAQ,IAAI,GACM;CAG5B,MAAM,aAAa,cAAc,QAAQ,SAAS,cAAc,CAAC;CAEjE,IAAI;EACF,cAAc,aAAa,UAAU;CACvC,SAAS,QAAQ;EACf,IAAI,2BAA2B,MAAM,GAAG,OAAO,CAAC;EAChD,MAAM;CACR;CAGA,MAAM,cAAe,MAAM,OADJ,cAAc,mBAAmB,UACT,CAAC,CAAC;CAEjD,IAAI,OAAO,YAAY,wBAAwB,YAC7C,MAAM,IAAI,MACR,GAAG,kBAAkB,uFACvB;CAGF,OAAO,YAAY,oBAAoB,EAAE,QAAQ,CAAC;AACpD;;AAGA,SAAgB,uBAAuB,MAAuB;CAC5D,MAAM,aAAa,KAAK,UAAU,IAAI;CACtC,MAAM,eAAe,WAAW,WAAW,MAAM,IAAI,aAAa,KAAK,WAAW,UAAU;CAC5F,OAAO,qCAAqC,KAAK,YAAY;AAC/D;;;;;;;;;AA+BA,IAAa,gBAAb,MAA2B;;mBAIL,YAAY,SAAS;sBAKlB,KAAK,KAAK,WAAW,uBAAuB;kBAKxB;qCAKrB,IAAI,IAQxB;gDAyQ+B,IAAI,IAAY;;;;;;;;;CAhQjD,MAAa,cAA6B;EACxC,MAAM,KAAK,gBAAgB;EAE3B,MAAM,cAAc,KAAK,KAAK,WAAW,cAAc;EACvD,MAAM,aAAa,KAAK,KAAK,WAAW,aAAa;EACrD,MAAM,mBAAmB,KAAK,KAAK,WAAW,mBAAmB;EAEjE,MAAM,CAAC,gBAAgB,eAAe,cAAc,qBAAqB,MAAM,QAAQ,IAAI;GACzF,KAAK,OAAO,KAAK,YAAY;GAC7B,KAAK,OAAO,WAAW;GACvB,KAAK,OAAO,UAAU;GACtB,KAAK,OAAO,gBAAgB;EAC9B,CAAC;EAED,IAAI,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,mBAEzD,MAAM,KAAK,eAAe;OACrB;GAEL,MAAM,KAAK,aAAa;GAExB,MAAM,KAAK,UAAU;EACvB;EAEA,MAAM,KAAK,yBAAyB;EAEpC,MAAM,KAAK,aAAa;CAC1B;;;;CAKA,MAAc,iBAAgC;EAE5C,MAAM,oBAAoB,MAAM,KAAK,eAAe,SAAS;EAC7D,IAAI,mBACF,KAAK,qBAAqB,iBAAiB;EAI7C,MAAM,KAAK,oBAAoB;CACjC;;;;CAKA,MAAc,YAA2B;EACvC,MAAM,QAAQ,kBAAkB,SAAS;EAEzC,IAAI,gBAAgB;EACpB,IAAI,iBAAiB;EAGrB,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAChC,IAAI,KAAK,WAAW,oBAAoB,GAAG;GACzC,MAAM,gBAAgB,KAAK,UAAU;GACrC,IAAI,CAAC,iBAAiB,cAAc,eAAe,YAAY,MAE7D,MAAM,KAAK,qBAAqB,IAAI;GAEtC;EACF;EAIF,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAAO;GACvC,IAAI,CAAC,KAAK,WAAW,aAAa,GAAG;GACrC,IAAI,KAAK,SAAS,OAAO,GAAG;GAG5B,MAAM,aAAa,KAAK,QAAQ,eAAe,EAAE,CAAC,CAAC,QAAQ,YAAY,EAAE;GAEzE,MAAM,gBAAgB,KAAK,UAAU,OAAO;GAE5C,IAAI,CAAC,iBAAiB,cAAc,eAAe,YAAY,MAAM;IAEnE,gBAAgB;IAChB,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,cAAc,UAAU;IAE9E,KAAK,YAAY,IAAI,YAAY;KAC/B,YAAY,YAAY;KACxB,UAAU,KAAK;KACf,cAAc,KAAK;KACnB,MAAM,KAAK;IACb,CAAC;GACH,OAAO;IAEL;IACA,KAAK,YAAY,IAAI,YAAY,aAAa;GAChD;EACF;EAEA,IAAI,eACF,MAAM,KAAK,0BAA0B;OAErC,WAAW,2BAA2B,eAAe,iBAAiB;CAE1E;;;;CAKA,MAAa,qBAAqB,YAAmC;EACnE,IAAI;GACF,MAAM,aAAa,MAAM,KAAK,yBAAyB,UAAU;GAEjE,IAAI,WAAW,WAAW,GAAG;IAC3B,YAAY,8CAA2C;IACvD;GACF;GAIA,MAAM,aADc,kBAAkB,SAAS,CAAC,CAAC,IAAI,UACxB,CAAC,EAAE,QAAQ;GAGxC,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;GAE3C,KAAK,SAAS,UAAU;IACtB;IACA,SAAS;GACX;GAMA,MAAM,UAAU;qBACD,WAAW;;;;;;;EALD,WACtB,KAAK,MAAM,OAAO,KAAK,eAAe,CAAC,EAAE,QAAQ,CAAC,CAClD,KAAK,IAUG,EAAE;;;;GAMb,MAAM,UADa,KAAK,KAAK,WAAW,cACf,GAAG,SAAS,OAAO;GAE5C,cAAc,4BAA4B,WAAW,KAAK,IAAI,GAAG;EACnE,SAAS,OAAO;GACd,YAAY,4CAA4C,OAAO;EACjE;CACF;;;;CAKA,AAAO,sBAAsB,aAA8B;EACzD,OACE,YAAY,SAAS,aAAa,KAClC,YAAY,SAAS,SAAS,KAC9B,KAAK,cAAc,WAAW,KAC9B,uBAAuB,WAAW;CAEtC;;;;CAKA,MAAa,iBAAiB,aAAoC;EAChE,IAAI,CAAC,KAAK,sBAAsB,WAAW,GACzC;EAIF,IAAI,YAAY,SAAS,gBAAgB,GAAG;GAC1C,MAAM,KAAK,qBAAqB,WAAW;GAC3C,MAAM,KAAK,aAAa;GACxB;EACF;EAEA,IAAI,KAAK,cAAc,WAAW,KAAK,uBAAuB,WAAW,GAAG;GAC1E,MAAM,KAAK,yBAAyB;GACpC;EACF;EAGA,MAAM,QAAQ,YAAY,MAAM,yBAAyB;EACzD,IAAI,CAAC,OACH;EAGF,MAAM,aAAa,MAAM;EACzB,IAAI,eAAe,SAAS;EAE5B,WAAW,mBAAmB,WAAW,cAAc;EAIvD,MAAM,aADc,kBAAkB,SAAS,CAAC,CAAC,IAAI,WACxB,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,SAAS;EAI5D,MAAM,aAAa,KADD,KAAK,QAAQ,IAAI,GAAG,YACN,GAAG,GAAG,WAAW,IAAI;EAErD,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,UAAU;EAEhE,KAAK,YAAY,IAAI,YAAY;GAC/B;GACA,UAAU,KAAK;GACf,cAAc,KAAK;GACnB,MAAM,KAAK;EACb,CAAC;EAGD,MAAM,KAAK,0BAA0B;EACrC,MAAM,KAAK,aAAa;CAC1B;;;;CAKA,MAAa,sBAAqC;EAChD,IAAI;GACF,MAAM,QAAQ,kBAAkB,SAAS;GAGzC,KAAK,YAAY,MAAM;GAEvB,KAAK,MAAM,CAAC,MAAM,gBAAgB,OAAO;IACvC,IAAI,CAAC,KAAK,WAAW,aAAa,GAAG;IACrC,IAAI,KAAK,SAAS,OAAO,GAAG;IAG5B,MAAM,aAAa,KAAK,QAAQ,eAAe,EAAE,CAAC,CAAC,QAAQ,YAAY,EAAE;IAGzE,MAAM,OAAO,MAAM,KAAK,kBAAkB,YAAY,cAAc,UAAU;IAE9E,KAAK,YAAY,IAAI,YAAY;KAC/B,YAAY,YAAY;KACxB,UAAU,KAAK;KACf,cAAc,KAAK;KACnB,MAAM,KAAK;IACb,CAAC;GACH;GAEA,MAAM,KAAK,0BAA0B;EACvC,SAAS,OAAO;GACd,YAAY,2CAA2C,OAAO;EAChE;CACF;;;;;;;;;;CAoBA,MAAc,2BAA0C;EACtD,MAAM,uBAAO,IAAI,IAAY;EAC7B,MAAM,8BAAc,IAAI,IAAY;EAEpC,KAAK,MAAM,CAAC,MAAM,gBAAgB,kBAAkB,SAAS,GAAG;GAC9D,IAAI,CAAC,+BAA+B,YAAY,MAAM,GACpD;GAGF,YAAY,IAAI,IAAI;GAEpB,MAAM,aAAa,MAAM,cAAc,YAAY,YAAY;GAC/D,IAAI,YACF,KAAK,MAAM,OAAO,uBAAuB,UAAU,GACjD,KAAK,IAAI,GAAG;EAGlB;EAEA,KAAK,MAAM,OAAO,MAAM,qCAAqC,GAC3D,KAAK,IAAI,GAAG;EAGd,KAAK,yBAAyB;EAM9B,MAAM,UAAU;;;;;;;;;EAJA,MAAM,KAAK,IAAI,CAAC,CAC7B,KAAK,CAAC,CACN,KAAK,QAAQ,OAAO,KAAK,UAAU,GAAG,EAAE,QAAQ,CAAC,CACjD,KAAK,IAUJ,EAAE;;;;EAKN,MAAM,UAAU,KAAK,KAAK,WAAW,mBAAmB,GAAG,SAAS,OAAO;EAC3E,cAAc,gCAAgC,KAAK,KAAK,MAAM;CAChE;CAEA,AAAQ,cAAc,MAAuB;EAC3C,OAAO,KAAK,UAAU,IAAI,CAAC,CAAC,SAAS,iBAAiB;CACxD;;;;;;;CAQA,AAAQ,eAAe,MAAsB;EAC3C,OAAO,6BAA6B,KAAK,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI;CAC7E;;;;CAKA,MAAc,4BAA2C;EACvD,MAAM,kBAID,CAAC;EACN,MAAM,UAAoB,CAAC;EAG3B,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;EAE3C,KAAK,SAAS,SAAS,CAAC;EAExB,KAAK,MAAM,CAAC,MAAM,SAAS,KAAK,aAAa;GAC3C,gBAAgB,KAAK;IACnB;IACA,UAAU,KAAK;IACf,cAAc,KAAK;GACrB,CAAC;GACD,QAAQ,KAAK,GAAG,KAAK,IAAI;GAGzB,KAAK,SAAS,OAAO,QAAQ;EAC/B;EAGA,MAAM,0BAAU,IAAI,IAAyB;EAC7C,KAAK,MAAM,QAAQ,iBACjB,IAAI,KAAK,YAAY,KAAK,cAAc;GACtC,IAAI,CAAC,QAAQ,IAAI,KAAK,YAAY,GAChC,QAAQ,IAAI,KAAK,8BAAc,IAAI,IAAI,CAAC;GAE1C,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAE,IAAI,KAAK,QAAQ;EACnD;EAaF,MAAM,UAAU;;;EAVS,MAAM,KAAK,QAAQ,QAAQ,CAAC,CAAC,CACnD,KAAK,CAAC,QAAQ,WAAW,iBAAiB,MAAM,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,WAAW,OAAO,GAAG,CAAC,CAC7F,KAAK,IAWK,EAAE;;;;;EATO,gBACnB,KAAK,SAAS,OAAO,KAAK,eAAe,KAAK,IAAI,EAAE,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC,CACtF,KAAK,IAYE,EAAE;;;;EAVO,QAAQ,KAAK,QAAQ,QAAQ,IAAI,SAAS,CAAC,CAAC,KAAK,IAc7D,EAAE;;;;EAMT,MAAM,UADa,KAAK,KAAK,WAAW,aACf,GAAG,SAAS,OAAO;EAE5C,cACE,2BAA2B,KAAK,YAAY,KAAK,YAAY,QAAQ,OAAO,MAC9E;CACF;;;;CASA,MAAc,eAAiC;EAC7C,IAAI;GACF,IAAI,MAAM,KAAK,OAAO,KAAK,YAAY,GAAG;IACxC,MAAM,UAAU,MAAM,SAAS,KAAK,cAAc,OAAO;IACzD,KAAK,WAAW,KAAK,MAAM,OAAO;IAClC,OAAO;GACT;EACF,QAAQ,CAER;EACA,KAAK,WAAW;EAChB,OAAO;CACT;;;;CAKA,MAAc,eAA8B;EAC1C,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,oBAAoB;EAE3C,KAAK,SAAS,gBAAgB,KAAK,IAAI;EAEvC,MAAM,UAAU,KAAK,cAAc,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,GAAG,OAAO;CACpF;;;;CAKA,AAAQ,sBAAuC;EAC7C,OAAO;GACL,SAAS;GACT,eAAe,KAAK,IAAI;GACxB,SAAS;GACT,QAAQ,CAAC;EACX;CACF;;;;;;;;;;;;;CAkBA,MAAc,kBACZ,YACA,YAKC;EACD,IAAI,CAAE,MAAM,KAAK,OAAO,UAAU,GAChC,OAAO;GAAE,UAAU;GAAM,cAAc;GAAM,MAAM,CAAC;EAAE;EAIxD,MAAM,aAAa,MAAM,cAAc,UAAU;EAEjD,IAAI,CAAC,YACH,OAAO;GAAE,UAAU;GAAM,cAAc;GAAM,MAAM,CAAC;EAAE;EAIxD,MAAM,gCAAgB,IAAI,IAAoB;EAC9C,MAAM,qCAAqB,IAAI,IAAY;EAC3C,IAAI,gBAA+B;EAGnC,MAAM,OAAiB,CAAC;EAExB,MAAM,iBAAiB,SAAwB;GAE7C,IAAI,GAAG,oBAAoB,IAAI,GAAG;IAChC,MAAM,kBAAkB,KAAK;IAC7B,IAAI,GAAG,gBAAgB,eAAe,GAAG;KACvC,MAAM,SAAS,gBAAgB;KAC/B,MAAM,eAAe,KAAK;KAC1B,IAAI,cAAc,iBAAiB,GAAG,eAAe,aAAa,aAAa,GAC7E,KAAK,MAAM,WAAW,aAAa,cAAc,UAC/C,cAAc,IAAI,QAAQ,KAAK,MAAM,MAAM;IAGjD;GACF;GAGA,IAAI,GAAG,uBAAuB,IAAI,GAEhC;QADkB,GAAG,aAAa,IACtB,CAAC,EAAE,MAAM,MAAM,EAAE,SAAS,GAAG,WAAW,aAAa,GAC/D,mBAAmB,IAAI,KAAK,KAAK,IAAI;GACvC;GAIF,IAAI,GAAG,uBAAuB,IAAI,GAEhC;QADkB,GAAG,aAAa,IACtB,CAAC,EAAE,MAAM,MAAM,EAAE,SAAS,GAAG,WAAW,aAAa,GAC/D,mBAAmB,IAAI,KAAK,KAAK,IAAI;GACvC;GAIF,IAAI,GAAG,sBAAsB,IAAI,GAAG;IAElC,IAAI,KAAK,QAAQ,GAAG,oBAAoB,KAAK,IAAI,GAC/C,gBAAgB,KAAK,KAAK,SAAS,QAAQ,UAAU;IAIvD,IAAI,KAAK,eAAe,GAAG,0BAA0B,KAAK,WAAW,GAAG;KACtE,MAAM,aAAa,SAAqC,WAAyB;MAC/E,KAAK,MAAM,QAAQ,QAAQ,YACzB,IAAI,GAAG,qBAAqB,IAAI,KAAK,KAAK,MAAM;OAC9C,MAAM,UAAU,KAAK,KAAK,QAAQ,UAAU;OAC5C,MAAM,UAAU,SAAS,GAAG,OAAO,GAAG,YAAY;OAClD,KAAK,KAAK,OAAO;OACjB,IAAI,GAAG,0BAA0B,KAAK,WAAW,GAC/C,UAAU,KAAK,aAAa,OAAO;MAEvC;KAEJ;KACA,UAAU,KAAK,aAAa,UAAU;IACxC;GACF;GAEA,GAAG,aAAa,MAAM,aAAa;EACrC;EAEA,GAAG,aAAa,YAAY,aAAa;EAGzC,IAAI,WAA0B;EAC9B,IAAI,eAA8B;EAElC,IAAI,eACF;OAAI,cAAc,IAAI,aAAa,GAAG;IACpC,WAAW;IACX,eAAe,cAAc,IAAI,aAAa;GAChD,OAAO,IAAI,mBAAmB,IAAI,aAAa,GAAG;IAChD,WAAW;IAEX,eAAe,SADM,KAAK,WAAW,UAAU,CAAC,CAAC,QAAQ,eAAe,EACrC;GACrC;;EAGF,OAAO;GAAE;GAAU;GAAc;EAAK;CACxC;;;;CAKA,MAAc,yBAAyB,YAAuC;EAC5E,MAAM,eAAe,QAAQ,UAAU;EAEvC,IAAI,CAAE,MAAM,KAAK,OAAO,YAAY,GAAI;GACtC,YAAY,oCAAoC,cAAc;GAC9D,OAAO,CAAC;EACV;EAEA,MAAM,aAAa,MAAM,cAAc,YAAY;EAEnD,IAAI,CAAC,YAAY;GACf,YAAY,0CAA0C,cAAc;GACpE,OAAO,CAAC;EACV;EAEA,MAAM,OAAiB,CAAC;EAExB,MAAM,SAAS,SAAwB;GACrC,IAAI,GAAG,qBAAqB,IAAI,GAG9B;QAFiB,KAAK,KAAK,QAAQ,UAExB,MAAM,aAAa,GAAG,0BAA0B,KAAK,WAAW,GACzE;UAAK,MAAM,QAAQ,KAAK,YAAY,YAClC,IAAI,GAAG,qBAAqB,IAAI,KAAK,GAAG,8BAA8B,IAAI,GAAG;MAC3E,MAAM,UAAU,KAAK,MAAM,QAAQ,UAAU;MAE7C,IAAI,SACF,KAAK,KAAK,OAAO;KAErB;IACF;GACF;GAGF,GAAG,aAAa,MAAM,KAAK;EAC7B;EAEA,GAAG,aAAa,YAAY,KAAK;EAEjC,OAAO;CACT;;;;CAKA,MAAc,eAAe,YAAiD;EAC5E,MAAM,gBAAgB,CAAC,cAAc,WAAW,MAAM,UAAU,WAAW,IAAI;EAE/E,KAAK,MAAM,QAAQ,eAAe;GAChC,MAAM,WAAW,KAAK,QAAQ,IAAI,GAAG,IAAI;GAEzC,IAAI,MAAM,KAAK,OAAO,QAAQ,GAC5B,OAAO;EAEX;EAEA,IAAI;GACF,MAAM,QAAQ,kBAAkB,SAAS;GAEzC,KAAK,MAAM,CAAC,aAAa,OACvB,IAAI,SAAS,SAAS,UAAU,YAAY,GAC1C,OAAO;EAGb,QAAQ,CAER;CAGF;;;;CAKA,MAAc,kBAAiC;EAC7C,MAAM,qBAAqB,KAAK,SAAS;CAC3C;;;;CAKA,MAAc,OAAO,MAAgC;EACnD,IAAI;GACF,MAAM,OAAO,MAAM,UAAU,IAAI;GACjC,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;;;;CAMA,IAAY,yBAAiD;EAC3D,OAAO;GACL,gBAAgB,KAAK,YAAY;GACjC,OAAO,YAAY,WAAW,OAAO;GACrC,UAAU,YAAY,cAAc,OAAO;GAC3C,QAAQ,YAAY,YAAY,OAAO;EACzC;CACF;;;;;;;;CASA,MAAa,4BAA2C;EACtD,MAAM,qBAAqB,KAAK,sBAAsB;CACxD;;;;;;;;;;;CAYA,MAAa,wBAAwB,eAAwC;EAC3E,MAAM,QAAQ,kBAAkB,SAAS;EAkBzC,IAAI,EAhBoC,MAAM,KAAK,KAAK,sBAAsB,CAAC,CAAC,MAC7E,SAAS,CAAC,MAAM,IAAI,IAAI,CAIK,KAC9B,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC,CAAC,CAAC,MAAM,SAAS;GAChD,MAAM,iBAAiB,KAAK,UAAU,IAAI;GAC1C,IAAI,eAAe,SAAS,aAAa,GAAG,OAAO;GACnD,IAAI,uBAAuB,cAAc,GAAG,OAAO;GACnD,IAAI,KAAK,uBAAuB,IAAI,cAAc,GAAG,OAAO;GAE5D,MAAM,cAAc,MAAM,IAAI,cAAc;GAC5C,OAAO,gBAAgB,UAAa,+BAA+B,YAAY,MAAM;EACvF,CAAC,IAEkB;EAUrB,MAAM,qBAAqB,KAAK,sBAAsB;CACxD;AACF;;;;AAKA,MAAa,gBAAgB,IAAI,cAAc"}
@@ -0,0 +1,118 @@
1
+ import ts from "typescript";
2
+
3
+ //#region ../core/src/generations/features/shared/merge-web-sitemap-config.ts
4
+ /**
5
+ * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.
6
+ *
7
+ * **String surgery, never parse-and-print** — same rationale as
8
+ * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting
9
+ * or comments a parse-and-print would discard. The TypeScript AST is used to
10
+ * *find where to cut*, never to regenerate the file — the splice below still
11
+ * copies the original source's bytes verbatim.
12
+ *
13
+ * Recognises the exported config object two ways: an inline `export default
14
+ * {...}`, or `export default <identifier>;` pointing back at a `const
15
+ * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared
16
+ * earlier in the file — the shape this generator itself scaffolds, and the
17
+ * shape every other generated config file in this project uses. Any other
18
+ * shape (a function call, a re-export, no default export at all) returns
19
+ * `"unrecognised"` rather than guessing at where to cut — clobbering an
20
+ * app-owned file the merge could not actually understand is worse than
21
+ * refusing.
22
+ *
23
+ * A `sitemap` property already present anywhere in the source (by key, so one
24
+ * a human has since hand-edited is still found) is left unchanged — that is
25
+ * what makes a second `warlock add sitemap` a no-op instead of a duplicate
26
+ * block. Detection walks the AST rather than regex-matching `sitemap\s*:`, so
27
+ * a comment like `// sitemap: TODO` — text, not a property — does not count.
28
+ *
29
+ * Before returning a merge, the resulting text is re-parsed and checked for
30
+ * syntax errors; a merge that would not itself parse is refused (as
31
+ * `"unrecognised"`) instead of written, since a matched-but-corrupt splice is
32
+ * worse than doing nothing.
33
+ *
34
+ * @param source The config file's current text.
35
+ * @returns What happened, and the new text when there is any.
36
+ */
37
+ function mergeWebSitemapConfig(source) {
38
+ const sourceFile = ts.createSourceFile("web.ts", source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
39
+ const objectLiteral = findExportedConfigObjectLiteral(sourceFile);
40
+ if (!objectLiteral) return { status: "unrecognised" };
41
+ if (hasTopLevelSitemapProperty(objectLiteral)) return { status: "already-present" };
42
+ const bodyStart = objectLiteral.getStart(sourceFile) + 1;
43
+ const closingBraceIndex = objectLiteral.getEnd() - 1;
44
+ const body = source.slice(bodyStart, closingBraceIndex);
45
+ const indentMatch = /\n([ \t]*)\S/.exec(body);
46
+ const indent = indentMatch ? indentMatch[1] : " ";
47
+ const block = `${indent}sitemap: {\n${indent}${indent}enabled: false,\n${indent}${indent}path: "/sitemap.xml",\n${indent}${indent}defaults: { changefreq: "weekly", priority: 0.5 },\n${indent}},\n`;
48
+ const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);
49
+ const next = `${source.slice(0, insertionPoint)}${needsComma ? ",\n" : ""}${block}${source.slice(closingBraceIndex)}`;
50
+ if (hasSyntaxErrors(next)) return { status: "unrecognised" };
51
+ return {
52
+ status: "merged",
53
+ next
54
+ };
55
+ }
56
+ /** Whether TEXT fails to parse as TypeScript source. */
57
+ function hasSyntaxErrors(text) {
58
+ const { diagnostics } = ts.transpileModule(text, {
59
+ compilerOptions: {
60
+ module: ts.ModuleKind.ESNext,
61
+ target: ts.ScriptTarget.ESNext
62
+ },
63
+ reportDiagnostics: true
64
+ });
65
+ return Boolean(diagnostics && diagnostics.length > 0);
66
+ }
67
+ /**
68
+ * Whether the exported config object literal already has a `sitemap`
69
+ * property directly on it — a `sitemap` key nested under some other
70
+ * property (e.g. `seo: { sitemap: true }`) is a different feature's field
71
+ * and must not block adding the top-level one.
72
+ */
73
+ function hasTopLevelSitemapProperty(objectLiteral) {
74
+ return objectLiteral.properties.some((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && propertyKeyName(property.name) === "sitemap");
75
+ }
76
+ /** The literal text of a property name node, when it is one of the plain shapes we care about. */
77
+ function propertyKeyName(name) {
78
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
79
+ }
80
+ /**
81
+ * Find the object literal a source file's default export ultimately points
82
+ * at, whether the export is inline or an identifier declared earlier in the
83
+ * file.
84
+ */
85
+ function findExportedConfigObjectLiteral(sourceFile) {
86
+ const exportAssignment = sourceFile.statements.find((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
87
+ if (!exportAssignment) return void 0;
88
+ const { expression } = exportAssignment;
89
+ if (ts.isObjectLiteralExpression(expression)) return expression;
90
+ if (!ts.isIdentifier(expression)) return void 0;
91
+ const identifier = expression.text;
92
+ for (const statement of sourceFile.statements) {
93
+ if (!ts.isVariableStatement(statement)) continue;
94
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) return declaration.initializer;
95
+ }
96
+ }
97
+ /**
98
+ * Where to insert a new property right before a closing brace, and whether
99
+ * the property immediately before it (if any) needs a trailing comma added —
100
+ * an object literal with two properties and no comma between them does not
101
+ * parse.
102
+ */
103
+ function lastPropertyEnd(source, closingBraceIndex) {
104
+ let cursor = closingBraceIndex - 1;
105
+ while (cursor >= 0 && /\s/.test(source.charAt(cursor))) cursor--;
106
+ if (cursor < 0 || source[cursor] === "{" || source[cursor] === ",") return {
107
+ index: closingBraceIndex,
108
+ needsComma: false
109
+ };
110
+ return {
111
+ index: cursor + 1,
112
+ needsComma: true
113
+ };
114
+ }
115
+
116
+ //#endregion
117
+ export { mergeWebSitemapConfig };
118
+ //# sourceMappingURL=merge-web-sitemap-config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-web-sitemap-config.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/merge-web-sitemap-config.ts"],"sourcesContent":["import ts from \"typescript\";\n\n/** What {@link mergeWebSitemapConfig} did, or could not do, to the source it was given. */\nexport type WebSitemapMergeResult =\n { status: \"merged\"; next: string } | { status: \"already-present\" } | { status: \"unrecognised\" };\n\n/**\n * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting\n * or comments a parse-and-print would discard. The TypeScript AST is used to\n * *find where to cut*, never to regenerate the file — the splice below still\n * copies the original source's bytes verbatim.\n *\n * Recognises the exported config object two ways: an inline `export default\n * {...}`, or `export default <identifier>;` pointing back at a `const\n * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared\n * earlier in the file — the shape this generator itself scaffolds, and the\n * shape every other generated config file in this project uses. Any other\n * shape (a function call, a re-export, no default export at all) returns\n * `\"unrecognised\"` rather than guessing at where to cut — clobbering an\n * app-owned file the merge could not actually understand is worse than\n * refusing.\n *\n * A `sitemap` property already present anywhere in the source (by key, so one\n * a human has since hand-edited is still found) is left unchanged — that is\n * what makes a second `warlock add sitemap` a no-op instead of a duplicate\n * block. Detection walks the AST rather than regex-matching `sitemap\\s*:`, so\n * a comment like `// sitemap: TODO` — text, not a property — does not count.\n *\n * Before returning a merge, the resulting text is re-parsed and checked for\n * syntax errors; a merge that would not itself parse is refused (as\n * `\"unrecognised\"`) instead of written, since a matched-but-corrupt splice is\n * worse than doing nothing.\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 mergeWebSitemapConfig(source: string): WebSitemapMergeResult {\n const sourceFile = ts.createSourceFile(\n \"web.ts\",\n source,\n ts.ScriptTarget.ESNext,\n /* setParentNodes */ true,\n ts.ScriptKind.TS,\n );\n\n const objectLiteral = findExportedConfigObjectLiteral(sourceFile);\n\n if (!objectLiteral) {\n return { status: \"unrecognised\" };\n }\n\n if (hasTopLevelSitemapProperty(objectLiteral)) {\n return { status: \"already-present\" };\n }\n\n const bodyStart = objectLiteral.getStart(sourceFile) + 1;\n const closingBraceIndex = objectLiteral.getEnd() - 1;\n const body = source.slice(bodyStart, closingBraceIndex);\n\n const indentMatch = /\\n([ \\t]*)\\S/.exec(body);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n const block =\n `${indent}sitemap: {\\n` +\n `${indent}${indent}enabled: false,\\n` +\n `${indent}${indent}path: \"/sitemap.xml\",\\n` +\n `${indent}${indent}defaults: { changefreq: \"weekly\", priority: 0.5 },\\n` +\n `${indent}},\\n`;\n\n const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);\n\n const next =\n `${source.slice(0, insertionPoint)}` +\n `${needsComma ? \",\\n\" : \"\"}` +\n `${block}` +\n `${source.slice(closingBraceIndex)}`;\n\n if (hasSyntaxErrors(next)) {\n return { status: \"unrecognised\" };\n }\n\n return { status: \"merged\", next };\n}\n\n/** Whether TEXT fails to parse as TypeScript source. */\nfunction hasSyntaxErrors(text: string): boolean {\n const { diagnostics } = ts.transpileModule(text, {\n compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext },\n reportDiagnostics: true,\n });\n\n return Boolean(diagnostics && diagnostics.length > 0);\n}\n\n/**\n * Whether the exported config object literal already has a `sitemap`\n * property directly on it — a `sitemap` key nested under some other\n * property (e.g. `seo: { sitemap: true }`) is a different feature's field\n * and must not block adding the top-level one.\n */\nfunction hasTopLevelSitemapProperty(objectLiteral: ts.ObjectLiteralExpression): boolean {\n return objectLiteral.properties.some(\n (property) =>\n (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&\n propertyKeyName(property.name) === \"sitemap\",\n );\n}\n\n/** The literal text of a property name node, when it is one of the plain shapes we care about. */\nfunction propertyKeyName(name: ts.PropertyName): string | undefined {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {\n return name.text;\n }\n\n return undefined;\n}\n\n/**\n * Find the object literal a source file's default export ultimately points\n * at, whether the export is inline or an identifier declared earlier in the\n * file.\n */\nfunction findExportedConfigObjectLiteral(\n sourceFile: ts.SourceFile,\n): ts.ObjectLiteralExpression | undefined {\n const exportAssignment = sourceFile.statements.find(\n (statement): statement is ts.ExportAssignment =>\n ts.isExportAssignment(statement) && !statement.isExportEquals,\n );\n\n if (!exportAssignment) return undefined;\n\n const { expression } = exportAssignment;\n\n if (ts.isObjectLiteralExpression(expression)) {\n return expression;\n }\n\n if (!ts.isIdentifier(expression)) return undefined;\n\n const identifier = expression.text;\n\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n\n for (const declaration of statement.declarationList.declarations) {\n if (\n ts.isIdentifier(declaration.name) &&\n declaration.name.text === identifier &&\n declaration.initializer &&\n ts.isObjectLiteralExpression(declaration.initializer)\n ) {\n return declaration.initializer;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Where to insert a new property right before a closing brace, and whether\n * the property immediately before it (if any) needs a trailing comma added —\n * an object literal with two properties and no comma between them does not\n * parse.\n */\nfunction lastPropertyEnd(\n source: string,\n closingBraceIndex: number,\n): { index: number; needsComma: boolean } {\n let cursor = closingBraceIndex - 1;\n\n while (cursor >= 0 && /\\s/.test(source.charAt(cursor))) {\n cursor--;\n }\n\n if (cursor < 0 || source[cursor] === \"{\" || source[cursor] === \",\") {\n return { index: closingBraceIndex, needsComma: false };\n }\n\n return { index: cursor + 1, needsComma: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,sBAAsB,QAAuC;CAC3E,MAAM,aAAa,GAAG,iBACpB,UACA,QACA,GAAG,aAAa,QACK,MACrB,GAAG,WAAW,EAChB;CAEA,MAAM,gBAAgB,gCAAgC,UAAU;CAEhE,IAAI,CAAC,eACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,IAAI,2BAA2B,aAAa,GAC1C,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,YAAY,cAAc,SAAS,UAAU,IAAI;CACvD,MAAM,oBAAoB,cAAc,OAAO,IAAI;CACnD,MAAM,OAAO,OAAO,MAAM,WAAW,iBAAiB;CAEtD,MAAM,cAAc,eAAe,KAAK,IAAI;CAC5C,MAAM,SAAS,cAAc,YAAY,KAAK;CAE9C,MAAM,QACJ,GAAG,OAAO,cACP,SAAS,OAAO,mBAChB,SAAS,OAAO,yBAChB,SAAS,OAAO,sDAChB,OAAO;CAEZ,MAAM,EAAE,OAAO,gBAAgB,eAAe,gBAAgB,QAAQ,iBAAiB;CAEvF,MAAM,OACJ,GAAG,OAAO,MAAM,GAAG,cAAc,IAC9B,aAAa,QAAQ,KACrB,QACA,OAAO,MAAM,iBAAiB;CAEnC,IAAI,gBAAgB,IAAI,GACtB,OAAO,EAAE,QAAQ,eAAe;CAGlC,OAAO;EAAE,QAAQ;EAAU;CAAK;AAClC;;AAGA,SAAS,gBAAgB,MAAuB;CAC9C,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,MAAM;EAC/C,iBAAiB;GAAE,QAAQ,GAAG,WAAW;GAAQ,QAAQ,GAAG,aAAa;EAAO;EAChF,mBAAmB;CACrB,CAAC;CAED,OAAO,QAAQ,eAAe,YAAY,SAAS,CAAC;AACtD;;;;;;;AAQA,SAAS,2BAA2B,eAAoD;CACtF,OAAO,cAAc,WAAW,MAC7B,cACE,GAAG,qBAAqB,QAAQ,KAAK,GAAG,8BAA8B,QAAQ,MAC/E,gBAAgB,SAAS,IAAI,MAAM,SACvC;AACF;;AAGA,SAAS,gBAAgB,MAA2C;CAClE,IAAI,GAAG,aAAa,IAAI,KAAK,GAAG,gBAAgB,IAAI,KAAK,GAAG,iBAAiB,IAAI,GAC/E,OAAO,KAAK;AAIhB;;;;;;AAOA,SAAS,gCACP,YACwC;CACxC,MAAM,mBAAmB,WAAW,WAAW,MAC5C,cACC,GAAG,mBAAmB,SAAS,KAAK,CAAC,UAAU,cACnD;CAEA,IAAI,CAAC,kBAAkB,OAAO;CAE9B,MAAM,EAAE,eAAe;CAEvB,IAAI,GAAG,0BAA0B,UAAU,GACzC,OAAO;CAGT,IAAI,CAAC,GAAG,aAAa,UAAU,GAAG,OAAO;CAEzC,MAAM,aAAa,WAAW;CAE9B,KAAK,MAAM,aAAa,WAAW,YAAY;EAC7C,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG;EAExC,KAAK,MAAM,eAAe,UAAU,gBAAgB,cAClD,IACE,GAAG,aAAa,YAAY,IAAI,KAChC,YAAY,KAAK,SAAS,cAC1B,YAAY,eACZ,GAAG,0BAA0B,YAAY,WAAW,GAEpD,OAAO,YAAY;CAGzB;AAGF;;;;;;;AAQA,SAAS,gBACP,QACA,mBACwC;CACxC,IAAI,SAAS,oBAAoB;CAEjC,OAAO,UAAU,KAAK,KAAK,KAAK,OAAO,OAAO,MAAM,CAAC,GACnD;CAGF,IAAI,SAAS,KAAK,OAAO,YAAY,OAAO,OAAO,YAAY,KAC7D,OAAO;EAAE,OAAO;EAAmB,YAAY;CAAM;CAGvD,OAAO;EAAE,OAAO,SAAS;EAAG,YAAY;CAAK;AAC/C"}
@@ -1,72 +1,75 @@
1
- import { rootPath } from "../../utils/paths.mjs";
1
+ import { srcPath } 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
+ import { mergeWebSitemapConfig } from "./shared/merge-web-sitemap-config.mjs";
5
5
  import { colors } from "@mongez/copper";
6
6
  import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
7
7
 
8
8
  //#region ../core/src/generations/features/sitemap.feature.ts
9
- const sitemapConfigStub = `import type { SitemapConfig } from "@warlock.js/sitemap";
10
-
9
+ const NEXT_STEPS = "Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment variable, then flip `sitemap.enabled` to true in src/config/web.ts.";
11
10
  /**
12
- * Runtime sitemap.xml generation.
13
- *
14
- * Ships DISABLED because a sitemap needs this application's public origin and
15
- * a freshly generated app has no way to know it. Two steps to turn it on:
16
- *
17
- * 1. set \`app.publicUrl\` in src/config/app.ts, or the PUBLIC_APP_URL
18
- * environment variable;
19
- * 2. flip \`enabled\` to true here.
20
- *
21
- * With it enabled and no origin configured, boot REFUSES rather than serving
22
- * absolute URLs built from a guessed host — a sitemap pointing at the wrong
23
- * domain is worse than one that never starts, because nothing downstream
24
- * reports it.
25
- */
26
- const sitemapConfig: SitemapConfig = {
27
- enabled: false,
28
- path: "/sitemap.xml",
29
- defaults: { changefreq: "weekly", priority: 0.5 },
11
+ * Ships DISABLED because a sitemap needs this application's public origin and
12
+ * a freshly generated app has no way to know it. Two steps to turn it on:
13
+ *
14
+ * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL
15
+ * environment variable;
16
+ * 2. flip `sitemap.enabled` to true here.
17
+ *
18
+ * With it enabled and no origin configured, generation REFUSES rather than
19
+ * serving absolute URLs built from a guessed host — a sitemap pointing at
20
+ * the wrong domain is worse than one that never starts, because nothing
21
+ * downstream reports it.
22
+ */
23
+ const freshWebConfigStub = `import type { WebSitemapConfig } from "@warlock.js/web/sitemap";
24
+
25
+ const webConfig: { sitemap: WebSitemapConfig } = {
26
+ sitemap: {
27
+ enabled: false,
28
+ path: "/sitemap.xml",
29
+ defaults: { changefreq: "weekly", priority: 0.5 },
30
+ },
30
31
  };
31
32
 
32
- export default sitemapConfig;
33
+ export default webConfig;
33
34
  `;
34
- /** Register the sitemap connector in the app-owned configuration without reformatting it. */
35
- async function registerSitemapConnector() {
36
- const configPath = rootPath("warlock.config.ts");
35
+ /**
36
+ * `warlock add sitemap` writes its policy into the app's \`src/config/web.ts\`,
37
+ * under the \`sitemap\` key — there is no \`src/config/sitemap.ts\` and no
38
+ * standalone connector to register; \`@warlock.js/web\` (already installed via
39
+ * the \`web\` requirement) reads \`web.sitemap\` itself.
40
+ *
41
+ * Creates \`web.ts\` when the app does not have one yet; otherwise merges a
42
+ * \`sitemap\` section into whatever is already there rather than clobbering it.
43
+ */
44
+ async function installSitemapConfig() {
45
+ const configPath = srcPath("config/web.ts");
37
46
  if (!await fileExistsAsync(configPath)) {
38
- console.log(`${colors.yellowBright("warlock.config.ts")} not found — add this yourself:\n import { sitemapConnector } from "@warlock.js/sitemap";\n export default defineConfig({ connectors: [sitemapConnector()] });`);
47
+ await putFileAsync(configPath, freshWebConfigStub);
48
+ console.log(`${colors.green("✓")} Created src/config/web.ts with a disabled sitemap section`);
49
+ console.log(NEXT_STEPS);
39
50
  return;
40
51
  }
41
- const current = await getFileAsync(configPath);
42
- if (current.includes("sitemapConnector")) {
43
- console.log(`${colors.yellowBright("sitemapConnector")} already registered, skipping...`);
52
+ const merge = mergeWebSitemapConfig(await getFileAsync(configPath));
53
+ if (merge.status === "already-present") {
54
+ console.log(`${colors.yellowBright("sitemap")} already configured in src/config/web.ts, skipping...`);
44
55
  return;
45
56
  }
46
- const importLine = "import { sitemapConnector } from \"@warlock.js/sitemap\";";
47
- let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
48
- const insertion = insertConnectorEntry(next, "sitemapConnector()");
49
- if (insertion.status === "already-present") return;
50
- if (insertion.status === "added") next = insertion.next;
51
- else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [sitemapConnector()],\n");
52
- else {
53
- console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [sitemapConnector()]\` yourself.`);
57
+ if (merge.status === "unrecognised") {
58
+ console.log(`${colors.redBright("✗")} Could not safely add a \`sitemap\` section to ${colors.yellowBright("src/config/web.ts")} its exported config object was not recognised, so nothing was written. Add it yourself:
59
+ sitemap: { enabled: false, path: "/sitemap.xml", defaults: { changefreq: "weekly", priority: 0.5 } },`);
60
+ process.exitCode = 1;
54
61
  return;
55
62
  }
56
- await putFileAsync(configPath, next);
57
- console.log(`${colors.green("✓")} Registered sitemapConnector in warlock.config.ts`);
58
- console.log("Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment variable — the sitemap route refuses to boot without it.");
63
+ await putFileAsync(configPath, merge.next);
64
+ console.log(`${colors.green("✓")} Added a disabled \`sitemap\` section to src/config/web.ts`);
65
+ console.log(NEXT_STEPS);
59
66
  }
60
67
  /** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */
61
68
  const sitemapFeature = {
62
- description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Creates src/config/sitemap.ts and registers sitemapConnector() in warlock.config.ts. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.",
69
+ description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.",
63
70
  requires: ["web"],
64
71
  dependencies: { "@warlock.js/sitemap": INSTALLED_WARLOCK_VERSION },
65
- ejectConfig: {
66
- content: sitemapConfigStub,
67
- name: "sitemap"
68
- },
69
- onExecuting: registerSitemapConnector
72
+ onExecuting: installSitemapConfig
70
73
  };
71
74
 
72
75
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.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 sitemapConfigStub = `import type { SitemapConfig } from \"@warlock.js/sitemap\";\n\n/**\n * Runtime sitemap.xml generation.\n *\n * Ships DISABLED because a sitemap needs this application's public origin and\n * a freshly generated app has no way to know it. Two steps to turn it on:\n *\n * 1. set \\`app.publicUrl\\` in src/config/app.ts, or the PUBLIC_APP_URL\n * environment variable;\n * 2. flip \\`enabled\\` to true here.\n *\n * With it enabled and no origin configured, boot REFUSES rather than serving\n * absolute URLs built from a guessed host — a sitemap pointing at the wrong\n * domain is worse than one that never starts, because nothing downstream\n * reports it.\n */\nconst sitemapConfig: SitemapConfig = {\n enabled: false,\n path: \"/sitemap.xml\",\n defaults: { changefreq: \"weekly\", priority: 0.5 },\n};\n\nexport default sitemapConfig;\n`;\n\n/** Register the sitemap connector in the app-owned configuration without reformatting it. */\nasync function registerSitemapConnector(): 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 { sitemapConnector } from \"@warlock.js/sitemap\";\\n` +\n ` export default defineConfig({ connectors: [sitemapConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"sitemapConnector\")) {\n console.log(`${colors.yellowBright(\"sitemapConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { sitemapConnector } from \"@warlock.js/sitemap\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n const insertion = insertConnectorEntry(next, \"sitemapConnector()\");\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(\n \"defineConfig({\",\n \"defineConfig({\\n connectors: [sitemapConnector()],\\n\",\n );\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) ` +\n \"add `connectors: [sitemapConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered sitemapConnector in warlock.config.ts`);\n console.log(\n \"Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment \" +\n \"variable — the sitemap route refuses to boot without it.\",\n );\n}\n\n/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */\nexport const sitemapFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Creates src/config/sitemap.ts and registers sitemapConnector() in warlock.config.ts. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.\",\n requires: [\"web\"],\n dependencies: {\n \"@warlock.js/sitemap\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: sitemapConfigStub,\n name: \"sitemap\",\n },\n onExecuting: registerSitemapConnector,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B1B,eAAe,2BAA0C;CACvD,MAAM,aAAa,SAAS,mBAAmB;CAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,iKAG9C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,UAAU;CAE7C,IAAI,QAAQ,SAAS,kBAAkB,GAAG;EACxC,QAAQ,IAAI,GAAG,OAAO,aAAa,kBAAkB,EAAE,iCAAiC;EAExF;CACF;CAEA,MAAM,aAAa;CACnB,IAAI,OAAO,QAAQ,SAAS,UAAU,IAAI,UAAU,GAAG,WAAW,IAAI;CAEtE,MAAM,YAAY,qBAAqB,MAAM,oBAAoB;CAEjE,IAAI,UAAU,WAAW,mBACvB;CAGF,IAAI,UAAU,WAAW,SACvB,OAAO,UAAU;MACZ,IAAI,KAAK,SAAS,gBAAgB,GACvC,OAAO,KAAK,QACV,kBACA,uDACF;MACK;EACL,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,8FAE9C;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,IAAI;CACnC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kDAAkD;CACnF,QAAQ,IACN,0IAEF;AACF;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CACF,UAAU,CAAC,KAAK;CAChB,cAAc,EACZ,uBAAuB,0BACzB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { srcPath } from \"../../utils\";\nimport { mergeWebSitemapConfig } from \"./shared/merge-web-sitemap-config\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst NEXT_STEPS =\n \"Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment \" +\n \"variable, then flip `sitemap.enabled` to true in src/config/web.ts.\";\n\n/**\n * Ships DISABLED because a sitemap needs this application's public origin and\n * a freshly generated app has no way to know it. Two steps to turn it on:\n *\n * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL\n * environment variable;\n * 2. flip `sitemap.enabled` to true here.\n *\n * With it enabled and no origin configured, generation REFUSES rather than\n * serving absolute URLs built from a guessed host — a sitemap pointing at\n * the wrong domain is worse than one that never starts, because nothing\n * downstream reports it.\n */\nconst freshWebConfigStub = `import type { WebSitemapConfig } from \"@warlock.js/web/sitemap\";\n\nconst webConfig: { sitemap: WebSitemapConfig } = {\n sitemap: {\n enabled: false,\n path: \"/sitemap.xml\",\n defaults: { changefreq: \"weekly\", priority: 0.5 },\n },\n};\n\nexport default webConfig;\n`;\n\n/**\n * `warlock add sitemap` writes its policy into the app's \\`src/config/web.ts\\`,\n * under the \\`sitemap\\` key — there is no \\`src/config/sitemap.ts\\` and no\n * standalone connector to register; \\`@warlock.js/web\\` (already installed via\n * the \\`web\\` requirement) reads \\`web.sitemap\\` itself.\n *\n * Creates \\`web.ts\\` when the app does not have one yet; otherwise merges a\n * \\`sitemap\\` section into whatever is already there rather than clobbering it.\n */\nasync function installSitemapConfig(): Promise<void> {\n const configPath = srcPath(\"config/web.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n await putFileAsync(configPath, freshWebConfigStub);\n console.log(`${colors.green(\"✓\")} Created src/config/web.ts with a disabled sitemap section`);\n console.log(NEXT_STEPS);\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n const merge = mergeWebSitemapConfig(current);\n\n if (merge.status === \"already-present\") {\n console.log(\n `${colors.yellowBright(\"sitemap\")} already configured in src/config/web.ts, skipping...`,\n );\n\n return;\n }\n\n if (merge.status === \"unrecognised\") {\n console.log(\n `${colors.redBright(\"✗\")} Could not safely add a \\`sitemap\\` section to ` +\n `${colors.yellowBright(\"src/config/web.ts\")} its exported config object was not ` +\n \"recognised, so nothing was written. Add it yourself:\\n\" +\n ' sitemap: { enabled: false, path: \"/sitemap.xml\", defaults: { changefreq: \"weekly\", priority: 0.5 } },',\n );\n\n process.exitCode = 1;\n\n return;\n }\n\n await putFileAsync(configPath, merge.next);\n console.log(`${colors.green(\"✓\")} Added a disabled \\`sitemap\\` section to src/config/web.ts`);\n console.log(NEXT_STEPS);\n}\n\n/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */\nexport const sitemapFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. \" +\n \"Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't \" +\n \"exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.\",\n requires: [\"web\"],\n dependencies: {\n \"@warlock.js/sitemap\": INSTALLED_WARLOCK_VERSION,\n },\n onExecuting: installSitemapConfig,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,aACJ;;;;;;;;;;;;;;AAgBF,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;AAsB3B,eAAe,uBAAsC;CACnD,MAAM,aAAa,QAAQ,eAAe;CAE1C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,MAAM,aAAa,YAAY,kBAAkB;EACjD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;EAC5F,QAAQ,IAAI,UAAU;EAEtB;CACF;CAGA,MAAM,QAAQ,sBAAsB,MADd,aAAa,UAAU,CACF;CAE3C,IAAI,MAAM,WAAW,mBAAmB;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,SAAS,EAAE,sDACpC;EAEA;CACF;CAEA,IAAI,MAAM,WAAW,gBAAgB;EACnC,QAAQ,IACN,GAAG,OAAO,UAAU,GAAG,EAAE,iDACpB,OAAO,aAAa,mBAAmB,EAAE;wGAGhD;EAEA,QAAQ,WAAW;EAEnB;CACF;CAEA,MAAM,aAAa,YAAY,MAAM,IAAI;CACzC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;CAC5F,QAAQ,IAAI,UAAU;AACxB;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CAGF,UAAU,CAAC,KAAK;CAChB,cAAc,EACZ,uBAAuB,0BACzB;CACA,aAAa;AACf"}