@emulsify/core 4.0.4 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/css-components.js +3 -0
- package/.storybook/css-dist.js +1 -0
- package/.storybook/main-static-assets.js +217 -0
- package/.storybook/main-vite.js +224 -0
- package/.storybook/main.js +10 -370
- package/.storybook/manager-head.css +120 -0
- package/.storybook/preview.js +8 -18
- package/.storybook/utils.js +1 -1
- package/README.md +11 -3
- package/config/a11y.config.js +2 -1
- package/config/vite/environment.js +2 -0
- package/config/vite/plugins/virtual-twig-asset-sources.js +29 -3
- package/config/vite/project-config.js +65 -0
- package/config/vite/project-structure.js +35 -1
- package/package.json +5 -1
- package/scripts/a11y.js +150 -13
- package/scripts/audit.js +14 -2
- package/src/storybook/preview-decorator.js +45 -0
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* dist/components so the mirrored component CSS is not loaded twice.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
// Load mirrored component output first so Drupal adapter stories match theme roots.
|
|
9
10
|
import.meta.glob('../../../../components/**/*.css', { eager: true });
|
|
11
|
+
|
|
12
|
+
// Keep shared dist CSS while avoiding duplicate generated component CSS.
|
|
10
13
|
import.meta.glob(
|
|
11
14
|
['../../../../dist/**/*.css', '!../../../../dist/components/**/*.css'],
|
|
12
15
|
{ eager: true },
|
package/.storybook/css-dist.js
CHANGED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keeps Storybook static directory config aligned to the consuming project.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<string|{from: string, to: string}>} staticDirs - Static directory entries.
|
|
10
|
+
* @returns {Array<string|{from: string, to: string}>} Existing static directory entries.
|
|
11
|
+
*/
|
|
12
|
+
function existingStaticDirs(staticDirs) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const existing = [];
|
|
15
|
+
|
|
16
|
+
for (const staticDir of staticDirs) {
|
|
17
|
+
const directory =
|
|
18
|
+
typeof staticDir === 'string' ? staticDir : staticDir.from;
|
|
19
|
+
|
|
20
|
+
if (!directory || !fs.existsSync(directory)) continue;
|
|
21
|
+
|
|
22
|
+
const key =
|
|
23
|
+
typeof staticDir === 'string'
|
|
24
|
+
? staticDir
|
|
25
|
+
: `${staticDir.from || ''}\0${staticDir.to || ''}`;
|
|
26
|
+
if (seen.has(key)) continue;
|
|
27
|
+
|
|
28
|
+
seen.add(key);
|
|
29
|
+
existing.push(staticDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return existing;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build static directory mounts for normalized project asset roots.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} env - Resolved project paths used by Storybook.
|
|
39
|
+
* @returns {Array<string|{from: string, to: string}>} Static directory entries.
|
|
40
|
+
*/
|
|
41
|
+
export function buildAssetStaticDirs(env) {
|
|
42
|
+
const configuredAssetRoots = Array.isArray(env.projectStructure?.assetRoots)
|
|
43
|
+
? env.projectStructure.assetRoots
|
|
44
|
+
: [];
|
|
45
|
+
const assetRoots = [
|
|
46
|
+
...configuredAssetRoots,
|
|
47
|
+
path.resolve(projectRoot, 'assets'),
|
|
48
|
+
path.resolve(projectRoot, 'src/assets'),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return existingStaticDirs([
|
|
52
|
+
...assetRoots.map((root) => ({
|
|
53
|
+
from: root,
|
|
54
|
+
to: '/assets',
|
|
55
|
+
})),
|
|
56
|
+
{
|
|
57
|
+
from: path.resolve(projectRoot, 'dist/assets'),
|
|
58
|
+
to: '/assets',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
from: path.resolve(projectRoot, 'dist/assets'),
|
|
62
|
+
to: '/',
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks whether a resolved file path stays inside an expected directory.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} filePath - Resolved candidate file path.
|
|
71
|
+
* @param {string} directory - Resolved directory that must contain the file.
|
|
72
|
+
* @returns {boolean} Whether the file path is inside the directory.
|
|
73
|
+
*/
|
|
74
|
+
function isWithinDirectory(filePath, directory) {
|
|
75
|
+
// `path.relative()` exposes traversal attempts as `..` or absolute paths.
|
|
76
|
+
const relativePath = path.relative(directory, filePath);
|
|
77
|
+
return Boolean(
|
|
78
|
+
relativePath &&
|
|
79
|
+
!relativePath.startsWith('..') &&
|
|
80
|
+
!path.isAbsolute(relativePath),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns a browser content type for generated files served by Storybook.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} filePath - Resolved file path being served.
|
|
88
|
+
* @returns {string} HTTP content type header value.
|
|
89
|
+
*/
|
|
90
|
+
function contentTypeForFile(filePath) {
|
|
91
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
92
|
+
// Keep this map small; unknown generated files can still download as binary.
|
|
93
|
+
const types = {
|
|
94
|
+
'.css': 'text/css; charset=utf-8',
|
|
95
|
+
'.gif': 'image/gif',
|
|
96
|
+
'.jpg': 'image/jpeg',
|
|
97
|
+
'.jpeg': 'image/jpeg',
|
|
98
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
99
|
+
'.json': 'application/json; charset=utf-8',
|
|
100
|
+
'.png': 'image/png',
|
|
101
|
+
'.svg': 'image/svg+xml; charset=utf-8',
|
|
102
|
+
'.webp': 'image/webp',
|
|
103
|
+
'.woff': 'font/woff',
|
|
104
|
+
'.woff2': 'font/woff2',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return types[extension] || 'application/octet-stream';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serves generated dist files that may not exist when `staticDirs` is built.
|
|
112
|
+
*
|
|
113
|
+
* Storybook validates static directories during config load, but Emulsify
|
|
114
|
+
* projects often generate `dist` after Storybook starts. This middleware keeps
|
|
115
|
+
* those generated asset URLs available without replacing Vite's CSS HMR.
|
|
116
|
+
*
|
|
117
|
+
* @param {import('http').IncomingMessage} req - Vite dev server request.
|
|
118
|
+
* @param {import('http').ServerResponse} res - Vite dev server response.
|
|
119
|
+
* @param {Function} next - Next middleware callback.
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
function serveGeneratedDistFile(req, res, next) {
|
|
123
|
+
const method = req.method || 'GET';
|
|
124
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
125
|
+
next();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let pathname = '';
|
|
130
|
+
try {
|
|
131
|
+
// Malformed URLs should fall through to Storybook's normal Vite server.
|
|
132
|
+
pathname = decodeURIComponent(
|
|
133
|
+
new URL(req.url || '/', 'http://localhost').pathname,
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
next();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// These URL shapes match Emulsify's compiled CSS and sprite references.
|
|
141
|
+
const routes = [
|
|
142
|
+
{
|
|
143
|
+
pathname: '/icons.svg',
|
|
144
|
+
file: path.resolve(projectRoot, 'dist/assets/icons.svg'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
prefix: '/assets/',
|
|
148
|
+
directory: path.resolve(projectRoot, 'dist/assets'),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
prefix: '/dist/',
|
|
152
|
+
directory: path.resolve(projectRoot, 'dist'),
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
const route = routes.find(({ prefix, pathname: routePathname }) =>
|
|
156
|
+
routePathname ? pathname === routePathname : pathname.startsWith(prefix),
|
|
157
|
+
);
|
|
158
|
+
if (!route) {
|
|
159
|
+
next();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const filePath = route.file
|
|
164
|
+
? route.file
|
|
165
|
+
: path.resolve(route.directory, pathname.slice(route.prefix.length));
|
|
166
|
+
// Resolve from known roots only, then reject traversal before reading.
|
|
167
|
+
if (route.directory && !isWithinDirectory(filePath, route.directory)) {
|
|
168
|
+
next();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const stats = fs.statSync(filePath);
|
|
174
|
+
if (!stats.isFile()) {
|
|
175
|
+
next();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (path.extname(filePath).toLowerCase() === '.css') {
|
|
179
|
+
next();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
res.statusCode = 200;
|
|
184
|
+
res.setHeader('Content-Type', contentTypeForFile(filePath));
|
|
185
|
+
res.setHeader('Content-Length', stats.size);
|
|
186
|
+
if (method === 'HEAD') {
|
|
187
|
+
res.end();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
res.end(fs.readFileSync(filePath));
|
|
191
|
+
} catch {
|
|
192
|
+
next();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Adds Vite dev-server access to generated dist files.
|
|
198
|
+
*
|
|
199
|
+
* CSS itself is still imported through native `import.meta.glob()` calls in the
|
|
200
|
+
* preview runtime; this plugin only fills the static-file gap for late-created
|
|
201
|
+
* dist assets.
|
|
202
|
+
*
|
|
203
|
+
* @returns {import('vite').Plugin} Vite middleware plugin.
|
|
204
|
+
*/
|
|
205
|
+
export function makeGeneratedDistFilesPlugin() {
|
|
206
|
+
return {
|
|
207
|
+
name: 'emulsify-generated-dist-files',
|
|
208
|
+
configureServer(server) {
|
|
209
|
+
server.middlewares.use(serveGeneratedDistFile);
|
|
210
|
+
// Watch generated assets so Vite notices files created after startup.
|
|
211
|
+
server.watcher.add([
|
|
212
|
+
path.join(projectRoot, 'dist/**/*.css'),
|
|
213
|
+
path.join(projectRoot, 'dist/assets/**/*'),
|
|
214
|
+
]);
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import viteConfig from '../config/vite/vite.config.js';
|
|
3
|
+
import { twigExtensionModuleSpecifiers } from '../config/vite/twig-extensions.js';
|
|
4
|
+
import {
|
|
5
|
+
mergeReactSingletonOptimizeDeps,
|
|
6
|
+
mergeReactSingletonResolve,
|
|
7
|
+
} from '../config/vite/utils/react-singleton.js';
|
|
8
|
+
import { makeGeneratedDistFilesPlugin } from './main-static-assets.js';
|
|
9
|
+
|
|
10
|
+
// Twig glob maps are provided by config/vite/plugins/virtual-twig-globs.js.
|
|
11
|
+
const twigVirtualModuleIds = [
|
|
12
|
+
'virtual:emulsify-twig-globs',
|
|
13
|
+
'virtual:emulsify-twig-asset-sources',
|
|
14
|
+
'virtual:emulsify-twig-extension-installers',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const twigRuntimeOptimizeDepsExclude = [
|
|
18
|
+
...twigVirtualModuleIds,
|
|
19
|
+
'@emulsify/core/storybook/twig/source-function',
|
|
20
|
+
'@emulsify/core/storybook/twig/source',
|
|
21
|
+
'@emulsify/core/storybook/twig/resolver',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Vite-generated Storybook chunks should not share `/assets` with project
|
|
26
|
+
* static files. Storybook copies staticDirs while the preview build runs, so
|
|
27
|
+
* keeping generated chunks in a separate folder avoids concurrent writers in
|
|
28
|
+
* `.out/assets`.
|
|
29
|
+
*
|
|
30
|
+
* @type {string}
|
|
31
|
+
*/
|
|
32
|
+
const storybookViteAssetsDir = 'storybook-assets';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge Storybook and project optimizeDeps excludes with Core Twig runtime IDs.
|
|
36
|
+
*
|
|
37
|
+
* Storybook's dependency optimizer runs before normal Vite virtual module
|
|
38
|
+
* resolution. Core Twig runtime modules import virtual IDs that must stay in
|
|
39
|
+
* the Vite module graph so Emulsify's virtual plugins can resolve them.
|
|
40
|
+
*
|
|
41
|
+
* @param {...string[]} excludeLists - Existing optimizeDeps exclude arrays.
|
|
42
|
+
* @returns {string[]} Merged exclude list.
|
|
43
|
+
*/
|
|
44
|
+
function mergeTwigRuntimeOptimizeDepsExcludes(...excludeLists) {
|
|
45
|
+
return Array.from(
|
|
46
|
+
new Set([
|
|
47
|
+
...excludeLists.flatMap((excludeList) =>
|
|
48
|
+
Array.isArray(excludeList) ? excludeList : [],
|
|
49
|
+
),
|
|
50
|
+
...twigRuntimeOptimizeDepsExclude,
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Keep Emulsify Twig virtual imports out of Storybook dependency prebundles.
|
|
57
|
+
*
|
|
58
|
+
* @returns {import('esbuild').Plugin} Esbuild plugin for optimizeDeps.
|
|
59
|
+
*/
|
|
60
|
+
function makeTwigVirtualModuleOptimizerPlugin() {
|
|
61
|
+
return {
|
|
62
|
+
name: 'emulsify-twig-virtual-modules',
|
|
63
|
+
setup(build) {
|
|
64
|
+
build.onResolve(
|
|
65
|
+
{ filter: /^virtual:emulsify-twig-(?:globs|asset-sources)$/ },
|
|
66
|
+
(args) => ({
|
|
67
|
+
path: args.path,
|
|
68
|
+
external: true,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds the Storybook Vite config merger.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} resolvedStorybookEnv - Resolved project paths used by Storybook.
|
|
79
|
+
* @returns {Function} Storybook `viteFinal` callback.
|
|
80
|
+
*/
|
|
81
|
+
export function createViteFinal(resolvedStorybookEnv) {
|
|
82
|
+
return async function viteFinal(config) {
|
|
83
|
+
const { mergeConfig } = await import('vite');
|
|
84
|
+
const env = resolvedStorybookEnv;
|
|
85
|
+
const storybookBuildConfig = config?.build || {};
|
|
86
|
+
|
|
87
|
+
// Keep using the `serve` branch of the shared Vite config here. Storybook
|
|
88
|
+
// has historically consumed that branch, while `mode` still reflects
|
|
89
|
+
// whether Storybook is running in development or production.
|
|
90
|
+
const mode = config?.mode || 'development';
|
|
91
|
+
const baseViteConfig =
|
|
92
|
+
typeof viteConfig === 'function'
|
|
93
|
+
? await viteConfig({ command: 'serve', mode })
|
|
94
|
+
: viteConfig;
|
|
95
|
+
const existingDefine = (config && config.define) || {};
|
|
96
|
+
const viteDefine = (baseViteConfig && baseViteConfig.define) || {};
|
|
97
|
+
|
|
98
|
+
// Allow Storybook's dev server to read component sources from the project
|
|
99
|
+
// root and any structure override paths used by Emulsify consumers.
|
|
100
|
+
const allowList = new Set([
|
|
101
|
+
...(config?.server?.fs?.allow || []),
|
|
102
|
+
env.projectDir,
|
|
103
|
+
path.resolve(env.projectDir, 'src'),
|
|
104
|
+
path.resolve(env.projectDir, 'components'),
|
|
105
|
+
path.resolve(env.projectDir, 'dist'),
|
|
106
|
+
...(Array.isArray(env.projectStructure?.sourceRoots)
|
|
107
|
+
? env.projectStructure.sourceRoots
|
|
108
|
+
: []),
|
|
109
|
+
...(Array.isArray(env.componentRoots) ? env.componentRoots : []),
|
|
110
|
+
...(Array.isArray(env.structureRoots) ? env.structureRoots : []),
|
|
111
|
+
...(env.namespaceRoots && typeof env.namespaceRoots === 'object'
|
|
112
|
+
? Object.values(env.namespaceRoots)
|
|
113
|
+
: []),
|
|
114
|
+
...(Array.isArray(env.projectStructure?.assetRoots)
|
|
115
|
+
? env.projectStructure.assetRoots
|
|
116
|
+
: []),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// Twig files are loaded through custom resolvers/plugins, so they need to
|
|
120
|
+
// be treated as importable assets by Storybook's Vite pipeline.
|
|
121
|
+
const assetsInclude = Array.from(
|
|
122
|
+
new Set([
|
|
123
|
+
...(config.assetsInclude || []),
|
|
124
|
+
...(baseViteConfig.assetsInclude || []),
|
|
125
|
+
'**/*.twig',
|
|
126
|
+
]),
|
|
127
|
+
);
|
|
128
|
+
const optimizeDepsInclude = mergeReactSingletonOptimizeDeps(
|
|
129
|
+
baseViteConfig?.optimizeDeps?.include,
|
|
130
|
+
config?.optimizeDeps?.include,
|
|
131
|
+
[
|
|
132
|
+
'twig',
|
|
133
|
+
'@emulsify/core/extensions/twig',
|
|
134
|
+
...twigExtensionModuleSpecifiers(env),
|
|
135
|
+
],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const mergedConfig = mergeConfig(config, {
|
|
139
|
+
...baseViteConfig,
|
|
140
|
+
resolve: mergeReactSingletonResolve(baseViteConfig, config),
|
|
141
|
+
define: {
|
|
142
|
+
// Preserve shared and Storybook-provided constants, then publish the
|
|
143
|
+
// resolved Emulsify environment to client-side code.
|
|
144
|
+
...viteDefine,
|
|
145
|
+
...existingDefine,
|
|
146
|
+
__EMULSIFY_ENV__: JSON.stringify(env),
|
|
147
|
+
'globalThis.__EMULSIFY_ENV__': JSON.stringify(env),
|
|
148
|
+
},
|
|
149
|
+
server: {
|
|
150
|
+
...(baseViteConfig?.server || {}),
|
|
151
|
+
fs: {
|
|
152
|
+
allow: Array.from(allowList),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
assetsInclude,
|
|
156
|
+
plugins: [
|
|
157
|
+
...(baseViteConfig?.plugins || []),
|
|
158
|
+
makeGeneratedDistFilesPlugin(),
|
|
159
|
+
],
|
|
160
|
+
esbuild: {
|
|
161
|
+
// Some downstream code is authored as `.js` files containing JSX, so
|
|
162
|
+
// keep Storybook's esbuild settings aligned with the shared Vite config.
|
|
163
|
+
jsx: 'automatic',
|
|
164
|
+
loader: 'jsx',
|
|
165
|
+
include: /.*\.jsx?$/,
|
|
166
|
+
exclude: [],
|
|
167
|
+
},
|
|
168
|
+
optimizeDeps: {
|
|
169
|
+
...(baseViteConfig?.optimizeDeps || {}),
|
|
170
|
+
...(config?.optimizeDeps || {}),
|
|
171
|
+
include: optimizeDepsInclude,
|
|
172
|
+
exclude: mergeTwigRuntimeOptimizeDepsExcludes(
|
|
173
|
+
baseViteConfig?.optimizeDeps?.exclude,
|
|
174
|
+
config?.optimizeDeps?.exclude,
|
|
175
|
+
),
|
|
176
|
+
esbuildOptions: {
|
|
177
|
+
...(baseViteConfig?.optimizeDeps?.esbuildOptions || {}),
|
|
178
|
+
...(config?.optimizeDeps?.esbuildOptions || {}),
|
|
179
|
+
plugins: [
|
|
180
|
+
...(baseViteConfig?.optimizeDeps?.esbuildOptions?.plugins || []),
|
|
181
|
+
...(config?.optimizeDeps?.esbuildOptions?.plugins || []),
|
|
182
|
+
makeTwigVirtualModuleOptimizerPlugin(),
|
|
183
|
+
],
|
|
184
|
+
loader: {
|
|
185
|
+
...(baseViteConfig?.optimizeDeps?.esbuildOptions?.loader || {}),
|
|
186
|
+
...(config?.optimizeDeps?.esbuildOptions?.loader || {}),
|
|
187
|
+
// Pre-bundle `.js` dependencies with the JSX loader for packages
|
|
188
|
+
// that ship JSX without a `.jsx` extension.
|
|
189
|
+
'.js': 'jsx',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...mergedConfig,
|
|
197
|
+
build: {
|
|
198
|
+
...(mergedConfig.build || {}),
|
|
199
|
+
...(storybookBuildConfig.outDir
|
|
200
|
+
? { outDir: storybookBuildConfig.outDir }
|
|
201
|
+
: {}),
|
|
202
|
+
assetsDir: storybookViteAssetsDir,
|
|
203
|
+
emptyOutDir: false,
|
|
204
|
+
},
|
|
205
|
+
resolve: mergeReactSingletonResolve(mergedConfig),
|
|
206
|
+
optimizeDeps: {
|
|
207
|
+
...(mergedConfig.optimizeDeps || {}),
|
|
208
|
+
include: mergeReactSingletonOptimizeDeps(
|
|
209
|
+
mergedConfig.optimizeDeps?.include,
|
|
210
|
+
),
|
|
211
|
+
exclude: mergeTwigRuntimeOptimizeDepsExcludes(
|
|
212
|
+
mergedConfig.optimizeDeps?.exclude,
|
|
213
|
+
),
|
|
214
|
+
esbuildOptions: {
|
|
215
|
+
...(mergedConfig.optimizeDeps?.esbuildOptions || {}),
|
|
216
|
+
loader: {
|
|
217
|
+
...(mergedConfig.optimizeDeps?.esbuildOptions?.loader || {}),
|
|
218
|
+
'.js': 'jsx',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
}
|