diff --git a/.changeset/loud-meals-visit.md b/.changeset/loud-meals-visit.md new file mode 100644 index 000000000000..738ca3a2f862 --- /dev/null +++ b/.changeset/loud-meals-visit.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +feat: support `--experimental-public` in local mode + +`--experimental-public` is an abstraction over Workers Sites, and we can leverage miniflare's inbuilt support for Sites to serve assets in local mode. diff --git a/packages/wrangler/src/__tests__/dev.test.tsx b/packages/wrangler/src/__tests__/dev.test.tsx index f8f011c8d7c0..1d82735e377b 100644 --- a/packages/wrangler/src/__tests__/dev.test.tsx +++ b/packages/wrangler/src/__tests__/dev.test.tsx @@ -856,6 +856,33 @@ describe("wrangler dev", () => { } `); }); + + it("should error if --experimental-public and --site are used together", async () => { + writeWranglerToml({ + main: "./index.js", + }); + fs.writeFileSync("index.js", `export default {};`); + await expect( + runWrangler("publish --experimental-public abc --site xyz") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot use --experimental-public and a Site configuration together."` + ); + }); + + it("should error if --experimental-public and config.site are used together", async () => { + writeWranglerToml({ + main: "./index.js", + site: { + bucket: "xyz", + }, + }); + fs.writeFileSync("index.js", `export default {};`); + await expect( + runWrangler("publish --experimental-public abc") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot use --experimental-public and a Site configuration together."` + ); + }); }); describe("--inspect", () => { diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index 9454c86a6622..d7debaef5cee 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -1404,6 +1404,33 @@ addEventListener('fetch', event => {});` expect(std.err).toMatchInlineSnapshot(`""`); }); + it("should error if --experimental-public and --site are used together", async () => { + writeWranglerToml({ + main: "./index.js", + }); + writeWorkerSource(); + await expect( + runWrangler("publish --experimental-public abc --site xyz") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot use --experimental-public and a Site configuration together."` + ); + }); + + it("should error if --experimental-public and config.site are used together", async () => { + writeWranglerToml({ + main: "./index.js", + site: { + bucket: "xyz", + }, + }); + writeWorkerSource(); + await expect( + runWrangler("publish --experimental-public abc") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot use --experimental-public and a Site configuration together."` + ); + }); + it("should not contain backslash for assets with nested directories", async () => { const assets = [ { filePath: "subdir/file-1.txt", content: "Content of file-1" }, @@ -5311,6 +5338,7 @@ function mockUploadWorkerRequest( options: { available_on_subdomain?: boolean; expectedEntry?: string; + expectedMainModule?: string; expectedType?: "esm" | "sw"; expectedBindings?: unknown; expectedModules?: Record; @@ -5324,6 +5352,7 @@ function mockUploadWorkerRequest( const { available_on_subdomain = true, expectedEntry, + expectedMainModule = "index.js", expectedType = "esm", expectedBindings, expectedModules = {}, @@ -5358,7 +5387,7 @@ function mockUploadWorkerRequest( formBody.get("metadata") as string ) as WorkerMetadata; if (expectedType === "esm") { - expect(metadata.main_module).toEqual("index.js"); + expect(metadata.main_module).toEqual(expectedMainModule); } else { expect(metadata.body_part).toEqual("index.js"); } diff --git a/packages/wrangler/src/bundle.ts b/packages/wrangler/src/bundle.ts index 2572fffdaf09..7d4fb9844bf5 100644 --- a/packages/wrangler/src/bundle.ts +++ b/packages/wrangler/src/bundle.ts @@ -175,7 +175,8 @@ function getEntryPoint( path.join(__dirname, "../templates/static-asset-facade.js"), "utf8" ) - .replace("__ENTRY_POINT__", entryFile), + // on windows, escape backslashes in the path (`\`) + .replace("__ENTRY_POINT__", entryFile.replaceAll("\\", "\\\\")), sourcefile: "static-asset-facade.js", resolveDir: path.dirname(entryFile), }, diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index 85b880450cad..76ec2a64da95 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -20,7 +20,6 @@ import type { Config } from "../config"; import type { Entry } from "../entry"; import type { AssetPaths } from "../sites"; import type { CfWorkerInit } from "../worker"; -import type { EsbuildBundle } from "./use-esbuild"; export type DevProps = { name?: string; @@ -54,10 +53,6 @@ export type DevProps = { }; export function DevImplementation(props: DevProps): JSX.Element { - const directory = useTmpDir(); - - useCustomBuild(props.entry, props.build); - if (props.public && props.entry.format === "service-worker") { throw new Error( "You cannot use the service-worker format with a `public` directory." @@ -82,37 +77,16 @@ export function DevImplementation(props: DevProps): JSX.Element { ); } - const bundle = useEsbuild({ - entry: props.entry, - destination: directory, - staticRoot: props.public, - jsxFactory: props.jsxFactory, - rules: props.rules, - jsxFragment: props.jsxFragment, - serveAssetsFromWorker: !!props.public, - tsconfig: props.tsconfig, - minify: props.minify, - nodeCompat: props.nodeCompat, - }); - // only load the UI if we're running in a supported environment const { isRawModeSupported } = useStdin(); return isRawModeSupported ? ( - + ) : ( - + ); } -type InteractiveDevSessionProps = DevProps & { - bundle: EsbuildBundle | undefined; -}; - -function InteractiveDevSession(props: InteractiveDevSessionProps) { +function InteractiveDevSession(props: DevProps) { const toggles = useHotkeys( { local: props.initialMode === "local", @@ -145,13 +119,33 @@ function InteractiveDevSession(props: InteractiveDevSessionProps) { ); } -type DevSessionProps = InteractiveDevSessionProps & { local: boolean }; +type DevSessionProps = DevProps & { + local: boolean; +}; function DevSession(props: DevSessionProps) { + useCustomBuild(props.entry, props.build); + + const directory = useTmpDir(); + + const bundle = useEsbuild({ + entry: props.entry, + destination: directory, + staticRoot: props.public, + jsxFactory: props.jsxFactory, + rules: props.rules, + jsxFragment: props.jsxFragment, + // In dev for remote mode, we serve --experimental-assets from the local proxy before we send the request to the worker. + serveAssetsFromWorker: !!props.public && !!props.local, + tsconfig: props.tsconfig, + minify: props.minify, + nodeCompat: props.nodeCompat, + }); + return props.local ? ( 0) { throw new Error( "⎔ Service bindings are not yet supported in local mode." diff --git a/packages/wrangler/src/dev/remote.tsx b/packages/wrangler/src/dev/remote.tsx index 850ab1dc419c..afb63b3a7b68 100644 --- a/packages/wrangler/src/dev/remote.tsx +++ b/packages/wrangler/src/dev/remote.tsx @@ -40,6 +40,7 @@ export function Remote(props: { accountId: props.accountId, bindings: props.bindings, assetPaths: props.assetPaths, + public: props.public, port: props.port, compatibilityDate: props.compatibilityDate, compatibilityFlags: props.compatibilityFlags, @@ -74,6 +75,7 @@ export function useWorker(props: { accountId: string | undefined; bindings: CfWorkerInit["bindings"]; assetPaths: AssetPaths | undefined; + public: string | undefined; port: number; compatibilityDate: string | undefined; compatibilityFlags: string[] | undefined; @@ -131,7 +133,7 @@ export function useWorker(props: { // include it in the kv namespace name regardless (since there's no // concept of service environments for kv namespaces yet). name + (!props.legacyEnv && props.env ? `-${props.env}` : ""), - assetPaths, + props.public ? undefined : assetPaths, true, false ); // TODO: cancellable? @@ -223,6 +225,7 @@ export function useWorker(props: { accountId, port, assetPaths, + props.public, compatibilityDate, compatibilityFlags, usageModel, diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index d876f285f02c..61ce33fc5d5d 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -14,7 +14,6 @@ export type EsbuildBundle = { entry: Entry; type: "esm" | "commonjs"; modules: CfModule[]; - serveAssetsFromWorker: boolean; }; export function useEsbuild({ @@ -67,8 +66,7 @@ export function useEsbuild({ const { resolvedEntryPointPath, bundleType, modules, stop } = await bundleWorker(entry, destination, { - // In dev, we serve assets from the local proxy before we send the request to the worker. - serveAssetsFromWorker: false, + serveAssetsFromWorker, jsxFactory, jsxFragment, rules, @@ -87,7 +85,6 @@ export function useEsbuild({ path: resolvedEntryPointPath, type: bundleType, modules, - serveAssetsFromWorker, }); } diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index ed1730eaef4f..73a23b9c6329 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -1086,6 +1086,12 @@ function createCLIParser(argv: string[]) { ); } + if (args["experimental-public"] && (args.site || config.site)) { + throw new Error( + "Cannot use --experimental-public and a Site configuration together." + ); + } + if (args.public) { throw new Error( "The --public field has been renamed to --experimental-public, and will change behaviour in the future." @@ -1229,7 +1235,7 @@ function createCLIParser(argv: string[]) { accountId={config.account_id} assetPaths={getAssetPaths( config, - args.site, + args["experimental-public"] || args.site, args.siteInclude, args.siteExclude )} @@ -1384,23 +1390,29 @@ function createCLIParser(argv: string[]) { }, async (args) => { await printWranglerBanner(); + + const configPath = + (args.config as ConfigPath) || + (args.script && findWranglerToml(path.dirname(args.script))); + const config = readConfig(configPath, args); + const entry = await getEntry(args, config, "publish"); + if (args["experimental-public"]) { logger.warn( "The --experimental-public field is experimental and will change in the future." ); } + if (args["experimental-public"] && (args.site || config.site)) { + throw new Error( + "Cannot use --experimental-public and a Site configuration together." + ); + } if (args.public) { throw new Error( "The --public field has been renamed to --experimental-public, and will change behaviour in the future." ); } - const configPath = - (args.config as ConfigPath) || - (args.script && findWranglerToml(path.dirname(args.script))); - const config = readConfig(configPath, args); - const entry = await getEntry(args, config, "publish"); - if (args.latest) { logger.warn( "Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your wrangler.toml.\n"