Emulsify Core favors predictable output and simple project configuration. The defaults are suitable for small and medium component libraries, and the release fixtures cover the main supported structures. Larger libraries should keep source roots intentional and use the fixture commands below to compare changes.
For repeated developer-workload measurements, use the opt-in audit, resolver, and browser benchmarks. The 4.5.0 measurements distinguish release behavior changes from the measured grouped-resolver optimization.
vite build --watch leaves JavaScript and CSS readable and emits external maps
for JavaScript and direct stylesheet entries. One-shot production builds minify
both and omit maps:
build: {
sourcemap: watching,
minify: !watching,
cssMinify: !watching,
}Vite normally discards the Sass/PostCSS map when a build extracts CSS as a
Rollup asset. Core retains the combined map for each direct stylesheet entry,
attaches it to the finalized CSS, and rebases source paths when Drupal moves a
component map from dist/components/ to root components/. Final asset URL
rewrites preserve line mappings; a length-changing URL can shift columns inside
that value without changing the selector or declaration's source line.
Development maps remain on disk for devtools but are excluded from reporter counts, sizes, largest-file summaries, verbose listings, and rebuild details.
Storybook's Twig resolver eagerly imports compiled Twig modules with import.meta.glob(..., { eager: true }), but raw Twig source strings and text asset strings for source() are loaded lazily.
This supports:
include()for Twig templates.source()for raw Twig source, loaded on demand and cached after the first request.source('@assets/...')for inline text assets, loaded on demand from build-time asset source maps.- Namespaces derived from
project.emulsify.json.
Compiled Twig modules stay eager because Storybook stories need synchronous render functions. Raw source loading is deferred because most projects call source() for only a small subset of templates and assets. The first render that asks for a lazy raw source may render without that source while the dynamic import resolves; Emulsify then re-renders the Twig story and subsequent renders read the cached string synchronously.
Large projects with many generated, archived, or CMS-only Twig files can still see larger Storybook output from compiled module imports, but lazy raw source loading reduces the retained string heap for templates and text assets that never call source().
For large libraries:
- Keep only active Storybook-rendered Twig files under Storybook source roots.
- Move generated or archived Twig files outside
src/components, root./components, or explicitvariant.structureImplementationsroots when Storybook does not need them. - Prefer explicit
variant.structureImplementationsroots when a repository has multiple source areas. - Avoid storing large raw fixtures under Twig roots unless stories need to render or
source()them.
source('@assets/foo.svg') first checks a Core-generated build-time asset
source map. That internal map is generated from configured assets.roots plus
existing assets and src/assets directories. SVG, HTML, Twig, CSS,
JavaScript, JSON, TXT, and Markdown files are lazy ?raw imports.
This removes the common synchronous XHR path for inline assets. A first render that requests a new text asset may render without it while the import resolves; the Storybook Twig renderer re-renders and subsequent reads are synchronous from memory.
The sync-XHR fallback is disabled by default. Its deprecated opt-in is reached only when the generated module reports no asset-root coverage; it is not a general fallback for a missing file in an active asset map:
platformAdapter: {
storybook: {
allowSyncXhrSource: true,
},
}That fallback blocks the main thread and is scheduled for removal in a future major release.
Storybook eagerly imports CSS from the selected render path by default so component styles are available in the iframe. none and wordpress projects load compiled CSS from dist/**/*.css. Drupal projects that mirror component output load component CSS from components/**/*.css and shared compiled CSS from dist/**/*.css excluding dist/components/**/*.css, because dist/components and root components represent the same component CSS through different paths.
The eager glob performs the CSS imports directly; no runtime iteration is needed after the glob runs.
Projects with very large CSS libraries can opt out and import CSS from their own Storybook preview override:
export const parameters = {
emulsify: {
loadAllCSS: false,
},
};When emulsify.loadAllCSS is false, Emulsify skips the eager CSS glob entirely.
Tailwind CSS v4 can scan project sources automatically, but explicit @source lines make Emulsify structures easier to reason about:
@import 'tailwindcss';
@source "../components";
@source "../../components";
@source "../foundation";
@source "../layout";
@source "../tokens";Use only the roots your project actually uses. Do not point Tailwind at dist/, .out/, node_modules/, generated fixture output, or archived templates.
Emulsify Core compiles JavaScript and Sass/CSS entries. It copies Twig templates, component metadata, and static component assets.
Copied files are intentionally not transformed by Vite. This keeps CMS-facing templates and metadata predictable and avoids unnecessary build work. The copy pass uses the normalized project structure model so copied files land beside the matching compiled output.
Use these commands to compare release-readiness behavior:
npm run fixtures:releaseRelease fixtures live under .github/fixtures/release/. They are repository
development assets for CI and local validation, and are not included in the npm
package installed by consuming projects.
Run one fixture when debugging a specific project shape:
npm run fixtures:release -- --fixture mixed-storybook
npm run fixtures:release -- --fixture large-twig-storybookList available fixtures:
npm run fixtures:release:listThe large-twig-storybook fixture reports Storybook build time, total output
size, total JavaScript size, fixture-owned JavaScript size, and generated Twig
component count. Build timing is trend data for comparison under equivalent
conditions; it has no fixed pass/fail duration budget.
There is a separate enforced byte ceiling in
scripts/release-fixtures.js: fixture-owned
JavaScript must total less than 182,329 bytes. The ceiling is exclusive, so
182,329 bytes fails. It applies only to the fixture's _content-*, gallery-*,
gallery.stories-*, and item-* JavaScript chunks under storybook-assets/,
not all Storybook manager/runtime chunks or the total output size. Changing
this byte limit is a fixture policy change; it is not a timing result.