@emulsify/core 4.3.2 → 4.4.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-static-assets.js +5 -8
- package/.storybook/main-vite.js +11 -3
- package/README.md +4 -5
- package/config/vite/entries.js +7 -2
- package/config/vite/environment.js +4 -0
- package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
- package/config/vite/plugins/assets/copy-src-assets.js +82 -12
- package/config/vite/plugins/assets/copy-twig-files.js +85 -16
- package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
- package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
- package/config/vite/plugins/assets/development-source-maps.js +273 -0
- package/config/vite/plugins/assets/mirror-components.js +98 -82
- package/config/vite/plugins/assets/output-freshness.js +235 -0
- package/config/vite/plugins/assets/source-file-index.js +7 -1
- package/config/vite/plugins/assets/stable-watch-output.js +165 -0
- package/config/vite/plugins/assets/storybook-output.js +27 -0
- package/config/vite/plugins/index.js +95 -9
- package/config/vite/plugins/reporter/asset-resolver.js +34 -6
- package/config/vite/plugins/reporter/build-errors.js +7 -3
- package/config/vite/plugins/reporter/diagnostics.js +140 -10
- package/config/vite/plugins/reporter/index.js +380 -75
- package/config/vite/plugins/reporter/render.js +297 -44
- package/config/vite/plugins/reporter/sass-logger.js +30 -0
- package/config/vite/plugins/reporter/source-roots.js +101 -21
- package/config/vite/plugins/reporter/strict-mode.js +99 -0
- package/config/vite/plugins/reporter/vite-logger.js +220 -8
- package/config/vite/plugins/reporter/watch-mode.js +6 -2
- package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
- package/config/vite/project-config.js +121 -21
- package/config/vite/project-structure.js +6 -0
- package/config/vite/utils/asset-roots.js +205 -0
- package/config/vite/utils/css-urls.js +350 -0
- package/config/vite/utils/fs-safe.js +38 -1
- package/config/vite/utils/source-maps.js +88 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +40 -29
- package/scripts/audit/checks/css-asset-references.js +256 -24
- package/scripts/audit/fix.js +836 -0
- package/scripts/audit/index.js +10 -2
- package/scripts/audit/lib/css.js +41 -35
- package/scripts/audit/lib/twig.js +11 -29
- package/scripts/audit/report.js +83 -5
- package/scripts/audit.js +87 -2
- package/src/storybook/twig/source-function.js +14 -10
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* structure using the same routing rules as compiled JS and CSS entries.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { copyFileSync, mkdirSync } from 'fs';
|
|
9
|
-
import { dirname, join } from 'path';
|
|
8
|
+
import { copyFileSync, mkdirSync, statSync } from 'fs';
|
|
9
|
+
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
copiedComponentOutputPath,
|
|
13
13
|
copiedGlobalOutputPath,
|
|
14
14
|
} from '../../project-structure.js';
|
|
15
|
+
import {
|
|
16
|
+
filesHaveSameBytes,
|
|
17
|
+
removeDestinationSymlink,
|
|
18
|
+
resolveFinalPath,
|
|
19
|
+
} from './output-freshness.js';
|
|
15
20
|
import {
|
|
16
21
|
createSourceFileIndex,
|
|
17
22
|
isComponentMetadataFile,
|
|
@@ -20,14 +25,17 @@ import {
|
|
|
20
25
|
/**
|
|
21
26
|
* Copy Twig templates and component metadata to `dist/`.
|
|
22
27
|
*
|
|
23
|
-
* @param {{ structure: object, sourceFileIndex?: object }} opts - Plugin options.
|
|
28
|
+
* @param {{ structure: object, sourceFileIndex?: object, diagnostics?: object, outputChanges?: Map<string, {kind: 'written'|'removed', bytes?: number}> }} opts - Plugin options.
|
|
24
29
|
* @returns {import('vite').PluginOption} Copy plugin.
|
|
25
30
|
*/
|
|
26
31
|
export function copyTwigFilesPlugin({
|
|
27
32
|
structure,
|
|
28
33
|
sourceFileIndex = createSourceFileIndex(structure),
|
|
34
|
+
diagnostics,
|
|
35
|
+
outputChanges,
|
|
29
36
|
}) {
|
|
30
37
|
let outDir = 'dist';
|
|
38
|
+
let projectDir = process.cwd();
|
|
31
39
|
let watching = false;
|
|
32
40
|
/** @type {Array<{absPath: string, relDest: string}>|undefined} */
|
|
33
41
|
let plan;
|
|
@@ -35,10 +43,12 @@ export function copyTwigFilesPlugin({
|
|
|
35
43
|
/**
|
|
36
44
|
* Resolve every file this plugin copies, paired with where it lands.
|
|
37
45
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
46
|
+
* Shared by both hooks, which keeps "gets copied to dist" and "a save
|
|
47
|
+
* triggers the copy" from drifting apart. Structural events for an
|
|
48
|
+
* individually watched file reset the plan, so a single-file rename can be
|
|
49
|
+
* copied on the next cycle. New files and component-directory changes sit
|
|
50
|
+
* outside that watch set and require a watcher restart. Previous destinations
|
|
51
|
+
* are not pruned; content-only edits keep the cached filesystem walk.
|
|
42
52
|
*
|
|
43
53
|
* @returns {Array<{absPath: string, relDest: string}>} Copy plan.
|
|
44
54
|
*/
|
|
@@ -86,9 +96,16 @@ export function copyTwigFilesPlugin({
|
|
|
86
96
|
/** Capture the final outDir. */
|
|
87
97
|
configResolved(cfg) {
|
|
88
98
|
outDir = cfg.build?.outDir || 'dist';
|
|
99
|
+
projectDir = cfg.root || process.cwd();
|
|
89
100
|
watching = Boolean(cfg.build?.watch);
|
|
90
101
|
},
|
|
91
102
|
|
|
103
|
+
watchChange(_id, { event } = {}) {
|
|
104
|
+
if (!watching || (event !== 'create' && event !== 'delete')) return;
|
|
105
|
+
sourceFileIndex.refresh?.();
|
|
106
|
+
plan = undefined;
|
|
107
|
+
},
|
|
108
|
+
|
|
92
109
|
// Twig is copied rather than compiled, so none of it reaches Rollup's module
|
|
93
110
|
// graph, and Rollup only watches what is in that graph. Without this, saving
|
|
94
111
|
// a template produced no rebuild at all: `dist/` kept the previous version
|
|
@@ -103,30 +120,82 @@ export function copyTwigFilesPlugin({
|
|
|
103
120
|
/** Copy before the mirror plugin moves dist/components to the project root. */
|
|
104
121
|
writeBundle() {
|
|
105
122
|
for (const { absPath, relDest } of copyPlan()) {
|
|
106
|
-
copyToOutDir(absPath, relDest);
|
|
123
|
+
const copyResult = copyToOutDir(absPath, relDest);
|
|
124
|
+
if (watching && copyResult.status === 'written') {
|
|
125
|
+
outputChanges?.set(relDest, {
|
|
126
|
+
kind: 'written',
|
|
127
|
+
bytes: copyResult.bytes,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (copyResult.status === 'failed' && copyResult.error) {
|
|
131
|
+
const errno = copyResult.error.code ?? 'unknown error';
|
|
132
|
+
const message = `Unable to copy ${absPath} to ${join(outDir, relDest)} (${errno}): ${copyResult.error.message}`;
|
|
133
|
+
diagnostics?.recordError?.({
|
|
134
|
+
message,
|
|
135
|
+
file: absPath,
|
|
136
|
+
outputState: 'incomplete',
|
|
137
|
+
});
|
|
138
|
+
this.warn?.(message);
|
|
139
|
+
}
|
|
107
140
|
}
|
|
108
141
|
},
|
|
109
142
|
};
|
|
110
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the output directory to an absolute path.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string} Absolute output directory.
|
|
148
|
+
*/
|
|
149
|
+
function absoluteOutDir() {
|
|
150
|
+
return isAbsolute(outDir) ? outDir : resolve(projectDir, outDir);
|
|
151
|
+
}
|
|
152
|
+
|
|
111
153
|
/**
|
|
112
154
|
* Copy one file into the output directory.
|
|
113
155
|
*
|
|
114
156
|
* @param {string} absPath - Absolute source path.
|
|
115
157
|
* @param {string} relDest - Destination relative to `outDir`.
|
|
116
|
-
* @returns {
|
|
158
|
+
* @returns {{status: 'written'|'skipped'|'failed', bytes?: number, error?: Error}} Copy result.
|
|
117
159
|
*/
|
|
118
160
|
function copyToOutDir(absPath, relDest) {
|
|
119
|
-
if (!relDest) return;
|
|
161
|
+
if (!relDest) return { status: 'failed' };
|
|
120
162
|
|
|
121
|
-
// Copied unconditionally, because `build.emptyOutDir` clears the output
|
|
122
|
-
// directory on every watch cycle and not just the first — a freshness check
|
|
123
|
-
// against the destination can never find anything to skip.
|
|
124
163
|
const destPath = join(outDir, relDest);
|
|
125
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
126
164
|
try {
|
|
165
|
+
// A rewritten template in the output tree is a full preview reload rather
|
|
166
|
+
// than a style swap, so byte-identical templates are skipped during watch.
|
|
167
|
+
// One-shot builds continue to copy unconditionally as before.
|
|
168
|
+
if (
|
|
169
|
+
watching &&
|
|
170
|
+
filesHaveSameBytes(
|
|
171
|
+
absPath,
|
|
172
|
+
resolveFinalPath(relDest, {
|
|
173
|
+
outDir: absoluteOutDir(),
|
|
174
|
+
projectDir,
|
|
175
|
+
mirrored: structure?.mirrorComponentOutput,
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
return { status: 'skipped' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
183
|
+
removeDestinationSymlink(destPath);
|
|
127
184
|
copyFileSync(absPath, destPath);
|
|
128
|
-
|
|
129
|
-
|
|
185
|
+
let bytes;
|
|
186
|
+
if (watching && outputChanges) {
|
|
187
|
+
try {
|
|
188
|
+
bytes = statSync(destPath).size;
|
|
189
|
+
} catch {
|
|
190
|
+
// The write still succeeded; size is optional reporting metadata.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { status: 'written', bytes };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'failed',
|
|
197
|
+
error,
|
|
198
|
+
};
|
|
130
199
|
}
|
|
131
200
|
}
|
|
132
201
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CSS asset URL rebase plugin.
|
|
3
|
+
*
|
|
4
|
+
* Repairs CSS `url()` references to project assets that Vite could not resolve.
|
|
5
|
+
* By default, repaired assets are emitted into the self-contained build output;
|
|
6
|
+
* projects that deploy the whole theme can opt into lean output that references
|
|
7
|
+
* the source asset tree instead. See `asset-url-rebase.js` for the repair rules;
|
|
8
|
+
* this module is the Vite wiring.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this runs in a normal-order `transform`
|
|
11
|
+
*
|
|
12
|
+
* The rewrite has to happen where the importing stylesheet is known, which
|
|
13
|
+
* rules out `generateBundle`. It also has to see Sass partials, which rules out
|
|
14
|
+
* `enforce: 'pre'`: `@use`d partials are loaded inside Dart Sass through Vite's
|
|
15
|
+
* own importer, never enter the module graph, and reach no plugin hook. A
|
|
16
|
+
* normal-order `transform` runs after `vite:css` has compiled Sass and
|
|
17
|
+
* attempted URL resolution, so it sees compiled CSS with every partial inlined
|
|
18
|
+
* and every interpolation expanded.
|
|
19
|
+
*
|
|
20
|
+
* That ordering also keeps ordinary URLs non-destructive: a resolved URL is
|
|
21
|
+
* already a `__VITE_ASSET__` placeholder by this point, so the only literals
|
|
22
|
+
* left are ones Vite gave up on. The reserved `@assets/...` namespace is the
|
|
23
|
+
* deliberate exception; the resolver bridge below prevents consumer aliases,
|
|
24
|
+
* packages, and same-named directories from claiming it first.
|
|
25
|
+
*
|
|
26
|
+
* ## Self-contained and lean output
|
|
27
|
+
*
|
|
28
|
+
* The default keeps `dist/` deployable on its own. Vite already copies assets it
|
|
29
|
+
* resolves, while this plugin explicitly emits assets for the URL forms Vite
|
|
30
|
+
* could not resolve. In both cases the relativizer points CSS at the output copy.
|
|
31
|
+
*
|
|
32
|
+
* With `assets.selfContainedOutput: false`, each source path is instead recorded
|
|
33
|
+
* in `publishedAssetSources`, keyed by the path the output copy would have had.
|
|
34
|
+
* `css-asset-relativizer.js` points CSS URLs at the source tree and removes a
|
|
35
|
+
* Vite copy only after that rewrite actually happens. Copies still referenced
|
|
36
|
+
* by JavaScript or another emitted file remain in the output. This matters for
|
|
37
|
+
* configured `assets.roots`, whose real location is not necessarily `assets/`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync } from 'fs';
|
|
41
|
+
import { relative } from 'path';
|
|
42
|
+
|
|
43
|
+
import { resolveAssetRoots } from '../../utils/asset-roots.js';
|
|
44
|
+
import { toPosixPath } from '../../utils/paths.js';
|
|
45
|
+
import {
|
|
46
|
+
ASSET_ALIAS_PREFIX,
|
|
47
|
+
rewriteStylesheetUrls,
|
|
48
|
+
} from './asset-url-rebase.js';
|
|
49
|
+
import { isStorybookOutput } from './storybook-output.js';
|
|
50
|
+
|
|
51
|
+
/** Stylesheet requests this plugin inspects. */
|
|
52
|
+
const STYLE_REQUEST_RE = /\.(css|p?css|sss|styl|stylus|less|sass|scss)(?:$|\?)/;
|
|
53
|
+
|
|
54
|
+
/** Query suffixes that are not stylesheet content. */
|
|
55
|
+
const NON_STYLE_QUERY_RE = /[?&](raw|url)(?:&|$)/;
|
|
56
|
+
|
|
57
|
+
/** Case-insensitive CSS URL function marker. */
|
|
58
|
+
const URL_FUNCTION_RE = /url\(/i;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Strip the Vite request query from a module id.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} id - Module id.
|
|
64
|
+
* @returns {string} Filesystem path.
|
|
65
|
+
*/
|
|
66
|
+
function stripRequestQuery(id) {
|
|
67
|
+
const index = id.indexOf('?');
|
|
68
|
+
return index === -1 ? id : id.slice(0, index);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determine whether an emitted asset is a copy of a project asset root file.
|
|
73
|
+
*
|
|
74
|
+
* Rollup records the source path an asset came from in `originalFileNames`.
|
|
75
|
+
* Anything the build generated — the SVG sprite, a JS chunk — has none, so this
|
|
76
|
+
* never mistakes generated output for a copy.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} chunk - Emitted bundle entry.
|
|
79
|
+
* @param {string[]} assetRootPrefixes - Project-relative asset root prefixes.
|
|
80
|
+
* @returns {string} Project-relative source path, or an empty string.
|
|
81
|
+
*/
|
|
82
|
+
function copiedAssetSource(chunk, assetRootPrefixes) {
|
|
83
|
+
const original = Array.isArray(chunk.originalFileNames)
|
|
84
|
+
? chunk.originalFileNames[0]
|
|
85
|
+
: chunk.originalFileName;
|
|
86
|
+
if (!original) return '';
|
|
87
|
+
|
|
88
|
+
const source = toPosixPath(original).replace(/^\.?\//, '');
|
|
89
|
+
|
|
90
|
+
return assetRootPrefixes.some((prefix) => source.startsWith(prefix))
|
|
91
|
+
? source
|
|
92
|
+
: '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Match the reserved alias at the start of a URL or after the relative path
|
|
97
|
+
* Sass inserts when rebasing an imported partial. The captured prefix is put
|
|
98
|
+
* back unchanged by the alias entry below.
|
|
99
|
+
*
|
|
100
|
+
* @type {RegExp}
|
|
101
|
+
*/
|
|
102
|
+
const ASSET_ALIAS_RESOLUTION_RE = new RegExp(
|
|
103
|
+
`^(?!/)((?:[^?#]*/)?)(?=${ASSET_ALIAS_PREFIX}/)`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop Vite's private CSS resolver after it recognizes the Core alias.
|
|
108
|
+
*
|
|
109
|
+
* Vite's CSS resolver does not call user `resolveId` hooks. A truthy result
|
|
110
|
+
* with an empty id makes its isolated alias container stop without resolving a
|
|
111
|
+
* project alias, package, or same-named directory. Vite then leaves the URL for
|
|
112
|
+
* this plugin's normal-order transform, which preserves Core's configured-root
|
|
113
|
+
* resolution, diagnostics, and deterministic output paths.
|
|
114
|
+
*
|
|
115
|
+
* @returns {{id: string}} Empty resolution veto.
|
|
116
|
+
*/
|
|
117
|
+
const reserveAssetAlias = () => ({ id: '' });
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Install the resolver veto after Vite has normalized config.
|
|
121
|
+
*
|
|
122
|
+
* Late installation is intentional: Vite warns about alias custom resolvers
|
|
123
|
+
* while normalizing config, but creates its private CSS resolver lazily on the
|
|
124
|
+
* first stylesheet transform. Its normal module alias plugin has already
|
|
125
|
+
* captured consumer entries, so ordinary JavaScript imports keep their
|
|
126
|
+
* existing behavior. A custom resolver created after this hook sees `@assets`
|
|
127
|
+
* as reserved too; projects must not use that stylesheet namespace as a
|
|
128
|
+
* package alias.
|
|
129
|
+
*
|
|
130
|
+
* This bridge intentionally targets Vite 8, which Core pins in package.json.
|
|
131
|
+
* Vite 9 removes alias custom resolvers, so a Vite-major upgrade must replace
|
|
132
|
+
* this bridge; the conflicting-alias release fixture is the fail-loud contract
|
|
133
|
+
* test for that upgrade.
|
|
134
|
+
*
|
|
135
|
+
* @param {import('vite').ResolvedConfig|object} config - Resolved Vite config.
|
|
136
|
+
* @returns {void}
|
|
137
|
+
*/
|
|
138
|
+
function reserveAssetAliasForCss(config) {
|
|
139
|
+
const aliasLists = new Set([
|
|
140
|
+
config?.resolve?.alias,
|
|
141
|
+
...Object.values(config?.environments || {}).map(
|
|
142
|
+
(environment) => environment?.resolve?.alias,
|
|
143
|
+
),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
for (const aliases of aliasLists) {
|
|
147
|
+
if (!Array.isArray(aliases)) continue;
|
|
148
|
+
if (aliases.some((entry) => entry?.customResolver === reserveAssetAlias)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
aliases.unshift({
|
|
153
|
+
find: ASSET_ALIAS_RESOLUTION_RE,
|
|
154
|
+
replacement: '$1',
|
|
155
|
+
customResolver: reserveAssetAlias,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rebase unresolvable CSS asset URLs and manage their output target.
|
|
162
|
+
*
|
|
163
|
+
* @param {{env?: object, diagnostics?: object, publishedAssetSources?: Map<string, string>, removablePublishedAssets?: Set<string>}} [opts={}] - Plugin options.
|
|
164
|
+
* @returns {import('vite').PluginOption} Rebase plugin.
|
|
165
|
+
*/
|
|
166
|
+
export function cssAssetRebasePlugin({
|
|
167
|
+
env = {},
|
|
168
|
+
diagnostics,
|
|
169
|
+
publishedAssetSources = new Map(),
|
|
170
|
+
removablePublishedAssets = new Set(),
|
|
171
|
+
} = {}) {
|
|
172
|
+
const enabled = env?.projectStructure?.assetRebase !== false;
|
|
173
|
+
const selfContainedOutput =
|
|
174
|
+
env?.projectStructure?.selfContainedOutput !== false;
|
|
175
|
+
const projectDir = env?.projectDir || process.cwd();
|
|
176
|
+
/** @type {Map<string, string>} Published path -> absolute source file. */
|
|
177
|
+
const pendingAssetEmissions = new Map();
|
|
178
|
+
|
|
179
|
+
/** @type {string[]} */
|
|
180
|
+
let roots = [];
|
|
181
|
+
/** @type {string[]} */
|
|
182
|
+
let assetRootPrefixes = [];
|
|
183
|
+
let ownsOutput = true;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
name: 'emulsify-css-asset-rebase',
|
|
187
|
+
|
|
188
|
+
configResolved(config) {
|
|
189
|
+
roots = resolveAssetRoots(env);
|
|
190
|
+
assetRootPrefixes = roots
|
|
191
|
+
.map((root) => `${toPosixPath(relative(projectDir, root))}/`)
|
|
192
|
+
.filter((prefix) => prefix !== '/' && !prefix.startsWith('..'));
|
|
193
|
+
|
|
194
|
+
// Storybook serves every asset root at `/assets` through staticDirs and
|
|
195
|
+
// copies them into its own output, so this plugin never owns that output.
|
|
196
|
+
ownsOutput = !isStorybookOutput(config);
|
|
197
|
+
|
|
198
|
+
if (enabled) reserveAssetAliasForCss(config);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Watch rebuilds must not inherit stale publication or emission state.
|
|
202
|
+
buildStart() {
|
|
203
|
+
publishedAssetSources.clear();
|
|
204
|
+
removablePublishedAssets.clear();
|
|
205
|
+
pendingAssetEmissions.clear();
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
transform(code, id) {
|
|
209
|
+
if (!enabled || !roots.length) return null;
|
|
210
|
+
if (!STYLE_REQUEST_RE.test(id) || NON_STYLE_QUERY_RE.test(id)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (!URL_FUNCTION_RE.test(code)) return null;
|
|
214
|
+
|
|
215
|
+
const importer = stripRequestQuery(id);
|
|
216
|
+
|
|
217
|
+
const { code: next, changed } = rewriteStylesheetUrls(
|
|
218
|
+
code,
|
|
219
|
+
importer,
|
|
220
|
+
roots,
|
|
221
|
+
(plan) => {
|
|
222
|
+
if (
|
|
223
|
+
plan.status === 'aliased' ||
|
|
224
|
+
plan.status === 'rebased' ||
|
|
225
|
+
plan.status === 'publish'
|
|
226
|
+
) {
|
|
227
|
+
if (ownsOutput) {
|
|
228
|
+
if (selfContainedOutput) {
|
|
229
|
+
const fileName = plan.emitAs.replace(/^\/+/, '');
|
|
230
|
+
pendingAssetEmissions.set(fileName, plan.file);
|
|
231
|
+
} else {
|
|
232
|
+
publishedAssetSources.set(
|
|
233
|
+
plan.emitAs,
|
|
234
|
+
toPosixPath(relative(projectDir, plan.file)),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Static assets are outside Rollup's module graph, so a swapped
|
|
239
|
+
// image would otherwise go unnoticed until an unrelated rebuild.
|
|
240
|
+
this.addWatchFile(plan.file);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// `missing` is deliberately not recorded: Vite already warned about
|
|
244
|
+
// that exact URL and the reporter's logger captures it. Recording it
|
|
245
|
+
// again would double the occurrence count.
|
|
246
|
+
if (
|
|
247
|
+
plan.status === 'aliased' ||
|
|
248
|
+
plan.status === 'rebased' ||
|
|
249
|
+
plan.status === 'ambiguous'
|
|
250
|
+
) {
|
|
251
|
+
diagnostics?.recordAssetRebase?.({
|
|
252
|
+
status: plan.status,
|
|
253
|
+
url: plan.originalUrl,
|
|
254
|
+
rewritten: plan.url,
|
|
255
|
+
importer,
|
|
256
|
+
resolvedAsset: plan.file,
|
|
257
|
+
candidates: plan.candidates,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!changed) return null;
|
|
264
|
+
|
|
265
|
+
// Core has already captured Vite's combined Sass/PostCSS map for its
|
|
266
|
+
// development emitter. This empty transform map is what Vite itself uses
|
|
267
|
+
// when CSS maps are off and keeps Rollup from warning about this rewrite.
|
|
268
|
+
return { code: next, map: { mappings: '' } };
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// Lean output records every Vite copy the relativizer may redirect. The
|
|
272
|
+
// relativizer owns deletion because only an actual CSS rewrite proves the
|
|
273
|
+
// copy is redundant; JS-only and generated assets must survive.
|
|
274
|
+
generateBundle(_, bundle) {
|
|
275
|
+
if (!enabled || !ownsOutput) return;
|
|
276
|
+
|
|
277
|
+
if (selfContainedOutput) {
|
|
278
|
+
// Vite may already have emitted this exact published path for an
|
|
279
|
+
// equivalent `/assets/...` CSS reference or a JavaScript import. Wait
|
|
280
|
+
// until the bundle is known so Core can fill only the missing paths;
|
|
281
|
+
// emitting eagerly from `transform` produces FILE_NAME_CONFLICT noise
|
|
282
|
+
// for the same file under the two accepted stylesheet spellings.
|
|
283
|
+
for (const [fileName, file] of pendingAssetEmissions) {
|
|
284
|
+
if (Object.hasOwn(bundle, fileName)) continue;
|
|
285
|
+
|
|
286
|
+
this.emitFile({
|
|
287
|
+
type: 'asset',
|
|
288
|
+
fileName,
|
|
289
|
+
source: readFileSync(file),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
296
|
+
if (chunk.type !== 'asset' || fileName.endsWith('.css')) continue;
|
|
297
|
+
|
|
298
|
+
const source = copiedAssetSource(chunk, assetRootPrefixes);
|
|
299
|
+
if (!source) continue;
|
|
300
|
+
|
|
301
|
+
publishedAssetSources.set(fileName, source);
|
|
302
|
+
removablePublishedAssets.add(fileName);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|