@emulsify/core 3.5.0 → 4.0.0

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.
Files changed (110) hide show
  1. package/.cli/init.js +40 -31
  2. package/.storybook/_drupal.js +129 -8
  3. package/.storybook/css-components.js +13 -0
  4. package/.storybook/css-dist.js +5 -0
  5. package/.storybook/emulsifyTheme.js +9 -6
  6. package/.storybook/main.js +397 -106
  7. package/.storybook/manager.js +9 -16
  8. package/.storybook/preview.js +88 -110
  9. package/.storybook/utils.js +69 -74
  10. package/README.md +110 -59
  11. package/config/.stylelintrc.json +2 -6
  12. package/config/a11y.config.js +9 -5
  13. package/config/babel.config.js +6 -11
  14. package/config/eslint.config.js +31 -3
  15. package/config/postcss.config.js +5 -0
  16. package/config/vite/entries.js +227 -0
  17. package/config/vite/environment.js +39 -0
  18. package/config/vite/platforms.js +70 -0
  19. package/config/vite/plugins/copy-src-assets.js +76 -0
  20. package/config/vite/plugins/copy-twig-files.js +84 -0
  21. package/config/vite/plugins/css-asset-relativizer.js +40 -0
  22. package/config/vite/plugins/index.js +105 -0
  23. package/config/vite/plugins/mirror-components.js +358 -0
  24. package/config/vite/plugins/require-context.js +311 -0
  25. package/config/vite/plugins/source-file-index.js +184 -0
  26. package/config/vite/plugins/svg-sprite.js +117 -0
  27. package/config/vite/plugins/twig-extension-installers.js +36 -0
  28. package/config/vite/plugins/twig-module.js +1251 -0
  29. package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
  30. package/config/vite/plugins/virtual-twig-globs.js +136 -0
  31. package/config/vite/plugins/vituum-patch.js +167 -0
  32. package/config/vite/plugins/yaml-module.js +133 -0
  33. package/config/vite/plugins.js +12 -0
  34. package/config/vite/project-config.js +192 -0
  35. package/config/vite/project-extensions.js +177 -0
  36. package/config/vite/project-structure.js +447 -0
  37. package/config/vite/twig-extensions.js +109 -0
  38. package/config/vite/utils/fs-safe.js +66 -0
  39. package/config/vite/utils/paths.js +40 -0
  40. package/config/vite/utils/react-singleton.js +85 -0
  41. package/config/vite/utils/unique.js +36 -0
  42. package/config/vite/vite.config.js +161 -0
  43. package/package.json +164 -75
  44. package/scripts/a11y.js +70 -16
  45. package/scripts/audit-twig-stories.js +378 -0
  46. package/scripts/audit.js +1602 -0
  47. package/scripts/check-node-version.js +18 -0
  48. package/scripts/loadYaml.js +5 -1
  49. package/src/extensions/index.js +8 -0
  50. package/src/extensions/react/index.js +12 -0
  51. package/src/extensions/react/register.js +45 -0
  52. package/src/extensions/shared/attributes.js +308 -0
  53. package/src/extensions/shared/html.js +41 -0
  54. package/src/extensions/shared/lists.js +38 -0
  55. package/src/extensions/shared/object.js +22 -0
  56. package/src/extensions/twig/function-map.js +20 -0
  57. package/src/extensions/twig/functions/add-attributes.js +39 -0
  58. package/src/extensions/twig/functions/bem.js +166 -0
  59. package/src/extensions/twig/index.js +13 -0
  60. package/src/extensions/twig/register.js +52 -0
  61. package/src/extensions/twig/tag-map.js +16 -0
  62. package/src/extensions/twig/tags/switch.js +266 -0
  63. package/src/storybook/index.js +14 -0
  64. package/src/storybook/main-config.js +132 -0
  65. package/src/storybook/platform-behaviors.js +60 -0
  66. package/src/storybook/preview-parameters.js +81 -0
  67. package/src/storybook/render-twig.js +295 -0
  68. package/src/storybook/twig/drupal-filters.js +7 -0
  69. package/src/storybook/twig/include-function.js +109 -0
  70. package/src/storybook/twig/include.js +28 -0
  71. package/src/storybook/twig/reference-paths.js +294 -0
  72. package/src/storybook/twig/resolver.js +318 -0
  73. package/src/storybook/twig/setup.js +39 -0
  74. package/src/storybook/twig/source-events.js +5 -0
  75. package/src/storybook/twig/source-extensions.js +24 -0
  76. package/src/storybook/twig/source-function.js +239 -0
  77. package/src/storybook/twig/source.js +39 -0
  78. package/.all-contributorsrc +0 -45
  79. package/.editorconfig +0 -5
  80. package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
  81. package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
  82. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  83. package/.github/dependabot.yml +0 -6
  84. package/.github/workflows/addtoprojects.yml +0 -21
  85. package/.github/workflows/contributors.yml +0 -37
  86. package/.github/workflows/lint.yml +0 -22
  87. package/.github/workflows/semantic-release.yml +0 -24
  88. package/.husky/commit-msg +0 -2
  89. package/.husky/pre-commit +0 -2
  90. package/.nvmrc +0 -1
  91. package/.prettierignore +0 -4
  92. package/.storybook/polyfills/twig-include.js +0 -40
  93. package/.storybook/polyfills/twig-resolver.js +0 -70
  94. package/.storybook/polyfills/twig-source.js +0 -65
  95. package/.storybook/webpack.config.js +0 -269
  96. package/CODE_OF_CONDUCT.md +0 -56
  97. package/commitlint.config.js +0 -5
  98. package/config/jest.config.js +0 -19
  99. package/config/webpack/app.js +0 -1
  100. package/config/webpack/loaders.js +0 -167
  101. package/config/webpack/optimizers.js +0 -26
  102. package/config/webpack/plugins.js +0 -283
  103. package/config/webpack/resolves.js +0 -157
  104. package/config/webpack/sdc-loader.js +0 -16
  105. package/config/webpack/webpack.common.js +0 -272
  106. package/config/webpack/webpack.dev.js +0 -41
  107. package/config/webpack/webpack.prod.js +0 -6
  108. package/release.config.cjs +0 -30
  109. package/scripts/a11y.test.js +0 -172
  110. package/scripts/loadYaml.test.js +0 -30
@@ -1,283 +0,0 @@
1
- import { resolve, dirname, relative } from 'path';
2
- import webpack from 'webpack';
3
- import { CleanWebpackPlugin } from 'clean-webpack-plugin';
4
- import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts';
5
- import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6
- import SVGSpritemapPlugin from 'svg-spritemap-webpack-plugin';
7
- import CopyPlugin from 'copy-webpack-plugin';
8
- import { sync as globSync } from 'glob';
9
- import fs from 'fs-extra';
10
- import emulsifyConfig from '../../../../../project.emulsify.json' with { type: 'json' };
11
-
12
- /**
13
- * Resolve the directory of this file (without fileURLToPath).
14
- * @type {string}
15
- */
16
- let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
17
- if (process.platform === 'win32' && _filename.startsWith('/')) {
18
- _filename = _filename.slice(1);
19
- }
20
- const _dirname = dirname(_filename);
21
-
22
- /**
23
- * Project root (five levels up).
24
- * @type {string}
25
- */
26
- const projectDir = resolve(_dirname, '../../../../..');
27
-
28
- /**
29
- * Where source files live.
30
- * Prefer `<project>/src`; fall back to `<project>/components` (legacy layout).
31
- * @type {string}
32
- */
33
- const srcPath = resolve(projectDir, 'src');
34
- const isSrcExists = fs.pathExistsSync(srcPath);
35
- const srcDir = isSrcExists ? srcPath : resolve(projectDir, 'components');
36
-
37
- /**
38
- * Where built assets live.
39
- * If `src/` exists, use `<project>/dist`; else write into `<project>/components`.
40
- * @type {string}
41
- */
42
- const distPath = isSrcExists
43
- ? resolve(projectDir, 'dist')
44
- : resolve(projectDir, 'components');
45
-
46
- /**
47
- * Platform switch (affects component output roots).
48
- * @type {boolean}
49
- */
50
- const isDrupal = emulsifyConfig?.project?.platform === 'drupal';
51
-
52
- /**
53
- * Component source root:
54
- * - with src/: `<project>/src/components`
55
- * - without src/: `<project>/components`
56
- * @type {string}
57
- */
58
- const componentsSrcRoot = isSrcExists ? resolve(srcDir, 'components') : srcDir;
59
-
60
- /**
61
- * Component output root (where compiled component assets go):
62
- * - Drupal + src/: `components/…`
63
- * - Otherwise: `dist/components/…`
64
- * (Relative to `projectDir`; used by CopyPlugins `to:` path.)
65
- * @type {string}
66
- */
67
- const componentsOutRoot =
68
- isDrupal && isSrcExists ? 'components' : 'dist/components';
69
-
70
- /**
71
- * Glob pattern for Twig & component meta files. These are copied as-is so
72
- * Drupal/WordPress themes can consume them alongside compiled assets.
73
- * @type {string}
74
- */
75
- const componentFilesPattern = resolve(
76
- srcDir,
77
- '**/*.{twig,component.yml,component.json}',
78
- );
79
-
80
- /**
81
- * Build CopyPlugin patterns from a glob matcher, preserving source structure.
82
- *
83
- * @param {string} filesMatcher - Glob for files to mirror.
84
- * @returns {Array<{from:string,to:string}>} Copy patterns for CopyPlugin.
85
- */
86
- function getPatterns(filesMatcher) {
87
- return globSync(filesMatcher).map((file) => {
88
- const projectPath = file.split('/src/')[0]; // base path before /src/
89
- const srcStructure = file.split(`${srcDir}/`)[1];
90
- const parentDir = srcStructure.split('/')[0];
91
-
92
- // Consolidate foundation/layout under "components" for Drupal.
93
- const consolidateDirs =
94
- parentDir === 'layout' || parentDir === 'foundation'
95
- ? '/components/'
96
- : '/';
97
-
98
- const filePath = file.split(/(foundation\/|components\/|layout\/)/)[2];
99
-
100
- const to = isDrupal
101
- ? `${projectPath}${consolidateDirs}${parentDir}/${filePath}`
102
- : `${projectPath}/dist/${parentDir}/${filePath}`;
103
-
104
- return { from: file, to };
105
- });
106
- }
107
-
108
- /**
109
- * CopyPlugin instance (only when `src/` exists):
110
- * copies Twig and component meta files 1:1 into their expected destinations.
111
- * @type {CopyPlugin|false}
112
- */
113
- const CopyTwigPlugin = isSrcExists
114
- ? new CopyPlugin({ patterns: getPatterns(componentFilesPattern) })
115
- : false;
116
-
117
- /* -------------------------------------------------------------------------- */
118
- /* COMPONENT & GLOBAL ASSETS */
119
- /* -------------------------------------------------------------------------- */
120
-
121
- /**
122
- * Asset allow-list (extensions we consider "static assets" to mirror).
123
- * Extend to suit your project (e.g., add `pdf`, `txt`, `xml`, etc.).
124
- * NOTE: We purposefully exclude code-like files via the filter below.
125
- * @type {RegExp}
126
- */
127
- const ASSET_EXT_RE =
128
- /\.(?:png|jpe?g|gif|svg|webp|avif|ico|bmp|heic|heif|mp4|webm|mp3|ogg|wav|aac|woff2?|ttf|otf|eot|json|webmanifest|manifest|pdf)$/i;
129
-
130
- /**
131
- * Exclude code & tooling files (don’t mirror these).
132
- * @type {RegExp}
133
- */
134
- const EXCLUDE_CODE_RE =
135
- /\.(?:jsx?|tsx?|mjs|cjs|vue|svelte|scss|sass|less|styl|css|map|twig|php|yml|yaml|md|markdown|story(?:book)?\.[jt]sx?|stories\.[jt]sx?|test\.[jt]sx?)$/i;
136
-
137
- /**
138
- * Shared filter for CopyPlugin patterns.
139
- * Decides whether a file should be copied as a "static asset".
140
- *
141
- * @param {string} resourcePath - Absolute file path on disk.
142
- * @param {string} base - The context directory for the pattern.
143
- * @returns {boolean} True if we should copy the file.
144
- */
145
- const assetFilter = (resourcePath, base) => {
146
- const rel = relative(base, resourcePath);
147
- // Guard: stay inside context
148
- if (rel.startsWith('..')) return false;
149
- // Exclude typical code/tooling files
150
- if (EXCLUDE_CODE_RE.test(rel)) return false;
151
- // Include known asset extensions
152
- return ASSET_EXT_RE.test(rel);
153
- };
154
-
155
- /**
156
- * Copy **all static assets inside components**, regardless of folder labels.
157
- *
158
- * Examples (all preserved under the component’s output root):
159
- * src/components/accordion/assets/dropdown-icon.svg
160
- * src/components/accordion/images/icons/chevron.svg
161
- * src/components/accordion/icon.svg (root-level asset)
162
- *
163
- * @type {CopyPlugin}
164
- */
165
- const CopyComponentAssetsPlugin = new CopyPlugin({
166
- patterns: [
167
- {
168
- // Start at the components root and evaluate every file
169
- from: '**/*',
170
- context: componentsSrcRoot,
171
- to: resolve(projectDir, componentsOutRoot, '[path][name][ext]'),
172
- noErrorOnMissing: true,
173
- globOptions: {
174
- dot: false,
175
- ignore: [
176
- '**/.DS_Store',
177
- '**/Thumbs.db',
178
- '**/node_modules/**',
179
- '**/dist/**',
180
- ],
181
- },
182
- // Only copy files that match our asset allow-list and are not code
183
- filter: (resourcePath) => assetFilter(resourcePath, componentsSrcRoot),
184
- },
185
- ],
186
- });
187
-
188
- /**
189
- * OPTIONAL: Copy **global (non-component) assets** that live under `src/`
190
- * but outside `src/components/` (e.g. layout/site assets).
191
- *
192
- * Mirrors them under `dist/global/…`.
193
- * Disabled when there is no `src/` directory.
194
- *
195
- * @type {CopyPlugin|false}
196
- */
197
- const CopyGlobalAssetsPlugin = isSrcExists
198
- ? new CopyPlugin({
199
- patterns: [
200
- {
201
- from: '!(components|util)/**/*',
202
- context: srcDir,
203
- to: resolve(projectDir, 'dist', 'global', '[path][name][ext]'),
204
- noErrorOnMissing: true,
205
- globOptions: {
206
- dot: false,
207
- ignore: [
208
- '**/.DS_Store',
209
- '**/Thumbs.db',
210
- '**/node_modules/**',
211
- '**/dist/**',
212
- ],
213
- },
214
- filter: (resourcePath) => assetFilter(resourcePath, srcDir),
215
- },
216
- ],
217
- })
218
- : false;
219
-
220
- /* -------------------------------------------------------------------------- */
221
- /* OTHER PLUGINS */
222
- /* -------------------------------------------------------------------------- */
223
-
224
- /**
225
- * CleanWebpackPlugin configuration.
226
- * Wipes out compiled CSS/JS in `distPath` before a build; keeps images.
227
- */
228
- const CleanPlugin = new CleanWebpackPlugin({
229
- protectWebpackAssets: false,
230
- cleanOnceBeforeBuildPatterns: [
231
- `${distPath}/**/*.css`,
232
- `${distPath}/**/*.js`,
233
- `!${distPath}/**/*.png`,
234
- `!${distPath}/**/*.jpg`,
235
- `!${distPath}/**/*.gif`,
236
- `!${distPath}/**/*.svg`,
237
- ],
238
- });
239
-
240
- /** Removes empty JS files generated for style-only entries. */
241
- const RemoveEmptyJS = new RemoveEmptyScriptsPlugin();
242
-
243
- /**
244
- * MiniCssExtractPlugin: emit CSS next to the entry key path (no hard-coded dist/).
245
- */
246
- const CssExtractPlugin = new MiniCssExtractPlugin({
247
- filename: ({ chunk }) => `${chunk.name}.css`,
248
- chunkFilename: ({ chunk }) => `${chunk.name}.css`,
249
- });
250
-
251
- /**
252
- * Generate a single SVG spritemap at `dist/icons.svg`.
253
- */
254
- const SpritePlugin = new SVGSpritemapPlugin(
255
- resolve(projectDir, 'assets/icons/**/*.svg'),
256
- {
257
- output: {
258
- filename: 'dist/icons.svg',
259
- chunk: { keep: true },
260
- },
261
- sprite: {
262
- prefix: '',
263
- generate: { title: false },
264
- },
265
- },
266
- );
267
-
268
- /** Build progress output. */
269
- const ProgressPlugin = new webpack.ProgressPlugin();
270
-
271
- /**
272
- * Export plugin instances keyed for easy inclusion in your Webpack config.
273
- */
274
- export default {
275
- ProgressPlugin,
276
- CleanWebpackPlugin: CleanPlugin,
277
- RemoveEmptyJS,
278
- MiniCssExtractPlugin: CssExtractPlugin,
279
- SpritePlugin,
280
- CopyTwigPlugin,
281
- CopyComponentAssetsPlugin,
282
- CopyGlobalAssetsPlugin,
283
- };
@@ -1,157 +0,0 @@
1
- /**
2
- * @fileoverview Configures Twig alias resolution for the project.
3
- * - Builds Twig alias map from files under the source directory
4
- * - Exposes a Webpack-style `resolve.alias` object for `.twig` files
5
- */
6
-
7
- import {
8
- basename,
9
- resolve,
10
- relative,
11
- isAbsolute,
12
- join,
13
- posix as path,
14
- } from 'node:path';
15
- import { sync as globSync } from 'glob';
16
- import fs from 'fs-extra';
17
- import emulsifyConfig from '../../../../../project.emulsify.json' with { type: 'json' };
18
-
19
- /**
20
- * Resolve the directory of this file (without fileURLToPath).
21
- * @type {string}
22
- */
23
- let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
24
- if (process.platform === 'win32' && _filename.startsWith('/')) {
25
- _filename = _filename.slice(1);
26
- }
27
- const _dirname = path.dirname(_filename);
28
-
29
- /** @type {string} Absolute project root (five levels up). */
30
- const projectDir = resolve(_dirname, '../../../../..');
31
-
32
- /** @type {string} Project machine name used to prefix Drupal aliases. */
33
- const projectName = String(emulsifyConfig?.project?.name || '').trim();
34
-
35
- /**
36
- * Determine the source directory: prefer `<project>/src` if it exists,
37
- * otherwise use `<project>/components`. If we choose `components` and it
38
- * does not exist, create it safely inside the project.
39
- *
40
- * @returns {string} Absolute path to the source directory.
41
- */
42
- function resolveOrCreateSrcDir() {
43
- const srcPreferred = resolve(projectDir, 'src');
44
- if (fs.pathExistsSync(srcPreferred)) return srcPreferred;
45
-
46
- const componentsFallback = resolve(projectDir, 'components');
47
- if (!fs.pathExistsSync(componentsFallback)) {
48
- ensureDirSafe(componentsFallback, {
49
- base: projectDir,
50
- allowedBasenames: new Set(['components']),
51
- });
52
- }
53
- return componentsFallback;
54
- }
55
-
56
- /**
57
- * Safely create a directory after validating it is a subpath of `base`
58
- * and its basename is explicitly allowed. This addresses
59
- * `security/detect-non-literal-fs-filename`.
60
- *
61
- * @param {string} dir - Absolute path to create.
62
- * @param {{ base: string, allowedBasenames: Set<string> }} opts - Safety options.
63
- * @returns {void}
64
- * @throws {Error} If the path is outside `base` or not allowed.
65
- */
66
- function ensureDirSafe(dir, { base, allowedBasenames }) {
67
- const rel = relative(base, dir);
68
- const name = basename(dir);
69
-
70
- // Block absolute or escaping paths (outside of base)
71
- if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
72
- throw new Error(`Refusing to create directory outside project: "${dir}"`);
73
- }
74
-
75
- // Only allow known, expected directory names
76
- if (!allowedBasenames.has(name)) {
77
- throw new Error(`Refusing to create unexpected directory: "${name}"`);
78
- }
79
-
80
- // The argument is validated; create the directory.
81
- // eslint-disable-next-line security/detect-non-literal-fs-filename
82
- fs.mkdirSync(dir, { recursive: true });
83
- }
84
-
85
- /** @type {string} Absolute source directory. */
86
- const srcDir = resolveOrCreateSrcDir();
87
-
88
- /** @type {string} Glob pattern for all non-partial Twig files (skip leading underscores). */
89
- const aliasPattern = resolve(srcDir, '**/!(_*).twig');
90
-
91
- /**
92
- * Read immediate subdirectories from a source directory.
93
- *
94
- * @param {string} source - Absolute directory to scan.
95
- * @returns {string[]} Array of directory names (basenames only).
96
- */
97
- function getDirectories(source) {
98
- /* eslint-disable security/detect-non-literal-fs-filename */
99
- const entries = fs.readdirSync(source, { withFileTypes: true });
100
- /* eslint-enable security/detect-non-literal-fs-filename */
101
- return entries.filter((d) => d.isDirectory()).map((d) => d.name);
102
- }
103
-
104
- /**
105
- * Strip a leading two-digit ordering prefix from a directory name
106
- * (e.g., "01-components" -> "components").
107
- *
108
- * @param {string} dir - Original directory name.
109
- * @returns {string} Cleaned directory name.
110
- */
111
- function cleanDirectoryName(dir) {
112
- return /^\d{2}-/.test(dir) ? dir.slice(3) : dir;
113
- }
114
-
115
- /**
116
- * Build a Twig alias object by:
117
- * - Adding per-file aliases for Drupal (e.g., "mytheme/button")
118
- * - Adding top-level section aliases (e.g., "@components", "@layout")
119
- *
120
- * @param {string} twigGlob - Glob pattern to locate Twig files.
121
- * @returns {Record<string, string>} Alias map ({ alias: absolutePath }).
122
- */
123
- function getAliases(twigGlob) {
124
- /** @type {Record<string, string>} */
125
- const aliases = {};
126
-
127
- // Per-file aliases for Drupal only: "<projectName>/<filename>"
128
- if (emulsifyConfig?.project?.platform === 'drupal' && projectName) {
129
- for (const file of globSync(twigGlob)) {
130
- const relToSrc = relative(srcDir, file);
131
- const fileName = basename(relToSrc).replace(/\.twig$/, '');
132
- aliases[`${projectName}/${fileName}`] = file;
133
- }
134
- }
135
-
136
- // Top-level "@section" aliases for easier imports
137
- const topDirs = getDirectories(srcDir);
138
- for (const dir of topDirs) {
139
- const name = cleanDirectoryName(dir);
140
- aliases[`@${name}`] = join(projectDir, basename(srcDir), dir);
141
- }
142
-
143
- return aliases;
144
- }
145
-
146
- /**
147
- * Webpack-style `resolve` config for Twig files.
148
- * @typedef {{ extensions: string[], alias: Record<string, string> }} TwigResolveConfig
149
- */
150
-
151
- /** @type {TwigResolveConfig} */
152
- const TwigResolve = {
153
- extensions: ['.twig'],
154
- alias: getAliases(aliasPattern),
155
- };
156
-
157
- export default { TwigResolve };
@@ -1,16 +0,0 @@
1
- /**
2
- * A loader function that replaces occurrences of "projectName:" with "projectName/".
3
- *
4
- * @param {string} source - The source string to process.
5
- * @returns {string} The transformed source.
6
- */
7
- export default function (source) {
8
- const projectName = this.getOptions().projectName || '';
9
- /* eslint-disable security/detect-non-literal-regexp */
10
- const result = source.replace(
11
- new RegExp(`${projectName}:`, 'g'),
12
- `${projectName}/`,
13
- );
14
- /* eslint-enable security/detect-non-literal-regexp */
15
- return result;
16
- }