@@ -6,7 +6,57 @@ import {
66 isNonDestructive ,
77} from "../utils/diff-json" ;
88import type { JsonLike } from "../utils/diff-json" ;
9- import type { Config , RawConfig } from "@cloudflare/workers-utils" ;
9+ import type {
10+ CfWorkerInit ,
11+ Config ,
12+ RawConfig ,
13+ } from "@cloudflare/workers-utils" ;
14+
15+ // Exhaustive map of all binding keys in CfWorkerInit["bindings"].
16+ // When a new binding type is added, TypeScript will error here until it is handled.
17+ const reorderableBindings = {
18+ // Top-level binding arrays
19+ kv_namespaces : true ,
20+ r2_buckets : true ,
21+ d1_databases : true ,
22+ services : true ,
23+ send_email : true ,
24+ vectorize : true ,
25+ hyperdrive : true ,
26+ workflows : true ,
27+ dispatch_namespaces : true ,
28+ mtls_certificates : true ,
29+ pipelines : true ,
30+ secrets_store_secrets : true ,
31+ ratelimits : true ,
32+ analytics_engine_datasets : true ,
33+ unsafe_hello_world : true ,
34+ worker_loaders : true ,
35+ vpc_services : true ,
36+
37+ // Wrapper objects containing binding arrays
38+ durable_objects : true ,
39+ queues : true ,
40+ logfwdr : true ,
41+
42+ // Non-array bindings (nothing to reorder)
43+ vars : false ,
44+ wasm_modules : false ,
45+ text_blobs : false ,
46+ data_blobs : false ,
47+ browser : false ,
48+ ai : false ,
49+ images : false ,
50+ media : false ,
51+ version_metadata : false ,
52+ unsafe : false ,
53+ assets : false ,
54+ } satisfies Record < keyof CfWorkerInit [ "bindings" ] , boolean > ;
55+
56+ /** Extracts the keys of T whose values are `true` */
57+ type ReorderableKeys < T extends Record < string , boolean > > = {
58+ [ K in keyof T ] : T [ K ] extends true ? K : never ;
59+ } [ keyof T ] ;
1060
1161/**
1262 * Object representing the difference of two configuration objects.
@@ -37,7 +87,7 @@ export function getRemoteConfigDiff(
3787 normalizeLocalResolvedConfigAsRemote ( localResolvedConfig ) ;
3888 const normalizedRemoteConfig = normalizeRemoteConfigAsResolvedLocal (
3989 remoteConfig ,
40- localResolvedConfig
90+ normalizedLocalConfig
4191 ) ;
4292
4393 const diff = diffJsonObjects (
@@ -245,19 +295,19 @@ function normalizeObservability(
245295 * - removing from the remote config all the default values that in the local config are either not present or undefined
246296 *
247297 * @param remoteConfig The remote config object to normalize
248- * @param localResolvedConfig The target/local (resolved) config object
298+ * @param localConfig The target/local (resolved) config object
249299 * @returns The remote config object normalized and ready to be compared with the local one
250300 */
251301function normalizeRemoteConfigAsResolvedLocal (
252302 remoteConfig : RawConfig ,
253- localResolvedConfig : Config
303+ localConfig : Config
254304) : Config {
255305 let normalizedRemote = { } as Config ;
256306
257307 // We start by adding all the local configs to the normalized remote config object
258308 // in this way we can make sure that local-only configurations are not shown as
259309 // differences between local and remote configs
260- Object . entries ( localResolvedConfig ) . forEach ( ( [ key , value ] ) => {
310+ Object . entries ( localConfig ) . forEach ( ( [ key , value ] ) => {
261311 if (
262312 // We want to skip observability since it has a remote default behavior
263313 // different from that of wrangler
@@ -287,12 +337,131 @@ function normalizeRemoteConfigAsResolvedLocal(
287337 // the configuration options in the same order as their config file)
288338 normalizedRemote = orderObjectFields (
289339 normalizedRemote as unknown as Record < string , unknown > ,
290- localResolvedConfig as unknown as Record < string , unknown >
340+ localConfig as unknown as Record < string , unknown >
291341 ) as unknown as Config ;
292342
343+ // Reorder binding arrays to match local's order so the diff is intuitive.
344+ // Binding array order doesn't matter semantically, but positional diffing
345+ // would show spurious changes if the same elements appear in different order.
346+ for ( const [ bindingKey , shouldReorder ] of Object . entries (
347+ reorderableBindings
348+ ) ) {
349+ if ( ! shouldReorder ) {
350+ continue ;
351+ }
352+
353+ const key = bindingKey as ReorderableKeys < typeof reorderableBindings > ;
354+
355+ // Handle wrapper objects that contain binding arrays as nested properties
356+ if ( key === "queues" ) {
357+ // Only producers are bindings (accessible from Worker code).
358+ // Consumers configure message delivery to the Worker and are
359+ // managed through the Queues API, not the Worker bindings API,
360+ // so they don't appear in the remote config.
361+ if ( normalizedRemote . queues ?. producers && localConfig . queues ?. producers ) {
362+ normalizedRemote . queues . producers = reorderBindings (
363+ normalizedRemote . queues . producers ,
364+ localConfig . queues . producers
365+ ) ;
366+ }
367+ continue ;
368+ }
369+
370+ if ( key === "durable_objects" ) {
371+ if (
372+ normalizedRemote . durable_objects ?. bindings &&
373+ localConfig . durable_objects ?. bindings
374+ ) {
375+ normalizedRemote . durable_objects . bindings = reorderBindings (
376+ normalizedRemote . durable_objects . bindings ,
377+ localConfig . durable_objects . bindings
378+ ) ;
379+ }
380+ continue ;
381+ }
382+
383+ if ( key === "logfwdr" ) {
384+ if ( normalizedRemote . logfwdr ?. bindings && localConfig . logfwdr ?. bindings ) {
385+ normalizedRemote . logfwdr . bindings = reorderBindings (
386+ normalizedRemote . logfwdr . bindings ,
387+ localConfig . logfwdr . bindings
388+ ) ;
389+ }
390+ continue ;
391+ }
392+
393+ // Top-level binding arrays
394+ reorderConfigBindings ( normalizedRemote , localConfig , key ) ;
395+ }
396+
293397 return normalizedRemote ;
294398}
295399
400+ /**
401+ * Generates a stable key for a binding object by JSON-serializing it with sorted keys,
402+ * so that objects with the same properties in different order produce the same key.
403+ */
404+ function getBindingKey ( obj : unknown ) : string {
405+ return JSON . stringify ( obj , ( _ , v ) =>
406+ v && typeof v === "object" && ! Array . isArray ( v )
407+ ? Object . fromEntries (
408+ Object . keys ( v )
409+ . sort ( )
410+ . map ( ( k ) => [ k , v [ k ] ] )
411+ )
412+ : v
413+ ) ;
414+ }
415+
416+ /**
417+ * Reorders a remote binding array to match the local array's order.
418+ * Elements present in both arrays are placed first (in local order),
419+ * followed by elements only in the remote array.
420+ *
421+ * @example
422+ * ```ts
423+ * reorderBindings(
424+ * [{ binding: "A" }, { binding: "B" }, { binding: "C" }], // remote
425+ * [{ binding: "C" }, { binding: "A" }, { binding: "D" }] // local
426+ * )
427+ * // => [{ binding: "C" }, { binding: "A" }, { binding: "B" }]
428+ * // matched C and A are placed in local order, then unmatched B is appended
429+ * ```
430+ */
431+ function reorderBindings < T > ( remote : T [ ] , local : T [ ] ) : T [ ] {
432+ const remoteByKey = new Map ( remote . map ( ( el ) => [ getBindingKey ( el ) , el ] ) ) ;
433+ const used = new Set < string > ( ) ;
434+ const result : T [ ] = [ ] ;
435+ for ( const binding of local ) {
436+ const key = getBindingKey ( binding ) ;
437+ const remoteEl = remoteByKey . get ( key ) ;
438+ if ( remoteEl !== undefined ) {
439+ result . push ( remoteEl ) ;
440+ used . add ( key ) ;
441+ }
442+ }
443+ for ( const binding of remote ) {
444+ if ( ! used . has ( getBindingKey ( binding ) ) ) {
445+ result . push ( binding ) ;
446+ }
447+ }
448+ return result ;
449+ }
450+
451+ /**
452+ * Reorders a top-level binding array on the remote config to match the local config's order.
453+ * Uses a generic key parameter so TypeScript can correlate the types of both accesses.
454+ */
455+ function reorderConfigBindings <
456+ K extends ReorderableKeys < typeof reorderableBindings > ,
457+ > ( normalizedRemote : Config , localConfig : Config , key : K ) : void {
458+ const remoteArr = normalizedRemote [ key ] ;
459+ const localArr = localConfig [ key ] ;
460+ if ( Array . isArray ( remoteArr ) && Array . isArray ( localArr ) ) {
461+ normalizedRemote [ key ] = reorderBindings ( remoteArr , localArr ) as Config [ K ] ;
462+ }
463+ }
464+
296465/**
297466 * This function reorders the fields of a given object so that they follow a given target object.
298467 * All the fields of the given object not present in the target object will be ordered last.
0 commit comments