mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): honor final RPC object properties
Resolve final object writes once at the RPC endpoint producer. Unknown computed writes and spreads remain barriers; overridden properties do not invent endpoints. Source, CLI and stdio MCP regressions cover the behavior. Co-authored-by: Vaishnavi220506 <vaishnaviharish2006@gmail.com>
This commit is contained in:
@@ -273,6 +273,12 @@ RPC client syntax also contributes network endpoint candidates, in both
|
||||
`OperationDefinition` document object. A graphql-tag `loc.source.body` copy is
|
||||
not reported twice. Anonymous operations are not reported.
|
||||
|
||||
Literal link options and precompiled document fields follow object property
|
||||
order: the final definition owns the key. A later spread, dynamic computed key,
|
||||
method, or accessor can make a field unknown, so an overwritten literal is not
|
||||
reported as the endpoint or operation. A final explicit property after a spread
|
||||
or dynamic key remains usable.
|
||||
|
||||
A procedure path is the server-side procedure name, not a resolved URL; the
|
||||
transport URL depends on the link configuration and batching. When the proxy is
|
||||
reached through a member expression, such as a webpack module binding
|
||||
|
||||
@@ -13,6 +13,50 @@ const rpcEndpoints = (source: string) =>
|
||||
mechanism.startsWith("trpc:") || mechanism.startsWith("graphql:"),
|
||||
);
|
||||
|
||||
describe("RPC object property ownership", () => {
|
||||
it.each([
|
||||
['url: "/trpc/old", url: "/trpc/live"', "/trpc/live"],
|
||||
['url: "/trpc/old", url: runtimeUrl', null],
|
||||
['url: "/trpc/old", ...options', null],
|
||||
['url: "/trpc/old", [key]: runtimeUrl', null],
|
||||
['url: "/trpc/old", get url() { return runtimeUrl; }', null],
|
||||
['url: "/trpc/old", url() {}', null],
|
||||
['...options, url: "/trpc/live", headers: {}', "/trpc/live"],
|
||||
['[key]: runtimeUrl, url: "/trpc/live"', "/trpc/live"],
|
||||
['url: "/trpc/old", ["url"]: "/trpc/live"', "/trpc/live"],
|
||||
] as const)(
|
||||
"reads only the final known link URL from {%s}",
|
||||
(options, value) => {
|
||||
expect(rpcEndpoints(`httpBatchLink({ ${options} });`)).toEqual(
|
||||
value === null
|
||||
? []
|
||||
: [{ kind: "network", value, mechanism: "trpc:link:httpBatchLink" }],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['operation: "mutation"', "Old", "graphql:mutation"],
|
||||
['name: { kind: "Name", value: "Current" }', "Current", "graphql:query"],
|
||||
['kind: "FragmentDefinition"', null, null],
|
||||
["name: runtimeName", null, null],
|
||||
["get name() { return runtimeName; }", null, null],
|
||||
["...options", null, null],
|
||||
["[key]: runtimeValue", null, null],
|
||||
] as const)(
|
||||
"honors final GraphQL document properties: %s",
|
||||
(override, value, mechanism) => {
|
||||
const source = `const doc = {
|
||||
kind: "OperationDefinition", operation: "query",
|
||||
name: { kind: "Name", value: "Old" }, ${override}
|
||||
};`;
|
||||
expect(rpcEndpoints(source)).toEqual(
|
||||
value === null ? [] : [{ kind: "network", value, mechanism }],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("tRPC procedure recognition", () => {
|
||||
it("recognizes React hook procedures on a renamed proxy root", () => {
|
||||
expect(
|
||||
|
||||
@@ -122,33 +122,30 @@ const objectKeys = (node: t.Node | undefined): readonly string[] =>
|
||||
})
|
||||
: [];
|
||||
|
||||
const objectStringProperty = (
|
||||
node: t.ObjectExpression,
|
||||
key: string,
|
||||
): string | undefined => {
|
||||
for (const property of node.properties)
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
semanticStaticPropertyKey(property.key, property.computed) === key &&
|
||||
t.isStringLiteral(property.value)
|
||||
)
|
||||
return property.value.value;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Later properties own a key; an intervening dynamic write leaves it unknown. */
|
||||
const objectProperty = (
|
||||
node: t.ObjectExpression,
|
||||
key: string,
|
||||
): t.Node | undefined => {
|
||||
for (const property of node.properties)
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
semanticStaticPropertyKey(property.key, property.computed) === key
|
||||
)
|
||||
return property.value;
|
||||
for (let index = node.properties.length - 1; index >= 0; index -= 1) {
|
||||
const property = node.properties[index];
|
||||
if (property === undefined || t.isSpreadElement(property)) return undefined;
|
||||
const name = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (name === null) return undefined;
|
||||
if (name === key)
|
||||
return t.isObjectProperty(property) ? property.value : undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const objectStringProperty = (
|
||||
node: t.ObjectExpression,
|
||||
key: string,
|
||||
): string | undefined => {
|
||||
const value = objectProperty(node, key);
|
||||
return t.isStringLiteral(value) ? value.value : undefined;
|
||||
};
|
||||
|
||||
const GRAPHQL_OPERATION_TYPES: ReadonlySet<string> = new Set([
|
||||
"query",
|
||||
"mutation",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import {
|
||||
analyzeJavaScriptDirectory,
|
||||
runNodeJsonControl,
|
||||
} from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains only final known RPC object properties through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-rpc-properties-");
|
||||
await writeFixtureFiles(root, {
|
||||
"entry.mjs": `
|
||||
function httpBatchLink(options) { return options.url; }
|
||||
const options = { url: "/trpc/runtime" };
|
||||
const key = "url";
|
||||
const links = [
|
||||
httpBatchLink({ url: "/trpc/old", url: "/trpc/live" }),
|
||||
httpBatchLink({ url: "/trpc/spread-old", ...options }),
|
||||
httpBatchLink({ url: "/trpc/computed-old", [key]: options.url }),
|
||||
httpBatchLink({ ...options, url: "/trpc/final", headers: {} }),
|
||||
];
|
||||
const doc = {
|
||||
kind: "OperationDefinition", operation: "query", operation: "mutation",
|
||||
name: { kind: "Name", value: "Old" },
|
||||
name: { kind: "Name", value: "Current" },
|
||||
};
|
||||
console.log(JSON.stringify([links, doc.operation, doc.name.value]));
|
||||
`,
|
||||
});
|
||||
// This control checks JavaScript object semantics, not a live tRPC client.
|
||||
expect(await runNodeJsonControl(root, "entry.mjs")).toEqual([
|
||||
["/trpc/live", "/trpc/runtime", "/trpc/runtime", "/trpc/final"],
|
||||
"mutation",
|
||||
"Current",
|
||||
]);
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
const endpoints = graph.nodes
|
||||
.filter((node) => node.kind === "endpoint")
|
||||
.flatMap((node) => node.observations)
|
||||
.filter(({ properties }) =>
|
||||
/^(trpc:link:|graphql:)/u.test(String(properties.mechanism)),
|
||||
)
|
||||
.map(({ properties }) => [properties.value, properties.mechanism]);
|
||||
expect(endpoints).toEqual(
|
||||
expect.arrayContaining([
|
||||
["/trpc/live", "trpc:link:httpBatchLink"],
|
||||
["/trpc/final", "trpc:link:httpBatchLink"],
|
||||
["Current", "graphql:mutation"],
|
||||
]),
|
||||
);
|
||||
expect(endpoints).toHaveLength(3);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user