Skip to content

Commit a9164b7

Browse files
committed
bug #9 [Rsbuild][Tests][Docs] Cover and document CDN support (Kocal)
This PR was squashed before being merged into the main branch. Discussion ---------- [Rsbuild][Tests][Docs] Cover and document CDN support | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT CDN support (absolute `publicPath`) already worked, but it had no end-to-end test coverage and no documentation. This branch closes that gap and, along the way, fixes a real bug the new test surfaced. ## Changes - Added `assets/test/integration/cdn.test.ts`: a real Vite build and a real Rsbuild build with an absolute (CDN) `publicPath` plus `manifestKeyPrefix`, asserting CDN-prefixed URLs in both `entrypoints.json` and `manifest.json`. This reaches parity with Encore's `functional.js` CDN test. - Fixed a bug the Rsbuild test surfaced: the Rsbuild adapter set `config.server.base` to the raw `publicPath`, which Rsbuild rejects when it's an absolute URL ("server.base should start with a slash"). It now falls back to `/` for an absolute `publicPath`. The local dev server can't serve from a CDN anyway, and the advertised dev URLs already come from `resolvePublicPath`. - Added a unit test for an explicit empty `manifestKeyPrefix` (parity with Encore's `config-generator.js`). - Investigated porting the second branch of Encore's `validatePublicPathAndManifestKeyPrefix` (throw when `outputPath` doesn't contain `publicPath`) and deliberately did not port it. In Reprise, `outputPath` (a filesystem directory) and `publicPath` (a URL prefix) are decoupled, so that heuristic rejects valid configs (it broke 15 tests when I tried it). Encore only needed the coupling for its webpack dev-server document root, which Reprise doesn't have. Recorded in the design spec and in `AGENTS.md`. - Docs: new "Using a CDN" section in `doc/index.rst` with both a Vite and an Rsbuild example, showing how to switch `publicPath` on `command === 'build'` and why `manifestKeyPrefix` is required with an absolute `publicPath`. Flipped the CDN "(planned)" marker to shipped in `doc/index.rst` and `README.md`, and clarified the guard note in `AGENTS.md`. - Added a convention to `AGENTS.md`: every user-facing feature ships with a `doc/index.rst` section showing both a Vite and an Rsbuild example. ## Testing `pnpm test` (69 passing), `pnpm lint`, `pnpm fmt:check`. Commits ------- c235b77 [Rsbuild][Tests][Docs] Cover and document CDN support
2 parents 74b3f75 + c235b77 commit a9164b7

8 files changed

Lines changed: 388 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Encore's real value to Symfony is two JSON files written into `outputPath`, cons
5454
```json
5555
{ "entrypoints": { "app": { "js": ["/build/runtime.js", "/build/app.js"], "css": ["/build/app.css"] } } }
5656
```
57-
- **`manifest.json`** — maps logical filename -> versioned/hashed URL, for cache-busting. Keys are prefixed with `manifestKeyPrefix` (defaults to `publicPath` minus leading slash). When `publicPath` is an absolute CDN URL (contains `://`), `manifestKeyPrefix` must be set explicitly. Encore enforces this by throwing (`../webpack-encore/lib/config/path-util.ts`, `validatePublicPathAndManifestKeyPrefix`); **porting that guard is still TODO** — the current factory does not throw and would use the absolute URL as the key prefix. The `publicPath === null` branch in `assets/src/index.ts` is likewise dead (`publicPath` always defaults to `build/`).
57+
- **`manifest.json`** — maps logical filename -> versioned/hashed URL, for cache-busting. Keys are prefixed with `manifestKeyPrefix` (defaults to `publicPath` minus leading slash). When `publicPath` is an absolute CDN URL (contains `://`), `manifestKeyPrefix` must be set explicitly. Reprise ports the relevant half of Encore's `validatePublicPathAndManifestKeyPrefix` (`../webpack-encore/lib/config/path-util.js`) in `normalizeOptions`: an absolute `publicPath` without an explicit `manifestKeyPrefix` throws. Encore's second branch — rejecting a `publicPath` not contained in `outputPath` — is intentionally not ported: Reprise's `outputPath` (a filesystem dir) and `publicPath` (a URL prefix) are decoupled, so that heuristic would reject valid configs. CDN URLs in `entrypoints.json`/`manifest.json` are covered end-to-end by `assets/test/integration/cdn.test.ts`.
5858

5959
### Dev server (build mode vs serve mode)
6060

@@ -94,5 +94,6 @@ Read-only clones under `.references/` (git-ignored) show how mature unplugins ar
9494

9595
- ESM only, strict TypeScript, ES2017 target. Use the `node:` prefix for Node builtins.
9696
- New public options go in `assets/src/types.ts` with JSDoc; keep bundler adapters trivial.
97+
- Documentation: any user-facing feature ships with a short section in `doc/index.rst`, and that section shows **both** a Vite and an Rsbuild example (the two supported bundlers) — never document one without the other. Flip the matching `*(planned)*` marker in the feature lists (`doc/index.rst` and `README.md`) when the feature lands. Match the existing sections' natural voice; draft/polish the prose with the `natural-writing-editor` agent.
9798
- Commit messages: Symfony style `[<Scope>] <Short description>` — PascalCase scope, imperative mood, capitalized first word, no trailing period; combine scopes as `[A][B]` when a change spans several. E.g. `[Stimulus] Emit forward-slash local controller paths`, `[Docs] Frame Stimulus usage as the Encore experience`, `[CI] Cancel superseded runs with a concurrency group`. This is the convention used across Symfony UX and WebpackEncoreBundle — **not** Conventional Commits (no `feat:`/`fix:`/`chore:` prefixes).
9899
- Releases: the published npm package lives in `assets/` (`@symfony/reprise`); its `prepublishOnly` runs the `tsdown` build before publish.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Symfony Reprise covers only the Symfony-side glue the bundlers leave out:
3030
- 🔖 **Asset versioning**: content-hash cache busting, wired into the manifest
3131
- 🔥 **Dev server & HMR**: points Twig at the running Vite/Rsbuild server
3232
- 🧩 **Symfony UX / Stimulus**: registers `controllers.json` and local controllers, eager or lazy
33-
- 🌐 **CDN support**: absolute `publicPath` _(planned)_
33+
- 🌐 **CDN support**: serve built assets from an absolute `publicPath`
3434
- 🛡️ **Subresource Integrity**: SRI hashes in `entrypoints.json` _(planned)_
3535
- 📦 **Shared runtime chunk**: one runtime shared across entries _(planned)_
3636

assets/src/rsbuild.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ export default function symfony(options?: Options): RsbuildPlugin {
5757
// `/build/` prefix and the Vite path (whose `base` is likewise the publicPath). Without
5858
// this the dev server serves at the origin root (`/`) while we advertise `/build/`, so
5959
// every advertised URL 404s.
60-
config.server.base = resolved.publicPath;
60+
// `server.base` must be a slash-path (Rsbuild rejects anything else). An absolute
61+
// (CDN) publicPath cannot be served by the local dev server, so fall back to the root;
62+
// in dev the advertised URLs come from `resolvePublicPath` (which keeps an absolute
63+
// publicPath as-is), so nothing is served under the CDN prefix locally anyway.
64+
config.server.base = resolved.publicPath.includes('://') ? '/' : resolved.publicPath;
6165
// Rsbuild's own config defaults `output.assetPrefix` to `'/'` before this hook runs
6266
// (it is never left `undefined`), so `??=` would never apply ours — assign unconditionally.
6367
// This drives the production build's asset URLs; in dev the serving path comes from

assets/test/core/options.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ describe('normalizeOptions', () => {
3030
expect(r.manifestKeyPrefix).toBe('build/');
3131
});
3232

33+
it('honors an explicit empty manifestKeyPrefix', () => {
34+
const r = normalizeOptions({ publicPath: '/build/', manifestKeyPrefix: '' }, '/app');
35+
expect(r.manifestKeyPrefix).toBe('');
36+
});
37+
3338
it('throws for an absolute publicPath without manifestKeyPrefix', () => {
3439
expect(() => normalizeOptions({ publicPath: 'https://cdn.example.com/x' }, '/app')).toThrow(
3540
/manifestKeyPrefix/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { mkdtempSync, readFileSync } from 'node:fs';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
import { createRsbuild } from '@rsbuild/core';
5+
import { build } from 'vite';
6+
import { describe, expect, it } from 'vitest';
7+
import SymfonyRsbuild from '../../src/rsbuild';
8+
import SymfonyVite from '../../src/vite';
9+
10+
const fixture = join(import.meta.dirname, '../fixtures/basic');
11+
const CDN = 'https://cdn.example.com/assets/';
12+
const CDN_URL_RE = /^https:\/\/cdn\.example\.com\/assets\//;
13+
14+
describe('absolute (CDN) publicPath', () => {
15+
it('vite build emits CDN-prefixed URLs in entrypoints.json and manifest.json', async () => {
16+
const out = mkdtempSync(join(tmpdir(), 'ups-cdn-vite-'));
17+
await build({
18+
root: fixture,
19+
logLevel: 'silent',
20+
build: {
21+
emptyOutDir: true,
22+
rollupOptions: { input: { app: join(fixture, 'app.js'), admin: join(fixture, 'admin.js') } },
23+
},
24+
plugins: [SymfonyVite({ outputPath: out, publicPath: CDN, manifestKeyPrefix: 'assets/' })],
25+
});
26+
27+
const entry = JSON.parse(readFileSync(join(out, 'entrypoints.json'), 'utf8'));
28+
expect(entry.publicPath).toBe(CDN);
29+
expect(entry.entryPoints.app.js[0]).toMatch(/^https:\/\/cdn\.example\.com\/assets\/app-.*\.js$/);
30+
31+
const manifest = JSON.parse(readFileSync(join(out, 'manifest.json'), 'utf8'));
32+
expect(manifest['assets/app.js']).toMatch(/^https:\/\/cdn\.example\.com\/assets\/app-.*\.js$/);
33+
for (const value of Object.values(manifest)) {
34+
expect(value).toMatch(CDN_URL_RE);
35+
}
36+
}, 30_000);
37+
38+
it('rsbuild build emits CDN-prefixed URLs in entrypoints.json and manifest.json', async () => {
39+
const out = mkdtempSync(join(tmpdir(), 'ups-cdn-rsbuild-'));
40+
const rsbuild = await createRsbuild({
41+
cwd: fixture,
42+
rsbuildConfig: {
43+
mode: 'production',
44+
source: { entry: { app: join(fixture, 'app.js'), admin: join(fixture, 'admin.js') } },
45+
plugins: [SymfonyRsbuild({ outputPath: out, publicPath: CDN, manifestKeyPrefix: 'assets/' })],
46+
},
47+
});
48+
await rsbuild.build();
49+
50+
const entry = JSON.parse(readFileSync(join(out, 'entrypoints.json'), 'utf8'));
51+
expect(entry.publicPath).toBe(CDN);
52+
expect(entry.entryPoints.app.js.some((u: string) => CDN_URL_RE.test(u))).toBe(true);
53+
54+
const manifest = JSON.parse(readFileSync(join(out, 'manifest.json'), 'utf8'));
55+
expect(Object.keys(manifest).length).toBeGreaterThan(0);
56+
for (const value of Object.values(manifest)) {
57+
expect(value).toMatch(CDN_URL_RE);
58+
}
59+
}, 60_000);
60+
});

doc/index.rst

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ leave out:
2121
- **Dev server and HMR**: points Twig at the running Vite/Rsbuild server
2222
- **Symfony UX / Stimulus**: registers ``controllers.json`` and local
2323
controllers, eager or lazy
24-
- **CDN support**: absolute ``publicPath`` *(planned)*
24+
- **CDN support**: serve built assets from an absolute ``publicPath``
2525
- **Subresource Integrity**: SRI hashes in ``entrypoints.json`` *(planned)*
2626
- **Shared runtime chunk**: one runtime shared across entries *(planned)*
2727

@@ -146,6 +146,67 @@ for Webpack's loader and needs an alias to the plain CSS build:
146146
147147
Check each package's own docs for this kind of tweak.
148148

149+
Using a CDN
150+
-----------
151+
152+
To serve your built assets from a CDN, set ``publicPath`` to the absolute CDN
153+
URL, for the production build only. In dev, the dev server serves assets
154+
directly, so keep the local ``/build/`` path there. Both bundlers expose the
155+
mode through the function form of their config, so switch on
156+
``command === 'build'``:
157+
158+
.. code-block:: javascript
159+
160+
// vite.config.ts (command is 'serve' or 'build')
161+
import { defineConfig } from 'vite'
162+
import Symfony from '@symfony/reprise/vite'
163+
164+
export default defineConfig(({ command }) => ({
165+
plugins: [
166+
Symfony({
167+
publicPath:
168+
command === 'build'
169+
? 'https://my-cool-app.com.global.prod.fastly.net/build/'
170+
: '/build/',
171+
manifestKeyPrefix: 'build/',
172+
}),
173+
],
174+
}))
175+
176+
.. code-block:: javascript
177+
178+
// rsbuild.config.ts (command is 'dev' or 'build')
179+
import { defineConfig } from '@rsbuild/core'
180+
import Symfony from '@symfony/reprise/rsbuild'
181+
182+
export default defineConfig(({ command }) => ({
183+
plugins: [
184+
Symfony({
185+
publicPath:
186+
command === 'build'
187+
? 'https://my-cool-app.com.global.prod.fastly.net/build/'
188+
: '/build/',
189+
manifestKeyPrefix: 'build/',
190+
}),
191+
],
192+
}))
193+
194+
With an absolute ``publicPath``, ``manifestKeyPrefix`` is **required**: Reprise
195+
has no way to guess the right prefix for the ``manifest.json`` keys, and
196+
throws a clear error if it's missing. Keys stay logical, values point at the
197+
CDN:
198+
199+
.. code-block:: json
200+
201+
{
202+
"build/app.js": "https://my-cool-app.com.global.prod.fastly.net/build/app-1a2b3c.js"
203+
}
204+
205+
``entrypoints.json`` is rewritten the same way, so the ``<script>`` and
206+
``<link>`` tags render with CDN URLs. You still have to upload the built files
207+
to the CDN yourself, or set up origin pull. For a CDN subdirectory, include it
208+
in the URL (``https://my-cool-app.com.global.prod.fastly.net/awesome-website/build/``).
209+
149210
.. _Vite: https://vite.dev/
150211
.. _Rsbuild: https://rsbuild.dev/
151212
.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# CDN end-to-end coverage — Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Close the test gap around absolute (CDN) `publicPath` and empty `manifestKeyPrefix`, reaching parity with Encore's `functional.js:256` and `config-generator.js:237`. Test-only; no production code changes.
6+
7+
**Architecture:** Two additions — a new cross-bundler integration test that runs a real Vite build and a real Rsbuild build with a CDN `publicPath` and asserts CDN-prefixed URLs in `entrypoints.json`/`manifest.json`, plus one unit test asserting an explicit empty `manifestKeyPrefix` is preserved. Both characterize existing behaviour.
8+
9+
**Tech Stack:** TypeScript (ESM, strict), vitest, `vite` + `@rsbuild/core` programmatic builds.
10+
11+
## Global Constraints
12+
13+
- ESM only, strict TypeScript, ES2017 target; `node:` prefix for Node builtins.
14+
- Tests live under `assets/test/`; run via `pnpm vitest run <file>` from the repo root; full suite via `pnpm test`.
15+
- Integration tests use `assets/test/fixtures/basic` (entries `app`, `admin`), a temp `outputPath` via `mkdtempSync`, and parse the emitted JSON — never the playground.
16+
- CDN config under test: `publicPath: 'https://cdn.example.com/assets/'` (trailing slash) + `manifestKeyPrefix: 'assets/'`.
17+
- Commit messages: Symfony style `[<Scope>] <Short description>`. Scopes: `[Tests]` for tests, `[Docs]` for AGENTS.md.
18+
- Spec: `docs/superpowers/specs/2026-07-10-cdn-manifestkeyprefix-guard-design.md`.
19+
20+
**Note on TDD framing:** these are characterization tests for behaviour that already exists, so they are expected to PASS on first run. If a CDN integration test FAILS, that is a real bug — stop and report it before continuing.
21+
22+
---
23+
24+
### Task 1: Empty manifestKeyPrefix unit test
25+
26+
**Files:**
27+
- Test: `assets/test/core/options.test.ts` (add one case to `describe('normalizeOptions')`)
28+
29+
**Interfaces:**
30+
- Consumes: `normalizeOptions(options, cwd): ResolvedOptions` — unchanged.
31+
- Produces: nothing new.
32+
33+
- [ ] **Step 1: Add the test**
34+
35+
Insert after the "honors an explicit manifestKeyPrefix" test (`options.test.ts:31`):
36+
37+
```ts
38+
it('honors an explicit empty manifestKeyPrefix', () => {
39+
const r = normalizeOptions({ publicPath: '/build/', manifestKeyPrefix: '' }, '/app');
40+
expect(r.manifestKeyPrefix).toBe('');
41+
});
42+
```
43+
44+
- [ ] **Step 2: Run it**
45+
46+
Run: `pnpm vitest run assets/test/core/options.test.ts`
47+
Expected: PASS (current code keeps `''` because `options?.manifestKeyPrefix ?? null` preserves the empty string and skips derivation).
48+
49+
- [ ] **Step 3: Commit**
50+
51+
```bash
52+
git add assets/test/core/options.test.ts
53+
git commit -m "[Tests] Cover explicit empty manifestKeyPrefix"
54+
```
55+
56+
---
57+
58+
### Task 2: CDN end-to-end integration test (Vite + Rsbuild)
59+
60+
**Files:**
61+
- Create: `assets/test/integration/cdn.test.ts`
62+
63+
**Interfaces:**
64+
- Consumes: default exports `../../src/vite` (Vite plugin) and `../../src/rsbuild` (Rsbuild plugin), both `(options?: Options) => plugin`; `Options` includes `outputPath`, `publicPath`, `manifestKeyPrefix`.
65+
- Produces: nothing new.
66+
67+
- [ ] **Step 1: Write the test file**
68+
69+
Create `assets/test/integration/cdn.test.ts`:
70+
71+
```ts
72+
import { mkdtempSync, readFileSync } from 'node:fs';
73+
import { tmpdir } from 'node:os';
74+
import { join } from 'node:path';
75+
import { createRsbuild } from '@rsbuild/core';
76+
import { build } from 'vite';
77+
import { describe, expect, it } from 'vitest';
78+
import SymfonyRsbuild from '../../src/rsbuild';
79+
import SymfonyVite from '../../src/vite';
80+
81+
const fixture = join(import.meta.dirname, '../fixtures/basic');
82+
const CDN = 'https://cdn.example.com/assets/';
83+
const CDN_URL_RE = /^https:\/\/cdn\.example\.com\/assets\//;
84+
85+
describe('absolute (CDN) publicPath', () => {
86+
it('vite build emits CDN-prefixed URLs in entrypoints.json and manifest.json', async () => {
87+
const out = mkdtempSync(join(tmpdir(), 'ups-cdn-vite-'));
88+
await build({
89+
root: fixture,
90+
logLevel: 'silent',
91+
build: {
92+
emptyOutDir: true,
93+
rollupOptions: { input: { app: join(fixture, 'app.js'), admin: join(fixture, 'admin.js') } },
94+
},
95+
plugins: [SymfonyVite({ outputPath: out, publicPath: CDN, manifestKeyPrefix: 'assets/' })],
96+
});
97+
98+
const entry = JSON.parse(readFileSync(join(out, 'entrypoints.json'), 'utf8'));
99+
expect(entry.publicPath).toBe(CDN);
100+
expect(entry.entryPoints.app.js[0]).toMatch(/^https:\/\/cdn\.example\.com\/assets\/app-.*\.js$/);
101+
102+
const manifest = JSON.parse(readFileSync(join(out, 'manifest.json'), 'utf8'));
103+
expect(manifest['assets/app.js']).toMatch(/^https:\/\/cdn\.example\.com\/assets\/app-.*\.js$/);
104+
for (const value of Object.values(manifest)) {
105+
expect(value).toMatch(CDN_URL_RE);
106+
}
107+
}, 30_000);
108+
109+
it('rsbuild build emits CDN-prefixed URLs in entrypoints.json and manifest.json', async () => {
110+
const out = mkdtempSync(join(tmpdir(), 'ups-cdn-rsbuild-'));
111+
const rsbuild = await createRsbuild({
112+
cwd: fixture,
113+
rsbuildConfig: {
114+
mode: 'production',
115+
source: { entry: { app: join(fixture, 'app.js'), admin: join(fixture, 'admin.js') } },
116+
plugins: [SymfonyRsbuild({ outputPath: out, publicPath: CDN, manifestKeyPrefix: 'assets/' })],
117+
},
118+
});
119+
await rsbuild.build();
120+
121+
const entry = JSON.parse(readFileSync(join(out, 'entrypoints.json'), 'utf8'));
122+
expect(entry.publicPath).toBe(CDN);
123+
expect(entry.entryPoints.app.js.some((u: string) => CDN_URL_RE.test(u))).toBe(true);
124+
125+
const manifest = JSON.parse(readFileSync(join(out, 'manifest.json'), 'utf8'));
126+
expect(Object.keys(manifest).length).toBeGreaterThan(0);
127+
for (const value of Object.values(manifest)) {
128+
expect(value).toMatch(CDN_URL_RE);
129+
}
130+
}, 60_000);
131+
});
132+
```
133+
134+
- [ ] **Step 2: Run the new file**
135+
136+
Run: `pnpm vitest run assets/test/integration/cdn.test.ts`
137+
Expected: PASS (2 tests). Absolute `publicPath` already flows through `joinUrl` in `buildEntrypoints`/`buildManifest`, so both files carry CDN URLs.
138+
If it FAILS: a real CDN bug — stop, diagnose (systematic-debugging), fix the production code, then re-run.
139+
140+
- [ ] **Step 3: Run the full suite**
141+
142+
Run: `pnpm test`
143+
Expected: PASS (was 66; now 69 — +1 unit from Task 1, +2 integration here).
144+
145+
- [ ] **Step 4: Commit**
146+
147+
```bash
148+
git add assets/test/integration/cdn.test.ts
149+
git commit -m "[Tests] Add CDN publicPath end-to-end build coverage for Vite and Rsbuild"
150+
```
151+
152+
---
153+
154+
### Task 3: Fix the stale AGENTS.md paragraph
155+
156+
**Files:**
157+
- Modify: `AGENTS.md` (the "The Symfony integration contract" section)
158+
159+
**Interfaces:** none.
160+
161+
- [ ] **Step 1: Replace the stale sentences**
162+
163+
In `AGENTS.md`, read the "The Symfony integration contract" section and replace:
164+
165+
> Encore enforces this by throwing (`../webpack-encore/lib/config/path-util.ts`, `validatePublicPathAndManifestKeyPrefix`); **porting that guard is still TODO** — the current factory does not throw and would use the absolute URL as the key prefix. The `publicPath === null` branch in `assets/src/index.ts` is likewise dead (`publicPath` always defaults to `build/`).
166+
167+
with:
168+
169+
> Reprise ports the relevant half of Encore's `validatePublicPathAndManifestKeyPrefix` (`../webpack-encore/lib/config/path-util.js`) in `normalizeOptions`: an absolute `publicPath` (containing `://`) without an explicit `manifestKeyPrefix` throws. Encore's second branch — rejecting a `publicPath` not contained in `outputPath` — is intentionally not ported: Reprise's `outputPath` (a filesystem dir) and `publicPath` (a URL prefix) are decoupled, so that heuristic would reject valid configs. CDN URLs in `entrypoints.json`/`manifest.json` are covered end-to-end by `assets/test/integration/cdn.test.ts`.
170+
171+
(If the surrounding wording differs slightly, keep it and swap only these sentences.)
172+
173+
- [ ] **Step 2: Commit**
174+
175+
```bash
176+
git add AGENTS.md
177+
git commit -m "[Docs] Clarify the manifestKeyPrefix guard and CDN coverage"
178+
```
179+
180+
---
181+
182+
## Self-Review
183+
184+
**Spec coverage:**
185+
- Empty manifestKeyPrefix parity → Task 1. ✓
186+
- CDN e2e (Vite + Rsbuild) → Task 2. ✓
187+
- Branch 2 rejection recorded, not implemented → no task (correct). ✓
188+
- AGENTS.md correction → Task 3. ✓
189+
190+
**Placeholder scan:** No TBD/TODO; all test code shown in full. ✓
191+
192+
**Type consistency:** Plugins imported as default exports `SymfonyVite`/`SymfonyRsbuild`, both `(options?: Options) => plugin`; assertions read `entry.publicPath`, `entry.entryPoints.app.js`, `manifest[...]` — matching the shapes in `vite-build.test.ts`/`rsbuild-build.test.ts`. ✓

0 commit comments

Comments
 (0)