Emulsify Core provides shared Vite and Storybook conventions. Project-specific framework tooling should live in the consuming project and connect through documented extension points.
Project-level extension locations live under config/emulsify-core:
| Extension Type | Directory | Why |
|---|---|---|
| Vite plugins/config patches | config/emulsify-core/vite/plugins.(mjs|js|cjs) |
Build-time Vite extensions are loaded only by Node/Vite. |
| Storybook overrides | config/emulsify-core/storybook/... |
Storybook preview/head overrides are project-facing assets that Storybook imports. |
| A11y config | config/emulsify-core/a11y.config.js |
Configure Pa11y ignores, manual Storybook IDs, and Storybook discovery behavior. |
Vite extensions should use config/emulsify-core/vite/. Storybook overrides should continue using config/emulsify-core/storybook/, and the a11y script continues to read config/emulsify-core/a11y.config.js.
Projects can extend the shared Vite config with one of these files:
config/emulsify-core/vite/plugins.mjsconfig/emulsify-core/vite/plugins.jsconfig/emulsify-core/vite/plugins.cjs
Supported plugin shapes:
export default [myVitePlugin()];export default ({ env }) => [myVitePlugin({ env })];Projects can also export extendConfig() when they need to patch Vite config beyond adding plugins:
export const extendConfig = (config, { env }) => ({
define: {
__PROJECT_NAME__: JSON.stringify(env.machineName),
},
});Use plugin arrays for normal framework integration. Use extendConfig() only when a plugin does not expose the needed config directly.
For Tailwind CSS v4, install Tailwind in the project:
npm install tailwindcss @tailwindcss/viteAdd the Tailwind Vite plugin from the project extension file:
// config/emulsify-core/vite/plugins.mjs
import tailwindcss from '@tailwindcss/vite';
export default () => [tailwindcss()];Create a CSS file that imports Tailwind. This example places it under src/global, but the file can live anywhere that makes sense for the project:
/* src/global/tailwind.css */
@import 'tailwindcss';
/* Choose the source roots your project uses. */
@source "../components";
@source "../../components";
@source "../foundation";
@source "../layout";
@source "../tokens";The @source lines are optional when Tailwind's automatic detection already sees the right files, but they make multi-root Emulsify projects explicit. Use ../components for src/components, ../../components for root ./components, and add one line for each variant.structureImplementations root that should be scanned. Keep @source paths focused on active component source directories so Tailwind does not scan generated output, archived templates, or dependency folders.
For production builds, import the Tailwind CSS file from a discovered JavaScript entry:
// src/global/tailwind.js
import './tailwind.css';For Storybook development, import the same CSS file from the project preview override so Twig and React stories see the same utility classes:
// config/emulsify-core/storybook/preview.js
import '../../../src/global/tailwind.css';
export const parameters = {};Tailwind detects complete class names in Twig, React, and other templates. Avoid constructing utility class fragments dynamically, such as text-${color}-600; map variants to complete class strings instead.
Other Vite-based framework integrations follow the same pattern:
- Install the framework package in the consuming project.
- Return its Vite plugin from
config/emulsify-core/vite/plugins.*. - Import any required framework CSS or setup files from a discovered project entry or Storybook preview override.
- Use
extendConfig()only when the framework needs additional Vite config.
Emulsify Core should not carry optional framework dependencies for every consuming project. Keep those dependencies local to the project that uses them.
Projects can provide config/emulsify-core/storybook/main.js to extend the shared Storybook main configuration. Use this for Storybook features that belong in Node-side config, such as addons, additional static directories, or final config shaping.
Project addons are appended to the Emulsify Core defaults, so a project can add one addon without repeating @storybook/addon-a11y, @storybook/addon-links, or @storybook/addon-themes.
npm install @storybook/addon-viewport// config/emulsify-core/storybook/main.js
export default {
addons: ['@storybook/addon-viewport'],
};Addon objects are also supported. If a project provides the same addon package name as a default addon, the project entry replaces the default entry so options can be customized without creating duplicates.
// config/emulsify-core/storybook/main.js
export default {
addons: [
{
name: '@storybook/addon-a11y',
options: {
manual: true,
},
},
],
};When a project intentionally wants to replace the full addon list, export replaceAddons.
// config/emulsify-core/storybook/main.js
export const replaceAddons = true;
export default {
addons: ['@storybook/addon-viewport'],
};For advanced cases, export extendConfig(). It receives the already-merged Storybook config and the resolved Emulsify environment.
// config/emulsify-core/storybook/main.js
export function extendConfig(config, { env }) {
const staticDirs = [...(config.staticDirs || [])];
if (env.platform === 'none') {
staticDirs.push('public');
}
return {
...config,
staticDirs,
};
}Projects can provide config/emulsify-core/storybook/preview.js to override or extend Storybook preview parameters. Missing override files are ignored. Default a11y parameters remain in place unless explicitly overridden.
export const parameters = {
layout: 'centered',
a11y: {
config: {
detailedReport: false,
},
},
};Preview overrides are loaded in the browser-bundled Storybook preview through Vite-safe imports. They should not rely on CommonJS require().
Drupal projects can use the same preview override to add project-specific browser settings. Emulsify Core's Drupal Storybook shim provides neutral window.drupalSettings defaults, then merges existing project-provided values when the shim loads:
window.drupalSettings = {
...(window.drupalSettings || {}),
exampleModule: {
apiUrl: '/example-endpoint',
},
};Emulsify Core registers its native Twig.js helpers automatically. Some projects
also need Drupal-compatible Twig.js filters and stubs from
twig-drupal-filters, such
as clean_class, clean_id, without, render, path, url, link(), or
attach_library(), so Drupal-authored templates can compile in Storybook.
Projects using the drupal platform adapter get those filters automatically:
{
"project": {
"platform": "drupal"
}
}none and wordpress projects can opt into only the Twig.js filters without
enabling Drupal behavior attachment, mirrored SDC output, or other Drupal
adapter behavior:
{
"project": {
"platform": "wordpress"
},
"storybook": {
"registerDrupalTwigFilters": true
}
}Restart Storybook after changing project.emulsify.json. The setting applies to
Storybook's shared Twig runtime and to imported .twig modules generated by
Emulsify Core's Vite plugin.
twig-drupal-filters helps Twig.js compile and render Drupal-style filters in
Storybook, but it does not provide Drupal services. Stub functions such as
attach_library() keep templates from failing during Storybook rendering; the
real Drupal runtime still owns library attachment and server-side behavior.
Preview head and manager head HTML remain separate extension points through:
config/emulsify-core/storybook/preview-head.htmlconfig/emulsify-core/storybook/manager-head.html
Use preview head for markup needed inside the story iframe, such as fonts, meta tags, or scripts that rendered components depend on. Use manager head for Storybook chrome only.
Emulsify Core exposes stable public package paths:
defineReactExtension is reserved for future React extension support. It currently returns the input unchanged. Adopting the import path is safe; the runtime is intentionally a no-op until the registry lands.
import { renderTwig } from '@emulsify/core/storybook';
import { registerTwigExtensions } from '@emulsify/core/extensions/twig';
import { defineReactExtension } from '@emulsify/core/extensions/react';Vite consumers can import the shared config from @emulsify/core/vite and public Vite plugin helpers from @emulsify/core/vite/plugins.