pastoria 1.0.15 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build_command.d.ts +2 -0
- package/dist/build_command.d.ts.map +1 -0
- package/dist/build_command.js +13 -0
- package/dist/build_command.js.map +1 -0
- package/dist/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/gen.d.ts +2 -0
- package/dist/gen.d.ts.map +1 -0
- package/dist/gen.js +19 -0
- package/dist/gen.js.map +1 -0
- package/dist/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 +74 -10
- 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 +8 -5
- package/templates/js_resource.ts +7 -1
- package/templates/router.tsx +187 -44
- package/CHANGELOG.md +0 -98
- package/src/build.ts +0 -306
- package/src/devserver.ts +0 -58
- package/src/generate.ts +0 -918
- package/src/index.ts +0 -40
- package/src/logger.ts +0 -12
- package/src/vite_plugin.ts +0 -109
- package/tsconfig.json +0 -21
package/templates/router.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
RouterOps,
|
|
6
6
|
} from 'pastoria-runtime';
|
|
7
7
|
import {createRouter} from 'radix3';
|
|
8
|
-
import {
|
|
8
|
+
import React, {
|
|
9
9
|
AnchorHTMLAttributes,
|
|
10
10
|
createContext,
|
|
11
11
|
PropsWithChildren,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
useEffect,
|
|
17
17
|
useMemo,
|
|
18
18
|
useState,
|
|
19
|
+
useTransition,
|
|
19
20
|
} from 'react';
|
|
20
21
|
import {preinit, preloadModule} from 'react-dom';
|
|
21
22
|
import {
|
|
@@ -25,12 +26,17 @@ import {
|
|
|
25
26
|
RelayEnvironmentProvider,
|
|
26
27
|
useEntryPointLoader,
|
|
27
28
|
} from 'react-relay/hooks';
|
|
29
|
+
import {PreloadableQueryRegistry} from 'relay-runtime';
|
|
28
30
|
import * as z from 'zod/v4-mini';
|
|
29
31
|
|
|
30
32
|
type RouterConf = typeof ROUTER_CONF;
|
|
33
|
+
type AnyRouteParams = z.infer<RouterConf[keyof RouterConf]['schema']>;
|
|
34
|
+
type AnyRouteEntryPoint = RouterConf[keyof RouterConf]['entrypoint'];
|
|
35
|
+
type LoadEntryPointFn = (params: {params: AnyRouteParams}) => void;
|
|
36
|
+
|
|
31
37
|
const ROUTER_CONF = {
|
|
32
38
|
noop: {
|
|
33
|
-
entrypoint: null! as EntryPoint<any>,
|
|
39
|
+
entrypoint: null! as EntryPoint<any, any>,
|
|
34
40
|
schema: z.object({}),
|
|
35
41
|
},
|
|
36
42
|
} as const;
|
|
@@ -39,12 +45,77 @@ export type RouteId = keyof RouterConf;
|
|
|
39
45
|
export type NavigationDirection = string | URL | ((nextUrl: URL) => void);
|
|
40
46
|
|
|
41
47
|
export interface EntryPointParams<R extends RouteId> {
|
|
42
|
-
params:
|
|
43
|
-
|
|
48
|
+
params: z.infer<RouterConf[R]['schema']>;
|
|
49
|
+
queries: QueryHelpersForRoute<R>;
|
|
50
|
+
entryPoints: EntryPointHelpersForRoute<R>;
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Load a route entry point with proper typing.
|
|
55
|
+
*
|
|
56
|
+
* This wrapper exists because TypeScript struggles with union type inference
|
|
57
|
+
* when calling loadEntryPoint with a union of entry point types. All route
|
|
58
|
+
* entry points accept {params: Record<string, unknown>}, so this is safe.
|
|
59
|
+
*/
|
|
60
|
+
function loadRouteEntryPoint(
|
|
61
|
+
provider: EnvironmentProvider,
|
|
62
|
+
entrypoint: AnyRouteEntryPoint,
|
|
63
|
+
params: {params: AnyRouteParams},
|
|
64
|
+
) {
|
|
65
|
+
// Cast needed because Relay's loadEntryPoint infers params from the entry point type.
|
|
66
|
+
// When entrypoint is a union, the inferred params become an intersection (contravariance),
|
|
67
|
+
// which resolves to `never`. Our entry points all accept the same params shape, so this is safe.
|
|
68
|
+
return loadEntryPoint(
|
|
69
|
+
provider,
|
|
70
|
+
entrypoint as unknown as EntryPoint<unknown, {params: AnyRouteParams}>,
|
|
71
|
+
params,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Convert bracket format [param] to colon format :param for radix3 router
|
|
76
|
+
function bracketToColon(path: string): string {
|
|
77
|
+
// Convert required params [param] to :param
|
|
78
|
+
// Note: optional params [[param]] are handled separately by expandOptionalRoutes
|
|
79
|
+
return path.replace(/\[([^\]]+)\]/g, ':$1');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Expand routes with optional params into multiple routes
|
|
83
|
+
// e.g., /greet/[[name]] becomes ['/greet', '/greet/:name']
|
|
84
|
+
function expandOptionalRoutes(
|
|
85
|
+
routePath: string,
|
|
86
|
+
config: RouterConf[keyof RouterConf],
|
|
87
|
+
): Array<[string, RouterConf[keyof RouterConf]]> {
|
|
88
|
+
// Check if route has optional params
|
|
89
|
+
const optionalMatch = routePath.match(/\/\[\[([^\]]+)\]\]/g);
|
|
90
|
+
if (!optionalMatch) {
|
|
91
|
+
// No optional params, just convert brackets to colons
|
|
92
|
+
return [[bracketToColon(routePath), config]];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For routes with optional params, create two routes:
|
|
96
|
+
// 1. Without the optional segment (e.g., /greet)
|
|
97
|
+
// 2. With the optional segment as required (e.g., /greet/:name)
|
|
98
|
+
const withoutOptional = routePath.replace(/\/\[\[([^\]]+)\]\]/g, '');
|
|
99
|
+
const withOptional = routePath.replace(/\[\[([^\]]+)\]\]/g, '[$1]');
|
|
100
|
+
|
|
101
|
+
const routes: Array<[string, RouterConf[keyof RouterConf]]> = [];
|
|
102
|
+
|
|
103
|
+
// Add the route without optional param (handles /greet)
|
|
104
|
+
const pathWithout = bracketToColon(withoutOptional) || '/';
|
|
105
|
+
routes.push([pathWithout, config]);
|
|
106
|
+
|
|
107
|
+
// Add the route with optional param as required (handles /greet/:name)
|
|
108
|
+
routes.push([bracketToColon(withOptional), config]);
|
|
109
|
+
|
|
110
|
+
return routes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create radix3 router with colon-format paths (radix3 uses :param syntax)
|
|
114
|
+
// Routes with optional params are expanded into multiple routes
|
|
46
115
|
const ROUTER = createRouter<RouterConf[keyof RouterConf]>({
|
|
47
|
-
routes:
|
|
116
|
+
routes: Object.fromEntries(
|
|
117
|
+
Object.entries(ROUTER_CONF).flatMap(([k, v]) => expandOptionalRoutes(k, v)),
|
|
118
|
+
) as Record<string, RouterConf[keyof RouterConf]>,
|
|
48
119
|
});
|
|
49
120
|
|
|
50
121
|
class RouterLocation {
|
|
@@ -66,7 +137,7 @@ class RouterLocation {
|
|
|
66
137
|
return ROUTER.lookup(this.pathname);
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
params() {
|
|
140
|
+
params(): AnyRouteParams {
|
|
70
141
|
const matchedRoute = this.route();
|
|
71
142
|
const params = {
|
|
72
143
|
...matchedRoute?.params,
|
|
@@ -76,7 +147,7 @@ class RouterLocation {
|
|
|
76
147
|
if (matchedRoute?.schema) {
|
|
77
148
|
return matchedRoute.schema.parse(params);
|
|
78
149
|
} else {
|
|
79
|
-
return params;
|
|
150
|
+
return params as AnyRouteParams;
|
|
80
151
|
}
|
|
81
152
|
}
|
|
82
153
|
|
|
@@ -127,6 +198,15 @@ export function router__hydrateStore(provider: EnvironmentProvider) {
|
|
|
127
198
|
if ('__router_ops' in window) {
|
|
128
199
|
const ops = (window as any).__router_ops as RouterOps;
|
|
129
200
|
for (const [op, payload] of ops) {
|
|
201
|
+
// Register the ConcreteRequest with PreloadableQueryRegistry so that
|
|
202
|
+
// loadQuery can find it and check store availability instead of
|
|
203
|
+
// immediately fetching. This is critical for nested entry points whose
|
|
204
|
+
// query modules haven't been loaded yet.
|
|
205
|
+
const concreteRequest = op.request?.node;
|
|
206
|
+
const queryId = concreteRequest?.params?.id;
|
|
207
|
+
if (queryId && concreteRequest) {
|
|
208
|
+
PreloadableQueryRegistry.set(queryId, concreteRequest);
|
|
209
|
+
}
|
|
130
210
|
env.commitPayload(op, payload);
|
|
131
211
|
}
|
|
132
212
|
}
|
|
@@ -142,10 +222,32 @@ export async function router__loadEntryPoint(
|
|
|
142
222
|
if (!initialRoute) return null;
|
|
143
223
|
|
|
144
224
|
await initialRoute.entrypoint?.root.load();
|
|
145
|
-
|
|
225
|
+
const ep = loadRouteEntryPoint(provider, initialRoute.entrypoint, {
|
|
146
226
|
params: initialLocation.params(),
|
|
147
|
-
schema: initialRoute.schema,
|
|
148
227
|
});
|
|
228
|
+
|
|
229
|
+
// Recursively load all nested entry point modules.
|
|
230
|
+
// This ensures their queries get registered in PreloadableQueryRegistry
|
|
231
|
+
// before the server tries to serialize them.
|
|
232
|
+
await loadNestedEntryPointModules(ep);
|
|
233
|
+
|
|
234
|
+
return ep;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Recursively load all nested entry point modules so their queries
|
|
239
|
+
* get registered in PreloadableQueryRegistry.
|
|
240
|
+
*/
|
|
241
|
+
async function loadNestedEntryPointModules(
|
|
242
|
+
entryPoint: AnyPreloadedEntryPoint | null,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
if (!entryPoint) return;
|
|
245
|
+
for (const nestedEntry of Object.values(entryPoint.entryPoints ?? {})) {
|
|
246
|
+
if (nestedEntry.rootModuleID) {
|
|
247
|
+
await JSResource.fromModuleId(nestedEntry.rootModuleID as any).load();
|
|
248
|
+
}
|
|
249
|
+
await loadNestedEntryPointModules(nestedEntry);
|
|
250
|
+
}
|
|
149
251
|
}
|
|
150
252
|
|
|
151
253
|
interface RouterContextValue {
|
|
@@ -229,13 +331,11 @@ export function router__createAppFromEntryPoint(
|
|
|
229
331
|
location.route()?.entrypoint,
|
|
230
332
|
);
|
|
231
333
|
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
schema,
|
|
238
|
-
});
|
|
334
|
+
useMemo(() => {
|
|
335
|
+
const route = location.route();
|
|
336
|
+
if (route) {
|
|
337
|
+
// Cast needed for same reason as loadRouteEntryPoint - see that function's docs
|
|
338
|
+
(loadEntryPointRef as LoadEntryPointFn)({params: location.params()});
|
|
239
339
|
}
|
|
240
340
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
341
|
}, [location]);
|
|
@@ -296,7 +396,7 @@ export function useRouteParams<R extends RouteId>(
|
|
|
296
396
|
|
|
297
397
|
function router__createPathForRoute(
|
|
298
398
|
routeId: RouteId,
|
|
299
|
-
inputParams: Record<string,
|
|
399
|
+
inputParams: Record<string, unknown>,
|
|
300
400
|
): string {
|
|
301
401
|
const schema = ROUTER_CONF[routeId].schema;
|
|
302
402
|
const params = schema.parse(inputParams);
|
|
@@ -305,19 +405,47 @@ function router__createPathForRoute(
|
|
|
305
405
|
const searchParams = new URLSearchParams();
|
|
306
406
|
|
|
307
407
|
Object.entries(params).forEach(([key, value]) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
408
|
+
// Check for optional param pattern first: [[key]]
|
|
409
|
+
const optionalPattern = `[[${key}]]`;
|
|
410
|
+
if (pathname.includes(optionalPattern)) {
|
|
411
|
+
if (value != null) {
|
|
311
412
|
pathname = pathname.replace(
|
|
312
|
-
|
|
413
|
+
optionalPattern,
|
|
313
414
|
encodeURIComponent(String(value)),
|
|
314
415
|
);
|
|
315
416
|
} else {
|
|
316
|
-
|
|
417
|
+
// Remove the optional segment entirely (including the preceding slash if present)
|
|
418
|
+
pathname = pathname.replace(`/${optionalPattern}`, '');
|
|
419
|
+
pathname = pathname.replace(optionalPattern, '');
|
|
317
420
|
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check for required param pattern: [key]
|
|
425
|
+
const paramPattern = `[${key}]`;
|
|
426
|
+
if (pathname.includes(paramPattern)) {
|
|
427
|
+
if (value != null) {
|
|
428
|
+
pathname = pathname.replace(
|
|
429
|
+
paramPattern,
|
|
430
|
+
encodeURIComponent(String(value)),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Not a path param, add to search params if non-null
|
|
437
|
+
if (value != null) {
|
|
438
|
+
searchParams.set(key, String(value));
|
|
318
439
|
}
|
|
319
440
|
});
|
|
320
441
|
|
|
442
|
+
// Clean up any double slashes that might result
|
|
443
|
+
pathname = pathname.replace(/\/+/g, '/');
|
|
444
|
+
// Ensure we don't end up with empty path
|
|
445
|
+
if (pathname === '') {
|
|
446
|
+
pathname = '/';
|
|
447
|
+
}
|
|
448
|
+
|
|
321
449
|
if (searchParams.size > 0) {
|
|
322
450
|
return pathname + '?' + searchParams.toString();
|
|
323
451
|
} else {
|
|
@@ -349,48 +477,63 @@ function router__evaluateNavigationDirection(nav: NavigationDirection) {
|
|
|
349
477
|
|
|
350
478
|
export function useNavigation() {
|
|
351
479
|
const {setLocation} = useContext(RouterContext);
|
|
480
|
+
const [isPending, startTransition] = useTransition();
|
|
352
481
|
|
|
353
482
|
return useMemo(() => {
|
|
354
483
|
function push(nav: NavigationDirection) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
484
|
+
startTransition(() => {
|
|
485
|
+
setLocation(
|
|
486
|
+
RouterLocation.parse(
|
|
487
|
+
router__evaluateNavigationDirection(nav),
|
|
488
|
+
'push',
|
|
489
|
+
),
|
|
490
|
+
);
|
|
491
|
+
});
|
|
358
492
|
}
|
|
359
493
|
|
|
360
494
|
function replace(nav: NavigationDirection) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
495
|
+
startTransition(() => {
|
|
496
|
+
setLocation(
|
|
497
|
+
RouterLocation.parse(
|
|
498
|
+
router__evaluateNavigationDirection(nav),
|
|
499
|
+
'replace',
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
});
|
|
367
503
|
}
|
|
368
504
|
|
|
369
505
|
function pushRoute<R extends RouteId>(
|
|
370
506
|
routeId: R,
|
|
371
507
|
params: z.input<RouterConf[R]['schema']>,
|
|
372
508
|
) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
509
|
+
startTransition(() => {
|
|
510
|
+
setLocation(
|
|
511
|
+
RouterLocation.parse(
|
|
512
|
+
router__createPathForRoute(routeId, params),
|
|
513
|
+
'push',
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
});
|
|
379
517
|
}
|
|
380
518
|
|
|
381
519
|
function replaceRoute<R extends RouteId>(
|
|
382
520
|
routeId: R,
|
|
383
521
|
params: z.input<RouterConf[R]['schema']>,
|
|
384
522
|
) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
523
|
+
startTransition(() => {
|
|
524
|
+
setLocation((prevLoc) =>
|
|
525
|
+
RouterLocation.parse(
|
|
526
|
+
router__createPathForRoute(routeId, {
|
|
527
|
+
...prevLoc.params(),
|
|
528
|
+
...params,
|
|
529
|
+
}),
|
|
530
|
+
'replace',
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
});
|
|
391
534
|
}
|
|
392
535
|
|
|
393
|
-
return {push, replace, pushRoute, replaceRoute} as const;
|
|
536
|
+
return {push, replace, pushRoute, replaceRoute, isPending} as const;
|
|
394
537
|
}, [setLocation]);
|
|
395
538
|
}
|
|
396
539
|
|
package/CHANGELOG.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# pastoria
|
|
2
|
-
|
|
3
|
-
## 1.0.15
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- 96bcdfd: Always run relay-compiler with repersist
|
|
8
|
-
|
|
9
|
-
## 1.0.14
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- a38076a: Trim down variables passed to entrypoints to only those needed
|
|
14
|
-
|
|
15
|
-
## 1.0.13
|
|
16
|
-
|
|
17
|
-
### Patch Changes
|
|
18
|
-
|
|
19
|
-
- 0ef0bdc: Automatically infer variable names and types from queries on
|
|
20
|
-
resource-routes if none provided
|
|
21
|
-
|
|
22
|
-
## 1.0.12
|
|
23
|
-
|
|
24
|
-
### Patch Changes
|
|
25
|
-
|
|
26
|
-
- 641b356: Add pastoria make command
|
|
27
|
-
|
|
28
|
-
## 1.0.11
|
|
29
|
-
|
|
30
|
-
### Patch Changes
|
|
31
|
-
|
|
32
|
-
- b139f3c: Bump patch versions
|
|
33
|
-
- Updated dependencies [b139f3c]
|
|
34
|
-
- pastoria-config@1.0.1
|
|
35
|
-
|
|
36
|
-
## 1.0.10
|
|
37
|
-
|
|
38
|
-
### Patch Changes
|
|
39
|
-
|
|
40
|
-
- 1ae49d3: Update router generator to automatically create an entrypoint and
|
|
41
|
-
resource
|
|
42
|
-
- 4ed4588: Don't duplicate JSResource import when generating code
|
|
43
|
-
- e7e5519: Detect queries and entrypoints from type annotations
|
|
44
|
-
|
|
45
|
-
## 1.0.9
|
|
46
|
-
|
|
47
|
-
### Patch Changes
|
|
48
|
-
|
|
49
|
-
- 60ab948: Add support for routing shorthands
|
|
50
|
-
- 37afd6c: Add support for @serverRoute
|
|
51
|
-
|
|
52
|
-
## 1.0.8
|
|
53
|
-
|
|
54
|
-
### Patch Changes
|
|
55
|
-
|
|
56
|
-
- 265903c: Add babel-plugin-relay dependency
|
|
57
|
-
|
|
58
|
-
## 1.0.7
|
|
59
|
-
|
|
60
|
-
### Patch Changes
|
|
61
|
-
|
|
62
|
-
- Move generated persisted queries output
|
|
63
|
-
|
|
64
|
-
## 1.0.6
|
|
65
|
-
|
|
66
|
-
### Patch Changes
|
|
67
|
-
|
|
68
|
-
- d81851d: Added a custom GraphQL server to remove the Yoga dependency
|
|
69
|
-
|
|
70
|
-
## 1.0.5
|
|
71
|
-
|
|
72
|
-
### Patch Changes
|
|
73
|
-
|
|
74
|
-
- Add pastoria-runtime to `noExternal`
|
|
75
|
-
|
|
76
|
-
## 1.0.4
|
|
77
|
-
|
|
78
|
-
### Patch Changes
|
|
79
|
-
|
|
80
|
-
- Update tsconfig to target nodenext modules
|
|
81
|
-
|
|
82
|
-
## 1.0.3
|
|
83
|
-
|
|
84
|
-
### Patch Changes
|
|
85
|
-
|
|
86
|
-
- Fix package.json bin config
|
|
87
|
-
|
|
88
|
-
## 1.0.2
|
|
89
|
-
|
|
90
|
-
### Patch Changes
|
|
91
|
-
|
|
92
|
-
- Remove usage of experimentalStripTypes
|
|
93
|
-
|
|
94
|
-
## 1.0.1
|
|
95
|
-
|
|
96
|
-
### Patch Changes
|
|
97
|
-
|
|
98
|
-
- Pastoria alpha version for testing
|