@emulsify/core 4.1.1 → 4.2.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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ See [Version Evolution](docs/version-evolution.md) for more release history.
34
34
 
35
35
  Twig and React are equally valid ways to build component libraries with Emulsify Core. The right authoring model depends on the consuming project:
36
36
 
37
- - Use Twig for CMS themes and server-rendered template systems. Drupal has a dedicated adapter today. WordPress and Timber projects should currently use `platform: "none"` unless a project adds its own platform-specific behavior.
37
+ - Use Twig for CMS themes and server-rendered template systems. Drupal has a Drupal-specific adapter, and WordPress/Timber projects can use the intentionally neutral `wordpress` adapter. WordPress runtime integration belongs in `emulsify-wordpress-theme`.
38
38
  - Use React for standalone UI libraries, application components, or projects that already use React.
39
39
  - Use mixed Twig and React when a design system needs to document both CMS-rendered and JavaScript-rendered components in the same Storybook instance.
40
40
 
@@ -85,7 +85,7 @@ The documentation is split by task:
85
85
  | [Component Authoring](docs/component-authoring.md) | Choosing Twig, React, or mixed Storybook authoring and comparing component examples. |
86
86
  | [Storybook](docs/storybook.md) | Rendering Twig stories, using `renderTwig()`, understanding Twig runtime helpers, and mixing Twig with React stories. |
87
87
  | [Project Structure And Output](docs/project-structure.md) | Configuring `src/components`, root `./components`, `variant.structureImplementations`, and expected output paths. |
88
- | [Platform Adapters](docs/platform-adapters.md) | Understanding `none`, `drupal`, platform resolution order, and Drupal SDC behavior. |
88
+ | [Platform Adapters](docs/platform-adapters.md) | Understanding `none`, `wordpress`, `drupal`, platform resolution order, and Drupal SDC behavior. |
89
89
  | [Extension Points](docs/extension-points.md) | Adding Vite plugins, Tailwind CSS, Storybook preview overrides, and other framework tooling. |
90
90
  | [Performance](docs/performance.md) | Understanding sourcemaps, eager Twig imports, Tailwind scanning, copied files, and fixture validation. |
91
91
  | [Native Twig Extensions](docs/native-twig-extensions.md) | Using `bem()`, `add_attributes()`, and `switch/case/default/endswitch` in Twig.js. |
@@ -94,24 +94,25 @@ The documentation is split by task:
94
94
 
95
95
  ## Known Limitations
96
96
 
97
- - Implemented platform adapters are currently `none` and `drupal`. WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. See [Platform Adapters](docs/platform-adapters.md).
97
+ - Implemented platform adapters are `none`, `wordpress`, and `drupal`. The `wordpress` adapter is intentionally neutral: it supports Core Twig authoring, Storybook, Vite, `bem()`, `add_attributes()`, `include()`, and `source()`, but it does not emulate WordPress or Timber PHP runtime behavior. Runtime integration belongs in `emulsify-wordpress-theme`. See [Platform Adapters](docs/platform-adapters.md).
98
98
  - Storybook's Twig resolver eagerly imports Twig modules and raw Twig source. This is reliable for `include()` and `source()`, but large Twig libraries should keep Storybook source roots intentional. See [Performance](docs/performance.md).
99
99
  - Production sourcemaps are enabled by default unless a project overrides Vite config through `config/emulsify-core/vite/plugins.*`. See [Performance](docs/performance.md).
100
100
  - Project extensions use the public `config/emulsify-core` directory: `config/emulsify-core/vite/plugins.*` for Vite, `config/emulsify-core/storybook/...` for Storybook, and `config/emulsify-core/a11y.config.js` for a11y. See [Extension Points](docs/extension-points.md).
101
101
  - Webpack-specific customizations must be migrated manually to Vite plugins or `extendConfig()`. See [Migration](docs/migration-4x.md).
102
- - Drupal SDC mirroring only applies when the Drupal adapter and SDC settings are enabled. `none` projects should expect output to remain in `dist/`. See [Platform Adapters](docs/platform-adapters.md).
102
+ - Drupal SDC mirroring only applies when the Drupal adapter and SDC settings are enabled. `none` and `wordpress` projects should expect output to remain in `dist/`. See [Platform Adapters](docs/platform-adapters.md).
103
103
 
104
104
  ## Supported Project Shapes
105
105
 
106
- Release-readiness coverage validates:
106
+ Core supports these project shapes:
107
107
 
108
108
  - Drupal SDC projects using `src/components`.
109
109
  - `none` platform Twig projects using `src/components`.
110
+ - `wordpress` platform Twig projects using `src/components`.
110
111
  - Root `./components` projects.
111
112
  - Projects using multiple `variant.structureImplementations`.
112
113
  - Mixed Twig + React Storybook projects.
113
114
 
114
- WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. The implemented adapters in this package are currently `none` and `drupal`.
115
+ WordPress and Timber projects should use `platform: "wordpress"` when they want Core's neutral WordPress adapter. The adapter keeps output in `dist/`, loads Storybook CSS from `dist/**/*.css`, and leaves WordPress runtime behavior to `emulsify-wordpress-theme`.
115
116
 
116
117
  ## Public Imports
117
118
 
@@ -125,7 +126,7 @@ import { defineReactExtension } from '@emulsify/core/extensions/react';
125
126
 
126
127
  `defineReactExtension` is reserved for future React extension support. It currently returns the input unchanged. Adopting the import path is safe; the runtime is intentionally a no-op until the registry lands. See [Extension Points](docs/extension-points.md#public-imports).
127
128
 
128
- Vite consumers can import the shared config from `@emulsify/core/vite` and public Vite plugin helpers from `@emulsify/core/vite/plugins`.
129
+ Vite consumers can import the shared config from `@emulsify/core/vite`, public Vite plugin helpers from `@emulsify/core/vite/plugins`, and platform adapter helpers from `@emulsify/core/vite/platforms`.
129
130
 
130
131
  ## Contributing
131
132
 
@@ -5,7 +5,21 @@
5
5
  * decisions can be used by Node-side Vite config and Storybook browser code.
6
6
  */
7
7
 
8
- const noneAdapter = {
8
+ /**
9
+ * Freeze adapter definition data while preserving cloneable plain objects.
10
+ *
11
+ * @param {object} adapter - Adapter definition.
12
+ * @returns {object} Frozen adapter definition.
13
+ */
14
+ function freezeAdapter(adapter) {
15
+ return Object.freeze({
16
+ ...adapter,
17
+ storybook: Object.freeze({ ...adapter.storybook }),
18
+ build: Object.freeze({ ...adapter.build }),
19
+ });
20
+ }
21
+
22
+ const noneAdapter = freezeAdapter({
9
23
  name: 'none',
10
24
  outputStrategy: 'dist',
11
25
  storybook: {
@@ -18,9 +32,9 @@ const noneAdapter = {
18
32
  build: {
19
33
  mirrorDistComponentsToRoot: false,
20
34
  },
21
- };
35
+ });
22
36
 
23
- const drupalAdapter = {
37
+ const drupalAdapter = freezeAdapter({
24
38
  name: 'drupal',
25
39
  outputStrategy: 'drupal-sdc',
26
40
  storybook: {
@@ -33,13 +47,29 @@ const drupalAdapter = {
33
47
  build: {
34
48
  mirrorDistComponentsToRoot: true,
35
49
  },
36
- };
50
+ });
37
51
 
38
- const adapters = {
52
+ const wordpressAdapter = freezeAdapter({
53
+ name: 'wordpress',
54
+ outputStrategy: 'dist',
55
+ storybook: {
56
+ loadDrupalBehaviorShim: false,
57
+ attachDrupalBehaviors: false,
58
+ registerDrupalTwigFilters: false,
59
+ loadMirroredComponentCss: false,
60
+ allowSyncXhrSource: false,
61
+ },
62
+ build: {
63
+ mirrorDistComponentsToRoot: false,
64
+ },
65
+ });
66
+
67
+ const adapters = Object.freeze({
39
68
  none: noneAdapter,
40
69
  generic: noneAdapter,
41
70
  drupal: drupalAdapter,
42
- };
71
+ wordpress: wordpressAdapter,
72
+ });
43
73
 
44
74
  /**
45
75
  * Deep-clone an adapter so callers can safely serialize or extend it.
@@ -80,10 +110,7 @@ export function normalizePlatformName(platform = 'none') {
80
110
  */
81
111
  export function getPlatformAdapter(platform = 'none') {
82
112
  const key = normalizePlatformName(platform);
83
- if (key === 'drupal') {
84
- return cloneAdapter(drupalAdapter);
85
- }
86
- return cloneAdapter(noneAdapter);
113
+ return cloneAdapter(adapters[key] || noneAdapter);
87
114
  }
88
115
 
89
116
  export { adapters };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emulsify/core",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "Bundled tooling for Storybook development + Vite Build",
5
5
  "keywords": [
6
6
  "component library",
@@ -127,6 +127,7 @@
127
127
  "./storybook/twig/source": "./src/storybook/twig/source.js",
128
128
  "./vite": "./config/vite/vite.config.js",
129
129
  "./vite/plugins": "./config/vite/plugins.js",
130
+ "./vite/platforms": "./config/vite/platforms.js",
130
131
  "./package.json": "./package.json"
131
132
  },
132
133
  "publishConfig": {
@@ -153,6 +154,7 @@
153
154
  "prettier": "npm run check-node-version && prettier --check --config config/.prettierrc.json --ignore-unknown \"**/*.{js,mjs,cjs,jsx,json,yml,yaml,scss,md,twig}\"",
154
155
  "prettier-fix": "npm run check-node-version && prettier --config config/.prettierrc.json --write --ignore-unknown \"**/*.{js,mjs,cjs,jsx,json,yml,yaml,scss,md,twig}\"",
155
156
  "semantic-release": "npm run check-node-version && semantic-release --config ./release.config.cjs",
157
+ "smoke:pack": "npm run check-node-version && node scripts/smoke-pack.js",
156
158
  "version:develop": "npm run check-node-version && node scripts/bump-version-from-commits.js",
157
159
  "storybook": "npm run check-node-version && NODE_OPTIONS=--no-deprecation storybook dev -p 6006 --no-open --exact-port",
158
160
  "storybook-build": "npm run check-node-version && storybook build -o .out",
@@ -164,7 +166,7 @@
164
166
  "@babel/core": "^7.29.7",
165
167
  "@babel/eslint-parser": "^7.28.6",
166
168
  "@babel/preset-env": "^7.28.3",
167
- "@emulsify/cli": "^1.11.4",
169
+ "@emulsify/cli": "^2.2.0",
168
170
  "@eslint/js": "^9.39.4",
169
171
  "@storybook/addon-a11y": "^10.1.4",
170
172
  "@storybook/addon-links": "^10.1.4",
@@ -172,31 +174,27 @@
172
174
  "@storybook/react": "^10.1.4",
173
175
  "@storybook/react-vite": "^10.1.4",
174
176
  "@vituum/vite-plugin-twig": "^1.1.0",
175
- "autoprefixer": "^10.4.21",
177
+ "autoprefixer": "^10.5.2",
176
178
  "axe-core": "^4.11.4",
177
179
  "babel-preset-minify": "^0.5.2",
178
180
  "concurrently": "^9.2.3",
179
181
  "eslint": "^9.39.4",
180
182
  "eslint-config-prettier": "^10.1.8",
181
183
  "eslint-plugin-import": "^2.32.0",
182
- "eslint-plugin-jest": "^29.0.1",
184
+ "eslint-plugin-jest": "^29.15.4",
183
185
  "eslint-plugin-prettier": "^5.5.4",
184
- "eslint-plugin-security": "^4.0.0",
185
- "eslint-plugin-storybook": "^10.1.4",
186
- "fs-extra": "^11.3.1",
186
+ "eslint-plugin-security": "^4.0.1",
187
+ "eslint-plugin-storybook": "^10.4.6",
187
188
  "glob": "^13.0.6",
188
- "graceful-fs": "^4.2.11",
189
189
  "jest": "^30.2.0",
190
190
  "jest-environment-jsdom": "^30.2.0",
191
191
  "js-yaml": "^4.1.0",
192
192
  "normalize.css": "^8.0.1",
193
193
  "open-cli": "^9.0.0",
194
194
  "pa11y": "^9.0.1",
195
- "postcss": "^8.5.4",
195
+ "postcss": "^8.5.16",
196
196
  "postcss-scss": "^4.0.9",
197
- "ramda": "^0.32.0",
198
- "regenerator-runtime": "^0.14.1",
199
- "sass": "^1.93.2",
197
+ "sass": "^1.101.0",
200
198
  "storybook": "^10.1.4",
201
199
  "stylelint": "^17.12.0",
202
200
  "stylelint-config-standard-scss": "^17.0.0",
@@ -211,18 +209,18 @@
211
209
  "yaml": "^2.8.1"
212
210
  },
213
211
  "devDependencies": {
214
- "@commitlint/cli": "^21.0.1",
215
- "@commitlint/config-conventional": "^21.0.1",
212
+ "@commitlint/cli": "^21.2.0",
213
+ "@commitlint/config-conventional": "^21.2.0",
216
214
  "@semantic-release/changelog": "^6.0.2",
217
215
  "@semantic-release/commit-analyzer": "^13.0.1",
218
216
  "@semantic-release/git": "^10.0.1",
219
- "@semantic-release/github": "^12.0.8",
217
+ "@semantic-release/github": "^12.0.9",
220
218
  "@semantic-release/npm": "^13.1.5",
221
219
  "@semantic-release/release-notes-generator": "^14.1.0",
222
220
  "husky": "^9.1.7",
223
- "lint-staged": "^17.0.5",
224
- "react": "^19.2.0",
225
- "react-dom": "^19.2.0",
221
+ "lint-staged": "^17.0.8",
222
+ "react": "^19.2.7",
223
+ "react-dom": "^19.2.7",
226
224
  "semantic-release": "^25.0.3"
227
225
  },
228
226
  "peerDependencies": {
package/scripts/a11y.js CHANGED
@@ -8,7 +8,6 @@
8
8
  import { existsSync, readFileSync } from 'fs';
9
9
  import path from 'path';
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
- import * as R from 'ramda';
12
11
  import pa11y from 'pa11y';
13
12
 
14
13
  import a11yConfig from '../config/a11y.config.js';
@@ -210,11 +209,12 @@ const resolvePa11yStoryIds = ({
210
209
  * @param {'error'|'warning'|'notice'} severity
211
210
  * @returns {'red'|'yellow'|'blue'|undefined}
212
211
  */
213
- const severityToColor = R.cond([
214
- [R.equals('error'), R.always('red')],
215
- [R.equals('warning'), R.always('yellow')],
216
- [R.equals('notice'), R.always('blue')],
217
- ]);
212
+ const severityToColor = (severity) =>
213
+ ({
214
+ error: 'red',
215
+ warning: 'yellow',
216
+ notice: 'blue',
217
+ })[severity];
218
218
 
219
219
  /**
220
220
  * @typedef {Object} Pa11yIssue
@@ -297,25 +297,20 @@ const lintComponent = async (name) =>
297
297
  * @param {string[]} names - List of Storybook story IDs.
298
298
  * @returns {Promise<void>}
299
299
  */
300
- const lintReportAndExit = R.pipe(
301
- /** @param {string[]} list */
302
- (list) => list.map(lintComponent),
303
- (promises) => Promise.all(promises),
304
- R.andThen(
305
- R.pipe(
306
- /** @param {Array<{issues: Pa11yIssue[], pageUrl: string}>} results */
307
- (results) => results.map(logReport),
308
- R.reject(R.equals(false)),
309
- R.unless(R.isEmpty, () => process.exit(1)),
310
- ),
311
- ),
312
- );
300
+ const lintReportAndExit = async (names) => {
301
+ const results = await Promise.all(names.map(lintComponent));
302
+ const hasIssues = results.map(logReport).some(Boolean);
303
+
304
+ if (hasIssues) {
305
+ process.exit(1);
306
+ }
307
+ };
313
308
 
314
309
  // Only perform linting/reporting when instructed via "-r".
315
310
  /* istanbul ignore next */
316
- if (R.includes(process.argv[2], ['-h', '--help'])) {
311
+ if (['-h', '--help'].includes(process.argv[2])) {
317
312
  printHelp();
318
- } else if (R.pathEq(['argv', 2], '-r')(process)) {
313
+ } else if (process.argv[2] === '-r') {
319
314
  loadProjectA11yConfig().then((projectConfig) => {
320
315
  applyProjectA11yConfig(projectConfig);
321
316
  return lintReportAndExit(resolvePa11yStoryIds());
@@ -84,7 +84,7 @@ function warnTextAssetSource(relPath, reason) {
84
84
 
85
85
  warnedAssetSources.add(relPath);
86
86
  console.warn(
87
- `source(): ${reason} for @assets/${relPath}. Synchronous XHR fallback is disabled by default because it blocks Storybook rendering. Move the asset under a configured asset root such as src/assets or assets, or temporarily enable platformAdapter.storybook.allowSyncXhrSource. The sync-XHR fallback is deprecated and will be removed in 4.2.`,
87
+ `source(): ${reason} for @assets/${relPath}. Synchronous XHR fallback is disabled by default because it blocks Storybook rendering. Move the asset under a configured asset root such as src/assets or assets, or temporarily enable platformAdapter.storybook.allowSyncXhrSource. The sync-XHR fallback is deprecated and scheduled for removal in a future major release.`,
88
88
  );
89
89
  }
90
90