Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add warning about wrangler dev with remote Durable Objects #687

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/four-bags-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"wrangler": patch
---

fix: add warning about `wrangler dev` with remote Durable Objects

Durable Objects that are being bound by `script_name` will not be isolated from the
live data during development with `wrangler dev`.
This change simply warns the developer about this, so that they can back out before
accidentally changing live data.

Fixes #319
38 changes: 37 additions & 1 deletion packages/wrangler/src/__tests__/dev.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { runInTempDir } from "./helpers/run-in-tmp";
import { runWrangler } from "./helpers/run-wrangler";
import writeWranglerToml from "./helpers/write-wrangler-toml";

describe("Dev component", () => {
describe("wrangler dev", () => {
mockAccountId();
mockApiToken();
runInTempDir();
Expand Down Expand Up @@ -480,6 +480,42 @@ describe("Dev component", () => {
expect(std.err).toMatchInlineSnapshot(`""`);
});
});

describe("durable_objects", () => {
it("should warn if there is one or more remote Durable Object", async () => {
writeWranglerToml({
main: "index.js",
durable_objects: {
bindings: [
{ name: "NAME_1", class_name: "CLASS_1" },
{
name: "NAME_2",
class_name: "CLASS_2",
script_name: "SCRIPT_A",
},
{ name: "NAME_3", class_name: "CLASS_3" },
{
name: "NAME_4",
class_name: "CLASS_4",
script_name: "SCRIPT_B",
},
],
},
});
fs.writeFileSync("index.js", `export default {};`);
await runWrangler("dev");
expect((Dev as jest.Mock).mock.calls[0][0].ip).toEqual("localhost");
expect(std.out).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`
"WARNING: You have Durable Object bindings, which 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\\"}"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
});
});
});

function mockGetZones(domain: string, zones: { id: string }[] = []) {
Expand Down
50 changes: 37 additions & 13 deletions packages/wrangler/src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type Entry = { file: string; directory: string; format: CfScriptFormat };
export async function getEntry(
args: { script?: string; format?: CfScriptFormat | undefined },
config: Config,
command: string
command: "dev" | "publish"
): Promise<Entry> {
let file: string;
let directory = process.cwd();
Expand Down Expand Up @@ -49,12 +49,24 @@ export async function getEntry(
args.format ?? config.build?.upload?.format
);

if (format === "service-worker" && hasDurableObjectImplementations(config)) {
const { localBindings, remoteBindings } =
partitionDurableObjectBindings(config);

if (command === "dev" && remoteBindings.length > 0) {
console.warn(
"WARNING: You have Durable Object bindings, which are not defined locally in the worker being developed.\n" +
"Be aware that changes to the data stored in these Durable Objects will be permanent and affect the live instances.\n" +
"Remote Durable Objects that are affected:\n" +
remoteBindings.map((b) => `- ${JSON.stringify(b)}`).join("\n")
);
}

if (format === "service-worker" && localBindings.length > 0) {
const errorMessage =
"You seem to be trying to use Durable Objects in a Worker written with Service Worker syntax.";
const addScriptName =
"You can use Durable Objects defined in other Workers by specifying a `script_name` in your wrangler.toml, where `script_name` is the name of the Worker that implements that Durable Object. For example:";
const addScriptNameExamples = generateAddScriptNameExamples(config);
const addScriptNameExamples = generateAddScriptNameExamples(localBindings);
const migrateText =
"Alternatively, migrate your worker to ES Module syntax to implement a Durable Object in this Worker:";
const migrateUrl =
Expand Down Expand Up @@ -174,29 +186,41 @@ export function fileExists(filePath: string): boolean {
return false;
}

type DurableObjectBindings = Config["durable_objects"]["bindings"];

/**
* Returns true if the given config contains Durable Object bindings that are implemented
* in this worker instead of being implemented elsewhere, and bound via a `script_name`
* property in wrangler.toml
* Groups the durable object bindings into two lists:
* those that are defined locally and those that refer to a durable object defined in another script.
*/
function hasDurableObjectImplementations(config: Config): boolean {
return config.durable_objects.bindings.some(
(binding) => binding.script_name === undefined
);
function partitionDurableObjectBindings(config: Config): {
localBindings: DurableObjectBindings;
remoteBindings: DurableObjectBindings;
} {
const localBindings: DurableObjectBindings = [];
const remoteBindings: DurableObjectBindings = [];
for (const binding of config.durable_objects.bindings) {
if (binding.script_name === undefined) {
localBindings.push(binding);
} else {
remoteBindings.push(binding);
}
}
return { localBindings, remoteBindings };
}

/**
* Generates some help text based on the Durable Object bindings in a given
* config indicating how the user can add a `script_name` field to bind an
* externally defined Durable Object.
*/
function generateAddScriptNameExamples(config: Config): string {
function generateAddScriptNameExamples(
localBindings: DurableObjectBindings
): string {
function exampleScriptName(binding_name: string): string {
return `${binding_name.toLowerCase().replaceAll("_", "-")}-worker`;
}

return config.durable_objects.bindings
.filter((binding) => binding.script_name === undefined)
return localBindings
.map(({ name, class_name }) => {
const script_name = exampleScriptName(name);
const currentBinding = `{ name = ${name}, class_name = ${class_name} }`;
Expand Down