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/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
- }