Skip to content

Commit

Permalink
implement custom builds, for dev and publish (#149)
Browse files Browse the repository at this point in the history
* implement custom builds, for `dev` and `publish`

The code here is a bit jank, but we should probably refactor the way we pass arguments to dev and publish to 'fix' it properly. Anyway.

This PR adds custom builds to wrangler2 (https://developers.cloudflare.com/workers/cli-wrangler/configuration#build).
- In `wrangler.toml`, you can configure `build.command` and `build.cwd` and it'll be called before `publish` or `dev`. There's some discussion to be had whether we're going to deprecate this config commands, but we'll support it until then anyway.
- We _don't_ support `build.watch_dir`. We could, and I'm happy to add it after we discuss; but the idea is that watching shouldn't be wrangler's responsibility (which we've already broken with plain `dev`, and `pages dev`, so maybe we're wrong)
- You can pass the command after a last `--` in the cli (no support for `cwd` there). eg - `wrangler dev output/index.js -- some-build-command --out output/index.js`.

* Add a changeset

* fix lint error

* update snapshot tests

* remove `--`, add `watch_dir`

- removed the `--` option. We should have documentation showing how to do this with bash etc.
- Added support for `watch_dir`
- also added a definition for running tests from the root because it was annoying me to keep changing dirs to run tests

* log the name of the file that changed during a custom build
  • Loading branch information
threepointone committed Dec 21, 2021
1 parent 3752acf commit 78cd080
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-cars-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Custom builds for `dev` and `publish`
1 change: 0 additions & 1 deletion .github/workflows/tests-typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@ jobs:

- name: Test
run: npm run test
working-directory: packages/wrangler
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
packages/wrangler/vendor/
packages/wrangler/wrangler-dist/
packages/example-worker-app/dist/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/example-worker-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
File renamed without changes.
File renamed without changes.
91 changes: 87 additions & 4 deletions packages/wrangler/src/dev.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,14 +17,15 @@ 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";
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";

Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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<string | void>(
// 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;
Expand All @@ -371,7 +454,7 @@ type EsbuildBundle = {
};

function useEsbuild(props: {
entry: string;
entry: void | string;
destination: string | void;
staticRoot: void | string;
jsxFactory: string | void;
Expand All @@ -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],
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ export async function main(argv: string[]): Promise<void> {
<Dev
name={args.name || config.name}
entry={filename}
buildCommand={config.build || {}}
format={format}
initialMode={args.local ? "local" : "remote"}
jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
Expand Down
12 changes: 12 additions & 0 deletions packages/wrangler/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cfetch from "./cfetch";
import assert from "node:assert";
import { syncAssets } from "./sites";
import makeModuleCollector from "./module-collection";
import { execa } from "execa";

type CfScriptFormat = void | "modules" | "service-worker";

Expand Down Expand Up @@ -76,6 +77,17 @@ export default async function publish(props: Props): Promise<void> {

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
Expand Down

0 comments on commit 78cd080

Please sign in to comment.