Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0680440
feat: remove storybook-html in favor of storybook-react v9.x
Jun 10, 2025
3d3518f
feat: install vite and dependencies
cienvaras Jun 29, 2025
4040960
feat: update storybook configuration
cienvaras Jun 30, 2025
60dbdcc
feat: remove all webpack specific plugins but leave the config files …
Sep 10, 2025
ab7efbd
feat: port file compilation logic from webpack and split up config
Sep 11, 2025
b255a4d
chore: version bump
Sep 11, 2025
4b167b5
feat: enable vite watching and refactor the entry array being passed …
Sep 12, 2025
89cd952
chore: remove eslint-disable flag
Sep 12, 2025
fdcb146
feat: configure compilation logic for JS/CSS file locations
Sep 15, 2025
602bd8a
feat: copy srcmaps to compiled folders
Sep 15, 2025
eb6b2bc
feat: copy srcmaps to compiled folders
Sep 15, 2025
918ef9d
feat: jsdoc standards for entries.js
Sep 15, 2025
4c25bb6
chore: cleanup unused packages
Sep 15, 2025
e6c6983
chore: prettier format changes
Sep 15, 2025
beb3b1f
Merge pull request #208 from emulsify-ds/feat-split-plugin-entries-en…
callinmullaney Sep 15, 2025
c62331d
fix: vite integration issues fix
cienvaras Sep 29, 2025
ec62bdb
chore: dependency updates
Oct 1, 2025
fc6316a
feat: ensure static assets are copied over with compiled assets
Oct 2, 2025
a7509d5
chore: eslint fixes for entries
Oct 2, 2025
e3ea68b
chore: eslint fixes for environment
Oct 2, 2025
858d2fb
chore: eslint fixes for plugins
Oct 2, 2025
7fb24e3
chore: eslint fixes for plugins
Oct 2, 2025
40d4a84
feat: add sass glob importer support
Oct 2, 2025
91cb110
feat: account for structureImplementations when compiling files for l…
Oct 3, 2025
ff75a72
feat: account for structureImplementations when compiling files for l…
Oct 3, 2025
8945833
chore: lint fixes
Oct 3, 2025
dcf8a68
chore: lint fixes
Oct 3, 2025
e07bb85
feat: change legacy reference to structure overrides
Oct 3, 2025
592ae21
feat: move svgsprite plugin to baseplugins
Oct 3, 2025
b3ebff0
chore: restore legacy emulsify compiling structure
Oct 3, 2025
0aa365d
feat: allow for plugin and vite.config extension per project
Oct 3, 2025
89ef589
feat: better code commenting for vite.config.js
Oct 3, 2025
2bbd0ae
Merge pull request #220 from emulsify-ds/emulsify-537
callinmullaney Oct 3, 2025
7a8bf21
chore: remove webpack references
Oct 28, 2025
2f67c22
feat: add twig polyfils for storybook configuration
Oct 29, 2025
9f79d9f
feat: preverve dir structure when assets are copied to dist
Dec 1, 2025
18c58fe
feat: add relativizr plugin for url() processed in scss
Dec 1, 2025
ca579a4
fix: patch broken svg attributes that contain a colon
Dec 1, 2025
6e10130
chore: remove console log
Dec 1, 2025
9ff70e5
feat: properly merge vite config so non-js files are loaded properly …
Dec 2, 2025
4e5a515
chore: update storybook to v10
cienvaras Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .storybook/emulsifyTheme.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Documentation on theming Storybook: https://storybook.js.org/docs/configurations/theming/
import { create } from '@storybook/theming';
import { create } from 'storybook/theming';

export default create({
base: 'dark',
Expand Down
125 changes: 107 additions & 18 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
* @module .storybook/main
*/

import { resolve } from 'path';
import fs from 'fs';
import path from 'path';
import path, { resolve } from 'path';
import { fileURLToPath } from 'url';
import configOverrides from '../../../../config/emulsify-core/storybook/main.js';
import viteConfig from '../config/vite/vite.config.js';
import { resolveEnvironment } from '../config/vite/environment.js';

/**
* The full path to the current file (ESM compatible).
* @type {string}
*/
const __filename = fileURLToPath(import.meta.url);
const _filename = fileURLToPath(import.meta.url);

/**
* The directory name of the current module file.
* @type {string}
*/
const __dirname = path.dirname(__filename);
const _dirname = path.dirname(_filename);

/**
* Safely apply any user-provided overrides or fall back to an empty object.
Expand All @@ -41,7 +42,7 @@ const config = {
* @type {string[]}
*/
stories: [
'../../../../(src|components)/**/*.stories.@(js|jsx|ts|tsx)',
'../../../../@(src|components)/**/*.stories.@(js|jsx|ts|tsx)',
],

/**
Expand All @@ -59,28 +60,26 @@ const config = {
* @type {string[]}
*/
addons: [
'../../../@storybook/addon-a11y',
'../../../@storybook/addon-links',
'../../../@storybook/addon-essentials',
'../../../@storybook/addon-themes',
'../../../@storybook/addon-styling-webpack',
'@storybook/addon-a11y',
'@storybook/addon-links',
'@storybook/addon-themes',
],

/**
* Core builder configuration for Storybook.
* @type {{builder: string, disableTelemetry: boolean}}
*/
core: {
builder: 'webpack5',
builder: '@storybook/builder-vite',
disableTelemetry: true,
},

/**
* Framework specification for Storybook (HTML + Webpack5).
* Framework specification for Storybook (HTML + Vite).
* @type {{name: string, options: object}}
*/
framework: {
name: '@storybook/html-webpack5',
name: '@storybook/react-vite',
options: {},
},

Expand Down Expand Up @@ -204,7 +203,7 @@ const config = {

// load external manager-head.html if present
const externalManagerHeadPath = resolve(
__dirname,
_dirname,
'../../../../config/emulsify-core/storybook/manager-head.html'
);
let externalManagerHtml = '';
Expand All @@ -213,8 +212,8 @@ const config = {
}

return `${head}
${inlineStyles}
${externalManagerHtml}`;
${inlineStyles}
${externalManagerHtml}`;
},

/**
Expand All @@ -224,7 +223,7 @@ ${externalManagerHtml}`;
*/
previewHead: (head) => {
const externalHeadPath = resolve(
__dirname,
_dirname,
'../../../../config/emulsify-core/storybook/preview-head.html'
);

Expand All @@ -234,7 +233,97 @@ ${externalManagerHtml}`;
}

return `${head}
${externalHtml}`;
${externalHtml}`;
},

// Storybook specific Vite configuration.
async viteFinal(config) {
const { mergeConfig } = await import('vite');
const env = resolveEnvironment();
const baseViteConfig =
typeof viteConfig === 'function'
? await viteConfig({ command: 'serve', mode: config?.mode || 'development' })
: viteConfig;
const existingDefine = (config && config.define) || {};
const viteDefine = (baseViteConfig && baseViteConfig.define) || {};
const allowList = new Set([
...(config?.server?.fs?.allow || []),
env.projectDir,
path.resolve(env.projectDir, 'src'),
path.resolve(env.projectDir, 'components'),
path.resolve(env.projectDir, 'dist'),
]);
const assetsInclude = Array.from(
new Set([...(config.assetsInclude || []), ...(baseViteConfig.assetsInclude || []), '**/*.twig']),
);
const toRootRel = (abs) => {
const rel = path.relative(env.projectDir, abs);
const normalized = rel.split(path.sep).join('/');
return `/${normalized}`.replace(/\/{2,}/g, '/');
};
const candidateRoots =
env.structureOverrides && Array.isArray(env.structureRoots) && env.structureRoots.length
? env.structureRoots
: env.srcDir
? [path.join(env.srcDir, 'components')]
: [];
const rootRels = candidateRoots.map(toRootRel);
const globBases = rootRels.length ? rootRels : ['/src/components', '/components'];
const twigGlobImports = `mergeGlobMaps([\n${globBases
.map((base) => ` import.meta.glob('${base}/**/*.twig', { eager: true })`)
.join(',\n')}\n])`;

return mergeConfig(config, {
...baseViteConfig,
define: {
...viteDefine,
...existingDefine,
__EMULSIFY_ENV__: JSON.stringify(env),
},
server: {
...(baseViteConfig?.server || {}),
fs: {
allow: Array.from(allowList),
},
},
assetsInclude,
plugins: [
...(baseViteConfig?.plugins || []),
{
name: 'emulsify-inject-twig-globs',
enforce: 'pre',
transform(code, id) {
const cleanId = id.split('?')[0];
if (!cleanId.endsWith('/.storybook/polyfills/twig-resolver.js')) return null;
const replaced = code.replace(
/__EMULSIFY_TWIG_GLOB_IMPORTS__/g,
twigGlobImports,
);
return replaced === code ? null : replaced;
},
},
],
esbuild: {
'jsx': 'automatic',
loader: 'jsx',
include: /.*\.jsx?$/,
exclude: [],
},
optimizeDeps: {
include: [
'path',
'twig',
'twig-drupal-filters',
'bem-twig-extension',
'add-attributes-twig-extension',
],
esbuildOptions: {
loader: {
'.js': 'jsx',
},
},
},
})
},

// Merge in user overrides without modifying original logic
Expand Down
3 changes: 1 addition & 2 deletions .storybook/manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// .storybook/manager.js

import { addons } from '@storybook/manager-api';
import { addons } from 'storybook/manager-api';
import emulsifyTheme from './emulsifyTheme';

/**
Expand Down Expand Up @@ -42,4 +42,3 @@ import('../../../../config/emulsify-core/storybook/theme')
theme: emulsifyTheme,
});
});

36 changes: 36 additions & 0 deletions .storybook/polyfills/twig-include.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

import resolveTemplate from './twig-resolver.js';

/**
* Twig `include()` polyfill.
* Mirrors Drupal behaviour inside Storybook.
* @param {string} templateName
* @param {Object} [variables]
* @param {boolean} [withContext=false]
* @return {string}
*/
function twigInclude(Twig) {
Twig.extendFunction('include', (...args) => {
let [templateName, variables = {}, withContext = false] = args;
if (typeof withContext !== 'boolean' && variables && typeof variables.with_context !== 'undefined') {
withContext = variables.with_context;
delete variables.with_context;
}

try {
const templateFn = resolveTemplate(templateName);
if (!templateFn) return '';

const finalContext = withContext && typeof this === 'object'
? { ...(this.context || {}), ...variables }
: variables;

return templateFn(finalContext);
} catch (err) {
console.error(`Twig include() failed for: ${templateName}`, err);
return '';
}
});
};

export default twigInclude;
129 changes: 129 additions & 0 deletions .storybook/polyfills/twig-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { getProjectMachineName } from '../utils';

const namespace = getProjectMachineName();

/**
* Build a dynamic module map of Twig files from all possible component roots.
* We rely on __EMULSIFY_ENV__ injected in .storybook/main.js via viteFinal(),
* using the same “structure overrides / roots” logic you use in environment.js.
*/
const ENV = (typeof __EMULSIFY_ENV__ !== 'undefined' && __EMULSIFY_ENV__) || {};

// Determine candidate roots: prefer structure overrides, otherwise src/components.
const candidateRoots = Array.isArray(ENV?.structureRoots) && ENV?.structureOverrides && ENV.structureRoots.length
? ENV.structureRoots
: (ENV?.srcDir ? [`${ENV.srcDir}/components`] : []);

/**
* Convert an absolute path to a Vite project-root-relative path, prefixed with "/".
* Keys produced by import.meta.glob() will use these forms.
* @param {string} abs
* @returns {string}
*/
function toRootRel(abs) {
if (!abs) return '';
const projectDir = ENV?.projectDir || '';
if (projectDir && abs.startsWith(projectDir)) {
const rel = abs.slice(projectDir.length);
return rel.startsWith('/') ? rel : `/${rel}`;
}
// Fall back to assuming it's already project-root-relative-ish.
return abs.startsWith('/') ? abs : `/${abs}`;
}

// Build globs for each candidate root. We’ll eagerly import all Twig modules.
const rootRels = candidateRoots.map(toRootRel);

// Vite doesn’t support an array directly in a single import.meta.glob(),
// so merge multiple glob maps into one.
function mergeGlobMaps(maps) {
return Object.assign({}, ...maps);
}

// Typical component layouts we want to support:
// - Nested component folders: /root/thing/thing.twig
// - Flat component files: /root/thing.twig
// We pre-load everything under each root so resolution is O(1).
const twigModules = mergeGlobMaps(
rootRels.flatMap((base) => [
import.meta.glob(`${base}/**/*.twig`, { eager: true }),
])
);

// Helper: generate likely keys for a given component “part” under every root.
// We try the canonical “part/part.twig”, then “part.twig”.
function candidateKeysForPart(part) {
const keys = [];
for (const base of rootRels) {
keys.push(`${base}/${part}/${part}.twig`);
keys.push(`${base}/${part}.twig`);
}
return keys;
}

/**
* Resolve template identifier to compiled Twig function.
* Supports: @component.twig, namespace:component, @namespace/component, namespace/component
* @param {string} name Template identifier
* @returns {Function|undefined} Compiled function or noop
*/
function resolveTemplate(name) {
// namespace:icon, @namespace/icon.twig
if (name.startsWith(`${namespace}:`) || name.startsWith(`@${namespace}/`)) {
const part = name.startsWith(`${namespace}:`)
? name.split(':')[1]
: name.replace(new RegExp(`^@?${namespace}/`), '').replace(/\.twig$/, '');

const candidates = candidateKeysForPart(part);
for (const key of candidates) {
const mod = twigModules[key];
if (mod) {
return mod.default ?? mod;
}
}

// eslint-disable-next-line no-console
console.error(`Cannot resolve Twig component for '${name}'. Tried: ${candidates.join(', ')}`);
}

// @icon.twig → icon/icon.twig (fallback to icon.twig)
if (name.startsWith('@') && name.endsWith('.twig')) {
const part = name.slice(1, -5); // remove leading @ and trailing .twig
const candidates = candidateKeysForPart(part);
for (const key of candidates) {
const mod = twigModules[key];
if (mod) {
return mod.default ?? mod;
}
}
// eslint-disable-next-line no-console
console.error(`Cannot resolve Twig shorthand template '${name}'. Tried: ${candidates.join(', ')}`);
}

// namespace/icon.twig via alias-like usage (without @)
if (name.startsWith(`${namespace}/`)) {
const part = name.replace(new RegExp(`^${namespace}/`), '').replace(/\.twig$/, '');
const candidates = candidateKeysForPart(part);
for (const key of candidates) {
const mod = twigModules[key];
if (mod) {
return mod.default ?? mod;
}
}
// eslint-disable-next-line no-console
console.error(`Cannot resolve Twig alias template '${name}'. Tried: ${candidates.join(', ')}`);
}

// Final attempt: direct key access if caller passed an exact glob key.
const direct = twigModules[name];
if (direct) {
return direct.default ?? direct;
}

// Vite environment: avoid require() fallback; return a safe noop.
// eslint-disable-next-line no-console
console.error(`Cannot resolve Twig template '${name}'`);
return () => '';
}

export default resolveTemplate;
Loading