@emulsify/core 4.3.2 → 4.5.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 (54) hide show
  1. package/.storybook/main-static-assets.js +5 -8
  2. package/.storybook/main-vite.js +11 -3
  3. package/README.md +14 -6
  4. package/config/a11y-wcag22.js +11 -0
  5. package/config/vite/entries.js +7 -2
  6. package/config/vite/environment.js +4 -0
  7. package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
  8. package/config/vite/plugins/assets/copy-src-assets.js +82 -12
  9. package/config/vite/plugins/assets/copy-twig-files.js +85 -16
  10. package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
  11. package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
  12. package/config/vite/plugins/assets/development-source-maps.js +273 -0
  13. package/config/vite/plugins/assets/mirror-components.js +98 -82
  14. package/config/vite/plugins/assets/output-freshness.js +235 -0
  15. package/config/vite/plugins/assets/source-file-index.js +13 -13
  16. package/config/vite/plugins/assets/stable-watch-output.js +165 -0
  17. package/config/vite/plugins/assets/storybook-output.js +27 -0
  18. package/config/vite/plugins/index.js +95 -9
  19. package/config/vite/plugins/reporter/asset-resolver.js +34 -6
  20. package/config/vite/plugins/reporter/build-errors.js +7 -3
  21. package/config/vite/plugins/reporter/diagnostics.js +140 -10
  22. package/config/vite/plugins/reporter/index.js +380 -75
  23. package/config/vite/plugins/reporter/render.js +297 -44
  24. package/config/vite/plugins/reporter/sass-logger.js +30 -0
  25. package/config/vite/plugins/reporter/source-roots.js +101 -21
  26. package/config/vite/plugins/reporter/strict-mode.js +99 -0
  27. package/config/vite/plugins/reporter/vite-logger.js +220 -8
  28. package/config/vite/plugins/reporter/watch-mode.js +6 -2
  29. package/config/vite/plugins/twig/twig-module.js +35 -258
  30. package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
  31. package/config/vite/project-config.js +121 -21
  32. package/config/vite/project-structure.js +6 -0
  33. package/config/vite/utils/asset-roots.js +205 -0
  34. package/config/vite/utils/css-urls.js +350 -0
  35. package/config/vite/utils/fs-safe.js +38 -1
  36. package/config/vite/utils/source-directory-skips.js +13 -0
  37. package/config/vite/utils/source-maps.js +88 -0
  38. package/config/vite/utils/twig-component-resolver.js +316 -0
  39. package/config/vite/vite.config.js +106 -42
  40. package/package.json +54 -40
  41. package/scripts/a11y.js +88 -9
  42. package/scripts/audit/checks/css-asset-references.js +256 -24
  43. package/scripts/audit/checks/twig-references.js +16 -5
  44. package/scripts/audit/fix.js +836 -0
  45. package/scripts/audit/index.js +10 -2
  46. package/scripts/audit/lib/css.js +41 -35
  47. package/scripts/audit/lib/story-ast.js +392 -0
  48. package/scripts/audit/lib/story-render-paths.js +600 -0
  49. package/scripts/audit/lib/story-selection.js +190 -0
  50. package/scripts/audit/lib/twig.js +372 -80
  51. package/scripts/audit/report.js +83 -5
  52. package/scripts/audit-twig-stories.js +73 -3
  53. package/scripts/audit.js +87 -2
  54. package/src/storybook/twig/source-function.js +14 -10
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @file Select statically readable CSF story exports without executing modules.
3
+ */
4
+
5
+ import {
6
+ identifierName,
7
+ moduleBody,
8
+ resolveModuleValue,
9
+ readStaticProperty,
10
+ unwrapExpression,
11
+ } from './story-ast.js';
12
+
13
+ const RESERVED_EXPORTS = new Set([
14
+ 'default',
15
+ '__esModule',
16
+ '__namedExportsOrder',
17
+ ]);
18
+
19
+ /**
20
+ * Recognize literal falsy values used by CSF filter and render defaults.
21
+ *
22
+ * @param {object} node - Babel expression.
23
+ * @param {Map<string, object>} declarations - Module declarations.
24
+ * @returns {boolean} TRUE only for a statically known falsy value.
25
+ */
26
+ export function isStaticFalsy(node, declarations) {
27
+ const value = resolveModuleValue(node, declarations);
28
+ if (value?.type === 'NullLiteral') return true;
29
+ if (
30
+ ['BooleanLiteral', 'NumericLiteral', 'StringLiteral'].includes(value?.type)
31
+ ) {
32
+ return !value.value;
33
+ }
34
+ return (
35
+ value?.type === 'Identifier' &&
36
+ value.name === 'undefined' &&
37
+ !declarations.has('undefined')
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Decode supported CSF filters; dynamic expressions retain an unknown state.
43
+ *
44
+ * @param {object} property - Static property lookup.
45
+ * @param {Map<string, object>} declarations - Module declarations.
46
+ * @returns {object} Absent, known array/regex, or unknown filter.
47
+ */
48
+ function readFilter(property, declarations) {
49
+ if (property.state !== 'known') return property;
50
+ const value = resolveModuleValue(property.node, declarations);
51
+ if (isStaticFalsy(value, declarations)) return { state: 'absent' };
52
+
53
+ if (value?.type === 'ArrayExpression') {
54
+ const elements = value.elements.map(unwrapExpression);
55
+ if (elements.every((element) => element?.type === 'StringLiteral')) {
56
+ return {
57
+ state: 'known',
58
+ strings: elements.map((element) => element.value),
59
+ };
60
+ }
61
+ }
62
+ if (value?.type === 'RegExpLiteral') {
63
+ try {
64
+ return {
65
+ state: 'known',
66
+ regex: new RegExp(value.pattern, value.flags),
67
+ };
68
+ } catch {
69
+ // A newer RegExp syntax may parse but be unsupported by this Node version.
70
+ }
71
+ }
72
+ return { state: 'unknown' };
73
+ }
74
+
75
+ /**
76
+ * Match the Storybook CSF array/regex contract, with no shared RegExp cursor.
77
+ *
78
+ * @param {string} name - Exported name, rather than a local binding or story name.
79
+ * @param {object} filter - Decoded static filter.
80
+ * @returns {boolean|null} Match, mismatch, or unknown.
81
+ */
82
+ function matchesFilter(name, filter) {
83
+ if (filter.state === 'unknown') return null;
84
+ if (filter.strings) return filter.strings.includes(name);
85
+ // Storybook's isExportStory uses String.match(). A fresh expression also keeps
86
+ // sticky filters from changing the result when another export was checked.
87
+ return Boolean(
88
+ name.match(new RegExp(filter.regex.source, filter.regex.flags)),
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Collect named exports and default metadata before selecting any render paths.
94
+ *
95
+ * @param {object} ast - Babel File or Program.
96
+ * @returns {object} Metadata, candidates, and unresolved re-export state.
97
+ */
98
+ function collectExports(ast) {
99
+ let metadata;
100
+ let hasUnknownExports = false;
101
+ const candidates = [];
102
+ for (const statement of moduleBody(ast)) {
103
+ if (statement.exportKind === 'type') continue;
104
+ if (statement.type === 'ExportDefaultDeclaration') {
105
+ metadata = statement.declaration;
106
+ continue;
107
+ }
108
+ if (statement.type === 'ExportAllDeclaration') {
109
+ hasUnknownExports = true;
110
+ continue;
111
+ }
112
+ if (statement.type !== 'ExportNamedDeclaration') continue;
113
+ const declaration = statement.declaration;
114
+ if (declaration?.declare) continue;
115
+ if (declaration?.type === 'FunctionDeclaration' && declaration.id) {
116
+ candidates.push({
117
+ name: declaration.id.name,
118
+ node: declaration,
119
+ lineNode: declaration,
120
+ });
121
+ } else if (declaration?.type === 'VariableDeclaration') {
122
+ for (const declarator of declaration.declarations) {
123
+ if (declarator.id?.type !== 'Identifier') {
124
+ hasUnknownExports = true;
125
+ continue;
126
+ }
127
+ candidates.push({
128
+ name: declarator.id.name,
129
+ node: declarator.init,
130
+ lineNode: declarator,
131
+ });
132
+ }
133
+ }
134
+ for (const specifier of statement.specifiers) {
135
+ if (specifier.exportKind === 'type') continue;
136
+ const name = identifierName(specifier.exported);
137
+ if (name === 'default') {
138
+ metadata = statement.source ? statement : specifier.local;
139
+ } else if (name) {
140
+ candidates.push({
141
+ name,
142
+ node: statement.source ? null : specifier.local,
143
+ lineNode: specifier,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ return { metadata, candidates, hasUnknownExports };
149
+ }
150
+
151
+ /**
152
+ * Apply includeStories and excludeStories before resolving effective renders.
153
+ *
154
+ * Unknown filters keep possible stories in the analysis, but a known exclusion
155
+ * (or a known inclusion mismatch) can still rule a candidate out. No module is
156
+ * imported and no call, getter, or arbitrary expression is evaluated.
157
+ *
158
+ * @param {object} ast - Babel File or Program.
159
+ * @param {Map<string, object>} declarations - Module declarations.
160
+ * @returns {object} Selected candidates, default render, and selection certainty.
161
+ */
162
+ export function selectStoryExports(ast, declarations) {
163
+ const { metadata, candidates, hasUnknownExports } = collectExports(ast);
164
+ const include = readFilter(
165
+ readStaticProperty(metadata, 'includeStories', declarations),
166
+ declarations,
167
+ );
168
+ const exclude = readFilter(
169
+ readStaticProperty(metadata, 'excludeStories', declarations),
170
+ declarations,
171
+ );
172
+ const stories = [];
173
+ let hasUnknownSelection = hasUnknownExports;
174
+ for (const candidate of candidates) {
175
+ if (RESERVED_EXPORTS.has(candidate.name)) continue;
176
+ const included =
177
+ include.state === 'absent' || matchesFilter(candidate.name, include);
178
+ const excluded =
179
+ exclude.state !== 'absent' && matchesFilter(candidate.name, exclude);
180
+ // Storybook requires inclusion and no exclusion; an exclusion always wins.
181
+ if (included === false || excluded === true) continue;
182
+ if (included === null || excluded === null) hasUnknownSelection = true;
183
+ stories.push(candidate);
184
+ }
185
+ return {
186
+ stories,
187
+ defaultRender: readStaticProperty(metadata, 'render', declarations),
188
+ hasUnknownSelection,
189
+ };
190
+ }