Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 2534818

Browse files
committed
Change bindings priority so it matches Miniflare 1's
From lowest to highest priority: 1) Wrangler [vars] 2) .env Variables 3) WASM Module Bindings 4) Custom Bindings
1 parent 8b7b00f commit 2534818

File tree

8 files changed

+134
-53
lines changed

8 files changed

+134
-53
lines changed

packages/cli-parser/src/help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function buildHelp<Plugins extends PluginSignatures>(
6565
for (const [key, meta] of entries) {
6666
const { type, typeFormat, alias, description = "", negatable } = meta;
6767
// Ignore API-only options (e.g. string script)
68-
if (type === OptionType.NONE) continue;
68+
if (type === OptionType.NONE || typeof key === "symbol") continue;
6969

7070
let name = argName(key, meta);
7171
if (negatable) name = `(no-)${name}`;

packages/cli-parser/src/parse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function parseArgv<Plugins extends PluginSignatures>(
5151
for (const [key, meta] of plugin.prototype.opts.entries()) {
5252
const type = meta.type;
5353
// Ignore API-only options (e.g. string script)
54-
if (type === OptionType.NONE) continue;
54+
if (type === OptionType.NONE || typeof key === "symbol") continue;
5555

5656
// Record arg so we can construct result options object later, note we're
5757
// including positional arguments here so they're included in the result

packages/core/src/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ export * from "./plugins";
4646
export * from "./standards";
4747
export * from "./storage";
4848

49+
/** @internal */
50+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
51+
export function _deepEqual(a: any, b: any): boolean {
52+
if (!dequal(a, b)) return false;
53+
54+
// Check top-level symbol properties are equal (used by BindingsPlugin for
55+
// Wrangler variables)
56+
if (typeof a === "object") {
57+
const aSymbols = Object.getOwnPropertySymbols(a);
58+
for (const aSymbol of aSymbols) {
59+
if (!(aSymbol in b) || !dequal(a[aSymbol], b[aSymbol])) return false;
60+
}
61+
return aSymbols.length === Object.getOwnPropertySymbols(b).length;
62+
}
63+
return true;
64+
}
65+
4966
export type CorePluginSignatures = PluginSignatures & {
5067
CorePlugin: typeof CorePlugin;
5168
};
@@ -120,6 +137,7 @@ function splitWranglerConfig<Plugins extends PluginSignatures>(
120137
const pluginResult = {} as PluginOptionsUnion<Plugins>;
121138
const pluginOverrides = overrides[name];
122139
for (const [key, meta] of plugin.prototype.opts?.entries() ?? []) {
140+
// TODO: merge options
123141
(pluginResult as any)[key] =
124142
pluginOverrides[key] ?? meta.fromWrangler?.(config, configDir);
125143
}
@@ -329,7 +347,7 @@ export class MiniflareCore<
329347
if (
330348
previous !== undefined &&
331349
!compatUpdate &&
332-
dequal(previous[name], options[name])
350+
_deepEqual(previous[name], options[name])
333351
) {
334352
continue;
335353
}
@@ -353,7 +371,7 @@ export class MiniflareCore<
353371
if (
354372
previous !== undefined &&
355373
!compatUpdate &&
356-
dequal(previous[name], options[name]) &&
374+
_deepEqual(previous[name], options[name]) &&
357375
// Make sure if we ran any beforeSetups and this plugin previously
358376
// returned scripts, that we rerun its setup
359377
!(ranBeforeSetup && this.#setupResults.get(name)?.script)

packages/core/src/plugins/bindings.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
} from "@miniflare/shared";
1212
import dotenv from "dotenv";
1313

14+
const kWranglerBindings = Symbol("kWranglerBindings");
15+
1416
export interface BindingsOptions {
1517
envPath?: boolean | string;
1618
bindings?: Record<string, any>;
@@ -36,11 +38,12 @@ export class BindingsPlugin
3638
})
3739
envPath?: boolean | string;
3840

41+
// We want custom bindings to override Wrangler bindings, so we can't put
42+
// fromWrangler in `bindings`. Using a symbol, means these low-priority
43+
// bindings can only be loaded from a Wrangler config.
3944
@Option({
4045
type: OptionType.OBJECT,
41-
alias: "b",
42-
description: "Binds variable/secret to environment",
43-
logName: "Custom Bindings",
46+
logName: "Wrangler Variables",
4447
fromWrangler: ({ vars }) => {
4548
if (!vars) return;
4649
// Wrangler stringifies all environment variables
@@ -49,6 +52,14 @@ export class BindingsPlugin
4952
);
5053
},
5154
})
55+
[kWranglerBindings]?: Record<string, any>;
56+
57+
@Option({
58+
type: OptionType.OBJECT,
59+
alias: "b",
60+
description: "Binds variable/secret to environment",
61+
logName: "Custom Bindings",
62+
})
5263
bindings?: Record<string, any>;
5364

5465
@Option({
@@ -80,19 +91,17 @@ export class BindingsPlugin
8091
}
8192

8293
async setup(): Promise<SetupResult> {
94+
// Bindings should be loaded in this order, from lowest to highest priority:
95+
// 1) Wrangler [vars]
96+
// 2) .env Variables
97+
// 3) WASM Module Bindings
98+
// 4) Custom Bindings
99+
83100
const bindings: Context = {};
84101
const watch: string[] = [];
85102

86-
// Load WebAssembly module bindings from files
87-
if (this.wasmBindings) {
88-
for (const [name, wasmPath] of Object.entries(this.wasmBindings)) {
89-
bindings[name] = new WebAssembly.Module(await fs.readFile(wasmPath));
90-
watch.push(wasmPath);
91-
}
92-
}
93-
94-
// Copy user's arbitrary bindings
95-
Object.assign(bindings, this.bindings);
103+
// Copy Wrangler bindings first
104+
Object.assign(bindings, this[kWranglerBindings]);
96105

97106
// Load bindings from .env file
98107
const envPath = this.envPath === true ? this.defaultEnvPath : this.envPath;
@@ -109,6 +118,17 @@ export class BindingsPlugin
109118
watch.push(envPath);
110119
}
111120

121+
// Load WebAssembly module bindings from files
122+
if (this.wasmBindings) {
123+
for (const [name, wasmPath] of Object.entries(this.wasmBindings)) {
124+
bindings[name] = new WebAssembly.Module(await fs.readFile(wasmPath));
125+
watch.push(wasmPath);
126+
}
127+
}
128+
129+
// Copy user's arbitrary bindings
130+
Object.assign(bindings, this.bindings);
131+
112132
return { globals: this.globals, bindings, watch };
113133
}
114134
}

packages/core/test/index.spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Request,
1313
RequestInfo,
1414
RequestInit,
15+
_deepEqual,
1516
} from "@miniflare/core";
1617
import { VMScriptRunner } from "@miniflare/runner-vm";
1718
import {
@@ -51,6 +52,16 @@ function waitForReload(mf: MiniflareCore<any>): Promise<unknown> {
5152
return reloadPromise;
5253
}
5354

55+
test("_deepEqual: checks top-level symbol property equality", (t) => {
56+
const a = Symbol("a");
57+
const b = Symbol("b");
58+
t.true(_deepEqual({ [a]: { a: 1 } }, { [a]: { a: 1 } }));
59+
t.false(_deepEqual({ [a]: { a: 1 } }, { [a]: { a: 2 } }));
60+
t.false(_deepEqual({ [a]: { a: 1 } }, { [b]: { a: 1 } }));
61+
t.false(_deepEqual({ [a]: { a: 1 } }, {}));
62+
t.false(_deepEqual({}, { [a]: { a: 1 } }));
63+
});
64+
5465
test("MiniflareCore: always loads CorePlugin first", async (t) => {
5566
const log = new TestLog();
5667
const ctx: MiniflareCoreContext = { log, storageFactory, scriptRunner };
@@ -229,15 +240,15 @@ test("MiniflareCore: #init: gets watching option once", async (t) => {
229240
const mf = useMiniflare(plugins, { wranglerConfigPath });
230241
let instances = await mf.getPlugins();
231242
t.true(instances.CorePlugin.watch);
232-
t.is(instances.BindingsPlugin.bindings?.KEY, "1");
243+
t.is((await instances.BindingsPlugin.setup()).bindings?.KEY, "1");
233244

234245
// Disable watching in wrangler.toml
235246
let reloadPromise = waitForReload(mf);
236247
await fs.writeFile(wranglerConfigPath, config("2", false));
237248
await reloadPromise;
238249
instances = await mf.getPlugins();
239250
t.false(instances.CorePlugin.watch);
240-
t.is(instances.BindingsPlugin.bindings?.KEY, "2");
251+
t.is((await instances.BindingsPlugin.setup()).bindings?.KEY, "2");
241252

242253
// Edit KEY again in wrangler.toml, file should still be watched even though
243254
// watching has been "disabled"
@@ -246,7 +257,7 @@ test("MiniflareCore: #init: gets watching option once", async (t) => {
246257
await reloadPromise;
247258
instances = await mf.getPlugins();
248259
t.false(instances.CorePlugin.watch);
249-
t.is(instances.BindingsPlugin.bindings?.KEY, "3");
260+
t.is((await instances.BindingsPlugin.setup()).bindings?.KEY, "3");
250261
});
251262
test("MiniflareCore: #init: creates all plugins on init", async (t) => {
252263
const log = new TestLog();
@@ -677,15 +688,15 @@ test("MiniflareCore: #watcherCallback: re-inits on wrangler config change", asyn
677688
);
678689
const plugins = await mf.getPlugins();
679690
const bindingsPlugin = plugins.BindingsPlugin;
680-
t.is(bindingsPlugin.bindings?.KEY, "value1");
691+
t.is((await bindingsPlugin.setup()).bindings?.KEY, "value1");
681692

682693
// Update wrangler config, expecting BindingsPlugin to be re-created
683694
log.logs = [];
684695
const reloadPromise = waitForReload(mf);
685696
await fs.writeFile(wranglerConfigPath, '[vars]\nKEY = "value2"');
686697
await reloadPromise;
687698
t.not(plugins.BindingsPlugin, bindingsPlugin); // re-created
688-
t.is(plugins.BindingsPlugin.bindings?.KEY, "value2");
699+
t.is((await plugins.BindingsPlugin.setup()).bindings?.KEY, "value2");
689700
startsWith(t, log.logsAtLevel(LogLevel.DEBUG), [
690701
`${relative(wranglerConfigPath)} changed...`,
691702
"Initialising worker...", // re-init

packages/core/test/plugins/bindings.spec.ts

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ test("BindingsPlugin: parses options from argv", (t) => {
5858
bindings: { KEY1: "value1", KEY2: "value2" },
5959
});
6060
});
61-
test("BindingsPlugin: parses options from wrangler config", (t) => {
62-
const options = parsePluginWranglerConfig(BindingsPlugin, {
63-
vars: { KEY1: "value1", KEY2: "value2", KEY3: true, KEY4: 42 },
61+
test("BindingsPlugin: parses options from wrangler config", async (t) => {
62+
let options = parsePluginWranglerConfig(BindingsPlugin, {
6463
wasm_modules: {
6564
MODULE1: "module1.wasm",
6665
MODULE2: "module2.wasm",
@@ -70,26 +69,45 @@ test("BindingsPlugin: parses options from wrangler config", (t) => {
7069
env_path: ".env.test",
7170
},
7271
});
73-
t.deepEqual(options, {
72+
t.like(options, {
7473
envPath: ".env.test",
75-
// Bindings should be stringified...
76-
bindings: { KEY1: "value1", KEY2: "value2", KEY3: "true", KEY4: "42" },
77-
// ...but globals shouldn't
7874
globals: { KEY5: "value5", KEY6: false, KEY7: 10 },
7975
wasmBindings: { MODULE1: "module1.wasm", MODULE2: "module2.wasm" },
8076
});
77+
78+
// Wrangler bindings are stored in the kWranglerBindings symbol, which isn't
79+
// exported, so setup the plugin and check they're included
80+
options = parsePluginWranglerConfig(BindingsPlugin, {
81+
vars: { KEY1: "value1", KEY2: "value2", KEY3: true, KEY4: 42 },
82+
});
83+
const log = new NoOpLog();
84+
const plugin = new BindingsPlugin(log, compat, options);
85+
const result = await plugin.setup();
86+
// Wrangler bindings should be stringified
87+
t.deepEqual(result.bindings, {
88+
KEY1: "value1",
89+
KEY2: "value2",
90+
KEY3: "true",
91+
KEY4: "42",
92+
});
8193
});
8294
test("BindingsPlugin: logs options", (t) => {
95+
// wranglerOptions should contain [kWranglerBindings]
96+
const wranglerOptions = parsePluginWranglerConfig(BindingsPlugin, {
97+
vars: { KEY1: "value1", KEY2: "value2" },
98+
});
8399
let logs = logPluginOptions(BindingsPlugin, {
100+
...wranglerOptions,
84101
envPath: ".env.custom",
85-
bindings: { KEY1: "value1", KEY2: "value2" },
86-
globals: { KEY3: "value3", KEY4: "value4" },
102+
bindings: { KEY3: "value3", KEY4: "value4" },
103+
globals: { KEY5: "value5", KEY6: "value6" },
87104
wasmBindings: { MODULE1: "module1.wasm", MODULE2: "module2.wasm" },
88105
});
89106
t.deepEqual(logs, [
90107
"Env Path: .env.custom",
91-
"Custom Bindings: KEY1, KEY2",
92-
"Custom Globals: KEY3, KEY4",
108+
"Wrangler Variables: KEY1, KEY2",
109+
"Custom Bindings: KEY3, KEY4",
110+
"Custom Globals: KEY5, KEY6",
93111
"WASM Bindings: MODULE1, MODULE2",
94112
]);
95113
logs = logPluginOptions(BindingsPlugin, { envPath: true });
@@ -182,27 +200,37 @@ test("BindingsPlugin: setup: loads WebAssembly bindings", async (t) => {
182200
});
183201
test("BindingsPlugin: setup: loads bindings from all sources", async (t) => {
184202
const log = new NoOpLog();
203+
204+
// Bindings should be loaded in this order, from lowest to highest priority:
205+
// 1) Wrangler [vars]
206+
// 2) .env Variables
207+
// 3) WASM Module Bindings
208+
// 4) Custom Bindings
209+
210+
// wranglerOptions should contain [kWranglerBindings]
211+
const wranglerOptions = parsePluginWranglerConfig(BindingsPlugin, {
212+
vars: { A: "wrangler", B: "wrangler", C: "wrangler", D: "wrangler" },
213+
});
214+
185215
const tmp = await useTmp(t);
186216
const envPath = path.join(tmp, ".env");
187-
await fs.writeFile(envPath, "C=c");
188-
const obj = { c: 3 };
217+
await fs.writeFile(envPath, "A=env\nB=env\nC=env");
218+
219+
const obj = { ping: "pong" };
189220
const plugin = new BindingsPlugin(log, compat, {
221+
...wranglerOptions,
190222
wasmBindings: {
191223
A: addModulePath,
192224
B: addModulePath,
193-
C: addModulePath,
194225
},
195-
bindings: { B: obj, C: obj },
226+
bindings: { A: obj },
196227
envPath,
197228
});
198229
const result = await plugin.setup();
199230
assert(result.bindings);
200231

201-
const instance = new WebAssembly.Instance(result.bindings.A);
202-
assert(typeof instance.exports.add === "function");
203-
t.is(instance.exports.add(1, 2), 3);
204-
205-
t.is(result.bindings.B, obj);
206-
207-
t.is(result.bindings.C, "c");
232+
t.is(result.bindings.D, "wrangler");
233+
t.is(result.bindings.C, "env");
234+
t.true(result.bindings.B instanceof WebAssembly.Module);
235+
t.is(result.bindings.A, obj);
208236
});

packages/shared/src/plugin.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { StorageFactory } from "./storage";
66
import { Awaitable } from "./sync";
77
import { WranglerConfig } from "./wrangler";
88

9-
export type Context = { [key: string]: any };
9+
export type Context = { [key: string | symbol]: any };
1010

1111
export enum OptionType {
1212
NONE, // never
@@ -51,9 +51,12 @@ export type OptionMetadata =
5151

5252
export function Option(
5353
metadata: OptionMetadata
54-
): (prototype: typeof Plugin.prototype, key: string) => void {
54+
): (prototype: typeof Plugin.prototype, key: string | symbol) => void {
5555
return function (prototype, key) {
56-
(prototype.opts ??= new Map<string, OptionMetadata>()).set(key, metadata);
56+
(prototype.opts ??= new Map<string | symbol, OptionMetadata>()).set(
57+
key,
58+
metadata
59+
);
5760
};
5861
}
5962

@@ -72,7 +75,7 @@ export abstract class Plugin<Options extends Context = never> {
7275
// noinspection JSUnusedLocalSymbols
7376
readonly #phantom!: Options;
7477
// Metadata added by @Option decorator
75-
opts?: Map<string, OptionMetadata>;
78+
opts?: Map<string | symbol, OptionMetadata>;
7679

7780
protected constructor(
7881
protected readonly log: Log,
@@ -106,7 +109,7 @@ export abstract class Plugin<Options extends Context = never> {
106109

107110
export type PluginSignature = {
108111
new (log: Log, compat: Compatibility, options?: Context): Plugin<Context>;
109-
prototype: { opts?: Map<string, OptionMetadata> };
112+
prototype: { opts?: Map<string | symbol, OptionMetadata> };
110113
};
111114
export type PluginSignatures = { [pluginName: string]: PluginSignature };
112115

@@ -157,7 +160,8 @@ export function logOptions<Plugins extends PluginSignatures>(
157160
for (const [key, meta] of plugin.prototype.opts?.entries() ?? []) {
158161
const value = pluginOptions[key];
159162
if (value === undefined || meta.type === OptionType.NONE) continue;
160-
const keyName = meta?.logName ?? titleCase(key);
163+
const keyName =
164+
meta?.logName ?? titleCase(typeof key === "symbol" ? "<symbol>" : key);
161165
let str: string | undefined;
162166
if (meta.logValue) {
163167
str = (meta.logValue as any)(value);

0 commit comments

Comments
 (0)