@kosmojs/lib 0.4.3 → 0.5.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/package.json +6 -6
- package/pkg/generators.d.ts +2 -2
- package/pkg/generic.d.ts +1 -0
- package/pkg/index.js +1506 -18
- package/pkg/index.js.map +1 -1
- package/pkg/render.d.ts +3 -1
- package/pkg/routes/base.d.ts +18 -0
- package/pkg/routes/paths.d.ts +1 -0
- package/pkg/vite.d.ts +20 -1
package/pkg/index.js
CHANGED
|
@@ -1,21 +1,1509 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import { styleText } from "node:util";
|
|
3
|
+
import crc from "crc/crc32";
|
|
4
|
+
import flattener from "tfusion";
|
|
5
|
+
import { ModuleResolutionKind, Project, SyntaxKind } from "ts-morph";
|
|
6
|
+
import { RequestValidationTargets, defaults } from "@kosmojs/core";
|
|
7
|
+
import { HTTPMethods } from "@kosmojs/core/api";
|
|
8
|
+
import { mergeConfig } from "vite";
|
|
9
|
+
import { access, constants, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, parse, posix, resolve } from "node:path";
|
|
11
|
+
import handlebars from "handlebars";
|
|
12
|
+
import picomatch from "picomatch";
|
|
13
|
+
import { glob } from "tinyglobby";
|
|
14
|
+
import { compile, parse as parse$1 } from "path-to-regexp";
|
|
15
|
+
import { lookup } from "mrmime";
|
|
16
|
+
import { Spinner } from "@topcli/spinner";
|
|
17
|
+
//#region src/generic.ts
|
|
18
|
+
var mergeConfigs = (...configs) => {
|
|
19
|
+
return [{ configFile: false }, ...configs].reduce((config, prev) => mergeConfig(config || {}, prev || {}), {});
|
|
20
|
+
};
|
|
21
|
+
var escapeTemplateLiterals = (origin) => {
|
|
22
|
+
return [[/(?<!\\)`/g, "\\`"], [/(?<!\\)\$\{/g, "\\${"]].reduce((text, [a, b]) => text.replace(a, b), origin);
|
|
23
|
+
};
|
|
24
|
+
var containsPathTraversalPatterns = (str) => {
|
|
25
|
+
return [/\.\.\//, /\/\.\//].some((e) => e.test(str));
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/paths.ts
|
|
29
|
+
var pathResolver = (sourceFolder) => {
|
|
30
|
+
const createPath = (...a) => {
|
|
31
|
+
return sourceFolder.root ? resolve(sourceFolder.root, join(...a)) : join(...a);
|
|
32
|
+
};
|
|
33
|
+
const createImport = {
|
|
34
|
+
src(a, { origin }) {
|
|
35
|
+
return origin === "src" ? join(defaults.srcPrefix, ...a) : join(defaults.appPrefix, defaults.srcDir, sourceFolder.name, ...a);
|
|
36
|
+
},
|
|
37
|
+
api(a, o) {
|
|
38
|
+
return this.src([defaults.apiDir, ...a], o);
|
|
39
|
+
},
|
|
40
|
+
entry(a, o) {
|
|
41
|
+
return this.src([defaults.entryDir, ...a], o);
|
|
42
|
+
},
|
|
43
|
+
pages(a, o) {
|
|
44
|
+
return this.src([defaults.pagesDir, ...a], o);
|
|
45
|
+
},
|
|
46
|
+
lib(a, { origin }) {
|
|
47
|
+
return origin === "src" ? join(defaults.libPrefix, ...a) : join(defaults.appPrefix, defaults.libDir, sourceFolder.name, ...a);
|
|
48
|
+
},
|
|
49
|
+
libCore(a, o) {
|
|
50
|
+
return this.lib(["core", ...a], o);
|
|
51
|
+
},
|
|
52
|
+
libApi(a, o) {
|
|
53
|
+
return this.lib([defaults.apiDir, ...a], o);
|
|
54
|
+
},
|
|
55
|
+
libEntry(a, o) {
|
|
56
|
+
return this.lib([defaults.entryDir, ...a], o);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
createPath: {
|
|
61
|
+
src(...a) {
|
|
62
|
+
return createPath(defaults.srcDir, sourceFolder.name, ...a);
|
|
63
|
+
},
|
|
64
|
+
api(...a) {
|
|
65
|
+
return this.src(defaults.apiDir, ...a);
|
|
66
|
+
},
|
|
67
|
+
pages(...a) {
|
|
68
|
+
return this.src(defaults.pagesDir, ...a);
|
|
69
|
+
},
|
|
70
|
+
entry(...a) {
|
|
71
|
+
return this.src(defaults.entryDir, ...a);
|
|
72
|
+
},
|
|
73
|
+
lib(...a) {
|
|
74
|
+
return createPath(defaults.libDir, sourceFolder.name, ...a);
|
|
75
|
+
},
|
|
76
|
+
libCore(...a) {
|
|
77
|
+
return this.lib("core", ...a);
|
|
78
|
+
},
|
|
79
|
+
libApi(...a) {
|
|
80
|
+
return this.lib(defaults.apiDir, ...a);
|
|
81
|
+
},
|
|
82
|
+
libEntry(...a) {
|
|
83
|
+
return this.lib(defaults.entryDir, ...a);
|
|
84
|
+
},
|
|
85
|
+
libPages(...a) {
|
|
86
|
+
return this.lib(defaults.pagesDir, ...a);
|
|
87
|
+
},
|
|
88
|
+
distDir(...a) {
|
|
89
|
+
return createPath(sourceFolder.distDir, sourceFolder.name, ...a);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
createImport,
|
|
93
|
+
createImportHelpers(o) {
|
|
94
|
+
if (!["src", "lib"].includes(o?.origin)) throw new Error(`createImportHelpers: required exactly one argument of shape { origin: "src|lib" }`);
|
|
95
|
+
return { createImport(key, ...a) {
|
|
96
|
+
return createImport[key](a.slice(0, -1), o);
|
|
97
|
+
} };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
var pathExists = async (path) => {
|
|
102
|
+
return access(path, constants.F_OK).then(() => true, () => false);
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/render.ts
|
|
106
|
+
var render = (template, context, options) => {
|
|
107
|
+
const { noEscape = true, renderer = handlebars } = { ...options };
|
|
108
|
+
return renderer.compile(template, { noEscape })(context);
|
|
109
|
+
};
|
|
110
|
+
var renderToFile = async (file, template, context, options) => {
|
|
111
|
+
const content = options?.render ? options.render(template, context, options) : render(template, context, options);
|
|
112
|
+
/**
|
|
113
|
+
* Two fs calls (exists + read) are worth it to avoid touching the file
|
|
114
|
+
* and triggering watchers unnecessarily.
|
|
115
|
+
* */
|
|
116
|
+
if (await pathExists(file)) {
|
|
117
|
+
const { overwrite = true } = { ...options };
|
|
118
|
+
if (overwrite === false) return;
|
|
119
|
+
const fileContent = await readFile(file, "utf8");
|
|
120
|
+
if (typeof overwrite === "function" && !overwrite(fileContent)) return;
|
|
121
|
+
if (crc(content) === crc(fileContent)) return;
|
|
122
|
+
}
|
|
123
|
+
await mkdir(dirname(file), { recursive: true });
|
|
124
|
+
await writeFile(file, content, "utf8");
|
|
125
|
+
};
|
|
126
|
+
var renderFactory = (options) => {
|
|
127
|
+
const createRenderer = (selfOoptions) => {
|
|
128
|
+
const renderer = handlebars.create();
|
|
129
|
+
renderer.registerPartial({
|
|
130
|
+
...options?.partials,
|
|
131
|
+
...selfOoptions?.partials
|
|
132
|
+
});
|
|
133
|
+
renderer.registerHelper({
|
|
134
|
+
...options?.helpers,
|
|
135
|
+
...selfOoptions?.helpers
|
|
136
|
+
});
|
|
137
|
+
return renderer;
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
render(template, context, selfOoptions) {
|
|
141
|
+
return render(template, context, {
|
|
142
|
+
...options,
|
|
143
|
+
...selfOoptions,
|
|
144
|
+
renderer: createRenderer(selfOoptions)
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
async renderToFile(file, template, context, selfOoptions) {
|
|
148
|
+
return renderToFile(options?.outdir ? join(options.outdir, file) : file, template, context, {
|
|
149
|
+
...options,
|
|
150
|
+
...selfOoptions,
|
|
151
|
+
renderer: createRenderer(selfOoptions)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/templates/resolved-types.hbs?raw
|
|
158
|
+
var resolved_types_default = "{{#each resolvedTypes}}\nexport type {{name}} = {{text}};\n{{/each}}\n";
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/templates/types.hbs?raw
|
|
161
|
+
var types_default = "{{#each typeDeclarations}}{{text}}\n{{/each}}\n\nexport type {{params.id}} = {\n {{#each paramsSchema}}\n \"{{name}}\"{{#unless isRequired}}?{{/unless}}: {{#if refinement}}\n {{refinement.text}},\n {{else}}\n {{#if isSplat}}Array<string>{{else}}string{{/if}},\n {{/if}}\n {{/each}}\n};\n\n{{#each validationTypes}}export type {{id}} = {{text}};\n{{/each}}\n";
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/ast.ts
|
|
164
|
+
var astFactory = () => {
|
|
165
|
+
const createProject = (opts) => new Project(opts);
|
|
166
|
+
const resolveRouteSignature = async (route, opts) => {
|
|
167
|
+
const { sourceFile = createProject().addSourceFileAtPath(route.fileFullpath) } = { ...opts };
|
|
168
|
+
const [typeDeclarations, referencedFiles] = extractTypeDeclarations(sourceFile, opts);
|
|
169
|
+
const defaultExport = extractDefaultExport(sourceFile);
|
|
170
|
+
const paramsRefinements = defaultExport ? extractParamsRefinements(defaultExport) : void 0;
|
|
171
|
+
const methods = defaultExport ? extractRouteMethods(route, defaultExport) : [];
|
|
172
|
+
return {
|
|
173
|
+
typeDeclarations,
|
|
174
|
+
paramsRefinements,
|
|
175
|
+
methods: methods.map((e) => e.method),
|
|
176
|
+
validationDefinitions: methods.flatMap((e) => e.validationDefinitions),
|
|
177
|
+
referencedFiles
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const extractDefaultExport = (sourceFile) => {
|
|
181
|
+
const [defaultExport] = sourceFile.getExportAssignments().flatMap((exportAssignment) => {
|
|
182
|
+
if (exportAssignment.isExportEquals()) return [];
|
|
183
|
+
const callExpression = exportAssignment.getExpression();
|
|
184
|
+
return callExpression.isKind(SyntaxKind.CallExpression) ? [callExpression] : [];
|
|
185
|
+
});
|
|
186
|
+
return defaultExport;
|
|
187
|
+
};
|
|
188
|
+
const extractParamsRefinements = (callExpression) => {
|
|
189
|
+
const [_routeName, paramsGeneric] = extractGenerics(callExpression);
|
|
190
|
+
if (!paramsGeneric?.isKind(SyntaxKind.TupleType)) return;
|
|
191
|
+
return paramsGeneric.getElements().map((node, index) => {
|
|
192
|
+
return {
|
|
193
|
+
index,
|
|
194
|
+
text: node.getText()
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const extractRouteMethods = (route, callExpression) => {
|
|
199
|
+
const funcDeclaration = callExpression.getFirstChildByKind(SyntaxKind.ArrowFunction) || callExpression.getFirstChildByKind(SyntaxKind.FunctionExpression);
|
|
200
|
+
if (!funcDeclaration) return [];
|
|
201
|
+
const arrayLiteralExpression = funcDeclaration.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
|
|
202
|
+
if (!arrayLiteralExpression) return [];
|
|
203
|
+
const callExpressions = [];
|
|
204
|
+
for (const e of arrayLiteralExpression.getChildrenOfKind(SyntaxKind.CallExpression)) {
|
|
205
|
+
const name = e.getExpression().getText();
|
|
206
|
+
if (HTTPMethods[name]) callExpressions.push([e, name]);
|
|
207
|
+
}
|
|
208
|
+
const methods = [];
|
|
209
|
+
for (const [callExpression, method] of callExpressions) {
|
|
210
|
+
const [vDefs, vOpts] = extractGenerics(callExpression);
|
|
211
|
+
methods.push({
|
|
212
|
+
method,
|
|
213
|
+
validationDefinitions: extractValidationDefinitions(route, method, vDefs, vOpts)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return methods;
|
|
217
|
+
};
|
|
218
|
+
/** Parse a boolean literal type node */
|
|
219
|
+
const parseRuntimeValidation = (typeNode) => {
|
|
220
|
+
if (typeNode.isKind(SyntaxKind.LiteralType)) {
|
|
221
|
+
const literal = typeNode.getFirstChild();
|
|
222
|
+
if (literal?.isKind(SyntaxKind.TrueKeyword)) return true;
|
|
223
|
+
else if (literal?.isKind(SyntaxKind.FalseKeyword)) return false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const extractResponseVariant = (typeNode) => {
|
|
227
|
+
if (!typeNode.isKind(SyntaxKind.TupleType)) return;
|
|
228
|
+
let status = 200;
|
|
229
|
+
let contentType;
|
|
230
|
+
let body;
|
|
231
|
+
const [statusNode, contentTypeNode, bodyNode] = typeNode.getElements();
|
|
232
|
+
if (statusNode?.isKind(SyntaxKind.LiteralType)) {
|
|
233
|
+
const literal = statusNode.getFirstChildByKind(SyntaxKind.NumericLiteral);
|
|
234
|
+
if (literal) status = Number(literal.getText());
|
|
235
|
+
}
|
|
236
|
+
if (contentTypeNode) contentType = extractStringLiteral(contentTypeNode);
|
|
237
|
+
if (bodyNode) {
|
|
238
|
+
body = bodyNode.getText();
|
|
239
|
+
if (["object"].includes(body)) body = "{}";
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
status,
|
|
243
|
+
contentType,
|
|
244
|
+
body
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
/** Parse opts TypeLiteral into a map keyed by target */
|
|
248
|
+
const parseValidationOptions = (typeNode) => {
|
|
249
|
+
const opts = {};
|
|
250
|
+
if (!typeNode?.isKind(SyntaxKind.TypeLiteral)) return opts;
|
|
251
|
+
for (const prop of typeNode.getMembers()) {
|
|
252
|
+
if (!prop.isKind(SyntaxKind.PropertySignature)) continue;
|
|
253
|
+
const target = prop.getName();
|
|
254
|
+
const typeNode = prop.getTypeNodeOrThrow();
|
|
255
|
+
if (!typeNode.isKind(SyntaxKind.TypeLiteral)) continue;
|
|
256
|
+
let contentType;
|
|
257
|
+
let runtimeValidation;
|
|
258
|
+
const customErrors = {};
|
|
259
|
+
for (const member of typeNode.getMembers()) {
|
|
260
|
+
if (!member.isKind(SyntaxKind.PropertySignature)) continue;
|
|
261
|
+
const name = propertyName(member);
|
|
262
|
+
const node = member.getTypeNodeOrThrow();
|
|
263
|
+
if (name === "contentType") contentType = extractStringLiteral(node);
|
|
264
|
+
else if (name === "runtimeValidation") runtimeValidation = parseRuntimeValidation(node);
|
|
265
|
+
else if (name.startsWith("error")) {
|
|
266
|
+
const literal = extractStringLiteral(node);
|
|
267
|
+
if (literal) customErrors[name] = literal;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
opts[target] = {
|
|
271
|
+
contentType,
|
|
272
|
+
runtimeValidation,
|
|
273
|
+
customErrors
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return opts;
|
|
277
|
+
};
|
|
278
|
+
const extractStringLiteral = (typeNode) => {
|
|
279
|
+
const literal = typeNode.isKind(SyntaxKind.LiteralType) ? typeNode.getFirstChildByKind(SyntaxKind.StringLiteral) : void 0;
|
|
280
|
+
return literal ? literal.getLiteralText() : void 0;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Extract validation definitions from route handler generics.
|
|
284
|
+
* Merges defs (schemas) and opts (validation options) into a flat array.
|
|
285
|
+
* */
|
|
286
|
+
const extractValidationDefinitions = (route, method, defsNode, optsNode) => {
|
|
287
|
+
const definitions = [];
|
|
288
|
+
if (!defsNode?.isKind(SyntaxKind.TypeLiteral)) return definitions;
|
|
289
|
+
const optsMap = parseValidationOptions(optsNode);
|
|
290
|
+
const createId = (target, hash) => {
|
|
291
|
+
return [
|
|
292
|
+
target.replace(/^./, (c) => c.toUpperCase()),
|
|
293
|
+
"T",
|
|
294
|
+
method,
|
|
295
|
+
crc(route.id + hash)
|
|
296
|
+
].join("");
|
|
297
|
+
};
|
|
298
|
+
for (const prop of defsNode.getMembers()) {
|
|
299
|
+
if (!prop.isKind(SyntaxKind.PropertySignature)) continue;
|
|
300
|
+
const target = prop.getName();
|
|
301
|
+
const typeNode = prop.getTypeNodeOrThrow();
|
|
302
|
+
if (target === "response") {
|
|
303
|
+
const variants = typeNode.isKind(SyntaxKind.UnionType) ? typeNode.getChildrenOfKind(SyntaxKind.TupleType) : [typeNode];
|
|
304
|
+
definitions.push({
|
|
305
|
+
...optsMap[target],
|
|
306
|
+
method,
|
|
307
|
+
target,
|
|
308
|
+
variants: variants.flatMap((e, i) => {
|
|
309
|
+
const { status, contentType, body } = { ...extractResponseVariant(e) };
|
|
310
|
+
if (!status) return [];
|
|
311
|
+
if (contentType && typeof contentType !== "string") {
|
|
312
|
+
console.warn(styleText(["bold", "red"], `✗ The second element of a response variant should specify the Response Content Type`));
|
|
313
|
+
console.warn(styleText(["blue"], ` Example: [200, "json", Schema]`));
|
|
314
|
+
console.warn(` Route: ${route.name}; Method: ${method}; Response Variant: #${i}`);
|
|
315
|
+
console.warn();
|
|
316
|
+
}
|
|
317
|
+
return [{
|
|
318
|
+
id: createId(target, JSON.stringify([
|
|
319
|
+
status,
|
|
320
|
+
contentType,
|
|
321
|
+
body
|
|
322
|
+
])),
|
|
323
|
+
status,
|
|
324
|
+
contentType,
|
|
325
|
+
body
|
|
326
|
+
}];
|
|
327
|
+
})
|
|
328
|
+
});
|
|
329
|
+
} else if (Object.keys(RequestValidationTargets).includes(target)) definitions.push({
|
|
330
|
+
...optsMap[target],
|
|
331
|
+
method,
|
|
332
|
+
target,
|
|
333
|
+
schema: {
|
|
334
|
+
id: createId(target),
|
|
335
|
+
text: typeNode.getText()
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return definitions;
|
|
340
|
+
};
|
|
341
|
+
const extractTypeDeclarations = (sourceFile, opts) => {
|
|
342
|
+
const declarations = [];
|
|
343
|
+
const referencedFiles = opts?.withReferencedFiles ? [] : void 0;
|
|
344
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
345
|
+
const modulePath = declaration.getModuleSpecifierValue();
|
|
346
|
+
const path = /^\.\.?\/?/.test(modulePath) ? opts?.relpathResolver ? opts.relpathResolver(modulePath) : modulePath : modulePath;
|
|
347
|
+
const typeOnlyDeclaration = declaration.isTypeOnly();
|
|
348
|
+
const defaultImport = typeOnlyDeclaration ? declaration.getDefaultImport() : void 0;
|
|
349
|
+
if (defaultImport) {
|
|
350
|
+
const name = defaultImport.getText();
|
|
351
|
+
const text = `import type ${name} from "${path}";`;
|
|
352
|
+
declarations.push({
|
|
353
|
+
importDeclaration: {
|
|
354
|
+
name,
|
|
355
|
+
path
|
|
356
|
+
},
|
|
357
|
+
text
|
|
358
|
+
});
|
|
359
|
+
if (referencedFiles) referencedFiles.push(...getReferencedFiles(defaultImport));
|
|
360
|
+
}
|
|
361
|
+
const namespaceImport = typeOnlyDeclaration ? declaration.getNamespaceImport() : void 0;
|
|
362
|
+
if (namespaceImport) {
|
|
363
|
+
const name = namespaceImport.getText();
|
|
364
|
+
const text = `import type * as ${name} from "${path}";`;
|
|
365
|
+
declarations.push({
|
|
366
|
+
importDeclaration: {
|
|
367
|
+
name,
|
|
368
|
+
path
|
|
369
|
+
},
|
|
370
|
+
text
|
|
371
|
+
});
|
|
372
|
+
if (referencedFiles) referencedFiles.push(...getReferencedFiles(namespaceImport));
|
|
373
|
+
}
|
|
374
|
+
for (const namedImport of declaration.getNamedImports()) if (namedImport.isTypeOnly() || typeOnlyDeclaration) {
|
|
375
|
+
const nameNode = namedImport.getNameNode();
|
|
376
|
+
const name = nameNode.getText();
|
|
377
|
+
const alias = namedImport.getAliasNode()?.getText();
|
|
378
|
+
const nameText = alias ? `${name} as ${alias}` : name;
|
|
379
|
+
declarations.push({
|
|
380
|
+
importDeclaration: {
|
|
381
|
+
name,
|
|
382
|
+
alias,
|
|
383
|
+
path
|
|
384
|
+
},
|
|
385
|
+
text: `import type { ${nameText} } from "${path}";`
|
|
386
|
+
});
|
|
387
|
+
if (referencedFiles) {
|
|
388
|
+
if (nameNode.isKind(SyntaxKind.Identifier)) referencedFiles.push(...getReferencedFiles(nameNode));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const declaration of sourceFile.getTypeAliases()) {
|
|
393
|
+
const name = declaration.getName();
|
|
394
|
+
const text = declaration.getFullText().trim();
|
|
395
|
+
declarations.push({
|
|
396
|
+
typeAliasDeclaration: { name },
|
|
397
|
+
text
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
for (const declaration of sourceFile.getInterfaces()) {
|
|
401
|
+
const name = declaration.getName();
|
|
402
|
+
const text = declaration.getFullText().trim();
|
|
403
|
+
declarations.push({
|
|
404
|
+
interfaceDeclaration: { name },
|
|
405
|
+
text
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (const declaration of sourceFile.getEnums()) {
|
|
409
|
+
const name = declaration.getName();
|
|
410
|
+
const text = declaration.getFullText().trim();
|
|
411
|
+
declarations.push({
|
|
412
|
+
enumDeclaration: { name },
|
|
413
|
+
text
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
417
|
+
const typeOnlyDeclaration = declaration.isTypeOnly();
|
|
418
|
+
const modulePath = declaration.getModuleSpecifierValue();
|
|
419
|
+
const path = modulePath ? /^\.\.?\/?/.test(modulePath) ? opts?.relpathResolver ? opts.relpathResolver(modulePath) : modulePath : modulePath : void 0;
|
|
420
|
+
for (const namedExport of declaration.getNamedExports()) if (namedExport.isTypeOnly() || typeOnlyDeclaration) {
|
|
421
|
+
const nameNode = namedExport.getNameNode();
|
|
422
|
+
const name = nameNode.getText();
|
|
423
|
+
const alias = namedExport.getAliasNode()?.getText();
|
|
424
|
+
const nameText = alias ? `${name} as ${alias}` : name;
|
|
425
|
+
declarations.push({
|
|
426
|
+
exportDeclaration: {
|
|
427
|
+
name,
|
|
428
|
+
alias: alias ?? name,
|
|
429
|
+
path
|
|
430
|
+
},
|
|
431
|
+
text: path ? `export type { ${nameText} } from "${path}";` : `export type { ${nameText} };`
|
|
432
|
+
});
|
|
433
|
+
if (referencedFiles) {
|
|
434
|
+
if (nameNode.isKind(SyntaxKind.Identifier)) referencedFiles.push(...getReferencedFiles(nameNode));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return referencedFiles ? [declarations, [...new Set(referencedFiles)]] : [declarations];
|
|
439
|
+
};
|
|
440
|
+
const getReferencedFiles = (importIdentifier) => {
|
|
441
|
+
return (importIdentifier?.getSymbol()?.getAliasedSymbol()?.getDeclarations() || []).flatMap((e) => {
|
|
442
|
+
const sourceFile = e.getSourceFile();
|
|
443
|
+
return sourceFile ? [sourceFile.getFilePath()] : [];
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
const extractGenerics = (callExpression) => {
|
|
447
|
+
return callExpression.getTypeArguments();
|
|
448
|
+
};
|
|
449
|
+
const typeResolverFactory = (sourceFolder) => {
|
|
450
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
451
|
+
const project = createProject({ compilerOptions: {
|
|
452
|
+
moduleResolution: ModuleResolutionKind.Bundler,
|
|
453
|
+
types: [],
|
|
454
|
+
noLib: true,
|
|
455
|
+
skipLibCheck: true,
|
|
456
|
+
paths: {
|
|
457
|
+
[`${defaults.appPrefix}/*`]: [`${sourceFolder.root}/*`],
|
|
458
|
+
[`${defaults.srcPrefix}/*`]: [createPath.src("*")],
|
|
459
|
+
[`${defaults.libPrefix}/*`]: [createPath.lib("*")]
|
|
460
|
+
}
|
|
461
|
+
} });
|
|
462
|
+
const withTypeboxSchema = (resolvedTypes) => {
|
|
463
|
+
const materializedTypes = render(resolved_types_default, { resolvedTypes });
|
|
464
|
+
const sourceFile = project.createSourceFile(`${crc(materializedTypes)}-${Date.now()}.ts`, materializedTypes, { overwrite: true });
|
|
465
|
+
return resolvedTypes.map((type) => {
|
|
466
|
+
const typeNode = sourceFile.getTypeAlias(type.name)?.getTypeNode();
|
|
467
|
+
if (!typeNode) return type;
|
|
468
|
+
const properties = [];
|
|
469
|
+
const numericProperties = [];
|
|
470
|
+
const booleanProperties = [];
|
|
471
|
+
for (const prop of type.properties || []) {
|
|
472
|
+
const propNode = typeNode.isKind(SyntaxKind.TypeLiteral) ? typeNode.getProperty(prop.nameDeclaration)?.getTypeNode() : void 0;
|
|
473
|
+
if (!propNode) continue;
|
|
474
|
+
properties.push({
|
|
475
|
+
...prop,
|
|
476
|
+
typeboxSchema: renderTypeboxSchema(propNode)
|
|
477
|
+
});
|
|
478
|
+
if (isNumericTypeNode(propNode)) numericProperties.push(prop.name);
|
|
479
|
+
if (isBooleanTypeNode(propNode)) booleanProperties.push(prop.name);
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
...type,
|
|
483
|
+
properties,
|
|
484
|
+
numericProperties,
|
|
485
|
+
booleanProperties,
|
|
486
|
+
typeboxSchema: renderTypeboxSchema(typeNode)
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
const literalTypesResolver = (literalTypes, options) => {
|
|
491
|
+
const sourceFile = project.createSourceFile(`${crc(literalTypes)}-${Date.now()}.ts`, literalTypes, { overwrite: true });
|
|
492
|
+
const resolvedTypes = flattener(project, sourceFile, {
|
|
493
|
+
...options,
|
|
494
|
+
stripComments: true
|
|
495
|
+
});
|
|
496
|
+
return withTypeboxSchema(resolvedTypes);
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
getSourceFile: (fileFullpath) => {
|
|
500
|
+
return project.getSourceFile(fileFullpath) || project.addSourceFileAtPath(fileFullpath);
|
|
501
|
+
},
|
|
502
|
+
refreshSourceFile: async (fileFullpath) => {
|
|
503
|
+
const sourceFile = project.getSourceFile(fileFullpath);
|
|
504
|
+
if (sourceFile) await sourceFile.refreshFromFileSystem();
|
|
505
|
+
},
|
|
506
|
+
literalTypesResolver
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
return {
|
|
510
|
+
createProject,
|
|
511
|
+
extractDefaultExport,
|
|
512
|
+
extractParamsRefinements,
|
|
513
|
+
extractRouteMethods,
|
|
514
|
+
extractTypeDeclarations,
|
|
515
|
+
resolveRouteSignature,
|
|
516
|
+
typeResolverFactory
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
/**
|
|
520
|
+
* Property name as declared, stripping quotes for string-literal keys.
|
|
521
|
+
* */
|
|
522
|
+
var propertyName = (property) => {
|
|
523
|
+
const nameNode = property.getNameNode();
|
|
524
|
+
return nameNode.isKind(SyntaxKind.StringLiteral) ? nameNode.getLiteralValue() : nameNode.getText();
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* Whether type node reduces to `number`.
|
|
528
|
+
*
|
|
529
|
+
* number -> true
|
|
530
|
+
* VRefine<number, {}> -> true (peel to first type argument)
|
|
531
|
+
* Array<number> -> true (peel to first type argument)
|
|
532
|
+
* number[] -> true (peel to element type)
|
|
533
|
+
* readonly number[] -> true (peel the type operator, then array)
|
|
534
|
+
* VRefine<Array<number>, {}> -> true (peels compose)
|
|
535
|
+
* 2, -2 -> true (literal types, sign included)
|
|
536
|
+
* 1 | -2 | 3 -> true (union with every member numeric)
|
|
537
|
+
* number | string -> false (mixed unions are not numeric)
|
|
538
|
+
*
|
|
539
|
+
* Does NOT descend into object properties or nested tuple elements.
|
|
540
|
+
* */
|
|
541
|
+
var isNumericTypeNode = (typeNode, refineTypeName = defaults.refineTypeName) => {
|
|
542
|
+
if (typeNode.isKind(SyntaxKind.ParenthesizedType)) return isNumericTypeNode(typeNode.getTypeNode(), refineTypeName);
|
|
543
|
+
if (typeNode.isKind(SyntaxKind.TypeReference)) {
|
|
544
|
+
const name = typeNode.getTypeName().getText();
|
|
545
|
+
return name === refineTypeName || name === "Array" ? isNumericTypeNode(typeNode.getTypeArguments()[0], refineTypeName) : false;
|
|
546
|
+
}
|
|
547
|
+
if (typeNode.isKind(SyntaxKind.ArrayType)) return isNumericTypeNode(typeNode.getElementTypeNode(), refineTypeName);
|
|
548
|
+
if (typeNode.isKind(SyntaxKind.TypeOperator)) return typeNode.getOperator() === SyntaxKind.ReadonlyKeyword ? isNumericTypeNode(typeNode.getTypeNode(), refineTypeName) : false;
|
|
549
|
+
if (typeNode.isKind(SyntaxKind.UnionType)) return typeNode.getTypeNodes().every((node) => isNumericTypeNode(node, refineTypeName));
|
|
550
|
+
if (typeNode.isKind(SyntaxKind.LiteralType)) {
|
|
551
|
+
const literal = typeNode.getLiteral();
|
|
552
|
+
return literal.isKind(SyntaxKind.PrefixUnaryExpression) ? literal.getOperand().isKind(SyntaxKind.NumericLiteral) : literal.isKind(SyntaxKind.NumericLiteral);
|
|
553
|
+
}
|
|
554
|
+
return typeNode.isKind(SyntaxKind.NumberKeyword);
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Whether type node reduces to `boolean`.
|
|
558
|
+
*
|
|
559
|
+
* boolean -> true
|
|
560
|
+
* Array<boolean> -> true
|
|
561
|
+
* boolean[] -> true
|
|
562
|
+
* readonly boolean[] -> true
|
|
563
|
+
* true, false -> true (literal types)
|
|
564
|
+
*
|
|
565
|
+
* Does NOT descend into object properties or nested tuple elements.
|
|
566
|
+
* */
|
|
567
|
+
var isBooleanTypeNode = (typeNode) => {
|
|
568
|
+
if (typeNode.isKind(SyntaxKind.ParenthesizedType)) return isBooleanTypeNode(typeNode.getTypeNode());
|
|
569
|
+
if (typeNode.isKind(SyntaxKind.TypeReference)) return typeNode.getTypeName().getText() === "Array" ? isBooleanTypeNode(typeNode.getTypeArguments()[0]) : false;
|
|
570
|
+
if (typeNode.isKind(SyntaxKind.ArrayType)) return isBooleanTypeNode(typeNode.getElementTypeNode());
|
|
571
|
+
if (typeNode.isKind(SyntaxKind.TypeOperator)) return typeNode.getOperator() === SyntaxKind.ReadonlyKeyword ? isBooleanTypeNode(typeNode.getTypeNode()) : false;
|
|
572
|
+
if (typeNode.isKind(SyntaxKind.LiteralType)) {
|
|
573
|
+
const literal = typeNode.getLiteral();
|
|
574
|
+
return literal.isKind(SyntaxKind.TrueKeyword) || literal.isKind(SyntaxKind.FalseKeyword);
|
|
575
|
+
}
|
|
576
|
+
return typeNode.isKind(SyntaxKind.BooleanKeyword);
|
|
577
|
+
};
|
|
578
|
+
/**
|
|
579
|
+
* Render a ts-morph TypeNode to TypeBox Script text, replacing every VRefine
|
|
580
|
+
* occurrence (at any depth) with the infix `with` form.
|
|
581
|
+
*
|
|
582
|
+
* VRefine<string, { format: "email" }> -> (string with { format: "email" })
|
|
583
|
+
* VRefine<string, { format: "email" }>[] -> (string with { format: "email" })[]
|
|
584
|
+
* VRefine<Array<string>, { minItems: 1 }> -> (Array<string> with { minItems: 1 })
|
|
585
|
+
* VRefine<Record<string, string>, { maxProperties: 20 }>
|
|
586
|
+
* -> (Record<string, string> with { maxProperties: 20 })
|
|
587
|
+
*
|
|
588
|
+
* The parentheses matter: for an array element the `with` clause must bind to the element,
|
|
589
|
+
* not the array, so a VRefine that is the element of an array is wrapped.
|
|
590
|
+
* So, wrapping unconditionally - harmless when not nested, required when it is.
|
|
591
|
+
*
|
|
592
|
+
* @param typeNode the node to render
|
|
593
|
+
* @param refineTypeName the configured refine type name
|
|
594
|
+
* */
|
|
595
|
+
var renderTypeboxSchema = (typeNode, refineTypeName = defaults.refineTypeName) => {
|
|
596
|
+
const traverse = (typeNode) => {
|
|
597
|
+
if (typeNode.isKind(SyntaxKind.TypeReference) && typeNode.getTypeName().getText() === refineTypeName) {
|
|
598
|
+
const [inner, opts] = typeNode.getTypeArguments();
|
|
599
|
+
if (!inner || !opts) return typeNode.getText();
|
|
600
|
+
return `(${traverse(inner)} with ${opts.getText()})`;
|
|
601
|
+
}
|
|
602
|
+
if (typeNode.isKind(SyntaxKind.ArrayType)) return `${traverse(typeNode.getElementTypeNode())}[]`;
|
|
603
|
+
if (typeNode.isKind(SyntaxKind.TypeLiteral)) {
|
|
604
|
+
const members = [];
|
|
605
|
+
for (const member of typeNode.getMembers()) if (member.isKind(SyntaxKind.PropertySignature)) {
|
|
606
|
+
const name = member.getName();
|
|
607
|
+
const optional = member.hasQuestionToken() ? "?" : "";
|
|
608
|
+
const t = member.getTypeNode();
|
|
609
|
+
const rendered = t ? traverse(t) : "unknown";
|
|
610
|
+
members.push(`${name}${optional}: ${rendered}`);
|
|
611
|
+
} else members.push(member.getText());
|
|
612
|
+
return `{ ${members.join(", ")} }`;
|
|
613
|
+
}
|
|
614
|
+
if (typeNode.isKind(SyntaxKind.TupleType)) return `[${typeNode.getElements().map((el) => traverse(el)).join(", ")}]`;
|
|
615
|
+
if (typeNode.isKind(SyntaxKind.ParenthesizedType)) return `(${traverse(typeNode.getTypeNode())})`;
|
|
616
|
+
if (typeNode.isKind(SyntaxKind.UnionType)) return typeNode.getTypeNodes().map((t) => traverse(t)).join(" | ");
|
|
617
|
+
if (typeNode.isKind(SyntaxKind.IntersectionType)) return typeNode.getTypeNodes().map((t) => traverse(t)).join(" & ");
|
|
618
|
+
if (typeNode.isKind(SyntaxKind.TypeReference)) {
|
|
619
|
+
const args = typeNode.getTypeArguments();
|
|
620
|
+
if (args.length === 0) return typeNode.getText();
|
|
621
|
+
return `${typeNode.getTypeName().getText()}<${args.map((a) => traverse(a)).join(", ")}>`;
|
|
622
|
+
}
|
|
623
|
+
return typeNode.getText();
|
|
624
|
+
};
|
|
625
|
+
return escapeTemplateLiterals(traverse(typeNode));
|
|
626
|
+
};
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/generators.ts
|
|
629
|
+
var defineGenerator = ({ meta, factory, dependencies, devDependencies }) => {
|
|
630
|
+
const wrapper = ((options) => {
|
|
631
|
+
return {
|
|
632
|
+
meta,
|
|
633
|
+
factory: (folder) => factory(folder, options),
|
|
634
|
+
dependencies,
|
|
635
|
+
devDependencies,
|
|
636
|
+
options
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
Object.assign(wrapper, {
|
|
640
|
+
meta,
|
|
641
|
+
dependencies,
|
|
642
|
+
devDependencies
|
|
643
|
+
});
|
|
644
|
+
return wrapper;
|
|
645
|
+
};
|
|
646
|
+
var defineGeneratorFactory = (factory) => factory;
|
|
647
|
+
/**
|
|
648
|
+
* Filter watched API routes by event.
|
|
649
|
+
* Handle 3 cases:
|
|
650
|
+
* - event is undefined (means initial call): process all routes.
|
|
651
|
+
* - `create` event given: process newly added route.
|
|
652
|
+
* Files can arrive already containing content (git checkout, cp) -
|
|
653
|
+
* clients must generate on create, not only on a subsequent update.
|
|
654
|
+
* - `update` event given: process the updated route,
|
|
655
|
+
* and any route referencing the updated file (shared types).
|
|
656
|
+
* */
|
|
657
|
+
var createWatchedApiRouteEntriesFilter = (event, eventsOfInterest) => {
|
|
658
|
+
return ({ kind, entry }) => {
|
|
659
|
+
if (!event) return true;
|
|
660
|
+
if (!eventsOfInterest.includes(event.kind)) return false;
|
|
661
|
+
if (kind === "apiRoute" || kind === "apiUse") return entry.fileFullpath !== event.file ? entry.referencedFiles?.includes(event.file) : true;
|
|
662
|
+
return false;
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* Filter watched page routes by event.
|
|
667
|
+
* Handle 3 cases:
|
|
668
|
+
* - event is undefined (means initial call): process all routes.
|
|
669
|
+
* - `create` event given: process newly added route.
|
|
670
|
+
* Files can arrive already containing content (git checkout, cp) -
|
|
671
|
+
* clients must generate on create, not only on a subsequent update.
|
|
672
|
+
* - `update` event given: process the updated route,
|
|
673
|
+
* and any route referencing the updated file (shared types).
|
|
674
|
+
* */
|
|
675
|
+
var createWatchedPageRouteEntriesFilter = (event, eventsOfInterest) => {
|
|
676
|
+
return ({ kind, entry }) => {
|
|
677
|
+
if (!event) return true;
|
|
678
|
+
if (!eventsOfInterest.includes(event.kind)) return false;
|
|
679
|
+
return kind === "pageRoute" || kind === "pageLayout" ? entry.fileFullpath === event.file : false;
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/routes/paths.ts
|
|
684
|
+
/**
|
|
685
|
+
* Parse a filesystem route path into structured PathToken array.
|
|
686
|
+
*
|
|
687
|
+
* Uses path-to-regexp v8 AST for parsing, with directory-friendly syntax:
|
|
688
|
+
* - [param] => required param - :param
|
|
689
|
+
* - {param} => optional param - {:param}
|
|
690
|
+
* - {...param} => splat - {*param}
|
|
691
|
+
*
|
|
692
|
+
* Direct :param syntax is prohibited outside {} to avoid ambiguity.
|
|
693
|
+
* Inside {} it is treated as path-to-regexp power syntax and used as-is.
|
|
694
|
+
*
|
|
695
|
+
* Throw error if path can not be compiled into path-to-regexp pattern.
|
|
696
|
+
*
|
|
697
|
+
* Examples:
|
|
698
|
+
* Required: [id], [name]
|
|
699
|
+
* Optional: {name}, {format}
|
|
700
|
+
* Splat: {...path}
|
|
701
|
+
* Mixed segments: shop/[id]-{name}
|
|
702
|
+
* Power syntax: {-v:version{-:pre}}, :name{@:version{.:min}}.js
|
|
703
|
+
* */
|
|
704
|
+
var pathTokensFactory = (path) => {
|
|
705
|
+
/**
|
|
706
|
+
* Recursively extract parts from path-to-regexp AST tokens.
|
|
707
|
+
* A param inside a group is optional; top-level params are required.
|
|
708
|
+
* Wildcard tokens are always splat.
|
|
709
|
+
* Slash-only text nodes (restored for parsing) are skipped.
|
|
710
|
+
* */
|
|
711
|
+
const extractParts = (tokens, createConst, insideGroup = false) => {
|
|
712
|
+
const parts = [];
|
|
713
|
+
for (const token of tokens) switch (token.type) {
|
|
714
|
+
case "text":
|
|
715
|
+
if (token.value !== "/") parts.push({
|
|
716
|
+
type: "static",
|
|
717
|
+
value: token.value
|
|
718
|
+
});
|
|
719
|
+
break;
|
|
720
|
+
case "param":
|
|
721
|
+
parts.push({
|
|
722
|
+
type: "param",
|
|
723
|
+
kind: insideGroup ? "optional" : "required",
|
|
724
|
+
name: token.name,
|
|
725
|
+
const: createConst(token.name)
|
|
726
|
+
});
|
|
727
|
+
break;
|
|
728
|
+
case "wildcard":
|
|
729
|
+
parts.push({
|
|
730
|
+
type: "param",
|
|
731
|
+
kind: "splat",
|
|
732
|
+
name: token.name,
|
|
733
|
+
const: createConst(token.name)
|
|
734
|
+
});
|
|
735
|
+
break;
|
|
736
|
+
case "group": parts.push(...extractParts(token.tokens, createConst, true));
|
|
737
|
+
}
|
|
738
|
+
return parts;
|
|
739
|
+
};
|
|
740
|
+
const patternTransforms = [
|
|
741
|
+
(s) => s.replace(/\[(\w+)\]/g, ":$1"),
|
|
742
|
+
(s) => s.replace(/\{(\w+)\}/g, "{:$1}"),
|
|
743
|
+
(s) => s.replace(/\{\.\.\./g, "{*"),
|
|
744
|
+
(s) => {
|
|
745
|
+
return s.startsWith("{") ? s.replace(/^\{/, "{/") : s;
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
const detectBareParams = (s) => {
|
|
749
|
+
let depth = 0;
|
|
750
|
+
for (const [i, ch] of [...s].entries()) if (ch === "{") depth += 1;
|
|
751
|
+
else if (ch === "}") depth -= 1;
|
|
752
|
+
else if (ch === ":" && depth === 0) return s.slice(i + 1).match(/^\w+/)?.[0] || ":";
|
|
753
|
+
};
|
|
754
|
+
const tokens = path.replace(/^index\/?/, "").split("/").flatMap((orig) => {
|
|
755
|
+
if (!orig.length) return [];
|
|
756
|
+
const bareParam = detectBareParams(orig);
|
|
757
|
+
if (bareParam === ":") throw new Error(`${path} contains colons outside braces, use : only within {}`);
|
|
758
|
+
else if (bareParam) throw new Error(`${path} contains bare params, use [${bareParam}] instead of :${bareParam}`);
|
|
759
|
+
const pattern = patternTransforms.reduce((src, fn) => fn(src), orig);
|
|
760
|
+
const { tokens } = parse$1(pattern.replace(/\{[^}]*\}|(\+)/g, (m, p1) => p1 ? "\\+" : m));
|
|
761
|
+
const parts = extractParts(tokens, (val) => {
|
|
762
|
+
return /\W/.test(val) || /^\d/.test(val) ? [val.replace(/^\d+|\W/g, "_"), crc(orig)].join("_") : val;
|
|
763
|
+
});
|
|
764
|
+
const isStatic = parts.length === 1 ? parts[0].type === "static" : false;
|
|
765
|
+
const isParam = parts.length === 1 ? parts[0].type === "param" : false;
|
|
766
|
+
return [{
|
|
767
|
+
kind: isStatic ? "static" : isParam ? "param" : "mixed",
|
|
768
|
+
orig,
|
|
769
|
+
pattern,
|
|
770
|
+
parts
|
|
771
|
+
}];
|
|
772
|
+
});
|
|
773
|
+
compile(tokens.map((e) => e.pattern).join("/"));
|
|
774
|
+
return tokens;
|
|
775
|
+
};
|
|
776
|
+
var createPathPattern = (tokens) => {
|
|
777
|
+
return tokens.map(({ pattern }, i) => {
|
|
778
|
+
const next = tokens[i + 1];
|
|
779
|
+
if (!next || next.pattern.includes("/")) return pattern;
|
|
780
|
+
return tokens.slice(i + 1).some((e) => {
|
|
781
|
+
return e.parts.some((e) => {
|
|
782
|
+
return e.type === "static" || e.kind === "required";
|
|
783
|
+
});
|
|
784
|
+
}) ? `${pattern}/` : pattern;
|
|
785
|
+
}).join("");
|
|
786
|
+
};
|
|
787
|
+
var createHonoPattern = (tokens) => {
|
|
788
|
+
const escapeStatic = (s) => s.replace(/[*+?^${}()|[\]\\]/g, "\\$&");
|
|
789
|
+
const tokensToPattern = (tokens) => {
|
|
790
|
+
return tokens.flatMap((t) => {
|
|
791
|
+
if (t.type === "text") return t.value === "/" ? [] : [escapeStatic(t.value)];
|
|
792
|
+
if (t.type === "param") return ["[^/]+"];
|
|
793
|
+
if (t.type === "wildcard") return [".+"];
|
|
794
|
+
if (t.type === "group") return [`(?:${tokensToPattern(t.tokens)})?`];
|
|
795
|
+
return [];
|
|
796
|
+
}).join("");
|
|
797
|
+
};
|
|
798
|
+
return tokens.flatMap((token, i) => {
|
|
799
|
+
if (token.kind === "static") return [token.parts[0].value];
|
|
800
|
+
if (token.kind === "param") {
|
|
801
|
+
const part = token.parts[0];
|
|
802
|
+
if (part.kind === "splat") return ["*"];
|
|
803
|
+
if (part.kind === "optional") return [`:${part.name}?`];
|
|
804
|
+
return [`:${part.name}`];
|
|
805
|
+
}
|
|
806
|
+
const { tokens } = parse$1(token.pattern);
|
|
807
|
+
return [`:_${i}{${tokensToPattern(tokens)}}`];
|
|
808
|
+
}).join("/");
|
|
809
|
+
};
|
|
810
|
+
var createH3Pattern = (tokens) => {
|
|
811
|
+
const escapeStatic = (s) => s.replace(/[-*+?^${}()|[\]\\]/g, "\\$&");
|
|
812
|
+
const tokensToPattern = (tokens) => {
|
|
813
|
+
return tokens.flatMap((t) => {
|
|
814
|
+
if (t.type === "text") return t.value === "/" ? [] : [escapeStatic(t.value)];
|
|
815
|
+
if (t.type === "param") return [`:${t.name}`];
|
|
816
|
+
if (t.type === "wildcard") return [`/**:${t.name}?`];
|
|
817
|
+
if (t.type === "group") return [`{${tokensToPattern(t.tokens)}}?`];
|
|
818
|
+
return [];
|
|
819
|
+
}).join("");
|
|
820
|
+
};
|
|
821
|
+
return tokens.flatMap((token) => {
|
|
822
|
+
if (token.kind === "static") return [token.parts[0].value];
|
|
823
|
+
if (token.kind === "param") {
|
|
824
|
+
const part = token.parts[0];
|
|
825
|
+
if (part.kind === "splat") return ["**"];
|
|
826
|
+
if (part.kind === "optional") return ["*"];
|
|
827
|
+
return [`:${part.name}`];
|
|
828
|
+
}
|
|
829
|
+
const { tokens } = parse$1(token.pattern);
|
|
830
|
+
return [tokensToPattern(tokens)];
|
|
831
|
+
}).join("/");
|
|
832
|
+
};
|
|
833
|
+
var createAliasPatterns = (aliasMap) => {
|
|
834
|
+
return Object.keys(aliasMap || {}).map((alias) => {
|
|
835
|
+
return createPathPattern(pathTokensFactory(alias));
|
|
836
|
+
});
|
|
837
|
+
};
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/routes/base.ts
|
|
840
|
+
var API_INDEX_BASENAME = "index";
|
|
841
|
+
var API_INDEX_PATTERN = `${API_INDEX_BASENAME}.ts`;
|
|
842
|
+
var API_USE_BASENAME = "use";
|
|
843
|
+
var API_USE_PATTERN = `use.ts`;
|
|
844
|
+
var PAGE_INDEX_BASENAME = "index";
|
|
845
|
+
var PAGE_INDEX_PATTERN = `${PAGE_INDEX_BASENAME}.{tsx,vue,svelte,mdx,md}`;
|
|
846
|
+
var PAGE_LAYOUT_BASENAME = "layout";
|
|
847
|
+
var PAGE_LAYOUT_PATTERN = `${PAGE_LAYOUT_BASENAME}.{tsx,vue,svelte,mdx}`;
|
|
848
|
+
var ROUTE_FILE_PATTERNS = [
|
|
849
|
+
`${defaults.apiDir}/**/${API_INDEX_PATTERN}`,
|
|
850
|
+
`${defaults.apiDir}/**/${API_USE_PATTERN}`,
|
|
851
|
+
`${defaults.pagesDir}/**/${PAGE_INDEX_PATTERN}`,
|
|
852
|
+
`${defaults.pagesDir}/**/${PAGE_LAYOUT_PATTERN}`
|
|
853
|
+
];
|
|
854
|
+
var scanRoutes = async (sourceFolder) => {
|
|
855
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
856
|
+
return glob(ROUTE_FILE_PATTERNS, {
|
|
857
|
+
cwd: createPath.src(),
|
|
858
|
+
absolute: true,
|
|
859
|
+
onlyFiles: true,
|
|
860
|
+
followSymbolicLinks: false,
|
|
861
|
+
ignore: [
|
|
862
|
+
`${defaults.apiDir}/${API_INDEX_PATTERN}`,
|
|
863
|
+
`${defaults.apiDir}/${API_USE_PATTERN}`,
|
|
864
|
+
`${defaults.pagesDir}/${PAGE_INDEX_PATTERN}`,
|
|
865
|
+
`${defaults.pagesDir}/${PAGE_LAYOUT_PATTERN}`
|
|
866
|
+
]
|
|
867
|
+
});
|
|
868
|
+
};
|
|
869
|
+
var isRouteFile = (file, sourceFolder) => {
|
|
870
|
+
const [_sourceFolder, _folder, ...rest] = resolve(sourceFolder.root, file).replace(`${sourceFolder.root}/${defaults.srcDir}/`, "").split("/");
|
|
871
|
+
/**
|
|
872
|
+
* Ensure the file:
|
|
873
|
+
* - is under the correct source root
|
|
874
|
+
* - belongs to a known route folder (`api/` or `pages/`)
|
|
875
|
+
* - is nested at least one level deep (not a direct child of the folder)
|
|
876
|
+
* */
|
|
877
|
+
if (!_folder || _sourceFolder !== sourceFolder.name || rest.length < 2) return false;
|
|
878
|
+
return picomatch.isMatch(join(_folder, ...rest), ROUTE_FILE_PATTERNS) ? [_folder, rest.join("/")] : false;
|
|
879
|
+
};
|
|
880
|
+
var isApiRoute = (file) => {
|
|
881
|
+
return picomatch.matchBase(file, `**/${API_INDEX_PATTERN}`);
|
|
882
|
+
};
|
|
883
|
+
var isApiUse = (file) => {
|
|
884
|
+
return picomatch.matchBase(file, `**/${API_USE_PATTERN}`);
|
|
885
|
+
};
|
|
886
|
+
var isPageRoute = (file) => {
|
|
887
|
+
return picomatch.matchBase(file, `**/${PAGE_INDEX_PATTERN}`);
|
|
888
|
+
};
|
|
889
|
+
var isPageLayout = (file) => {
|
|
890
|
+
return picomatch.matchBase(file, `**/${PAGE_LAYOUT_PATTERN}`);
|
|
891
|
+
};
|
|
892
|
+
var createRouteEntry = (fileFullpath, sourceFolder) => {
|
|
893
|
+
const resolvedPaths = isRouteFile(fileFullpath, sourceFolder);
|
|
894
|
+
if (!resolvedPaths) return;
|
|
895
|
+
const [folder, file] = resolvedPaths;
|
|
896
|
+
const id = `${file.replace(/\W+/g, "_")}_${crc(file)}`;
|
|
897
|
+
const name = dirname(file);
|
|
898
|
+
try {
|
|
899
|
+
const pathTokens = pathTokensFactory(dirname(file));
|
|
900
|
+
return {
|
|
901
|
+
id,
|
|
902
|
+
name,
|
|
903
|
+
folder,
|
|
904
|
+
file,
|
|
905
|
+
fileFullpath,
|
|
906
|
+
pathTokens,
|
|
907
|
+
pathPattern: createPathPattern(pathTokens),
|
|
908
|
+
honoPattern: createHonoPattern(pathTokens),
|
|
909
|
+
h3Pattern: createH3Pattern(pathTokens)
|
|
910
|
+
};
|
|
911
|
+
} catch (error) {
|
|
912
|
+
console.error(`❗${styleText("red", "ERROR")}: Failed parsing path for "${styleText("cyan", file)}"`);
|
|
913
|
+
console.error(error.message);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
/**
|
|
918
|
+
* Weight of a single path segment.
|
|
919
|
+
*
|
|
920
|
+
* Fixed values per kind prevent complex mixed segments
|
|
921
|
+
* from outscoring multiple statics.
|
|
922
|
+
*
|
|
923
|
+
* static: 4 (exact match, most specific)
|
|
924
|
+
* mixed: 3+ (static + param parts; more statics = slightly higher)
|
|
925
|
+
* required: 2 (matches any single segment)
|
|
926
|
+
* optional: 1 (may or may not match)
|
|
927
|
+
* splat: 0 (matches anything, least specific)
|
|
928
|
+
* */
|
|
929
|
+
var segmentWeight = (token) => {
|
|
930
|
+
if (token.kind === "static") return 4;
|
|
931
|
+
if (token.kind === "mixed") {
|
|
932
|
+
let statics = 0;
|
|
933
|
+
let optionals = 0;
|
|
934
|
+
for (const part of token.parts) if (part.type === "static") statics += 1;
|
|
935
|
+
else if (part.kind === "optional" || part.kind === "splat") optionals += 1;
|
|
936
|
+
return 3 + statics * .01 - optionals * .005;
|
|
937
|
+
}
|
|
938
|
+
switch (token.parts[0].kind) {
|
|
939
|
+
case "required": return 2;
|
|
940
|
+
case "optional": return 1;
|
|
941
|
+
case "splat": return 0;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* Compare two route entries for registration order.
|
|
946
|
+
*
|
|
947
|
+
* More specific routes sort first (higher priority).
|
|
948
|
+
* Segments are compared left-to-right; first difference wins.
|
|
949
|
+
*
|
|
950
|
+
* When one route is a prefix of the other, the trailing segment
|
|
951
|
+
* determines order: a trailing splat loses (shorter sibling first),
|
|
952
|
+
* anything else wins (longer route is more specific).
|
|
953
|
+
*
|
|
954
|
+
* At equal weight and length, lexicographic tiebreak ensures
|
|
955
|
+
* deterministic ordering across JS engines.
|
|
956
|
+
* */
|
|
957
|
+
var sortRoutes = (a, b) => {
|
|
958
|
+
const pairs = Array.from({ length: Math.max(a.pathTokens.length, b.pathTokens.length) }, (_, i) => [a.pathTokens[i], b.pathTokens[i]]);
|
|
959
|
+
for (const [at, bt] of pairs) {
|
|
960
|
+
if (!at || !bt) {
|
|
961
|
+
const extra = at ?? bt;
|
|
962
|
+
if (extra?.kind === "param" ? extra.parts[0].kind === "splat" : false) return at ? 1 : -1;
|
|
963
|
+
return at ? -1 : 1;
|
|
964
|
+
}
|
|
965
|
+
const aw = segmentWeight(at);
|
|
966
|
+
const bw = segmentWeight(bt);
|
|
967
|
+
if (aw !== bw) return bw - aw;
|
|
968
|
+
}
|
|
969
|
+
return a.pathPattern.localeCompare(b.pathPattern);
|
|
970
|
+
};
|
|
971
|
+
/**
|
|
972
|
+
* Compare two route entries for first-match resolution order,
|
|
973
|
+
* as used by the generated svelte/mdx routers.
|
|
974
|
+
*
|
|
975
|
+
* Identical to `sortRoutes` except when one route is a prefix of the other
|
|
976
|
+
* and the trailing extra segment is an optional parameter: there the SHORTER route goes first.
|
|
977
|
+
* The longer route's pattern also matches the bare path (optional omitted),
|
|
978
|
+
* but only the shorter route can serve it - deeper URLs never match the shorter one's pattern,
|
|
979
|
+
* so it must get the first shot:
|
|
980
|
+
*
|
|
981
|
+
* /blog -> `blog`, not `blog/{category}/{tag}`
|
|
982
|
+
* /admin/x/resources -> `admin/[tenant]/resources`, not `.../resources/{type}`
|
|
983
|
+
*
|
|
984
|
+
* Trailing splats already sort shorter-first in `sortRoutes`;
|
|
985
|
+
* trailing static or required segments make the two patterns disjoint,
|
|
986
|
+
* so their relative order is immaterial for first-match and stays as registration order.
|
|
987
|
+
* */
|
|
988
|
+
var sortRoutesForResolution = (a, b) => {
|
|
989
|
+
const shorter = a.pathTokens.length < b.pathTokens.length ? a : b;
|
|
990
|
+
const longer = shorter === a ? b : a;
|
|
991
|
+
if (a.pathTokens.length !== b.pathTokens.length && shorter.pathTokens.every((token, i) => {
|
|
992
|
+
const counterpart = longer.pathTokens[i];
|
|
993
|
+
return segmentWeight(token) === segmentWeight(counterpart);
|
|
994
|
+
})) {
|
|
995
|
+
const extra = longer.pathTokens[shorter.pathTokens.length];
|
|
996
|
+
if (extra.kind === "param" && extra.parts[0].kind === "optional") return shorter === a ? -1 : 1;
|
|
997
|
+
}
|
|
998
|
+
return sortRoutes(a, b);
|
|
999
|
+
};
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region src/routes/resolve.ts
|
|
1002
|
+
var resolverFactory = (sourceFolder, cacheFactory) => {
|
|
1003
|
+
const { generators } = sourceFolder.config;
|
|
1004
|
+
const { refineTypeName = defaults.refineTypeName } = typeof sourceFolder.config.validation === "object" ? { ...sourceFolder.config.validation } : {};
|
|
1005
|
+
const { typeResolverFactory, resolveRouteSignature } = astFactory();
|
|
1006
|
+
const resolveTypes = generators.some(({ meta }) => meta.resolveTypes);
|
|
1007
|
+
const { literalTypesResolver, getSourceFile, refreshSourceFile } = typeResolverFactory(sourceFolder);
|
|
1008
|
+
return {
|
|
1009
|
+
pageLayoutResolver(entry) {
|
|
1010
|
+
const { name } = entry;
|
|
1011
|
+
const handler = async () => {
|
|
1012
|
+
return {
|
|
1013
|
+
kind: "pageLayout",
|
|
1014
|
+
entry
|
|
1015
|
+
};
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
name,
|
|
1019
|
+
handler
|
|
1020
|
+
};
|
|
1021
|
+
},
|
|
1022
|
+
pageRouteResolver(entry) {
|
|
1023
|
+
const { id, name, folder, file, fileFullpath, pathTokens, pathPattern, honoPattern, h3Pattern } = entry;
|
|
1024
|
+
const handler = async () => {
|
|
1025
|
+
return {
|
|
1026
|
+
kind: "pageRoute",
|
|
1027
|
+
entry: {
|
|
1028
|
+
id,
|
|
1029
|
+
name,
|
|
1030
|
+
pathTokens,
|
|
1031
|
+
pathPattern,
|
|
1032
|
+
honoPattern,
|
|
1033
|
+
h3Pattern,
|
|
1034
|
+
params: { schema: pathTokens.flatMap((e) => {
|
|
1035
|
+
return e.parts.filter((p) => p.type === "param");
|
|
1036
|
+
}) },
|
|
1037
|
+
folder,
|
|
1038
|
+
file,
|
|
1039
|
+
fileFullpath
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
name,
|
|
1045
|
+
handler
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
apiUseResolver(entry) {
|
|
1049
|
+
const { name } = entry;
|
|
1050
|
+
const handler = async () => {
|
|
1051
|
+
return {
|
|
1052
|
+
kind: "apiUse",
|
|
1053
|
+
entry
|
|
1054
|
+
};
|
|
1055
|
+
};
|
|
1056
|
+
return {
|
|
1057
|
+
name,
|
|
1058
|
+
handler
|
|
1059
|
+
};
|
|
1060
|
+
},
|
|
1061
|
+
apiRouteResolver(entry) {
|
|
1062
|
+
const { id, name, file, folder, fileFullpath, pathTokens, pathPattern, honoPattern, h3Pattern } = entry;
|
|
1063
|
+
const handler = async (updatedFile) => {
|
|
1064
|
+
const paramsSchema = pathTokens.flatMap((e) => {
|
|
1065
|
+
return e.parts.flatMap((p) => {
|
|
1066
|
+
return p.type === "param" ? [p] : [];
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
const optionalParams = paramsSchema.length ? paramsSchema.filter((e) => e.kind === "required").length === 0 : true;
|
|
1070
|
+
const cacheStore = cacheFactory ? cacheFactory({
|
|
1071
|
+
id,
|
|
1072
|
+
file,
|
|
1073
|
+
fileFullpath
|
|
1074
|
+
}, sourceFolder, { resolveTypes }) : void 0;
|
|
1075
|
+
let cache = cacheStore ? await cacheStore.get({ validate: true }) : void 0;
|
|
1076
|
+
if (!cache) {
|
|
1077
|
+
if (updatedFile === fileFullpath) await refreshSourceFile(fileFullpath);
|
|
1078
|
+
const { typeDeclarations, paramsRefinements, methods, validationDefinitions, referencedFiles = [] } = await resolveRouteSignature({
|
|
1079
|
+
id,
|
|
1080
|
+
name,
|
|
1081
|
+
fileFullpath,
|
|
1082
|
+
optionalParams
|
|
1083
|
+
}, {
|
|
1084
|
+
withReferencedFiles: true,
|
|
1085
|
+
sourceFile: getSourceFile(fileFullpath),
|
|
1086
|
+
relpathResolver(path) {
|
|
1087
|
+
return join(defaults.srcPrefix, defaults.apiDir, dirname(file), path);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
const validationTypes = validationDefinitions.flatMap((def) => {
|
|
1091
|
+
return def.target === "response" ? def.variants.flatMap(({ id, body }) => {
|
|
1092
|
+
return body ? [{
|
|
1093
|
+
id,
|
|
1094
|
+
text: body
|
|
1095
|
+
}] : [];
|
|
1096
|
+
}) : [def.schema];
|
|
1097
|
+
});
|
|
1098
|
+
const typesFile = pathResolver(sourceFolder).createPath.libApi(dirname(file), "types.ts");
|
|
1099
|
+
const params = {
|
|
1100
|
+
id: ["ParamsT", crc(name)].join(""),
|
|
1101
|
+
schema: paramsSchema,
|
|
1102
|
+
resolvedType: void 0
|
|
1103
|
+
};
|
|
1104
|
+
const typesFileContent = render(types_default, {
|
|
1105
|
+
params,
|
|
1106
|
+
paramsSchema: paramsSchema.map((param, index) => {
|
|
1107
|
+
return {
|
|
1108
|
+
...param,
|
|
1109
|
+
isRequired: param.kind === "required",
|
|
1110
|
+
isSplat: param.kind === "splat",
|
|
1111
|
+
refinement: paramsRefinements?.at(index)
|
|
1112
|
+
};
|
|
1113
|
+
}),
|
|
1114
|
+
typeDeclarations,
|
|
1115
|
+
validationTypes
|
|
1116
|
+
});
|
|
1117
|
+
const resolvedTypes = resolveTypes ? literalTypesResolver(typesFileContent, {
|
|
1118
|
+
stripComments: true,
|
|
1119
|
+
overrides: { [refineTypeName]: refineTypeName },
|
|
1120
|
+
withProperties: [params.id, ...validationTypes.map(({ id }) => id)]
|
|
1121
|
+
}) : void 0;
|
|
1122
|
+
/**
|
|
1123
|
+
* Deploy types.ts file; required by core generators (like fetch).
|
|
1124
|
+
* If types resolved, write resolved types;
|
|
1125
|
+
* otherwise write original types extracted from API route.
|
|
1126
|
+
* */
|
|
1127
|
+
await renderToFile(typesFile, resolvedTypes ? resolved_types_default : typesFileContent, { resolvedTypes });
|
|
1128
|
+
params.resolvedType = resolvedTypes?.find((e) => e.name === params.id);
|
|
1129
|
+
cache = {
|
|
1130
|
+
hash: 0,
|
|
1131
|
+
referencedFiles: {},
|
|
1132
|
+
params,
|
|
1133
|
+
methods,
|
|
1134
|
+
typeDeclarations,
|
|
1135
|
+
validationDefinitions: validationDefinitions.map((def) => {
|
|
1136
|
+
return {
|
|
1137
|
+
...def,
|
|
1138
|
+
...def.target === "response" ? { variants: def.variants.map((variant) => {
|
|
1139
|
+
return {
|
|
1140
|
+
...variant,
|
|
1141
|
+
resolvedType: resolvedTypes?.find((e) => e.name === variant.id)
|
|
1142
|
+
};
|
|
1143
|
+
}) } : { schema: {
|
|
1144
|
+
...def.schema,
|
|
1145
|
+
resolvedType: resolvedTypes?.find((e) => e.name === def.schema.id)
|
|
1146
|
+
} }
|
|
1147
|
+
};
|
|
1148
|
+
})
|
|
1149
|
+
};
|
|
1150
|
+
if (cacheStore) {
|
|
1151
|
+
const { referencedFiles: _void, ...rest } = cache;
|
|
1152
|
+
cache = await cacheStore.set({
|
|
1153
|
+
...rest,
|
|
1154
|
+
referencedFiles
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const validationDefinitions = cache.validationDefinitions.flatMap((def) => {
|
|
1159
|
+
let augmentedDef = def;
|
|
1160
|
+
if (def.target === "response") augmentedDef = {
|
|
1161
|
+
...def,
|
|
1162
|
+
variants: def.variants.flatMap((variant, i) => {
|
|
1163
|
+
if (typeof variant.contentType !== "string") return [variant];
|
|
1164
|
+
if (variant.contentType.includes("/")) return [variant];
|
|
1165
|
+
const contentType = lookup(variant.contentType);
|
|
1166
|
+
if (!contentType) {
|
|
1167
|
+
console.warn(styleText(["bold", "red"], "✗ Failed resolving Response Content Type"));
|
|
1168
|
+
console.warn(` Invalid value provided for mime-types lookup - ${variant.contentType}`);
|
|
1169
|
+
console.warn(styleText(["cyan"], ` Response variant #${i} excluded from route schemas`));
|
|
1170
|
+
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
1171
|
+
console.warn();
|
|
1172
|
+
return [];
|
|
1173
|
+
}
|
|
1174
|
+
return [{
|
|
1175
|
+
...variant,
|
|
1176
|
+
contentType
|
|
1177
|
+
}];
|
|
1178
|
+
})
|
|
1179
|
+
};
|
|
1180
|
+
else if (def.contentType && !def.contentType.includes("/")) {
|
|
1181
|
+
const contentType = lookup(def.contentType);
|
|
1182
|
+
if (!contentType) {
|
|
1183
|
+
console.warn(styleText(["bold", "red"], "✗ Failed resolving Response Content Type"));
|
|
1184
|
+
console.warn(` Invalid value provided for mime-types lookup - ${def.contentType}`);
|
|
1185
|
+
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
1186
|
+
console.warn();
|
|
1187
|
+
} else augmentedDef = {
|
|
1188
|
+
...def,
|
|
1189
|
+
contentType
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
return augmentedDef ? [augmentedDef] : [];
|
|
1193
|
+
});
|
|
1194
|
+
return {
|
|
1195
|
+
kind: "apiRoute",
|
|
1196
|
+
entry: {
|
|
1197
|
+
id,
|
|
1198
|
+
name,
|
|
1199
|
+
pathTokens,
|
|
1200
|
+
pathPattern,
|
|
1201
|
+
honoPattern,
|
|
1202
|
+
h3Pattern,
|
|
1203
|
+
params: cache.params,
|
|
1204
|
+
optionalParams,
|
|
1205
|
+
folder,
|
|
1206
|
+
file,
|
|
1207
|
+
fileFullpath,
|
|
1208
|
+
methods: cache.methods,
|
|
1209
|
+
typeDeclarations: cache.typeDeclarations,
|
|
1210
|
+
validationDefinitions,
|
|
1211
|
+
referencedFiles: Object.keys(cache.referencedFiles).map((e) => resolve(sourceFolder.root, e))
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
};
|
|
1215
|
+
return {
|
|
1216
|
+
name,
|
|
1217
|
+
handler
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
};
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/routes/nesting.ts
|
|
1224
|
+
var nestedRoutesFactory = (routeEntries) => {
|
|
1225
|
+
const entryStack = structuredClone(routeEntries).sort(sortRoutes);
|
|
1226
|
+
/**
|
|
1227
|
+
* Group entries by name (at the same level, both layout and index share same name).
|
|
1228
|
+
*
|
|
1229
|
+
* Handle 3 cases:
|
|
1230
|
+
* - both index and layout exists
|
|
1231
|
+
* - only index exists
|
|
1232
|
+
* - only layout exists
|
|
1233
|
+
*
|
|
1234
|
+
* */
|
|
1235
|
+
const transformEntries = (entries, parent) => {
|
|
1236
|
+
return [...new Set(entries.map((e) => e.name))].flatMap((name) => {
|
|
1237
|
+
const nameEntries = entryStack.flatMap(({ fileFullpath, ...entry }) => {
|
|
1238
|
+
return entry.name === name ? [entry] : [];
|
|
1239
|
+
});
|
|
1240
|
+
const index = nameEntries.find((e) => basename(e.file).startsWith(PAGE_INDEX_BASENAME));
|
|
1241
|
+
const layout = nameEntries.find((e) => basename(e.file).startsWith(PAGE_LAYOUT_BASENAME));
|
|
1242
|
+
if (index || layout) return [{
|
|
1243
|
+
index: index ? {
|
|
1244
|
+
...index,
|
|
1245
|
+
pathTokens: index.pathTokens.slice(parent?.pathTokens.length || 0)
|
|
1246
|
+
} : void 0,
|
|
1247
|
+
layout: layout ? {
|
|
1248
|
+
...layout,
|
|
1249
|
+
pathTokens: layout.pathTokens.slice(parent?.pathTokens.length || 0)
|
|
1250
|
+
} : void 0,
|
|
1251
|
+
parent: parent?.name,
|
|
1252
|
+
children: transformEntries(findDescendantEntries(index || layout), index || layout)
|
|
1253
|
+
}];
|
|
1254
|
+
return [];
|
|
1255
|
+
});
|
|
1256
|
+
};
|
|
1257
|
+
/**
|
|
1258
|
+
* Recursively finds direct descendant routes.
|
|
1259
|
+
*
|
|
1260
|
+
* "Direct" means the next existing route in the path hierarchy,
|
|
1261
|
+
* skipping intermediate path segments that don't have index/layout files.
|
|
1262
|
+
*
|
|
1263
|
+
* Example: blog/post/[slug]/index.tsx is a direct child of blog/index.tsx
|
|
1264
|
+
* even though "post" segment exists (it has no index.tsx).
|
|
1265
|
+
* */
|
|
1266
|
+
const findDescendantEntries = ({ name, pathTokens }) => {
|
|
1267
|
+
const potentialChildren = entryStack.filter((entry) => {
|
|
1268
|
+
if (entry.pathTokens.length <= pathTokens.length) return false;
|
|
1269
|
+
if (!entry.name.startsWith(posix.join(name, "/"))) return false;
|
|
1270
|
+
return true;
|
|
1271
|
+
});
|
|
1272
|
+
return potentialChildren.filter((child) => {
|
|
1273
|
+
return !potentialChildren.some((intermediate) => {
|
|
1274
|
+
if (intermediate === child) return false;
|
|
1275
|
+
if (intermediate.pathTokens.length <= pathTokens.length) return false;
|
|
1276
|
+
if (intermediate.pathTokens.length >= child.pathTokens.length) return false;
|
|
1277
|
+
return child.name.startsWith(posix.join(intermediate.name, "/"));
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
};
|
|
1281
|
+
return transformEntries(entryStack.filter((entry) => {
|
|
1282
|
+
return !entryStack.some((potential) => {
|
|
1283
|
+
if (potential === entry) return false;
|
|
1284
|
+
if (potential.pathTokens.length >= entry.pathTokens.length) return false;
|
|
1285
|
+
return entry.name.startsWith(posix.join(potential.name, "/"));
|
|
1286
|
+
});
|
|
1287
|
+
}));
|
|
1288
|
+
};
|
|
1289
|
+
//#endregion
|
|
1290
|
+
//#region src/routes/index.ts
|
|
1291
|
+
var routesFactory = async (sourceFolder, cacheFactory) => {
|
|
1292
|
+
const { apiRouteResolver, apiUseResolver, pageRouteResolver, pageLayoutResolver } = resolverFactory(sourceFolder, cacheFactory);
|
|
1293
|
+
const resolversFactory = (routeFiles) => {
|
|
1294
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
1295
|
+
const entries = routeFiles.flatMap((file) => {
|
|
1296
|
+
const entry = createRouteEntry(file, sourceFolder);
|
|
1297
|
+
return entry ? [entry] : [];
|
|
1298
|
+
});
|
|
1299
|
+
for (const entry of entries) if (entry.folder === defaults.apiDir) {
|
|
1300
|
+
if (isApiRoute(entry.file)) resolvers.set(entry.fileFullpath, apiRouteResolver(entry));
|
|
1301
|
+
else if (isApiUse(entry.file)) resolvers.set(entry.fileFullpath, apiUseResolver(entry));
|
|
1302
|
+
} else if (entry.folder === defaults.pagesDir) {
|
|
1303
|
+
if (isPageRoute(entry.file)) resolvers.set(entry.fileFullpath, pageRouteResolver(entry));
|
|
1304
|
+
else if (isPageLayout(entry.file)) resolvers.set(entry.fileFullpath, pageLayoutResolver(entry));
|
|
1305
|
+
}
|
|
1306
|
+
return resolvers;
|
|
1307
|
+
};
|
|
1308
|
+
return {
|
|
1309
|
+
resolvers: resolversFactory(await scanRoutes(sourceFolder)),
|
|
1310
|
+
resolversFactory
|
|
1311
|
+
};
|
|
1312
|
+
};
|
|
1313
|
+
//#endregion
|
|
1314
|
+
//#region src/spinner.ts
|
|
1315
|
+
var spinnerFactory = (startText) => {
|
|
1316
|
+
if (!process.stdout.isTTY || process.env.CI || process.env.TERM === "dumb") return {
|
|
1317
|
+
text() {},
|
|
1318
|
+
append() {},
|
|
1319
|
+
succeed() {},
|
|
1320
|
+
failed() {}
|
|
1321
|
+
};
|
|
1322
|
+
const spinner = new Spinner().start(startText);
|
|
1323
|
+
let _text = startText;
|
|
1324
|
+
return {
|
|
1325
|
+
text(text) {
|
|
1326
|
+
_text = text;
|
|
1327
|
+
spinner.text = text;
|
|
1328
|
+
},
|
|
1329
|
+
append(text) {
|
|
1330
|
+
spinner.text = `${_text} › ${text}`;
|
|
1331
|
+
},
|
|
1332
|
+
succeed(text) {
|
|
1333
|
+
if (text) this.append(text);
|
|
1334
|
+
else this.text(_text);
|
|
1335
|
+
spinner.succeed();
|
|
1336
|
+
},
|
|
1337
|
+
failed(text) {
|
|
1338
|
+
if (text) this.text([_text, text].join("\n"));
|
|
1339
|
+
spinner.failed();
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
};
|
|
1343
|
+
//#endregion
|
|
1344
|
+
//#region src/vite.ts
|
|
1345
|
+
/**
|
|
1346
|
+
* Resolves Kosmo's generated import prefixes to absolute file paths.
|
|
1347
|
+
*
|
|
1348
|
+
* WHY THIS EXISTS
|
|
1349
|
+
* ---------------
|
|
1350
|
+
* Every source folder is scaffolded with three tsconfig `paths` prefixes
|
|
1351
|
+
* that let generated and user code import across
|
|
1352
|
+
* the folder's layout without long relative chains:
|
|
1353
|
+
*
|
|
1354
|
+
* appPrefix "@" -> the project root (<root>/*)
|
|
1355
|
+
* srcPrefix "~" -> the folder's src dir (<root>/src/<name>/*)
|
|
1356
|
+
* libPrefix "_" -> the folder's lib dir (<root>/lib/<name>/*)
|
|
1357
|
+
*
|
|
1358
|
+
* These are the backbone of the generated code: `defineRoute` and friends are
|
|
1359
|
+
* emitted with `_/...` imports, pages import layouts and components via `~/...`,
|
|
1360
|
+
* and `@/...` reaches shared app-level modules. If a bundler cannot resolve
|
|
1361
|
+
* them, nothing the framework generates can be loaded.
|
|
1362
|
+
*
|
|
1363
|
+
* Historically Vite resolved these through its built-in `resolve.tsconfigPaths: true`,
|
|
1364
|
+
* reading the `paths` straight from the generated tsconfig. That option
|
|
1365
|
+
* regressed in Vite 8.1.0 (still broken as of 8.1.3): it stops resolving
|
|
1366
|
+
* `paths` mappings, especially those defined in an extended / referenced base
|
|
1367
|
+
* config - which is exactly Kosmo's structure, where a source folder's
|
|
1368
|
+
* tsconfig `extends` `lib/<name>/tsconfig.json` and the paths use the
|
|
1369
|
+
* TypeScript `${configDir}` variable. The failure surfaces as:
|
|
1370
|
+
*
|
|
1371
|
+
* [vite] Failed to resolve import "_/pageSamples/welcome.vue" from
|
|
1372
|
+
* "src/<name>/pages/index/index.vue". Does the file exist?
|
|
1373
|
+
*
|
|
1374
|
+
* (Upstream: vitejs/vite #22139, #22047, #22371, #21889, #21856.)
|
|
1375
|
+
*
|
|
1376
|
+
* Rather than pin Vite to 8.0.x indefinitely, or depend on the general-purpose
|
|
1377
|
+
* `vite-tsconfig-paths` plugin, this internal plugin resolves ONLY
|
|
1378
|
+
* known prefixes against dirs derived from the source folder. That makes it:
|
|
1379
|
+
*
|
|
1380
|
+
* - Correct on 8.1.x, where native `tsconfigPaths` is broken.
|
|
1381
|
+
* - Faster: no tsconfig discovery, no `extends` chain walking, no glob
|
|
1382
|
+
* matching - just a prefix check and a handful of `stat` probes,
|
|
1383
|
+
* cached by Vite per specifier.
|
|
1384
|
+
* - Non-blocking: probes via async `fs/promises` `stat`, so `resolveId` never
|
|
1385
|
+
* stalls the event loop.
|
|
1386
|
+
* - `.vue` / `.mdx`-aware: `resolveId` sees every specifier regardless of the
|
|
1387
|
+
* importer's type, so imports from non-JS files resolve without the
|
|
1388
|
+
* `loose` / `allowJs` escape hatches `vite-tsconfig-paths` needs.
|
|
1389
|
+
*
|
|
1390
|
+
* A project with custom `paths` beyond these three prefixes should install
|
|
1391
|
+
* `vite-tsconfig-paths`; this plugin is internal and intentionally narrow.
|
|
1392
|
+
*
|
|
1393
|
+
* The prefix -> dir bases come from `pathResolver`, the same source of truth the
|
|
1394
|
+
* tsconfig generator uses, so alias resolution here can never drift from the
|
|
1395
|
+
* paths that were written into the generated tsconfig.
|
|
1396
|
+
* */
|
|
1397
|
+
var tsconfigPaths = (sourceFolder) => {
|
|
1398
|
+
/**
|
|
1399
|
+
* Candidate extensions for an extensionless specifier, in resolution priority order.
|
|
1400
|
+
* Mirrors the renderable file types the generators emit;
|
|
1401
|
+
* a new page type (e.g. `.svelte`) must be added here or its aliased imports won't resolve.
|
|
1402
|
+
* */
|
|
1403
|
+
const EXTENSIONS = [
|
|
1404
|
+
".ts",
|
|
1405
|
+
".tsx",
|
|
1406
|
+
".js",
|
|
1407
|
+
".jsx",
|
|
1408
|
+
".mjs",
|
|
1409
|
+
".vue",
|
|
1410
|
+
".svelte",
|
|
1411
|
+
".md",
|
|
1412
|
+
".mdx",
|
|
1413
|
+
".json"
|
|
1414
|
+
];
|
|
1415
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
1416
|
+
const table = [
|
|
1417
|
+
[`${defaults.srcPrefix}/`, createPath.src()],
|
|
1418
|
+
[`${defaults.libPrefix}/`, createPath.lib()],
|
|
1419
|
+
[`${defaults.appPrefix}/`, sourceFolder.root]
|
|
1420
|
+
];
|
|
1421
|
+
const resolveWithin = async (baseDir, rest) => {
|
|
1422
|
+
const target = join(baseDir, rest);
|
|
1423
|
+
const { ext } = parse(rest);
|
|
1424
|
+
if (EXTENSIONS.includes(ext)) {
|
|
1425
|
+
for (const candidate of [target, `${target}/index${ext}`]) if (await isFile(candidate)) return candidate;
|
|
1426
|
+
return target;
|
|
1427
|
+
}
|
|
1428
|
+
for (const e of EXTENSIONS) for (const candidate of [`${target}${e}`, `${target}/index${e}`]) if (await isFile(candidate)) return candidate;
|
|
1429
|
+
return target;
|
|
1430
|
+
};
|
|
1431
|
+
return {
|
|
1432
|
+
name: "kosmo:tsconfigPaths",
|
|
1433
|
+
enforce: "pre",
|
|
1434
|
+
async resolveId(source) {
|
|
1435
|
+
if (isAbsolute(source) || source.startsWith(".")) return;
|
|
1436
|
+
for (const [prefix, baseDir] of table) if (source.startsWith(prefix)) return resolveWithin(baseDir, source.replace(prefix, ""));
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
};
|
|
1440
|
+
var nodePrefix = () => {
|
|
1441
|
+
return {
|
|
1442
|
+
name: "kosmojs:node-prefix",
|
|
1443
|
+
enforce: "pre",
|
|
1444
|
+
resolveId(source) {
|
|
1445
|
+
return builtinModules.includes(source) ? {
|
|
1446
|
+
id: `node:${source}`,
|
|
1447
|
+
external: true
|
|
1448
|
+
} : void 0;
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
};
|
|
1452
|
+
/**
|
|
1453
|
+
* Resolves the CSR/SSR variant of env-sensitive modules.
|
|
1454
|
+
*
|
|
1455
|
+
* WHY THIS EXISTS
|
|
1456
|
+
* ---------------
|
|
1457
|
+
* A handful of generated modules must differ between the client graph and the SSR bundle -
|
|
1458
|
+
* the fetch transport (a no-op in the browser, an in-process dispatch on the server)
|
|
1459
|
+
* and the TanStack Query client (a singleton in the browser, per-request on the server).
|
|
1460
|
+
*
|
|
1461
|
+
* Every build installs this plugin; but only the SSR bundle installs it with `kind: "ssr"`.
|
|
1462
|
+
* Nothing is written, so concurrent processes cannot disturb each other.
|
|
1463
|
+
*
|
|
1464
|
+
* Generators declare their modules via `factory.virtualModules()`.
|
|
1465
|
+
* */
|
|
1466
|
+
var virtualModules = (sourceFolder, { kind, command }) => {
|
|
1467
|
+
/**
|
|
1468
|
+
* Rollup's / Rolldown's convention for ids owned by a plugin:
|
|
1469
|
+
* the NUL prefix keeps other plugins and Node resolution from touching them.
|
|
1470
|
+
* */
|
|
1471
|
+
const VIRTUAL_PREFIX = "\0";
|
|
1472
|
+
const modules = sourceFolder.config.generators.flatMap(({ factory }) => {
|
|
1473
|
+
return factory(sourceFolder).virtualModules?.() || [];
|
|
1474
|
+
});
|
|
1475
|
+
const virtualSources = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (const { specifier, csr, ssr } of modules) virtualSources.set(specifier, {
|
|
1477
|
+
csr,
|
|
1478
|
+
ssr
|
|
1479
|
+
});
|
|
1480
|
+
{
|
|
1481
|
+
const source = `export const command = "${command}";`;
|
|
1482
|
+
virtualSources.set("virtual:kosmo/env", {
|
|
1483
|
+
csr: source,
|
|
1484
|
+
ssr: source
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
name: "kosmo:virtualModules",
|
|
1489
|
+
enforce: "pre",
|
|
1490
|
+
resolveId(source) {
|
|
1491
|
+
return virtualSources.has(source) ? VIRTUAL_PREFIX + source : void 0;
|
|
1492
|
+
},
|
|
1493
|
+
load(id) {
|
|
1494
|
+
return id.startsWith(VIRTUAL_PREFIX) ? virtualSources.get(id.slice(1))?.[kind] : void 0;
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
};
|
|
1498
|
+
var vitePlugins = {
|
|
1499
|
+
tsconfigPaths,
|
|
1500
|
+
nodePrefix,
|
|
1501
|
+
virtualModules
|
|
1502
|
+
};
|
|
1503
|
+
var isFile = async (path) => {
|
|
1504
|
+
return stat(path).then((e) => e.isFile()).catch(() => false);
|
|
15
1505
|
};
|
|
1506
|
+
//#endregion
|
|
1507
|
+
export { API_INDEX_BASENAME, API_INDEX_PATTERN, API_USE_BASENAME, API_USE_PATTERN, PAGE_INDEX_BASENAME, PAGE_INDEX_PATTERN, PAGE_LAYOUT_BASENAME, PAGE_LAYOUT_PATTERN, astFactory, containsPathTraversalPatterns, createAliasPatterns, createH3Pattern, createHonoPattern, createPathPattern, createRouteEntry, createWatchedApiRouteEntriesFilter, createWatchedPageRouteEntriesFilter, defineGenerator, defineGeneratorFactory, escapeTemplateLiterals, isApiRoute, isApiUse, isPageLayout, isPageRoute, isRouteFile, mergeConfigs, nestedRoutesFactory, pathExists, pathResolver, pathTokensFactory, render, renderFactory, renderToFile, routesFactory, scanRoutes, sortRoutes, sortRoutesForResolution, spinnerFactory, tsconfigPaths, virtualModules, vitePlugins };
|
|
16
1508
|
|
|
17
|
-
{{#each validationTypes}}export type {{id}} = {{text}};
|
|
18
|
-
{{/each}}
|
|
19
|
-
`,F=()=>{let e=e=>new a(e),u=async(t,n)=>{let{sourceFile:r=e().addSourceFileAtPath(t.fileFullpath)}={...n},[i,a]=y(r,n),o=d(r),s=o?f(o):void 0,c=o?p(t,o):[];return{typeDeclarations:i,paramsRefinements:s,methods:c.map(e=>e.method),validationDefinitions:c.flatMap(e=>e.validationDefinitions),referencedFiles:a}},d=e=>{let[t]=e.getExportAssignments().flatMap(e=>{if(e.isExportEquals())return[];let t=e.getExpression();return t.isKind(o.CallExpression)?[t]:[]});return t},f=e=>{let[t,n]=x(e);if(n?.isKind(o.TupleType))return n.getElements().map((e,t)=>({index:t,text:e.getText()}))},p=(e,t)=>{let n=t.getFirstChildByKind(o.ArrowFunction)||t.getFirstChildByKind(o.FunctionExpression);if(!n)return[];let r=n.getFirstChildByKind(o.ArrayLiteralExpression);if(!r)return[];let i=[];for(let e of r.getChildrenOfKind(o.CallExpression)){let t=e.getExpression().getText();l[t]&&i.push([e,t])}let a=[];for(let[t,n]of i){let[r,i]=x(t);a.push({method:n,validationDefinitions:v(e,n,r,i)})}return a},m=e=>{if(e.isKind(o.LiteralType)){let t=e.getFirstChild();if(t?.isKind(o.TrueKeyword))return!0;if(t?.isKind(o.FalseKeyword))return!1}},h=e=>{if(!e.isKind(o.TupleType))return;let t=200,n,r,[i,a,s]=e.getElements();if(i?.isKind(o.LiteralType)){let e=i.getFirstChildByKind(o.NumericLiteral);e&&(t=Number(e.getText()))}return a&&(n=_(a)),s&&(r=s.getText(),[`object`].includes(r)&&(r=`{}`)),{status:t,contentType:n,body:r}},g=e=>{let t={};if(!e?.isKind(o.TypeLiteral))return t;for(let n of e.getMembers()){if(!n.isKind(o.PropertySignature))continue;let e=n.getName(),r=n.getTypeNodeOrThrow();if(!r.isKind(o.TypeLiteral))continue;let i,a,s={};for(let e of r.getMembers()){if(!e.isKind(o.PropertySignature))continue;let t=ie(e),n=e.getTypeNodeOrThrow();if(t===`contentType`)i=_(n);else if(t===`runtimeValidation`)a=m(n);else if(t.startsWith(`error`)){let e=_(n);e&&(s[t]=e)}}t[e]={contentType:i,runtimeValidation:a,customErrors:s}}return t},_=e=>{let t=e.isKind(o.LiteralType)?e.getFirstChildByKind(o.StringLiteral):void 0;return t?t.getLiteralText():void 0},v=(e,r,i,a)=>{let c=[];if(!i?.isKind(o.TypeLiteral))return c;let l=g(a),u=(t,i)=>[t.replace(/^./,e=>e.toUpperCase()),`T`,r,n(e.id+i)].join(``);for(let n of i.getMembers()){if(!n.isKind(o.PropertySignature))continue;let i=n.getName(),a=n.getTypeNodeOrThrow();if(i===`response`){let n=a.isKind(o.UnionType)?a.getChildrenOfKind(o.TupleType):[a];c.push({...l[i],method:r,target:i,variants:n.flatMap((n,a)=>{let{status:o,contentType:s,body:c}={...h(n)};return o?(s&&typeof s!=`string`&&(console.warn(t([`bold`,`red`],`✗ The second element of a response variant should specify the Response Content Type`)),console.warn(t([`blue`],` Example: [200, "json", Schema]`)),console.warn(` Route: ${e.name}; Method: ${r}; Response Variant: #${a}`),console.warn()),[{id:u(i,JSON.stringify([o,s,c])),status:o,contentType:s,body:c}]):[]})})}else Object.keys(s).includes(i)&&c.push({...l[i],method:r,target:i,schema:{id:u(i),text:a.getText()}})}return c},y=(e,t)=>{let n=[],r=t?.withReferencedFiles?[]:void 0;for(let i of e.getImportDeclarations()){let e=i.getModuleSpecifierValue(),a=/^\.\.?\/?/.test(e)&&t?.relpathResolver?t.relpathResolver(e):e,s=i.isTypeOnly(),c=s?i.getDefaultImport():void 0;if(c){let e=c.getText(),t=`import type ${e} from "${a}";`;n.push({importDeclaration:{name:e,path:a},text:t}),r&&r.push(...b(c))}let l=s?i.getNamespaceImport():void 0;if(l){let e=l.getText(),t=`import type * as ${e} from "${a}";`;n.push({importDeclaration:{name:e,path:a},text:t}),r&&r.push(...b(l))}for(let e of i.getNamedImports())if(e.isTypeOnly()||s){let t=e.getNameNode(),i=t.getText(),s=e.getAliasNode()?.getText(),c=s?`${i} as ${s}`:i;n.push({importDeclaration:{name:i,alias:s,path:a},text:`import type { ${c} } from "${a}";`}),r&&t.isKind(o.Identifier)&&r.push(...b(t))}}for(let t of e.getTypeAliases()){let e=t.getName(),r=t.getFullText().trim();n.push({typeAliasDeclaration:{name:e},text:r})}for(let t of e.getInterfaces()){let e=t.getName(),r=t.getFullText().trim();n.push({interfaceDeclaration:{name:e},text:r})}for(let t of e.getEnums()){let e=t.getName(),r=t.getFullText().trim();n.push({enumDeclaration:{name:e},text:r})}for(let i of e.getExportDeclarations()){let e=i.isTypeOnly(),a=i.getModuleSpecifierValue(),s=a?/^\.\.?\/?/.test(a)&&t?.relpathResolver?t.relpathResolver(a):a:void 0;for(let t of i.getNamedExports())if(t.isTypeOnly()||e){let e=t.getNameNode(),i=e.getText(),a=t.getAliasNode()?.getText(),c=a?`${i} as ${a}`:i;n.push({exportDeclaration:{name:i,alias:a??i,path:s},text:s?`export type { ${c} } from "${s}";`:`export type { ${c} };`}),r&&e.isKind(o.Identifier)&&r.push(...b(e))}}return r?[n,[...new Set(r)]]:[n]},b=e=>(e?.getSymbol()?.getAliasedSymbol()?.getDeclarations()||[]).flatMap(e=>{let t=e.getSourceFile();return t?[t.getFilePath()]:[]}),x=e=>e.getTypeArguments();return{createProject:e,extractDefaultExport:d,extractParamsRefinements:f,extractRouteMethods:p,extractTypeDeclarations:y,resolveRouteSignature:u,typeResolverFactory:t=>{let{createPath:a}=A(t),s=e({compilerOptions:{moduleResolution:i.Bundler,types:[],noLib:!0,skipLibCheck:!0,paths:{[`${c.appPrefix}/*`]:[`${t.root}/*`],[`${c.srcPrefix}/*`]:[a.src(`*`)],[`${c.libPrefix}/*`]:[a.lib(`*`)]}}}),l=e=>{let t=M(P,{resolvedTypes:e}),r=s.createSourceFile(`${n(t)}-${Date.now()}.ts`,t,{overwrite:!0});return e.map(e=>{let t=r.getTypeAlias(e.name)?.getTypeNode();if(!t)return e;let n=[],i=[],a=[];for(let r of e.properties||[]){let e=t.isKind(o.TypeLiteral)?t.getProperty(r.nameDeclaration)?.getTypeNode():void 0;e&&(n.push({...r,typeboxSchema:R(e)}),I(e)&&i.push(r.name),L(e)&&a.push(r.name))}return{...e,properties:n,numericProperties:i,booleanProperties:a,typeboxSchema:R(t)}})};return{getSourceFile:e=>s.getSourceFile(e)||s.addSourceFileAtPath(e),refreshSourceFile:async e=>{let t=s.getSourceFile(e);t&&await t.refreshFromFileSystem()},literalTypesResolver:(e,t)=>{let i=s.createSourceFile(`${n(e)}-${Date.now()}.ts`,e,{overwrite:!0}),a=r(s,i,{...t,stripComments:!0});return l(a)}}}}},ie=e=>{let t=e.getNameNode();return t.isKind(o.StringLiteral)?t.getLiteralValue():t.getText()},I=(e,t=c.refineTypeName)=>{if(e.isKind(o.ParenthesizedType))return I(e.getTypeNode(),t);if(e.isKind(o.TypeReference)){let n=e.getTypeName().getText();return n===t||n===`Array`?I(e.getTypeArguments()[0],t):!1}if(e.isKind(o.ArrayType))return I(e.getElementTypeNode(),t);if(e.isKind(o.TypeOperator))return e.getOperator()===o.ReadonlyKeyword&&I(e.getTypeNode(),t);if(e.isKind(o.UnionType))return e.getTypeNodes().every(e=>I(e,t));if(e.isKind(o.LiteralType)){let t=e.getLiteral();return t.isKind(o.PrefixUnaryExpression)?t.getOperand().isKind(o.NumericLiteral):t.isKind(o.NumericLiteral)}return e.isKind(o.NumberKeyword)},L=e=>{if(e.isKind(o.ParenthesizedType))return L(e.getTypeNode());if(e.isKind(o.TypeReference))return e.getTypeName().getText()===`Array`&&L(e.getTypeArguments()[0]);if(e.isKind(o.ArrayType))return L(e.getElementTypeNode());if(e.isKind(o.TypeOperator))return e.getOperator()===o.ReadonlyKeyword&&L(e.getTypeNode());if(e.isKind(o.LiteralType)){let t=e.getLiteral();return t.isKind(o.TrueKeyword)||t.isKind(o.FalseKeyword)}return e.isKind(o.BooleanKeyword)},R=(e,t=c.refineTypeName)=>{let n=e=>{if(e.isKind(o.TypeReference)&&e.getTypeName().getText()===t){let[t,r]=e.getTypeArguments();return!t||!r?e.getText():`(${n(t)} with ${r.getText()})`}if(e.isKind(o.ArrayType))return`${n(e.getElementTypeNode())}[]`;if(e.isKind(o.TypeLiteral)){let t=[];for(let r of e.getMembers())if(r.isKind(o.PropertySignature)){let e=r.getName(),i=r.hasQuestionToken()?`?`:``,a=r.getTypeNode(),o=a?n(a):`unknown`;t.push(`${e}${i}: ${o}`)}else t.push(r.getText());return`{ ${t.join(`, `)} }`}if(e.isKind(o.TupleType))return`[${e.getElements().map(e=>n(e)).join(`, `)}]`;if(e.isKind(o.ParenthesizedType))return`(${n(e.getTypeNode())})`;if(e.isKind(o.UnionType))return e.getTypeNodes().map(e=>n(e)).join(` | `);if(e.isKind(o.IntersectionType))return e.getTypeNodes().map(e=>n(e)).join(` & `);if(e.isKind(o.TypeReference)){let t=e.getTypeArguments();return t.length===0?e.getText():`${e.getTypeName().getText()}<${t.map(e=>n(e)).join(`, `)}>`}return e.getText()};return te(n(e))},ae=({meta:e,factory:t,dependencies:n,devDependencies:r})=>{let i=(i=>({meta:e,factory:e=>t(e,i),dependencies:n,devDependencies:r,options:i}));return Object.assign(i,{meta:e,dependencies:n,devDependencies:r}),i},oe=e=>e,se=(e,t)=>({kind:n,entry:r})=>e?t.includes(e.kind)&&(n===`apiRoute`||n===`apiUse`)?r.fileFullpath===e.file||r.referencedFiles?.includes(e.file):!1:!0,ce=(e,t)=>({kind:n,entry:r})=>e?t.includes(e.kind)&&(n===`pageRoute`||n===`pageLayout`)?r.fileFullpath===e.file:!1:!0,z=e=>{let t=(e,n,r=!1)=>{let i=[];for(let a of e)switch(a.type){case`text`:a.value!==`/`&&i.push({type:`static`,value:a.value});break;case`param`:i.push({type:`param`,kind:r?`optional`:`required`,name:a.name,const:n(a.name)});break;case`wildcard`:i.push({type:`param`,kind:`splat`,name:a.name,const:n(a.name)});break;case`group`:i.push(...t(a.tokens,n,!0))}return i},r=[e=>e.replace(/\[(\w+)\]/g,`:$1`),e=>e.replace(/\{(\w+)\}/g,`{:$1}`),e=>e.replace(/\{\.\.\./g,`{*`),e=>e.startsWith(`{`)?e.replace(/^\{/,`{/`):e],i=e=>{let t=0;for(let[n,r]of[...e].entries())if(r===`{`)t+=1;else if(r===`}`)--t;else if(r===`:`&&t===0)return e.slice(n+1).match(/^\w+/)?.[0]||`:`},a=e.replace(/^index\/?/,``).split(`/`).flatMap(a=>{if(!a.length)return[];let o=i(a);if(o===`:`)throw Error(`${e} contains colons outside braces, use : only within {}`);if(o)throw Error(`${e} contains bare params, use [${o}] instead of :${o}`);let s=r.reduce((e,t)=>t(e),a),{tokens:c}=D(s.replace(/\{[^}]*\}|(\+)/g,(e,t)=>t?`\\+`:e)),l=t(c,e=>/\W/.test(e)||/^\d/.test(e)?[e.replace(/^\d+|\W/g,`_`),n(a)].join(`_`):e),u=l.length===1&&l[0].type===`static`,d=l.length===1&&l[0].type===`param`;return[{kind:u?`static`:d?`param`:`mixed`,orig:a,pattern:s,parts:l}]});return E(a.map(e=>e.pattern).join(`/`)),a},B=e=>e.map(({pattern:t},n)=>{let r=e[n+1];return!r||r.pattern.includes(`/`)?t:e.slice(n+1).some(e=>e.parts.some(e=>e.type===`static`||e.kind===`required`))?`${t}/`:t}).join(``),V=e=>{let t=e=>e.replace(/[*+?^${}()|[\]\\]/g,`\\$&`),n=e=>e.flatMap(e=>e.type===`text`?e.value===`/`?[]:[t(e.value)]:e.type===`param`?[`[^/]+`]:e.type===`wildcard`?[`.+`]:e.type===`group`?[`(?:${n(e.tokens)})?`]:[]).join(``);return e.flatMap((e,t)=>{if(e.kind===`static`)return[e.parts[0].value];if(e.kind===`param`){let t=e.parts[0];return t.kind===`splat`?[`*`]:t.kind===`optional`?[`:${t.name}?`]:[`:${t.name}`]}let{tokens:r}=D(e.pattern);return[`:_${t}{${n(r)}}`]}).join(`/`)},H=e=>{let t=e=>e.replace(/[-*+?^${}()|[\]\\]/g,`\\$&`),n=e=>e.flatMap(e=>e.type===`text`?e.value===`/`?[]:[t(e.value)]:e.type===`param`?[`:${e.name}`]:e.type===`wildcard`?[`/**:${e.name}?`]:e.type===`group`?[`{${n(e.tokens)}}?`]:[]).join(``);return e.flatMap(e=>{if(e.kind===`static`)return[e.parts[0].value];if(e.kind===`param`){let t=e.parts[0];return t.kind===`splat`?[`**`]:t.kind===`optional`?[`*`]:[`:${t.name}`]}let{tokens:t}=D(e.pattern);return[n(t)]}).join(`/`)},U=`index`,W=`${U}.ts`,le=`use`,G=`use.ts`,K=`index`,q=`${K}.{tsx,vue,svelte,mdx,md}`,J=`layout`,Y=`${J}.{tsx,vue,svelte,mdx}`,X=[`${c.apiDir}/**/${W}`,`${c.apiDir}/**/${G}`,`${c.pagesDir}/**/${q}`,`${c.pagesDir}/**/${Y}`],Z=async e=>{let{createPath:t}=A(e);return T(X,{cwd:t.src(),absolute:!0,onlyFiles:!0,followSymbolicLinks:!1,ignore:[`${c.apiDir}/${W}`,`${c.apiDir}/${G}`,`${c.pagesDir}/${q}`,`${c.pagesDir}/${Y}`]})},Q=(e,t)=>{let[n,r,...i]=S(t.root,e).replace(`${t.root}/${c.srcDir}/`,``).split(`/`);return!r||n!==t.name||i.length<2?!1:w.isMatch(b(r,...i),X)?[r,i.join(`/`)]:!1},$=e=>w.matchBase(e,`**/${W}`),ue=e=>w.matchBase(e,`**/${G}`),de=e=>w.matchBase(e,`**/${q}`),fe=e=>w.matchBase(e,`**/${Y}`),pe=(e,r)=>{let i=Q(e,r);if(!i)return;let[a,o]=i,s=`${o.replace(/\W+/g,`_`)}_${n(o)}`,c=v(o);try{let t=z(v(o));return{id:s,name:c,folder:a,file:o,fileFullpath:e,pathTokens:t,pathPattern:B(t),honoPattern:V(t),h3Pattern:H(t)}}catch(e){console.error(`❗${t(`red`,`ERROR`)}: Failed parsing path for "${t(`cyan`,o)}"`),console.error(e.message);return}},me=e=>{if(e.kind===`static`)return 4;if(e.kind===`mixed`){let t=0,n=0;for(let r of e.parts)r.type===`static`?t+=1:(r.kind===`optional`||r.kind===`splat`)&&(n+=1);return 3+t*.01-n*.005}switch(e.parts[0].kind){case`required`:return 2;case`optional`:return 1;case`splat`:return 0}},he=(e,t)=>{let n=Array.from({length:Math.max(e.pathTokens.length,t.pathTokens.length)},(n,r)=>[e.pathTokens[r],t.pathTokens[r]]);for(let[e,t]of n){if(!e||!t){let n=e??t;return n?.kind===`param`&&n.parts[0].kind===`splat`?e?1:-1:e?-1:1}let n=me(e),r=me(t);if(n!==r)return r-n}return e.pathPattern.localeCompare(t.pathPattern)},ge=(e,r)=>{let{generators:i=[],refineTypeName:a=c.refineTypeName}=e.config,{typeResolverFactory:o,resolveRouteSignature:s}=F(),l=i.some(({meta:e})=>e.resolveTypes),{literalTypesResolver:u,getSourceFile:d,refreshSourceFile:f}=o(e);return{pageLayoutResolver(e){let{name:t}=e;return{name:t,handler:async()=>({kind:`pageLayout`,entry:e})}},pageRouteResolver(e){let{id:t,name:n,folder:r,file:i,fileFullpath:a,pathTokens:o,pathPattern:s,honoPattern:c,h3Pattern:l}=e;return{name:n,handler:async()=>({kind:`pageRoute`,entry:{id:t,name:n,pathTokens:o,pathPattern:s,honoPattern:c,h3Pattern:l,params:{schema:o.flatMap(e=>e.parts.filter(e=>e.type===`param`))},folder:r,file:i,fileFullpath:a}})}},apiUseResolver(e){let{name:t}=e;return{name:t,handler:async()=>({kind:`apiUse`,entry:e})}},apiRouteResolver(i){let{id:o,name:p,file:m,folder:h,fileFullpath:g,pathTokens:_,pathPattern:y,honoPattern:x,h3Pattern:C}=i;return{name:p,handler:async i=>{let w=_.flatMap(e=>e.parts.flatMap(e=>e.type===`param`?[e]:[])),T=!w.length||w.filter(e=>e.kind===`required`).length===0,E=r?r({id:o,file:m,fileFullpath:g},e,{resolveTypes:l}):void 0,D=E?await E.get({validate:!0}):void 0;if(!D){i===g&&await f(g);let{typeDeclarations:t,paramsRefinements:r,methods:h,validationDefinitions:_,referencedFiles:y=[]}=await s({id:o,name:p,fileFullpath:g,optionalParams:T},{withReferencedFiles:!0,sourceFile:d(g),relpathResolver(e){return b(c.srcPrefix,c.apiDir,v(m),e)}}),x=_.flatMap(e=>e.target===`response`?e.variants.flatMap(({id:e,body:t})=>t?[{id:e,text:t}]:[]):[e.schema]),S=A(e).createPath.libApi(v(m),`types.ts`),C={id:[`ParamsT`,n(p)].join(``),schema:w,resolvedType:void 0},O=M(re,{params:C,paramsSchema:w.map((e,t)=>({...e,isRequired:e.kind===`required`,isSplat:e.kind===`splat`,refinement:r?.at(t)})),typeDeclarations:t,validationTypes:x}),k=l?u(O,{stripComments:!0,overrides:{[a]:a},withProperties:[C.id,...x.map(({id:e})=>e)]}):void 0;if(await N(S,k?P:O,{resolvedTypes:k}),C.resolvedType=k?.find(e=>e.name===C.id),D={hash:0,referencedFiles:{},params:C,methods:h,typeDeclarations:t,validationDefinitions:_.map(e=>({...e,...e.target===`response`?{variants:e.variants.map(e=>({...e,resolvedType:k?.find(t=>t.name===e.id)}))}:{schema:{...e.schema,resolvedType:k?.find(t=>t.name===e.schema.id)}}}))},E){let{referencedFiles:e,...t}=D;D=await E.set({...t,referencedFiles:y})}}let k=D.validationDefinitions.flatMap(e=>{let n=e;if(e.target===`response`)n={...e,variants:e.variants.flatMap((n,r)=>{if(typeof n.contentType!=`string`||n.contentType.includes(`/`))return[n];let i=O(n.contentType);return i?[{...n,contentType:i}]:(console.warn(t([`bold`,`red`],`✗ Failed resolving Response Content Type`)),console.warn(` Invalid value provided for mime-types lookup - ${n.contentType}`),console.warn(t([`cyan`],` Response variant #${r} excluded from route schemas`)),console.warn(` Route: ${p}; Method: ${e.method}`),console.warn(),[])})};else if(e.contentType&&!e.contentType.includes(`/`)){let r=O(e.contentType);r?n={...e,contentType:r}:(console.warn(t([`bold`,`red`],`✗ Failed resolving Response Content Type`)),console.warn(` Invalid value provided for mime-types lookup - ${e.contentType}`),console.warn(` Route: ${p}; Method: ${e.method}`),console.warn())}return n?[n]:[]});return{kind:`apiRoute`,entry:{id:o,name:p,pathTokens:_,pathPattern:y,honoPattern:x,h3Pattern:C,params:D.params,optionalParams:T,folder:h,file:m,fileFullpath:g,methods:D.methods,typeDeclarations:D.typeDeclarations,validationDefinitions:k,referencedFiles:Object.keys(D.referencedFiles).map(t=>S(e.root,t))}}}}}}},_e=e=>{let t=structuredClone(e).sort(he),n=(e,i)=>[...new Set(e.map(e=>e.name))].flatMap(e=>{let a=t.flatMap(({fileFullpath:t,...n})=>n.name===e?[n]:[]),o=a.find(e=>_(e.file).startsWith(K)),s=a.find(e=>_(e.file).startsWith(J));return o||s?[{index:o?{...o,pathTokens:o.pathTokens.slice(i?.pathTokens.length||0)}:void 0,layout:s?{...s,pathTokens:s.pathTokens.slice(i?.pathTokens.length||0)}:void 0,parent:i?.name,children:n(r(o||s),o||s)}]:[]}),r=({name:e,pathTokens:n})=>{let r=t.filter(t=>!(t.pathTokens.length<=n.length||!t.name.startsWith(`${e}/`)));return r.filter(e=>!r.some(t=>t===e||t.pathTokens.length<=n.length||t.pathTokens.length>=e.pathTokens.length?!1:e.name.startsWith(`${t.name}/`)))};return n(t.filter(e=>!t.some(t=>t===e||t.pathTokens.length>=e.pathTokens.length?!1:e.name.startsWith(`${t.name}/`))))},ve=async(e,t)=>{let{apiRouteResolver:n,apiUseResolver:r,pageRouteResolver:i,pageLayoutResolver:a}=ge(e,t),o=t=>{let o=new Map,s=t.flatMap(t=>{let n=pe(t,e);return n?[n]:[]});for(let e of s)e.folder===c.apiDir?$(e.file)?o.set(e.fileFullpath,n(e)):ue(e.file)&&o.set(e.fileFullpath,r(e)):e.folder===c.pagesDir&&(de(e.file)?o.set(e.fileFullpath,i(e)):fe(e.file)&&o.set(e.fileFullpath,a(e)));return o};return{resolvers:o(await Z(e)),resolversFactory:o}},ye=e=>{if(!process.stdout.isTTY||process.env.CI||process.env.TERM===`dumb`)return{text(){},append(){},succeed(){},failed(){}};let t=new k().start(e),n=e;return{text(e){n=e,t.text=e},append(e){t.text=`${n} › ${e}`},succeed(e){e?this.append(e):this.text(n),t.succeed()},failed(e){e&&this.text([n,e].join(`
|
|
20
|
-
`)),t.failed()}}},be=e=>{let t=[`.ts`,`.tsx`,`.js`,`.jsx`,`.mjs`,`.vue`,`.svelte`,`.md`,`.mdx`,`.json`],{createPath:n}=A(e),r=[[`${c.srcPrefix}/`,n.src()],[`${c.libPrefix}/`,n.lib()],[`${c.appPrefix}/`,e.root]],i=async(e,n)=>{let r=b(e,n),{ext:i}=x(n);if(t.includes(i)){for(let e of[r,`${r}/index${i}`])if(await Se(e))return e;return r}for(let e of t)for(let t of[`${r}${e}`,`${r}/index${e}`])if(await Se(t))return t;return r};return{name:`kosmo:tsconfigPaths`,enforce:`pre`,async resolveId(e){if(!(y(e)||e.startsWith(`.`))){for(let[t,n]of r)if(e.startsWith(t))return i(n,e.replace(t,``))}}}},xe={tsconfigPaths:be,nodePrefix:()=>({name:`kosmojs:node-prefix`,enforce:`pre`,resolveId(t){return e.includes(t)?{id:`node:${t}`,external:!0}:void 0}})},Se=async e=>h(e).then(e=>e.isFile()).catch(()=>!1);export{U as API_INDEX_BASENAME,W as API_INDEX_PATTERN,le as API_USE_BASENAME,G as API_USE_PATTERN,K as PAGE_INDEX_BASENAME,q as PAGE_INDEX_PATTERN,J as PAGE_LAYOUT_BASENAME,Y as PAGE_LAYOUT_PATTERN,F as astFactory,H as createH3Pattern,V as createHonoPattern,B as createPathPattern,pe as createRouteEntry,se as createWatchedApiRouteEntriesFilter,ce as createWatchedPageRouteEntriesFilter,ae as defineGenerator,oe as defineGeneratorFactory,te as escapeTemplateLiterals,$ as isApiRoute,ue as isApiUse,fe as isPageLayout,de as isPageRoute,Q as isRouteFile,ee as mergeConfigs,_e as nestedRoutesFactory,j as pathExists,A as pathResolver,z as pathTokensFactory,M as render,ne as renderFactory,N as renderToFile,ve as routesFactory,Z as scanRoutes,he as sortRoutes,ye as spinnerFactory,be as tsconfigPaths,xe as vitePlugins};
|
|
21
1509
|
//# sourceMappingURL=index.js.map
|