@warlock.js/core 5.16.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 +22 -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/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 +7 -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/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.map +1 -1
- package/esm/http/response.mjs +7 -1
- 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/index.d.mts +9 -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 +17 -1
- package/package.json +22 -21
- 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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"}
|
|
@@ -724,7 +724,7 @@ import { setCurrentLocaleCode } from "@mongez/localization";
|
|
|
724
724
|
import { Form, useFormControl, type FormControlProps } from "@mongez/react-form";
|
|
725
725
|
import { transX } from "@mongez/react-localization";
|
|
726
726
|
import { v } from "@warlock.js/seal";
|
|
727
|
-
import { Link, type PageProps } from "@warlock.js/web";
|
|
727
|
+
import { Link, type PageConfig, type PageProps } from "@warlock.js/web";
|
|
728
728
|
import { useState } from "react";
|
|
729
729
|
|
|
730
730
|
export { register } from "./index.register";
|
|
@@ -734,13 +734,14 @@ export { register } from "./index.register";
|
|
|
734
734
|
* instead of returning JSON.
|
|
735
735
|
*
|
|
736
736
|
* The URL and stable hydration name are the ones this file DECLARES below.
|
|
737
|
-
* This page answers \`GET "/"\` because \`route.path = "/"\`, not because of
|
|
737
|
+
* This page answers \`GET "/"\` because \`config.route.path = "/"\`, not because of
|
|
738
738
|
* where the file lives. A page file with
|
|
739
|
-
* no \`route\`
|
|
739
|
+
* no \`config.route\` declaration derives its URL from the filesystem.
|
|
740
740
|
*/
|
|
741
|
-
export const
|
|
742
|
-
|
|
743
|
-
|
|
741
|
+
export const config = {
|
|
742
|
+
route: { path: "/", name: "index" },
|
|
743
|
+
metadata: { title: "Home" },
|
|
744
|
+
} as const satisfies PageConfig;
|
|
744
745
|
|
|
745
746
|
const contactSchema = v.object({
|
|
746
747
|
name: v.string().min(2),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n */\nconst notificationSchema = v.object({\n user_id: v.string(),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration + accessors all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. Spread it to add your own columns\n * (remember to mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\nimport { inApp, type Id } from \"@warlock.js/notifications\";\n\n/**\n * The authenticated user's notification HTTP surface — thin wrappers over the\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\n * rows). Notifications are produced by domain events, never over HTTP, so there\n * is no create. Trim or split these as your app grows.\n */\n\n/**\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\n */\nfunction recipientId(user: unknown): Id {\n if (user && typeof user === \"object\" && \"id\" in user) {\n const id = (user as { id?: unknown }).id;\n\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n\n throw new Error(\"Authenticated request is missing a usable user id\");\n}\n\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\n const id = request.input(\"id\");\n const userId = recipientId(request.locals.user);\n\n await inApp.markAsRead(userId, id);\n const notification = await inApp.find(userId, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n\n/** DELETE /notifications — dismiss all for the user. */\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user));\n\n return response.noContent();\n};\n\nclearNotificationsController.description = \"Clear notifications\";\n\n/** DELETE /notifications/:id — dismiss one. */\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\n\n return response.noContent();\n};\n\ndeleteNotificationController.description = \"Delete notification\";\n`;\n\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport {\n clearNotificationsController,\n deleteNotificationController,\n listNotificationsController,\n markAllNotificationsReadController,\n markNotificationReadController,\n unreadNotificationsCountController,\n} from \"./controllers/notifications.controller\";\n\n/**\n * Notification routes — the authenticated user's read + dismiss surface.\n *\n * Notifications are produced by domain events (never created over HTTP), so\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\n * don't need; if your app reads notifications over sockets/GraphQL instead,\n * delete this file + the controllers entirely.\n */\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\n router.get(\"/\", listNotificationsController);\n router.get(\"/unread-count\", unreadNotificationsCountController);\n router.patch(\"/read-all\", markAllNotificationsReadController);\n router.patch(\"/:id/read\", markNotificationReadController);\n router.delete(\"/\", clearNotificationsController);\n router.delete(\"/:id\", deleteNotificationController);\n});\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController);\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`route\\` export is REFUSED by both the dev server and the build.\n */\nexport const route = { path: \"/\", name: \"index\" } as const;\n\nexport const metadata = { title: \"Home\" };\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n */\nconst notificationSchema = v.object({\n user_id: v.string(),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration + accessors all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. Spread it to add your own columns\n * (remember to mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\nimport { inApp, type Id } from \"@warlock.js/notifications\";\n\n/**\n * The authenticated user's notification HTTP surface — thin wrappers over the\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\n * rows). Notifications are produced by domain events, never over HTTP, so there\n * is no create. Trim or split these as your app grows.\n */\n\n/**\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\n */\nfunction recipientId(user: unknown): Id {\n if (user && typeof user === \"object\" && \"id\" in user) {\n const id = (user as { id?: unknown }).id;\n\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n\n throw new Error(\"Authenticated request is missing a usable user id\");\n}\n\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\n const id = request.input(\"id\");\n const userId = recipientId(request.locals.user);\n\n await inApp.markAsRead(userId, id);\n const notification = await inApp.find(userId, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n\n/** DELETE /notifications — dismiss all for the user. */\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user));\n\n return response.noContent();\n};\n\nclearNotificationsController.description = \"Clear notifications\";\n\n/** DELETE /notifications/:id — dismiss one. */\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\n\n return response.noContent();\n};\n\ndeleteNotificationController.description = \"Delete notification\";\n`;\n\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport {\n clearNotificationsController,\n deleteNotificationController,\n listNotificationsController,\n markAllNotificationsReadController,\n markNotificationReadController,\n unreadNotificationsCountController,\n} from \"./controllers/notifications.controller\";\n\n/**\n * Notification routes — the authenticated user's read + dismiss surface.\n *\n * Notifications are produced by domain events (never created over HTTP), so\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\n * don't need; if your app reads notifications over sockets/GraphQL instead,\n * delete this file + the controllers entirely.\n */\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\n router.get(\"/\", listNotificationsController);\n router.get(\"/unread-count\", unreadNotificationsCountController);\n router.patch(\"/read-all\", markAllNotificationsReadController);\n router.patch(\"/:id/read\", markNotificationReadController);\n router.delete(\"/\", clearNotificationsController);\n router.delete(\"/:id\", deleteNotificationController);\n});\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController);\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageConfig, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`config.route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`config.route\\` declaration derives its URL from the filesystem.\n */\nexport const config = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n} as const satisfies PageConfig;\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Response } from "./response.mjs";
|
|
2
|
+
import { Request } from "./request.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/http/csrf-default-guard.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether the current request is in scope for the default core CSRF-Origin
|
|
7
|
+
* guard at all.
|
|
8
|
+
*/
|
|
9
|
+
declare function requiresDefaultCsrfGuard(request: Request): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Run the default core CSRF-Origin guard for the current request.
|
|
12
|
+
*
|
|
13
|
+
* Returns `undefined` when the request is out of scope or passes the
|
|
14
|
+
* Origin/Referer check — the caller continues into route middleware and the
|
|
15
|
+
* handler as normal. Returns the 403 {@link Response} when the guard refuses
|
|
16
|
+
* the request; the caller must return that value immediately, the same
|
|
17
|
+
* short-circuit contract every other middleware in the chain follows.
|
|
18
|
+
*
|
|
19
|
+
* Uses the SAME translated message and client-visible error code
|
|
20
|
+
* `authMiddleware("cookie:*")`'s own CSRF-Origin check uses
|
|
21
|
+
* (`auth.errors.csrfOriginMismatch`, `HttpErrorCodes.CsrfOriginMismatch` /
|
|
22
|
+
* `AuthErrorCodes.CsrfOriginMismatch`, both `"EC006"`) — one message, one
|
|
23
|
+
* error code, regardless of which seam caught the request. Logs exactly once
|
|
24
|
+
* per rejection here; a route that also carries `authMiddleware("cookie:*")`
|
|
25
|
+
* never reaches that middleware's own check for a request THIS guard already
|
|
26
|
+
* rejected, because a guard rejection short-circuits `runMiddleware()`
|
|
27
|
+
* entirely — so a rejected request is never logged twice.
|
|
28
|
+
*/
|
|
29
|
+
declare function runDefaultCsrfGuard(request: Request, response: Response, translate: (key: string) => string, logRejection: (reason: string) => void): Promise<Response | undefined>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { requiresDefaultCsrfGuard, runDefaultCsrfGuard };
|
|
32
|
+
//# sourceMappingURL=csrf-default-guard.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf-default-guard.d.mts","names":[],"sources":["../../../../../../../core/src/http/csrf-default-guard.ts"],"mappings":";;;;;;AAyIA;;iBAAgB,wBAAA,CAAyB,OAAgB,EAAP,OAAO;;AAAA;AA2BzD;;;;;;;;;;;;;;;;;iBAAsB,mBAAA,CACpB,OAAA,EAAS,OAAA,EACT,QAAA,EAAU,QAAA,EACV,SAAA,GAAY,GAAA,qBACZ,YAAA,GAAe,MAAA,oBACd,OAAA,CAAQ,QAAA"}
|