@kosmojs/lib 0.1.0 → 0.1.1
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 +7 -6
- package/pkg/index.js +1116 -1386
- package/pkg/index.js.map +1 -7
- package/pkg/paths.d.ts +2 -2
package/pkg/index.js
CHANGED
|
@@ -1,1453 +1,1183 @@
|
|
|
1
|
-
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { styleText } from "node:util";
|
|
4
4
|
import crc from "crc/crc32";
|
|
5
5
|
import { flattener } from "tfusion";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "@
|
|
14
|
-
|
|
6
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
7
|
+
import picomatch from "picomatch";
|
|
8
|
+
import { access, constants, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import handlebars from "handlebars";
|
|
10
|
+
import { glob } from "tinyglobby";
|
|
11
|
+
import { parse } from "path-to-regexp";
|
|
12
|
+
import mimeTypes from "mime-types";
|
|
13
|
+
import { Spinner } from "@topcli/spinner";
|
|
14
|
+
//#region ../core/pkg/index.js
|
|
15
|
+
var defaults = {
|
|
16
|
+
appPrefix: "@",
|
|
17
|
+
srcPrefix: "~",
|
|
18
|
+
libPrefix: "_",
|
|
19
|
+
coreDir: "core",
|
|
20
|
+
srcDir: "src",
|
|
21
|
+
libDir: "lib",
|
|
22
|
+
configDir: "config",
|
|
23
|
+
apiDir: "api",
|
|
24
|
+
pagesDir: "pages",
|
|
25
|
+
entryDir: "entry",
|
|
26
|
+
fetchDir: "fetch",
|
|
27
|
+
refineTypeName: "VRefine"
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Request metadata validation targets.
|
|
31
|
+
* */
|
|
32
|
+
var RequestMetadataTargets = {
|
|
33
|
+
query: "URL query parameters",
|
|
34
|
+
headers: "HTTP request headers",
|
|
35
|
+
cookies: "HTTP cookies"
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Request body validation targets.
|
|
39
|
+
*
|
|
40
|
+
* Body formats are mutually exclusive - only one should be specified per handler.
|
|
41
|
+
*
|
|
42
|
+
* **Development behavior:**
|
|
43
|
+
* - If multiple formats are defined, the builder displays a warning and
|
|
44
|
+
* disables validation schemas for the affected handler.
|
|
45
|
+
* - If an unsuitable target is defined (e.g., `json`, `form`)
|
|
46
|
+
* for a method without a body like GET, HEAD), a warning is displayed and
|
|
47
|
+
* validation schemas are disabled for that handler.
|
|
48
|
+
*
|
|
49
|
+
* This ensures misconfigurations are detected during development
|
|
50
|
+
* for runtime to execute without false positive validation failures.
|
|
51
|
+
*
|
|
52
|
+
* Always define exactly one target that is suitable for current handler.
|
|
53
|
+
* */
|
|
54
|
+
var RequestBodyTargets = {
|
|
55
|
+
json: "JSON request body",
|
|
56
|
+
form: "URL-encoded or Multipart form",
|
|
57
|
+
raw: "Raw body format (string/Buffer/ArrayBuffer/Blob)"
|
|
58
|
+
};
|
|
59
|
+
var RequestValidationTargets = {
|
|
60
|
+
...RequestMetadataTargets,
|
|
61
|
+
...RequestBodyTargets
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../core/pkg/api/index.js
|
|
65
|
+
var HTTPMethods = /* @__PURE__ */ function(HTTPMethods) {
|
|
66
|
+
HTTPMethods["HEAD"] = "HEAD";
|
|
67
|
+
HTTPMethods["OPTIONS"] = "OPTIONS";
|
|
68
|
+
HTTPMethods["GET"] = "GET";
|
|
69
|
+
HTTPMethods["PUT"] = "PUT";
|
|
70
|
+
HTTPMethods["PATCH"] = "PATCH";
|
|
71
|
+
HTTPMethods["POST"] = "POST";
|
|
72
|
+
HTTPMethods["DELETE"] = "DELETE";
|
|
73
|
+
return HTTPMethods;
|
|
74
|
+
}({});
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/ast.ts
|
|
15
77
|
var astFactory = () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
338
|
-
const typeOnlyDeclaration = declaration.isTypeOnly();
|
|
339
|
-
const modulePath = declaration.getModuleSpecifierValue();
|
|
340
|
-
const path = modulePath ? /^\.\.?\/?/.test(modulePath) ? opts?.relpathResolver ? opts.relpathResolver(modulePath) : modulePath : modulePath : void 0;
|
|
341
|
-
for (const namedExport of declaration.getNamedExports()) {
|
|
342
|
-
if (namedExport.isTypeOnly() || typeOnlyDeclaration) {
|
|
343
|
-
const nameNode = namedExport.getNameNode();
|
|
344
|
-
const name = nameNode.getText();
|
|
345
|
-
const alias = namedExport.getAliasNode()?.getText();
|
|
346
|
-
const nameText = alias ? `${name} as ${alias}` : name;
|
|
347
|
-
declarations.push({
|
|
348
|
-
exportDeclaration: {
|
|
349
|
-
name,
|
|
350
|
-
alias: alias ?? name,
|
|
351
|
-
path
|
|
352
|
-
},
|
|
353
|
-
text: path ? `export type { ${nameText} } from "${path}";` : `export type { ${nameText} };`
|
|
354
|
-
});
|
|
355
|
-
if (referencedFiles) {
|
|
356
|
-
if (nameNode.isKind(SyntaxKind.Identifier)) {
|
|
357
|
-
referencedFiles.push(...getReferencedFiles(nameNode));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return referencedFiles ? [declarations, [...new Set(referencedFiles)]] : [declarations];
|
|
364
|
-
};
|
|
365
|
-
const getReferencedFiles = (importIdentifier) => {
|
|
366
|
-
const declarations = importIdentifier?.getSymbol()?.getAliasedSymbol()?.getDeclarations() || [];
|
|
367
|
-
return declarations.flatMap((e) => {
|
|
368
|
-
const sourceFile = e.getSourceFile();
|
|
369
|
-
return sourceFile ? [sourceFile.getFilePath()] : [];
|
|
370
|
-
});
|
|
371
|
-
};
|
|
372
|
-
const extractGenerics = (callExpression) => {
|
|
373
|
-
return callExpression.getTypeArguments();
|
|
374
|
-
};
|
|
375
|
-
const typeResolverFactory = ({ root, name }) => {
|
|
376
|
-
const project = createProject({
|
|
377
|
-
tsConfigFilePath: resolve(
|
|
378
|
-
root,
|
|
379
|
-
join(defaults.srcDir, name, "tsconfig.json")
|
|
380
|
-
),
|
|
381
|
-
skipAddingFilesFromTsConfig: true
|
|
382
|
-
});
|
|
383
|
-
const literalTypesResolver = (literalTypes, options) => {
|
|
384
|
-
const sourceFile = project.createSourceFile(
|
|
385
|
-
`${crc(literalTypes)}-${Date.now()}.ts`,
|
|
386
|
-
literalTypes,
|
|
387
|
-
{ overwrite: true }
|
|
388
|
-
);
|
|
389
|
-
const resolvedTypes = flattener(project, sourceFile, {
|
|
390
|
-
...options,
|
|
391
|
-
stripComments: true
|
|
392
|
-
});
|
|
393
|
-
project.removeSourceFile(sourceFile);
|
|
394
|
-
return resolvedTypes;
|
|
395
|
-
};
|
|
396
|
-
return {
|
|
397
|
-
getSourceFile: (fileFullpath) => {
|
|
398
|
-
return project.getSourceFile(fileFullpath) || project.addSourceFileAtPath(fileFullpath);
|
|
399
|
-
},
|
|
400
|
-
refreshSourceFile: async (fileFullpath) => {
|
|
401
|
-
const sourceFile = project.getSourceFile(fileFullpath);
|
|
402
|
-
if (sourceFile) {
|
|
403
|
-
await sourceFile.refreshFromFileSystem();
|
|
404
|
-
}
|
|
405
|
-
},
|
|
406
|
-
literalTypesResolver
|
|
407
|
-
};
|
|
408
|
-
};
|
|
409
|
-
return {
|
|
410
|
-
createProject,
|
|
411
|
-
extractDefaultExport,
|
|
412
|
-
extractParamsRefinements,
|
|
413
|
-
extractRouteMethods,
|
|
414
|
-
extractTypeDeclarations,
|
|
415
|
-
resolveRouteSignature,
|
|
416
|
-
typeResolverFactory
|
|
417
|
-
};
|
|
78
|
+
const createProject = (opts) => new Project(opts);
|
|
79
|
+
const resolveRouteSignature = async (route, opts) => {
|
|
80
|
+
const { sourceFile = createProject().addSourceFileAtPath(route.fileFullpath) } = { ...opts };
|
|
81
|
+
const [typeDeclarations, referencedFiles] = extractTypeDeclarations(sourceFile, opts);
|
|
82
|
+
const defaultExport = extractDefaultExport(sourceFile);
|
|
83
|
+
const paramsRefinements = defaultExport ? extractParamsRefinements(defaultExport) : void 0;
|
|
84
|
+
const methods = defaultExport ? extractRouteMethods(route, defaultExport) : [];
|
|
85
|
+
return {
|
|
86
|
+
typeDeclarations,
|
|
87
|
+
paramsRefinements,
|
|
88
|
+
methods: methods.map((e) => e.method),
|
|
89
|
+
validationDefinitions: methods.flatMap((e) => e.validationDefinitions),
|
|
90
|
+
referencedFiles
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const extractDefaultExport = (sourceFile) => {
|
|
94
|
+
const [defaultExport] = sourceFile.getExportAssignments().flatMap((exportAssignment) => {
|
|
95
|
+
if (exportAssignment.isExportEquals()) return [];
|
|
96
|
+
const callExpression = exportAssignment.getExpression();
|
|
97
|
+
return callExpression.isKind(SyntaxKind.CallExpression) ? [callExpression] : [];
|
|
98
|
+
});
|
|
99
|
+
return defaultExport;
|
|
100
|
+
};
|
|
101
|
+
const extractParamsRefinements = (callExpression) => {
|
|
102
|
+
const [_routeName, paramsGeneric] = extractGenerics(callExpression);
|
|
103
|
+
if (!paramsGeneric?.isKind(SyntaxKind.TupleType)) return;
|
|
104
|
+
return paramsGeneric.getElements().map((node, index) => {
|
|
105
|
+
return {
|
|
106
|
+
index,
|
|
107
|
+
text: node.getText()
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
const extractRouteMethods = (route, callExpression) => {
|
|
112
|
+
const funcDeclaration = callExpression.getFirstChildByKind(SyntaxKind.ArrowFunction) || callExpression.getFirstChildByKind(SyntaxKind.FunctionExpression);
|
|
113
|
+
if (!funcDeclaration) return [];
|
|
114
|
+
const arrayLiteralExpression = funcDeclaration.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
|
|
115
|
+
if (!arrayLiteralExpression) return [];
|
|
116
|
+
const callExpressions = [];
|
|
117
|
+
for (const e of arrayLiteralExpression.getChildrenOfKind(SyntaxKind.CallExpression)) {
|
|
118
|
+
const name = e.getExpression().getText();
|
|
119
|
+
if (HTTPMethods[name]) callExpressions.push([e, name]);
|
|
120
|
+
}
|
|
121
|
+
const methods = [];
|
|
122
|
+
for (const [callExpression, method] of callExpressions) {
|
|
123
|
+
const [vDefs, vOpts] = extractGenerics(callExpression);
|
|
124
|
+
methods.push({
|
|
125
|
+
method,
|
|
126
|
+
validationDefinitions: extractValidationDefinitions(route, method, vDefs, vOpts)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return methods;
|
|
130
|
+
};
|
|
131
|
+
/** Parse a boolean literal type node */
|
|
132
|
+
const parseRuntimeValidation = (typeNode) => {
|
|
133
|
+
if (typeNode.isKind(SyntaxKind.LiteralType)) {
|
|
134
|
+
const literal = typeNode.getFirstChild();
|
|
135
|
+
if (literal?.isKind(SyntaxKind.TrueKeyword)) return true;
|
|
136
|
+
else if (literal?.isKind(SyntaxKind.FalseKeyword)) return false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const extractResponseVariant = (typeNode) => {
|
|
140
|
+
if (!typeNode.isKind(SyntaxKind.TupleType)) return;
|
|
141
|
+
let status = 200;
|
|
142
|
+
let contentType;
|
|
143
|
+
let body;
|
|
144
|
+
const [statusNode, contentTypeNode, bodyNode] = typeNode.getElements();
|
|
145
|
+
if (statusNode?.isKind(SyntaxKind.LiteralType)) {
|
|
146
|
+
const literal = statusNode.getFirstChildByKind(SyntaxKind.NumericLiteral);
|
|
147
|
+
if (literal) status = Number(literal.getText());
|
|
148
|
+
}
|
|
149
|
+
if (contentTypeNode) contentType = extractStringLiteral(contentTypeNode);
|
|
150
|
+
if (bodyNode) {
|
|
151
|
+
body = bodyNode.getText();
|
|
152
|
+
if (["object"].includes(body)) body = "{}";
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
status,
|
|
156
|
+
contentType,
|
|
157
|
+
body
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** Parse opts TypeLiteral into a map keyed by target */
|
|
161
|
+
const parseValidationOptions = (typeNode) => {
|
|
162
|
+
const opts = {};
|
|
163
|
+
if (!typeNode?.isKind(SyntaxKind.TypeLiteral)) return opts;
|
|
164
|
+
for (const prop of typeNode.getMembers()) {
|
|
165
|
+
if (!prop.isKind(SyntaxKind.PropertySignature)) continue;
|
|
166
|
+
const target = prop.getName();
|
|
167
|
+
const typeNode = prop.getTypeNodeOrThrow();
|
|
168
|
+
if (!typeNode.isKind(SyntaxKind.TypeLiteral)) continue;
|
|
169
|
+
let contentType;
|
|
170
|
+
let runtimeValidation;
|
|
171
|
+
const customErrors = {};
|
|
172
|
+
for (const member of typeNode.getMembers()) {
|
|
173
|
+
if (!member.isKind(SyntaxKind.PropertySignature)) continue;
|
|
174
|
+
const nameNode = member.getNameNode();
|
|
175
|
+
const valueNode = member.getTypeNodeOrThrow();
|
|
176
|
+
const name = nameNode.isKind(SyntaxKind.StringLiteral) ? nameNode.getLiteralText() : nameNode.getText();
|
|
177
|
+
if (name === "contentType") contentType = extractStringLiteral(valueNode);
|
|
178
|
+
else if (name === "runtimeValidation") runtimeValidation = parseRuntimeValidation(valueNode);
|
|
179
|
+
else if (name.startsWith("error")) {
|
|
180
|
+
const literal = extractStringLiteral(valueNode);
|
|
181
|
+
if (literal) customErrors[name] = literal;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
opts[target] = {
|
|
185
|
+
contentType,
|
|
186
|
+
runtimeValidation,
|
|
187
|
+
customErrors
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return opts;
|
|
191
|
+
};
|
|
192
|
+
const extractStringLiteral = (typeNode) => {
|
|
193
|
+
const literal = typeNode.isKind(SyntaxKind.LiteralType) ? typeNode.getFirstChildByKind(SyntaxKind.StringLiteral) : void 0;
|
|
194
|
+
return literal ? literal.getLiteralText() : void 0;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Extract validation definitions from route handler generics.
|
|
198
|
+
* Merges defs (schemas) and opts (validation options) into a flat array.
|
|
199
|
+
* */
|
|
200
|
+
const extractValidationDefinitions = (route, method, defsNode, optsNode) => {
|
|
201
|
+
const definitions = [];
|
|
202
|
+
if (!defsNode?.isKind(SyntaxKind.TypeLiteral)) return definitions;
|
|
203
|
+
const optsMap = parseValidationOptions(optsNode);
|
|
204
|
+
const createId = (target, hash) => {
|
|
205
|
+
return [
|
|
206
|
+
target.replace(/^./, (c) => c.toUpperCase()),
|
|
207
|
+
"T",
|
|
208
|
+
method,
|
|
209
|
+
crc(route.id + hash)
|
|
210
|
+
].join("");
|
|
211
|
+
};
|
|
212
|
+
for (const prop of defsNode.getMembers()) {
|
|
213
|
+
if (!prop.isKind(SyntaxKind.PropertySignature)) continue;
|
|
214
|
+
const target = prop.getName();
|
|
215
|
+
const typeNode = prop.getTypeNodeOrThrow();
|
|
216
|
+
if (target === "response") {
|
|
217
|
+
const variants = typeNode.isKind(SyntaxKind.UnionType) ? typeNode.getChildrenOfKind(SyntaxKind.TupleType) : [typeNode];
|
|
218
|
+
definitions.push({
|
|
219
|
+
...optsMap[target],
|
|
220
|
+
method,
|
|
221
|
+
target,
|
|
222
|
+
variants: variants.flatMap((e, i) => {
|
|
223
|
+
const { status, contentType, body } = extractResponseVariant(e) || {};
|
|
224
|
+
if (!status) return [];
|
|
225
|
+
if (contentType && typeof contentType !== "string") {
|
|
226
|
+
console.warn(styleText(["bold", "red"], `✗ The second element of a response variant should specify the Response Content Type`));
|
|
227
|
+
console.warn(styleText(["blue"], ` Example: [200, "json", Schema]`));
|
|
228
|
+
console.warn(` Route: ${route.name}; Method: ${method}; Response Variant: #${i}`);
|
|
229
|
+
console.warn();
|
|
230
|
+
}
|
|
231
|
+
return [{
|
|
232
|
+
id: createId(target, JSON.stringify([
|
|
233
|
+
status,
|
|
234
|
+
contentType,
|
|
235
|
+
body
|
|
236
|
+
])),
|
|
237
|
+
status,
|
|
238
|
+
contentType,
|
|
239
|
+
body
|
|
240
|
+
}];
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
} else if (Object.keys(RequestValidationTargets).includes(target)) definitions.push({
|
|
244
|
+
...optsMap[target],
|
|
245
|
+
method,
|
|
246
|
+
target,
|
|
247
|
+
schema: {
|
|
248
|
+
id: createId(target),
|
|
249
|
+
text: typeNode.getText()
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return definitions;
|
|
254
|
+
};
|
|
255
|
+
const extractTypeDeclarations = (sourceFile, opts) => {
|
|
256
|
+
const declarations = [];
|
|
257
|
+
const referencedFiles = opts?.withReferencedFiles ? [] : void 0;
|
|
258
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
259
|
+
const modulePath = declaration.getModuleSpecifierValue();
|
|
260
|
+
const path = /^\.\.?\/?/.test(modulePath) ? opts?.relpathResolver ? opts.relpathResolver(modulePath) : modulePath : modulePath;
|
|
261
|
+
const typeOnlyDeclaration = declaration.isTypeOnly();
|
|
262
|
+
const defaultImport = typeOnlyDeclaration ? declaration.getDefaultImport() : void 0;
|
|
263
|
+
if (defaultImport) {
|
|
264
|
+
const name = defaultImport.getText();
|
|
265
|
+
const text = `import type ${name} from "${path}";`;
|
|
266
|
+
declarations.push({
|
|
267
|
+
importDeclaration: {
|
|
268
|
+
name,
|
|
269
|
+
path
|
|
270
|
+
},
|
|
271
|
+
text
|
|
272
|
+
});
|
|
273
|
+
if (referencedFiles) referencedFiles.push(...getReferencedFiles(defaultImport));
|
|
274
|
+
}
|
|
275
|
+
const namespaceImport = typeOnlyDeclaration ? declaration.getNamespaceImport() : void 0;
|
|
276
|
+
if (namespaceImport) {
|
|
277
|
+
const name = namespaceImport.getText();
|
|
278
|
+
const text = `import type * as ${name} from "${path}";`;
|
|
279
|
+
declarations.push({
|
|
280
|
+
importDeclaration: {
|
|
281
|
+
name,
|
|
282
|
+
path
|
|
283
|
+
},
|
|
284
|
+
text
|
|
285
|
+
});
|
|
286
|
+
if (referencedFiles) referencedFiles.push(...getReferencedFiles(namespaceImport));
|
|
287
|
+
}
|
|
288
|
+
for (const namedImport of declaration.getNamedImports()) if (namedImport.isTypeOnly() || typeOnlyDeclaration) {
|
|
289
|
+
const nameNode = namedImport.getNameNode();
|
|
290
|
+
const name = nameNode.getText();
|
|
291
|
+
const alias = namedImport.getAliasNode()?.getText();
|
|
292
|
+
const nameText = alias ? `${name} as ${alias}` : name;
|
|
293
|
+
declarations.push({
|
|
294
|
+
importDeclaration: {
|
|
295
|
+
name,
|
|
296
|
+
alias,
|
|
297
|
+
path
|
|
298
|
+
},
|
|
299
|
+
text: `import type { ${nameText} } from "${path}";`
|
|
300
|
+
});
|
|
301
|
+
if (referencedFiles) {
|
|
302
|
+
if (nameNode.isKind(SyntaxKind.Identifier)) referencedFiles.push(...getReferencedFiles(nameNode));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const declaration of sourceFile.getTypeAliases()) {
|
|
307
|
+
const name = declaration.getName();
|
|
308
|
+
const text = declaration.getFullText().trim();
|
|
309
|
+
declarations.push({
|
|
310
|
+
typeAliasDeclaration: { name },
|
|
311
|
+
text
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
for (const declaration of sourceFile.getInterfaces()) {
|
|
315
|
+
const name = declaration.getName();
|
|
316
|
+
const text = declaration.getFullText().trim();
|
|
317
|
+
declarations.push({
|
|
318
|
+
interfaceDeclaration: { name },
|
|
319
|
+
text
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
for (const declaration of sourceFile.getEnums()) {
|
|
323
|
+
const name = declaration.getName();
|
|
324
|
+
const text = declaration.getFullText().trim();
|
|
325
|
+
declarations.push({
|
|
326
|
+
enumDeclaration: { name },
|
|
327
|
+
text
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
331
|
+
const typeOnlyDeclaration = declaration.isTypeOnly();
|
|
332
|
+
const modulePath = declaration.getModuleSpecifierValue();
|
|
333
|
+
const path = modulePath ? /^\.\.?\/?/.test(modulePath) ? opts?.relpathResolver ? opts.relpathResolver(modulePath) : modulePath : modulePath : void 0;
|
|
334
|
+
for (const namedExport of declaration.getNamedExports()) if (namedExport.isTypeOnly() || typeOnlyDeclaration) {
|
|
335
|
+
const nameNode = namedExport.getNameNode();
|
|
336
|
+
const name = nameNode.getText();
|
|
337
|
+
const alias = namedExport.getAliasNode()?.getText();
|
|
338
|
+
const nameText = alias ? `${name} as ${alias}` : name;
|
|
339
|
+
declarations.push({
|
|
340
|
+
exportDeclaration: {
|
|
341
|
+
name,
|
|
342
|
+
alias: alias ?? name,
|
|
343
|
+
path
|
|
344
|
+
},
|
|
345
|
+
text: path ? `export type { ${nameText} } from "${path}";` : `export type { ${nameText} };`
|
|
346
|
+
});
|
|
347
|
+
if (referencedFiles) {
|
|
348
|
+
if (nameNode.isKind(SyntaxKind.Identifier)) referencedFiles.push(...getReferencedFiles(nameNode));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return referencedFiles ? [declarations, [...new Set(referencedFiles)]] : [declarations];
|
|
353
|
+
};
|
|
354
|
+
const getReferencedFiles = (importIdentifier) => {
|
|
355
|
+
return (importIdentifier?.getSymbol()?.getAliasedSymbol()?.getDeclarations() || []).flatMap((e) => {
|
|
356
|
+
const sourceFile = e.getSourceFile();
|
|
357
|
+
return sourceFile ? [sourceFile.getFilePath()] : [];
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
const extractGenerics = (callExpression) => {
|
|
361
|
+
return callExpression.getTypeArguments();
|
|
362
|
+
};
|
|
363
|
+
const typeResolverFactory = ({ root, name }) => {
|
|
364
|
+
const project = createProject({
|
|
365
|
+
tsConfigFilePath: resolve(root, join(defaults.srcDir, name, "tsconfig.json")),
|
|
366
|
+
skipAddingFilesFromTsConfig: true
|
|
367
|
+
});
|
|
368
|
+
const literalTypesResolver = (literalTypes, options) => {
|
|
369
|
+
const sourceFile = project.createSourceFile(`${crc(literalTypes)}-${Date.now()}.ts`, literalTypes, { overwrite: true });
|
|
370
|
+
const resolvedTypes = flattener(project, sourceFile, {
|
|
371
|
+
...options,
|
|
372
|
+
stripComments: true
|
|
373
|
+
});
|
|
374
|
+
project.removeSourceFile(sourceFile);
|
|
375
|
+
return resolvedTypes;
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
getSourceFile: (fileFullpath) => {
|
|
379
|
+
return project.getSourceFile(fileFullpath) || project.addSourceFileAtPath(fileFullpath);
|
|
380
|
+
},
|
|
381
|
+
refreshSourceFile: async (fileFullpath) => {
|
|
382
|
+
const sourceFile = project.getSourceFile(fileFullpath);
|
|
383
|
+
if (sourceFile) await sourceFile.refreshFromFileSystem();
|
|
384
|
+
},
|
|
385
|
+
literalTypesResolver
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
createProject,
|
|
390
|
+
extractDefaultExport,
|
|
391
|
+
extractParamsRefinements,
|
|
392
|
+
extractRouteMethods,
|
|
393
|
+
extractTypeDeclarations,
|
|
394
|
+
resolveRouteSignature,
|
|
395
|
+
typeResolverFactory
|
|
396
|
+
};
|
|
418
397
|
};
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
import picomatch from "picomatch";
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/generators.ts
|
|
422
400
|
var defineGenerator = (f) => f;
|
|
423
401
|
var defineGeneratorFactory = (f) => f;
|
|
424
402
|
var createTemplateResolver = (customTemplates, defaultTemplate) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return defaultTemplate;
|
|
432
|
-
}
|
|
433
|
-
return typeof match[1] === "function" ? match[1](entry) : match[1];
|
|
434
|
-
};
|
|
403
|
+
const templates = Object.entries({ ...customTemplates }).map(([pattern, template]) => [picomatch(pattern), template]);
|
|
404
|
+
return (pattern, entry) => {
|
|
405
|
+
const match = templates.find(([isMatch]) => isMatch(pattern));
|
|
406
|
+
if (!match) return defaultTemplate;
|
|
407
|
+
return typeof match[1] === "function" ? match[1](entry) : match[1];
|
|
408
|
+
};
|
|
435
409
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
import { access, constants } from "node:fs/promises";
|
|
439
|
-
import { join as join2, resolve as resolve2 } from "node:path";
|
|
440
|
-
import { defaults as defaults2 } from "@kosmojs/core";
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/paths.ts
|
|
441
412
|
var pathResolver = (sourceFolder) => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (!["src", "lib"].includes(o?.origin)) {
|
|
510
|
-
throw new Error(
|
|
511
|
-
`createImportHelpers: required exactly one argument of shape { origin: "src|lib" }`
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
return {
|
|
515
|
-
createImport(key, ...a) {
|
|
516
|
-
return createImport[key](a.slice(0, -1), o);
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
};
|
|
413
|
+
const createPath = (...a) => {
|
|
414
|
+
return sourceFolder.root ? resolve(sourceFolder.root, join(...a)) : join(...a);
|
|
415
|
+
};
|
|
416
|
+
const createImport = {
|
|
417
|
+
src(a, { origin }) {
|
|
418
|
+
return origin === "src" ? join(defaults.srcPrefix, ...a) : join(defaults.appPrefix, defaults.srcDir, sourceFolder.name, ...a);
|
|
419
|
+
},
|
|
420
|
+
api(a, o) {
|
|
421
|
+
return this.src([defaults.apiDir, ...a], o);
|
|
422
|
+
},
|
|
423
|
+
pages(a, o) {
|
|
424
|
+
return this.src([defaults.pagesDir, ...a], o);
|
|
425
|
+
},
|
|
426
|
+
lib(a, { origin }) {
|
|
427
|
+
return origin === "src" ? join(defaults.libPrefix, ...a) : join(defaults.appPrefix, defaults.libDir, sourceFolder.name, ...a);
|
|
428
|
+
},
|
|
429
|
+
libCore(a, o) {
|
|
430
|
+
return this.lib(["core", ...a], o);
|
|
431
|
+
},
|
|
432
|
+
libApi(a, o) {
|
|
433
|
+
return this.lib([defaults.apiDir, ...a], o);
|
|
434
|
+
},
|
|
435
|
+
libEntry(a, o) {
|
|
436
|
+
return this.lib([defaults.entryDir, ...a], o);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
createPath: {
|
|
441
|
+
src(...a) {
|
|
442
|
+
return createPath(defaults.srcDir, sourceFolder.name, ...a);
|
|
443
|
+
},
|
|
444
|
+
api(...a) {
|
|
445
|
+
return this.src(defaults.apiDir, ...a);
|
|
446
|
+
},
|
|
447
|
+
pages(...a) {
|
|
448
|
+
return this.src(defaults.pagesDir, ...a);
|
|
449
|
+
},
|
|
450
|
+
entry(...a) {
|
|
451
|
+
return this.src(defaults.entryDir, ...a);
|
|
452
|
+
},
|
|
453
|
+
lib(...a) {
|
|
454
|
+
return createPath(defaults.libDir, sourceFolder.name, ...a);
|
|
455
|
+
},
|
|
456
|
+
libCore(...a) {
|
|
457
|
+
return this.lib("core", ...a);
|
|
458
|
+
},
|
|
459
|
+
libApi(...a) {
|
|
460
|
+
return this.lib(defaults.apiDir, ...a);
|
|
461
|
+
},
|
|
462
|
+
libEntry(...a) {
|
|
463
|
+
return this.lib(defaults.entryDir, ...a);
|
|
464
|
+
},
|
|
465
|
+
libPages(...a) {
|
|
466
|
+
return this.lib(defaults.pagesDir, ...a);
|
|
467
|
+
},
|
|
468
|
+
distDir(...a) {
|
|
469
|
+
return createPath(sourceFolder.distDir, sourceFolder.name, ...a);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
createImport,
|
|
473
|
+
createImportHelpers(o) {
|
|
474
|
+
if (!["src", "lib"].includes(o?.origin)) throw new Error(`createImportHelpers: required exactly one argument of shape { origin: "src|lib" }`);
|
|
475
|
+
return { createImport(key, ...a) {
|
|
476
|
+
return createImport[key](a.slice(0, -1), o);
|
|
477
|
+
} };
|
|
478
|
+
}
|
|
479
|
+
};
|
|
521
480
|
};
|
|
522
481
|
var pathExists = async (path) => {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
482
|
+
try {
|
|
483
|
+
await access(path, constants.F_OK);
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
529
488
|
};
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
533
|
-
import { dirname, join as join3 } from "node:path";
|
|
534
|
-
import crc2 from "crc/crc32";
|
|
535
|
-
import handlebars from "handlebars";
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/render.ts
|
|
536
491
|
var render = (template, context, options) => {
|
|
537
|
-
|
|
538
|
-
|
|
492
|
+
const { noEscape = true, renderer = handlebars } = { ...options };
|
|
493
|
+
return renderer.compile(template, { noEscape })(context);
|
|
539
494
|
};
|
|
540
495
|
var renderToFile = async (file, template, context, options) => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
await mkdir(dirname(file), { recursive: true });
|
|
556
|
-
await writeFile(file, content, "utf8");
|
|
496
|
+
const content = render(template, context, options);
|
|
497
|
+
/**
|
|
498
|
+
* Two fs calls (exists + read) are worth it to avoid touching the file
|
|
499
|
+
* and triggering watchers unnecessarily.
|
|
500
|
+
* */
|
|
501
|
+
if (await pathExists(file)) {
|
|
502
|
+
const { overwrite = true } = { ...options };
|
|
503
|
+
if (overwrite === false) return;
|
|
504
|
+
const fileContent = await readFile(file, "utf8");
|
|
505
|
+
if (typeof overwrite === "function" && !overwrite(fileContent)) return;
|
|
506
|
+
if (crc(content) === crc(fileContent)) return;
|
|
507
|
+
}
|
|
508
|
+
await mkdir(dirname(file), { recursive: true });
|
|
509
|
+
await writeFile(file, content, "utf8");
|
|
557
510
|
};
|
|
558
511
|
var renderFactory = (options) => {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
renderer: createRenderer(selfOoptions)
|
|
588
|
-
}
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
};
|
|
512
|
+
const createRenderer = (selfOoptions) => {
|
|
513
|
+
const renderer = handlebars.create();
|
|
514
|
+
renderer.registerPartial({
|
|
515
|
+
...options?.partials,
|
|
516
|
+
...selfOoptions?.partials
|
|
517
|
+
});
|
|
518
|
+
renderer.registerHelper({
|
|
519
|
+
...options?.helpers,
|
|
520
|
+
...selfOoptions?.helpers
|
|
521
|
+
});
|
|
522
|
+
return renderer;
|
|
523
|
+
};
|
|
524
|
+
return {
|
|
525
|
+
render(template, context, selfOoptions) {
|
|
526
|
+
return render(template, context, {
|
|
527
|
+
...options,
|
|
528
|
+
...selfOoptions,
|
|
529
|
+
renderer: createRenderer(selfOoptions)
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
async renderToFile(file, template, context, selfOoptions) {
|
|
533
|
+
return renderToFile(options?.outdir ? join(options.outdir, file) : file, template, context, {
|
|
534
|
+
...options,
|
|
535
|
+
...selfOoptions,
|
|
536
|
+
renderer: createRenderer(selfOoptions)
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
};
|
|
592
540
|
};
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
return;
|
|
685
|
-
};
|
|
686
|
-
const tokens = path.replace(/^index\/?/, "").split("/").flatMap((orig) => {
|
|
687
|
-
if (!orig.length) {
|
|
688
|
-
return [];
|
|
689
|
-
}
|
|
690
|
-
const bareParam = detectBareParams(orig);
|
|
691
|
-
if (bareParam === ":") {
|
|
692
|
-
throw new Error(
|
|
693
|
-
`${path} contains colons outside braces, use : only within {}`
|
|
694
|
-
);
|
|
695
|
-
} else if (bareParam) {
|
|
696
|
-
throw new Error(
|
|
697
|
-
`${path} contains bare params, use [${bareParam}] instead of :${bareParam}`
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
const pattern = patternTransforms.reduce((src, fn) => fn(src), orig);
|
|
701
|
-
const { tokens: tokens2 } = parse(
|
|
702
|
-
pattern.replace(/\{[^}]*\}|(\+)/g, (m, p1) => p1 ? "\\+" : m)
|
|
703
|
-
);
|
|
704
|
-
const parts = extractParts(tokens2, (val) => {
|
|
705
|
-
return /\W/.test(val) || /^\d/.test(val) ? [val.replace(/^\d+|\W/g, "_"), crc3(orig)].join("_") : val;
|
|
706
|
-
});
|
|
707
|
-
const isStatic = parts.length === 1 ? parts[0].type === "static" : false;
|
|
708
|
-
const isParam = parts.length === 1 ? parts[0].type === "param" : false;
|
|
709
|
-
const kind = isStatic ? "static" : isParam ? "param" : "mixed";
|
|
710
|
-
return [
|
|
711
|
-
{
|
|
712
|
-
kind,
|
|
713
|
-
orig,
|
|
714
|
-
pattern,
|
|
715
|
-
parts
|
|
716
|
-
}
|
|
717
|
-
];
|
|
718
|
-
});
|
|
719
|
-
return tokens;
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/routes/paths.ts
|
|
543
|
+
/**
|
|
544
|
+
* Parse a filesystem route path into structured PathToken array.
|
|
545
|
+
*
|
|
546
|
+
* Uses path-to-regexp v8 AST for parsing, with directory-friendly syntax:
|
|
547
|
+
* - [param] => required param - :param
|
|
548
|
+
* - {param} => optional param - {:param}
|
|
549
|
+
* - {...param} => splat - {*param}
|
|
550
|
+
*
|
|
551
|
+
* Direct :param syntax is prohibited outside {} to avoid ambiguity.
|
|
552
|
+
* Inside {} it is treated as path-to-regexp power syntax and used as-is.
|
|
553
|
+
*
|
|
554
|
+
* Examples:
|
|
555
|
+
* Required: [id], [name]
|
|
556
|
+
* Optional: {name}, {format}
|
|
557
|
+
* Splat: {...path}
|
|
558
|
+
* Mixed segments: shop/[id]-{name}
|
|
559
|
+
* Power syntax: {-v:version{-:pre}}, :name{@:version{.:min}}.js
|
|
560
|
+
* */
|
|
561
|
+
var pathTokensFactory = (path, { transformStaticValue = normalizeStaticValue } = {}) => {
|
|
562
|
+
/**
|
|
563
|
+
* Recursively extract parts from path-to-regexp AST tokens.
|
|
564
|
+
* A param inside a group is optional; top-level params are required.
|
|
565
|
+
* Wildcard tokens are always splat.
|
|
566
|
+
* Slash-only text nodes (restored for parsing) are skipped.
|
|
567
|
+
* */
|
|
568
|
+
const extractParts = (tokens, createConst, insideGroup = false) => {
|
|
569
|
+
const parts = [];
|
|
570
|
+
for (const token of tokens) switch (token.type) {
|
|
571
|
+
case "text":
|
|
572
|
+
if (token.value !== "/") parts.push({
|
|
573
|
+
type: "static",
|
|
574
|
+
value: transformStaticValue(token.value)
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
case "param":
|
|
578
|
+
parts.push({
|
|
579
|
+
type: "param",
|
|
580
|
+
kind: insideGroup ? "optional" : "required",
|
|
581
|
+
name: token.name,
|
|
582
|
+
const: createConst(token.name)
|
|
583
|
+
});
|
|
584
|
+
break;
|
|
585
|
+
case "wildcard":
|
|
586
|
+
parts.push({
|
|
587
|
+
type: "param",
|
|
588
|
+
kind: "splat",
|
|
589
|
+
name: token.name,
|
|
590
|
+
const: createConst(token.name)
|
|
591
|
+
});
|
|
592
|
+
break;
|
|
593
|
+
case "group":
|
|
594
|
+
parts.push(...extractParts(token.tokens, createConst, true));
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
return parts;
|
|
598
|
+
};
|
|
599
|
+
const patternTransforms = [
|
|
600
|
+
(s) => s.replace(/\[(\w+)\]/g, ":$1"),
|
|
601
|
+
(s) => s.replace(/\{(\w+)\}/g, "{:$1}"),
|
|
602
|
+
(s) => s.replace(/\{\.\.\./g, "{*"),
|
|
603
|
+
(s) => {
|
|
604
|
+
return s.startsWith("{") ? s.replace(/^\{/, "{/") : s;
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
const detectBareParams = (s) => {
|
|
608
|
+
let depth = 0;
|
|
609
|
+
for (const [i, ch] of [...s].entries()) if (ch === "{") depth += 1;
|
|
610
|
+
else if (ch === "}") depth -= 1;
|
|
611
|
+
else if (ch === ":" && depth === 0) return s.slice(i + 1).match(/^\w+/)?.[0] || ":";
|
|
612
|
+
};
|
|
613
|
+
return path.replace(/^index\/?/, "").split("/").flatMap((orig) => {
|
|
614
|
+
if (!orig.length) return [];
|
|
615
|
+
const bareParam = detectBareParams(orig);
|
|
616
|
+
if (bareParam === ":") throw new Error(`${path} contains colons outside braces, use : only within {}`);
|
|
617
|
+
else if (bareParam) throw new Error(`${path} contains bare params, use [${bareParam}] instead of :${bareParam}`);
|
|
618
|
+
const pattern = patternTransforms.reduce((src, fn) => fn(src), orig);
|
|
619
|
+
const { tokens } = parse(pattern.replace(/\{[^}]*\}|(\+)/g, (m, p1) => p1 ? "\\+" : m));
|
|
620
|
+
const parts = extractParts(tokens, (val) => {
|
|
621
|
+
return /\W/.test(val) || /^\d/.test(val) ? [val.replace(/^\d+|\W/g, "_"), crc(orig)].join("_") : val;
|
|
622
|
+
});
|
|
623
|
+
const isStatic = parts.length === 1 ? parts[0].type === "static" : false;
|
|
624
|
+
const isParam = parts.length === 1 ? parts[0].type === "param" : false;
|
|
625
|
+
return [{
|
|
626
|
+
kind: isStatic ? "static" : isParam ? "param" : "mixed",
|
|
627
|
+
orig,
|
|
628
|
+
pattern,
|
|
629
|
+
parts
|
|
630
|
+
}];
|
|
631
|
+
});
|
|
720
632
|
};
|
|
721
633
|
var createPathPattern = (tokens) => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
});
|
|
732
|
-
return slashRequired ? `${pattern}/` : pattern;
|
|
733
|
-
}).join("");
|
|
634
|
+
return tokens.map(({ pattern }, i) => {
|
|
635
|
+
const next = tokens[i + 1];
|
|
636
|
+
if (!next || next.pattern.includes("/")) return pattern;
|
|
637
|
+
return tokens.slice(i + 1).some((e) => {
|
|
638
|
+
return e.parts.some((e) => {
|
|
639
|
+
return e.type === "static" || e.kind === "required";
|
|
640
|
+
});
|
|
641
|
+
}) ? `${pattern}/` : pattern;
|
|
642
|
+
}).join("");
|
|
734
643
|
};
|
|
735
644
|
var createHonoPattern = (tokens) => {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (t.type === "group") {
|
|
761
|
-
return [`(?:${tokensToRegex(t.tokens)})?`];
|
|
762
|
-
}
|
|
763
|
-
return [];
|
|
764
|
-
}).join("");
|
|
765
|
-
};
|
|
766
|
-
return tokens.flatMap((token, i) => {
|
|
767
|
-
if (token.kind === "static") {
|
|
768
|
-
return [staticValue(token.parts[0])];
|
|
769
|
-
}
|
|
770
|
-
if (token.kind === "param") {
|
|
771
|
-
return [paramValue(token.parts[0])];
|
|
772
|
-
}
|
|
773
|
-
const { tokens: tokens2 } = parse(token.pattern.replace(/\//g, ""));
|
|
774
|
-
const regex = tokensToRegex(tokens2);
|
|
775
|
-
return [`:_${i}{${regex}}`];
|
|
776
|
-
}).join("/");
|
|
645
|
+
const staticValue = ({ value }) => {
|
|
646
|
+
return normalizeStaticValue(value);
|
|
647
|
+
};
|
|
648
|
+
const paramValue = (p) => {
|
|
649
|
+
if (p.kind === "splat") return `*`;
|
|
650
|
+
if (p.kind === "optional") return `:${p.name}?`;
|
|
651
|
+
return `:${p.name}`;
|
|
652
|
+
};
|
|
653
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
654
|
+
const tokensToRegex = (tokens) => {
|
|
655
|
+
return tokens.flatMap((t) => {
|
|
656
|
+
if (t.type === "text") return t.value === "/" ? [] : [escapeRegex(t.value)];
|
|
657
|
+
if (t.type === "wildcard") return [".+"];
|
|
658
|
+
if (t.type === "param") return ["[^/]+"];
|
|
659
|
+
if (t.type === "group") return [`(?:${tokensToRegex(t.tokens)})?`];
|
|
660
|
+
return [];
|
|
661
|
+
}).join("");
|
|
662
|
+
};
|
|
663
|
+
return tokens.flatMap((token, i) => {
|
|
664
|
+
if (token.kind === "static") return [staticValue(token.parts[0])];
|
|
665
|
+
if (token.kind === "param") return [paramValue(token.parts[0])];
|
|
666
|
+
const { tokens } = parse(token.pattern.replace(/\//g, ""));
|
|
667
|
+
return [`:_${i}{${tokensToRegex(tokens)}}`];
|
|
668
|
+
}).join("/");
|
|
777
669
|
};
|
|
778
670
|
var normalizeStaticValue = (value) => {
|
|
779
|
-
|
|
671
|
+
return value.replace(/\+/g, "\\\\+");
|
|
780
672
|
};
|
|
781
|
-
|
|
782
|
-
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/routes/base.ts
|
|
783
675
|
var API_INDEX_BASENAME = "index";
|
|
784
676
|
var API_INDEX_PATTERN = `${API_INDEX_BASENAME}.ts`;
|
|
785
677
|
var API_USE_BASENAME = "use";
|
|
786
|
-
var API_USE_PATTERN =
|
|
678
|
+
var API_USE_PATTERN = `use.ts`;
|
|
787
679
|
var PAGE_INDEX_BASENAME = "index";
|
|
788
680
|
var PAGE_INDEX_PATTERN = `${PAGE_INDEX_BASENAME}.{tsx,vue,mdx,md}`;
|
|
789
681
|
var PAGE_LAYOUT_BASENAME = "layout";
|
|
790
682
|
var PAGE_LAYOUT_PATTERN = `${PAGE_LAYOUT_BASENAME}.{tsx,vue,mdx}`;
|
|
791
683
|
var ROUTE_FILE_PATTERNS = [
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// match index files in pages dir
|
|
797
|
-
`${defaults3.pagesDir}/**/${PAGE_INDEX_PATTERN}`,
|
|
798
|
-
// match layout files in pages dir
|
|
799
|
-
`${defaults3.pagesDir}/**/${PAGE_LAYOUT_PATTERN}`
|
|
684
|
+
`${defaults.apiDir}/**/${API_INDEX_PATTERN}`,
|
|
685
|
+
`${defaults.apiDir}/**/${API_USE_PATTERN}`,
|
|
686
|
+
`${defaults.pagesDir}/**/${PAGE_INDEX_PATTERN}`,
|
|
687
|
+
`${defaults.pagesDir}/**/${PAGE_LAYOUT_PATTERN}`
|
|
800
688
|
];
|
|
801
689
|
var scanRoutes = async (sourceFolder) => {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
});
|
|
690
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
691
|
+
return glob(ROUTE_FILE_PATTERNS, {
|
|
692
|
+
cwd: createPath.src(),
|
|
693
|
+
absolute: true,
|
|
694
|
+
onlyFiles: true,
|
|
695
|
+
followSymbolicLinks: false,
|
|
696
|
+
ignore: [
|
|
697
|
+
`${defaults.apiDir}/${API_INDEX_PATTERN}`,
|
|
698
|
+
`${defaults.apiDir}/${API_USE_PATTERN}`,
|
|
699
|
+
`${defaults.pagesDir}/${PAGE_INDEX_PATTERN}`,
|
|
700
|
+
`${defaults.pagesDir}/${PAGE_LAYOUT_PATTERN}`
|
|
701
|
+
]
|
|
702
|
+
});
|
|
816
703
|
};
|
|
817
704
|
var isRouteFile = (file, sourceFolder) => {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
return picomatch2.isMatch(join4(_folder, ...rest), ROUTE_FILE_PATTERNS) ? [_folder, rest.join("/")] : false;
|
|
705
|
+
const [_sourceFolder, _folder, ...rest] = resolve(sourceFolder.root, file).replace(`${sourceFolder.root}/${defaults.srcDir}/`, "").split("/");
|
|
706
|
+
/**
|
|
707
|
+
* Ensure the file:
|
|
708
|
+
* - is under the correct source root
|
|
709
|
+
* - belongs to a known route folder (`api/` or `pages/`)
|
|
710
|
+
* - is nested at least one level deep (not a direct child of the folder)
|
|
711
|
+
* */
|
|
712
|
+
if (!_folder || _sourceFolder !== sourceFolder.name || rest.length < 2) return false;
|
|
713
|
+
return picomatch.isMatch(join(_folder, ...rest), ROUTE_FILE_PATTERNS) ? [_folder, rest.join("/")] : false;
|
|
829
714
|
};
|
|
830
715
|
var isApiRoute = (file) => {
|
|
831
|
-
|
|
716
|
+
return picomatch.matchBase(file, `**/${API_INDEX_PATTERN}`);
|
|
832
717
|
};
|
|
833
718
|
var isApiUse = (file) => {
|
|
834
|
-
|
|
719
|
+
return picomatch.matchBase(file, `**/${API_USE_PATTERN}`);
|
|
835
720
|
};
|
|
836
721
|
var isPageRoute = (file) => {
|
|
837
|
-
|
|
722
|
+
return picomatch.matchBase(file, `**/${PAGE_INDEX_PATTERN}`);
|
|
838
723
|
};
|
|
839
724
|
var isPageLayout = (file) => {
|
|
840
|
-
|
|
725
|
+
return picomatch.matchBase(file, `**/${PAGE_LAYOUT_PATTERN}`);
|
|
841
726
|
};
|
|
842
727
|
var createRouteEntry = (fileFullpath, sourceFolder) => {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
);
|
|
866
|
-
console.error(error);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
728
|
+
const resolvedPaths = isRouteFile(fileFullpath, sourceFolder);
|
|
729
|
+
if (!resolvedPaths) return;
|
|
730
|
+
const [folder, file] = resolvedPaths;
|
|
731
|
+
const id = `${file.replace(/\W+/g, "_")}_${crc(file)}`;
|
|
732
|
+
const name = dirname(file);
|
|
733
|
+
try {
|
|
734
|
+
const pathTokens = pathTokensFactory(dirname(file));
|
|
735
|
+
return {
|
|
736
|
+
id,
|
|
737
|
+
name,
|
|
738
|
+
folder,
|
|
739
|
+
file,
|
|
740
|
+
fileFullpath,
|
|
741
|
+
pathTokens,
|
|
742
|
+
pathPattern: createPathPattern(pathTokens),
|
|
743
|
+
honoPattern: createHonoPattern(pathTokens)
|
|
744
|
+
};
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error(`❗${styleText("red", "ERROR")}: Failed parsing path for "${styleText("cyan", file)}"`);
|
|
747
|
+
console.error(error);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
869
750
|
};
|
|
751
|
+
/**
|
|
752
|
+
* Weight of a single path segment.
|
|
753
|
+
*
|
|
754
|
+
* Fixed values per kind prevent complex mixed segments
|
|
755
|
+
* from outscoring multiple statics.
|
|
756
|
+
*
|
|
757
|
+
* static: 4 (exact match, most specific)
|
|
758
|
+
* mixed: 3+ (static + param parts; more statics = slightly higher)
|
|
759
|
+
* required: 2 (matches any single segment)
|
|
760
|
+
* optional: 1 (may or may not match)
|
|
761
|
+
* splat: 0 (matches anything, least specific)
|
|
762
|
+
* */
|
|
870
763
|
var segmentWeight = (token) => {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
return 3 + statics * 0.01 - optionals * 5e-3;
|
|
885
|
-
}
|
|
886
|
-
const part = token.parts[0];
|
|
887
|
-
switch (part.kind) {
|
|
888
|
-
case "required":
|
|
889
|
-
return 2;
|
|
890
|
-
case "optional":
|
|
891
|
-
return 1;
|
|
892
|
-
case "splat":
|
|
893
|
-
return 0;
|
|
894
|
-
}
|
|
764
|
+
if (token.kind === "static") return 4;
|
|
765
|
+
if (token.kind === "mixed") {
|
|
766
|
+
let statics = 0;
|
|
767
|
+
let optionals = 0;
|
|
768
|
+
for (const part of token.parts) if (part.type === "static") statics += 1;
|
|
769
|
+
else if (part.kind === "optional" || part.kind === "splat") optionals += 1;
|
|
770
|
+
return 3 + statics * .01 - optionals * .005;
|
|
771
|
+
}
|
|
772
|
+
switch (token.parts[0].kind) {
|
|
773
|
+
case "required": return 2;
|
|
774
|
+
case "optional": return 1;
|
|
775
|
+
case "splat": return 0;
|
|
776
|
+
}
|
|
895
777
|
};
|
|
778
|
+
/**
|
|
779
|
+
* Compare two route entries for registration order.
|
|
780
|
+
*
|
|
781
|
+
* More specific routes sort first (higher priority).
|
|
782
|
+
* Segments are compared left-to-right; first difference wins.
|
|
783
|
+
*
|
|
784
|
+
* When one route is a prefix of the other, the trailing segment
|
|
785
|
+
* determines order: a trailing splat loses (shorter sibling first),
|
|
786
|
+
* anything else wins (longer route is more specific).
|
|
787
|
+
*
|
|
788
|
+
* At equal weight and length, lexicographic tiebreak ensures
|
|
789
|
+
* deterministic ordering across JS engines.
|
|
790
|
+
* */
|
|
896
791
|
var sortRoutes = (a, b) => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
const aw = segmentWeight(at);
|
|
911
|
-
const bw = segmentWeight(bt);
|
|
912
|
-
if (aw !== bw) {
|
|
913
|
-
return bw - aw;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return a.pathPattern.localeCompare(b.pathPattern);
|
|
792
|
+
const pairs = Array.from({ length: Math.max(a.pathTokens.length, b.pathTokens.length) }, (_, i) => [a.pathTokens[i], b.pathTokens[i]]);
|
|
793
|
+
for (const [at, bt] of pairs) {
|
|
794
|
+
if (!at || !bt) {
|
|
795
|
+
const extra = at ?? bt;
|
|
796
|
+
if (extra?.kind === "param" ? extra.parts[0].kind === "splat" : false) return at ? 1 : -1;
|
|
797
|
+
return at ? -1 : 1;
|
|
798
|
+
}
|
|
799
|
+
const aw = segmentWeight(at);
|
|
800
|
+
const bw = segmentWeight(bt);
|
|
801
|
+
if (aw !== bw) return bw - aw;
|
|
802
|
+
}
|
|
803
|
+
return a.pathPattern.localeCompare(b.pathPattern);
|
|
917
804
|
};
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
import { dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
|
|
921
|
-
import { styleText as styleText3 } from "node:util";
|
|
922
|
-
import crc5 from "crc/crc32";
|
|
923
|
-
import mimeTypes from "mime-types";
|
|
924
|
-
import {
|
|
925
|
-
defaults as defaults4
|
|
926
|
-
} from "@kosmojs/core";
|
|
927
|
-
|
|
928
|
-
// src/templates/resolved-types.hbs
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/templates/resolved-types.hbs?raw
|
|
929
807
|
var resolved_types_default = "{{#each resolvedTypes}}\nexport type {{name}} = {{text}};\n{{/each}}\n";
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
var types_default =
|
|
933
|
-
|
|
934
|
-
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/templates/types.hbs?raw
|
|
810
|
+
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";
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/routes/resolve.ts
|
|
935
813
|
var resolverFactory = (sourceFolder, cacheFactory) => {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
)
|
|
1161
|
-
);
|
|
1162
|
-
console.warn(
|
|
1163
|
-
` Invalid value provided for mime-types lookup - ${variant.contentType}`
|
|
1164
|
-
);
|
|
1165
|
-
console.warn(
|
|
1166
|
-
styleText3(
|
|
1167
|
-
["cyan"],
|
|
1168
|
-
` Response variant #${i} excluded from route schemas`
|
|
1169
|
-
)
|
|
1170
|
-
);
|
|
1171
|
-
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
1172
|
-
console.warn();
|
|
1173
|
-
return [];
|
|
1174
|
-
}
|
|
1175
|
-
return [{ ...variant, contentType }];
|
|
1176
|
-
})
|
|
1177
|
-
};
|
|
1178
|
-
} else if (def.contentType && !def.contentType.includes("/")) {
|
|
1179
|
-
const contentType = mimeTypes.lookup(def.contentType);
|
|
1180
|
-
if (contentType === false) {
|
|
1181
|
-
console.warn(
|
|
1182
|
-
styleText3(
|
|
1183
|
-
["bold", "red"],
|
|
1184
|
-
"\u2717 Failed resolving Response Content Type"
|
|
1185
|
-
)
|
|
1186
|
-
);
|
|
1187
|
-
console.warn(
|
|
1188
|
-
` Invalid value provided for mime-types lookup - ${def.contentType}`
|
|
1189
|
-
);
|
|
1190
|
-
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
1191
|
-
console.warn();
|
|
1192
|
-
} else {
|
|
1193
|
-
augmentedDef = { ...def, contentType };
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return augmentedDef ? [augmentedDef] : [];
|
|
1197
|
-
}
|
|
1198
|
-
);
|
|
1199
|
-
const entry2 = {
|
|
1200
|
-
id,
|
|
1201
|
-
name,
|
|
1202
|
-
pathTokens,
|
|
1203
|
-
pathPattern,
|
|
1204
|
-
honoPattern,
|
|
1205
|
-
params: cache.params,
|
|
1206
|
-
numericParams: cache.numericParams,
|
|
1207
|
-
optionalParams,
|
|
1208
|
-
folder,
|
|
1209
|
-
file,
|
|
1210
|
-
fileFullpath,
|
|
1211
|
-
methods: cache.methods,
|
|
1212
|
-
typeDeclarations: cache.typeDeclarations,
|
|
1213
|
-
validationDefinitions,
|
|
1214
|
-
referencedFiles: Object.keys(cache.referencedFiles).map(
|
|
1215
|
-
// expand referenced files path,
|
|
1216
|
-
// they are stored as relative in cache
|
|
1217
|
-
(e) => resolve4(sourceFolder.root, e)
|
|
1218
|
-
)
|
|
1219
|
-
};
|
|
1220
|
-
return {
|
|
1221
|
-
kind: "apiRoute",
|
|
1222
|
-
entry: entry2
|
|
1223
|
-
};
|
|
1224
|
-
};
|
|
1225
|
-
return { name, handler };
|
|
1226
|
-
}
|
|
1227
|
-
};
|
|
814
|
+
const { generators = [], refineTypeName = defaults.refineTypeName } = sourceFolder.config;
|
|
815
|
+
const { typeResolverFactory, resolveRouteSignature } = astFactory();
|
|
816
|
+
const resolveTypes = generators.some(({ meta }) => meta.resolveTypes);
|
|
817
|
+
const { literalTypesResolver, getSourceFile, refreshSourceFile } = typeResolverFactory(sourceFolder);
|
|
818
|
+
return {
|
|
819
|
+
pageLayoutResolver(entry) {
|
|
820
|
+
const { name } = entry;
|
|
821
|
+
const handler = async () => {
|
|
822
|
+
return {
|
|
823
|
+
kind: "pageLayout",
|
|
824
|
+
entry
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
name,
|
|
829
|
+
handler
|
|
830
|
+
};
|
|
831
|
+
},
|
|
832
|
+
pageRouteResolver(entry) {
|
|
833
|
+
const { id, name, folder, file, fileFullpath, pathTokens, pathPattern, honoPattern } = entry;
|
|
834
|
+
const handler = async () => {
|
|
835
|
+
return {
|
|
836
|
+
kind: "pageRoute",
|
|
837
|
+
entry: {
|
|
838
|
+
id,
|
|
839
|
+
name,
|
|
840
|
+
pathTokens,
|
|
841
|
+
pathPattern,
|
|
842
|
+
honoPattern,
|
|
843
|
+
params: { schema: pathTokens.flatMap((e) => {
|
|
844
|
+
return e.parts.filter((p) => p.type === "param");
|
|
845
|
+
}) },
|
|
846
|
+
folder,
|
|
847
|
+
file,
|
|
848
|
+
fileFullpath
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
name,
|
|
854
|
+
handler
|
|
855
|
+
};
|
|
856
|
+
},
|
|
857
|
+
apiUseResolver(entry) {
|
|
858
|
+
const { name } = entry;
|
|
859
|
+
const handler = async () => {
|
|
860
|
+
return {
|
|
861
|
+
kind: "apiUse",
|
|
862
|
+
entry
|
|
863
|
+
};
|
|
864
|
+
};
|
|
865
|
+
return {
|
|
866
|
+
name,
|
|
867
|
+
handler
|
|
868
|
+
};
|
|
869
|
+
},
|
|
870
|
+
apiRouteResolver(entry) {
|
|
871
|
+
const { id, name, file, folder, fileFullpath, pathTokens, pathPattern, honoPattern } = entry;
|
|
872
|
+
const handler = async (updatedFile) => {
|
|
873
|
+
const paramsSchema = pathTokens.flatMap((e) => {
|
|
874
|
+
return e.parts.flatMap((p) => {
|
|
875
|
+
return p.type === "param" ? [p] : [];
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
const optionalParams = paramsSchema.length ? paramsSchema.filter((e) => e.kind === "required").length === 0 : true;
|
|
879
|
+
const cacheStore = cacheFactory ? cacheFactory({
|
|
880
|
+
id,
|
|
881
|
+
file,
|
|
882
|
+
fileFullpath
|
|
883
|
+
}, sourceFolder, { resolveTypes }) : void 0;
|
|
884
|
+
let cache = cacheStore ? await cacheStore.get({ validate: true }) : void 0;
|
|
885
|
+
if (!cache) {
|
|
886
|
+
if (updatedFile === fileFullpath) await refreshSourceFile(fileFullpath);
|
|
887
|
+
const { typeDeclarations, paramsRefinements, methods, validationDefinitions, referencedFiles = [] } = await resolveRouteSignature({
|
|
888
|
+
id,
|
|
889
|
+
name,
|
|
890
|
+
fileFullpath,
|
|
891
|
+
optionalParams
|
|
892
|
+
}, {
|
|
893
|
+
withReferencedFiles: true,
|
|
894
|
+
sourceFile: getSourceFile(fileFullpath),
|
|
895
|
+
relpathResolver(path) {
|
|
896
|
+
return join(sourceFolder.name, defaults.apiDir, dirname(file), path);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
const validationTypes = validationDefinitions.flatMap((def) => {
|
|
900
|
+
return def.target === "response" ? def.variants.flatMap(({ id, body }) => {
|
|
901
|
+
return body ? [{
|
|
902
|
+
id,
|
|
903
|
+
text: body
|
|
904
|
+
}] : [];
|
|
905
|
+
}) : [def.schema];
|
|
906
|
+
});
|
|
907
|
+
const numericParams = paramsRefinements ? paramsRefinements.flatMap(({ text, index }) => {
|
|
908
|
+
if (text === "number") {
|
|
909
|
+
const param = paramsSchema.at(index);
|
|
910
|
+
return param ? [param.name] : [];
|
|
911
|
+
}
|
|
912
|
+
return [];
|
|
913
|
+
}) : [];
|
|
914
|
+
const typesFile = pathResolver(sourceFolder).createPath.libApi(dirname(file), "types.ts");
|
|
915
|
+
const params = {
|
|
916
|
+
id: ["ParamsT", crc(name)].join(""),
|
|
917
|
+
schema: paramsSchema,
|
|
918
|
+
resolvedType: void 0
|
|
919
|
+
};
|
|
920
|
+
const typesFileContent = render(types_default, {
|
|
921
|
+
params,
|
|
922
|
+
paramsSchema: paramsSchema.map((param, index) => {
|
|
923
|
+
return {
|
|
924
|
+
...param,
|
|
925
|
+
isRequired: param.kind === "required",
|
|
926
|
+
isSplat: param.kind === "splat",
|
|
927
|
+
refinement: paramsRefinements?.at(index)
|
|
928
|
+
};
|
|
929
|
+
}),
|
|
930
|
+
typeDeclarations,
|
|
931
|
+
validationTypes
|
|
932
|
+
});
|
|
933
|
+
const resolvedTypes = resolveTypes ? literalTypesResolver(typesFileContent, {
|
|
934
|
+
stripComments: true,
|
|
935
|
+
overrides: { [refineTypeName]: refineTypeName },
|
|
936
|
+
withProperties: [params.id, ...validationTypes.flatMap(({ id }) => id)]
|
|
937
|
+
}) : void 0;
|
|
938
|
+
/**
|
|
939
|
+
* Deploy types.ts file; required by core generators (like fetch).
|
|
940
|
+
* If types resolved, write resolved types;
|
|
941
|
+
* otherwise write original types extracted from API route.
|
|
942
|
+
* */
|
|
943
|
+
await renderToFile(typesFile, resolvedTypes ? resolved_types_default : typesFileContent, { resolvedTypes });
|
|
944
|
+
params.resolvedType = resolvedTypes?.find((e) => e.name === params.id);
|
|
945
|
+
cache = {
|
|
946
|
+
hash: 0,
|
|
947
|
+
referencedFiles: {},
|
|
948
|
+
params,
|
|
949
|
+
methods,
|
|
950
|
+
typeDeclarations,
|
|
951
|
+
numericParams,
|
|
952
|
+
validationDefinitions: validationDefinitions.map((def) => {
|
|
953
|
+
return {
|
|
954
|
+
...def,
|
|
955
|
+
...def.target === "response" ? { variants: def.variants.map((variant) => {
|
|
956
|
+
return {
|
|
957
|
+
...variant,
|
|
958
|
+
resolvedType: resolvedTypes?.find((e) => e.name === variant.id)
|
|
959
|
+
};
|
|
960
|
+
}) } : { schema: {
|
|
961
|
+
...def.schema,
|
|
962
|
+
resolvedType: resolvedTypes?.find((e) => e.name === def.schema.id)
|
|
963
|
+
} }
|
|
964
|
+
};
|
|
965
|
+
})
|
|
966
|
+
};
|
|
967
|
+
if (cacheStore) {
|
|
968
|
+
const { referencedFiles: _void, ...rest } = cache;
|
|
969
|
+
cache = await cacheStore.set({
|
|
970
|
+
...rest,
|
|
971
|
+
referencedFiles
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const validationDefinitions = cache.validationDefinitions.flatMap((def) => {
|
|
976
|
+
let augmentedDef = def;
|
|
977
|
+
if (def.target === "response") augmentedDef = {
|
|
978
|
+
...def,
|
|
979
|
+
variants: def.variants.flatMap((variant, i) => {
|
|
980
|
+
if (typeof variant.contentType !== "string") return [variant];
|
|
981
|
+
if (variant.contentType.includes("/")) return [variant];
|
|
982
|
+
const contentType = mimeTypes.lookup(variant.contentType);
|
|
983
|
+
if (contentType === false) {
|
|
984
|
+
console.warn(styleText(["bold", "red"], "✗ Failed resolving Response Content Type"));
|
|
985
|
+
console.warn(` Invalid value provided for mime-types lookup - ${variant.contentType}`);
|
|
986
|
+
console.warn(styleText(["cyan"], ` Response variant #${i} excluded from route schemas`));
|
|
987
|
+
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
988
|
+
console.warn();
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
return [{
|
|
992
|
+
...variant,
|
|
993
|
+
contentType
|
|
994
|
+
}];
|
|
995
|
+
})
|
|
996
|
+
};
|
|
997
|
+
else if (def.contentType && !def.contentType.includes("/")) {
|
|
998
|
+
const contentType = mimeTypes.lookup(def.contentType);
|
|
999
|
+
if (contentType === false) {
|
|
1000
|
+
console.warn(styleText(["bold", "red"], "✗ Failed resolving Response Content Type"));
|
|
1001
|
+
console.warn(` Invalid value provided for mime-types lookup - ${def.contentType}`);
|
|
1002
|
+
console.warn(` Route: ${name}; Method: ${def.method}`);
|
|
1003
|
+
console.warn();
|
|
1004
|
+
} else augmentedDef = {
|
|
1005
|
+
...def,
|
|
1006
|
+
contentType
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
return augmentedDef ? [augmentedDef] : [];
|
|
1010
|
+
});
|
|
1011
|
+
return {
|
|
1012
|
+
kind: "apiRoute",
|
|
1013
|
+
entry: {
|
|
1014
|
+
id,
|
|
1015
|
+
name,
|
|
1016
|
+
pathTokens,
|
|
1017
|
+
pathPattern,
|
|
1018
|
+
honoPattern,
|
|
1019
|
+
params: cache.params,
|
|
1020
|
+
numericParams: cache.numericParams,
|
|
1021
|
+
optionalParams,
|
|
1022
|
+
folder,
|
|
1023
|
+
file,
|
|
1024
|
+
fileFullpath,
|
|
1025
|
+
methods: cache.methods,
|
|
1026
|
+
typeDeclarations: cache.typeDeclarations,
|
|
1027
|
+
validationDefinitions,
|
|
1028
|
+
referencedFiles: Object.keys(cache.referencedFiles).map((e) => resolve(sourceFolder.root, e))
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
};
|
|
1032
|
+
return {
|
|
1033
|
+
name,
|
|
1034
|
+
handler
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1228
1038
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
import { basename } from "node:path";
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/routes/nesting.ts
|
|
1232
1041
|
var nestedRoutesFactory = (routeEntries) => {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
});
|
|
1297
|
-
return !hasIntermediateRoute;
|
|
1298
|
-
});
|
|
1299
|
-
};
|
|
1300
|
-
const rootEntries = entryStack.filter((entry) => {
|
|
1301
|
-
const hasParent = entryStack.some((potential) => {
|
|
1302
|
-
if (potential === entry) {
|
|
1303
|
-
return false;
|
|
1304
|
-
}
|
|
1305
|
-
if (potential.pathTokens.length >= entry.pathTokens.length) {
|
|
1306
|
-
return false;
|
|
1307
|
-
}
|
|
1308
|
-
return entry.name.startsWith(`${potential.name}/`);
|
|
1309
|
-
});
|
|
1310
|
-
return !hasParent;
|
|
1311
|
-
});
|
|
1312
|
-
return transformEntries(rootEntries);
|
|
1042
|
+
const entryStack = structuredClone(routeEntries).sort(sortRoutes);
|
|
1043
|
+
/**
|
|
1044
|
+
* Group entries by name (at the same level, both layout and index share same name).
|
|
1045
|
+
*
|
|
1046
|
+
* Handle 3 cases:
|
|
1047
|
+
* - both index and layout exists
|
|
1048
|
+
* - only index exists
|
|
1049
|
+
* - only layout exists
|
|
1050
|
+
*
|
|
1051
|
+
* */
|
|
1052
|
+
const transformEntries = (entries, parent) => {
|
|
1053
|
+
return [...new Set(entries.map((e) => e.name))].flatMap((name) => {
|
|
1054
|
+
const nameEntries = entryStack.flatMap(({ fileFullpath, ...entry }) => {
|
|
1055
|
+
return entry.name === name ? [entry] : [];
|
|
1056
|
+
});
|
|
1057
|
+
const index = nameEntries.find((e) => basename(e.file).startsWith(PAGE_INDEX_BASENAME));
|
|
1058
|
+
const layout = nameEntries.find((e) => basename(e.file).startsWith(PAGE_LAYOUT_BASENAME));
|
|
1059
|
+
if (index || layout) return [{
|
|
1060
|
+
index: index ? {
|
|
1061
|
+
...index,
|
|
1062
|
+
pathTokens: index.pathTokens.slice(parent?.pathTokens.length || 0)
|
|
1063
|
+
} : void 0,
|
|
1064
|
+
layout: layout ? {
|
|
1065
|
+
...layout,
|
|
1066
|
+
pathTokens: layout.pathTokens.slice(parent?.pathTokens.length || 0)
|
|
1067
|
+
} : void 0,
|
|
1068
|
+
parent: parent?.name,
|
|
1069
|
+
children: transformEntries(findDescendantEntries(index || layout), index || layout)
|
|
1070
|
+
}];
|
|
1071
|
+
return [];
|
|
1072
|
+
});
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Recursively finds direct descendant routes.
|
|
1076
|
+
*
|
|
1077
|
+
* "Direct" means the next existing route in the path hierarchy,
|
|
1078
|
+
* skipping intermediate path segments that don't have index/layout files.
|
|
1079
|
+
*
|
|
1080
|
+
* Example: blog/post/[slug]/index.tsx is a direct child of blog/index.tsx
|
|
1081
|
+
* even though "post" segment exists (it has no index.tsx).
|
|
1082
|
+
* */
|
|
1083
|
+
const findDescendantEntries = ({ name, pathTokens }) => {
|
|
1084
|
+
const potentialChildren = entryStack.filter((entry) => {
|
|
1085
|
+
if (entry.pathTokens.length <= pathTokens.length) return false;
|
|
1086
|
+
if (!entry.name.startsWith(`${name}/`)) return false;
|
|
1087
|
+
return true;
|
|
1088
|
+
});
|
|
1089
|
+
return potentialChildren.filter((child) => {
|
|
1090
|
+
return !potentialChildren.some((intermediate) => {
|
|
1091
|
+
if (intermediate === child) return false;
|
|
1092
|
+
if (intermediate.pathTokens.length <= pathTokens.length) return false;
|
|
1093
|
+
if (intermediate.pathTokens.length >= child.pathTokens.length) return false;
|
|
1094
|
+
return child.name.startsWith(`${intermediate.name}/`);
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
};
|
|
1098
|
+
return transformEntries(entryStack.filter((entry) => {
|
|
1099
|
+
return !entryStack.some((potential) => {
|
|
1100
|
+
if (potential === entry) return false;
|
|
1101
|
+
if (potential.pathTokens.length >= entry.pathTokens.length) return false;
|
|
1102
|
+
return entry.name.startsWith(`${potential.name}/`);
|
|
1103
|
+
});
|
|
1104
|
+
}));
|
|
1313
1105
|
};
|
|
1314
|
-
|
|
1315
|
-
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/routes/index.ts
|
|
1316
1108
|
var routesFactory = async (sourceFolder, cacheFactory) => {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (isPageRoute(entry.file)) {
|
|
1338
|
-
resolvers.set(entry.fileFullpath, pageRouteResolver(entry));
|
|
1339
|
-
} else if (isPageLayout(entry.file)) {
|
|
1340
|
-
resolvers.set(entry.fileFullpath, pageLayoutResolver(entry));
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
return resolvers;
|
|
1345
|
-
};
|
|
1346
|
-
const routeFiles = await scanRoutes(sourceFolder);
|
|
1347
|
-
return {
|
|
1348
|
-
resolvers: resolversFactory(routeFiles),
|
|
1349
|
-
resolversFactory
|
|
1350
|
-
};
|
|
1109
|
+
const { apiRouteResolver, apiUseResolver, pageRouteResolver, pageLayoutResolver } = resolverFactory(sourceFolder, cacheFactory);
|
|
1110
|
+
const resolversFactory = (routeFiles) => {
|
|
1111
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
1112
|
+
const entries = routeFiles.flatMap((file) => {
|
|
1113
|
+
const entry = createRouteEntry(file, sourceFolder);
|
|
1114
|
+
return entry ? [entry] : [];
|
|
1115
|
+
});
|
|
1116
|
+
for (const entry of entries) if (entry.folder === defaults.apiDir) {
|
|
1117
|
+
if (isApiRoute(entry.file)) resolvers.set(entry.fileFullpath, apiRouteResolver(entry));
|
|
1118
|
+
else if (isApiUse(entry.file)) resolvers.set(entry.fileFullpath, apiUseResolver(entry));
|
|
1119
|
+
} else if (entry.folder === defaults.pagesDir) {
|
|
1120
|
+
if (isPageRoute(entry.file)) resolvers.set(entry.fileFullpath, pageRouteResolver(entry));
|
|
1121
|
+
else if (isPageLayout(entry.file)) resolvers.set(entry.fileFullpath, pageLayoutResolver(entry));
|
|
1122
|
+
}
|
|
1123
|
+
return resolvers;
|
|
1124
|
+
};
|
|
1125
|
+
return {
|
|
1126
|
+
resolvers: resolversFactory(await scanRoutes(sourceFolder)),
|
|
1127
|
+
resolversFactory
|
|
1128
|
+
};
|
|
1351
1129
|
};
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
import { Spinner } from "@topcli/spinner";
|
|
1130
|
+
//#endregion
|
|
1131
|
+
//#region src/spinner.ts
|
|
1355
1132
|
var spinnerFactory = (startText) => {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
this.text([_text, text].join("\n"));
|
|
1377
|
-
}
|
|
1378
|
-
spinner.failed();
|
|
1379
|
-
}
|
|
1380
|
-
};
|
|
1133
|
+
const spinner = new Spinner().start(startText);
|
|
1134
|
+
let _text = startText;
|
|
1135
|
+
return {
|
|
1136
|
+
text(text) {
|
|
1137
|
+
_text = text;
|
|
1138
|
+
spinner.text = text;
|
|
1139
|
+
},
|
|
1140
|
+
append(text) {
|
|
1141
|
+
spinner.text = `${_text} › ${text}`;
|
|
1142
|
+
},
|
|
1143
|
+
succeed(text) {
|
|
1144
|
+
if (text) this.append(text);
|
|
1145
|
+
else this.text(_text);
|
|
1146
|
+
spinner.succeed();
|
|
1147
|
+
},
|
|
1148
|
+
failed(text) {
|
|
1149
|
+
if (text) this.text([_text, text].join("\n"));
|
|
1150
|
+
spinner.failed();
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1381
1153
|
};
|
|
1382
1154
|
var withSpinner = (text, pipe, spinner) => pipe(spinner || spinnerFactory(text));
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
import { defaults as defaults6 } from "@kosmojs/core";
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/typebox.ts
|
|
1386
1157
|
var typeboxLiteralText = (text, sourceFolder) => {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
/**
|
|
1394
|
-
* TypeBox's built-in `Options` type is not configurable.
|
|
1395
|
-
* To allow a custom type name, exposing `refineTypeName` option,
|
|
1396
|
-
* defaulted to VRefine, then renaming it to `Options`.
|
|
1397
|
-
* */
|
|
1398
|
-
[new RegExp(`\\b${refineTypeName}\\s*<`, "g"), "Options<"]
|
|
1399
|
-
].reduce((text2, [a, b]) => text2.replace(a, b), text);
|
|
1158
|
+
const { refineTypeName = defaults.refineTypeName } = sourceFolder.config;
|
|
1159
|
+
return [
|
|
1160
|
+
[/(?<!\\)`/g, "\\`"],
|
|
1161
|
+
[/(?<!\\)\$\{/g, "\\${"],
|
|
1162
|
+
[new RegExp(`\\b${refineTypeName}\\s*<`, "g"), "Options<"]
|
|
1163
|
+
].reduce((text, [a, b]) => text.replace(a, b), text);
|
|
1400
1164
|
};
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
import { builtinModules } from "node:module";
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src/vite.ts
|
|
1404
1167
|
var nodePrefix = () => {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
|
|
1168
|
+
return {
|
|
1169
|
+
name: "kosmojs:node-prefix",
|
|
1170
|
+
enforce: "pre",
|
|
1171
|
+
resolveId(source) {
|
|
1172
|
+
return builtinModules.includes(source) ? {
|
|
1173
|
+
id: `node:${source}`,
|
|
1174
|
+
external: true
|
|
1175
|
+
} : void 0;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1415
1178
|
};
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
PAGE_INDEX_BASENAME,
|
|
1422
|
-
PAGE_INDEX_PATTERN,
|
|
1423
|
-
PAGE_LAYOUT_BASENAME,
|
|
1424
|
-
PAGE_LAYOUT_PATTERN,
|
|
1425
|
-
astFactory,
|
|
1426
|
-
createHonoPattern,
|
|
1427
|
-
createPathPattern,
|
|
1428
|
-
createRouteEntry,
|
|
1429
|
-
createTemplateResolver,
|
|
1430
|
-
defineGenerator,
|
|
1431
|
-
defineGeneratorFactory,
|
|
1432
|
-
isApiRoute,
|
|
1433
|
-
isApiUse,
|
|
1434
|
-
isPageLayout,
|
|
1435
|
-
isPageRoute,
|
|
1436
|
-
isRouteFile,
|
|
1437
|
-
nestedRoutesFactory,
|
|
1438
|
-
normalizeStaticValue,
|
|
1439
|
-
pathExists,
|
|
1440
|
-
pathResolver,
|
|
1441
|
-
pathTokensFactory,
|
|
1442
|
-
render,
|
|
1443
|
-
renderFactory,
|
|
1444
|
-
renderToFile,
|
|
1445
|
-
routesFactory,
|
|
1446
|
-
scanRoutes,
|
|
1447
|
-
sortRoutes,
|
|
1448
|
-
spinnerFactory,
|
|
1449
|
-
typeboxLiteralText,
|
|
1450
|
-
vitePlugins,
|
|
1451
|
-
withSpinner
|
|
1452
|
-
};
|
|
1453
|
-
//# sourceMappingURL=index.js.map
|
|
1179
|
+
var vitePlugins = { nodePrefix };
|
|
1180
|
+
//#endregion
|
|
1181
|
+
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, createHonoPattern, createPathPattern, createRouteEntry, createTemplateResolver, defineGenerator, defineGeneratorFactory, isApiRoute, isApiUse, isPageLayout, isPageRoute, isRouteFile, nestedRoutesFactory, normalizeStaticValue, pathExists, pathResolver, pathTokensFactory, render, renderFactory, renderToFile, routesFactory, scanRoutes, sortRoutes, spinnerFactory, typeboxLiteralText, vitePlugins, withSpinner };
|
|
1182
|
+
|
|
1183
|
+
//# sourceMappingURL=index.js.map
|