Skip to content

Commit cda8ca9

Browse files
committed
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.
1 parent 4eb70f9 commit cda8ca9

File tree

9 files changed

+152
-51
lines changed

9 files changed

+152
-51
lines changed

.changeset/loud-meals-visit.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
feat: support `--experimental-public` in local mode
6+
7+
`--experimental-public` is an abstraction over Workers Sites, and we can leverage miniflare's inbuilt support for Sites to serve assets in local mode.

packages/wrangler/src/__tests__/dev.test.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,33 @@ describe("wrangler dev", () => {
856856
}
857857
`);
858858
});
859+
860+
it("should error if --experimental-public and --site are used together", async () => {
861+
writeWranglerToml({
862+
main: "./index.js",
863+
});
864+
fs.writeFileSync("index.js", `export default {};`);
865+
await expect(
866+
runWrangler("publish --experimental-public abc --site xyz")
867+
).rejects.toThrowErrorMatchingInlineSnapshot(
868+
`"Cannot use --experimental-public and a Site configuration together."`
869+
);
870+
});
871+
872+
it("should error if --experimental-public and config.site are used together", async () => {
873+
writeWranglerToml({
874+
main: "./index.js",
875+
site: {
876+
bucket: "xyz",
877+
},
878+
});
879+
fs.writeFileSync("index.js", `export default {};`);
880+
await expect(
881+
runWrangler("publish --experimental-public abc")
882+
).rejects.toThrowErrorMatchingInlineSnapshot(
883+
`"Cannot use --experimental-public and a Site configuration together."`
884+
);
885+
});
859886
});
860887

861888
describe("--inspect", () => {

packages/wrangler/src/__tests__/publish.test.ts

+66-1
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,69 @@ addEventListener('fetch', event => {});`
14041404
expect(std.err).toMatchInlineSnapshot(`""`);
14051405
});
14061406

1407+
it("should upload all the files in the directory specified by `--experimental-public`", async () => {
1408+
const assets = [
1409+
{ filePath: "assets/file-1.txt", content: "Content of file-1" },
1410+
{ filePath: "assets/file-2.txt", content: "Content of file-2" },
1411+
];
1412+
const kvNamespace = {
1413+
title: "__test-name-workers_sites_assets",
1414+
id: "__test-name-workers_sites_assets-id",
1415+
};
1416+
writeWranglerToml({
1417+
main: "./index.js",
1418+
});
1419+
writeWorkerSource();
1420+
writeAssets(assets);
1421+
mockUploadWorkerRequest({
1422+
expectedMainModule: "stdin.js",
1423+
});
1424+
mockSubDomainRequest();
1425+
mockListKVNamespacesRequest(kvNamespace);
1426+
mockKeyListRequest(kvNamespace.id, []);
1427+
mockUploadAssetsToKVRequest(kvNamespace.id, assets);
1428+
await runWrangler("publish --experimental-public assets");
1429+
1430+
expect(std.out).toMatchInlineSnapshot(`
1431+
"Reading assets/file-1.txt...
1432+
Uploading as assets/file-1.2ca234f380.txt...
1433+
Reading assets/file-2.txt...
1434+
Uploading as assets/file-2.5938485188.txt...
1435+
↗️ Done syncing assets
1436+
Uploaded test-name (TIMINGS)
1437+
Published test-name (TIMINGS)
1438+
test-name.test-sub-domain.workers.dev"
1439+
`);
1440+
expect(std.err).toMatchInlineSnapshot(`""`);
1441+
});
1442+
1443+
it("should error if --experimental-public and --site are used together", async () => {
1444+
writeWranglerToml({
1445+
main: "./index.js",
1446+
});
1447+
writeWorkerSource();
1448+
await expect(
1449+
runWrangler("publish --experimental-public abc --site xyz")
1450+
).rejects.toThrowErrorMatchingInlineSnapshot(
1451+
`"Cannot use --experimental-public and a Site configuration together."`
1452+
);
1453+
});
1454+
1455+
it("should error if --experimental-public and config.site are used together", async () => {
1456+
writeWranglerToml({
1457+
main: "./index.js",
1458+
site: {
1459+
bucket: "xyz",
1460+
},
1461+
});
1462+
writeWorkerSource();
1463+
await expect(
1464+
runWrangler("publish --experimental-public abc")
1465+
).rejects.toThrowErrorMatchingInlineSnapshot(
1466+
`"Cannot use --experimental-public and a Site configuration together."`
1467+
);
1468+
});
1469+
14071470
it("should not contain backslash for assets with nested directories", async () => {
14081471
const assets = [
14091472
{ filePath: "subdir/file-1.txt", content: "Content of file-1" },
@@ -5258,6 +5321,7 @@ function mockUploadWorkerRequest(
52585321
options: {
52595322
available_on_subdomain?: boolean;
52605323
expectedEntry?: string;
5324+
expectedMainModule?: string;
52615325
expectedType?: "esm" | "sw";
52625326
expectedBindings?: unknown;
52635327
expectedModules?: Record<string, string>;
@@ -5271,6 +5335,7 @@ function mockUploadWorkerRequest(
52715335
const {
52725336
available_on_subdomain = true,
52735337
expectedEntry,
5338+
expectedMainModule = "index.js",
52745339
expectedType = "esm",
52755340
expectedBindings,
52765341
expectedModules = {},
@@ -5304,7 +5369,7 @@ function mockUploadWorkerRequest(
53045369
formBody.get("metadata") as string
53055370
) as WorkerMetadata;
53065371
if (expectedType === "esm") {
5307-
expect(metadata.main_module).toEqual("index.js");
5372+
expect(metadata.main_module).toEqual(expectedMainModule);
53085373
} else {
53095374
expect(metadata.body_part).toEqual("index.js");
53105375
}

packages/wrangler/src/bundle.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ function getEntryPoint(
177177
path.join(__dirname, "../templates/static-asset-facade.js"),
178178
"utf8"
179179
)
180-
.replace("__ENTRY_POINT__", entryFile),
180+
// on windows, escape backslashes in the path (`\`)
181+
.replace("__ENTRY_POINT__", entryFile.replaceAll("\\", "\\\\")),
181182
sourcefile: "static-asset-facade.js",
182183
resolveDir: path.dirname(entryFile),
183184
},

packages/wrangler/src/dev/dev.tsx

+26-32
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type { Config } from "../config";
2020
import type { Entry } from "../entry";
2121
import type { AssetPaths } from "../sites";
2222
import type { CfWorkerInit } from "../worker";
23-
import type { EsbuildBundle } from "./use-esbuild";
2423

2524
export type DevProps = {
2625
name?: string;
@@ -58,10 +57,6 @@ export type DevProps = {
5857
};
5958

6059
export function DevImplementation(props: DevProps): JSX.Element {
61-
const directory = useTmpDir();
62-
63-
useCustomBuild(props.entry, props.build);
64-
6560
if (props.public && props.entry.format === "service-worker") {
6661
throw new Error(
6762
"You cannot use the service-worker format with a `public` directory."
@@ -86,37 +81,16 @@ export function DevImplementation(props: DevProps): JSX.Element {
8681
);
8782
}
8883

89-
const bundle = useEsbuild({
90-
entry: props.entry,
91-
destination: directory,
92-
staticRoot: props.public,
93-
jsxFactory: props.jsxFactory,
94-
rules: props.rules,
95-
jsxFragment: props.jsxFragment,
96-
serveAssetsFromWorker: !!props.public,
97-
tsconfig: props.tsconfig,
98-
minify: props.minify,
99-
nodeCompat: props.nodeCompat,
100-
});
101-
10284
// only load the UI if we're running in a supported environment
10385
const { isRawModeSupported } = useStdin();
10486
return isRawModeSupported ? (
105-
<InteractiveDevSession {...props} bundle={bundle} />
87+
<InteractiveDevSession {...props} />
10688
) : (
107-
<DevSession
108-
{...props}
109-
bundle={bundle}
110-
local={props.initialMode === "local"}
111-
/>
89+
<DevSession {...props} local={props.initialMode === "local"} />
11290
);
11391
}
11492

115-
type InteractiveDevSessionProps = DevProps & {
116-
bundle: EsbuildBundle | undefined;
117-
};
118-
119-
function InteractiveDevSession(props: InteractiveDevSessionProps) {
93+
function InteractiveDevSession(props: DevProps) {
12094
const toggles = useHotkeys(
12195
{
12296
local: props.initialMode === "local",
@@ -149,13 +123,33 @@ function InteractiveDevSession(props: InteractiveDevSessionProps) {
149123
);
150124
}
151125

152-
type DevSessionProps = InteractiveDevSessionProps & { local: boolean };
126+
type DevSessionProps = DevProps & {
127+
local: boolean;
128+
};
153129

154130
function DevSession(props: DevSessionProps) {
131+
useCustomBuild(props.entry, props.build);
132+
133+
const directory = useTmpDir();
134+
135+
const bundle = useEsbuild({
136+
entry: props.entry,
137+
destination: directory,
138+
staticRoot: props.public,
139+
jsxFactory: props.jsxFactory,
140+
rules: props.rules,
141+
jsxFragment: props.jsxFragment,
142+
// In dev for remote mode, we serve --experimental-assets from the local proxy before we send the request to the worker.
143+
serveAssetsFromWorker: !!props.public && !!props.local,
144+
tsconfig: props.tsconfig,
145+
minify: props.minify,
146+
nodeCompat: props.nodeCompat,
147+
});
148+
155149
return props.local ? (
156150
<Local
157151
name={props.name}
158-
bundle={props.bundle}
152+
bundle={bundle}
159153
format={props.entry.format}
160154
compatibilityDate={props.compatibilityDate}
161155
compatibilityFlags={props.compatibilityFlags}
@@ -172,7 +166,7 @@ function DevSession(props: DevSessionProps) {
172166
) : (
173167
<Remote
174168
name={props.name}
175-
bundle={props.bundle}
169+
bundle={bundle}
176170
format={props.entry.format}
177171
accountId={props.accountId}
178172
bindings={props.bindings}

packages/wrangler/src/dev/local.tsx

-5
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ function useLocalWorker({
8282
abortSignal: abortController.signal,
8383
});
8484

85-
if (publicDirectory) {
86-
throw new Error(
87-
'⎔ A "public" folder is not yet supported in local mode.'
88-
);
89-
}
9085
if (bindings.services && bindings.services.length > 0) {
9186
throw new Error(
9287
"⎔ Service bindings are not yet supported in local mode."

packages/wrangler/src/dev/remote.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function Remote(props: {
4040
accountId: props.accountId,
4141
bindings: props.bindings,
4242
assetPaths: props.assetPaths,
43+
public: props.public,
4344
port: props.port,
4445
compatibilityDate: props.compatibilityDate,
4546
compatibilityFlags: props.compatibilityFlags,
@@ -74,6 +75,7 @@ export function useWorker(props: {
7475
accountId: string | undefined;
7576
bindings: CfWorkerInit["bindings"];
7677
assetPaths: AssetPaths | undefined;
78+
public: string | undefined;
7779
port: number;
7880
compatibilityDate: string | undefined;
7981
compatibilityFlags: string[] | undefined;
@@ -131,7 +133,7 @@ export function useWorker(props: {
131133
// include it in the kv namespace name regardless (since there's no
132134
// concept of service environments for kv namespaces yet).
133135
name + (!props.legacyEnv && props.env ? `-${props.env}` : ""),
134-
assetPaths,
136+
props.public ? undefined : assetPaths,
135137
true,
136138
false
137139
); // TODO: cancellable?
@@ -223,6 +225,7 @@ export function useWorker(props: {
223225
accountId,
224226
port,
225227
assetPaths,
228+
props.public,
226229
compatibilityDate,
227230
compatibilityFlags,
228231
usageModel,

packages/wrangler/src/dev/use-esbuild.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export type EsbuildBundle = {
1414
entry: Entry;
1515
type: "esm" | "commonjs";
1616
modules: CfModule[];
17-
serveAssetsFromWorker: boolean;
1817
};
1918

2019
export function useEsbuild({
@@ -67,8 +66,7 @@ export function useEsbuild({
6766

6867
const { resolvedEntryPointPath, bundleType, modules, stop } =
6968
await bundleWorker(entry, destination, {
70-
// In dev, we serve assets from the local proxy before we send the request to the worker.
71-
serveAssetsFromWorker: false,
69+
serveAssetsFromWorker,
7270
jsxFactory,
7371
jsxFragment,
7472
rules,
@@ -87,7 +85,6 @@ export function useEsbuild({
8785
path: resolvedEntryPointPath,
8886
type: bundleType,
8987
modules,
90-
serveAssetsFromWorker,
9188
});
9289
}
9390

packages/wrangler/src/index.tsx

+19-7
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,12 @@ function createCLIParser(argv: string[]) {
10861086
);
10871087
}
10881088

1089+
if (args["experimental-public"] && (args.site || config.site)) {
1090+
throw new Error(
1091+
"Cannot use --experimental-public and a Site configuration together."
1092+
);
1093+
}
1094+
10891095
if (args.public) {
10901096
throw new Error(
10911097
"The --public field has been renamed to --experimental-public, and will change behaviour in the future."
@@ -1229,7 +1235,7 @@ function createCLIParser(argv: string[]) {
12291235
accountId={config.account_id}
12301236
assetPaths={getAssetPaths(
12311237
config,
1232-
args.site,
1238+
args["experimental-public"] || args.site,
12331239
args.siteInclude,
12341240
args.siteExclude
12351241
)}
@@ -1384,23 +1390,29 @@ function createCLIParser(argv: string[]) {
13841390
},
13851391
async (args) => {
13861392
await printWranglerBanner();
1393+
1394+
const configPath =
1395+
(args.config as ConfigPath) ||
1396+
(args.script && findWranglerToml(path.dirname(args.script)));
1397+
const config = readConfig(configPath, args);
1398+
const entry = await getEntry(args, config, "publish");
1399+
13871400
if (args["experimental-public"]) {
13881401
logger.warn(
13891402
"The --experimental-public field is experimental and will change in the future."
13901403
);
13911404
}
1405+
if (args["experimental-public"] && (args.site || config.site)) {
1406+
throw new Error(
1407+
"Cannot use --experimental-public and a Site configuration together."
1408+
);
1409+
}
13921410
if (args.public) {
13931411
throw new Error(
13941412
"The --public field has been renamed to --experimental-public, and will change behaviour in the future."
13951413
);
13961414
}
13971415

1398-
const configPath =
1399-
(args.config as ConfigPath) ||
1400-
(args.script && findWranglerToml(path.dirname(args.script)));
1401-
const config = readConfig(configPath, args);
1402-
const entry = await getEntry(args, config, "publish");
1403-
14041416
if (args.latest) {
14051417
logger.warn(
14061418
"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"

0 commit comments

Comments
 (0)