Skip to content

Commit 9b74e6a

Browse files
committed
[Stimulus] Lock the deterministic local controller ordering with a test
Port symfony/ux#3703. Reprise already sorts local controllers (listLocalControllers ends with `.sort()`), so the generated module is deterministic today; add a regression test asserting the exact emission order and a comment documenting why the sort matters (stable content hash regardless of filesystem iteration order).
1 parent 2b4f553 commit 9b74e6a

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

assets/src/core/stimulus.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,14 @@ function listLocalControllers(dir: string): string[] {
175175
} catch {
176176
return []; // dir absent -> no local controllers
177177
}
178-
return entries
179-
.map((e) => String(e).replace(/\\/g, '/'))
180-
.filter((e) => LOCAL_CONTROLLER_RE.test(e))
181-
.sort();
178+
return (
179+
entries
180+
.map((e) => String(e).replace(/\\/g, '/'))
181+
.filter((e) => LOCAL_CONTROLLER_RE.test(e))
182+
// Sort so the generated module -- and therefore its content hash -- stays stable
183+
// regardless of the filesystem's iteration order. See symfony/ux#3703.
184+
.sort()
185+
);
182186
}
183187

184188
function localIdentifier(rel: string): string {

assets/test/core/stimulus.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ describe('generateControllersModule — local', () => {
8686
expect(src).toContain('export const lazyControllers = {}');
8787
});
8888

89+
it('emits controllers in a stable, name-sorted order (deterministic output/hash)', () => {
90+
// Local controllers are sorted by filename so the generated module -- and thus its
91+
// content hash -- stays stable regardless of the order the filesystem returns the files
92+
// in (symfony/ux#3703). Assert the exact identifier order the module is rendered in.
93+
const src = generateControllersModule(localOpts, root, false);
94+
const identifiers = [...src.matchAll(/^ {2}"([^"]+)":/gm)].map((m) => m[1]);
95+
expect(identifiers).toEqual([
96+
// eagerControllers: third-party first, then local controllers sorted by filename
97+
'acme--ux-hello--hello',
98+
'above-imports',
99+
'admin--user',
100+
'greet',
101+
// lazyControllers: third-party first, then local controllers sorted by filename
102+
'acme--ux-map--map',
103+
'heavy',
104+
'preserved-comment',
105+
'single-line',
106+
]);
107+
});
89108
});
90109

91110
describe('generateControllersModule — identifier collision', () => {

0 commit comments

Comments
 (0)