mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Box URL into checked-dynamic unions beside fetch's Request and Response (#1459)
This commit is contained in:
@@ -619,6 +619,20 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp
|
||||
: lowerer.lowerExpr(argument);
|
||||
stmts.push({ kind: "exprStmt", expr: discarded, loc: discarded.loc });
|
||||
}
|
||||
// A union input holding a URL arm (`string | URL`) converts through the
|
||||
// checked-dynamic input path, which reads the URL's href at the call
|
||||
// like the bare-URL arm below; narrowing it to a string would throw on
|
||||
// the URL arm.
|
||||
const unionWithUrl =
|
||||
input.type.kind === "union" &&
|
||||
(lowerer.unions.get(input.type.unionId)?.arms.some((arm) => arm.kind === "url") ?? false) &&
|
||||
lowerer.dynConvertible(input.type);
|
||||
const dynInput: IrExpr | null =
|
||||
input.type.kind === "dyn"
|
||||
? inputRef
|
||||
: unionWithUrl
|
||||
? { kind: "dynFrom", value: inputRef, type: DYN, loc: locOf(inputNode) }
|
||||
: null;
|
||||
const url: IrExpr =
|
||||
input.type.kind === "url"
|
||||
? {
|
||||
@@ -628,13 +642,13 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp
|
||||
type: STRING,
|
||||
loc: locOf(inputNode),
|
||||
}
|
||||
: input.type.kind === "dyn"
|
||||
? inputRef
|
||||
: dynInput !== null
|
||||
? dynInput
|
||||
: lowerer.coerceInto(inputNode, inputRef, STRING);
|
||||
const answer: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: input.type.kind === "dyn" ? "fetch.input" : "fetch.start",
|
||||
args: [input.type.kind === "dyn" ? inputRef : url, initRef],
|
||||
fn: dynInput !== null ? "fetch.input" : "fetch.start",
|
||||
args: [url, initRef],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -1360,8 +1360,9 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx):
|
||||
if (
|
||||
!symbol ||
|
||||
!["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(symbol.name) ||
|
||||
!symbol.declarations
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))
|
||||
!symbol.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const args = checker.getTypeArguments(type as ts.TypeReference);
|
||||
@@ -2513,12 +2514,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (
|
||||
psym &&
|
||||
WEBCRYPTO_DYN_TYPES.has(psym.name) &&
|
||||
psym.declarations
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
psym.declarations.some(
|
||||
(d) =>
|
||||
(ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
) {
|
||||
return DYN;
|
||||
}
|
||||
@@ -4184,7 +4184,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// semantics would DEGRADE under the collapse — class instances,
|
||||
// Maps/Sets, functions (closure identity, `x === String`
|
||||
// narrowing), promises, regexes, generators, handles — keep their
|
||||
// existing union homes and fences below. A pending recursive
|
||||
// existing union homes and fences below. URL is the
|
||||
// exception: its handle boxes by reference and keeps every member,
|
||||
// so it collapses too (dynSubsumableUnionArm). A pending recursive
|
||||
// placeholder cannot collapse (back-references already hold the
|
||||
// union id); the degenerate recursive spelling keeps its fence.
|
||||
if (
|
||||
@@ -6285,6 +6287,13 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
return true;
|
||||
// URL boxes BY REFERENCE (its checked-dynamic handle keeps identity,
|
||||
// reads and writes its members through the native accessors, answers
|
||||
// `instanceof URL`, and checks back out to the typed value), so it
|
||||
// rides the collapse beside fetch's Request/Response handles:
|
||||
// `string | URL | Request`, fetch's own input type under @types/node.
|
||||
case "url":
|
||||
return true;
|
||||
case "record":
|
||||
case "array":
|
||||
return canConvertToDyn(
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6252-fetch-input-url-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/fetch-input-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// `typeof globalThis.fetch` under @types/node takes `string | URL | Request`:
|
||||
// unions holding URL handles beside fetch handles compile (URL boxes by
|
||||
// reference into the checked-dynamic representation), and a custom fetch
|
||||
// cast to that type through `unknown` keeps working, including an extra
|
||||
// init member written through a record view (the dispatcher pattern).
|
||||
import * as http from "node:http";
|
||||
type FetchImpl = typeof globalThis.fetch;
|
||||
interface DispatchingInit {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
dispatcher?: { name: string };
|
||||
}
|
||||
function customFetch(input: string | URL, init?: DispatchingInit): Promise<Response> {
|
||||
console.log("custom fetch", typeof input === "string" ? "string" : "url", init?.dispatcher?.name ?? "none");
|
||||
return fetch(input, { method: init?.method, headers: init?.headers });
|
||||
}
|
||||
const server = http.createServer((req, res) => res.end(`ok ${req.method} ${req.url}`));
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const address = server.address();
|
||||
const port = address !== null && typeof address === "object" ? address.port : 0;
|
||||
const impl: FetchImpl = customFetch as unknown as FetchImpl;
|
||||
const init: RequestInit = { method: "POST" };
|
||||
(init as Record<string, unknown>).dispatcher = { name: "pinned" };
|
||||
const res = await impl(`http://127.0.0.1:${port}/a`, init);
|
||||
console.log(res.status, await res.text());
|
||||
const viaUrl = await impl(new URL(`http://127.0.0.1:${port}/b`));
|
||||
console.log(viaUrl.status, await viaUrl.text());
|
||||
const direct = await fetch(new URL(`http://127.0.0.1:${port}/c`));
|
||||
console.log(direct.status, await direct.text());
|
||||
server.close();
|
||||
});
|
||||
|
||||
function describeInput(input: string | Request | URL): string {
|
||||
if (typeof input === "string") return `string ${input}`;
|
||||
if (input instanceof URL) return `url ${input.href} ${input.hostname}`;
|
||||
return `request ${input.method} ${input.url}`;
|
||||
}
|
||||
console.log(describeInput("http://a.test/x"));
|
||||
const u = new URL("http://b.test:8080/p?q=1");
|
||||
console.log(describeInput(u));
|
||||
console.log(describeInput(new Request("http://c.test/r", { method: "PUT" })));
|
||||
const items: (string | URL | Request)[] = [u, "s"];
|
||||
console.log(items.length, items[0] === u, String(items[0]));
|
||||
let slot: string | URL | Request = u;
|
||||
if (slot instanceof URL) { slot.pathname = "/changed"; }
|
||||
console.log(u.href);
|
||||
const params: string | Request | URL = new URL("http://d.test/?b=2");
|
||||
console.log(typeof params === "string" ? params : params instanceof URL ? params.searchParams.get("b") : "req");
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// `typeof globalThis.fetch` under @types/node takes `string | URL | Request`:
|
||||
// unions holding URL handles beside fetch handles compile (URL boxes by
|
||||
// reference into the checked-dynamic representation), and a custom fetch
|
||||
// cast to that type through `unknown` keeps working, including an extra
|
||||
// init member written through a record view (the dispatcher pattern).
|
||||
import * as http from "node:http";
|
||||
type FetchImpl = typeof globalThis.fetch;
|
||||
interface DispatchingInit {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
dispatcher?: { name: string };
|
||||
}
|
||||
function customFetch(input: string | URL, init?: DispatchingInit): Promise<Response> {
|
||||
console.log("custom fetch", typeof input === "string" ? "string" : "url", init?.dispatcher?.name ?? "none");
|
||||
return fetch(input, { method: init?.method, headers: init?.headers });
|
||||
}
|
||||
const server = http.createServer((req, res) => res.end(`ok ${req.method} ${req.url}`));
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const address = server.address();
|
||||
const port = address !== null && typeof address === "object" ? address.port : 0;
|
||||
const impl: FetchImpl = customFetch as unknown as FetchImpl;
|
||||
const init: RequestInit = { method: "POST" };
|
||||
(init as Record<string, unknown>).dispatcher = { name: "pinned" };
|
||||
const res = await impl(`http://127.0.0.1:${port}/a`, init);
|
||||
console.log(res.status, await res.text());
|
||||
const viaUrl = await impl(new URL(`http://127.0.0.1:${port}/b`));
|
||||
console.log(viaUrl.status, await viaUrl.text());
|
||||
const direct = await fetch(new URL(`http://127.0.0.1:${port}/c`));
|
||||
console.log(direct.status, await direct.text());
|
||||
server.close();
|
||||
});
|
||||
|
||||
function describeInput(input: string | Request | URL): string {
|
||||
if (typeof input === "string") return `string ${input}`;
|
||||
if (input instanceof URL) return `url ${input.href} ${input.hostname}`;
|
||||
return `request ${input.method} ${input.url}`;
|
||||
}
|
||||
console.log(describeInput("http://a.test/x"));
|
||||
const u = new URL("http://b.test:8080/p?q=1");
|
||||
console.log(describeInput(u));
|
||||
console.log(describeInput(new Request("http://c.test/r", { method: "PUT" })));
|
||||
const items: (string | URL | Request)[] = [u, "s"];
|
||||
console.log(items.length, items[0] === u, String(items[0]));
|
||||
let slot: string | URL | Request = u;
|
||||
if (slot instanceof URL) { slot.pathname = "/changed"; }
|
||||
console.log(u.href);
|
||||
const params: string | Request | URL = new URL("http://d.test/?b=2");
|
||||
console.log(typeof params === "string" ? params : params instanceof URL ? params.searchParams.get("b") : "req");
|
||||
@@ -73,6 +73,27 @@ test("node-types: Headers, Request and Response members typed by undici-types ru
|
||||
expect(comparableStderr(native.stderr)).toBe(node.stderr);
|
||||
});
|
||||
|
||||
test("node-types: fetch's string | URL | Request input and a custom fetch cast to typeof fetch", async () => {
|
||||
const outDir = outDirFor("fetch-input-unions");
|
||||
const entry = join(nodeTypesDir, "fetch-input-unions.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(
|
||||
outDir,
|
||||
process.platform === "win32" ? "fetch-input-unions.exe" : "fetch-input-unions",
|
||||
),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
|
||||
if (!result.ok) return;
|
||||
const [native, node] = await Promise.all([
|
||||
execFileAsync(result.binaryPath),
|
||||
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
|
||||
]);
|
||||
expect(native.stdout).toBe(node.stdout);
|
||||
expect(comparableStderr(native.stderr)).toBe(node.stderr);
|
||||
});
|
||||
|
||||
test("node-types: http header arrays and server.address() follow @types/node's types", async () => {
|
||||
const outDir = outDirFor("http-headers-address");
|
||||
const entry = join(nodeTypesDir, "http-headers-address.ts");
|
||||
|
||||
Reference in New Issue
Block a user