@rangojs/router 0.0.0-experimental.112 → 0.0.0-experimental.114
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/bin/rango.js +74 -3
- package/dist/vite/index.js +133 -18
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +35 -24
- package/skills/caching/SKILL.md +115 -7
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/hooks/SKILL.md +40 -22
- package/skills/links/SKILL.md +10 -10
- package/skills/loader/SKILL.md +3 -3
- package/skills/rango/SKILL.md +16 -10
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +85 -3
- package/src/browser/react/location-state-shared.ts +93 -3
- package/src/browser/react/use-reverse.ts +19 -12
- package/src/build/route-types/per-module-writer.ts +4 -1
- package/src/build/route-types/router-processing.ts +14 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/handle.ts +3 -5
- package/src/loader-store.ts +62 -25
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/reverse.ts +16 -13
- package/src/route-definition/dsl-helpers.ts +5 -2
- package/src/route-definition/helpers-types.ts +31 -10
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/router-options.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +17 -4
- package/src/router/segment-resolution/revalidation.ts +17 -4
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +2 -0
- package/src/segment-system.tsx +59 -10
- package/src/server/context.ts +26 -0
- package/src/server/cookie-store.ts +28 -4
- package/src/types/handler-context.ts +5 -2
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +9 -1
- package/src/use-loader.tsx +89 -42
- package/src/vite/plugins/expose-ids/export-analysis.ts +68 -12
- package/src/vite/plugins/expose-internal-ids.ts +12 -4
- package/src/vite/plugins/use-cache-transform.ts +12 -10
- package/src/vite/router-discovery.ts +14 -2
package/src/use-loader.tsx
CHANGED
|
@@ -187,17 +187,20 @@ export interface UseLoaderOptions {
|
|
|
187
187
|
*/
|
|
188
188
|
key?: string;
|
|
189
189
|
/**
|
|
190
|
-
* Cross-loader refresh group. Tag reads of DIFFERENT loaders with
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* Cross-loader refresh group tag(s). Tag reads of DIFFERENT loaders with a
|
|
191
|
+
* shared name, then call `useRefreshLoaders()(name)` to refresh the whole group
|
|
192
|
+
* at once. Pass an array to tag one read into several groups — it is refreshed
|
|
193
|
+
* when ANY of its groups is refreshed, so a coarse tag can cover the whole set
|
|
194
|
+
* while a finer tag targets a subset. Each member is refreshed with a plain GET
|
|
195
|
+
* against the current route URL — no params, no body, no mutation methods —
|
|
196
|
+
* because a group spans heterogeneous loaders with different param/return
|
|
197
|
+
* shapes.
|
|
195
198
|
*
|
|
196
199
|
* For parameterized sharing of a SINGLE loader, use `key` instead; group
|
|
197
200
|
* members should be registered or non-parameterized-keyed reads (a plain-GET
|
|
198
201
|
* group refresh would drop any per-call params).
|
|
199
202
|
*/
|
|
200
|
-
refreshGroup?: string;
|
|
203
|
+
refreshGroup?: string | string[];
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
/**
|
|
@@ -254,29 +257,42 @@ function useLoaderInternal<T>(
|
|
|
254
257
|
// only hooks with the same `key` refresh together. Default (no key) keeps the
|
|
255
258
|
// historical behavior: one bucket per loader id.
|
|
256
259
|
const key = options?.key;
|
|
257
|
-
|
|
260
|
+
// Normalize the refresh-group tag(s) to a stable, deduped, sorted list. The
|
|
261
|
+
// joined `groupKey` string is the subscribe effect's dependency, so passing an
|
|
262
|
+
// inline array literal (`refreshGroup={["a", "b"]}`) does not force a
|
|
263
|
+
// resubscribe on every render. An empty list means "no groups" — identical to
|
|
264
|
+
// omitting the option (`hasGroups` stays false, no private bucket is created).
|
|
265
|
+
const refreshGroupOption = options?.refreshGroup;
|
|
266
|
+
const groupKey =
|
|
267
|
+
refreshGroupOption === undefined
|
|
268
|
+
? ""
|
|
269
|
+
: JSON.stringify(
|
|
270
|
+
typeof refreshGroupOption === "string"
|
|
271
|
+
? [refreshGroupOption]
|
|
272
|
+
: [...new Set(refreshGroupOption)].sort(),
|
|
273
|
+
);
|
|
274
|
+
const groupList = useMemo<string[]>(
|
|
275
|
+
() => (groupKey === "" ? [] : (JSON.parse(groupKey) as string[])),
|
|
276
|
+
[groupKey],
|
|
277
|
+
);
|
|
278
|
+
const hasGroups = groupList.length > 0;
|
|
258
279
|
// A grouped reader with no explicit key gets a private per-hook bucket so a
|
|
259
280
|
// cross-loader group refresh cannot leak into the bare `loader.$$id` bucket
|
|
260
281
|
// shared by unrelated unkeyed readers. Sharing within a group is opt-in via
|
|
261
282
|
// an explicit `key`.
|
|
262
283
|
const privateBucketIdRef = useRef<string | null>(null);
|
|
263
|
-
if (
|
|
264
|
-
refreshGroup !== undefined &&
|
|
265
|
-
key === undefined &&
|
|
266
|
-
privateBucketIdRef.current === null
|
|
267
|
-
) {
|
|
284
|
+
if (hasGroups && key === undefined && privateBucketIdRef.current === null) {
|
|
268
285
|
privateBucketIdRef.current = `__rg${privateGroupBucketSeq++}`;
|
|
269
286
|
}
|
|
270
287
|
const effectiveKey =
|
|
271
|
-
key ??
|
|
272
|
-
(refreshGroup !== undefined ? privateBucketIdRef.current! : undefined);
|
|
288
|
+
key ?? (hasGroups ? privateBucketIdRef.current! : undefined);
|
|
273
289
|
const bucketKey =
|
|
274
290
|
effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
|
|
275
291
|
|
|
276
292
|
// Plain-GET refresh thunk registered with the store for cross-loader group
|
|
277
293
|
// refresh (useRefreshLoaders). Always shares into this hook's bucket, never
|
|
278
294
|
// touches lastSharedRequestIdRef (so a group refresh never render-throws —
|
|
279
|
-
// errors surface via `error` and reject the
|
|
295
|
+
// errors surface via `error` and reject the refreshGroups() promise instead),
|
|
280
296
|
// and sends no params/body. Stable across navigations (depends only on
|
|
281
297
|
// loaderId + bucketKey), so the store keeps one current thunk per bucket.
|
|
282
298
|
const groupRefetch = useCallback(async (): Promise<void> => {
|
|
@@ -340,16 +356,17 @@ function useLoaderInternal<T>(
|
|
|
340
356
|
{
|
|
341
357
|
loaderId,
|
|
342
358
|
ephemeral: !hasContextData,
|
|
343
|
-
group:
|
|
344
|
-
refetch:
|
|
359
|
+
group: hasGroups ? groupList : undefined,
|
|
360
|
+
refetch: hasGroups ? groupRefetch : undefined,
|
|
345
361
|
},
|
|
346
362
|
);
|
|
347
363
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional:
|
|
348
364
|
// sharedSnapshot is captured for the one-shot init sync; we don't want
|
|
349
365
|
// to re-subscribe on every snapshot change. bucketKey, hasContextData,
|
|
350
|
-
//
|
|
351
|
-
// subscription (groupRefetch is stable
|
|
352
|
-
|
|
366
|
+
// groupKey, and groupRefetch are the only inputs that require a fresh
|
|
367
|
+
// subscription (groupList is memoized on groupKey; groupRefetch is stable
|
|
368
|
+
// per bucketKey).
|
|
369
|
+
}, [bucketKey, hasContextData, groupKey, groupRefetch]);
|
|
353
370
|
|
|
354
371
|
// Local state holds the result of:
|
|
355
372
|
// - parameterized / mutation `load()` calls (load({ params }), POST,
|
|
@@ -360,9 +377,13 @@ function useLoaderInternal<T>(
|
|
|
360
377
|
// prevents two unrelated components from accidentally sharing data
|
|
361
378
|
// through the global store just because they reference the same
|
|
362
379
|
// loader id.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
380
|
+
// `has` distinguishes a committed local result (including `null`/`undefined`)
|
|
381
|
+
// from "no local load yet", so a load() that resolves to a falsy value is not
|
|
382
|
+
// discarded in favor of the shared snapshot or the seeded context.
|
|
383
|
+
const [localFetchedData, setLocalFetchedData] = useState<{
|
|
384
|
+
has: boolean;
|
|
385
|
+
value: T | undefined;
|
|
386
|
+
}>({ has: false, value: undefined });
|
|
366
387
|
const [localIsLoading, setLocalIsLoading] = useState(false);
|
|
367
388
|
const [localError, setLocalError] = useState<Error | null>(null);
|
|
368
389
|
|
|
@@ -385,7 +406,7 @@ function useLoaderInternal<T>(
|
|
|
385
406
|
const prevContextDataRef = useRef(contextData);
|
|
386
407
|
useEffect(() => {
|
|
387
408
|
if (prevContextDataRef.current !== contextData) {
|
|
388
|
-
setLocalFetchedData(undefined);
|
|
409
|
+
setLocalFetchedData({ has: false, value: undefined });
|
|
389
410
|
setLocalIsLoading(false);
|
|
390
411
|
setLocalError(null);
|
|
391
412
|
lastSharedRequestIdRef.current = null;
|
|
@@ -396,10 +417,14 @@ function useLoaderInternal<T>(
|
|
|
396
417
|
}
|
|
397
418
|
}, [contextData, loaderId]);
|
|
398
419
|
|
|
399
|
-
// Read priority: a parameterized load() result overrides the shared
|
|
400
|
-
// snapshot;
|
|
401
|
-
|
|
402
|
-
|
|
420
|
+
// Read priority: a committed parameterized load() result overrides the shared
|
|
421
|
+
// snapshot; a committed shared snapshot overrides the server-seeded context.
|
|
422
|
+
// `has`/`hasValue` gate each level so a committed falsy value is not skipped.
|
|
423
|
+
const data = localFetchedData.has
|
|
424
|
+
? localFetchedData.value
|
|
425
|
+
: sharedSnapshot.hasValue
|
|
426
|
+
? (sharedSnapshot.value as T | undefined)
|
|
427
|
+
: contextData;
|
|
403
428
|
const isLoading = localIsLoading || sharedSnapshot.isLoading;
|
|
404
429
|
const error = localError ?? sharedSnapshot.error;
|
|
405
430
|
|
|
@@ -553,7 +578,7 @@ function useLoaderInternal<T>(
|
|
|
553
578
|
// if a newer load() was issued from this hook before this one
|
|
554
579
|
// resolved, drop the stale result.
|
|
555
580
|
startTransition(() => {
|
|
556
|
-
setLocalFetchedData(result);
|
|
581
|
+
setLocalFetchedData({ has: true, value: result });
|
|
557
582
|
setLocalIsLoading(false);
|
|
558
583
|
});
|
|
559
584
|
}
|
|
@@ -712,14 +737,21 @@ export function useFetchLoader<T>(
|
|
|
712
737
|
}
|
|
713
738
|
|
|
714
739
|
/**
|
|
715
|
-
*
|
|
740
|
+
* Get a stable function that refreshes loaders by cross-loader group tag.
|
|
716
741
|
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
* switch). Members are tagged via
|
|
722
|
-
* `useFetchLoader(Loader, { refreshGroup })
|
|
742
|
+
* The returned `refresh(groups)` takes one group name or an array of names and
|
|
743
|
+
* re-runs every currently-mounted read tagged with ANY of them, with a plain GET
|
|
744
|
+
* against the current route URL. This is the cross-loader counterpart to the
|
|
745
|
+
* single-loader `key`: use it to refresh a set of DIFFERENT loaders together
|
|
746
|
+
* (e.g. profile + orders after an account switch). Members are tagged via
|
|
747
|
+
* `useLoader(Loader, { refreshGroup })` / `useFetchLoader(Loader, { refreshGroup })`,
|
|
748
|
+
* where `refreshGroup` is one name or several.
|
|
749
|
+
*
|
|
750
|
+
* Passing the group(s) to the returned function rather than to the hook lets a
|
|
751
|
+
* single `useRefreshLoaders()` instance refresh different groups depending on
|
|
752
|
+
* context, and lets one call refresh several groups at once — their members are
|
|
753
|
+
* unioned and deduped, so a loader tagged into two of the named groups is fetched
|
|
754
|
+
* exactly once.
|
|
723
755
|
*
|
|
724
756
|
* Group refresh never render-throws: a failing member surfaces its error via
|
|
725
757
|
* that read's `error` state, and the returned promise rejects with an
|
|
@@ -736,15 +768,30 @@ export function useFetchLoader<T>(
|
|
|
736
768
|
* return <span>{data.name}</span>;
|
|
737
769
|
* }
|
|
738
770
|
* function Orders() {
|
|
739
|
-
*
|
|
771
|
+
* // Tagged into two groups: refreshed by "account" (the whole set) or "orders".
|
|
772
|
+
* const { data } = useLoader(OrdersLoader, {
|
|
773
|
+
* key: userId,
|
|
774
|
+
* refreshGroup: ["account", "orders"],
|
|
775
|
+
* });
|
|
740
776
|
* return <span>{data.count} orders</span>;
|
|
741
777
|
* }
|
|
742
|
-
* function
|
|
743
|
-
* const
|
|
744
|
-
* return
|
|
778
|
+
* function RefreshButtons() {
|
|
779
|
+
* const refresh = useRefreshLoaders();
|
|
780
|
+
* return (
|
|
781
|
+
* <>
|
|
782
|
+
* <button onClick={() => refresh("account")}>Refresh account</button>
|
|
783
|
+
* <button onClick={() => refresh("orders")}>Refresh orders</button>
|
|
784
|
+
* <button onClick={() => refresh(["account", "orders"])}>Refresh both</button>
|
|
785
|
+
* </>
|
|
786
|
+
* );
|
|
745
787
|
* }
|
|
746
788
|
* ```
|
|
747
789
|
*/
|
|
748
|
-
export function useRefreshLoaders(
|
|
749
|
-
|
|
790
|
+
export function useRefreshLoaders(): (
|
|
791
|
+
groups: string | string[],
|
|
792
|
+
) => Promise<void> {
|
|
793
|
+
return useCallback(
|
|
794
|
+
(groups: string | string[]) => loaderStore.refreshGroups(groups),
|
|
795
|
+
[],
|
|
796
|
+
);
|
|
750
797
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
buildExportMap,
|
|
7
7
|
escapeRegExp,
|
|
8
8
|
} from "../expose-id-utils.js";
|
|
9
|
+
import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
|
|
9
10
|
import type { CreateExportBinding } from "./types.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -59,19 +60,57 @@ export function isExportOnlyFile(
|
|
|
59
60
|
return true;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
function createCallPattern(fnNames: string[]): RegExp {
|
|
64
|
+
return new RegExp(
|
|
65
|
+
`\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
|
|
66
|
+
"g",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Counts real create*() call sites, ignoring occurrences inside comments and
|
|
71
|
+
// string literals. Used by the unsupported-shape warning heuristic and the
|
|
72
|
+
// inline-extraction pre-check.
|
|
66
73
|
export function countCreateCallsForNames(
|
|
67
74
|
code: string,
|
|
68
75
|
fnNames: string[],
|
|
69
76
|
): number {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
return codeMatchIndices(code, createCallPattern(fnNames)).length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Convert a 0-based byte offset to a 1-based { line, column }. */
|
|
81
|
+
export function offsetToLineColumn(
|
|
82
|
+
code: string,
|
|
83
|
+
index: number,
|
|
84
|
+
): { line: number; column: number } {
|
|
85
|
+
let line = 1;
|
|
86
|
+
let lineStart = 0;
|
|
87
|
+
const end = Math.min(index, code.length);
|
|
88
|
+
for (let i = 0; i < end; i++) {
|
|
89
|
+
if (code[i] === "\n") {
|
|
90
|
+
line++;
|
|
91
|
+
lineStart = i + 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { line, column: index - lineStart + 1 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Locate every real create*() call site (comment/string-free) that is NOT one
|
|
99
|
+
* of the supported, id-injectable export bindings, returning each as a 1-based
|
|
100
|
+
* { line, column }. The empty result means every call is in a supported shape.
|
|
101
|
+
* Both binding-collection paths anchor `callExprStart` at the start of the
|
|
102
|
+
* create* identifier — exactly where this pattern matches — so the set
|
|
103
|
+
* difference is exact.
|
|
104
|
+
*/
|
|
105
|
+
export function findUnsupportedCreateCallSites(
|
|
106
|
+
code: string,
|
|
107
|
+
fnNames: string[],
|
|
108
|
+
supportedBindings: CreateExportBinding[],
|
|
109
|
+
): Array<{ line: number; column: number }> {
|
|
110
|
+
const supported = new Set(supportedBindings.map((b) => b.callExprStart));
|
|
111
|
+
return codeMatchIndices(code, createCallPattern(fnNames))
|
|
112
|
+
.filter((index) => !supported.has(index))
|
|
113
|
+
.map((index) => offsetToLineColumn(code, index));
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
export function getImportedFnNames(
|
|
@@ -306,9 +345,25 @@ export function collectCreateExportBindings(
|
|
|
306
345
|
export function buildUnsupportedShapeWarning(
|
|
307
346
|
filePath: string,
|
|
308
347
|
fnName: string,
|
|
348
|
+
sites: Array<{ line: number; column: number }> = [],
|
|
309
349
|
): string {
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
const lines = [`[rango] Unsupported ${fnName} shape in "${filePath}".`];
|
|
351
|
+
|
|
352
|
+
// Point at the exact call(s) so the location is clickable in the terminal/IDE
|
|
353
|
+
// (file:line:column) instead of leaving the user to scan the whole file.
|
|
354
|
+
if (sites.length === 1) {
|
|
355
|
+
const s = sites[0];
|
|
356
|
+
lines.push(
|
|
357
|
+
`The ${fnName}(...) call at ${filePath}:${s.line}:${s.column} has no stable $$id injected — it is not in a supported shape.`,
|
|
358
|
+
);
|
|
359
|
+
} else if (sites.length > 1) {
|
|
360
|
+
lines.push(
|
|
361
|
+
`These ${fnName}(...) calls have no stable $$id injected — they are not in a supported shape:`,
|
|
362
|
+
);
|
|
363
|
+
for (const s of sites) lines.push(` - ${filePath}:${s.line}:${s.column}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
lines.push(
|
|
312
367
|
`Supported shapes are:`,
|
|
313
368
|
` - export const X = ${fnName}(...)`,
|
|
314
369
|
` - const X = ${fnName}(...); export { X }`,
|
|
@@ -316,5 +371,6 @@ export function buildUnsupportedShapeWarning(
|
|
|
316
371
|
`Potentially unsupported forms include:`,
|
|
317
372
|
` - export let/var X = ${fnName}(...)`,
|
|
318
373
|
` - inline ${fnName}(...) calls`,
|
|
319
|
-
|
|
374
|
+
);
|
|
375
|
+
return lines.join("\n");
|
|
320
376
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getImportedFnNames,
|
|
29
29
|
collectCreateExportBindings,
|
|
30
30
|
buildUnsupportedShapeWarning,
|
|
31
|
+
findUnsupportedCreateCallSites,
|
|
31
32
|
isExportOnlyFile,
|
|
32
33
|
} from "./expose-ids/export-analysis.js";
|
|
33
34
|
import {
|
|
@@ -370,14 +371,21 @@ ${lazyImports.join(",\n")}
|
|
|
370
371
|
if (!hasCode) continue;
|
|
371
372
|
|
|
372
373
|
const fnNames = getFnNames(cfg.fnName);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
// Locate the real (comment/string-free) create* calls not covered by
|
|
375
|
+
// a supported, id-injectable export shape. Empty means every call is
|
|
376
|
+
// fine — in particular, a create*() token in a comment or string no
|
|
377
|
+
// longer trips a spurious warning.
|
|
378
|
+
const sites = findUnsupportedCreateCallSites(
|
|
379
|
+
code,
|
|
380
|
+
fnNames,
|
|
381
|
+
getBindings(code, fnNames),
|
|
382
|
+
);
|
|
383
|
+
if (sites.length === 0) continue;
|
|
376
384
|
|
|
377
385
|
const warnKey = `${id}::${cfg.fnName}`;
|
|
378
386
|
if (unsupportedShapeWarnings.has(warnKey)) continue;
|
|
379
387
|
unsupportedShapeWarnings.add(warnKey);
|
|
380
|
-
this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName));
|
|
388
|
+
this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName, sites));
|
|
381
389
|
}
|
|
382
390
|
|
|
383
391
|
// --- Loader: track for manifest (RSC env only) ---
|
|
@@ -30,6 +30,13 @@ const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
|
|
|
30
30
|
// and should not be wrapped (children can't be cache-keyed).
|
|
31
31
|
const LAYOUT_TEMPLATE_PATTERN = /\/(layout|template)\.(tsx?|jsx?)$/;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Grammar for a valid function-level directive: `use cache` optionally followed
|
|
35
|
+
* by `: <profile-name>`. The single source of truth for both the transform and
|
|
36
|
+
* the near-miss validator below.
|
|
37
|
+
*/
|
|
38
|
+
export const USE_CACHE_DIRECTIVE_RE: RegExp = /^use cache(:\s*[\w-]+)?$/;
|
|
39
|
+
|
|
33
40
|
export function useCacheTransform(): Plugin {
|
|
34
41
|
let projectRoot = "";
|
|
35
42
|
let isBuild = false;
|
|
@@ -116,9 +123,9 @@ export function useCacheTransform(): Plugin {
|
|
|
116
123
|
transformHoistInlineDirective,
|
|
117
124
|
);
|
|
118
125
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
126
|
+
// Check for near-miss directives on the function-level path. The
|
|
127
|
+
// file-level branch above returns earlier (it wraps every export
|
|
128
|
+
// regardless), so this runs only when there is no file-level directive.
|
|
122
129
|
warnOnNearMissDirectives(ast, id, this.warn.bind(this));
|
|
123
130
|
|
|
124
131
|
if (functionResult) return functionResult;
|
|
@@ -219,7 +226,7 @@ function transformFunctionLevelUseCache(
|
|
|
219
226
|
) {
|
|
220
227
|
try {
|
|
221
228
|
const { output, names } = transformHoistInlineDirective(code, ast, {
|
|
222
|
-
directive:
|
|
229
|
+
directive: USE_CACHE_DIRECTIVE_RE,
|
|
223
230
|
runtime: (
|
|
224
231
|
value: string,
|
|
225
232
|
name: string,
|
|
@@ -273,11 +280,6 @@ function findFileLevelDirective(
|
|
|
273
280
|
return null;
|
|
274
281
|
}
|
|
275
282
|
|
|
276
|
-
/**
|
|
277
|
-
* The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
|
|
278
|
-
*/
|
|
279
|
-
const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
|
|
280
|
-
|
|
281
283
|
/**
|
|
282
284
|
* Regex for near-miss: starts with "use cache:" but has invalid tokens.
|
|
283
285
|
*/
|
|
@@ -307,7 +309,7 @@ function warnOnNearMissDirectives(
|
|
|
307
309
|
if (
|
|
308
310
|
value.startsWith("use cache") &&
|
|
309
311
|
NEAR_MISS_RE.test(value) &&
|
|
310
|
-
!
|
|
312
|
+
!USE_CACHE_DIRECTIVE_RE.test(value)
|
|
311
313
|
) {
|
|
312
314
|
const profilePart = value.slice("use cache:".length).trim();
|
|
313
315
|
warn(
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
findNestedRouterConflict,
|
|
18
18
|
findRouterFiles,
|
|
19
19
|
} from "../build/generate-route-types.js";
|
|
20
|
+
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
20
21
|
import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
21
22
|
import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
|
|
22
23
|
import {
|
|
@@ -1184,8 +1185,19 @@ export function createRouterDiscoveryPlugin(
|
|
|
1184
1185
|
trimmed.startsWith('"use client"') ||
|
|
1185
1186
|
trimmed.startsWith("'use client'");
|
|
1186
1187
|
if (!inRecoveryMode && isUseClient) return;
|
|
1187
|
-
|
|
1188
|
-
|
|
1188
|
+
// Cheap raw pre-check first; only when a candidate token is present
|
|
1189
|
+
// do we confirm it occurs in real code (not a comment/string) via a
|
|
1190
|
+
// single allocation-free code-region scan. Most saved files contain
|
|
1191
|
+
// neither token and skip the scan entirely. This avoids a comment or
|
|
1192
|
+
// string mention spuriously marking a file relevant and triggering an
|
|
1193
|
+
// unnecessary re-discovery on save.
|
|
1194
|
+
let hasUrls = source.includes("urls(");
|
|
1195
|
+
let hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
1196
|
+
if (hasUrls) hasUrls = firstCodeMatchIndex(source, /urls\(/g) >= 0;
|
|
1197
|
+
if (hasCreateRouter) {
|
|
1198
|
+
hasCreateRouter =
|
|
1199
|
+
firstCodeMatchIndex(source, /\bcreateRouter\s*[<(]/g) >= 0;
|
|
1200
|
+
}
|
|
1189
1201
|
if (!inRecoveryMode && !hasUrls && !hasCreateRouter) return;
|
|
1190
1202
|
if (inRecoveryMode) {
|
|
1191
1203
|
debugDiscovery?.(
|