@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148
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/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +609 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +238 -349
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -18
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +85 -80
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- package/src/server/flush.ts +0 -183
package/src/plugins/routing.ts
CHANGED
|
@@ -10,24 +10,25 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Plugin, ViteDevServer } from 'vite';
|
|
12
12
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
13
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
import { scanRoutes } from '../routing/scanner.js';
|
|
16
15
|
import { generateRouteMap } from '../routing/codegen.js';
|
|
17
|
-
import { collectInterceptionRewrites } from '../routing/interception.js';
|
|
18
16
|
import {
|
|
19
17
|
buildDirToRouteMap,
|
|
20
18
|
writeSegmentTypes,
|
|
21
19
|
type SegmentInfo,
|
|
22
20
|
} from '../routing/segment-codegen.js';
|
|
23
21
|
import { validateSchemaAgainstRoutes } from '../routing/schema-validation.js';
|
|
24
|
-
|
|
22
|
+
import {
|
|
23
|
+
generateManifestModule,
|
|
24
|
+
generateSearchParamsRegistryModule,
|
|
25
|
+
generateSchemaModule,
|
|
26
|
+
} from '../routing/manifest-codegen.js';
|
|
25
27
|
import {
|
|
26
28
|
lintConventions,
|
|
27
29
|
checkAppDirExists,
|
|
28
30
|
formatConventionWarnings,
|
|
29
31
|
} from '../routing/convention-lint.js';
|
|
30
|
-
import type { RouteTree, SegmentNode, RouteFile } from '../routing/types.js';
|
|
31
32
|
import type { PluginContext } from '../plugin-context.js';
|
|
32
33
|
|
|
33
34
|
const VIRTUAL_MODULE_ID = 'virtual:timber-route-manifest';
|
|
@@ -413,347 +414,3 @@ function invalidateManifest(server: ViteDevServer): void {
|
|
|
413
414
|
// Trigger HMR full reload — route changes are structural
|
|
414
415
|
server.hot.send({ type: 'full-reload' });
|
|
415
416
|
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* TIM-830: Walk the route tree and collect every page that exports a
|
|
419
|
-
* `searchParams` definition, then emit a module that statically imports
|
|
420
|
-
* each export and registers it into the shared search-params registry.
|
|
421
|
-
*
|
|
422
|
-
* Keys use the un-interpolated route pattern (e.g. '/products/[id]') so
|
|
423
|
-
* <Link> can look up the definition from the original `href` prop before
|
|
424
|
-
* segmentParams are interpolated.
|
|
425
|
-
*/
|
|
426
|
-
/** Schema file extensions to check, in priority order. */
|
|
427
|
-
const SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* TIM-931: Generate the virtual:timber-schema module.
|
|
431
|
-
*
|
|
432
|
-
* When app/schema.{ts,tsx,js,jsx} exists, generates a module that
|
|
433
|
-
* imports the schema and resolves its segmentParams codecs into a
|
|
434
|
-
* bare-name codec map for runtime coercion.
|
|
435
|
-
*
|
|
436
|
-
* When no schema exists, exports null.
|
|
437
|
-
*/
|
|
438
|
-
function generateSchemaModule(appDir: string): string {
|
|
439
|
-
let schemaPath: string | null = null;
|
|
440
|
-
for (const ext of SCHEMA_EXTENSIONS) {
|
|
441
|
-
const candidate = join(appDir, `schema${ext}`);
|
|
442
|
-
if (existsSync(candidate)) {
|
|
443
|
-
schemaPath = candidate;
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (!schemaPath) {
|
|
449
|
-
return [
|
|
450
|
-
'// Auto-generated schema loader — no app/schema.ts found.',
|
|
451
|
-
'export default null;',
|
|
452
|
-
].join('\n');
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return [
|
|
456
|
-
'// Auto-generated schema loader — do not edit.',
|
|
457
|
-
'// Generated by timber-routing plugin (TIM-931).',
|
|
458
|
-
'',
|
|
459
|
-
`import schema from ${JSON.stringify(schemaPath)};`,
|
|
460
|
-
`import { resolveSchemaCodecs } from '@timber-js/app/params';`,
|
|
461
|
-
'',
|
|
462
|
-
'const globalCodecs = schema?.segmentParams',
|
|
463
|
-
' ? resolveSchemaCodecs(schema.segmentParams)',
|
|
464
|
-
' : null;',
|
|
465
|
-
'',
|
|
466
|
-
'export default globalCodecs;',
|
|
467
|
-
].join('\n');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function generateSearchParamsRegistryModule(tree: RouteTree): string {
|
|
471
|
-
const entries: Array<{ urlPath: string; filePath: string }> = [];
|
|
472
|
-
|
|
473
|
-
function walk(node: SegmentNode): void {
|
|
474
|
-
// params.ts is the canonical location for `export const searchParams`;
|
|
475
|
-
// page.tsx is the legacy fallback (see codegen.ts `collectRoutes`).
|
|
476
|
-
if (node.page) {
|
|
477
|
-
if (node.params && fileHasSearchParamsExport(node.params.filePath)) {
|
|
478
|
-
entries.push({ urlPath: node.urlPath, filePath: node.params.filePath });
|
|
479
|
-
} else if (fileHasSearchParamsExport(node.page.filePath)) {
|
|
480
|
-
entries.push({ urlPath: node.urlPath, filePath: node.page.filePath });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
for (const child of node.children) walk(child);
|
|
484
|
-
for (const slot of Object.values(node.slots)) walk(slot);
|
|
485
|
-
}
|
|
486
|
-
walk(tree.root);
|
|
487
|
-
|
|
488
|
-
// Deterministic order for stable output / snapshots
|
|
489
|
-
entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
490
|
-
|
|
491
|
-
const lines: string[] = [];
|
|
492
|
-
lines.push('// Auto-generated search-params registry — do not edit.');
|
|
493
|
-
lines.push('// Generated by timber-routing plugin (TIM-830).');
|
|
494
|
-
lines.push('');
|
|
495
|
-
lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
|
|
496
|
-
lines.push('');
|
|
497
|
-
|
|
498
|
-
// Static imports — Vite/Rolldown tree-shakes the page component body out
|
|
499
|
-
// of this chunk, leaving only the `searchParams` named export.
|
|
500
|
-
entries.forEach((entry, i) => {
|
|
501
|
-
lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
|
|
502
|
-
});
|
|
503
|
-
lines.push('');
|
|
504
|
-
|
|
505
|
-
entries.forEach((entry, i) => {
|
|
506
|
-
lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
|
|
507
|
-
});
|
|
508
|
-
lines.push('');
|
|
509
|
-
|
|
510
|
-
// Exporting an empty object keeps the module a real ESM module even
|
|
511
|
-
// when there are zero entries (no top-level imports in that case).
|
|
512
|
-
lines.push('export {};');
|
|
513
|
-
lines.push('');
|
|
514
|
-
|
|
515
|
-
return lines.join('\n');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Lightweight check for a `searchParams` named export. Mirrors
|
|
520
|
-
* `fileHasExport('searchParams')` in routing/codegen.ts so we stay
|
|
521
|
-
* in sync with what the type codegen detects.
|
|
522
|
-
*/
|
|
523
|
-
function fileHasSearchParamsExport(filePath: string): boolean {
|
|
524
|
-
try {
|
|
525
|
-
if (!existsSync(filePath)) return false;
|
|
526
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
527
|
-
return (
|
|
528
|
-
/export\s+(const|let|var)\s+searchParams\b/.test(content) ||
|
|
529
|
-
/export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content)
|
|
530
|
-
);
|
|
531
|
-
} catch {
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Generate the virtual module source code for the route manifest.
|
|
538
|
-
*
|
|
539
|
-
* The output is a default-exported object containing the serialized route tree.
|
|
540
|
-
* All file references use absolute paths (required for virtual modules).
|
|
541
|
-
*/
|
|
542
|
-
function generateManifestModule(tree: RouteTree, viteRoot: string): string {
|
|
543
|
-
const imports: string[] = [];
|
|
544
|
-
let importIndex = 0;
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Create a lazy import expression for a route file.
|
|
548
|
-
* Returns the import variable name.
|
|
549
|
-
*/
|
|
550
|
-
function addImport(file: RouteFile): string {
|
|
551
|
-
const varName = `_route${importIndex++}`;
|
|
552
|
-
imports.push(`const ${varName} = () => import("${file.filePath}");`);
|
|
553
|
-
return varName;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Serialize a segment node to a JS object literal.
|
|
558
|
-
*/
|
|
559
|
-
function serializeNode(node: SegmentNode, indent: string): string {
|
|
560
|
-
const nextIndent = indent + ' ';
|
|
561
|
-
const parts: string[] = [];
|
|
562
|
-
|
|
563
|
-
parts.push(`${nextIndent}segmentName: ${JSON.stringify(node.segmentName)},`);
|
|
564
|
-
parts.push(`${nextIndent}segmentType: ${JSON.stringify(node.segmentType)},`);
|
|
565
|
-
parts.push(`${nextIndent}urlPath: ${JSON.stringify(node.urlPath)},`);
|
|
566
|
-
|
|
567
|
-
if (node.paramName) {
|
|
568
|
-
parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (node.interceptionMarker) {
|
|
572
|
-
parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
|
|
573
|
-
}
|
|
574
|
-
if (node.interceptedSegmentName) {
|
|
575
|
-
parts.push(
|
|
576
|
-
`${nextIndent}interceptedSegmentName: ${JSON.stringify(node.interceptedSegmentName)},`
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// File conventions — absolute paths as lazy imports
|
|
581
|
-
if (node.page) {
|
|
582
|
-
const v = addImport(node.page);
|
|
583
|
-
parts.push(
|
|
584
|
-
`${nextIndent}page: { load: ${v}, filePath: ${JSON.stringify(node.page.filePath)} },`
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
if (node.layout) {
|
|
588
|
-
const v = addImport(node.layout);
|
|
589
|
-
parts.push(
|
|
590
|
-
`${nextIndent}layout: { load: ${v}, filePath: ${JSON.stringify(node.layout.filePath)} },`
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
if (node.middleware) {
|
|
594
|
-
const v = addImport(node.middleware);
|
|
595
|
-
parts.push(
|
|
596
|
-
`${nextIndent}middleware: { load: ${v}, filePath: ${JSON.stringify(node.middleware.filePath)} },`
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
if (node.access) {
|
|
600
|
-
const v = addImport(node.access);
|
|
601
|
-
parts.push(
|
|
602
|
-
`${nextIndent}access: { load: ${v}, filePath: ${JSON.stringify(node.access.filePath)} },`
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
if (node.route) {
|
|
606
|
-
const v = addImport(node.route);
|
|
607
|
-
parts.push(
|
|
608
|
-
`${nextIndent}route: { load: ${v}, filePath: ${JSON.stringify(node.route.filePath)} },`
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
if (node.error) {
|
|
612
|
-
const v = addImport(node.error);
|
|
613
|
-
parts.push(
|
|
614
|
-
`${nextIndent}error: { load: ${v}, filePath: ${JSON.stringify(node.error.filePath)} },`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
if (node.default) {
|
|
618
|
-
const v = addImport(node.default);
|
|
619
|
-
parts.push(
|
|
620
|
-
`${nextIndent}default: { load: ${v}, filePath: ${JSON.stringify(node.default.filePath)} },`
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
if (node.denied) {
|
|
624
|
-
const v = addImport(node.denied);
|
|
625
|
-
parts.push(
|
|
626
|
-
`${nextIndent}denied: { load: ${v}, filePath: ${JSON.stringify(node.denied.filePath)} },`
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
if (node.params) {
|
|
630
|
-
const v = addImport(node.params);
|
|
631
|
-
parts.push(
|
|
632
|
-
`${nextIndent}params: { load: ${v}, filePath: ${JSON.stringify(node.params.filePath)} },`
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
// searchParams is now a named export from page.tsx, not a separate file.
|
|
636
|
-
// The page module's searchParams export is loaded via the page's lazy import.
|
|
637
|
-
// Runtime registration happens in the route loader using the page module.
|
|
638
|
-
|
|
639
|
-
// Status-code files
|
|
640
|
-
if (node.statusFiles && Object.keys(node.statusFiles).length > 0) {
|
|
641
|
-
const statusEntries: string[] = [];
|
|
642
|
-
for (const [code, file] of Object.entries(node.statusFiles)) {
|
|
643
|
-
const v = addImport(file);
|
|
644
|
-
statusEntries.push(
|
|
645
|
-
`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
parts.push(`${nextIndent}statusFiles: {\n${statusEntries.join(',\n')}\n${nextIndent}},`);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// JSON status-code files
|
|
652
|
-
if (node.jsonStatusFiles && Object.keys(node.jsonStatusFiles).length > 0) {
|
|
653
|
-
const jsonEntries: string[] = [];
|
|
654
|
-
for (const [code, file] of Object.entries(node.jsonStatusFiles)) {
|
|
655
|
-
const v = addImport(file);
|
|
656
|
-
jsonEntries.push(
|
|
657
|
-
`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
parts.push(`${nextIndent}jsonStatusFiles: {\n${jsonEntries.join(',\n')}\n${nextIndent}},`);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Legacy status files
|
|
664
|
-
if (node.legacyStatusFiles && Object.keys(node.legacyStatusFiles).length > 0) {
|
|
665
|
-
const legacyEntries: string[] = [];
|
|
666
|
-
for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
|
|
667
|
-
const v = addImport(file);
|
|
668
|
-
legacyEntries.push(
|
|
669
|
-
`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
parts.push(
|
|
673
|
-
`${nextIndent}legacyStatusFiles: {\n${legacyEntries.join(',\n')}\n${nextIndent}},`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.)
|
|
678
|
-
if (node.metadataRoutes && Object.keys(node.metadataRoutes).length > 0) {
|
|
679
|
-
const metaEntries: string[] = [];
|
|
680
|
-
for (const [name, file] of Object.entries(node.metadataRoutes)) {
|
|
681
|
-
const v = addImport(file);
|
|
682
|
-
metaEntries.push(
|
|
683
|
-
`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
parts.push(`${nextIndent}metadataRoutes: {\n${metaEntries.join(',\n')}\n${nextIndent}},`);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Children
|
|
690
|
-
if (node.children.length > 0) {
|
|
691
|
-
const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
|
|
692
|
-
parts.push(`${nextIndent}children: [\n${childNodes.join(',\n')}\n${nextIndent}],`);
|
|
693
|
-
} else {
|
|
694
|
-
parts.push(`${nextIndent}children: [],`);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Parallel slots
|
|
698
|
-
const slotKeys = Object.keys(node.slots);
|
|
699
|
-
if (slotKeys.length > 0) {
|
|
700
|
-
const slotEntries: string[] = [];
|
|
701
|
-
for (const slotName of slotKeys) {
|
|
702
|
-
const slotNode = node.slots[slotName]!;
|
|
703
|
-
slotEntries.push(
|
|
704
|
-
`${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + ' ')}`
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
parts.push(`${nextIndent}slots: {\n${slotEntries.join(',\n')}\n${nextIndent}},`);
|
|
708
|
-
} else {
|
|
709
|
-
parts.push(`${nextIndent}slots: {},`);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return `${indent}{\n${parts.join('\n')}\n${indent}}`;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const rootSerialized = serializeNode(tree.root, ' ');
|
|
716
|
-
|
|
717
|
-
// Proxy file
|
|
718
|
-
let proxyLine = '';
|
|
719
|
-
if (tree.proxy) {
|
|
720
|
-
const v = addImport(tree.proxy);
|
|
721
|
-
proxyLine = ` proxy: { load: ${v}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Global error page (Tier 2)
|
|
725
|
-
let globalErrorLine = '';
|
|
726
|
-
if (tree.globalError) {
|
|
727
|
-
const v = addImport(tree.globalError);
|
|
728
|
-
globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Interception rewrites — computed at build time from the route tree.
|
|
732
|
-
// Only interceptedPattern and interceptingPrefix are needed at runtime.
|
|
733
|
-
const rewrites = collectInterceptionRewrites(tree.root);
|
|
734
|
-
const rewritesLine =
|
|
735
|
-
rewrites.length > 0
|
|
736
|
-
? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
|
|
737
|
-
: '';
|
|
738
|
-
|
|
739
|
-
const code = [
|
|
740
|
-
'// Auto-generated route manifest — do not edit.',
|
|
741
|
-
'// Generated by timber-routing plugin.',
|
|
742
|
-
'',
|
|
743
|
-
...imports,
|
|
744
|
-
'',
|
|
745
|
-
'const manifest = {',
|
|
746
|
-
` viteRoot: ${JSON.stringify(viteRoot)},`,
|
|
747
|
-
proxyLine,
|
|
748
|
-
globalErrorLine,
|
|
749
|
-
rewritesLine,
|
|
750
|
-
` root: ${rootSerialized},`,
|
|
751
|
-
'};',
|
|
752
|
-
'',
|
|
753
|
-
'export default manifest;',
|
|
754
|
-
]
|
|
755
|
-
.filter((line) => line !== undefined && line !== '')
|
|
756
|
-
.join('\n');
|
|
757
|
-
|
|
758
|
-
return code;
|
|
759
|
-
}
|
|
@@ -21,6 +21,10 @@ export interface RouteEntry {
|
|
|
21
21
|
searchParamsPagePath?: string;
|
|
22
22
|
/** Whether this is an API route (route.ts) vs page route */
|
|
23
23
|
isApiRoute: boolean;
|
|
24
|
+
/** Whether this entry lives inside a parallel slot (@name). Slot entries
|
|
25
|
+
* share their parent's urlPath — urlPath-keyed emissions dedupe them,
|
|
26
|
+
* preferring the non-slot entry. segmentPath-keyed emissions keep all. */
|
|
27
|
+
isSlotRoute?: boolean;
|
|
24
28
|
/** Whether a global schema (app/schema.ts) exists for this app */
|
|
25
29
|
hasSchema?: boolean;
|
|
26
30
|
}
|
package/src/routing/codegen.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* No runtime overhead — purely static type generation.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import type { RouteTree, SegmentNode } from './types.js';
|
|
14
14
|
import type { ParamEntry, RouteEntry } from './codegen-types.js';
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
emitResolveSegmentFieldHelper,
|
|
19
19
|
formatSearchParamsType,
|
|
20
20
|
} from './codegen-shared.js';
|
|
21
|
+
import { readFileCached } from './file-cache.js';
|
|
21
22
|
import { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';
|
|
22
23
|
|
|
23
24
|
/** Options for route map generation. */
|
|
@@ -48,7 +49,7 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
|
|
|
48
49
|
const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
|
|
49
50
|
|
|
50
51
|
const routes: RouteEntry[] = [];
|
|
51
|
-
collectRoutes(tree.root, [], [], '', routes, hasSchema);
|
|
52
|
+
collectRoutes(tree.root, [], [], '', routes, hasSchema, false);
|
|
52
53
|
|
|
53
54
|
// Sort routes alphabetically for deterministic output
|
|
54
55
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
@@ -82,7 +83,8 @@ function collectRoutes(
|
|
|
82
83
|
ancestorParamsFiles: string[],
|
|
83
84
|
parentTreePath: string,
|
|
84
85
|
routes: RouteEntry[],
|
|
85
|
-
hasSchema: boolean
|
|
86
|
+
hasSchema: boolean,
|
|
87
|
+
insideSlot: boolean
|
|
86
88
|
): void {
|
|
87
89
|
// Build the tree path (includes groups, slots — uniquely identifies this segment).
|
|
88
90
|
// Root node has empty segmentName, producing treePath = ''.
|
|
@@ -156,6 +158,7 @@ function collectRoutes(
|
|
|
156
158
|
params: resolvedParams,
|
|
157
159
|
hasSearchParams: false,
|
|
158
160
|
isApiRoute,
|
|
161
|
+
isSlotRoute: insideSlot,
|
|
159
162
|
hasSchema,
|
|
160
163
|
};
|
|
161
164
|
|
|
@@ -175,15 +178,63 @@ function collectRoutes(
|
|
|
175
178
|
|
|
176
179
|
// Recurse into children
|
|
177
180
|
for (const child of node.children) {
|
|
178
|
-
collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema);
|
|
181
|
+
collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot);
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
// Recurse into slots (they share the parent's URL path, but may have their own pages)
|
|
182
185
|
for (const slot of Object.values(node.slots)) {
|
|
183
|
-
collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema);
|
|
186
|
+
collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true);
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
|
|
192
|
+
* interface, useQueryStates overloads, and typed Link variants).
|
|
193
|
+
*
|
|
194
|
+
* Parallel-slot pages share their parent's urlPath; emitting one entry per
|
|
195
|
+
* slot page produced duplicate interface keys (TS2300/TS2717 under
|
|
196
|
+
* skipLibCheck:false) and duplicate overloads. The non-slot entry is the
|
|
197
|
+
* canonical one; when only slot entries exist (parent has no page), the
|
|
198
|
+
* lexicographically smallest segmentPath wins so output is independent of
|
|
199
|
+
* slot scan order. A slot's searchParams definition is carried onto the
|
|
200
|
+
* merged entry when the canonical entry has none — never silently dropped.
|
|
201
|
+
*
|
|
202
|
+
* segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
|
|
203
|
+
* TimberSegmentParams) must NOT use this — segmentPaths are already unique
|
|
204
|
+
* and slot entries feed those APIs directly.
|
|
205
|
+
*/
|
|
206
|
+
function dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {
|
|
207
|
+
const byPath = new Map<string, RouteEntry>();
|
|
208
|
+
for (const route of routes) {
|
|
209
|
+
const existing = byPath.get(route.urlPath);
|
|
210
|
+
if (!existing) {
|
|
211
|
+
byPath.set(route.urlPath, route);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let winner = existing;
|
|
216
|
+
let loser = route;
|
|
217
|
+
const routeWins =
|
|
218
|
+
(existing.isSlotRoute && !route.isSlotRoute) ||
|
|
219
|
+
(!!existing.isSlotRoute === !!route.isSlotRoute &&
|
|
220
|
+
route.segmentPath.localeCompare(existing.segmentPath) < 0);
|
|
221
|
+
if (routeWins) {
|
|
222
|
+
winner = route;
|
|
223
|
+
loser = existing;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!winner.hasSearchParams && loser.hasSearchParams) {
|
|
227
|
+
winner = {
|
|
228
|
+
...winner,
|
|
229
|
+
hasSearchParams: true,
|
|
230
|
+
searchParamsPagePath: loser.searchParamsPagePath,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
byPath.set(route.urlPath, winner);
|
|
234
|
+
}
|
|
235
|
+
return [...byPath.values()];
|
|
236
|
+
}
|
|
237
|
+
|
|
187
238
|
/**
|
|
188
239
|
* Determine the TypeScript type for a segment's param.
|
|
189
240
|
*/
|
|
@@ -207,7 +258,7 @@ function paramTypeForSegment(segmentType: string): ParamEntry['type'] {
|
|
|
207
258
|
function fileHasExport(filePath: string, exportName: string): boolean {
|
|
208
259
|
if (!existsSync(filePath)) return false;
|
|
209
260
|
try {
|
|
210
|
-
const content =
|
|
261
|
+
const content = readFileCached(filePath);
|
|
211
262
|
const e = exportName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
263
|
return (
|
|
213
264
|
new RegExp(`export\\s+(const|let|var)\\s+${e}\\b`).test(content) ||
|
|
@@ -366,10 +417,14 @@ function formatDeclarationFile(
|
|
|
366
417
|
lines.push('');
|
|
367
418
|
}
|
|
368
419
|
|
|
420
|
+
// urlPath-keyed emissions use the deduped list (slot pages share their
|
|
421
|
+
// parent's urlPath); segmentPath-keyed emissions below keep all entries.
|
|
422
|
+
const uniqueByUrlPath = dedupeRoutesByUrlPath(routes);
|
|
423
|
+
|
|
369
424
|
lines.push("declare module '@timber-js/app' {");
|
|
370
425
|
lines.push(' interface Routes {');
|
|
371
426
|
|
|
372
|
-
for (const route of
|
|
427
|
+
for (const route of uniqueByUrlPath) {
|
|
373
428
|
const paramsType = formatParamsType(route.params, importBase, hasSchema);
|
|
374
429
|
const searchParamsType = formatSearchParamsType(route, importBase);
|
|
375
430
|
|
|
@@ -393,7 +448,10 @@ function formatDeclarationFile(
|
|
|
393
448
|
lines.push('}');
|
|
394
449
|
lines.push('');
|
|
395
450
|
|
|
396
|
-
|
|
451
|
+
// useQueryStates + typed Link are urlPath-keyed → deduped list.
|
|
452
|
+
const pageRoutes = uniqueByUrlPath.filter((r) => !r.isApiRoute);
|
|
453
|
+
// getSegmentParams / useSegmentParams are segmentPath-keyed → full list,
|
|
454
|
+
// so slot segments keep their own typed overloads.
|
|
397
455
|
const dynamicRoutes = routes.filter((r) => r.params.length > 0);
|
|
398
456
|
|
|
399
457
|
// Generate @timber-js/app/server augmentation — typed getSegmentParams
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* Design doc: 07-routing.md, 10-error-handling.md
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
15
|
import type { RouteTree, SegmentNode } from './types.js';
|
|
16
16
|
import { swallow } from '../server/logger.js';
|
|
17
|
+
import { readFileCached } from './file-cache.js';
|
|
17
18
|
|
|
18
19
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -141,7 +142,7 @@ function checkRouteExports(node: SegmentNode, warnings: ConventionWarning[]): vo
|
|
|
141
142
|
if (node.route) {
|
|
142
143
|
const filePath = node.route.filePath;
|
|
143
144
|
try {
|
|
144
|
-
const source =
|
|
145
|
+
const source = readFileCached(filePath);
|
|
145
146
|
if (!EXPORT_PATTERN.test(source)) {
|
|
146
147
|
// Check if there's a default export (common mistake)
|
|
147
148
|
const hasDefaultExport = /export\s+default\b/.test(source);
|
|
@@ -265,7 +266,7 @@ function checkFileDefaultExport(
|
|
|
265
266
|
warnings: ConventionWarning[]
|
|
266
267
|
): void {
|
|
267
268
|
try {
|
|
268
|
-
const source =
|
|
269
|
+
const source = readFileCached(filePath);
|
|
269
270
|
if (!DEFAULT_EXPORT_PATTERN.test(source)) {
|
|
270
271
|
warnings.push({
|
|
271
272
|
id: `NO_DEFAULT_EXPORT:${filePath}`,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const cache = new Map<string, { mtimeMs: number; size: number; content: string }>();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read a file's text content, returning a cached copy when the file
|
|
7
|
+
* hasn't been modified since the last read (checked via mtime + size).
|
|
8
|
+
*
|
|
9
|
+
* Falls back to a fresh read on any stat/read error.
|
|
10
|
+
*/
|
|
11
|
+
export function readFileCached(filePath: string): string {
|
|
12
|
+
try {
|
|
13
|
+
const stat = statSync(filePath);
|
|
14
|
+
const entry = cache.get(filePath);
|
|
15
|
+
if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;
|
|
16
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
17
|
+
cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, content });
|
|
18
|
+
return content;
|
|
19
|
+
} catch {
|
|
20
|
+
cache.delete(filePath);
|
|
21
|
+
return readFileSync(filePath, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -32,11 +32,6 @@ export interface InterceptionRewrite {
|
|
|
32
32
|
* E.g., "/feed" for a (.)photo/[id] inside /feed/@modal/.
|
|
33
33
|
*/
|
|
34
34
|
interceptingPrefix: string;
|
|
35
|
-
/**
|
|
36
|
-
* Segments chain from root → intercepting leaf. Used to build the element
|
|
37
|
-
* tree when the interception is active.
|
|
38
|
-
*/
|
|
39
|
-
segmentPath: SegmentNode[];
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
/**
|
|
@@ -94,13 +89,7 @@ function collectFromInterceptingNode(
|
|
|
94
89
|
interceptedBase === '/' ? `/${segmentName}` : `${interceptedBase}/${segmentName}`;
|
|
95
90
|
|
|
96
91
|
// Find all leaf pages in the intercepting sub-tree
|
|
97
|
-
collectLeavesWithRewrites(
|
|
98
|
-
interceptingNode,
|
|
99
|
-
interceptedUrlBase,
|
|
100
|
-
parentUrlPath,
|
|
101
|
-
[...ancestors, interceptingNode],
|
|
102
|
-
rewrites
|
|
103
|
-
);
|
|
92
|
+
collectLeavesWithRewrites(interceptingNode, interceptedUrlBase, parentUrlPath, rewrites);
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
/**
|
|
@@ -111,14 +100,12 @@ function collectLeavesWithRewrites(
|
|
|
111
100
|
node: SegmentNode,
|
|
112
101
|
interceptedUrlPath: string,
|
|
113
102
|
interceptingPrefix: string,
|
|
114
|
-
segmentPath: SegmentNode[],
|
|
115
103
|
rewrites: InterceptionRewrite[]
|
|
116
104
|
): void {
|
|
117
105
|
if (node.page) {
|
|
118
106
|
rewrites.push({
|
|
119
107
|
interceptedPattern: interceptedUrlPath,
|
|
120
108
|
interceptingPrefix,
|
|
121
|
-
segmentPath: [...segmentPath],
|
|
122
109
|
});
|
|
123
110
|
}
|
|
124
111
|
|
|
@@ -127,13 +114,7 @@ function collectLeavesWithRewrites(
|
|
|
127
114
|
child.segmentType === 'group'
|
|
128
115
|
? interceptedUrlPath
|
|
129
116
|
: `${interceptedUrlPath}/${child.segmentName}`;
|
|
130
|
-
collectLeavesWithRewrites(
|
|
131
|
-
child,
|
|
132
|
-
childUrl,
|
|
133
|
-
interceptingPrefix,
|
|
134
|
-
[...segmentPath, child],
|
|
135
|
-
rewrites
|
|
136
|
-
);
|
|
117
|
+
collectLeavesWithRewrites(child, childUrl, interceptingPrefix, rewrites);
|
|
137
118
|
}
|
|
138
119
|
}
|
|
139
120
|
|