@emulsify/core 4.2.1 → 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.
- package/.storybook/main-vite.js +91 -36
- package/README.md +185 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
- package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
- package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
- package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
- package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
- package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
- package/config/vite/plugins/index.js +23 -14
- package/config/vite/plugins/reporter/asset-resolver.js +541 -0
- package/config/vite/plugins/reporter/build-errors.js +284 -0
- package/config/vite/plugins/reporter/diagnostics.js +366 -0
- package/config/vite/plugins/reporter/format.js +199 -0
- package/config/vite/plugins/reporter/index.js +308 -0
- package/config/vite/plugins/reporter/render.js +758 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/vite-logger.js +188 -0
- package/config/vite/plugins/reporter/watch-mode.js +40 -0
- package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
- package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
- package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +197 -85
- package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
- package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
- package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
- package/config/vite/plugins.js +1 -1
- package/config/vite/project-config.js +1 -1
- package/config/vite/project-structure.js +1 -1
- package/config/vite/utils/lru.js +77 -0
- package/config/vite/utils/package-version.js +42 -0
- package/config/vite/utils/paths.js +1 -9
- package/config/vite/utils/react-singleton.js +1 -1
- package/config/vite/vite.config.js +51 -5
- package/package.json +93 -58
- package/scripts/a11y.js +115 -23
- package/scripts/audit/checks/core-imports.js +78 -0
- package/scripts/audit/checks/css-asset-references.js +99 -0
- package/scripts/audit/checks/drupal-assumptions.js +48 -0
- package/scripts/audit/checks/files-outside-roots.js +53 -0
- package/scripts/audit/checks/generated-package-scripts.js +113 -0
- package/scripts/audit/checks/legacy-twig-stories.js +33 -0
- package/scripts/audit/checks/package-overrides.js +91 -0
- package/scripts/audit/checks/project-config.js +71 -0
- package/scripts/audit/checks/story-discovery.js +35 -0
- package/scripts/audit/checks/twig-references.js +69 -0
- package/scripts/audit/checks/twig-volume.js +54 -0
- package/scripts/audit/checks/webpack-patterns.js +86 -0
- package/scripts/audit/index.js +177 -0
- package/scripts/audit/lib/css.js +165 -0
- package/scripts/audit/lib/files.js +168 -0
- package/scripts/audit/lib/findings.js +31 -0
- package/scripts/audit/lib/package-json.js +65 -0
- package/scripts/audit/lib/twig.js +227 -0
- package/scripts/audit/report.js +273 -0
- package/scripts/audit-twig-stories.js +115 -78
- package/scripts/audit.js +150 -1632
- package/scripts/check-node-version.js +136 -10
- package/scripts/inspect-components.js +456 -0
- package/scripts/lib/cli.js +179 -0
- package/scripts/lib/fs.js +31 -0
- package/scripts/lib/proc.js +78 -0
- package/scripts/lib/text.js +14 -0
- package/scripts/loadYaml.js +2 -2
- package/src/extensions/shared/attributes.js +3 -3
- package/src/extensions/shared/lists.js +2 -6
- package/src/extensions/shared/root-relative.js +38 -0
- package/src/storybook/index.js +4 -0
- package/src/storybook/render-twig.js +1 -1
- package/src/storybook/render-web-component.js +459 -0
- package/src/storybook/twig/asset-source-runtime.js +193 -0
- package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
- package/src/storybook/twig/reference-paths.js +2 -13
- package/src/storybook/twig/setup.js +12 -2
- package/src/storybook/twig/source-function.js +5 -2
- package/config/vite/utils/unique.js +0 -36
- package/src/storybook/twig/include.js +0 -28
- package/src/storybook/twig/source-events.js +0 -5
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Presentation helpers for the Emulsify develop reporter.
|
|
3
|
+
*
|
|
4
|
+
* `npm run develop` runs Vite and Storybook under `concurrently`, which means
|
|
5
|
+
* this process writes to a pipe rather than a terminal. Two consequences shape
|
|
6
|
+
* everything here:
|
|
7
|
+
*
|
|
8
|
+
* 1. Cursor control is unavailable. Anything that rewrites a line in place
|
|
9
|
+
* degrades into concatenated garbage — that is the cause of the
|
|
10
|
+
* `pages.scssDeprecation Warning` collisions in the default output. Every
|
|
11
|
+
* helper below produces complete, append-only lines.
|
|
12
|
+
* 2. Color is auto-disabled by Node when the stream is not a TTY. Since the
|
|
13
|
+
* pipe is an implementation detail of the task runner rather than a signal
|
|
14
|
+
* that the human cannot see color, color support is resolved explicitly
|
|
15
|
+
* from the environment and applied with stream validation turned off.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { styleText } from 'node:util';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Human-facing names for platforms whose casing is not a simple capitalization.
|
|
22
|
+
*
|
|
23
|
+
* @type {Record<string, string>}
|
|
24
|
+
*/
|
|
25
|
+
const PLATFORM_LABELS = {
|
|
26
|
+
drupal: 'Drupal',
|
|
27
|
+
wordpress: 'WordPress',
|
|
28
|
+
none: 'none',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Status glyphs. Deliberately ASCII-adjacent so they survive any terminal font.
|
|
33
|
+
*
|
|
34
|
+
* @type {Record<string, string>}
|
|
35
|
+
*/
|
|
36
|
+
export const SYMBOLS = {
|
|
37
|
+
ok: '\u2713',
|
|
38
|
+
error: '\u2717',
|
|
39
|
+
warning: '!',
|
|
40
|
+
change: '~',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Decide whether ANSI color should be emitted.
|
|
45
|
+
*
|
|
46
|
+
* Honors the `NO_COLOR` and `FORCE_COLOR` conventions before falling back to
|
|
47
|
+
* TTY detection, so piping through `concurrently` does not silently strip the
|
|
48
|
+
* formatting that makes the summary scannable.
|
|
49
|
+
*
|
|
50
|
+
* @param {{NO_COLOR?: string, FORCE_COLOR?: string}} [env] - Environment variables.
|
|
51
|
+
* @param {{isTTY?: boolean}} [stream] - Destination stream.
|
|
52
|
+
* @returns {boolean} TRUE when color should be emitted.
|
|
53
|
+
* @see https://no-color.org/
|
|
54
|
+
*/
|
|
55
|
+
export function supportsColor(env = process.env, stream = process.stdout) {
|
|
56
|
+
if (env.NO_COLOR) return false;
|
|
57
|
+
if (env.FORCE_COLOR !== undefined) return env.FORCE_COLOR !== '0';
|
|
58
|
+
return Boolean(stream?.isTTY);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether block-drawing characters will render correctly.
|
|
63
|
+
*
|
|
64
|
+
* The banner wordmark is built from half-block glyphs. On a terminal without a
|
|
65
|
+
* UTF-8 locale those degrade into replacement characters, which looks broken
|
|
66
|
+
* rather than branded, so the reporter falls back to plain text instead.
|
|
67
|
+
*
|
|
68
|
+
* Windows consoles are excluded unless running under Windows Terminal or the
|
|
69
|
+
* VS Code integrated terminal, which are the two that handle these glyphs
|
|
70
|
+
* reliably.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} [env] - Environment variables.
|
|
73
|
+
* @param {string} [platform] - Node platform identifier.
|
|
74
|
+
* @returns {boolean} TRUE when block glyphs are safe to emit.
|
|
75
|
+
*/
|
|
76
|
+
export function supportsUnicode(
|
|
77
|
+
env = process.env,
|
|
78
|
+
platform = process.platform,
|
|
79
|
+
) {
|
|
80
|
+
if (env.EMULSIFY_NO_UNICODE) return false;
|
|
81
|
+
|
|
82
|
+
if (platform === 'win32') {
|
|
83
|
+
return Boolean(env.WT_SESSION) || env.TERM_PROGRAM === 'vscode';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (env.TERM === 'linux' || env.TERM === 'dumb') return false;
|
|
87
|
+
|
|
88
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || '';
|
|
89
|
+
return /UTF-?8$/i.test(locale);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a styling function that respects the resolved color support.
|
|
94
|
+
*
|
|
95
|
+
* @param {boolean} enabled - Whether color is enabled.
|
|
96
|
+
* @returns {(format: string|string[], text: string) => string} Styling function.
|
|
97
|
+
*/
|
|
98
|
+
export function createStyler(enabled) {
|
|
99
|
+
return (format, text) => {
|
|
100
|
+
if (!enabled || !format) return text;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return styleText(format, text, { validateStream: false });
|
|
104
|
+
} catch {
|
|
105
|
+
// An unrecognized format name should never break a build.
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Render a platform identifier as a display label.
|
|
113
|
+
*
|
|
114
|
+
* @param {string|undefined} platform - Platform identifier.
|
|
115
|
+
* @returns {string} Display label.
|
|
116
|
+
*/
|
|
117
|
+
export function platformLabel(platform) {
|
|
118
|
+
if (!platform) return 'none';
|
|
119
|
+
const normalized = String(platform).toLowerCase();
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
PLATFORM_LABELS[normalized] ||
|
|
123
|
+
normalized.charAt(0).toUpperCase() + normalized.slice(1)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format a duration for display.
|
|
129
|
+
*
|
|
130
|
+
* @param {number} milliseconds - Elapsed milliseconds.
|
|
131
|
+
* @returns {string} Formatted duration.
|
|
132
|
+
*/
|
|
133
|
+
export function formatDuration(milliseconds) {
|
|
134
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 0) return '0ms';
|
|
135
|
+
if (milliseconds < 1000) return `${Math.round(milliseconds)}ms`;
|
|
136
|
+
return `${(milliseconds / 1000).toFixed(2)}s`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format a wall-clock timestamp for rebuild lines.
|
|
141
|
+
*
|
|
142
|
+
* @param {Date} [date] - Date to format.
|
|
143
|
+
* @returns {string} `HH:MM:SS` timestamp.
|
|
144
|
+
*/
|
|
145
|
+
export function formatClockTime(date = new Date()) {
|
|
146
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
147
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Shorten an absolute path for display relative to the project root.
|
|
152
|
+
*
|
|
153
|
+
* Paths outside the project — most often inside `node_modules` — keep only
|
|
154
|
+
* their trailing segments so the line stays readable.
|
|
155
|
+
*
|
|
156
|
+
* @param {string|undefined} filePath - Path to shorten.
|
|
157
|
+
* @param {string} [projectDir] - Project root.
|
|
158
|
+
* @returns {string} Display path.
|
|
159
|
+
*/
|
|
160
|
+
export function displayPath(filePath, projectDir) {
|
|
161
|
+
if (!filePath) return '<unknown>';
|
|
162
|
+
|
|
163
|
+
const normalized = filePath.split('\\').join('/');
|
|
164
|
+
const root = projectDir
|
|
165
|
+
? projectDir.split('\\').join('/').replace(/\/$/, '')
|
|
166
|
+
: '';
|
|
167
|
+
|
|
168
|
+
if (root && normalized.startsWith(`${root}/`)) {
|
|
169
|
+
return normalized.slice(root.length + 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const segments = normalized.split('/');
|
|
173
|
+
return segments.length > 3 ? segments.slice(-3).join('/') : normalized;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Append a line reference to a display path when the line is known.
|
|
178
|
+
*
|
|
179
|
+
* @param {string|undefined} filePath - Path to render.
|
|
180
|
+
* @param {number|undefined} line - 1-based line number.
|
|
181
|
+
* @param {string} [projectDir] - Project root.
|
|
182
|
+
* @returns {string} Path with optional line suffix.
|
|
183
|
+
*/
|
|
184
|
+
export function displayLocation(filePath, line, projectDir) {
|
|
185
|
+
const base = displayPath(filePath, projectDir);
|
|
186
|
+
return line == null ? base : `${base}:${line}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pluralize a noun against a count.
|
|
191
|
+
*
|
|
192
|
+
* @param {number} count - Item count.
|
|
193
|
+
* @param {string} singular - Singular noun.
|
|
194
|
+
* @param {string} [plural] - Explicit plural form.
|
|
195
|
+
* @returns {string} Count and correctly inflected noun.
|
|
196
|
+
*/
|
|
197
|
+
export function pluralize(count, singular, plural = `${singular}s`) {
|
|
198
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
199
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Develop reporter plugin for Emulsify.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the interleaved Vite/Sass output of `npm run develop` with a short
|
|
5
|
+
* banner, one summary per build cycle, and a single line per rebuild.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this plugin is scoped so narrowly
|
|
8
|
+
*
|
|
9
|
+
* `makePlugins()` is consumed twice during `npm run develop`: once by the Vite
|
|
10
|
+
* watcher, and once by Storybook, whose `viteFinal` spreads the shared plugin
|
|
11
|
+
* array into its own config. A reporter that printed unconditionally would emit
|
|
12
|
+
* its banner from both processes.
|
|
13
|
+
*
|
|
14
|
+
* Two guards prevent that:
|
|
15
|
+
*
|
|
16
|
+
* - `apply: 'build'` excludes Storybook's dev server, which resolves with
|
|
17
|
+
* `command: 'serve'`. This mirrors the existing guard on the component
|
|
18
|
+
* mirror plugin.
|
|
19
|
+
* - A `config.build.watch` check excludes `storybook build`, `npm run build`,
|
|
20
|
+
* and every fixture verification, all of which run a one-shot production
|
|
21
|
+
* build where `--watch` is absent. Those commands keep their current output
|
|
22
|
+
* byte for byte.
|
|
23
|
+
*
|
|
24
|
+
* The net effect is that the reporter only speaks for the long-running watcher
|
|
25
|
+
* started by `develop`, and Storybook keeps printing its own ready box.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { join, relative } from 'node:path';
|
|
29
|
+
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
buildAssetRows,
|
|
34
|
+
buildImportRows,
|
|
35
|
+
createAssetResolver,
|
|
36
|
+
findLikelySource,
|
|
37
|
+
sharedMissingDirectory,
|
|
38
|
+
} from './asset-resolver.js';
|
|
39
|
+
import { classifyBuildError } from './build-errors.js';
|
|
40
|
+
import { renderBanner, renderRebuild, renderSummary } from './render.js';
|
|
41
|
+
import { createStyler, supportsColor, supportsUnicode } from './format.js';
|
|
42
|
+
import { resolvePackageVersion } from '../../utils/package-version.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the stylesheet glob handed to `sass-migrator`.
|
|
46
|
+
*
|
|
47
|
+
* The migrator takes entrypoints and the docs recommend passing every
|
|
48
|
+
* stylesheet in the package, so a glob rooted at the project's source
|
|
49
|
+
* directory is both valid and copy-pasteable. Falling back to `src` keeps the
|
|
50
|
+
* command sensible when the source directory cannot be resolved.
|
|
51
|
+
*
|
|
52
|
+
* @param {{projectDir?: string, srcDir?: string}} env - Project environment.
|
|
53
|
+
* @returns {string} Glob relative to the project root.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSourceGlob({ projectDir, srcDir } = {}) {
|
|
56
|
+
if (!projectDir || !srcDir) return 'src/**/*.scss';
|
|
57
|
+
|
|
58
|
+
const relativeSrc = relative(projectDir, srcDir).split('\\').join('/');
|
|
59
|
+
if (!relativeSrc || relativeSrc.startsWith('..')) return 'src/**/*.scss';
|
|
60
|
+
|
|
61
|
+
return `${relativeSrc}/**/*.scss`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Count the entries in a resolved Rollup input option.
|
|
66
|
+
*
|
|
67
|
+
* @param {string|string[]|Record<string, string>|undefined} input - Rollup input.
|
|
68
|
+
* @returns {number|undefined} Entry count, when determinable.
|
|
69
|
+
*/
|
|
70
|
+
export function countEntries(input) {
|
|
71
|
+
if (!input) return undefined;
|
|
72
|
+
if (typeof input === 'string') return 1;
|
|
73
|
+
if (Array.isArray(input)) return input.length;
|
|
74
|
+
if (typeof input === 'object') return Object.keys(input).length;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create the Emulsify develop reporter plugin.
|
|
80
|
+
*
|
|
81
|
+
* @param {{
|
|
82
|
+
* env: {projectDir?: string, platform?: string},
|
|
83
|
+
* diagnostics: ReturnType<import('./diagnostics.js').createDiagnosticsCollector>,
|
|
84
|
+
* write?: (line: string) => void,
|
|
85
|
+
* now?: () => number,
|
|
86
|
+
* clock?: () => Date,
|
|
87
|
+
* colorEnabled?: boolean,
|
|
88
|
+
* version?: string
|
|
89
|
+
* }} options - Plugin options.
|
|
90
|
+
* @returns {import('vite').PluginOption} Develop reporter plugin.
|
|
91
|
+
*/
|
|
92
|
+
export function developReporterPlugin({
|
|
93
|
+
env = {},
|
|
94
|
+
diagnostics,
|
|
95
|
+
write = (line) => process.stdout.write(`${line}\n`),
|
|
96
|
+
now = () => Date.now(),
|
|
97
|
+
clock = () => new Date(),
|
|
98
|
+
colorEnabled,
|
|
99
|
+
unicodeEnabled,
|
|
100
|
+
version,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const styler = createStyler(
|
|
103
|
+
colorEnabled === undefined ? supportsColor() : colorEnabled,
|
|
104
|
+
);
|
|
105
|
+
const unicode =
|
|
106
|
+
unicodeEnabled === undefined ? supportsUnicode() : unicodeEnabled;
|
|
107
|
+
|
|
108
|
+
let watching = false;
|
|
109
|
+
let outDir = 'dist';
|
|
110
|
+
let entryCount;
|
|
111
|
+
let cycleStart = 0;
|
|
112
|
+
let cyclePrinted = true;
|
|
113
|
+
let firstCycleComplete = false;
|
|
114
|
+
let changedFiles = [];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write an array of finished lines to the destination stream.
|
|
118
|
+
*
|
|
119
|
+
* @param {string[]} lines - Lines to emit.
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
const emit = (lines) => lines.forEach((line) => write(line));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Unpack a build failure and record everything it contains.
|
|
126
|
+
*
|
|
127
|
+
* Rolldown wraps every failure of a cycle into one error whose message is
|
|
128
|
+
* just a count, so it has to be opened up before anything useful can be
|
|
129
|
+
* reported. Shared by the build and generate phases, which fail through
|
|
130
|
+
* different hooks but produce the same shapes.
|
|
131
|
+
*
|
|
132
|
+
* @param {Error} error - Build or render error.
|
|
133
|
+
* @returns {void}
|
|
134
|
+
*/
|
|
135
|
+
const captureFailure = (error) => {
|
|
136
|
+
const { importErrors, syntaxErrors, otherErrors } =
|
|
137
|
+
classifyBuildError(error);
|
|
138
|
+
|
|
139
|
+
for (const importError of importErrors) {
|
|
140
|
+
diagnostics.recordImportError(importError);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const syntaxError of syntaxErrors) {
|
|
144
|
+
diagnostics.recordSyntaxError(syntaxError);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const detailed = importErrors.length > 0 || syntaxErrors.length > 0;
|
|
148
|
+
|
|
149
|
+
for (const other of otherErrors) {
|
|
150
|
+
// The aggregate wrapper carries no location and would only add a row
|
|
151
|
+
// reading "Build failed with N errors" above the detail it wraps.
|
|
152
|
+
if (detailed && /^Build failed with \d+ error/.test(other.message)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
diagnostics.recordError(other);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Print the summary or rebuild line for the cycle that just finished.
|
|
161
|
+
*
|
|
162
|
+
* Guarded so the cycle reports exactly once regardless of whether Rollup
|
|
163
|
+
* reaches `writeBundle`, `closeBundle`, or neither after a failure.
|
|
164
|
+
*
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
const reportCycle = () => {
|
|
168
|
+
if (!watching || cyclePrinted) return;
|
|
169
|
+
cyclePrinted = true;
|
|
170
|
+
|
|
171
|
+
const snapshot = diagnostics.snapshot();
|
|
172
|
+
const durationMs = now() - cycleStart;
|
|
173
|
+
|
|
174
|
+
if (firstCycleComplete) {
|
|
175
|
+
emit(
|
|
176
|
+
renderRebuild({
|
|
177
|
+
snapshot,
|
|
178
|
+
durationMs,
|
|
179
|
+
changedFiles,
|
|
180
|
+
projectDir: env.projectDir,
|
|
181
|
+
styler,
|
|
182
|
+
now: clock(),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
firstCycleComplete = true;
|
|
187
|
+
// Enriching costs a filesystem read per stylesheet, so it only runs when
|
|
188
|
+
// the build actually produced unresolved URLs. A resolver is built per
|
|
189
|
+
// cycle so its read cache never serves stale contents after an edit.
|
|
190
|
+
const needsResolver =
|
|
191
|
+
snapshot.unresolvedAssets.length > 0 ||
|
|
192
|
+
snapshot.importErrors.length > 0 ||
|
|
193
|
+
snapshot.syntaxErrors.length > 0;
|
|
194
|
+
const resolver = needsResolver ? createAssetResolver(env) : undefined;
|
|
195
|
+
|
|
196
|
+
const assetRows = resolver
|
|
197
|
+
? buildAssetRows(snapshot.unresolvedAssets, resolver)
|
|
198
|
+
: [];
|
|
199
|
+
|
|
200
|
+
const importRows = resolver
|
|
201
|
+
? buildImportRows(snapshot.importErrors, resolver, env.projectDir)
|
|
202
|
+
: [];
|
|
203
|
+
const sharedDirectory = sharedMissingDirectory(
|
|
204
|
+
importRows,
|
|
205
|
+
env.projectDir,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// The minifier reports against generated CSS, so the only route back to
|
|
209
|
+
// a source file is searching for the literals in the offending rule.
|
|
210
|
+
const syntaxErrors = resolver
|
|
211
|
+
? snapshot.syntaxErrors.map((error) => ({
|
|
212
|
+
...error,
|
|
213
|
+
lead: findLikelySource(error.declaration, resolver),
|
|
214
|
+
}))
|
|
215
|
+
: snapshot.syntaxErrors;
|
|
216
|
+
|
|
217
|
+
emit(
|
|
218
|
+
renderSummary({
|
|
219
|
+
snapshot,
|
|
220
|
+
durationMs,
|
|
221
|
+
outDir,
|
|
222
|
+
projectDir: env.projectDir,
|
|
223
|
+
sourceGlob: resolveSourceGlob(env),
|
|
224
|
+
assetRows,
|
|
225
|
+
syntaxErrors,
|
|
226
|
+
importErrors: {
|
|
227
|
+
rows: importRows,
|
|
228
|
+
sharedDirectory,
|
|
229
|
+
directoryExists: Boolean(
|
|
230
|
+
sharedDirectory &&
|
|
231
|
+
env.projectDir &&
|
|
232
|
+
existsSync(join(env.projectDir, sharedDirectory)),
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
styler,
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
changedFiles = [];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
name: 'emulsify-develop-reporter',
|
|
245
|
+
|
|
246
|
+
// Storybook's dev server resolves as `serve`, so this keeps the reporter
|
|
247
|
+
// out of the Storybook process entirely.
|
|
248
|
+
apply: 'build',
|
|
249
|
+
|
|
250
|
+
// Report after the plugins that actually produce diagnostics have run.
|
|
251
|
+
enforce: 'post',
|
|
252
|
+
|
|
253
|
+
configResolved(config) {
|
|
254
|
+
watching = Boolean(config.build?.watch);
|
|
255
|
+
if (!watching) return;
|
|
256
|
+
|
|
257
|
+
outDir = config.build?.outDir || 'dist';
|
|
258
|
+
entryCount = countEntries(config.build?.rollupOptions?.input);
|
|
259
|
+
|
|
260
|
+
emit(
|
|
261
|
+
renderBanner({
|
|
262
|
+
version: version || resolvePackageVersion(env.projectDir),
|
|
263
|
+
platform: env.platform,
|
|
264
|
+
entryCount,
|
|
265
|
+
unicode,
|
|
266
|
+
styler,
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
buildStart() {
|
|
272
|
+
if (!watching) return;
|
|
273
|
+
diagnostics.reset();
|
|
274
|
+
cycleStart = now();
|
|
275
|
+
cyclePrinted = false;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
watchChange(id) {
|
|
279
|
+
if (!watching) return;
|
|
280
|
+
changedFiles.push(id);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
buildEnd(error) {
|
|
284
|
+
if (!watching || !error) return;
|
|
285
|
+
captureFailure(error);
|
|
286
|
+
// A failed cycle never reaches writeBundle, so report from here instead.
|
|
287
|
+
reportCycle();
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// Failures while generating output — CSS minification among them — occur
|
|
291
|
+
// after `buildEnd` and abort before `writeBundle`, so without this hook a
|
|
292
|
+
// broken build produced no summary at all. It also stops a later
|
|
293
|
+
// `closeBundle` from reporting success over a build that failed.
|
|
294
|
+
renderError(error) {
|
|
295
|
+
if (!watching || !error) return;
|
|
296
|
+
captureFailure(error);
|
|
297
|
+
reportCycle();
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
writeBundle() {
|
|
301
|
+
reportCycle();
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
closeBundle() {
|
|
305
|
+
reportCycle();
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|