Skip to content

Latest commit

 

History

History
275 lines (207 loc) · 8.75 KB

File metadata and controls

275 lines (207 loc) · 8.75 KB

Symfony Reprise

EXPERIMENTAL This bundle is experimental and is likely to change, or even change drastically.

Webpack Encore gave Symfony first-class asset integration for Webpack. Symfony Reprise brings the same to Vite and Rsbuild.

Vite and Rsbuild already handle Sass/Less/PostCSS, TypeScript, JSX/Vue/Svelte, code splitting, content hashing, source maps, minification and HMR on their own, so Symfony Reprise does not reimplement any of that. It covers only the Symfony-side glue the bundlers leave out:

  • Multiple entries: build several independent entry points from one config
  • entrypoints.json: generated in both build and dev-server modes
  • manifest.json: maps each logical filename to its hashed URL
  • Asset versioning: content-hash cache busting, wired into the manifest
  • Dev server and HMR: points Twig at the running Vite/Rsbuild server
  • Symfony UX / Stimulus: registers controllers.json and local controllers, eager or lazy
  • CDN support: serve built assets from an absolute publicPath
  • Subresource Integrity: SRI hashes in entrypoints.json
  • Shared runtime chunk: one runtime shared across entries (planned)

It generates the Encore-compatible entrypoints.json and manifest.json that Reprise's own Symfony bundle (RepriseBundle, still a stub) reads to render the <script> and <link> tags, wires up the native dev server, and turns your Stimulus controllers into a running application.

Installation

Install the bundle with Composer and Symfony Flex:

$ composer require symfony/reprise

Then install the npm package:

$ npm install @symfony/reprise --save-dev

Vite

// vite.config.ts
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'

export default defineConfig({
  plugins: [
    Symfony({ /* options */ }),
  ],
})

Rsbuild

// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import Symfony from '@symfony/reprise/rsbuild'

export default defineConfig({
  plugins: [Symfony({ /* options */ })],
})

Symfony UX / Stimulus controllers

This is the Vite/Rsbuild counterpart of what @symfony/stimulus-bridge did for Webpack Encore: it turns your controllers.json into a Stimulus application, with the same enable step, same helper, same local-controllers convention.

Enable it by pointing the plugin at your controllers.json (this is what turns the feature on):

Symfony({
  stimulus: 'assets/controllers.json',
})
// or, to override the local controllers dir:
Symfony({
  stimulus: {
    controllersJson: 'assets/controllers.json',
    controllersDir: 'assets/controllers',
  },
})

Then start the app from your entry:

import { startStimulusApp } from '@symfony/reprise/stimulus'

const app = startStimulusApp()

Local controllers. Any assets/controllers/*_controller.{js,ts} is registered automatically. The filename becomes the identifier (hello_controller.js becomes hello, admin/user_controller.js becomes admin--user). To load a controller on demand, put a stimulusFetch: 'lazy' comment directly above the class (after the imports) — a block or a single-line comment both work:

import { Controller } from '@hotwired/stimulus'

/* stimulusFetch: 'lazy' */
export default class extends Controller {}

(// stimulusFetch: 'lazy' on the line above the class works too, as does a preserved /*! stimulusFetch: 'lazy' */ comment — the form tsc and esbuild keep through minification.)

Third-party UX packages. Controllers declared in controllers.json are resolved from node_modules, so install them with your package manager, the same as you would with Webpack Encore:

$ npm install @hotwired/stimulus @symfony/ux-turbo @symfony/ux-leaflet-map

Some packages need a bit of bundler-specific setup on top, the same way they did under Webpack Encore. UX Leaflet Map, for instance, ships a CSS file meant for Webpack's loader and needs an alias to the plain CSS build:

// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
    },
  },
})

Check each package's own docs for this kind of tweak.

Using a CDN

To serve your built assets from a CDN, set publicPath to the absolute CDN URL, for the production build only. In dev, the dev server serves assets directly, so keep the local /build/ path there. Both bundlers expose the mode through the function form of their config, so switch on command === 'build':

// vite.config.ts  (command is 'serve' or 'build')
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'

export default defineConfig(({ command }) => ({
  plugins: [
    Symfony({
      publicPath:
        command === 'build'
          ? 'https://my-cool-app.com.global.prod.fastly.net/build/'
          : '/build/',
      manifestKeyPrefix: 'build/',
    }),
  ],
}))
// rsbuild.config.ts  (command is 'dev' or 'build')
import { defineConfig } from '@rsbuild/core'
import Symfony from '@symfony/reprise/rsbuild'

export default defineConfig(({ command }) => ({
  plugins: [
    Symfony({
      publicPath:
        command === 'build'
          ? 'https://my-cool-app.com.global.prod.fastly.net/build/'
          : '/build/',
      manifestKeyPrefix: 'build/',
    }),
  ],
}))

With an absolute publicPath, manifestKeyPrefix is required: Reprise has no way to guess the right prefix for the manifest.json keys, and throws a clear error if it's missing. Keys stay logical, values point at the CDN:

{
  "build/app.js": "https://my-cool-app.com.global.prod.fastly.net/build/app-1a2b3c.js"
}

entrypoints.json is rewritten the same way, so the <script> and <link> tags render with CDN URLs. You still have to upload the built files to the CDN yourself, or set up origin pull. For a CDN subdirectory, include it in the URL (https://my-cool-app.com.global.prod.fastly.net/awesome-website/build/).

Subresource Integrity

When enabled, Reprise adds an integrity map to entrypoints.json (asset URL -> SRI hash). Reprise's Symfony bundle reads that map and renders integrity="..." on the generated <script> and <link> tags, so the browser refuses any asset whose bytes were tampered with.

The integrity option takes an object { enabled, algorithms? }. It only makes sense for the production build: the dev server serves changing in-memory assets, so no hashes are emitted in dev. As with the CDN example, toggle it with command === 'build':

// vite.config.ts  (command is 'serve' or 'build')
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'

export default defineConfig(({ command }) => ({
  plugins: [
    Symfony({
      integrity: { enabled: command === 'build', algorithms: ['sha384'] },
    }),
  ],
}))
// rsbuild.config.ts  (command is 'dev' or 'build')
import { defineConfig } from '@rsbuild/core'
import Symfony from '@symfony/reprise/rsbuild'

export default defineConfig(({ command }) => ({
  plugins: [
    Symfony({
      integrity: { enabled: command === 'build', algorithms: ['sha384'] },
    }),
  ],
}))

algorithms is optional and defaults to ['sha384']. Accepted values are 'sha256', 'sha384' and 'sha512'. Passing several (e.g. ['sha256', 'sha512']) writes multiple space-separated hashes per file, which the browser treats as "any one of these must match".

The resulting entrypoints.json gets an extra integrity section:

{
  "integrity": {
    "/build/app-1a2b3c.js": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K...",
    "/build/app-4d5e6f.css": "sha384-9ehJ4G8v3aQ2p1o0..."
  }
}

Hashes cover every referenced file in each entry (js, css, and preloaded/dynamic chunks), and since they're computed from the files actually written to disk, they stay correct through minification and hashing.