chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# @cloudflare/runtime-types
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1b965c5`](https://github.com/cloudflare/workers-sdk/commit/1b965c51babff16ae7657335d93badebd50c310f)]:
|
||||
- miniflare@4.20260709.0
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@cloudflare/runtime-types",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/runtime-types#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cloudflare/workers-sdk/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cloudflare/workers-sdk.git",
|
||||
"directory": "packages/runtime-types"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"check:type": "tsc",
|
||||
"dev": "tsdown --watch",
|
||||
"test:ci": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"miniflare": "workspace:*",
|
||||
"workerd": "catalog:default"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-tsconfig": "workspace:*",
|
||||
"@cloudflare/workers-utils": "workspace:*",
|
||||
"@types/node": "catalog:default",
|
||||
"tsdown": "0.16.3",
|
||||
"typescript": "catalog:default",
|
||||
"vitest": "catalog:default"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { getRuntimeHeader, RUNTIME_HEADER_COMMENT_PREFIX } from "../header";
|
||||
|
||||
describe("getRuntimeHeader", () => {
|
||||
it("includes the workerd version and compatibility date", ({ expect }) => {
|
||||
expect(getRuntimeHeader("1.0.0-test", "2024-11-06")).toBe(
|
||||
`${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 `
|
||||
);
|
||||
});
|
||||
|
||||
it("sorts compatibility flags alphabetically", ({ expect }) => {
|
||||
expect(getRuntimeHeader("1.0.0-test", "2024-11-06", ["b", "a", "c"])).toBe(
|
||||
`${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 a,b,c`
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to no flags when none are provided", ({ expect }) => {
|
||||
expect(getRuntimeHeader("1.0.0-test", "2024-11-06", [])).toBe(
|
||||
`${RUNTIME_HEADER_COMMENT_PREFIX}1.0.0-test 2024-11-06 `
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers";
|
||||
import { beforeEach, describe, it, vi } from "vitest";
|
||||
import { getRuntimeHeader, RUNTIME_TYPES_MARKER } from "../header";
|
||||
import { generateRuntimeTypes } from "../runtime";
|
||||
|
||||
const OUT_FILE = "worker-configuration.d.ts";
|
||||
|
||||
const WORKERD_VERSION = "1.0.0-test";
|
||||
|
||||
const { MiniflareMock, dispatchFetchMock, disposeMock } = vi.hoisted(() => {
|
||||
const dispatch = vi.fn();
|
||||
const dispose = vi.fn();
|
||||
const constructor = vi.fn(function () {
|
||||
return {
|
||||
dispatchFetch: dispatch,
|
||||
dispose: dispose,
|
||||
};
|
||||
});
|
||||
return {
|
||||
MiniflareMock: constructor,
|
||||
dispatchFetchMock: dispatch,
|
||||
disposeMock: dispose,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("workerd", () => ({
|
||||
// Must be a literal: vi.mock factories are hoisted above top-level variables.
|
||||
version: "1.0.0-test",
|
||||
default: "/fake/workerd",
|
||||
}));
|
||||
vi.mock("miniflare", () => ({ Miniflare: MiniflareMock }));
|
||||
|
||||
describe("generateRuntimeTypes", () => {
|
||||
runInTempDir();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
dispatchFetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
text: async () => "GENERATED",
|
||||
});
|
||||
});
|
||||
|
||||
it("generates types when the out file does not exist (cache miss)", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const result = await generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
outFile: OUT_FILE,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
runtimeHeader: getRuntimeHeader(WORKERD_VERSION, "2024-11-06", []),
|
||||
runtimeTypes: "GENERATED",
|
||||
isCached: false,
|
||||
});
|
||||
expect(MiniflareMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("strips nodejs_compat flags from the dispatch URL but keeps them in the header", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const result = await generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
compatibilityFlags: ["nodejs_compat", "flag_b", "flag_a"],
|
||||
outFile: OUT_FILE,
|
||||
});
|
||||
|
||||
// nodejs_compat flags are stripped from the dispatch URL; the remaining
|
||||
// flags keep their original caller-provided order.
|
||||
expect(dispatchFetchMock).toHaveBeenCalledWith(
|
||||
"http://dummy.com/2024-11-06+flag_b+flag_a"
|
||||
);
|
||||
expect(result.runtimeHeader).toBe(
|
||||
getRuntimeHeader(WORKERD_VERSION, "2024-11-06", [
|
||||
"nodejs_compat",
|
||||
"flag_b",
|
||||
"flag_a",
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("returns cached types when the header and marker match", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const header = getRuntimeHeader(WORKERD_VERSION, "2024-11-06", ["flag_a"]);
|
||||
await seed({
|
||||
[OUT_FILE]: `${header}\nsome preamble\n${RUNTIME_TYPES_MARKER}\nCACHED TYPES\nmore`,
|
||||
});
|
||||
|
||||
const result = await generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
compatibilityFlags: ["flag_a"],
|
||||
outFile: OUT_FILE,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
runtimeHeader: header,
|
||||
runtimeTypes: "CACHED TYPES\nmore",
|
||||
isCached: true,
|
||||
});
|
||||
expect(MiniflareMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("regenerates when the cached header is stale", async ({ expect }) => {
|
||||
const staleHeader = getRuntimeHeader(WORKERD_VERSION, "2020-01-01", []);
|
||||
await seed({
|
||||
[OUT_FILE]: `${staleHeader}\n${RUNTIME_TYPES_MARKER}\nOLD`,
|
||||
});
|
||||
|
||||
const result = await generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
outFile: OUT_FILE,
|
||||
});
|
||||
|
||||
expect(result.isCached).toBe(false);
|
||||
expect(result.runtimeTypes).toBe("GENERATED");
|
||||
expect(MiniflareMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("regenerates when the marker is missing even if the header matches", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const header = getRuntimeHeader(WORKERD_VERSION, "2024-11-06", []);
|
||||
await seed({ [OUT_FILE]: `${header}\nNO MARKER HERE` });
|
||||
|
||||
const result = await generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
outFile: OUT_FILE,
|
||||
});
|
||||
|
||||
expect(result.isCached).toBe(false);
|
||||
expect(MiniflareMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects and still disposes Miniflare when the response is not ok", async ({
|
||||
expect,
|
||||
}) => {
|
||||
dispatchFetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
text: async () => "boom",
|
||||
});
|
||||
|
||||
await expect(
|
||||
generateRuntimeTypes({
|
||||
compatibilityDate: "2024-11-06",
|
||||
outFile: OUT_FILE,
|
||||
})
|
||||
).rejects.toThrow("boom");
|
||||
expect(disposeMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rethrows non-ENOENT read errors without generating", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// Passing a directory as the out file triggers EISDIR on read.
|
||||
await expect(
|
||||
generateRuntimeTypes({ compatibilityDate: "2024-11-06", outFile: "." })
|
||||
).rejects.toThrow();
|
||||
expect(MiniflareMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Prefix of the comment written above the generated runtime types. Used to
|
||||
* detect when runtime types need to be regenerated (the full header encodes the
|
||||
* workerd version, compatibility date and flags).
|
||||
*/
|
||||
export const RUNTIME_HEADER_COMMENT_PREFIX =
|
||||
"// Runtime types generated with workerd@";
|
||||
|
||||
/**
|
||||
* Marker line written immediately before the generated runtime types. Used to
|
||||
* locate the start of the runtime section within a combined `.d.ts` file.
|
||||
*/
|
||||
export const RUNTIME_TYPES_MARKER = "// Begin runtime types";
|
||||
|
||||
/**
|
||||
* Generates the runtime header string used in the generated types file.
|
||||
* This header is used to detect when runtime types need to be regenerated.
|
||||
*/
|
||||
export function getRuntimeHeader(
|
||||
workerdVersion: string,
|
||||
compatibilityDate: string,
|
||||
compatibilityFlags: string[] = []
|
||||
): string {
|
||||
return `${RUNTIME_HEADER_COMMENT_PREFIX}${workerdVersion} ${compatibilityDate} ${[...compatibilityFlags].sort().join(",")}`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getRuntimeHeader,
|
||||
RUNTIME_HEADER_COMMENT_PREFIX,
|
||||
RUNTIME_TYPES_MARKER,
|
||||
} from "./header";
|
||||
export { generateRuntimeTypes } from "./runtime";
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as fsp from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { version } from "workerd";
|
||||
import {
|
||||
getRuntimeHeader,
|
||||
RUNTIME_HEADER_COMMENT_PREFIX,
|
||||
RUNTIME_TYPES_MARKER,
|
||||
} from "./header";
|
||||
|
||||
// `require.resolve` is not available in native ESM, so reconstruct it from the
|
||||
// current module URL. This resolves correctly both when this package is run
|
||||
// directly (ESM) and when it is bundled into a consumer.
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Generates runtime types for a Workers project based on the provided compatibility settings.
|
||||
*
|
||||
* This function is designed to be isolated and portable, making it easy to integrate into various
|
||||
* build processes or development workflows. It handles the whole process of generating runtime
|
||||
* types, from spawning the workerd process (via Miniflare) to returning the generated types.
|
||||
*
|
||||
* If an `outFile` already contains runtime types generated for the same workerd version,
|
||||
* compatibility date and flags, the cached types are returned instead of regenerating them.
|
||||
*
|
||||
* @example
|
||||
* import { generateRuntimeTypes } from "@cloudflare/runtime-types";
|
||||
*
|
||||
* const { runtimeHeader, runtimeTypes, isCached } = await generateRuntimeTypes({
|
||||
* compatibilityDate: "2024-11-06",
|
||||
* compatibilityFlags: ["nodejs_compat"],
|
||||
* outFile: "worker-configuration.d.ts",
|
||||
* });
|
||||
*
|
||||
* @remarks
|
||||
* `nodejs_compat` flags are ignored as there is currently no mechanism to generate these
|
||||
* dynamically; consumers should instead prompt users to install `@types/node`.
|
||||
*/
|
||||
export async function generateRuntimeTypes({
|
||||
compatibilityDate,
|
||||
compatibilityFlags = [],
|
||||
outFile,
|
||||
}: {
|
||||
compatibilityDate: string;
|
||||
compatibilityFlags?: string[];
|
||||
outFile: string;
|
||||
}): Promise<{
|
||||
runtimeHeader: string;
|
||||
runtimeTypes: string;
|
||||
isCached: boolean;
|
||||
}> {
|
||||
const header = getRuntimeHeader(
|
||||
version,
|
||||
compatibilityDate,
|
||||
compatibilityFlags
|
||||
);
|
||||
|
||||
try {
|
||||
const lines = (await fsp.readFile(outFile, "utf8")).split("\n");
|
||||
const existingHeader = lines.find((line) =>
|
||||
line.startsWith(RUNTIME_HEADER_COMMENT_PREFIX)
|
||||
);
|
||||
const existingTypesStart = lines.findIndex(
|
||||
(line) => line === RUNTIME_TYPES_MARKER
|
||||
);
|
||||
if (existingHeader === header && existingTypesStart !== -1) {
|
||||
return {
|
||||
runtimeHeader: header,
|
||||
runtimeTypes: lines.slice(existingTypesStart + 1).join("\n"),
|
||||
isCached: true,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e as { code: string }).code !== "ENOENT") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const types = await generate({
|
||||
compatibilityDate,
|
||||
// Ignore nodejs compat flags as there is currently no mechanism to generate these dynamically.
|
||||
compatibilityFlags: compatibilityFlags.filter(
|
||||
(flag) => !flag.includes("nodejs_compat")
|
||||
),
|
||||
});
|
||||
|
||||
return { runtimeHeader: header, runtimeTypes: types, isCached: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates runtime types for Cloudflare Workers by spawning a workerd process with the type-generation
|
||||
* worker, and then making a request to that worker to fetch types.
|
||||
*/
|
||||
async function generate({
|
||||
compatibilityDate,
|
||||
compatibilityFlags = [],
|
||||
}: {
|
||||
compatibilityDate: string;
|
||||
compatibilityFlags?: string[];
|
||||
}) {
|
||||
const worker = (
|
||||
await fsp.readFile(require.resolve("workerd/worker.mjs"))
|
||||
).toString();
|
||||
const mf = new Miniflare({
|
||||
// Must stay before the 2024-09-23 nodejs_compat v1->v2 switchover: the
|
||||
// workerd RTTI worker only runs under nodejs_compat v1. This date is
|
||||
// internal to type generation and never appears in the output/header.
|
||||
compatibilityDate: "2024-01-01",
|
||||
compatibilityFlags: ["nodejs_compat", "rtti_api"],
|
||||
modules: true,
|
||||
script: worker,
|
||||
});
|
||||
|
||||
const flagsString = compatibilityFlags.length
|
||||
? `+${compatibilityFlags.join("+")}`
|
||||
: "";
|
||||
|
||||
const path = `http://dummy.com/${compatibilityDate}${flagsString}`;
|
||||
|
||||
try {
|
||||
const res = await mf.dispatchFetch(path);
|
||||
const text = await res.text();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
} finally {
|
||||
await mf.dispose();
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
declare module "workerd" {
|
||||
const path: string;
|
||||
export default path;
|
||||
export const compatibilityDate: string;
|
||||
export const version: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@cloudflare/workers-tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
},
|
||||
platform: "node",
|
||||
outDir: "dist",
|
||||
dts: true,
|
||||
tsconfig: "tsconfig.json",
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://turbo.build/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["**/__tests__/**/*.test.ts"],
|
||||
reporters: ["default"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user