@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.
- package/CHANGELOG.md +33 -0
- package/esm/cli/commands/migrate.command.mjs +5 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs +2 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +16 -23
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +7 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +1 -18
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/read-bound-port.mjs +32 -0
- package/esm/connectors/read-bound-port.mjs.map +1 -0
- package/esm/dev-server/development-server.mjs +5 -1
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/extract-translation-keys.mjs +68 -0
- package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
- package/esm/dev-server/translation-keys-sources.mjs +19 -0
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +64 -11
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
- package/esm/generations/features/sitemap.feature.mjs +50 -47
- package/esm/generations/features/sitemap.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +7 -6
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/csrf-default-guard.d.mts +32 -0
- package/esm/http/csrf-default-guard.d.mts.map +1 -0
- package/esm/http/csrf-default-guard.mjs +156 -0
- package/esm/http/csrf-default-guard.mjs.map +1 -0
- package/esm/http/csrf-origin-policy.d.mts +53 -0
- package/esm/http/csrf-origin-policy.d.mts.map +1 -0
- package/esm/http/csrf-origin-policy.mjs +104 -0
- package/esm/http/csrf-origin-policy.mjs.map +1 -0
- package/esm/http/error-codes.d.mts +14 -1
- package/esm/http/error-codes.d.mts.map +1 -1
- package/esm/http/error-codes.mjs +13 -0
- package/esm/http/error-codes.mjs.map +1 -1
- package/esm/http/index.d.mts +8 -1
- package/esm/http/index.mjs +7 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +6 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/parse-urlencoded-body.mjs +27 -0
- package/esm/http/parse-urlencoded-body.mjs.map +1 -0
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +8 -0
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +2 -2
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +7 -3
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +9 -2
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +18 -4
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/uploads/detect-image-format.mjs +59 -0
- package/esm/http/uploads/detect-image-format.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variant.mjs +46 -0
- package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variants.d.mts +49 -0
- package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
- package/esm/http/uploads/generate-image-variants.mjs +141 -0
- package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
- package/esm/http/uploads/image-variant-types.d.mts +93 -0
- package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
- package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.mjs +17 -0
- package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
- package/esm/http/uploads/index.d.mts +4 -0
- package/esm/http/uploads/index.mjs +5 -0
- package/esm/http/uploads/is-path-inside.mjs +26 -0
- package/esm/http/uploads/is-path-inside.mjs.map +1 -0
- package/esm/http/uploads/load-variant-source.mjs +42 -0
- package/esm/http/uploads/load-variant-source.mjs.map +1 -0
- package/esm/http/uploads/matches-if-none-match.mjs +13 -0
- package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
- package/esm/http/uploads/read-file-head.mjs +20 -0
- package/esm/http/uploads/read-file-head.mjs.map +1 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
- package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-upload-path.mjs +56 -0
- package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
- package/esm/http/uploads/single-flight.mjs +40 -0
- package/esm/http/uploads/single-flight.mjs.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
- package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
- package/esm/http/uploads/variant-cache-key.mjs +45 -0
- package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
- package/esm/http/uploads/variant-generations.mjs +16 -0
- package/esm/http/uploads/variant-generations.mjs.map +1 -0
- package/esm/http/uploads-types.d.mts +102 -1
- package/esm/http/uploads-types.d.mts.map +1 -1
- package/esm/http/xmlable.d.mts +14 -0
- package/esm/http/xmlable.d.mts.map +1 -0
- package/esm/index.d.mts +10 -2
- package/esm/index.mjs +8 -2
- package/esm/router/types.d.mts +23 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/index.d.mts +1 -0
- package/esm/storage/index.mjs +1 -0
- package/esm/storage/scoped-storage.d.mts +1 -0
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +3 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +2 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +4 -0
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
- package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
- package/llms-full.txt +47 -7
- package/llms.txt +1 -1
- package/package.json +22 -21
- package/skills/configure-app/SKILL.md +6 -6
- package/skills/create-controller/SKILL.md +14 -0
- package/skills/health-checks/SKILL.md +8 -0
- package/skills/send-response/SKILL.md +2 -0
- package/skills/use-localization/SKILL.md +16 -0
- package/skills/use-middleware/SKILL.md +1 -1
- package/esm/dev-server/translation-type-generator.mjs +0 -28
- 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-
|
|
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
|
-
/**
|
|
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 (!
|
|
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
|
|
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
|
|
463
|
-
* touched a config file,
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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":"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"}
|
|
@@ -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 {
|
|
1
|
+
import { srcPath } from "../../utils/paths.mjs";
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
4
|
-
import {
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
33
|
+
export default webConfig;
|
|
33
34
|
`;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
if (
|
|
43
|
-
console.log(`${colors.yellowBright("
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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("✓")}
|
|
58
|
-
console.log(
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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"}
|