Skip to content

Commit 0fe1df4

Browse files
committed
bug #10 [Stimulus] Detect preserved /*! ... */ lazy comments and lock deterministic ordering (Kocal)
This PR was squashed before being merged into the main branch. Discussion ---------- [Stimulus] Detect preserved `/*! ... */` lazy comments and lock deterministic ordering | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT Reprise's Stimulus virtual module (`assets/src/core/stimulus.ts`) reimplements StimulusBundle's `ControllersMapGenerator` for bundlers, since the webpack loader Encore relied on doesn't exist outside webpack. StimulusBundle just merged two small fixes to that PHP generator, so this PR ports both to the TS side. Two commits, one per upstream PR. **1. Detect the preserved `/*! stimulusFetch: 'lazy' */` comment form** (port of symfony/ux#3702) A controller opts into lazy loading by adding a `stimulusFetch: 'lazy'` comment above its class. The problem: tsc and esbuild strip regular block comments during minification, but keep "preserved" ones written as `/*! ... */`. Reprise's lazy-detection regex only matched the plain `/* ... */` form, so the marker was lost as soon as the controller went through a minifying build. This adds the optional `!` to the block-comment branch so both forms are detected. New fixture controller + test included. **2. Sort local controllers for a deterministic loader output** (port of symfony/ux#3703) Upstream, controllers were iterated in filesystem order, so the generated loader (and its content hash) could shift between builds even with no actual change, breaking cache busting. Reprise already sorts its local controllers (`listLocalControllers` ends with `.sort()`), so there's no behavior change here. This commit just adds a regression test that pins the exact emission order, plus a comment explaining why the sort matters. All tests pass (`assets/test`), and `oxlint`/`oxfmt` are clean. Added a short docs note in `doc/index.rst` for the preserved-comment form. Commits ------- 16caeb7 [Stimulus] Detect preserved `/*! ... */` lazy comments and lock deterministic ordering
2 parents a9164b7 + 16caeb7 commit 0fe1df4

4 files changed

Lines changed: 43 additions & 6 deletions

File tree

assets/src/core/stimulus.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ interface ResolvedController {
5454
// which is why a stray `stimulusFetch: 'lazy'` sitting above the imports (or anywhere else) does
5555
// NOT flip the controller to lazy. It may be a block comment or a single-line one, single or
5656
// double quotes; a block comment may sit on the class's own line, a line comment must precede it.
57+
// A preserved block comment (`/*! ... */`, the form tsc/esbuild keep so the marker survives
58+
// minification) is recognised too.
5759
const LAZY_COMMENT_RE =
58-
/(?:\/\*\s*stimulusFetch:\s*['"]lazy['"]\s*\*\/|\/\/\s*stimulusFetch:\s*['"]lazy['"])\s*(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\b/i;
60+
/(?:\/\*!?\s*stimulusFetch:\s*['"]lazy['"]\s*\*\/|\/\/\s*stimulusFetch:\s*['"]lazy['"])\s*(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\b/i;
5961
const LOCAL_CONTROLLER_RE = /[-_]controller\.[jt]s$/;
6062

6163
export function generateControllersModule(opts: ResolvedStimulusOptions, root: string, isDev: boolean): string {
@@ -173,10 +175,14 @@ function listLocalControllers(dir: string): string[] {
173175
} catch {
174176
return []; // dir absent -> no local controllers
175177
}
176-
return entries
177-
.map((e) => String(e).replace(/\\/g, '/'))
178-
.filter((e) => LOCAL_CONTROLLER_RE.test(e))
179-
.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+
);
180186
}
181187

182188
function localIdentifier(rel: string): string {

assets/test/core/stimulus.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ describe('generateControllersModule — local', () => {
5959
expect(src).toContain(posix(join(root, 'controllers/single_line_controller.js')));
6060
});
6161

62+
it('detects the lazy marker inside a preserved /*! ... */ comment', () => {
63+
const src = generateControllersModule(localOpts, root, false);
64+
// tsc/esbuild keep `/*! ... */` comments through minification, so the marker survives.
65+
expect(src).toContain(`"preserved-comment": () => import(`);
66+
expect(src).toContain(posix(join(root, 'controllers/preserved_comment_controller.js')));
67+
});
68+
6269
it('ignores a lazy marker that sits above the imports (it must be directly above the class)', () => {
6370
const src = generateControllersModule(localOpts, root, false);
6471
// The marker in `above_imports_controller.js` precedes the imports, not the class,
@@ -78,6 +85,26 @@ describe('generateControllersModule — local', () => {
7885
expect(src).toContain('export const eagerControllers = {}');
7986
expect(src).toContain('export const lazyControllers = {}');
8087
});
88+
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+
});
81108
});
82109

83110
describe('generateControllersModule — identifier collision', () => {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/*! stimulusFetch: 'lazy' */
2+
export default class {}

doc/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ imports) — a block or a single-line comment both work:
119119
/* stimulusFetch: 'lazy' */
120120
export default class extends Controller {}
121121
122-
(``// stimulusFetch: 'lazy'`` on the line above the class works too.)
122+
(``// stimulusFetch: 'lazy'`` on the line above the class works too, as does a
123+
preserved ``/*! stimulusFetch: 'lazy' */`` comment — the form tsc and esbuild keep
124+
through minification.)
123125

124126
**Third-party UX packages.** Controllers declared in ``controllers.json`` are
125127
resolved from ``node_modules``, so install them with your package manager, the

0 commit comments

Comments
 (0)