@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
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/README.md +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import ts from "typescript";
|
|
16
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
17
17
|
import type { ScanFilter } from "./scan-filter.js";
|
|
18
|
+
import { firstCodeMatchIndex } from "./source-scan.js";
|
|
18
19
|
import {
|
|
19
20
|
resolveImportedVariable,
|
|
20
21
|
resolveImportPath,
|
|
@@ -38,6 +39,8 @@ function countPublicRouteEntries(source: string): number {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
42
|
+
// Global variant for the code-region scan (firstCodeMatchIndex sets lastIndex).
|
|
43
|
+
const ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
|
|
41
44
|
|
|
42
45
|
function isRoutableSourceFile(name: string): boolean {
|
|
43
46
|
return (
|
|
@@ -45,7 +48,9 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
45
48
|
name.endsWith(".tsx") ||
|
|
46
49
|
name.endsWith(".js") ||
|
|
47
50
|
name.endsWith(".jsx")) &&
|
|
48
|
-
!name.includes(".gen.")
|
|
51
|
+
!name.includes(".gen.") &&
|
|
52
|
+
!name.includes(".test.") &&
|
|
53
|
+
!name.includes(".spec.")
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -59,7 +64,7 @@ function findRouterFilesRecursive(
|
|
|
59
64
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
60
65
|
} catch (err) {
|
|
61
66
|
console.warn(
|
|
62
|
-
`[
|
|
67
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
63
68
|
);
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
@@ -70,7 +75,15 @@ function findRouterFilesRecursive(
|
|
|
70
75
|
for (const entry of entries) {
|
|
71
76
|
const fullPath = join(dir, entry.name);
|
|
72
77
|
if (entry.isDirectory()) {
|
|
73
|
-
if (
|
|
78
|
+
if (
|
|
79
|
+
entry.name === "node_modules" ||
|
|
80
|
+
entry.name === "dist" ||
|
|
81
|
+
entry.name === "coverage" ||
|
|
82
|
+
entry.name === "__tests__" ||
|
|
83
|
+
entry.name === "__mocks__" ||
|
|
84
|
+
entry.name.startsWith(".")
|
|
85
|
+
)
|
|
86
|
+
continue;
|
|
74
87
|
childDirs.push(fullPath);
|
|
75
88
|
continue;
|
|
76
89
|
}
|
|
@@ -80,7 +93,17 @@ function findRouterFilesRecursive(
|
|
|
80
93
|
|
|
81
94
|
try {
|
|
82
95
|
const source = readFileSync(fullPath, "utf-8");
|
|
83
|
-
|
|
96
|
+
// Fast path: most files contain no `createRouter(` at all, so the cheap
|
|
97
|
+
// raw regex short-circuits before the code-region scan. Only a file that
|
|
98
|
+
// mentions the token (real call OR a comment/string mention) is rescanned
|
|
99
|
+
// over code regions — allocation-free, never building a stripped copy —
|
|
100
|
+
// so a mention inside a comment or string is not mistaken for a real
|
|
101
|
+
// router file (which previously triggered a spurious "Multiple routers
|
|
102
|
+
// found" error).
|
|
103
|
+
if (
|
|
104
|
+
ROUTER_CALL_PATTERN.test(source) &&
|
|
105
|
+
firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
|
|
106
|
+
) {
|
|
84
107
|
routerFilesInDir.push(fullPath);
|
|
85
108
|
}
|
|
86
109
|
} catch {
|
|
@@ -132,7 +155,7 @@ export function findNestedRouterConflict(
|
|
|
132
155
|
|
|
133
156
|
export function formatNestedRouterConflictError(
|
|
134
157
|
conflict: { ancestor: string; nested: string },
|
|
135
|
-
prefix = "[
|
|
158
|
+
prefix = "[rango]",
|
|
136
159
|
): string {
|
|
137
160
|
return (
|
|
138
161
|
`${prefix} Nested router roots are not supported.\n` +
|
|
@@ -147,13 +170,26 @@ export function formatNestedRouterConflictError(
|
|
|
147
170
|
// ---------------------------------------------------------------------------
|
|
148
171
|
|
|
149
172
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
173
|
+
* Result of extracting URL patterns from a router file.
|
|
174
|
+
* - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
|
|
175
|
+
* - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
|
|
176
|
+
*/
|
|
177
|
+
export type UrlsExtractionResult =
|
|
178
|
+
| { kind: "variable"; name: string }
|
|
179
|
+
| { kind: "inline"; block: string };
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract the url patterns from a router file using AST.
|
|
183
|
+
* Detects four patterns:
|
|
152
184
|
* 1. createRouter(...).routes(variableName)
|
|
153
185
|
* 2. createRouter({ urls: variableName, ... })
|
|
154
|
-
*
|
|
186
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
187
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
188
|
+
* Returns either a variable name or an inline code block.
|
|
155
189
|
*/
|
|
156
|
-
export function
|
|
190
|
+
export function extractUrlsFromRouter(
|
|
191
|
+
code: string,
|
|
192
|
+
): UrlsExtractionResult | null {
|
|
157
193
|
const sourceFile = ts.createSourceFile(
|
|
158
194
|
"router.tsx",
|
|
159
195
|
code,
|
|
@@ -161,7 +197,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
161
197
|
true,
|
|
162
198
|
ts.ScriptKind.TSX,
|
|
163
199
|
);
|
|
164
|
-
let result:
|
|
200
|
+
let result: UrlsExtractionResult | null = null;
|
|
165
201
|
|
|
166
202
|
function isCreateRouterCall(node: ts.Node): boolean {
|
|
167
203
|
if (!ts.isCallExpression(node)) return false;
|
|
@@ -169,44 +205,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
169
205
|
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
170
206
|
}
|
|
171
207
|
|
|
208
|
+
/** Check if a node is an arrow/function expression (inline builder). */
|
|
209
|
+
function isInlineBuilder(node: ts.Node): boolean {
|
|
210
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Check if a .routes() call chains from createRouter(). */
|
|
214
|
+
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
215
|
+
if (
|
|
216
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
217
|
+
node.expression.name.text !== "routes"
|
|
218
|
+
)
|
|
219
|
+
return false;
|
|
220
|
+
let inner: ts.Expression = node.expression.expression;
|
|
221
|
+
while (
|
|
222
|
+
ts.isCallExpression(inner) &&
|
|
223
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
224
|
+
) {
|
|
225
|
+
inner = inner.expression.expression;
|
|
226
|
+
}
|
|
227
|
+
return isCreateRouterCall(inner);
|
|
228
|
+
}
|
|
229
|
+
|
|
172
230
|
function visit(node: ts.Node) {
|
|
173
231
|
if (result) return;
|
|
174
232
|
|
|
175
|
-
// Pattern 1: createRouter(...).routes(variableName)
|
|
176
|
-
// The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
|
|
233
|
+
// Pattern 1 & 3: createRouter(...).routes(variableName | builder)
|
|
177
234
|
if (
|
|
178
235
|
ts.isCallExpression(node) &&
|
|
179
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
180
|
-
node.expression.name.text === "routes" &&
|
|
181
236
|
node.arguments.length >= 1 &&
|
|
182
|
-
|
|
237
|
+
isRoutesOnCreateRouter(node)
|
|
183
238
|
) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
ts.isPropertyAccessExpression(inner.expression)
|
|
190
|
-
) {
|
|
191
|
-
inner = inner.expression.expression;
|
|
192
|
-
}
|
|
193
|
-
if (isCreateRouterCall(inner)) {
|
|
194
|
-
result = (node.arguments[0] as ts.Identifier).text;
|
|
195
|
-
return;
|
|
239
|
+
const arg = node.arguments[0];
|
|
240
|
+
if (ts.isIdentifier(arg)) {
|
|
241
|
+
result = { kind: "variable", name: arg.text };
|
|
242
|
+
} else if (isInlineBuilder(arg)) {
|
|
243
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
196
244
|
}
|
|
245
|
+
return;
|
|
197
246
|
}
|
|
198
247
|
|
|
199
|
-
// Pattern 2: createRouter({ urls: variableName, ... })
|
|
248
|
+
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
200
249
|
if (isCreateRouterCall(node)) {
|
|
201
250
|
const callExpr = node as ts.CallExpression;
|
|
202
|
-
for (const
|
|
251
|
+
for (const callArg of callExpr.arguments) {
|
|
252
|
+
if (ts.isObjectLiteralExpression(callArg)) {
|
|
253
|
+
for (const prop of callArg.properties) {
|
|
254
|
+
if (
|
|
255
|
+
ts.isPropertyAssignment(prop) &&
|
|
256
|
+
ts.isIdentifier(prop.name) &&
|
|
257
|
+
prop.name.text === "urls"
|
|
258
|
+
) {
|
|
259
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
260
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
261
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
262
|
+
result = {
|
|
263
|
+
kind: "inline",
|
|
264
|
+
block: prop.initializer.getText(sourceFile),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
ts.forEachChild(node, visit);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
visit(sourceFile);
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
283
|
+
* Returns the basename value or undefined if not present.
|
|
284
|
+
*/
|
|
285
|
+
export function extractBasenameFromRouter(code: string): string | undefined {
|
|
286
|
+
const sourceFile = ts.createSourceFile(
|
|
287
|
+
"router.tsx",
|
|
288
|
+
code,
|
|
289
|
+
ts.ScriptTarget.Latest,
|
|
290
|
+
true,
|
|
291
|
+
ts.ScriptKind.TSX,
|
|
292
|
+
);
|
|
293
|
+
let result: string | undefined;
|
|
294
|
+
|
|
295
|
+
function visit(node: ts.Node) {
|
|
296
|
+
if (result !== undefined) return;
|
|
297
|
+
if (
|
|
298
|
+
ts.isCallExpression(node) &&
|
|
299
|
+
ts.isIdentifier(node.expression) &&
|
|
300
|
+
node.expression.text === "createRouter"
|
|
301
|
+
) {
|
|
302
|
+
for (const arg of node.arguments) {
|
|
203
303
|
if (ts.isObjectLiteralExpression(arg)) {
|
|
204
304
|
for (const prop of arg.properties) {
|
|
205
305
|
if (
|
|
206
306
|
ts.isPropertyAssignment(prop) &&
|
|
207
307
|
ts.isIdentifier(prop.name) &&
|
|
208
|
-
prop.name.text === "
|
|
209
|
-
ts.
|
|
308
|
+
prop.name.text === "basename" &&
|
|
309
|
+
ts.isStringLiteral(prop.initializer)
|
|
210
310
|
) {
|
|
211
311
|
result = prop.initializer.text;
|
|
212
312
|
return;
|
|
@@ -215,7 +315,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
215
315
|
}
|
|
216
316
|
}
|
|
217
317
|
}
|
|
218
|
-
|
|
219
318
|
ts.forEachChild(node, visit);
|
|
220
319
|
}
|
|
221
320
|
|
|
@@ -223,9 +322,70 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
223
322
|
return result;
|
|
224
323
|
}
|
|
225
324
|
|
|
325
|
+
/** @deprecated Use extractUrlsFromRouter instead */
|
|
326
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
327
|
+
const result = extractUrlsFromRouter(code);
|
|
328
|
+
return result?.kind === "variable" ? result.name : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Apply a basename prefix to all route patterns in a result set. */
|
|
332
|
+
function applyBasenameToRoutes(
|
|
333
|
+
result: {
|
|
334
|
+
routes: Record<string, string>;
|
|
335
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
336
|
+
},
|
|
337
|
+
basename: string,
|
|
338
|
+
): {
|
|
339
|
+
routes: Record<string, string>;
|
|
340
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
341
|
+
} {
|
|
342
|
+
const prefixed: Record<string, string> = {};
|
|
343
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
344
|
+
if (pattern === "/") {
|
|
345
|
+
prefixed[name] = basename;
|
|
346
|
+
} else if (basename.endsWith("/") && pattern.startsWith("/")) {
|
|
347
|
+
prefixed[name] = basename + pattern.slice(1);
|
|
348
|
+
} else {
|
|
349
|
+
prefixed[name] = basename + pattern;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Filesystem path of the generated route-types file for a router source file.
|
|
356
|
+
// Native separators — matches the self-gen-tracking Map key the watcher compares.
|
|
357
|
+
export function genFileTsPath(sourceFile: string): string {
|
|
358
|
+
const base = pathBasename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
|
|
359
|
+
return join(dirname(sourceFile), `${base}.named-routes.gen.ts`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Search schemas for the gen file: prefer the runtime manifest's; when it omits
|
|
363
|
+
// them (some module-runner flows) fall back to static parsing filtered to the
|
|
364
|
+
// public route-name set. Returns the runtime value unchanged otherwise.
|
|
365
|
+
export function resolveSearchSchemas(
|
|
366
|
+
publicRouteNames: string[],
|
|
367
|
+
runtimeSchemas: Record<string, Record<string, string>> | undefined,
|
|
368
|
+
sourceFile: string,
|
|
369
|
+
): Record<string, Record<string, string>> | undefined {
|
|
370
|
+
if (runtimeSchemas && Object.keys(runtimeSchemas).length > 0) {
|
|
371
|
+
return runtimeSchemas;
|
|
372
|
+
}
|
|
373
|
+
const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
|
|
374
|
+
if (Object.keys(staticParsed.searchSchemas).length === 0) {
|
|
375
|
+
return runtimeSchemas;
|
|
376
|
+
}
|
|
377
|
+
const filtered: Record<string, Record<string, string>> = {};
|
|
378
|
+
for (const name of publicRouteNames) {
|
|
379
|
+
const schema = staticParsed.searchSchemas[name];
|
|
380
|
+
if (schema) filtered[name] = schema;
|
|
381
|
+
}
|
|
382
|
+
return Object.keys(filtered).length > 0 ? filtered : runtimeSchemas;
|
|
383
|
+
}
|
|
384
|
+
|
|
226
385
|
/**
|
|
227
386
|
* Resolve routes and search schemas from a router source file by following the
|
|
228
|
-
* variable passed to `.routes(...)` or `urls: ...` in createRouter options
|
|
387
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
388
|
+
* or by parsing an inline builder function directly.
|
|
229
389
|
*/
|
|
230
390
|
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
231
391
|
routes: Record<string, string>;
|
|
@@ -238,21 +398,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
238
398
|
return { routes: {}, searchSchemas: {} };
|
|
239
399
|
}
|
|
240
400
|
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
401
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
402
|
+
if (!extraction) {
|
|
243
403
|
return { routes: {}, searchSchemas: {} };
|
|
244
404
|
}
|
|
245
405
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
406
|
+
// Detect basename from createRouter({ basename: "..." })
|
|
407
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
408
|
+
const basename = rawBasename
|
|
409
|
+
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
410
|
+
: undefined;
|
|
411
|
+
|
|
412
|
+
let result: {
|
|
413
|
+
routes: Record<string, string>;
|
|
414
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Inline builder: extract routes directly from the function body
|
|
418
|
+
if (extraction.kind === "inline") {
|
|
419
|
+
result = buildCombinedRouteMapWithSearch(
|
|
420
|
+
routerFilePath,
|
|
421
|
+
undefined,
|
|
422
|
+
undefined,
|
|
423
|
+
undefined,
|
|
424
|
+
extraction.block,
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
// Variable reference: follow imports or same-file declaration
|
|
428
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
429
|
+
if (imported) {
|
|
430
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
431
|
+
if (!targetFile) {
|
|
432
|
+
return { routes: {}, searchSchemas: {} };
|
|
433
|
+
}
|
|
434
|
+
result = buildCombinedRouteMapWithSearch(
|
|
435
|
+
targetFile,
|
|
436
|
+
imported.exportedName,
|
|
437
|
+
);
|
|
438
|
+
} else {
|
|
439
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
251
440
|
}
|
|
252
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
253
441
|
}
|
|
254
442
|
|
|
255
|
-
|
|
443
|
+
// Apply basename prefix to all extracted route patterns
|
|
444
|
+
if (basename) {
|
|
445
|
+
result = applyBasenameToRoutes(result, basename);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return result;
|
|
256
449
|
}
|
|
257
450
|
|
|
258
451
|
// ---------------------------------------------------------------------------
|
|
@@ -275,12 +468,26 @@ export function detectUnresolvableIncludes(
|
|
|
275
468
|
return [];
|
|
276
469
|
}
|
|
277
470
|
|
|
278
|
-
// Extract the urls
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
471
|
+
// Extract the urls source from the router file
|
|
472
|
+
const extraction = extractUrlsFromRouter(source);
|
|
473
|
+
if (!extraction) return [];
|
|
474
|
+
|
|
475
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
281
476
|
|
|
282
|
-
|
|
283
|
-
|
|
477
|
+
if (extraction.kind === "inline") {
|
|
478
|
+
// Inline builder: parse directly
|
|
479
|
+
buildCombinedRouteMapWithSearch(
|
|
480
|
+
realPath,
|
|
481
|
+
undefined,
|
|
482
|
+
new Set(),
|
|
483
|
+
diagnostics,
|
|
484
|
+
extraction.block,
|
|
485
|
+
);
|
|
486
|
+
return diagnostics;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Variable reference: resolve where it comes from
|
|
490
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
284
491
|
let targetFile: string;
|
|
285
492
|
let exportedName: string | undefined;
|
|
286
493
|
|
|
@@ -302,10 +509,9 @@ export function detectUnresolvableIncludes(
|
|
|
302
509
|
} else {
|
|
303
510
|
// Same-file urls() definition
|
|
304
511
|
targetFile = realPath;
|
|
305
|
-
exportedName =
|
|
512
|
+
exportedName = extraction.name;
|
|
306
513
|
}
|
|
307
514
|
|
|
308
|
-
const diagnostics: UnresolvableInclude[] = [];
|
|
309
515
|
buildCombinedRouteMapWithSearch(
|
|
310
516
|
targetFile,
|
|
311
517
|
exportedName,
|
|
@@ -365,7 +571,10 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
|
365
571
|
export function writeCombinedRouteTypes(
|
|
366
572
|
root: string,
|
|
367
573
|
knownRouterFiles?: string[],
|
|
368
|
-
opts?: {
|
|
574
|
+
opts?: {
|
|
575
|
+
preserveIfLarger?: boolean;
|
|
576
|
+
onWrite?: (outPath: string, content: string) => void;
|
|
577
|
+
},
|
|
369
578
|
): void {
|
|
370
579
|
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
371
580
|
try {
|
|
@@ -373,7 +582,7 @@ export function writeCombinedRouteTypes(
|
|
|
373
582
|
if (existsSync(oldCombinedPath)) {
|
|
374
583
|
unlinkSync(oldCombinedPath);
|
|
375
584
|
console.log(
|
|
376
|
-
`[
|
|
585
|
+
`[rango] Removed stale combined route types: ${oldCombinedPath}`,
|
|
377
586
|
);
|
|
378
587
|
}
|
|
379
588
|
} catch {}
|
|
@@ -387,44 +596,23 @@ export function writeCombinedRouteTypes(
|
|
|
387
596
|
}
|
|
388
597
|
|
|
389
598
|
for (const routerFilePath of routerFilePaths) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
407
|
-
if (imported) {
|
|
408
|
-
// Variable is imported from another module
|
|
409
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
410
|
-
if (!targetFile) continue;
|
|
411
|
-
result = buildCombinedRouteMapWithSearch(
|
|
412
|
-
targetFile,
|
|
413
|
-
imported.exportedName,
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
// Variable is defined in the same file
|
|
417
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
599
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
600
|
+
if (
|
|
601
|
+
Object.keys(result.routes).length === 0 &&
|
|
602
|
+
Object.keys(result.searchSchemas).length === 0
|
|
603
|
+
) {
|
|
604
|
+
// Check if the file even has a createRouter call — if not, skip entirely.
|
|
605
|
+
// If it does, fall through to write an empty placeholder below.
|
|
606
|
+
let routerSource: string;
|
|
607
|
+
try {
|
|
608
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
609
|
+
} catch {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
418
613
|
}
|
|
419
614
|
|
|
420
|
-
const
|
|
421
|
-
/\.(tsx?|jsx?)$/,
|
|
422
|
-
"",
|
|
423
|
-
);
|
|
424
|
-
const outPath = join(
|
|
425
|
-
dirname(routerFilePath),
|
|
426
|
-
`${routerBasename}.named-routes.gen.ts`,
|
|
427
|
-
);
|
|
615
|
+
const outPath = genFileTsPath(routerFilePath);
|
|
428
616
|
const existing = existsSync(outPath)
|
|
429
617
|
? readFileSync(outPath, "utf-8")
|
|
430
618
|
: null;
|
|
@@ -435,6 +623,7 @@ export function writeCombinedRouteTypes(
|
|
|
435
623
|
if (Object.keys(result.routes).length === 0) {
|
|
436
624
|
if (!existing) {
|
|
437
625
|
const emptySource = generateRouteTypesSource({});
|
|
626
|
+
opts?.onWrite?.(outPath, emptySource);
|
|
438
627
|
writeFileSync(outPath, emptySource);
|
|
439
628
|
}
|
|
440
629
|
continue;
|
|
@@ -460,9 +649,10 @@ export function writeCombinedRouteTypes(
|
|
|
460
649
|
continue;
|
|
461
650
|
}
|
|
462
651
|
}
|
|
652
|
+
opts?.onWrite?.(outPath, source);
|
|
463
653
|
writeFileSync(outPath, source);
|
|
464
654
|
console.log(
|
|
465
|
-
`[
|
|
655
|
+
`[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
|
|
466
656
|
);
|
|
467
657
|
}
|
|
468
658
|
}
|
|
@@ -54,14 +54,21 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
54
54
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
55
|
} catch (err) {
|
|
56
56
|
console.warn(
|
|
57
|
-
`[
|
|
57
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
58
58
|
);
|
|
59
59
|
return results;
|
|
60
60
|
}
|
|
61
61
|
for (const entry of entries) {
|
|
62
62
|
const fullPath = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
65
72
|
results.push(...findTsFiles(fullPath, filter));
|
|
66
73
|
} else if (
|
|
67
74
|
(entry.name.endsWith(".ts") ||
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Allocation-light, linear-time source scanning for the build-time scanners.
|
|
2
|
+
//
|
|
3
|
+
// The router-file scanner, the HMR relevance check, and the unsupported-shape
|
|
4
|
+
// warning all need to know whether a token like `createRouter(` / `createLoader(`
|
|
5
|
+
// appears in REAL code versus inside a comment or string literal. Rather than
|
|
6
|
+
// build a full comment/string-stripped copy of the source (which on a large
|
|
7
|
+
// file allocates an O(n) string plus, naively, a per-char array), these helpers
|
|
8
|
+
// run the regex over the whole source ONCE (the engine sweeps left-to-right,
|
|
9
|
+
// O(n)) and classify each match's offset with a forward, O(1)-memory cursor that
|
|
10
|
+
// advances monotonically across the source.
|
|
11
|
+
//
|
|
12
|
+
// Time: O(n) — one native regex sweep plus one forward classification pass.
|
|
13
|
+
// Memory: O(1) for the boolean check; O(#matches) for the index list. No
|
|
14
|
+
// stripped copy and no per-char array are ever materialized.
|
|
15
|
+
//
|
|
16
|
+
// Pragmatic scanner, not a full tokenizer: regex literals are not special-cased
|
|
17
|
+
// (a target token inside one is implausible) and template interpolations are
|
|
18
|
+
// treated as opaque string content. One intentional consequence: a token whose
|
|
19
|
+
// match would only complete by treating an interleaved comment as whitespace
|
|
20
|
+
// (e.g. `createRouter /* x */ (`) is not detected — real calls never interleave
|
|
21
|
+
// a comment between the callee and its arguments.
|
|
22
|
+
|
|
23
|
+
// JS line terminators end a `//` comment: LF, CR, LS (U+2028), PS (U+2029).
|
|
24
|
+
function isLineTerminator(ch: string): boolean {
|
|
25
|
+
const c = ch.charCodeAt(0);
|
|
26
|
+
// LF, CR, LS (U+2028), PS (U+2029)
|
|
27
|
+
return c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a classifier that answers "is offset `q` in code (not a comment or
|
|
32
|
+
* string)?" for STRICTLY INCREASING `q`. The internal cursor only moves forward,
|
|
33
|
+
* so a full left-to-right sequence of queries costs O(n) total with O(1) memory.
|
|
34
|
+
*/
|
|
35
|
+
function makeCodeClassifier(code: string): (q: number) => boolean {
|
|
36
|
+
const n = code.length;
|
|
37
|
+
let i = 0; // forward cursor: everything before `i` is already classified
|
|
38
|
+
let skipStart = -1; // last detected comment/string region (cache)
|
|
39
|
+
let skipEnd = -1;
|
|
40
|
+
|
|
41
|
+
return (q: number): boolean => {
|
|
42
|
+
if (q >= skipStart && q < skipEnd) return false; // q in the cached region
|
|
43
|
+
while (i < n && i <= q) {
|
|
44
|
+
const c = code[i];
|
|
45
|
+
const d = i + 1 < n ? code[i + 1] : "";
|
|
46
|
+
let end = -1;
|
|
47
|
+
if (c === "/" && d === "/") {
|
|
48
|
+
let j = i + 2;
|
|
49
|
+
while (j < n && !isLineTerminator(code[j])) j++;
|
|
50
|
+
end = j;
|
|
51
|
+
} else if (c === "/" && d === "*") {
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
|
|
54
|
+
end = Math.min(n, j + 2);
|
|
55
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
56
|
+
let j = i + 1;
|
|
57
|
+
while (j < n) {
|
|
58
|
+
if (code[j] === "\\") {
|
|
59
|
+
j += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (code[j] === c) {
|
|
63
|
+
j++;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
end = j;
|
|
69
|
+
}
|
|
70
|
+
if (end >= 0) {
|
|
71
|
+
// Comment/string region [i, end). `q >= i` here (loop condition).
|
|
72
|
+
if (q < end) {
|
|
73
|
+
skipStart = i;
|
|
74
|
+
skipEnd = end;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
i = end;
|
|
78
|
+
} else {
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true; // reached q in code mode
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Index of the first match of `pattern` that occurs in code (not in a comment
|
|
88
|
+
* or string), or -1. `pattern` MUST be a global (`/g`) regex. Single native
|
|
89
|
+
* regex sweep with early-exit; O(1) extra memory.
|
|
90
|
+
*/
|
|
91
|
+
export function firstCodeMatchIndex(code: string, pattern: RegExp): number {
|
|
92
|
+
const inCode = makeCodeClassifier(code);
|
|
93
|
+
pattern.lastIndex = 0;
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
96
|
+
if (inCode(m.index)) return m.index;
|
|
97
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Byte offsets of every match of `pattern` that occurs in code (not in a
|
|
104
|
+
* comment or string). `pattern` MUST be a global (`/g`) regex. Each offset is
|
|
105
|
+
* the match start — the same byte offset a raw `pattern.exec` reports. O(n)
|
|
106
|
+
* time, O(#matches) memory.
|
|
107
|
+
*/
|
|
108
|
+
export function codeMatchIndices(code: string, pattern: RegExp): number[] {
|
|
109
|
+
const inCode = makeCodeClassifier(code);
|
|
110
|
+
const indices: number[] = [];
|
|
111
|
+
pattern.lastIndex = 0;
|
|
112
|
+
let m: RegExpExecArray | null;
|
|
113
|
+
while ((m = pattern.exec(code)) !== null) {
|
|
114
|
+
if (inCode(m.index)) indices.push(m.index);
|
|
115
|
+
if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
|
|
116
|
+
}
|
|
117
|
+
return indices;
|
|
118
|
+
}
|