@emulsify/core 4.2.1 → 4.3.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.
Files changed (84) hide show
  1. package/.storybook/main-vite.js +102 -37
  2. package/.storybook/main.js +20 -0
  3. package/.storybook/ready-reporter.js +230 -0
  4. package/README.md +186 -56
  5. package/config/a11y.config.js +2 -1
  6. package/config/vite/entries.js +4 -4
  7. package/config/vite/plugins/assets/copy-src-assets.js +120 -0
  8. package/config/vite/plugins/assets/copy-twig-files.js +130 -0
  9. package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
  10. package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
  11. package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
  12. package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
  13. package/config/vite/plugins/index.js +23 -14
  14. package/config/vite/plugins/reporter/asset-resolver.js +541 -0
  15. package/config/vite/plugins/reporter/build-errors.js +284 -0
  16. package/config/vite/plugins/reporter/diagnostics.js +367 -0
  17. package/config/vite/plugins/reporter/format.js +240 -0
  18. package/config/vite/plugins/reporter/index.js +417 -0
  19. package/config/vite/plugins/reporter/render.js +1332 -0
  20. package/config/vite/plugins/reporter/sass-logger.js +237 -0
  21. package/config/vite/plugins/reporter/source-roots.js +561 -0
  22. package/config/vite/plugins/reporter/verbosity.js +119 -0
  23. package/config/vite/plugins/reporter/vite-logger.js +249 -0
  24. package/config/vite/plugins/reporter/watch-mode.js +40 -0
  25. package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
  26. package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
  27. package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +197 -85
  28. package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
  29. package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
  30. package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
  31. package/config/vite/plugins.js +1 -1
  32. package/config/vite/project-config.js +1 -1
  33. package/config/vite/project-structure.js +1 -1
  34. package/config/vite/utils/lru.js +77 -0
  35. package/config/vite/utils/package-version.js +42 -0
  36. package/config/vite/utils/paths.js +1 -9
  37. package/config/vite/utils/react-singleton.js +1 -1
  38. package/config/vite/vite.config.js +91 -5
  39. package/package.json +95 -57
  40. package/scripts/a11y.js +115 -23
  41. package/scripts/audit/checks/core-imports.js +78 -0
  42. package/scripts/audit/checks/css-asset-references.js +99 -0
  43. package/scripts/audit/checks/drupal-assumptions.js +48 -0
  44. package/scripts/audit/checks/files-outside-roots.js +53 -0
  45. package/scripts/audit/checks/generated-package-scripts.js +113 -0
  46. package/scripts/audit/checks/legacy-twig-stories.js +33 -0
  47. package/scripts/audit/checks/package-overrides.js +91 -0
  48. package/scripts/audit/checks/project-config.js +71 -0
  49. package/scripts/audit/checks/story-discovery.js +35 -0
  50. package/scripts/audit/checks/twig-references.js +69 -0
  51. package/scripts/audit/checks/twig-volume.js +54 -0
  52. package/scripts/audit/checks/webpack-patterns.js +86 -0
  53. package/scripts/audit/index.js +177 -0
  54. package/scripts/audit/lib/css.js +165 -0
  55. package/scripts/audit/lib/files.js +168 -0
  56. package/scripts/audit/lib/findings.js +31 -0
  57. package/scripts/audit/lib/package-json.js +65 -0
  58. package/scripts/audit/lib/twig.js +227 -0
  59. package/scripts/audit/report.js +273 -0
  60. package/scripts/audit-twig-stories.js +115 -78
  61. package/scripts/audit.js +150 -1632
  62. package/scripts/check-node-version.js +136 -10
  63. package/scripts/inspect-components.js +456 -0
  64. package/scripts/lib/cli.js +179 -0
  65. package/scripts/lib/fs.js +31 -0
  66. package/scripts/lib/proc.js +78 -0
  67. package/scripts/lib/text.js +14 -0
  68. package/scripts/loadYaml.js +2 -2
  69. package/src/extensions/shared/attributes.js +3 -3
  70. package/src/extensions/shared/lists.js +2 -6
  71. package/src/extensions/shared/root-relative.js +38 -0
  72. package/src/storybook/index.js +4 -0
  73. package/src/storybook/render-twig.js +1 -1
  74. package/src/storybook/render-web-component.js +459 -0
  75. package/src/storybook/twig/asset-source-runtime.js +193 -0
  76. package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
  77. package/src/storybook/twig/reference-paths.js +2 -13
  78. package/src/storybook/twig/setup.js +12 -2
  79. package/src/storybook/twig/source-function.js +5 -2
  80. package/config/vite/plugins/copy-src-assets.js +0 -76
  81. package/config/vite/plugins/copy-twig-files.js +0 -84
  82. package/config/vite/utils/unique.js +0 -36
  83. package/src/storybook/twig/include.js +0 -28
  84. package/src/storybook/twig/source-events.js +0 -5
package/README.md CHANGED
@@ -2,47 +2,85 @@
2
2
 
3
3
  # Emulsify Core
4
4
 
5
- An open-source toolset for creating and implementing design systems.
5
+ An open-source foundation for building design systems across component
6
+ libraries, CMS themes, Storybook, and the handoff between design and
7
+ development.
6
8
 
7
- **Emulsify Core** provides shared [Vite](https://vite.dev/) build configuration and a [Storybook](https://storybook.js.org/) component library setup for component-driven development. Twig-based components and React components are both supported authoring models. A project can be Twig-first, React-first, or intentionally mixed.
9
+ **Emulsify Core** provides shared [Vite](https://vite.dev/) build configuration
10
+ and a [Storybook](https://storybook.js.org/) workspace for component-driven
11
+ development with Twig, React, and autonomous custom elements.
8
12
 
9
13
  ## How Emulsify Core Works
10
14
 
11
- - Vite builds project JavaScript, Sass/CSS, Twig templates, component metadata, and static component assets.
12
- - Storybook uses the React/Vite framework.
13
- - Twig files can render in React-based Storybook through `renderTwig()`.
14
- - React components render through Storybook's React/Vite support.
15
- - Twig and React stories can coexist in the same Storybook instance.
16
- - `project.emulsify.json` is the source of truth for platform and structure configuration.
17
- - Platform-specific behavior is controlled by adapters instead of being assumed globally.
18
- - Node.js 24 or later is required.
15
+ Emulsify Core is the shared frontend layer for Emulsify projects. It gives teams
16
+ one place to build, document, and test components while leaving platform-specific
17
+ implementation details where they belong.
18
+
19
+ - Vite builds project JavaScript, Sass/CSS, Twig templates, component metadata,
20
+ and static component assets.
21
+ - Storybook runs on the React/Vite framework.
22
+ - Twig files render in React-based Storybook through `renderTwig()`.
23
+ - React components render through Storybook's standard React support.
24
+ - Autonomous custom elements render in Storybook through
25
+ `renderWebComponent()`.
26
+ - Twig, React, and custom element stories can live together in one Storybook
27
+ workspace.
28
+ - `project.emulsify.json` is the source of truth for platform and structure
29
+ configuration.
30
+ - Platform adapters control CMS-specific behavior instead of assuming it
31
+ globally.
32
+
33
+ ## Node.js Runtime Policy
34
+
35
+ - Consumers are supported on Node.js 24.13.0 or later. The strictest published
36
+ toolchain dependency, `stylelint-selector-bem-pattern` 5, requires that patch.
37
+ - Contributors should use Node.js 24.18.0, the exact version pinned in `.nvmrc`.
38
+ - CI also uses Node.js 24.18.0 by reading `.nvmrc`, so local development and
39
+ automated checks share the same recommended runtime.
19
40
 
20
41
  ## Project Evolution
21
42
 
22
- Emulsify Core has grown through each major release while keeping the same practical goal: make component-library tooling easier to share across real projects.
43
+ Emulsify Core began as shared Webpack and Storybook tooling, adopted ESM in
44
+ 3.x, and moved to Vite and React/Vite Storybook in 4.x. The current project
45
+ model supports CMS themes, standalone component libraries, and mixed Twig,
46
+ React, and custom element Storybook workspaces.
23
47
 
24
- - `1.x` established Emulsify Core as a reusable package for Storybook, Webpack, linting, a11y checks, project overrides, and asset handling.
25
- - `2.x` expanded component structure support, improved Drupal SDC compatibility, upgraded Storybook, and made more project files configurable from consuming projects.
26
- - `3.x` modernized the runtime around ESM and Node 24, continued Storybook and dependency upgrades, improved component asset copying, and strengthened compatibility for existing Drupal-oriented builds.
27
- - The current release moves the build system to Vite, runs Storybook on React/Vite, supports Twig and React stories side by side, and normalizes platform and project-structure behavior through `project.emulsify.json`.
48
+ See [Version Evolution](docs/version-evolution.md) for major-version history
49
+ and the [4.3.0 release notes](docs/releases/4.3.0.md) for the compatibility
50
+ changes and additions in that release.
28
51
 
29
- The latest version is the next evolution of that work: faster builds, clearer public APIs, less global Drupal assumption, and a broader foundation for CMS themes, standalone UI libraries, and mixed component systems.
52
+ ## Authoring Models
30
53
 
31
- See [Version Evolution](docs/version-evolution.md) for more release history.
54
+ Emulsify Core supports Twig and React authoring workflows plus a focused
55
+ Storybook adapter for web components built with autonomous custom elements. The
56
+ right choice depends on how the design system will be used.
32
57
 
33
- ## Authoring Models
58
+ - Use Twig for CMS themes and server-rendered template systems. Drupal has a
59
+ Drupal-specific adapter, and WordPress/Timber projects can use the
60
+ intentionally neutral `wordpress` adapter. WordPress runtime integration
61
+ belongs in `emulsify-wordpress-theme`.
62
+ - Use React for standalone UI libraries, application components, or projects
63
+ that already use React.
64
+ - Use web components built with autonomous custom elements for
65
+ framework-neutral browser components that fit the adapter's documented
66
+ property, attribute, default-slot, and native event boundaries.
67
+ - Use mixed authoring when a design system needs to document CMS-rendered,
68
+ framework-rendered, and framework-neutral components in the same Storybook
69
+ instance.
34
70
 
35
- Twig and React are equally valid ways to build component libraries with Emulsify Core. The right authoring model depends on the consuming project:
71
+ See [Component Authoring](docs/component-authoring.md) for Twig, React, custom
72
+ element, mixed Storybook, and shared Sass examples.
36
73
 
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
- - Use React for standalone UI libraries, application components, or projects that already use React.
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.
74
+ ## Installation And Project Setup
40
75
 
41
- See [Component Authoring](docs/component-authoring.md) for Twig, React, mixed Storybook, and shared Sass examples.
76
+ Emulsify Core can enter a project in two common ways. Both are supported and both
77
+ are valid. The right setup path depends on whether you are wiring Core into a
78
+ project yourself or starting from an Emulsify starter.
42
79
 
43
- ## Basic Usage
80
+ ### Manual Setup With npm
44
81
 
45
- Installation and project scripts are usually provided by a starter or platform integration. Manual setup starts with:
82
+ Use npm when you want to add Emulsify Core to an existing project, a custom
83
+ starter, or a project that owns its own setup decisions.
46
84
 
47
85
  ```sh
48
86
  npm install @emulsify/core
@@ -63,74 +101,164 @@ Every project should provide a `project.emulsify.json` file at the project root:
63
101
  }
64
102
  ```
65
103
 
66
- Asset files are discovered from the default asset roots and any additional
67
- roots configured in `project.emulsify.json`. Use asset roots when a project
68
- stores fonts, images, icons, or other static files outside the default
69
- locations.
104
+ Asset files are discovered from the default asset roots and any additional roots
105
+ configured in `project.emulsify.json`. Use asset roots when a project stores
106
+ fonts, images, icons, or other static files outside the default locations.
70
107
 
71
108
  Common project scripts call the shared Emulsify Core Vite and Storybook config:
72
109
 
73
110
  - `storybook`: starts Storybook development.
74
111
  - `storybook-build`: builds static Storybook output.
75
- - `build`: runs the Vite build for JS, CSS, copied Twig templates, component metadata, and static component assets.
112
+ - `build`: runs the Vite build for JS, CSS, copied Twig templates, component
113
+ metadata, and static component assets.
76
114
  - `lint`: lints maintained project source.
77
115
 
116
+ ### Streamlined Setup With Emulsify CLI
117
+
118
+ Use the [Emulsify CLI](https://github.com/emulsify-ds/emulsify-cli) when you
119
+ want a starter-driven setup with project scaffolding, starter hooks, system
120
+ installation, and component generation.
121
+
122
+ Install the CLI globally:
123
+
124
+ ```sh
125
+ npm install -g @emulsify/cli
126
+ ```
127
+
128
+ Then initialize a project from a starter:
129
+
130
+ ```sh
131
+ emulsify init "My Project" ./path/to/projects --platform none
132
+ ```
133
+
134
+ Global or direct CLI installation is still the supported way to run
135
+ `emulsify init` before a project exists. For Core 4.x compatibility, projects
136
+ that install `@emulsify/core` also receive a project-local `emulsify` binary
137
+ through Core's `@emulsify/cli` dependency. That local binary can be invoked with
138
+ `npx --no-install emulsify --help`; it is a compatibility bridge for existing
139
+ Core consumers, not a replacement for choosing an explicit CLI installation
140
+ when scaffolding a new project.
141
+
142
+ The CLI supports built-in `drupal`, `wordpress`, and `none` platforms. It can
143
+ create starter projects, install component systems, list available components,
144
+ install system components, and generate local components. The starter owns the
145
+ project structure and scripts; Emulsify Core still provides the shared build and
146
+ Storybook foundation underneath.
147
+
78
148
  ## Documentation
79
149
 
80
150
  The documentation is split by task:
81
151
 
82
- | Topic | Use This When |
83
- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
84
- | [Version Evolution](docs/version-evolution.md) | Understanding how Emulsify Core has evolved across major releases. |
85
- | [Component Authoring](docs/component-authoring.md) | Choosing Twig, React, or mixed Storybook authoring and comparing component examples. |
86
- | [Storybook](docs/storybook.md) | Rendering Twig stories, using `renderTwig()`, understanding Twig runtime helpers, and mixing Twig with React stories. |
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`, `wordpress`, `drupal`, platform resolution order, and Drupal SDC behavior. |
89
- | [Extension Points](docs/extension-points.md) | Adding Vite plugins, Tailwind CSS, Storybook preview overrides, and other framework tooling. |
90
- | [Performance](docs/performance.md) | Understanding sourcemaps, eager Twig imports, Tailwind scanning, copied files, and fixture validation. |
91
- | [Native Twig Extensions](docs/native-twig-extensions.md) | Using `bem()`, `add_attributes()`, and `switch/case/default/endswitch` in Twig.js. |
92
- | [Release Verification](docs/release.md) | Running 4.x release checks, tarball smoke tests, and semantic-release dry runs before publishing. |
93
- | [Migration](docs/migration-4x.md) | Upgrading from earlier versions while preserving existing structures. |
152
+ | Topic | Use This When |
153
+ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
154
+ | [Version Evolution](docs/version-evolution.md) | Understanding how Emulsify Core has evolved across major releases. |
155
+ | [Component Authoring](docs/component-authoring.md) | Choosing Twig, React, custom element, or mixed Storybook authoring and comparing component examples. |
156
+ | [Storybook](docs/storybook.md) | Rendering Twig and custom element stories, using Storybook helpers, and mixing authoring models. |
157
+ | [Project Structure And Output](docs/project-structure.md) | Configuring `src/components`, root `./components`, `variant.structureImplementations`, and expected output paths. |
158
+ | [Platform Adapters](docs/platform-adapters.md) | Understanding `none`, `wordpress`, `drupal`, platform resolution order, and Drupal SDC behavior. |
159
+ | [Extension Points](docs/extension-points.md) | Adding Vite plugins, Tailwind CSS, Storybook preview overrides, and other framework tooling. |
160
+ | [Dependency Contract](docs/dependency-contract.md) | Understanding why generated themes rely on Core runtime dependencies and npm hoisting. |
161
+ | [Performance](docs/performance.md) | Understanding sourcemaps, eager Twig imports, Tailwind scanning, copied files, and fixture validation. |
162
+ | [Native Twig Extensions](docs/native-twig-extensions.md) | Using `bem()`, `add_attributes()`, and `switch/case/default/endswitch` in Twig.js. |
163
+ | [Component Inspector](docs/component-inspector.md) | Listing recognized Twig component templates, exact references, shorthand collisions, and expected output paths. |
164
+ | [Project Audit](docs/audit.md) | Running human or versioned JSON project audits and configuring CI failure thresholds. |
165
+ | [Release Verification](docs/release.md) | Running 4.x release checks, tarball smoke tests, and semantic-release dry runs before publishing. |
166
+ | [Release Pull Request Review](docs/release-review.md) | Reviewing large release pull requests by independent API, runtime, consumer, accessibility, and release risks. |
167
+ | [Migration To 4.x](docs/migration-4x.md) | Upgrading a pre-4.x/Webpack project while preserving existing structures. |
168
+ | [4.3.0 Release Notes](docs/releases/4.3.0.md) | Reviewing the 4.3.0 scope, compatibility changes, public APIs, limitations, and verification evidence. |
169
+ | [4.3.1 Release Notes](docs/releases/4.3.1.md) | Reviewing the 4.3.1 develop reporter changes, verbosity controls, and scope limits. |
94
170
 
95
171
  ## Known Limitations
96
172
 
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
- - 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
- - Production sourcemaps are enabled by default unless a project overrides Vite config through `config/emulsify-core/vite/plugins.*`. See [Performance](docs/performance.md).
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
- - 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` and `wordpress` projects should expect output to remain in `dist/`. See [Platform Adapters](docs/platform-adapters.md).
173
+ Emulsify Core is intentionally focused on the shared build and Storybook layer.
174
+ Some integration work still belongs to the project, starter, theme, or platform
175
+ package using it.
176
+
177
+ - Implemented platform adapters are `none`, `wordpress`, and `drupal`. The
178
+ `wordpress` adapter is intentionally neutral: it supports Core Twig authoring,
179
+ Storybook, Vite, `bem()`, `add_attributes()`, `include()`, and `source()`, but
180
+ it does not emulate WordPress or Timber PHP runtime behavior. Runtime
181
+ integration belongs in `emulsify-wordpress-theme`. See
182
+ [Platform Adapters](docs/platform-adapters.md).
183
+ - Storybook eagerly imports compiled Twig modules for synchronous rendering,
184
+ while raw Twig and text asset sources load lazily when `source()` requests
185
+ them. Large Twig libraries should still keep Storybook source roots
186
+ intentional. See [Performance](docs/performance.md).
187
+ - Production sourcemaps are enabled by default unless a project overrides Vite
188
+ config through `config/emulsify-core/vite/plugins.*`. See
189
+ [Performance](docs/performance.md).
190
+ - Project extensions use the public `config/emulsify-core` directory:
191
+ `config/emulsify-core/vite/plugins.*` for Vite,
192
+ `config/emulsify-core/storybook/...` for Storybook, and
193
+ `config/emulsify-core/a11y.config.js` for a11y. See
194
+ [Extension Points](docs/extension-points.md).
195
+ - Webpack-specific customizations must be migrated manually to Vite plugins or
196
+ `extendConfig()`. See [Migration](docs/migration-4x.md).
197
+ - Drupal SDC mirroring only applies when the Drupal adapter and SDC settings are
198
+ enabled. `none` and `wordpress` projects should expect output to remain in
199
+ `dist/`. See [Platform Adapters](docs/platform-adapters.md).
200
+ - Generated themes that depend only on `@emulsify/core` assume npm's flat
201
+ `node_modules` hoisting for script binaries and shared config packages. pnpm's
202
+ isolated linker and Yarn Plug'n'Play are unsupported for that one-dependency
203
+ generated-theme pattern unless the consuming project declares each tool
204
+ package itself. See [Dependency Contract](docs/dependency-contract.md).
205
+ - The Storybook renderer for custom elements supports autonomous custom elements,
206
+ light DOM children for an unnamed default slot, and explicitly mapped native
207
+ events. It does not provide a named-slot composition API or support
208
+ customized built-in elements. See
209
+ [Storybook](docs/storybook.md#known-limitations).
103
210
 
104
211
  ## Supported Project Shapes
105
212
 
106
- Core supports these project shapes:
213
+ Emulsify Core is designed to meet projects where they are. These project shapes
214
+ are supported:
107
215
 
108
216
  - Drupal SDC projects using `src/components`.
109
217
  - `none` platform Twig projects using `src/components`.
110
218
  - `wordpress` platform Twig projects using `src/components`.
111
219
  - Root `./components` projects.
112
220
  - Projects using multiple `variant.structureImplementations`.
113
- - Mixed Twig + React Storybook projects.
221
+ - Mixed Storybook projects with Twig, React, and custom elements.
114
222
 
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`.
223
+ WordPress and Timber projects should use `platform: "wordpress"` when they want
224
+ Core's neutral WordPress adapter. The adapter keeps output in `dist/`, loads
225
+ Storybook CSS from `dist/**/*.css`, and leaves WordPress runtime behavior to
226
+ `emulsify-wordpress-theme`.
116
227
 
117
228
  ## Public Imports
118
229
 
119
- Emulsify Core exposes stable public package paths:
230
+ Emulsify Core exposes stable public package paths so consuming projects do not
231
+ need to reach into internal files:
120
232
 
121
233
  ```js
122
234
  import { renderTwig } from '@emulsify/core/storybook';
235
+ import {
236
+ defineCustomElement,
237
+ renderWebComponent,
238
+ } from '@emulsify/core/storybook';
123
239
  import { registerTwigExtensions } from '@emulsify/core/extensions/twig';
124
240
  import { defineReactExtension } from '@emulsify/core/extensions/react';
125
241
  ```
126
242
 
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).
243
+ `defineReactExtension` is reserved for future React extension support. It
244
+ currently returns the input unchanged. Adopting the import path is safe; the
245
+ runtime is intentionally a no-op until the registry lands. See
246
+ [Extension Points](docs/extension-points.md#public-imports).
247
+
248
+ Vite consumers can import the shared config from `@emulsify/core/vite`, public
249
+ Vite plugin helpers from `@emulsify/core/vite/plugins`, and platform adapter
250
+ helpers from `@emulsify/core/vite/platforms`.
128
251
 
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`.
252
+ `defineCustomElement()` and `renderWebComponent()` use the existing
253
+ `@emulsify/core/storybook` entry point; they do not require a dedicated package
254
+ subpath. The generated Twig asset-source runtime is internal and is not a
255
+ public package export.
130
256
 
131
257
  ## Contributing
132
258
 
133
- Maintained JavaScript source, config, scripts, and tests should use consistent comments:
259
+ Contributions should keep the codebase clear for the next person working in it.
260
+ Maintained JavaScript source, config, scripts, and tests should use consistent
261
+ comments:
134
262
 
135
263
  - Start each maintained JS file with a short JSDoc file block that explains the file's responsibility.
136
264
  - Use JSDoc blocks for exported functions, complex helpers, and public contracts.
@@ -138,7 +266,9 @@ Maintained JavaScript source, config, scripts, and tests should use consistent c
138
266
  - Keep comments concise and factual. Prefer explaining why behavior exists instead of restating the code.
139
267
  - Use YAML or shell comments in workflow, hook, and fixture files where the format supports comments.
140
268
 
141
- Do not add comments to JSON files, lockfiles, binary assets, generated output, legal documents, or dependency files. Those formats either do not support comments or should remain exact artifacts.
269
+ Do not add comments to JSON files, lockfiles, binary assets, generated output,
270
+ legal documents, or dependency files. Those formats either do not support
271
+ comments or should remain exact artifacts.
142
272
 
143
273
  Please also follow the issue template and pull request templates provided. See below for the correct places to post issues:
144
274
 
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  export default {
9
- storybookBuildDir: '../../../../.out',
9
+ storybookBuildDir: '.out',
10
10
  discoverStories: true,
11
11
  pa11y: {
12
+ actions: ['wait for element #storybook-root > * to be visible'],
12
13
  includeNotices: false,
13
14
  includeWarnings: false,
14
15
  runners: ['axe'],
@@ -24,10 +24,10 @@ import {
24
24
  resolveProjectStructure,
25
25
  storybookStyleOutputPath,
26
26
  } from './project-structure.js';
27
- import { createSourceFileIndex } from './plugins/source-file-index.js';
28
- import { replaceLastSlash, toPosix } from './utils/paths.js';
27
+ import { createSourceFileIndex } from './plugins/assets/source-file-index.js';
28
+ import { replaceLastSlash, toPosixPath } from './utils/paths.js';
29
29
 
30
- export { replaceLastSlash, toPosix };
30
+ export { replaceLastSlash, toPosixPath };
31
31
 
32
32
  /** Remove characters that would confuse Rollup naming or file systems. */
33
33
  export const sanitizePath = (s) => s.replace(/[^a-zA-Z0-9/_-]/g, '');
@@ -123,7 +123,7 @@ export function buildInputs(ctx) {
123
123
  */
124
124
  const add = (key, abs) => {
125
125
  if (!key) return;
126
- const clean = sanitizePath(toPosix(key)).replace(/^\/+/, '');
126
+ const clean = sanitizePath(toPosixPath(key)).replace(/^\/+/, '');
127
127
  if (!clean) return;
128
128
  safeSetKey(inputs, clean, abs);
129
129
  };
@@ -0,0 +1,120 @@
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
+ let watching = false;
33
+ /** @type {Array<{absPath: string, relDest: string}>|undefined} */
34
+ let plan;
35
+
36
+ /**
37
+ * Resolve every asset this plugin copies, paired with where it lands.
38
+ *
39
+ * Shared by both hooks for the same reason as the Twig copier: watching and
40
+ * copying have to be driven by one list, or a file can end up copied on a full
41
+ * build and ignored on a save.
42
+ *
43
+ * @returns {Array<{absPath: string, relDest: string}>} Copy plan.
44
+ */
45
+ const copyPlan = () => {
46
+ if (plan) return plan;
47
+
48
+ plan = [];
49
+
50
+ for (const file of sourceFileIndex.componentFiles()) {
51
+ if (!isStaticSourceAsset(file.absPath)) continue;
52
+
53
+ plan.push({
54
+ absPath: file.absPath,
55
+ relDest: copiedComponentOutputPath(file.absPath, structure),
56
+ });
57
+ }
58
+
59
+ for (const file of sourceFileIndex.globalFiles()) {
60
+ if (!isStaticSourceAsset(file.absPath)) continue;
61
+ if (findSourceRoot(file.absPath, structure.componentRootRecords))
62
+ continue;
63
+
64
+ plan.push({
65
+ absPath: file.absPath,
66
+ relDest: copiedGlobalOutputPath(file.absPath, structure),
67
+ });
68
+ }
69
+
70
+ return plan;
71
+ };
72
+
73
+ return {
74
+ name: 'emulsify-copy-all-src-assets',
75
+ apply: 'build',
76
+ enforce: 'post',
77
+
78
+ /** Capture outDir. */
79
+ configResolved(cfg) {
80
+ outDir = cfg.build?.outDir || 'dist';
81
+ watching = Boolean(cfg.build?.watch);
82
+ },
83
+
84
+ // Static assets are copied rather than compiled, so like Twig they are absent
85
+ // from Rollup's module graph and a save would otherwise go unnoticed. Swapping
86
+ // an SVG or a font left the old bytes in `dist/` until an unrelated rebuild.
87
+ buildStart() {
88
+ if (!watching) return;
89
+ for (const { absPath } of copyPlan()) this.addWatchFile(absPath);
90
+ },
91
+
92
+ /** Copy before the mirror plugin moves dist/components to the project root. */
93
+ writeBundle() {
94
+ for (const { absPath, relDest } of copyPlan()) {
95
+ copyToOutDir(absPath, relDest);
96
+ }
97
+ },
98
+ };
99
+
100
+ /**
101
+ * Copy one file into the output directory.
102
+ *
103
+ * @param {string} absPath - Absolute source path.
104
+ * @param {string} relDest - Destination relative to `outDir`.
105
+ * @returns {void}
106
+ */
107
+ function copyToOutDir(absPath, relDest) {
108
+ if (!relDest) return;
109
+
110
+ // Copied unconditionally; see the note in copy-twig-files.js — `emptyOutDir`
111
+ // clears the destination on every cycle, so nothing is ever up to date.
112
+ const destPath = join(outDir, relDest);
113
+ mkdirSync(dirname(destPath), { recursive: true });
114
+ try {
115
+ copyFileSync(absPath, destPath);
116
+ } catch {
117
+ /* noop */
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,130 @@
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
+ let watching = false;
36
+ /** @type {Array<{absPath: string, relDest: string}>|undefined} */
37
+ let plan;
38
+
39
+ /**
40
+ * Resolve every file this plugin copies, paired with where it lands.
41
+ *
42
+ * Built once and reused, because the source index is resolved at config time
43
+ * and does not change across watch cycles. Both hooks below read this same
44
+ * list, which is what keeps "gets copied to dist" and "a save triggers the
45
+ * copy" from drifting apart — a file cannot be added to one without the other.
46
+ *
47
+ * @returns {Array<{absPath: string, relDest: string}>} Copy plan.
48
+ */
49
+ const copyPlan = () => {
50
+ if (plan) return plan;
51
+
52
+ plan = [];
53
+
54
+ for (const file of sourceFileIndex.componentFiles()) {
55
+ const isTwig = file.absPath.endsWith('.twig');
56
+
57
+ if (!isTwig && !isComponentMetadataFile(file.absPath)) continue;
58
+ if (isTwig && isPartial(file.relPath)) continue;
59
+
60
+ plan.push({
61
+ absPath: file.absPath,
62
+ relDest: copiedComponentOutputPath(file.absPath, structure),
63
+ });
64
+ }
65
+
66
+ for (const file of sourceFileIndex.globalFiles()) {
67
+ if (!file.absPath.endsWith('.twig')) continue;
68
+ if (isPartial(file.relPath)) continue;
69
+
70
+ plan.push({
71
+ absPath: file.absPath,
72
+ relDest: copiedGlobalOutputPath(file.absPath, structure),
73
+ });
74
+ }
75
+
76
+ return plan;
77
+ };
78
+
79
+ return {
80
+ name: 'emulsify-copy-twig-files',
81
+ apply: 'build',
82
+ enforce: 'post',
83
+
84
+ /** Capture the final outDir. */
85
+ configResolved(cfg) {
86
+ outDir = cfg.build?.outDir || 'dist';
87
+ watching = Boolean(cfg.build?.watch);
88
+ },
89
+
90
+ // Twig is copied rather than compiled, so none of it reaches Rollup's module
91
+ // graph, and Rollup only watches what is in that graph. Without this, saving
92
+ // a template produced no rebuild at all: `dist/` kept the previous version
93
+ // until some unrelated stylesheet happened to change. Storybook renders Twig
94
+ // through its own pipeline and looked correct throughout, so the stale copy
95
+ // was only visible to whatever consumes `dist/` — which on Drupal is the site.
96
+ buildStart() {
97
+ if (!watching) return;
98
+ for (const { absPath } of copyPlan()) this.addWatchFile(absPath);
99
+ },
100
+
101
+ /** Copy before the mirror plugin moves dist/components to the project root. */
102
+ writeBundle() {
103
+ for (const { absPath, relDest } of copyPlan()) {
104
+ copyToOutDir(absPath, relDest);
105
+ }
106
+ },
107
+ };
108
+
109
+ /**
110
+ * Copy one file into the output directory.
111
+ *
112
+ * @param {string} absPath - Absolute source path.
113
+ * @param {string} relDest - Destination relative to `outDir`.
114
+ * @returns {void}
115
+ */
116
+ function copyToOutDir(absPath, relDest) {
117
+ if (!relDest) return;
118
+
119
+ // Copied unconditionally, because `build.emptyOutDir` clears the output
120
+ // directory on every watch cycle and not just the first — a freshness check
121
+ // against the destination can never find anything to skip.
122
+ const destPath = join(outDir, relDest);
123
+ mkdirSync(dirname(destPath), { recursive: true });
124
+ try {
125
+ copyFileSync(absPath, destPath);
126
+ } catch {
127
+ /* noop */
128
+ }
129
+ }
130
+ }
@@ -3,6 +3,13 @@
3
3
  *
4
4
  * Rewrites emitted CSS references to root assets so nested CSS files can keep
5
5
  * resolving copied assets correctly from their final output directories.
6
+ *
7
+ * Sourcemap warning: this rewrites emitted CSS in `generateBundle` without
8
+ * adjusting positions, and every replacement changes the length of the line it
9
+ * sits on. Vite does not emit sourcemaps for extracted CSS today, so there is
10
+ * nothing to invalidate. If CSS sourcemaps are ever added, this plugin has to
11
+ * shift mappings as it rewrites (for example via MagicString) or each mapping
12
+ * after the first rewritten `url()` will silently resolve to the wrong column.
6
13
  */
7
14
 
8
15
  import { posix as pathPosix } from 'path';
@@ -26,6 +33,8 @@ export function cssAssetUrlRelativizer({ assetsRoot = 'assets' } = {}) {
26
33
 
27
34
  const fromDir = pathPosix.dirname(fileName);
28
35
 
36
+ // Length-changing rewrite: read the sourcemap warning in the file
37
+ // header before pairing this plugin with CSS sourcemaps.
29
38
  chunk.source = chunk.source.replace(
30
39
  /url\((['"]?)\/?assets\/([^)'"]+)\1\)/g,
31
40
  (match, quote = '', rest) => {