pastoria 1.0.12 → 1.0.13

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 CHANGED
@@ -31,15 +31,14 @@ import * as path from 'node:path';
31
31
  import pc from 'picocolors';
32
32
  import {
33
33
  CodeBlockWriter,
34
- IndentationText,
35
34
  Project,
36
35
  SourceFile,
37
36
  Symbol,
38
37
  SyntaxKind,
39
38
  ts,
40
39
  TypeFlags,
41
- WriterFunction,
42
40
  } from 'ts-morph';
41
+ import {logInfo, logWarn} from './logger.js';
43
42
 
44
43
  async function loadRouterTemplates(project: Project, filename: string) {
45
44
  async function loadSourceFile(fileName: string, templateFileName: string) {
@@ -90,6 +89,36 @@ export interface PastoriaMetadata {
90
89
  export const PASTORIA_TAG_REGEX =
91
90
  /@(route|resource|appRoot|param|gqlContext|serverRoute)\b/;
92
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
+
93
122
  function collectPastoriaMetadata(project: Project): PastoriaMetadata {
94
123
  const resources = new Map<string, RouterResource>();
95
124
  const routes = new Map<string, RouterRoute>();
@@ -158,10 +187,7 @@ function collectPastoriaMetadata(project: Project): PastoriaMetadata {
158
187
  switch (tag.tagName.getText()) {
159
188
  case 'appRoot': {
160
189
  if (appRoot != null) {
161
- console.warn(
162
- pc.yellow('Warning:'),
163
- 'Multiple @appRoot tags found. Using the first one.',
164
- );
190
+ logWarn('Multiple @appRoot tags found. Using the first one.');
165
191
  } else {
166
192
  appRoot = {
167
193
  sourceFile,
@@ -195,8 +221,7 @@ function collectPastoriaMetadata(project: Project): PastoriaMetadata {
195
221
 
196
222
  if (extendsPastoriaRootContext) {
197
223
  if (gqlContext != null) {
198
- console.warn(
199
- pc.yellow('Warning:'),
224
+ logWarn(
200
225
  'Multiple classes with @gqlContext extending PastoriaRootContext found. Using the first one.',
201
226
  );
202
227
  } else {
@@ -308,35 +333,55 @@ function getResourceQueriesAndEntryPoints(symbol: Symbol): {
308
333
  return resource;
309
334
  }
310
335
 
311
- function zodSchemaOfType(tc: ts.TypeChecker, t: ts.Type): string {
312
- if (t.getFlags() & TypeFlags.String) {
336
+ function zodSchemaOfType(
337
+ sf: SourceFile,
338
+ tc: ts.TypeChecker,
339
+ t: ts.Type,
340
+ ): string {
341
+ if (t.aliasSymbol) {
342
+ const decl = t.aliasSymbol.declarations?.at(0);
343
+ if (decl == null) {
344
+ logWarn('Could not handle type:', tc.typeToString(t));
345
+ return `z.any()`;
346
+ } else {
347
+ const importPath = sf.getRelativePathAsModuleSpecifierTo(
348
+ decl.getSourceFile().fileName,
349
+ );
350
+
351
+ return `z.transform((s: string) => s as import('${importPath}').${t.aliasSymbol.getName()})`;
352
+ }
353
+ } else if (t.getFlags() & TypeFlags.String) {
313
354
  return `z.pipe(z.string(), z.transform(decodeURIComponent))`;
314
355
  } else if (t.getFlags() & TypeFlags.Number) {
315
356
  return `z.coerce.number<number>()`;
316
357
  } else if (t.getFlags() & TypeFlags.Null) {
317
358
  return `z.preprocess(s => s == null ? undefined : s, z.undefined())`;
318
359
  } else if (t.isUnion()) {
319
- const isRepresentingOptional =
320
- t.types.length === 2 &&
321
- t.types.some((s) => s.getFlags() & TypeFlags.Null);
322
-
323
- if (isRepresentingOptional) {
324
- const nonOptionalType = t.types.find(
325
- (s) => !(s.getFlags() & TypeFlags.Null),
326
- )!;
360
+ const nullishTypes: ts.Type[] = [];
361
+ const nonNullishTypes: ts.Type[] = [];
362
+ for (const s of t.types) {
363
+ const flags = s.getFlags();
364
+ if (flags & TypeFlags.Null || flags & TypeFlags.Undefined) {
365
+ nullishTypes.push(s);
366
+ } else {
367
+ nonNullishTypes.push(s);
368
+ }
369
+ }
327
370
 
328
- return `z.pipe(z.nullish(${zodSchemaOfType(tc, nonOptionalType)}), z.transform(s => s == null ? undefined : s))`;
371
+ if (nullishTypes.length > 0 && nonNullishTypes.length > 0) {
372
+ const nonOptionalType = t.getNonNullableType();
373
+ return `z.pipe(z.nullish(${zodSchemaOfType(sf, tc, nonOptionalType)}), z.transform(s => s == null ? undefined : s))`;
329
374
  } else {
330
- return `z.union([${t.types.map((it) => zodSchemaOfType(tc, it)).join(', ')}])`;
375
+ return `z.union([${t.types.map((it) => zodSchemaOfType(sf, tc, it)).join(', ')}])`;
331
376
  }
332
377
  } else if (tc.isArrayLikeType(t)) {
333
378
  const typeArg = tc.getTypeArguments(t as ts.TypeReference)[0];
334
379
  const argZodSchema =
335
- typeArg == null ? `z.any()` : zodSchemaOfType(tc, typeArg);
380
+ typeArg == null ? `z.any()` : zodSchemaOfType(sf, tc, typeArg);
336
381
 
337
382
  return `z.array(${argZodSchema})`;
338
383
  } else {
339
- console.log('Could not handle type:', tc.typeToString(t));
384
+ logWarn('Could not handle type:', tc.typeToString(t));
340
385
  return `z.any()`;
341
386
  }
342
387
  }
@@ -415,7 +460,7 @@ async function generateRouter(project: Project, metadata: PastoriaMetadata) {
415
460
  routerConf.getPropertyOrThrow('noop').remove();
416
461
 
417
462
  let entryPointImportIndex = 0;
418
- for (const [
463
+ for (let [
419
464
  routeName,
420
465
  {sourceFile, symbol, params},
421
466
  ] of metadata.routes.entries()) {
@@ -457,6 +502,10 @@ async function generateRouter(project: Project, metadata: PastoriaMetadata) {
457
502
  },
458
503
  });
459
504
 
505
+ if (params.size === 0 && consumedQueries.size > 0) {
506
+ params = collectQueryParameters(project, Array.from(consumedQueries));
507
+ }
508
+
460
509
  for (const query of consumedQueries) {
461
510
  routerTemplate.addImportDeclaration({
462
511
  moduleSpecifier: `#genfiles/queries/${query}$parameters`,
@@ -497,7 +546,7 @@ async function generateRouter(project: Project, metadata: PastoriaMetadata) {
497
546
  writer.writeLine(`schema: z.object({`);
498
547
  for (const [paramName, paramType] of Array.from(params)) {
499
548
  writer.writeLine(
500
- ` ${paramName}: ${zodSchemaOfType(tc, paramType)},`,
549
+ ` ${paramName}: ${zodSchemaOfType(routerTemplate, tc, paramType)},`,
501
550
  );
502
551
  }
503
552
 
@@ -508,7 +557,7 @@ async function generateRouter(project: Project, metadata: PastoriaMetadata) {
508
557
  },
509
558
  });
510
559
 
511
- console.log(
560
+ logInfo(
512
561
  'Created route',
513
562
  pc.cyan(routeName),
514
563
  'for',
@@ -559,7 +608,7 @@ async function generateJsResource(
559
608
  },
560
609
  });
561
610
 
562
- console.log(
611
+ logInfo(
563
612
  'Created resource',
564
613
  pc.cyan(resourceName),
565
614
  'for',
@@ -607,7 +656,7 @@ export {${appRootSymbol.getName()} as App} from '${moduleSpecifier}';
607
656
 
608
657
  await appRootFile.save();
609
658
 
610
- console.log(
659
+ logInfo(
611
660
  'Created app root for',
612
661
  pc.green(appRootSymbol.getName()),
613
662
  'exported from',
@@ -643,7 +692,7 @@ async function generateGraphqlContext(
643
692
  export {${contextSymbol.getName()} as Context} from '${moduleSpecifier}';
644
693
  `);
645
694
 
646
- console.log(
695
+ logInfo(
647
696
  'Created GraphQL context for',
648
697
  pc.green(contextSymbol.getName()),
649
698
  'exported from',
@@ -663,10 +712,7 @@ import {PastoriaRootContext} from 'pastoria-runtime/server';
663
712
  export class Context extends PastoriaRootContext {}
664
713
  `);
665
714
 
666
- console.log(
667
- 'No @gqlContext found, generating default',
668
- pc.green('Context'),
669
- );
715
+ logInfo('No @gqlContext found, generating default', pc.green('Context'));
670
716
  }
671
717
 
672
718
  await contextFile.save();
@@ -715,7 +761,7 @@ export const router = express.Router();
715
761
  `router.use('${routeName}', ${importAlias})`,
716
762
  );
717
763
 
718
- console.log(
764
+ logInfo(
719
765
  'Created server handler',
720
766
  pc.cyan(routeName),
721
767
  'for',
@@ -744,3 +790,96 @@ export async function generatePastoriaArtifacts(
744
790
  await generateJsResource(project, metadata);
745
791
  await generateServerHandler(project, metadata);
746
792
  }
793
+
794
+ export interface PastoriaCapabilities {
795
+ hasAppRoot: boolean;
796
+ hasServerHandler: boolean;
797
+ }
798
+
799
+ export function generateClientEntry({
800
+ hasAppRoot,
801
+ }: PastoriaCapabilities): string {
802
+ const appImport = hasAppRoot
803
+ ? `import {App} from '#genfiles/router/app_root';`
804
+ : '';
805
+ const appValue = hasAppRoot ? 'App' : 'null';
806
+
807
+ return `// Generated by Pastoria.
808
+ import {createRouterApp} from '#genfiles/router/router';
809
+ ${appImport}
810
+ import {hydrateRoot} from 'react-dom/client';
811
+
812
+ async function main() {
813
+ const RouterApp = await createRouterApp();
814
+ hydrateRoot(document, <RouterApp App={${appValue}} />);
815
+ }
816
+
817
+ main();
818
+ `;
819
+ }
820
+
821
+ export function generateServerEntry({
822
+ hasAppRoot,
823
+ hasServerHandler,
824
+ }: PastoriaCapabilities): string {
825
+ const appImport = hasAppRoot
826
+ ? `import {App} from '#genfiles/router/app_root';`
827
+ : '';
828
+ const appValue = hasAppRoot ? 'App' : 'null';
829
+
830
+ const serverHandlerImport = hasServerHandler
831
+ ? `import {router as serverHandler} from '#genfiles/router/server_handler';`
832
+ : '';
833
+ const serverHandlerUse = hasServerHandler
834
+ ? ' router.use(serverHandler)'
835
+ : '';
836
+
837
+ return `// Generated by Pastoria.
838
+ import {JSResource} from '#genfiles/router/js_resource';
839
+ import {
840
+ listRoutes,
841
+ router__createAppFromEntryPoint,
842
+ router__loadEntryPoint,
843
+ } from '#genfiles/router/router';
844
+ import {getSchema} from '#genfiles/schema/schema';
845
+ import {Context} from '#genfiles/router/context';
846
+ ${appImport}
847
+ ${serverHandlerImport}
848
+ import express from 'express';
849
+ import {GraphQLSchema, specifiedDirectives} from 'graphql';
850
+ import {PastoriaConfig} from 'pastoria-config';
851
+ import {createRouterHandler} from 'pastoria-runtime/server';
852
+ import type {Manifest} from 'vite';
853
+
854
+ const schemaConfig = getSchema().toConfig();
855
+ const schema = new GraphQLSchema({
856
+ ...schemaConfig,
857
+ directives: [...specifiedDirectives, ...schemaConfig.directives],
858
+ });
859
+
860
+ export function createHandler(
861
+ persistedQueries: Record<string, string>,
862
+ config: Required<PastoriaConfig>,
863
+ manifest?: Manifest,
864
+ ) {
865
+ const routeHandler = createRouterHandler(
866
+ listRoutes(),
867
+ JSResource.srcOfModuleId,
868
+ router__loadEntryPoint,
869
+ router__createAppFromEntryPoint,
870
+ ${appValue},
871
+ schema,
872
+ (req) => Context.createFromRequest(req),
873
+ persistedQueries,
874
+ config,
875
+ manifest,
876
+ );
877
+
878
+ const router = express.Router();
879
+ router.use(routeHandler);
880
+ ${serverHandlerUse}
881
+
882
+ return router;
883
+ }
884
+ `;
885
+ }
package/src/index.ts CHANGED
@@ -30,7 +30,7 @@ async function main() {
30
30
  'Specific build steps to run (schema, relay, router). If not provided, will infer from changed files.',
31
31
  )
32
32
  .option('-B, --always-make', 'Always make, never cache')
33
- .option('--release', 'Build for production')
33
+ .option('-R, --release', 'Build for production')
34
34
  .option('-w, --watch', 'Watch for changes and rebuild')
35
35
  .action(createBuild);
36
36
 
package/src/logger.ts ADDED
@@ -0,0 +1,12 @@
1
+ import {createLogger} from 'vite';
2
+ import pc from 'picocolors';
3
+
4
+ export const logger = createLogger('info', {prefix: pc.greenBright('[MAKE]')});
5
+
6
+ export function logInfo(...messages: string[]) {
7
+ logger.info(messages.join(' '), {timestamp: true});
8
+ }
9
+
10
+ export function logWarn(...messages: string[]) {
11
+ logger.warn(messages.join(' '), {timestamp: true});
12
+ }
@@ -0,0 +1,109 @@
1
+ import tailwindcss from '@tailwindcss/vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import {access} from 'node:fs/promises';
4
+ import {
5
+ InlineConfig,
6
+ PluginOption,
7
+ type BuildEnvironmentOptions,
8
+ type Plugin,
9
+ } from 'vite';
10
+ import {cjsInterop} from 'vite-plugin-cjs-interop';
11
+ import {
12
+ generateClientEntry,
13
+ generateServerEntry,
14
+ PastoriaCapabilities,
15
+ } from './generate.js';
16
+ import {logger} from './logger.js';
17
+
18
+ async function determineCapabilities(): Promise<PastoriaCapabilities> {
19
+ const capabilities: PastoriaCapabilities = {
20
+ hasAppRoot: false,
21
+ hasServerHandler: false,
22
+ };
23
+
24
+ async function hasAppRoot() {
25
+ try {
26
+ await access('__generated__/router/app_root.ts');
27
+ capabilities.hasAppRoot = true;
28
+ } catch {}
29
+ }
30
+
31
+ async function hasServerHandler() {
32
+ try {
33
+ await access('__generated__/router/server_handler.ts');
34
+ capabilities.hasServerHandler = true;
35
+ } catch {}
36
+ }
37
+
38
+ await Promise.all([hasAppRoot(), hasServerHandler()]);
39
+ return capabilities;
40
+ }
41
+
42
+ function pastoriaEntryPlugin(): Plugin {
43
+ const clientEntryModuleId = 'virtual:pastoria-entry-client.tsx';
44
+ const serverEntryModuleId = 'virtual:pastoria-entry-server.tsx';
45
+
46
+ return {
47
+ name: 'pastoria-entry',
48
+ resolveId(id) {
49
+ if (id === clientEntryModuleId) {
50
+ return clientEntryModuleId; // Return without \0 prefix so React plugin can see .tsx extension
51
+ } else if (id === serverEntryModuleId) {
52
+ return serverEntryModuleId;
53
+ }
54
+ },
55
+ async load(id) {
56
+ const capabilities = await determineCapabilities();
57
+ if (id === clientEntryModuleId) {
58
+ return generateClientEntry(capabilities);
59
+ } else if (id === serverEntryModuleId) {
60
+ return generateServerEntry(capabilities);
61
+ }
62
+ },
63
+ };
64
+ }
65
+
66
+ export const CLIENT_BUILD: BuildEnvironmentOptions = {
67
+ outDir: 'dist/client',
68
+ rollupOptions: {
69
+ input: 'virtual:pastoria-entry-client.tsx',
70
+ },
71
+ };
72
+
73
+ export const SERVER_BUILD: BuildEnvironmentOptions = {
74
+ outDir: 'dist/server',
75
+ ssr: true,
76
+ rollupOptions: {
77
+ input: 'virtual:pastoria-entry-server.tsx',
78
+ },
79
+ };
80
+
81
+ export function createBuildConfig(
82
+ buildEnv: BuildEnvironmentOptions,
83
+ ): InlineConfig {
84
+ return {
85
+ appType: 'custom' as const,
86
+ customLogger: logger,
87
+ build: {
88
+ ...buildEnv,
89
+ assetsInlineLimit: 0,
90
+ manifest: true,
91
+ ssrManifest: true,
92
+ },
93
+ plugins: [
94
+ pastoriaEntryPlugin(),
95
+ tailwindcss() as PluginOption,
96
+ react({
97
+ babel: {
98
+ plugins: [['babel-plugin-react-compiler', {}], 'relay'],
99
+ },
100
+ }),
101
+ cjsInterop({
102
+ dependencies: ['react-relay', 'react-relay/hooks', 'relay-runtime'],
103
+ }),
104
+ ],
105
+ ssr: {
106
+ noExternal: ['pastoria-runtime'],
107
+ },
108
+ };
109
+ }
@@ -245,20 +245,7 @@ export function router__createAppFromEntryPoint(
245
245
 
246
246
  return (
247
247
  <RouterContext value={routerContextValue}>
248
- {'fallback' in entryPoint.entryPoints ? (
249
- <Suspense
250
- fallback={
251
- <EntryPointContainer
252
- entryPointReference={entryPoint.entryPoints.fallback}
253
- props={{}}
254
- />
255
- }
256
- >
257
- <EntryPointContainer entryPointReference={entryPoint} props={{}} />
258
- </Suspense>
259
- ) : (
260
- <EntryPointContainer entryPointReference={entryPoint} props={{}} />
261
- )}
248
+ <EntryPointContainer entryPointReference={entryPoint} props={{}} />
262
249
  </RouterContext>
263
250
  );
264
251
  }
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
4
  "module": "nodenext",
5
- "lib": ["ES2022", "DOM"],
5
+ "lib": ["ES2022", "DOM", "ESNext.Collection"],
6
6
  "moduleResolution": "nodenext",
7
7
  "outDir": "./dist",
8
8
  "rootDir": "./src",