diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 748804057..0b1869a2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,11 @@ jobs: with: deno-version: v1.x + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - name: Install pnpm uses: pnpm/action-setup@v2 with: diff --git a/packages/adapter/adapter-node/package.json b/packages/adapter/adapter-node/package.json index 8bf14019e..389448f9e 100644 --- a/packages/adapter/adapter-node/package.json +++ b/packages/adapter/adapter-node/package.json @@ -8,7 +8,9 @@ ], "exports": { ".": "./dist/index.js", - "./native-fetch": "./dist/native-fetch.js" + "./native-fetch": "./dist/native-fetch.js", + "./request": "./dist/request.js", + "./response": "./dist/response.js" }, "typesVersions": { "*": { diff --git a/packages/adapter/adapter-node/src/common.ts b/packages/adapter/adapter-node/src/common.ts index ff58486d7..0070f4689 100644 --- a/packages/adapter/adapter-node/src/common.ts +++ b/packages/adapter/adapter-node/src/common.ts @@ -1,5 +1,4 @@ import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; -import { once } from "node:events"; import { createServer as createHttpServer, Server as HttpServer, @@ -8,7 +7,8 @@ import { ServerOptions, } from "node:http"; import type { Socket } from "node:net"; -import { Readable } from "node:stream"; +import { NodeRequestAdapterOptions, createRequestAdapter } from "./request"; +import { sendResponse } from "./response"; interface PossiblyEncryptedSocket extends Socket { encrypted?: boolean; @@ -32,27 +32,7 @@ export type NodeMiddleware = ( ) => void; /** Adapter options */ -export interface NodeAdapterOptions { - /** - * Set the origin part of the URL to a constant value. - * It defaults to `process.env.ORIGIN`. If neither is set, - * the origin is computed from the protocol and hostname. - * To determine the protocol, `req.protocol` is tried first. - * If `trustProxy` is set, `X-Forwarded-Proto` header is used. - * Otherwise, `req.socket.encrypted` is used. - * To determine the hostname, `X-Forwarded-Host` - * (if `trustProxy` is set) or `Host` header is used. - */ - origin?: string; - /** - * Whether to trust `X-Forwarded-*` headers. `X-Forwarded-Proto` - * and `X-Forwarded-Host` are used to determine the origin when - * `origin` and `process.env.ORIGIN` are not set. `X-Forwarded-For` - * is used to determine the IP address. The leftmost values are used - * if multiple values are set. Defaults to true if `process.env.TRUST_PROXY` - * is set to `1`, otherwise false. - */ - trustProxy?: boolean; +export interface NodeAdapterOptions extends NodeRequestAdapterOptions { /** * Whether to call the next middleware in the chain even if the request * was handled. @default true @@ -74,78 +54,12 @@ export function createMiddleware( handler: HattipHandler, options: NodeAdapterOptions = {}, ): NodeMiddleware { - const { - origin = process.env.ORIGIN, - trustProxy = process.env.TRUST_PROXY === "1", - alwaysCallNext = true, - } = options; - - let { protocol, host } = origin - ? new URL(origin) - : ({} as Record); + const { alwaysCallNext = true, ...requestOptions } = options; - if (protocol) { - protocol = protocol.slice(0, -1); - } + const requestAdapter = createRequestAdapter(requestOptions); return async (req, res, next) => { - // TODO: Support the newer `Forwarded` standard header - function getForwardedHeader(name: string) { - return (String(req.headers["x-forwarded-" + name]) || "") - .split(",", 1)[0] - .trim(); - } - - protocol = - protocol || - req.protocol || - (trustProxy && getForwardedHeader("proto")) || - (req.socket?.encrypted && "https") || - "http"; - - host = - host || (trustProxy && getForwardedHeader("host")) || req.headers.host; - - if (!host) { - console.warn( - "Could not automatically determine the origin host, using 'localhost'. " + - "Use the 'origin' option or the 'ORIGIN' environment variable to set the origin explicitly.", - ); - host = "localhost"; - } - - const ip = - req.ip || - (trustProxy && getForwardedHeader("for")) || - req.socket?.remoteAddress || - ""; - - let headers = req.headers as any; - if (headers[":method"]) { - headers = Object.fromEntries( - Object.entries(headers).filter(([key]) => !key.startsWith(":")), - ); - } - - const request = new Request(protocol + "://" + host + req.url, { - method: req.method, - headers, - body: - req.method === "GET" || req.method === "HEAD" - ? undefined - : req.socket // Deno has no req.socket and can't convert req to ReadableStream - ? (req as any) - : // Convert to a ReadableStream for Deno - new ReadableStream({ - start(controller) { - req.on("data", (chunk) => controller.enqueue(chunk)); - req.on("end", () => controller.close()); - req.on("error", (err) => controller.error(err)); - }, - }), - // @ts-expect-error: Node requires this for streams - duplex: "half", - }); + const [request, ip] = requestAdapter(req); let passThroughCalled = false; @@ -176,42 +90,14 @@ export function createMiddleware( const response = await handler(context); - if (passThroughCalled) { - next?.(); + if (passThroughCalled && next) { + next(); return; } - const body: Readable | null = - response.body instanceof Readable - ? response.body - : response.body instanceof ReadableStream && - typeof Readable.fromWeb === "function" - ? Readable.fromWeb(response.body as any) - : response.body - ? Readable.from(response.body as any) - : null; - - res.statusCode = response.status; - for (const [key, value] of response.headers) { - if (key === "set-cookie") { - const setCookie = response.headers.getSetCookie(); - res.setHeader("set-cookie", setCookie); - } else { - res.setHeader(key, value); - } - } - - if (body) { - body.pipe(res, { end: true }); - await Promise.race([once(res, "finish"), once(res, "error")]).catch( - () => { - // Ignore errors - }, - ); - } else { - res.setHeader("content-length", "0"); - res.end(); - } + await sendResponse(response, res).catch((error) => { + console.error(error); + }); if (next && alwaysCallNext) { next(); diff --git a/packages/adapter/adapter-node/src/request.ts b/packages/adapter/adapter-node/src/request.ts new file mode 100644 index 000000000..c3a92ac56 --- /dev/null +++ b/packages/adapter/adapter-node/src/request.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import type { IncomingMessage } from "node:http"; +import type { Socket } from "node:net"; + +// @ts-ignore +const deno = typeof Deno !== "undefined"; +// @ts-ignore +const bun = typeof Bun !== "undefined"; + +interface PossiblyEncryptedSocket extends Socket { + encrypted?: boolean; +} + +/** + * `IncomingMessage` possibly augmented with some environment-specific + * properties. + */ +export interface DecoratedRequest extends Omit { + ip?: string; + protocol?: string; + socket?: PossiblyEncryptedSocket; + rawBody?: Buffer | null; +} + +/** Adapter options */ +export interface NodeRequestAdapterOptions { + /** + * Set the origin part of the URL to a constant value. + * It defaults to `process.env.ORIGIN`. If neither is set, + * the origin is computed from the protocol and hostname. + * To determine the protocol, `req.protocol` is tried first. + * If `trustProxy` is set, `X-Forwarded-Proto` header is used. + * Otherwise, `req.socket.encrypted` is used. + * To determine the hostname, `X-Forwarded-Host` + * (if `trustProxy` is set) or `Host` header is used. + */ + origin?: string; + /** + * Whether to trust `X-Forwarded-*` headers. `X-Forwarded-Proto` + * and `X-Forwarded-Host` are used to determine the origin when + * `origin` and `process.env.ORIGIN` are not set. `X-Forwarded-For` + * is used to determine the IP address. The leftmost values are used + * if multiple values are set. Defaults to true if `process.env.TRUST_PROXY` + * is set to `1`, otherwise false. + */ + trustProxy?: boolean; + /** + * Can the Request constructor accept a ReadableStream? + */ + // canUseReadableStream: boolean; +} + +/** Create a function that converts a Node HTTP request into a fetch API `Request` object */ +export function createRequestAdapter( + options: NodeRequestAdapterOptions = {}, +): (req: DecoratedRequest) => [request: Request, ip: string] { + const { + origin = process.env.ORIGIN, + trustProxy = process.env.TRUST_PROXY === "1", + } = options; + + let { protocol, host } = origin + ? new URL(origin) + : ({} as Record); + + if (protocol) { + protocol = protocol.slice(0, -1); + } + + let warned = false; + + return function requestAdapter(req) { + // TODO: Support the newer `Forwarded` standard header + function parseForwardedHeader(name: string) { + return (headers["x-forwarded-" + name] || "").split(",", 1)[0].trim(); + } + + let headers = req.headers as any; + if (headers[":method"]) { + headers = Object.fromEntries( + Object.entries(headers).filter(([key]) => !key.startsWith(":")), + ); + } + + const ip = + req.ip || + (trustProxy && parseForwardedHeader("for")) || + req.socket?.remoteAddress || + ""; + + protocol = + protocol || + req.protocol || + (trustProxy && parseForwardedHeader("proto")) || + (req.socket?.encrypted && "https") || + "http"; + + host = host || (trustProxy && parseForwardedHeader("host")) || headers.host; + + if (!host && !warned) { + console.warn( + "Could not automatically determine the origin host, using 'localhost'. " + + "Use the 'origin' option or the 'ORIGIN' environment variable to set the origin explicitly.", + ); + warned = true; + host = "localhost"; + } + + const request = new Request(protocol + "://" + host + req.url, { + method: req.method, + headers, + body: convertBody(req), + // @ts-expect-error: Node requires this when the body is a ReadableStream + duplex: "half", + }); + + return [request, ip]; + }; +} + +function convertBody(req: DecoratedRequest): BodyInit | null | undefined { + if (req.method === "GET" || req.method === "HEAD") { + return; + } + + // Needed for Google Cloud Functions and some other environments + // that pre-parse the body. + if (req.rawBody !== undefined) { + return req.rawBody; + } + + if (!bun && !deno) { + // Real Node can handle ReadableStream + return req as any; + } + + return new ReadableStream({ + start(controller) { + req.on("data", (chunk) => controller.enqueue(chunk)); + req.on("end", () => controller.close()); + req.on("error", (err) => controller.error(err)); + }, + }); +} diff --git a/packages/adapter/adapter-node/src/response.ts b/packages/adapter/adapter-node/src/response.ts new file mode 100644 index 000000000..b85887ab5 --- /dev/null +++ b/packages/adapter/adapter-node/src/response.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { ServerResponse } from "node:http"; +import { Readable } from "node:stream"; + +// @ts-ignore +const deno = typeof Deno !== "undefined"; + +if (deno) { + // Workaround for https://github.com/denoland/deno/issues/19993 + const oldSet = Headers.prototype.set; + Headers.prototype.set = function set(key: string, value: string | string[]) { + if (Array.isArray(value)) { + this.delete(key); + value.forEach((v) => this.append(key, v)); + } else { + oldSet.call(this, key, value); + } + }; +} + +/** + * Send a fetch API Response into a Node.js HTTP response stream. + */ +export async function sendResponse( + fetchResponse: Response, + nodeResponse: ServerResponse, +): Promise { + const { body: fetchBody } = fetchResponse; + + let body: Readable | null = null; + if (fetchBody instanceof Readable) { + body = fetchBody; + } else if (fetchBody instanceof ReadableStream) { + if (!deno && Readable.fromWeb) { + // Available in Node.js 17+ + body = Readable.fromWeb(fetchBody as any); + } else { + const reader = fetchBody.getReader(); + body = new Readable({ + async read() { + const { done, value } = await reader.read(); + this.push(done ? null : value); + }, + }); + } + } else if (fetchBody) { + Readable.from(fetchBody as any); + } + + nodeResponse.statusCode = fetchResponse.status; + if (fetchResponse.statusText) { + nodeResponse.statusMessage = fetchResponse.statusText; + } + + const uniqueHeaderNames = new Set(fetchResponse.headers.keys()); + + for (const key of uniqueHeaderNames) { + if (key === "set-cookie") { + const setCookie = fetchResponse.headers.getSetCookie(); + if (nodeResponse.appendHeader) { + for (const cookie of setCookie) { + nodeResponse.appendHeader("set-cookie", cookie); + } + } else { + // Workaround for https://github.com/denoland/deno/issues/19993 + nodeResponse.setHeader("set-cookie", setCookie); + } + } else { + nodeResponse.setHeader(key, fetchResponse.headers.get(key)!); + } + } + + if (body) { + body.pipe(nodeResponse, { end: true }); + await new Promise((resolve, reject) => { + nodeResponse.once("error", reject); + nodeResponse.once("finish", resolve); + }); + } else { + nodeResponse.setHeader("content-length", "0"); + nodeResponse.end(); + } +} diff --git a/packages/adapter/adapter-node/tsup.config.ts b/packages/adapter/adapter-node/tsup.config.ts index c5539c47c..ea9b9b753 100644 --- a/packages/adapter/adapter-node/tsup.config.ts +++ b/packages/adapter/adapter-node/tsup.config.ts @@ -2,7 +2,12 @@ import { defineConfig } from "tsup"; export default defineConfig([ { - entry: ["./src/index.ts", "./src/native-fetch.ts"], + entry: [ + "./src/index.ts", + "./src/native-fetch.ts", + "./src/request.ts", + "./src/response.ts", + ], format: ["esm"], platform: "node", target: "node14", diff --git a/packages/base/polyfills/src/get-set-cookie.ts b/packages/base/polyfills/src/get-set-cookie.ts index 9b865c2bd..c0acae678 100644 --- a/packages/base/polyfills/src/get-set-cookie.ts +++ b/packages/base/polyfills/src/get-set-cookie.ts @@ -1,10 +1,7 @@ import type {} from "@hattip/core"; -const SET_COOKIE = Symbol("set-cookie"); - declare global { interface Headers { - [SET_COOKIE]?: string[]; raw?(): Record; getAll?(name: string): string[]; } @@ -16,7 +13,7 @@ export default function install() { } if (typeof globalThis.Headers.prototype.getAll === "function") { - globalThis.Headers.prototype.getSetCookie = function () { + globalThis.Headers.prototype.getSetCookie = function getSetCookie() { return this.getAll!("Set-Cookie"); }; @@ -39,104 +36,8 @@ export default function install() { } globalThis.Headers.prototype.getSetCookie = function getSetCookie() { - return this[SET_COOKIE] || []; - }; - - const originalAppend = globalThis.Headers.prototype.append; - globalThis.Headers.prototype.append = function append( - name: string, - value: string, - ) { - if (name.toLowerCase() === "set-cookie") { - if (!this[SET_COOKIE]) { - this[SET_COOKIE] = []; - } - - this[SET_COOKIE].push(value); - } - - return originalAppend.call(this, name, value); - }; - - const originalDelete = globalThis.Headers.prototype.delete; - globalThis.Headers.prototype.delete = function deleteHeader(name: string) { - if (name.toLowerCase() === "set-cookie") { - this[SET_COOKIE] = []; - } - - return originalDelete.call(this, name); + return [...this] + .filter(([key]) => key.toLowerCase() === "set-cookie") + .map(([, value]) => value); }; - - const originalSet = globalThis.Headers.prototype.set; - globalThis.Headers.prototype.set = function setHeader( - name: string, - value: string, - ) { - if (name.toLowerCase() === "set-cookie") { - this[SET_COOKIE] = [value]; - } - - return originalSet.call(this, name, value); - }; - - const Headers = class MyHeaders extends globalThis.Headers { - constructor(init?: HeadersInit) { - super(init); - if (!init) { - return; - } - if (init instanceof MyHeaders || init instanceof MyHeaders) { - this[SET_COOKIE] = init[SET_COOKIE]; - } else if (Array.isArray(init)) { - this[SET_COOKIE] = init - .filter(([key]) => key.toLowerCase() === "set-cookie") - .map(([, value]) => value); - } else { - this[SET_COOKIE] = []; - for (const [key, value] of Object.entries(init)) { - if (key.toLowerCase() === "set-cookie") { - if (typeof value === "string") { - this[SET_COOKIE]!.push(value); - } else if (Array.isArray(value)) { - this[SET_COOKIE]!.push(...(value as string[])); - } - } - } - } - } - }; - - const Response = class extends globalThis.Response { - constructor(body: any, init?: ResponseInit) { - super(body, init); - - const headers = new Headers(init?.headers); - - Object.defineProperty(this, "headers", { - value: headers, - writable: false, - }); - } - }; - - Object.defineProperty(Response, "name", { - value: "Response", - writable: false, - }); - - const originalFetch = globalThis.fetch; - Object.defineProperty(globalThis, "Headers", { value: Headers }); - Object.defineProperty(globalThis, "Response", { value: Response }); - Object.defineProperty(globalThis, "fetch", { - value: (input: any, init?: any) => - originalFetch(input, init).then((r) => { - Object.setPrototypeOf(r, Response.prototype); - const headers = new Headers(r.headers); - Object.defineProperty(r, "headers", { - value: headers, - writable: false, - }); - return r; - }), - }); } diff --git a/packages/base/polyfills/src/node-fetch.ts b/packages/base/polyfills/src/node-fetch.ts index 872fd164b..196874ca2 100644 --- a/packages/base/polyfills/src/node-fetch.ts +++ b/packages/base/polyfills/src/node-fetch.ts @@ -21,15 +21,9 @@ export default function install() { define("FormData"); define("Headers"); - if (globalThis.Request) { - installHalfDuplexRequest(); - } else { - define("Request"); - } - if (globalThis.Response) return; - // node-fetch doesn't allow constructing a Request from ReadableStream + // node-fetch doesn't allow constructing a Response or a Request from ReadableStream // see: https://github.com/node-fetch/node-fetch/issues/1096 class Response extends nodeFetch.Response { constructor(input: BodyInit, init?: ResponseInit) { @@ -41,12 +35,42 @@ export default function install() { } } + class Request extends nodeFetch.Request { + constructor(input: RequestInfo | URL, init?: RequestInit) { + if (init?.body instanceof ReadableStream) { + const body = Readable.from(init.body as any); + init = new Proxy(init, { + get(target, prop) { + if (prop === "body") { + return body; + } + + return target[prop as keyof RequestInit]; + }, + }); + } + + super(input, init); + } + } + Object.defineProperty(Response, "name", { value: "Response", writable: false, }); + Object.defineProperty(Request, "name", { + value: "Request", + writable: false, + }); + define("Response", Response); + if (globalThis.Request) { + installHalfDuplexRequest(); + } else { + define("Request", Request); + } + define("fetch", (input: any, init: any) => nodeFetch.default(input, init).then((r) => { Object.setPrototypeOf(r, Response.prototype); diff --git a/packages/bundler/bundler-deno/deno-env-shim.js b/packages/bundler/bundler-deno/deno-env-shim.js index 831507dbe..49b41ab7c 100644 --- a/packages/bundler/bundler-deno/deno-env-shim.js +++ b/packages/bundler/bundler-deno/deno-env-shim.js @@ -1,3 +1,29 @@ /* eslint-disable */ -globalThis.process = globalThis.process || {}; -globalThis.process.env = Deno.env.toObject(); +import { Buffer as __nodeBuffer } from "node:buffer"; +import { + clearImmediate as __nodeClearImmediate, + clearInterval as __nodeClearInterval, + clearTimeout as __nodeClearTimeout, + setImmediate as __nodeSetImmediate, + setInterval as __nodeSetInterval, + setTimeout as __nodeSetTimeout, +} from "node:timers"; +import { console as __nodeConsole } from "node:console"; +import { process as __nodeProcess } from "node:process"; +import { performance as __nodePerformance } from "node:perf_hooks"; +import { createRequire as __nodeCreateRequire } from "node:module"; + +Object.assign(globalThis, { + global: globalThis, + Buffer: __nodeBuffer, + clearImmediate: __nodeClearImmediate, + clearInterval: __nodeClearInterval, + clearTimeout: __nodeClearTimeout, + setImmediate: __nodeSetImmediate, + setInterval: __nodeSetInterval, + setTimeout: __nodeSetTimeout, + console: __nodeConsole, + process: __nodeProcess, + performance: __nodePerformance, + require: __nodeCreateRequire(import.meta.url), +}); diff --git a/packages/bundler/bundler-deno/shims/buffer.js b/packages/bundler/bundler-deno/shims/buffer.js new file mode 100644 index 000000000..4ecc06345 --- /dev/null +++ b/packages/bundler/bundler-deno/shims/buffer.js @@ -0,0 +1 @@ +export { Buffer } from "node:buffer"; diff --git a/packages/bundler/bundler-deno/shims/console.js b/packages/bundler/bundler-deno/shims/console.js new file mode 100644 index 000000000..d17bedff0 --- /dev/null +++ b/packages/bundler/bundler-deno/shims/console.js @@ -0,0 +1 @@ +export { default as console } from "node:console"; diff --git a/packages/bundler/bundler-deno/shims/filename.js b/packages/bundler/bundler-deno/shims/filename.js new file mode 100644 index 000000000..133977fd8 --- /dev/null +++ b/packages/bundler/bundler-deno/shims/filename.js @@ -0,0 +1,5 @@ +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +export const __filename = fileURLToPath(import.meta.url); +export const __dirname = dirname(__filename); diff --git a/packages/bundler/bundler-deno/shims/global.js b/packages/bundler/bundler-deno/shims/global.js new file mode 100644 index 000000000..ab31eae0a --- /dev/null +++ b/packages/bundler/bundler-deno/shims/global.js @@ -0,0 +1 @@ +export const global = globalThis; diff --git a/packages/bundler/bundler-deno/shims/performance.js b/packages/bundler/bundler-deno/shims/performance.js new file mode 100644 index 000000000..ac4088b33 --- /dev/null +++ b/packages/bundler/bundler-deno/shims/performance.js @@ -0,0 +1 @@ +export { performance } from "node:perf_hooks"; diff --git a/packages/bundler/bundler-deno/shims/process.js b/packages/bundler/bundler-deno/shims/process.js new file mode 100644 index 000000000..f5761f90d --- /dev/null +++ b/packages/bundler/bundler-deno/shims/process.js @@ -0,0 +1 @@ +export { default as process } from "node:process"; diff --git a/packages/bundler/bundler-deno/shims/timers.js b/packages/bundler/bundler-deno/shims/timers.js new file mode 100644 index 000000000..a564055ca --- /dev/null +++ b/packages/bundler/bundler-deno/shims/timers.js @@ -0,0 +1,8 @@ +export { + clearImmediate, + clearInterval, + clearTimeout, + setImmediate, + setInterval, + setTimeout, +} from "node:timers"; diff --git a/packages/bundler/bundler-deno/src/cli.ts b/packages/bundler/bundler-deno/src/cli.ts index b129c37b8..7281fb59d 100644 --- a/packages/bundler/bundler-deno/src/cli.ts +++ b/packages/bundler/bundler-deno/src/cli.ts @@ -13,13 +13,18 @@ cli "-s, --staticDir ", "Static files directory to copy next to the output", ) + .option( + "-n, --nodeCompat", + "Enable Node.js compatibility (e.g. polyfilling Node.js globals)", + { default: false }, + ) .action( async ( input: string, output: string, - { staticDir }: { staticDir?: string }, + { staticDir, nodeCompat }: { staticDir?: string; nodeCompat?: boolean }, ) => { - await bundler({ input, output, staticDir }); + await bundler({ input, output, staticDir, nodeCompat }); }, ); diff --git a/packages/bundler/bundler-deno/src/index.ts b/packages/bundler/bundler-deno/src/index.ts index c1840c141..c047512f0 100644 --- a/packages/bundler/bundler-deno/src/index.ts +++ b/packages/bundler/bundler-deno/src/index.ts @@ -5,7 +5,7 @@ import cpr from "cpr"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; -const dirname = path.dirname(fileURLToPath(import.meta.url)); +const shimsDir = fileURLToPath(new URL("../shims", import.meta.url)); /** * Bundling options @@ -22,27 +22,78 @@ export interface BundlingOptions { * @default undefined */ staticDir?: string; + /** + * Enable Node.js compatibility (e.g. polyfilling Node.js globals). + */ + nodeCompat?: boolean; } export default async function bundle( options: BundlingOptions, manipulateEsbuildOptions?: (options: BuildOptions) => void | Promise, ) { - const { input, output, staticDir } = options; + const { input, output, staticDir, nodeCompat = false } = options; + const filter = new RegExp(`^(node:)?(${builtinModules.join("|")})$`); const esbuildOptions: BuildOptions = { logLevel: "info", bundle: true, - minify: true, + // minify: true, entryPoints: [input], outfile: output, + inject: nodeCompat + ? [ + "global.js", + "buffer.js", + "console.js", + "filename.js", + "performance.js", + "process.js", + "timers.js", + ].map((file) => path.join(shimsDir, file)) + : [], platform: "node", target: "chrome96", format: "esm", - mainFields: ["module", "main", "browser"], - conditions: ["deno", "worker", "import", "require"], - external: [...builtinModules, "https:*"], - inject: [path.resolve(dirname, "../deno-env-shim.js")], + conditions: ["deno", "import", "module", "require", "default"], + external: ["https:*", "http:*", "node:*"], + plugins: nodeCompat + ? [ + { + name: "node-builtins", + + setup(build) { + build.onResolve({ filter }, async ({ path, kind }) => { + const [, , moduleName] = path.match(filter)!; + + return kind === "require-call" + ? { + path: `${moduleName}`, + namespace: "node:require", + sideEffects: false, + } + : { + path: `node:${moduleName}`, + external: true, + sideEffects: false, + }; + }); + + build.onLoad( + { + namespace: "node:require", + filter: /.*/, + }, + async ({ path }) => { + return { + contents: `import all from "${path}"; module.exports = all;`, + }; + }, + ); + }, + }, + ] + : undefined, }; await manipulateEsbuildOptions?.(esbuildOptions); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d528fc0..08018a54f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1010,6 +1010,9 @@ importers: testbed/basic: dependencies: + '@google-cloud/functions-framework': + specifier: ^3.3.0 + version: 3.3.0 '@graphql-tools/schema': specifier: ^10.0.0 version: 10.0.0(graphql@16.7.1) @@ -2505,6 +2508,23 @@ packages: magic-string: 0.30.1 regexpu-core: 5.3.2 + /@google-cloud/functions-framework@3.3.0: + resolution: {integrity: sha512-+4O1dX5VNRK1W1NyAia7zy5jLf88ytuz39/1kVUUaNiOf76YbMZKV0YjZwfk7uEwRrC6l2wynK1G+q8Gb5DeVw==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + '@types/express': 4.17.17 + body-parser: 1.20.1 + cloudevents: 7.0.2 + express: 4.18.2 + minimist: 1.2.8 + on-finished: 2.4.1 + read-pkg-up: 7.0.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: false + /@graphql-tools/executor@1.1.0(graphql@16.7.1): resolution: {integrity: sha512-+1wmnaUHETSYxiK/ELsT60x584Rw3QKBB7F/7fJ83HKPnLifmE2Dm/K9Eyt6L0Ppekf1jNUbWBpmBGb8P5hAeg==} engines: {node: '>=16.0.0'} @@ -4178,7 +4198,6 @@ packages: /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - dev: true /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -4763,7 +4782,6 @@ packages: optional: true dependencies: ajv: 8.12.0 - dev: true /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -4781,7 +4799,6 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true /all-node-versions@11.3.0: resolution: {integrity: sha512-psMkc5s3qpr+QMfires9bC4azRYciPWql1wqZKMsYRh1731qefQDH2X4+O19xSBX6u0Ra/8Y5diG6y/fEmqKsw==} @@ -5130,7 +5147,6 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: true /avvio@8.2.1: resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} @@ -5219,6 +5235,10 @@ packages: engines: {node: '>=0.6'} dev: true + /bignumber.js@9.1.1: + resolution: {integrity: sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -5696,6 +5716,18 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + /cloudevents@7.0.2: + resolution: {integrity: sha512-WiOqWsNkMZmMMZ6xa3kzx/MA+8+V+c5eGkStZIcik+Px2xCobmzcacw1EOGyfhODaQKkIv8TxXOOLzV69oXFqA==} + engines: {node: '>=16 <=20'} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + json-bigint: 1.0.0 + process: 0.11.10 + util: 0.12.5 + uuid: 8.3.2 + dev: false + /code-point-at@1.1.0: resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} @@ -6420,7 +6452,6 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 - dev: true /error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -7198,7 +7229,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -7482,6 +7512,14 @@ packages: array-back: 3.1.0 dev: true + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -7543,7 +7581,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: true /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -7897,7 +7934,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: true /got@12.6.1: resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} @@ -7998,7 +8034,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 - dev: true /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -8077,6 +8112,10 @@ packages: engines: {node: '>=16.0.0'} dev: false + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: false + /hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -8328,6 +8367,14 @@ packages: kind-of: 6.0.3 dev: true + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -8338,7 +8385,6 @@ packages: /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -8378,7 +8424,6 @@ packages: /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: true /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} @@ -8391,7 +8436,6 @@ packages: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: has: 1.0.3 - dev: true /is-data-descriptor@0.1.4: resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} @@ -8482,6 +8526,13 @@ packages: engines: {node: '>=12'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -8646,7 +8697,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: true /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -8782,13 +8832,18 @@ packages: hasBin: true dev: false + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + dependencies: + bignumber.js: 9.1.1 + dev: false + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -8796,7 +8851,6 @@ packages: /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8955,7 +9009,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true /lint-staged@13.2.3: resolution: {integrity: sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg==} @@ -9059,6 +9112,13 @@ packages: engines: {node: '>=14'} dev: true + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: false + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -9940,6 +10000,15 @@ packages: semver: 7.5.4 dev: true + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.2 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: false + /normalize-package-data@3.0.3: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} engines: {node: '>=10'} @@ -10264,6 +10333,13 @@ packages: engines: {node: '>=4'} dev: true + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -10278,6 +10354,13 @@ packages: yocto-queue: 1.0.0 dev: true + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: false + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -10341,6 +10424,11 @@ packages: engines: {node: '>=14.16'} dev: true + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: false + /p-wait-for@4.1.0: resolution: {integrity: sha512-i8nE5q++9h8oaQHWltS1Tnnv4IoMDOlqN7C0KFG2OdbK0iFJIt6CROZ8wfBM+K4Pxqfnq4C4lkkpXqTEpB5DZw==} engines: {node: '>=12'} @@ -10399,7 +10487,6 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true /parse-ms@3.0.0: resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} @@ -10422,7 +10509,6 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true /path-exists@5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} @@ -10444,7 +10530,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-scurry@1.10.1: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} @@ -10702,7 +10787,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: true /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -10793,7 +10877,6 @@ packages: /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} - dev: true /pupa@3.1.0: resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} @@ -10906,6 +10989,15 @@ packages: loose-envify: 1.4.0 dev: false + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: false + /read-pkg-up@9.1.0: resolution: {integrity: sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10915,6 +11007,16 @@ packages: type-fest: 2.19.0 dev: true + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: false + /read-pkg@7.1.0: resolution: {integrity: sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==} engines: {node: '>=12.20'} @@ -11080,7 +11182,6 @@ packages: /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: true /require-in-the-middle@6.0.0(supports-color@9.4.0): resolution: {integrity: sha512-+dtWQ7l2lqQDxheaG3jjyN1QI37gEwvzACSgjYi4/C2y+ZTUMeRW8BIOm+9NBKvwaMBUSZfPXVOt1skB0vBkRw==} @@ -11135,7 +11236,6 @@ packages: is-core-module: 2.12.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /resolve@2.0.0-next.4: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} @@ -11370,6 +11470,11 @@ packages: semver: 7.5.4 dev: true + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -11645,22 +11750,18 @@ packages: dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.13 - dev: true /spdx-exceptions@2.3.0: resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true /spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.13 - dev: true /spdx-license-ids@3.0.13: resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} - dev: true /split-string@3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} @@ -11984,7 +12085,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /symbol-observable@1.2.0: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} @@ -12430,10 +12530,14 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: false + /type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - dev: true /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} @@ -12667,7 +12771,6 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.0 - dev: true /urix@0.1.0: resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} @@ -12686,6 +12789,16 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.10 + which-typed-array: 1.1.10 + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -12693,7 +12806,6 @@ packages: /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - dev: true /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} @@ -12708,7 +12820,6 @@ packages: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true /validate-npm-package-name@4.0.0: resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} @@ -12960,7 +13071,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: true /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} diff --git a/testbed/basic/bun.ts b/testbed/basic/bun.ts new file mode 100644 index 000000000..4d3984c53 --- /dev/null +++ b/testbed/basic/bun.ts @@ -0,0 +1,35 @@ +declare global { + interface Headers { + __entries?: [string, string][]; + } +} + +// @ts-ignore +if (typeof Bun !== "undefined") { + const originalEntries = Headers.prototype.entries; + + Headers.prototype.append = function append(key: string, value: string) { + this.__entries = this.__entries ?? [...originalEntries.call(this)]; + this.__entries.push([key, value]); + }; + + Headers.prototype.entries = function entries() { + return this.__entries ?? originalEntries.call(this); + }; + + Headers.prototype[Symbol.iterator] = function* () { + yield* this.entries(); + }; +} + +export {}; + +const headers = new Headers({ + "Content-Type": "text/plain; charset=utf-8", +}); + +headers.append("Set-Cookie", "foo=bar"); + +const entries = [...headers]; + +console.log(entries); diff --git a/testbed/basic/ci.test.ts b/testbed/basic/ci.test.ts index f5eb050e5..e345e6995 100644 --- a/testbed/basic/ci.test.ts +++ b/testbed/basic/ci.test.ts @@ -1,5 +1,11 @@ import "./install-polyfills.js"; -import { test, expect, describe, beforeAll, afterAll } from "vitest"; +import { + test as originalTest, + expect, + describe, + beforeAll, + afterAll, +} from "vitest"; import { ChildProcess, spawn } from "node:child_process"; import psTree from "ps-tree"; import { kill } from "node:process"; @@ -16,6 +22,7 @@ let cases: Array<{ skipStreamingTest?: boolean; skipCryptoTest?: boolean; skipStaticFileTest?: boolean; + tryStreamingWithoutCompression?: boolean; }>; const nodeVersions = process.versions.node.split("."); @@ -40,6 +47,8 @@ if (process.env.CI === "true") { ); } + const bunAvailable = process.platform !== "win32"; + const uwsAvailable = false; // nodeVersionMajor >= 18 && process.platform === "linux"; // if (!uwsAvailable) { // console.warn( @@ -63,6 +72,38 @@ if (process.env.CI === "true") { name: "Node with native fetch", command: "node --experimental-fetch entry-node-native-fetch.js", }, + { + name: "Deno", + command: "pnpm build:deno && pnpm start:deno", + requiresForwardedIp: true, + skipStreamingTest: true, + tryStreamingWithoutCompression: true, + }, + { + name: "Deno with std/http", + command: "pnpm build:deno-std && pnpm start:deno", + }, + { + name: "Deno with node:http", + command: "pnpm build:deno-node && pnpm start:deno", + requiresForwardedIp: true, + skipStreamingTest: true, + tryStreamingWithoutCompression: true, + envOverride: { + TRUST_PROXY: "1", + }, + }, + bunAvailable && { + name: "Bun", + command: "bun run entry-bun.js", + skipStreamingTest: true, + }, + bunAvailable && { + name: "Bun with node:http", + command: "bun run entry-node-native-fetch.js", + skipStreamingTest: true, + skipCryptoTest: true, // https://github.com/oven-sh/bun/issues/4070 + }, wranglerAvailable && { name: "Cloudflare Workers", command: "pnpm build:cfw && pnpm start:cfw", @@ -78,10 +119,6 @@ if (process.env.CI === "true") { command: "pnpm build:netlify-edge && pnpm start:netlify", skipStreamingTest: true, }, - { - name: "Deno", - command: "pnpm build:deno && pnpm start:deno", - }, uwsAvailable && { name: "uWebSockets.js", command: "node --no-experimental-fetch entry-uws.js", @@ -90,6 +127,11 @@ if (process.env.CI === "true") { name: "Lagon", command: "lagon dev entry-lagon.js -p public --port 3000", }, + { + name: "Google Cloud Functions", + command: "functions-framework --port=3000", + requiresForwardedIp: true, + }, ].filter(Boolean) as typeof cases; host = "http://127.0.0.1:3000"; } else { @@ -98,9 +140,27 @@ if (process.env.CI === "true") { name: "Existing server", }, ]; - host = process.env.TEST_HOST || "http://localhost:3000"; + host = process.env.TEST_HOST || "http://127.0.0.1:3000"; } +const test = originalTest as typeof originalTest & { + failsIf(condition: any): typeof originalTest.fails | typeof originalTest; +}; + +test.failsIf = function failsIf(condition) { + if (condition) { + function fails(name, fn, options) { + return originalTest.fails(`[EXPECTED FAIL] ${name}`, fn, options); + } + + Object.assign(fails, originalTest); + + return fails as typeof originalTest.fails; + } + + return originalTest; +}; + let cp: ChildProcess | undefined; describe.each(cases)( @@ -114,6 +174,7 @@ describe.each(cases)( requiresForwardedIp, skipCryptoTest, skipStaticFileTest, + tryStreamingWithoutCompression, }) => { beforeAll(async () => { const original = fetch; @@ -249,7 +310,7 @@ describe.each(cases)( ); }); - test.skipIf(skipStaticFileTest)("serves static files", async () => { + test.failsIf(skipStaticFileTest)("serves static files", async () => { const response = await fetch(host + "/static.txt"); const text = await response.text(); @@ -272,13 +333,29 @@ describe.each(cases)( ); }); - test.skipIf(skipStreamingTest)( + test.failsIf(skipStreamingTest)( "doesn't fully buffer binary stream", async () => { const response = await fetch(host + "/bin-stream?delay=1"); let chunks = 0; - for await (const chunk of response.body as AsyncIterable) { + for await (const _chunk of response.body as AsyncIterable) { + chunks++; + } + + expect(chunks).toBeGreaterThan(3); + }, + ); + + test.runIf(tryStreamingWithoutCompression)( + "doesn't fully buffer binary stream with no compression", + async () => { + const response = await fetch(host + "/bin-stream?delay=1", { + headers: { "Accept-Encoding": "" }, + }); + + let chunks = 0; + for await (const _chunk of response.body as AsyncIterable) { chunks++; } @@ -366,7 +443,7 @@ describe.each(cases)( expect(r3).toStrictEqual({ data: { sum: 3 } }); }); - test.skipIf(skipCryptoTest)("session", async () => { + test.failsIf(skipCryptoTest)("session", async () => { const response = await fetch(host + "/session"); const text = await response.text(); expect(text).toEqual("You have visited this page 1 time(s)."); diff --git a/testbed/basic/entry-deno-std.js b/testbed/basic/entry-deno-std.js new file mode 100644 index 000000000..79eff7967 --- /dev/null +++ b/testbed/basic/entry-deno-std.js @@ -0,0 +1,35 @@ +import { createServeHandler } from "@hattip/adapter-deno"; +import hattipHandler from "./index.js"; +import { walk } from "https://deno.land/std/fs/walk.ts"; +import { serve } from "https://deno.land/std/http/server.ts"; +import { serveDir } from "https://deno.land/std/http/file_server.ts"; + +const staticDir = "public"; +const walker = walk(staticDir, { includeDirs: false }); +const staticFiles = new Set(); + +for await (const entry of walker) { + staticFiles.add(entry.path.slice(staticDir.length).replace(/\\/g, "/")); +} + +const handler = createServeHandler(hattipHandler); + +// TODO: Deno.serve doesn't stream when using compression. Track here: https://github.com/denoland/deno/issues/19889 +serve( + async (request, connInfo) => { + const url = new URL(request.url); + const pathname = url.pathname; + + if (staticFiles.has(pathname)) { + return serveDir(request, { fsRoot: staticDir }); + } else if (staticFiles.has(pathname + "/index.html")) { + url.pathname = pathname + "/index.html"; + return serveDir(new Request(url, request), { + fsRoot: staticDir, + }); + } + + return handler(request, connInfo); + }, + { port: 3000 }, +); diff --git a/testbed/basic/entry-deno.js b/testbed/basic/entry-deno.js index 79eff7967..3b25304ef 100644 --- a/testbed/basic/entry-deno.js +++ b/testbed/basic/entry-deno.js @@ -1,7 +1,6 @@ import { createServeHandler } from "@hattip/adapter-deno"; import hattipHandler from "./index.js"; import { walk } from "https://deno.land/std/fs/walk.ts"; -import { serve } from "https://deno.land/std/http/server.ts"; import { serveDir } from "https://deno.land/std/http/file_server.ts"; const staticDir = "public"; @@ -14,22 +13,18 @@ for await (const entry of walker) { const handler = createServeHandler(hattipHandler); -// TODO: Deno.serve doesn't stream when using compression. Track here: https://github.com/denoland/deno/issues/19889 -serve( - async (request, connInfo) => { - const url = new URL(request.url); - const pathname = url.pathname; +Deno.serve({ port: 3000 }, async (request, connInfo) => { + const url = new URL(request.url); + const pathname = url.pathname; - if (staticFiles.has(pathname)) { - return serveDir(request, { fsRoot: staticDir }); - } else if (staticFiles.has(pathname + "/index.html")) { - url.pathname = pathname + "/index.html"; - return serveDir(new Request(url, request), { - fsRoot: staticDir, - }); - } + if (staticFiles.has(pathname)) { + return serveDir(request, { fsRoot: staticDir }); + } else if (staticFiles.has(pathname + "/index.html")) { + url.pathname = pathname + "/index.html"; + return serveDir(new Request(url, request), { + fsRoot: staticDir, + }); + } - return handler(request, connInfo); - }, - { port: 3000 }, -); + return handler(request, connInfo); +}); diff --git a/testbed/basic/entry-gcf.js b/testbed/basic/entry-gcf.js new file mode 100644 index 000000000..00cf6fffb --- /dev/null +++ b/testbed/basic/entry-gcf.js @@ -0,0 +1,12 @@ +// @ts-check +import connect from "connect"; +import { createMiddleware } from "@hattip/adapter-node"; +import handler from "./index.js"; +import sirv from "sirv"; + +const app = connect(); + +app.use(sirv("public")); +app.use(createMiddleware(handler)); + +export { app as function }; diff --git a/testbed/basic/fastly/static-publish.rc.js b/testbed/basic/fastly/static-publish.rc.js index 8680afdb8..a05c06769 100644 --- a/testbed/basic/fastly/static-publish.rc.js +++ b/testbed/basic/fastly/static-publish.rc.js @@ -8,26 +8,26 @@ /** @type {import('@fastly/compute-js-static-publish').StaticPublisherConfig} */ const config = { - rootDir: "../public", - // kvStoreName: false, - // excludeDirs: [ './node_modules' ], - // excludeDotFiles: true, - // includeWellKnown: true, - // contentAssetInclusionTest: (filename) => true, - // contentCompression: [ 'br', 'gzip' ], // For this config value, default is [] if kvStoreName is null. - // moduleAssetInclusionTest: (filename) => false, - // contentTypes: [ - // { test: /.custom$/, contentType: 'application/x-custom', text: false }, - // ], - server: { - publicDirPrefix: "", - staticItems: [], - // compression: [ 'br', 'gzip' ], - spaFile: false, - notFoundPageFile: false, - autoExt: [], - autoIndex: ["index.html","index.htm"], - }, + rootDir: "../public", + // kvStoreName: false, + // excludeDirs: [ './node_modules' ], + // excludeDotFiles: true, + // includeWellKnown: true, + // contentAssetInclusionTest: (filename) => true, + // contentCompression: [ 'br', 'gzip' ], // For this config value, default is [] if kvStoreName is null. + // moduleAssetInclusionTest: (filename) => false, + // contentTypes: [ + // { test: /.custom$/, contentType: 'application/x-custom', text: false }, + // ], + server: { + publicDirPrefix: "", + staticItems: [], + // compression: [ 'br', 'gzip' ], + spaFile: false, + notFoundPageFile: false, + autoExt: [], + autoIndex: ["index.html", "index.htm"], + }, }; export default config; diff --git a/testbed/basic/package.json b/testbed/basic/package.json index 13dda7422..e076e2ce8 100644 --- a/testbed/basic/package.json +++ b/testbed/basic/package.json @@ -14,10 +14,13 @@ "build:vercel": "hattip-vercel -c --staticDir public --serverless entry-vercel-serverless.js", "build:vercel-edge": "hattip-vercel -c --staticDir public --edge entry-vercel-edge.js", "build:deno": "hattip-deno entry-deno.js dist/deno/index.js --staticDir public", + "build:deno-std": "hattip-deno entry-deno-std.js dist/deno/index.js --staticDir public", + "build:deno-node": "hattip-deno entry-node-native-fetch.js dist/deno/index.js --staticDir public --nodeCompat", "start:deno": "deno run --allow-read --allow-net --allow-env dist/deno/index.js", "start:fastly": "cd fastly && fastly compute serve --addr=\"127.0.0.1:3000\"", "ci": "vitest --reporter=verbose --no-threads" }, + "main": "entry-gcf.js", "devDependencies": { "@fastly/compute-js-static-publish": "^5.1.1", "@fastly/js-compute": "^3.1.1", @@ -54,6 +57,7 @@ "wrangler": "^3.3.0" }, "dependencies": { + "@google-cloud/functions-framework": "^3.3.0", "@graphql-tools/schema": "^10.0.0", "@hattip/adapter-node": "workspace:*", "@hattip/compose": "workspace:*",