pastoria 1.1.0 → 1.2.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/dist/devserver.d.ts.map +1 -1
- package/dist/devserver.js +1 -3
- package/dist/devserver.js.map +1 -1
- package/dist/filesystem.d.ts +188 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +358 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/generate.d.ts +47 -50
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +747 -450
- package/dist/generate.js.map +1 -1
- package/dist/index.js +41 -13
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +6 -1
- package/dist/logger.js.map +1 -1
- package/dist/vite_plugin.js +1 -1
- package/dist/vite_plugin.js.map +1 -1
- package/package.json +5 -7
- package/templates/js_resource.ts +7 -1
- package/templates/router.tsx +188 -45
- package/justfile +0 -41
package/dist/generate.js
CHANGED
|
@@ -1,67 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Router Code Generator
|
|
3
3
|
*
|
|
4
|
-
* This script generates type-safe router configuration files
|
|
5
|
-
*
|
|
4
|
+
* This script generates type-safe router configuration files using filesystem-based
|
|
5
|
+
* routing, similar to Next.js App Router.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
|
7
|
+
* ## Filesystem Routing Convention
|
|
16
8
|
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
9
|
+
* Routes are defined by the directory structure under `pastoria/`:
|
|
10
|
+
* - `pastoria/page.tsx` → Route `/`
|
|
11
|
+
* - `pastoria/about/page.tsx` → Route `/about`
|
|
12
|
+
* - `pastoria/post/[slug]/page.tsx` → Route `/post/:slug`
|
|
13
|
+
* - `pastoria/app.tsx` → Root layout component
|
|
14
|
+
* - `pastoria/api/.../route.ts` → API route handlers
|
|
15
|
+
* - `pastoria/*.page.tsx` → Nested entry points
|
|
16
|
+
* - `pastoria/entrypoint.ts` → Manual entry point definitions
|
|
24
17
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
18
|
+
* ## Page Files
|
|
19
|
+
*
|
|
20
|
+
* Each `page.tsx` should:
|
|
21
|
+
* - Default export a React component
|
|
22
|
+
* - Optionally export `queries` object mapping query refs to Relay query types
|
|
23
|
+
*
|
|
24
|
+
* ## Generated Files
|
|
25
|
+
*
|
|
26
|
+
* Output is placed in `__generated__/router/`:
|
|
27
|
+
* - `js_resource.ts` - Resource configuration for lazy loading
|
|
28
|
+
* - `router.tsx` - Client-side router with type-safe routes
|
|
29
|
+
* - `app_root.ts` - Re-export of the app root component
|
|
30
|
+
* - `environment.ts` - Re-export of the user's PastoriaEnvironment
|
|
31
|
+
* - `server_handler.ts` - API route handlers
|
|
32
|
+
* - `types.ts` - PageProps and other type definitions
|
|
27
33
|
*/
|
|
28
34
|
import { readFile } from 'node:fs/promises';
|
|
29
35
|
import * as path from 'node:path';
|
|
30
36
|
import pc from 'picocolors';
|
|
31
|
-
import { SyntaxKind,
|
|
37
|
+
import { SyntaxKind, TypeFlags, } from 'ts-morph';
|
|
38
|
+
import { scanFilesystemRoutes, toRouterPath, } from './filesystem.js';
|
|
32
39
|
import { logInfo, logWarn } from './logger.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Template Loading
|
|
42
|
+
// ============================================================================
|
|
43
|
+
/**
|
|
44
|
+
* Loads a template file and creates a new source file with a generated header.
|
|
45
|
+
*/
|
|
46
|
+
async function loadRouterTemplate(project, filename) {
|
|
47
|
+
const templatePath = path.join(import.meta.dirname, '../templates', filename);
|
|
48
|
+
const outputPath = path.join('__generated__/router', filename);
|
|
49
|
+
const template = await readFile(templatePath, 'utf-8');
|
|
50
|
+
const warningComment = `/*
|
|
37
51
|
* This file was generated by \`pastoria\`.
|
|
38
|
-
* Do not modify this file directly.
|
|
52
|
+
* Do not modify this file directly.
|
|
39
53
|
*/
|
|
40
54
|
|
|
41
55
|
`;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const template = path.join(import.meta.dirname, '../templates', filename);
|
|
47
|
-
const output = path.join('__generated__/router', filename);
|
|
48
|
-
return loadSourceFile(output, template);
|
|
56
|
+
return project.createSourceFile(outputPath, warningComment + template, {
|
|
57
|
+
overwrite: true,
|
|
58
|
+
});
|
|
49
59
|
}
|
|
50
|
-
//
|
|
51
|
-
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Query Parameter Collection
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Collects query variables from Relay-generated query type files.
|
|
65
|
+
*
|
|
66
|
+
* For each query, looks up the generated `${queryName}$variables` type
|
|
67
|
+
* and extracts all property names and types.
|
|
68
|
+
*/
|
|
52
69
|
function collectQueryParameters(project, queries) {
|
|
53
70
|
const vars = new Map();
|
|
54
71
|
for (const query of queries) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
// Find the generated query file
|
|
73
|
+
const queryFile = project.getSourceFile(`__generated__/queries/${query}.graphql.ts`);
|
|
74
|
+
if (!queryFile)
|
|
75
|
+
continue;
|
|
76
|
+
// Get the variables type export
|
|
77
|
+
const variablesType = queryFile
|
|
78
|
+
.getExportedDeclarations()
|
|
58
79
|
.get(`${query}$variables`)
|
|
59
80
|
?.at(0)
|
|
60
81
|
?.getType();
|
|
61
|
-
if (variablesType
|
|
82
|
+
if (!variablesType)
|
|
62
83
|
continue;
|
|
84
|
+
// Extract each property from the variables type
|
|
63
85
|
for (const property of variablesType.getProperties()) {
|
|
64
|
-
// TODO: Detect conflicting types among properties declared.
|
|
65
86
|
const propertyName = property.getName();
|
|
66
87
|
const propertyType = property.getValueDeclaration()?.getType();
|
|
67
88
|
if (propertyType) {
|
|
@@ -71,197 +92,16 @@ function collectQueryParameters(project, queries) {
|
|
|
71
92
|
}
|
|
72
93
|
return vars;
|
|
73
94
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (sourceFile.getFilePath().includes('__generated__')) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// Skip files that don't contain any Pastoria JSDoc tags
|
|
86
|
-
const fileText = sourceFile.getFullText();
|
|
87
|
-
if (!PASTORIA_TAG_REGEX.test(fileText)) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
sourceFile.getExportSymbols().forEach((symbol) => {
|
|
91
|
-
let routerResource = null;
|
|
92
|
-
let routerRoute = null;
|
|
93
|
-
const routeParams = new Map();
|
|
94
|
-
function visitJSDocTags(tag) {
|
|
95
|
-
if (ts.isJSDoc(tag)) {
|
|
96
|
-
tag.tags?.forEach(visitJSDocTags);
|
|
97
|
-
}
|
|
98
|
-
else if (ts.isJSDocParameterTag(tag)) {
|
|
99
|
-
const typeNode = tag.typeExpression?.type;
|
|
100
|
-
const tc = project.getTypeChecker().compilerObject;
|
|
101
|
-
const type = typeNode == null
|
|
102
|
-
? tc.getUnknownType()
|
|
103
|
-
: tc.getTypeFromTypeNode(typeNode);
|
|
104
|
-
routeParams.set(tag.name.getText(), type);
|
|
105
|
-
}
|
|
106
|
-
else if (typeof tag.comment === 'string') {
|
|
107
|
-
switch (tag.tagName.getText()) {
|
|
108
|
-
case 'route': {
|
|
109
|
-
routerRoute = [
|
|
110
|
-
tag.comment,
|
|
111
|
-
{ sourceFile, symbol, params: routeParams },
|
|
112
|
-
];
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case 'resource': {
|
|
116
|
-
routerResource = [
|
|
117
|
-
tag.comment,
|
|
118
|
-
{
|
|
119
|
-
sourceFile,
|
|
120
|
-
symbol,
|
|
121
|
-
queries: new Map(),
|
|
122
|
-
entryPoints: new Map(),
|
|
123
|
-
},
|
|
124
|
-
];
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
case 'serverRoute': {
|
|
128
|
-
serverHandlers.set(tag.comment, { sourceFile, symbol });
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
// Handle tags without comments (like @ExportedSymbol, @gqlContext)
|
|
135
|
-
switch (tag.tagName.getText()) {
|
|
136
|
-
case 'appRoot': {
|
|
137
|
-
if (appRoot != null) {
|
|
138
|
-
logWarn('Multiple @appRoot tags found. Using the first one.');
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
appRoot = {
|
|
142
|
-
sourceFile,
|
|
143
|
-
symbol,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case 'gqlContext': {
|
|
149
|
-
// Check if this class extends PastoriaRootContext
|
|
150
|
-
const declarations = symbol.getDeclarations();
|
|
151
|
-
let extendsPastoriaRootContext = false;
|
|
152
|
-
for (const decl of declarations) {
|
|
153
|
-
if (decl.isKind(SyntaxKind.ClassDeclaration)) {
|
|
154
|
-
const classDecl = decl.asKindOrThrow(SyntaxKind.ClassDeclaration);
|
|
155
|
-
const extendsClause = classDecl.getExtends();
|
|
156
|
-
if (extendsClause != null) {
|
|
157
|
-
const baseClassName = extendsClause
|
|
158
|
-
.getExpression()
|
|
159
|
-
.getText();
|
|
160
|
-
if (baseClassName === 'PastoriaRootContext') {
|
|
161
|
-
extendsPastoriaRootContext = true;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (extendsPastoriaRootContext) {
|
|
168
|
-
if (gqlContext != null) {
|
|
169
|
-
logWarn('Multiple classes with @gqlContext extending PastoriaRootContext found. Using the first one.');
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
gqlContext = {
|
|
173
|
-
sourceFile,
|
|
174
|
-
symbol,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
symbol
|
|
184
|
-
.getDeclarations()
|
|
185
|
-
.flatMap((decl) => ts.getJSDocCommentsAndTags(decl.compilerNode))
|
|
186
|
-
.forEach(visitJSDocTags);
|
|
187
|
-
if (routerRoute != null) {
|
|
188
|
-
const [routeName, routeSymbol] = routerRoute;
|
|
189
|
-
routes.set(routeName, routeSymbol);
|
|
190
|
-
const { entryPoints, queries } = getResourceQueriesAndEntryPoints(routeSymbol.symbol);
|
|
191
|
-
if (routerResource == null && (entryPoints.size || queries.size)) {
|
|
192
|
-
resources.set(`route(${routeName})`, {
|
|
193
|
-
...routeSymbol,
|
|
194
|
-
entryPoints,
|
|
195
|
-
queries,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (routerResource != null) {
|
|
200
|
-
const [resourceName, resourceSymbol] = routerResource;
|
|
201
|
-
const { entryPoints, queries } = getResourceQueriesAndEntryPoints(resourceSymbol.symbol);
|
|
202
|
-
resourceSymbol.queries = queries;
|
|
203
|
-
resourceSymbol.entryPoints = entryPoints;
|
|
204
|
-
resources.set(resourceName, resourceSymbol);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
project.getSourceFiles().forEach(visitRouterNodes);
|
|
209
|
-
return { resources, routes, appRoot, gqlContext, serverHandlers };
|
|
210
|
-
}
|
|
211
|
-
function getResourceQueriesAndEntryPoints(symbol) {
|
|
212
|
-
const resource = {
|
|
213
|
-
queries: new Map(),
|
|
214
|
-
entryPoints: new Map(),
|
|
215
|
-
};
|
|
216
|
-
const decl = symbol.getValueDeclaration();
|
|
217
|
-
if (!decl)
|
|
218
|
-
return resource;
|
|
219
|
-
const t = decl.getType();
|
|
220
|
-
const aliasSymbol = t.getAliasSymbol();
|
|
221
|
-
if (aliasSymbol?.getName() === 'EntryPointComponent') {
|
|
222
|
-
const [queries, entryPoints] = t.getAliasTypeArguments();
|
|
223
|
-
queries?.getProperties().forEach((prop) => {
|
|
224
|
-
const queryRef = prop.getName();
|
|
225
|
-
const queryName = prop
|
|
226
|
-
.getValueDeclaration()
|
|
227
|
-
?.getType()
|
|
228
|
-
.getAliasSymbol()
|
|
229
|
-
?.getName();
|
|
230
|
-
if (queryName) {
|
|
231
|
-
resource.queries.set(queryRef, queryName);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
entryPoints?.getProperties().forEach((prop) => {
|
|
235
|
-
const epRef = prop.getName();
|
|
236
|
-
const entryPointTypeRef = prop
|
|
237
|
-
.getValueDeclaration()
|
|
238
|
-
?.asKind(SyntaxKind.PropertySignature)
|
|
239
|
-
?.getTypeNode()
|
|
240
|
-
?.asKind(SyntaxKind.TypeReference);
|
|
241
|
-
const entryPointTypeName = entryPointTypeRef?.getTypeName().getText();
|
|
242
|
-
if (entryPointTypeName !== 'EntryPoint') {
|
|
243
|
-
// TODO: Warn about found types not named EntryPoint
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const entryPointInner = entryPointTypeRef?.getTypeArguments().at(0);
|
|
247
|
-
const moduleTypeRef = entryPointInner?.asKind(SyntaxKind.TypeReference);
|
|
248
|
-
if (moduleTypeRef != null) {
|
|
249
|
-
const resourceName = moduleTypeRef
|
|
250
|
-
?.getTypeArguments()
|
|
251
|
-
.at(0)
|
|
252
|
-
?.asKind(SyntaxKind.LiteralType)
|
|
253
|
-
?.getLiteral()
|
|
254
|
-
.asKind(SyntaxKind.StringLiteral)
|
|
255
|
-
?.getLiteralText();
|
|
256
|
-
if (resourceName) {
|
|
257
|
-
resource.entryPoints.set(epRef, resourceName);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
return resource;
|
|
263
|
-
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Zod Schema Generation
|
|
97
|
+
// ============================================================================
|
|
98
|
+
/**
|
|
99
|
+
* Generates a Zod schema string for a TypeScript type.
|
|
100
|
+
*
|
|
101
|
+
* Used to create runtime validation schemas for route parameters.
|
|
102
|
+
*/
|
|
264
103
|
function zodSchemaOfType(sf, tc, t) {
|
|
104
|
+
// Handle type aliases (custom types)
|
|
265
105
|
if (t.aliasSymbol) {
|
|
266
106
|
const decl = t.aliasSymbol.declarations?.at(0);
|
|
267
107
|
if (decl == null) {
|
|
@@ -273,16 +113,18 @@ function zodSchemaOfType(sf, tc, t) {
|
|
|
273
113
|
return `z.transform((s: string) => s as import('${importPath}').${t.aliasSymbol.getName()})`;
|
|
274
114
|
}
|
|
275
115
|
}
|
|
276
|
-
|
|
116
|
+
// Handle primitive types
|
|
117
|
+
if (t.getFlags() & TypeFlags.String) {
|
|
277
118
|
return `z.pipe(z.string(), z.transform(decodeURIComponent))`;
|
|
278
119
|
}
|
|
279
|
-
|
|
120
|
+
if (t.getFlags() & TypeFlags.Number) {
|
|
280
121
|
return `z.coerce.number<number>()`;
|
|
281
122
|
}
|
|
282
|
-
|
|
123
|
+
if (t.getFlags() & TypeFlags.Null) {
|
|
283
124
|
return `z.preprocess(s => s == null ? undefined : s, z.undefined())`;
|
|
284
125
|
}
|
|
285
|
-
|
|
126
|
+
// Handle union types
|
|
127
|
+
if (t.isUnion()) {
|
|
286
128
|
const nullishTypes = [];
|
|
287
129
|
const nonNullishTypes = [];
|
|
288
130
|
for (const s of t.types) {
|
|
@@ -302,282 +144,748 @@ function zodSchemaOfType(sf, tc, t) {
|
|
|
302
144
|
return `z.union([${t.types.map((it) => zodSchemaOfType(sf, tc, it)).join(', ')}])`;
|
|
303
145
|
}
|
|
304
146
|
}
|
|
305
|
-
|
|
147
|
+
// Handle array types
|
|
148
|
+
if (tc.isArrayLikeType(t)) {
|
|
306
149
|
const typeArg = tc.getTypeArguments(t)[0];
|
|
307
150
|
const argZodSchema = typeArg == null ? `z.any()` : zodSchemaOfType(sf, tc, typeArg);
|
|
308
151
|
return `z.array(${argZodSchema})`;
|
|
309
152
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
153
|
+
// Fallback
|
|
154
|
+
logWarn('Could not handle type:', tc.typeToString(t));
|
|
155
|
+
return `z.any()`;
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Entry Point Generation
|
|
159
|
+
// ============================================================================
|
|
160
|
+
/**
|
|
161
|
+
* Collects all query names from a page and its nested entry points.
|
|
162
|
+
*/
|
|
163
|
+
function collectAllQueries(page) {
|
|
164
|
+
const queries = new Set();
|
|
165
|
+
// Add main page queries
|
|
166
|
+
for (const queryName of page.queries.values()) {
|
|
167
|
+
queries.add(queryName);
|
|
168
|
+
}
|
|
169
|
+
// Add nested entry point queries
|
|
170
|
+
for (const nestedPage of page.nestedEntryPoints.values()) {
|
|
171
|
+
for (const queryName of nestedPage.queries.values()) {
|
|
172
|
+
queries.add(queryName);
|
|
173
|
+
}
|
|
313
174
|
}
|
|
175
|
+
return queries;
|
|
314
176
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Writes an entry point definition for a filesystem-based page.
|
|
179
|
+
*
|
|
180
|
+
* Generates a getPreloadProps function that:
|
|
181
|
+
* 1. Sets up query preloading with the provided variables
|
|
182
|
+
* 2. Includes any nested entry points
|
|
183
|
+
*
|
|
184
|
+
* Note: Params are already parsed and typed by ROUTER_CONF schema via
|
|
185
|
+
* EntryPointParams<R>, so no additional parsing is needed here.
|
|
186
|
+
*/
|
|
187
|
+
function writeFilesystemEntryPoint(writer, project, page, resourceName, routePath, schemaExpression) {
|
|
188
|
+
const hasNestedEntryPoints = page.nestedEntryPoints.size > 0;
|
|
189
|
+
// Define schema locally so it can be referenced by wrappedGetPreloadProps
|
|
190
|
+
writer.writeLine(`const schema = ${schemaExpression};`);
|
|
191
|
+
// Generate query helper functions
|
|
192
|
+
writer.write('const queryHelpers = ').block(() => {
|
|
193
|
+
for (const [queryRef, queryName] of page.queries.entries()) {
|
|
194
|
+
writer.writeLine(`${queryRef}: (variables: ${queryName}$variables) => ({ parameters: ${queryName}Parameters, variables }),`);
|
|
322
195
|
}
|
|
323
|
-
|
|
196
|
+
});
|
|
197
|
+
writer.writeLine(';');
|
|
198
|
+
// Generate entry point helper functions
|
|
199
|
+
writer.write('const entryPointHelpers = ').block(() => {
|
|
200
|
+
for (const [epName, nestedPage] of page.nestedEntryPoints.entries()) {
|
|
201
|
+
const nestedResourceName = `${resourceName}#${epName}`;
|
|
202
|
+
// Build the combined variables type from all queries in this nested entry point
|
|
203
|
+
const nestedQueryNames = Array.from(nestedPage.queries.values());
|
|
204
|
+
const variablesType = nestedQueryNames.length === 0
|
|
205
|
+
? 'Record<string, never>'
|
|
206
|
+
: nestedQueryNames.map((q) => `${q}$variables`).join(' & ');
|
|
324
207
|
writer
|
|
325
|
-
.write(
|
|
208
|
+
.write(`${epName}: (variables: ${variablesType}) => (`)
|
|
326
209
|
.block(() => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (hasVariables) {
|
|
337
|
-
const varNames = Array.from(queryVars.keys());
|
|
338
|
-
// Always pick from the variables object
|
|
339
|
-
writer.write(`variables: {`);
|
|
340
|
-
writer.write(varNames.map((v) => `${v}: variables.${v}`).join(', '));
|
|
341
|
-
writer.write(`}`);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
// Query has no variables, pass empty object
|
|
345
|
-
writer.write(`variables: {}`);
|
|
346
|
-
}
|
|
347
|
-
writer.newLine();
|
|
348
|
-
})
|
|
349
|
-
.write(',');
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
.writeLine(',');
|
|
353
|
-
writer.write('entryPoints:').block(() => {
|
|
354
|
-
for (const [epRef, subresourceName,] of resource.entryPoints.entries()) {
|
|
355
|
-
const subresource = metadata.resources.get(subresourceName);
|
|
356
|
-
if (subresource) {
|
|
357
|
-
writer
|
|
358
|
-
.write(`${epRef}:`)
|
|
359
|
-
.block(() => {
|
|
360
|
-
writer.writeLine(`entryPointParams: {},`);
|
|
361
|
-
writer.write('entryPoint:').block(() => {
|
|
362
|
-
writeEntryPoint(writer, project, metadata, consumedQueries, subresourceName, subresource, false);
|
|
210
|
+
writer.writeLine('entryPointParams: {},');
|
|
211
|
+
writer.write('entryPoint: ').block(() => {
|
|
212
|
+
writer.writeLine(`root: JSResource.fromModuleId('${nestedResourceName}'),`);
|
|
213
|
+
writer.write('getPreloadProps() ').block(() => {
|
|
214
|
+
writer.write('return ').block(() => {
|
|
215
|
+
writer.write('queries: ').block(() => {
|
|
216
|
+
for (const [nestedQueryRef, nestedQueryName,] of nestedPage.queries.entries()) {
|
|
217
|
+
writer.writeLine(`${nestedQueryRef}: { parameters: ${nestedQueryName}Parameters, variables },`);
|
|
218
|
+
}
|
|
363
219
|
});
|
|
364
|
-
|
|
365
|
-
.writeLine('
|
|
220
|
+
writer.writeLine(',');
|
|
221
|
+
writer.writeLine('entryPoints: undefined');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
writer.writeLine('),');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
writer.writeLine(';');
|
|
230
|
+
// Write getPreloadProps using the helpers
|
|
231
|
+
writer
|
|
232
|
+
.write(`function getPreloadProps({params, queries, entryPoints}: EntryPointParams<'${routePath}'>)`)
|
|
233
|
+
.block(() => {
|
|
234
|
+
// Params are already parsed and typed via EntryPointParams<R>
|
|
235
|
+
writer.writeLine('const variables = params;');
|
|
236
|
+
writer.write('return ').block(() => {
|
|
237
|
+
// Write queries using helpers
|
|
238
|
+
writer.write('queries: ').block(() => {
|
|
239
|
+
for (const [queryRef, _queryName] of page.queries.entries()) {
|
|
240
|
+
const queryVars = collectQueryParameters(project, [
|
|
241
|
+
page.queries.get(queryRef),
|
|
242
|
+
]);
|
|
243
|
+
if (queryVars.size > 0) {
|
|
244
|
+
const varNames = Array.from(queryVars.keys());
|
|
245
|
+
writer.writeLine(`${queryRef}: queries.${queryRef}({${varNames.map((v) => `${v}: variables.${v}`).join(', ')}}),`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
writer.writeLine(`${queryRef}: queries.${queryRef}({}),`);
|
|
366
249
|
}
|
|
367
250
|
}
|
|
368
251
|
});
|
|
252
|
+
writer.writeLine(',');
|
|
253
|
+
// Write entry points using helpers
|
|
254
|
+
if (hasNestedEntryPoints) {
|
|
255
|
+
writer.write('entryPoints: ').block(() => {
|
|
256
|
+
for (const [epName, nestedPage,] of page.nestedEntryPoints.entries()) {
|
|
257
|
+
// Collect all variables needed by this nested entry point
|
|
258
|
+
const nestedVars = collectQueryParameters(project, Array.from(nestedPage.queries.values()));
|
|
259
|
+
if (nestedVars.size > 0) {
|
|
260
|
+
const varNames = Array.from(nestedVars.keys());
|
|
261
|
+
writer.writeLine(`${epName}: entryPoints.${epName}({${varNames.map((v) => `${v}: variables.${v}`).join(', ')}}),`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
writer.writeLine(`${epName}: entryPoints.${epName}({}),`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
writer.writeLine('entryPoints: undefined');
|
|
271
|
+
}
|
|
369
272
|
});
|
|
370
273
|
});
|
|
371
274
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Router Generation
|
|
277
|
+
// ============================================================================
|
|
278
|
+
/**
|
|
279
|
+
* Generates the router.tsx file with all route configurations.
|
|
280
|
+
*
|
|
281
|
+
* For each page discovered in the filesystem:
|
|
282
|
+
* 1. Generates an entry point function with getPreloadProps
|
|
283
|
+
* 2. Creates a route configuration entry with Zod schema
|
|
284
|
+
*/
|
|
285
|
+
async function generateRouter(project, fsMetadata) {
|
|
286
|
+
const routerTemplate = await loadRouterTemplate(project, 'router.tsx');
|
|
287
|
+
// Get the ROUTER_CONF object to add routes to
|
|
376
288
|
const routerConf = routerTemplate
|
|
377
289
|
.getVariableDeclarationOrThrow('ROUTER_CONF')
|
|
378
290
|
.getInitializerIfKindOrThrow(SyntaxKind.AsExpression)
|
|
379
291
|
.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
292
|
+
// Only remove the noop placeholder if there are actual routes
|
|
293
|
+
// (keeping it prevents type errors when the config is empty)
|
|
294
|
+
const hasRoutes = fsMetadata.pages.size > 0 || fsMetadata.entryPoints.size > 0;
|
|
295
|
+
if (hasRoutes) {
|
|
296
|
+
routerConf.getPropertyOrThrow('noop').remove();
|
|
297
|
+
}
|
|
298
|
+
// Add JSResource import
|
|
299
|
+
routerTemplate.addImportDeclaration({
|
|
300
|
+
moduleSpecifier: './js_resource',
|
|
301
|
+
namedImports: ['JSResource', 'ModuleType'],
|
|
302
|
+
});
|
|
303
|
+
// Add helper types import from types.ts
|
|
304
|
+
routerTemplate.addImportDeclaration({
|
|
305
|
+
moduleSpecifier: './types',
|
|
306
|
+
namedImports: ['QueryHelpersForRoute', 'EntryPointHelpersForRoute'],
|
|
307
|
+
isTypeOnly: true,
|
|
308
|
+
});
|
|
309
|
+
const tc = project.getTypeChecker().compilerObject;
|
|
310
|
+
// Process each page
|
|
311
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
312
|
+
// Create a unique resource name for this page
|
|
313
|
+
const resourceName = `fs:page(${routePath})`;
|
|
314
|
+
const safeResourceName = resourceName.replace(/[^a-zA-Z0-9]/g, '_');
|
|
315
|
+
// Check if this page has a custom entrypoint.ts
|
|
316
|
+
const hasCustomEntryPoint = page.customEntryPointPath != null;
|
|
317
|
+
// Import custom entrypoint exports if present
|
|
318
|
+
let customEntryPointAlias = null;
|
|
319
|
+
if (hasCustomEntryPoint) {
|
|
320
|
+
customEntryPointAlias = `customEp_${safeResourceName}`;
|
|
321
|
+
routerTemplate.addImportDeclaration({
|
|
322
|
+
moduleSpecifier: routerTemplate.getRelativePathAsModuleSpecifierTo(path.join(process.cwd(), page.customEntryPointPath)),
|
|
323
|
+
defaultImport: customEntryPointAlias,
|
|
324
|
+
namedImports: [
|
|
325
|
+
{ name: 'schema', alias: `${customEntryPointAlias}_schema` },
|
|
326
|
+
],
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// Collect all queries consumed by this page and its nested entry points
|
|
330
|
+
const consumedQueries = collectAllQueries(page);
|
|
331
|
+
// Add imports for query parameters and variable types (only needed for generated entry points)
|
|
332
|
+
if (!hasCustomEntryPoint) {
|
|
333
|
+
for (const queryName of consumedQueries) {
|
|
334
|
+
routerTemplate.addImportDeclaration({
|
|
335
|
+
moduleSpecifier: `#genfiles/queries/${queryName}$parameters`,
|
|
336
|
+
defaultImport: `${queryName}Parameters`,
|
|
337
|
+
});
|
|
338
|
+
// Add type-only import for variables (doesn't import runtime code)
|
|
339
|
+
routerTemplate.addImportDeclaration({
|
|
340
|
+
moduleSpecifier: `#genfiles/queries/${queryName}.graphql`,
|
|
341
|
+
namedImports: [`${queryName}$variables`],
|
|
342
|
+
isTypeOnly: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Build params schema from route params + query variables
|
|
347
|
+
// Track both the type and whether it's optional
|
|
348
|
+
const params = new Map();
|
|
349
|
+
// Add filesystem params as string types (using routeParams for optional info)
|
|
350
|
+
for (const routeParam of page.routeParams) {
|
|
351
|
+
params.set(routeParam.name, {
|
|
352
|
+
type: tc.getStringType(),
|
|
353
|
+
optional: routeParam.optional,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// Also include query variables (only needed for generated entry points)
|
|
357
|
+
if (!hasCustomEntryPoint && consumedQueries.size > 0) {
|
|
358
|
+
const queryParams = collectQueryParameters(project, Array.from(consumedQueries));
|
|
359
|
+
// Merge query params with route params (route params take precedence)
|
|
360
|
+
for (const [paramName, paramType] of queryParams) {
|
|
361
|
+
if (!params.has(paramName)) {
|
|
362
|
+
params.set(paramName, { type: paramType, optional: false });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Build schema expression string for the entry point to capture
|
|
367
|
+
let schemaExpression;
|
|
368
|
+
if (hasCustomEntryPoint) {
|
|
369
|
+
// Use the imported schema from the custom entrypoint
|
|
370
|
+
schemaExpression = `${customEntryPointAlias}_schema`;
|
|
371
|
+
}
|
|
372
|
+
else if (params.size === 0) {
|
|
373
|
+
schemaExpression = 'z.object({})';
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const schemaFields = Array.from(params.entries())
|
|
377
|
+
.map(([paramName, { type: paramType, optional: isOptional }]) => {
|
|
378
|
+
const baseSchema = zodSchemaOfType(routerTemplate, tc, paramType);
|
|
379
|
+
// Wrap optional params with z.optional()
|
|
380
|
+
const schema = isOptional ? `z.optional(${baseSchema})` : baseSchema;
|
|
381
|
+
return `${paramName}: ${schema}`;
|
|
382
|
+
})
|
|
383
|
+
.join(', ');
|
|
384
|
+
schemaExpression = `z.object({ ${schemaFields} })`;
|
|
385
|
+
}
|
|
386
|
+
// Generate the entry point function
|
|
387
|
+
if (hasCustomEntryPoint) {
|
|
388
|
+
// Custom entrypoint: use imported getPreloadProps but still wrap with helpers
|
|
389
|
+
// First, add query parameter imports needed for the helpers
|
|
390
|
+
for (const queryName of consumedQueries) {
|
|
393
391
|
routerTemplate.addImportDeclaration({
|
|
394
|
-
moduleSpecifier:
|
|
395
|
-
|
|
392
|
+
moduleSpecifier: `#genfiles/queries/${queryName}$parameters`,
|
|
393
|
+
defaultImport: `${queryName}Parameters`,
|
|
394
|
+
});
|
|
395
|
+
routerTemplate.addImportDeclaration({
|
|
396
|
+
moduleSpecifier: `#genfiles/queries/${queryName}.graphql`,
|
|
397
|
+
namedImports: [`${queryName}$variables`],
|
|
398
|
+
isTypeOnly: true,
|
|
396
399
|
});
|
|
397
400
|
}
|
|
398
|
-
const consumedQueries = new Set();
|
|
399
401
|
routerTemplate.addFunction({
|
|
400
|
-
name:
|
|
401
|
-
returnType: `EntryPoint<ModuleType<'${resourceName}'>, EntryPointParams<'${routeName}'>>`,
|
|
402
|
+
name: `entrypoint_${safeResourceName}`,
|
|
402
403
|
statements: (writer) => {
|
|
404
|
+
// Generate query helpers
|
|
405
|
+
writer.write('const queryHelpers = ').block(() => {
|
|
406
|
+
for (const [queryRef, queryName] of page.queries.entries()) {
|
|
407
|
+
writer.writeLine(`${queryRef}: (variables: ${queryName}$variables) => ({ parameters: ${queryName}Parameters, variables }),`);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
writer.writeLine(';');
|
|
411
|
+
// Generate entry point helpers
|
|
412
|
+
writer.write('const entryPointHelpers = ').block(() => {
|
|
413
|
+
for (const [epName, nestedPage,] of page.nestedEntryPoints.entries()) {
|
|
414
|
+
const nestedResourceName = `${resourceName}#${epName}`;
|
|
415
|
+
const nestedQueryNames = Array.from(nestedPage.queries.values());
|
|
416
|
+
const variablesType = nestedQueryNames.length === 0
|
|
417
|
+
? 'Record<string, never>'
|
|
418
|
+
: nestedQueryNames.map((q) => `${q}$variables`).join(' & ');
|
|
419
|
+
writer
|
|
420
|
+
.write(`${epName}: (variables: ${variablesType}) => (`)
|
|
421
|
+
.block(() => {
|
|
422
|
+
writer.writeLine('entryPointParams: {},');
|
|
423
|
+
writer.write('entryPoint: ').block(() => {
|
|
424
|
+
writer.writeLine(`root: JSResource.fromModuleId('${nestedResourceName}'),`);
|
|
425
|
+
writer.write('getPreloadProps() ').block(() => {
|
|
426
|
+
writer.write('return ').block(() => {
|
|
427
|
+
writer.write('queries: ').block(() => {
|
|
428
|
+
for (const [nestedQueryRef, nestedQueryName,] of nestedPage.queries.entries()) {
|
|
429
|
+
writer.writeLine(`${nestedQueryRef}: { parameters: ${nestedQueryName}Parameters, variables },`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
writer.writeLine(',');
|
|
433
|
+
writer.writeLine('entryPoints: undefined');
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
writer.writeLine('),');
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
writer.writeLine(';');
|
|
403
442
|
writer.write('return ').block(() => {
|
|
404
|
-
|
|
443
|
+
writer.writeLine(`root: JSResource.fromModuleId('${resourceName}'),`);
|
|
444
|
+
writer.writeLine(`getPreloadProps: (p: {params: z.infer<typeof ${customEntryPointAlias}_schema>}) => ${customEntryPointAlias}({`);
|
|
445
|
+
writer.writeLine(' params: p.params,');
|
|
446
|
+
writer.writeLine(' queries: queryHelpers,');
|
|
447
|
+
writer.writeLine(' entryPoints: entryPointHelpers,');
|
|
448
|
+
writer.writeLine('}),');
|
|
405
449
|
});
|
|
406
450
|
},
|
|
407
451
|
});
|
|
408
|
-
if (params.size === 0 && consumedQueries.size > 0) {
|
|
409
|
-
params = collectQueryParameters(project, Array.from(consumedQueries));
|
|
410
|
-
}
|
|
411
|
-
for (const query of consumedQueries) {
|
|
412
|
-
routerTemplate.addImportDeclaration({
|
|
413
|
-
moduleSpecifier: `#genfiles/queries/${query}$parameters`,
|
|
414
|
-
defaultImport: `${query}Parameters`,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
entryPointExpression = entryPointFunctionName + '()';
|
|
418
452
|
}
|
|
419
453
|
else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
{
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
454
|
+
// Generated entrypoint: create getPreloadProps from page queries
|
|
455
|
+
routerTemplate.addFunction({
|
|
456
|
+
name: `entrypoint_${safeResourceName}`,
|
|
457
|
+
statements: (writer) => {
|
|
458
|
+
writeFilesystemEntryPoint(writer, project, page, resourceName, routePath, schemaExpression);
|
|
459
|
+
writer.write('return ').block(() => {
|
|
460
|
+
writer.writeLine(`root: JSResource.fromModuleId('${resourceName}'),`);
|
|
461
|
+
writer.writeLine('getPreloadProps: (p: {params: Record<string, unknown>}) => getPreloadProps({');
|
|
462
|
+
writer.writeLine(' params: p.params as z.infer<typeof schema>,');
|
|
463
|
+
writer.writeLine(' queries: queryHelpers,');
|
|
464
|
+
writer.writeLine(' entryPoints: entryPointHelpers,');
|
|
465
|
+
writer.writeLine('}),');
|
|
466
|
+
});
|
|
467
|
+
},
|
|
430
468
|
});
|
|
431
|
-
entryPointExpression = importAlias;
|
|
432
469
|
}
|
|
470
|
+
// Add route configuration (uses bracket format, converted to colon for radix3 at runtime)
|
|
433
471
|
routerConf.addPropertyAssignment({
|
|
434
|
-
name: `"${
|
|
472
|
+
name: `"${routePath}"`,
|
|
435
473
|
initializer: (writer) => {
|
|
436
|
-
writer
|
|
437
|
-
.
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
writer.writeLine(`schema: z.object({})`);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
writer.writeLine(`schema: z.object({`);
|
|
445
|
-
for (const [paramName, paramType] of Array.from(params)) {
|
|
446
|
-
writer.writeLine(` ${paramName}: ${zodSchemaOfType(routerTemplate, tc, paramType)},`);
|
|
447
|
-
}
|
|
448
|
-
writer.writeLine('})');
|
|
449
|
-
}
|
|
450
|
-
})
|
|
451
|
-
.write('} as const');
|
|
474
|
+
writer.write('{').indent(() => {
|
|
475
|
+
writer.writeLine(`entrypoint: entrypoint_${safeResourceName}(),`);
|
|
476
|
+
writer.writeLine(`schema: ${schemaExpression}`);
|
|
477
|
+
});
|
|
478
|
+
writer.write('} as const');
|
|
452
479
|
},
|
|
453
480
|
});
|
|
454
|
-
logInfo('Created route', pc.cyan(
|
|
481
|
+
logInfo('Created route', pc.cyan(routePath), 'from', pc.yellow(page.filePath), hasCustomEntryPoint ? '(custom entrypoint)' : '');
|
|
455
482
|
}
|
|
456
483
|
await routerTemplate.save();
|
|
457
484
|
}
|
|
458
|
-
|
|
459
|
-
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// JS Resource Generation
|
|
487
|
+
// ============================================================================
|
|
488
|
+
/**
|
|
489
|
+
* Generates the js_resource.ts file for lazy loading.
|
|
490
|
+
*
|
|
491
|
+
* Each page.tsx and *.page.tsx file becomes a lazy-loadable resource.
|
|
492
|
+
*/
|
|
493
|
+
async function generateJsResource(project, fsMetadata) {
|
|
494
|
+
const jsResourceTemplate = await loadRouterTemplate(project, 'js_resource.ts');
|
|
460
495
|
const resourceConf = jsResourceTemplate
|
|
461
496
|
.getVariableDeclarationOrThrow('RESOURCE_CONF')
|
|
462
497
|
.getInitializerIfKindOrThrow(SyntaxKind.AsExpression)
|
|
463
498
|
.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
499
|
+
// Only remove the noop placeholder if there are actual pages
|
|
500
|
+
const hasPages = fsMetadata.pages.size > 0;
|
|
501
|
+
if (hasPages) {
|
|
502
|
+
resourceConf.getPropertyOrThrow('noop').remove();
|
|
503
|
+
}
|
|
504
|
+
// Process main pages
|
|
505
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
506
|
+
const resourceName = `fs:page(${routePath})`;
|
|
507
|
+
const moduleSpecifier = jsResourceTemplate.getRelativePathAsModuleSpecifierTo(path.join(process.cwd(), page.filePath));
|
|
468
508
|
resourceConf.addPropertyAssignment({
|
|
469
509
|
name: `"${resourceName}"`,
|
|
470
510
|
initializer: (writer) => {
|
|
471
511
|
writer.block(() => {
|
|
472
|
-
writer
|
|
473
|
-
|
|
474
|
-
.writeLine(`loader: () => import("${moduleSpecifier}").then(m => m.${symbol.getName()})`);
|
|
512
|
+
writer.writeLine(`src: "${page.filePath}",`);
|
|
513
|
+
writer.writeLine(`loader: (): Promise<ComponentType<PageProps<'${routePath}'>>> => import("${moduleSpecifier}").then(m => m.default)`);
|
|
475
514
|
});
|
|
476
515
|
},
|
|
477
516
|
});
|
|
478
|
-
logInfo('Created resource', pc.cyan(resourceName), '
|
|
517
|
+
logInfo('Created resource', pc.cyan(resourceName), 'from', pc.yellow(page.filePath));
|
|
518
|
+
// Process nested entry points for this page
|
|
519
|
+
for (const [epName, nestedPage] of page.nestedEntryPoints.entries()) {
|
|
520
|
+
const nestedResourceName = `${resourceName}#${epName}`;
|
|
521
|
+
const nestedModuleSpecifier = jsResourceTemplate.getRelativePathAsModuleSpecifierTo(path.join(process.cwd(), nestedPage.filePath));
|
|
522
|
+
// Nested entry points use flattened keys like '/#search_results'
|
|
523
|
+
const nestedRouteKey = `${routePath}#${epName}`;
|
|
524
|
+
resourceConf.addPropertyAssignment({
|
|
525
|
+
name: `"${nestedResourceName}"`,
|
|
526
|
+
initializer: (writer) => {
|
|
527
|
+
writer.block(() => {
|
|
528
|
+
writer.writeLine(`src: "${nestedPage.filePath}",`);
|
|
529
|
+
writer.writeLine(`loader: (): Promise<ComponentType<PageProps<'${nestedRouteKey}'>>> => import("${nestedModuleSpecifier}").then(m => m.default)`);
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
logInfo('Created nested resource', pc.cyan(nestedResourceName), 'from', pc.yellow(nestedPage.filePath));
|
|
534
|
+
}
|
|
479
535
|
}
|
|
480
536
|
await jsResourceTemplate.save();
|
|
481
537
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// Server Handler Generation
|
|
540
|
+
// ============================================================================
|
|
541
|
+
/**
|
|
542
|
+
* Generates server_handler.ts with API route handlers.
|
|
543
|
+
*
|
|
544
|
+
* Each route.ts file in the filesystem exports an express.Router
|
|
545
|
+
* that handles requests at that path.
|
|
546
|
+
*/
|
|
547
|
+
async function generateServerHandler(project, fsMetadata) {
|
|
548
|
+
if (fsMetadata.apiRoutes.size === 0) {
|
|
549
|
+
// No API routes found, delete the file if it exists
|
|
486
550
|
await project
|
|
487
|
-
.getSourceFile('__generated__/router/
|
|
551
|
+
.getSourceFile('__generated__/router/server_handler.ts')
|
|
488
552
|
?.deleteImmediately();
|
|
489
553
|
return;
|
|
490
554
|
}
|
|
491
|
-
const
|
|
492
|
-
const appRootSymbol = appRoot.symbol;
|
|
493
|
-
const filePath = path.relative(targetDir, appRootSourceFile.getFilePath());
|
|
494
|
-
const appRootFile = project.createSourceFile('__generated__/router/app_root.ts', '', { overwrite: true });
|
|
495
|
-
const moduleSpecifier = appRootFile.getRelativePathAsModuleSpecifierTo(appRootSourceFile.getFilePath());
|
|
496
|
-
appRootFile.addStatements(`/*
|
|
555
|
+
const serverHandlerFile = project.createSourceFile('__generated__/router/server_handler.ts', `/*
|
|
497
556
|
* This file was generated by \`pastoria\`.
|
|
498
557
|
* Do not modify this file directly.
|
|
499
558
|
*/
|
|
500
559
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
560
|
+
import express from 'express';
|
|
561
|
+
export const router = express.Router();
|
|
562
|
+
`, { overwrite: true });
|
|
563
|
+
let importIndex = 0;
|
|
564
|
+
for (const [routePath, apiRoute] of fsMetadata.apiRoutes.entries()) {
|
|
565
|
+
// Convert [param] to :param for Express router
|
|
566
|
+
const routerPath = toRouterPath(routePath);
|
|
567
|
+
const importAlias = `route${importIndex++}`;
|
|
568
|
+
const moduleSpecifier = serverHandlerFile.getRelativePathAsModuleSpecifierTo(path.join(process.cwd(), apiRoute.filePath));
|
|
569
|
+
serverHandlerFile.addImportDeclaration({
|
|
570
|
+
moduleSpecifier,
|
|
571
|
+
defaultImport: importAlias,
|
|
572
|
+
});
|
|
573
|
+
serverHandlerFile.addStatements(`router.use('${routerPath}', ${importAlias});`);
|
|
574
|
+
logInfo('Created API route', pc.cyan(routePath), 'from', pc.yellow(apiRoute.filePath));
|
|
575
|
+
}
|
|
576
|
+
await serverHandlerFile.save();
|
|
505
577
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
contextFile.addStatements(`/*
|
|
516
|
-
* This file was generated by \`pastoria\`.
|
|
517
|
-
* Do not modify this file directly.
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// Type Generation
|
|
580
|
+
// ============================================================================
|
|
581
|
+
/**
|
|
582
|
+
* Generates types.ts with PageProps type definitions.
|
|
583
|
+
*
|
|
584
|
+
* Creates types like PageProps<'/posts'> that include:
|
|
585
|
+
* - queries: Preloaded query references
|
|
586
|
+
* - entryPoints: Nested entry point references
|
|
518
587
|
*/
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
588
|
+
async function generateTypes(project, fsMetadata) {
|
|
589
|
+
const typesFile = project.createSourceFile('__generated__/router/types.ts', '', { overwrite: true });
|
|
590
|
+
// Collect all query types to import (both the query type and $variables type)
|
|
591
|
+
const queryImports = new Set();
|
|
592
|
+
const variablesImports = new Set();
|
|
593
|
+
// Helper to build queries type string
|
|
594
|
+
function buildQueriesType(queries) {
|
|
595
|
+
if (queries.size === 0)
|
|
596
|
+
return '{}';
|
|
597
|
+
return `{ ${Array.from(queries.entries())
|
|
598
|
+
.map(([ref, queryTypeName]) => {
|
|
599
|
+
queryImports.add(queryTypeName);
|
|
600
|
+
return `${ref}: ${queryTypeName}`;
|
|
601
|
+
})
|
|
602
|
+
.join('; ')} }`;
|
|
603
|
+
}
|
|
604
|
+
// Helper to build query helper type for a route
|
|
605
|
+
function buildQueryHelpersType(queries) {
|
|
606
|
+
if (queries.size === 0)
|
|
607
|
+
return '{}';
|
|
608
|
+
return `{ ${Array.from(queries.entries())
|
|
609
|
+
.map(([ref, queryTypeName]) => {
|
|
610
|
+
variablesImports.add(queryTypeName);
|
|
611
|
+
return `${ref}: (variables: ${queryTypeName}$variables) => { parameters: unknown; variables: ${queryTypeName}$variables }`;
|
|
612
|
+
})
|
|
613
|
+
.join('; ')} }`;
|
|
614
|
+
}
|
|
615
|
+
// Helper to build entry point helper type for a route
|
|
616
|
+
function buildEntryPointHelpersType(routePath, nestedEntryPoints) {
|
|
617
|
+
if (nestedEntryPoints.size === 0)
|
|
618
|
+
return '{}';
|
|
619
|
+
return `{ ${Array.from(nestedEntryPoints.entries())
|
|
620
|
+
.map(([epName, nestedPage]) => {
|
|
621
|
+
const nestedTypeName = routeToTypeName(routePath, epName);
|
|
622
|
+
// Build intersection of all query variable types for this nested entry point
|
|
623
|
+
const queryNames = Array.from(nestedPage.queries.values());
|
|
624
|
+
queryNames.forEach((q) => variablesImports.add(q));
|
|
625
|
+
const variablesType = queryNames.length === 0
|
|
626
|
+
? 'Record<string, never>'
|
|
627
|
+
: queryNames.map((q) => `${q}$variables`).join(' & ');
|
|
628
|
+
// Optional entry points can return undefined
|
|
629
|
+
const baseReturnType = `{ entryPointParams: Record<string, never>; entryPoint: EntryPoint<EntryPointComponent<${nestedTypeName}['queries'], ${nestedTypeName}['entryPoints'], {}, {}>, {}> }`;
|
|
630
|
+
const returnType = nestedPage.optional
|
|
631
|
+
? `${baseReturnType} | undefined`
|
|
632
|
+
: baseReturnType;
|
|
633
|
+
return `${epName}: (variables: ${variablesType}) => ${returnType}`;
|
|
634
|
+
})
|
|
635
|
+
.join('; ')} }`;
|
|
636
|
+
}
|
|
637
|
+
// Helper to convert route path to a valid TypeScript identifier
|
|
638
|
+
function routeToTypeName(routePath, nestedName) {
|
|
639
|
+
// Convert /hello/[name] to Hello$name, /hello/[[name]] to Hello$$name, / to Root
|
|
640
|
+
const pathPart = routePath === '/'
|
|
641
|
+
? 'Root'
|
|
642
|
+
: routePath
|
|
643
|
+
.slice(1) // remove leading /
|
|
644
|
+
.split('/')
|
|
645
|
+
.map((segment) => {
|
|
646
|
+
// Check for optional param first: [[name]] -> $$name
|
|
647
|
+
if (segment.startsWith('[[') && segment.endsWith(']]')) {
|
|
648
|
+
return '$$' + segment.slice(2, -2);
|
|
649
|
+
}
|
|
650
|
+
// Check for required param: [name] -> $name
|
|
651
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
652
|
+
return '$' + segment.slice(1, -1);
|
|
653
|
+
}
|
|
654
|
+
// Static segment: hello -> Hello (capitalize first letter)
|
|
655
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
656
|
+
})
|
|
657
|
+
.join('');
|
|
658
|
+
if (nestedName) {
|
|
659
|
+
// Nested entry point: append _nestedName
|
|
660
|
+
return `Route${pathPart}_${nestedName}`;
|
|
661
|
+
}
|
|
662
|
+
return `Route${pathPart}`;
|
|
663
|
+
}
|
|
664
|
+
// Phase 1: Generate type aliases for nested entry points (leaf nodes - no dependencies)
|
|
665
|
+
const nestedTypeAliases = [];
|
|
666
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
667
|
+
for (const [epName, nestedPage] of page.nestedEntryPoints.entries()) {
|
|
668
|
+
const typeName = routeToTypeName(routePath, epName);
|
|
669
|
+
const queriesType = buildQueriesType(nestedPage.queries);
|
|
670
|
+
nestedTypeAliases.push(`type ${typeName} = { queries: ${queriesType}; entryPoints: {} };`);
|
|
671
|
+
}
|
|
523
672
|
}
|
|
524
|
-
|
|
525
|
-
|
|
673
|
+
// Phase 2: Generate type aliases for main pages (can reference nested types)
|
|
674
|
+
const mainTypeAliases = [];
|
|
675
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
676
|
+
const typeName = routeToTypeName(routePath);
|
|
677
|
+
const queriesType = buildQueriesType(page.queries);
|
|
678
|
+
let entryPointsType = '{}';
|
|
679
|
+
if (page.nestedEntryPoints.size > 0) {
|
|
680
|
+
const epTypes = Array.from(page.nestedEntryPoints.entries())
|
|
681
|
+
.map(([epName, nestedPage]) => {
|
|
682
|
+
const nestedTypeName = routeToTypeName(routePath, epName);
|
|
683
|
+
const optionalMarker = nestedPage.optional ? '?' : '';
|
|
684
|
+
return `${epName}${optionalMarker}: EntryPoint<EntryPointComponent<${nestedTypeName}['queries'], ${nestedTypeName}['entryPoints'], {}, {}>, {}>`;
|
|
685
|
+
})
|
|
686
|
+
.join('; ');
|
|
687
|
+
entryPointsType = `{ ${epTypes} }`;
|
|
688
|
+
}
|
|
689
|
+
mainTypeAliases.push(`type ${typeName} = { queries: ${queriesType}; entryPoints: ${entryPointsType} };`);
|
|
690
|
+
}
|
|
691
|
+
// Phase 3: Build PageQueryMap entries referencing the type aliases
|
|
692
|
+
const routeTypes = [];
|
|
693
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
694
|
+
const typeName = routeToTypeName(routePath);
|
|
695
|
+
routeTypes.push(` '${routePath}': ${typeName}`);
|
|
696
|
+
for (const epName of page.nestedEntryPoints.keys()) {
|
|
697
|
+
const nestedTypeName = routeToTypeName(routePath, epName);
|
|
698
|
+
routeTypes.push(` '${routePath}#${epName}': ${nestedTypeName}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Phase 4: Generate helper type aliases for each route
|
|
702
|
+
const queryHelperAliases = [];
|
|
703
|
+
const entryPointHelperAliases = [];
|
|
704
|
+
const queryHelpersMapEntries = [];
|
|
705
|
+
const entryPointHelpersMapEntries = [];
|
|
706
|
+
for (const [routePath, page] of fsMetadata.pages.entries()) {
|
|
707
|
+
const typeName = routeToTypeName(routePath);
|
|
708
|
+
// Query helpers type
|
|
709
|
+
const queryHelpersType = buildQueryHelpersType(page.queries);
|
|
710
|
+
queryHelperAliases.push(`type QueryHelpers_${typeName} = ${queryHelpersType};`);
|
|
711
|
+
queryHelpersMapEntries.push(` '${routePath}': QueryHelpers_${typeName}`);
|
|
712
|
+
// Entry point helpers type
|
|
713
|
+
const entryPointHelpersType = buildEntryPointHelpersType(routePath, page.nestedEntryPoints);
|
|
714
|
+
entryPointHelperAliases.push(`type EntryPointHelpers_${typeName} = ${entryPointHelpersType};`);
|
|
715
|
+
entryPointHelpersMapEntries.push(` '${routePath}': EntryPointHelpers_${typeName}`);
|
|
716
|
+
}
|
|
717
|
+
// Generate import statements for query types
|
|
718
|
+
const queryImportStatements = Array.from(queryImports)
|
|
719
|
+
.map((queryTypeName) => `import type {${queryTypeName}} from '#genfiles/queries/${queryTypeName}.graphql';`)
|
|
720
|
+
.join('\n');
|
|
721
|
+
// Generate import statements for $variables types
|
|
722
|
+
const variablesImportStatements = Array.from(variablesImports)
|
|
723
|
+
.map((queryTypeName) => `import type {${queryTypeName}$variables} from '#genfiles/queries/${queryTypeName}.graphql';`)
|
|
724
|
+
.join('\n');
|
|
725
|
+
typesFile.addStatements(`/*
|
|
526
726
|
* This file was generated by \`pastoria\`.
|
|
527
727
|
* Do not modify this file directly.
|
|
728
|
+
*
|
|
729
|
+
* Type definitions for filesystem-based routing.
|
|
528
730
|
*/
|
|
529
731
|
|
|
530
|
-
import {
|
|
732
|
+
import type {EntryPoint, EntryPointComponent, EntryPointProps} from 'react-relay/hooks';
|
|
733
|
+
${queryImportStatements ? '\n' + queryImportStatements : ''}
|
|
734
|
+
${variablesImportStatements ? '\n' + variablesImportStatements : ''}
|
|
735
|
+
|
|
736
|
+
// Route type aliases - nested entry points (leaf nodes)
|
|
737
|
+
${nestedTypeAliases.join('\n')}
|
|
738
|
+
|
|
739
|
+
// Route type aliases - main pages
|
|
740
|
+
${mainTypeAliases.join('\n')}
|
|
531
741
|
|
|
532
742
|
/**
|
|
533
|
-
*
|
|
743
|
+
* Map of route paths to their query types.
|
|
744
|
+
* Nested entry points use the format: '/route#entryPointName'
|
|
534
745
|
*/
|
|
535
|
-
export
|
|
536
|
-
|
|
537
|
-
logInfo('No @gqlContext found, generating default', pc.green('Context'));
|
|
538
|
-
}
|
|
539
|
-
await contextFile.save();
|
|
746
|
+
export interface PageQueryMap {
|
|
747
|
+
${routeTypes.join(';\n')}${routeTypes.length > 0 ? ';' : ''}
|
|
540
748
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
749
|
+
|
|
750
|
+
// Query helper type aliases for each route
|
|
751
|
+
${queryHelperAliases.join('\n')}
|
|
752
|
+
|
|
753
|
+
// Entry point helper type aliases for each route
|
|
754
|
+
${entryPointHelperAliases.join('\n')}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Query helper functions for a route.
|
|
758
|
+
* Each helper takes typed variables and returns {parameters, variables} for Relay.
|
|
759
|
+
*/
|
|
760
|
+
export interface QueryHelpersMap {
|
|
761
|
+
${queryHelpersMapEntries.join(';\n')}${queryHelpersMapEntries.length > 0 ? ';' : ''}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export type QueryHelpersForRoute<R extends string> = R extends keyof QueryHelpersMap
|
|
765
|
+
? QueryHelpersMap[R]
|
|
766
|
+
: {};
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Entry point helper functions for a route.
|
|
770
|
+
* Each helper takes typed variables and returns the nested entry point configuration.
|
|
771
|
+
*/
|
|
772
|
+
export interface EntryPointHelpersMap {
|
|
773
|
+
${entryPointHelpersMapEntries.join(';\n')}${entryPointHelpersMapEntries.length > 0 ? ';' : ''}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export type EntryPointHelpersForRoute<R extends string> = R extends keyof EntryPointHelpersMap
|
|
777
|
+
? EntryPointHelpersMap[R]
|
|
778
|
+
: {};
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Props type for a page component at the given route.
|
|
782
|
+
* Uses EntryPointProps to transform raw query types to PreloadedQuery<...>.
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* \`\`\`typescript
|
|
786
|
+
* // Main page
|
|
787
|
+
* export default function BlogPosts({ queries }: PageProps<'/posts'>) {
|
|
788
|
+
* const data = usePreloadedQuery(query, queries.posts);
|
|
789
|
+
* }
|
|
790
|
+
*
|
|
791
|
+
* // Nested entry point
|
|
792
|
+
* export default function Sidebar({ queries }: PageProps<'/posts#sidebar'>) {
|
|
793
|
+
* const data = usePreloadedQuery(query, queries.sidebarData);
|
|
794
|
+
* }
|
|
795
|
+
* \`\`\`
|
|
796
|
+
*/
|
|
797
|
+
export type PageProps<R extends keyof PageQueryMap> = EntryPointProps<
|
|
798
|
+
PageQueryMap[R]['queries'],
|
|
799
|
+
PageQueryMap[R]['entryPoints'],
|
|
800
|
+
{},
|
|
801
|
+
{}
|
|
802
|
+
>;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Return type for getPreloadProps in entrypoint.ts files.
|
|
806
|
+
* This type matches the structure expected by Relay's loadEntryPoint.
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* \`\`\`typescript
|
|
810
|
+
* export default function getPreloadProps({
|
|
811
|
+
* params,
|
|
812
|
+
* queries,
|
|
813
|
+
* entryPoints,
|
|
814
|
+
* }: EntryPointParams<'/posts'>): PreloadPropsForRoute<'/posts'> {
|
|
815
|
+
* return {
|
|
816
|
+
* queries: {
|
|
817
|
+
* postsQuery: queries.postsQuery(params),
|
|
818
|
+
* },
|
|
819
|
+
* entryPoints: {
|
|
820
|
+
* sidebar: entryPoints.sidebar({}),
|
|
821
|
+
* },
|
|
822
|
+
* };
|
|
823
|
+
* }
|
|
824
|
+
* \`\`\`
|
|
825
|
+
*/
|
|
826
|
+
export type PreloadPropsForRoute<R extends keyof PageQueryMap> = {
|
|
827
|
+
queries: {
|
|
828
|
+
[K in keyof PageQueryMap[R]['queries']]: {
|
|
829
|
+
parameters: unknown;
|
|
830
|
+
variables: unknown;
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
entryPoints: PageQueryMap[R]['entryPoints'] extends Record<string, never>
|
|
834
|
+
? undefined
|
|
835
|
+
: {
|
|
836
|
+
[K in keyof PageQueryMap[R]['entryPoints']]: {
|
|
837
|
+
entryPointParams: unknown;
|
|
838
|
+
entryPoint: unknown;
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
};
|
|
842
|
+
`);
|
|
843
|
+
await typesFile.save();
|
|
844
|
+
logInfo('Generated PageProps types for', pc.cyan(`${fsMetadata.pages.size} routes`));
|
|
565
845
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
846
|
+
// ============================================================================
|
|
847
|
+
// Main Generation Functions
|
|
848
|
+
// ============================================================================
|
|
849
|
+
/**
|
|
850
|
+
* Generates all Pastoria artifacts from the filesystem.
|
|
851
|
+
*
|
|
852
|
+
* This is the main entry point for code generation.
|
|
853
|
+
*/
|
|
854
|
+
export async function generatePastoriaArtifacts(project) {
|
|
855
|
+
// Scan the pastoria/ directory for routing files
|
|
856
|
+
const fsMetadata = scanFilesystemRoutes(project);
|
|
857
|
+
// Generate all artifacts
|
|
858
|
+
await Promise.all([
|
|
859
|
+
generateRouter(project, fsMetadata),
|
|
860
|
+
generateJsResource(project, fsMetadata),
|
|
861
|
+
generateServerHandler(project, fsMetadata),
|
|
862
|
+
generateTypes(project, fsMetadata),
|
|
863
|
+
]);
|
|
864
|
+
return fsMetadata;
|
|
571
865
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Detects what capabilities are available based on source files.
|
|
868
|
+
*/
|
|
869
|
+
export async function detectCapabilities() {
|
|
870
|
+
const fs = await import('node:fs/promises');
|
|
871
|
+
const hasAppRoot = await fs
|
|
872
|
+
.stat('pastoria/app.tsx')
|
|
873
|
+
.then(() => true)
|
|
874
|
+
.catch(() => false);
|
|
875
|
+
const hasServerHandler = await fs
|
|
876
|
+
.stat('__generated__/router/server_handler.ts')
|
|
877
|
+
.then(() => true)
|
|
878
|
+
.catch(() => false);
|
|
879
|
+
return { hasAppRoot, hasServerHandler };
|
|
576
880
|
}
|
|
881
|
+
// ============================================================================
|
|
882
|
+
// Entry Point Generation (for Vite plugin)
|
|
883
|
+
// ============================================================================
|
|
884
|
+
/**
|
|
885
|
+
* Generates the client entry point code.
|
|
886
|
+
*/
|
|
577
887
|
export function generateClientEntry({ hasAppRoot, }) {
|
|
578
|
-
const appImport = hasAppRoot
|
|
579
|
-
? `import {App} from '#genfiles/router/app_root';`
|
|
580
|
-
: '';
|
|
888
|
+
const appImport = hasAppRoot ? `import App from './pastoria/app';` : '';
|
|
581
889
|
const appValue = hasAppRoot ? 'App' : 'null';
|
|
582
890
|
return `// Generated by Pastoria.
|
|
583
891
|
import {createRouterApp} from '#genfiles/router/router';
|
|
@@ -592,10 +900,11 @@ async function main() {
|
|
|
592
900
|
main();
|
|
593
901
|
`;
|
|
594
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Generates the server entry point code.
|
|
905
|
+
*/
|
|
595
906
|
export function generateServerEntry({ hasAppRoot, hasServerHandler, }) {
|
|
596
|
-
const appImport = hasAppRoot
|
|
597
|
-
? `import {App} from '#genfiles/router/app_root';`
|
|
598
|
-
: '';
|
|
907
|
+
const appImport = hasAppRoot ? `import App from './pastoria/app';` : '';
|
|
599
908
|
const appValue = hasAppRoot ? 'App' : 'null';
|
|
600
909
|
const serverHandlerImport = hasServerHandler
|
|
601
910
|
? `import {router as serverHandler} from '#genfiles/router/server_handler';`
|
|
@@ -610,25 +919,15 @@ import {
|
|
|
610
919
|
router__createAppFromEntryPoint,
|
|
611
920
|
router__loadEntryPoint,
|
|
612
921
|
} from '#genfiles/router/router';
|
|
613
|
-
import
|
|
614
|
-
import {Context} from '#genfiles/router/context';
|
|
922
|
+
import environment from './pastoria/environment';
|
|
615
923
|
${appImport}
|
|
616
924
|
${serverHandlerImport}
|
|
617
925
|
import express from 'express';
|
|
618
|
-
import {GraphQLSchema, specifiedDirectives} from 'graphql';
|
|
619
|
-
import {PastoriaConfig} from 'pastoria-config';
|
|
620
926
|
import {createRouterHandler} from 'pastoria-runtime/server';
|
|
621
927
|
import type {Manifest} from 'vite';
|
|
622
928
|
|
|
623
|
-
const schemaConfig = getSchema().toConfig();
|
|
624
|
-
const schema = new GraphQLSchema({
|
|
625
|
-
...schemaConfig,
|
|
626
|
-
directives: [...specifiedDirectives, ...schemaConfig.directives],
|
|
627
|
-
});
|
|
628
|
-
|
|
629
929
|
export function createHandler(
|
|
630
930
|
persistedQueries: Record<string, string>,
|
|
631
|
-
config: Required<PastoriaConfig>,
|
|
632
931
|
manifest?: Manifest,
|
|
633
932
|
) {
|
|
634
933
|
const routeHandler = createRouterHandler(
|
|
@@ -637,10 +936,8 @@ export function createHandler(
|
|
|
637
936
|
router__loadEntryPoint,
|
|
638
937
|
router__createAppFromEntryPoint,
|
|
639
938
|
${appValue},
|
|
640
|
-
|
|
641
|
-
(req) => Context.createFromRequest(req),
|
|
939
|
+
environment,
|
|
642
940
|
persistedQueries,
|
|
643
|
-
config,
|
|
644
941
|
manifest,
|
|
645
942
|
);
|
|
646
943
|
|