Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
symfony/reprise
claude-code
cursor
symfony-bundle
vite
rsbuild
unplugin
pnpm-workspace
asset-bundling
monorepo

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Install
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.

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 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.
    { "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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource