Skip to content

Commit 44a311a

Browse files
Merge branch 'develop' into fix-EMULSIF-57-preserve-minimized
2 parents 49ccbe1 + cd8a1fa commit 44a311a

15 files changed

Lines changed: 7900 additions & 11480 deletions

.github/workflows/addtoprojects.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ jobs:
1818
# You can target a repository in a different organization
1919
# to the issue
2020
project-url: https://github.com/emulsify-ds/emulsify-core
21-
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
21+
github-token: ${{ secrets.GH_TOKEN }}

.storybook/_drupal.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ window.Drupal = { behaviors: {} };
3232
Drupal.attachBehaviors = function (context, settings) {
3333
context = context || document;
3434
settings = settings || drupalSettings;
35-
/** @type {Object.<string, {attach: Function}>} */
36-
const behaviors = Drupal.behaviors;
35+
/** @type {Array<{attach?: Function}>} */
36+
const behaviors = Object.values(Drupal.behaviors);
3737

3838
// Iterate through each behavior and invoke its attach method if defined.
39-
Object.keys(behaviors).forEach(function (i) {
40-
if (typeof behaviors[i].attach === 'function') {
39+
behaviors.forEach(function (behavior) {
40+
if (typeof behavior.attach === 'function') {
4141
try {
42-
behaviors[i].attach(context, settings);
42+
behavior.attach(context, settings);
4343
} catch (e) {
4444
Drupal.throwError(e);
4545
}

.storybook/emulsifyTheme.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Documentation on theming Storybook: https://storybook.js.org/docs/configurations/theming/
2-
import { create } from '@storybook/theming';
2+
import { create } from 'storybook/theming';
33

44
export default create({
55
base: 'dark',

.storybook/main.js

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { resolve } from 'path';
1111
import fs from 'fs';
1212
import path from 'path';
1313
import { fileURLToPath } from 'url';
14-
import configOverrides from '../../../../config/emulsify-core/storybook/main.js';
14+
import { createRequire } from 'module';
15+
import extendWebpackConfig from './webpack.config.js';
16+
17+
const require = createRequire(import.meta.url);
1518

1619
/**
1720
* The full path to the current file (ESM compatible).
@@ -23,26 +26,70 @@ const _filename = fileURLToPath(import.meta.url);
2326
* The directory name of the current module file.
2427
* @type {string}
2528
*/
26-
const _dirname = path.dirname(_filename);
29+
const _dirname = path.dirname(_filename);
30+
31+
/**
32+
* Migrate the consumer Storybook theme import from "@storybook/theming" to
33+
* "storybook/theming" when needed.
34+
*
35+
* This runs opportunistically during startup and never throws so Storybook
36+
* startup is resilient across all projects.
37+
*/
38+
const migrateConsumerThemeImport = () => {
39+
try {
40+
const themeConfigPath = resolve(
41+
_dirname,
42+
'../../../../config/emulsify-core/storybook/theme.js',
43+
);
44+
45+
if (!fs.existsSync(themeConfigPath)) {
46+
return;
47+
}
48+
49+
const originalThemeConfig = fs.readFileSync(themeConfigPath, 'utf8');
50+
51+
if (!originalThemeConfig.includes('@storybook/theming')) {
52+
return;
53+
}
54+
55+
const migratedThemeConfig = originalThemeConfig.replace(
56+
/(['"])@storybook\/theming\1/g,
57+
'$1storybook/theming$1',
58+
);
59+
60+
if (migratedThemeConfig !== originalThemeConfig) {
61+
fs.writeFileSync(themeConfigPath, migratedThemeConfig, 'utf8');
62+
}
63+
} catch {
64+
// Ignore migration failures so Storybook startup is never blocked.
65+
}
66+
};
67+
68+
migrateConsumerThemeImport();
2769

2870
/**
2971
* Safely apply any user-provided overrides or fall back to an empty object.
3072
* @type {object}
3173
*/
32-
const safeConfigOverrides = configOverrides || {};
74+
const safeConfigOverrides = (() => {
75+
try {
76+
const overridesModule = require('../../../../config/emulsify-core/storybook/main.js');
77+
return overridesModule.default || overridesModule || {};
78+
} catch {
79+
return {};
80+
}
81+
})();
3382

3483
/**
3584
* Primary Storybook configuration object.
36-
* @type {import('@storybook/core-common').StorybookConfig}
85+
* @type {import('storybook/internal/types').StorybookConfig}
3786
*/
3887
const config = {
3988
/**
4089
* Patterns for locating story files under src or components directories.
4190
* @type {string[]}
4291
*/
43-
stories: [
44-
'../../../../(src|components)/**/*.stories.@(js|jsx|ts|tsx)',
45-
],
92+
stories: ['../../../../@(src|components)/**/*.stories.@(js|jsx|ts|tsx)'],
4693

4794
/**
4895
* Directories to serve as static assets in the Storybook build.
@@ -52,35 +99,39 @@ const config = {
5299
'../../../../assets/images',
53100
'../../../../assets/icons',
54101
'../../../../dist',
102+
'../../../../assets/videos',
55103
],
56104

57105
/**
58106
* List of Storybook addons to enable various features.
59107
* @type {string[]}
60108
*/
61109
addons: [
62-
'../../../@storybook/addon-a11y',
63-
'../../../@storybook/addon-links',
64-
'../../../@storybook/addon-essentials',
65-
'../../../@storybook/addon-themes',
66-
'../../../@storybook/addon-styling-webpack',
110+
'@storybook/addon-a11y',
111+
'@storybook/addon-links',
112+
'@storybook/addon-themes',
113+
'@storybook/addon-styling-webpack',
67114
],
68115

69116
/**
70117
* Core builder configuration for Storybook.
71-
* @type {{builder: string, disableTelemetry: boolean}}
118+
* Storybook 9 splits the HTML renderer from the webpack builder, so the
119+
* builder must be declared explicitly instead of relying on html-webpack5.
120+
* @type {{builder: {name: string}, disableTelemetry: boolean}}
72121
*/
73122
core: {
74-
builder: 'webpack5',
123+
builder: {
124+
name: '@storybook/builder-webpack5',
125+
},
75126
disableTelemetry: true,
76127
},
77128

78129
/**
79-
* Framework specification for Storybook (HTML + Webpack5).
130+
* Framework specification for Storybook's HTML renderer.
80131
* @type {{name: string, options: object}}
81132
*/
82133
framework: {
83-
name: '@storybook/html-webpack5',
134+
name: '@storybook/server-webpack5',
84135
options: {},
85136
},
86137

@@ -205,7 +256,7 @@ const config = {
205256
// load external manager-head.html if present
206257
const externalManagerHeadPath = resolve(
207258
_dirname,
208-
'../../../../config/emulsify-core/storybook/manager-head.html'
259+
'../../../../config/emulsify-core/storybook/manager-head.html',
209260
);
210261
let externalManagerHtml = '';
211262
if (fs.existsSync(externalManagerHeadPath)) {
@@ -225,7 +276,7 @@ ${externalManagerHtml}`;
225276
previewHead: (head) => {
226277
const externalHeadPath = resolve(
227278
_dirname,
228-
'../../../../config/emulsify-core/storybook/preview-head.html'
279+
'../../../../config/emulsify-core/storybook/preview-head.html',
229280
);
230281

231282
let externalHtml = '';
@@ -237,6 +288,16 @@ ${externalManagerHtml}`;
237288
${externalHtml}`;
238289
},
239290

291+
/**
292+
* Forward Storybook 9's webpack hook to the existing shared webpack helper so
293+
* custom Twig, Sass, YAML, and resolver behavior still applies.
294+
* @param {object} storybookConfig - Storybook's generated webpack config.
295+
* @param {object} options - Storybook webpack hook options.
296+
* @returns {Promise<object>} The merged webpack config.
297+
*/
298+
webpackFinal: async (storybookConfig, options) =>
299+
extendWebpackConfig({ config: storybookConfig, ...options }),
300+
240301
// Merge in user overrides without modifying original logic
241302
...safeConfigOverrides,
242303
};

.storybook/manager.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// .storybook/manager.js
22

3-
import { addons } from '@storybook/manager-api';
4-
import emulsifyTheme from './emulsifyTheme';
3+
import { addons } from 'storybook/manager-api';
4+
import emulsifyTheme from './emulsifyTheme.js';
55

66
/**
77
* Dynamically import the user-provided Storybook theme override.
@@ -19,7 +19,8 @@ import('../../../../config/emulsify-core/storybook/theme')
1919
*/
2020
const isEmptyObject =
2121
!customTheme ||
22-
(typeof customTheme === 'object' && Object.keys(customTheme).length === 0);
22+
(typeof customTheme === 'object' &&
23+
Object.keys(customTheme).length === 0);
2324

2425
/**
2526
* Apply the chosen theme to Storybook’s manager UI configuration.
@@ -42,4 +43,3 @@ import('../../../../config/emulsify-core/storybook/theme')
4243
theme: emulsifyTheme,
4344
});
4445
});
45-
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import resolveTemplate from './twig-resolver.js';
2+
3+
/**
4+
* Twig `include()` polyfill.
5+
* Mirrors Drupal behaviour inside Storybook.
6+
* @param {string} templateName
7+
* @param {Object} [variables]
8+
* @param {boolean} [withContext=false]
9+
* @return {string}
10+
*/
11+
function twigInclude(Twig) {
12+
Twig.extendFunction('include', (...args) => {
13+
let [templateName, variables = {}, withContext = false] = args;
14+
if (
15+
typeof withContext !== 'boolean' &&
16+
variables &&
17+
typeof variables.with_context !== 'undefined'
18+
) {
19+
withContext = variables.with_context;
20+
delete variables.with_context;
21+
}
22+
23+
try {
24+
const templateFn = resolveTemplate(templateName);
25+
if (!templateFn) return '';
26+
27+
const finalContext =
28+
withContext && typeof this === 'object'
29+
? { ...(this.context || {}), ...variables }
30+
: variables;
31+
32+
return templateFn(finalContext);
33+
} catch (err) {
34+
console.error(`Twig include() failed for: ${templateName}`, err);
35+
return '';
36+
}
37+
});
38+
}
39+
40+
export default twigInclude;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { getProjectMachineName } from '../utils.js';
2+
3+
const namespace = getProjectMachineName();
4+
5+
const twigComponents = require.context(
6+
'../../../../../src/components/',
7+
true,
8+
/\.twig$/,
9+
);
10+
11+
/**
12+
* Resolve template identifier to compiled Twig function.
13+
* Supports: @component.twig, namespace:component, @namespace/component, namespace/component
14+
* @param {string} name Template identifier
15+
* @returns {Function|undefined} Compiled function or noop
16+
*/
17+
function resolveTemplate(name) {
18+
// namespace:icon, @namespace/icon.twig
19+
if (name.startsWith(`${namespace}:`) || name.startsWith(`@${namespace}/`)) {
20+
const part = name.startsWith(`${namespace}:`)
21+
? name.split(':')[1]
22+
: name.replace(`${namespace}/`, '').replace('.twig', '');
23+
const path = `./${part}/${part}.twig`;
24+
try {
25+
const mod = twigComponents(path);
26+
return mod && mod.default ? mod.default : mod;
27+
} catch {
28+
console.error(`Cannot resolve Twig component for '${name}' at '${path}'`);
29+
}
30+
}
31+
32+
// @icon.twig → icon/icon.twig
33+
if (name.startsWith('@') && name.endsWith('.twig')) {
34+
const part = name.slice(1, -5); // remove leading @ and trailing .twig
35+
const path = `./${part}/${part}.twig`;
36+
try {
37+
return twigComponents(path).default || twigComponents(path);
38+
} catch {
39+
console.error(
40+
`Cannot resolve Twig shorthand template '${name}' at '${path}'`,
41+
);
42+
}
43+
}
44+
45+
// namespace/icon.twig via webpack alias
46+
if (name.startsWith(`${namespace}/`)) {
47+
const part = name.slice(namespace.length + 1).replace('.twig', '');
48+
const path = `./${part}/${part}.twig`;
49+
try {
50+
return twigComponents(path).default || twigComponents(path);
51+
} catch {
52+
console.error(
53+
`Cannot resolve Twig alias template '${name}' at '${path}'`,
54+
);
55+
}
56+
}
57+
58+
try {
59+
// Storybook resolves runtime Twig requests through webpack, so this
60+
// fallback intentionally loads a module path determined at render time.
61+
// eslint-disable-next-line security/detect-non-literal-require
62+
const mod = require(name);
63+
return mod && mod.default ? mod.default : mod;
64+
} catch (error) {
65+
console.error(`Cannot resolve Twig template '${name}'`, error);
66+
return () => '';
67+
}
68+
}
69+
70+
export default resolveTemplate;

0 commit comments

Comments
 (0)