tw-migrate 0.1.0 → 0.2.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/README.md +13 -1
- package/bin/tw-migrate.js +15 -10
- package/html.js +74 -36
- package/index.d.ts +1 -1
- package/index.js +1283 -312
- package/native.js +18 -11
- package/package.json +14 -12
- package/style-compiler.js +52 -37
- package/vue.js +541 -0
- package/rfcs/batch-css-migration.md +0 -297
- package/rfcs/browser-ecosystem-e2e.md +0 -338
- package/rfcs/css-to-tailwind-migration-cli.md +0 -521
- package/rfcs/preprocessor-and-html-migration.md +0 -497
package/index.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
lstat,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
realpath,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
stat,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
|
|
18
|
+
import { parseHtmlSource, utf8OffsetMap } from "./html.js";
|
|
19
|
+
import { planBatchMigration, staticImports, validateCss } from "./native.js";
|
|
20
|
+
import { analyzeVueSource, loadProjectVueCompiler, verifyVueSource } from "./vue.js";
|
|
10
21
|
import {
|
|
11
22
|
compileLessEntry,
|
|
12
23
|
compileSassEntry,
|
|
@@ -14,28 +25,40 @@ import {
|
|
|
14
25
|
isSassPath,
|
|
15
26
|
loadProjectLess,
|
|
16
27
|
loadProjectSass,
|
|
17
|
-
} from
|
|
28
|
+
} from "./style-compiler.js";
|
|
18
29
|
|
|
19
30
|
const run = promisify(execFile);
|
|
20
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
31
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
32
|
+
".html",
|
|
33
|
+
".vue",
|
|
34
|
+
".js",
|
|
35
|
+
".jsx",
|
|
36
|
+
".mjs",
|
|
37
|
+
".cjs",
|
|
38
|
+
".ts",
|
|
39
|
+
".tsx",
|
|
40
|
+
".mts",
|
|
41
|
+
".cts",
|
|
42
|
+
]);
|
|
21
43
|
const STYLESHEET_SYNTAX = new Map([
|
|
22
|
-
[
|
|
23
|
-
[
|
|
24
|
-
[
|
|
25
|
-
[
|
|
44
|
+
[".css", "css"],
|
|
45
|
+
[".scss", "scss"],
|
|
46
|
+
[".sass", "sass"],
|
|
47
|
+
[".less", "less"],
|
|
26
48
|
]);
|
|
27
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
28
|
-
const RECOVERABLE_INPUT_ERROR =
|
|
49
|
+
const IGNORED_DIRECTORIES = new Set([".git", ".next", "build", "dist", "node_modules"]);
|
|
50
|
+
const RECOVERABLE_INPUT_ERROR = "TW_MIGRATE_RECOVERABLE_INPUT:";
|
|
51
|
+
const compareStrings = (left, right) => (left > right) - (left < right);
|
|
29
52
|
|
|
30
53
|
export async function migrate(options = {}) {
|
|
31
|
-
if (
|
|
32
|
-
throw new TypeError(
|
|
54
|
+
if ("cssFile" in options) {
|
|
55
|
+
throw new TypeError("cssFile has been replaced by styleFile");
|
|
33
56
|
}
|
|
34
57
|
if (options.styleFile && options.workspaces) {
|
|
35
|
-
throw new TypeError(
|
|
58
|
+
throw new TypeError("styleFile cannot be combined with workspaces");
|
|
36
59
|
}
|
|
37
60
|
if (options.tailwindCss && options.workspaces) {
|
|
38
|
-
throw new TypeError(
|
|
61
|
+
throw new TypeError("tailwindCss cannot be combined with workspaces");
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
const scope = await resolveScope(options);
|
|
@@ -43,9 +66,14 @@ export async function migrate(options = {}) {
|
|
|
43
66
|
|
|
44
67
|
// A prior interrupted run's scope is unknowable from the current flags
|
|
45
68
|
// (it may have covered other packages), so scan the whole workspace root.
|
|
46
|
-
const leftovers = await collectFiles(workspaceRoot, (path) =>
|
|
69
|
+
const leftovers = await collectFiles(workspaceRoot, (path) =>
|
|
70
|
+
basename(path).includes(".tw-migrate-"),
|
|
71
|
+
);
|
|
47
72
|
if (leftovers.length > 0) {
|
|
48
|
-
const listed = leftovers
|
|
73
|
+
const listed = leftovers
|
|
74
|
+
.sort(compareStrings)
|
|
75
|
+
.map((path) => ` ${normalizedRelativePath(cwd, path)}`)
|
|
76
|
+
.join("\n");
|
|
49
77
|
throw new Error(
|
|
50
78
|
`Found leftover tw-migrate files from an interrupted run:\n${listed}\n` +
|
|
51
79
|
'Restore each ".<name>.tw-migrate-backup-*" file by renaming it back to "<name>", ' +
|
|
@@ -58,22 +86,28 @@ export async function migrate(options = {}) {
|
|
|
58
86
|
const sourcePaths = scope.scannedPaths.filter((path) => SOURCE_EXTENSIONS.has(extension(path)));
|
|
59
87
|
const [styleSources, sourceCandidates] = await Promise.all([
|
|
60
88
|
readSources(stylePaths, snapshots),
|
|
61
|
-
Promise.all(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
Promise.all(
|
|
90
|
+
sourcePaths.map(async (path) => {
|
|
91
|
+
const source = await readFile(path, "utf8");
|
|
92
|
+
// Scan-only scripts are always retained as reference-only inputs: even
|
|
93
|
+
// without a ".module." mention they can render components whose trees
|
|
94
|
+
// the closed-world relationship proofs must see. Scan-only HTML matters
|
|
95
|
+
// solely as a potential stylesheet consumer, and HTML entities can
|
|
96
|
+
// encode any part of a linked filename, so retain ignored HTML
|
|
97
|
+
// containing a link for parse5 to classify safely.
|
|
98
|
+
const mayReferenceModule = extension(path) !== ".html" || /<link\b/i.test(source);
|
|
99
|
+
if (!scope.targetable.has(path) && !mayReferenceModule) return undefined;
|
|
100
|
+
return { path, source: recordSnapshot(snapshots, path, source) };
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
...selectedPackages.map((packageRoot) =>
|
|
104
|
+
snapshotFile(snapshots, join(packageRoot, "package.json")),
|
|
105
|
+
),
|
|
74
106
|
]);
|
|
75
107
|
const sourceFiles = sourceCandidates.filter(Boolean);
|
|
76
|
-
|
|
108
|
+
// An explicit .vue selection is a source file, not a stylesheet input; only
|
|
109
|
+
// real stylesheets may enter the stylesheet maps.
|
|
110
|
+
if (explicitStyle && isStylesheetPath(explicitStyle) && !styleSources.has(explicitStyle)) {
|
|
77
111
|
styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
|
|
78
112
|
}
|
|
79
113
|
if (configuredEntry && !styleSources.has(configuredEntry)) {
|
|
@@ -110,28 +144,48 @@ export async function migrate(options = {}) {
|
|
|
110
144
|
const deleted = [...deletedPaths]
|
|
111
145
|
.map((path) => ({ path, before: originals.get(path) }))
|
|
112
146
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
113
|
-
const operations = [...changed, ...deleted].sort((left, right) =>
|
|
147
|
+
const operations = [...changed, ...deleted].sort((left, right) =>
|
|
148
|
+
left.path.localeCompare(right.path),
|
|
149
|
+
);
|
|
114
150
|
const changedFiles = operations.map((file) => normalizedRelativePath(cwd, file.path));
|
|
115
151
|
const diff = operations
|
|
116
|
-
.map((file) =>
|
|
117
|
-
|
|
152
|
+
.map((file) =>
|
|
153
|
+
unifiedDiff(
|
|
154
|
+
normalizedRelativePath(cwd, file.path),
|
|
155
|
+
file.before,
|
|
156
|
+
"source" in file ? file.source : "",
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
.join("");
|
|
118
160
|
|
|
119
161
|
if (options.write && operations.length > 0) {
|
|
120
162
|
await verifySnapshots(snapshots);
|
|
121
163
|
await writeChanges(changed, deleted);
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
warnings.sort(
|
|
125
|
-
|
|
166
|
+
warnings.sort(
|
|
167
|
+
(left, right) =>
|
|
168
|
+
left.file.localeCompare(right.file) ||
|
|
169
|
+
left.start - right.start ||
|
|
170
|
+
left.end - right.end ||
|
|
171
|
+
left.code.localeCompare(right.code),
|
|
172
|
+
);
|
|
126
173
|
failures.sort((left, right) => left.package.localeCompare(right.package));
|
|
127
174
|
return {
|
|
128
175
|
changedFiles,
|
|
129
176
|
diff,
|
|
130
177
|
convertedRules,
|
|
131
178
|
retainedRules,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
179
|
+
// `stylesheet` is internal compile-failure attribution, not public shape.
|
|
180
|
+
rules: rules.map(({ stylesheet: _, ...rule }) => ({
|
|
181
|
+
...rule,
|
|
182
|
+
file: normalizedRelativePath(cwd, rule.file),
|
|
183
|
+
})),
|
|
184
|
+
candidates: [...candidates].sort(compareStrings),
|
|
185
|
+
warnings: warnings.map((warning) => ({
|
|
186
|
+
...warning,
|
|
187
|
+
file: normalizedRelativePath(cwd, warning.file),
|
|
188
|
+
})),
|
|
135
189
|
failures,
|
|
136
190
|
};
|
|
137
191
|
}
|
|
@@ -140,38 +194,43 @@ async function resolveScope(options) {
|
|
|
140
194
|
const cwd = await realpath(resolve(options.cwd ?? process.cwd()));
|
|
141
195
|
const currentPackage = await findPackageRoot(cwd);
|
|
142
196
|
const gitRoot = await findGitRoot(currentPackage);
|
|
143
|
-
const workspaceRoot =
|
|
144
|
-
? gitRoot
|
|
145
|
-
: currentPackage;
|
|
197
|
+
const workspaceRoot =
|
|
198
|
+
gitRoot && !(await isIgnoredByGit(gitRoot, currentPackage)) ? gitRoot : currentPackage;
|
|
146
199
|
const allPaths = await discoverFiles(workspaceRoot, workspaceRoot === gitRoot);
|
|
147
200
|
// Ignore filtering scopes what gets migrated, never what gets scanned:
|
|
148
201
|
// gitignored consumers and stylesheets must still block unsafe deletion.
|
|
149
|
-
const scannedPaths =
|
|
150
|
-
|
|
151
|
-
|
|
202
|
+
const scannedPaths =
|
|
203
|
+
workspaceRoot === gitRoot
|
|
204
|
+
? [
|
|
205
|
+
...new Set([
|
|
206
|
+
...allPaths,
|
|
207
|
+
...(await collectFiles(workspaceRoot, isRelevantDiscoveredFile)),
|
|
208
|
+
]),
|
|
209
|
+
]
|
|
210
|
+
: [...allPaths];
|
|
152
211
|
const explicitStyle = options.styleFile ? resolve(cwd, options.styleFile) : undefined;
|
|
153
212
|
const configuredEntry = options.tailwindCss ? resolve(cwd, options.tailwindCss) : undefined;
|
|
154
|
-
if (explicitStyle && !isStylesheetPath(explicitStyle)) {
|
|
155
|
-
throw new TypeError(
|
|
213
|
+
if (explicitStyle && !isStylesheetPath(explicitStyle) && extension(explicitStyle) !== ".vue") {
|
|
214
|
+
throw new TypeError("Only .css, .scss, .sass, .less, and .vue files can be migrated");
|
|
156
215
|
}
|
|
157
|
-
if (configuredEntry && extension(configuredEntry) !==
|
|
158
|
-
throw new TypeError(
|
|
216
|
+
if (configuredEntry && extension(configuredEntry) !== ".css") {
|
|
217
|
+
throw new TypeError("The Tailwind CSS entry must be a .css file");
|
|
159
218
|
}
|
|
160
219
|
if (configuredEntry && !(await stat(configuredEntry)).isFile()) {
|
|
161
|
-
throw new TypeError(
|
|
220
|
+
throw new TypeError("The Tailwind CSS entry must be a file");
|
|
162
221
|
}
|
|
163
222
|
for (const path of [explicitStyle, configuredEntry]) {
|
|
164
223
|
if (path) await rejectSymlinkTarget(path, currentPackage);
|
|
165
224
|
}
|
|
166
225
|
if (explicitStyle && !isWithin(currentPackage, explicitStyle)) {
|
|
167
|
-
throw new TypeError(
|
|
226
|
+
throw new TypeError("The selected stylesheet must belong to the current package");
|
|
168
227
|
}
|
|
169
228
|
for (const path of [explicitStyle, configuredEntry]) {
|
|
170
229
|
if (path && !allPaths.includes(path)) allPaths.push(path);
|
|
171
230
|
if (path && !scannedPaths.includes(path)) scannedPaths.push(path);
|
|
172
231
|
}
|
|
173
|
-
allPaths.sort();
|
|
174
|
-
scannedPaths.sort();
|
|
232
|
+
allPaths.sort(compareStrings);
|
|
233
|
+
scannedPaths.sort(compareStrings);
|
|
175
234
|
const targetable = new Set(allPaths);
|
|
176
235
|
|
|
177
236
|
const allPackageRoots = await discoverPackageRoots(workspaceRoot, allPaths);
|
|
@@ -181,13 +240,15 @@ async function resolveScope(options) {
|
|
|
181
240
|
const owner = await findPackageRoot(dirname(path));
|
|
182
241
|
if (!allPackageRoots.includes(owner)) allPackageRoots.push(owner);
|
|
183
242
|
}
|
|
184
|
-
allPackageRoots.sort();
|
|
185
|
-
const pathOwners = new Map(
|
|
243
|
+
allPackageRoots.sort(compareStrings);
|
|
244
|
+
const pathOwners = new Map(
|
|
245
|
+
scannedPaths.map((path) => [path, owningPackage(path, allPackageRoots)]),
|
|
246
|
+
);
|
|
186
247
|
if (explicitStyle && pathOwners.get(explicitStyle) !== currentPackage) {
|
|
187
|
-
throw new TypeError(
|
|
248
|
+
throw new TypeError("The selected stylesheet must belong to the current package");
|
|
188
249
|
}
|
|
189
250
|
if (configuredEntry && pathOwners.get(configuredEntry) !== currentPackage) {
|
|
190
|
-
throw new TypeError(
|
|
251
|
+
throw new TypeError("The Tailwind CSS entry must belong to the current package");
|
|
191
252
|
}
|
|
192
253
|
const selectedPackages = options.workspaces ? allPackageRoots : [currentPackage];
|
|
193
254
|
return {
|
|
@@ -216,6 +277,7 @@ async function planPackage(context, packageRoot) {
|
|
|
216
277
|
pathOwners,
|
|
217
278
|
targetable,
|
|
218
279
|
writablePackages,
|
|
280
|
+
scannedPaths,
|
|
219
281
|
} = context;
|
|
220
282
|
let preparedHtml;
|
|
221
283
|
try {
|
|
@@ -231,15 +293,50 @@ async function planPackage(context, packageRoot) {
|
|
|
231
293
|
if (!options.force || isIntegrityError(error)) throw error;
|
|
232
294
|
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
233
295
|
}
|
|
296
|
+
let preparedVue;
|
|
297
|
+
try {
|
|
298
|
+
preparedVue = await preparePackageVue({
|
|
299
|
+
packageRoot,
|
|
300
|
+
sourceFiles,
|
|
301
|
+
styleSources,
|
|
302
|
+
pathOwners,
|
|
303
|
+
targetable,
|
|
304
|
+
explicitStyle,
|
|
305
|
+
snapshots,
|
|
306
|
+
workspaceRoot,
|
|
307
|
+
workspaces: options.workspaces,
|
|
308
|
+
scannedPaths,
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (!options.force || isIntegrityError(error)) throw error;
|
|
312
|
+
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
313
|
+
}
|
|
314
|
+
// An explicit .vue selection that produced nothing to plan must surface
|
|
315
|
+
// its retention warnings without requiring an unrelated Tailwind entry.
|
|
316
|
+
if (
|
|
317
|
+
explicitStyle &&
|
|
318
|
+
extension(explicitStyle) === ".vue" &&
|
|
319
|
+
preparedVue.stylesheets.length === 0 &&
|
|
320
|
+
![...preparedVue.stylePaths].some((path) => !isStylesheetModule(path))
|
|
321
|
+
) {
|
|
322
|
+
return vueWarningsOnlyResult(preparedVue);
|
|
323
|
+
}
|
|
234
324
|
const packageSources = [
|
|
235
|
-
...sourceFiles
|
|
325
|
+
...sourceFiles
|
|
326
|
+
.filter((file) => extension(file.path) !== ".html")
|
|
327
|
+
.map((file) => preparedVue.files.get(file.path) ?? file),
|
|
236
328
|
...preparedHtml.files,
|
|
237
329
|
];
|
|
238
330
|
const ownedStyles = [...styleSources.keys()].filter(
|
|
239
|
-
(path) =>
|
|
240
|
-
|
|
331
|
+
(path) =>
|
|
332
|
+
pathOwners.get(path) === packageRoot &&
|
|
333
|
+
(targetable.has(path) ||
|
|
334
|
+
preparedHtml.stylePaths.has(path) ||
|
|
335
|
+
preparedVue.stylePaths.has(path)),
|
|
241
336
|
);
|
|
242
|
-
if (ownedStyles.length === 0)
|
|
337
|
+
if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) {
|
|
338
|
+
return vueWarningsOnlyResult(preparedVue);
|
|
339
|
+
}
|
|
243
340
|
|
|
244
341
|
let tailwindPath;
|
|
245
342
|
let tailwindEntries;
|
|
@@ -255,18 +352,27 @@ async function planPackage(context, packageRoot) {
|
|
|
255
352
|
}
|
|
256
353
|
|
|
257
354
|
const excludedEntries = new Set([...tailwindEntries, tailwindPath]);
|
|
258
|
-
const
|
|
259
|
-
?
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
355
|
+
const explicitCss =
|
|
356
|
+
explicitStyle && extension(explicitStyle) !== ".vue" ? explicitStyle : undefined;
|
|
357
|
+
const targets = explicitCss
|
|
358
|
+
? [explicitCss]
|
|
359
|
+
: explicitStyle
|
|
360
|
+
? [...preparedVue.stylePaths].filter(
|
|
361
|
+
(path) => !isStylesheetModule(path) && !excludedEntries.has(path),
|
|
362
|
+
)
|
|
363
|
+
: ownedStyles.filter(
|
|
364
|
+
(path) =>
|
|
365
|
+
!excludedEntries.has(path) &&
|
|
366
|
+
!preparedHtml.generatedPaths.has(path) &&
|
|
367
|
+
(!isPreprocessorPath(path) ||
|
|
368
|
+
preparedHtml.stylePaths.has(path) ||
|
|
369
|
+
packageSources.some((file) => sourceReferencesStyle(file, path))),
|
|
370
|
+
);
|
|
371
|
+
if (targets.length === 0 && preparedVue.stylesheets.length === 0) {
|
|
372
|
+
return vueWarningsOnlyResult(preparedVue);
|
|
373
|
+
}
|
|
268
374
|
if (targets.some((path) => excludedEntries.has(path))) {
|
|
269
|
-
throw new Error(
|
|
375
|
+
throw new Error("The Tailwind CSS entry cannot be migrated.");
|
|
270
376
|
}
|
|
271
377
|
|
|
272
378
|
let tailwind;
|
|
@@ -281,8 +387,9 @@ async function planPackage(context, packageRoot) {
|
|
|
281
387
|
const owner = pathOwners.get(file.path);
|
|
282
388
|
return {
|
|
283
389
|
...file,
|
|
284
|
-
writable:
|
|
285
|
-
|
|
390
|
+
writable:
|
|
391
|
+
targetable.has(file.path) &&
|
|
392
|
+
(options.workspaces ? writablePackages.has(owner) : owner === packageRoot),
|
|
286
393
|
};
|
|
287
394
|
});
|
|
288
395
|
let stylesheets;
|
|
@@ -291,9 +398,9 @@ async function planPackage(context, packageRoot) {
|
|
|
291
398
|
try {
|
|
292
399
|
stylesheets = [];
|
|
293
400
|
const compilerDependents = new Map();
|
|
294
|
-
for (const stylePath of targets.sort()) {
|
|
401
|
+
for (const stylePath of targets.sort(compareStrings)) {
|
|
295
402
|
await rejectSymlinkTarget(stylePath, packageRoot);
|
|
296
|
-
const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith(
|
|
403
|
+
const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
|
|
297
404
|
const stylesheet = {
|
|
298
405
|
cssPath: stylePath,
|
|
299
406
|
cssSource: styleSources.get(stylePath),
|
|
@@ -309,7 +416,7 @@ async function planPackage(context, packageRoot) {
|
|
|
309
416
|
// Compile the snapshotted source, not the on-disk file: code loaded
|
|
310
417
|
// during planning (e.g. Tailwind plugins) may have rewritten it since.
|
|
311
418
|
compiled = await compileSassEntry(sass, stylePath, stylesheet.cssSource);
|
|
312
|
-
} else if (extension(stylePath) ===
|
|
419
|
+
} else if (extension(stylePath) === ".less") {
|
|
313
420
|
less ??= await loadProjectLess(packageRoot);
|
|
314
421
|
compiled = await compileLessEntry(less, stylePath, stylesheet.cssSource);
|
|
315
422
|
}
|
|
@@ -317,7 +424,7 @@ async function planPackage(context, packageRoot) {
|
|
|
317
424
|
validateCss(compiled.css);
|
|
318
425
|
for (const loadedPath of compiled.loadedPaths) {
|
|
319
426
|
if (!isProjectInput(workspaceRoot, loadedPath)) continue;
|
|
320
|
-
const source = await
|
|
427
|
+
const source = await snapshotLoadedSource(snapshots, loadedPath);
|
|
321
428
|
if (!styleSources.has(loadedPath)) styleSources.set(loadedPath, source);
|
|
322
429
|
if (loadedPath !== stylePath) {
|
|
323
430
|
const dependents = compilerDependents.get(loadedPath) ?? [];
|
|
@@ -334,13 +441,16 @@ async function planPackage(context, packageRoot) {
|
|
|
334
441
|
const dependents = compilerDependents.get(stylesheet.cssPath) ?? [];
|
|
335
442
|
if (dependents.length === 0) continue;
|
|
336
443
|
stylesheet.isPartial = true;
|
|
337
|
-
stylesheet.cssDependents = [...new Set([...stylesheet.cssDependents, ...dependents])].sort(
|
|
444
|
+
stylesheet.cssDependents = [...new Set([...stylesheet.cssDependents, ...dependents])].sort(
|
|
445
|
+
compareStrings,
|
|
446
|
+
);
|
|
338
447
|
}
|
|
339
448
|
} catch (error) {
|
|
340
449
|
if (!options.force || isIntegrityError(error)) throw error;
|
|
341
450
|
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
342
451
|
}
|
|
343
452
|
|
|
453
|
+
stylesheets.push(...preparedVue.stylesheets);
|
|
344
454
|
const request = {
|
|
345
455
|
stylesheets,
|
|
346
456
|
tailwindPath: tailwind.path,
|
|
@@ -366,18 +476,75 @@ async function planPackage(context, packageRoot) {
|
|
|
366
476
|
|
|
367
477
|
removeMigratedHtmlLinks(plan, preparedHtml);
|
|
368
478
|
plan.warnings.push(...preparedHtml.warnings);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
|
|
374
|
-
if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
|
|
375
|
-
const source = changed?.source ?? '';
|
|
376
|
-
if (isSassPath(stylesheet.cssPath)) {
|
|
377
|
-
validateCss((await compileSassEntry(sass, stylesheet.cssPath, source)).css);
|
|
378
|
-
} else {
|
|
379
|
-
validateCss((await compileLessEntry(less, stylesheet.cssPath, source)).css);
|
|
479
|
+
plan.warnings.push(...preparedVue.warnings);
|
|
480
|
+
try {
|
|
481
|
+
for (const file of plan.files.filter((file) => extension(file.path) === ".html")) {
|
|
482
|
+
parseHtmlSource(file.path, file.source);
|
|
380
483
|
}
|
|
484
|
+
for (const file of plan.files.filter((file) => extension(file.path) === ".vue")) {
|
|
485
|
+
const includeUnscoped = preparedVue.unscopedPaths.has(file.path);
|
|
486
|
+
const blocks = verifyVueSource(preparedVue.compiler, file.path, file.source, includeUnscoped);
|
|
487
|
+
// Blocks the migration never edited are byte-identical to the authored
|
|
488
|
+
// input and need no recompilation; a caller that only received a
|
|
489
|
+
// template edit must not suddenly require a preprocessor.
|
|
490
|
+
const untouched = new Map();
|
|
491
|
+
for (const block of verifyVueSource(
|
|
492
|
+
preparedVue.compiler,
|
|
493
|
+
file.path,
|
|
494
|
+
snapshots.get(file.path),
|
|
495
|
+
includeUnscoped,
|
|
496
|
+
)) {
|
|
497
|
+
const key = `${block.syntax}\0${block.content}`;
|
|
498
|
+
untouched.set(key, (untouched.get(key) ?? 0) + 1);
|
|
499
|
+
}
|
|
500
|
+
for (const [index, block] of blocks.entries()) {
|
|
501
|
+
const key = `${block.syntax}\0${block.content}`;
|
|
502
|
+
const remaining = untouched.get(key) ?? 0;
|
|
503
|
+
if (remaining > 0) {
|
|
504
|
+
untouched.set(key, remaining - 1);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (block.syntax === "css") {
|
|
508
|
+
validateCss(block.content);
|
|
509
|
+
} else if (block.syntax === "less") {
|
|
510
|
+
validateCss(
|
|
511
|
+
(
|
|
512
|
+
await compileLessEntry(
|
|
513
|
+
(preparedVue.less ??= await loadProjectLess(packageRoot)),
|
|
514
|
+
`${file.path}.${index}.less`,
|
|
515
|
+
block.content,
|
|
516
|
+
)
|
|
517
|
+
).css,
|
|
518
|
+
);
|
|
519
|
+
} else {
|
|
520
|
+
validateCss(
|
|
521
|
+
(
|
|
522
|
+
await compileSassEntry(
|
|
523
|
+
(preparedVue.sass ??= await loadProjectSass(packageRoot)),
|
|
524
|
+
`${file.path}.${index}.${block.syntax}`,
|
|
525
|
+
block.content,
|
|
526
|
+
{ virtualEntry: true },
|
|
527
|
+
)
|
|
528
|
+
).css,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const stylesheet of stylesheets.filter((stylesheet) =>
|
|
534
|
+
isPreprocessorPath(stylesheet.cssPath),
|
|
535
|
+
)) {
|
|
536
|
+
const changed = plan.files.find((file) => file.path === stylesheet.cssPath);
|
|
537
|
+
if (!changed && !plan.deletedFiles.includes(stylesheet.cssPath)) continue;
|
|
538
|
+
const source = changed?.source ?? "";
|
|
539
|
+
if (isSassPath(stylesheet.cssPath)) {
|
|
540
|
+
validateCss((await compileSassEntry(sass, stylesheet.cssPath, source)).css);
|
|
541
|
+
} else {
|
|
542
|
+
validateCss((await compileLessEntry(less, stylesheet.cssPath, source)).css);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (!options.force || !isMissingStyleCompilerError(error)) throw error;
|
|
547
|
+
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
381
548
|
}
|
|
382
549
|
return { plan };
|
|
383
550
|
}
|
|
@@ -398,12 +565,18 @@ function replanCompileFailures(tailwind, request, initialPlan) {
|
|
|
398
565
|
for (const rule of plan.rules) {
|
|
399
566
|
const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
|
|
400
567
|
if (failed.length === 0) continue;
|
|
401
|
-
|
|
402
|
-
|
|
568
|
+
// Keyed by entry index, not cssPath: same-path Vue entries (scoped and
|
|
569
|
+
// module blocks) reuse local rule spans, so path-level attribution
|
|
570
|
+
// would block unrelated rules in the sibling entry.
|
|
571
|
+
let blocked = blockedByStylesheet.get(rule.stylesheet);
|
|
572
|
+
if (!blocked) blockedByStylesheet.set(rule.stylesheet, (blocked = new Map()));
|
|
403
573
|
const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
|
|
404
574
|
let entry = blocked.get(key);
|
|
405
575
|
if (!entry) {
|
|
406
|
-
blocked.set(
|
|
576
|
+
blocked.set(
|
|
577
|
+
key,
|
|
578
|
+
(entry = { ruleId: rule.ruleId, authoredSpan: rule.authoredSpan, candidates: new Set() }),
|
|
579
|
+
);
|
|
407
580
|
progressed = true;
|
|
408
581
|
}
|
|
409
582
|
for (const candidate of failed) entry.candidates.add(candidate);
|
|
@@ -411,20 +584,29 @@ function replanCompileFailures(tailwind, request, initialPlan) {
|
|
|
411
584
|
if (!progressed || iteration >= maxIterations) {
|
|
412
585
|
throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
|
|
413
586
|
}
|
|
414
|
-
plan = JSON.parse(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
587
|
+
plan = JSON.parse(
|
|
588
|
+
planBatchMigration(
|
|
589
|
+
JSON.stringify({
|
|
590
|
+
...request,
|
|
591
|
+
stylesheets: request.stylesheets.map((stylesheet, index) => ({
|
|
592
|
+
...stylesheet,
|
|
593
|
+
blockedRules: [...(blockedByStylesheet.get(index)?.values() ?? [])].map(
|
|
594
|
+
(entry) => entry.ruleId,
|
|
595
|
+
),
|
|
596
|
+
})),
|
|
597
|
+
}),
|
|
598
|
+
),
|
|
599
|
+
);
|
|
422
600
|
}
|
|
423
|
-
for (const [
|
|
601
|
+
for (const [index, blocked] of blockedByStylesheet) {
|
|
602
|
+
const cssPath = request.stylesheets[index].cssPath;
|
|
424
603
|
for (const { authoredSpan, candidates } of blocked.values()) {
|
|
425
|
-
const failed = [...candidates]
|
|
604
|
+
const failed = [...candidates]
|
|
605
|
+
.sort(compareStrings)
|
|
606
|
+
.map((candidate) => `\`${candidate}\``)
|
|
607
|
+
.join(", ");
|
|
426
608
|
plan.warnings.push({
|
|
427
|
-
code:
|
|
609
|
+
code: "candidate-compilation-failure",
|
|
428
610
|
file: cssPath,
|
|
429
611
|
// ruleId is a compiled-domain span; warnings anchor to the authored file.
|
|
430
612
|
start: authoredSpan.start,
|
|
@@ -451,8 +633,8 @@ function removeMigratedHtmlLinks(plan, preparedHtml) {
|
|
|
451
633
|
const original = preparedHtml.files.find((file) => file.path === filePath);
|
|
452
634
|
if (!original) continue;
|
|
453
635
|
const source = planned?.source ?? original.source;
|
|
454
|
-
const links = parseHtmlSource(filePath, source)
|
|
455
|
-
.filter((link) => removable.has(`${link.href}\0${link.media}`))
|
|
636
|
+
const links = parseHtmlSource(filePath, source)
|
|
637
|
+
.links.filter((link) => removable.has(`${link.href}\0${link.media}`))
|
|
456
638
|
.sort((left, right) => right.tagStart - left.tagStart);
|
|
457
639
|
let bytes = Buffer.from(source);
|
|
458
640
|
for (const link of links) {
|
|
@@ -477,14 +659,16 @@ function mergePlans(plans, originals) {
|
|
|
477
659
|
|
|
478
660
|
for (const plan of plans) {
|
|
479
661
|
for (const file of plan.files) {
|
|
480
|
-
if (!originals.has(file.path))
|
|
662
|
+
if (!originals.has(file.path))
|
|
663
|
+
throw new Error(`Planned file is outside the source snapshot: ${file.path}`);
|
|
481
664
|
if (filesByPath.has(file.path) || deletedPaths.has(file.path)) {
|
|
482
665
|
throw new Error(`Multiple package groups planned changes for ${file.path}`);
|
|
483
666
|
}
|
|
484
667
|
filesByPath.set(file.path, file);
|
|
485
668
|
}
|
|
486
669
|
for (const path of plan.deletedFiles) {
|
|
487
|
-
if (!originals.has(path))
|
|
670
|
+
if (!originals.has(path))
|
|
671
|
+
throw new Error(`Planned deletion is outside the source snapshot: ${path}`);
|
|
488
672
|
if (filesByPath.has(path) || deletedPaths.has(path)) {
|
|
489
673
|
throw new Error(`Multiple package groups planned changes for ${path}`);
|
|
490
674
|
}
|
|
@@ -510,10 +694,10 @@ async function findPackageRoot(start) {
|
|
|
510
694
|
let directory = start;
|
|
511
695
|
while (true) {
|
|
512
696
|
try {
|
|
513
|
-
await readFile(join(directory,
|
|
697
|
+
await readFile(join(directory, "package.json"), "utf8");
|
|
514
698
|
return directory;
|
|
515
699
|
} catch (error) {
|
|
516
|
-
if (error.code !==
|
|
700
|
+
if (error.code !== "ENOENT") throw error;
|
|
517
701
|
}
|
|
518
702
|
const parent = dirname(directory);
|
|
519
703
|
if (parent === directory) throw new Error(`No package.json was found from ${start}`);
|
|
@@ -523,7 +707,7 @@ async function findPackageRoot(start) {
|
|
|
523
707
|
|
|
524
708
|
async function findGitRoot(cwd) {
|
|
525
709
|
try {
|
|
526
|
-
const { stdout } = await run(
|
|
710
|
+
const { stdout } = await run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
527
711
|
return resolve(stdout.trim());
|
|
528
712
|
} catch {
|
|
529
713
|
return undefined;
|
|
@@ -533,7 +717,7 @@ async function findGitRoot(cwd) {
|
|
|
533
717
|
async function isIgnoredByGit(gitRoot, path) {
|
|
534
718
|
if (gitRoot === path) return false;
|
|
535
719
|
try {
|
|
536
|
-
await run(
|
|
720
|
+
await run("git", ["check-ignore", "-q", "--", relative(gitRoot, path)], { cwd: gitRoot });
|
|
537
721
|
return true;
|
|
538
722
|
} catch {
|
|
539
723
|
return false;
|
|
@@ -543,37 +727,36 @@ async function isIgnoredByGit(gitRoot, path) {
|
|
|
543
727
|
async function discoverFiles(root, useGit) {
|
|
544
728
|
if (!useGit) return collectFiles(root, isRelevantDiscoveredFile);
|
|
545
729
|
|
|
546
|
-
const { stdout } = await run(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
);
|
|
730
|
+
const { stdout } = await run("git", ["ls-files", "-co", "--exclude-standard", "-z", "--", "."], {
|
|
731
|
+
cwd: root,
|
|
732
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
733
|
+
});
|
|
551
734
|
const paths = stdout
|
|
552
|
-
.split(
|
|
735
|
+
.split("\0")
|
|
553
736
|
.filter(Boolean)
|
|
554
737
|
.map((path) => resolve(root, path))
|
|
555
738
|
.filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
|
|
556
|
-
const existing = await Promise.all(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
739
|
+
const existing = await Promise.all(
|
|
740
|
+
paths.map(async (path) => {
|
|
741
|
+
try {
|
|
742
|
+
return (await stat(path)).isFile() ? path : undefined;
|
|
743
|
+
} catch {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
return existing.filter(Boolean).sort(compareStrings);
|
|
564
749
|
}
|
|
565
750
|
|
|
566
751
|
async function discoverPackageRoots(workspaceRoot, paths) {
|
|
567
|
-
const roots = paths
|
|
568
|
-
.filter((path) => basename(path) === 'package.json')
|
|
569
|
-
.map(dirname);
|
|
752
|
+
const roots = paths.filter((path) => basename(path) === "package.json").map(dirname);
|
|
570
753
|
try {
|
|
571
|
-
await readFile(join(workspaceRoot,
|
|
754
|
+
await readFile(join(workspaceRoot, "package.json"), "utf8");
|
|
572
755
|
roots.push(workspaceRoot);
|
|
573
756
|
} catch (error) {
|
|
574
|
-
if (error.code !==
|
|
757
|
+
if (error.code !== "ENOENT") throw error;
|
|
575
758
|
}
|
|
576
|
-
return [...new Set(roots)].sort();
|
|
759
|
+
return [...new Set(roots)].sort(compareStrings);
|
|
577
760
|
}
|
|
578
761
|
|
|
579
762
|
function owningPackage(path, packageRoots) {
|
|
@@ -587,13 +770,17 @@ function isWithin(root, path) {
|
|
|
587
770
|
}
|
|
588
771
|
|
|
589
772
|
function hasIgnoredDirectory(root, path) {
|
|
590
|
-
return relative(root, path)
|
|
773
|
+
return relative(root, path)
|
|
774
|
+
.split(/[\\/]/)
|
|
775
|
+
.some((part) => IGNORED_DIRECTORIES.has(part));
|
|
591
776
|
}
|
|
592
777
|
|
|
593
778
|
function isRelevantDiscoveredFile(path) {
|
|
594
|
-
return
|
|
595
|
-
|
|
596
|
-
|
|
779
|
+
return (
|
|
780
|
+
basename(path) === "package.json" ||
|
|
781
|
+
isStylesheetPath(path) ||
|
|
782
|
+
SOURCE_EXTENSIONS.has(extension(path))
|
|
783
|
+
);
|
|
597
784
|
}
|
|
598
785
|
|
|
599
786
|
function isStylesheetPath(path) {
|
|
@@ -611,9 +798,707 @@ function isStylesheetModule(path) {
|
|
|
611
798
|
|
|
612
799
|
function sourceReferencesStyle(file, stylePath) {
|
|
613
800
|
let importPath = normalizedRelativePath(dirname(file.path), stylePath);
|
|
614
|
-
if (!importPath.startsWith(
|
|
615
|
-
return [`'${importPath}'`, `"${importPath}"`, `\`${importPath}\``]
|
|
616
|
-
|
|
801
|
+
if (!importPath.startsWith(".")) importPath = `./${importPath}`;
|
|
802
|
+
return [`'${importPath}'`, `"${importPath}"`, `\`${importPath}\``].some((literal) =>
|
|
803
|
+
file.source.includes(literal),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function isLocalVueReference(reference) {
|
|
808
|
+
return (
|
|
809
|
+
reference.startsWith(".") ||
|
|
810
|
+
reference.startsWith("/") ||
|
|
811
|
+
reference.startsWith("@/") ||
|
|
812
|
+
reference.startsWith("~/") ||
|
|
813
|
+
reference.startsWith("#") ||
|
|
814
|
+
reference.includes("/") ||
|
|
815
|
+
reference.includes(".vue")
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// An unresolved local reference only threatens the caller proof when it
|
|
820
|
+
// could actually name a Vue file: `.vue` spellings, extensionless paths, and
|
|
821
|
+
// aliases. An explicit foreign extension (`./utils.ts`) cannot.
|
|
822
|
+
function couldResolveToVue(reference) {
|
|
823
|
+
const path = reference.split(/[?#]/, 1)[0];
|
|
824
|
+
return path.endsWith(".vue") || extension(path) === "";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function vueReferenceTarget(importer, reference, vuePaths) {
|
|
828
|
+
if (!reference.startsWith(".")) return undefined;
|
|
829
|
+
const target = resolve(dirname(importer), reference);
|
|
830
|
+
return [target, `${target}.vue`, join(target, "index.vue")].find((path) => vuePaths.has(path));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function vueLiteralTargets(file, vuePaths) {
|
|
834
|
+
return new Set(
|
|
835
|
+
[...file.source.matchAll(/(["'`])([^"'`\r\n]+)\1/g)].flatMap((match) => {
|
|
836
|
+
// A local glob can match `.vue` files without spelling the extension
|
|
837
|
+
// (`import.meta.glob("./components/*")`), so any glob-bearing local
|
|
838
|
+
// pattern conservatively opens every caller surface.
|
|
839
|
+
if (
|
|
840
|
+
/[?*[\]{}]/.test(match[2]) &&
|
|
841
|
+
(match[2].includes("vue") || isLocalVueReference(match[2]))
|
|
842
|
+
) {
|
|
843
|
+
return [...vuePaths];
|
|
844
|
+
}
|
|
845
|
+
const target = vueReferenceTarget(file.path, match[2], vuePaths);
|
|
846
|
+
if (target) return [target];
|
|
847
|
+
return match[2].includes(".vue") ? [...vuePaths] : [];
|
|
848
|
+
}),
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function normalizedVueTag(value) {
|
|
853
|
+
return value.replaceAll("-", "").toLowerCase();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function buildVueComponentGraph(
|
|
857
|
+
ownedVue,
|
|
858
|
+
sourceFiles,
|
|
859
|
+
analyses,
|
|
860
|
+
{ styleSources, pathOwners, packageRoot },
|
|
861
|
+
) {
|
|
862
|
+
const vuePaths = new Set(ownedVue.map((file) => file.path));
|
|
863
|
+
const callers = new Map([...vuePaths].map((path) => [path, []]));
|
|
864
|
+
const callerOpen = new Set();
|
|
865
|
+
// A stylesheet import that does not resolve to a package-owned file (a
|
|
866
|
+
// dependency or aliased stylesheet) still loads globally and can shadow
|
|
867
|
+
// scoped deletions, but its selectors cannot be analyzed.
|
|
868
|
+
let unresolvedStyleImport = false;
|
|
869
|
+
const stylesheetReference = /\.(?:css|scss|sass|less)(?:$|[?#])/;
|
|
870
|
+
|
|
871
|
+
for (const file of ownedVue) {
|
|
872
|
+
const analysis = analyses.get(file.path);
|
|
873
|
+
if (analysis.retained) {
|
|
874
|
+
for (const target of vuePaths) callerOpen.add(target);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const bindings = analysis.componentImports
|
|
878
|
+
.map((entry) => ({
|
|
879
|
+
...entry,
|
|
880
|
+
target: vueReferenceTarget(file.path, entry.source, vuePaths),
|
|
881
|
+
}))
|
|
882
|
+
.filter((entry) => entry.target);
|
|
883
|
+
analysis.resolvedComponents = analysis.componentSites.flatMap((site) => {
|
|
884
|
+
const matches = bindings.filter(
|
|
885
|
+
(entry) => normalizedVueTag(entry.local) === normalizedVueTag(site.tag),
|
|
886
|
+
);
|
|
887
|
+
if (matches.length !== 1) return [];
|
|
888
|
+
const edge = { parent: file.path, child: matches[0].target, site };
|
|
889
|
+
callers.get(edge.child).push(edge);
|
|
890
|
+
if (site.idAttribute || analysis.dynamic) {
|
|
891
|
+
callerOpen.add(edge.child);
|
|
892
|
+
analyses.get(edge.child).rootIdsOverridden = true;
|
|
893
|
+
}
|
|
894
|
+
// A component rendered as a single-root parent's root chains the
|
|
895
|
+
// parent's own fallthrough surface down to this child; that chain is
|
|
896
|
+
// not modeled, so the child cannot be closed.
|
|
897
|
+
if (analysis.alwaysRenderedRoots < 2 && analysis.rootStarts.includes(site.nodeStart)) {
|
|
898
|
+
callerOpen.add(edge.child);
|
|
899
|
+
}
|
|
900
|
+
return [edge];
|
|
901
|
+
});
|
|
902
|
+
analysis.componentsOpen = analysis.resolvedComponents.length !== analysis.componentSites.length;
|
|
903
|
+
analysis.setupImports = new Set(bindings.map((entry) => entry.target));
|
|
904
|
+
if (analysis.componentsOpen) {
|
|
905
|
+
for (const target of vuePaths) callerOpen.add(target);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
for (const file of sourceFiles) {
|
|
910
|
+
let references;
|
|
911
|
+
let blockReferences = [];
|
|
912
|
+
if (extension(file.path) === ".vue") {
|
|
913
|
+
const fileAnalysis = analyses.get(file.path);
|
|
914
|
+
references = fileAnalysis?.scriptStyleImports ?? [];
|
|
915
|
+
blockReferences = fileAnalysis?.styleBlockImports?.map((entry) => entry.reference) ?? [];
|
|
916
|
+
} else {
|
|
917
|
+
try {
|
|
918
|
+
references = staticImports(file.path, file.source);
|
|
919
|
+
} catch {
|
|
920
|
+
references = [];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (
|
|
924
|
+
/\bimport\s*\(/.test(file.source) ||
|
|
925
|
+
references.some(
|
|
926
|
+
(reference) =>
|
|
927
|
+
!stylesheetReference.test(reference) &&
|
|
928
|
+
isLocalVueReference(reference) &&
|
|
929
|
+
couldResolveToVue(reference) &&
|
|
930
|
+
!vueReferenceTarget(file.path, reference, vuePaths),
|
|
931
|
+
)
|
|
932
|
+
) {
|
|
933
|
+
for (const target of vuePaths) callerOpen.add(target);
|
|
934
|
+
}
|
|
935
|
+
const resolvesOwned = (reference) =>
|
|
936
|
+
stylesheetReferenceTargets(file.path, reference, styleSources).some(
|
|
937
|
+
(path) => pathOwners.get(path) === packageRoot,
|
|
938
|
+
);
|
|
939
|
+
if (
|
|
940
|
+
pathOwners.get(file.path) === packageRoot &&
|
|
941
|
+
(references.some(
|
|
942
|
+
(reference) =>
|
|
943
|
+
stylesheetReference.test(reference) &&
|
|
944
|
+
// Bare script specifiers load package CSS this analysis cannot see.
|
|
945
|
+
(!reference.startsWith(".") || !resolvesOwned(reference)),
|
|
946
|
+
) ||
|
|
947
|
+
blockReferences.some((reference) => !resolvesOwned(reference)))
|
|
948
|
+
) {
|
|
949
|
+
unresolvedStyleImport = true;
|
|
950
|
+
}
|
|
951
|
+
const imported = new Set(
|
|
952
|
+
references.flatMap((reference) => {
|
|
953
|
+
const target = vueReferenceTarget(file.path, reference, vuePaths);
|
|
954
|
+
return target ? [target] : [];
|
|
955
|
+
}),
|
|
956
|
+
);
|
|
957
|
+
for (const target of imported) {
|
|
958
|
+
if (extension(file.path) !== ".vue" || !analyses.get(file.path)?.setupImports?.has(target)) {
|
|
959
|
+
callerOpen.add(target);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
for (const target of vueLiteralTargets(file, vuePaths)) {
|
|
963
|
+
if (!imported.has(target)) callerOpen.add(target);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return { callers, callerOpen, unresolvedStyleImport };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Vue analysis can produce retention warnings even when nothing remains to
|
|
971
|
+
// plan (all blocks unsupported, unsupported Vue version); surface them
|
|
972
|
+
// through an otherwise empty plan instead of dropping them.
|
|
973
|
+
function vueWarningsOnlyResult(preparedVue) {
|
|
974
|
+
if (preparedVue.warnings.length === 0) return {};
|
|
975
|
+
return {
|
|
976
|
+
plan: {
|
|
977
|
+
files: [],
|
|
978
|
+
deletedFiles: [],
|
|
979
|
+
unlinkedFiles: [],
|
|
980
|
+
candidates: [],
|
|
981
|
+
rules: [],
|
|
982
|
+
warnings: preparedVue.warnings,
|
|
983
|
+
convertedRules: 0,
|
|
984
|
+
retainedRules: 0,
|
|
985
|
+
},
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Lower this package's own Vue SFCs into their planner dual identity: one
|
|
990
|
+
// stylesheet entry per SFC with migratable plain-CSS scoped blocks, plus a
|
|
991
|
+
// consumer entry carrying the template's literal class sites. Files whose
|
|
992
|
+
// blocks or templates cannot be analyzed keep their raw source and warn.
|
|
993
|
+
async function preparePackageVue({
|
|
994
|
+
packageRoot,
|
|
995
|
+
sourceFiles,
|
|
996
|
+
styleSources,
|
|
997
|
+
pathOwners,
|
|
998
|
+
targetable,
|
|
999
|
+
explicitStyle,
|
|
1000
|
+
snapshots,
|
|
1001
|
+
workspaceRoot,
|
|
1002
|
+
workspaces,
|
|
1003
|
+
scannedPaths,
|
|
1004
|
+
}) {
|
|
1005
|
+
const none = {
|
|
1006
|
+
files: new Map(),
|
|
1007
|
+
stylesheets: [],
|
|
1008
|
+
stylePaths: new Set(),
|
|
1009
|
+
unscopedPaths: new Set(),
|
|
1010
|
+
warnings: [],
|
|
1011
|
+
compiler: undefined,
|
|
1012
|
+
};
|
|
1013
|
+
// An explicit non-vue stylesheet selection plans only that stylesheet.
|
|
1014
|
+
if (explicitStyle && extension(explicitStyle) !== ".vue") return none;
|
|
1015
|
+
// Ignore filtering scopes what gets migrated, never what gets scanned: a
|
|
1016
|
+
// gitignored SFC's retained style blocks still shadow scoped deletions.
|
|
1017
|
+
const ownedVue = sourceFiles.filter(
|
|
1018
|
+
(file) => extension(file.path) === ".vue" && pathOwners.get(file.path) === packageRoot,
|
|
1019
|
+
);
|
|
1020
|
+
const selected = ownedVue.filter(
|
|
1021
|
+
(file) => targetable.has(file.path) && (!explicitStyle || file.path === explicitStyle),
|
|
1022
|
+
);
|
|
1023
|
+
if (selected.length === 0) return none;
|
|
1024
|
+
|
|
1025
|
+
const loaded = await loadProjectVueCompiler(packageRoot);
|
|
1026
|
+
const warnings = [];
|
|
1027
|
+
if (loaded.unsupportedVersion) {
|
|
1028
|
+
for (const file of selected) {
|
|
1029
|
+
warnings.push({
|
|
1030
|
+
code: "unsupported-vue-version",
|
|
1031
|
+
file: file.path,
|
|
1032
|
+
start: 0,
|
|
1033
|
+
end: 0,
|
|
1034
|
+
message: `Vue ${loaded.unsupportedVersion} is not supported; only Vue 3 SFCs are analyzed.`,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return { ...none, warnings };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Every owned SFC is analyzed even under explicit selection: retained
|
|
1041
|
+
// style blocks anywhere in the package feed the cascade-shadow gate below.
|
|
1042
|
+
const analyses = new Map(
|
|
1043
|
+
ownedVue.map((file) => [file.path, analyzeVueSource(loaded.compiler, file.path, file.source)]),
|
|
1044
|
+
);
|
|
1045
|
+
const selectedPaths = new Set(selected.map((file) => file.path));
|
|
1046
|
+
for (const file of ownedVue) {
|
|
1047
|
+
const analysis = analyses.get(file.path);
|
|
1048
|
+
if (analysis.retained) continue;
|
|
1049
|
+
for (const edge of analysis.styleBlockImports) {
|
|
1050
|
+
const local = stylesheetReferenceTargets(file.path, edge.reference, styleSources).some(
|
|
1051
|
+
(path) => pathOwners.get(path) === packageRoot,
|
|
1052
|
+
);
|
|
1053
|
+
if (local) continue;
|
|
1054
|
+
analysis.escapeUnverifiable = true;
|
|
1055
|
+
if (selectedPaths.has(file.path)) {
|
|
1056
|
+
warnings.push({
|
|
1057
|
+
code: "unsupported-sfc-block",
|
|
1058
|
+
file: file.path,
|
|
1059
|
+
start: edge.start,
|
|
1060
|
+
end: edge.end,
|
|
1061
|
+
message: `The external style ${JSON.stringify(edge.reference)} could not be resolved inside the package.`,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
let sass;
|
|
1067
|
+
let less;
|
|
1068
|
+
const compileBlocks = async (file, blocks) => {
|
|
1069
|
+
for (const block of blocks.filter((block) => block.syntax !== "css")) {
|
|
1070
|
+
const virtualPath = `${file.path}.${block.syntax}`;
|
|
1071
|
+
const compiled = isSassPath(virtualPath)
|
|
1072
|
+
? await compileSassEntry(
|
|
1073
|
+
(sass ??= await loadProjectSass(packageRoot)),
|
|
1074
|
+
virtualPath,
|
|
1075
|
+
block.content,
|
|
1076
|
+
{ virtualEntry: true },
|
|
1077
|
+
)
|
|
1078
|
+
: await compileLessEntry(
|
|
1079
|
+
(less ??= await loadProjectLess(packageRoot)),
|
|
1080
|
+
virtualPath,
|
|
1081
|
+
block.content,
|
|
1082
|
+
);
|
|
1083
|
+
validateCss(compiled.css);
|
|
1084
|
+
for (const loadedPath of compiled.loadedPaths) {
|
|
1085
|
+
if (loadedPath === virtualPath || !isProjectInput(workspaceRoot, loadedPath)) continue;
|
|
1086
|
+
const source = await snapshotLoadedSource(snapshots, loadedPath);
|
|
1087
|
+
if (!styleSources.has(loadedPath)) styleSources.set(loadedPath, source);
|
|
1088
|
+
if (!pathOwners.has(loadedPath)) pathOwners.set(loadedPath, packageRoot);
|
|
1089
|
+
}
|
|
1090
|
+
block.analysisSource = compiled.css;
|
|
1091
|
+
block.sourcePath = virtualPath;
|
|
1092
|
+
block.sourceMappings = compiled.sourceMappings;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
for (const file of selected) {
|
|
1096
|
+
const analysis = analyses.get(file.path);
|
|
1097
|
+
if (!analysis.retained) {
|
|
1098
|
+
await compileBlocks(file, [...analysis.blocks, ...analysis.moduleBlocks]);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const vueGraph = buildVueComponentGraph(ownedVue, sourceFiles, analyses, {
|
|
1102
|
+
styleSources,
|
|
1103
|
+
pathOwners,
|
|
1104
|
+
packageRoot,
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Non-scoped CSS in the package is unlayered and can outrank the layered
|
|
1108
|
+
// utilities that replace a deleted scoped rule. The planner retains any
|
|
1109
|
+
// scoped rule whose classes this pool also targets, so collect every
|
|
1110
|
+
// package stylesheet, HTML source (inline style blocks are never parsed),
|
|
1111
|
+
// and retained Vue style block.
|
|
1112
|
+
// The shadow corpus is a list of parseable CSS pieces whose selector
|
|
1113
|
+
// surface the planner indexes; anything whose selectors cannot be proven
|
|
1114
|
+
// (interpolation or `&`-concatenation in preprocessor text, inline HTML
|
|
1115
|
+
// style blocks, unanalyzable SFCs, unextractable escapes) marks the whole
|
|
1116
|
+
// corpus unverifiable and retains every closed deletion.
|
|
1117
|
+
const generatesSelectors = (text) => /#\{|@\{|&[\w-]/.test(text);
|
|
1118
|
+
const vueShadowCss = [];
|
|
1119
|
+
const vueShadowModuleCss = [];
|
|
1120
|
+
let vueShadowUnverifiable = vueGraph.unresolvedStyleImport;
|
|
1121
|
+
for (const [path, source] of styleSources) {
|
|
1122
|
+
if (pathOwners.get(path) !== packageRoot) continue;
|
|
1123
|
+
if (isPreprocessorPath(path) && generatesSelectors(source)) {
|
|
1124
|
+
vueShadowUnverifiable = true;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
// Module class and id names are localized at build time; the planner
|
|
1128
|
+
// indexes only their global (type/attribute/:global) selector surface.
|
|
1129
|
+
if (isStylesheetModule(path)) vueShadowModuleCss.push(source);
|
|
1130
|
+
else vueShadowCss.push({ path, source });
|
|
1131
|
+
}
|
|
1132
|
+
for (const file of sourceFiles) {
|
|
1133
|
+
if (
|
|
1134
|
+
extension(file.path) === ".html" &&
|
|
1135
|
+
pathOwners.get(file.path) === packageRoot &&
|
|
1136
|
+
/<style/i.test(file.source)
|
|
1137
|
+
) {
|
|
1138
|
+
vueShadowUnverifiable = true;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
for (const file of ownedVue) {
|
|
1142
|
+
const analysis = analyses.get(file.path);
|
|
1143
|
+
if (analysis.retained) {
|
|
1144
|
+
if (/<style/i.test(file.source)) vueShadowUnverifiable = true;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (analysis.escapeUnverifiable || analysis.scriptImportsUnverifiable) {
|
|
1148
|
+
vueShadowUnverifiable = true;
|
|
1149
|
+
}
|
|
1150
|
+
for (const text of analysis.shadowPreprocessorTexts) {
|
|
1151
|
+
if (generatesSelectors(text)) vueShadowUnverifiable = true;
|
|
1152
|
+
else vueShadowCss.push({ path: file.path, source: text });
|
|
1153
|
+
}
|
|
1154
|
+
for (const text of analysis.unscopedShadowPreprocessorTexts) {
|
|
1155
|
+
if (generatesSelectors(text)) vueShadowUnverifiable = true;
|
|
1156
|
+
else {
|
|
1157
|
+
vueShadowCss.push({ path: file.path, source: text, migratingUnscoped: true });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
vueShadowCss.push(
|
|
1161
|
+
...analysis.shadowCssTexts.map((source) => ({ path: file.path, source })),
|
|
1162
|
+
...analysis.unscopedShadowCssTexts.map((source) => ({
|
|
1163
|
+
path: file.path,
|
|
1164
|
+
source,
|
|
1165
|
+
migratingUnscoped: true,
|
|
1166
|
+
})),
|
|
1167
|
+
...analysis.blocks.map((block) => ({
|
|
1168
|
+
path: file.path,
|
|
1169
|
+
source: block.analysisSource ?? block.content,
|
|
1170
|
+
scoped: true,
|
|
1171
|
+
})),
|
|
1172
|
+
);
|
|
1173
|
+
vueShadowModuleCss.push(
|
|
1174
|
+
...analysis.shadowModuleCssTexts,
|
|
1175
|
+
...analysis.moduleBlocks.map((block) => block.analysisSource ?? block.content),
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const packageIsPrivate =
|
|
1180
|
+
JSON.parse(await snapshotFile(snapshots, join(packageRoot, "package.json"))).private === true;
|
|
1181
|
+
const files = new Map();
|
|
1182
|
+
const stylesheets = [];
|
|
1183
|
+
const stylePaths = new Set();
|
|
1184
|
+
const unscopedPaths = new Set();
|
|
1185
|
+
const ownedSources = sourceFiles.filter((file) => pathOwners.get(file.path) === packageRoot);
|
|
1186
|
+
// A scanned HTML file without a stylesheet link never enters sourceFiles,
|
|
1187
|
+
// but its class attributes are still a global usage surface (e.g. a
|
|
1188
|
+
// gitignored mount shell), so its existence alone defeats the proof.
|
|
1189
|
+
const sourcePathSet = new Set(sourceFiles.map((file) => file.path));
|
|
1190
|
+
const hiddenHtml = scannedPaths.some(
|
|
1191
|
+
(path) =>
|
|
1192
|
+
extension(path) === ".html" &&
|
|
1193
|
+
pathOwners.get(path) === packageRoot &&
|
|
1194
|
+
!sourcePathSet.has(path),
|
|
1195
|
+
);
|
|
1196
|
+
const projectWideUsageProven =
|
|
1197
|
+
packageIsPrivate &&
|
|
1198
|
+
workspaceRoot === packageRoot &&
|
|
1199
|
+
!workspaces &&
|
|
1200
|
+
!explicitStyle &&
|
|
1201
|
+
!hiddenHtml &&
|
|
1202
|
+
ownedSources.every((file) => targetable.has(file.path)) &&
|
|
1203
|
+
ownedVue.every((file) => !analyses.get(file.path).retained);
|
|
1204
|
+
const elementsByFile = new Map();
|
|
1205
|
+
const addElement = (path, element, cssPaths) => {
|
|
1206
|
+
if ((!element.classAttribute && !element.moduleBinding) || cssPaths.length === 0) return;
|
|
1207
|
+
const elements = elementsByFile.get(path) ?? [];
|
|
1208
|
+
elements.push({ ...element, cssPaths: [...new Set(cssPaths)] });
|
|
1209
|
+
elementsByFile.set(path, elements);
|
|
1210
|
+
};
|
|
1211
|
+
const elementClasses = (element) =>
|
|
1212
|
+
element.matchClasses ?? element.classAttribute?.value.split(/\s+/).filter(Boolean) ?? [];
|
|
1213
|
+
const componentRootSite = (site, root) => ({
|
|
1214
|
+
...site,
|
|
1215
|
+
matchClasses: [...elementClasses(site), ...elementClasses(root)],
|
|
1216
|
+
matchIds: [site.idAttribute?.value ?? root.idAttribute?.value].filter(Boolean),
|
|
1217
|
+
matchTag: root.tag,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
for (const file of selected) {
|
|
1221
|
+
const analysis = analyses.get(file.path);
|
|
1222
|
+
if (analysis.retained) continue;
|
|
1223
|
+
const importedStyles = new Set(
|
|
1224
|
+
[
|
|
1225
|
+
// A bare script specifier is a package import; resolving it against
|
|
1226
|
+
// the SFC directory could bind an unrelated local file.
|
|
1227
|
+
...analysis.scriptStyleImports
|
|
1228
|
+
.filter(
|
|
1229
|
+
(reference) =>
|
|
1230
|
+
reference.startsWith(".") &&
|
|
1231
|
+
STYLESHEET_SYNTAX.has(extension(reference.split(/[?#]/, 1)[0])),
|
|
1232
|
+
)
|
|
1233
|
+
.flatMap((reference) => stylesheetReferenceTargets(file.path, reference, styleSources)),
|
|
1234
|
+
...analysis.styleBlockImports.flatMap((entry) =>
|
|
1235
|
+
stylesheetReferenceTargets(file.path, entry.reference, styleSources),
|
|
1236
|
+
),
|
|
1237
|
+
].filter((path) => pathOwners.get(path) === packageRoot),
|
|
1238
|
+
);
|
|
1239
|
+
for (const path of importedStyles) {
|
|
1240
|
+
stylePaths.add(path);
|
|
1241
|
+
if (extension(path) !== ".css") continue;
|
|
1242
|
+
for (const imported of cssImports(styleSources.get(path))) {
|
|
1243
|
+
// Conditional imports need variant-aware contexts; retaining them is
|
|
1244
|
+
// safer than attaching an unconditional utility.
|
|
1245
|
+
if (imported.media) continue;
|
|
1246
|
+
for (const target of stylesheetReferenceTargets(path, imported.href, styleSources)) {
|
|
1247
|
+
if (pathOwners.get(target) === packageRoot) importedStyles.add(target);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const ownContexts = [
|
|
1252
|
+
...(analysis.blocks.length > 0 ||
|
|
1253
|
+
analysis.unscopedBlocks.length > 0 ||
|
|
1254
|
+
(analysis.moduleBlocks.length > 0 && !analysis.moduleClosureBroken)
|
|
1255
|
+
? [file.path]
|
|
1256
|
+
: []),
|
|
1257
|
+
...[...importedStyles].filter((path) => !isStylesheetModule(path)),
|
|
1258
|
+
];
|
|
1259
|
+
for (const element of analysis.htmlElements) {
|
|
1260
|
+
const ownElement =
|
|
1261
|
+
analysis.rootIdsOverridden && analysis.rootStarts.includes(element.nodeStart)
|
|
1262
|
+
? { ...element, matchIds: [] }
|
|
1263
|
+
: element;
|
|
1264
|
+
addElement(file.path, ownElement, ownContexts);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
for (const parent of ownedVue) {
|
|
1269
|
+
const analysis = analyses.get(parent.path);
|
|
1270
|
+
if (analysis.retained) continue;
|
|
1271
|
+
for (const edge of analysis.resolvedComponents) {
|
|
1272
|
+
const childAnalysis = analyses.get(edge.child);
|
|
1273
|
+
// Vue only inherits call-site attributes onto a single-root child, so
|
|
1274
|
+
// the total root count must gate the rewrite independently of writable sites.
|
|
1275
|
+
const singleRoot =
|
|
1276
|
+
childAnalysis?.rootStarts.length === 1 &&
|
|
1277
|
+
!childAnalysis.rootVFor &&
|
|
1278
|
+
!childAnalysis.rootFragment;
|
|
1279
|
+
const childRoots = childAnalysis?.htmlElements.filter((element) =>
|
|
1280
|
+
childAnalysis.rootStarts.includes(element.nodeStart),
|
|
1281
|
+
);
|
|
1282
|
+
if (
|
|
1283
|
+
selectedPaths.has(parent.path) &&
|
|
1284
|
+
singleRoot &&
|
|
1285
|
+
childRoots?.length === 1 &&
|
|
1286
|
+
!childAnalysis.fallthroughUnverifiable
|
|
1287
|
+
) {
|
|
1288
|
+
addElement(parent.path, componentRootSite(edge.site, childRoots[0]), [parent.path]);
|
|
1289
|
+
}
|
|
1290
|
+
if (
|
|
1291
|
+
selectedPaths.has(edge.child) &&
|
|
1292
|
+
singleRoot &&
|
|
1293
|
+
// A self-recursive edge is already covered by the parent-style site
|
|
1294
|
+
// above; adding the same span twice would produce overlapping edits.
|
|
1295
|
+
edge.parent !== edge.child &&
|
|
1296
|
+
!childAnalysis?.fallthroughUnverifiable
|
|
1297
|
+
) {
|
|
1298
|
+
addElement(parent.path, edge.site, [edge.child]);
|
|
1299
|
+
if (childRoots?.length === 1) {
|
|
1300
|
+
// The caller's classes land on the child root, so module planning
|
|
1301
|
+
// must see them next to the root's binding; the read-only class
|
|
1302
|
+
// attribute marks the record as shadow-only (never rewritten, never
|
|
1303
|
+
// counted as a reference).
|
|
1304
|
+
const shadowSite = {
|
|
1305
|
+
...componentRootSite(edge.site, childRoots[0]),
|
|
1306
|
+
moduleBinding: childRoots[0].moduleBinding,
|
|
1307
|
+
};
|
|
1308
|
+
if (shadowSite.classAttribute) {
|
|
1309
|
+
shadowSite.classAttribute = { ...shadowSite.classAttribute, writable: false };
|
|
1310
|
+
addElement(parent.path, shadowSite, [edge.child]);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
for (const file of selected) {
|
|
1318
|
+
await rejectSymlinkTarget(file.path, packageRoot);
|
|
1319
|
+
const analysis = analyses.get(file.path);
|
|
1320
|
+
warnings.push(...analysis.warnings);
|
|
1321
|
+
if (analysis.retained) continue;
|
|
1322
|
+
const componentOpen =
|
|
1323
|
+
analysis.componentsOpen ||
|
|
1324
|
+
analysis.resolvedComponents.some((edge) => {
|
|
1325
|
+
const child = analyses.get(edge.child);
|
|
1326
|
+
return (
|
|
1327
|
+
!child ||
|
|
1328
|
+
child.retained ||
|
|
1329
|
+
child.dynamic ||
|
|
1330
|
+
child.fallthroughUnverifiable ||
|
|
1331
|
+
child.rootVFor ||
|
|
1332
|
+
child.rootFragment ||
|
|
1333
|
+
child.rootStarts.length !== 1 ||
|
|
1334
|
+
!child.htmlElements.some((element) => element.nodeStart === child.rootStarts[0])
|
|
1335
|
+
);
|
|
1336
|
+
});
|
|
1337
|
+
const callers = vueGraph.callers.get(file.path);
|
|
1338
|
+
const callerOpen =
|
|
1339
|
+
!packageIsPrivate ||
|
|
1340
|
+
callers.length === 0 ||
|
|
1341
|
+
vueGraph.callerOpen.has(file.path) ||
|
|
1342
|
+
callers.some(
|
|
1343
|
+
(edge) => analyses.get(edge.parent)?.dynamic || analyses.get(edge.parent)?.retained,
|
|
1344
|
+
);
|
|
1345
|
+
const retention = analysis.dynamic
|
|
1346
|
+
? "dynamic-template-class"
|
|
1347
|
+
: componentOpen
|
|
1348
|
+
? "component-class-target"
|
|
1349
|
+
: analysis.alwaysRenderedRoots < 2 && callerOpen
|
|
1350
|
+
? "open-root-fallthrough"
|
|
1351
|
+
: undefined;
|
|
1352
|
+
// ponytail: unscoped deletion is limited to private single-source packages;
|
|
1353
|
+
// widen it only when the dependency graph can prove stylesheet co-loading.
|
|
1354
|
+
const migrateUnscoped =
|
|
1355
|
+
analysis.blocks.length === 0 &&
|
|
1356
|
+
analysis.unscopedBlocks.length > 0 &&
|
|
1357
|
+
projectWideUsageProven &&
|
|
1358
|
+
!analysis.dynamic &&
|
|
1359
|
+
// Injected and slotted content can use any class an unscoped rule
|
|
1360
|
+
// targets without ever receiving its utility.
|
|
1361
|
+
!analysis.vHtml &&
|
|
1362
|
+
!analysis.hasSlot &&
|
|
1363
|
+
!analysis.escapeUnverifiable &&
|
|
1364
|
+
!componentOpen &&
|
|
1365
|
+
ownedSources.length === 1 &&
|
|
1366
|
+
ownedSources[0].path === file.path;
|
|
1367
|
+
if (migrateUnscoped) {
|
|
1368
|
+
await compileBlocks(file, analysis.unscopedBlocks);
|
|
1369
|
+
unscopedPaths.add(file.path);
|
|
1370
|
+
}
|
|
1371
|
+
if (analysis.unscopedBlocks.length > 0 && !migrateUnscoped) {
|
|
1372
|
+
for (const block of analysis.unscopedBlocks) {
|
|
1373
|
+
warnings.push({
|
|
1374
|
+
code: "unscoped-style-block",
|
|
1375
|
+
file: file.path,
|
|
1376
|
+
start: block.contentStart,
|
|
1377
|
+
end: block.contentEnd,
|
|
1378
|
+
message: "The global reach of this unscoped style block could not be proven.",
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// Module classes are hashed, so corpus CSS cannot target them directly --
|
|
1383
|
+
// but unknown classes can still land on a binding element (a dynamic
|
|
1384
|
+
// surface anywhere in the template, or parent fallthrough onto a single
|
|
1385
|
+
// root) and compete for the same properties, so those surfaces must be
|
|
1386
|
+
// closed exactly like the scoped ones. `componentOpen` is exempt: a child
|
|
1387
|
+
// component's root can never carry the hashed class, because a `$style`
|
|
1388
|
+
// binding on a component tag is already an opaque (dynamic) surface.
|
|
1389
|
+
const moduleRetention = analysis.dynamic
|
|
1390
|
+
? "dynamic-template-class"
|
|
1391
|
+
: analysis.alwaysRenderedRoots < 2 && callerOpen
|
|
1392
|
+
? "open-root-fallthrough"
|
|
1393
|
+
: undefined;
|
|
1394
|
+
const migrateModule =
|
|
1395
|
+
analysis.moduleBlocks.length > 0 && !analysis.moduleClosureBroken && !moduleRetention;
|
|
1396
|
+
if (analysis.moduleBlocks.length > 0 && !migrateModule) {
|
|
1397
|
+
const [code, message] = analysis.moduleClosureBroken
|
|
1398
|
+
? [
|
|
1399
|
+
"unsupported-css-module-reference",
|
|
1400
|
+
"`$style` is referenced outside provable direct member accesses, so the module is retained.",
|
|
1401
|
+
]
|
|
1402
|
+
: moduleRetention === "dynamic-template-class"
|
|
1403
|
+
? [
|
|
1404
|
+
"dynamic-template-class",
|
|
1405
|
+
"A dynamic class binding makes the template's class set unprovable, so the module is retained.",
|
|
1406
|
+
]
|
|
1407
|
+
: [
|
|
1408
|
+
"open-root-fallthrough",
|
|
1409
|
+
"A parent component can merge classes onto the single root element, so the module is retained.",
|
|
1410
|
+
];
|
|
1411
|
+
for (const block of analysis.moduleBlocks) {
|
|
1412
|
+
warnings.push({
|
|
1413
|
+
code,
|
|
1414
|
+
file: file.path,
|
|
1415
|
+
start: block.contentStart,
|
|
1416
|
+
end: block.contentEnd,
|
|
1417
|
+
message,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Module blocks were already compiled alongside the scoped blocks for
|
|
1422
|
+
// the shadow corpus; recompiling here would double the preprocessor work.
|
|
1423
|
+
const vueBlocks = migrateUnscoped ? analysis.unscopedBlocks : analysis.blocks;
|
|
1424
|
+
if (vueBlocks.length === 0 && !migrateModule) continue;
|
|
1425
|
+
const vueRetention = migrateUnscoped ? undefined : retention;
|
|
1426
|
+
// Same-file scoped blocks stay out of the general corpus; the batch planner
|
|
1427
|
+
// adds back only their retained selectors when it plans the module entry.
|
|
1428
|
+
const shadowCss = vueShadowCss
|
|
1429
|
+
.filter(
|
|
1430
|
+
(entry) =>
|
|
1431
|
+
entry.path !== file.path ||
|
|
1432
|
+
(!entry.scoped && !(migrateUnscoped && entry.migratingUnscoped)),
|
|
1433
|
+
)
|
|
1434
|
+
.map((entry) => entry.source);
|
|
1435
|
+
if (migrateModule) {
|
|
1436
|
+
// Module classes are hashed, so this entry is always a closed world;
|
|
1437
|
+
// its own block text remains in the module shadow channel, which can
|
|
1438
|
+
// only over-retain. It plans before the scoped entry so scoped
|
|
1439
|
+
// planning can see which `$style` bindings survived: a live binding
|
|
1440
|
+
// is an opaque cascade surface (its retained module rule competes
|
|
1441
|
+
// unlayered with a replacement utility).
|
|
1442
|
+
stylesheets.push({
|
|
1443
|
+
cssPath: file.path,
|
|
1444
|
+
cssSource: file.source,
|
|
1445
|
+
cssModuleId: normalizedRelativePath(packageRoot, file.path),
|
|
1446
|
+
syntax: "css",
|
|
1447
|
+
isModule: true,
|
|
1448
|
+
vueBlocks: analysis.moduleBlocks,
|
|
1449
|
+
vueModule: true,
|
|
1450
|
+
vueShadowCss: shadowCss,
|
|
1451
|
+
vueShadowModuleCss,
|
|
1452
|
+
vueShadowUnverifiable,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
if (vueBlocks.length > 0) {
|
|
1456
|
+
stylesheets.push({
|
|
1457
|
+
cssPath: file.path,
|
|
1458
|
+
cssSource: file.source,
|
|
1459
|
+
cssModuleId: normalizedRelativePath(packageRoot, file.path),
|
|
1460
|
+
syntax: "css",
|
|
1461
|
+
isModule: !vueRetention,
|
|
1462
|
+
vueBlocks,
|
|
1463
|
+
vueRetention,
|
|
1464
|
+
vueUnscoped: migrateUnscoped,
|
|
1465
|
+
vueShadowCss: vueRetention ? undefined : shadowCss,
|
|
1466
|
+
vueShadowModuleCss: vueRetention ? undefined : vueShadowModuleCss,
|
|
1467
|
+
vueShadowUnverifiable: vueRetention ? undefined : vueShadowUnverifiable,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
for (const file of ownedVue) {
|
|
1473
|
+
const analysis = analyses.get(file.path);
|
|
1474
|
+
if (analysis.retained) continue;
|
|
1475
|
+
const elements = elementsByFile.get(file.path) ?? [];
|
|
1476
|
+
const contextPaths = [...new Set(elements.flatMap((element) => element.cssPaths))];
|
|
1477
|
+
if (contextPaths.length === 0) continue;
|
|
1478
|
+
await rejectSymlinkTarget(file.path, packageRoot);
|
|
1479
|
+
files.set(file.path, {
|
|
1480
|
+
...file,
|
|
1481
|
+
htmlElements: elements,
|
|
1482
|
+
htmlStylesheets: contextPaths.map((cssPath) => ({
|
|
1483
|
+
cssPath,
|
|
1484
|
+
variants: [],
|
|
1485
|
+
direct: cssPath === file.path,
|
|
1486
|
+
analyzable: true,
|
|
1487
|
+
})),
|
|
1488
|
+
htmlReferencesSafe: !analysis.dynamic,
|
|
1489
|
+
htmlScriptText: analysis.scriptText,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
return {
|
|
1493
|
+
files,
|
|
1494
|
+
stylesheets,
|
|
1495
|
+
stylePaths,
|
|
1496
|
+
unscopedPaths,
|
|
1497
|
+
warnings,
|
|
1498
|
+
compiler: loaded.compiler,
|
|
1499
|
+
sass,
|
|
1500
|
+
less,
|
|
1501
|
+
};
|
|
617
1502
|
}
|
|
618
1503
|
|
|
619
1504
|
async function preparePackageHtml({
|
|
@@ -630,7 +1515,7 @@ async function preparePackageHtml({
|
|
|
630
1515
|
const removableLinks = [];
|
|
631
1516
|
const warnings = [];
|
|
632
1517
|
|
|
633
|
-
const htmlFiles = sourceFiles.filter((file) => extension(file.path) ===
|
|
1518
|
+
const htmlFiles = sourceFiles.filter((file) => extension(file.path) === ".html");
|
|
634
1519
|
// Package-owned HTML goes first so stylesheets it discovers are claimed for
|
|
635
1520
|
// this package before foreign consumers are matched against that ownership.
|
|
636
1521
|
const orderedFiles = [
|
|
@@ -651,57 +1536,71 @@ async function preparePackageHtml({
|
|
|
651
1536
|
const base = parsed.bases[0];
|
|
652
1537
|
if (base) {
|
|
653
1538
|
const baseReference = base.href.split(/[?#]/, 1)[0];
|
|
654
|
-
const basePath =
|
|
655
|
-
|
|
656
|
-
|
|
1539
|
+
const basePath =
|
|
1540
|
+
base.writable &&
|
|
1541
|
+
(baseReference === ""
|
|
1542
|
+
? file.path
|
|
1543
|
+
: localHtmlReference(referenceRoot, dirname(file.path), base.href));
|
|
657
1544
|
if (!basePath || !isWithin(referenceRoot, basePath)) {
|
|
658
1545
|
if (!foreign) {
|
|
659
|
-
warnings.push(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1546
|
+
warnings.push(
|
|
1547
|
+
htmlWarning(
|
|
1548
|
+
"unsupported-html-base",
|
|
1549
|
+
file.path,
|
|
1550
|
+
base.start,
|
|
1551
|
+
base.end,
|
|
1552
|
+
"A remote or unrepresentable base URL prevents safe stylesheet link resolution.",
|
|
1553
|
+
),
|
|
1554
|
+
);
|
|
666
1555
|
}
|
|
667
1556
|
linkBase = undefined;
|
|
668
1557
|
} else {
|
|
669
|
-
linkBase = base.href.split(/[?#]/, 1)[0].endsWith(
|
|
1558
|
+
linkBase = base.href.split(/[?#]/, 1)[0].endsWith("/") ? basePath : dirname(basePath);
|
|
670
1559
|
}
|
|
671
1560
|
}
|
|
672
1561
|
for (const link of parsed.links) {
|
|
673
1562
|
const linkedPath = linkBase && localHtmlReference(referenceRoot, linkBase, link.href);
|
|
674
|
-
if (
|
|
675
|
-
|
|
1563
|
+
if (
|
|
1564
|
+
!linkedPath ||
|
|
1565
|
+
!isWithin(packageRoot, linkedPath) ||
|
|
1566
|
+
(foreign && pathOwners.get(linkedPath) !== packageRoot)
|
|
1567
|
+
) {
|
|
676
1568
|
if (!foreign) {
|
|
677
|
-
warnings.push(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1569
|
+
warnings.push(
|
|
1570
|
+
htmlWarning(
|
|
1571
|
+
"unsupported-html-stylesheet-link",
|
|
1572
|
+
file.path,
|
|
1573
|
+
link.start,
|
|
1574
|
+
link.end,
|
|
1575
|
+
"Only local package stylesheet links are analyzed.",
|
|
1576
|
+
),
|
|
1577
|
+
);
|
|
684
1578
|
}
|
|
685
1579
|
continue;
|
|
686
1580
|
}
|
|
687
1581
|
const variants = mediaVariants(link.media);
|
|
688
1582
|
if (variants === undefined) {
|
|
689
|
-
const cssPath =
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1583
|
+
const cssPath =
|
|
1584
|
+
(foreign
|
|
1585
|
+
? undefined
|
|
1586
|
+
: inferredPreprocessorPath({
|
|
1587
|
+
path: linkedPath,
|
|
1588
|
+
packageRoot,
|
|
1589
|
+
styleSources,
|
|
1590
|
+
pathOwners,
|
|
1591
|
+
styleDependents,
|
|
1592
|
+
})) ?? linkedPath;
|
|
696
1593
|
contexts.push({ cssPath, variants: [], direct: true, analyzable: false });
|
|
697
1594
|
if (!foreign) {
|
|
698
|
-
warnings.push(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1595
|
+
warnings.push(
|
|
1596
|
+
htmlWarning(
|
|
1597
|
+
"unsupported-link-media",
|
|
1598
|
+
file.path,
|
|
1599
|
+
link.start,
|
|
1600
|
+
link.end,
|
|
1601
|
+
`The stylesheet link media condition ${JSON.stringify(link.media)} cannot be represented safely.`,
|
|
1602
|
+
),
|
|
1603
|
+
);
|
|
705
1604
|
}
|
|
706
1605
|
continue;
|
|
707
1606
|
}
|
|
@@ -736,13 +1635,15 @@ async function preparePackageHtml({
|
|
|
736
1635
|
if (foreign && contexts.length === 0) continue;
|
|
737
1636
|
if (!foreign && contexts.length > 0) {
|
|
738
1637
|
for (const attribute of parsed.dynamicAttributes) {
|
|
739
|
-
warnings.push(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1638
|
+
warnings.push(
|
|
1639
|
+
htmlWarning(
|
|
1640
|
+
"dynamic-html-attribute",
|
|
1641
|
+
file.path,
|
|
1642
|
+
attribute.start,
|
|
1643
|
+
attribute.end,
|
|
1644
|
+
"This HTML attribute is not a safely writable quoted literal.",
|
|
1645
|
+
),
|
|
1646
|
+
);
|
|
746
1647
|
}
|
|
747
1648
|
}
|
|
748
1649
|
files.push({
|
|
@@ -758,29 +1659,31 @@ async function preparePackageHtml({
|
|
|
758
1659
|
}
|
|
759
1660
|
|
|
760
1661
|
async function collectHtmlStyleContexts(state) {
|
|
761
|
-
const key = `${state.path}\0${state.variants.join(
|
|
1662
|
+
const key = `${state.path}\0${state.variants.join(":")}`;
|
|
762
1663
|
if (state.visited.has(key)) return;
|
|
763
1664
|
state.visited.add(key);
|
|
764
1665
|
if (!isStylesheetPath(state.path)) return;
|
|
765
1666
|
const owner = state.pathOwners.get(state.path);
|
|
766
1667
|
if (owner && owner !== state.packageRoot) {
|
|
767
|
-
state.warnings.push(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1668
|
+
state.warnings.push(
|
|
1669
|
+
htmlWarning(
|
|
1670
|
+
"cross-package-stylesheet-link",
|
|
1671
|
+
state.path,
|
|
1672
|
+
0,
|
|
1673
|
+
0,
|
|
1674
|
+
"A stylesheet owned by another package is not analyzed outside workspace mode.",
|
|
1675
|
+
),
|
|
1676
|
+
);
|
|
774
1677
|
return;
|
|
775
1678
|
}
|
|
776
1679
|
|
|
777
1680
|
let source;
|
|
778
1681
|
try {
|
|
779
|
-
source =
|
|
780
|
-
?? await snapshotFile(state.snapshots, state.path);
|
|
1682
|
+
source =
|
|
1683
|
+
state.styleSources.get(state.path) ?? (await snapshotFile(state.snapshots, state.path));
|
|
781
1684
|
} catch (error) {
|
|
782
|
-
if (error.code ===
|
|
783
|
-
if (extension(state.path) ===
|
|
1685
|
+
if (error.code === "ENOENT") {
|
|
1686
|
+
if (extension(state.path) === ".css") addInferredPreprocessorContext(state);
|
|
784
1687
|
return;
|
|
785
1688
|
}
|
|
786
1689
|
throw error;
|
|
@@ -788,7 +1691,7 @@ async function collectHtmlStyleContexts(state) {
|
|
|
788
1691
|
if (!state.styleSources.has(state.path)) state.styleSources.set(state.path, source);
|
|
789
1692
|
if (!owner) state.pathOwners.set(state.path, state.packageRoot);
|
|
790
1693
|
|
|
791
|
-
if (extension(state.path) ===
|
|
1694
|
+
if (extension(state.path) === ".css" && addInferredPreprocessorContext(state)) return;
|
|
792
1695
|
|
|
793
1696
|
state.stylePaths.add(state.path);
|
|
794
1697
|
state.contexts.push({
|
|
@@ -797,18 +1700,20 @@ async function collectHtmlStyleContexts(state) {
|
|
|
797
1700
|
direct: state.direct,
|
|
798
1701
|
analyzable: true,
|
|
799
1702
|
});
|
|
800
|
-
if (extension(state.path) !==
|
|
1703
|
+
if (extension(state.path) !== ".css") return;
|
|
801
1704
|
|
|
802
1705
|
for (const imported of cssImports(source)) {
|
|
803
1706
|
const variants = mediaVariants(imported.media);
|
|
804
1707
|
if (variants === undefined) {
|
|
805
|
-
state.warnings.push(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1708
|
+
state.warnings.push(
|
|
1709
|
+
htmlWarning(
|
|
1710
|
+
"unsupported-link-media",
|
|
1711
|
+
state.path,
|
|
1712
|
+
imported.start,
|
|
1713
|
+
imported.end,
|
|
1714
|
+
`The stylesheet import media condition ${JSON.stringify(imported.media)} cannot be represented safely.`,
|
|
1715
|
+
),
|
|
1716
|
+
);
|
|
812
1717
|
continue;
|
|
813
1718
|
}
|
|
814
1719
|
const importedPath = localHtmlReference(state.packageRoot, dirname(state.path), imported.href);
|
|
@@ -816,8 +1721,10 @@ async function collectHtmlStyleContexts(state) {
|
|
|
816
1721
|
// Link-discovered stylesheets never went through indexStylesheetDependents,
|
|
817
1722
|
// so record their import edges here or deletion could leave this importer
|
|
818
1723
|
// pointing at a removed module.
|
|
819
|
-
if (
|
|
820
|
-
|
|
1724
|
+
if (
|
|
1725
|
+
importedPath !== state.path &&
|
|
1726
|
+
(isStylesheetModule(importedPath) || isPreprocessorPath(importedPath))
|
|
1727
|
+
) {
|
|
821
1728
|
addStyleDependent(state.styleDependents, importedPath, state.path);
|
|
822
1729
|
}
|
|
823
1730
|
await collectHtmlStyleContexts({
|
|
@@ -832,13 +1739,14 @@ async function collectHtmlStyleContexts(state) {
|
|
|
832
1739
|
}
|
|
833
1740
|
|
|
834
1741
|
function inferredPreprocessorPath(state) {
|
|
835
|
-
const stem = basename(state.path,
|
|
836
|
-
const matches = [...state.styleSources.keys()].filter(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1742
|
+
const stem = basename(state.path, ".css");
|
|
1743
|
+
const matches = [...state.styleSources.keys()].filter(
|
|
1744
|
+
(path) =>
|
|
1745
|
+
isPreprocessorPath(path) &&
|
|
1746
|
+
state.pathOwners.get(path) === state.packageRoot &&
|
|
1747
|
+
!basename(path).startsWith("_") &&
|
|
1748
|
+
!state.styleDependents.has(path) &&
|
|
1749
|
+
basename(path, extension(path)) === stem,
|
|
842
1750
|
);
|
|
843
1751
|
return matches.length === 1 ? matches[0] : undefined;
|
|
844
1752
|
}
|
|
@@ -847,9 +1755,12 @@ function addInferredPreprocessorContext(state) {
|
|
|
847
1755
|
// A source importing the generated CSS pins the artifact itself: excluding
|
|
848
1756
|
// it from planning while migrating the inferred entry could delete the only
|
|
849
1757
|
// source able to rebuild the file that import depends on.
|
|
850
|
-
if (
|
|
851
|
-
|
|
852
|
-
|
|
1758
|
+
if (
|
|
1759
|
+
state.styleSources.has(state.path) &&
|
|
1760
|
+
state.sourceFiles.some(
|
|
1761
|
+
(file) => extension(file.path) !== ".html" && sourceReferencesStyle(file, state.path),
|
|
1762
|
+
)
|
|
1763
|
+
) {
|
|
853
1764
|
return false;
|
|
854
1765
|
}
|
|
855
1766
|
const path = inferredPreprocessorPath(state);
|
|
@@ -862,13 +1773,15 @@ function addInferredPreprocessorContext(state) {
|
|
|
862
1773
|
direct: state.direct,
|
|
863
1774
|
analyzable: true,
|
|
864
1775
|
});
|
|
865
|
-
state.warnings.push(
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1776
|
+
state.warnings.push(
|
|
1777
|
+
htmlWarning(
|
|
1778
|
+
"inferred-preprocessor-source",
|
|
1779
|
+
state.path,
|
|
1780
|
+
0,
|
|
1781
|
+
0,
|
|
1782
|
+
`The linked CSS was matched to the unique preprocessor filename ${basename(path)}.`,
|
|
1783
|
+
),
|
|
1784
|
+
);
|
|
872
1785
|
return true;
|
|
873
1786
|
}
|
|
874
1787
|
|
|
@@ -883,7 +1796,7 @@ function cssImports(source) {
|
|
|
883
1796
|
for (let index = 0; index < masked.length; index += 1) {
|
|
884
1797
|
const character = masked[index];
|
|
885
1798
|
if (quote) {
|
|
886
|
-
if (character ===
|
|
1799
|
+
if (character === "\\") index += 1;
|
|
887
1800
|
else if (character === quote) quote = undefined;
|
|
888
1801
|
continue;
|
|
889
1802
|
}
|
|
@@ -891,18 +1804,22 @@ function cssImports(source) {
|
|
|
891
1804
|
quote = character;
|
|
892
1805
|
continue;
|
|
893
1806
|
}
|
|
894
|
-
if (character ===
|
|
1807
|
+
if (character === "{") {
|
|
895
1808
|
depth += 1;
|
|
896
1809
|
importsAllowed = false;
|
|
897
1810
|
continue;
|
|
898
1811
|
}
|
|
899
|
-
if (character ===
|
|
1812
|
+
if (character === "}") {
|
|
900
1813
|
depth = Math.max(0, depth - 1);
|
|
901
1814
|
continue;
|
|
902
1815
|
}
|
|
903
|
-
if (
|
|
904
|
-
||
|
|
905
|
-
|
|
1816
|
+
if (
|
|
1817
|
+
!importsAllowed ||
|
|
1818
|
+
depth !== 0 ||
|
|
1819
|
+
masked.slice(index, index + 7).toLowerCase() !== "@import" ||
|
|
1820
|
+
/[-\w]/.test(masked[index + 7] ?? "")
|
|
1821
|
+
)
|
|
1822
|
+
continue;
|
|
906
1823
|
|
|
907
1824
|
let end = index + 7;
|
|
908
1825
|
let importQuote;
|
|
@@ -910,52 +1827,64 @@ function cssImports(source) {
|
|
|
910
1827
|
for (; end < masked.length; end += 1) {
|
|
911
1828
|
const next = masked[end];
|
|
912
1829
|
if (importQuote) {
|
|
913
|
-
if (next ===
|
|
1830
|
+
if (next === "\\") end += 1;
|
|
914
1831
|
else if (next === importQuote) importQuote = undefined;
|
|
915
1832
|
} else if (next === '"' || next === "'") importQuote = next;
|
|
916
|
-
else if (next ===
|
|
917
|
-
else if (next ===
|
|
918
|
-
else if (next ===
|
|
1833
|
+
else if (next === "(") parentheses += 1;
|
|
1834
|
+
else if (next === ")") parentheses = Math.max(0, parentheses - 1);
|
|
1835
|
+
else if (next === ";" && parentheses === 0) break;
|
|
919
1836
|
}
|
|
920
1837
|
if (end >= masked.length) continue;
|
|
921
1838
|
const statement = masked.slice(index, end + 1);
|
|
922
|
-
const match =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1839
|
+
const match =
|
|
1840
|
+
/^@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^"'()\s;]+))\s*\)?\s*([^;]*);$/i.exec(
|
|
1841
|
+
statement,
|
|
1842
|
+
);
|
|
1843
|
+
if (match)
|
|
1844
|
+
imports.push({
|
|
1845
|
+
href: match[1] ?? match[2],
|
|
1846
|
+
media: match[3].trim(),
|
|
1847
|
+
start: index,
|
|
1848
|
+
end: end + 1,
|
|
1849
|
+
});
|
|
929
1850
|
index = end;
|
|
930
1851
|
}
|
|
931
|
-
|
|
1852
|
+
// Offsets are collected as string indices and converted together, so the
|
|
1853
|
+
// source prefix is scanned once rather than twice per import.
|
|
1854
|
+
const offsets = utf8OffsetMap(
|
|
1855
|
+
source,
|
|
1856
|
+
imports.flatMap((imported) => [imported.start, imported.end]),
|
|
1857
|
+
);
|
|
1858
|
+
return imports.map((imported) => ({
|
|
1859
|
+
...imported,
|
|
1860
|
+
start: offsets.get(imported.start),
|
|
1861
|
+
end: offsets.get(imported.end),
|
|
1862
|
+
}));
|
|
932
1863
|
}
|
|
933
1864
|
|
|
934
1865
|
function localHtmlReference(packageRoot, base, reference) {
|
|
935
1866
|
const path = reference.split(/[?#]/, 1)[0];
|
|
936
|
-
if (!path || path.startsWith(
|
|
1867
|
+
if (!path || path.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(path)) return undefined;
|
|
937
1868
|
let decoded;
|
|
938
1869
|
try {
|
|
939
1870
|
decoded = decodeURIComponent(path);
|
|
940
1871
|
} catch {
|
|
941
1872
|
return undefined;
|
|
942
1873
|
}
|
|
943
|
-
return decoded.startsWith(
|
|
944
|
-
? resolve(packageRoot, `.${decoded}`)
|
|
945
|
-
: resolve(base, decoded);
|
|
1874
|
+
return decoded.startsWith("/") ? resolve(packageRoot, `.${decoded}`) : resolve(base, decoded);
|
|
946
1875
|
}
|
|
947
1876
|
|
|
948
1877
|
function mediaVariants(media) {
|
|
949
1878
|
const normalized = media.trim().toLowerCase();
|
|
950
|
-
if (!normalized || normalized ===
|
|
951
|
-
if (normalized ===
|
|
1879
|
+
if (!normalized || normalized === "all") return [];
|
|
1880
|
+
if (normalized === "print") return ["print"];
|
|
952
1881
|
return undefined;
|
|
953
1882
|
}
|
|
954
1883
|
|
|
955
1884
|
function deduplicateHtmlContexts(contexts) {
|
|
956
1885
|
const unique = new Map();
|
|
957
1886
|
for (const context of contexts) {
|
|
958
|
-
const key = `${context.cssPath}\0${context.variants.join(
|
|
1887
|
+
const key = `${context.cssPath}\0${context.variants.join(":")}\0${context.analyzable}`;
|
|
959
1888
|
const existing = unique.get(key);
|
|
960
1889
|
if (existing) existing.direct ||= context.direct;
|
|
961
1890
|
else unique.set(key, { ...context });
|
|
@@ -968,8 +1897,10 @@ function htmlWarning(code, file, start, end, message) {
|
|
|
968
1897
|
}
|
|
969
1898
|
|
|
970
1899
|
function isProjectInput(workspaceRoot, path) {
|
|
971
|
-
return
|
|
972
|
-
|
|
1900
|
+
return (
|
|
1901
|
+
isWithin(workspaceRoot, path) &&
|
|
1902
|
+
!relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules")
|
|
1903
|
+
);
|
|
973
1904
|
}
|
|
974
1905
|
|
|
975
1906
|
async function rejectSymlinkTarget(path, root) {
|
|
@@ -982,7 +1913,21 @@ async function rejectSymlinkTarget(path, root) {
|
|
|
982
1913
|
}
|
|
983
1914
|
|
|
984
1915
|
async function snapshotFile(snapshots, path) {
|
|
985
|
-
return recordSnapshot(snapshots, path, await readFile(path,
|
|
1916
|
+
return recordSnapshot(snapshots, path, await readFile(path, "utf8"));
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// The compiler just loaded this dependency, so its disappearance is a source
|
|
1920
|
+
// change during planning -- a fatal integrity error `--force` must not
|
|
1921
|
+
// downgrade to a recoverable package failure.
|
|
1922
|
+
async function snapshotLoadedSource(snapshots, path) {
|
|
1923
|
+
try {
|
|
1924
|
+
return await snapshotFile(snapshots, path);
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
if (error.code === "ENOENT") {
|
|
1927
|
+
throw new Error(`Source changed during planning: ${path}`);
|
|
1928
|
+
}
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
986
1931
|
}
|
|
987
1932
|
|
|
988
1933
|
function recordSnapshot(snapshots, path, source) {
|
|
@@ -996,7 +1941,7 @@ function recordSnapshot(snapshots, path, source) {
|
|
|
996
1941
|
function packageFailure(workspaceRoot, packageRoot, error) {
|
|
997
1942
|
const message = error instanceof Error ? error.message : String(error);
|
|
998
1943
|
return {
|
|
999
|
-
package: normalizedRelativePath(workspaceRoot, packageRoot) ||
|
|
1944
|
+
package: normalizedRelativePath(workspaceRoot, packageRoot) || ".",
|
|
1000
1945
|
message: message.startsWith(RECOVERABLE_INPUT_ERROR)
|
|
1001
1946
|
? message.slice(RECOVERABLE_INPUT_ERROR.length)
|
|
1002
1947
|
: message,
|
|
@@ -1008,13 +1953,20 @@ function isRecoverablePlanningError(error) {
|
|
|
1008
1953
|
return message.startsWith(RECOVERABLE_INPUT_ERROR);
|
|
1009
1954
|
}
|
|
1010
1955
|
|
|
1956
|
+
function isMissingStyleCompilerError(error) {
|
|
1957
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1958
|
+
return /^(?:Sass|Less) must be installed in the target project\.$/.test(message);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1011
1961
|
function isIntegrityError(error) {
|
|
1012
1962
|
const message = error instanceof Error ? error.message : String(error);
|
|
1013
|
-
return message.startsWith(
|
|
1963
|
+
return message.startsWith("Source changed during planning:");
|
|
1014
1964
|
}
|
|
1015
1965
|
|
|
1016
1966
|
async function readSources(paths, snapshots) {
|
|
1017
|
-
return new Map(
|
|
1967
|
+
return new Map(
|
|
1968
|
+
await Promise.all(paths.map(async (path) => [path, await snapshotFile(snapshots, path)])),
|
|
1969
|
+
);
|
|
1018
1970
|
}
|
|
1019
1971
|
|
|
1020
1972
|
async function collectFiles(root, include) {
|
|
@@ -1036,20 +1988,16 @@ async function collectFiles(root, include) {
|
|
|
1036
1988
|
}
|
|
1037
1989
|
|
|
1038
1990
|
function extension(path) {
|
|
1039
|
-
const match = /\.[
|
|
1040
|
-
return match?.[0] ??
|
|
1991
|
+
const match = /\.[^./]+$/.exec(path);
|
|
1992
|
+
return match?.[0] ?? "";
|
|
1041
1993
|
}
|
|
1042
1994
|
|
|
1043
1995
|
function normalizedRelativePath(root, path) {
|
|
1044
|
-
return relative(root, path).split(sep).join(
|
|
1996
|
+
return relative(root, path).split(sep).join("/");
|
|
1045
1997
|
}
|
|
1046
1998
|
|
|
1047
1999
|
function maskCssComments(source) {
|
|
1048
|
-
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g,
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function utf8Offset(source, index) {
|
|
1052
|
-
return Buffer.byteLength(source.slice(0, index));
|
|
2000
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
|
|
1053
2001
|
}
|
|
1054
2002
|
|
|
1055
2003
|
function indexStylesheetDependents(styleSources) {
|
|
@@ -1057,26 +2005,30 @@ function indexStylesheetDependents(styleSources) {
|
|
|
1057
2005
|
for (const [path, rawSource] of styleSources) {
|
|
1058
2006
|
const source = maskCssComments(rawSource);
|
|
1059
2007
|
const references = [
|
|
1060
|
-
...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)]
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)]
|
|
1065
|
-
|
|
1066
|
-
.
|
|
1067
|
-
|
|
1068
|
-
|
|
2008
|
+
...[...source.matchAll(/composes\s*:[^;{}]*?\bfrom\s+["']([^"']+)["']/g)].map(
|
|
2009
|
+
(match) => match[1],
|
|
2010
|
+
),
|
|
2011
|
+
...[...source.matchAll(/@(?:use|forward)\s+["']([^"']+)["']/g)].map((match) => match[1]),
|
|
2012
|
+
...[...source.matchAll(/@import\s+(?:\([^)]*\)\s*)?(?:([^;{}]+);|([^;{}\r\n]+))/g)].flatMap(
|
|
2013
|
+
(statement) =>
|
|
2014
|
+
[...(statement[1] ?? statement[2]).matchAll(/["']([^"']+)["']/g)].map(
|
|
2015
|
+
(match) => match[1],
|
|
2016
|
+
),
|
|
2017
|
+
),
|
|
2018
|
+
...[...source.matchAll(/@import\s+url\(\s*([^"'()\s]+)\s*\)/g)].map((match) => match[1]),
|
|
1069
2019
|
];
|
|
1070
2020
|
for (const reference of new Set(references)) {
|
|
1071
2021
|
for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
|
|
1072
|
-
if (target === path || (!isStylesheetModule(target) && !isPreprocessorPath(target)))
|
|
2022
|
+
if (target === path || (!isStylesheetModule(target) && !isPreprocessorPath(target)))
|
|
2023
|
+
continue;
|
|
1073
2024
|
const paths = dependents.get(target) ?? [];
|
|
1074
2025
|
paths.push(path);
|
|
1075
2026
|
dependents.set(target, paths);
|
|
1076
2027
|
}
|
|
1077
2028
|
}
|
|
1078
2029
|
}
|
|
1079
|
-
for (const [target, paths] of dependents)
|
|
2030
|
+
for (const [target, paths] of dependents)
|
|
2031
|
+
dependents.set(target, [...new Set(paths)].sort(compareStrings));
|
|
1080
2032
|
return dependents;
|
|
1081
2033
|
}
|
|
1082
2034
|
|
|
@@ -1084,7 +2036,7 @@ function addStyleDependent(styleDependents, target, importer) {
|
|
|
1084
2036
|
const paths = styleDependents.get(target) ?? [];
|
|
1085
2037
|
if (paths.includes(importer)) return;
|
|
1086
2038
|
paths.push(importer);
|
|
1087
|
-
paths.sort();
|
|
2039
|
+
paths.sort(compareStrings);
|
|
1088
2040
|
styleDependents.set(target, paths);
|
|
1089
2041
|
}
|
|
1090
2042
|
|
|
@@ -1093,45 +2045,54 @@ function stylesheetReferenceTargets(importer, reference, styleSources) {
|
|
|
1093
2045
|
const candidates = STYLESHEET_SYNTAX.has(extension(target))
|
|
1094
2046
|
? [target]
|
|
1095
2047
|
: [...STYLESHEET_SYNTAX.keys()].flatMap((syntax) => [
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
2048
|
+
`${target}${syntax}`,
|
|
2049
|
+
join(dirname(target), `_${basename(target)}${syntax}`),
|
|
2050
|
+
join(target, `_index${syntax}`),
|
|
2051
|
+
join(target, `index${syntax}`),
|
|
2052
|
+
]);
|
|
1101
2053
|
return candidates.filter((path) => styleSources.has(path));
|
|
1102
2054
|
}
|
|
1103
2055
|
|
|
1104
2056
|
function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
|
|
1105
2057
|
const entries = stylePaths.filter((path) => {
|
|
1106
|
-
if (extension(path) !==
|
|
2058
|
+
if (extension(path) !== ".css") return false;
|
|
1107
2059
|
const source = maskCssComments(styleSources.get(path));
|
|
1108
2060
|
return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
|
|
1109
2061
|
});
|
|
1110
2062
|
if (configuredPath) return { path: configuredPath, entries };
|
|
1111
|
-
if (entries.length === 0)
|
|
1112
|
-
|
|
2063
|
+
if (entries.length === 0)
|
|
2064
|
+
throw new Error("No Tailwind v4 CSS entry was found. Pass --tailwind-css.");
|
|
2065
|
+
if (entries.length > 1)
|
|
2066
|
+
throw new Error("Multiple Tailwind CSS entries were found. Pass --tailwind-css.");
|
|
1113
2067
|
return { path: entries[0], entries };
|
|
1114
2068
|
}
|
|
1115
2069
|
|
|
1116
2070
|
async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
|
|
1117
|
-
const projectRequire = createRequire(join(packageRoot,
|
|
2071
|
+
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
1118
2072
|
let packagePath;
|
|
1119
2073
|
try {
|
|
1120
|
-
packagePath = projectRequire.resolve(
|
|
2074
|
+
packagePath = projectRequire.resolve("tailwindcss/package.json");
|
|
1121
2075
|
} catch {
|
|
1122
|
-
throw new Error(
|
|
2076
|
+
throw new Error("Tailwind v4 must be installed in the target project.");
|
|
1123
2077
|
}
|
|
1124
|
-
const packageJson = JSON.parse(await readFile(packagePath,
|
|
1125
|
-
if (!String(packageJson.version).startsWith(
|
|
2078
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
2079
|
+
if (!String(packageJson.version).startsWith("4."))
|
|
2080
|
+
throw new Error(`Tailwind v4 is required; found ${packageJson.version}.`);
|
|
1126
2081
|
|
|
1127
|
-
const modulePath = projectRequire.resolve(
|
|
2082
|
+
const modulePath = projectRequire.resolve("tailwindcss");
|
|
1128
2083
|
const tailwindModule = await import(pathToFileURL(modulePath));
|
|
1129
|
-
const { __unstable__loadDesignSystem: loadDesignSystem } =
|
|
2084
|
+
const { __unstable__loadDesignSystem: loadDesignSystem } =
|
|
2085
|
+
tailwindModule.default ?? tailwindModule;
|
|
1130
2086
|
const css = await snapshotFile(snapshots, tailwindCss);
|
|
1131
2087
|
const base = dirname(tailwindCss);
|
|
1132
2088
|
const loadModule = createModuleLoader(snapshots, workspaceRoot);
|
|
1133
|
-
const loadStylesheet = createStylesheetLoader(
|
|
1134
|
-
|
|
2089
|
+
const loadStylesheet = createStylesheetLoader(
|
|
2090
|
+
projectRequire,
|
|
2091
|
+
packagePath,
|
|
2092
|
+
snapshots,
|
|
2093
|
+
workspaceRoot,
|
|
2094
|
+
);
|
|
2095
|
+
const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
|
|
1135
2096
|
const themeTokens = {
|
|
1136
2097
|
...extractThemeTokens(defaultTheme),
|
|
1137
2098
|
...(await extractThemeTokensFromGraph(css, base, loadStylesheet)),
|
|
@@ -1142,8 +2103,9 @@ async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot)
|
|
|
1142
2103
|
|
|
1143
2104
|
function extractThemeTokens(css) {
|
|
1144
2105
|
const tokens = {};
|
|
1145
|
-
for (const block of css.matchAll(/@theme[
|
|
1146
|
-
for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g))
|
|
2106
|
+
for (const block of css.matchAll(/@theme[^{]*\{([^}]*)\}/gs)) {
|
|
2107
|
+
for (const match of block[1].matchAll(/--([\w-]+):\s*([^;{}]+);/g))
|
|
2108
|
+
tokens[match[1]] = match[2].trim();
|
|
1147
2109
|
}
|
|
1148
2110
|
return tokens;
|
|
1149
2111
|
}
|
|
@@ -1155,7 +2117,10 @@ async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = new
|
|
|
1155
2117
|
if (seen.has(key)) continue;
|
|
1156
2118
|
seen.add(key);
|
|
1157
2119
|
const loaded = await loadStylesheet(match[1], base);
|
|
1158
|
-
Object.assign(
|
|
2120
|
+
Object.assign(
|
|
2121
|
+
tokens,
|
|
2122
|
+
await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen),
|
|
2123
|
+
);
|
|
1159
2124
|
}
|
|
1160
2125
|
return Object.assign(tokens, extractThemeTokens(css));
|
|
1161
2126
|
}
|
|
@@ -1167,7 +2132,7 @@ function invalidCandidates(tailwind, candidates) {
|
|
|
1167
2132
|
|
|
1168
2133
|
function createModuleLoader(snapshots, workspaceRoot) {
|
|
1169
2134
|
return async (id, base) => {
|
|
1170
|
-
const path = createRequire(join(base,
|
|
2135
|
+
const path = createRequire(join(base, "package.json")).resolve(id);
|
|
1171
2136
|
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
1172
2137
|
const imported = await import(pathToFileURL(path));
|
|
1173
2138
|
return { path, base: dirname(path), module: imported.default ?? imported };
|
|
@@ -1178,36 +2143,42 @@ function createStylesheetLoader(projectRequire, tailwindPackagePath, snapshots,
|
|
|
1178
2143
|
const tailwindRoot = dirname(tailwindPackagePath);
|
|
1179
2144
|
return async (id, base) => {
|
|
1180
2145
|
let path;
|
|
1181
|
-
if (id ===
|
|
1182
|
-
else if (id.startsWith(
|
|
1183
|
-
const subpath = id.slice(
|
|
1184
|
-
path = join(tailwindRoot, subpath.endsWith(
|
|
1185
|
-
} else if (id.startsWith(
|
|
2146
|
+
if (id === "tailwindcss") path = join(tailwindRoot, "index.css");
|
|
2147
|
+
else if (id.startsWith("tailwindcss/")) {
|
|
2148
|
+
const subpath = id.slice("tailwindcss/".length);
|
|
2149
|
+
path = join(tailwindRoot, subpath.endsWith(".css") ? subpath : `${subpath}.css`);
|
|
2150
|
+
} else if (id.startsWith(".") || isAbsolute(id)) path = resolve(base, id);
|
|
1186
2151
|
else path = projectRequire.resolve(id);
|
|
1187
2152
|
const content = isProjectInput(workspaceRoot, path)
|
|
1188
2153
|
? await snapshotFile(snapshots, path)
|
|
1189
|
-
: await readFile(path,
|
|
2154
|
+
: await readFile(path, "utf8");
|
|
1190
2155
|
return { content, base: dirname(path) };
|
|
1191
2156
|
};
|
|
1192
2157
|
}
|
|
1193
2158
|
|
|
1194
2159
|
function unifiedDiff(path, before, after) {
|
|
1195
|
-
const oldLines = before.split(
|
|
1196
|
-
const newLines = after.split(
|
|
2160
|
+
const oldLines = before.split("\n");
|
|
2161
|
+
const newLines = after.split("\n");
|
|
1197
2162
|
return [
|
|
1198
2163
|
`--- a/${path}\n`,
|
|
1199
2164
|
`+++ b/${path}\n`,
|
|
1200
2165
|
`@@ -1,${oldLines.length} +1,${newLines.length} @@\n`,
|
|
1201
|
-
...oldLines.map(
|
|
1202
|
-
|
|
1203
|
-
|
|
2166
|
+
...oldLines.map(
|
|
2167
|
+
(line, index) => `${index === oldLines.length - 1 && line === "" ? " " : "-"}${line}\n`,
|
|
2168
|
+
),
|
|
2169
|
+
...newLines.map(
|
|
2170
|
+
(line, index) => `${index === newLines.length - 1 && line === "" ? " " : "+"}${line}\n`,
|
|
2171
|
+
),
|
|
2172
|
+
].join("");
|
|
1204
2173
|
}
|
|
1205
2174
|
|
|
1206
2175
|
async function verifySnapshots(snapshots) {
|
|
1207
|
-
for (const [path, before] of [...snapshots].sort(([left], [right]) =>
|
|
2176
|
+
for (const [path, before] of [...snapshots].sort(([left], [right]) =>
|
|
2177
|
+
left.localeCompare(right),
|
|
2178
|
+
)) {
|
|
1208
2179
|
let current;
|
|
1209
2180
|
try {
|
|
1210
|
-
current = await readFile(path,
|
|
2181
|
+
current = await readFile(path, "utf8");
|
|
1211
2182
|
} catch (error) {
|
|
1212
2183
|
throw new Error(`Source changed after planning: ${path} (${error.code ?? error.message})`);
|
|
1213
2184
|
}
|