mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve route flag values and lexical RPC names
This commit is contained in:
@@ -298,9 +298,11 @@ RPC client syntax also contributes network endpoint candidates, in both
|
||||
initializer and no binding assignments; shadowed locals do not inherit client
|
||||
identity. `subscribe` additionally requires a tRPC observer
|
||||
argument (`onData`, `onError`, `onStarted`, `onStopped`, `onComplete`, or
|
||||
`onConnectionStateChange`). `this` roots, browser globals, and any path
|
||||
containing an RTK Query `endpoints` segment are never procedures; cache-key
|
||||
helpers such as `queryKey` issue no request and are excluded.
|
||||
`onConnectionStateChange`). `this` roots and unshadowed browser globals are
|
||||
excluded. A lexical client can be named `history`, and its router segments can
|
||||
use browser-global spellings. RTK Query `endpoints` chains are excluded only
|
||||
when the root has no recognized tRPC client binding. Cache-key helpers such as
|
||||
`queryKey` issue no request and are excluded.
|
||||
- `trpc:link:<link>` records the literal `url` option of `httpLink`,
|
||||
`httpBatchLink`, `httpBatchStreamLink`, and `httpSubscriptionLink` when the
|
||||
link's callee name survives bundling, including `(0, module.httpBatchLink)`
|
||||
|
||||
@@ -61,6 +61,10 @@ The shared parser handles literal router calls, nested route objects, typed
|
||||
`_$` trees with explicitly inferred property-name fallbacks, Next App Router
|
||||
page paths, and navigation targets. It follows stable lexical bindings and
|
||||
keeps unresolved parents, mutations, spreads, and dynamic fields explicit.
|
||||
Index-route flags use the same value-at-read boundary as path fields: erased
|
||||
type wrappers and initialized stable boolean aliases preserve `true`/`false`;
|
||||
uninitialized or unresolved flags retain an interpretation unknown instead of
|
||||
silently disappearing.
|
||||
A selected route-builder root owns its configuration even when mutation prevents
|
||||
trusting its value. Ordinary wrapper keys do not become route ancestors. Missing
|
||||
outer ancestors of unbound typed subtrees remain unknown rather than invented.
|
||||
|
||||
@@ -30,6 +30,38 @@ const analyze = (source: string, budget?: number) => {
|
||||
return complete(scan.finishSteps());
|
||||
};
|
||||
|
||||
it.each(["true", "(true as const)", "INDEX"])(
|
||||
"retains an index route with flag %s",
|
||||
(flag) => {
|
||||
const facts = analyze(
|
||||
`const INDEX=true;createBrowserRouter([{index:${flag},Component:Home}]);`,
|
||||
);
|
||||
expect(facts.candidates.map(({ path }) => path)).toEqual(["/"]);
|
||||
expect(facts.unresolved).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"createBrowserRouter([{index:INDEX,Component:Home}]);const INDEX=true;",
|
||||
"const INDEX=runtime;createBrowserRouter([{index:INDEX,Component:Home}]);",
|
||||
])("retains unknown index flags without inventing a route: %s", (source) => {
|
||||
const facts = analyze(source);
|
||||
expect(facts.candidates).toEqual([]);
|
||||
expect(facts.unresolved).toEqual([
|
||||
expect.objectContaining({
|
||||
reason: expect.stringContaining("index-route flag"),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a known false index flag absent", () => {
|
||||
const facts = analyze(
|
||||
"const INDEX=false;createBrowserRouter([{index:INDEX,Component:Home}]);",
|
||||
);
|
||||
expect(facts.candidates).toEqual([]);
|
||||
expect(facts.unresolved).toEqual([]);
|
||||
});
|
||||
|
||||
it("joins router children and preserves each segment's source", () => {
|
||||
const facts = analyze(
|
||||
`createBrowserRouter([{path:'/reports',children:[{path:':id?',children:[{path:'settings'}]}]}]);`,
|
||||
|
||||
@@ -982,8 +982,16 @@ export class JavaScriptRouteScan {
|
||||
if (!t.isIdentifier(node)) return node;
|
||||
if (seen.has(node) || !this.#queue(node)) return;
|
||||
seen.add(node);
|
||||
const binding = index.bindingForReferenceAt(node);
|
||||
if (binding === undefined) return;
|
||||
const read = semanticBindingAtRead(
|
||||
binding,
|
||||
node,
|
||||
index.bindings.parentsByNode,
|
||||
);
|
||||
if (!read.hasDefiniteInitializer) return;
|
||||
const initializer = semanticStableInitializer(
|
||||
index.bindingForReferenceAt(node),
|
||||
read.binding,
|
||||
index.bindings,
|
||||
);
|
||||
if (
|
||||
@@ -1379,7 +1387,11 @@ export class JavaScriptRouteScan {
|
||||
fields.unknownKeys ||
|
||||
[...fields.dynamicNames].some(
|
||||
(key) =>
|
||||
typed || key === "path" || key === "route" || key === "children",
|
||||
typed ||
|
||||
key === "path" ||
|
||||
key === "route" ||
|
||||
key === "children" ||
|
||||
key === "index",
|
||||
);
|
||||
if (uncertain) {
|
||||
this.#unknown(
|
||||
@@ -1433,8 +1445,16 @@ export class JavaScriptRouteScan {
|
||||
};
|
||||
emit = true;
|
||||
} else if (!typed) {
|
||||
const value = own.get("index")?.value;
|
||||
const flag = own.get("index")?.value;
|
||||
const value =
|
||||
flag === undefined ? undefined : this.#resolve(flag, site, index);
|
||||
emit = t.isBooleanLiteral(value) && value.value;
|
||||
if (flag !== undefined && !t.isBooleanLiteral(value))
|
||||
this.#unknown(
|
||||
mechanism,
|
||||
flag,
|
||||
"The index-route flag is not a known boolean at this use.",
|
||||
);
|
||||
}
|
||||
if (emit && link !== undefined) {
|
||||
const segments: JavaScriptRouteSegment[] = [];
|
||||
|
||||
@@ -68,6 +68,36 @@ describe("RPC object property ownership", () => {
|
||||
});
|
||||
|
||||
describe("tRPC procedure recognition", () => {
|
||||
it("preserves lexical clients and router members with browser-global spellings", () => {
|
||||
expect(
|
||||
rpcEndpoints(`function scope() {
|
||||
const history=createTRPCClient({links:[httpBatchLink({url:'/api/trpc'})]});
|
||||
history.archive.query();
|
||||
history.history.list.query();
|
||||
history.endpoints.list.query();
|
||||
navigator.permissions.query({name:'geolocation'});
|
||||
api.endpoints.list.useQuery();
|
||||
}`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "network",
|
||||
value: "/api/trpc",
|
||||
mechanism: "trpc:link:httpBatchLink",
|
||||
},
|
||||
{ kind: "network", value: "archive", mechanism: "trpc:procedure:query" },
|
||||
{
|
||||
kind: "network",
|
||||
value: "history.list",
|
||||
mechanism: "trpc:procedure:query",
|
||||
},
|
||||
{
|
||||
kind: "network",
|
||||
value: "endpoints.list",
|
||||
mechanism: "trpc:procedure:query",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes React hook procedures on a renamed proxy root", () => {
|
||||
expect(
|
||||
rpcEndpoints(`
|
||||
|
||||
@@ -169,8 +169,9 @@ const trpcLinkUrl = (
|
||||
* Create the per-source scan. Hook and option-factory methods are distinctive.
|
||||
* `query`/`mutate` need a root bound to a tRPC client factory, or a tRPC source
|
||||
* marker with a router path of at least two segments; `subscribe` needs the
|
||||
* marker and a tRPC observer argument. `this` roots, browser globals and RTK
|
||||
* Query `endpoints` chains are never procedures.
|
||||
* marker and a tRPC observer argument. Unshadowed browser globals and `this`
|
||||
* roots are excluded; RTK Query `endpoints` exclusions apply only without a
|
||||
* recognized client binding. Router segment spelling does not supply identity.
|
||||
*/
|
||||
export const createJavaScriptRpcScan = (
|
||||
source: string,
|
||||
@@ -187,15 +188,18 @@ export const createJavaScriptRpcScan = (
|
||||
): JavaScriptRpcEndpoint | undefined => {
|
||||
const path = memberPath(node.callee);
|
||||
if (path === undefined || t.isThisExpression(path.root)) return undefined;
|
||||
if (t.isIdentifier(path.root) && BROWSER_GLOBAL_NAMES.has(path.root.name))
|
||||
const bound = clientRoots.has(path.root);
|
||||
if (
|
||||
t.isIdentifier(path.root) &&
|
||||
BROWSER_GLOBAL_NAMES.has(path.root.name) &&
|
||||
index.bindingForReferenceAt(path.root) === undefined
|
||||
)
|
||||
return undefined;
|
||||
const method = path.properties[path.properties.length - 1];
|
||||
const segments = path.properties.slice(0, -1);
|
||||
const first = segments[0];
|
||||
if (method === undefined || first === undefined) return undefined;
|
||||
if (BROWSER_GLOBAL_NAMES.has(first) || segments.includes("endpoints"))
|
||||
return undefined;
|
||||
const bound = clientRoots.has(path.root);
|
||||
if (!bound && segments.includes("endpoints")) return undefined;
|
||||
const recognized = TRPC_HOOK_METHODS.has(method)
|
||||
? true
|
||||
: TRPC_REQUEST_METHODS.has(method)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"preserves route flags and lexical RPC names through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-route-rpc-identity-");
|
||||
await writeFixtureFiles(root, {
|
||||
"entry.ts": `
|
||||
const INDEX=true;
|
||||
createBrowserRouter([{index:INDEX,Component:Home}, {path:'/wrapped',children:[{index:(true as const),Component:Home}]}]);
|
||||
function clientScope() {
|
||||
const history=createTRPCClient({links:[httpBatchLink({url:'/api/trpc'})]});
|
||||
history.archive.query();history.history.list.query();
|
||||
navigator.permissions.query({name:'geolocation'});
|
||||
api.endpoints.list.useQuery();
|
||||
}
|
||||
`,
|
||||
});
|
||||
const result = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
expect(result.route_inventory?.routes.map(({ path }) => path)).toEqual([
|
||||
"/",
|
||||
"/wrapped",
|
||||
]);
|
||||
const operations = result.graph.nodes
|
||||
.filter(({ kind }) => kind === "endpoint")
|
||||
.flatMap(({ observations }) => observations)
|
||||
.filter(
|
||||
({ properties }) => properties.mechanism === "trpc:procedure:query",
|
||||
)
|
||||
.map(({ properties }) => properties.value)
|
||||
.sort();
|
||||
expect(operations).toEqual(["archive", "history.list"]);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user