Reprise 1.3.0 is out, with modulepreload under a strict CSP, a Stimulus dev loop
without restarts, and safer writes of the metadata files. This is also the first
release post since Reprise 1.0, so it covers three earlier changes that only
got a changelog line: metadataPath from 1.2.0, style entries from 1.1.3,
and Vite's top-level input in dev from 1.1.0.
Modulepreload Links Under a Strict CSP
Under a nonce-based Content-Security-Policy (script-src 'nonce-...', the
kind NelmioSecurityBundle sets up), the <link rel="modulepreload"> tags
Reprise renders, and their matching HTTP Link: preload headers, never
carried a nonce. The browser blocks them, and once a modulepreload is blocked
the module it points to fails to load, so the app's JavaScript never runs.
A nonce passed to reprise_entry_script_tags() through attributes,
or set in the global script_attributes, now reaches the entry's
modulepreload links and their Link: header, while the other attributes stay
on the script tag:
1 2 3 4
{{ reprise_entry_script_tags('app', attributes={
nonce: csp_nonce('script'),
'data-turbo-track': 'reload',
}) }}
1 2
<link rel="modulepreload" href="/build/shared.js" nonce="r4nd0m">
<script src="/build/app.js" type="module" nonce="r4nd0m" data-turbo-track="reload"></script>
RenderAssetTagEvent is now dispatched for modulepreload links too, under a
new modulepreload type ($event->isModulepreload()). And the Link:
header now copies nonce, integrity and crossorigin from the final
tag, after the listeners ran, instead of recomputing them from
entrypoints.json: a listener changing them used to leave the header out of
sync with the tag, and the browser discards a preload that does not match.
Stimulus Controllers Without a Dev Server Restart
The virtual:symfony/controllers module was generated once, when the dev
server started. Adding or removing a local controller, toggling a
stimulusFetch: 'lazy' comment, or a controllers.json updated by Symfony
Flex after a composer require symfony/ux-* all needed a dev server restart
to show up. Webpack Encore picked these changes up on its own.
Both Vite and Rsbuild now regenerate the module and reload the page when one of
those files changes. It is a full page reload, not hot replacement of the
controller. Local controllers can also use the .jsx and .tsx
extensions, like they could with Encore:
1 2 3
assets/controllers/hello_controller.js -> hello
assets/controllers/chart_controller.tsx -> chart
assets/controllers/admin/user_controller.jsx -> admin--user
No More Rebuild Loop Under Rsbuild
With @rsbuild/plugin-tailwindcss (Tailwind CSS v4 watches the project's
directories, the one holding outputPath included), rsbuild dev and
rsbuild build --watch rebuilt forever: after every compile, Reprise
rewrites entrypoints.json, manifest.json and, in dev, the copied files,
the watcher sees the directory change, and Rspack compiles again. Vite was not
affected, since its watcher already ignores build.outDir. The Rsbuild
integration now adds outputPath and the metadata files to Rspack's
watchOptions.ignored, keeping Rspack's default ignores when you set none
and merging with yours (RegExp, glob or function) when you do.
Those rewrites also raced with PHP. writeFileSync() truncates a file before
writing it, so a request landing in between could read an empty
entrypoints.json and fail with a JsonException. On both bundlers, the
two files are now written to a temporary file next to them and renamed over
the target, with a fallback to a direct write when the rename fails (Windows
can refuse it while another process holds the file open).
Keep the Metadata Out of the Public Directory
Shipped in 1.2.0. Reprise wrote entrypoints.json and manifest.json
into outputPath, next to the compiled assets. Upload that directory to
public object storage or a CDN and the two files go with it, unless you
exclude them by hand.
The new metadataPath option (defaulting to outputPath) writes them
somewhere else, while the compiled assets and copied files stay in
outputPath. AssetMapper gets the same idea in Symfony 8.2 with its new
metadata_dir option.
1 2 3 4 5 6 7 8 9 10 11 12
// vite.config.ts
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'
export default defineConfig({
plugins: [
Symfony({
outputPath: 'public/build',
metadataPath: 'var/reprise',
}),
],
})
1 2 3 4 5 6 7 8 9 10 11 12
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import Symfony from '@symfony/reprise/rsbuild'
export default defineConfig({
plugins: [
Symfony({
outputPath: 'public/build',
metadataPath: 'var/reprise',
}),
],
})
1 2 3
# config/packages/reprise.yaml
reprise:
output_path: '%kernel.project_dir%/var/reprise'
If you rely on manifest.json for asset() calls, point
framework.assets.json_manifest_path at the new location too, as the
metadataPath documentation shows.
Style Entries Ship CSS Only
Shipped in 1.1.3. A style entry is an entry that points straight at a
stylesheet, what Encore's addStyleEntry() did. Under Vite, it compiled to an
empty JavaScript chunk that Vite pruned after Reprise had recorded it: the
rendered <script> returned a 404, manifest.json gained a dead
build/theme.js key, and with integrity enabled the build failed
outright. Under Rsbuild, the entry advertised a runtime-only JavaScript file,
which meant a pointless <script> and the same dead key.
A style entry now builds to CSS only on both bundlers, with no JavaScript file
in entrypoints.json or manifest.json. On Rsbuild, the runtime-only file
is also deleted from the build, the way Encore's DeleteUnusedEntriesJSPlugin
did:
1 2 3 4 5 6 7 8 9 10
// vite.config.ts
export default defineConfig({
input: {
app: './assets/app.js',
theme: './assets/styles/theme.scss',
},
plugins: [
Symfony(),
],
})
1 2 3 4 5 6 7 8 9 10 11 12 13
// rsbuild.config.ts
export default defineConfig({
source: {
entry: {
app: './assets/app.js',
theme: './assets/styles/theme.scss',
},
},
plugins: [
pluginSass(),
Symfony(),
],
})
reprise_entry_link_tags('theme') renders the <link>, and
reprise_entry_script_tags('theme') has nothing to render. Reprise treats an
entry as a style entry by its file extension, as the style entries documentation lists.
Vite's Top-Level Input in Dev
Shipped in 1.1.0. Vite 8.2 added a top-level input option, which Vite
recommends over build.rolldownOptions.input because dev uses it too.
Reprise's dev collector only read build.rolldownOptions.input and
build.rollupOptions.input, so a project on the new option built fine, but
its dev entrypoints.json had no entries.
The dev collector now falls back to the top-level input after the nested
ones, the same precedence vite build applies, so dev and build name the
same entries:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// vite.config.ts
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'
export default defineConfig({
input: {
app: './assets/app.js',
},
plugins: [
Symfony({
// options
}),
],
})
On Vite 8.1 and older, entries stay in build.rollupOptions.input.
Rsbuild is unaffected: its entries live in source.entry.
Full Changelog
- #134 [TagRenderer] Let CSP nonces reach modulepreload links and their Link headers (@Kocal)
- #135 [Rsbuild] Treat build --watch as a build, not as dev (@Kocal)
- #136 [Options] Guarantee a trailing slash on publicPath and manifestKeyPrefix (@Kocal)
- #137 [Vite] Preload chunks an entry reaches through another chunk (@Kocal)
- #138 [Stimulus] Pick up controller changes in dev without a restart (@Kocal)
- #139 [Rsbuild] Stop the rebuild loop when a tool watches the output directory (@Kocal)
- #141 [Cache] Do not cache a missing entrypoints.json (@Kocal)
- #144 [Tests] Backdate every fixture path in the watch-loop test (@Kocal)
- #143 [Stimulus] Register .jsx and .tsx local controllers (@Kocal)
- #142 [Rsbuild] Honour devServerOrigin (@Kocal)
- #140 [Emit] Write entrypoints.json and manifest.json atomically (@Kocal)
- #145 [DevServer] Advertise a reachable origin for wildcard and IPv6 hosts (@Kocal)
- #147 [Format] Drop
joinUrl(@Kocal) - #146 [Vite] Drop the
ViteOutputChunktype (@Kocal) - #151 [CI] Test Symfony 8.1 instead of 8.0 (@Kocal)
- #148 [Repo] Remove leftovers and stale configuration (@Kocal)
- #150 [Tests] Share integration test helpers and clean up temp dirs (@Kocal)
- #152 [Emit] Write the Symfony files through a single pipeline (@Kocal)
- #149 [TagRenderer] Make the preload header follow the final tag (@Kocal)
- #153 [Vite][Rsbuild] Move each bundler's hooks into its own module (@Kocal)
- #154 [Rsbuild] Ask Rspack only for the stats fields the manifest needs (@Kocal)
Reprise follows Symfony's backward compatibility promise since 1.0, and three of the changes above came from contributors outside the maintainer. Keep the feedback coming: issues and pull requests are welcome on GitHub.