@emulsify/core 3.5.0 → 4.0.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/.cli/init.js +40 -31
- package/.storybook/_drupal.js +129 -8
- package/.storybook/css-components.js +13 -0
- package/.storybook/css-dist.js +5 -0
- package/.storybook/emulsifyTheme.js +9 -6
- package/.storybook/main.js +397 -106
- package/.storybook/manager.js +9 -16
- package/.storybook/preview.js +88 -110
- package/.storybook/utils.js +69 -74
- package/README.md +110 -59
- package/config/.stylelintrc.json +2 -6
- package/config/a11y.config.js +9 -5
- package/config/babel.config.js +6 -11
- package/config/eslint.config.js +31 -3
- package/config/postcss.config.js +5 -0
- package/config/vite/entries.js +227 -0
- package/config/vite/environment.js +39 -0
- package/config/vite/platforms.js +70 -0
- package/config/vite/plugins/copy-src-assets.js +76 -0
- package/config/vite/plugins/copy-twig-files.js +84 -0
- package/config/vite/plugins/css-asset-relativizer.js +40 -0
- package/config/vite/plugins/index.js +105 -0
- package/config/vite/plugins/mirror-components.js +358 -0
- package/config/vite/plugins/require-context.js +311 -0
- package/config/vite/plugins/source-file-index.js +184 -0
- package/config/vite/plugins/svg-sprite.js +117 -0
- package/config/vite/plugins/twig-extension-installers.js +36 -0
- package/config/vite/plugins/twig-module.js +1251 -0
- package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
- package/config/vite/plugins/virtual-twig-globs.js +136 -0
- package/config/vite/plugins/vituum-patch.js +167 -0
- package/config/vite/plugins/yaml-module.js +133 -0
- package/config/vite/plugins.js +12 -0
- package/config/vite/project-config.js +192 -0
- package/config/vite/project-extensions.js +177 -0
- package/config/vite/project-structure.js +447 -0
- package/config/vite/twig-extensions.js +109 -0
- package/config/vite/utils/fs-safe.js +66 -0
- package/config/vite/utils/paths.js +40 -0
- package/config/vite/utils/react-singleton.js +85 -0
- package/config/vite/utils/unique.js +36 -0
- package/config/vite/vite.config.js +161 -0
- package/package.json +164 -75
- package/scripts/a11y.js +70 -16
- package/scripts/audit-twig-stories.js +378 -0
- package/scripts/audit.js +1602 -0
- package/scripts/check-node-version.js +18 -0
- package/scripts/loadYaml.js +5 -1
- package/src/extensions/index.js +8 -0
- package/src/extensions/react/index.js +12 -0
- package/src/extensions/react/register.js +45 -0
- package/src/extensions/shared/attributes.js +308 -0
- package/src/extensions/shared/html.js +41 -0
- package/src/extensions/shared/lists.js +38 -0
- package/src/extensions/shared/object.js +22 -0
- package/src/extensions/twig/function-map.js +20 -0
- package/src/extensions/twig/functions/add-attributes.js +39 -0
- package/src/extensions/twig/functions/bem.js +166 -0
- package/src/extensions/twig/index.js +13 -0
- package/src/extensions/twig/register.js +95 -0
- package/src/extensions/twig/tag-map.js +16 -0
- package/src/extensions/twig/tags/switch.js +266 -0
- package/src/storybook/index.js +14 -0
- package/src/storybook/main-config.js +132 -0
- package/src/storybook/platform-behaviors.js +60 -0
- package/src/storybook/preview-parameters.js +81 -0
- package/src/storybook/render-twig.js +295 -0
- package/src/storybook/twig/drupal-filters.js +7 -0
- package/src/storybook/twig/include-function.js +109 -0
- package/src/storybook/twig/include.js +28 -0
- package/src/storybook/twig/reference-paths.js +294 -0
- package/src/storybook/twig/resolver.js +318 -0
- package/src/storybook/twig/setup.js +39 -0
- package/src/storybook/twig/source-events.js +5 -0
- package/src/storybook/twig/source-extensions.js +24 -0
- package/src/storybook/twig/source-function.js +239 -0
- package/src/storybook/twig/source.js +39 -0
- package/.all-contributorsrc +0 -45
- package/.editorconfig +0 -5
- package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
- package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
- package/.github/dependabot.yml +0 -6
- package/.github/workflows/addtoprojects.yml +0 -21
- package/.github/workflows/contributors.yml +0 -37
- package/.github/workflows/lint.yml +0 -22
- package/.github/workflows/semantic-release.yml +0 -24
- package/.husky/commit-msg +0 -2
- package/.husky/pre-commit +0 -2
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.storybook/polyfills/twig-include.js +0 -40
- package/.storybook/polyfills/twig-resolver.js +0 -70
- package/.storybook/polyfills/twig-source.js +0 -65
- package/.storybook/webpack.config.js +0 -269
- package/CODE_OF_CONDUCT.md +0 -56
- package/commitlint.config.js +0 -5
- package/config/jest.config.js +0 -19
- package/config/webpack/app.js +0 -1
- package/config/webpack/loaders.js +0 -167
- package/config/webpack/optimizers.js +0 -26
- package/config/webpack/plugins.js +0 -283
- package/config/webpack/resolves.js +0 -157
- package/config/webpack/sdc-loader.js +0 -16
- package/config/webpack/webpack.common.js +0 -272
- package/config/webpack/webpack.dev.js +0 -41
- package/config/webpack/webpack.prod.js +0 -6
- package/release.config.cjs +0 -30
- package/scripts/a11y.test.js +0 -172
- package/scripts/loadYaml.test.js +0 -30
package/config/a11y.config.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared accessibility linting configuration.
|
|
3
|
+
*
|
|
4
|
+
* These defaults are consumed by the pa11y script and can be extended by
|
|
5
|
+
* consuming projects through their local Emulsify config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default {
|
|
2
9
|
storybookBuildDir: '../../../../.out',
|
|
3
10
|
pa11y: {
|
|
4
11
|
includeNotices: false,
|
|
5
12
|
includeWarnings: false,
|
|
6
13
|
runners: ['axe'],
|
|
7
14
|
},
|
|
8
|
-
//
|
|
9
|
-
// basis, which results in the linter reporting some errors that
|
|
10
|
-
// should be ignored. These codes and descriptions allow for those
|
|
11
|
-
// errors to be targeted specifically.
|
|
15
|
+
// Ignore rules that are noisy for isolated component pages.
|
|
12
16
|
ignore: {
|
|
13
17
|
codes: ['landmark-one-main', 'page-has-heading-one'],
|
|
14
18
|
descriptions: ['Ensures all page content is contained by landmarks'],
|
package/config/babel.config.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Babel configuration for test and legacy transpilation paths.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
export default (api) => {
|
|
2
6
|
api.cache(true);
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'minify',
|
|
7
|
-
{
|
|
8
|
-
builtIns: false,
|
|
9
|
-
mangle: {
|
|
10
|
-
reserved: ['Drupal', 'drupalSettings', 'once'],
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
],
|
|
14
|
-
];
|
|
8
|
+
// Disable Babel's generated comments so minified output stays compact.
|
|
9
|
+
const presets = [['minify', { builtIns: false }]];
|
|
15
10
|
const comments = false;
|
|
16
11
|
|
|
17
12
|
return { presets, comments };
|
package/config/eslint.config.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file ESLint flat configuration for Emulsify Core.
|
|
3
|
+
*/
|
|
4
|
+
|
|
2
5
|
import js from '@eslint/js';
|
|
6
|
+
import babelParser from '@babel/eslint-parser';
|
|
7
|
+
import importPlugin from 'eslint-plugin-import';
|
|
3
8
|
import pluginSecurity from 'eslint-plugin-security';
|
|
4
9
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
5
10
|
|
|
6
11
|
export default [
|
|
7
|
-
//
|
|
12
|
+
// Start with core and plugin recommendations before project overrides.
|
|
8
13
|
js.configs.recommended,
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
importPlugin.flatConfigs.recommended,
|
|
11
16
|
pluginSecurity.configs.recommended,
|
|
12
17
|
eslintPluginPrettierRecommended,
|
|
13
18
|
|
|
14
19
|
{
|
|
15
20
|
name: 'emulsify-core-config',
|
|
16
21
|
languageOptions: {
|
|
22
|
+
parser: babelParser,
|
|
23
|
+
parserOptions: {
|
|
24
|
+
requireConfigFile: false,
|
|
25
|
+
babelOptions: {
|
|
26
|
+
babelrc: false,
|
|
27
|
+
configFile: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
17
30
|
sourceType: 'module',
|
|
18
31
|
ecmaVersion: 'latest',
|
|
19
32
|
globals: {
|
|
@@ -28,10 +41,15 @@ export default [
|
|
|
28
41
|
ignores: ['**/*.min.js', '**/node_modules/**/*'],
|
|
29
42
|
|
|
30
43
|
rules: {
|
|
44
|
+
// Keep historical project conventions while warning on risky patterns.
|
|
31
45
|
strict: 0,
|
|
32
46
|
'consistent-return': 'off',
|
|
33
47
|
'no-underscore-dangle': 'off',
|
|
34
48
|
'max-nested-callbacks': ['warn', 3],
|
|
49
|
+
'import/extensions': 'off',
|
|
50
|
+
'import/no-unresolved': 'off',
|
|
51
|
+
'import/no-extraneous-dependencies': 'warn',
|
|
52
|
+
'import/no-mutable-exports': 'warn',
|
|
35
53
|
'no-plusplus': ['warn', { allowForLoopAfterthoughts: true }],
|
|
36
54
|
'no-param-reassign': 'off',
|
|
37
55
|
'no-prototype-builtins': 'off',
|
|
@@ -45,5 +63,15 @@ export default [
|
|
|
45
63
|
],
|
|
46
64
|
quotes: ['error', 'single'],
|
|
47
65
|
},
|
|
66
|
+
|
|
67
|
+
settings: {
|
|
68
|
+
'import/ignore': ['\\.(scss|less|css)$'],
|
|
69
|
+
'import/resolver': {
|
|
70
|
+
node: {
|
|
71
|
+
extensions: ['.js', '.jsx'],
|
|
72
|
+
moduleDirectory: ['src', 'node_modules'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
48
76
|
},
|
|
49
77
|
];
|
package/config/postcss.config.js
CHANGED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Entries map builder for Vite/Rollup.
|
|
3
|
+
*
|
|
4
|
+
* Builds a keyed input map (for `build.rollupOptions.input`) where the map key
|
|
5
|
+
* encodes the final folder inside the Vite outDir (default `dist/`).
|
|
6
|
+
*
|
|
7
|
+
* Modern projects:
|
|
8
|
+
* - Global/base assets -> "global/..."
|
|
9
|
+
* - Component assets -> "components/..." (or mirrored to ./components when Drupal)
|
|
10
|
+
* - SDC=true removes the injected "/css" or "/js" bucket
|
|
11
|
+
*
|
|
12
|
+
* Component Structure Overrides projects (project.emulsify.json: variant.structureImplementations):
|
|
13
|
+
* - Only compile JS/SCSS.
|
|
14
|
+
* - JS -> "js/<relative-without-ext>"
|
|
15
|
+
* - CSS -> "css/<relative-without-ext>"
|
|
16
|
+
* - Twig/assets copying is handled by plugins using the same structure model.
|
|
17
|
+
* - cl-* / sb-* SCSS -> "storybook/<path-without-ext>"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import { basename, resolve } from 'path';
|
|
22
|
+
import {
|
|
23
|
+
compiledAssetOutputPath,
|
|
24
|
+
resolveProjectStructure,
|
|
25
|
+
storybookStyleOutputPath,
|
|
26
|
+
} from './project-structure.js';
|
|
27
|
+
import { createSourceFileIndex } from './plugins/source-file-index.js';
|
|
28
|
+
import { replaceLastSlash, toPosix } from './utils/paths.js';
|
|
29
|
+
|
|
30
|
+
export { replaceLastSlash, toPosix };
|
|
31
|
+
|
|
32
|
+
/** Remove characters that would confuse Rollup naming or file systems. */
|
|
33
|
+
export const sanitizePath = (s) => s.replace(/[^a-zA-Z0-9/_-]/g, '');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} BuildContext
|
|
37
|
+
* @property {string} projectDir
|
|
38
|
+
* @property {string} srcDir
|
|
39
|
+
* @property {boolean} srcExists
|
|
40
|
+
* @property {boolean} isDrupal - kept for downstream logic parity
|
|
41
|
+
* @property {boolean} SDC
|
|
42
|
+
* @property {boolean} structureOverrides
|
|
43
|
+
* @property {string[]} [structureRoots]
|
|
44
|
+
* @property {object} [sourceFileIndex]
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/* -------------------------------------------------------------------------- */
|
|
48
|
+
/* Utilities */
|
|
49
|
+
/* -------------------------------------------------------------------------- */
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Safe map setter that avoids prototype pollution keys.
|
|
53
|
+
* @param {Record<string,string>} map
|
|
54
|
+
* @param {string} key
|
|
55
|
+
* @param {string} value
|
|
56
|
+
*/
|
|
57
|
+
function safeSetKey(map, key, value) {
|
|
58
|
+
const forbidden = ['__proto__', 'prototype', 'constructor'];
|
|
59
|
+
if (!key || forbidden.some((bad) => key.includes(bad))) return;
|
|
60
|
+
map[key] = value; // eslint-disable-line security/detect-object-injection
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return an absolute path from a source index entry or string. */
|
|
64
|
+
const entryPath = (entry) =>
|
|
65
|
+
typeof entry === 'string' ? entry : entry.absPath;
|
|
66
|
+
|
|
67
|
+
/** Determine whether a file should be compiled as a JS entry. */
|
|
68
|
+
const isJavaScriptEntry = (entry) => {
|
|
69
|
+
const filePath = entryPath(entry);
|
|
70
|
+
return (
|
|
71
|
+
/\.jsx?$/.test(filePath) &&
|
|
72
|
+
!/\.(stories|component|min|test)\.jsx?$/.test(filePath)
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Determine whether a file should be compiled as a regular SCSS entry. */
|
|
77
|
+
const isScssEntry = (entry) => {
|
|
78
|
+
const filePath = entryPath(entry);
|
|
79
|
+
const name = basename(filePath);
|
|
80
|
+
return (
|
|
81
|
+
/\.scss$/.test(name) &&
|
|
82
|
+
!name.startsWith('_') &&
|
|
83
|
+
!name.startsWith('cl-') &&
|
|
84
|
+
!name.startsWith('sb-')
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Determine whether a file should be emitted under the Storybook style path. */
|
|
89
|
+
const isStorybookScssEntry = (entry) => {
|
|
90
|
+
const filePath = entryPath(entry);
|
|
91
|
+
return /\.scss$/.test(filePath) && /(?:cl-|sb-)/.test(basename(filePath));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/* -------------------------------------------------------------------------- */
|
|
95
|
+
/* Inputs builder */
|
|
96
|
+
/* -------------------------------------------------------------------------- */
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the Rollup/Vite input map.
|
|
100
|
+
*
|
|
101
|
+
* Keys are paths **relative to outDir**, without extensions. Examples:
|
|
102
|
+
* - "global/layout/css/layout"
|
|
103
|
+
* - "components/accordion/js/accordion" (or without "/js" when SDC=true)
|
|
104
|
+
*
|
|
105
|
+
* For Component Structure Overrides (variant.structureImplementations present),
|
|
106
|
+
* only JS/CSS keys are produced under "js/**" and "css/**".
|
|
107
|
+
*
|
|
108
|
+
* @param {BuildContext} ctx
|
|
109
|
+
* @returns {Record<string, string>}
|
|
110
|
+
*/
|
|
111
|
+
export function buildInputs(ctx) {
|
|
112
|
+
const structure = ctx.projectStructure || resolveProjectStructure(ctx);
|
|
113
|
+
const sourceFileIndex =
|
|
114
|
+
ctx.sourceFileIndex || createSourceFileIndex(structure);
|
|
115
|
+
|
|
116
|
+
/** @type {Record<string, string>} */
|
|
117
|
+
const inputs = {};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add a key/file pair into the inputs map safely (sanitized + POSIX).
|
|
121
|
+
* @param {string|null} key
|
|
122
|
+
* @param {string} abs
|
|
123
|
+
*/
|
|
124
|
+
const add = (key, abs) => {
|
|
125
|
+
if (!key) return;
|
|
126
|
+
const clean = sanitizePath(toPosix(key)).replace(/^\/+/, '');
|
|
127
|
+
if (!clean) return;
|
|
128
|
+
safeSetKey(inputs, clean, abs);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/* ------------------------------------------------------------------------ */
|
|
132
|
+
/* STRUCTURE OVERRIDES BRANCH */
|
|
133
|
+
/* ------------------------------------------------------------------------ */
|
|
134
|
+
if (structure.structureOverrides) {
|
|
135
|
+
// Gather JS and SCSS from each declared variant root directory.
|
|
136
|
+
const componentFiles = sourceFileIndex.componentFiles();
|
|
137
|
+
const jsFiles = componentFiles.filter(isJavaScriptEntry);
|
|
138
|
+
const scssFiles = componentFiles.filter(isScssEntry);
|
|
139
|
+
const storybookScss = componentFiles.filter(isStorybookScssEntry);
|
|
140
|
+
|
|
141
|
+
// JS files emit under dist/js using the path below components when possible.
|
|
142
|
+
for (const entry of jsFiles) {
|
|
143
|
+
const file = entryPath(entry);
|
|
144
|
+
add(compiledAssetOutputPath(file, 'js', structure, ctx), file);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// SCSS files emit under dist/css using the same relative path rules.
|
|
148
|
+
for (const entry of scssFiles) {
|
|
149
|
+
const file = entryPath(entry);
|
|
150
|
+
add(compiledAssetOutputPath(file, 'css', structure, ctx), file);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Storybook and component-library styles stay under dist/storybook.
|
|
154
|
+
for (const entry of storybookScss) {
|
|
155
|
+
const file = entryPath(entry);
|
|
156
|
+
add(storybookStyleOutputPath(file, structure, ctx), file);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return inputs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ------------------------------------------------------------------------ */
|
|
163
|
+
/* MODERN BRANCH (existing behavior preserved) */
|
|
164
|
+
/* ------------------------------------------------------------------------ */
|
|
165
|
+
const globalFiles = sourceFileIndex.globalFiles();
|
|
166
|
+
const componentFiles = sourceFileIndex.componentFiles();
|
|
167
|
+
|
|
168
|
+
// Global JS
|
|
169
|
+
for (const entry of globalFiles.filter(isJavaScriptEntry)) {
|
|
170
|
+
const file = entryPath(entry);
|
|
171
|
+
add(compiledAssetOutputPath(file, 'js', structure, ctx), file);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Component JS
|
|
175
|
+
for (const entry of componentFiles.filter(isJavaScriptEntry)) {
|
|
176
|
+
const file = entryPath(entry);
|
|
177
|
+
add(compiledAssetOutputPath(file, 'js', structure, ctx), file);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Global SCSS
|
|
181
|
+
for (const entry of globalFiles.filter(isScssEntry)) {
|
|
182
|
+
const file = entryPath(entry);
|
|
183
|
+
add(compiledAssetOutputPath(file, 'css', structure, ctx), file);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Component SCSS
|
|
187
|
+
for (const entry of componentFiles.filter(isScssEntry)) {
|
|
188
|
+
const file = entryPath(entry);
|
|
189
|
+
add(compiledAssetOutputPath(file, 'css', structure, ctx), file);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Storybook/CL SCSS
|
|
193
|
+
for (const entry of sourceFileIndex.all().filter(isStorybookScssEntry)) {
|
|
194
|
+
const file = entryPath(entry);
|
|
195
|
+
add(storybookStyleOutputPath(file, structure, ctx), file);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return inputs;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convenience wrapper that infers `srcDir` and returns an inputs map.
|
|
203
|
+
* @param {string} projectDir
|
|
204
|
+
* @param {boolean} [isDrupal=false]
|
|
205
|
+
* @param {boolean} [SDC=false]
|
|
206
|
+
* @returns {Record<string,string>}
|
|
207
|
+
*/
|
|
208
|
+
export function buildInputsFromProject(
|
|
209
|
+
projectDir,
|
|
210
|
+
isDrupal = false,
|
|
211
|
+
SDC = false,
|
|
212
|
+
) {
|
|
213
|
+
const srcPath = resolve(projectDir, 'src');
|
|
214
|
+
const srcExists = fs.existsSync(srcPath);
|
|
215
|
+
const srcDir = srcExists ? srcPath : resolve(projectDir, 'components');
|
|
216
|
+
|
|
217
|
+
const ctx = {
|
|
218
|
+
projectDir,
|
|
219
|
+
srcDir,
|
|
220
|
+
srcExists,
|
|
221
|
+
isDrupal,
|
|
222
|
+
SDC,
|
|
223
|
+
structureOverrides: false,
|
|
224
|
+
structureRoots: [],
|
|
225
|
+
};
|
|
226
|
+
return buildInputs(ctx);
|
|
227
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Environment resolution for Emulsify + Vite.
|
|
3
|
+
*
|
|
4
|
+
* Reads project settings and exposes a normalized "env" object used by
|
|
5
|
+
* entries, plugins, and the Vite config.
|
|
6
|
+
*
|
|
7
|
+
* Highlights:
|
|
8
|
+
* - `platform`: from env var or project.emulsify.json (default "generic").
|
|
9
|
+
* - `SDC`: boolean from project.emulsify.json `project.singleDirectoryComponents`.
|
|
10
|
+
* - `structureOverrides`: true when safe `variant.structureImplementations` exist.
|
|
11
|
+
* - `structureRoots`: array of directories from `variant.structureImplementations`.
|
|
12
|
+
* - `platformAdapter`: active adapter for platform-specific behavior.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveProjectConfig } from './project-config.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve environment details for the current project.
|
|
19
|
+
*
|
|
20
|
+
* @returns {{
|
|
21
|
+
* projectDir: string,
|
|
22
|
+
* srcDir: string,
|
|
23
|
+
* srcExists: boolean,
|
|
24
|
+
* platform: 'drupal' | 'generic' | string,
|
|
25
|
+
* SDC: boolean,
|
|
26
|
+
* structureOverrides: boolean,
|
|
27
|
+
* structureRoots: string[],
|
|
28
|
+
* structureImplementations: Array<{name: string, directory: string}>,
|
|
29
|
+
* componentRoots: string[],
|
|
30
|
+
* globalRoots: string[],
|
|
31
|
+
* namespaceRoots: Record<string, string>,
|
|
32
|
+
* outputStrategy: string,
|
|
33
|
+
* projectStructure: object,
|
|
34
|
+
* platformAdapter: object
|
|
35
|
+
* }}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveEnvironment() {
|
|
38
|
+
return resolveProjectConfig(process.cwd(), process.env);
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Platform adapter definitions for Emulsify project behavior.
|
|
3
|
+
*
|
|
4
|
+
* Adapters expose platform-specific defaults as serializable data so the same
|
|
5
|
+
* decisions can be used by Node-side Vite config and Storybook browser code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const genericAdapter = {
|
|
9
|
+
name: 'generic',
|
|
10
|
+
outputStrategy: 'dist',
|
|
11
|
+
storybook: {
|
|
12
|
+
loadDrupalBehaviorShim: false,
|
|
13
|
+
attachDrupalBehaviors: false,
|
|
14
|
+
registerDrupalTwigFilters: false,
|
|
15
|
+
loadMirroredComponentCss: false,
|
|
16
|
+
allowSyncXhrSource: false,
|
|
17
|
+
},
|
|
18
|
+
build: {
|
|
19
|
+
mirrorDistComponentsToRoot: false,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const drupalAdapter = {
|
|
24
|
+
name: 'drupal',
|
|
25
|
+
outputStrategy: 'drupal-sdc',
|
|
26
|
+
storybook: {
|
|
27
|
+
loadDrupalBehaviorShim: true,
|
|
28
|
+
attachDrupalBehaviors: true,
|
|
29
|
+
registerDrupalTwigFilters: true,
|
|
30
|
+
loadMirroredComponentCss: true,
|
|
31
|
+
allowSyncXhrSource: false,
|
|
32
|
+
},
|
|
33
|
+
build: {
|
|
34
|
+
mirrorDistComponentsToRoot: true,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const adapters = {
|
|
39
|
+
generic: genericAdapter,
|
|
40
|
+
drupal: drupalAdapter,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Deep-clone an adapter so callers can safely serialize or extend it.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} adapter - Adapter definition.
|
|
47
|
+
* @returns {object} Adapter clone.
|
|
48
|
+
*/
|
|
49
|
+
function cloneAdapter(adapter) {
|
|
50
|
+
return JSON.parse(JSON.stringify(adapter));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the platform adapter for a normalized platform name.
|
|
55
|
+
*
|
|
56
|
+
* Unknown platforms intentionally use generic behavior while preserving the
|
|
57
|
+
* resolved `platform` string separately on the environment object.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} [platform='generic'] - Normalized platform name.
|
|
60
|
+
* @returns {object} Serializable platform adapter.
|
|
61
|
+
*/
|
|
62
|
+
export function getPlatformAdapter(platform = 'generic') {
|
|
63
|
+
const key = (platform || 'generic').toString().toLowerCase().trim();
|
|
64
|
+
if (key === 'drupal') {
|
|
65
|
+
return cloneAdapter(drupalAdapter);
|
|
66
|
+
}
|
|
67
|
+
return cloneAdapter(genericAdapter);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { adapters };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Static source asset copy plugin.
|
|
3
|
+
*
|
|
4
|
+
* Copies non-code source assets beside the JS/CSS/Twig output that references
|
|
5
|
+
* them, preserving component and global routing semantics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { copyFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
copiedComponentOutputPath,
|
|
13
|
+
copiedGlobalOutputPath,
|
|
14
|
+
findSourceRoot,
|
|
15
|
+
} from '../project-structure.js';
|
|
16
|
+
import {
|
|
17
|
+
createSourceFileIndex,
|
|
18
|
+
isStaticSourceAsset,
|
|
19
|
+
} from './source-file-index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Copy non-code assets from source roots to `dist/`.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ structure: object, sourceFileIndex?: object }} opts - Plugin options.
|
|
25
|
+
* @returns {import('vite').PluginOption} Copy plugin.
|
|
26
|
+
*/
|
|
27
|
+
export function copyAllSrcAssetsPlugin({
|
|
28
|
+
structure,
|
|
29
|
+
sourceFileIndex = createSourceFileIndex(structure),
|
|
30
|
+
}) {
|
|
31
|
+
let outDir = 'dist';
|
|
32
|
+
|
|
33
|
+
const copyToOutDir = (absPath, relDest) => {
|
|
34
|
+
if (!relDest) return;
|
|
35
|
+
const destPath = join(outDir, relDest);
|
|
36
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
37
|
+
try {
|
|
38
|
+
copyFileSync(absPath, destPath);
|
|
39
|
+
} catch {
|
|
40
|
+
/* noop */
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: 'emulsify-copy-all-src-assets',
|
|
46
|
+
apply: 'build',
|
|
47
|
+
enforce: 'post',
|
|
48
|
+
|
|
49
|
+
/** Capture outDir. */
|
|
50
|
+
configResolved(cfg) {
|
|
51
|
+
outDir = cfg.build?.outDir || 'dist';
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/** Copy before the mirror plugin moves dist/components to the project root. */
|
|
55
|
+
writeBundle() {
|
|
56
|
+
for (const file of sourceFileIndex.componentFiles()) {
|
|
57
|
+
if (!isStaticSourceAsset(file.absPath)) continue;
|
|
58
|
+
copyToOutDir(
|
|
59
|
+
file.absPath,
|
|
60
|
+
copiedComponentOutputPath(file.absPath, structure),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const file of sourceFileIndex.globalFiles()) {
|
|
65
|
+
if (!isStaticSourceAsset(file.absPath)) continue;
|
|
66
|
+
if (findSourceRoot(file.absPath, structure.componentRootRecords)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
copyToOutDir(
|
|
70
|
+
file.absPath,
|
|
71
|
+
copiedGlobalOutputPath(file.absPath, structure),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Twig template and component metadata copy plugin.
|
|
3
|
+
*
|
|
4
|
+
* Copies canonical source Twig files and component metadata to the emitted dist
|
|
5
|
+
* structure using the same routing rules as compiled JS and CSS entries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { copyFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
copiedComponentOutputPath,
|
|
13
|
+
copiedGlobalOutputPath,
|
|
14
|
+
} from '../project-structure.js';
|
|
15
|
+
import {
|
|
16
|
+
createSourceFileIndex,
|
|
17
|
+
isComponentMetadataFile,
|
|
18
|
+
} from './source-file-index.js';
|
|
19
|
+
|
|
20
|
+
/** Determine whether a Twig file is a partial (filename starts with `_`). */
|
|
21
|
+
const isPartial = (filePath) =>
|
|
22
|
+
(filePath.split('/')?.pop() || '').trim().startsWith('_');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Copy Twig templates and component metadata to `dist/`.
|
|
26
|
+
*
|
|
27
|
+
* @param {{ structure: object, sourceFileIndex?: object }} opts - Plugin options.
|
|
28
|
+
* @returns {import('vite').PluginOption} Copy plugin.
|
|
29
|
+
*/
|
|
30
|
+
export function copyTwigFilesPlugin({
|
|
31
|
+
structure,
|
|
32
|
+
sourceFileIndex = createSourceFileIndex(structure),
|
|
33
|
+
}) {
|
|
34
|
+
let outDir = 'dist';
|
|
35
|
+
|
|
36
|
+
const copyToOutDir = (absPath, relDest) => {
|
|
37
|
+
if (!relDest) return;
|
|
38
|
+
const destPath = join(outDir, relDest);
|
|
39
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
40
|
+
try {
|
|
41
|
+
copyFileSync(absPath, destPath);
|
|
42
|
+
} catch {
|
|
43
|
+
/* noop */
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name: 'emulsify-copy-twig-files',
|
|
49
|
+
apply: 'build',
|
|
50
|
+
enforce: 'post',
|
|
51
|
+
|
|
52
|
+
/** Capture the final outDir. */
|
|
53
|
+
configResolved(cfg) {
|
|
54
|
+
outDir = cfg.build?.outDir || 'dist';
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/** Copy before the mirror plugin moves dist/components to the project root. */
|
|
58
|
+
writeBundle() {
|
|
59
|
+
for (const file of sourceFileIndex.componentFiles()) {
|
|
60
|
+
if (file.absPath.endsWith('.twig')) {
|
|
61
|
+
if (isPartial(file.relPath)) continue;
|
|
62
|
+
copyToOutDir(
|
|
63
|
+
file.absPath,
|
|
64
|
+
copiedComponentOutputPath(file.absPath, structure),
|
|
65
|
+
);
|
|
66
|
+
} else if (isComponentMetadataFile(file.absPath)) {
|
|
67
|
+
copyToOutDir(
|
|
68
|
+
file.absPath,
|
|
69
|
+
copiedComponentOutputPath(file.absPath, structure),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const file of sourceFileIndex.globalFiles()) {
|
|
75
|
+
if (!file.absPath.endsWith('.twig')) continue;
|
|
76
|
+
if (isPartial(file.relPath)) continue;
|
|
77
|
+
copyToOutDir(
|
|
78
|
+
file.absPath,
|
|
79
|
+
copiedGlobalOutputPath(file.absPath, structure),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CSS asset URL relativizer plugin.
|
|
3
|
+
*
|
|
4
|
+
* Rewrites emitted CSS references to root assets so nested CSS files can keep
|
|
5
|
+
* resolving copied assets correctly from their final output directories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { posix as pathPosix } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rewrites any `url(assets/...)` found in emitted CSS to a path relative to the
|
|
12
|
+
* CSS file's directory.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ assetsRoot?: string }} [opts] - Plugin options.
|
|
15
|
+
* @returns {import('vite').PluginOption} CSS asset URL plugin.
|
|
16
|
+
*/
|
|
17
|
+
export function cssAssetUrlRelativizer({ assetsRoot = 'assets' } = {}) {
|
|
18
|
+
return {
|
|
19
|
+
name: 'emulsify-css-asset-url-relativizer',
|
|
20
|
+
apply: 'build',
|
|
21
|
+
generateBundle(_, bundle) {
|
|
22
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
23
|
+
if (chunk.type !== 'asset') continue;
|
|
24
|
+
if (!fileName.endsWith('.css')) continue;
|
|
25
|
+
if (typeof chunk.source !== 'string') continue;
|
|
26
|
+
|
|
27
|
+
const fromDir = pathPosix.dirname(fileName);
|
|
28
|
+
|
|
29
|
+
chunk.source = chunk.source.replace(
|
|
30
|
+
/url\((['"]?)\/?assets\/([^)'"]+)\1\)/g,
|
|
31
|
+
(match, quote = '', rest) => {
|
|
32
|
+
const target = pathPosix.join(assetsRoot, rest);
|
|
33
|
+
const rel = pathPosix.relative(fromDir, target);
|
|
34
|
+
return `url(${quote}${rel}${quote})`;
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|