From dcffc931d879b0332571ae8ee0c9d4e14c5c3064 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 28 Jun 2022 13:24:34 +0100 Subject: [PATCH] feat: `publish --no-build` (#1300) This adds a `--no-build` flag to `wrangler publish`. We've had a bunch of people asking to be able to upload a worker directly, without any modifications. While there are tradeoffs to this approach (any linked modules etc won't work), we understand that people who need this functionality are aware of it (and the usecases that have presented themselves all seem to match this). --- .changeset/spotty-onions-exist.md | 7 + packages/wrangler/src/__tests__/dev.test.tsx | 373 +++++++++--------- .../wrangler/src/__tests__/publish.test.ts | 13 + packages/wrangler/src/dev.tsx | 320 ++++++++------- packages/wrangler/src/dev/dev.tsx | 4 +- packages/wrangler/src/dev/use-esbuild.ts | 77 ++-- packages/wrangler/src/index.tsx | 260 ++++++------ packages/wrangler/src/preview.tsx | 1 + packages/wrangler/src/publish.ts | 56 ++- 9 files changed, 615 insertions(+), 496 deletions(-) create mode 100644 .changeset/spotty-onions-exist.md diff --git a/.changeset/spotty-onions-exist.md b/.changeset/spotty-onions-exist.md new file mode 100644 index 000000000000..d6f9e75b13b4 --- /dev/null +++ b/.changeset/spotty-onions-exist.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +feat: `publish --no-build` + +This adds a `--no-build` flag to `wrangler publish`. We've had a bunch of people asking to be able to upload a worker directly, without any modifications. While there are tradeoffs to this approach (any linked modules etc won't work), we understand that people who need this functionality are aware of it (and the usecases that have presented themselves all seem to match this). diff --git a/packages/wrangler/src/__tests__/dev.test.tsx b/packages/wrangler/src/__tests__/dev.test.tsx index 63b0f617bf5b..bdf478a6097d 100644 --- a/packages/wrangler/src/__tests__/dev.test.tsx +++ b/packages/wrangler/src/__tests__/dev.test.tsx @@ -38,20 +38,20 @@ describe("wrangler dev", () => { expect(std.out).toMatchInlineSnapshot(`""`); expect(std.warn.replaceAll(currentDate, "")) .toMatchInlineSnapshot(` - "▲ [WARNING] No compatibility_date was specified. Using today's date: . - - Add one to your wrangler.toml file: - \`\`\` - compatibility_date = \\"\\" - \`\`\` - or pass it in your terminal: - \`\`\` - --compatibility-date= - \`\`\` - See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information. - - " - `); + "▲ [WARNING] No compatibility_date was specified. Using today's date: . + + Add one to your wrangler.toml file: + \`\`\` + compatibility_date = \\"\\" + \`\`\` + or pass it in your terminal: + \`\`\` + --compatibility-date= + \`\`\` + See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information. + + " + `); expect(std.err).toMatchInlineSnapshot(`""`); }); @@ -79,14 +79,14 @@ describe("wrangler dev", () => { ); expect(std.out).toMatchInlineSnapshot(` - " - If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" - `); + " + If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" + `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] Missing entry-point: The entry-point should be specified via the command line (e.g. \`wrangler dev path/to/script\`) or the \`main\` config field. + "X [ERROR] Missing entry-point: The entry-point should be specified via the command line (e.g. \`wrangler dev path/to/script\`) or the \`main\` config field. - " - `); + " + `); }); it("should use `main` from the top-level environment", async () => { @@ -503,9 +503,9 @@ describe("wrangler dev", () => { await runWrangler("dev index.js"); expect(fs.readFileSync("index.js", "utf-8")).toMatchInlineSnapshot(` - "export default { fetch(){ return new Response(123) } } - " - `); + "export default { fetch(){ return new Response(123) } } + " + `); expect(std.out).toMatchInlineSnapshot( `"Running custom build: echo \\"custom build\\" && echo \\"export default { fetch(){ return new Response(123) } }\\" > index.js"` @@ -529,17 +529,17 @@ describe("wrangler dev", () => { The \`main\` property in wrangler.toml should point to the file generated by the custom build." `); expect(std.out).toMatchInlineSnapshot(` - "Running custom build: node -e \\"console.log('custom build');\\" + "Running custom build: node -e \\"console.log('custom build');\\" - If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" - `); + If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose" + `); expect(std.err).toMatchInlineSnapshot(` - "X [ERROR] The expected output file at \\"index.js\\" was not found after running custom build: node -e \\"console.log('custom build');\\". + "X [ERROR] The expected output file at \\"index.js\\" was not found after running custom build: node -e \\"console.log('custom build');\\". - The \`main\` property in wrangler.toml should point to the file generated by the custom build. + The \`main\` property in wrangler.toml should point to the file generated by the custom build. - " - `); + " + `); expect(std.warn).toMatchInlineSnapshot(`""`); }); }); @@ -570,13 +570,13 @@ describe("wrangler dev", () => { ); expect(std.out).toMatchInlineSnapshot(`""`); expect(std.warn).toMatchInlineSnapshot(` - "▲ [WARNING] Setting upstream-protocol to http is not currently implemented. + "▲ [WARNING] Setting upstream-protocol to http is not currently implemented. - If this is required in your project, please add your use case to the following issue: - https://github.com/cloudflare/wrangler2/issues/583. + If this is required in your project, please add your use case to the following issue: + https://github.com/cloudflare/wrangler2/issues/583. - " - `); + " + `); expect(std.err).toMatchInlineSnapshot(`""`); }); }); @@ -763,41 +763,41 @@ describe("wrangler dev", () => { await runWrangler("dev"); expect((Dev as jest.Mock).mock.calls[0][0].ip).toEqual("localhost"); expect(std.out).toMatchInlineSnapshot(` - "Your worker has access to the following bindings: - - Durable Objects: - - NAME_1: CLASS_1 - - NAME_2: CLASS_2 (defined in SCRIPT_A) - - NAME_3: CLASS_3 - - NAME_4: CLASS_4 (defined in SCRIPT_B)" - `); + "Your worker has access to the following bindings: + - Durable Objects: + - NAME_1: CLASS_1 + - NAME_2: CLASS_2 (defined in SCRIPT_A) + - NAME_3: CLASS_3 + - NAME_4: CLASS_4 (defined in SCRIPT_B)" + `); expect(std.warn).toMatchInlineSnapshot(` - "▲ [WARNING] Processing wrangler.toml configuration: + "▲ [WARNING] Processing wrangler.toml configuration: - - In wrangler.toml, you have configured [durable_objects] exported by this Worker (CLASS_1, - CLASS_3), but no [migrations] for them. This may not work as expected until you add a [migrations] - section to your wrangler.toml. Add this configuration to your wrangler.toml: + - In wrangler.toml, you have configured [durable_objects] exported by this Worker (CLASS_1, + CLASS_3), but no [migrations] for them. This may not work as expected until you add a [migrations] + section to your wrangler.toml. Add this configuration to your wrangler.toml: - \`\`\` - [[migrations]] - tag = \\"v1\\" # Should be unique for each entry - new_classes = [\\"CLASS_1\\", \\"CLASS_3\\"] - \`\`\` + \`\`\` + [[migrations]] + tag = \\"v1\\" # Should be unique for each entry + new_classes = [\\"CLASS_1\\", \\"CLASS_3\\"] + \`\`\` - Refer to - https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml - for more details. + Refer to + https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml + for more details. - ▲ [WARNING] WARNING: You have Durable Object bindings that are not defined locally in the worker being developed. + ▲ [WARNING] WARNING: You have Durable Object bindings that are not defined locally in the worker being developed. - Be aware that changes to the data stored in these Durable Objects will be permanent and affect the - live instances. - Remote Durable Objects that are affected: - - {\\"name\\":\\"NAME_2\\",\\"class_name\\":\\"CLASS_2\\",\\"script_name\\":\\"SCRIPT_A\\"} - - {\\"name\\":\\"NAME_4\\",\\"class_name\\":\\"CLASS_4\\",\\"script_name\\":\\"SCRIPT_B\\"} + Be aware that changes to the data stored in these Durable Objects will be permanent and affect the + live instances. + Remote Durable Objects that are affected: + - {\\"name\\":\\"NAME_2\\",\\"class_name\\":\\"CLASS_2\\",\\"script_name\\":\\"SCRIPT_A\\"} + - {\\"name\\":\\"NAME_4\\",\\"class_name\\":\\"CLASS_4\\",\\"script_name\\":\\"SCRIPT_B\\"} - " - `); + " + `); expect(std.err).toMatchInlineSnapshot(`""`); }); }); @@ -844,17 +844,17 @@ describe("wrangler dev", () => { UNQUOTED: "unquoted value", // Note that whitespace is trimmed }); expect(std.out).toMatchInlineSnapshot(` - "Using vars defined in .dev.vars - Your worker has access to the following bindings: - - Vars: - - VAR_1: \\"(hidden)\\" - - VAR_2: \\"original value 2\\" - - VAR_3: \\"(hidden)\\" - - VAR_MULTI_LINE_1: \\"(hidden)\\" - - VAR_MULTI_LINE_2: \\"(hidden)\\" - - EMPTY: \\"(hidden)\\" - - UNQUOTED: \\"(hidden)\\"" - `); + "Using vars defined in .dev.vars + Your worker has access to the following bindings: + - Vars: + - VAR_1: \\"(hidden)\\" + - VAR_2: \\"original value 2\\" + - VAR_3: \\"(hidden)\\" + - VAR_MULTI_LINE_1: \\"(hidden)\\" + - VAR_MULTI_LINE_2: \\"(hidden)\\" + - EMPTY: \\"(hidden)\\" + - UNQUOTED: \\"(hidden)\\"" + `); expect(std.warn).toMatchInlineSnapshot(`""`); expect(std.err).toMatchInlineSnapshot(`""`); }); @@ -869,54 +869,55 @@ describe("wrangler dev", () => { ); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "X [ERROR] Not enough arguments following: site - - ", - "out": " - wrangler dev [script] - - 👂 Start a local server for developing your worker - - Positionals: - script The path to an entry point for your worker [string] - - Flags: - -c, --config Path to .toml configuration file [string] - -h, --help Show help [boolean] - -v, --version Show version number [boolean] - - Options: - --name Name of the worker [string] - --format Choose an entry type [deprecated] [choices: \\"modules\\", \\"service-worker\\"] - -e, --env Perform on a specific environment [string] - --compatibility-date Date to use for compatibility checks [string] - --compatibility-flags, --compatibility-flag Flags to use for compatibility checks [array] - --latest Use the latest version of the worker runtime [boolean] [default: true] - --ip IP address to listen on, defaults to \`localhost\` [string] - --port Port to listen on [number] - --inspector-port Port for devtools to connect to [number] - --routes, --route Routes to upload [array] - --host Host to forward requests to, defaults to the zone of project [string] - --local-protocol Protocol to listen to requests on, defaults to http. [choices: \\"http\\", \\"https\\"] - --local-upstream Host to act as origin in local mode, defaults to dev.host or route [string] - --assets Static assets to be served [string] - --site Root folder of static assets for Workers Sites [string] - --site-include Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded. [array] - --site-exclude Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded. [array] - --upstream-protocol Protocol to forward requests to host on, defaults to https. [choices: \\"http\\", \\"https\\"] - --jsx-factory The function that is called for each JSX element [string] - --jsx-fragment The function that is called for each JSX fragment [string] - --tsconfig Path to a custom tsconfig.json file [string] - -l, --local Run on my machine [boolean] [default: false] - --minify Minify the script [boolean] - --node-compat Enable node.js compatibility [boolean] - --experimental-enable-local-persistence Enable persistence for this session (only for local mode) [boolean] - --inspect Enable dev tools [deprecated] [boolean]", - "warn": "", - } - `); + Object { + "debug": "", + "err": "X [ERROR] Not enough arguments following: site + + ", + "out": " + wrangler dev [script] + + 👂 Start a local server for developing your worker + + Positionals: + script The path to an entry point for your worker [string] + + Flags: + -c, --config Path to .toml configuration file [string] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + + Options: + --name Name of the worker [string] + --no-build Skip internal build steps and directly publish script [boolean] [default: false] + --format Choose an entry type [deprecated] [choices: \\"modules\\", \\"service-worker\\"] + -e, --env Perform on a specific environment [string] + --compatibility-date Date to use for compatibility checks [string] + --compatibility-flags, --compatibility-flag Flags to use for compatibility checks [array] + --latest Use the latest version of the worker runtime [boolean] [default: true] + --ip IP address to listen on, defaults to \`localhost\` [string] + --port Port to listen on [number] + --inspector-port Port for devtools to connect to [number] + --routes, --route Routes to upload [array] + --host Host to forward requests to, defaults to the zone of project [string] + --local-protocol Protocol to listen to requests on, defaults to http. [choices: \\"http\\", \\"https\\"] + --local-upstream Host to act as origin in local mode, defaults to dev.host or route [string] + --assets Static assets to be served [string] + --site Root folder of static assets for Workers Sites [string] + --site-include Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded. [array] + --site-exclude Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded. [array] + --upstream-protocol Protocol to forward requests to host on, defaults to https. [choices: \\"http\\", \\"https\\"] + --jsx-factory The function that is called for each JSX element [string] + --jsx-fragment The function that is called for each JSX fragment [string] + --tsconfig Path to a custom tsconfig.json file [string] + -l, --local Run on my machine [boolean] [default: false] + --minify Minify the script [boolean] + --node-compat Enable node.js compatibility [boolean] + --experimental-enable-local-persistence Enable persistence for this session (only for local mode) [boolean] + --inspect Enable dev tools [deprecated] [boolean]", + "warn": "", + } + `); }); it("should error if --assets and --site are used together", async () => { @@ -999,15 +1000,15 @@ describe("wrangler dev", () => { await runWrangler('dev --assets "./assets"'); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "", - "warn": "▲ [WARNING] The --assets argument is experimental and may change or break at any time - - ", - } - `); + Object { + "debug": "", + "err": "", + "out": "", + "warn": "▲ [WARNING] The --assets argument is experimental and may change or break at any time + + ", + } + `); }); it("should warn if config.assets is used", async () => { @@ -1019,17 +1020,17 @@ describe("wrangler dev", () => { await runWrangler("dev"); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "", - "warn": "▲ [WARNING] Processing wrangler.toml configuration: + Object { + "debug": "", + "err": "", + "out": "", + "warn": "▲ [WARNING] Processing wrangler.toml configuration: - - \\"assets\\" fields are experimental and may change or break at any time. + - \\"assets\\" fields are experimental and may change or break at any time. - ", - } - `); + ", + } + `); }); }); @@ -1038,15 +1039,15 @@ describe("wrangler dev", () => { fs.writeFileSync("index.js", `export default {};`); await runWrangler("dev index.js --inspect"); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "", - "warn": "▲ [WARNING] Passing --inspect is unnecessary, now you can always connect to devtools. - - ", - } - `); + Object { + "debug": "", + "err": "", + "out": "", + "warn": "▲ [WARNING] Passing --inspect is unnecessary, now you can always connect to devtools. + + ", + } + `); }); }); @@ -1061,23 +1062,23 @@ describe("wrangler dev", () => { fs.writeFileSync("index.js", `export default {};`); await runWrangler("dev index.js"); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "Your worker has access to the following bindings: - - Services: - - WorkerA: A - - WorkerB: B - staging", - "warn": "▲ [WARNING] Processing wrangler.toml configuration: + Object { + "debug": "", + "err": "", + "out": "Your worker has access to the following bindings: + - Services: + - WorkerA: A + - WorkerB: B - staging", + "warn": "▲ [WARNING] Processing wrangler.toml configuration: - - \\"services\\" fields are experimental and may change or break at any time. + - \\"services\\" fields are experimental and may change or break at any time. - ▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B@staging) + ▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B@staging) - ", - } - `); + ", + } + `); }); }); @@ -1092,23 +1093,23 @@ describe("wrangler dev", () => { fs.writeFileSync("index.js", `export default {};`); await runWrangler("dev index.js"); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "Your worker has access to the following bindings: - - Services: - - WorkerA: A - - WorkerB: B - staging", - "warn": "▲ [WARNING] Processing wrangler.toml configuration: + Object { + "debug": "", + "err": "", + "out": "Your worker has access to the following bindings: + - Services: + - WorkerA: A + - WorkerB: B - staging", + "warn": "▲ [WARNING] Processing wrangler.toml configuration: - - \\"services\\" fields are experimental and may change or break at any time. + - \\"services\\" fields are experimental and may change or break at any time. - ▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B@staging) + ▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B@staging) - ", - } - `); + ", + } + `); }); it("should mask vars that were overriden in .dev.vars", async () => { @@ -1128,18 +1129,18 @@ describe("wrangler dev", () => { fs.writeFileSync("index.js", `export default {};`); await runWrangler("dev index.js"); expect(std).toMatchInlineSnapshot(` - Object { - "debug": "", - "err": "", - "out": "Using vars defined in .dev.vars - Your worker has access to the following bindings: - - Vars: - - variable: \\"123\\" - - overriden: \\"(hidden)\\" - - SECRET: \\"(hidden)\\"", - "warn": "", - } - `); + Object { + "debug": "", + "err": "", + "out": "Using vars defined in .dev.vars + Your worker has access to the following bindings: + - Vars: + - variable: \\"123\\" + - overriden: \\"(hidden)\\" + - SECRET: \\"(hidden)\\"", + "warn": "", + } + `); }); }); }); diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index 7d57a2dc0188..d3e56eb2c175 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -6020,6 +6020,19 @@ addEventListener('fetch', event => {});` `); }); }); + + describe("--no-build", () => { + it("should not transform the source code before publishing it", async () => { + writeWranglerToml(); + const scriptContent = ` + import X from '@cloudflare/no-such-package'; // let's add an import that doesn't exist + const xyz = 123; // a statement that would otherwise be compiled out + `; + fs.writeFileSync("index.js", scriptContent); + await runWrangler("publish index.js --no-build --dry-run --outdir dist"); + expect(fs.readFileSync("dist/index.js", "utf-8")).toMatch(scriptContent); + }); + }); }); /** Write mock assets to the file system so they can be uploaded. */ diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index 81a875079511..edb498bea6ba 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -29,6 +29,7 @@ interface DevArgs { config?: string; script?: string; name?: string; + build?: boolean; format?: string; env?: string; "compatibility-date"?: string; @@ -59,156 +60,174 @@ interface DevArgs { } export function devOptions(yargs: Argv): Argv { - return yargs - .positional("script", { - describe: "The path to an entry point for your worker", - type: "string", - }) - .option("name", { - describe: "Name of the worker", - type: "string", - requiresArg: true, - }) - .option("format", { - choices: ["modules", "service-worker"] as const, - describe: "Choose an entry type", - deprecated: true, - }) - .option("env", { - describe: "Perform on a specific environment", - type: "string", - requiresArg: true, - alias: "e", - }) - .option("compatibility-date", { - describe: "Date to use for compatibility checks", - type: "string", - requiresArg: true, - }) - .option("compatibility-flags", { - describe: "Flags to use for compatibility checks", - alias: "compatibility-flag", - type: "string", - requiresArg: true, - array: true, - }) - .option("latest", { - describe: "Use the latest version of the worker runtime", - type: "boolean", - default: true, - }) - .option("ip", { - describe: "IP address to listen on, defaults to `localhost`", - type: "string", - requiresArg: true, - }) - .option("port", { - describe: "Port to listen on", - type: "number", - }) - .option("inspector-port", { - describe: "Port for devtools to connect to", - type: "number", - }) - .option("routes", { - describe: "Routes to upload", - alias: "route", - type: "string", - requiresArg: true, - array: true, - }) - .option("host", { - type: "string", - requiresArg: true, - describe: "Host to forward requests to, defaults to the zone of project", - }) - .option("local-protocol", { - describe: "Protocol to listen to requests on, defaults to http.", - choices: ["http", "https"] as const, - }) - .options("local-upstream", { - type: "string", - describe: - "Host to act as origin in local mode, defaults to dev.host or route", - }) - .option("experimental-public", { - describe: "Static assets to be served", - type: "string", - requiresArg: true, - deprecated: true, - hidden: true, - }) - .option("assets", { - describe: "Static assets to be served", - type: "string", - requiresArg: true, - }) - .option("site", { - describe: "Root folder of static assets for Workers Sites", - type: "string", - requiresArg: true, - }) - .option("site-include", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", - type: "string", - requiresArg: true, - array: true, - }) - .option("site-exclude", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", - type: "string", - requiresArg: true, - array: true, - }) - .option("upstream-protocol", { - describe: "Protocol to forward requests to host on, defaults to https.", - choices: ["http", "https"] as const, - }) - .option("jsx-factory", { - describe: "The function that is called for each JSX element", - type: "string", - requiresArg: true, - }) - .option("jsx-fragment", { - describe: "The function that is called for each JSX fragment", - type: "string", - requiresArg: true, - }) - .option("tsconfig", { - describe: "Path to a custom tsconfig.json file", - type: "string", - requiresArg: true, - }) - .option("local", { - alias: "l", - describe: "Run on my machine", - type: "boolean", - default: false, // I bet this will a point of contention. We'll revisit it. - }) - .option("minify", { - describe: "Minify the script", - type: "boolean", - }) - .option("node-compat", { - describe: "Enable node.js compatibility", - type: "boolean", - }) - .option("experimental-enable-local-persistence", { - describe: "Enable persistence for this session (only for local mode)", - type: "boolean", - }) - .option("inspect", { - describe: "Enable dev tools", - type: "boolean", - deprecated: true, - }) - .option("legacy-env", { - type: "boolean", - describe: "Use legacy environments", - hidden: true, - }); + return ( + yargs + .positional("script", { + describe: "The path to an entry point for your worker", + type: "string", + }) + .option("name", { + describe: "Name of the worker", + type: "string", + requiresArg: true, + }) + // We want to have a --no-build flag, but yargs requires that + // we also have a --build flag (that it adds the --no to by itself) + // So we make a --build flag, but hide it, and then add a --no-build flag + // that's visible to the user but doesn't "do" anything. + .option("build", { + describe: "Run wrangler's compilation step before publishing", + type: "boolean", + default: true, + hidden: true, + }) + .option("no-build", { + describe: "Skip internal build steps and directly publish script", + type: "boolean", + default: false, + }) + .option("format", { + choices: ["modules", "service-worker"] as const, + describe: "Choose an entry type", + deprecated: true, + }) + .option("env", { + describe: "Perform on a specific environment", + type: "string", + requiresArg: true, + alias: "e", + }) + .option("compatibility-date", { + describe: "Date to use for compatibility checks", + type: "string", + requiresArg: true, + }) + .option("compatibility-flags", { + describe: "Flags to use for compatibility checks", + alias: "compatibility-flag", + type: "string", + requiresArg: true, + array: true, + }) + .option("latest", { + describe: "Use the latest version of the worker runtime", + type: "boolean", + default: true, + }) + .option("ip", { + describe: "IP address to listen on, defaults to `localhost`", + type: "string", + requiresArg: true, + }) + .option("port", { + describe: "Port to listen on", + type: "number", + }) + .option("inspector-port", { + describe: "Port for devtools to connect to", + type: "number", + }) + .option("routes", { + describe: "Routes to upload", + alias: "route", + type: "string", + requiresArg: true, + array: true, + }) + .option("host", { + type: "string", + requiresArg: true, + describe: + "Host to forward requests to, defaults to the zone of project", + }) + .option("local-protocol", { + describe: "Protocol to listen to requests on, defaults to http.", + choices: ["http", "https"] as const, + }) + .options("local-upstream", { + type: "string", + describe: + "Host to act as origin in local mode, defaults to dev.host or route", + }) + .option("experimental-public", { + describe: "Static assets to be served", + type: "string", + requiresArg: true, + deprecated: true, + hidden: true, + }) + .option("assets", { + describe: "Static assets to be served", + type: "string", + requiresArg: true, + }) + .option("site", { + describe: "Root folder of static assets for Workers Sites", + type: "string", + requiresArg: true, + }) + .option("site-include", { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", + type: "string", + requiresArg: true, + array: true, + }) + .option("site-exclude", { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", + type: "string", + requiresArg: true, + array: true, + }) + .option("upstream-protocol", { + describe: "Protocol to forward requests to host on, defaults to https.", + choices: ["http", "https"] as const, + }) + .option("jsx-factory", { + describe: "The function that is called for each JSX element", + type: "string", + requiresArg: true, + }) + .option("jsx-fragment", { + describe: "The function that is called for each JSX fragment", + type: "string", + requiresArg: true, + }) + .option("tsconfig", { + describe: "Path to a custom tsconfig.json file", + type: "string", + requiresArg: true, + }) + .option("local", { + alias: "l", + describe: "Run on my machine", + type: "boolean", + default: false, // I bet this will a point of contention. We'll revisit it. + }) + .option("minify", { + describe: "Minify the script", + type: "boolean", + }) + .option("node-compat", { + describe: "Enable node.js compatibility", + type: "boolean", + }) + .option("experimental-enable-local-persistence", { + describe: "Enable persistence for this session (only for local mode)", + type: "boolean", + }) + .option("inspect", { + describe: "Enable dev tools", + type: "boolean", + deprecated: true, + }) + .option("legacy-env", { + type: "boolean", + describe: "Use legacy environments", + hidden: true, + }) + ); } export async function devHandler(args: ArgumentsCamelCase) { @@ -412,6 +431,7 @@ export async function devHandler(args: ArgumentsCamelCase) { return ( (); const { exit } = useApp(); useEffect(() => { let stopWatching: (() => void) | undefined = undefined; + function updateBundle() { + // nothing really changes here, so let's increment the id + // to change the return object's identity + setBundle((previousBundle) => { + assert( + previousBundle, + "Rebuild triggered with no previous build available" + ); + return { ...previousBundle, id: previousBundle.id + 1 }; + }); + } + const watchMode: WatchMode = { async onRebuild(error) { if (error) logger.error("Watch build failed:", error); else { - // nothing really changes here, so let's increment the id - // to change the return object's identity - setBundle((previousBundle) => { - assert( - previousBundle, - "Rebuild triggered with no previous build available" - ); - return { ...previousBundle, id: previousBundle.id + 1 }; - }); + updateBundle(); } }, }; @@ -64,23 +71,48 @@ export function useEsbuild({ async function build() { if (!destination) return; - const { resolvedEntryPointPath, bundleType, modules, stop } = - await bundleWorker(entry, destination, { - serveAssetsFromWorker, - jsxFactory, - jsxFragment, - rules, - watch: watchMode, - tsconfig, - minify, - nodeCompat, - define, - checkFetch: true, - }); + const { + resolvedEntryPointPath, + bundleType, + modules, + stop, + }: Awaited> = noBuild + ? { + modules: [], + resolvedEntryPointPath: entry.file, + bundleType: entry.format === "modules" ? "esm" : "commonjs", + stop: undefined, + } + : await bundleWorker(entry, destination, { + serveAssetsFromWorker, + jsxFactory, + jsxFragment, + rules, + watch: watchMode, + tsconfig, + minify, + nodeCompat, + define, + checkFetch: true, + }); // Capture the `stop()` method to use as the `useEffect()` destructor. stopWatching = stop; + // if "noBuild" is true, then we need to manually watch the entry point and + // trigger "builds" when it changes + if (noBuild) { + const watcher = watch(entry.file, { + persistent: true, + }).on("change", async (_event) => { + updateBundle(); + }); + + stopWatching = () => { + watcher.close(); + }; + } + setBundle({ id: 0, entry, @@ -109,6 +141,7 @@ export function useEsbuild({ rules, tsconfig, exit, + noBuild, minify, nodeCompat, define, diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index dabe3ad0777d..a387241f7c80 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -353,127 +353,144 @@ function createCLIParser(argv: string[]) { "publish [script]", "🆙 Publish your Worker to Cloudflare.", (yargs) => { - return yargs - .option("env", { - type: "string", - requiresArg: true, - describe: "Perform on a specific environment", - alias: "e", - }) - .positional("script", { - describe: "The path to an entry point for your worker", - type: "string", - requiresArg: true, - }) - .option("name", { - describe: "Name of the worker", - type: "string", - requiresArg: true, - }) - .option("outdir", { - describe: "Output directory for the bundled worker", - type: "string", - requiresArg: true, - }) - .option("format", { - choices: ["modules", "service-worker"] as const, - describe: "Choose an entry type", - deprecated: true, - }) - .option("compatibility-date", { - describe: "Date to use for compatibility checks", - type: "string", - requiresArg: true, - }) - .option("compatibility-flags", { - describe: "Flags to use for compatibility checks", - alias: "compatibility-flag", - type: "string", - requiresArg: true, - array: true, - }) - .option("latest", { - describe: "Use the latest version of the worker runtime", - type: "boolean", - default: false, - }) - .option("experimental-public", { - describe: "Static assets to be served", - type: "string", - requiresArg: true, - deprecated: true, - hidden: true, - }) - .option("assets", { - describe: "Static assets to be served", - type: "string", - requiresArg: true, - }) - .option("site", { - describe: "Root folder of static assets for Workers Sites", - type: "string", - requiresArg: true, - }) - .option("site-include", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", - type: "string", - requiresArg: true, - array: true, - }) - .option("site-exclude", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", - type: "string", - requiresArg: true, - array: true, - }) - .option("triggers", { - describe: "cron schedules to attach", - alias: ["schedule", "schedules"], - type: "string", - requiresArg: true, - array: true, - }) - .option("routes", { - describe: "Routes to upload", - alias: "route", - type: "string", - requiresArg: true, - array: true, - }) - .option("jsx-factory", { - describe: "The function that is called for each JSX element", - type: "string", - requiresArg: true, - }) - .option("jsx-fragment", { - describe: "The function that is called for each JSX fragment", - type: "string", - requiresArg: true, - }) - .option("tsconfig", { - describe: "Path to a custom tsconfig.json file", - type: "string", - requiresArg: true, - }) - .option("minify", { - describe: "Minify the script", - type: "boolean", - }) - .option("node-compat", { - describe: "Enable node.js compatibility", - type: "boolean", - }) - .option("dry-run", { - describe: "Don't actually publish", - type: "boolean", - }) - .option("legacy-env", { - type: "boolean", - describe: "Use legacy environments", - hidden: true, - }); + return ( + yargs + .option("env", { + type: "string", + requiresArg: true, + describe: "Perform on a specific environment", + alias: "e", + }) + .positional("script", { + describe: "The path to an entry point for your worker", + type: "string", + requiresArg: true, + }) + .option("name", { + describe: "Name of the worker", + type: "string", + requiresArg: true, + }) + // We want to have a --no-build flag, but yargs requires that + // we also have a --build flag (that it adds the --no to by itself) + // So we make a --build flag, but hide it, and then add a --no-build flag + // that's visible to the user but doesn't "do" anything. + .option("build", { + describe: "Run wrangler's compilation step before publishing", + type: "boolean", + default: true, + hidden: true, + }) + .option("no-build", { + describe: "Skip internal build steps and directly publish script", + type: "boolean", + default: false, + }) + .option("outdir", { + describe: "Output directory for the bundled worker", + type: "string", + requiresArg: true, + }) + .option("format", { + choices: ["modules", "service-worker"] as const, + describe: "Choose an entry type", + deprecated: true, + }) + .option("compatibility-date", { + describe: "Date to use for compatibility checks", + type: "string", + requiresArg: true, + }) + .option("compatibility-flags", { + describe: "Flags to use for compatibility checks", + alias: "compatibility-flag", + type: "string", + requiresArg: true, + array: true, + }) + .option("latest", { + describe: "Use the latest version of the worker runtime", + type: "boolean", + default: false, + }) + .option("experimental-public", { + describe: "Static assets to be served", + type: "string", + requiresArg: true, + deprecated: true, + hidden: true, + }) + .option("assets", { + describe: "Static assets to be served", + type: "string", + requiresArg: true, + }) + .option("site", { + describe: "Root folder of static assets for Workers Sites", + type: "string", + requiresArg: true, + }) + .option("site-include", { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", + type: "string", + requiresArg: true, + array: true, + }) + .option("site-exclude", { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", + type: "string", + requiresArg: true, + array: true, + }) + .option("triggers", { + describe: "cron schedules to attach", + alias: ["schedule", "schedules"], + type: "string", + requiresArg: true, + array: true, + }) + .option("routes", { + describe: "Routes to upload", + alias: "route", + type: "string", + requiresArg: true, + array: true, + }) + .option("jsx-factory", { + describe: "The function that is called for each JSX element", + type: "string", + requiresArg: true, + }) + .option("jsx-fragment", { + describe: "The function that is called for each JSX fragment", + type: "string", + requiresArg: true, + }) + .option("tsconfig", { + describe: "Path to a custom tsconfig.json file", + type: "string", + requiresArg: true, + }) + .option("minify", { + describe: "Minify the script", + type: "boolean", + }) + .option("node-compat", { + describe: "Enable node.js compatibility", + type: "boolean", + }) + .option("dry-run", { + describe: "Don't actually publish", + type: "boolean", + }) + .option("legacy-env", { + type: "boolean", + describe: "Use legacy environments", + hidden: true, + }) + ); }, async (args) => { await printWranglerBanner(); @@ -546,6 +563,7 @@ function createCLIParser(argv: string[]) { isWorkersSite: Boolean(args.site || config.site), outDir: args.outdir, dryRun: args.dryRun, + noBuild: !args.build, }); } ); diff --git a/packages/wrangler/src/preview.tsx b/packages/wrangler/src/preview.tsx index 74aae96798a9..b7efb6408fe2 100644 --- a/packages/wrangler/src/preview.tsx +++ b/packages/wrangler/src/preview.tsx @@ -79,6 +79,7 @@ export async function previewHandler(args: ArgumentsCamelCase) { routes={undefined} legacyEnv={isLegacyEnv(config)} build={config.build || {}} + noBuild={false} define={config.define} minify={undefined} nodeCompat={config.node_compat} diff --git a/packages/wrangler/src/publish.ts b/packages/wrangler/src/publish.ts index 6d358fd7b527..e3aec0367f39 100644 --- a/packages/wrangler/src/publish.ts +++ b/packages/wrangler/src/publish.ts @@ -46,6 +46,7 @@ type Props = { nodeCompat: boolean | undefined; outDir: string | undefined; dryRun: boolean | undefined; + noBuild: boolean | undefined; }; type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute; @@ -340,22 +341,45 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ); } try { - const { modules, resolvedEntryPointPath, bundleType } = await bundleWorker( - props.entry, - typeof destination === "string" ? destination : destination.path, - { - serveAssetsFromWorker: - !props.isWorkersSite && Boolean(props.assetPaths), - jsxFactory, - jsxFragment, - rules: props.rules, - tsconfig: props.tsconfig ?? config.tsconfig, - minify, - nodeCompat, - define: config.define, - checkFetch: false, - } - ); + if (props.noBuild) { + // if we're not building, let's just copy the entry to the destination directory + const destinationDir = + typeof destination === "string" ? destination : destination.path; + mkdirSync(destinationDir, { recursive: true }); + writeFileSync( + path.join(destinationDir, path.basename(props.entry.file)), + readFileSync(props.entry.file, "utf-8") + ); + } + + const { + modules, + resolvedEntryPointPath, + bundleType, + }: Awaited> = props.noBuild + ? // we can skip the whole bundling step and mock a bundle here + { + modules: [], + resolvedEntryPointPath: props.entry.file, + bundleType: props.entry.format === "modules" ? "esm" : "commonjs", + stop: undefined, + } + : await bundleWorker( + props.entry, + typeof destination === "string" ? destination : destination.path, + { + serveAssetsFromWorker: + !props.isWorkersSite && Boolean(props.assetPaths), + jsxFactory, + jsxFragment, + rules: props.rules, + tsconfig: props.tsconfig ?? config.tsconfig, + minify, + nodeCompat, + define: config.define, + checkFetch: false, + } + ); const content = readFileSync(resolvedEntryPointPath, { encoding: "utf-8",