Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6380,7 +6380,9 @@ function lowerOptionalStringNumber(
// with the original receiver rather than a materialized property view.
const callbackProperty = (() => {
const prop = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
return prop !== undefined && lowerer.checker.declarationsOf(prop).some((decl) =>
// Library declarations spell native methods as readonly function
// properties (undici-types `Headers.get`); those still dispatch by name.
return prop !== undefined && !lowerer.isStdlibSymbol(prop) && lowerer.checker.declarationsOf(prop).some((decl) =>
ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl));
})();
const dispatched = callbackProperty ? null : lowerDynDispatchMethodCall(lowerer, call, access, recv, arrayReceiver);
Expand Down
25 changes: 25 additions & 0 deletions tests/fixtures/node-types/fetch-headers.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// undici-types declares Headers members as readonly function properties
// (`readonly get: (name: string) => string | null`). A fetched Response
// is a native handle that answers those names as methods, so each call
// must dispatch by name instead of reading the member and calling it.
const base = process.argv[2];

const res = await fetch(`${base}/headers`);
console.log(
"direct:",
res.headers.get("x-kind"),
res.headers.get("missing"),
res.headers.has("x-kind"),
res.headers.has("missing"),
);

const headers: Headers = res.headers;
console.log("typed local:", headers.get("x-kind"), headers.has("content-type"));

const contentType = (source: Headers): string | null => source.get("content-type");
console.log("parameter:", contentType(res.headers));

const request = async (path: string): Promise<Response> => fetch(`${base}${path}`);
const viaHelper = await request("/headers");
console.log("helper:", viaHelper.headers.get("x-kind"), await viaHelper.text());
await res.text();
31 changes: 31 additions & 0 deletions tests/harness/project-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/
import { execFile } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
Expand Down Expand Up @@ -187,6 +188,36 @@ test("node-types: fetch AbortSignal and readable bodies lower statically", async
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
});

test("node-types: fetched Response headers dispatch their undici-types members", async () => {
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "text/plain", "x-kind": "greeting" });
response.end("hello");
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address();
if (address === null || typeof address === "string") throw new Error("missing server address");
const base = `http://127.0.0.1:${address.port}`;
const entry = join(nodeTypesDir, "fetch-headers.mts");
const outDir = outDirFor("node-fetch-headers");
const result = await compile(entry, {
outPath: join(outDir, "fetch-headers"),
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, [base]),
execFileAsync(process.execPath, [entry, base]),
]);
expect(native.stdout).toBe(node.stdout);
expect(native.stderr).toBe(node.stderr);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});

test("node-types: imported TypeScript sources can use the RequestInfo global", async () => {
const entry = join(nodeTypesDir, "source-import/main.ts");
expect(analyze(entry).coverage.preflightFailed).toBe(false);
Expand Down