@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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/AGENTS.md +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2170 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -22,6 +22,61 @@ export interface UnresolvableInclude {
|
|
|
22
22
|
detail: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Per-scan memo
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-scan memo shared across the include-resolution recursion. Keys are
|
|
31
|
+
* absolute file paths; values cache the readFileSync result and the
|
|
32
|
+
* ts.SourceFile parsed from the FULL file source. A separate blockSources map
|
|
33
|
+
* caches parses of extracted sub-blocks (urls() call text), keyed by the exact
|
|
34
|
+
* block string, so the two extractors (routes + includes) for the same block
|
|
35
|
+
* share a single parse.
|
|
36
|
+
*
|
|
37
|
+
* The memo is purely a performance accelerator: same input -> same parse, so
|
|
38
|
+
* generated output is identical to the un-memoized path. A fresh memo is
|
|
39
|
+
* created per top-level scan entry, so stale file edits between scans are never
|
|
40
|
+
* served.
|
|
41
|
+
*/
|
|
42
|
+
export interface ScanMemo {
|
|
43
|
+
files: Map<string, string>;
|
|
44
|
+
blockSourceFiles: Map<string, ts.SourceFile>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createScanMemo(): ScanMemo {
|
|
48
|
+
return { files: new Map(), blockSourceFiles: new Map() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseBlock(memo: ScanMemo | undefined, block: string): ts.SourceFile {
|
|
52
|
+
if (memo) {
|
|
53
|
+
const cached = memo.blockSourceFiles.get(block);
|
|
54
|
+
if (cached) return cached;
|
|
55
|
+
}
|
|
56
|
+
const sf = ts.createSourceFile(
|
|
57
|
+
"input.tsx",
|
|
58
|
+
block,
|
|
59
|
+
ts.ScriptTarget.Latest,
|
|
60
|
+
true,
|
|
61
|
+
ts.ScriptKind.TSX,
|
|
62
|
+
);
|
|
63
|
+
if (memo) memo.blockSourceFiles.set(block, sf);
|
|
64
|
+
return sf;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readSourceMemoized(
|
|
68
|
+
memo: ScanMemo | undefined,
|
|
69
|
+
realPath: string,
|
|
70
|
+
): string {
|
|
71
|
+
if (memo) {
|
|
72
|
+
const cached = memo.files.get(realPath);
|
|
73
|
+
if (cached !== undefined) return cached;
|
|
74
|
+
}
|
|
75
|
+
const source = readFileSync(realPath, "utf-8");
|
|
76
|
+
if (memo) memo.files.set(realPath, source);
|
|
77
|
+
return source;
|
|
78
|
+
}
|
|
79
|
+
|
|
25
80
|
// ---------------------------------------------------------------------------
|
|
26
81
|
// AST-based include() parsing
|
|
27
82
|
// ---------------------------------------------------------------------------
|
|
@@ -42,15 +97,164 @@ function extractNamePrefixFromInclude(node: ts.CallExpression): string | null {
|
|
|
42
97
|
return null;
|
|
43
98
|
}
|
|
44
99
|
|
|
100
|
+
/**
|
|
101
|
+
* True when the thunk transforms its dynamic import via a `.then(cb)` whose
|
|
102
|
+
* callback selects a NAMED export other than `default` (e.g.
|
|
103
|
+
* `import("./x").then((m) => m.routes)`). The static resolver walks the module's
|
|
104
|
+
* `export default`, so such a selector would resolve the WRONG export — the
|
|
105
|
+
* caller must treat it as unresolvable instead of silently mis-resolving. Returns
|
|
106
|
+
* false for the supported shapes (no `.then`, `.then((m) => m)`,
|
|
107
|
+
* `.then((m) => m.default)`) and for any shape it cannot positively identify as a
|
|
108
|
+
* non-default member selection (those keep the existing resolve-via-default path).
|
|
109
|
+
*/
|
|
110
|
+
function thenSelectsNonDefaultMember(expr: ts.Expression): boolean {
|
|
111
|
+
const findThenCall = (e: ts.Expression): ts.CallExpression | null => {
|
|
112
|
+
if (
|
|
113
|
+
ts.isCallExpression(e) &&
|
|
114
|
+
ts.isPropertyAccessExpression(e.expression) &&
|
|
115
|
+
e.expression.name.text === "then"
|
|
116
|
+
) {
|
|
117
|
+
return e;
|
|
118
|
+
}
|
|
119
|
+
if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
|
|
120
|
+
return findThenCall(e.expression.expression);
|
|
121
|
+
}
|
|
122
|
+
if (ts.isPropertyAccessExpression(e)) return findThenCall(e.expression);
|
|
123
|
+
if (ts.isParenthesizedExpression(e)) return findThenCall(e.expression);
|
|
124
|
+
if (ts.isAwaitExpression(e)) return findThenCall(e.expression);
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const thenCall = findThenCall(expr);
|
|
129
|
+
if (!thenCall || thenCall.arguments.length === 0) return false;
|
|
130
|
+
const cb = thenCall.arguments[0];
|
|
131
|
+
if (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)) return false;
|
|
132
|
+
|
|
133
|
+
let ret: ts.Expression | undefined;
|
|
134
|
+
if (ts.isBlock(cb.body)) {
|
|
135
|
+
for (const stmt of cb.body.statements) {
|
|
136
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
137
|
+
ret = stmt.expression;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
ret = cb.body;
|
|
143
|
+
}
|
|
144
|
+
if (!ret) return false;
|
|
145
|
+
|
|
146
|
+
if (ts.isPropertyAccessExpression(ret)) {
|
|
147
|
+
return ret.name.text !== "default";
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract the module specifier from an async include thunk
|
|
154
|
+
* (`() => import("./mod")`). Handles arrow and function-expression thunks,
|
|
155
|
+
* concise or block bodies, and `import("./mod").then(...)` chains. Returns the
|
|
156
|
+
* specifier string, or null when the second arg is not a dynamic-import thunk
|
|
157
|
+
* (so the caller can fall through to identifier / factory-call / dynamic-expr
|
|
158
|
+
* classification).
|
|
159
|
+
*/
|
|
160
|
+
function extractDynamicImportSpecifier(node: ts.Expression): string | null {
|
|
161
|
+
let body: ts.ConciseBody | undefined;
|
|
162
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
163
|
+
body = node.body;
|
|
164
|
+
}
|
|
165
|
+
if (!body) return null;
|
|
166
|
+
|
|
167
|
+
// Concise body is the expression itself; block body: first `return <expr>`.
|
|
168
|
+
let expr: ts.Expression | undefined;
|
|
169
|
+
if (ts.isBlock(body)) {
|
|
170
|
+
for (const stmt of body.statements) {
|
|
171
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
172
|
+
expr = stmt.expression;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
expr = body;
|
|
178
|
+
}
|
|
179
|
+
if (!expr) return null;
|
|
180
|
+
|
|
181
|
+
// Descend `import("./mod").then(...)` / parenthesized / await wrappers down to
|
|
182
|
+
// the `import(...)` call itself.
|
|
183
|
+
const findImportCall = (e: ts.Expression): ts.CallExpression | null => {
|
|
184
|
+
if (
|
|
185
|
+
ts.isCallExpression(e) &&
|
|
186
|
+
e.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
187
|
+
) {
|
|
188
|
+
return e;
|
|
189
|
+
}
|
|
190
|
+
if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
|
|
191
|
+
return findImportCall(e.expression.expression);
|
|
192
|
+
}
|
|
193
|
+
if (ts.isPropertyAccessExpression(e)) return findImportCall(e.expression);
|
|
194
|
+
if (ts.isParenthesizedExpression(e)) return findImportCall(e.expression);
|
|
195
|
+
if (ts.isAwaitExpression(e)) return findImportCall(e.expression);
|
|
196
|
+
return null;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const importCall = findImportCall(expr);
|
|
200
|
+
if (!importCall || importCall.arguments.length === 0) return null;
|
|
201
|
+
// A `.then()` selecting a non-default named export can't be statically
|
|
202
|
+
// resolved to `export default`; return null so the caller emits a diagnostic
|
|
203
|
+
// rather than silently generating types for the wrong export.
|
|
204
|
+
if (thenSelectsNonDefaultMember(expr)) return null;
|
|
205
|
+
return getStringValue(importCall.arguments[0]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a module's `export default` to either a same-file `urls()` variable
|
|
210
|
+
* name (`export default shopPatterns`) or the inline call block
|
|
211
|
+
* (`export default urls(...)`). Used to walk async include modules
|
|
212
|
+
* (`() => import("./mod")`), whose convention is `export default urls(...)`.
|
|
213
|
+
*/
|
|
214
|
+
function resolveDefaultExportTarget(
|
|
215
|
+
code: string,
|
|
216
|
+
sourceFile: ts.SourceFile,
|
|
217
|
+
): { variableName?: string; inlineBlock?: string } | null {
|
|
218
|
+
let result: { variableName?: string; inlineBlock?: string } | null = null;
|
|
219
|
+
|
|
220
|
+
function visit(node: ts.Node) {
|
|
221
|
+
if (result) return;
|
|
222
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
223
|
+
const expr = node.expression;
|
|
224
|
+
if (ts.isIdentifier(expr)) {
|
|
225
|
+
result = { variableName: expr.text };
|
|
226
|
+
} else if (ts.isCallExpression(expr)) {
|
|
227
|
+
const callee = expr.expression;
|
|
228
|
+
if (ts.isIdentifier(callee) && callee.text === "urls") {
|
|
229
|
+
result = { inlineBlock: expr.getText(sourceFile) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
ts.forEachChild(node, visit);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
visit(sourceFile);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
45
241
|
/**
|
|
46
242
|
* Extract include() calls with diagnostics for unresolvable ones.
|
|
47
243
|
* Returns both resolved includes (identifier second args) and unresolvable
|
|
48
244
|
* includes (factory calls, etc.) with reasons.
|
|
49
245
|
*/
|
|
50
|
-
export function extractIncludesWithDiagnostics(
|
|
246
|
+
export function extractIncludesWithDiagnostics(
|
|
247
|
+
code: string,
|
|
248
|
+
sourceFileArg?: ts.SourceFile,
|
|
249
|
+
): {
|
|
51
250
|
resolved: Array<{
|
|
52
251
|
pathPrefix: string;
|
|
53
|
-
variableName
|
|
252
|
+
// Exactly one of variableName / moduleSpecifier is set. variableName is the
|
|
253
|
+
// classic `include("/api", apiUrls)` identifier form; moduleSpecifier is the
|
|
254
|
+
// async `include("/api", () => import("./api"))` form, carrying the imported
|
|
255
|
+
// module path so resolution can walk its `export default`.
|
|
256
|
+
variableName?: string;
|
|
257
|
+
moduleSpecifier?: string;
|
|
54
258
|
namePrefix: string | null;
|
|
55
259
|
}>;
|
|
56
260
|
unresolvable: Array<{
|
|
@@ -60,16 +264,20 @@ export function extractIncludesWithDiagnostics(code: string): {
|
|
|
60
264
|
detail: string;
|
|
61
265
|
}>;
|
|
62
266
|
} {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ts.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
267
|
+
// Reuse a caller-provided SourceFile (parsed once per scan) when given.
|
|
268
|
+
const sourceFile =
|
|
269
|
+
sourceFileArg ??
|
|
270
|
+
ts.createSourceFile(
|
|
271
|
+
"input.tsx",
|
|
272
|
+
code,
|
|
273
|
+
ts.ScriptTarget.Latest,
|
|
274
|
+
true,
|
|
275
|
+
ts.ScriptKind.TSX,
|
|
276
|
+
);
|
|
70
277
|
const resolved: Array<{
|
|
71
278
|
pathPrefix: string;
|
|
72
|
-
variableName
|
|
279
|
+
variableName?: string;
|
|
280
|
+
moduleSpecifier?: string;
|
|
73
281
|
namePrefix: string | null;
|
|
74
282
|
}> = [];
|
|
75
283
|
const unresolvable: Array<{
|
|
@@ -97,12 +305,22 @@ export function extractIncludesWithDiagnostics(code: string): {
|
|
|
97
305
|
const secondArg = node.arguments[1];
|
|
98
306
|
const namePrefix = extractNamePrefixFromInclude(node);
|
|
99
307
|
|
|
308
|
+
const dynamicImportSpec = extractDynamicImportSpecifier(secondArg);
|
|
309
|
+
|
|
100
310
|
if (ts.isIdentifier(secondArg)) {
|
|
101
311
|
resolved.push({
|
|
102
312
|
pathPrefix,
|
|
103
313
|
variableName: secondArg.text,
|
|
104
314
|
namePrefix,
|
|
105
315
|
});
|
|
316
|
+
} else if (dynamicImportSpec !== null) {
|
|
317
|
+
// Async include: `include("/api", () => import("./api"))`. Resolvable
|
|
318
|
+
// statically by walking the imported module's `export default`.
|
|
319
|
+
resolved.push({
|
|
320
|
+
pathPrefix,
|
|
321
|
+
moduleSpecifier: dynamicImportSpec,
|
|
322
|
+
namePrefix,
|
|
323
|
+
});
|
|
106
324
|
} else if (ts.isCallExpression(secondArg)) {
|
|
107
325
|
const callText = secondArg.expression.getText(sourceFile);
|
|
108
326
|
unresolvable.push({
|
|
@@ -140,7 +358,13 @@ export function resolveImportedVariable(
|
|
|
140
358
|
code: string,
|
|
141
359
|
localName: string,
|
|
142
360
|
): { specifier: string; exportedName: string } | null {
|
|
143
|
-
|
|
361
|
+
// Allow an optional leading default binding before the named-import brace so
|
|
362
|
+
// a combined `import Foo, { bar } from "..."` is matched (the named members
|
|
363
|
+
// are the only part we resolve; the default binding is skipped). Without the
|
|
364
|
+
// optional `(?:[\w$]+\s*,\s*)?` segment, the `Foo, ` prefix breaks the match
|
|
365
|
+
// and a legitimate static named import surfaces as `unresolvable-import`.
|
|
366
|
+
const importRegex =
|
|
367
|
+
/import\s*(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
144
368
|
let match;
|
|
145
369
|
|
|
146
370
|
while ((match = importRegex.exec(code)) !== null) {
|
|
@@ -206,14 +430,18 @@ export function resolveImportPath(
|
|
|
206
430
|
function extractUrlsBlockForVariable(
|
|
207
431
|
code: string,
|
|
208
432
|
varName: string,
|
|
433
|
+
sourceFileArg?: ts.SourceFile,
|
|
209
434
|
): string | null {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
ts.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
435
|
+
// Reuse a caller-provided full-source SourceFile (parsed once per scan).
|
|
436
|
+
const sourceFile =
|
|
437
|
+
sourceFileArg ??
|
|
438
|
+
ts.createSourceFile(
|
|
439
|
+
"input.tsx",
|
|
440
|
+
code,
|
|
441
|
+
ts.ScriptTarget.Latest,
|
|
442
|
+
true,
|
|
443
|
+
ts.ScriptKind.TSX,
|
|
444
|
+
);
|
|
217
445
|
let result: string | null = null;
|
|
218
446
|
|
|
219
447
|
function visit(node: ts.Node) {
|
|
@@ -249,11 +477,15 @@ function buildRouteMapFromBlock(
|
|
|
249
477
|
visited: Set<string>,
|
|
250
478
|
searchSchemasOut?: Record<string, Record<string, string>>,
|
|
251
479
|
diagnosticsOut?: UnresolvableInclude[],
|
|
480
|
+
memo?: ScanMemo,
|
|
252
481
|
): Record<string, string> {
|
|
253
482
|
const routeMap: Record<string, string> = {};
|
|
254
483
|
|
|
484
|
+
// Parse the block once and share the SourceFile between both extractors.
|
|
485
|
+
const blockSourceFile = parseBlock(memo, block);
|
|
486
|
+
|
|
255
487
|
// Extract local path() routes
|
|
256
|
-
const localRoutes = extractRoutesFromSource(block);
|
|
488
|
+
const localRoutes = extractRoutesFromSource(block, blockSourceFile);
|
|
257
489
|
for (const { name, pattern, search } of localRoutes) {
|
|
258
490
|
routeMap[name] = pattern;
|
|
259
491
|
if (search && searchSchemasOut) {
|
|
@@ -262,8 +494,10 @@ function buildRouteMapFromBlock(
|
|
|
262
494
|
}
|
|
263
495
|
|
|
264
496
|
// Extract include() calls with diagnostics for unresolvable ones
|
|
265
|
-
const { resolved: includes, unresolvable } =
|
|
266
|
-
|
|
497
|
+
const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(
|
|
498
|
+
block,
|
|
499
|
+
blockSourceFile,
|
|
500
|
+
);
|
|
267
501
|
|
|
268
502
|
if (diagnosticsOut) {
|
|
269
503
|
for (const entry of unresolvable) {
|
|
@@ -271,58 +505,120 @@ function buildRouteMapFromBlock(
|
|
|
271
505
|
}
|
|
272
506
|
}
|
|
273
507
|
|
|
274
|
-
for (const
|
|
508
|
+
for (const inc of includes) {
|
|
509
|
+
const { pathPrefix, namePrefix } = inc;
|
|
275
510
|
let childResult: {
|
|
276
511
|
routes: Record<string, string>;
|
|
277
512
|
searchSchemas: Record<string, Record<string, string>>;
|
|
278
513
|
};
|
|
279
514
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
515
|
+
if (inc.moduleSpecifier) {
|
|
516
|
+
// Async include `() => import("./mod")`: resolve the imported module's
|
|
517
|
+
// `export default` (convention: `export default urls(...)`), then recurse
|
|
518
|
+
// into it exactly like an identifier include. Nested include()s inside the
|
|
519
|
+
// async module — eager or async — resolve through the same recursion.
|
|
520
|
+
const targetFile = resolveImportPath(inc.moduleSpecifier, filePath);
|
|
284
521
|
if (!targetFile) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
293
|
-
}
|
|
522
|
+
diagnosticsOut?.push({
|
|
523
|
+
pathPrefix,
|
|
524
|
+
namePrefix,
|
|
525
|
+
reason: "file-not-found",
|
|
526
|
+
sourceFile: filePath,
|
|
527
|
+
detail: `import("${inc.moduleSpecifier}") resolved to no file`,
|
|
528
|
+
});
|
|
294
529
|
continue;
|
|
295
530
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
531
|
+
let targetSource: string;
|
|
532
|
+
try {
|
|
533
|
+
targetSource = readSourceMemoized(memo, resolve(targetFile));
|
|
534
|
+
} catch (err) {
|
|
535
|
+
// Every other failure path in this function emits a diagnostic; a bare
|
|
536
|
+
// `continue` here would drop the group from generated types with no
|
|
537
|
+
// signal at all. Emit one like the siblings do.
|
|
538
|
+
diagnosticsOut?.push({
|
|
539
|
+
pathPrefix,
|
|
540
|
+
namePrefix,
|
|
541
|
+
reason: "file-not-found",
|
|
542
|
+
sourceFile: filePath,
|
|
543
|
+
detail: `import("${inc.moduleSpecifier}") resolved to "${targetFile}" but reading it failed: ${(err as Error)?.message ?? String(err)}`,
|
|
544
|
+
});
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const def = resolveDefaultExportTarget(
|
|
548
|
+
targetSource,
|
|
549
|
+
parseBlock(memo, targetSource),
|
|
307
550
|
);
|
|
308
|
-
if (!
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
317
|
-
}
|
|
551
|
+
if (!def) {
|
|
552
|
+
diagnosticsOut?.push({
|
|
553
|
+
pathPrefix,
|
|
554
|
+
namePrefix,
|
|
555
|
+
reason: "unresolvable-import",
|
|
556
|
+
sourceFile: filePath,
|
|
557
|
+
detail: `import("${inc.moduleSpecifier}") has no resolvable \`export default urls(...)\``,
|
|
558
|
+
});
|
|
318
559
|
continue;
|
|
319
560
|
}
|
|
320
|
-
|
|
321
|
-
|
|
561
|
+
if (def.inlineBlock) {
|
|
562
|
+
// `export default urls(...)` — recurse into the inline call block.
|
|
563
|
+
childResult = buildCombinedRouteMapWithSearch(
|
|
564
|
+
targetFile,
|
|
565
|
+
undefined,
|
|
566
|
+
visited,
|
|
567
|
+
diagnosticsOut,
|
|
568
|
+
def.inlineBlock,
|
|
569
|
+
memo,
|
|
570
|
+
);
|
|
571
|
+
} else {
|
|
572
|
+
// `export default <name>`. That name may be a same-file
|
|
573
|
+
// `const name = urls(...)` OR itself re-exported from an import
|
|
574
|
+
// (`import { name } from "./x"; export default name`). The shared helper
|
|
575
|
+
// resolves the import chain first, then falls back to same-file
|
|
576
|
+
// extraction — without this a re-exported default silently yields empty
|
|
577
|
+
// routes. Resolution scope is the imported module (targetFile); the
|
|
578
|
+
// diagnostic is still attributed to the file with the async include.
|
|
579
|
+
const variableName = def.variableName!;
|
|
580
|
+
const resolved = resolveIncludedVariable({
|
|
581
|
+
variableName,
|
|
582
|
+
resolutionFile: targetFile,
|
|
583
|
+
resolutionSource: targetSource,
|
|
584
|
+
reportFile: filePath,
|
|
585
|
+
memo,
|
|
586
|
+
visited,
|
|
587
|
+
diagnosticsOut,
|
|
588
|
+
pathPrefix,
|
|
589
|
+
namePrefix,
|
|
590
|
+
fileNotFoundDetail: (specifier) =>
|
|
591
|
+
`import("${inc.moduleSpecifier}") default re-exports "${variableName}" from "${specifier}", which resolved to no file`,
|
|
592
|
+
notFoundDetail: () =>
|
|
593
|
+
`import("${inc.moduleSpecifier}") default "${variableName}" not found in its imports or same-file scope`,
|
|
594
|
+
});
|
|
595
|
+
if (!resolved) continue;
|
|
596
|
+
childResult = resolved;
|
|
597
|
+
}
|
|
598
|
+
} else if (inc.variableName) {
|
|
599
|
+
// Identifier include `include(prefix, patterns)`: resolve `patterns`
|
|
600
|
+
// (imported or same-file) through the same shared helper as the async
|
|
601
|
+
// `export default <name>` form above.
|
|
602
|
+
const variableName = inc.variableName;
|
|
603
|
+
const resolved = resolveIncludedVariable({
|
|
322
604
|
variableName,
|
|
605
|
+
resolutionFile: filePath,
|
|
606
|
+
resolutionSource: fullSource,
|
|
607
|
+
reportFile: filePath,
|
|
608
|
+
memo,
|
|
323
609
|
visited,
|
|
324
610
|
diagnosticsOut,
|
|
325
|
-
|
|
611
|
+
pathPrefix,
|
|
612
|
+
namePrefix,
|
|
613
|
+
fileNotFoundDetail: (specifier) =>
|
|
614
|
+
`import "${specifier}" resolved to no file`,
|
|
615
|
+
notFoundDetail: () =>
|
|
616
|
+
`variable "${variableName}" not found in imports or same-file scope`,
|
|
617
|
+
});
|
|
618
|
+
if (!resolved) continue;
|
|
619
|
+
childResult = resolved;
|
|
620
|
+
} else {
|
|
621
|
+
continue;
|
|
326
622
|
}
|
|
327
623
|
|
|
328
624
|
// Includes without a name keep their child names private to the mounted
|
|
@@ -354,6 +650,94 @@ function buildRouteMapFromBlock(
|
|
|
354
650
|
return routeMap;
|
|
355
651
|
}
|
|
356
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Resolve an include's target VARIABLE to its child route map. The variable may
|
|
655
|
+
* be imported (`import { name } from "./x"`) or defined in the same file
|
|
656
|
+
* (`const name = urls(...)`). Shared by the identifier include branch and the
|
|
657
|
+
* async-include `export default <name>` branch so a future import-resolution fix
|
|
658
|
+
* cannot silently miss one form. Returns null (with a diagnostic pushed) when
|
|
659
|
+
* the variable is unresolvable — the caller should skip the include.
|
|
660
|
+
*
|
|
661
|
+
* `resolutionFile`/`resolutionSource` is the module whose import + same-file
|
|
662
|
+
* scope is searched; `reportFile` is the file the diagnostic is attributed to
|
|
663
|
+
* (the one containing the include() call), which differs for async includes.
|
|
664
|
+
*/
|
|
665
|
+
function resolveIncludedVariable(opts: {
|
|
666
|
+
variableName: string;
|
|
667
|
+
resolutionFile: string;
|
|
668
|
+
resolutionSource: string;
|
|
669
|
+
reportFile: string;
|
|
670
|
+
memo: ScanMemo | undefined;
|
|
671
|
+
visited: Set<string> | undefined;
|
|
672
|
+
diagnosticsOut: UnresolvableInclude[] | undefined;
|
|
673
|
+
pathPrefix: string;
|
|
674
|
+
namePrefix: string | null;
|
|
675
|
+
fileNotFoundDetail: (specifier: string) => string;
|
|
676
|
+
notFoundDetail: () => string;
|
|
677
|
+
}): {
|
|
678
|
+
routes: Record<string, string>;
|
|
679
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
680
|
+
} | null {
|
|
681
|
+
const {
|
|
682
|
+
variableName,
|
|
683
|
+
resolutionFile,
|
|
684
|
+
resolutionSource,
|
|
685
|
+
reportFile,
|
|
686
|
+
memo,
|
|
687
|
+
visited,
|
|
688
|
+
diagnosticsOut,
|
|
689
|
+
pathPrefix,
|
|
690
|
+
namePrefix,
|
|
691
|
+
} = opts;
|
|
692
|
+
const imported = resolveImportedVariable(resolutionSource, variableName);
|
|
693
|
+
if (imported) {
|
|
694
|
+
const targetFile = resolveImportPath(imported.specifier, resolutionFile);
|
|
695
|
+
if (!targetFile) {
|
|
696
|
+
diagnosticsOut?.push({
|
|
697
|
+
pathPrefix,
|
|
698
|
+
namePrefix,
|
|
699
|
+
reason: "file-not-found",
|
|
700
|
+
sourceFile: reportFile,
|
|
701
|
+
detail: opts.fileNotFoundDetail(imported.specifier),
|
|
702
|
+
});
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
return buildCombinedRouteMapWithSearch(
|
|
706
|
+
targetFile,
|
|
707
|
+
imported.exportedName,
|
|
708
|
+
visited,
|
|
709
|
+
diagnosticsOut,
|
|
710
|
+
undefined,
|
|
711
|
+
memo,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
// Not imported: it must be a same-file `const name = urls(...)`. Confirm it
|
|
715
|
+
// exists so an undefined name emits a diagnostic instead of empty routes.
|
|
716
|
+
const sameFileBlock = extractUrlsBlockForVariable(
|
|
717
|
+
resolutionSource,
|
|
718
|
+
variableName,
|
|
719
|
+
parseBlock(memo, resolutionSource),
|
|
720
|
+
);
|
|
721
|
+
if (!sameFileBlock) {
|
|
722
|
+
diagnosticsOut?.push({
|
|
723
|
+
pathPrefix,
|
|
724
|
+
namePrefix,
|
|
725
|
+
reason: "unresolvable-import",
|
|
726
|
+
sourceFile: reportFile,
|
|
727
|
+
detail: opts.notFoundDetail(),
|
|
728
|
+
});
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
return buildCombinedRouteMapWithSearch(
|
|
732
|
+
resolutionFile,
|
|
733
|
+
variableName,
|
|
734
|
+
visited,
|
|
735
|
+
diagnosticsOut,
|
|
736
|
+
undefined,
|
|
737
|
+
memo,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
357
741
|
/**
|
|
358
742
|
* Build route map and search schemas together.
|
|
359
743
|
* Internal helper used by the include resolution path.
|
|
@@ -361,6 +745,9 @@ function buildRouteMapFromBlock(
|
|
|
361
745
|
* @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
|
|
362
746
|
* builder function). When provided, variableName is ignored and the block
|
|
363
747
|
* is parsed directly for path()/include() calls.
|
|
748
|
+
* @param memo - Per-scan readFileSync/parse memo. A fresh one is created at the
|
|
749
|
+
* top-level entry and threaded through the recursion so each file is read and
|
|
750
|
+
* parsed once per scan. Behavior-preserving (same input -> same output).
|
|
364
751
|
*/
|
|
365
752
|
export function buildCombinedRouteMapWithSearch(
|
|
366
753
|
filePath: string,
|
|
@@ -368,11 +755,13 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
368
755
|
visited?: Set<string>,
|
|
369
756
|
diagnosticsOut?: UnresolvableInclude[],
|
|
370
757
|
inlineBlock?: string,
|
|
758
|
+
memo?: ScanMemo,
|
|
371
759
|
): {
|
|
372
760
|
routes: Record<string, string>;
|
|
373
761
|
searchSchemas: Record<string, Record<string, string>>;
|
|
374
762
|
} {
|
|
375
763
|
visited = visited ?? new Set();
|
|
764
|
+
memo = memo ?? createScanMemo();
|
|
376
765
|
const realPath = resolve(filePath);
|
|
377
766
|
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
378
767
|
if (visited.has(key)) {
|
|
@@ -383,7 +772,7 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
383
772
|
|
|
384
773
|
let source: string;
|
|
385
774
|
try {
|
|
386
|
-
source =
|
|
775
|
+
source = readSourceMemoized(memo, realPath);
|
|
387
776
|
} catch {
|
|
388
777
|
return { routes: {}, searchSchemas: {} };
|
|
389
778
|
}
|
|
@@ -392,7 +781,11 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
392
781
|
if (inlineBlock) {
|
|
393
782
|
block = inlineBlock;
|
|
394
783
|
} else if (variableName) {
|
|
395
|
-
const extracted = extractUrlsBlockForVariable(
|
|
784
|
+
const extracted = extractUrlsBlockForVariable(
|
|
785
|
+
source,
|
|
786
|
+
variableName,
|
|
787
|
+
parseBlock(memo, source),
|
|
788
|
+
);
|
|
396
789
|
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
397
790
|
block = extracted;
|
|
398
791
|
} else {
|
|
@@ -407,6 +800,7 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
407
800
|
visited,
|
|
408
801
|
searchSchemas,
|
|
409
802
|
diagnosticsOut,
|
|
803
|
+
memo,
|
|
410
804
|
);
|
|
411
805
|
|
|
412
806
|
// Remove from visited so sibling branches can include the same variable
|
|
@@ -37,12 +37,15 @@ export function formatRouteEntry(
|
|
|
37
37
|
): string {
|
|
38
38
|
const hasSearch = search && Object.keys(search).length > 0;
|
|
39
39
|
|
|
40
|
+
// JSON.stringify the pattern and search values so backslashes and quotes in a
|
|
41
|
+
// route pattern (e.g. a custom regex constraint) survive interpolation into
|
|
42
|
+
// both the type-level string and the runtime NamedRoutes value.
|
|
40
43
|
if (!hasSearch) {
|
|
41
|
-
return ` ${key}:
|
|
44
|
+
return ` ${key}: ${JSON.stringify(pattern)},`;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const searchBody = Object.entries(search!)
|
|
45
|
-
.map(([k, v]) => `${k}:
|
|
48
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
46
49
|
.join(", ");
|
|
47
|
-
return ` ${key}: { path:
|
|
50
|
+
return ` ${key}: { path: ${JSON.stringify(pattern)}, search: { ${searchBody} } },`;
|
|
48
51
|
}
|