@warlock.js/core 5.25.0 → 5.27.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 +53 -0
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +5 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/openapi-generator.action.mjs +43 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.command.mjs +44 -0
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
- package/esm/cli/commands/typings-generator.command.mjs +4 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/build-development-openapi-document.mjs +36 -0
- package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
- package/esm/dev-server/collect-development-api-routes.mjs +27 -0
- package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs +9 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +6 -3
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +17 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/legacy-locales-warning.mjs +16 -0
- package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
- package/esm/dev-server/model-resource-typings.mjs +246 -0
- package/esm/dev-server/model-resource-typings.mjs.map +1 -0
- package/esm/dev-server/module-loader.mjs +16 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +2 -2
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +7 -3
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +73 -6
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +38 -18
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +8 -2
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs +1 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +1 -17
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -20
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +4 -3
- package/esm/http/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/request.d.mts +9 -26
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +1 -22
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/serialized.d.mts +114 -0
- package/esm/http/serialized.d.mts.map +1 -0
- package/esm/index.d.mts +12 -5
- package/esm/index.mjs +8 -2
- package/esm/localization/index.mjs +4 -0
- package/esm/localization/parse-locale-dictionary.d.mts +23 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/parse-locale-dictionary.mjs +215 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
- package/esm/localization/register-locale-dictionary.d.mts +12 -0
- package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/register-locale-dictionary.mjs +58 -0
- package/esm/localization/register-locale-dictionary.mjs.map +1 -0
- package/esm/openapi/build-openapi-document.d.mts +17 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -0
- package/esm/openapi/build-openapi-document.mjs +124 -0
- package/esm/openapi/build-openapi-document.mjs.map +1 -0
- package/esm/openapi/cast-schema.mjs +89 -0
- package/esm/openapi/cast-schema.mjs.map +1 -0
- package/esm/openapi/collect-openapi-document.mjs +40 -0
- package/esm/openapi/collect-openapi-document.mjs.map +1 -0
- package/esm/openapi/development-openapi-provider.d.mts +17 -0
- package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
- package/esm/openapi/development-openapi-provider.mjs +21 -0
- package/esm/openapi/development-openapi-provider.mjs.map +1 -0
- package/esm/openapi/error-responses.mjs +67 -0
- package/esm/openapi/error-responses.mjs.map +1 -0
- package/esm/openapi/index.d.mts +3 -0
- package/esm/openapi/index.mjs +4 -0
- package/esm/openapi/openapi-child-output.mjs +57 -0
- package/esm/openapi/openapi-child-output.mjs.map +1 -0
- package/esm/openapi/openapi-protocol.mjs +30 -0
- package/esm/openapi/openapi-protocol.mjs.map +1 -0
- package/esm/openapi/paths.mjs +63 -0
- package/esm/openapi/paths.mjs.map +1 -0
- package/esm/openapi/request-schemas.mjs +138 -0
- package/esm/openapi/request-schemas.mjs.map +1 -0
- package/esm/openapi/response-schemas.mjs +85 -0
- package/esm/openapi/response-schemas.mjs.map +1 -0
- package/esm/openapi/schema-registry.mjs +143 -0
- package/esm/openapi/schema-registry.mjs.map +1 -0
- package/esm/openapi/security.mjs +98 -0
- package/esm/openapi/security.mjs.map +1 -0
- package/esm/openapi/types.d.mts +119 -0
- package/esm/openapi/types.d.mts.map +1 -0
- package/esm/production/production-builder.mjs +11 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/registration-child-runner.mjs +139 -0
- package/esm/production/registration-child-runner.mjs.map +1 -0
- package/esm/production/route-registration-child.mjs +30 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +22 -119
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/define-resource.d.mts +44 -14
- package/esm/resource/define-resource.d.mts.map +1 -1
- package/esm/resource/define-resource.mjs +0 -40
- package/esm/resource/define-resource.mjs.map +1 -1
- package/esm/resource/index.d.mts +1 -1
- package/esm/resource/resource.d.mts +4 -4
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts +89 -4
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/restful/restful.d.mts +5 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/router/index.d.mts +1 -1
- package/esm/router/named-api-routes-with-responses.mjs +59 -0
- package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
- package/esm/router/response-schema-types.mjs +100 -0
- package/esm/router/response-schema-types.mjs.map +1 -0
- package/esm/router/router.d.mts +2 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +38 -11
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +24 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.d.mts +10 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +16 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +48 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +383 -80
- package/llms.txt +4 -3
- package/package.json +21 -21
- package/skills/README.md +4 -3
- package/skills/create-controller/SKILL.md +43 -2
- package/skills/define-resource/SKILL.md +100 -1
- package/skills/generate-openapi/SKILL.md +120 -0
- package/skills/index.md +3 -3
- package/skills/test-http/SKILL.md +9 -1
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/validate-input/SKILL.md +2 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative } from "node:path";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/dev-server/model-resource-typings.ts
|
|
6
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
7
|
+
function hasModifier(node, kind) {
|
|
8
|
+
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
|
|
9
|
+
}
|
|
10
|
+
function unwrapParentheses(expression) {
|
|
11
|
+
let current = expression;
|
|
12
|
+
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
/** Top-level names this file declares, and the names each is exported under. */
|
|
16
|
+
function readLocalDeclarations(sourceFile) {
|
|
17
|
+
const declared = /* @__PURE__ */ new Set();
|
|
18
|
+
const exportNames = /* @__PURE__ */ new Map();
|
|
19
|
+
const addExport = (local, exported) => {
|
|
20
|
+
const names = exportNames.get(local) ?? [];
|
|
21
|
+
names.push(exported);
|
|
22
|
+
exportNames.set(local, names);
|
|
23
|
+
};
|
|
24
|
+
for (const statement of sourceFile.statements) {
|
|
25
|
+
const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);
|
|
26
|
+
const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
|
|
27
|
+
if ((ts.isClassDeclaration(statement) || ts.isFunctionDeclaration(statement) || ts.isEnumDeclaration(statement)) && statement.name) {
|
|
28
|
+
declared.add(statement.name.text);
|
|
29
|
+
if (exported) addExport(statement.name.text, isDefault ? "default" : statement.name.text);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ts.isVariableStatement(statement)) {
|
|
33
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
34
|
+
if (!ts.isIdentifier(declaration.name)) continue;
|
|
35
|
+
declared.add(declaration.name.text);
|
|
36
|
+
if (exported) addExport(declaration.name.text, declaration.name.text);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && !statement.isTypeOnly && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
41
|
+
for (const element of statement.exportClause.elements) {
|
|
42
|
+
if (element.isTypeOnly) continue;
|
|
43
|
+
addExport((element.propertyName ?? element.name).text, element.name.text);
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) addExport(statement.expression.text, "default");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
declared,
|
|
51
|
+
exportNames
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Prefer a named export over `default`; among named exports, the first sorted. */
|
|
55
|
+
function pickExportName(names) {
|
|
56
|
+
if (!names || names.length === 0) return void 0;
|
|
57
|
+
return names.filter((name) => name !== "default").sort()[0] ?? "default";
|
|
58
|
+
}
|
|
59
|
+
function readImportBindings(sourceFile) {
|
|
60
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const statement of sourceFile.statements) {
|
|
62
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
63
|
+
const clause = statement.importClause;
|
|
64
|
+
const specifier = statement.moduleSpecifier.text;
|
|
65
|
+
if (!clause) continue;
|
|
66
|
+
if (clause.name) bindings.set(clause.name.text, {
|
|
67
|
+
specifier,
|
|
68
|
+
exportName: "default"
|
|
69
|
+
});
|
|
70
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) bindings.set(element.name.text, {
|
|
71
|
+
specifier,
|
|
72
|
+
exportName: (element.propertyName ?? element.name).text
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return bindings;
|
|
76
|
+
}
|
|
77
|
+
/** The identifier a class assigns to `static resource`, when it is a bare identifier. */
|
|
78
|
+
function readStaticResourceReference(member) {
|
|
79
|
+
if (!ts.isPropertyDeclaration(member)) return void 0;
|
|
80
|
+
if (!hasModifier(member, ts.SyntaxKind.StaticKeyword)) return void 0;
|
|
81
|
+
if (hasModifier(member, ts.SyntaxKind.DeclareKeyword)) return void 0;
|
|
82
|
+
if (!((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) && member.name.text === "resource")) return void 0;
|
|
83
|
+
if (!member.initializer) return void 0;
|
|
84
|
+
const initializer = unwrapParentheses(member.initializer);
|
|
85
|
+
return ts.isIdentifier(initializer) ? { name: initializer.text } : "computed";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read every model class of one file that declares `static resource = <Identifier>`
|
|
89
|
+
* and resolve it to a model export plus a resource export. Pure: the only
|
|
90
|
+
* outside knowledge is the injected {@link ExtractModelResourcesInput.resolveImport}.
|
|
91
|
+
*/
|
|
92
|
+
function extractModelResourceEntries(input) {
|
|
93
|
+
const result = {
|
|
94
|
+
entries: [],
|
|
95
|
+
skipped: []
|
|
96
|
+
};
|
|
97
|
+
if (!input.source.includes("resource")) return result;
|
|
98
|
+
const sourceFile = ts.createSourceFile(input.absolutePath, input.source, ts.ScriptTarget.ESNext, false, input.absolutePath.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
99
|
+
const { declared, exportNames } = readLocalDeclarations(sourceFile);
|
|
100
|
+
const imports = readImportBindings(sourceFile);
|
|
101
|
+
for (const statement of sourceFile.statements) {
|
|
102
|
+
if (!ts.isClassDeclaration(statement) || !statement.name) continue;
|
|
103
|
+
const className = statement.name.text;
|
|
104
|
+
let reference;
|
|
105
|
+
for (const member of statement.members) reference = readStaticResourceReference(member) ?? reference;
|
|
106
|
+
if (reference === void 0) continue;
|
|
107
|
+
const skip = (reason) => {
|
|
108
|
+
result.skipped.push({
|
|
109
|
+
className,
|
|
110
|
+
modelFile: input.absolutePath,
|
|
111
|
+
reason
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
if (reference === "computed") {
|
|
115
|
+
skip("`static resource` is not a plain identifier");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (statement.typeParameters?.some((parameter) => parameter.default === void 0)) {
|
|
119
|
+
skip("the model class is generic");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const modelExport = pickExportName(exportNames.get(className));
|
|
123
|
+
if (modelExport === void 0) {
|
|
124
|
+
skip("the model class is not exported");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (modelExport !== "default" && !IDENTIFIER.test(modelExport)) {
|
|
128
|
+
skip("the model export name is not an identifier");
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const binding = imports.get(reference.name);
|
|
132
|
+
let resourceFile;
|
|
133
|
+
let resourceExport;
|
|
134
|
+
if (binding) {
|
|
135
|
+
resourceFile = input.resolveImport(binding.specifier);
|
|
136
|
+
resourceExport = binding.exportName;
|
|
137
|
+
} else if (declared.has(reference.name)) {
|
|
138
|
+
resourceFile = input.absolutePath;
|
|
139
|
+
resourceExport = pickExportName(exportNames.get(reference.name));
|
|
140
|
+
}
|
|
141
|
+
if (resourceFile === void 0 || resourceExport === void 0) {
|
|
142
|
+
skip(`\`${reference.name}\` cannot be resolved to an exported module member`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (resourceExport !== "default" && !IDENTIFIER.test(resourceExport)) {
|
|
146
|
+
skip("the resource export name is not an identifier");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
result.entries.push({
|
|
150
|
+
className,
|
|
151
|
+
modelFile: input.absolutePath,
|
|
152
|
+
modelExport,
|
|
153
|
+
resourceFile,
|
|
154
|
+
resourceExport
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
/** Module specifier, relative to the typings directory, without a TypeScript extension. */
|
|
160
|
+
function toTypeImportSpecifier(typingsDirectory, absoluteFile) {
|
|
161
|
+
const path = relative(typingsDirectory, absoluteFile).replaceAll("\\", "/");
|
|
162
|
+
if (isAbsolute(path)) return void 0;
|
|
163
|
+
const withoutExtension = path.replace(/\.(?:d\.)?(?:ts|tsx|mts|cts)$/, "");
|
|
164
|
+
return withoutExtension.startsWith(".") ? withoutExtension : `./${withoutExtension}`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Registry keys: the class name, suffixed `_2`, `_3`, ... when several model
|
|
168
|
+
* files declare the same class name. Order is by class name, then by file path,
|
|
169
|
+
* so the same set of models always yields the same keys and the same text.
|
|
170
|
+
*/
|
|
171
|
+
function assignRegistryKeys(entries) {
|
|
172
|
+
const sorted = [...entries].sort((a, b) => a.className.localeCompare(b.className, "en") || a.modelFile.localeCompare(b.modelFile, "en") || a.modelExport.localeCompare(b.modelExport, "en"));
|
|
173
|
+
const taken = /* @__PURE__ */ new Set();
|
|
174
|
+
const counts = /* @__PURE__ */ new Map();
|
|
175
|
+
return sorted.map((entry) => {
|
|
176
|
+
let count = (counts.get(entry.className) ?? 0) + 1;
|
|
177
|
+
let key = count === 1 ? entry.className : `${entry.className}_${count}`;
|
|
178
|
+
while (taken.has(key)) {
|
|
179
|
+
count += 1;
|
|
180
|
+
key = `${entry.className}_${count}`;
|
|
181
|
+
}
|
|
182
|
+
counts.set(entry.className, count);
|
|
183
|
+
taken.add(key);
|
|
184
|
+
return {
|
|
185
|
+
key,
|
|
186
|
+
entry
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Text of the generated declaration file.
|
|
192
|
+
*
|
|
193
|
+
* @param entries Every resolved model, in any order.
|
|
194
|
+
* @param typingsDirectory Absolute directory the file is written to; import
|
|
195
|
+
* specifiers are relative to it.
|
|
196
|
+
*/
|
|
197
|
+
function renderModelResourceRegistry(entries, typingsDirectory) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
for (const { key, entry } of assignRegistryKeys(entries)) {
|
|
200
|
+
const modelPath = toTypeImportSpecifier(typingsDirectory, entry.modelFile);
|
|
201
|
+
const resourcePath = toTypeImportSpecifier(typingsDirectory, entry.resourceFile);
|
|
202
|
+
if (modelPath === void 0 || resourcePath === void 0) continue;
|
|
203
|
+
const model = `import(${JSON.stringify(modelPath)}).${entry.modelExport}`;
|
|
204
|
+
const resource = `typeof import(${JSON.stringify(resourcePath)}).${entry.resourceExport}`;
|
|
205
|
+
lines.push(` ${JSON.stringify(key)}: { model: ${model}; resource: ${resource} };`);
|
|
206
|
+
}
|
|
207
|
+
return `// Auto-generated by Warlock.js - DO NOT EDIT
|
|
208
|
+
// One entry per model class that declares \`static resource = <Resource>\`, so
|
|
209
|
+
// \`Serialized<Model>\` resolves to the resource output. Models whose resource
|
|
210
|
+
// cannot be resolved statically are left out.
|
|
211
|
+
|
|
212
|
+
import "@warlock.js/core";
|
|
213
|
+
|
|
214
|
+
declare module "@warlock.js/core" {
|
|
215
|
+
interface ModelResourceRegistry {
|
|
216
|
+
${lines.join("\n")}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Write `content` to `filePath` through a temporary sibling and a rename, so a
|
|
223
|
+
* reader (the editor's TypeScript server) never sees a half-written file, and
|
|
224
|
+
* skip the write entirely when the file already holds exactly this text.
|
|
225
|
+
*
|
|
226
|
+
* @returns `true` when the file was written.
|
|
227
|
+
*/
|
|
228
|
+
async function writeFileIfChanged(filePath, content) {
|
|
229
|
+
try {
|
|
230
|
+
if (await readFile(filePath, "utf-8") === content) return false;
|
|
231
|
+
} catch {}
|
|
232
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
233
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
234
|
+
try {
|
|
235
|
+
await writeFile(temporaryPath, content, "utf-8");
|
|
236
|
+
await rename(temporaryPath, filePath);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
await rm(temporaryPath, { force: true });
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
export { extractModelResourceEntries, renderModelResourceRegistry, writeFileIfChanged };
|
|
246
|
+
//# sourceMappingURL=model-resource-typings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-resource-typings.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/model-resource-typings.ts"],"sourcesContent":["import { mkdir, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, isAbsolute, relative } from \"node:path\";\nimport ts from \"typescript\";\n\n/**\n * Static `ModelResourceRegistry` generation.\n *\n * `Serialized<User>` can only resolve a model to its resource output through an\n * entry in the augmentable `ModelResourceRegistry` (see `http/serialized.ts`).\n * This module derives those entries from source: a model class that declares\n * `static resource = UserResource` produces\n *\n * ```ts\n * \"User\": { model: import(\"<model file>\").User; resource: typeof import(\"<resource file>\").UserResource };\n * ```\n *\n * Everything here is syntactic. It reads one `ts.createSourceFile` AST per model\n * file, follows the identifier through that file's own imports, and never\n * imports or executes application code (`read-config-ast.ts` explains why a\n * type checker is not an option). Anything it cannot prove from the AST alone\n * (a computed expression, `lazy(...)`, a namespace import, a class that is not\n * exported) is skipped, and nothing is recorded for it: a guessed entry would\n * make `Serialized` lie, which is worse than the raw-data type it falls back to.\n */\n\n/** One model class whose resource could be resolved statically. */\nexport type ModelResourceEntry = {\n /** The model class name (the registry key before de-duplication). */\n className: string;\n /** Absolute path of the model file. */\n modelFile: string;\n /** How the model file exports the class: its export name, or `\"default\"`. */\n modelExport: string;\n /** Absolute path of the module that exports the resource. */\n resourceFile: string;\n /** How that module exports the resource: its export name, or `\"default\"`. */\n resourceExport: string;\n};\n\n/** A model that declares `static resource` but could not be resolved. */\nexport type SkippedModelResource = {\n className: string;\n modelFile: string;\n reason: string;\n};\n\nexport type ModelResourceExtraction = {\n entries: ModelResourceEntry[];\n skipped: SkippedModelResource[];\n};\n\nexport type ExtractModelResourcesInput = {\n /** Source text of the model file. */\n source: string;\n /** Absolute path of the model file. */\n absolutePath: string;\n /**\n * Resolve an import specifier of the model file (relative or tsconfig alias) to\n * an absolute file path, or `undefined` when it does not point at app source.\n */\n resolveImport: (specifier: string) => string | undefined;\n};\n\ntype ImportBinding = { specifier: string; exportName: string };\n\nconst IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\nfunction hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {\n return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);\n}\n\nfunction unwrapParentheses(expression: ts.Expression): ts.Expression {\n let current = expression;\n\n while (ts.isParenthesizedExpression(current)) {\n current = current.expression;\n }\n\n return current;\n}\n\n/** Top-level names this file declares, and the names each is exported under. */\nfunction readLocalDeclarations(sourceFile: ts.SourceFile): {\n declared: Set<string>;\n exportNames: Map<string, string[]>;\n} {\n const declared = new Set<string>();\n const exportNames = new Map<string, string[]>();\n\n const addExport = (local: string, exported: string): void => {\n const names = exportNames.get(local) ?? [];\n\n names.push(exported);\n exportNames.set(local, names);\n };\n\n for (const statement of sourceFile.statements) {\n const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);\n const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);\n\n if (\n (ts.isClassDeclaration(statement) ||\n ts.isFunctionDeclaration(statement) ||\n ts.isEnumDeclaration(statement)) &&\n statement.name\n ) {\n declared.add(statement.name.text);\n\n if (exported) {\n addExport(statement.name.text, isDefault ? \"default\" : statement.name.text);\n }\n\n continue;\n }\n\n if (ts.isVariableStatement(statement)) {\n for (const declaration of statement.declarationList.declarations) {\n if (!ts.isIdentifier(declaration.name)) continue;\n\n declared.add(declaration.name.text);\n\n if (exported) {\n addExport(declaration.name.text, declaration.name.text);\n }\n }\n\n continue;\n }\n\n // `export { A, B as C }` (local form only; a `from` clause re-exports a foreign name).\n if (\n ts.isExportDeclaration(statement) &&\n !statement.moduleSpecifier &&\n !statement.isTypeOnly &&\n statement.exportClause &&\n ts.isNamedExports(statement.exportClause)\n ) {\n for (const element of statement.exportClause.elements) {\n if (element.isTypeOnly) continue;\n\n addExport((element.propertyName ?? element.name).text, element.name.text);\n }\n\n continue;\n }\n\n // `export default UserResource;`\n if (\n ts.isExportAssignment(statement) &&\n !statement.isExportEquals &&\n ts.isIdentifier(statement.expression)\n ) {\n addExport(statement.expression.text, \"default\");\n }\n }\n\n return { declared, exportNames };\n}\n\n/** Prefer a named export over `default`; among named exports, the first sorted. */\nfunction pickExportName(names: readonly string[] | undefined): string | undefined {\n if (!names || names.length === 0) return undefined;\n\n const named = names.filter((name) => name !== \"default\").sort();\n\n return named[0] ?? \"default\";\n}\n\nfunction readImportBindings(sourceFile: ts.SourceFile): Map<string, ImportBinding> {\n const bindings = new Map<string, ImportBinding>();\n\n for (const statement of sourceFile.statements) {\n if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {\n continue;\n }\n\n const clause = statement.importClause;\n const specifier = statement.moduleSpecifier.text;\n\n if (!clause) continue;\n\n if (clause.name) {\n bindings.set(clause.name.text, { specifier, exportName: \"default\" });\n }\n\n // A namespace import (`import * as ns`) cannot be the bare identifier a\n // `static resource = X` reads, so it never becomes a binding.\n if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {\n for (const element of clause.namedBindings.elements) {\n bindings.set(element.name.text, {\n specifier,\n exportName: (element.propertyName ?? element.name).text,\n });\n }\n }\n }\n\n return bindings;\n}\n\n/** The identifier a class assigns to `static resource`, when it is a bare identifier. */\nfunction readStaticResourceReference(\n member: ts.ClassElement,\n): { name: string } | \"computed\" | undefined {\n if (!ts.isPropertyDeclaration(member)) return undefined;\n if (!hasModifier(member, ts.SyntaxKind.StaticKeyword)) return undefined;\n if (hasModifier(member, ts.SyntaxKind.DeclareKeyword)) return undefined;\n\n const isResourceName =\n (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&\n member.name.text === \"resource\";\n\n if (!isResourceName) return undefined;\n if (!member.initializer) return undefined;\n\n const initializer = unwrapParentheses(member.initializer);\n\n return ts.isIdentifier(initializer) ? { name: initializer.text } : \"computed\";\n}\n\n/**\n * Read every model class of one file that declares `static resource = <Identifier>`\n * and resolve it to a model export plus a resource export. Pure: the only\n * outside knowledge is the injected {@link ExtractModelResourcesInput.resolveImport}.\n */\nexport function extractModelResourceEntries(\n input: ExtractModelResourcesInput,\n): ModelResourceExtraction {\n const result: ModelResourceExtraction = { entries: [], skipped: [] };\n\n // Cheap exit: most model files declare no resource.\n if (!input.source.includes(\"resource\")) return result;\n\n const sourceFile = ts.createSourceFile(\n input.absolutePath,\n input.source,\n ts.ScriptTarget.ESNext,\n /* setParentNodes */ false,\n input.absolutePath.endsWith(\"x\") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,\n );\n\n const { declared, exportNames } = readLocalDeclarations(sourceFile);\n const imports = readImportBindings(sourceFile);\n\n for (const statement of sourceFile.statements) {\n if (!ts.isClassDeclaration(statement) || !statement.name) continue;\n\n const className = statement.name.text;\n\n let reference: { name: string } | \"computed\" | undefined;\n\n for (const member of statement.members) {\n reference = readStaticResourceReference(member) ?? reference;\n }\n\n if (reference === undefined) continue;\n\n const skip = (reason: string): void => {\n result.skipped.push({ className, modelFile: input.absolutePath, reason });\n };\n\n if (reference === \"computed\") {\n skip(\"`static resource` is not a plain identifier\");\n continue;\n }\n\n const requiresTypeArguments = statement.typeParameters?.some(\n (parameter) => parameter.default === undefined,\n );\n\n if (requiresTypeArguments) {\n skip(\"the model class is generic\");\n continue;\n }\n\n const modelExport = pickExportName(exportNames.get(className));\n\n if (modelExport === undefined) {\n skip(\"the model class is not exported\");\n continue;\n }\n\n if (modelExport !== \"default\" && !IDENTIFIER.test(modelExport)) {\n skip(\"the model export name is not an identifier\");\n continue;\n }\n\n const binding = imports.get(reference.name);\n\n let resourceFile: string | undefined;\n let resourceExport: string | undefined;\n\n if (binding) {\n resourceFile = input.resolveImport(binding.specifier);\n resourceExport = binding.exportName;\n } else if (declared.has(reference.name)) {\n resourceFile = input.absolutePath;\n resourceExport = pickExportName(exportNames.get(reference.name));\n }\n\n if (resourceFile === undefined || resourceExport === undefined) {\n skip(`\\`${reference.name}\\` cannot be resolved to an exported module member`);\n continue;\n }\n\n if (resourceExport !== \"default\" && !IDENTIFIER.test(resourceExport)) {\n skip(\"the resource export name is not an identifier\");\n continue;\n }\n\n result.entries.push({\n className,\n modelFile: input.absolutePath,\n modelExport,\n resourceFile,\n resourceExport,\n });\n }\n\n return result;\n}\n\n/** Module specifier, relative to the typings directory, without a TypeScript extension. */\nfunction toTypeImportSpecifier(typingsDirectory: string, absoluteFile: string): string | undefined {\n const path = relative(typingsDirectory, absoluteFile).replaceAll(\"\\\\\", \"/\");\n\n // A different drive (Windows) has no relative form; do not emit a guess.\n if (isAbsolute(path)) return undefined;\n\n const withoutExtension = path.replace(/\\.(?:d\\.)?(?:ts|tsx|mts|cts)$/, \"\");\n\n return withoutExtension.startsWith(\".\") ? withoutExtension : `./${withoutExtension}`;\n}\n\n/**\n * Registry keys: the class name, suffixed `_2`, `_3`, ... when several model\n * files declare the same class name. Order is by class name, then by file path,\n * so the same set of models always yields the same keys and the same text.\n */\nexport function assignRegistryKeys(\n entries: readonly ModelResourceEntry[],\n): Array<{ key: string; entry: ModelResourceEntry }> {\n const sorted = [...entries].sort(\n (a, b) =>\n a.className.localeCompare(b.className, \"en\") ||\n a.modelFile.localeCompare(b.modelFile, \"en\") ||\n a.modelExport.localeCompare(b.modelExport, \"en\"),\n );\n\n const taken = new Set<string>();\n const counts = new Map<string, number>();\n\n return sorted.map((entry) => {\n let count = (counts.get(entry.className) ?? 0) + 1;\n let key = count === 1 ? entry.className : `${entry.className}_${count}`;\n\n while (taken.has(key)) {\n count += 1;\n key = `${entry.className}_${count}`;\n }\n\n counts.set(entry.className, count);\n taken.add(key);\n\n return { key, entry };\n });\n}\n\n/**\n * Text of the generated declaration file.\n *\n * @param entries Every resolved model, in any order.\n * @param typingsDirectory Absolute directory the file is written to; import\n * specifiers are relative to it.\n */\nexport function renderModelResourceRegistry(\n entries: readonly ModelResourceEntry[],\n typingsDirectory: string,\n): string {\n const lines: string[] = [];\n\n for (const { key, entry } of assignRegistryKeys(entries)) {\n const modelPath = toTypeImportSpecifier(typingsDirectory, entry.modelFile);\n const resourcePath = toTypeImportSpecifier(typingsDirectory, entry.resourceFile);\n\n if (modelPath === undefined || resourcePath === undefined) continue;\n\n const model = `import(${JSON.stringify(modelPath)}).${entry.modelExport}`;\n const resource = `typeof import(${JSON.stringify(resourcePath)}).${entry.resourceExport}`;\n\n lines.push(` ${JSON.stringify(key)}: { model: ${model}; resource: ${resource} };`);\n }\n\n return `// Auto-generated by Warlock.js - DO NOT EDIT\n// One entry per model class that declares \\`static resource = <Resource>\\`, so\n// \\`Serialized<Model>\\` resolves to the resource output. Models whose resource\n// cannot be resolved statically are left out.\n\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface ModelResourceRegistry {\n${lines.join(\"\\n\")}\n }\n}\n`;\n}\n\n/**\n * Write `content` to `filePath` through a temporary sibling and a rename, so a\n * reader (the editor's TypeScript server) never sees a half-written file, and\n * skip the write entirely when the file already holds exactly this text.\n *\n * @returns `true` when the file was written.\n */\nexport async function writeFileIfChanged(filePath: string, content: string): Promise<boolean> {\n try {\n if ((await readFile(filePath, \"utf-8\")) === content) return false;\n } catch {\n // Missing file: write it.\n }\n\n await mkdir(dirname(filePath), { recursive: true });\n\n const temporaryPath = `${filePath}.${process.pid}.tmp`;\n\n try {\n await writeFile(temporaryPath, content, \"utf-8\");\n await rename(temporaryPath, filePath);\n } catch (error) {\n await rm(temporaryPath, { force: true });\n\n throw error;\n }\n\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { router } from "../router/router.mjs";
|
|
2
|
+
import { registerGroupedTranslationKeys, registerLocaleDictionary } from "../localization/register-locale-dictionary.mjs";
|
|
3
|
+
import "../localization/index.mjs";
|
|
2
4
|
import { devLogError, formatModuleNotFoundError } from "./dev-logger.mjs";
|
|
3
5
|
import { importAppModule } from "../loader/app-module-importer.mjs";
|
|
6
|
+
import { extractTranslationKeys } from "./extract-translation-keys.mjs";
|
|
7
|
+
import { warnForLegacyLocales } from "./legacy-locales-warning.mjs";
|
|
4
8
|
import { getDevelopmentModelModuleRegistry } from "./model-module-registry.mjs";
|
|
5
9
|
import { pathToFileURL } from "node:url";
|
|
10
|
+
import ts from "typescript";
|
|
6
11
|
|
|
7
12
|
//#region ../core/src/dev-server/module-loader.ts
|
|
8
13
|
/**
|
|
@@ -72,6 +77,17 @@ var ModuleLoader = class {
|
|
|
72
77
|
*/
|
|
73
78
|
async loadModule(file, type) {
|
|
74
79
|
if (file.relativePath.endsWith(".env")) return void 0;
|
|
80
|
+
if (type === "locale" && /\/utils\/locales\.(ts|tsx)$/.test(file.relativePath)) warnForLegacyLocales(this.specialFilesCollector.getFilesByType("locale").map((locale) => locale.relativePath));
|
|
81
|
+
if (type === "locale" && file.relativePath.endsWith(".json")) {
|
|
82
|
+
const cleanup = registerLocaleDictionary({
|
|
83
|
+
sourceFile: file.relativePath,
|
|
84
|
+
source: file.source,
|
|
85
|
+
defaultNamespace: file.relativePath.split("/")[2]
|
|
86
|
+
});
|
|
87
|
+
file.addCleanup(cleanup);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (type === "locale") registerGroupedTranslationKeys(file.relativePath, extractTranslationKeys(ts.createSourceFile(file.relativePath, file.source, ts.ScriptTarget.Latest)));
|
|
75
91
|
globalThis.__currentModuleFile = file;
|
|
76
92
|
try {
|
|
77
93
|
const fileUrl = pathToFileURL(file.absolutePath).href;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { getDevelopmentModelModuleRegistry } from \"./model-module-registry\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes after main so handlers are\n * bound, worker last so scheduled jobs and queue definitions see routes\n * already registered. Dev always loads worker files since dev runs every role.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\n \"locale\",\n \"event\",\n \"main\",\n \"route\",\n \"worker\",\n] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n public constructor(private readonly specialFilesCollector: SpecialFilesCollector) {\n // Web creates its Vite bridge after Core's development orchestration has\n // constructed ModuleLoader. Create the carrier now so a zero-model app can\n // subscribe before a model is added later through HMR.\n getDevelopmentModelModuleRegistry();\n }\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(\n options: { onBeforeLoad?: (file: FileManager) => void | Promise<void> } = {},\n ): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await options.onBeforeLoad?.(file);\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(file: FileManager, type: string): Promise<T | undefined> {\n if (file.relativePath.endsWith(\".env\")) return undefined;\n\n globalThis.__currentModuleFile = file;\n\n try {\n const fileUrl = pathToFileURL(file.absolutePath).href;\n\n const load = async () => {\n const module = (await importAppModule(fileUrl)) as Record<string, unknown>;\n this.loadedModules.set(file.absolutePath, module);\n this.registerCleanup(file, module);\n if (type === \"model\") {\n getDevelopmentModelModuleRegistry().publish(file.absolutePath, fileUrl, module);\n }\n return module as T;\n };\n\n return type === \"route\" ? await router.withSourceFile(file.relativePath, load) : await load();\n } catch (error: any) {\n if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n devLogError(formatModuleNotFoundError(error));\n } else {\n // Pass the error through so devLogError prints the stack. With dev\n // source maps enabled, frames remap to the original `.ts` line/col\n // (e.g. the actual call site of an undefined symbol) instead of\n // vanishing — a one-line \"x is not defined\" is undebuggable.\n devLogError(\n `Failed to load ${type}: ${file.relativePath} - ${error?.message || error}`,\n error,\n );\n }\n\n // RETHROW after logging. Previously this returned undefined, so a route\n // file that threw on import or registration disappeared with no boot\n // error — the whole surface 404'd silently. Boot callers abort on the\n // throw; the HMR batch-reload handler catches it, keeps the server\n // alive, and shows it loudly.\n throw error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error);\n } finally {\n globalThis.__currentModuleFile = undefined;\n }\n }\n\n /**\n * Reload a previously-loaded special file. The version-bump that makes the\n * import fresh is done by the caller (layer-executor) before invoking this.\n * Routes are removed from the registry first so re-registration is clean.\n */\n public async reloadModule(file: FileManager): Promise<void> {\n const moduleType = this.specialFilesCollector.getFileType(file.relativePath);\n if (!moduleType) return;\n\n this.runCleanup(file);\n\n if (moduleType === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.loadedModules.delete(file.absolutePath);\n await this.loadModule(file, moduleType);\n }\n\n /**\n * Run every cleanup hook the file registered (or `$cleanup` on its exports)\n * and reset the list. Called once per HMR reload.\n */\n public runCleanup(file: FileManager): void {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") {\n hook();\n } else if (\n hook &&\n typeof (hook as { unsubscribe?: () => void }).unsubscribe === \"function\"\n ) {\n (hook as { unsubscribe: () => void }).unsubscribe();\n }\n } catch {\n // Cleanup errors shouldn't break HMR.\n }\n }\n file.resetCleanup();\n }\n\n /**\n * Clean up after a file whose source was deleted from disk.\n */\n public cleanupDeletedModule(file: FileManager): void {\n this.loadedModules.delete(file.absolutePath);\n\n if (file.type === \"model\") {\n getDevelopmentModelModuleRegistry().remove(file.absolutePath);\n }\n\n if (file.type === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.runCleanup(file);\n }\n\n /**\n * Scan a freshly-loaded module for cleanup handlers and register them on\n * the FileManager so they run before the next reload. Priority: explicit\n * `export function cleanup()` → `.$cleanup` on any exported value.\n */\n private registerCleanup(file: FileManager, module: Record<string, unknown>): void {\n if (typeof module.cleanup === \"function\") {\n file.addCleanup(module.cleanup as CleanupFunction);\n return;\n }\n\n const cleanups: CleanupFunction[] = [];\n\n for (const exportedValue of Object.values(module)) {\n const cleanup = (exportedValue as { $cleanup?: unknown })?.$cleanup;\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup.bind(exportedValue) as CleanupFunction);\n }\n }\n\n if (cleanups.length > 0) {\n file.addCleanup(cleanups);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"module-loader.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/module-loader.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport ts from \"typescript\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { registerGroupedTranslationKeys, registerLocaleDictionary } from \"../localization\";\nimport { router } from \"../router/router\";\nimport { devLogError, formatModuleNotFoundError } from \"./dev-logger\";\nimport { extractTranslationKeys } from \"./extract-translation-keys\";\nimport type { CleanupFunction, FileManager } from \"./file-manager\";\nimport { warnForLegacyLocales } from \"./legacy-locales-warning\";\nimport { getDevelopmentModelModuleRegistry } from \"./model-module-registry\";\nimport type { SpecialFileType, SpecialFilesCollector } from \"./special-files-collector\";\n\ndeclare global {\n var __currentModuleFile: FileManager | undefined;\n}\n\n/**\n * Thrown when a special file (route/event/main/locale) fails to import or\n * register. Carries the file that failed and the original cause so a broken\n * module surfaces with its origin instead of vanishing into a 404.\n */\nexport class ModuleLoadError extends Error {\n public constructor(\n public readonly type: string,\n public readonly file: FileManager,\n public readonly cause: unknown,\n ) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n super(`Failed to load ${type}: ${file.relativePath} - ${reason}`);\n this.name = \"ModuleLoadError\";\n }\n}\n\n/**\n * The \"special\" file kinds that the dev server eagerly imports at boot.\n * Order matters: locales first (used by everything), events before main so\n * listeners are registered when main runs, routes after main so handlers are\n * bound, worker last so scheduled jobs and queue definitions see routes\n * already registered. Dev always loads worker files since dev runs every role.\n */\nconst SPECIAL_TYPES: readonly SpecialFileType[] = [\n \"locale\",\n \"event\",\n \"main\",\n \"route\",\n \"worker\",\n] as const;\n\n/**\n * Loads application modules through the ESM loader hook. The hook stamps\n * `?v=N` on every import, so each reload is a fresh module without any\n * userland cache-busting plumbing.\n */\nexport class ModuleLoader {\n private readonly loadedModules = new Map<string, unknown>();\n\n public constructor(private readonly specialFilesCollector: SpecialFilesCollector) {\n // Web creates its Vite bridge after Core's development orchestration has\n // constructed ModuleLoader. Create the carrier now so a zero-model app can\n // subscribe before a model is added later through HMR.\n getDevelopmentModelModuleRegistry();\n }\n\n /**\n * Eagerly load every special file at boot, in the canonical order so\n * later phases (e.g. routes) see state earlier phases registered.\n *\n * A failing module no longer vanishes silently: each failure is collected\n * (so one broken file doesn't hide the others) and, once every file has been\n * attempted, an aggregate error is thrown. The boot callers\n * (`DevelopmentServer.start`, the HTTP test bootstrap) already wrap this in a\n * try/catch that logs and aborts the boot — so a broken route module aborts\n * boot loudly rather than booting into a surface that 404s.\n */\n public async loadAll(\n options: { onBeforeLoad?: (file: FileManager) => void | Promise<void> } = {},\n ): Promise<void> {\n const failures: ModuleLoadError[] = [];\n\n for (const type of SPECIAL_TYPES) {\n for (const file of this.specialFilesCollector.getFilesByType(type)) {\n try {\n await options.onBeforeLoad?.(file);\n await this.loadModule(file, type);\n } catch (error) {\n failures.push(\n error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error),\n );\n }\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(\n failures,\n `${failures.length} module(s) failed to load during boot:\\n` +\n failures.map((failure) => ` - ${failure.message}`).join(\"\\n\"),\n );\n }\n }\n\n /**\n * Import a file through the loader hook. For routes, scopes the import in\n * `router.withSourceFile()` so the added routes carry their origin.\n */\n public async loadModule<T = unknown>(file: FileManager, type: string): Promise<T | undefined> {\n if (file.relativePath.endsWith(\".env\")) return undefined;\n\n if (type === \"locale\" && /\\/utils\\/locales\\.(ts|tsx)$/.test(file.relativePath)) {\n warnForLegacyLocales(\n this.specialFilesCollector.getFilesByType(\"locale\").map((locale) => locale.relativePath),\n );\n }\n\n if (type === \"locale\" && file.relativePath.endsWith(\".json\")) {\n const cleanup = registerLocaleDictionary({\n sourceFile: file.relativePath,\n source: file.source,\n defaultNamespace: file.relativePath.split(\"/\")[2]!,\n });\n file.addCleanup(cleanup);\n return undefined;\n }\n\n if (type === \"locale\") {\n registerGroupedTranslationKeys(\n file.relativePath,\n extractTranslationKeys(\n ts.createSourceFile(file.relativePath, file.source, ts.ScriptTarget.Latest),\n ),\n );\n }\n\n globalThis.__currentModuleFile = file;\n\n try {\n const fileUrl = pathToFileURL(file.absolutePath).href;\n\n const load = async () => {\n const module = (await importAppModule(fileUrl)) as Record<string, unknown>;\n this.loadedModules.set(file.absolutePath, module);\n this.registerCleanup(file, module);\n if (type === \"model\") {\n getDevelopmentModelModuleRegistry().publish(file.absolutePath, fileUrl, module);\n }\n return module as T;\n };\n\n return type === \"route\" ? await router.withSourceFile(file.relativePath, load) : await load();\n } catch (error: any) {\n if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n devLogError(formatModuleNotFoundError(error));\n } else {\n // Pass the error through so devLogError prints the stack. With dev\n // source maps enabled, frames remap to the original `.ts` line/col\n // (e.g. the actual call site of an undefined symbol) instead of\n // vanishing — a one-line \"x is not defined\" is undebuggable.\n devLogError(\n `Failed to load ${type}: ${file.relativePath} - ${error?.message || error}`,\n error,\n );\n }\n\n // RETHROW after logging. Previously this returned undefined, so a route\n // file that threw on import or registration disappeared with no boot\n // error — the whole surface 404'd silently. Boot callers abort on the\n // throw; the HMR batch-reload handler catches it, keeps the server\n // alive, and shows it loudly.\n throw error instanceof ModuleLoadError ? error : new ModuleLoadError(type, file, error);\n } finally {\n globalThis.__currentModuleFile = undefined;\n }\n }\n\n /**\n * Reload a previously-loaded special file. The version-bump that makes the\n * import fresh is done by the caller (layer-executor) before invoking this.\n * Routes are removed from the registry first so re-registration is clean.\n */\n public async reloadModule(file: FileManager): Promise<void> {\n const moduleType = this.specialFilesCollector.getFileType(file.relativePath);\n if (!moduleType) return;\n\n this.runCleanup(file);\n\n if (moduleType === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.loadedModules.delete(file.absolutePath);\n await this.loadModule(file, moduleType);\n }\n\n /**\n * Run every cleanup hook the file registered (or `$cleanup` on its exports)\n * and reset the list. Called once per HMR reload.\n */\n public runCleanup(file: FileManager): void {\n for (const hook of file.cleanup) {\n try {\n if (typeof hook === \"function\") {\n hook();\n } else if (\n hook &&\n typeof (hook as { unsubscribe?: () => void }).unsubscribe === \"function\"\n ) {\n (hook as { unsubscribe: () => void }).unsubscribe();\n }\n } catch {\n // Cleanup errors shouldn't break HMR.\n }\n }\n file.resetCleanup();\n }\n\n /**\n * Clean up after a file whose source was deleted from disk.\n */\n public cleanupDeletedModule(file: FileManager): void {\n this.loadedModules.delete(file.absolutePath);\n\n if (file.type === \"model\") {\n getDevelopmentModelModuleRegistry().remove(file.absolutePath);\n }\n\n if (file.type === \"route\") {\n router.removeRoutesBySourceFile(file.relativePath);\n }\n\n this.runCleanup(file);\n }\n\n /**\n * Scan a freshly-loaded module for cleanup handlers and register them on\n * the FileManager so they run before the next reload. Priority: explicit\n * `export function cleanup()` → `.$cleanup` on any exported value.\n */\n private registerCleanup(file: FileManager, module: Record<string, unknown>): void {\n if (typeof module.cleanup === \"function\") {\n file.addCleanup(module.cleanup as CleanupFunction);\n return;\n }\n\n const cleanups: CleanupFunction[] = [];\n\n for (const exportedValue of Object.values(module)) {\n const cleanup = (exportedValue as { $cleanup?: unknown })?.$cleanup;\n if (typeof cleanup === \"function\") {\n cleanups.push(cleanup.bind(exportedValue) as CleanupFunction);\n }\n }\n\n if (cleanups.length > 0) {\n file.addCleanup(cleanups);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -10,8 +10,8 @@ const isRouteFile = (path) => /^src\/app\/[^/]+\/routes\.(ts|tsx)$/.test(normali
|
|
|
10
10
|
const isWorkerFile = (path) => /^src\/app\/([^/]+\/)?worker\.(ts|tsx)$/.test(normalize(path));
|
|
11
11
|
/** `src/app/<module>/events/<file>.ts` */
|
|
12
12
|
const isEventFile = (path) => /^src\/app\/[^/]+\/events\/[^/]+\.(ts|tsx)$/.test(normalize(path));
|
|
13
|
-
/** `src/app/<module>/utils/locales.ts` */
|
|
14
|
-
const isLocaleFile = (path) => /^src\/app\/[^/]+\/utils\/locales\.(ts|tsx)$/.test(normalize(path));
|
|
13
|
+
/** `src/app/<module>/utils/locales.ts` or `locales.json` */
|
|
14
|
+
const isLocaleFile = (path) => /^src\/app\/[^/]+\/utils\/locales\.(ts|tsx|json)$/.test(normalize(path));
|
|
15
15
|
/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */
|
|
16
16
|
const isCodeFile = (path) => /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));
|
|
17
17
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"special-file-patterns.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-file-patterns.ts"],"sourcesContent":["const normalize = (path: string) => path.replace(/\\\\/g, \"/\");\n\n/** `src/config/**` — any depth, the config key is the raw path */\nexport const isConfigFile = (path: string) => /^src\\/config\\/.*\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/main.ts` or `src/app/<module>/main.ts` */\nexport const isMainFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?main\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/routes.ts` — one level deep */\nexport const isRouteFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/routes\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/worker.ts` or `src/app/<module>/worker.ts` — mirrors `isMainFile`'s optional module dir */\nexport const isWorkerFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?worker\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/events/<file>.ts` */\nexport const isEventFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/events\\/[^/]+\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/utils/locales.ts` */\nexport const isLocaleFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/utils\\/locales\\.(ts|tsx)$/.test(normalize(path));\n\n/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */\nexport const isCodeFile = (path: string) =>\n /\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));\n"],"mappings":";AAAA,MAAM,aAAa,SAAiB,KAAK,QAAQ,OAAO,GAAG;;AAG3D,MAAa,gBAAgB,SAAiB,8BAA8B,KAAK,UAAU,IAAI,CAAC;;AAGhG,MAAa,cAAc,SACzB,uCAAuC,KAAK,UAAU,IAAI,CAAC;;AAG7D,MAAa,eAAe,SAC1B,sCAAsC,KAAK,UAAU,IAAI,CAAC;;AAG5D,MAAa,gBAAgB,SAC3B,yCAAyC,KAAK,UAAU,IAAI,CAAC;;AAG/D,MAAa,eAAe,SAC1B,6CAA6C,KAAK,UAAU,IAAI,CAAC;;AAGnE,MAAa,gBAAgB,SAC3B,
|
|
1
|
+
{"version":3,"file":"special-file-patterns.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/special-file-patterns.ts"],"sourcesContent":["const normalize = (path: string) => path.replace(/\\\\/g, \"/\");\n\n/** `src/config/**` — any depth, the config key is the raw path */\nexport const isConfigFile = (path: string) => /^src\\/config\\/.*\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/main.ts` or `src/app/<module>/main.ts` */\nexport const isMainFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?main\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/routes.ts` — one level deep */\nexport const isRouteFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/routes\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/worker.ts` or `src/app/<module>/worker.ts` — mirrors `isMainFile`'s optional module dir */\nexport const isWorkerFile = (path: string) =>\n /^src\\/app\\/([^/]+\\/)?worker\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/events/<file>.ts` */\nexport const isEventFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/events\\/[^/]+\\.(ts|tsx)$/.test(normalize(path));\n\n/** `src/app/<module>/utils/locales.ts` or `locales.json` */\nexport const isLocaleFile = (path: string) =>\n /^src\\/app\\/[^/]+\\/utils\\/locales\\.(ts|tsx|json)$/.test(normalize(path));\n\n/** Source files the dev server can hot-reload; anything else is data (`.sql`, `.json`, …) */\nexport const isCodeFile = (path: string) =>\n /\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/.test(normalize(path));\n"],"mappings":";AAAA,MAAM,aAAa,SAAiB,KAAK,QAAQ,OAAO,GAAG;;AAG3D,MAAa,gBAAgB,SAAiB,8BAA8B,KAAK,UAAU,IAAI,CAAC;;AAGhG,MAAa,cAAc,SACzB,uCAAuC,KAAK,UAAU,IAAI,CAAC;;AAG7D,MAAa,eAAe,SAC1B,sCAAsC,KAAK,UAAU,IAAI,CAAC;;AAG5D,MAAa,gBAAgB,SAC3B,yCAAyC,KAAK,UAAU,IAAI,CAAC;;AAG/D,MAAa,eAAe,SAC1B,6CAA6C,KAAK,UAAU,IAAI,CAAC;;AAGnE,MAAa,gBAAgB,SAC3B,mDAAmD,KAAK,UAAU,IAAI,CAAC;;AAGzE,MAAa,cAAc,SACzB,qCAAqC,KAAK,UAAU,IAAI,CAAC"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
//#region ../core/src/dev-server/translation-keys-sources.ts
|
|
2
2
|
const GROUPED_TRANSLATIONS_MARKER = "groupedTranslations";
|
|
3
|
+
/** `src/app/<module>/utils/locales.json`, the data-only module dictionary. */
|
|
4
|
+
function isModuleLocaleDictionaryPath(path) {
|
|
5
|
+
return /^src\/app\/[^/]+\/utils\/locales\.json$/.test(path.replace(/\\\\/g, "/"));
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Cheap pre-filter for candidate translation-registering source files.
|
|
5
9
|
*
|
|
@@ -10,10 +14,10 @@ const GROUPED_TRANSLATIONS_MARKER = "groupedTranslations";
|
|
|
10
14
|
* every file — this text check keeps that cheap by skipping the AST parse
|
|
11
15
|
* for the files that plainly do not call it, without executing app code.
|
|
12
16
|
*/
|
|
13
|
-
function isTranslationRegisteringSource(source) {
|
|
14
|
-
return source.includes(GROUPED_TRANSLATIONS_MARKER);
|
|
17
|
+
function isTranslationRegisteringSource(path, source) {
|
|
18
|
+
return isModuleLocaleDictionaryPath(path) || source.includes(GROUPED_TRANSLATIONS_MARKER);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
//#endregion
|
|
18
|
-
export { isTranslationRegisteringSource };
|
|
22
|
+
export { isModuleLocaleDictionaryPath, isTranslationRegisteringSource };
|
|
19
23
|
//# sourceMappingURL=translation-keys-sources.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-keys-sources.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/translation-keys-sources.ts"],"sourcesContent":["const GROUPED_TRANSLATIONS_MARKER = \"groupedTranslations\";\n\n/**\n * Cheap pre-filter for candidate translation-registering source files.\n *\n * `groupedTranslations` calls used to be scanned for on dedicated\n * `src/**/utils/locales.*` files only, so a web module registering keys\n * elsewhere (e.g. `src/web/layout.register.ts`) was silently invisible to the\n * generator. Any app source file can call it, so discovery has to consider\n * every file — this text check keeps that cheap by skipping the AST parse\n * for the files that plainly do not call it, without executing app code.\n */\nexport function isTranslationRegisteringSource(source: string): boolean {\n return source.includes(GROUPED_TRANSLATIONS_MARKER);\n}\n"],"mappings":";AAAA,MAAM,8BAA8B;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"translation-keys-sources.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/translation-keys-sources.ts"],"sourcesContent":["const GROUPED_TRANSLATIONS_MARKER = \"groupedTranslations\";\n\n/** `src/app/<module>/utils/locales.json`, the data-only module dictionary. */\nexport function isModuleLocaleDictionaryPath(path: string): boolean {\n return /^src\\/app\\/[^/]+\\/utils\\/locales\\.json$/.test(path.replace(/\\\\\\\\/g, \"/\"));\n}\n\n/**\n * Cheap pre-filter for candidate translation-registering source files.\n *\n * `groupedTranslations` calls used to be scanned for on dedicated\n * `src/**/utils/locales.*` files only, so a web module registering keys\n * elsewhere (e.g. `src/web/layout.register.ts`) was silently invisible to the\n * generator. Any app source file can call it, so discovery has to consider\n * every file — this text check keeps that cheap by skipping the AST parse\n * for the files that plainly do not call it, without executing app code.\n */\nexport function isTranslationRegisteringSource(path: string, source: string): boolean {\n return isModuleLocaleDictionaryPath(path) || source.includes(GROUPED_TRANSLATIONS_MARKER);\n}\n"],"mappings":";AAAA,MAAM,8BAA8B;;AAGpC,SAAgB,6BAA6B,MAAuB;CAClE,OAAO,0CAA0C,KAAK,KAAK,QAAQ,SAAS,GAAG,CAAC;AAClF;;;;;;;;;;;AAYA,SAAgB,+BAA+B,MAAc,QAAyB;CACpF,OAAO,6BAA6B,IAAI,KAAK,OAAO,SAAS,2BAA2B;AAC1F"}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { Path } from "../utils/normalized-path.mjs";
|
|
2
2
|
import { warlockPath } from "../utils/paths.mjs";
|
|
3
3
|
import "../utils/index.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { parseLocaleDictionary } from "../localization/parse-locale-dictionary.mjs";
|
|
5
|
+
import "../localization/index.mjs";
|
|
6
|
+
import { devLogDim, devLogError, devLogInfo, devLogSuccess, devServeLog } from "./dev-logger.mjs";
|
|
7
|
+
import { parseImports } from "./parse-imports.mjs";
|
|
8
|
+
import { getFilesFromDirectory } from "./utils.mjs";
|
|
9
|
+
import { extractTranslationKeys } from "./extract-translation-keys.mjs";
|
|
5
10
|
import { filesOrchestrator } from "./files-orchestrator.mjs";
|
|
6
11
|
import { readConfigAst } from "./read-config-ast.mjs";
|
|
7
12
|
import { runTypingsGeneration } from "./run-typings-generation.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
13
|
+
import { isModuleLocaleDictionaryPath, isTranslationRegisteringSource } from "./translation-keys-sources.mjs";
|
|
14
|
+
import { extractModelResourceEntries, renderModelResourceRegistry, writeFileIfChanged } from "./model-resource-typings.mjs";
|
|
10
15
|
import { join, resolve } from "path";
|
|
11
16
|
import { ensureDirectoryAsync } from "@warlock.js/fs";
|
|
12
17
|
import { constants } from "fs";
|
|
@@ -55,6 +60,7 @@ var TypeGenerator = class {
|
|
|
55
60
|
this.manifest = null;
|
|
56
61
|
this.configCache = /* @__PURE__ */ new Map();
|
|
57
62
|
this.translationSourceFiles = /* @__PURE__ */ new Set();
|
|
63
|
+
this.modelResourceSourceFiles = /* @__PURE__ */ new Set();
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
66
|
* Generate all framework type definitions
|
|
@@ -80,6 +86,7 @@ var TypeGenerator = class {
|
|
|
80
86
|
await this.reconcile();
|
|
81
87
|
}
|
|
82
88
|
await this.generateTranslationTypes();
|
|
89
|
+
await this.generateModelResourceTypes();
|
|
83
90
|
await this.saveManifest();
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
@@ -233,11 +240,30 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
|
|
|
233
240
|
const keys = /* @__PURE__ */ new Set();
|
|
234
241
|
const sourceFiles = /* @__PURE__ */ new Set();
|
|
235
242
|
for (const [path, fileManager] of filesOrchestrator.getFiles()) {
|
|
236
|
-
if (!isTranslationRegisteringSource(fileManager.source)) continue;
|
|
243
|
+
if (!isTranslationRegisteringSource(path, fileManager.source)) continue;
|
|
237
244
|
sourceFiles.add(path);
|
|
245
|
+
if (isModuleLocaleDictionaryPath(path)) {
|
|
246
|
+
const dictionary = parseLocaleDictionary({
|
|
247
|
+
sourceFile: path,
|
|
248
|
+
source: fileManager.source,
|
|
249
|
+
defaultNamespace: path.split("/")[2]
|
|
250
|
+
});
|
|
251
|
+
for (const key of Object.keys(dictionary.entries)) keys.add(key);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
238
254
|
const sourceFile = await readConfigAst(fileManager.absolutePath);
|
|
239
255
|
if (sourceFile) for (const key of extractTranslationKeys(sourceFile)) keys.add(key);
|
|
240
256
|
}
|
|
257
|
+
for (const absolutePath of await getFilesFromDirectory(join(process.cwd(), "src", "app"), "**/utils/locales.json")) {
|
|
258
|
+
const path = Path.toRelative(absolutePath);
|
|
259
|
+
sourceFiles.add(path);
|
|
260
|
+
const dictionary = parseLocaleDictionary({
|
|
261
|
+
sourceFile: path,
|
|
262
|
+
source: await readFile(absolutePath, "utf-8"),
|
|
263
|
+
defaultNamespace: path.split("/")[2]
|
|
264
|
+
});
|
|
265
|
+
for (const key of Object.keys(dictionary.entries)) keys.add(key);
|
|
266
|
+
}
|
|
241
267
|
for (const key of await listRouteLocaleKeysForTypeGeneration()) keys.add(key);
|
|
242
268
|
this.translationSourceFiles = sourceFiles;
|
|
243
269
|
const content = `// Auto-generated by Warlock.js - DO NOT EDIT
|
|
@@ -256,6 +282,42 @@ ${Array.from(keys).sort().map((key) => ` ${JSON.stringify(key)}: true;`).join
|
|
|
256
282
|
await writeFile(join(this.outputDir, "translations.d.ts"), content, "utf-8");
|
|
257
283
|
devLogSuccess(`Generated translation types: ${keys.size} keys`);
|
|
258
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Generate `model-resources.d.ts`: the `ModelResourceRegistry` augmentation
|
|
287
|
+
* behind `Serialized<Model>`.
|
|
288
|
+
*
|
|
289
|
+
* Reads every discovered model file (`*.model.ts`) and records the ones that
|
|
290
|
+
* declare `static resource = <Identifier>`. Static AST read only, no app
|
|
291
|
+
* module is imported; see `model-resource-typings.ts` for what is skipped.
|
|
292
|
+
* The file is rewritten only when its text changes.
|
|
293
|
+
*/
|
|
294
|
+
async generateModelResourceTypes() {
|
|
295
|
+
try {
|
|
296
|
+
const entries = [];
|
|
297
|
+
const sourceFiles = /* @__PURE__ */ new Set();
|
|
298
|
+
const skippedModels = [];
|
|
299
|
+
for (const [path, fileManager] of filesOrchestrator.getFiles()) {
|
|
300
|
+
if (fileManager.type !== "model" || !fileManager.source.includes("resource")) continue;
|
|
301
|
+
const imports = await parseImports(fileManager.source, fileManager.absolutePath);
|
|
302
|
+
const extraction = extractModelResourceEntries({
|
|
303
|
+
source: fileManager.source,
|
|
304
|
+
absolutePath: fileManager.absolutePath,
|
|
305
|
+
resolveImport: (specifier) => imports.get(specifier)?.absolutePath
|
|
306
|
+
});
|
|
307
|
+
if (extraction.entries.length > 0) sourceFiles.add(path);
|
|
308
|
+
for (const entry of extraction.entries) {
|
|
309
|
+
entries.push(entry);
|
|
310
|
+
sourceFiles.add(Path.toRelative(entry.resourceFile));
|
|
311
|
+
}
|
|
312
|
+
for (const skipped of extraction.skipped) skippedModels.push(`${skipped.className} (${skipped.reason})`);
|
|
313
|
+
}
|
|
314
|
+
this.modelResourceSourceFiles = sourceFiles;
|
|
315
|
+
if (await writeFileIfChanged(join(this.outputDir, "model-resources.d.ts"), renderModelResourceRegistry(entries, this.outputDir))) devLogSuccess(`Generated model resource types: ${entries.length} models`);
|
|
316
|
+
if (skippedModels.length > 0) devLogDim(`Model resources not typed (not statically resolvable): ${skippedModels.join(", ")}`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
devServeLog(`⚠️ Failed to generate model resource types: ${error}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
259
321
|
isLocalesFile(path) {
|
|
260
322
|
return Path.normalize(path).includes("/utils/locales.");
|
|
261
323
|
}
|
|
@@ -513,13 +575,18 @@ ${allKeys.map((key) => ` "${key}": true;`).join("\n")}
|
|
|
513
575
|
*/
|
|
514
576
|
async executeTypingsGenerator(upcomingFiles) {
|
|
515
577
|
const files = filesOrchestrator.getFiles();
|
|
516
|
-
|
|
578
|
+
const aKnownTranslationSourceVanished = Array.from(this.translationSourceFiles).some((path) => !files.has(path));
|
|
579
|
+
const aKnownModelResourceSourceVanished = Array.from(this.modelResourceSourceFiles).some((path) => !files.has(path));
|
|
580
|
+
if (!(aKnownTranslationSourceVanished || aKnownModelResourceSourceVanished || Array.from(new Set(upcomingFiles)).some((file) => {
|
|
517
581
|
const normalizedPath = Path.normalize(file);
|
|
518
582
|
if (normalizedPath.includes("src/config/")) return true;
|
|
519
583
|
if (isRouteLocalesJsonPath(normalizedPath)) return true;
|
|
584
|
+
if (isModuleLocaleDictionaryPath(normalizedPath)) return true;
|
|
520
585
|
if (this.translationSourceFiles.has(normalizedPath)) return true;
|
|
586
|
+
if (this.modelResourceSourceFiles.has(normalizedPath)) return true;
|
|
587
|
+
if (files.get(normalizedPath)?.type === "model") return true;
|
|
521
588
|
const fileManager = files.get(normalizedPath);
|
|
522
|
-
return fileManager !== void 0 && isTranslationRegisteringSource(fileManager.source);
|
|
589
|
+
return fileManager !== void 0 && isTranslationRegisteringSource(normalizedPath, fileManager.source);
|
|
523
590
|
}))) return;
|
|
524
591
|
await runTypingsGeneration(this.typingsGenerationPorts);
|
|
525
592
|
}
|