wrangler
Advanced tools
#!/usr/bin/env node | ||
const { spawn } = require("child_process"); | ||
const { join } = require("path"); | ||
const { spawn } = require("node:child_process"); | ||
const { join } = require("node:path"); | ||
const semiver = require("semiver"); | ||
@@ -5,0 +5,0 @@ |
{ | ||
"name": "wrangler", | ||
"version": "0.0.0-cf88928", | ||
"version": "0.0.0-cf91cc1", | ||
"author": "wrangler@cloudflare.com", | ||
@@ -39,6 +39,7 @@ "description": "Command-line interface for all things Cloudflare Workers", | ||
"dependencies": { | ||
"esbuild": "0.14.1", | ||
"miniflare": "2.0.0-rc.4", | ||
"esbuild": "0.14.14", | ||
"miniflare": "2.3.0", | ||
"path-to-regexp": "^6.2.0", | ||
"semiver": "^1.1.0" | ||
"semiver": "^1.1.0", | ||
"xxhash-wasm": "^1.0.1" | ||
}, | ||
@@ -49,22 +50,35 @@ "optionalDependencies": { | ||
"devDependencies": { | ||
"@babel/types": "^7.16.0", | ||
"@iarna/toml": "^2.2.5", | ||
"@sentry/cli": "^1.71.0", | ||
"@sentry/integrations": "^6.17.6", | ||
"@sentry/node": "^6.17.6", | ||
"@types/command-exists": "^1.2.0", | ||
"@types/estree": "^0.0.50", | ||
"@types/mime": "^2.0.3", | ||
"@types/prompts": "^2.0.14", | ||
"@types/react": "^17.0.37", | ||
"@types/serve-static": "^1.13.10", | ||
"@types/signal-exit": "^3.0.1", | ||
"@types/ws": "^8.2.1", | ||
"@types/yargs": "^17.0.7", | ||
"acorn": "^8.6.0", | ||
"chokidar": "^3.5.2", | ||
"clipboardy": "^3.0.0", | ||
"cmd-shim": "^4.1.0", | ||
"command-exists": "^1.2.9", | ||
"devtools-protocol": "^0.0.955664", | ||
"execa": "^6.0.0", | ||
"finalhandler": "^1.1.2", | ||
"find-up": "^6.2.0", | ||
"formdata-node": "^4.3.1", | ||
"http-proxy": "^1.18.1", | ||
"ignore": "^5.2.0", | ||
"ink": "^3.2.0", | ||
"ink-select-input": "^4.2.1", | ||
"ink-table": "^3.0.0", | ||
"ink-testing-library": "^2.1.0", | ||
"ink-text-input": "^4.0.2", | ||
"node-fetch": "^3.1.0", | ||
"jest-fetch-mock": "^3.0.3", | ||
"jest-websocket-mock": "^2.3.0", | ||
"mime": "^3.0.0", | ||
"open": "^8.4.0", | ||
"prompts": "^2.4.2", | ||
"react": "^17.0.2", | ||
@@ -75,3 +89,3 @@ "react-error-boundary": "^3.1.4", | ||
"tmp-promise": "^3.0.3", | ||
"undici": "^4.11.1", | ||
"undici": "4.13.0", | ||
"ws": "^8.3.0", | ||
@@ -83,6 +97,6 @@ "yargs": "^17.3.0" | ||
"bin", | ||
"lib", | ||
"pages", | ||
"miniflare-config-stubs", | ||
"wrangler-dist", | ||
"static-asset-facade.js", | ||
"templates", | ||
"vendor", | ||
@@ -93,6 +107,9 @@ "import_meta_url.js" | ||
"clean": "rm -rf wrangler-dist", | ||
"check:type": "tsc", | ||
"bundle": "node -r esbuild-register scripts/bundle.ts", | ||
"build": "npm run clean && npm run bundle", | ||
"prepublishOnly": "npm run build", | ||
"start": "npm run bundle && NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js", | ||
"test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true" | ||
"test": "jest --silent=false --verbose=true", | ||
"test-watch": "npm run test -- --runInBand --testTimeout=50000 --watch" | ||
}, | ||
@@ -104,8 +121,9 @@ "engines": { | ||
"restoreMocks": true, | ||
"testTimeout": 30000, | ||
"testRegex": ".*.(test|spec)\\.[jt]sx?$", | ||
"transformIgnorePatterns": [ | ||
"node_modules/(?!node-fetch|fetch-blob|find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|data-uri-to-buffer|formdata-polyfill|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream)" | ||
"node_modules/(?!find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream)" | ||
], | ||
"moduleNameMapper": { | ||
"clipboardy": "<rootDir>/src/__tests__/clipboardy-mock.js" | ||
"clipboardy": "<rootDir>/src/__tests__/helpers/clipboardy-mock.js" | ||
}, | ||
@@ -124,2 +142,2 @@ "transform": { | ||
} | ||
} | ||
} |
import * as fs from "node:fs"; | ||
import * as fsp from "node:fs/promises"; | ||
import * as os from "node:os"; | ||
import * as path from "node:path"; | ||
import * as TOML from "@iarna/toml"; | ||
import { main } from "../index"; | ||
import { setMock, unsetAllMocks } from "./mock-cfetch"; | ||
import { mockConfirm } from "./mock-dialogs"; | ||
import { version as wranglerVersion } from "../../package.json"; | ||
import { getPackageManager } from "../package-manager"; | ||
import { mockConsoleMethods } from "./helpers/mock-console"; | ||
import { mockConfirm } from "./helpers/mock-dialogs"; | ||
import { runInTempDir } from "./helpers/run-in-tmp"; | ||
import { runWrangler } from "./helpers/run-wrangler"; | ||
import type { PackageManager } from "../package-manager"; | ||
jest.mock("../cfetch", () => jest.requireActual("./mock-cfetch")); | ||
describe("wrangler", () => { | ||
let mockPackageManager: PackageManager; | ||
runInTempDir(); | ||
async function w(cmd?: string) { | ||
const logSpy = jest.spyOn(console, "log").mockImplementation(); | ||
const errorSpy = jest.spyOn(console, "error").mockImplementation(); | ||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(); | ||
try { | ||
await main(cmd?.split(" ") ?? []); | ||
return { | ||
stdout: logSpy.mock.calls.flat(2).join("\n"), | ||
stderr: errorSpy.mock.calls.flat(2).join("\n"), | ||
warnings: warnSpy.mock.calls.flat(2).join("\n"), | ||
beforeEach(() => { | ||
mockPackageManager = { | ||
addDevDeps: jest.fn(), | ||
install: jest.fn(), | ||
}; | ||
} finally { | ||
logSpy.mockRestore(); | ||
errorSpy.mockRestore(); | ||
warnSpy.mockRestore(); | ||
} | ||
} | ||
(getPackageManager as jest.Mock).mockResolvedValue(mockPackageManager); | ||
}); | ||
describe("wrangler", () => { | ||
const std = mockConsoleMethods(); | ||
describe("no command", () => { | ||
it("should display a list of available commands", async () => { | ||
const { stdout, stderr } = await w(); | ||
await runWrangler(); | ||
expect(stdout).toMatchInlineSnapshot(` | ||
expect(std.out).toMatchInlineSnapshot(` | ||
"wrangler | ||
@@ -40,5 +35,6 @@ | ||
wrangler init [name] 📥 Create a wrangler.toml configuration file | ||
wrangler dev <filename> 👂 Start a local server for developing your worker | ||
wrangler whoami 🕵️ Retrieve your user info and test your auth config | ||
wrangler dev [script] 👂 Start a local server for developing your worker | ||
wrangler publish [script] 🆙 Publish your Worker to Cloudflare. | ||
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker. | ||
wrangler tail [name] 🦚 Starts a log tailing session for a published Worker. | ||
wrangler secret 🤫 Generate a secret that can be referenced in the worker script | ||
@@ -49,2 +45,3 @@ wrangler kv:namespace 🗂️ Interact with your Workers KV Namespaces | ||
wrangler pages ⚡️ Configure Cloudflare Pages | ||
wrangler r2 📦 Interact with an R2 store | ||
@@ -60,3 +57,3 @@ Flags: | ||
expect(stderr).toMatchInlineSnapshot(`""`); | ||
expect(std.err).toMatchInlineSnapshot(`""`); | ||
}); | ||
@@ -67,5 +64,10 @@ }); | ||
it("should display an error", async () => { | ||
const { stdout, stderr } = await w("invalid-command"); | ||
await expect( | ||
runWrangler("invalid-command") | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Unknown command: invalid-command."` | ||
); | ||
expect(stdout).toMatchInlineSnapshot(` | ||
expect(std.out).toMatchInlineSnapshot(`""`); | ||
expect(std.err).toMatchInlineSnapshot(` | ||
"wrangler | ||
@@ -75,5 +77,6 @@ | ||
wrangler init [name] 📥 Create a wrangler.toml configuration file | ||
wrangler dev <filename> 👂 Start a local server for developing your worker | ||
wrangler whoami 🕵️ Retrieve your user info and test your auth config | ||
wrangler dev [script] 👂 Start a local server for developing your worker | ||
wrangler publish [script] 🆙 Publish your Worker to Cloudflare. | ||
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker. | ||
wrangler tail [name] 🦚 Starts a log tailing session for a published Worker. | ||
wrangler secret 🤫 Generate a secret that can be referenced in the worker script | ||
@@ -84,2 +87,3 @@ wrangler kv:namespace 🗂️ Interact with your Workers KV Namespaces | ||
wrangler pages ⚡️ Configure Cloudflare Pages | ||
wrangler r2 📦 Interact with an R2 store | ||
@@ -92,7 +96,4 @@ Flags: | ||
Options: | ||
-l, --local Run on my machine [boolean] [default: false]" | ||
`); | ||
-l, --local Run on my machine [boolean] [default: false] | ||
expect(stderr).toMatchInlineSnapshot(` | ||
" | ||
Unknown command: invalid-command." | ||
@@ -104,16 +105,16 @@ `); | ||
describe("init", () => { | ||
const ogcwd = process.cwd(); | ||
let tmpDir: string; | ||
beforeEach(async () => { | ||
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "init-")); | ||
process.chdir(tmpDir); | ||
it("should create a wrangler.toml", async () => { | ||
mockConfirm({ | ||
text: "No package.json found. Would you like to create one?", | ||
result: false, | ||
}); | ||
await runWrangler("init"); | ||
const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); | ||
expect(typeof parsed.compatibility_date).toBe("string"); | ||
expect(parsed.name).toContain("wrangler-tests"); | ||
expect(fs.existsSync("./package.json")).toBe(false); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(false); | ||
}); | ||
afterEach(async () => { | ||
process.chdir(ogcwd); | ||
await fsp.rm(tmpDir, { recursive: true }); | ||
}); | ||
it("should create a wrangler.toml", async () => { | ||
it("should create a named Worker wrangler.toml", async () => { | ||
mockConfirm({ | ||
@@ -123,5 +124,6 @@ text: "No package.json found. Would you like to create one?", | ||
}); | ||
await w("init"); | ||
await runWrangler("init my-worker"); | ||
const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); | ||
expect(typeof parsed.compatibility_date).toBe("string"); | ||
expect(parsed.name).toBe("my-worker"); | ||
expect(fs.existsSync("./package.json")).toBe(false); | ||
@@ -132,3 +134,7 @@ expect(fs.existsSync("./tsconfig.json")).toBe(false); | ||
it("should display warning when wrangler.toml already exists, and exit if user does not want to carry on", async () => { | ||
fs.closeSync(fs.openSync("./wrangler.toml", "w")); | ||
fs.writeFileSync( | ||
"./wrangler.toml", | ||
'compatibility_date="something-else"', // use a fake value to make sure the file is not overwritten | ||
"utf-8" | ||
); | ||
mockConfirm({ | ||
@@ -138,10 +144,14 @@ text: "Do you want to continue initializing this project?", | ||
}); | ||
const { stderr } = await w("init"); | ||
expect(stderr).toContain("wrangler.toml file already exists!"); | ||
await runWrangler("init"); | ||
expect(std.warn).toContain("wrangler.toml file already exists!"); | ||
const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); | ||
expect(typeof parsed.compatibility_date).toBe("undefined"); | ||
expect(parsed.compatibility_date).toBe("something-else"); | ||
}); | ||
it("should display warning when wrangler.toml already exists, but continue if user does want to carry on", async () => { | ||
fs.closeSync(fs.openSync("./wrangler.toml", "w")); | ||
fs.writeFileSync( | ||
"./wrangler.toml", | ||
`compatibility_date="something-else"`, | ||
"utf-8" | ||
); | ||
mockConfirm( | ||
@@ -157,6 +167,6 @@ { | ||
); | ||
const { stderr } = await w("init"); | ||
expect(stderr).toContain("wrangler.toml file already exists!"); | ||
await runWrangler("init"); | ||
expect(std.warn).toContain("wrangler.toml file already exists!"); | ||
const parsed = TOML.parse(await fsp.readFile("./wrangler.toml", "utf-8")); | ||
expect(typeof parsed.compatibility_date).toBe("string"); | ||
expect(parsed.compatibility_date).toBe("something-else"); | ||
}); | ||
@@ -171,7 +181,11 @@ | ||
{ | ||
text: "Would you like to use typescript?", | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: false, | ||
} | ||
); | ||
await w("init"); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./package.json")).toBe(true); | ||
@@ -181,12 +195,48 @@ const packageJson = JSON.parse( | ||
); | ||
expect(packageJson.name).toEqual("worker"); // TODO: should we infer the name from the directory? | ||
expect(packageJson.version).toEqual("0.0.1"); | ||
expect(packageJson.name).toContain("wrangler-tests"); | ||
expect(packageJson.version).toEqual("0.0.0"); | ||
expect(packageJson.devDependencies).toEqual({ | ||
wrangler: expect.any(String), | ||
}); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(false); | ||
expect(mockPackageManager.install).toHaveBeenCalled(); | ||
}); | ||
it("should create a package.json, with the specified name, if none is found and user confirms", async () => { | ||
mockConfirm( | ||
{ | ||
text: "No package.json found. Would you like to create one?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: false, | ||
} | ||
); | ||
await runWrangler("init my-worker"); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
expect(packageJson.name).toBe("my-worker"); | ||
}); | ||
it("should not touch an existing package.json in the same directory", async () => { | ||
mockConfirm({ | ||
text: "Would you like to use typescript?", | ||
result: false, | ||
}); | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: false, | ||
} | ||
); | ||
@@ -199,3 +249,3 @@ fs.writeFileSync( | ||
await w("init"); | ||
await runWrangler("init"); | ||
const packageJson = JSON.parse( | ||
@@ -208,7 +258,50 @@ fs.readFileSync("./package.json", "utf-8") | ||
it("should offer to install wrangler into an existing package.json", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: false, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
"utf-8" | ||
); | ||
await runWrangler("init"); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
expect(packageJson.name).toEqual("test"); | ||
expect(packageJson.version).toEqual("1.0.0"); | ||
expect(mockPackageManager.addDevDeps).toHaveBeenCalledWith( | ||
`wrangler@${wranglerVersion}` | ||
); | ||
}); | ||
it("should not touch an existing package.json in an ancestor directory", async () => { | ||
mockConfirm({ | ||
text: "Would you like to use typescript?", | ||
result: false, | ||
}); | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: false, | ||
} | ||
); | ||
@@ -224,3 +317,3 @@ fs.writeFileSync( | ||
await w("init"); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./package.json")).toBe(false); | ||
@@ -232,6 +325,286 @@ expect(fs.existsSync("../../package.json")).toBe(true); | ||
); | ||
expect(packageJson.name).toEqual("test"); | ||
expect(packageJson.version).toEqual("1.0.0"); | ||
expect(packageJson).toMatchInlineSnapshot(` | ||
Object { | ||
"name": "test", | ||
"version": "1.0.0", | ||
} | ||
`); | ||
}); | ||
it("should offer to create a worker in a non-typescript project", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: true, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
"utf-8" | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./src/index.js")).toBe(true); | ||
expect(fs.existsSync("./src/index.ts")).toBe(false); | ||
}); | ||
it("should offer to create a worker in a typescript project", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.ts?", | ||
result: true, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
"utf-8" | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.existsSync("./src/index.ts")).toBe(true); | ||
}); | ||
it("should add scripts for a typescript project with .ts extension", async () => { | ||
mockConfirm( | ||
{ | ||
text: "No package.json found. Would you like to create one?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.ts?", | ||
result: true, | ||
} | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./package.json")).toBe(true); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.existsSync("./src/index.ts")).toBe(true); | ||
expect(packageJson.scripts.start).toBe("wrangler dev src/index.ts"); | ||
expect(packageJson.scripts.publish).toBe("wrangler publish src/index.ts"); | ||
expect(packageJson.name).toContain("wrangler-tests"); | ||
expect(packageJson.version).toEqual("0.0.0"); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
"✨ Successfully created wrangler.toml | ||
✨ Created package.json | ||
✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies | ||
To start developing on your worker, run npm start. | ||
To publish your worker on to the internet, run npm run publish. | ||
✨ Created src/index.ts" | ||
`); | ||
}); | ||
it("should not overwrite package.json scripts for a typescript project", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.ts?", | ||
result: true, | ||
} | ||
); | ||
await fsp.writeFile( | ||
"./package.json", | ||
JSON.stringify({ | ||
scripts: { | ||
start: "test-start", | ||
publish: "test-publish", | ||
}, | ||
}) | ||
); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.existsSync("./src/index.ts")).toBe(true); | ||
expect(packageJson.scripts.start).toBe("test-start"); | ||
expect(packageJson.scripts.publish).toBe("test-publish"); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
"✨ Successfully created wrangler.toml | ||
✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies | ||
To start developing on your worker, npx wrangler dev src/index.ts | ||
To publish your worker on to the internet, npx wrangler publish src/index.ts | ||
✨ Created src/index.ts" | ||
`); | ||
}); | ||
it("should add missing scripts for a non-ts project with .js extension", async () => { | ||
mockConfirm( | ||
{ | ||
text: "No package.json found. Would you like to create one?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: true, | ||
} | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./package.json")).toBe(true); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
expect(fs.existsSync("./src/index.js")).toBe(true); | ||
expect(fs.existsSync("./src/index.ts")).toBe(false); | ||
expect(packageJson.scripts.start).toBe("wrangler dev src/index.js"); | ||
expect(packageJson.scripts.publish).toBe("wrangler publish src/index.js"); | ||
expect(packageJson.name).toContain("wrangler-tests"); | ||
expect(packageJson.version).toEqual("0.0.0"); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
"✨ Successfully created wrangler.toml | ||
✨ Created package.json | ||
To start developing on your worker, run npm start. | ||
To publish your worker on to the internet, run npm run publish. | ||
✨ Created src/index.js" | ||
`); | ||
}); | ||
it("should not overwrite package.json scripts for a non-ts project with .js extension", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.js?", | ||
result: true, | ||
} | ||
); | ||
await fsp.writeFile( | ||
"./package.json", | ||
JSON.stringify({ | ||
scripts: { | ||
start: "test-start", | ||
publish: "test-publish", | ||
}, | ||
}) | ||
); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./src/index.js")).toBe(true); | ||
expect(fs.existsSync("./src/index.ts")).toBe(false); | ||
expect(packageJson.scripts.start).toBe("test-start"); | ||
expect(packageJson.scripts.publish).toBe("test-publish"); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
"✨ Successfully created wrangler.toml | ||
To start developing on your worker, npx wrangler dev src/index.js | ||
To publish your worker on to the internet, npx wrangler publish src/index.js | ||
✨ Created src/index.js" | ||
`); | ||
}); | ||
it("should not offer to create a worker in a non-ts project if a file already exists at the location", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: false, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
"utf-8" | ||
); | ||
fs.mkdirSync("./src", { recursive: true }); | ||
const PLACEHOLDER = "/* placeholder text */"; | ||
fs.writeFileSync("./src/index.js", PLACEHOLDER, "utf-8"); | ||
await runWrangler("init"); | ||
expect(fs.readFileSync("./src/index.js", "utf-8")).toBe(PLACEHOLDER); | ||
expect(fs.existsSync("./src/index.ts")).toBe(false); | ||
}); | ||
it("should not offer to create a worker in a ts project if a file already exists at the location", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to use TypeScript?", | ||
result: true, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
"utf-8" | ||
); | ||
fs.mkdirSync("./src", { recursive: true }); | ||
const PLACEHOLDER = "/* placeholder text */"; | ||
fs.writeFileSync("./src/index.ts", PLACEHOLDER, "utf-8"); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.readFileSync("./src/index.ts", "utf-8")).toBe(PLACEHOLDER); | ||
}); | ||
it("should create a tsconfig.json and install `workers-types` if none is found and user confirms", async () => { | ||
@@ -244,7 +617,11 @@ mockConfirm( | ||
{ | ||
text: "Would you like to use typescript?", | ||
text: "Would you like to use TypeScript?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.ts?", | ||
result: false, | ||
} | ||
); | ||
await w("init"); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(true); | ||
@@ -257,8 +634,6 @@ const tsconfigJson = JSON.parse( | ||
]); | ||
const packageJson = JSON.parse( | ||
fs.readFileSync("./package.json", "utf-8") | ||
expect(mockPackageManager.addDevDeps).toHaveBeenCalledWith( | ||
"@cloudflare/workers-types", | ||
"typescript" | ||
); | ||
expect(packageJson.devDependencies).toEqual({ | ||
"@cloudflare/workers-types": expect.any(String), | ||
}); | ||
}); | ||
@@ -269,3 +644,10 @@ | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
JSON.stringify({ | ||
name: "test", | ||
version: "1.0.0", | ||
devDependencies: { | ||
wrangler: "0.0.0", | ||
"@cloudflare/workers-types": "0.0.0", | ||
}, | ||
}), | ||
"utf-8" | ||
@@ -279,3 +661,3 @@ ); | ||
await w("init"); | ||
await runWrangler("init"); | ||
const tsconfigJson = JSON.parse( | ||
@@ -287,6 +669,23 @@ fs.readFileSync("./tsconfig.json", "utf-8") | ||
it("should not touch an existing package.json in an ancestor directory", async () => { | ||
it("should offer to install type definitions in an existing typescript project", async () => { | ||
mockConfirm( | ||
{ | ||
text: "Would you like to install wrangler into your package.json?", | ||
result: false, | ||
}, | ||
{ | ||
text: "Would you like to install the type definitions for Workers into your package.json?", | ||
result: true, | ||
}, | ||
{ | ||
text: "Would you like to create a Worker at src/index.ts?", | ||
result: false, | ||
} | ||
); | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ name: "test", version: "1.0.0" }), | ||
JSON.stringify({ | ||
name: "test", | ||
version: "1.0.0", | ||
}), | ||
"utf-8" | ||
@@ -300,6 +699,36 @@ ); | ||
await runWrangler("init"); | ||
const tsconfigJson = JSON.parse( | ||
fs.readFileSync("./tsconfig.json", "utf-8") | ||
); | ||
// unchanged tsconfig | ||
expect(tsconfigJson.compilerOptions).toEqual({}); | ||
expect(mockPackageManager.addDevDeps).toHaveBeenCalledWith( | ||
"@cloudflare/workers-types" | ||
); | ||
}); | ||
it("should not touch an existing tsconfig.json in an ancestor directory", async () => { | ||
fs.writeFileSync( | ||
"./package.json", | ||
JSON.stringify({ | ||
name: "test", | ||
version: "1.0.0", | ||
devDependencies: { | ||
wrangler: "0.0.0", | ||
"@cloudflare/workers-types": "0.0.0", | ||
}, | ||
}), | ||
"utf-8" | ||
); | ||
fs.writeFileSync( | ||
"./tsconfig.json", | ||
JSON.stringify({ compilerOptions: {} }), | ||
"utf-8" | ||
); | ||
fs.mkdirSync("./sub-1/sub-2", { recursive: true }); | ||
process.chdir("./sub-1/sub-2"); | ||
await w("init"); | ||
await runWrangler("init"); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(false); | ||
@@ -314,5 +743,26 @@ expect(fs.existsSync("../../tsconfig.json")).toBe(true); | ||
it("should initialize with no interactive prompts if `--yes` is used", async () => { | ||
await runWrangler("init --yes"); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.existsSync("./src/index.ts")).toBe(true); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(true); | ||
expect(fs.existsSync("./package.json")).toBe(true); | ||
expect(fs.existsSync("./wrangler.toml")).toBe(true); | ||
}); | ||
it("should initialize with no interactive prompts if `--y` is used", async () => { | ||
await runWrangler("init -y"); | ||
expect(fs.existsSync("./src/index.js")).toBe(false); | ||
expect(fs.existsSync("./src/index.ts")).toBe(true); | ||
expect(fs.existsSync("./tsconfig.json")).toBe(true); | ||
expect(fs.existsSync("./package.json")).toBe(true); | ||
expect(fs.existsSync("./wrangler.toml")).toBe(true); | ||
}); | ||
it("should error if `--type` is used", async () => { | ||
const noValue = await w("init --type"); | ||
expect(noValue.stderr).toMatchInlineSnapshot( | ||
await expect( | ||
runWrangler("init --type") | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"The --type option is no longer supported."` | ||
@@ -323,4 +773,5 @@ ); | ||
it("should error if `--type javascript` is used", async () => { | ||
const javascriptValue = await w("init --type javascript"); | ||
expect(javascriptValue.stderr).toMatchInlineSnapshot( | ||
await expect( | ||
runWrangler("init --type javascript") | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"The --type option is no longer supported."` | ||
@@ -331,4 +782,5 @@ ); | ||
it("should error if `--type rust` is used", async () => { | ||
const rustValue = await w("init --type rust"); | ||
expect(rustValue.stderr).toMatchInlineSnapshot( | ||
await expect( | ||
runWrangler("init --type rust") | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"The --type option is no longer supported."` | ||
@@ -339,81 +791,28 @@ ); | ||
it("should error if `--type webpack` is used", async () => { | ||
const webpackValue = await w("init --type webpack"); | ||
expect(webpackValue.stderr).toMatchInlineSnapshot(` | ||
"The --type option is no longer supported. | ||
If you wish to use webpack then you will need to create a custom build." | ||
`); | ||
await expect(runWrangler("init --type webpack")).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
"The --type option is no longer supported. | ||
If you wish to use webpack then you will need to create a custom build." | ||
`); | ||
}); | ||
}); | ||
describe("kv:namespace", () => { | ||
afterEach(() => { | ||
unsetAllMocks(); | ||
describe("preview", () => { | ||
it("should throw an error if the deprecated command is used with positional arguments", async () => { | ||
await expect(runWrangler("preview GET")).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
"DEPRECATION WARNING: | ||
The \`wrangler preview\` command has been deprecated. | ||
Try using \`wrangler dev\` to to try out a worker during development. | ||
" | ||
`); | ||
await expect(runWrangler("preview GET 'Some Body'")).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
"DEPRECATION WARNING: | ||
The \`wrangler preview\` command has been deprecated. | ||
Try using \`wrangler dev\` to to try out a worker during development. | ||
" | ||
`); | ||
}); | ||
it("can create a namespace", async () => { | ||
const KVNamespaces: { title: string; id: string }[] = []; | ||
setMock("/accounts/:accountId/storage/kv/namespaces", (uri, init) => { | ||
expect(init.method === "POST"); | ||
expect(uri[0]).toEqual( | ||
"/accounts/some-account-id/storage/kv/namespaces" | ||
); | ||
const { title } = JSON.parse(init.body); | ||
expect(title).toEqual("worker-UnitTestNamespace"); | ||
KVNamespaces.push({ title, id: "some-namespace-id" }); | ||
return { id: "some-namespace-id" }; | ||
}); | ||
await w("kv:namespace create UnitTestNamespace"); | ||
expect(KVNamespaces).toEqual([ | ||
{ | ||
title: "worker-UnitTestNamespace", | ||
id: "some-namespace-id", | ||
}, | ||
]); | ||
}); | ||
it("can list namespaces", async () => { | ||
const KVNamespaces: { title: string; id: string }[] = [ | ||
{ title: "title-1", id: "id-1" }, | ||
{ title: "title-2", id: "id-2" }, | ||
]; | ||
setMock( | ||
"/accounts/:accountId/storage/kv/namespaces\\?:qs", | ||
(uri, init) => { | ||
expect(uri[0]).toContain( | ||
"/accounts/some-account-id/storage/kv/namespaces" | ||
); | ||
expect(uri[2]).toContain("per_page=100"); | ||
expect(uri[2]).toContain("order=title"); | ||
expect(uri[2]).toContain("direction=asc"); | ||
expect(uri[2]).toContain("page=1"); | ||
expect(init).toBe(undefined); | ||
return KVNamespaces; | ||
} | ||
); | ||
const { stdout } = await w("kv:namespace list"); | ||
const namespaces = JSON.parse(stdout) as { id: string; title: string }[]; | ||
expect(namespaces).toEqual(KVNamespaces); | ||
}); | ||
it("can delete a namespace", async () => { | ||
let accountId = ""; | ||
let namespaceId = ""; | ||
setMock( | ||
"/accounts/:accountId/storage/kv/namespaces/:namespaceId", | ||
(uri, init) => { | ||
accountId = uri[1]; | ||
namespaceId = uri[2]; | ||
expect(uri[0]).toEqual( | ||
"/accounts/some-account-id/storage/kv/namespaces/some-namespace-id" | ||
); | ||
expect(init.method).toBe("DELETE"); | ||
} | ||
); | ||
await w(`kv:namespace delete --namespace-id some-namespace-id`); | ||
expect(accountId).toEqual("some-account-id"); | ||
expect(namespaceId).toEqual("some-namespace-id"); | ||
}); | ||
}); | ||
}); |
@@ -0,3 +1,43 @@ | ||
import fetchMock from "jest-fetch-mock"; | ||
import { | ||
fetchInternal, | ||
fetchKVGetValue, | ||
getCloudflareAPIBaseURL, | ||
} from "../cfetch/internal"; | ||
import { confirm, prompt } from "../dialogs"; | ||
import { mockFetchInternal, mockFetchKVGetValue } from "./helpers/mock-cfetch"; | ||
import { MockWebSocket } from "./helpers/mock-web-socket"; | ||
jest.mock("ws", () => { | ||
return { | ||
__esModule: true, | ||
default: MockWebSocket, | ||
}; | ||
}); | ||
jest.mock("undici", () => { | ||
return { | ||
...jest.requireActual("undici"), | ||
fetch: jest.requireActual("jest-fetch-mock"), | ||
}; | ||
}); | ||
// Outside of the Sentry tests themselves, we mock Sentry to ensure that it doesn't actually send any data and | ||
// that it doesn't interfere with the rest of the tests. | ||
jest.mock("../reporting"); | ||
fetchMock.doMock(() => { | ||
// Any un-mocked fetches should throw | ||
throw new Error("Unexpected fetch request"); | ||
}); | ||
jest.mock("../package-manager"); | ||
jest.mock("../cfetch/internal"); | ||
(fetchInternal as jest.Mock).mockImplementation(mockFetchInternal); | ||
(fetchKVGetValue as jest.Mock).mockImplementation(mockFetchKVGetValue); | ||
(getCloudflareAPIBaseURL as jest.Mock).mockReturnValue( | ||
"https://api.cloudflare.com/client/v4" | ||
); | ||
jest.mock("../dialogs"); | ||
@@ -4,0 +44,0 @@ |
@@ -0,70 +1,9 @@ | ||
import { readFileSync } from "node:fs"; | ||
import { FormData, File } from "undici"; | ||
import type { | ||
CfWorkerInit, | ||
CfModuleType, | ||
CfVariable, | ||
CfModule, | ||
CfDurableObjectMigrations, | ||
} from "./worker.js"; | ||
import { FormData, Blob } from "formdata-node"; | ||
// Credit: https://stackoverflow.com/a/9458996 | ||
function toBase64(source: BufferSource): string { | ||
let result = ""; | ||
const buffer = source instanceof ArrayBuffer ? source : source.buffer; | ||
const bytes = new Uint8Array(buffer); | ||
for (let i = 0; i < bytes.byteLength; i++) { | ||
result += String.fromCharCode(bytes[i]); | ||
} | ||
return btoa(result); | ||
} | ||
function toBinding( | ||
name: string, | ||
variable: CfVariable | ||
): Record<string, unknown> { | ||
if (typeof variable === "string") { | ||
return { name, type: "plain_text", text: variable }; | ||
} | ||
if ("namespaceId" in variable) { | ||
return { | ||
name, | ||
type: "kv_namespace", | ||
namespace_id: variable.namespaceId, | ||
}; | ||
} | ||
if ("class_name" in variable) { | ||
return { | ||
name, | ||
type: "durable_object_namespace", | ||
class_name: variable.class_name, | ||
...(variable.script_name && { | ||
script_name: variable.script_name, | ||
}), | ||
}; | ||
} | ||
const { format, algorithm, usages, data } = variable; | ||
if (format) { | ||
let key_base64; | ||
let key_jwk; | ||
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { | ||
key_base64 = toBase64(data); | ||
} else { | ||
key_jwk = data; | ||
} | ||
return { | ||
name, | ||
type: "secret_key", | ||
format, | ||
algorithm, | ||
usages, | ||
key_base64, | ||
key_jwk, | ||
}; | ||
} | ||
throw new TypeError("Unsupported variable: " + variable); | ||
} | ||
export function toMimeType(type: CfModuleType): string { | ||
@@ -87,7 +26,24 @@ switch (type) { | ||
function toModule(module: CfModule, entryType?: CfModuleType): Blob { | ||
const { type: moduleType, content } = module; | ||
const type = toMimeType(moduleType ?? entryType); | ||
return new Blob([content], { type }); | ||
export interface WorkerMetadata { | ||
/** The name of the entry point module. Only exists when the worker is in the ES module format */ | ||
main_module?: string; | ||
/** The name of the entry point module. Only exists when the worker is in the Service Worker format */ | ||
body_part?: string; | ||
compatibility_date?: string; | ||
compatibility_flags?: string[]; | ||
usage_model?: "bundled" | "unbound"; | ||
migrations?: CfDurableObjectMigrations; | ||
bindings: ( | ||
| { type: "kv_namespace"; name: string; namespace_id: string } | ||
| { type: "plain_text"; name: string; text: string } | ||
| { type: "json"; name: string; json: unknown } | ||
| { type: "wasm_module"; name: string; part: string } | ||
| { | ||
type: "durable_object_namespace"; | ||
name: string; | ||
class_name: string; | ||
script_name?: string; | ||
} | ||
| { type: "r2_bucket"; name: string; bucket_name: string } | ||
)[]; | ||
} | ||
@@ -103,3 +59,3 @@ | ||
modules, | ||
variables, | ||
bindings, | ||
migrations, | ||
@@ -110,14 +66,95 @@ usage_model, | ||
} = worker; | ||
const { name, type: mainType } = main; | ||
const bindings = []; | ||
for (const [name, variable] of Object.entries(variables ?? {})) { | ||
const binding = toBinding(name, variable); | ||
bindings.push(binding); | ||
const metadataBindings: WorkerMetadata["bindings"] = []; | ||
bindings.kv_namespaces?.forEach(({ id, binding }) => { | ||
metadataBindings.push({ | ||
name: binding, | ||
type: "kv_namespace", | ||
namespace_id: id, | ||
}); | ||
}); | ||
bindings.durable_objects?.bindings.forEach( | ||
({ name, class_name, script_name }) => { | ||
metadataBindings.push({ | ||
name, | ||
type: "durable_object_namespace", | ||
class_name: class_name, | ||
...(script_name && { script_name }), | ||
}); | ||
} | ||
); | ||
bindings.r2_buckets?.forEach(({ binding, bucket_name }) => { | ||
metadataBindings.push({ | ||
name: binding, | ||
type: "r2_bucket", | ||
bucket_name, | ||
}); | ||
}); | ||
Object.entries(bindings.vars || {})?.forEach(([key, value]) => { | ||
if (typeof value === "string") { | ||
metadataBindings.push({ name: key, type: "plain_text", text: value }); | ||
} else { | ||
metadataBindings.push({ name: key, type: "json", json: value }); | ||
} | ||
}); | ||
for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) { | ||
metadataBindings.push({ | ||
name, | ||
type: "wasm_module", | ||
part: name, | ||
}); | ||
formData.set( | ||
name, | ||
new File([readFileSync(filePath)], filePath, { | ||
type: "application/wasm", | ||
}) | ||
); | ||
} | ||
// TODO: this object should be typed | ||
const metadata = { | ||
...(mainType !== "commonjs" ? { main_module: name } : { body_part: name }), | ||
bindings, | ||
if (main.type === "commonjs") { | ||
// This is a service-worker format worker. | ||
// So we convert all `.wasm` modules into `wasm_module` bindings. | ||
for (const [index, module] of Object.entries(modules || [])) { | ||
if (module.type === "compiled-wasm") { | ||
// The "name" of the module is a file path. We use it | ||
// to instead be a "part" of the body, and a reference | ||
// that we can use inside our source. This identifier has to be a valid | ||
// JS identifier, so we replace all non alphanumeric characters | ||
// with an underscore. | ||
const name = module.name.replace(/[^a-zA-Z0-9_$]/g, "_"); | ||
metadataBindings.push({ | ||
name, | ||
type: "wasm_module", | ||
part: name, | ||
}); | ||
// Add the module to the form data. | ||
formData.set( | ||
name, | ||
new File([module.content], module.name, { | ||
type: "application/wasm", | ||
}) | ||
); | ||
// And then remove it from the modules collection | ||
modules?.splice(parseInt(index, 10), 1); | ||
} | ||
} | ||
} | ||
if (bindings.unsafe) { | ||
// @ts-expect-error unsafe bindings don't need to match a specific type here | ||
metadataBindings.push(...bindings.unsafe); | ||
} | ||
const metadata: WorkerMetadata = { | ||
...(main.type !== "commonjs" | ||
? { main_module: main.name } | ||
: { body_part: main.name }), | ||
bindings: metadataBindings, | ||
...(compatibility_date && { compatibility_date }), | ||
@@ -131,3 +168,3 @@ ...(compatibility_flags && { compatibility_flags }), | ||
if (mainType === "commonjs" && modules && modules.length > 0) { | ||
if (main.type === "commonjs" && modules && modules.length > 0) { | ||
throw new TypeError( | ||
@@ -139,5 +176,8 @@ "More than one module can only be specified when type = 'esm'" | ||
for (const module of [main].concat(modules || [])) { | ||
const { name } = module; | ||
const blob = toModule(module, mainType ?? "esm"); | ||
formData.set(name, blob, name); | ||
formData.set( | ||
module.name, | ||
new File([module.content], module.name, { | ||
type: toMimeType(module.type ?? main.type ?? "esm"), | ||
}) | ||
); | ||
} | ||
@@ -144,0 +184,0 @@ |
@@ -1,5 +0,6 @@ | ||
import cfetch from "../cfetch"; | ||
import { URL } from "node:url"; | ||
import { fetch } from "undici"; | ||
import { fetchResult } from "../cfetch"; | ||
import { toFormData } from "./form_data"; | ||
import type { CfAccount, CfWorkerInit } from "./worker"; | ||
import fetch from "node-fetch"; | ||
@@ -63,3 +64,3 @@ /** | ||
const { exchange_url } = await cfetch<{ exchange_url: string }>(initUrl); | ||
const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl); | ||
const { inspector_websocket, token } = (await ( | ||
@@ -110,5 +111,4 @@ await fetch(exchange_url) | ||
const { preview_token } = await cfetch<{ preview_token: string }>(url, { | ||
const { preview_token } = await fetchResult<{ preview_token: string }>(url, { | ||
method: "POST", | ||
// @ts-expect-error TODO: fix this | ||
body: formData, | ||
@@ -115,0 +115,0 @@ headers: { |
@@ -0,4 +1,4 @@ | ||
import { fetch } from "undici"; | ||
import { previewToken } from "./preview"; | ||
import type { CfPreviewToken } from "./preview"; | ||
import { previewToken } from "./preview"; | ||
import fetch from "node-fetch"; | ||
@@ -26,2 +26,7 @@ /** | ||
/** | ||
* The type of Worker | ||
*/ | ||
export type CfScriptFormat = "modules" | "service-worker"; | ||
/** | ||
* A module type. | ||
@@ -57,3 +62,3 @@ */ | ||
*/ | ||
content: string | BufferSource; | ||
content: string | Buffer; | ||
/** | ||
@@ -68,12 +73,29 @@ * The module type. | ||
/** | ||
* A map of variable names to values. | ||
*/ | ||
interface CfVars { | ||
[key: string]: unknown; | ||
} | ||
/** | ||
* A KV namespace. | ||
*/ | ||
export interface CfKvNamespace { | ||
/** | ||
* The namespace ID. | ||
*/ | ||
namespaceId: string; | ||
interface CfKvNamespace { | ||
binding: string; | ||
id: string; | ||
} | ||
export interface CfDurableObject { | ||
/** | ||
* A binding to a wasm module (in service worker format) | ||
*/ | ||
interface CfWasmModuleBindings { | ||
[key: string]: string; | ||
} | ||
/** | ||
* A Durable Object. | ||
*/ | ||
interface CfDurableObject { | ||
name: string; | ||
class_name: string; | ||
@@ -83,3 +105,13 @@ script_name?: string; | ||
interface CfDOMigrations { | ||
interface CfR2Bucket { | ||
binding: string; | ||
bucket_name: string; | ||
} | ||
interface CfUnsafeBinding { | ||
name: string; | ||
type: string; | ||
} | ||
export interface CfDurableObjectMigrations { | ||
old_tag?: string; | ||
@@ -89,3 +121,6 @@ new_tag: string; | ||
new_classes?: string[]; | ||
renamed_classes?: string[]; | ||
renamed_classes?: { | ||
from: string; | ||
to: string; | ||
}[]; | ||
deleted_classes?: string[]; | ||
@@ -96,31 +131,2 @@ }[]; | ||
/** | ||
* A `WebCrypto` key. | ||
* | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey | ||
*/ | ||
export interface CfCryptoKey { | ||
/** | ||
* The format. | ||
*/ | ||
format: string; | ||
/** | ||
* The algorithm. | ||
*/ | ||
algorithm: string; | ||
/** | ||
* The usages. | ||
*/ | ||
usages: string[]; | ||
/** | ||
* The data. | ||
*/ | ||
data: BufferSource | JsonWebKey; | ||
} | ||
/** | ||
* A variable (aka. environment variable). | ||
*/ | ||
export type CfVariable = string | CfKvNamespace | CfCryptoKey | CfDurableObject; | ||
/** | ||
* Options for creating a `CfWorker`. | ||
@@ -132,3 +138,3 @@ */ | ||
*/ | ||
name: string | void; | ||
name: string | undefined; | ||
/** | ||
@@ -141,11 +147,18 @@ * The entrypoint module. | ||
*/ | ||
modules: void | CfModule[]; | ||
modules: undefined | CfModule[]; | ||
/** | ||
* The map of names to variables. (aka. environment variables) | ||
* All the bindings | ||
*/ | ||
variables?: { [name: string]: CfVariable }; | ||
migrations: void | CfDOMigrations; | ||
compatibility_date: string | void; | ||
compatibility_flags: void | string[]; | ||
usage_model: void | "bundled" | "unbound"; | ||
bindings: { | ||
vars: CfVars | undefined; | ||
kv_namespaces: CfKvNamespace[] | undefined; | ||
wasm_modules: CfWasmModuleBindings | undefined; | ||
durable_objects: { bindings: CfDurableObject[] } | undefined; | ||
r2_buckets: CfR2Bucket[] | undefined; | ||
unsafe: CfUnsafeBinding[] | undefined; | ||
}; | ||
migrations: undefined | CfDurableObjectMigrations; | ||
compatibility_date: string | undefined; | ||
compatibility_flags: undefined | string[]; | ||
usage_model: undefined | "bundled" | "unbound"; | ||
} | ||
@@ -152,0 +165,0 @@ |
@@ -0,10 +1,19 @@ | ||
import process from "process"; | ||
import { hideBin } from "yargs/helpers"; | ||
import { reportError, initReporting } from "./reporting"; | ||
import { main } from "."; | ||
main(process.argv.slice(2)).catch((cause) => { | ||
const { name, message } = cause; | ||
if (name === "CloudflareError") { | ||
console.error("\x1b[31m", message); | ||
return; | ||
} | ||
throw cause; | ||
try { | ||
initReporting(); | ||
} catch (error) {} | ||
process.on("uncaughtExceptionMonitor", async (err, origin) => { | ||
await reportError(err, origin); | ||
}); | ||
main(hideBin(process.argv)).catch(() => { | ||
// The logging of any error that was thrown from `main()` is handled in the `yargs.fail()` handler. | ||
// Here we just want to ensure that the process exits with a non-zero code. | ||
// We don't want to do this inside the `main()` function, since that would kill the process when running our tests. | ||
process.exit(1); | ||
}); |
@@ -1,122 +0,671 @@ | ||
// we're going to manually write both the type definition AND | ||
// the validator for the config, so that we can give better error messages | ||
import assert from "node:assert"; | ||
type DOMigration = { | ||
tag: string; | ||
new_classes?: string[]; | ||
renamed_classes?: string[]; | ||
deleted_classes?: string[]; | ||
}; | ||
/** | ||
* This is the static type definition for the configuration object. | ||
* It reflects the configuration that you can write in wrangler.toml, | ||
* and optionally augment with arguments passed directly to wrangler. | ||
* The type definition doesn't fully reflect the constraints applied | ||
* to the configuration, but it is a good starting point. Later, we | ||
* also defined a validator function that will validate the configuration | ||
* with the same rules as the type definition, as well as the extra | ||
* constraints. The type definition is good for asserting correctness | ||
* in the wrangler codebase, whereas the validator function is useful | ||
* for signalling errors in the configuration to a user of wrangler. | ||
* | ||
* For more information about the configuration object, see the | ||
* documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration | ||
* | ||
* Legend for the annotations: | ||
* | ||
* *:optional means providing a value isn't mandatory | ||
* *:deprecated means the field itself isn't necessary anymore in wrangler.toml | ||
* *:breaking means the deprecation/optionality is a breaking change from wrangler 1 | ||
* *:todo means there's more work to be done (with details attached) | ||
* *:inherited means the field is copied to all environments | ||
*/ | ||
export type Config = { | ||
/** | ||
* The name of your worker. Alphanumeric + dashes only. | ||
* | ||
* @optional | ||
* @inherited | ||
*/ | ||
name?: string; | ||
type Project = "webpack" | "javascript" | "rust"; | ||
/** | ||
* The entrypoint/path to the JavaScript file that will be executed. | ||
* | ||
* @optional | ||
* @inherited | ||
* @todo this needs to be implemented! | ||
*/ | ||
main?: string; | ||
type Site = { | ||
// inherited | ||
bucket: string; | ||
"entry-point": string; | ||
include?: string[]; | ||
exclude?: string[]; | ||
}; | ||
/** | ||
* This is the ID of the account associated with your zone. | ||
* You might have more than one account, so make sure to use | ||
* the ID of the account associated with the zone/route you | ||
* provide, if you provide one. It can also be specified through | ||
* the CLOUDFLARE_ACCOUNT_ID environment variable. | ||
* | ||
* @optional | ||
* @inherited | ||
*/ | ||
account_id?: string; | ||
type Dev = { | ||
ip?: string; | ||
port?: number; | ||
local_protocol?: string; | ||
upstream_protocol?: string; | ||
}; | ||
/** | ||
* The project "type". A holdover from wrangler 1.x. | ||
* Valid values were "webpack", "javascript", and "rust". | ||
* | ||
* @deprecated DO NOT USE THIS. Most common features now work out of the box with wrangler, including modules, jsx, typescript, etc. If you need anything more, use a custom build. | ||
* @optional | ||
* @inherited | ||
* @breaking | ||
*/ | ||
type?: "webpack" | "javascript" | "rust"; | ||
type Vars = { [key: string]: string }; | ||
/** | ||
* A date in the form yyyy-mm-dd, which will be used to determine | ||
* which version of the Workers runtime is used. More details at | ||
* https://developers.cloudflare.com/workers/platform/compatibility-dates | ||
* @optional true for `dev`, false for `publish` | ||
* @inherited | ||
*/ | ||
compatibility_date?: string; | ||
type Cron = string; // TODO: we should be able to parse a cron pattern with ts | ||
/** | ||
* A list of flags that enable features from upcoming features of | ||
* the Workers runtime, usually used together with compatibility_flags. | ||
* More details at | ||
* https://developers.cloudflare.com/workers/platform/compatibility-dates | ||
* | ||
* @optional | ||
* @inherited | ||
*/ | ||
compatibility_flags?: string[]; | ||
type KVNamespace = { | ||
binding?: string; | ||
preview_id: string; | ||
id: string; | ||
}; | ||
/** | ||
* Whether we use <name>.<subdomain>.workers.dev to | ||
* test and deploy your worker. | ||
* | ||
* @default `true` (This is a breaking change from wrangler 1) | ||
* @optional | ||
* @inherited | ||
* @breaking | ||
*/ | ||
workers_dev?: boolean; | ||
type DurableObject = { | ||
name: string; | ||
class_name: string; | ||
script_name?: string; | ||
/** | ||
* The zone ID of the zone you want to deploy to. You can find this | ||
* in your domain page on the dashboard. | ||
* | ||
* @deprecated This is unnecessary since we can deduce this from routes directly. | ||
* @optional | ||
* @inherited | ||
*/ | ||
zone_id?: string; | ||
/** | ||
* A list of routes that your worker should be published to. | ||
* Only one of `routes` or `route` is required. | ||
* | ||
* @optional false only when workers_dev is false, and there's no scheduled worker | ||
* @inherited | ||
*/ | ||
routes?: string[]; | ||
/** | ||
* A route that your worker should be published to. Literally | ||
* the same as routes, but only one. | ||
* Only one of `routes` or `route` is required. | ||
* | ||
* @optional false only when workers_dev is false, and there's no scheduled worker | ||
* @inherited | ||
*/ | ||
route?: string; | ||
/** | ||
* Path to the webpack config to use when building your worker. | ||
* A holdover from wrangler 1.x, used with `type: "webpack"`. | ||
* | ||
* @deprecated DO NOT USE THIS. Most common features now work out of the box with wrangler, including modules, jsx, typescript, etc. If you need anything more, use a custom build. | ||
* @inherited | ||
* @breaking | ||
*/ | ||
webpack_config?: string; | ||
/** | ||
* The function to use to replace jsx syntax. | ||
* | ||
* @default `"React.createElement"` | ||
* @optional | ||
* @inherited | ||
*/ | ||
jsx_factory?: string; | ||
/** | ||
* The function to use to replace jsx fragment syntax. | ||
* | ||
* @default `"React.Fragment"` | ||
* @optional | ||
* @inherited | ||
*/ | ||
jsx_fragment?: string; | ||
/** | ||
* A map of environment variables to set when deploying your worker. | ||
* | ||
* NB: these are not inherited, and HAVE to be duplicated across all environments. | ||
* | ||
* @default `{}` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
vars?: { [key: string]: unknown }; | ||
/** | ||
* A list of durable objects that your worker should be bound to. | ||
* For more information about Durable Objects, see the documentation at | ||
* https://developers.cloudflare.com/workers/learning/using-durable-objects | ||
* NB: these are not inherited, and HAVE to be duplicated across all environments. | ||
* | ||
* @default `{ bindings: [] }` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
durable_objects?: { | ||
bindings: { | ||
/** The name of the binding used to refer to the Durable Object */ | ||
name: string; | ||
/** The exported class name of the Durable Object */ | ||
class_name: string; | ||
/** The script where the Durable Object is defined (if it's external to this worker) */ | ||
script_name?: string; | ||
}[]; | ||
}; | ||
/** | ||
* These specify any Workers KV Namespaces you want to | ||
* access from inside your Worker. To learn more about KV Namespaces, | ||
* see the documentation at https://developers.cloudflare.com/workers/learning/how-kv-works | ||
* NB: these are not inherited, and HAVE to be duplicated across all environments. | ||
* | ||
* @default `[]` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
kv_namespaces?: { | ||
/** The binding name used to refer to the KV Namespace */ | ||
binding: string; | ||
/** The ID of the KV namespace */ | ||
id: string; | ||
/** The ID of the KV namespace used during `wrangler dev` */ | ||
preview_id?: string; | ||
}[]; | ||
r2_buckets?: { | ||
/** The binding name used to refer to the R2 bucket in the worker. */ | ||
binding: string; | ||
/** The name of this R2 bucket at the edge. */ | ||
bucket_name: string; | ||
/** The preview name of this R2 bucket at the edge. */ | ||
preview_bucket_name?: string; | ||
}[]; | ||
/** | ||
* A list of services that your worker should be bound to. | ||
* NB: these are not inherited, and HAVE to be duplicated across all environments. | ||
* | ||
* @default `[]` | ||
* @optional | ||
* @deprecated DO NOT USE. We'd added this to test the new service binding system, but the proper way to test experimental features is to use `unsafe.bindings` configuration. | ||
* @inherited false | ||
*/ | ||
experimental_services?: { | ||
/** The binding name used to refer to the Service */ | ||
name: string; | ||
/** The name of the Service being bound */ | ||
service: string; | ||
/** The Service's environment */ | ||
environment: string; | ||
}[]; | ||
/** | ||
* A list of wasm modules that your worker should be bound to. This is | ||
* the "legacy" way of binding to a wasm module. ES module workers should | ||
* do proper module imports. | ||
* NB: these ARE NOT inherited, and SHOULD NOT be duplicated across all environments. | ||
*/ | ||
wasm_modules?: { | ||
[key: string]: string; | ||
}; | ||
/** | ||
* "Unsafe" tables for features that aren't directly supported by wrangler. | ||
* NB: these are not inherited, and HAVE to be duplicated across all environments. | ||
* | ||
* @default `[]` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
unsafe?: { | ||
/** | ||
* A set of bindings that should be put into a Worker's upload metadata without changes. These | ||
* can be used to implement bindings for features that haven't released and aren't supported | ||
* directly by wrangler or miniflare. | ||
*/ | ||
bindings?: { | ||
name: string; | ||
type: string; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[key: string]: any; | ||
}[]; | ||
}; | ||
/** | ||
* A list of migrations that should be uploaded with your Worker. | ||
* These define changes in your Durable Object declarations. | ||
* More details at https://developers.cloudflare.com/workers/learning/using-durable-objects#configuring-durable-object-classes-with-migrations | ||
* NB: these ARE NOT inherited, and SHOULD NOT be duplicated across all environments. | ||
* | ||
* @default `[]` | ||
* @optional | ||
* @inherited true | ||
*/ | ||
migrations?: { | ||
/** A unique identifier for this migration. */ | ||
tag: string; | ||
/** The new Durable Objects being defined. */ | ||
new_classes?: string[]; | ||
/** The Durable Objects being renamed. */ | ||
renamed_classes?: { | ||
from: string; | ||
to: string; | ||
}[]; | ||
/** The Durable Objects being removed. */ | ||
deleted_classes?: string[]; | ||
}[]; | ||
/** | ||
* The definition of a Worker Site, a feature that lets you upload | ||
* static assets with your Worker. | ||
* More details at https://developers.cloudflare.com/workers/platform/sites | ||
* NB: This IS NOT inherited, and SHOULD NOT be duplicated across all environments. | ||
* | ||
* @default `undefined` | ||
* @optional | ||
* @inherited true | ||
*/ | ||
site?: { | ||
/** | ||
* The directory containing your static assets. It must be | ||
* a path relative to your wrangler.toml file. | ||
* Example: bucket = "./public" | ||
* | ||
* optional false | ||
*/ | ||
bucket: string; | ||
/** | ||
* The location of your Worker script. | ||
* | ||
* @deprecated DO NOT use this (it's a holdover from wrangler 1.x). Either use the top level `main` field, or pass the path to your entry file as a command line argument. | ||
* @breaking | ||
*/ | ||
"entry-point"?: string; | ||
/** | ||
* An exclusive list of .gitignore-style patterns that match file | ||
* or directory names from your bucket location. Only matched | ||
* items will be uploaded. Example: include = ["upload_dir"] | ||
* | ||
* @optional | ||
* @default `[]` | ||
*/ | ||
include?: string[]; | ||
/** | ||
* A list of .gitignore-style patterns that match files or | ||
* directories in your bucket that should be excluded from | ||
* uploads. Example: exclude = ["ignore_dir"] | ||
* | ||
* @optional | ||
* @default `[]` | ||
*/ | ||
exclude?: string[]; | ||
}; | ||
/** | ||
* "Cron" definitions to trigger a worker's "scheduled" function. | ||
* Lets you call workers periodically, much like a cron job. | ||
* More details here https://developers.cloudflare.com/workers/platform/cron-triggers | ||
* | ||
* @inherited | ||
* @default `{ crons: [] }` | ||
* @optional | ||
*/ | ||
triggers?: { crons: string[] }; | ||
/** | ||
* Options to configure the development server that your worker will use. | ||
* NB: This is NOT inherited, and SHOULD NOT be duplicated across all environments. | ||
* | ||
* @default `{}` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
dev?: { | ||
/** | ||
* IP address for the local dev server to listen on, | ||
* | ||
* @default `127.0.0.1` | ||
* @todo this needs to be implemented | ||
*/ | ||
ip?: string; | ||
/** | ||
* Port for the local dev server to listen on | ||
* | ||
* @default `8787` | ||
*/ | ||
port?: number; | ||
/** | ||
* Protocol that local wrangler dev server listens to requests on. | ||
* | ||
* @default `http` | ||
* @todo this needs to be implemented | ||
*/ | ||
local_protocol?: string; | ||
/** | ||
* Protocol that wrangler dev forwards requests on | ||
* | ||
* @default `https` | ||
* @todo this needs to be implemented | ||
*/ | ||
upstream_protocol?: string; | ||
}; | ||
/** | ||
* Specifies the Usage Model for your Worker. There are two options - | ||
* [bundled](https://developers.cloudflare.com/workers/platform/limits#bundled-usage-model) and | ||
* [unbound](https://developers.cloudflare.com/workers/platform/limits#unbound-usage-model). | ||
* For newly created Workers, if the Usage Model is omitted | ||
* it will be set to the [default Usage Model set on the account](https://dash.cloudflare.com/?account=workers/default-usage-model). | ||
* For existing Workers, if the Usage Model is omitted, it will be | ||
* set to the Usage Model configured in the dashboard for that Worker. | ||
*/ | ||
usage_model?: undefined | "bundled" | "unbound"; | ||
/** | ||
* Configures a custom build step to be run by Wrangler when | ||
* building your Worker. Refer to the [custom builds documentation](https://developers.cloudflare.com/workers/cli-wrangler/configuration#build) | ||
* for more details. | ||
* | ||
* @default `undefined` | ||
* @optional | ||
* @inherited false | ||
*/ | ||
build?: { | ||
/** The command used to build your Worker. On Linux and macOS, the command is executed in the `sh` shell and the `cmd` shell for Windows. The `&&` and `||` shell operators may be used. */ | ||
command?: string; | ||
/** The directory in which the command is executed. */ | ||
cwd?: string; | ||
/** The directory to watch for changes while using wrangler dev, defaults to the current working directory */ | ||
watch_dir?: string; | ||
} & /** | ||
* Much of the rest of this configuration isn't necessary anymore | ||
* in wrangler2. We infer the format automatically, and we can pass | ||
* the path to the script either in the CLI (or, @todo, as the top level | ||
* `main` property). | ||
*/ { | ||
/** | ||
* We only really need to specify the entry point. | ||
* The format is deduced automatically in wrangler2. | ||
* | ||
* @deprecated Instead use top level main | ||
*/ | ||
upload?: { | ||
/** | ||
* The format of the Worker script. | ||
* | ||
* @deprecated We infer the format automatically now. | ||
*/ | ||
format?: "modules" | "service-worker"; | ||
/** | ||
* The directory you wish to upload your worker from, | ||
* relative to the wrangler.toml file. | ||
* | ||
* Defaults to the directory containing the wrangler.toml file. | ||
* | ||
* @deprecated | ||
* @breaking In wrangler 1, this defaults to ./dist, whereas in wrangler 2 it defaults to ./ | ||
*/ | ||
dir?: string; | ||
/** | ||
* The path to the Worker script, relative to `upload.dir`. | ||
* | ||
* @deprecated This will be replaced by a command line argument. | ||
*/ | ||
main?: string; | ||
/** | ||
* An ordered list of rules that define which modules to import, | ||
* and what type to import them as. You will need to specify rules | ||
* to use Text, Data, and CompiledWasm modules, or when you wish to | ||
* have a .js file be treated as an ESModule instead of CommonJS. | ||
* | ||
* @deprecated These are now inferred automatically for major file types, but you can still specify them manually. | ||
* @todo this needs to be implemented! | ||
* @breaking | ||
*/ | ||
rules?: { | ||
type: "ESModule" | "CommonJS" | "Text" | "Data" | "CompiledWasm"; | ||
globs: string[]; | ||
fallthrough?: boolean; | ||
}; | ||
}; | ||
}; | ||
/** | ||
* The `env` section defines overrides for the configuration for | ||
* different environments. Most fields can be overridden, while | ||
* some have to be specifically duplicated in every environment. | ||
* For more information, see the documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration#environments | ||
*/ | ||
env?: { | ||
[envName: string]: | ||
| undefined | ||
| Omit<Config, "env" | "wasm_modules" | "migrations" | "site" | "dev">; | ||
}; | ||
}; | ||
type Build = { | ||
command?: string; | ||
cwd?: string; | ||
watch_dir?: string; | ||
} & ( | ||
| { | ||
upload?: { | ||
format: "service-worker"; | ||
main: string; | ||
}; | ||
type ValidationResults = ( | ||
| { key: string; info: string } | ||
| { key: string; error: string } | ||
| { key: string; warning: string } | ||
)[]; | ||
/** | ||
* We also define a validation function that manually validates | ||
* every field in the configuration as per the type definitions, | ||
* as well as extra constraints we apply to some fields, as well | ||
* as some constraints on combinations of fields. This is useful for | ||
* presenting errors and messages to the user. Eventually, we will | ||
* combine this with some automatic config rewriting tools. | ||
* | ||
*/ | ||
export async function validateConfig( | ||
_config: Partial<Config> | ||
): Promise<ValidationResults> { | ||
const results: ValidationResults = []; | ||
return results; | ||
} | ||
/** | ||
* Process the environments (`env`) specified in the `config`. | ||
* | ||
* The environments configuration is complicated since each environment is a customized version of the main config. | ||
* Some of the configuration can be inherited from the main config, while other configuration must replace what is in the main config. | ||
* | ||
* This function ensures that each environment is set up correctly with inherited configuration, as necessary. | ||
* It will log a warning if an environment is missing required configuration. | ||
*/ | ||
export function normaliseAndValidateEnvironmentsConfig(config: Config) { | ||
if (config.env == undefined) { | ||
// There are no environments specified so there is nothing to do here. | ||
return; | ||
} | ||
const environments = config.env; | ||
for (const envKey of Object.keys(environments)) { | ||
const environment = environments[envKey]; | ||
// Given how TOML works, there should never be an environment containing nothing. | ||
// I.e. if there is a section in a TOML file, then the parser will create an object for it. | ||
// But it may be possible in the future if we change how the configuration is stored. | ||
assert( | ||
environment, | ||
`Environment ${envKey} is specified in the config but not defined.` | ||
); | ||
// Fall back on "inherited fields" from the config, if not specified in the environment. | ||
type InheritedField = keyof Omit< | ||
Config, | ||
"env" | "migrations" | "wasm_modules" | "site" | "dev" | ||
>; | ||
const inheritedFields: InheritedField[] = [ | ||
"name", | ||
"account_id", | ||
"workers_dev", | ||
"compatibility_date", | ||
"compatibility_flags", | ||
"zone_id", | ||
"routes", | ||
"route", | ||
"jsx_factory", | ||
"jsx_fragment", | ||
"triggers", | ||
"usage_model", | ||
]; | ||
for (const inheritedField of inheritedFields) { | ||
if (config[inheritedField] !== undefined) { | ||
if (environment[inheritedField] === undefined) { | ||
(environment[inheritedField] as typeof environment[InheritedField]) = | ||
config[inheritedField]; // TODO: - shallow or deep copy? | ||
} | ||
} | ||
} | ||
| { | ||
upload?: { | ||
format: "modules"; | ||
dir?: string; | ||
main?: string; | ||
rules?: { | ||
type: "ESModule" | "CommonJS" | "Text" | "Data" | "CompiledWasm"; | ||
globs: string[]; // can we use typescript for these patterns? | ||
fallthrough?: boolean; | ||
}; | ||
}; | ||
// Warn if there is a "required" field in the top level config that has not been specified specified in the environment. | ||
// These required fields are `vars`, `durable_objects`, `kv_namespaces`, and "r2_buckets". | ||
// Each of them has different characteristics that need to be checked. | ||
// `vars` is just an object | ||
if (config.vars !== undefined) { | ||
if (environment.vars === undefined) { | ||
console.warn( | ||
`In your configuration, "vars" exists at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "vars" is not inherited by environments.\n` + | ||
`Please add "vars" to "env.${envKey}".` | ||
); | ||
} else { | ||
for (const varField of Object.keys(config.vars)) { | ||
if (!(varField in environment.vars)) { | ||
console.warn( | ||
`In your configuration, "vars.${varField}" exists at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "vars" is not inherited by environments.\n` + | ||
`Please add "vars.${varField}" to "env.${envKey}".` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
); | ||
type UsageModel = "bundled" | "unbound"; | ||
// `durable_objects` is an object containing a `bindings` array | ||
if (config.durable_objects !== undefined) { | ||
if (environment.durable_objects === undefined) { | ||
console.warn( | ||
`In your configuration, "durable_objects.bindings" exists at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "durable_objects" is not inherited by environments.\n` + | ||
`Please add "durable_objects.bindings" to "env.${envKey}".` | ||
); | ||
} else { | ||
const envBindingNames = new Set( | ||
environment.durable_objects.bindings.map((b) => b.name) | ||
); | ||
for (const bindingName of config.durable_objects.bindings.map( | ||
(b) => b.name | ||
)) { | ||
if (!envBindingNames.has(bindingName)) { | ||
console.warn( | ||
`In your configuration, there is a durable_objects binding with name "${bindingName}" at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "durable_objects" is not inherited by environments.\n` + | ||
`Please add a binding for "${bindingName}" to "env.${envKey}.durable_objects.bindings".` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
type Env = { | ||
name?: string; // inherited | ||
account_id?: string; // inherited | ||
workers_dev?: boolean; // inherited | ||
compatibility_date?: string; // inherited | ||
compatibility_flags?: string[]; // inherited | ||
zone_id?: string; // inherited | ||
routes?: string[]; // inherited | ||
route?: string; // inherited | ||
webpack_config?: string; // inherited | ||
site?: Site; | ||
jsx_factory?: string; // inherited | ||
jsx_fragment?: string; // inherited | ||
// we should use typescript to parse cron patterns | ||
triggers?: { crons: Cron[] }; // inherited | ||
vars?: Vars; | ||
durable_objects?: { bindings: DurableObject[] }; | ||
kv_namespaces?: KVNamespace[]; | ||
usage_model?: UsageModel; // inherited | ||
}; | ||
// `kv_namespaces` contains an array of namespace bindings | ||
if (config.kv_namespaces !== undefined) { | ||
if (environment.kv_namespaces === undefined) { | ||
console.warn( | ||
`In your configuration, "kv_namespaces" exists at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "kv_namespaces" is not inherited by environments.\n` + | ||
`Please add "kv_namespaces" to "env.${envKey}".` | ||
); | ||
} else { | ||
const envBindings = new Set( | ||
environment.kv_namespaces.map((kvNamespace) => kvNamespace.binding) | ||
); | ||
for (const bindingName of config.kv_namespaces.map( | ||
(kvNamespace) => kvNamespace.binding | ||
)) { | ||
if (!envBindings.has(bindingName)) { | ||
console.warn( | ||
`In your configuration, there is a kv_namespaces with binding "${bindingName}" at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "kv_namespaces" is not inherited by environments.\n` + | ||
`Please add a binding for "${bindingName}" to "env.${envKey}.kv_namespaces".` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
export type Config = { | ||
name?: string; // inherited | ||
account_id?: string; // inherited | ||
// @deprecated Don't use this | ||
type?: Project; // top level | ||
compatibility_date?: string; // inherited | ||
compatibility_flags?: string[]; // inherited | ||
// -- there's some mutually exclusive logic for this next block, | ||
// but I didn't bother for now | ||
workers_dev?: boolean; // inherited | ||
zone_id?: string; // inherited | ||
routes?: string[]; // inherited | ||
route?: string; // inherited | ||
// -- end mutually exclusive stuff | ||
// @deprecated Don't use this | ||
webpack_config?: string; // inherited | ||
jsx_factory?: string; // inherited | ||
jsx_fragment?: string; // inherited | ||
vars?: Vars; | ||
migrations?: DOMigration[]; | ||
durable_objects?: { bindings: DurableObject[] }; | ||
kv_namespaces?: KVNamespace[]; | ||
site?: Site; // inherited | ||
// we should use typescript to parse cron patterns | ||
triggers?: { crons: Cron[] }; // inherited | ||
dev?: Dev; | ||
usage_model?: UsageModel; // inherited | ||
// top level | ||
build?: Build; | ||
env?: { [envName: string]: void | Env }; | ||
}; | ||
// `r2_buckets` contains an array of bucket bindings | ||
if (config.r2_buckets !== undefined) { | ||
if (environment.r2_buckets === undefined) { | ||
console.warn( | ||
`In your configuration, "r2_buckets" exists at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "r2_buckets" is not inherited by environments.\n` + | ||
`Please add "r2_buckets" to "env.${envKey}".` | ||
); | ||
} else { | ||
const envBindings = new Set( | ||
environment.r2_buckets.map((r2Bucket) => r2Bucket.binding) | ||
); | ||
for (const bindingName of config.r2_buckets.map( | ||
(r2Bucket) => r2Bucket.binding | ||
)) { | ||
if (!envBindings.has(bindingName)) { | ||
console.warn( | ||
`In your configuration, there is a r2_buckets with binding "${bindingName}" at the top level, but not on "env.${envKey}".\n` + | ||
`This is not what you probably want, since "r2_buckets" is not inherited by environments.\n` + | ||
`Please add a binding for "${bindingName}" to "env.${envKey}.r2_buckets".` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
@@ -1,6 +0,6 @@ | ||
import type { CfModule } from "./api/worker"; | ||
import crypto from "node:crypto"; | ||
import { readFile } from "node:fs/promises"; | ||
import path from "node:path"; | ||
import type { CfModule, CfScriptFormat } from "./api/worker"; | ||
import type esbuild from "esbuild"; | ||
import path from "node:path"; | ||
import { readFile } from "node:fs/promises"; | ||
import crypto from "node:crypto"; | ||
@@ -15,3 +15,5 @@ // This is a combination of an esbuild plugin and a mutable array | ||
export default function makeModuleCollector(): { | ||
export default function makeModuleCollector(props: { | ||
format: CfScriptFormat; | ||
}): { | ||
modules: CfModule[]; | ||
@@ -27,3 +29,3 @@ plugin: esbuild.Plugin; | ||
build.onStart(() => { | ||
// reset the moduels collection | ||
// reset the module collection array | ||
modules.splice(0); | ||
@@ -35,7 +37,6 @@ }); | ||
// we can expand this list later | ||
{ filter: /.*\.(pem|txt|html|wasm)$/ }, | ||
{ filter: /.*\.(wasm)$/ }, | ||
async (args: esbuild.OnResolveArgs) => { | ||
// take the file and massage it to a | ||
// transportable/manageable format | ||
const fileExt = path.extname(args.path); | ||
const filePath = path.join(args.resolveDir, args.path); | ||
@@ -51,10 +52,10 @@ const fileContent = await readFile(filePath); | ||
modules.push({ | ||
name: fileName, | ||
name: "./" + fileName, | ||
content: fileContent, | ||
type: fileExt === ".wasm" ? "compiled-wasm" : "text", | ||
type: "compiled-wasm", | ||
}); | ||
return { | ||
path: fileName, // change the reference to the changed module | ||
external: true, // mark it as external in the bundle | ||
path: "./" + fileName, // change the reference to the changed module | ||
external: props.format === "modules", // mark it as external in the bundle | ||
namespace: "wrangler-module-collector-ns", // just a tag, this isn't strictly necessary | ||
@@ -65,2 +66,21 @@ watchFiles: [filePath], // we also add the file to esbuild's watch list | ||
); | ||
if (props.format !== "modules") { | ||
build.onLoad( | ||
{ filter: /.*\.(wasm)$/ }, | ||
async (args: esbuild.OnLoadArgs) => { | ||
return { | ||
// We replace the the wasm module with an identifier | ||
// that we'll separately add to the form upload | ||
// as part of [wasm_modules]. This identifier has to be a valid | ||
// JS identifier, so we replace all non alphanumeric characters | ||
// with an underscore. | ||
contents: `export default ${args.path.replace( | ||
/[^a-zA-Z0-9_$]/g, | ||
"_" | ||
)};`, | ||
}; | ||
} | ||
); | ||
} | ||
}, | ||
@@ -67,0 +87,0 @@ }, |
@@ -1,28 +0,31 @@ | ||
import type { CfWorkerInit } from "./api/worker"; | ||
import assert from "node:assert"; | ||
import { existsSync, readFileSync } from "node:fs"; | ||
import path from "node:path"; | ||
import { URLSearchParams } from "node:url"; | ||
import { execaCommand } from "execa"; | ||
import tmp from "tmp-promise"; | ||
import { toFormData } from "./api/form_data"; | ||
import esbuild from "esbuild"; | ||
import tmp from "tmp-promise"; | ||
import { bundleWorker } from "./bundle"; | ||
import { fetchResult } from "./cfetch"; | ||
import guessWorkerFormat from "./guess-worker-format"; | ||
import { syncAssets } from "./sites"; | ||
import type { CfScriptFormat, CfWorkerInit } from "./api/worker"; | ||
import type { Config } from "./config"; | ||
import path from "path"; | ||
import { readFile } from "fs/promises"; | ||
import cfetch from "./cfetch"; | ||
import assert from "node:assert"; | ||
import { syncAssets } from "./sites"; | ||
import makeModuleCollector from "./module-collection"; | ||
import type { AssetPaths } from "./sites"; | ||
type CfScriptFormat = void | "modules" | "service-worker"; | ||
type Props = { | ||
config: Config; | ||
format?: CfScriptFormat; | ||
script?: string; | ||
name?: string; | ||
env?: string; | ||
public?: string; | ||
site?: string; | ||
triggers?: (string | number)[]; | ||
routes?: (string | number)[]; | ||
legacyEnv?: boolean; | ||
jsxFactory: void | string; | ||
jsxFragment: void | string; | ||
format: CfScriptFormat | undefined; | ||
entry: { file: string; directory: string }; | ||
name: string | undefined; | ||
env: string | undefined; | ||
compatibilityDate: string | undefined; | ||
compatibilityFlags: string[] | undefined; | ||
assetPaths: AssetPaths | undefined; | ||
triggers: (string | number)[] | undefined; | ||
routes: (string | number)[] | undefined; | ||
legacyEnv: boolean | undefined; | ||
jsxFactory: undefined | string; | ||
jsxFragment: undefined | string; | ||
experimentalPublic: boolean; | ||
}; | ||
@@ -35,17 +38,19 @@ | ||
export default async function publish(props: Props): Promise<void> { | ||
if (props.public && props.format === "service-worker") { | ||
// TODO: check config too | ||
throw new Error( | ||
"You cannot use the service worker format with a public directory." | ||
); | ||
} | ||
// TODO: warn if git/hg has uncommitted changes | ||
const { config } = props; | ||
const { | ||
account_id: accountId, | ||
build, | ||
// @ts-expect-error hidden | ||
__path__, | ||
} = config; | ||
const { account_id: accountId, workers_dev: deployToWorkersDev = true } = | ||
config; | ||
const envRootObj = | ||
props.env && config.env ? config.env[props.env] || {} : config; | ||
assert( | ||
envRootObj.compatibility_date || props.compatibilityDate, | ||
"A compatibility_date is required when publishing. Add one to your wrangler.toml file, or pass it in your terminal as --compatibility_date. See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information." | ||
); | ||
if (accountId === undefined) { | ||
throw new Error("No account_id provided."); | ||
} | ||
const triggers = props.triggers || config.triggers?.crons; | ||
@@ -65,284 +70,291 @@ const routes = props.routes || config.routes; | ||
let file: string; | ||
if (props.script) { | ||
file = props.script; | ||
} else { | ||
assert(build?.upload?.main, "missing main file"); | ||
file = path.join(path.dirname(__path__), build.upload.main); | ||
if (config.site?.["entry-point"]) { | ||
console.warn( | ||
"Deprecation notice: The `site.entry-point` config field is no longer used.\n" + | ||
"The entry-point should be specified via the command line (e.g. `wrangler publish path/to/script`) or the `build.upload.main` config field.\n" + | ||
"Please remove the `site.entry-point` field from the `wrangler.toml` file." | ||
); | ||
} | ||
if (props.legacyEnv) { | ||
scriptName += props.env ? `-${props.env}` : ""; | ||
} | ||
const envName = props.env ?? "production"; | ||
assert( | ||
!config.site || config.site.bucket, | ||
"A [site] definition requires a `bucket` field with a path to the site's public directory." | ||
); | ||
const destination = await tmp.dir({ unsafeCleanup: true }); | ||
try { | ||
if (props.legacyEnv) { | ||
scriptName += props.env ? `-${props.env}` : ""; | ||
} | ||
const envName = props.env ?? "production"; | ||
const moduleCollector = makeModuleCollector(); | ||
const result = await esbuild.build({ | ||
...(props.public | ||
? { | ||
stdin: { | ||
contents: ( | ||
await readFile( | ||
path.join(__dirname, "../static-asset-facade.js"), | ||
"utf8" | ||
) | ||
).replace("__ENTRY_POINT__", path.join(process.cwd(), file)), | ||
sourcefile: "static-asset-facade.js", | ||
resolveDir: path.dirname(file), | ||
}, | ||
} | ||
: { entryPoints: [file] }), | ||
bundle: true, | ||
nodePaths: props.public ? [path.join(__dirname, "../vendor")] : undefined, | ||
outdir: destination.path, | ||
external: ["__STATIC_CONTENT_MANIFEST"], | ||
format: "esm", | ||
sourcemap: true, | ||
metafile: true, | ||
conditions: ["worker", "browser"], | ||
loader: { | ||
".js": "jsx", | ||
}, | ||
plugins: [moduleCollector.plugin], | ||
...(jsxFactory && { jsxFactory }), | ||
...(jsxFragment && { jsxFragment }), | ||
}); | ||
if (props.config.build?.command) { | ||
// TODO: add a deprecation message here? | ||
console.log("running:", props.config.build.command); | ||
await execaCommand(props.config.build.command, { | ||
shell: true, | ||
stdout: "inherit", | ||
stderr: "inherit", | ||
timeout: 1000 * 30, | ||
...(props.config.build?.cwd && { cwd: props.config.build.cwd }), | ||
}); | ||
const chunks = Object.entries(result.metafile.outputs).find( | ||
([_path, { entryPoint }]) => | ||
entryPoint === | ||
(props.public | ||
? path.join(path.dirname(file), "static-asset-facade.js") | ||
: file) | ||
); | ||
let fileExists = false; | ||
try { | ||
// Use require.resolve to use node's resolution algorithm, | ||
// this lets us use paths without explicit .js extension | ||
// TODO: we should probably remove this, because it doesn't | ||
// take into consideration other extensions like .tsx, .ts, .jsx, etc | ||
fileExists = existsSync(require.resolve(props.entry.file)); | ||
} catch (e) { | ||
// fail silently, usually means require.resolve threw MODULE_NOT_FOUND | ||
} | ||
if (fileExists === false) { | ||
throw new Error(`Could not resolve "${props.entry.file}".`); | ||
} | ||
} | ||
const { format } = props; | ||
const bundle = { | ||
type: chunks[1].exports.length > 0 ? "esm" : "commonjs", | ||
exports: chunks[1].exports, | ||
}; | ||
const format = await guessWorkerFormat(props.entry, props.format); | ||
// TODO: instead of bundling the facade with the worker, we should just bundle the worker and expose it as a module. | ||
// That way we'll be able to accurately tell if this is a service worker or not. | ||
if (props.experimentalPublic && format === "service-worker") { | ||
// TODO: check config too | ||
throw new Error( | ||
"You cannot publish in the service worker format with a public directory." | ||
); | ||
} | ||
if (format === "modules" && bundle.type === "commonjs") { | ||
console.error("⎔ Cannot use modules with a commonjs bundle."); | ||
// TODO: a much better error message here, with what to do next | ||
return; | ||
} | ||
if (format === "service-worker" && bundle.type !== "esm") { | ||
console.error("⎔ Cannot use service-worker with a esm bundle."); | ||
// TODO: a much better error message here, with what to do next | ||
return; | ||
} | ||
if ("wasm_modules" in config && format === "modules") { | ||
throw new Error( | ||
"You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code" | ||
); | ||
} | ||
const content = await readFile(chunks[0], { encoding: "utf-8" }); | ||
await destination.cleanup(); | ||
const { modules, resolvedEntryPointPath, bundleType } = await bundleWorker( | ||
props.entry, | ||
props.experimentalPublic, | ||
destination.path, | ||
jsxFactory, | ||
jsxFragment, | ||
format | ||
); | ||
// if config.migrations | ||
// get current migration tag | ||
let migrations; | ||
if ("migrations" in config) { | ||
const scripts = await cfetch<{ id: string; migration_tag: string }[]>( | ||
`/accounts/${accountId}/workers/scripts` | ||
); | ||
const script = scripts.find((script) => script.id === scriptName); | ||
if (script?.migration_tag) { | ||
// was already published once | ||
const foundIndex = config.migrations.findIndex( | ||
(migration) => migration.tag === script.migration_tag | ||
); | ||
if (foundIndex === -1) { | ||
console.warn( | ||
`The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already delated it. Applying all available migrations to the script...` | ||
let content = readFileSync(resolvedEntryPointPath, { | ||
encoding: "utf-8", | ||
}); | ||
// if config.migrations | ||
// get current migration tag | ||
let migrations; | ||
if (config.migrations !== undefined) { | ||
const scripts = await fetchResult< | ||
{ id: string; migration_tag: string }[] | ||
>(`/accounts/${accountId}/workers/scripts`); | ||
const script = scripts.find(({ id }) => id === scriptName); | ||
if (script?.migration_tag) { | ||
// was already published once | ||
const foundIndex = config.migrations.findIndex( | ||
(migration) => migration.tag === script.migration_tag | ||
); | ||
if (foundIndex === -1) { | ||
console.warn( | ||
`The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...` | ||
); | ||
migrations = { | ||
old_tag: script.migration_tag, | ||
new_tag: config.migrations[config.migrations.length - 1].tag, | ||
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest), | ||
}; | ||
} else { | ||
migrations = { | ||
old_tag: script.migration_tag, | ||
new_tag: config.migrations[config.migrations.length - 1].tag, | ||
steps: config.migrations | ||
.slice(foundIndex + 1) | ||
.map(({ tag: _tag, ...rest }) => rest), | ||
}; | ||
} | ||
} else { | ||
migrations = { | ||
old_tag: script.migration_tag, | ||
new_tag: config.migrations[config.migrations.length - 1].tag, | ||
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest), | ||
}; | ||
} else { | ||
migrations = { | ||
old_tag: script.migration_tag, | ||
new_tag: config.migrations[config.migrations.length - 1].tag, | ||
steps: config.migrations | ||
.slice(foundIndex + 1) | ||
.map(({ tag: _tag, ...rest }) => rest), | ||
}; | ||
} | ||
} else { | ||
migrations = { | ||
new_tag: config.migrations[config.migrations.length - 1].tag, | ||
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest), | ||
}; | ||
} | ||
} | ||
const assets = | ||
props.public || props.site || props.config.site?.bucket // TODO: allow both | ||
? await syncAssets( | ||
accountId, | ||
scriptName, | ||
props.public || props.site || props.config.site?.bucket, | ||
false | ||
) | ||
: { manifest: undefined, namespace: undefined }; | ||
const assets = await syncAssets( | ||
accountId, | ||
scriptName, | ||
props.assetPaths, | ||
false, | ||
props.env | ||
); | ||
const envRootObj = props.env ? config.env[props.env] || {} : config; | ||
const bindings: CfWorkerInit["bindings"] = { | ||
kv_namespaces: (envRootObj.kv_namespaces || []).concat( | ||
assets.namespace | ||
? { binding: "__STATIC_CONTENT", id: assets.namespace } | ||
: [] | ||
), | ||
vars: envRootObj.vars, | ||
wasm_modules: config.wasm_modules, | ||
durable_objects: envRootObj.durable_objects, | ||
r2_buckets: envRootObj.r2_buckets, | ||
unsafe: envRootObj.unsafe?.bindings, | ||
}; | ||
const worker: CfWorkerInit = { | ||
name: scriptName, | ||
main: { | ||
name: path.basename(chunks[0]), | ||
content: content, | ||
type: bundle.type === "esm" ? "esm" : "commonjs", | ||
}, | ||
variables: { | ||
...(envRootObj?.vars || {}), | ||
...(envRootObj?.kv_namespaces || []).reduce( | ||
(obj, { binding, preview_id: _preview_id, id }) => { | ||
return { ...obj, [binding]: { namespaceId: id } }; | ||
}, | ||
{} | ||
), | ||
...(envRootObj?.durable_objects?.bindings || []).reduce( | ||
(obj, { name, class_name, script_name }) => { | ||
return { | ||
...obj, | ||
[name]: { class_name, ...(script_name && { script_name }) }, | ||
}; | ||
}, | ||
{} | ||
), | ||
...(assets.namespace | ||
? { __STATIC_CONTENT: { namespaceId: assets.namespace } } | ||
: {}), | ||
}, | ||
...(migrations && { migrations }), | ||
modules: assets.manifest | ||
? moduleCollector.modules.concat({ | ||
if (assets.manifest) { | ||
if (bundleType === "esm") { | ||
modules.push({ | ||
name: "__STATIC_CONTENT_MANIFEST", | ||
content: JSON.stringify(assets.manifest), | ||
type: "text", | ||
}) | ||
: moduleCollector.modules, | ||
compatibility_date: config.compatibility_date, | ||
compatibility_flags: config.compatibility_flags, | ||
usage_model: config.usage_model, | ||
}; | ||
}); | ||
} else { | ||
content = `const __STATIC_CONTENT_MANIFEST = ${JSON.stringify( | ||
assets.manifest | ||
)};\n${content}`; | ||
} | ||
} | ||
const start = Date.now(); | ||
function formatTime(duration: number) { | ||
return `(${(duration / 1000).toFixed(2)} sec)`; | ||
} | ||
const worker: CfWorkerInit = { | ||
name: scriptName, | ||
main: { | ||
name: path.basename(resolvedEntryPointPath), | ||
content: content, | ||
type: bundleType, | ||
}, | ||
bindings, | ||
migrations, | ||
modules, | ||
compatibility_date: config.compatibility_date, | ||
compatibility_flags: config.compatibility_flags, | ||
usage_model: config.usage_model, | ||
}; | ||
const notProd = !props.legacyEnv && props.env; | ||
const workerName = notProd ? `${scriptName} (${envName})` : scriptName; | ||
const workerUrl = notProd | ||
? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}` | ||
: `/accounts/${accountId}/workers/scripts/${scriptName}`; | ||
const start = Date.now(); | ||
const notProd = !props.legacyEnv && props.env; | ||
const workerName = notProd ? `${scriptName} (${envName})` : scriptName; | ||
const workerUrl = notProd | ||
? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}` | ||
: `/accounts/${accountId}/workers/scripts/${scriptName}`; | ||
// Upload the script so it has time to propogate. | ||
const { available_on_subdomain } = await cfetch( | ||
`${workerUrl}?available_on_subdomain=true`, | ||
{ | ||
method: "PUT", | ||
// @ts-expect-error: TODO: fix this type error! | ||
body: toFormData(worker), | ||
} | ||
); | ||
// Upload the script so it has time to propagate. | ||
const { available_on_subdomain } = await fetchResult( | ||
workerUrl, | ||
{ | ||
method: "PUT", | ||
body: toFormData(worker), | ||
}, | ||
new URLSearchParams({ available_on_subdomain: "true" }) | ||
); | ||
const uploadMs = Date.now() - start; | ||
console.log("Uploaded", workerName, formatTime(uploadMs)); | ||
const deployments: Promise<string[]>[] = []; | ||
const uploadMs = Date.now() - start; | ||
console.log("Uploaded", workerName, formatTime(uploadMs)); | ||
const deployments: Promise<string[]>[] = []; | ||
const userSubdomain = ( | ||
await cfetch<{ subdomain: string }>( | ||
`/accounts/${accountId}/workers/subdomain` | ||
) | ||
).subdomain; | ||
const userSubdomain = ( | ||
await fetchResult<{ subdomain: string }>( | ||
`/accounts/${accountId}/workers/subdomain` | ||
) | ||
).subdomain; | ||
const scriptURL = | ||
props.legacyEnv || !props.env | ||
? `${scriptName}.${userSubdomain}.workers.dev` | ||
: `${envName}.${scriptName}.${userSubdomain}.workers.dev`; | ||
// Enable the `workers.dev` subdomain. | ||
// TODO: Make this configurable. | ||
if (!available_on_subdomain) { | ||
deployments.push( | ||
cfetch(`${workerUrl}/subdomain`, { | ||
if (deployToWorkersDev) { | ||
// Deploy to a subdomain of `workers.dev` | ||
const scriptURL = | ||
props.legacyEnv || !props.env | ||
? `${scriptName}.${userSubdomain}.workers.dev` | ||
: `${envName}.${scriptName}.${userSubdomain}.workers.dev`; | ||
if (!available_on_subdomain) { | ||
// Enable the `workers.dev` subdomain. | ||
deployments.push( | ||
fetchResult(`${workerUrl}/subdomain`, { | ||
method: "POST", | ||
body: JSON.stringify({ enabled: true }), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}) | ||
.then(() => [scriptURL]) | ||
// Add a delay when the subdomain is first created. | ||
// This is to prevent an issue where a negative cache-hit | ||
// causes the subdomain to be unavailable for 30 seconds. | ||
// This is a temporary measure until we fix this on the edge. | ||
.then(async (url) => { | ||
await sleep(3000); | ||
return url; | ||
}) | ||
); | ||
} else { | ||
deployments.push(Promise.resolve([scriptURL])); | ||
} | ||
} else { | ||
// Disable the workers.dev deployment | ||
fetchResult(`${workerUrl}/subdomain`, { | ||
method: "POST", | ||
body: JSON.stringify({ enabled: true }), | ||
body: JSON.stringify({ enabled: false }), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}) | ||
.then(() => [scriptURL]) | ||
// Add a delay when the subdomain is first created. | ||
// This is to prevent an issue where a negative cache-hit | ||
// causes the subdomain to be unavailable for 30 seconds. | ||
// This is a temporary measure until we fix this on the edge. | ||
.then(async (url) => { | ||
await sleep(3000); | ||
return url; | ||
}); | ||
} | ||
// Update routing table for the script. | ||
if (routes && routes.length) { | ||
deployments.push( | ||
fetchResult(`${workerUrl}/routes`, { | ||
// TODO: PATCH will not delete previous routes on this script, | ||
// whereas PUT will. We need to decide on the default behaviour | ||
// and how to configure it. | ||
method: "PUT", | ||
body: JSON.stringify(routes.map((pattern) => ({ pattern }))), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}).then(() => { | ||
if (routes.length > 10) { | ||
return routes | ||
.slice(0, 9) | ||
.map(String) | ||
.concat([`...and ${routes.length - 10} more routes`]); | ||
} | ||
return routes.map(String); | ||
}) | ||
); | ||
} else { | ||
deployments.push(Promise.resolve([scriptURL])); | ||
} | ||
); | ||
} | ||
// Update routing table for the script. | ||
if (routes && routes.length) { | ||
deployments.push( | ||
cfetch(`${workerUrl}/routes`, { | ||
// TODO: PATCH will not delete previous routes on this script, | ||
// whereas PUT will. We need to decide on the default behaviour | ||
// and how to configure it. | ||
method: "PUT", | ||
body: JSON.stringify(routes.map((pattern) => ({ pattern }))), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}).then(() => { | ||
if (routes.length > 10) { | ||
return routes | ||
.slice(0, 9) | ||
.map(String) | ||
.concat([`...and ${routes.length - 10} more routes`]); | ||
} | ||
return routes.map(String); | ||
}) | ||
); | ||
} | ||
// Configure any schedules for the script. | ||
// TODO: rename this to `schedules`? | ||
if (triggers && triggers.length) { | ||
deployments.push( | ||
fetchResult(`${workerUrl}/schedules`, { | ||
// TODO: Unlike routes, this endpoint does not support PATCH. | ||
// So technically, this will override any previous schedules. | ||
// We should change the endpoint to support PATCH. | ||
method: "PUT", | ||
body: JSON.stringify(triggers.map((cron) => ({ cron }))), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}).then(() => triggers.map(String)) | ||
); | ||
} | ||
// Configure any schedules for the script. | ||
// TODO: rename this to `schedules`? | ||
if (triggers && triggers.length) { | ||
deployments.push( | ||
cfetch(`${workerUrl}/schedules`, { | ||
// TODO: Unlike routes, this endpoint does not support PATCH. | ||
// So technically, this will override any previous schedules. | ||
// We should change the endpoint to support PATCH. | ||
method: "PUT", | ||
body: JSON.stringify(triggers.map((cron) => ({ cron }))), | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}).then(() => triggers.map(String)) | ||
); | ||
} | ||
const targets = await Promise.all(deployments); | ||
const deployMs = Date.now() - start - uploadMs; | ||
if (!deployments.length) { | ||
return; | ||
if (deployments.length > 0) { | ||
console.log("Published", workerName, formatTime(deployMs)); | ||
for (const target of targets.flat()) { | ||
console.log(" ", target); | ||
} | ||
} else { | ||
console.log("No publish targets for", workerName, formatTime(deployMs)); | ||
} | ||
} finally { | ||
await destination.cleanup(); | ||
} | ||
} | ||
const targets = await Promise.all(deployments); | ||
const deployMs = Date.now() - start - uploadMs; | ||
console.log("Deployed", workerName, formatTime(deployMs)); | ||
for (const target of targets.flat()) { | ||
console.log(" ", target); | ||
} | ||
function formatTime(duration: number) { | ||
return `(${(duration / 1000).toFixed(2)} sec)`; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
14094207
12.86%107
50.7%18302
80.81%1
-50%1
-50%6
20%41
46.43%