@finesoft/front 0.5.1 → 0.5.2
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/README.md +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-DIU6Sxl3.mjs +1237 -0
- package/dist/browser-kFMjlLGT.d.mts +262 -0
- package/dist/browser.d.mts +7 -2
- package/dist/browser.mjs +9 -1
- package/dist/controller-types-CgmJ6-le.d.mts +16 -0
- package/dist/cookies-Bpf9VayB.d.mts +779 -0
- package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
- package/dist/host-guard-DDWxLpFL.mjs +222 -0
- package/dist/http-B6CJqDyf.d.mts +46 -0
- package/dist/http-CaxrMD1A.d.mts +1 -0
- package/dist/http-D70PL72H.mjs +257 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +2 -0
- package/dist/index-node.d.mts +15 -0
- package/dist/index-node.mjs +15 -0
- package/dist/index.d.mts +48 -697
- package/dist/index.mjs +44 -261
- package/dist/load-node.d.mts +5 -0
- package/dist/load-node.mjs +10 -0
- package/dist/load-portable.d.mts +5 -0
- package/dist/load-portable.mjs +8 -0
- package/dist/lru-map-BKoUAySU.mjs +50 -0
- package/dist/messages-CAt2QdGr.mjs +140 -0
- package/dist/native-contract-DuR25hYB.d.mts +14 -0
- package/dist/native-contract.d.mts +2 -0
- package/dist/native-contract.mjs +1 -0
- package/dist/node-D9hB4dsz.d.mts +35 -0
- package/dist/node.d.mts +2 -0
- package/dist/node.mjs +59 -0
- package/dist/path-CGFl2w7D.mjs +113 -0
- package/dist/path-CXT6xGPO.d.mts +261 -0
- package/dist/portable-CaxrMD1A.d.mts +1 -0
- package/dist/portable.d.mts +11 -0
- package/dist/portable.mjs +12 -0
- package/dist/proxy-1SphZ7x7.mjs +436 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -0
- package/dist/public-types-BcJM-AYc.mjs +835 -0
- package/dist/react-DhwBRw01.d.mts +16 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +29 -0
- package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
- package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
- package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
- package/dist/session-DnB4ZC3x.d.mts +1279 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/src-qwx7Vw8g.mjs +3807 -0
- package/dist/ssr-BEUNDvbj.d.mts +210 -0
- package/dist/ssr-C8xnYXoY.mjs +357 -0
- package/dist/ssr.d.mts +3 -0
- package/dist/ssr.mjs +3 -0
- package/dist/svelte-Dr5to3SE.d.mts +16 -0
- package/dist/svelte.d.mts +3 -0
- package/dist/svelte.mjs +13 -0
- package/dist/typegen-C-WeJCtf.d.mts +12 -0
- package/dist/typegen-cli.d.mts +1 -0
- package/dist/typegen-cli.mjs +11 -0
- package/dist/typegen.d.mts +3 -0
- package/dist/typegen.mjs +2 -0
- package/dist/types-BuaZHRG7.mjs +402 -0
- package/dist/undici-CPfL25Hr.mjs +22262 -0
- package/dist/vite-Cj4SPA8D.d.mts +277 -0
- package/dist/vite.d.mts +4 -0
- package/dist/vite.mjs +2354 -0
- package/dist/vue-DGmzuKho.d.mts +32 -0
- package/dist/vue.d.mts +3 -0
- package/dist/vue.mjs +56 -0
- package/dist/web.d.mts +6 -0
- package/dist/web.mjs +8 -0
- package/dist/worker.d.mts +2 -0
- package/dist/worker.mjs +2 -0
- package/docs/01-getting-started.md +67 -199
- package/docs/02-routing-and-controllers.md +163 -241
- package/docs/03-middleware.md +10 -212
- package/docs/04-rendering-and-hydration.md +6 -333
- package/docs/05-i18n.md +7 -237
- package/docs/06-http-client.md +20 -263
- package/docs/07-di-container.md +23 -257
- package/docs/08-observability.md +6 -286
- package/docs/09-server-and-deployment.md +59 -219
- package/docs/10-features-platform-pwa.md +7 -231
- package/docs/11-navigation.md +143 -288
- package/docs/12-session-restoration.md +6 -214
- package/docs/README.md +7 -7
- package/docs/advanced/custom-action-handler.md +29 -229
- package/docs/advanced/custom-adapter.md +7 -259
- package/docs/advanced/custom-event-recorder.md +7 -312
- package/docs/advanced/inline-proxy-codegen.md +6 -185
- package/docs/advanced/multi-tenant-scopes.md +10 -323
- package/docs/engineering/ci-release-flow.md +35 -222
- package/docs/engineering/project-structure.md +34 -277
- package/docs/engineering/testing.md +8 -310
- package/docs/pitfalls/container-scope-leak.md +2 -214
- package/docs/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/pitfalls/ssr-hydration-mismatch.md +4 -160
- package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/docs/zh/01-getting-started.md +67 -199
- package/docs/zh/02-routing-and-controllers.md +166 -244
- package/docs/zh/03-middleware.md +10 -212
- package/docs/zh/04-rendering-and-hydration.md +6 -333
- package/docs/zh/05-i18n.md +7 -237
- package/docs/zh/06-http-client.md +20 -263
- package/docs/zh/07-di-container.md +23 -257
- package/docs/zh/08-observability.md +6 -283
- package/docs/zh/09-server-and-deployment.md +59 -219
- package/docs/zh/10-features-platform-pwa.md +7 -231
- package/docs/zh/11-navigation.md +130 -290
- package/docs/zh/12-session-restoration.md +6 -214
- package/docs/zh/README.md +4 -4
- package/docs/zh/advanced/custom-action-handler.md +29 -229
- package/docs/zh/advanced/custom-adapter.md +7 -259
- package/docs/zh/advanced/custom-event-recorder.md +7 -312
- package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
- package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
- package/docs/zh/engineering/ci-release-flow.md +35 -222
- package/docs/zh/engineering/project-structure.md +34 -277
- package/docs/zh/engineering/testing.md +8 -310
- package/docs/zh/pitfalls/container-scope-leak.md +2 -214
- package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +4 -160
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BHhVWXik.mjs +0 -2
- package/dist/browser-BV2BBXm7.d.mts +0 -2811
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import { t as LruMap } from "./lru-map-BKoUAySU.mjs";
|
|
2
|
+
import "./src-Ftl_0rhu.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region ../server/src/controller-types.ts
|
|
8
|
+
const OUTPUT = ".finesoft/controller-types.d.ts";
|
|
9
|
+
const GENERATED = "// Generated by @finesoft/front. Edit route schemas, not this file.\n";
|
|
10
|
+
const PROBE = "__finesoftRouteInputProbe";
|
|
11
|
+
const moduleNames = /* @__PURE__ */ new Set([
|
|
12
|
+
"@finesoft/front",
|
|
13
|
+
"@finesoft/core",
|
|
14
|
+
"@finesoft/web",
|
|
15
|
+
"@finesoft/ssr"
|
|
16
|
+
]);
|
|
17
|
+
const cached = /* @__PURE__ */ new Map();
|
|
18
|
+
const hash = (content) => createHash("sha256").update(content === void 0 ? "missing" : `file:${content}`).digest("hex");
|
|
19
|
+
let retained;
|
|
20
|
+
const slash$1 = (value) => value.replaceAll("\\", "/");
|
|
21
|
+
const inside = (root, file) => {
|
|
22
|
+
const relative = path.relative(root, file);
|
|
23
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
24
|
+
};
|
|
25
|
+
function assertEditablePath(root, file) {
|
|
26
|
+
let existing = file;
|
|
27
|
+
while (!fs.existsSync(existing)) {
|
|
28
|
+
if (fs.lstatSync(existing, { throwIfNoEntry: false })) throw Error(`Controller type generation cannot edit outside ${root}`);
|
|
29
|
+
existing = path.dirname(existing);
|
|
30
|
+
}
|
|
31
|
+
if (!inside(root, fs.realpathSync(existing))) throw Error(`Controller type generation cannot edit outside ${root}`);
|
|
32
|
+
}
|
|
33
|
+
function applyEdits(source, edits) {
|
|
34
|
+
let boundary = source.length;
|
|
35
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
36
|
+
if (edit.end > boundary) throw Error("Overlapping controller type edits");
|
|
37
|
+
source = source.slice(0, edit.start) + edit.text + source.slice(edit.end);
|
|
38
|
+
boundary = edit.start;
|
|
39
|
+
}
|
|
40
|
+
return source;
|
|
41
|
+
}
|
|
42
|
+
function stamp(files) {
|
|
43
|
+
return files.map((file) => {
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs.statSync(file);
|
|
46
|
+
return `${file}:${stat.mtimeMs}:${stat.size}`;
|
|
47
|
+
} catch {
|
|
48
|
+
return `${file}:missing`;
|
|
49
|
+
}
|
|
50
|
+
}).join("\n");
|
|
51
|
+
}
|
|
52
|
+
/** Generates type-only references; never imports or executes application modules. */
|
|
53
|
+
function generateControllerTypes(options = {}) {
|
|
54
|
+
return generateTypes(options);
|
|
55
|
+
}
|
|
56
|
+
function generateTypes(options, observe, force = false) {
|
|
57
|
+
const requestedRoot = path.resolve(options.root ?? process.cwd());
|
|
58
|
+
const requestedConfig = path.resolve(requestedRoot, options.tsconfig ?? "tsconfig.json");
|
|
59
|
+
if (!fs.existsSync(requestedConfig) || !fs.existsSync(path.join(requestedRoot, "src"))) return {
|
|
60
|
+
controllers: 0,
|
|
61
|
+
changed: []
|
|
62
|
+
};
|
|
63
|
+
const root = fs.realpathSync(requestedRoot);
|
|
64
|
+
const configFile = fs.realpathSync(requestedConfig);
|
|
65
|
+
const compiler = createRequire(path.join(root, "package.json"))("typescript");
|
|
66
|
+
const tokens = (source) => {
|
|
67
|
+
const scanner = compiler.createScanner(compiler.ScriptTarget.Latest, true);
|
|
68
|
+
scanner.setText(source);
|
|
69
|
+
const result = [];
|
|
70
|
+
while (scanner.scan() !== compiler.SyntaxKind.EndOfFileToken) result.push(`${scanner.getToken()}:${scanner.getTokenValue() || scanner.getTokenText()}`);
|
|
71
|
+
return result.join("\n");
|
|
72
|
+
};
|
|
73
|
+
const configFiles = /* @__PURE__ */ new Set([configFile]);
|
|
74
|
+
const configContents = /* @__PURE__ */ new Map();
|
|
75
|
+
const readConfig = (file) => {
|
|
76
|
+
configFiles.add(file);
|
|
77
|
+
const content = compiler.sys.readFile(file);
|
|
78
|
+
configContents.set(file, content);
|
|
79
|
+
return content;
|
|
80
|
+
};
|
|
81
|
+
const config = compiler.readConfigFile(configFile, readConfig);
|
|
82
|
+
if (config.error) throw Error(compiler.flattenDiagnosticMessageText(config.error.messageText, "\n"));
|
|
83
|
+
const parsed = compiler.parseJsonConfigFileContent(config.config, {
|
|
84
|
+
...compiler.sys,
|
|
85
|
+
readFile: readConfig
|
|
86
|
+
}, path.dirname(configFile));
|
|
87
|
+
if (parsed.errors.length) throw Error(parsed.errors.map((error) => compiler.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"));
|
|
88
|
+
if (!parsed.fileNames.length && parsed.projectReferences?.length) {
|
|
89
|
+
const projects = [];
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
const inspect = (reference) => {
|
|
92
|
+
const file = compiler.resolveProjectReferencePath(reference);
|
|
93
|
+
if (seen.has(file)) return;
|
|
94
|
+
seen.add(file);
|
|
95
|
+
const project = compiler.getParsedCommandLineOfConfigFile(file, {}, {
|
|
96
|
+
...compiler.sys,
|
|
97
|
+
onUnRecoverableConfigFileDiagnostic: (error) => {
|
|
98
|
+
throw Error(compiler.flattenDiagnosticMessageText(error.messageText, "\n"));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (project?.fileNames.some((file) => inside(path.join(root, "src"), file))) projects.push(file);
|
|
102
|
+
else project?.projectReferences?.forEach(inspect);
|
|
103
|
+
};
|
|
104
|
+
parsed.projectReferences.forEach(inspect);
|
|
105
|
+
if (projects.length === 1) return generateTypes({
|
|
106
|
+
...options,
|
|
107
|
+
tsconfig: projects[0]
|
|
108
|
+
}, observe, force);
|
|
109
|
+
throw Error("Select the application project with controllerTypes.tsconfig; the solution config must reference exactly one project containing src.");
|
|
110
|
+
}
|
|
111
|
+
const cacheKey = `${root}#${configFile}`;
|
|
112
|
+
const rootFiles = parsed.fileNames.filter((file) => inside(root, file));
|
|
113
|
+
const previous = cached.get(cacheKey);
|
|
114
|
+
const initialFiles = [.../* @__PURE__ */ new Set([
|
|
115
|
+
...configFiles,
|
|
116
|
+
...rootFiles,
|
|
117
|
+
...previous?.files ?? []
|
|
118
|
+
])].sort();
|
|
119
|
+
const inputStamps = new Map(initialFiles.map((file) => [file, stamp([file])]));
|
|
120
|
+
if (!force && previous && previous.stamp === [...inputStamps.values()].join("\n")) {
|
|
121
|
+
observe?.(previous);
|
|
122
|
+
return {
|
|
123
|
+
...previous.result,
|
|
124
|
+
changed: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const output = path.join(root, OUTPUT);
|
|
128
|
+
const typeImports = (source) => source.statements.filter((statement) => compiler.isImportDeclaration(statement) && compiler.isStringLiteral(statement.moduleSpecifier) && path.resolve(path.dirname(source.fileName), statement.moduleSpecifier.text) === output.replace(/\.d\.ts$/, "") && statement.importClause?.isTypeOnly === true && !!statement.importClause.namedBindings && compiler.isNamedImports(statement.importClause.namedBindings));
|
|
129
|
+
const typeImport = (type) => {
|
|
130
|
+
if (!compiler.isTypeReferenceNode(type) || !compiler.isIdentifier(type.typeName)) return;
|
|
131
|
+
let owner = type.parent;
|
|
132
|
+
while (owner && !compiler.isClassDeclaration(owner)) owner = owner.parent;
|
|
133
|
+
if (!owner?.name) return;
|
|
134
|
+
const className = owner.name.text;
|
|
135
|
+
const source = type.getSourceFile();
|
|
136
|
+
const key = `${slash$1(path.relative(root, source.fileName))}#${className}`;
|
|
137
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 12);
|
|
138
|
+
const exports = ["Input", "Failure"].flatMap((suffix) => {
|
|
139
|
+
const name = `${className}${suffix}`;
|
|
140
|
+
return [name, `${name}_${hash}`];
|
|
141
|
+
});
|
|
142
|
+
return typeImports(source).flatMap((statement) => statement.importClause.namedBindings.elements).find((element) => element.name.text === type.getText() && exports.includes((element.propertyName ?? element.name).text) && checker.getSymbolAtLocation(type.typeName) === checker.getSymbolAtLocation(element.name));
|
|
143
|
+
};
|
|
144
|
+
const settings = {
|
|
145
|
+
...parsed.options,
|
|
146
|
+
noEmit: true,
|
|
147
|
+
skipLibCheck: true
|
|
148
|
+
};
|
|
149
|
+
const memoize = (read) => {
|
|
150
|
+
const values = /* @__PURE__ */ new Map();
|
|
151
|
+
return (file) => {
|
|
152
|
+
if (!values.has(file)) values.set(file, read(file));
|
|
153
|
+
return values.get(file);
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const readFiles = /* @__PURE__ */ new Set();
|
|
157
|
+
const readFile = memoize((file) => {
|
|
158
|
+
readFiles.add(file);
|
|
159
|
+
if (!inputStamps.has(file)) inputStamps.set(file, stamp([file]));
|
|
160
|
+
return compiler.sys.readFile(file);
|
|
161
|
+
});
|
|
162
|
+
const fileExists = memoize(compiler.sys.fileExists.bind(compiler.sys));
|
|
163
|
+
const directoryExists = memoize(compiler.sys.directoryExists.bind(compiler.sys));
|
|
164
|
+
const realpath = compiler.sys.realpath && memoize(compiler.sys.realpath.bind(compiler.sys));
|
|
165
|
+
const resolutions = compiler.createModuleResolutionCache(root, (file) => compiler.sys.useCaseSensitiveFileNames ? file : file.toLowerCase(), settings);
|
|
166
|
+
let lastProgram = retained?.key === cacheKey && retained.compiler === compiler ? retained.program : void 0;
|
|
167
|
+
let firstPhase = true;
|
|
168
|
+
const programFiles = [...rootFiles, output];
|
|
169
|
+
const buildProgram = (options, overrides = /* @__PURE__ */ new Map()) => {
|
|
170
|
+
const host = compiler.createCompilerHost(options);
|
|
171
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
172
|
+
let previous = lastProgram;
|
|
173
|
+
host.readFile = (file) => overrides.get(file) ?? readFile(file);
|
|
174
|
+
host.fileExists = (file) => overrides.has(file) || fileExists(file);
|
|
175
|
+
host.directoryExists = (directory) => directory === path.dirname(output) && overrides.has(output) || directoryExists(directory);
|
|
176
|
+
host.realpath = realpath;
|
|
177
|
+
if (overrides.has(output) && !lastProgram?.getSourceFile(output)) resolutions.clear();
|
|
178
|
+
host.getModuleResolutionCache = () => resolutions;
|
|
179
|
+
host.resolveModuleNameLiterals = (imports, file, redirected, options, source) => imports.map((specifier) => compiler.resolveModuleName(specifier.text, file, options, host, resolutions, redirected, compiler.getModeForUsageLocation(source, specifier, options)));
|
|
180
|
+
const refreshResolutions = firstPhase;
|
|
181
|
+
firstPhase = false;
|
|
182
|
+
host.hasInvalidatedResolutions = () => refreshResolutions;
|
|
183
|
+
host.getSourceFile = (file, language, onError, fresh) => {
|
|
184
|
+
const source = previous?.getSourceFile(file);
|
|
185
|
+
const target = typeof language === "number" ? language : language.languageVersion;
|
|
186
|
+
const format = typeof language === "number" ? void 0 : language.impliedNodeFormat;
|
|
187
|
+
if (!fresh && source && source.languageVersion === target && source.impliedNodeFormat === format && source.text === host.readFile(file)) return source;
|
|
188
|
+
return getSourceFile(file, language, onError, fresh);
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
return lastProgram = compiler.createProgram(programFiles, options, host, previous);
|
|
192
|
+
} finally {
|
|
193
|
+
previous = void 0;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const sourceProgram = buildProgram(settings);
|
|
197
|
+
const syntaxErrors = sourceProgram.getSyntacticDiagnostics();
|
|
198
|
+
if (syntaxErrors.length) throw Error(`Fix source syntax before generating controller types: ${syntaxErrors.map((error) => `${error.file?.fileName}: ${compiler.flattenDiagnosticMessageText(error.messageText, "\n")}`).join("\n")}`);
|
|
199
|
+
const checker = sourceProgram.getTypeChecker();
|
|
200
|
+
const resolve = (symbol) => symbol && (symbol.flags & compiler.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol);
|
|
201
|
+
const definitions = /* @__PURE__ */ new Set();
|
|
202
|
+
const bases = /* @__PURE__ */ new Set();
|
|
203
|
+
const serverBases = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const source of sourceProgram.getSourceFiles()) {
|
|
205
|
+
if (!inside(root, source.fileName) || source.isDeclarationFile) continue;
|
|
206
|
+
for (const statement of source.statements) {
|
|
207
|
+
if (!compiler.isImportDeclaration(statement) || !compiler.isStringLiteral(statement.moduleSpecifier) || !moduleNames.has(statement.moduleSpecifier.text)) continue;
|
|
208
|
+
const symbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
209
|
+
if (!symbol) continue;
|
|
210
|
+
for (const exported of checker.getExportsOfModule(symbol)) {
|
|
211
|
+
const resolved = resolve(exported);
|
|
212
|
+
if (resolved && exported.name === "definePage") definitions.add(resolved);
|
|
213
|
+
if (resolved && exported.name === "BaseController") bases.add(resolved);
|
|
214
|
+
if (resolved && exported.name === "BaseServerController") {
|
|
215
|
+
bases.add(resolved);
|
|
216
|
+
serverBases.add(resolved);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const registrations = [];
|
|
222
|
+
const serverController = (node) => {
|
|
223
|
+
const base = node.heritageClauses?.find((clause) => clause.token === compiler.SyntaxKind.ExtendsKeyword)?.types[0];
|
|
224
|
+
return !!base && serverBases.has(resolve(checker.getSymbolAtLocation(base.expression)));
|
|
225
|
+
};
|
|
226
|
+
const classes = [];
|
|
227
|
+
const visit = (node) => {
|
|
228
|
+
if (compiler.isClassDeclaration(node) && node.name) {
|
|
229
|
+
const base = node.heritageClauses?.find((clause) => clause.token === compiler.SyntaxKind.ExtendsKeyword)?.types[0];
|
|
230
|
+
if (base && bases.has(resolve(checker.getSymbolAtLocation(base.expression)))) classes.push(node);
|
|
231
|
+
}
|
|
232
|
+
if (compiler.isCallExpression(node) && definitions.has(resolve(checker.getSymbolAtLocation(node.expression))) && node.arguments[0]) {
|
|
233
|
+
const input = checker.getTypeAtLocation(node.arguments[0]);
|
|
234
|
+
const routes = input.getProperty("routes");
|
|
235
|
+
const create = input.getProperty("create");
|
|
236
|
+
if (routes && create) {
|
|
237
|
+
const factory = checker.getTypeOfSymbolAtLocation(create, node);
|
|
238
|
+
for (const signature of factory.getCallSignatures()) {
|
|
239
|
+
const result = signature.getReturnType();
|
|
240
|
+
for (const type of result.isUnion() ? result.types : [result]) {
|
|
241
|
+
const declaration = type.getSymbol()?.valueDeclaration;
|
|
242
|
+
if (!declaration || !compiler.isClassDeclaration(declaration) || !declaration.name) continue;
|
|
243
|
+
const base = declaration.heritageClauses?.find((clause) => clause.token === compiler.SyntaxKind.ExtendsKeyword)?.types[0];
|
|
244
|
+
if (!base || !bases.has(resolve(checker.getSymbolAtLocation(base.expression)))) continue;
|
|
245
|
+
if (!inside(root, declaration.getSourceFile().fileName)) throw Error(`Controller type generation cannot edit outside ${root}`);
|
|
246
|
+
registrations.push({
|
|
247
|
+
call: node,
|
|
248
|
+
controller: declaration
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
compiler.forEachChild(node, visit);
|
|
255
|
+
};
|
|
256
|
+
for (const source of sourceProgram.getSourceFiles()) if (inside(root, source.fileName) && !source.isDeclarationFile && !source.fileName.includes(`${path.sep}node_modules${path.sep}`)) visit(source);
|
|
257
|
+
const virtual = /* @__PURE__ */ new Map();
|
|
258
|
+
const calls = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const { call } of registrations) {
|
|
260
|
+
const file = call.getSourceFile().fileName;
|
|
261
|
+
let positions = calls.get(file);
|
|
262
|
+
if (!positions) calls.set(file, positions = /* @__PURE__ */ new Set());
|
|
263
|
+
positions.add(call.expression.getStart());
|
|
264
|
+
}
|
|
265
|
+
for (const [file, positions] of calls) {
|
|
266
|
+
const source = sourceProgram.getSourceFile(file);
|
|
267
|
+
const edits = [];
|
|
268
|
+
const replace = (node) => {
|
|
269
|
+
if (compiler.isCallExpression(node) && positions.has(node.expression.getStart())) edits.push({
|
|
270
|
+
start: node.expression.getStart(),
|
|
271
|
+
end: node.expression.end,
|
|
272
|
+
text: PROBE
|
|
273
|
+
});
|
|
274
|
+
compiler.forEachChild(node, replace);
|
|
275
|
+
};
|
|
276
|
+
replace(source);
|
|
277
|
+
virtual.set(file, applyEdits(source.text, edits) + `\ndeclare function ${PROBE}<const R extends readonly (string | import("@finesoft/front").PageRoute)[]>(input: { routes: R; [key: string]: unknown }): import("@finesoft/front").RouteInputFor<R>;\n`);
|
|
278
|
+
}
|
|
279
|
+
virtual.set(output, "export {};\n");
|
|
280
|
+
const program = buildProgram(settings, virtual);
|
|
281
|
+
const types = program.getTypeChecker();
|
|
282
|
+
const inferred = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const [file] of calls) {
|
|
284
|
+
const found = [];
|
|
285
|
+
const collect = (node) => {
|
|
286
|
+
if (compiler.isCallExpression(node) && compiler.isIdentifier(node.expression) && node.expression.text === PROBE) found.push(types.getTypeAtLocation(node));
|
|
287
|
+
compiler.forEachChild(node, collect);
|
|
288
|
+
};
|
|
289
|
+
collect(program.getSourceFile(file));
|
|
290
|
+
inferred.set(file, found);
|
|
291
|
+
}
|
|
292
|
+
const byCall = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const [file] of calls) [...new Set(registrations.filter((entry) => entry.call.getSourceFile().fileName === file).map((entry) => entry.call))].forEach((call, index) => byCall.set(call, inferred.get(file)[index]));
|
|
294
|
+
const printer = compiler.createPrinter();
|
|
295
|
+
const outputSource = program.getSourceFile(output);
|
|
296
|
+
const factory = compiler.factory;
|
|
297
|
+
const schemaOutputs = /* @__PURE__ */ new Map();
|
|
298
|
+
let schemaOutputsReady = false;
|
|
299
|
+
const propertyType = (type, key, location) => {
|
|
300
|
+
const property = types.getNonNullableType(type).getProperty(key);
|
|
301
|
+
return property && types.getTypeOfSymbolAtLocation(property, location);
|
|
302
|
+
};
|
|
303
|
+
const schemaOutput = (type) => {
|
|
304
|
+
if (!(type.flags & (compiler.TypeFlags.Object | compiler.TypeFlags.Intersection))) return;
|
|
305
|
+
if (!schemaOutputsReady) {
|
|
306
|
+
schemaOutputsReady = true;
|
|
307
|
+
for (const source of program.getSourceFiles()) {
|
|
308
|
+
const symbol = types.getSymbolAtLocation(source);
|
|
309
|
+
if (!symbol || source.fileName === output) continue;
|
|
310
|
+
for (const exported of types.getExportsOfModule(symbol)) {
|
|
311
|
+
const type = types.getTypeOfSymbolAtLocation(exported, source);
|
|
312
|
+
const signatures = type.getCallSignatures();
|
|
313
|
+
const schema = signatures.length ? signatures[0].getReturnType() : type;
|
|
314
|
+
const standard = propertyType(schema, "~standard", source);
|
|
315
|
+
const metadata = standard && propertyType(standard, "types", source);
|
|
316
|
+
const result = metadata && propertyType(metadata, "output", source);
|
|
317
|
+
if (!result || !(result.flags & (compiler.TypeFlags.Object | compiler.TypeFlags.Intersection))) continue;
|
|
318
|
+
let modulePath = slash$1(path.relative(path.dirname(output), source.fileName)).replace(/(\.d)?\.([cm]?)tsx?$/, ".$2js");
|
|
319
|
+
if (!modulePath.startsWith(".")) modulePath = `./${modulePath}`;
|
|
320
|
+
const reference = factory.createImportTypeNode(factory.createLiteralTypeNode(factory.createStringLiteral(modulePath)), void 0, factory.createIdentifier(exported.name), void 0, true);
|
|
321
|
+
const schemaReference = signatures.length ? factory.createTypeReferenceNode("ReturnType", [reference]) : reference;
|
|
322
|
+
schemaOutputs.set(result, factory.createImportTypeNode(factory.createLiteralTypeNode(factory.createStringLiteral("@finesoft/front")), void 0, factory.createIdentifier("InferOutput"), [schemaReference]));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return schemaOutputs.get(type);
|
|
327
|
+
};
|
|
328
|
+
const buildTypeNode = (type, depth = 0) => {
|
|
329
|
+
const schema = depth >= 2 ? schemaOutput(type) : void 0;
|
|
330
|
+
if (schema) return schema;
|
|
331
|
+
if (type.isUnion()) return factory.createUnionTypeNode(type.types.map((value) => buildTypeNode(value, depth)));
|
|
332
|
+
const node = types.typeToTypeNode(type, outputSource, compiler.NodeBuilderFlags.NoTruncation | compiler.NodeBuilderFlags.UseFullyQualifiedType | compiler.NodeBuilderFlags.InTypeAlias);
|
|
333
|
+
if (!node) throw Error("Cannot serialize inferred controller input");
|
|
334
|
+
if (!compiler.isTypeLiteralNode(node)) return node;
|
|
335
|
+
return factory.updateTypeLiteralNode(node, factory.createNodeArray(node.members.map((member) => {
|
|
336
|
+
if (!compiler.isPropertySignature(member) || !member.type || !(compiler.isIdentifier(member.name) || compiler.isStringLiteral(member.name))) return member;
|
|
337
|
+
const value = propertyType(type, member.name.text, outputSource);
|
|
338
|
+
return value ? factory.updatePropertySignature(member, member.modifiers, member.name, member.questionToken, buildTypeNode(value, depth + 1)) : member;
|
|
339
|
+
})));
|
|
340
|
+
};
|
|
341
|
+
const printType = (type) => {
|
|
342
|
+
if (!type || type.flags & (compiler.TypeFlags.Any | compiler.TypeFlags.Unknown)) throw Error("Cannot infer controller input. Check route schemas and @finesoft/front types.");
|
|
343
|
+
const node = buildTypeNode(type);
|
|
344
|
+
const rejectAny = (value) => {
|
|
345
|
+
if (value.kind === compiler.SyntaxKind.AnyKeyword) throw Error("Cannot generate an any controller input. Check route schemas and their imports.");
|
|
346
|
+
compiler.forEachChild(value, rejectAny);
|
|
347
|
+
};
|
|
348
|
+
rejectAny(node);
|
|
349
|
+
const transformed = compiler.transform(node, [(context) => {
|
|
350
|
+
const visitType = (value) => {
|
|
351
|
+
if (compiler.isImportTypeNode(value) && compiler.isLiteralTypeNode(value.argument) && compiler.isStringLiteral(value.argument.literal) && path.isAbsolute(value.argument.literal.text)) {
|
|
352
|
+
let relative = slash$1(path.relative(path.dirname(output), value.argument.literal.text));
|
|
353
|
+
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
354
|
+
return context.factory.updateImportTypeNode(value, context.factory.createLiteralTypeNode(context.factory.createStringLiteral(relative)), value.attributes, value.qualifier, value.typeArguments, value.isTypeOf);
|
|
355
|
+
}
|
|
356
|
+
return compiler.visitEachChild(value, visitType, context);
|
|
357
|
+
};
|
|
358
|
+
return (node) => compiler.visitNode(node, visitType);
|
|
359
|
+
}]);
|
|
360
|
+
try {
|
|
361
|
+
return printer.printNode(compiler.EmitHint.Unspecified, transformed.transformed[0], outputSource);
|
|
362
|
+
} finally {
|
|
363
|
+
transformed.dispose();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const managed = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const { call, controller } of registrations) {
|
|
368
|
+
const file = controller.getSourceFile();
|
|
369
|
+
if (!controller.members.filter((member) => compiler.isMethodDeclaration(member) && compiler.isIdentifier(member.name) && ["execute", "fallback"].includes(member.name.text)).some((method) => method.parameters.some((parameter) => !parameter.type || typeImport(parameter.type)))) continue;
|
|
370
|
+
const name = controller.name.text;
|
|
371
|
+
let entry = managed.get(controller);
|
|
372
|
+
if (!entry) managed.set(controller, entry = {
|
|
373
|
+
node: controller,
|
|
374
|
+
key: `${slash$1(path.relative(root, file.fileName))}#${name}`,
|
|
375
|
+
name,
|
|
376
|
+
inputs: []
|
|
377
|
+
});
|
|
378
|
+
entry.inputs.push(printType(byCall.get(call)));
|
|
379
|
+
}
|
|
380
|
+
const saved = fs.existsSync(output) ? fs.readFileSync(output, "utf8") : "";
|
|
381
|
+
let replayable = true;
|
|
382
|
+
if (saved.startsWith(GENERATED)) {
|
|
383
|
+
const previous = compiler.createSourceFile(output, saved, compiler.ScriptTarget.Latest, true);
|
|
384
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
385
|
+
for (const statement of previous.statements) if (compiler.isInterfaceDeclaration(statement) && statement.name.text === "ControllerInputs") {
|
|
386
|
+
for (const member of statement.members) if (compiler.isPropertySignature(member) && compiler.isStringLiteral(member.name) && member.type) inputs.set(member.name.text, member.type.getText(previous));
|
|
387
|
+
}
|
|
388
|
+
for (const controller of classes) {
|
|
389
|
+
if (managed.has(controller)) continue;
|
|
390
|
+
const source = controller.getSourceFile();
|
|
391
|
+
const name = controller.name.text;
|
|
392
|
+
const key = `${slash$1(path.relative(root, source.fileName))}#${name}`;
|
|
393
|
+
if (inputs.has(key) && controller.members.some((member) => compiler.isMethodDeclaration(member) && member.parameters.some((parameter) => parameter.type && typeImport(parameter.type)))) {
|
|
394
|
+
replayable = false;
|
|
395
|
+
managed.set(controller, {
|
|
396
|
+
node: controller,
|
|
397
|
+
key,
|
|
398
|
+
name,
|
|
399
|
+
inputs: [inputs.get(key)]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const updates = /* @__PURE__ */ new Map();
|
|
405
|
+
const sourceEdits = /* @__PURE__ */ new Map();
|
|
406
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
407
|
+
const names = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const entry of managed.values()) names.set(entry.name, (names.get(entry.name) ?? 0) + 1);
|
|
409
|
+
const exportedName = (entry, suffix = "Input") => `${entry.name}${suffix}${names.get(entry.name) > 1 ? `_${createHash("sha256").update(entry.key).digest("hex").slice(0, 12)}` : ""}`;
|
|
410
|
+
const modulePaths = /* @__PURE__ */ new Map();
|
|
411
|
+
const localNames = /* @__PURE__ */ new Map();
|
|
412
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
413
|
+
const ownedImports = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const entry of managed.values()) {
|
|
415
|
+
const annotations = entry.node.members.flatMap((member) => compiler.isMethodDeclaration(member) && compiler.isIdentifier(member.name) && ["execute", "fallback"].includes(member.name.text) ? member.parameters.flatMap((parameter) => parameter.type ? [parameter.type] : []) : []);
|
|
416
|
+
for (const clause of entry.node.heritageClauses ?? []) if (clause.token === compiler.SyntaxKind.ExtendsKeyword) annotations.push(...clause.types[0].typeArguments?.slice(0, 1) ?? []);
|
|
417
|
+
for (const type of annotations) {
|
|
418
|
+
const imported = typeImport(type);
|
|
419
|
+
if (imported) ownedImports.add(imported);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const entry of managed.values()) {
|
|
423
|
+
const source = entry.node.getSourceFile();
|
|
424
|
+
fileCounts.set(source, (fileCounts.get(source) ?? 0) + 1);
|
|
425
|
+
if (localNames.has(source)) continue;
|
|
426
|
+
const imported = new Set(typeImports(source).flatMap((statement) => statement.importClause.namedBindings.elements.filter((element) => ownedImports.has(element)).map((element) => element.name.text)));
|
|
427
|
+
const used = /* @__PURE__ */ new Set();
|
|
428
|
+
const visit = (node) => {
|
|
429
|
+
if (compiler.isIdentifier(node) && !imported.has(node.text)) used.add(node.text);
|
|
430
|
+
compiler.forEachChild(node, visit);
|
|
431
|
+
};
|
|
432
|
+
visit(source);
|
|
433
|
+
localNames.set(source, used);
|
|
434
|
+
}
|
|
435
|
+
for (const entry of managed.values()) {
|
|
436
|
+
const source = entry.node.getSourceFile();
|
|
437
|
+
let edits = sourceEdits.get(source);
|
|
438
|
+
if (!edits) sourceEdits.set(source, edits = []);
|
|
439
|
+
const imports = typeImports(source).flatMap((statement) => statement.importClause.namedBindings.elements);
|
|
440
|
+
const alias = (suffix) => {
|
|
441
|
+
const used = localNames.get(source);
|
|
442
|
+
const exported = `${entry.name}${suffix}`;
|
|
443
|
+
let name = imports.find((element) => {
|
|
444
|
+
const name = (element.propertyName ?? element.name).text;
|
|
445
|
+
return ownedImports.has(element) && (name === exported || name.startsWith(exported) && /^_[a-f0-9]{12}$/.test(name.slice(exported.length)));
|
|
446
|
+
})?.name.text ?? (fileCounts.get(source) === 1 ? suffix : exported);
|
|
447
|
+
if (used.has(name)) name = `${entry.name}${suffix}`;
|
|
448
|
+
for (let i = 2; used.has(name); i++) name = `${entry.name}${suffix}${i}`;
|
|
449
|
+
used.add(name);
|
|
450
|
+
return name;
|
|
451
|
+
};
|
|
452
|
+
const parameterType = alias("Input");
|
|
453
|
+
const failureType = alias("Failure");
|
|
454
|
+
let modulePath = slash$1(path.relative(path.dirname(source.fileName), output)).replace(/\.d\.ts$/, "");
|
|
455
|
+
if (!modulePath.startsWith(".")) modulePath = `./${modulePath}`;
|
|
456
|
+
const list = aliases.get(source) ?? [];
|
|
457
|
+
const importAs = (suffix, local) => {
|
|
458
|
+
const name = exportedName(entry, suffix);
|
|
459
|
+
list.push(name === local ? name : `${name} as ${local}`);
|
|
460
|
+
};
|
|
461
|
+
importAs("Input", parameterType);
|
|
462
|
+
aliases.set(source, list);
|
|
463
|
+
modulePaths.set(source, modulePath);
|
|
464
|
+
for (const member of entry.node.members) {
|
|
465
|
+
if (!compiler.isMethodDeclaration(member) || !compiler.isIdentifier(member.name) || !["execute", "fallback"].includes(member.name.text)) continue;
|
|
466
|
+
if (member.parameters.length > 1) throw Error(`Use ${member.name.text}({ params, query, context${member.name.text === "fallback" ? ", error" : ""} }) in ${source.fileName}`);
|
|
467
|
+
const parameter = member.parameters[0];
|
|
468
|
+
if (!parameter || parameter.type && !typeImport(parameter.type)) continue;
|
|
469
|
+
const desired = member.name.text === "execute" ? parameterType : failureType;
|
|
470
|
+
if (member.name.text === "fallback") importAs("Failure", failureType);
|
|
471
|
+
edits.push(parameter.type ? {
|
|
472
|
+
start: parameter.type.getStart(),
|
|
473
|
+
end: parameter.type.end,
|
|
474
|
+
text: desired
|
|
475
|
+
} : {
|
|
476
|
+
start: parameter.questionToken?.end ?? parameter.name.end,
|
|
477
|
+
end: parameter.questionToken?.end ?? parameter.name.end,
|
|
478
|
+
text: `: ${desired}`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const base = entry.node.heritageClauses.find((clause) => clause.token === compiler.SyntaxKind.ExtendsKeyword).types[0];
|
|
482
|
+
const execute = entry.node.members.find((member) => compiler.isMethodDeclaration(member) && compiler.isIdentifier(member.name) && member.name.text === "execute");
|
|
483
|
+
let result = "import(\"@finesoft/front\").BasePage";
|
|
484
|
+
if (execute?.type) {
|
|
485
|
+
const type = checker.getTypeAtLocation(execute.type);
|
|
486
|
+
const awaited = checker.getAwaitedType(type);
|
|
487
|
+
result = awaited === type ? execute.type.getText() : `Awaited<${execute.type.getText()}>`;
|
|
488
|
+
if (compiler.isTypeReferenceNode(execute.type) && execute.type.typeName.getText() === "Promise" && execute.type.typeArguments?.length === 1) {
|
|
489
|
+
const value = execute.type.typeArguments[0];
|
|
490
|
+
if (checker.getTypeAtLocation(value) === awaited) result = value.getText();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const argumentsText = `<${parameterType}, ${result}>`;
|
|
494
|
+
if ((!base.typeArguments?.length || typeImport(base.typeArguments[0])) && tokens(source.text.slice(base.expression.end, base.end)) !== tokens(argumentsText)) edits.push({
|
|
495
|
+
start: base.expression.end,
|
|
496
|
+
end: base.end,
|
|
497
|
+
text: argumentsText
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
for (const [source, edits] of sourceEdits) {
|
|
501
|
+
const imports = typeImports(source);
|
|
502
|
+
const specifiers = [...aliases.get(source), ...imports.flatMap((statement) => statement.importClause.namedBindings.elements.filter((element) => !ownedImports.has(element)).map((element) => element.getText()))];
|
|
503
|
+
const desired = [...new Set(specifiers)].sort();
|
|
504
|
+
const declaration = `import type { ${desired.join(", ")} } from ${JSON.stringify(modulePaths.get(source))};`;
|
|
505
|
+
if (imports.length) {
|
|
506
|
+
const [first, ...rest] = imports;
|
|
507
|
+
if (first.importClause.namedBindings.elements.map((element) => tokens(element.getText())).sort().join("\n") !== desired.map(tokens).sort().join("\n")) edits.push({
|
|
508
|
+
start: first.getStart(),
|
|
509
|
+
end: first.end,
|
|
510
|
+
text: declaration
|
|
511
|
+
});
|
|
512
|
+
for (const statement of rest) edits.push({
|
|
513
|
+
start: statement.getStart(),
|
|
514
|
+
end: statement.end,
|
|
515
|
+
text: ""
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
const position = source.statements.filter(compiler.isImportDeclaration).at(-1)?.end ?? source.statements[0]?.getStart() ?? source.end;
|
|
519
|
+
edits.push({
|
|
520
|
+
start: position,
|
|
521
|
+
end: position,
|
|
522
|
+
text: `\n${declaration}\n`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
updates.set(source.fileName, applyEdits(source.text, edits));
|
|
526
|
+
}
|
|
527
|
+
if (managed.size || fs.existsSync(output)) {
|
|
528
|
+
if (fs.existsSync(output) && !fs.readFileSync(output, "utf8").startsWith(GENERATED)) throw Error(`Refusing to overwrite unowned controller declarations: ${output}`);
|
|
529
|
+
updates.set(output, `${GENERATED}export interface ControllerInputs {\n${[...managed.values()].sort((a, b) => a.key.localeCompare(b.key)).map((entry) => ` ${JSON.stringify(entry.key)}: ${[...new Set(entry.inputs)].join(" | ")};`).join("\n")}\n}\n${[...managed.values()].sort((a, b) => a.key.localeCompare(b.key)).map((entry) => `export type ${exportedName(entry)} = ControllerInputs[${JSON.stringify(entry.key)}] & { readonly context: ${serverController(entry.node) ? "import(\"@finesoft/front\").ServerControllerContext" : "import(\"@finesoft/front\").ControllerContext"} };\nexport type ${exportedName(entry, "Failure")} = ${exportedName(entry)} & { readonly error: Error };`).join("\n")}\n`);
|
|
530
|
+
}
|
|
531
|
+
if (updates.has(output)) {
|
|
532
|
+
const validation = buildProgram({
|
|
533
|
+
...settings,
|
|
534
|
+
skipLibCheck: false
|
|
535
|
+
}, updates);
|
|
536
|
+
const generated = validation.getSourceFile(output);
|
|
537
|
+
const errors = [...validation.getSyntacticDiagnostics(generated), ...validation.getSemanticDiagnostics(generated)];
|
|
538
|
+
if (errors.length) throw Error(`Cannot generate controller declarations:\n${compiler.formatDiagnosticsWithColorAndContext(errors, {
|
|
539
|
+
getCanonicalFileName: (file) => file,
|
|
540
|
+
getCurrentDirectory: () => root,
|
|
541
|
+
getNewLine: () => "\n"
|
|
542
|
+
})}`);
|
|
543
|
+
}
|
|
544
|
+
for (const [source] of sourceEdits) if (fs.readFileSync(source.fileName, "utf8") !== source.text) throw Error(`Controller changed during type generation: ${source.fileName}`);
|
|
545
|
+
for (const file of updates.keys()) assertEditablePath(root, file);
|
|
546
|
+
const changed = [];
|
|
547
|
+
for (const [file, content] of updates) {
|
|
548
|
+
if (fs.existsSync(file)) {
|
|
549
|
+
const existing = fs.readFileSync(file, "utf8");
|
|
550
|
+
if (file === output ? tokens(existing) === tokens(content) : existing === content) continue;
|
|
551
|
+
}
|
|
552
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
553
|
+
fs.writeFileSync(file, content);
|
|
554
|
+
inputStamps.set(file, stamp([file]));
|
|
555
|
+
changed.push(file);
|
|
556
|
+
}
|
|
557
|
+
const files = [.../* @__PURE__ */ new Set([
|
|
558
|
+
...configFiles,
|
|
559
|
+
...rootFiles,
|
|
560
|
+
output,
|
|
561
|
+
...readFiles
|
|
562
|
+
])].sort();
|
|
563
|
+
const result = {
|
|
564
|
+
controllers: managed.size,
|
|
565
|
+
changed
|
|
566
|
+
};
|
|
567
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const file of files) {
|
|
569
|
+
if (file === output) continue;
|
|
570
|
+
const unchanged = !force && inputStamps.get(file) === previous?.stamps.get(file);
|
|
571
|
+
hashes.set(file, unchanged && previous?.hashes.get(file) || hash(configContents.get(file) ?? updates.get(file) ?? readFile(file)));
|
|
572
|
+
}
|
|
573
|
+
const snapshot = {
|
|
574
|
+
compiler,
|
|
575
|
+
root,
|
|
576
|
+
files,
|
|
577
|
+
stamp: files.map((file) => inputStamps.get(file) ?? stamp([file])).join("\n"),
|
|
578
|
+
stamps: inputStamps,
|
|
579
|
+
hashes,
|
|
580
|
+
output,
|
|
581
|
+
content: changed.includes(output) ? updates.get(output) : saved,
|
|
582
|
+
replayable,
|
|
583
|
+
result
|
|
584
|
+
};
|
|
585
|
+
cached.set(cacheKey, snapshot);
|
|
586
|
+
retained = {
|
|
587
|
+
key: cacheKey,
|
|
588
|
+
compiler,
|
|
589
|
+
program: lastProgram
|
|
590
|
+
};
|
|
591
|
+
observe?.(snapshot);
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
/** Watch events are the invalidation source; one-shot calls still inspect the complete project. */
|
|
595
|
+
function createControllerTypeWatcher(options) {
|
|
596
|
+
let snapshot;
|
|
597
|
+
let onChange;
|
|
598
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
599
|
+
const directories = /* @__PURE__ */ new Map();
|
|
600
|
+
const versions = new LruMap(8);
|
|
601
|
+
const key = (hashes) => hash([...hashes.values()].join(""));
|
|
602
|
+
const subscribe = () => {
|
|
603
|
+
if (!onChange || !snapshot) return;
|
|
604
|
+
const { compiler, hashes } = snapshot;
|
|
605
|
+
if (!compiler.sys.watchFile || !compiler.sys.watchDirectory) throw Error("TypeScript file watching is unavailable");
|
|
606
|
+
for (const [file, subscription] of subscriptions) {
|
|
607
|
+
if (hashes.has(file)) continue;
|
|
608
|
+
subscription.close();
|
|
609
|
+
subscriptions.delete(file);
|
|
610
|
+
}
|
|
611
|
+
for (const file of hashes.keys()) if (!subscriptions.has(file)) subscriptions.set(file, compiler.sys.watchFile(file, () => onChange?.(file), 250, {
|
|
612
|
+
watchFile: inside(snapshot.root, file) && !slash$1(file).includes("/node_modules/") ? compiler.WatchFileKind.UseFsEvents : compiler.WatchFileKind.UseFsEventsOnParentDirectory,
|
|
613
|
+
fallbackPolling: compiler.PollingWatchKind.PriorityInterval,
|
|
614
|
+
synchronousWatchDirectory: true
|
|
615
|
+
}));
|
|
616
|
+
const desired = /* @__PURE__ */ new Set();
|
|
617
|
+
for (const file of hashes.keys()) {
|
|
618
|
+
const directory = path.dirname(file);
|
|
619
|
+
if (inside(snapshot.root, directory) && !slash$1(file).includes("/node_modules/")) continue;
|
|
620
|
+
desired.add(directory);
|
|
621
|
+
desired.add(path.dirname(directory));
|
|
622
|
+
}
|
|
623
|
+
for (const [directory, subscription] of directories) {
|
|
624
|
+
if (desired.has(directory)) continue;
|
|
625
|
+
subscription.close();
|
|
626
|
+
directories.delete(directory);
|
|
627
|
+
}
|
|
628
|
+
for (const directory of desired) if (!directories.has(directory)) directories.set(directory, compiler.sys.watchDirectory(directory, (file) => {
|
|
629
|
+
if (!snapshot?.hashes.has(file)) onChange?.(file);
|
|
630
|
+
}, false, {
|
|
631
|
+
watchDirectory: compiler.WatchDirectoryKind.UseFsEvents,
|
|
632
|
+
synchronousWatchDirectory: true
|
|
633
|
+
}));
|
|
634
|
+
};
|
|
635
|
+
const generate = (force) => {
|
|
636
|
+
return {
|
|
637
|
+
...generateTypes(options, (next) => {
|
|
638
|
+
if (snapshot?.files.join("\n") !== next.files.join("\n")) versions.clear();
|
|
639
|
+
snapshot = next;
|
|
640
|
+
subscribe();
|
|
641
|
+
if (!next.replayable) {
|
|
642
|
+
versions.clear();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const fingerprint = key(next.hashes);
|
|
646
|
+
versions.set(fingerprint, {
|
|
647
|
+
content: next.content,
|
|
648
|
+
controllers: next.result.controllers
|
|
649
|
+
});
|
|
650
|
+
}, force),
|
|
651
|
+
cache: "generated"
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
generate(false);
|
|
655
|
+
return {
|
|
656
|
+
watch(listener) {
|
|
657
|
+
onChange = listener;
|
|
658
|
+
subscribe();
|
|
659
|
+
if (snapshot) {
|
|
660
|
+
for (const file of snapshot.hashes.keys()) if (stamp([file]) !== snapshot.stamps.get(file)) onChange(file);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
update(files, cachedOnly = false) {
|
|
664
|
+
if (!snapshot) return cachedOnly ? void 0 : generate(true);
|
|
665
|
+
if (snapshot.content && !fs.existsSync(snapshot.output)) return cachedOnly ? void 0 : generate(true);
|
|
666
|
+
const hashes = new Map(snapshot.hashes);
|
|
667
|
+
let changed = false;
|
|
668
|
+
for (const file of files) {
|
|
669
|
+
if (!hashes.has(file) || file.endsWith(".json") || !fs.existsSync(file)) {
|
|
670
|
+
versions.clear();
|
|
671
|
+
return cachedOnly ? void 0 : generate(true);
|
|
672
|
+
}
|
|
673
|
+
const next = hash(fs.readFileSync(file, "utf8"));
|
|
674
|
+
changed ||= next !== hashes.get(file);
|
|
675
|
+
hashes.set(file, next);
|
|
676
|
+
}
|
|
677
|
+
if (!changed) return {
|
|
678
|
+
...snapshot.result,
|
|
679
|
+
changed: [],
|
|
680
|
+
cache: "unchanged"
|
|
681
|
+
};
|
|
682
|
+
const fingerprint = key(hashes);
|
|
683
|
+
const known = versions.get(fingerprint);
|
|
684
|
+
if (!known) return cachedOnly ? void 0 : generate(true);
|
|
685
|
+
const exists = fs.existsSync(snapshot.output);
|
|
686
|
+
const existing = exists ? fs.readFileSync(snapshot.output, "utf8") : "";
|
|
687
|
+
if (exists && !existing.startsWith(GENERATED)) throw Error(`Refusing to overwrite unowned controller declarations: ${snapshot.output}`);
|
|
688
|
+
const written = [];
|
|
689
|
+
if (existing !== known.content) {
|
|
690
|
+
assertEditablePath(snapshot.root, snapshot.output);
|
|
691
|
+
fs.mkdirSync(path.dirname(snapshot.output), { recursive: true });
|
|
692
|
+
fs.writeFileSync(snapshot.output, known.content);
|
|
693
|
+
written.push(snapshot.output);
|
|
694
|
+
}
|
|
695
|
+
const result = {
|
|
696
|
+
controllers: known.controllers,
|
|
697
|
+
changed: written
|
|
698
|
+
};
|
|
699
|
+
snapshot = {
|
|
700
|
+
...snapshot,
|
|
701
|
+
hashes,
|
|
702
|
+
content: known.content,
|
|
703
|
+
result
|
|
704
|
+
};
|
|
705
|
+
return {
|
|
706
|
+
...result,
|
|
707
|
+
cache: "reused"
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
close() {
|
|
711
|
+
onChange = void 0;
|
|
712
|
+
for (const subscription of subscriptions.values()) subscription.close();
|
|
713
|
+
for (const subscription of directories.values()) subscription.close();
|
|
714
|
+
subscriptions.clear();
|
|
715
|
+
directories.clear();
|
|
716
|
+
snapshot = void 0;
|
|
717
|
+
versions.clear();
|
|
718
|
+
releaseControllerTypes(options.root ?? process.cwd());
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/** Release development-only compiler state when its host closes. */
|
|
723
|
+
function releaseControllerTypes(root) {
|
|
724
|
+
const prefix = `${fs.existsSync(root) ? fs.realpathSync(root) : path.resolve(root)}#`;
|
|
725
|
+
for (const key of cached.keys()) if (key.startsWith(prefix)) cached.delete(key);
|
|
726
|
+
if (retained?.key.startsWith(prefix)) retained = void 0;
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region ../server/src/public-types.ts
|
|
730
|
+
const slash = (value) => value.replaceAll("\\", "/");
|
|
731
|
+
const relative = (from, to) => {
|
|
732
|
+
const name = slash(path.relative(from, to));
|
|
733
|
+
return name.startsWith("./") || name.startsWith("../") ? name : `./${name}`;
|
|
734
|
+
};
|
|
735
|
+
/** Maintain an exact root alias with only the project's installed platform/native contracts. */
|
|
736
|
+
function generateFrontTypes(options = {}) {
|
|
737
|
+
const requestedRoot = path.resolve(options.root ?? process.cwd());
|
|
738
|
+
const requestedConfig = path.resolve(requestedRoot, options.tsconfig ?? "tsconfig.json");
|
|
739
|
+
if (!fs.existsSync(requestedConfig)) return [];
|
|
740
|
+
const root = fs.realpathSync(requestedRoot);
|
|
741
|
+
const configFile = fs.realpathSync(requestedConfig);
|
|
742
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
743
|
+
let front;
|
|
744
|
+
try {
|
|
745
|
+
front = require.resolve("@finesoft/front");
|
|
746
|
+
} catch {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
const c = require("typescript");
|
|
750
|
+
const input = fs.readFileSync(configFile, "utf8");
|
|
751
|
+
const parsed = c.parseConfigFileTextToJson(configFile, input);
|
|
752
|
+
if (parsed.error) throw Error(c.flattenDiagnosticMessageText(parsed.error.messageText, "\n"));
|
|
753
|
+
const output = path.join(root, ".finesoft/front.d.ts");
|
|
754
|
+
const resolved = c.parseJsonConfigFileContent(parsed.config, c.sys, path.dirname(configFile), void 0, configFile);
|
|
755
|
+
const configBase = resolved.options.baseUrl ?? path.dirname(configFile);
|
|
756
|
+
const alias = relative(configBase, output);
|
|
757
|
+
const existing = resolved.options.paths?.["@finesoft/front"];
|
|
758
|
+
if (existing && !(existing.length === 1 && path.resolve(configBase, existing[0]) === output)) return [];
|
|
759
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
760
|
+
if (pkg.name === "@finesoft/front") return [];
|
|
761
|
+
const deps = {
|
|
762
|
+
...pkg.dependencies,
|
|
763
|
+
...pkg.devDependencies,
|
|
764
|
+
...pkg.peerDependencies
|
|
765
|
+
};
|
|
766
|
+
const installed = (name) => {
|
|
767
|
+
if (!deps[name]) return false;
|
|
768
|
+
try {
|
|
769
|
+
require.resolve(`${name}/package.json`);
|
|
770
|
+
return true;
|
|
771
|
+
} catch {
|
|
772
|
+
try {
|
|
773
|
+
require.resolve(name);
|
|
774
|
+
return true;
|
|
775
|
+
} catch {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const entry = (name) => JSON.stringify(relative(path.dirname(output), path.join(path.dirname(front), `${name}.mjs`)));
|
|
781
|
+
const renderers = [
|
|
782
|
+
"react",
|
|
783
|
+
"vue",
|
|
784
|
+
"svelte"
|
|
785
|
+
].filter(installed);
|
|
786
|
+
const lines = [
|
|
787
|
+
"// Generated by @finesoft/front. Re-run finesoft-types after changing dependencies.",
|
|
788
|
+
`export * from ${entry("portable")};`,
|
|
789
|
+
`export { generateFrontTypes, generateControllerTypes } from ${entry("typegen")};`,
|
|
790
|
+
`export type { FrontTypeOptions, ControllerTypeOptions, ControllerTypeResult } from ${entry("typegen")};`
|
|
791
|
+
];
|
|
792
|
+
if (installed("@types/node") || installed("@hono/node-server")) lines.push(`export * from ${entry("node")};`);
|
|
793
|
+
if (installed("vite") || installed("vite-plus")) lines.push(`export * from ${entry("vite")};`);
|
|
794
|
+
if (renderers.length) {
|
|
795
|
+
lines.push(`import type { NativeAPI } from ${entry("native-contract")};`);
|
|
796
|
+
for (const name of renderers) lines.push(`import type * as ${name} from ${entry(name)};`);
|
|
797
|
+
lines.push(`export interface NativeBindings { ${renderers.map((name) => `${name}: typeof ${name};`).join(" ")} }`, "export type Renderer = keyof NativeBindings;", `interface NativeViewTypes { ${renderers.map((name) => `${name}: ${name}.NativeView;`).join(" ")} }`, "export type NativeView<R extends Renderer> = NativeViewTypes[R];", "export type NativeViews<R extends Renderer> = Readonly<Record<string, NativeView<R>>>;", "export declare const Outlet: NativeAPI<NativeBindings>[\"Outlet\"];", "export declare const useSnapshot: NativeAPI<NativeBindings>[\"useSnapshot\"];");
|
|
798
|
+
}
|
|
799
|
+
const changed = [];
|
|
800
|
+
const write = (file, content) => {
|
|
801
|
+
assertEditablePath(root, file);
|
|
802
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content) return;
|
|
803
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
804
|
+
fs.writeFileSync(file, content);
|
|
805
|
+
changed.push(file);
|
|
806
|
+
};
|
|
807
|
+
write(output, lines.join("\n") + "\n");
|
|
808
|
+
if (!existing) {
|
|
809
|
+
const source = c.parseJsonText(configFile, input);
|
|
810
|
+
const object = source.statements[0]?.expression;
|
|
811
|
+
if (!object || !c.isObjectLiteralExpression(object)) throw Error(`Invalid tsconfig: ${configFile}`);
|
|
812
|
+
const property = (value, name) => value.properties.find((item) => c.isPropertyAssignment(item) && (c.isStringLiteral(item.name) || c.isIdentifier(item.name)) && item.name.text === name);
|
|
813
|
+
const compilerOptions = property(object, "compilerOptions");
|
|
814
|
+
const compilerObject = compilerOptions && c.isObjectLiteralExpression(compilerOptions.initializer) ? compilerOptions.initializer : void 0;
|
|
815
|
+
const paths = compilerObject && property(compilerObject, "paths");
|
|
816
|
+
const pathsObject = paths && c.isObjectLiteralExpression(paths.initializer) ? paths.initializer : void 0;
|
|
817
|
+
const target = pathsObject ?? compilerObject ?? object;
|
|
818
|
+
const inherited = !pathsObject && resolved.options.paths ? resolved.options.paths : {};
|
|
819
|
+
const pathsBase = resolved.options.baseUrl ?? (typeof resolved.options.pathsBasePath === "string" ? resolved.options.pathsBasePath : path.dirname(configFile));
|
|
820
|
+
const mapping = Object.entries({
|
|
821
|
+
...inherited,
|
|
822
|
+
"@finesoft/front": [alias]
|
|
823
|
+
}).map(([name, values]) => `${JSON.stringify(name)}: ${JSON.stringify(name === "@finesoft/front" ? values : values.map((value) => relative(configBase, path.resolve(pathsBase, value))))}`).join(",\n");
|
|
824
|
+
const addition = pathsObject ? mapping : compilerObject ? `"paths": { ${mapping} }` : `"compilerOptions": { "paths": { ${mapping} } }`;
|
|
825
|
+
const last = target.properties.at(-1);
|
|
826
|
+
const position = last?.end ?? target.getStart(source) + 1;
|
|
827
|
+
const insertion = last ? `,\n${addition}` : `\n${addition}\n`;
|
|
828
|
+
write(configFile, input.slice(0, position) + insertion + input.slice(position));
|
|
829
|
+
}
|
|
830
|
+
return changed;
|
|
831
|
+
}
|
|
832
|
+
//#endregion
|
|
833
|
+
export { createControllerTypeWatcher as n, generateControllerTypes as r, generateFrontTypes as t };
|
|
834
|
+
|
|
835
|
+
//# sourceMappingURL=public-types-BcJM-AYc.mjs.map
|