This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
php artisan boost:add-skill symfony/reprise
Save this content to: AGENTS.md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
**Reprise** (`symfony/reprise`) — a Symfony bundle that brings Webpack Encore's key features to modern bundlers: **Vite** and **Rsbuild/Rspack**. The bundle is a Composer package (PHP `src/`/`tests/` at the repo root); its JS side is the `@symfony/reprise` npm package, an [unplugin](https://github.com/unjs/unplugin) living under `assets/`. Full ESM. Past the stub stage: the Encore-parity feature set (entrypoints/manifest, asset versioning, CDN `publicPath`, dev server + HMR, SRI, file copy, Stimulus) works on both bundlers, and the PHP `RepriseBundle` renders the tags.
**Design principle — do NOT re-implement what the bundler already does.** Vite and Rsbuild natively handle Sass/Less/PostCSS, TypeScript, code splitting, content hashing, source maps, minification, HMR, and the dev server. This plugin does NOT wrap or re-expose any of those (so no `enableSassLoader()`-style API from Encore). Its job is the **Symfony integration glue** that the bundlers do not provide — see below.
## Monorepo layout
The repo is a **Composer bundle** (`symfony/reprise`, PHP `src/`/`tests/` at the root) plus the **`assets/` npm package** (`@symfony/reprise`, the JS plugin and its tests), tied together by a pnpm workspace at the repo root.
## Commands
Package manager is **pnpm** (enforced via `packageManager` field). Node 22 (`.nvmrc`). The root `pnpm build`/`dev`/`typecheck`/`test`/`lint`/`fmt` scripts run from the workspace root; `build`/`dev`/`typecheck`/`test` delegate to the `assets` package, while `lint` (Oxlint) and `fmt` (Oxfmt) run at the root over the whole repo.
- `pnpm build` — delegates to `assets`, build via `tsdown` (bundles every `assets/src/*.ts` to `assets/dist/`)
- `pnpm dev` — delegates to `assets`, `tsdown -w`, watch/rebuild
- `pnpm typecheck` — delegates to `assets`, `tsc --noEmit` via `assets/tsconfig.typecheck.json` (bundler resolution + DOM libs, so it type-checks the sources the way tsdown/Vite/Rspack resolve them; the build `tsconfig.json` stays `nodenext`). tsdown does not type-check, so CI runs this in the `lint` job.
- `pnpm lint` — `oxlint` at the root (config in `.oxlintrc.json`); `pnpm lint:fix` auto-fixes. `playground/` and `assets/test/fixtures/` are ignored (not library source)
- `pnpm fmt` / `pnpm fmt:check` — `oxfmt` at the root (config in `.oxfmtrc.json`); `fmt:check` is the read-only variant CI runs
- `pnpm test` — delegates to `assets`, run tests (vitest, scoped to `assets/test/` via `assets/vitest.config.ts`)
- `pnpm test test/core/format.test.ts` — run a single test file (path relative to `assets/`)
- `pnpm test -t "<test name>"` — run a single test by name
### Playground (manual end-to-end verification)
`playground/` is a **full Symfony 7 PHP app** used to exercise the plugin against a real backend. It defines two entries (`app`, `admin`) and imports the plugin directly from `../assets/src` (Vite via `playground/vite.config.ts`, Rsbuild via `playground/rsbuild.config.ts`).
Run from the playground dir (there is no root wrapper script):
- `npm -C playground run vite:dev` / `npm -C playground run vite:build`
- `npm -C playground run rsbuild:dev` / `npm -C playground run rsbuild:build`
## Architecture
Bundler-agnostic core + per-bundler adapters, all under `assets/src/`:
- `assets/src/core/` — pure, no bundler imports: `options.ts` (`normalizeOptions` + CDN guard + `resolvePublicPath`), `dev-server.ts` (`resolveDevOrigin`), `format.ts` (`buildEntrypoints`/`buildManifest` in the frozen v1 format), `emit.ts` (`writeMetadata()` builds the context, the SRI hashes and both files, then writes them atomically; `watchIgnore()` gives watchers every path the build writes: `outputPath` and those metadata files).
- `assets/src/collectors/` — turn a bundler's output into the shared `NormalizedGraph`: `vite.ts` (`bundleToGraph` from the Rollup bundle in build, `configToDevGraph` from the resolved config in serve) and `rspack.ts` (`statsToGraph` from the Rspack stats JSON).
- `assets/src/index.ts` — the `unpluginFactory` (Vite + Rsbuild) + `createUnplugin` default export. Universal `resolveId`/`load` serve the Stimulus virtual module for both bundlers. It holds the shared `isDev` state (set by either bundler, read by `load`) and plugs in the hook objects from `bundlers/`.
- `assets/src/bundlers/` — the per-bundler hook objects, in a subdirectory because tsdown publishes every top-level `src/*.ts` as an entry. `vite.ts` (`viteHooks`) calls the collectors + core: `config()` sets `base`/`outDir` and disables Vite's own manifest/publicDir copy; `generateBundle` collects the graph and `writeBundle` writes the two files into `metadataPath` on build; `configureServer` writes the dev-flavoured files pointing at the dev-server origin. `rsbuild.ts` (`rsbuildHooks`) does the same job for Rsbuild in `setup(api)` — `api.modifyRsbuildConfig` forces `tools.htmlPlugin = false` (no per-entry HTML), disables the public-dir copy (output lives under `public/build`), sets the output paths + dev origin, and extends Rspack's `watchOptions.ignored` with those paths through `mergeWatchIgnored`; `api.onAfterCreateCompiler` taps `compiler.hooks.done` to run `statsToGraph` + core. `@rsbuild/core` is an optional peer, so it is never imported (the `rspack` namespace is read off the compiler as `c.rspack`), which keeps it out of the Vite bundle.
- `assets/src/vite.ts` — one-line unplugin adapter `createVitePlugin(unpluginFactory)` (`@symfony/reprise/vite`).
- `assets/src/rsbuild.ts` — one-line unplugin adapter `createRsbuildPlugin(unpluginFactory)` (`@symfony/reprise/rsbuild`). unplugin's Rsbuild adapter forwards the real Rsbuild plugin `api` to the factory's `rsbuild.setup` hook (so full config-level control is preserved) and injects the raw plugin into the Rspack config via `api.modifyRspackConfig`, which is how the universal `resolveId`/`load` reach Rspack.
- `assets/src/types.ts` — public `Options` (`outputPath`, `metadataPath`, `publicPath`, `manifestKeyPrefix`, `devServerOrigin`) + the frozen `EntrypointsJson`/`ManifestJson`/`EntryFiles` shapes (`js`/`css`/`preload`/`dynamic`).
Both bundlers go through the one `unpluginFactory`: Vite via `createVitePlugin`, Rsbuild via `createRsbuildPlugin` (unplugin ≥3 added Rsbuild support — the earlier hand-written native `RsbuildPlugin` existed only because unplugin 2.x had none). The shared factory is what lets the Stimulus virtual module be a single universal `resolveId`/`load` implementation instead of per-bundler plumbing. The raw Rspack unplugin adapter is not used — Rsbuild is the supported Rspack layer.
## The Symfony integration contract (the core of this project)
Encore's real value to Symfony is two JSON files written into `outputPath`, consumed by Reprise's **own** Symfony bundle (`RepriseBundle`, the PHP side under `src/`) via its Twig helpers that render the `<script>`/`<link>`/`asset()` tags. Reprise does **not** use `symfony/webpack-encore-bundle`. Generating these two files in Encore-compatible format is the primary work:
- **`entrypoints.json`** — maps each entry name to its asset URLs grouped by type, in load order (runtime chunks before app chunks). Optional `integrity` section for SRI hashes.
```json
{ "entrypoints": { "app": { "js": ["/build/runtime.js", "/build/app.js"], "css": ["/build/app.css"] } } }
```
- **`manifest.json`** — maps logical filename -> versioned/hashed URL, for cache-busting. Keys are prefixed with `manifestKeyPrefix` (defaults to `publicPath` minus leading slash). When `publicPath` points off the docroot — an absolute CDN URL (`://`) or a protocol-relative one (`//`), as classified by `isAbsolutePublicPath` — `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 or protocol-relative `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`.
### Dev server (build mode vs serve mode)
The plugin must behave differently depending on the bundler mode:
- **Build mode** (`vite build`, `rsbuild build`): assets are written to `outputPath` with content hashes; `entrypoints.json`/`manifest.json` point at those files under `publicPath`.
- **Serve/dev mode** (`vite`, `rsbuild dev`): the bundler's own dev server holds modules in memory and serves them over HTTP with native ESM + HMR. Here `entrypoints.json` must instead point at the dev server origin (e.g. `http://127.0.0.1:5173/build/app.js`) and inject the HMR client (`@vite/client`; React additionally needs the refresh preamble), so RepriseBundle's Twig tags load from the running dev server rather than from disk.
The dev server itself is native to Vite/Rsbuild — this plugin does not run one. Its only dev-server responsibility is detecting the mode (unplugin `meta`, or Vite's `configResolved` `command === 'serve'` vs `'build'`; Rsbuild/Rspack expose the same distinction) and emitting the dev-flavored `entrypoints.json` plus client injection. Encore's counterpart is `configureDevServerOptions()` (webpack-dev-server) in the reference `index.ts`, but that whole layer is replaced by the native dev server.
### Symfony UX / Stimulus controllers
Symfony UX ships Stimulus controllers from Composer packages, declared in `assets/controllers.json` (which controllers are enabled, `fetch: eager|lazy`, and each package's `autoimport` CSS). Local project controllers live in `assets/controllers/`.
Encore wires this with `enableStimulusBridge(controllerJsonPath)` (reference `lib/WebpackConfig.ts:882`), which only (1) adds the entries declared in `controllers.json`'s `entrypoints` map and (2) aliases `@symfony/stimulus-bridge/controllers.json` to the real file. The actual controller registration lives in the `@symfony/stimulus-bridge` npm package, whose webpack loader (`@symfony/stimulus-bridge/loader!./controllers.json`) plus a `require.context`-based lazy loader turn that JSON and the `assets/controllers/` dir into a registered Stimulus `Application`.
That loader is webpack-only, so it must be reimplemented here as a bundler-agnostic **virtual module** (unplugin `resolveId`/`load`): parse `controllers.json`, resolve each third-party controller from its npm package (honoring enabled + eager/lazy + `autoimport`), glob the local `assets/controllers/` dir, and emit the code that registers them on the Stimulus app. Prior art: `vite-plugin-symfony`'s `virtual:symfony/controllers` module.
Implemented (see the README feature list): `entrypoints.json` (build + dev), `manifest.json`, asset versioning wired into the manifest, absolute/CDN `publicPath`, dev-server + HMR, SRI hashes, file copy, Symfony UX / Stimulus controllers — all on both Vite and Rsbuild.
## Reference: the project being replaced
The original Encore lives at `../webpack-encore`. Consult it for exact output formats and semantics:
- `index.ts` — full Encore public API (what to selectively port vs. drop as bundler-native)
- `lib/webpack/entry-points-plugin.ts` — canonical `entrypoints.json` generation logic
- `lib/plugins/manifest.ts` and `lib/utils/manifest-key-prefix-helper.ts` — `manifest.json` + key-prefix edge cases
- `test_apps/npm-with-babel/public/build/{entrypoints,manifest}.json` — real example outputs
The Encore bundle for Symfony lives at `../webpack-encore-bundle`.
## Reference: unplugin examples
Read-only clones under `.references/` (git-ignored) show how mature unplugins are built — same `index.ts` factory + thin per-bundler entry convention we use. See `reference-repos.md` for the list; each clone has an `AGENTS.md` on what to study. Most relevant: `.references/unplugin-icons` (virtual `resolveId`/`load` — the model for the Stimulus module) and `.references/unplugin-auto-import` (virtual-module code injection + `.d.ts` generation).
## Conventions
- ESM only, strict TypeScript (type-checked in CI via `pnpm typecheck`), ES2022 target (via `@tsconfig/node22`). Use the `node:` prefix for Node builtins.
- New public options go in `assets/src/types.ts` with JSDoc; keep bundler adapters trivial.
- 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. Add the feature to the README feature list when it lands. Match the existing sections' natural voice; draft/polish the prose with the `natural-writing-editor` agent.
- Tests: a functional/integration test for one bundler (Vite or Rsbuild) always ships with its equivalent for the other — never cover one bundler without the other, including the negative/off cases.
- Commit messages: Symfony style `[<Scope>] <Short description>` — PascalCase scope, imperative mood, capitalized first word, no trailing period. A feature commit uses the feature's **own name** as the scope (e.g. `[Integrity]`, `[Manifest]`) and does **not** tack on `[Tests]` or `[Docs]` for the tests and docs it naturally includes; `[Tests]`/`[Docs]` are only for changes that are _exclusively_ tests or documentation. Combine scopes as `[A][B]` only when a change genuinely spans several distinct components. 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).
- Releases: the published npm package lives in `assets/` (`@symfony/reprise`); its `prepublishOnly` runs the `tsdown` build before publish.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Reprise (symfony/reprise) — a Symfony bundle that brings Webpack Encore's key features to modern bundlers: Vite and Rsbuild/Rspack. The bundle is a Composer package (PHP src//tests/ at the repo root); its JS side is the @symfony/reprise npm package, an unplugin living under assets/. Full ESM. Past the stub stage: the Encore-parity feature set (entrypoints/manifest, asset versioning, CDN publicPath, dev server + HMR, SRI, file copy, Stimulus) works on both bundlers, and the PHP RepriseBundle renders the tags.
Design principle — do NOT re-implement what the bundler already does. Vite and Rsbuild natively handle Sass/Less/PostCSS, TypeScript, code splitting, content hashing, source maps, minification, HMR, and the dev server. This plugin does NOT wrap or re-expose any of those (so no enableSassLoader()-style API from Encore). Its job is the Symfony integration glue that the bundlers do not provide — see below.
The repo is a Composer bundle (symfony/reprise, PHP src//tests/ at the root) plus the assets/ npm package (@symfony/reprise, the JS plugin and its tests), tied together by a pnpm workspace at the repo root.
Package manager is pnpm (enforced via packageManager field). Node 22 (.nvmrc). The root pnpm build/dev/typecheck/test/lint/fmt scripts run from the workspace root; build/dev/typecheck/test delegate to the assets package, while lint (Oxlint) and fmt (Oxfmt) run at the root over the whole repo.
pnpm build — delegates to assets, build via tsdown (bundles every assets/src/*.ts to assets/dist/)pnpm dev — delegates to assets, tsdown -w, watch/rebuildpnpm typecheck — delegates to assets, tsc --noEmit via assets/tsconfig.typecheck.json (bundler resolution + DOM libs, so it type-checks the sources the way tsdown/Vite/Rspack resolve them; the build tsconfig.json stays nodenext). tsdown does not type-check, so CI runs this in the lint job.pnpm lint — oxlint at the root (config in .oxlintrc.json); pnpm lint:fix auto-fixes. playground/ and assets/test/fixtures/ are ignored (not library source)pnpm fmt / pnpm fmt:check — oxfmt at the root (config in .oxfmtrc.json); fmt:check is the read-only variant CI runspnpm test — delegates to assets, run tests (vitest, scoped to assets/test/ via assets/vitest.config.ts)pnpm test test/core/format.test.ts — run a single test file (path relative to assets/)pnpm test -t "<test name>" — run a single test by nameplayground/ is a full Symfony 7 PHP app used to exercise the plugin against a real backend. It defines two entries (app, admin) and imports the plugin directly from ../assets/src (Vite via playground/vite.config.ts, Rsbuild via playground/rsbuild.config.ts).
Run from the playground dir (there is no root wrapper script):
npm -C playground run vite:dev / npm -C playground run vite:buildnpm -C playground run rsbuild:dev / npm -C playground run rsbuild:buildBundler-agnostic core + per-bundler adapters, all under assets/src/:
assets/src/core/ — pure, no bundler imports: options.ts (normalizeOptions + CDN guard + resolvePublicPath), dev-server.ts (resolveDevOrigin), format.ts (buildEntrypoints/buildManifest in the frozen v1 format), emit.ts (writeMetadata() builds the context, the SRI hashes and both files, then writes them atomically; watchIgnore() gives watchers every path the build writes: outputPath and those metadata files).assets/src/collectors/ — turn a bundler's output into the shared NormalizedGraph: vite.ts (bundleToGraph from the Rollup bundle in build, configToDevGraph from the resolved config in serve) and rspack.ts (statsToGraph from the Rspack stats JSON).assets/src/index.ts — the unpluginFactory (Vite + Rsbuild) + createUnplugin default export. Universal resolveId/load serve the Stimulus virtual module for both bundlers. It holds the shared isDev state (set by either bundler, read by load) and plugs in the hook objects from bundlers/.assets/src/bundlers/ — the per-bundler hook objects, in a subdirectory because tsdown publishes every top-level src/*.ts as an entry. vite.ts (viteHooks) calls the collectors + core: config() sets base/outDir and disables Vite's own manifest/publicDir copy; generateBundle collects the graph and writeBundle writes the two files into metadataPath on build; configureServer writes the dev-flavoured files pointing at the dev-server origin. rsbuild.ts (rsbuildHooks) does the same job for Rsbuild in setup(api) — api.modifyRsbuildConfig forces tools.htmlPlugin = false (no per-entry HTML), disables the public-dir copy (output lives under public/build), sets the output paths + dev origin, and extends Rspack's watchOptions.ignored with those paths through mergeWatchIgnored; api.onAfterCreateCompiler taps compiler.hooks.done to run statsToGraph + core. @rsbuild/core is an optional peer, so it is never imported (the rspack namespace is read off the compiler as c.rspack), which keeps it out of the Vite bundle.assets/src/vite.ts — one-line unplugin adapter createVitePlugin(unpluginFactory) (@symfony/reprise/vite).assets/src/rsbuild.ts — one-line unplugin adapter createRsbuildPlugin(unpluginFactory) (@symfony/reprise/rsbuild). unplugin's Rsbuild adapter forwards the real Rsbuild plugin api to the factory's rsbuild.setup hook (so full config-level control is preserved) and injects the raw plugin into the Rspack config via api.modifyRspackConfig, which is how the universal resolveId/load reach Rspack.assets/src/types.ts — public Options (outputPath, metadataPath, publicPath, manifestKeyPrefix, devServerOrigin) + the frozen EntrypointsJson/ManifestJson/EntryFiles shapes (js/css/preload/dynamic).Both bundlers go through the one unpluginFactory: Vite via createVitePlugin, Rsbuild via createRsbuildPlugin (unplugin ≥3 added Rsbuild support — the earlier hand-written native RsbuildPlugin existed only because unplugin 2.x had none). The shared factory is what lets the Stimulus virtual module be a single universal resolveId/load implementation instead of per-bundler plumbing. The raw Rspack unplugin adapter is not used — Rsbuild is the supported Rspack layer.
Encore's real value to Symfony is two JSON files written into outputPath, consumed by Reprise's own Symfony bundle (RepriseBundle, the PHP side under src/) via its Twig helpers that render the <script>/<link>/asset() tags. Reprise does not use symfony/webpack-encore-bundle. Generating these two files in Encore-compatible format is the primary work:
entrypoints.json — maps each entry name to its asset URLs grouped by type, in load order (runtime chunks before app chunks). Optional integrity section for SRI hashes.
{ "entrypoints": { "app": { "js": ["/build/runtime.js", "/build/app.js"], "css": ["/build/app.css"] } } }
manifest.json — maps logical filename -> versioned/hashed URL, for cache-busting. Keys are prefixed with manifestKeyPrefix (defaults to publicPath minus leading slash). When publicPath points off the docroot — an absolute CDN URL (://) or a protocol-relative one (//), as classified by isAbsolutePublicPath — 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 or protocol-relative 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.The plugin must behave differently depending on the bundler mode:
vite build, rsbuild build): assets are written to outputPath with content hashes; entrypoints.json/manifest.json point at those files under publicPath.vite, rsbuild dev): the bundler's own dev server holds modules in memory and serves them over HTTP with native ESM + HMR. Here entrypoints.json must instead point at the dev server origin (e.g. http://127.0.0.1:5173/build/app.js) and inject the HMR client (@vite/client; React additionally needs the refresh preamble), so RepriseBundle's Twig tags load from the running dev server rather than from disk.The dev server itself is native to Vite/Rsbuild — this plugin does not run one. Its only dev-server responsibility is detecting the mode (unplugin meta, or Vite's configResolved command === 'serve' vs 'build'; Rsbuild/Rspack expose the same distinction) and emitting the dev-flavored entrypoints.json plus client injection. Encore's counterpart is configureDevServerOptions() (webpack-dev-server) in the reference index.ts, but that whole layer is replaced by the native dev server.
Symfony UX ships Stimulus controllers from Composer packages, declared in assets/controllers.json (which controllers are enabled, fetch: eager|lazy, and each package's autoimport CSS). Local project controllers live in assets/controllers/.
Encore wires this with enableStimulusBridge(controllerJsonPath) (reference lib/WebpackConfig.ts:882), which only (1) adds the entries declared in controllers.json's entrypoints map and (2) aliases @symfony/stimulus-bridge/controllers.json to the real file. The actual controller registration lives in the @symfony/stimulus-bridge npm package, whose webpack loader (@symfony/stimulus-bridge/loader!./controllers.json) plus a require.context-based lazy loader turn that JSON and the assets/controllers/ dir into a registered Stimulus Application.
That loader is webpack-only, so it must be reimplemented here as a bundler-agnostic virtual module (unplugin resolveId/load): parse controllers.json, resolve each third-party controller from its npm package (honoring enabled + eager/lazy + autoimport), glob the local assets/controllers/ dir, and emit the code that registers them on the Stimulus app. Prior art: vite-plugin-symfony's virtual:symfony/controllers module.
Implemented (see the README feature list): entrypoints.json (build + dev), manifest.json, asset versioning wired into the manifest, absolute/CDN publicPath, dev-server + HMR, SRI hashes, file copy, Symfony UX / Stimulus controllers — all on both Vite and Rsbuild.
The original Encore lives at ../webpack-encore. Consult it for exact output formats and semantics:
index.ts — full Encore public API (what to selectively port vs. drop as bundler-native)lib/webpack/entry-points-plugin.ts — canonical entrypoints.json generation logiclib/plugins/manifest.ts and lib/utils/manifest-key-prefix-helper.ts — manifest.json + key-prefix edge casestest_apps/npm-with-babel/public/build/{entrypoints,manifest}.json — real example outputsThe Encore bundle for Symfony lives at ../webpack-encore-bundle.
Read-only clones under .references/ (git-ignored) show how mature unplugins are built — same index.ts factory + thin per-bundler entry convention we use. See reference-repos.md for the list; each clone has an AGENTS.md on what to study. Most relevant: .references/unplugin-icons (virtual resolveId/load — the model for the Stimulus module) and .references/unplugin-auto-import (virtual-module code injection + .d.ts generation).
pnpm typecheck), ES2022 target (via @tsconfig/node22). Use the node: prefix for Node builtins.assets/src/types.ts with JSDoc; keep bundler adapters trivial.doc/index.rst, and that section shows both a Vite and an Rsbuild example (the two supported bundlers) — never document one without the other. Add the feature to the README feature list when it lands. Match the existing sections' natural voice; draft/polish the prose with the natural-writing-editor agent.[<Scope>] <Short description> — PascalCase scope, imperative mood, capitalized first word, no trailing period. A feature commit uses the feature's own name as the scope (e.g. [Integrity], [Manifest]) and does not tack on [Tests] or [Docs] for the tests and docs it naturally includes; [Tests]/[Docs] are only for changes that are exclusively tests or documentation. Combine scopes as [A][B] only when a change genuinely spans several distinct components. 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).assets/ (@symfony/reprise); its prepublishOnly runs the tsdown build before publish.How can I help you explore Laravel packages today?