diff --git a/.changeset/orange-cars-sip.md b/.changeset/orange-cars-sip.md new file mode 100644 index 0000000000..53fa531a19 --- /dev/null +++ b/.changeset/orange-cars-sip.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Custom builds for `dev` and `publish` diff --git a/.github/workflows/tests-typecheck.yml b/.github/workflows/tests-typecheck.yml index b5b4605bb2..fd775057ae 100644 --- a/.github/workflows/tests-typecheck.yml +++ b/.github/workflows/tests-typecheck.yml @@ -35,4 +35,3 @@ jobs: - name: Test run: npm run test - working-directory: packages/wrangler diff --git a/.prettierignore b/.prettierignore index 3a8d0d84a3..c9359e9dc4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ packages/wrangler/vendor/ packages/wrangler/wrangler-dist/ +packages/example-worker-app/dist/ \ No newline at end of file diff --git a/package.json b/package.json index 7ab81cb35e..d04308fa8a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "scripts": { "lint": "eslint packages/**", "check": "prettier packages/** --check && tsc && npm run lint", - "prettify": "prettier packages/** --write" + "prettify": "prettier packages/** --write", + "test": "npm run test --workspace=wrangler" }, "engines": { "node": ">=16.0.0" diff --git a/packages/example-worker-app/.gitignore b/packages/example-worker-app/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/packages/example-worker-app/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/example-worker-app/dep.js b/packages/example-worker-app/src/dep.js similarity index 100% rename from packages/example-worker-app/dep.js rename to packages/example-worker-app/src/dep.js diff --git a/packages/example-worker-app/index.js b/packages/example-worker-app/src/index.js similarity index 100% rename from packages/example-worker-app/index.js rename to packages/example-worker-app/src/index.js diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index b12866eeb5..16d4dfc916 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -1,5 +1,6 @@ import esbuild from "esbuild"; import { readFile } from "fs/promises"; +import { existsSync } from "fs"; import type { DirectoryResult } from "tmp-promise"; import tmp from "tmp-promise"; import type { CfPreviewToken } from "./api/preview"; @@ -16,7 +17,6 @@ import onExit from "signal-exit"; import { syncAssets } from "./sites"; import clipboardy from "clipboardy"; import http from "node:http"; -import serveStatic from "serve-static"; import commandExists from "command-exists"; import assert from "assert"; import { getAPIToken } from "./user"; @@ -24,6 +24,8 @@ import fetch from "node-fetch"; import makeModuleCollector from "./module-collection"; import { withErrorBoundary, useErrorHandler } from "react-error-boundary"; import { createHttpProxy } from "./proxy"; +import { execa } from "execa"; +import { watch } from "chokidar"; type CfScriptFormat = void | "modules" | "service-worker"; @@ -42,6 +44,11 @@ type Props = { compatibilityDate: void | string; compatibilityFlags: void | string[]; usageModel: void | "bundled" | "unbound"; + buildCommand: { + command?: undefined | string; + cwd?: undefined | string; + watch_dir?: undefined | string; + }; }; function Dev(props: Props): JSX.Element { @@ -54,8 +61,13 @@ function Dev(props: Props): JSX.Element { const apiToken = getAPIToken(); const directory = useTmpDir(); + // if there isn't a build command, we just return the entry immediately + // ideally there would be a conditional here, but the rules of hooks + // kinda forbid that, so we thread the entry through useCustomBuild + const entry = useCustomBuild(props.entry, props.buildCommand); + const bundle = useEsbuild({ - entry: props.entry, + entry, destination: directory, staticRoot: props.public, jsxFactory: props.jsxFactory, @@ -361,6 +373,77 @@ function useTmpDir(): string | void { return directory?.path; } +function runCommand() {} + +function useCustomBuild( + expectedEntry: string, + props: { + command?: undefined | string; + cwd?: undefined | string; + watch_dir?: undefined | string; + } +): void | string { + const [entry, setEntry] = useState( + // if there's no build command, just return the expected entry + props.command ? null : expectedEntry + ); + const { command, cwd, watch_dir } = props; + useEffect(() => { + if (!command) return; + let cmd, interval; + console.log("running:", command); + const commandPieces = command.split(" "); + cmd = execa(commandPieces[0], commandPieces.slice(1), { + ...(cwd && { cwd }), + stderr: "inherit", + stdout: "inherit", + }); + if (watch_dir) { + watch(watch_dir, { persistent: true, ignoreInitial: true }).on( + "all", + (_event, _path) => { + console.log(`The file ${path} changed, restarting build...`); + cmd.kill(); + cmd = execa(commandPieces[0], commandPieces.slice(1), { + ...(cwd && { cwd }), + stderr: "inherit", + stdout: "inherit", + }); + } + ); + } + + // check every so often whether `expectedEntry` exists + // if it does, we're done + const startedAt = Date.now(); + interval = setInterval(() => { + if (existsSync(expectedEntry)) { + clearInterval(interval); + setEntry(expectedEntry); + } else { + const elapsed = Date.now() - startedAt; + // timeout after 30 seconds of waiting + if (elapsed > 1000 * 60 * 30) { + console.error("⎔ Build timed out."); + clearInterval(interval); + cmd.kill(); + } + } + }, 200); + // TODO: we could probably timeout here after a while + + return () => { + if (cmd) { + cmd.kill(); + cmd = undefined; + } + clearInterval(interval); + interval = undefined; + }; + }, [command, cwd, expectedEntry, watch_dir]); + return entry; +} + type EsbuildBundle = { id: number; path: string; @@ -371,7 +454,7 @@ type EsbuildBundle = { }; function useEsbuild(props: { - entry: string; + entry: void | string; destination: string | void; staticRoot: void | string; jsxFactory: string | void; @@ -382,7 +465,7 @@ function useEsbuild(props: { useEffect(() => { let result: esbuild.BuildResult; async function build() { - if (!destination) return; + if (!destination || !entry) return; const moduleCollector = makeModuleCollector(); result = await esbuild.build({ entryPoints: [entry], diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index 8a4adda024..892eac2d5c 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -492,6 +492,7 @@ export async function main(argv: string[]): Promise { { const destination = await tmp.dir({ unsafeCleanup: true }); + if (props.config.build?.command) { + // TODO: add a deprecation message here? + console.log("running:", props.config.build.command); + const buildCommandPieces = props.config.build.command.split(" "); + await execa(buildCommandPieces[0], buildCommandPieces.slice(1), { + stdout: "inherit", + stderr: "inherit", + ...(props.config.build?.cwd && { cwd: props.config.build.cwd }), + }); + } + const moduleCollector = makeModuleCollector(); const result = await esbuild.build({ ...(props.public