@emulsify/core 4.3.1 → 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 +96 -25
- 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
|
@@ -1,49 +1,329 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file CSS asset URL relativizer plugin.
|
|
3
3
|
*
|
|
4
|
-
* Rewrites emitted CSS references to
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* Rewrites emitted CSS references to project assets so each stylesheet resolves
|
|
5
|
+
* them correctly from wherever it ends up on disk.
|
|
6
|
+
*
|
|
7
|
+
* ## What the path is relative to
|
|
8
|
+
*
|
|
9
|
+
* By default, `dist/` is self-contained and a rewritten URL points at the asset
|
|
10
|
+
* copy inside the output. With `assets.selfContainedOutput: false`,
|
|
11
|
+
* `css-asset-rebase.js` supplies `publishedAssetSources`, which maps each
|
|
12
|
+
* published path to where the file lives in the source tree. That indirection
|
|
13
|
+
* matters for a configured `assets.roots` directory, whose real location is not
|
|
14
|
+
* necessarily `assets/`.
|
|
15
|
+
*
|
|
16
|
+
* Two cases stay output-relative. Component CSS mirrored out of `dist/` already
|
|
17
|
+
* sits at the project root, so its path within the output is the project path.
|
|
18
|
+
* And a Storybook build copies every asset root into its own output and serves
|
|
19
|
+
* them at `/assets`, so nothing there should reach outside that output.
|
|
20
|
+
*
|
|
21
|
+
* An asset with no entry in the map lives in the output. This includes every
|
|
22
|
+
* project asset in the default self-contained mode and generated assets such as
|
|
23
|
+
* the SVG sprite in either mode. In lean mode, a mapped Vite copy is removed
|
|
24
|
+
* only after this plugin actually rewrites a CSS URL to its source location;
|
|
25
|
+
* copies referenced by JavaScript or other emitted files remain available.
|
|
26
|
+
*
|
|
27
|
+
* Development-map caveat: Core captures the Sass/PostCSS map before this
|
|
28
|
+
* plugin rewrites finalized asset URLs. Replacements preserve line structure,
|
|
29
|
+
* so selectors and declarations still resolve to their authored source lines.
|
|
30
|
+
* A length-changing replacement can shift later mapping columns on that same
|
|
31
|
+
* generated line, including positions inside the rewritten `url()` value.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { isAbsolute, posix as pathPosix, relative, resolve } from 'path';
|
|
35
|
+
|
|
36
|
+
import { resolveAssetTail } from '../../utils/asset-roots.js';
|
|
37
|
+
import { replaceStylesheetUrlTokens } from '../../utils/css-urls.js';
|
|
38
|
+
import { toPosixPath } from '../../utils/paths.js';
|
|
39
|
+
import { PUBLIC_ASSET_PREFIX, splitUrlSuffix } from './asset-url-rebase.js';
|
|
40
|
+
import { isStorybookOutput } from './storybook-output.js';
|
|
41
|
+
|
|
42
|
+
/** Stylesheet facades Vite may retain as empty Rollup chunks. */
|
|
43
|
+
const STYLE_FACADE_RE =
|
|
44
|
+
/\.(?:css|p?css|sss|styl|stylus|less|sass|scss)(?:$|\?)/;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Match an emitted URL to the published path recorded by the rebase plugin.
|
|
48
|
+
*
|
|
49
|
+
* Root-absolute URLs name bundle paths directly. A relative emitted URL is
|
|
50
|
+
* resolved from the CSS asset's bundle location. Protocol-relative URLs are
|
|
51
|
+
* never project output, and an encoded or otherwise non-exact path stays in
|
|
52
|
+
* the bundle rather than being guessed at.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} urlPath - URL path without a query or fragment.
|
|
55
|
+
* @param {string} cssFileName - Stylesheet path inside the bundle.
|
|
56
|
+
* @param {Map<string, string>} publishedAssetSources - Recorded bundle paths.
|
|
57
|
+
* @returns {string} Matching published path, or an empty string.
|
|
58
|
+
*/
|
|
59
|
+
function recordedPublishedPath(urlPath, cssFileName, publishedAssetSources) {
|
|
60
|
+
if (!urlPath || urlPath.startsWith('//')) return '';
|
|
61
|
+
|
|
62
|
+
if (urlPath.startsWith('/')) {
|
|
63
|
+
const direct = urlPath.slice(1);
|
|
64
|
+
|
|
65
|
+
return publishedAssetSources.has(direct) ? direct : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const relativePublished = pathPosix.normalize(
|
|
69
|
+
pathPosix.join(pathPosix.dirname(cssFileName), urlPath),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return publishedAssetSources.has(relativePublished) ? relativePublished : '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Identify an empty Vite chunk whose asset metadata belongs to extracted CSS.
|
|
77
|
+
*
|
|
78
|
+
* Vite records CSS image dependencies on the stylesheet's empty JavaScript
|
|
79
|
+
* facade as `importedAssets`. Those are not independent JavaScript consumers:
|
|
80
|
+
* the emitted CSS was already rewritten above and the facade is omitted from
|
|
81
|
+
* disk. Treating that metadata as a live JS reference would retain every copy.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} output - Rollup output entry.
|
|
84
|
+
* @returns {boolean} TRUE for an extracted-stylesheet facade.
|
|
85
|
+
*/
|
|
86
|
+
function isCssFacadeChunk(output) {
|
|
87
|
+
return Boolean(
|
|
88
|
+
output?.type === 'chunk' &&
|
|
89
|
+
typeof output.code === 'string' &&
|
|
90
|
+
!output.code.trim() &&
|
|
91
|
+
STYLE_FACADE_RE.test(output.facadeModuleId || '') &&
|
|
92
|
+
output.viteMetadata?.importedCss?.size,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Determine whether an emitted non-CSS file still needs a published asset.
|
|
98
|
+
*
|
|
99
|
+
* Rollup and Vite expose referenced assets as metadata, while scanning emitted
|
|
100
|
+
* text is a conservative fallback for synthetic bundles and other output
|
|
101
|
+
* plugins. A false positive only retains a redundant copy; a false negative
|
|
102
|
+
* would ship a broken reference.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} published - Asset path inside the bundle.
|
|
105
|
+
* @param {object} bundle - Rollup output bundle.
|
|
106
|
+
* @returns {boolean} TRUE when deletion would break another emitted file.
|
|
107
|
+
*/
|
|
108
|
+
function isReferencedOutsideCss(published, bundle) {
|
|
109
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
110
|
+
if (fileName === published) continue;
|
|
111
|
+
|
|
112
|
+
const isCssAsset = output.type === 'asset' && fileName.endsWith('.css');
|
|
113
|
+
if (!isCssAsset && !isCssFacadeChunk(output)) {
|
|
114
|
+
if (output.referencedFiles?.includes?.(published)) return true;
|
|
115
|
+
if (output.viteMetadata?.importedAssets?.has?.(published)) return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (output.type === 'chunk') {
|
|
119
|
+
if (typeof output.code === 'string' && output.code.includes(published)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isCssAsset) {
|
|
126
|
+
// String CSS assets were already processed above. If another plugin
|
|
127
|
+
// emitted binary CSS, keep candidates because it could not be rewritten.
|
|
128
|
+
if (typeof output.source !== 'string') return true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
typeof output.source === 'string' &&
|
|
134
|
+
output.source.includes(published)
|
|
135
|
+
) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Confirm a bundle entry is a copied source asset, not generated output.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} output - Rollup output entry.
|
|
147
|
+
* @returns {boolean} TRUE for Vite copies with source provenance.
|
|
13
148
|
*/
|
|
149
|
+
function isCopiedAsset(output) {
|
|
150
|
+
if (output?.type !== 'asset') return false;
|
|
151
|
+
const originals = Array.isArray(output.originalFileNames)
|
|
152
|
+
? output.originalFileNames
|
|
153
|
+
: output.originalFileName
|
|
154
|
+
? [output.originalFileName]
|
|
155
|
+
: [];
|
|
14
156
|
|
|
15
|
-
|
|
157
|
+
return originals.length > 0;
|
|
158
|
+
}
|
|
16
159
|
|
|
17
160
|
/**
|
|
18
161
|
* Rewrites any `url(assets/...)` found in emitted CSS to a path relative to the
|
|
19
|
-
* CSS file's
|
|
162
|
+
* CSS file's location on disk.
|
|
20
163
|
*
|
|
21
|
-
* @param {{
|
|
164
|
+
* @param {{assetsRoot?: string, env?: object, publishedAssetSources?: Map<string, string>, removablePublishedAssets?: Set<string>}} [opts] - Plugin options.
|
|
22
165
|
* @returns {import('vite').PluginOption} CSS asset URL plugin.
|
|
23
166
|
*/
|
|
24
|
-
export function cssAssetUrlRelativizer({
|
|
167
|
+
export function cssAssetUrlRelativizer({
|
|
168
|
+
assetsRoot = 'assets',
|
|
169
|
+
env = {},
|
|
170
|
+
publishedAssetSources = new Map(),
|
|
171
|
+
removablePublishedAssets = new Set(),
|
|
172
|
+
} = {}) {
|
|
173
|
+
const enabled = env?.projectStructure?.assetRebase !== false;
|
|
174
|
+
const selfContainedOutput =
|
|
175
|
+
env?.projectStructure?.selfContainedOutput !== false;
|
|
176
|
+
const projectDir = env?.projectDir || process.cwd();
|
|
177
|
+
const mirrorComponentOutput = Boolean(
|
|
178
|
+
env?.projectStructure?.mirrorComponentOutput,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
let outDirFromProject = 'dist';
|
|
182
|
+
let ownsOutput = true;
|
|
183
|
+
let copiedPublicDir = '';
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the directory an emitted stylesheet occupies, project-relative.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} fileName - Emitted CSS path within the output directory.
|
|
189
|
+
* @returns {string} Directory the URL resolves from.
|
|
190
|
+
*/
|
|
191
|
+
const stylesheetDirectory = (fileName) => {
|
|
192
|
+
const withinOutput = pathPosix.dirname(fileName);
|
|
193
|
+
|
|
194
|
+
// Storybook output is self-contained, and mirrored component CSS is moved
|
|
195
|
+
// out of the output directory to the project root; in both cases the path
|
|
196
|
+
// within the output is already the right base.
|
|
197
|
+
if (!ownsOutput) return withinOutput;
|
|
198
|
+
if (mirrorComponentOutput && fileName.startsWith('components/')) {
|
|
199
|
+
return withinOutput;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return pathPosix.join(outDirFromProject, withinOutput);
|
|
203
|
+
};
|
|
204
|
+
|
|
25
205
|
return {
|
|
26
206
|
name: 'emulsify-css-asset-url-relativizer',
|
|
27
207
|
apply: 'build',
|
|
208
|
+
|
|
209
|
+
configResolved(config) {
|
|
210
|
+
// Vite resolves `outDir` against the project root before handing it over,
|
|
211
|
+
// but accept a relative value too so the plugin is testable in isolation.
|
|
212
|
+
const outDir = config?.build?.outDir || 'dist';
|
|
213
|
+
const absoluteOutDir = isAbsolute(outDir)
|
|
214
|
+
? outDir
|
|
215
|
+
: resolve(projectDir, outDir);
|
|
216
|
+
|
|
217
|
+
outDirFromProject = toPosixPath(relative(projectDir, absoluteOutDir));
|
|
218
|
+
ownsOutput = !isStorybookOutput(config);
|
|
219
|
+
|
|
220
|
+
const publicDir = config?.publicDir;
|
|
221
|
+
copiedPublicDir =
|
|
222
|
+
config?.build?.copyPublicDir !== false &&
|
|
223
|
+
typeof publicDir === 'string' &&
|
|
224
|
+
publicDir
|
|
225
|
+
? isAbsolute(publicDir)
|
|
226
|
+
? publicDir
|
|
227
|
+
: resolve(projectDir, publicDir)
|
|
228
|
+
: '';
|
|
229
|
+
},
|
|
230
|
+
|
|
28
231
|
generateBundle(_, bundle) {
|
|
232
|
+
if (!enabled) return;
|
|
233
|
+
|
|
234
|
+
const rewrittenPublishedAssets = new Set();
|
|
235
|
+
const publicAssetCopies = new Map();
|
|
236
|
+
|
|
237
|
+
const hasPublicAssetCopy = (published) => {
|
|
238
|
+
if (!copiedPublicDir) return false;
|
|
239
|
+
if (!publicAssetCopies.has(published)) {
|
|
240
|
+
publicAssetCopies.set(
|
|
241
|
+
published,
|
|
242
|
+
resolveAssetTail(published, [copiedPublicDir]).status ===
|
|
243
|
+
'resolved',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return publicAssetCopies.get(published);
|
|
248
|
+
};
|
|
249
|
+
|
|
29
250
|
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
30
251
|
if (chunk.type !== 'asset') continue;
|
|
31
252
|
if (!fileName.endsWith('.css')) continue;
|
|
32
253
|
if (typeof chunk.source !== 'string') continue;
|
|
33
254
|
|
|
34
|
-
const fromDir =
|
|
255
|
+
const fromDir = stylesheetDirectory(fileName);
|
|
256
|
+
|
|
257
|
+
// Length-changing rewrite: read the development-map caveat in the file
|
|
258
|
+
// header before changing how or where this transform runs.
|
|
259
|
+
chunk.source = replaceStylesheetUrlTokens(
|
|
260
|
+
chunk.source,
|
|
261
|
+
({ match, quote, value }) => {
|
|
262
|
+
const { path: urlPath, suffix } = splitUrlSuffix(value);
|
|
263
|
+
const absolutePrefix = `/${PUBLIC_ASSET_PREFIX}/`;
|
|
264
|
+
const barePrefix = `${PUBLIC_ASSET_PREFIX}/`;
|
|
265
|
+
const rest = urlPath.startsWith(absolutePrefix)
|
|
266
|
+
? urlPath.slice(absolutePrefix.length)
|
|
267
|
+
: urlPath.startsWith(barePrefix)
|
|
268
|
+
? urlPath.slice(barePrefix.length)
|
|
269
|
+
: '';
|
|
270
|
+
// `/assets/...` remains the public alias and honors a customized
|
|
271
|
+
// output-side root. Other Vite-resolved URLs name their recorded
|
|
272
|
+
// bundle path directly, such as `/src/assets/...`.
|
|
273
|
+
const published = rest
|
|
274
|
+
? pathPosix.join(assetsRoot, rest)
|
|
275
|
+
: recordedPublishedPath(urlPath, fileName, publishedAssetSources);
|
|
276
|
+
|
|
277
|
+
if (!published) return match;
|
|
35
278
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
279
|
+
// Only rewrite toward a target something actually put there. The
|
|
280
|
+
// rebase plugin emits nothing for an `ambiguous` or `missing` URL,
|
|
281
|
+
// so rewriting those would invent a confident path into the output
|
|
282
|
+
// that no file occupies — turning a reported problem into a broken
|
|
283
|
+
// URL that survives a green build. Leaving the authored URL alone
|
|
284
|
+
// keeps the reporter's diagnostic the only account of it.
|
|
285
|
+
//
|
|
286
|
+
// Restricted to `ownsOutput`: a Storybook build also copies every
|
|
287
|
+
// static directory beside its bundle, so there the bundle is not
|
|
288
|
+
// the whole truth about what the output contains.
|
|
289
|
+
if (
|
|
290
|
+
ownsOutput &&
|
|
291
|
+
!Object.hasOwn(bundle, published) &&
|
|
292
|
+
!publishedAssetSources.has(published) &&
|
|
293
|
+
!hasPublicAssetCopy(published)
|
|
294
|
+
) {
|
|
295
|
+
return match;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// A copied or generated asset is reached inside the output. In lean
|
|
299
|
+
// mode, a mapped project asset is reached where it lives in source.
|
|
300
|
+
// Both targets are project-relative so the same subtraction works.
|
|
301
|
+
const inOutput = ownsOutput
|
|
302
|
+
? pathPosix.join(outDirFromProject, published)
|
|
303
|
+
: published;
|
|
304
|
+
const target = publishedAssetSources.get(published) || inOutput;
|
|
42
305
|
const rel = pathPosix.relative(fromDir, target);
|
|
43
|
-
|
|
306
|
+
|
|
307
|
+
if (removablePublishedAssets.has(published)) {
|
|
308
|
+
rewrittenPublishedAssets.add(published);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return `url(${quote}${rel}${suffix}${quote})`;
|
|
44
312
|
},
|
|
45
313
|
);
|
|
46
314
|
}
|
|
315
|
+
|
|
316
|
+
if (!ownsOutput || selfContainedOutput) return;
|
|
317
|
+
|
|
318
|
+
for (const published of rewrittenPublishedAssets) {
|
|
319
|
+
if (!removablePublishedAssets.has(published)) continue;
|
|
320
|
+
|
|
321
|
+
const output = bundle[published];
|
|
322
|
+
if (!isCopiedAsset(output)) continue;
|
|
323
|
+
if (isReferencedOutsideCss(published, bundle)) continue;
|
|
324
|
+
|
|
325
|
+
delete bundle[published];
|
|
326
|
+
}
|
|
47
327
|
},
|
|
48
328
|
};
|
|
49
329
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Development source maps for Vite's extracted CSS assets.
|
|
3
|
+
*
|
|
4
|
+
* Vite keeps the Sass/PostCSS map through its CSS transform, then discards it
|
|
5
|
+
* when `vite build` extracts the stylesheet as a Rollup asset. The capture
|
|
6
|
+
* plugin runs after Vite compiles CSS but before Core rewrites asset URLs. The
|
|
7
|
+
* emitter later pairs each direct stylesheet entry with its finalized CSS
|
|
8
|
+
* asset, writes a sibling map, and adds the browser-facing map comment.
|
|
9
|
+
*
|
|
10
|
+
* Core's later URL rewrites preserve line structure. They can shift columns
|
|
11
|
+
* inside a rewritten `url()`, but selector and declaration line mappings stay
|
|
12
|
+
* anchored to their authored Sass sources, which is what browser style
|
|
13
|
+
* inspection uses.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, isAbsolute, posix, relative, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { safeExists } from '../../utils/fs-safe.js';
|
|
21
|
+
import { toPosixPath } from '../../utils/paths.js';
|
|
22
|
+
|
|
23
|
+
/** Stylesheet requests whose compiled maps can be captured. */
|
|
24
|
+
const STYLE_REQUEST_RE =
|
|
25
|
+
/\.(?:css|p?css|sss|styl|stylus|less|sass|scss)(?:$|\?)/;
|
|
26
|
+
|
|
27
|
+
/** Query suffixes that represent asset contents rather than a stylesheet. */
|
|
28
|
+
const NON_STYLE_QUERY_RE = /[?&](?:raw|url)(?:&|$)/;
|
|
29
|
+
|
|
30
|
+
/** Existing external source-map annotations supplied by a project plugin. */
|
|
31
|
+
const SOURCE_MAP_COMMENT_RE = /\/\*[#@]\s*sourceMappingURL=[^*]+\*\//;
|
|
32
|
+
|
|
33
|
+
/** URI schemes that are not local filesystem paths. */
|
|
34
|
+
const URI_SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
|
|
35
|
+
|
|
36
|
+
/** Remove Vite's request query from a module id. */
|
|
37
|
+
const cleanId = (id) => String(id).split('?', 1)[0];
|
|
38
|
+
|
|
39
|
+
/** Clone a combined Rollup map before another transform can mutate it. */
|
|
40
|
+
const cloneMap = (map) => JSON.parse(JSON.stringify(map));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve one captured map source to a local file when possible.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} source - Captured source-map source.
|
|
46
|
+
* @param {string} sourceRoot - Optional captured source root.
|
|
47
|
+
* @param {string} entryId - Absolute stylesheet entry id.
|
|
48
|
+
* @returns {string} Absolute source path, or an empty string for virtual/remote sources.
|
|
49
|
+
*/
|
|
50
|
+
function resolveMapSource(source, sourceRoot, entryId) {
|
|
51
|
+
if (!source || source.startsWith('\0')) return '';
|
|
52
|
+
|
|
53
|
+
if (source.startsWith('file:')) {
|
|
54
|
+
try {
|
|
55
|
+
return fileURLToPath(source);
|
|
56
|
+
} catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isAbsolute(source)) return source;
|
|
62
|
+
if (URI_SCHEME_RE.test(source) || source.startsWith('//')) return '';
|
|
63
|
+
|
|
64
|
+
let base = dirname(entryId);
|
|
65
|
+
if (sourceRoot) {
|
|
66
|
+
if (sourceRoot.startsWith('file:')) {
|
|
67
|
+
try {
|
|
68
|
+
base = fileURLToPath(sourceRoot);
|
|
69
|
+
} catch {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
} else if (isAbsolute(sourceRoot)) {
|
|
73
|
+
base = sourceRoot;
|
|
74
|
+
} else if (URI_SCHEME_RE.test(sourceRoot) || sourceRoot.startsWith('//')) {
|
|
75
|
+
return '';
|
|
76
|
+
} else {
|
|
77
|
+
base = resolve(base, sourceRoot);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return resolve(base, source);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Make captured source paths portable from the emitted map's directory.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} captured - Captured combined source map.
|
|
88
|
+
* @param {string} entryId - Absolute stylesheet entry id.
|
|
89
|
+
* @param {string} mapFile - Absolute emitted map path.
|
|
90
|
+
* @param {string} cssFileName - Output-relative CSS filename.
|
|
91
|
+
* @returns {object} External source-map payload.
|
|
92
|
+
*/
|
|
93
|
+
function externalizeMap(captured, entryId, mapFile, cssFileName) {
|
|
94
|
+
const sourceRoot = captured.sourceRoot || '';
|
|
95
|
+
const preserveSourceRoot = Boolean(
|
|
96
|
+
sourceRoot &&
|
|
97
|
+
!sourceRoot.startsWith('file:') &&
|
|
98
|
+
!isAbsolute(sourceRoot) &&
|
|
99
|
+
(URI_SCHEME_RE.test(sourceRoot) || sourceRoot.startsWith('//')),
|
|
100
|
+
);
|
|
101
|
+
const sourceFiles = captured.sources.map((source) =>
|
|
102
|
+
resolveMapSource(source, sourceRoot, entryId),
|
|
103
|
+
);
|
|
104
|
+
const sources = captured.sources.map((source, index) => {
|
|
105
|
+
const sourceFile = sourceFiles[index];
|
|
106
|
+
if (!sourceFile) return source;
|
|
107
|
+
return toPosixPath(relative(dirname(mapFile), sourceFile));
|
|
108
|
+
});
|
|
109
|
+
const capturedContentsAreAligned =
|
|
110
|
+
captured.sourcesContent?.length === captured.sources.length;
|
|
111
|
+
const sourcesContent = captured.sources.map((_, index) => {
|
|
112
|
+
const existing = capturedContentsAreAligned
|
|
113
|
+
? captured.sourcesContent[index]
|
|
114
|
+
: null;
|
|
115
|
+
if (existing != null) return existing;
|
|
116
|
+
|
|
117
|
+
const sourceFile = sourceFiles[index];
|
|
118
|
+
if (!sourceFile || !safeExists(sourceFile)) return null;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync(sourceFile, 'utf8');
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = {
|
|
128
|
+
...captured,
|
|
129
|
+
file: posix.basename(cssFileName),
|
|
130
|
+
sources,
|
|
131
|
+
sourcesContent,
|
|
132
|
+
};
|
|
133
|
+
if (!preserveSourceRoot) delete result.sourceRoot;
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Add a candidate entry id for one emitted CSS asset. */
|
|
138
|
+
function addCandidate(candidates, cssFileName, entryId) {
|
|
139
|
+
if (!candidates.has(cssFileName)) candidates.set(cssFileName, new Set());
|
|
140
|
+
candidates.get(cssFileName).add(entryId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create the two plugins that bridge Vite's extracted-CSS source-map gap.
|
|
145
|
+
*
|
|
146
|
+
* The capture plugin and emitter must surround Core's CSS URL plugins in the
|
|
147
|
+
* shared plugin array. They intentionally share one map cache across watch
|
|
148
|
+
* rebuilds because Rollup may reuse an unchanged stylesheet's transform.
|
|
149
|
+
*
|
|
150
|
+
* @param {{projectDir?: string, developmentBuild?: boolean}} [opts={}] - Project paths and invocation mode.
|
|
151
|
+
* @returns {{capture: import('vite').PluginOption, emit: import('vite').PluginOption}}
|
|
152
|
+
*/
|
|
153
|
+
export function developmentCssSourceMapPlugins({
|
|
154
|
+
projectDir = process.cwd(),
|
|
155
|
+
developmentBuild = false,
|
|
156
|
+
} = {}) {
|
|
157
|
+
/** @type {Map<string, object>} */
|
|
158
|
+
const capturedMaps = new Map();
|
|
159
|
+
let outDir = resolve(projectDir, 'dist');
|
|
160
|
+
let watching = Boolean(developmentBuild);
|
|
161
|
+
let sourceMapsEnabled = false;
|
|
162
|
+
|
|
163
|
+
const configResolved = (config) => {
|
|
164
|
+
// The invocation signal is available before Vite resolves its config;
|
|
165
|
+
// checking the resolved watch option as well supports projects that turn
|
|
166
|
+
// watch mode on from an extension instead of the CLI flag.
|
|
167
|
+
watching = Boolean(developmentBuild || config?.build?.watch);
|
|
168
|
+
// Consumer overrides remain authoritative. Core emits visible external CSS
|
|
169
|
+
// maps only for its default `true` policy, not `false`, `hidden`, or inline.
|
|
170
|
+
sourceMapsEnabled = config?.build?.sourcemap === true;
|
|
171
|
+
const configuredOutDir = config?.build?.outDir || 'dist';
|
|
172
|
+
outDir = isAbsolute(configuredOutDir)
|
|
173
|
+
? configuredOutDir
|
|
174
|
+
: resolve(projectDir, configuredOutDir);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const capture = {
|
|
178
|
+
name: 'emulsify-development-css-map-capture',
|
|
179
|
+
apply: 'build',
|
|
180
|
+
configResolved,
|
|
181
|
+
transform(_code, id) {
|
|
182
|
+
if (!watching || !sourceMapsEnabled) return null;
|
|
183
|
+
if (!STYLE_REQUEST_RE.test(id) || NON_STYLE_QUERY_RE.test(id)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const map = this.getCombinedSourcemap();
|
|
188
|
+
if (
|
|
189
|
+
!map?.mappings ||
|
|
190
|
+
!Array.isArray(map.sources) ||
|
|
191
|
+
!map.sources.length
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
capturedMaps.set(cleanId(id), cloneMap(map));
|
|
197
|
+
return null;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const emit = {
|
|
202
|
+
name: 'emulsify-development-css-map-emit',
|
|
203
|
+
apply: 'build',
|
|
204
|
+
configResolved,
|
|
205
|
+
generateBundle(_options, bundle) {
|
|
206
|
+
if (!watching || !sourceMapsEnabled || !capturedMaps.size) return;
|
|
207
|
+
|
|
208
|
+
/** @type {Map<string, Set<string>>} */
|
|
209
|
+
const candidates = new Map();
|
|
210
|
+
|
|
211
|
+
// A pure CSS entry still has its facade chunk at this point. Vite removes
|
|
212
|
+
// that empty JavaScript placeholder in its later CSS generateBundle hook.
|
|
213
|
+
for (const output of Object.values(bundle)) {
|
|
214
|
+
if (output.type !== 'chunk' || !output.facadeModuleId) continue;
|
|
215
|
+
|
|
216
|
+
const entryId = cleanId(output.facadeModuleId);
|
|
217
|
+
if (!capturedMaps.has(entryId)) continue;
|
|
218
|
+
|
|
219
|
+
for (const cssFileName of output.viteMetadata?.importedCss || []) {
|
|
220
|
+
addCandidate(candidates, cssFileName, entryId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const [cssFileName, output] of Object.entries(bundle)) {
|
|
225
|
+
if (output.type !== 'asset' || !cssFileName.endsWith('.css')) continue;
|
|
226
|
+
if (typeof output.source !== 'string') continue;
|
|
227
|
+
if (SOURCE_MAP_COMMENT_RE.test(output.source)) continue;
|
|
228
|
+
|
|
229
|
+
// Metadata is a fallback for Vite-compatible emitters that omit the
|
|
230
|
+
// empty facade. A map is emitted only for one unambiguous direct entry;
|
|
231
|
+
// concatenated CSS from several modules is deliberately skipped.
|
|
232
|
+
const entryIds = candidates.get(cssFileName) || new Set();
|
|
233
|
+
if (!entryIds.size) {
|
|
234
|
+
const originals = Array.isArray(output.originalFileNames)
|
|
235
|
+
? output.originalFileNames
|
|
236
|
+
: output.originalFileName
|
|
237
|
+
? [output.originalFileName]
|
|
238
|
+
: [];
|
|
239
|
+
for (const original of originals) {
|
|
240
|
+
const entryId = isAbsolute(original)
|
|
241
|
+
? cleanId(original)
|
|
242
|
+
: resolve(projectDir, cleanId(original));
|
|
243
|
+
if (capturedMaps.has(entryId)) entryIds.add(entryId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (entryIds.size !== 1) continue;
|
|
247
|
+
|
|
248
|
+
const [entryId] = entryIds;
|
|
249
|
+
const mapFileName = `${cssFileName}.map`;
|
|
250
|
+
if (Object.hasOwn(bundle, mapFileName)) continue;
|
|
251
|
+
|
|
252
|
+
const mapFile = resolve(outDir, ...mapFileName.split('/'));
|
|
253
|
+
const sourceMap = externalizeMap(
|
|
254
|
+
capturedMaps.get(entryId),
|
|
255
|
+
entryId,
|
|
256
|
+
mapFile,
|
|
257
|
+
cssFileName,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
this.emitFile({
|
|
261
|
+
type: 'asset',
|
|
262
|
+
fileName: mapFileName,
|
|
263
|
+
source: `${JSON.stringify(sourceMap)}\n`,
|
|
264
|
+
});
|
|
265
|
+
output.source = `${output.source.trimEnd()}\n/*# sourceMappingURL=${posix.basename(
|
|
266
|
+
mapFileName,
|
|
267
|
+
)} */\n`;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return { capture, emit };
|
|
273
|
+
}
|