pastoria 1.0.14 → 1.1.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/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/build_command.d.ts +2 -0
- package/dist/build_command.d.ts.map +1 -0
- package/dist/build_command.js +13 -0
- package/dist/build_command.js.map +1 -0
- package/dist/gen.d.ts +2 -0
- package/dist/gen.d.ts.map +1 -0
- package/dist/gen.js +19 -0
- package/dist/gen.js.map +1 -0
- package/dist/index.js +45 -9
- package/dist/index.js.map +1 -1
- package/justfile +41 -0
- package/package.json +6 -1
- package/CHANGELOG.md +0 -92
- package/src/build.ts +0 -306
- package/src/devserver.ts +0 -58
- package/src/generate.ts +0 -918
- package/src/index.ts +0 -40
- package/src/logger.ts +0 -12
- package/src/vite_plugin.ts +0 -109
- package/tsconfig.json +0 -21
package/src/generate.ts
DELETED
|
@@ -1,918 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Router Code Generator
|
|
3
|
-
*
|
|
4
|
-
* This script generates type-safe router configuration files by scanning TypeScript
|
|
5
|
-
* source code for JSDoc annotations. It's part of the "Pastoria" routing framework.
|
|
6
|
-
*
|
|
7
|
-
* How it works:
|
|
8
|
-
* 1. Scans all TypeScript files in the project for exported functions/classes
|
|
9
|
-
* 2. Looks for JSDoc tags: @route, @resource, @appRoot, and @param
|
|
10
|
-
* 3. Looks for exported classes that extend PastoriaRootContext for GraphQL context
|
|
11
|
-
* 4. Generates files from templates:
|
|
12
|
-
* - js_resource.ts: Resource configuration for lazy loading
|
|
13
|
-
* - router.tsx: Client-side router with type-safe routes
|
|
14
|
-
* - app_root.ts: Re-export of the app root component (if @appRoot is found)
|
|
15
|
-
* - context.ts: Re-export of user's context class, or generate a default one
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* - Add @route <route-name> to functions to create routes
|
|
19
|
-
* - Add @param <name> <type> to document route parameters
|
|
20
|
-
* - Add @resource <resource-name> to exports for lazy loading
|
|
21
|
-
* - Add @appRoot to a component to designate it as the application root wrapper
|
|
22
|
-
* - Add @gqlContext to a class extending PastoriaRootContext to provide a custom GraphQL context
|
|
23
|
-
* - Add @serverRoute to functions to add an express handler
|
|
24
|
-
*
|
|
25
|
-
* The generator automatically creates Zod schemas for route parameters based on
|
|
26
|
-
* TypeScript types, enabling runtime validation and type safety.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import {readFile} from 'node:fs/promises';
|
|
30
|
-
import * as path from 'node:path';
|
|
31
|
-
import pc from 'picocolors';
|
|
32
|
-
import {
|
|
33
|
-
CodeBlockWriter,
|
|
34
|
-
Project,
|
|
35
|
-
SourceFile,
|
|
36
|
-
Symbol,
|
|
37
|
-
SyntaxKind,
|
|
38
|
-
ts,
|
|
39
|
-
TypeFlags,
|
|
40
|
-
} from 'ts-morph';
|
|
41
|
-
import {logInfo, logWarn} from './logger.js';
|
|
42
|
-
|
|
43
|
-
async function loadRouterTemplates(project: Project, filename: string) {
|
|
44
|
-
async function loadSourceFile(fileName: string, templateFileName: string) {
|
|
45
|
-
const template = await readFile(templateFileName, 'utf-8');
|
|
46
|
-
const warningComment = `/*
|
|
47
|
-
* This file was generated by \`pastoria\`.
|
|
48
|
-
* Do not modify this file directly. Instead, edit the template at ${path.basename(templateFileName)}.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
`;
|
|
52
|
-
return project.createSourceFile(fileName, warningComment + template, {
|
|
53
|
-
overwrite: true,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const template = path.join(import.meta.dirname, '../templates', filename);
|
|
58
|
-
const output = path.join('__generated__/router', filename);
|
|
59
|
-
return loadSourceFile(output, template);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type RouterResource = {
|
|
63
|
-
sourceFile: SourceFile;
|
|
64
|
-
symbol: Symbol;
|
|
65
|
-
queries: Map<string, string>;
|
|
66
|
-
entryPoints: Map<string, string>;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type RouterRoute = {
|
|
70
|
-
sourceFile: SourceFile;
|
|
71
|
-
symbol: Symbol;
|
|
72
|
-
params: Map<string, ts.Type>;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type ExportedSymbol = {
|
|
76
|
-
sourceFile: SourceFile;
|
|
77
|
-
symbol: Symbol;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export interface PastoriaMetadata {
|
|
81
|
-
resources: Map<string, RouterResource>;
|
|
82
|
-
routes: Map<string, RouterRoute>;
|
|
83
|
-
serverHandlers: Map<string, ExportedSymbol>;
|
|
84
|
-
appRoot: ExportedSymbol | null;
|
|
85
|
-
gqlContext: ExportedSymbol | null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Regex to quickly check if a file contains any Pastoria JSDoc tags
|
|
89
|
-
export const PASTORIA_TAG_REGEX =
|
|
90
|
-
/@(route|resource|appRoot|param|gqlContext|serverRoute)\b/;
|
|
91
|
-
|
|
92
|
-
function collectQueryParameters(
|
|
93
|
-
project: Project,
|
|
94
|
-
queries: string[],
|
|
95
|
-
): Map<string, ts.Type> {
|
|
96
|
-
const vars = new Map<string, ts.Type>();
|
|
97
|
-
|
|
98
|
-
for (const query of queries) {
|
|
99
|
-
const variablesType = project
|
|
100
|
-
.getSourceFile(`__generated__/queries/${query}.graphql.ts`)
|
|
101
|
-
?.getExportedDeclarations()
|
|
102
|
-
.get(`${query}$variables`)
|
|
103
|
-
?.at(0)
|
|
104
|
-
?.getType();
|
|
105
|
-
|
|
106
|
-
if (variablesType == null) continue;
|
|
107
|
-
|
|
108
|
-
for (const property of variablesType.getProperties()) {
|
|
109
|
-
// TODO: Detect conflicting types among properties declared.
|
|
110
|
-
const propertyName = property.getName();
|
|
111
|
-
const propertyType = property.getValueDeclaration()?.getType();
|
|
112
|
-
|
|
113
|
-
if (propertyType) {
|
|
114
|
-
vars.set(propertyName, propertyType.compilerType);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return vars;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function collectPastoriaMetadata(project: Project): PastoriaMetadata {
|
|
123
|
-
const resources = new Map<string, RouterResource>();
|
|
124
|
-
const routes = new Map<string, RouterRoute>();
|
|
125
|
-
const serverHandlers = new Map<string, ExportedSymbol>();
|
|
126
|
-
let appRoot: ExportedSymbol | null = null;
|
|
127
|
-
let gqlContext: ExportedSymbol | null = null;
|
|
128
|
-
|
|
129
|
-
function visitRouterNodes(sourceFile: SourceFile) {
|
|
130
|
-
// Skip generated files
|
|
131
|
-
if (sourceFile.getFilePath().includes('__generated__')) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Skip files that don't contain any Pastoria JSDoc tags
|
|
136
|
-
const fileText = sourceFile.getFullText();
|
|
137
|
-
if (!PASTORIA_TAG_REGEX.test(fileText)) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
sourceFile.getExportSymbols().forEach((symbol) => {
|
|
142
|
-
let routerResource = null as [string, RouterResource] | null;
|
|
143
|
-
let routerRoute = null as [string, RouterRoute] | null;
|
|
144
|
-
const routeParams = new Map<string, ts.Type>();
|
|
145
|
-
|
|
146
|
-
function visitJSDocTags(tag: ts.JSDoc | ts.JSDocTag) {
|
|
147
|
-
if (ts.isJSDoc(tag)) {
|
|
148
|
-
tag.tags?.forEach(visitJSDocTags);
|
|
149
|
-
} else if (ts.isJSDocParameterTag(tag)) {
|
|
150
|
-
const typeNode = tag.typeExpression?.type;
|
|
151
|
-
const tc = project.getTypeChecker().compilerObject;
|
|
152
|
-
|
|
153
|
-
const type =
|
|
154
|
-
typeNode == null
|
|
155
|
-
? tc.getUnknownType()
|
|
156
|
-
: tc.getTypeFromTypeNode(typeNode);
|
|
157
|
-
|
|
158
|
-
routeParams.set(tag.name.getText(), type);
|
|
159
|
-
} else if (typeof tag.comment === 'string') {
|
|
160
|
-
switch (tag.tagName.getText()) {
|
|
161
|
-
case 'route': {
|
|
162
|
-
routerRoute = [
|
|
163
|
-
tag.comment,
|
|
164
|
-
{sourceFile, symbol, params: routeParams},
|
|
165
|
-
];
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case 'resource': {
|
|
169
|
-
routerResource = [
|
|
170
|
-
tag.comment,
|
|
171
|
-
{
|
|
172
|
-
sourceFile,
|
|
173
|
-
symbol,
|
|
174
|
-
queries: new Map(),
|
|
175
|
-
entryPoints: new Map(),
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
case 'serverRoute': {
|
|
181
|
-
serverHandlers.set(tag.comment, {sourceFile, symbol});
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
// Handle tags without comments (like @ExportedSymbol, @gqlContext)
|
|
187
|
-
switch (tag.tagName.getText()) {
|
|
188
|
-
case 'appRoot': {
|
|
189
|
-
if (appRoot != null) {
|
|
190
|
-
logWarn('Multiple @appRoot tags found. Using the first one.');
|
|
191
|
-
} else {
|
|
192
|
-
appRoot = {
|
|
193
|
-
sourceFile,
|
|
194
|
-
symbol,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
case 'gqlContext': {
|
|
200
|
-
// Check if this class extends PastoriaRootContext
|
|
201
|
-
const declarations = symbol.getDeclarations();
|
|
202
|
-
let extendsPastoriaRootContext = false;
|
|
203
|
-
|
|
204
|
-
for (const decl of declarations) {
|
|
205
|
-
if (decl.isKind(SyntaxKind.ClassDeclaration)) {
|
|
206
|
-
const classDecl = decl.asKindOrThrow(
|
|
207
|
-
SyntaxKind.ClassDeclaration,
|
|
208
|
-
);
|
|
209
|
-
const extendsClause = classDecl.getExtends();
|
|
210
|
-
if (extendsClause != null) {
|
|
211
|
-
const baseClassName = extendsClause
|
|
212
|
-
.getExpression()
|
|
213
|
-
.getText();
|
|
214
|
-
if (baseClassName === 'PastoriaRootContext') {
|
|
215
|
-
extendsPastoriaRootContext = true;
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (extendsPastoriaRootContext) {
|
|
223
|
-
if (gqlContext != null) {
|
|
224
|
-
logWarn(
|
|
225
|
-
'Multiple classes with @gqlContext extending PastoriaRootContext found. Using the first one.',
|
|
226
|
-
);
|
|
227
|
-
} else {
|
|
228
|
-
gqlContext = {
|
|
229
|
-
sourceFile,
|
|
230
|
-
symbol,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
symbol
|
|
241
|
-
.getDeclarations()
|
|
242
|
-
.flatMap((decl) => ts.getJSDocCommentsAndTags(decl.compilerNode))
|
|
243
|
-
.forEach(visitJSDocTags);
|
|
244
|
-
|
|
245
|
-
if (routerRoute != null) {
|
|
246
|
-
const [routeName, routeSymbol] = routerRoute;
|
|
247
|
-
routes.set(routeName, routeSymbol);
|
|
248
|
-
|
|
249
|
-
const {entryPoints, queries} = getResourceQueriesAndEntryPoints(
|
|
250
|
-
routeSymbol.symbol,
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
if (routerResource == null && (entryPoints.size || queries.size)) {
|
|
254
|
-
resources.set(`route(${routeName})`, {
|
|
255
|
-
...routeSymbol,
|
|
256
|
-
entryPoints,
|
|
257
|
-
queries,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (routerResource != null) {
|
|
263
|
-
const [resourceName, resourceSymbol] = routerResource;
|
|
264
|
-
const {entryPoints, queries} = getResourceQueriesAndEntryPoints(
|
|
265
|
-
resourceSymbol.symbol,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
resourceSymbol.queries = queries;
|
|
269
|
-
resourceSymbol.entryPoints = entryPoints;
|
|
270
|
-
resources.set(resourceName, resourceSymbol);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
project.getSourceFiles().forEach(visitRouterNodes);
|
|
276
|
-
return {resources, routes, appRoot, gqlContext, serverHandlers};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function getResourceQueriesAndEntryPoints(symbol: Symbol): {
|
|
280
|
-
queries: Map<string, string>;
|
|
281
|
-
entryPoints: Map<string, string>;
|
|
282
|
-
} {
|
|
283
|
-
const resource = {
|
|
284
|
-
queries: new Map<string, string>(),
|
|
285
|
-
entryPoints: new Map<string, string>(),
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const decl = symbol.getValueDeclaration();
|
|
289
|
-
if (!decl) return resource;
|
|
290
|
-
|
|
291
|
-
const t = decl.getType();
|
|
292
|
-
const aliasSymbol = t.getAliasSymbol();
|
|
293
|
-
|
|
294
|
-
if (aliasSymbol?.getName() === 'EntryPointComponent') {
|
|
295
|
-
const [queries, entryPoints] = t.getAliasTypeArguments();
|
|
296
|
-
|
|
297
|
-
queries?.getProperties().forEach((prop) => {
|
|
298
|
-
const queryRef = prop.getName();
|
|
299
|
-
const queryName = prop
|
|
300
|
-
.getValueDeclaration()
|
|
301
|
-
?.getType()
|
|
302
|
-
.getAliasSymbol()
|
|
303
|
-
?.getName();
|
|
304
|
-
|
|
305
|
-
if (queryName) {
|
|
306
|
-
resource.queries.set(queryRef, queryName);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
entryPoints?.getProperties().forEach((prop) => {
|
|
311
|
-
const epRef = prop.getName();
|
|
312
|
-
const entryPointTypeRef = prop
|
|
313
|
-
.getValueDeclaration()
|
|
314
|
-
?.asKind(SyntaxKind.PropertySignature)
|
|
315
|
-
?.getTypeNode()
|
|
316
|
-
?.asKind(SyntaxKind.TypeReference);
|
|
317
|
-
|
|
318
|
-
const entryPointTypeName = entryPointTypeRef?.getTypeName().getText();
|
|
319
|
-
if (entryPointTypeName !== 'EntryPoint') {
|
|
320
|
-
// TODO: Warn about found types not named EntryPoint
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const entryPointInner = entryPointTypeRef?.getTypeArguments().at(0);
|
|
325
|
-
const moduleTypeRef = entryPointInner?.asKind(SyntaxKind.TypeReference);
|
|
326
|
-
if (moduleTypeRef != null) {
|
|
327
|
-
const resourceName = moduleTypeRef
|
|
328
|
-
?.getTypeArguments()
|
|
329
|
-
.at(0)
|
|
330
|
-
?.asKind(SyntaxKind.LiteralType)
|
|
331
|
-
?.getLiteral()
|
|
332
|
-
.asKind(SyntaxKind.StringLiteral)
|
|
333
|
-
?.getLiteralText();
|
|
334
|
-
|
|
335
|
-
if (resourceName) {
|
|
336
|
-
resource.entryPoints.set(epRef, resourceName);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return resource;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function zodSchemaOfType(
|
|
346
|
-
sf: SourceFile,
|
|
347
|
-
tc: ts.TypeChecker,
|
|
348
|
-
t: ts.Type,
|
|
349
|
-
): string {
|
|
350
|
-
if (t.aliasSymbol) {
|
|
351
|
-
const decl = t.aliasSymbol.declarations?.at(0);
|
|
352
|
-
if (decl == null) {
|
|
353
|
-
logWarn('Could not handle type:', tc.typeToString(t));
|
|
354
|
-
return `z.any()`;
|
|
355
|
-
} else {
|
|
356
|
-
const importPath = sf.getRelativePathAsModuleSpecifierTo(
|
|
357
|
-
decl.getSourceFile().fileName,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
return `z.transform((s: string) => s as import('${importPath}').${t.aliasSymbol.getName()})`;
|
|
361
|
-
}
|
|
362
|
-
} else if (t.getFlags() & TypeFlags.String) {
|
|
363
|
-
return `z.pipe(z.string(), z.transform(decodeURIComponent))`;
|
|
364
|
-
} else if (t.getFlags() & TypeFlags.Number) {
|
|
365
|
-
return `z.coerce.number<number>()`;
|
|
366
|
-
} else if (t.getFlags() & TypeFlags.Null) {
|
|
367
|
-
return `z.preprocess(s => s == null ? undefined : s, z.undefined())`;
|
|
368
|
-
} else if (t.isUnion()) {
|
|
369
|
-
const nullishTypes: ts.Type[] = [];
|
|
370
|
-
const nonNullishTypes: ts.Type[] = [];
|
|
371
|
-
for (const s of t.types) {
|
|
372
|
-
const flags = s.getFlags();
|
|
373
|
-
if (flags & TypeFlags.Null || flags & TypeFlags.Undefined) {
|
|
374
|
-
nullishTypes.push(s);
|
|
375
|
-
} else {
|
|
376
|
-
nonNullishTypes.push(s);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (nullishTypes.length > 0 && nonNullishTypes.length > 0) {
|
|
381
|
-
const nonOptionalType = t.getNonNullableType();
|
|
382
|
-
return `z.pipe(z.nullish(${zodSchemaOfType(sf, tc, nonOptionalType)}), z.transform(s => s == null ? undefined : s))`;
|
|
383
|
-
} else {
|
|
384
|
-
return `z.union([${t.types.map((it) => zodSchemaOfType(sf, tc, it)).join(', ')}])`;
|
|
385
|
-
}
|
|
386
|
-
} else if (tc.isArrayLikeType(t)) {
|
|
387
|
-
const typeArg = tc.getTypeArguments(t as ts.TypeReference)[0];
|
|
388
|
-
const argZodSchema =
|
|
389
|
-
typeArg == null ? `z.any()` : zodSchemaOfType(sf, tc, typeArg);
|
|
390
|
-
|
|
391
|
-
return `z.array(${argZodSchema})`;
|
|
392
|
-
} else {
|
|
393
|
-
logWarn('Could not handle type:', tc.typeToString(t));
|
|
394
|
-
return `z.any()`;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function writeEntryPoint(
|
|
399
|
-
writer: CodeBlockWriter,
|
|
400
|
-
project: Project,
|
|
401
|
-
metadata: PastoriaMetadata,
|
|
402
|
-
consumedQueries: Set<string>,
|
|
403
|
-
resourceName: string,
|
|
404
|
-
resource: RouterResource,
|
|
405
|
-
parseVars = true,
|
|
406
|
-
) {
|
|
407
|
-
writer.writeLine(`root: JSResource.fromModuleId('${resourceName}'),`);
|
|
408
|
-
|
|
409
|
-
writer
|
|
410
|
-
.write(`getPreloadProps(${parseVars ? '{params, schema}' : ''})`)
|
|
411
|
-
.block(() => {
|
|
412
|
-
if (parseVars) {
|
|
413
|
-
writer.writeLine('const variables = schema.parse(params);');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
writer.write('return').block(() => {
|
|
417
|
-
writer
|
|
418
|
-
.write('queries:')
|
|
419
|
-
.block(() => {
|
|
420
|
-
for (const [queryRef, query] of resource.queries.entries()) {
|
|
421
|
-
consumedQueries.add(query);
|
|
422
|
-
|
|
423
|
-
// Determine which variables this specific query needs
|
|
424
|
-
const queryVars = collectQueryParameters(project, [query]);
|
|
425
|
-
const hasVariables = queryVars.size > 0;
|
|
426
|
-
|
|
427
|
-
writer
|
|
428
|
-
.write(`${queryRef}:`)
|
|
429
|
-
.block(() => {
|
|
430
|
-
writer.writeLine(`parameters: ${query}Parameters,`);
|
|
431
|
-
|
|
432
|
-
if (hasVariables) {
|
|
433
|
-
const varNames = Array.from(queryVars.keys());
|
|
434
|
-
// Always pick from the variables object
|
|
435
|
-
writer.write(`variables: {`);
|
|
436
|
-
writer.write(
|
|
437
|
-
varNames.map((v) => `${v}: variables.${v}`).join(', '),
|
|
438
|
-
);
|
|
439
|
-
writer.write(`}`);
|
|
440
|
-
} else {
|
|
441
|
-
// Query has no variables, pass empty object
|
|
442
|
-
writer.write(`variables: {}`);
|
|
443
|
-
}
|
|
444
|
-
writer.newLine();
|
|
445
|
-
})
|
|
446
|
-
.write(',');
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
.writeLine(',');
|
|
450
|
-
|
|
451
|
-
writer.write('entryPoints:').block(() => {
|
|
452
|
-
for (const [
|
|
453
|
-
epRef,
|
|
454
|
-
subresourceName,
|
|
455
|
-
] of resource.entryPoints.entries()) {
|
|
456
|
-
const subresource = metadata.resources.get(subresourceName);
|
|
457
|
-
if (subresource) {
|
|
458
|
-
writer
|
|
459
|
-
.write(`${epRef}:`)
|
|
460
|
-
.block(() => {
|
|
461
|
-
writer.writeLine(`entryPointParams: {},`);
|
|
462
|
-
writer.write('entryPoint:').block(() => {
|
|
463
|
-
writeEntryPoint(
|
|
464
|
-
writer,
|
|
465
|
-
project,
|
|
466
|
-
metadata,
|
|
467
|
-
consumedQueries,
|
|
468
|
-
subresourceName,
|
|
469
|
-
subresource,
|
|
470
|
-
false,
|
|
471
|
-
);
|
|
472
|
-
});
|
|
473
|
-
})
|
|
474
|
-
.writeLine(',');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async function generateRouter(project: Project, metadata: PastoriaMetadata) {
|
|
483
|
-
const routerTemplate = await loadRouterTemplates(project, 'router.tsx');
|
|
484
|
-
const tc = project.getTypeChecker().compilerObject;
|
|
485
|
-
|
|
486
|
-
let didAddJsResourceImport = false;
|
|
487
|
-
const routerConf = routerTemplate
|
|
488
|
-
.getVariableDeclarationOrThrow('ROUTER_CONF')
|
|
489
|
-
.getInitializerIfKindOrThrow(SyntaxKind.AsExpression)
|
|
490
|
-
.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
491
|
-
|
|
492
|
-
routerConf.getPropertyOrThrow('noop').remove();
|
|
493
|
-
|
|
494
|
-
let entryPointImportIndex = 0;
|
|
495
|
-
for (let [
|
|
496
|
-
routeName,
|
|
497
|
-
{sourceFile, symbol, params},
|
|
498
|
-
] of metadata.routes.entries()) {
|
|
499
|
-
const filePath = path.relative(process.cwd(), sourceFile.getFilePath());
|
|
500
|
-
let entryPointExpression: string;
|
|
501
|
-
|
|
502
|
-
// Resource-routes are combined declarations of a resource and a route
|
|
503
|
-
// where we should generate the entrypoint for the route.
|
|
504
|
-
const isResourceRoute = Array.from(metadata.resources.entries()).find(
|
|
505
|
-
([, {symbol: resourceSymbol}]) => symbol === resourceSymbol,
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
if (isResourceRoute) {
|
|
509
|
-
const [resourceName, resource] = isResourceRoute;
|
|
510
|
-
const entryPointFunctionName = `entrypoint_${resourceName.replace(/\W/g, '')}`;
|
|
511
|
-
|
|
512
|
-
if (!didAddJsResourceImport) {
|
|
513
|
-
didAddJsResourceImport = true;
|
|
514
|
-
routerTemplate.addImportDeclaration({
|
|
515
|
-
moduleSpecifier: './js_resource',
|
|
516
|
-
namedImports: ['JSResource', 'ModuleType'],
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const consumedQueries = new Set<string>();
|
|
521
|
-
routerTemplate.addFunction({
|
|
522
|
-
name: entryPointFunctionName,
|
|
523
|
-
returnType: `EntryPoint<ModuleType<'${resourceName}'>, EntryPointParams<'${routeName}'>>`,
|
|
524
|
-
statements: (writer) => {
|
|
525
|
-
writer.write('return ').block(() => {
|
|
526
|
-
writeEntryPoint(
|
|
527
|
-
writer,
|
|
528
|
-
project,
|
|
529
|
-
metadata,
|
|
530
|
-
consumedQueries,
|
|
531
|
-
resourceName,
|
|
532
|
-
resource,
|
|
533
|
-
);
|
|
534
|
-
});
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
if (params.size === 0 && consumedQueries.size > 0) {
|
|
539
|
-
params = collectQueryParameters(project, Array.from(consumedQueries));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
for (const query of consumedQueries) {
|
|
543
|
-
routerTemplate.addImportDeclaration({
|
|
544
|
-
moduleSpecifier: `#genfiles/queries/${query}$parameters`,
|
|
545
|
-
defaultImport: `${query}Parameters`,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
entryPointExpression = entryPointFunctionName + '()';
|
|
550
|
-
} else {
|
|
551
|
-
const importAlias = `e${entryPointImportIndex++}`;
|
|
552
|
-
const moduleSpecifier = routerTemplate.getRelativePathAsModuleSpecifierTo(
|
|
553
|
-
sourceFile.getFilePath(),
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
routerTemplate.addImportDeclaration({
|
|
557
|
-
moduleSpecifier,
|
|
558
|
-
namedImports: [
|
|
559
|
-
{
|
|
560
|
-
name: symbol.getName(),
|
|
561
|
-
alias: importAlias,
|
|
562
|
-
},
|
|
563
|
-
],
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
entryPointExpression = importAlias;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
routerConf.addPropertyAssignment({
|
|
570
|
-
name: `"${routeName}"`,
|
|
571
|
-
initializer: (writer) => {
|
|
572
|
-
writer
|
|
573
|
-
.write('{')
|
|
574
|
-
.indent(() => {
|
|
575
|
-
writer.writeLine(`entrypoint: ${entryPointExpression},`);
|
|
576
|
-
if (params.size === 0) {
|
|
577
|
-
writer.writeLine(`schema: z.object({})`);
|
|
578
|
-
} else {
|
|
579
|
-
writer.writeLine(`schema: z.object({`);
|
|
580
|
-
for (const [paramName, paramType] of Array.from(params)) {
|
|
581
|
-
writer.writeLine(
|
|
582
|
-
` ${paramName}: ${zodSchemaOfType(routerTemplate, tc, paramType)},`,
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
writer.writeLine('})');
|
|
587
|
-
}
|
|
588
|
-
})
|
|
589
|
-
.write('} as const');
|
|
590
|
-
},
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
logInfo(
|
|
594
|
-
'Created route',
|
|
595
|
-
pc.cyan(routeName),
|
|
596
|
-
'for',
|
|
597
|
-
pc.green(symbol.getName()),
|
|
598
|
-
'exported from',
|
|
599
|
-
pc.yellow(filePath),
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
await routerTemplate.save();
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async function generateJsResource(
|
|
607
|
-
project: Project,
|
|
608
|
-
metadata: PastoriaMetadata,
|
|
609
|
-
) {
|
|
610
|
-
const jsResourceTemplate = await loadRouterTemplates(
|
|
611
|
-
project,
|
|
612
|
-
'js_resource.ts',
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
const resourceConf = jsResourceTemplate
|
|
616
|
-
.getVariableDeclarationOrThrow('RESOURCE_CONF')
|
|
617
|
-
.getInitializerIfKindOrThrow(SyntaxKind.AsExpression)
|
|
618
|
-
.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
619
|
-
|
|
620
|
-
resourceConf.getPropertyOrThrow('noop').remove();
|
|
621
|
-
for (const [
|
|
622
|
-
resourceName,
|
|
623
|
-
{sourceFile, symbol},
|
|
624
|
-
] of metadata.resources.entries()) {
|
|
625
|
-
const filePath = path.relative(process.cwd(), sourceFile.getFilePath());
|
|
626
|
-
const moduleSpecifier =
|
|
627
|
-
jsResourceTemplate.getRelativePathAsModuleSpecifierTo(
|
|
628
|
-
sourceFile.getFilePath(),
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
resourceConf.addPropertyAssignment({
|
|
632
|
-
name: `"${resourceName}"`,
|
|
633
|
-
initializer: (writer) => {
|
|
634
|
-
writer.block(() => {
|
|
635
|
-
writer
|
|
636
|
-
.writeLine(`src: "${filePath}",`)
|
|
637
|
-
.writeLine(
|
|
638
|
-
`loader: () => import("${moduleSpecifier}").then(m => m.${symbol.getName()})`,
|
|
639
|
-
);
|
|
640
|
-
});
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
logInfo(
|
|
645
|
-
'Created resource',
|
|
646
|
-
pc.cyan(resourceName),
|
|
647
|
-
'for',
|
|
648
|
-
pc.green(symbol.getName()),
|
|
649
|
-
'exported from',
|
|
650
|
-
pc.yellow(filePath),
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
await jsResourceTemplate.save();
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function generateAppRoot(project: Project, metadata: PastoriaMetadata) {
|
|
658
|
-
const targetDir = process.cwd();
|
|
659
|
-
const appRoot: ExportedSymbol | null = metadata.appRoot;
|
|
660
|
-
|
|
661
|
-
if (appRoot == null) {
|
|
662
|
-
await project
|
|
663
|
-
.getSourceFile('__generated__/router/app_root.ts')
|
|
664
|
-
?.deleteImmediately();
|
|
665
|
-
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const appRootSourceFile: SourceFile = appRoot.sourceFile;
|
|
670
|
-
const appRootSymbol: Symbol = appRoot.symbol;
|
|
671
|
-
const filePath = path.relative(targetDir, appRootSourceFile.getFilePath());
|
|
672
|
-
const appRootFile = project.createSourceFile(
|
|
673
|
-
'__generated__/router/app_root.ts',
|
|
674
|
-
'',
|
|
675
|
-
{overwrite: true},
|
|
676
|
-
);
|
|
677
|
-
|
|
678
|
-
const moduleSpecifier = appRootFile.getRelativePathAsModuleSpecifierTo(
|
|
679
|
-
appRootSourceFile.getFilePath(),
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
appRootFile.addStatements(`/*
|
|
683
|
-
* This file was generated by \`pastoria\`.
|
|
684
|
-
* Do not modify this file directly.
|
|
685
|
-
*/
|
|
686
|
-
|
|
687
|
-
export {${appRootSymbol.getName()} as App} from '${moduleSpecifier}';
|
|
688
|
-
`);
|
|
689
|
-
|
|
690
|
-
await appRootFile.save();
|
|
691
|
-
|
|
692
|
-
logInfo(
|
|
693
|
-
'Created app root for',
|
|
694
|
-
pc.green(appRootSymbol.getName()),
|
|
695
|
-
'exported from',
|
|
696
|
-
pc.yellow(filePath),
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async function generateGraphqlContext(
|
|
701
|
-
project: Project,
|
|
702
|
-
metadata: PastoriaMetadata,
|
|
703
|
-
) {
|
|
704
|
-
const targetDir = process.cwd();
|
|
705
|
-
const gqlContext: ExportedSymbol | null = metadata.gqlContext;
|
|
706
|
-
const contextFile = project.createSourceFile(
|
|
707
|
-
'__generated__/router/context.ts',
|
|
708
|
-
'',
|
|
709
|
-
{overwrite: true},
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
if (gqlContext != null) {
|
|
713
|
-
const contextSourceFile: SourceFile = gqlContext.sourceFile;
|
|
714
|
-
const contextSymbol: Symbol = gqlContext.symbol;
|
|
715
|
-
const filePath = path.relative(targetDir, contextSourceFile.getFilePath());
|
|
716
|
-
const moduleSpecifier = contextFile.getRelativePathAsModuleSpecifierTo(
|
|
717
|
-
contextSourceFile.getFilePath(),
|
|
718
|
-
);
|
|
719
|
-
|
|
720
|
-
contextFile.addStatements(`/*
|
|
721
|
-
* This file was generated by \`pastoria\`.
|
|
722
|
-
* Do not modify this file directly.
|
|
723
|
-
*/
|
|
724
|
-
|
|
725
|
-
export {${contextSymbol.getName()} as Context} from '${moduleSpecifier}';
|
|
726
|
-
`);
|
|
727
|
-
|
|
728
|
-
logInfo(
|
|
729
|
-
'Created GraphQL context for',
|
|
730
|
-
pc.green(contextSymbol.getName()),
|
|
731
|
-
'exported from',
|
|
732
|
-
pc.yellow(filePath),
|
|
733
|
-
);
|
|
734
|
-
} else {
|
|
735
|
-
contextFile.addStatements(`/*
|
|
736
|
-
* This file was generated by \`pastoria\`.
|
|
737
|
-
* Do not modify this file directly.
|
|
738
|
-
*/
|
|
739
|
-
|
|
740
|
-
import {PastoriaRootContext} from 'pastoria-runtime/server';
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* @gqlContext
|
|
744
|
-
*/
|
|
745
|
-
export class Context extends PastoriaRootContext {}
|
|
746
|
-
`);
|
|
747
|
-
|
|
748
|
-
logInfo('No @gqlContext found, generating default', pc.green('Context'));
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
await contextFile.save();
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
async function generateServerHandler(
|
|
755
|
-
project: Project,
|
|
756
|
-
metadata: PastoriaMetadata,
|
|
757
|
-
) {
|
|
758
|
-
if (metadata.serverHandlers.size === 0) {
|
|
759
|
-
await project
|
|
760
|
-
.getSourceFile('__generated__/router/server_handler.ts')
|
|
761
|
-
?.deleteImmediately();
|
|
762
|
-
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const sourceText = `import express from 'express';
|
|
767
|
-
export const router = express.Router();
|
|
768
|
-
`;
|
|
769
|
-
|
|
770
|
-
const serverHandlerTemplate = project.createSourceFile(
|
|
771
|
-
'__generated__/router/server_handler.ts',
|
|
772
|
-
sourceText,
|
|
773
|
-
{overwrite: true},
|
|
774
|
-
);
|
|
775
|
-
|
|
776
|
-
let serverHandlerImportIndex = 0;
|
|
777
|
-
for (const [
|
|
778
|
-
routeName,
|
|
779
|
-
{symbol, sourceFile},
|
|
780
|
-
] of metadata.serverHandlers.entries()) {
|
|
781
|
-
const importAlias = `e${serverHandlerImportIndex++}`;
|
|
782
|
-
const filePath = path.relative(process.cwd(), sourceFile.getFilePath());
|
|
783
|
-
const moduleSpecifier =
|
|
784
|
-
serverHandlerTemplate.getRelativePathAsModuleSpecifierTo(
|
|
785
|
-
sourceFile.getFilePath(),
|
|
786
|
-
);
|
|
787
|
-
|
|
788
|
-
serverHandlerTemplate.addImportDeclaration({
|
|
789
|
-
moduleSpecifier,
|
|
790
|
-
namedImports: [{name: symbol.getName(), alias: importAlias}],
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
serverHandlerTemplate.addStatements(
|
|
794
|
-
`router.use('${routeName}', ${importAlias})`,
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
logInfo(
|
|
798
|
-
'Created server handler',
|
|
799
|
-
pc.cyan(routeName),
|
|
800
|
-
'for',
|
|
801
|
-
pc.green(symbol.getName()),
|
|
802
|
-
'exported from',
|
|
803
|
-
pc.yellow(filePath),
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
await serverHandlerTemplate.save();
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export async function generatePastoriaExports(project: Project) {
|
|
811
|
-
const metadata = collectPastoriaMetadata(project);
|
|
812
|
-
|
|
813
|
-
await generateAppRoot(project, metadata);
|
|
814
|
-
await generateGraphqlContext(project, metadata);
|
|
815
|
-
return metadata;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
export async function generatePastoriaArtifacts(
|
|
819
|
-
project: Project,
|
|
820
|
-
metadata = collectPastoriaMetadata(project),
|
|
821
|
-
) {
|
|
822
|
-
await generateRouter(project, metadata);
|
|
823
|
-
await generateJsResource(project, metadata);
|
|
824
|
-
await generateServerHandler(project, metadata);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
export interface PastoriaCapabilities {
|
|
828
|
-
hasAppRoot: boolean;
|
|
829
|
-
hasServerHandler: boolean;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export function generateClientEntry({
|
|
833
|
-
hasAppRoot,
|
|
834
|
-
}: PastoriaCapabilities): string {
|
|
835
|
-
const appImport = hasAppRoot
|
|
836
|
-
? `import {App} from '#genfiles/router/app_root';`
|
|
837
|
-
: '';
|
|
838
|
-
const appValue = hasAppRoot ? 'App' : 'null';
|
|
839
|
-
|
|
840
|
-
return `// Generated by Pastoria.
|
|
841
|
-
import {createRouterApp} from '#genfiles/router/router';
|
|
842
|
-
${appImport}
|
|
843
|
-
import {hydrateRoot} from 'react-dom/client';
|
|
844
|
-
|
|
845
|
-
async function main() {
|
|
846
|
-
const RouterApp = await createRouterApp();
|
|
847
|
-
hydrateRoot(document, <RouterApp App={${appValue}} />);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
main();
|
|
851
|
-
`;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
export function generateServerEntry({
|
|
855
|
-
hasAppRoot,
|
|
856
|
-
hasServerHandler,
|
|
857
|
-
}: PastoriaCapabilities): string {
|
|
858
|
-
const appImport = hasAppRoot
|
|
859
|
-
? `import {App} from '#genfiles/router/app_root';`
|
|
860
|
-
: '';
|
|
861
|
-
const appValue = hasAppRoot ? 'App' : 'null';
|
|
862
|
-
|
|
863
|
-
const serverHandlerImport = hasServerHandler
|
|
864
|
-
? `import {router as serverHandler} from '#genfiles/router/server_handler';`
|
|
865
|
-
: '';
|
|
866
|
-
const serverHandlerUse = hasServerHandler
|
|
867
|
-
? ' router.use(serverHandler)'
|
|
868
|
-
: '';
|
|
869
|
-
|
|
870
|
-
return `// Generated by Pastoria.
|
|
871
|
-
import {JSResource} from '#genfiles/router/js_resource';
|
|
872
|
-
import {
|
|
873
|
-
listRoutes,
|
|
874
|
-
router__createAppFromEntryPoint,
|
|
875
|
-
router__loadEntryPoint,
|
|
876
|
-
} from '#genfiles/router/router';
|
|
877
|
-
import {getSchema} from '#genfiles/schema/schema';
|
|
878
|
-
import {Context} from '#genfiles/router/context';
|
|
879
|
-
${appImport}
|
|
880
|
-
${serverHandlerImport}
|
|
881
|
-
import express from 'express';
|
|
882
|
-
import {GraphQLSchema, specifiedDirectives} from 'graphql';
|
|
883
|
-
import {PastoriaConfig} from 'pastoria-config';
|
|
884
|
-
import {createRouterHandler} from 'pastoria-runtime/server';
|
|
885
|
-
import type {Manifest} from 'vite';
|
|
886
|
-
|
|
887
|
-
const schemaConfig = getSchema().toConfig();
|
|
888
|
-
const schema = new GraphQLSchema({
|
|
889
|
-
...schemaConfig,
|
|
890
|
-
directives: [...specifiedDirectives, ...schemaConfig.directives],
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
export function createHandler(
|
|
894
|
-
persistedQueries: Record<string, string>,
|
|
895
|
-
config: Required<PastoriaConfig>,
|
|
896
|
-
manifest?: Manifest,
|
|
897
|
-
) {
|
|
898
|
-
const routeHandler = createRouterHandler(
|
|
899
|
-
listRoutes(),
|
|
900
|
-
JSResource.srcOfModuleId,
|
|
901
|
-
router__loadEntryPoint,
|
|
902
|
-
router__createAppFromEntryPoint,
|
|
903
|
-
${appValue},
|
|
904
|
-
schema,
|
|
905
|
-
(req) => Context.createFromRequest(req),
|
|
906
|
-
persistedQueries,
|
|
907
|
-
config,
|
|
908
|
-
manifest,
|
|
909
|
-
);
|
|
910
|
-
|
|
911
|
-
const router = express.Router();
|
|
912
|
-
router.use(routeHandler);
|
|
913
|
-
${serverHandlerUse}
|
|
914
|
-
|
|
915
|
-
return router;
|
|
916
|
-
}
|
|
917
|
-
`;
|
|
918
|
-
}
|