@emulsify/core 4.2.0 → 4.3.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 (79) hide show
  1. package/.storybook/main-vite.js +91 -36
  2. package/README.md +185 -56
  3. package/config/a11y.config.js +2 -1
  4. package/config/vite/entries.js +4 -4
  5. package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
  6. package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
  7. package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
  8. package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
  9. package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
  10. package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
  11. package/config/vite/plugins/index.js +23 -14
  12. package/config/vite/plugins/reporter/asset-resolver.js +541 -0
  13. package/config/vite/plugins/reporter/build-errors.js +284 -0
  14. package/config/vite/plugins/reporter/diagnostics.js +366 -0
  15. package/config/vite/plugins/reporter/format.js +199 -0
  16. package/config/vite/plugins/reporter/index.js +308 -0
  17. package/config/vite/plugins/reporter/render.js +758 -0
  18. package/config/vite/plugins/reporter/sass-logger.js +237 -0
  19. package/config/vite/plugins/reporter/vite-logger.js +188 -0
  20. package/config/vite/plugins/reporter/watch-mode.js +40 -0
  21. package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
  22. package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
  23. package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +387 -115
  24. package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
  25. package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
  26. package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
  27. package/config/vite/plugins.js +1 -1
  28. package/config/vite/project-config.js +1 -1
  29. package/config/vite/project-structure.js +1 -1
  30. package/config/vite/utils/lru.js +77 -0
  31. package/config/vite/utils/package-version.js +42 -0
  32. package/config/vite/utils/paths.js +1 -9
  33. package/config/vite/utils/react-singleton.js +1 -1
  34. package/config/vite/vite.config.js +51 -5
  35. package/package.json +93 -58
  36. package/scripts/a11y.js +115 -23
  37. package/scripts/audit/checks/core-imports.js +78 -0
  38. package/scripts/audit/checks/css-asset-references.js +99 -0
  39. package/scripts/audit/checks/drupal-assumptions.js +48 -0
  40. package/scripts/audit/checks/files-outside-roots.js +53 -0
  41. package/scripts/audit/checks/generated-package-scripts.js +113 -0
  42. package/scripts/audit/checks/legacy-twig-stories.js +33 -0
  43. package/scripts/audit/checks/package-overrides.js +91 -0
  44. package/scripts/audit/checks/project-config.js +71 -0
  45. package/scripts/audit/checks/story-discovery.js +35 -0
  46. package/scripts/audit/checks/twig-references.js +69 -0
  47. package/scripts/audit/checks/twig-volume.js +54 -0
  48. package/scripts/audit/checks/webpack-patterns.js +86 -0
  49. package/scripts/audit/index.js +177 -0
  50. package/scripts/audit/lib/css.js +165 -0
  51. package/scripts/audit/lib/files.js +168 -0
  52. package/scripts/audit/lib/findings.js +31 -0
  53. package/scripts/audit/lib/package-json.js +65 -0
  54. package/scripts/audit/lib/twig.js +227 -0
  55. package/scripts/audit/report.js +273 -0
  56. package/scripts/audit-twig-stories.js +115 -78
  57. package/scripts/audit.js +150 -1632
  58. package/scripts/check-node-version.js +136 -10
  59. package/scripts/inspect-components.js +456 -0
  60. package/scripts/lib/cli.js +179 -0
  61. package/scripts/lib/fs.js +31 -0
  62. package/scripts/lib/proc.js +78 -0
  63. package/scripts/lib/text.js +14 -0
  64. package/scripts/loadYaml.js +2 -2
  65. package/src/extensions/shared/attributes.js +3 -3
  66. package/src/extensions/shared/lists.js +2 -6
  67. package/src/extensions/shared/root-relative.js +38 -0
  68. package/src/storybook/index.js +4 -0
  69. package/src/storybook/render-twig.js +1 -1
  70. package/src/storybook/render-web-component.js +459 -0
  71. package/src/storybook/twig/asset-source-runtime.js +193 -0
  72. package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
  73. package/src/storybook/twig/reference-paths.js +2 -13
  74. package/src/storybook/twig/resolver.js +20 -3
  75. package/src/storybook/twig/setup.js +12 -2
  76. package/src/storybook/twig/source-function.js +5 -2
  77. package/config/vite/utils/unique.js +0 -36
  78. package/src/storybook/twig/include.js +0 -28
  79. package/src/storybook/twig/source-events.js +0 -5
@@ -5,16 +5,26 @@
5
5
  * resident unless a Twig template calls `source('@assets/...')`.
6
6
  */
7
7
 
8
- import { readdirSync } from 'fs';
8
+ import { readFileSync, readdirSync } from 'fs';
9
9
  import { relative, resolve } from 'path';
10
- import { safeExists } from '../utils/fs-safe.js';
11
- import { toPosixPath } from '../utils/paths.js';
12
- import { unique } from '../utils/unique.js';
13
- import { INLINE_ASSET_EXTS } from '../../../src/storybook/twig/source-extensions.js';
10
+ import { fileURLToPath } from 'url';
11
+ import { safeExists } from '../../utils/fs-safe.js';
12
+ import { toPosixPath } from '../../utils/paths.js';
13
+ import { unique } from '../../../../src/extensions/shared/lists.js';
14
+ import { toRootRelativePath } from '../../../../src/extensions/shared/root-relative.js';
15
+ import { INLINE_ASSET_EXTS } from '../../../../src/storybook/twig/constants.js';
14
16
 
15
17
  export const VIRTUAL_TWIG_ASSET_SOURCES_ID =
16
18
  'virtual:emulsify-twig-asset-sources';
17
19
  const RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID = `\0${VIRTUAL_TWIG_ASSET_SOURCES_ID}`;
20
+ const VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID =
21
+ 'virtual:emulsify-twig-asset-source-runtime';
22
+ const RESOLVED_VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID = `\0${VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID}`;
23
+ const ASSET_SOURCE_RUNTIME_URL = new URL(
24
+ '../../../../src/storybook/twig/asset-source-runtime.js',
25
+ import.meta.url,
26
+ );
27
+ const ASSET_SOURCE_RUNTIME_PATH = fileURLToPath(ASSET_SOURCE_RUNTIME_URL);
18
28
  const GENERATED_ASSET_ALIASES = new Set(['icons.svg']);
19
29
  const GENERATED_ASSET_ROOTS = ['/dist/assets'];
20
30
  const PUBLIC_ASSET_ROOTS = new Map([
@@ -22,35 +32,6 @@ const PUBLIC_ASSET_ROOTS = new Map([
22
32
  ['/dist/assets', '/assets'],
23
33
  ]);
24
34
 
25
- /**
26
- * Convert an absolute project path to a Vite root-relative glob base.
27
- *
28
- * @param {string} projectDir - Absolute project root.
29
- * @param {string} absolutePath - Absolute asset root path.
30
- * @returns {string} Vite root-relative path.
31
- */
32
- function toRootRelativePath(projectDir, absolutePath) {
33
- if (!absolutePath) return '';
34
-
35
- const normalizedProjectDir = toPosixPath(projectDir || '').replace(
36
- /\/+$/,
37
- '',
38
- );
39
- const normalizedPath = toPosixPath(absolutePath).replace(/\/+$/, '');
40
-
41
- if (
42
- normalizedProjectDir &&
43
- normalizedPath.startsWith(`${normalizedProjectDir}/`)
44
- ) {
45
- return `/${normalizedPath.slice(normalizedProjectDir.length + 1)}`.replace(
46
- /\/{2,}/g,
47
- '/',
48
- );
49
- }
50
-
51
- return `/${normalizedPath.replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
52
- }
53
-
54
35
  /**
55
36
  * Resolve a configured asset root to an absolute filesystem path.
56
37
  *
@@ -275,11 +256,13 @@ export function generateVirtualTwigAssetSourcesModule(env) {
275
256
  .join(',\n');
276
257
 
277
258
  return `/**
278
- * Virtual module generated by config/vite/plugins/virtual-twig-asset-sources.js.
259
+ * Virtual module generated by config/vite/plugins/twig/virtual-twig-asset-sources.js.
279
260
  *
280
261
  * Raw text assets stay lazy and load only when Twig source() requests them.
281
262
  */
282
263
 
264
+ import { createAssetSourceRuntime } from '${VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID}';
265
+
283
266
  export const assetRootPrefixes = ${JSON.stringify(rootPrefixes)};
284
267
  export const generatedAssetRootPrefixes = ${JSON.stringify(allGeneratedRootPrefixes)};
285
268
  export const generatedAssetAliases = ${JSON.stringify(
@@ -299,107 +282,18 @@ ${globEntries}
299
282
  ${fetchEntries ? `${globEntries ? ',\n' : ''}{\n${fetchEntries}\n}` : ''}
300
283
  ]);
301
284
 
302
- const sourceTextCache = new Map();
303
- const sourceLoadPromises = new Map();
304
-
305
- const unique = (values) => Array.from(new Set(values.filter(Boolean)));
306
-
307
- const normalizeAssetPath = (assetPath) =>
308
- String(assetPath || '')
309
- .replace(/^@assets\\//, '')
310
- .replace(/^\\/?assets\\//, '')
311
- .replace(/^\\/+/, '');
312
-
313
- const candidateKeysForAssetPath = (assetPath) => {
314
- const rawPath = String(assetPath || '');
315
- const normalized = normalizeAssetPath(rawPath);
316
- const directPath = rawPath.startsWith('/') ? rawPath : \`/\${rawPath}\`;
317
- const generatedCandidates = generatedAssetAliases.includes(normalized)
318
- ? generatedAssetRootPrefixes.map((root) =>
319
- \`\${root.replace(/\\/+$/, '')}/\${normalized}\`,
320
- )
321
- : [];
322
-
323
- return unique([
324
- rawPath,
325
- directPath,
326
- normalized ? \`/\${normalized}\` : '',
327
- ...generatedCandidates,
328
- ...assetRootPrefixes.map((root) =>
329
- \`\${root.replace(/\\/+$/, '')}/\${normalized}\`,
330
- ),
331
- ]);
332
- };
333
-
334
- const findAssetKey = (assetPath) =>
335
- candidateKeysForAssetPath(assetPath).find((key) =>
336
- Object.hasOwnProperty.call(assets, key),
337
- );
338
-
339
- const normalizeSourceText = (value) => {
340
- const source = value?.default ?? value;
341
- return typeof source === 'string' ? source : undefined;
342
- };
343
-
344
- export const coversAssetPath = (assetPath) =>
345
- (assetRootPrefixes.length > 0 || generatedAssetRootPrefixes.length > 0) &&
346
- normalizeAssetPath(assetPath).length > 0;
347
-
348
- export const hasAssetText = (assetPath) => Boolean(findAssetKey(assetPath));
349
-
350
- export const isAssetTextLoading = (assetPath) => {
351
- const key = findAssetKey(assetPath);
352
- return Boolean(key && sourceLoadPromises.has(key));
353
- };
354
-
355
- export const whenAssetTextLoaded = (assetPath) => {
356
- const key = findAssetKey(assetPath);
357
- return key ? sourceLoadPromises.get(key) : undefined;
358
- };
359
-
360
- export const getAssetText = (assetPath) => {
361
- const key = findAssetKey(assetPath);
362
- if (!key) return undefined;
363
- if (sourceTextCache.has(key)) {
364
- return sourceTextCache.get(key);
365
- }
366
-
367
- const loader = assets[key];
368
- const sourceText = normalizeSourceText(loader);
369
- if (typeof sourceText === 'string') {
370
- sourceTextCache.set(key, sourceText);
371
- return sourceText;
372
- }
373
-
374
- if (typeof loader === 'function' && !sourceLoadPromises.has(key)) {
375
- let loadedSource;
376
- try {
377
- loadedSource = loader();
378
- } catch (error) {
379
- loadedSource = Promise.reject(error);
380
- }
381
-
382
- const sourceLoad = Promise.resolve(loadedSource)
383
- .then((loaded) => {
384
- const loadedText = normalizeSourceText(loaded);
385
- if (typeof loadedText === 'string') {
386
- sourceTextCache.set(key, loadedText);
387
- }
388
- return loadedText;
389
- })
390
- .catch((error) => {
391
- console.error(\`source(): failed to load asset \${key}\`, error);
392
- return undefined;
393
- })
394
- .finally(() => {
395
- sourceLoadPromises.delete(key);
396
- });
397
-
398
- sourceLoadPromises.set(key, sourceLoad);
399
- }
400
-
401
- return undefined;
402
- };
285
+ const assetSourceRuntime = createAssetSourceRuntime({
286
+ assets,
287
+ assetRootPrefixes,
288
+ generatedAssetRootPrefixes,
289
+ generatedAssetAliases,
290
+ });
291
+
292
+ export const coversAssetPath = assetSourceRuntime.coversAssetPath;
293
+ export const hasAssetText = assetSourceRuntime.hasAssetText;
294
+ export const isAssetTextLoading = assetSourceRuntime.isAssetTextLoading;
295
+ export const whenAssetTextLoaded = assetSourceRuntime.whenAssetTextLoaded;
296
+ export const getAssetText = assetSourceRuntime.getAssetText;
403
297
  `;
404
298
  }
405
299
 
@@ -416,6 +310,9 @@ export function virtualTwigAssetSourcesPlugin(env) {
416
310
  if (id === VIRTUAL_TWIG_ASSET_SOURCES_ID) {
417
311
  return RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID;
418
312
  }
313
+ if (id === VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID) {
314
+ return RESOLVED_VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID;
315
+ }
419
316
 
420
317
  return null;
421
318
  },
@@ -423,6 +320,10 @@ export function virtualTwigAssetSourcesPlugin(env) {
423
320
  if (id === RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID) {
424
321
  return generateVirtualTwigAssetSourcesModule(env);
425
322
  }
323
+ if (id === RESOLVED_VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID) {
324
+ this.addWatchFile(ASSET_SOURCE_RUNTIME_PATH);
325
+ return readFileSync(ASSET_SOURCE_RUNTIME_PATH, 'utf8');
326
+ }
426
327
 
427
328
  return null;
428
329
  },
@@ -5,8 +5,8 @@
5
5
  * virtual module instead of replacing placeholder strings in resolver source.
6
6
  */
7
7
 
8
- import { toPosixPath } from '../utils/paths.js';
9
- import { unique } from '../utils/unique.js';
8
+ import { unique } from '../../../../src/extensions/shared/lists.js';
9
+ import { toRootRelativePath } from '../../../../src/extensions/shared/root-relative.js';
10
10
 
11
11
  export const VIRTUAL_TWIG_GLOBS_ID = 'virtual:emulsify-twig-globs';
12
12
  const RESOLVED_VIRTUAL_TWIG_GLOBS_ID = `\0${VIRTUAL_TWIG_GLOBS_ID}`;
@@ -17,35 +17,6 @@ const RESOLVED_VIRTUAL_TWIG_GLOBS_ID = `\0${VIRTUAL_TWIG_GLOBS_ID}`;
17
17
  * @property {Record<string, Function>} sources - Lazy raw Twig source loaders.
18
18
  */
19
19
 
20
- /**
21
- * Convert an absolute project path to a Vite root-relative glob base.
22
- *
23
- * @param {string} projectDir - Absolute project root.
24
- * @param {string} absolutePath - Absolute Twig root path.
25
- * @returns {string} Vite root-relative path.
26
- */
27
- function toRootRelativePath(projectDir, absolutePath) {
28
- if (!absolutePath) return '';
29
-
30
- const normalizedProjectDir = toPosixPath(projectDir || '').replace(
31
- /\/+$/,
32
- '',
33
- );
34
- const normalizedPath = toPosixPath(absolutePath).replace(/\/+$/, '');
35
-
36
- if (
37
- normalizedProjectDir &&
38
- normalizedPath.startsWith(`${normalizedProjectDir}/`)
39
- ) {
40
- return `/${normalizedPath.slice(normalizedProjectDir.length + 1)}`.replace(
41
- /\/{2,}/g,
42
- '/',
43
- );
44
- }
45
-
46
- return `/${normalizedPath.replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
47
- }
48
-
49
20
  /**
50
21
  * Build Vite glob patterns from a resolved Emulsify environment.
51
22
  *
@@ -85,7 +56,7 @@ export function generateVirtualTwigGlobsModule(env) {
85
56
  : '';
86
57
 
87
58
  return `/**
88
- * Virtual module generated by config/vite/plugins/virtual-twig-globs.js.
59
+ * Virtual module generated by config/vite/plugins/twig/virtual-twig-globs.js.
89
60
  *
90
61
  * Compiled Twig modules stay eager for synchronous story rendering. Raw source
91
62
  * entries stay lazy and load only when Twig source() requests a template.
@@ -8,9 +8,9 @@
8
8
  import twig from '@vituum/vite-plugin-twig';
9
9
  import Twig from 'twig';
10
10
 
11
- import { registerTwigExtensions } from '../../../src/extensions/twig/index.js';
11
+ import { registerTwigExtensions } from '../../../../src/extensions/twig/index.js';
12
12
  import { makeTwigPluginOptions } from './twig-module.js';
13
- import { registerConfiguredTwigExtensions } from '../twig-extensions.js';
13
+ import { installProjectTwigExtensions } from './extensions.js';
14
14
 
15
15
  const EXPECTED_PLUGIN_NAMES = [
16
16
  '@vituum/vite-plugin-core:bundle',
@@ -140,7 +140,7 @@ function stripExpectedHooks(pluginOption) {
140
140
  */
141
141
  export function makeTwigPlugins(env, options = makeTwigPluginOptions(env)) {
142
142
  registerTwigExtensions(Twig);
143
- registerConfiguredTwigExtensions(Twig, options);
143
+ installProjectTwigExtensions(Twig, options);
144
144
 
145
145
  const twigPlugins = twig(options);
146
146
  const normalizedPlugins = Array.isArray(twigPlugins)
@@ -9,4 +9,4 @@ export { makePlugins } from './plugins/index.js';
9
9
  export {
10
10
  makeTwigNamespaces,
11
11
  makeTwigPluginOptions,
12
- } from './plugins/twig-module.js';
12
+ } from './plugins/twig/twig-module.js';
@@ -11,7 +11,7 @@ import { normalize, resolve, sep } from 'path';
11
11
  import { getPlatformAdapter, normalizePlatformName } from './platforms.js';
12
12
  import { resolveProjectStructure } from './project-structure.js';
13
13
  import { safeExists, safeReadJson } from './utils/fs-safe.js';
14
- import { unique } from './utils/unique.js';
14
+ import { unique } from '../../src/extensions/shared/lists.js';
15
15
 
16
16
  /**
17
17
  * Cache normalized project config by project root and relevant env signature.
@@ -10,7 +10,7 @@
10
10
  import { basename, normalize, relative, resolve, sep } from 'path';
11
11
  import { safeExists } from './utils/fs-safe.js';
12
12
  import { replaceLastSlash, toPosixPath } from './utils/paths.js';
13
- import { unique } from './utils/unique.js';
13
+ import { unique } from '../../src/extensions/shared/lists.js';
14
14
 
15
15
  export { replaceLastSlash, toPosixPath };
16
16
 
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file Tiny Map-like LRU cache helper.
3
+ */
4
+
5
+ /**
6
+ * Create a bounded insertion-order LRU cache.
7
+ *
8
+ * @template K,V
9
+ * @param {number} maxEntries - Maximum number of entries to retain.
10
+ * @returns {{
11
+ * readonly size: number,
12
+ * has(key: K): boolean,
13
+ * get(key: K): V|undefined,
14
+ * set(key: K, value: V): object,
15
+ * delete(key: K): boolean,
16
+ * clear(): void,
17
+ * entries(): IterableIterator<[K, V]>,
18
+ * keys(): IterableIterator<K>,
19
+ * values(): IterableIterator<V>,
20
+ * [Symbol.iterator](): IterableIterator<[K, V]>
21
+ * }} Map-like LRU cache.
22
+ */
23
+ export function createLruCache(maxEntries) {
24
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
25
+ throw new RangeError('LRU cache size must be a positive integer.');
26
+ }
27
+
28
+ const entries = new Map();
29
+ const cache = {
30
+ get size() {
31
+ return entries.size;
32
+ },
33
+ has(key) {
34
+ return entries.has(key);
35
+ },
36
+ get(key) {
37
+ if (!entries.has(key)) return undefined;
38
+
39
+ const value = entries.get(key);
40
+ entries.delete(key);
41
+ entries.set(key, value);
42
+ return value;
43
+ },
44
+ set(key, value) {
45
+ if (entries.has(key)) {
46
+ entries.delete(key);
47
+ }
48
+ entries.set(key, value);
49
+
50
+ while (entries.size > maxEntries) {
51
+ entries.delete(entries.keys().next().value);
52
+ }
53
+
54
+ return cache;
55
+ },
56
+ delete(key) {
57
+ return entries.delete(key);
58
+ },
59
+ clear() {
60
+ entries.clear();
61
+ },
62
+ entries() {
63
+ return entries.entries();
64
+ },
65
+ keys() {
66
+ return entries.keys();
67
+ },
68
+ values() {
69
+ return entries.values();
70
+ },
71
+ [Symbol.iterator]() {
72
+ return entries[Symbol.iterator]();
73
+ },
74
+ };
75
+
76
+ return cache;
77
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file Emulsify Core package version resolution.
3
+ *
4
+ * Shared by the component mirror plugin, which stamps the version into its
5
+ * state marker, and the develop reporter, which prints it in the banner.
6
+ */
7
+
8
+ import { join } from 'path';
9
+
10
+ import { safeReadJson } from './fs-safe.js';
11
+
12
+ /**
13
+ * Resolve the installed Core package version.
14
+ *
15
+ * `import.meta.url` is deliberately avoided so Jest's CommonJS transform can
16
+ * load callers of this module without extra plumbing. Candidates are ordered
17
+ * from most to least specific: the consuming project's installed copy, the
18
+ * current working directory's installed copy, then the working directory
19
+ * itself for the case where Core is building its own source tree.
20
+ *
21
+ * @param {string} [projectDir] - Project directory running the build.
22
+ * @returns {string} Emulsify Core package version, or `0.0.0` when unknown.
23
+ */
24
+ export function resolvePackageVersion(projectDir) {
25
+ const candidates = [
26
+ projectDir && join(projectDir, 'node_modules/@emulsify/core/package.json'),
27
+ join(process.cwd(), 'node_modules/@emulsify/core/package.json'),
28
+ join(process.cwd(), 'package.json'),
29
+ ].filter(Boolean);
30
+
31
+ for (const candidate of candidates) {
32
+ const candidatePackage = safeReadJson(candidate).data;
33
+ if (
34
+ candidatePackage?.name === '@emulsify/core' &&
35
+ candidatePackage.version
36
+ ) {
37
+ return candidatePackage.version;
38
+ }
39
+ }
40
+
41
+ return '0.0.0';
42
+ }
@@ -14,18 +14,10 @@ import { sep } from 'path';
14
14
  * @param {string} filePath - Filesystem path.
15
15
  * @returns {string} Path using forward slashes.
16
16
  */
17
- export function toPosix(filePath) {
17
+ export function toPosixPath(filePath) {
18
18
  return filePath.split(sep).join('/').replace(/\\/g, '/');
19
19
  }
20
20
 
21
- /**
22
- * Normalize a filesystem path to POSIX separators.
23
- *
24
- * @param {string} filePath - Filesystem path.
25
- * @returns {string} Path using forward slashes.
26
- */
27
- export const toPosixPath = toPosix;
28
-
29
21
  /**
30
22
  * Replace the final slash in a POSIX path with a custom segment.
31
23
  *
@@ -2,7 +2,7 @@
2
2
  * @file React singleton helpers for Storybook and Vite config.
3
3
  */
4
4
 
5
- import { unique } from './unique.js';
5
+ import { unique } from '../../../src/extensions/shared/lists.js';
6
6
 
7
7
  /**
8
8
  * React modules that must resolve from the consumer project root.
@@ -13,17 +13,32 @@
13
13
  * parts of it by returning a patch object from `extendConfig(...)`.
14
14
  *
15
15
  * Notes:
16
- * - CSS & JS sourcemaps are enabled.
16
+ * - JS sourcemaps come from `build.sourcemap`. Extracted CSS gets no map from
17
+ * `vite build`: `vite:css-post` emits CSS through
18
+ * `this.emitFile({ type: 'asset' })`, Rollup/Rolldown assets carry no map,
19
+ * and `finalizeCss()` -> `minifyCSS()` returns code only. To trace a rule
20
+ * back to its `.scss` partial, let Vite compile the SCSS in Storybook: set
21
+ * `parameters.emulsify.loadAllCSS = false` in
22
+ * `config/emulsify-core/storybook/preview.js` and import the SCSS entry
23
+ * there, so `css.devSourcemap` can chain the map to source. Loading the
24
+ * compiled CSS instead yields an identity map whose only source is the
25
+ * compiled `.css` file.
26
+ * - CSS is left unminified during `vite build --watch` so the develop loop
27
+ * stays readable; one-shot builds keep minification.
17
28
  * - CSS assets keep their path and drop the internal `__style` suffix if present.
18
29
  */
19
30
 
20
- import { defineConfig, mergeConfig } from 'vite';
31
+ import { createLogger, defineConfig, mergeConfig } from 'vite';
21
32
  import path from 'node:path';
22
33
 
23
34
  import { resolveEnvironment } from './environment.js';
24
35
  import { makePlugins } from './plugins.js';
25
36
  import { buildInputs } from './entries.js';
26
- import { createSourceFileIndex } from './plugins/source-file-index.js';
37
+ import { createSourceFileIndex } from './plugins/assets/source-file-index.js';
38
+ import { createDiagnosticsCollector } from './plugins/reporter/diagnostics.js';
39
+ import { createSassOptions } from './plugins/reporter/sass-logger.js';
40
+ import { createReporterLogger } from './plugins/reporter/vite-logger.js';
41
+ import { isWatchInvocation } from './plugins/reporter/watch-mode.js';
27
42
  import { loadProjectExtensions } from './project-extensions.js';
28
43
  import { mergeReactSingletonResolve } from './utils/react-singleton.js';
29
44
 
@@ -44,7 +59,14 @@ export default defineConfig(async () => {
44
59
  /** @type {EmulsifyEnv} */
45
60
  const env = resolveEnvironment();
46
61
  const sourceFileIndex = createSourceFileIndex(env.projectStructure);
47
- const envWithSourceFileIndex = { ...env, sourceFileIndex };
62
+
63
+ // The develop reporter takes over output only for `vite build --watch`, the
64
+ // watcher `npm run develop` runs. One-shot builds, Storybook, and the release
65
+ // fixture verifications keep their existing output untouched, so no warning
66
+ // is ever collected without also being reported.
67
+ const watching = isWatchInvocation();
68
+ const diagnostics = watching ? createDiagnosticsCollector() : undefined;
69
+ const envWithSourceFileIndex = { ...env, sourceFileIndex, diagnostics };
48
70
 
49
71
  // Build the Rollup/Vite entry map: keys encode output paths, values source files.
50
72
  /** @type {Record<string, string>} */
@@ -79,12 +101,29 @@ export default defineConfig(async () => {
79
101
  // Core plugin set + project-provided plugins (if any).
80
102
  plugins: [...makePlugins(envWithSourceFileIndex), ...projectPlugins],
81
103
 
104
+ // Route Vite's own diagnostics through the reporter during a watch build so
105
+ // unresolved CSS asset URLs are summarized rather than printed mid-build.
106
+ ...(diagnostics
107
+ ? { customLogger: createReporterLogger(diagnostics, createLogger()) }
108
+ : {}),
109
+
82
110
  // Keep React-based story helpers on the consumer project's React singleton.
83
111
  resolve: mergeReactSingletonResolve(),
84
112
 
85
113
  // Generate CSS sourcemaps in dev; JS sourcemaps are set in `build.sourcemap`.
114
+ // These map only what Vite itself compiles. A preview that imports
115
+ // already-compiled CSS gets an identity map pointing at that `.css` file,
116
+ // so import SCSS entries when styles need to resolve to their partials.
86
117
  css: {
87
118
  devSourcemap: true,
119
+
120
+ // During a watch build, route Sass warnings into the diagnostics
121
+ // collector instead of letting Dart Sass print a formatted block per
122
+ // occurrence. The reporter prints one deduplicated tally per cycle, so
123
+ // the deprecation debt stays visible without the repetition.
124
+ ...(diagnostics
125
+ ? { preprocessorOptions: { scss: createSassOptions(diagnostics) } }
126
+ : {}),
88
127
  },
89
128
 
90
129
  build: {
@@ -94,9 +133,16 @@ export default defineConfig(async () => {
94
133
  // All outputs are written into ./dist/
95
134
  outDir: 'dist/',
96
135
 
97
- // Emit production sourcemaps as well.
136
+ // Emit JS sourcemaps. Extracted CSS is not covered; see the file header.
98
137
  sourcemap: true,
99
138
 
139
+ // Vite cannot map extracted CSS, so during `vite build --watch` the
140
+ // readable stylesheet is the debugging aid: keep it unminified so
141
+ // devtools shows one declaration per line instead of a single long line.
142
+ // One-shot `vite build`, `storybook build`, and the release fixture
143
+ // verifications still minify, so nothing a platform ships changes.
144
+ cssMinify: !watching,
145
+
100
146
  rollupOptions: {
101
147
  // Multi-entry input map constructed above.
102
148
  input: entries,