tw-migrate 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +146 -0
- package/{index.d.ts → dist/index.d.ts} +17 -13
- package/dist/index.js +2187 -0
- package/package.json +20 -27
- package/bin/tw-migrate.js +0 -51
- package/html.js +0 -191
- package/index.js +0 -2225
- package/native.js +0 -41
- package/style-compiler.js +0 -140
- package/vue.js +0 -541
package/index.js
DELETED
|
@@ -1,2225 +0,0 @@
|
|
|
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";
|
|
21
|
-
import {
|
|
22
|
-
compileLessEntry,
|
|
23
|
-
compileSassEntry,
|
|
24
|
-
isPreprocessorPath,
|
|
25
|
-
isSassPath,
|
|
26
|
-
loadProjectLess,
|
|
27
|
-
loadProjectSass,
|
|
28
|
-
} from "./style-compiler.js";
|
|
29
|
-
|
|
30
|
-
const run = promisify(execFile);
|
|
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
|
-
]);
|
|
43
|
-
const STYLESHEET_SYNTAX = new Map([
|
|
44
|
-
[".css", "css"],
|
|
45
|
-
[".scss", "scss"],
|
|
46
|
-
[".sass", "sass"],
|
|
47
|
-
[".less", "less"],
|
|
48
|
-
]);
|
|
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);
|
|
52
|
-
|
|
53
|
-
export async function migrate(options = {}) {
|
|
54
|
-
if ("cssFile" in options) {
|
|
55
|
-
throw new TypeError("cssFile has been replaced by styleFile");
|
|
56
|
-
}
|
|
57
|
-
if (options.styleFile && options.workspaces) {
|
|
58
|
-
throw new TypeError("styleFile cannot be combined with workspaces");
|
|
59
|
-
}
|
|
60
|
-
if (options.tailwindCss && options.workspaces) {
|
|
61
|
-
throw new TypeError("tailwindCss cannot be combined with workspaces");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const scope = await resolveScope(options);
|
|
65
|
-
const { cwd, workspaceRoot, selectedPackages, explicitStyle, configuredEntry } = scope;
|
|
66
|
-
|
|
67
|
-
// A prior interrupted run's scope is unknowable from the current flags
|
|
68
|
-
// (it may have covered other packages), so scan the whole workspace root.
|
|
69
|
-
const leftovers = await collectFiles(workspaceRoot, (path) =>
|
|
70
|
-
basename(path).includes(".tw-migrate-"),
|
|
71
|
-
);
|
|
72
|
-
if (leftovers.length > 0) {
|
|
73
|
-
const listed = leftovers
|
|
74
|
-
.sort(compareStrings)
|
|
75
|
-
.map((path) => ` ${normalizedRelativePath(cwd, path)}`)
|
|
76
|
-
.join("\n");
|
|
77
|
-
throw new Error(
|
|
78
|
-
`Found leftover tw-migrate files from an interrupted run:\n${listed}\n` +
|
|
79
|
-
'Restore each ".<name>.tw-migrate-backup-*" file by renaming it back to "<name>", ' +
|
|
80
|
-
'delete any remaining ".<name>.tw-migrate-*" staging files, then re-run.',
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const snapshots = new Map();
|
|
85
|
-
const stylePaths = scope.scannedPaths.filter(isStylesheetPath);
|
|
86
|
-
const sourcePaths = scope.scannedPaths.filter((path) => SOURCE_EXTENSIONS.has(extension(path)));
|
|
87
|
-
const [styleSources, sourceCandidates] = await Promise.all([
|
|
88
|
-
readSources(stylePaths, snapshots),
|
|
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
|
-
),
|
|
106
|
-
]);
|
|
107
|
-
const sourceFiles = sourceCandidates.filter(Boolean);
|
|
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)) {
|
|
111
|
-
styleSources.set(explicitStyle, await snapshotFile(snapshots, explicitStyle));
|
|
112
|
-
}
|
|
113
|
-
if (configuredEntry && !styleSources.has(configuredEntry)) {
|
|
114
|
-
styleSources.set(configuredEntry, await snapshotFile(snapshots, configuredEntry));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const context = {
|
|
118
|
-
...scope,
|
|
119
|
-
options,
|
|
120
|
-
snapshots,
|
|
121
|
-
styleSources,
|
|
122
|
-
sourceFiles,
|
|
123
|
-
styleDependents: indexStylesheetDependents(styleSources),
|
|
124
|
-
};
|
|
125
|
-
const failures = [];
|
|
126
|
-
const plans = [];
|
|
127
|
-
for (const packageRoot of selectedPackages) {
|
|
128
|
-
const result = await planPackage(context, packageRoot);
|
|
129
|
-
if (result.failure) failures.push(result.failure);
|
|
130
|
-
else if (result.plan) plans.push(result.plan);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const originals = new Map([
|
|
134
|
-
...styleSources,
|
|
135
|
-
...sourceFiles.map((file) => [file.path, file.source]),
|
|
136
|
-
]);
|
|
137
|
-
const { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules } =
|
|
138
|
-
mergePlans(plans, originals);
|
|
139
|
-
|
|
140
|
-
const changed = [...filesByPath.values()]
|
|
141
|
-
.map((file) => ({ ...file, before: originals.get(file.path) }))
|
|
142
|
-
.filter((file) => file.before !== file.source)
|
|
143
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
144
|
-
const deleted = [...deletedPaths]
|
|
145
|
-
.map((path) => ({ path, before: originals.get(path) }))
|
|
146
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
147
|
-
const operations = [...changed, ...deleted].sort((left, right) =>
|
|
148
|
-
left.path.localeCompare(right.path),
|
|
149
|
-
);
|
|
150
|
-
const changedFiles = operations.map((file) => normalizedRelativePath(cwd, file.path));
|
|
151
|
-
const diff = operations
|
|
152
|
-
.map((file) =>
|
|
153
|
-
unifiedDiff(
|
|
154
|
-
normalizedRelativePath(cwd, file.path),
|
|
155
|
-
file.before,
|
|
156
|
-
"source" in file ? file.source : "",
|
|
157
|
-
),
|
|
158
|
-
)
|
|
159
|
-
.join("");
|
|
160
|
-
|
|
161
|
-
if (options.write && operations.length > 0) {
|
|
162
|
-
await verifySnapshots(snapshots);
|
|
163
|
-
await writeChanges(changed, deleted);
|
|
164
|
-
}
|
|
165
|
-
|
|
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
|
-
);
|
|
173
|
-
failures.sort((left, right) => left.package.localeCompare(right.package));
|
|
174
|
-
return {
|
|
175
|
-
changedFiles,
|
|
176
|
-
diff,
|
|
177
|
-
convertedRules,
|
|
178
|
-
retainedRules,
|
|
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
|
-
})),
|
|
189
|
-
failures,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function resolveScope(options) {
|
|
194
|
-
const cwd = await realpath(resolve(options.cwd ?? process.cwd()));
|
|
195
|
-
const currentPackage = await findPackageRoot(cwd);
|
|
196
|
-
const gitRoot = await findGitRoot(currentPackage);
|
|
197
|
-
const workspaceRoot =
|
|
198
|
-
gitRoot && !(await isIgnoredByGit(gitRoot, currentPackage)) ? gitRoot : currentPackage;
|
|
199
|
-
const allPaths = await discoverFiles(workspaceRoot, workspaceRoot === gitRoot);
|
|
200
|
-
// Ignore filtering scopes what gets migrated, never what gets scanned:
|
|
201
|
-
// gitignored consumers and stylesheets must still block unsafe deletion.
|
|
202
|
-
const scannedPaths =
|
|
203
|
-
workspaceRoot === gitRoot
|
|
204
|
-
? [
|
|
205
|
-
...new Set([
|
|
206
|
-
...allPaths,
|
|
207
|
-
...(await collectFiles(workspaceRoot, isRelevantDiscoveredFile)),
|
|
208
|
-
]),
|
|
209
|
-
]
|
|
210
|
-
: [...allPaths];
|
|
211
|
-
const explicitStyle = options.styleFile ? resolve(cwd, options.styleFile) : undefined;
|
|
212
|
-
const configuredEntry = options.tailwindCss ? resolve(cwd, options.tailwindCss) : undefined;
|
|
213
|
-
if (explicitStyle && !isStylesheetPath(explicitStyle) && extension(explicitStyle) !== ".vue") {
|
|
214
|
-
throw new TypeError("Only .css, .scss, .sass, .less, and .vue files can be migrated");
|
|
215
|
-
}
|
|
216
|
-
if (configuredEntry && extension(configuredEntry) !== ".css") {
|
|
217
|
-
throw new TypeError("The Tailwind CSS entry must be a .css file");
|
|
218
|
-
}
|
|
219
|
-
if (configuredEntry && !(await stat(configuredEntry)).isFile()) {
|
|
220
|
-
throw new TypeError("The Tailwind CSS entry must be a file");
|
|
221
|
-
}
|
|
222
|
-
for (const path of [explicitStyle, configuredEntry]) {
|
|
223
|
-
if (path) await rejectSymlinkTarget(path, currentPackage);
|
|
224
|
-
}
|
|
225
|
-
if (explicitStyle && !isWithin(currentPackage, explicitStyle)) {
|
|
226
|
-
throw new TypeError("The selected stylesheet must belong to the current package");
|
|
227
|
-
}
|
|
228
|
-
for (const path of [explicitStyle, configuredEntry]) {
|
|
229
|
-
if (path && !allPaths.includes(path)) allPaths.push(path);
|
|
230
|
-
if (path && !scannedPaths.includes(path)) scannedPaths.push(path);
|
|
231
|
-
}
|
|
232
|
-
allPaths.sort(compareStrings);
|
|
233
|
-
scannedPaths.sort(compareStrings);
|
|
234
|
-
const targetable = new Set(allPaths);
|
|
235
|
-
|
|
236
|
-
const allPackageRoots = await discoverPackageRoots(workspaceRoot, allPaths);
|
|
237
|
-
if (!allPackageRoots.includes(currentPackage)) allPackageRoots.push(currentPackage);
|
|
238
|
-
for (const path of [explicitStyle, configuredEntry]) {
|
|
239
|
-
if (!path) continue;
|
|
240
|
-
const owner = await findPackageRoot(dirname(path));
|
|
241
|
-
if (!allPackageRoots.includes(owner)) allPackageRoots.push(owner);
|
|
242
|
-
}
|
|
243
|
-
allPackageRoots.sort(compareStrings);
|
|
244
|
-
const pathOwners = new Map(
|
|
245
|
-
scannedPaths.map((path) => [path, owningPackage(path, allPackageRoots)]),
|
|
246
|
-
);
|
|
247
|
-
if (explicitStyle && pathOwners.get(explicitStyle) !== currentPackage) {
|
|
248
|
-
throw new TypeError("The selected stylesheet must belong to the current package");
|
|
249
|
-
}
|
|
250
|
-
if (configuredEntry && pathOwners.get(configuredEntry) !== currentPackage) {
|
|
251
|
-
throw new TypeError("The Tailwind CSS entry must belong to the current package");
|
|
252
|
-
}
|
|
253
|
-
const selectedPackages = options.workspaces ? allPackageRoots : [currentPackage];
|
|
254
|
-
return {
|
|
255
|
-
cwd,
|
|
256
|
-
workspaceRoot,
|
|
257
|
-
scannedPaths,
|
|
258
|
-
targetable,
|
|
259
|
-
explicitStyle,
|
|
260
|
-
configuredEntry,
|
|
261
|
-
pathOwners,
|
|
262
|
-
selectedPackages,
|
|
263
|
-
writablePackages: new Set(selectedPackages),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function planPackage(context, packageRoot) {
|
|
268
|
-
const {
|
|
269
|
-
options,
|
|
270
|
-
snapshots,
|
|
271
|
-
workspaceRoot,
|
|
272
|
-
explicitStyle,
|
|
273
|
-
configuredEntry,
|
|
274
|
-
styleSources,
|
|
275
|
-
sourceFiles,
|
|
276
|
-
styleDependents,
|
|
277
|
-
pathOwners,
|
|
278
|
-
targetable,
|
|
279
|
-
writablePackages,
|
|
280
|
-
scannedPaths,
|
|
281
|
-
} = context;
|
|
282
|
-
let preparedHtml;
|
|
283
|
-
try {
|
|
284
|
-
preparedHtml = await preparePackageHtml({
|
|
285
|
-
packageRoot,
|
|
286
|
-
sourceFiles,
|
|
287
|
-
styleSources,
|
|
288
|
-
snapshots,
|
|
289
|
-
pathOwners,
|
|
290
|
-
styleDependents,
|
|
291
|
-
});
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (!options.force || isIntegrityError(error)) throw error;
|
|
294
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
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
|
-
}
|
|
324
|
-
const packageSources = [
|
|
325
|
-
...sourceFiles
|
|
326
|
-
.filter((file) => extension(file.path) !== ".html")
|
|
327
|
-
.map((file) => preparedVue.files.get(file.path) ?? file),
|
|
328
|
-
...preparedHtml.files,
|
|
329
|
-
];
|
|
330
|
-
const ownedStyles = [...styleSources.keys()].filter(
|
|
331
|
-
(path) =>
|
|
332
|
-
pathOwners.get(path) === packageRoot &&
|
|
333
|
-
(targetable.has(path) ||
|
|
334
|
-
preparedHtml.stylePaths.has(path) ||
|
|
335
|
-
preparedVue.stylePaths.has(path)),
|
|
336
|
-
);
|
|
337
|
-
if (ownedStyles.length === 0 && preparedVue.stylesheets.length === 0) {
|
|
338
|
-
return vueWarningsOnlyResult(preparedVue);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let tailwindPath;
|
|
342
|
-
let tailwindEntries;
|
|
343
|
-
try {
|
|
344
|
-
({ path: tailwindPath, entries: tailwindEntries } = resolveTailwindEntry(
|
|
345
|
-
ownedStyles,
|
|
346
|
-
styleSources,
|
|
347
|
-
configuredEntry,
|
|
348
|
-
));
|
|
349
|
-
} catch (error) {
|
|
350
|
-
if (!options.force) throw error;
|
|
351
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const excludedEntries = new Set([...tailwindEntries, tailwindPath]);
|
|
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
|
-
}
|
|
374
|
-
if (targets.some((path) => excludedEntries.has(path))) {
|
|
375
|
-
throw new Error("The Tailwind CSS entry cannot be migrated.");
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
let tailwind;
|
|
379
|
-
try {
|
|
380
|
-
tailwind = await loadTailwind(packageRoot, tailwindPath, snapshots, workspaceRoot);
|
|
381
|
-
} catch (error) {
|
|
382
|
-
if (!options.force || isIntegrityError(error)) throw error;
|
|
383
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const files = packageSources.map((file) => {
|
|
387
|
-
const owner = pathOwners.get(file.path);
|
|
388
|
-
return {
|
|
389
|
-
...file,
|
|
390
|
-
writable:
|
|
391
|
-
targetable.has(file.path) &&
|
|
392
|
-
(options.workspaces ? writablePackages.has(owner) : owner === packageRoot),
|
|
393
|
-
};
|
|
394
|
-
});
|
|
395
|
-
let stylesheets;
|
|
396
|
-
let sass;
|
|
397
|
-
let less;
|
|
398
|
-
try {
|
|
399
|
-
stylesheets = [];
|
|
400
|
-
const compilerDependents = new Map();
|
|
401
|
-
for (const stylePath of targets.sort(compareStrings)) {
|
|
402
|
-
await rejectSymlinkTarget(stylePath, packageRoot);
|
|
403
|
-
const isPartial = isSassPath(stylePath) && basename(stylePath).startsWith("_");
|
|
404
|
-
const stylesheet = {
|
|
405
|
-
cssPath: stylePath,
|
|
406
|
-
cssSource: styleSources.get(stylePath),
|
|
407
|
-
cssModuleId: normalizedRelativePath(packageRoot, stylePath),
|
|
408
|
-
cssDependents: styleDependents.get(stylePath) ?? [],
|
|
409
|
-
syntax: stylesheetSyntax(stylePath),
|
|
410
|
-
isModule: isStylesheetModule(stylePath),
|
|
411
|
-
isPartial,
|
|
412
|
-
};
|
|
413
|
-
let compiled;
|
|
414
|
-
if (isSassPath(stylePath) && !isPartial) {
|
|
415
|
-
sass ??= await loadProjectSass(packageRoot);
|
|
416
|
-
// Compile the snapshotted source, not the on-disk file: code loaded
|
|
417
|
-
// during planning (e.g. Tailwind plugins) may have rewritten it since.
|
|
418
|
-
compiled = await compileSassEntry(sass, stylePath, stylesheet.cssSource);
|
|
419
|
-
} else if (extension(stylePath) === ".less") {
|
|
420
|
-
less ??= await loadProjectLess(packageRoot);
|
|
421
|
-
compiled = await compileLessEntry(less, stylePath, stylesheet.cssSource);
|
|
422
|
-
}
|
|
423
|
-
if (compiled) {
|
|
424
|
-
validateCss(compiled.css);
|
|
425
|
-
for (const loadedPath of compiled.loadedPaths) {
|
|
426
|
-
if (!isProjectInput(workspaceRoot, loadedPath)) continue;
|
|
427
|
-
const source = await snapshotLoadedSource(snapshots, loadedPath);
|
|
428
|
-
if (!styleSources.has(loadedPath)) styleSources.set(loadedPath, source);
|
|
429
|
-
if (loadedPath !== stylePath) {
|
|
430
|
-
const dependents = compilerDependents.get(loadedPath) ?? [];
|
|
431
|
-
dependents.push(stylePath);
|
|
432
|
-
compilerDependents.set(loadedPath, dependents);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
stylesheet.analysisSource = compiled.css;
|
|
436
|
-
stylesheet.sourceMappings = compiled.sourceMappings;
|
|
437
|
-
}
|
|
438
|
-
stylesheets.push(stylesheet);
|
|
439
|
-
}
|
|
440
|
-
for (const stylesheet of stylesheets) {
|
|
441
|
-
const dependents = compilerDependents.get(stylesheet.cssPath) ?? [];
|
|
442
|
-
if (dependents.length === 0) continue;
|
|
443
|
-
stylesheet.isPartial = true;
|
|
444
|
-
stylesheet.cssDependents = [...new Set([...stylesheet.cssDependents, ...dependents])].sort(
|
|
445
|
-
compareStrings,
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
} catch (error) {
|
|
449
|
-
if (!options.force || isIntegrityError(error)) throw error;
|
|
450
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
stylesheets.push(...preparedVue.stylesheets);
|
|
454
|
-
const request = {
|
|
455
|
-
stylesheets,
|
|
456
|
-
tailwindPath: tailwind.path,
|
|
457
|
-
tailwindSource: tailwind.css,
|
|
458
|
-
utilityPrefix: tailwind.designSystem.theme.prefix,
|
|
459
|
-
themeTokens: tailwind.themeTokens,
|
|
460
|
-
files,
|
|
461
|
-
};
|
|
462
|
-
let plan;
|
|
463
|
-
try {
|
|
464
|
-
plan = JSON.parse(planBatchMigration(JSON.stringify(request)));
|
|
465
|
-
} catch (error) {
|
|
466
|
-
if (!options.force || !isRecoverablePlanningError(error)) throw error;
|
|
467
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
plan = replanCompileFailures(tailwind, request, plan);
|
|
472
|
-
} catch (error) {
|
|
473
|
-
if (!options.force) throw error;
|
|
474
|
-
return { failure: packageFailure(workspaceRoot, packageRoot, error) };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
removeMigratedHtmlLinks(plan, preparedHtml);
|
|
478
|
-
plan.warnings.push(...preparedHtml.warnings);
|
|
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);
|
|
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) };
|
|
548
|
-
}
|
|
549
|
-
return { plan };
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// A candidate Tailwind refuses to compile retains its owning rule(s) instead
|
|
553
|
-
// of aborting the run: block those rules and replan until every applied
|
|
554
|
-
// candidate compiles. Each iteration blocks at least one new rule, so the
|
|
555
|
-
// loop is bounded by the rule count; if a failing candidate cannot be
|
|
556
|
-
// attributed to a new rule, fall back to the package-level failure path.
|
|
557
|
-
function replanCompileFailures(tailwind, request, initialPlan) {
|
|
558
|
-
let plan = initialPlan;
|
|
559
|
-
const blockedByStylesheet = new Map();
|
|
560
|
-
const maxIterations = plan.rules.length + 1;
|
|
561
|
-
for (let iteration = 0; ; iteration += 1) {
|
|
562
|
-
const failing = invalidCandidates(tailwind, plan.candidates);
|
|
563
|
-
if (failing.length === 0) break;
|
|
564
|
-
let progressed = false;
|
|
565
|
-
for (const rule of plan.rules) {
|
|
566
|
-
const failed = rule.candidates.filter((candidate) => failing.includes(candidate));
|
|
567
|
-
if (failed.length === 0) continue;
|
|
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()));
|
|
573
|
-
const key = `${rule.ruleId.start}-${rule.ruleId.end}`;
|
|
574
|
-
let entry = blocked.get(key);
|
|
575
|
-
if (!entry) {
|
|
576
|
-
blocked.set(
|
|
577
|
-
key,
|
|
578
|
-
(entry = { ruleId: rule.ruleId, authoredSpan: rule.authoredSpan, candidates: new Set() }),
|
|
579
|
-
);
|
|
580
|
-
progressed = true;
|
|
581
|
-
}
|
|
582
|
-
for (const candidate of failed) entry.candidates.add(candidate);
|
|
583
|
-
}
|
|
584
|
-
if (!progressed || iteration >= maxIterations) {
|
|
585
|
-
throw new Error(`Tailwind did not generate CSS for candidate: ${failing[0]}`);
|
|
586
|
-
}
|
|
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
|
-
);
|
|
600
|
-
}
|
|
601
|
-
for (const [index, blocked] of blockedByStylesheet) {
|
|
602
|
-
const cssPath = request.stylesheets[index].cssPath;
|
|
603
|
-
for (const { authoredSpan, candidates } of blocked.values()) {
|
|
604
|
-
const failed = [...candidates]
|
|
605
|
-
.sort(compareStrings)
|
|
606
|
-
.map((candidate) => `\`${candidate}\``)
|
|
607
|
-
.join(", ");
|
|
608
|
-
plan.warnings.push({
|
|
609
|
-
code: "candidate-compilation-failure",
|
|
610
|
-
file: cssPath,
|
|
611
|
-
// ruleId is a compiled-domain span; warnings anchor to the authored file.
|
|
612
|
-
start: authoredSpan.start,
|
|
613
|
-
end: authoredSpan.end,
|
|
614
|
-
message: `Tailwind did not generate CSS for ${failed}, so the rule is retained.`,
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return plan;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function removeMigratedHtmlLinks(plan, preparedHtml) {
|
|
622
|
-
const unlinked = new Set(plan.unlinkedFiles);
|
|
623
|
-
const linksByFile = new Map();
|
|
624
|
-
for (const link of preparedHtml.removableLinks) {
|
|
625
|
-
if (!unlinked.has(link.cssPath)) continue;
|
|
626
|
-
const links = linksByFile.get(link.filePath) ?? new Set();
|
|
627
|
-
links.add(`${link.href}\0${link.media}`);
|
|
628
|
-
linksByFile.set(link.filePath, links);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
for (const [filePath, removable] of linksByFile) {
|
|
632
|
-
const planned = plan.files.find((file) => file.path === filePath);
|
|
633
|
-
const original = preparedHtml.files.find((file) => file.path === filePath);
|
|
634
|
-
if (!original) continue;
|
|
635
|
-
const source = planned?.source ?? original.source;
|
|
636
|
-
const links = parseHtmlSource(filePath, source)
|
|
637
|
-
.links.filter((link) => removable.has(`${link.href}\0${link.media}`))
|
|
638
|
-
.sort((left, right) => right.tagStart - left.tagStart);
|
|
639
|
-
let bytes = Buffer.from(source);
|
|
640
|
-
for (const link of links) {
|
|
641
|
-
bytes = Buffer.concat([bytes.subarray(0, link.tagStart), bytes.subarray(link.tagEnd)]);
|
|
642
|
-
}
|
|
643
|
-
const updated = bytes.toString();
|
|
644
|
-
if (updated === source) continue;
|
|
645
|
-
if (planned) planned.source = updated;
|
|
646
|
-
else plan.files.push({ path: filePath, source: updated });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function mergePlans(plans, originals) {
|
|
651
|
-
const filesByPath = new Map();
|
|
652
|
-
const deletedPaths = new Set();
|
|
653
|
-
const candidates = new Set();
|
|
654
|
-
const rules = [];
|
|
655
|
-
const warnings = [];
|
|
656
|
-
const seenWarnings = new Set();
|
|
657
|
-
let convertedRules = 0;
|
|
658
|
-
let retainedRules = 0;
|
|
659
|
-
|
|
660
|
-
for (const plan of plans) {
|
|
661
|
-
for (const file of plan.files) {
|
|
662
|
-
if (!originals.has(file.path))
|
|
663
|
-
throw new Error(`Planned file is outside the source snapshot: ${file.path}`);
|
|
664
|
-
if (filesByPath.has(file.path) || deletedPaths.has(file.path)) {
|
|
665
|
-
throw new Error(`Multiple package groups planned changes for ${file.path}`);
|
|
666
|
-
}
|
|
667
|
-
filesByPath.set(file.path, file);
|
|
668
|
-
}
|
|
669
|
-
for (const path of plan.deletedFiles) {
|
|
670
|
-
if (!originals.has(path))
|
|
671
|
-
throw new Error(`Planned deletion is outside the source snapshot: ${path}`);
|
|
672
|
-
if (filesByPath.has(path) || deletedPaths.has(path)) {
|
|
673
|
-
throw new Error(`Multiple package groups planned changes for ${path}`);
|
|
674
|
-
}
|
|
675
|
-
deletedPaths.add(path);
|
|
676
|
-
}
|
|
677
|
-
for (const candidate of plan.candidates) candidates.add(candidate);
|
|
678
|
-
convertedRules += plan.convertedRules;
|
|
679
|
-
retainedRules += plan.retainedRules;
|
|
680
|
-
rules.push(...plan.rules);
|
|
681
|
-
// Per-stylesheet planning repeats the same source-site warning once per
|
|
682
|
-
// stylesheet; the user-facing report keeps the first of each.
|
|
683
|
-
for (const warning of plan.warnings) {
|
|
684
|
-
const key = `${warning.code}\0${warning.file}\0${warning.start}\0${warning.end}`;
|
|
685
|
-
if (seenWarnings.has(key)) continue;
|
|
686
|
-
seenWarnings.add(key);
|
|
687
|
-
warnings.push(warning);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
return { filesByPath, deletedPaths, candidates, rules, warnings, convertedRules, retainedRules };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
async function findPackageRoot(start) {
|
|
694
|
-
let directory = start;
|
|
695
|
-
while (true) {
|
|
696
|
-
try {
|
|
697
|
-
await readFile(join(directory, "package.json"), "utf8");
|
|
698
|
-
return directory;
|
|
699
|
-
} catch (error) {
|
|
700
|
-
if (error.code !== "ENOENT") throw error;
|
|
701
|
-
}
|
|
702
|
-
const parent = dirname(directory);
|
|
703
|
-
if (parent === directory) throw new Error(`No package.json was found from ${start}`);
|
|
704
|
-
directory = parent;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
async function findGitRoot(cwd) {
|
|
709
|
-
try {
|
|
710
|
-
const { stdout } = await run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
711
|
-
return resolve(stdout.trim());
|
|
712
|
-
} catch {
|
|
713
|
-
return undefined;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async function isIgnoredByGit(gitRoot, path) {
|
|
718
|
-
if (gitRoot === path) return false;
|
|
719
|
-
try {
|
|
720
|
-
await run("git", ["check-ignore", "-q", "--", relative(gitRoot, path)], { cwd: gitRoot });
|
|
721
|
-
return true;
|
|
722
|
-
} catch {
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
async function discoverFiles(root, useGit) {
|
|
728
|
-
if (!useGit) return collectFiles(root, isRelevantDiscoveredFile);
|
|
729
|
-
|
|
730
|
-
const { stdout } = await run("git", ["ls-files", "-co", "--exclude-standard", "-z", "--", "."], {
|
|
731
|
-
cwd: root,
|
|
732
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
733
|
-
});
|
|
734
|
-
const paths = stdout
|
|
735
|
-
.split("\0")
|
|
736
|
-
.filter(Boolean)
|
|
737
|
-
.map((path) => resolve(root, path))
|
|
738
|
-
.filter((path) => !hasIgnoredDirectory(root, path) && isRelevantDiscoveredFile(path));
|
|
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);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async function discoverPackageRoots(workspaceRoot, paths) {
|
|
752
|
-
const roots = paths.filter((path) => basename(path) === "package.json").map(dirname);
|
|
753
|
-
try {
|
|
754
|
-
await readFile(join(workspaceRoot, "package.json"), "utf8");
|
|
755
|
-
roots.push(workspaceRoot);
|
|
756
|
-
} catch (error) {
|
|
757
|
-
if (error.code !== "ENOENT") throw error;
|
|
758
|
-
}
|
|
759
|
-
return [...new Set(roots)].sort(compareStrings);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function owningPackage(path, packageRoots) {
|
|
763
|
-
return packageRoots
|
|
764
|
-
.filter((root) => isWithin(root, path))
|
|
765
|
-
.sort((left, right) => right.length - left.length)[0];
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function isWithin(root, path) {
|
|
769
|
-
return path === root || path.startsWith(`${root}${sep}`);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function hasIgnoredDirectory(root, path) {
|
|
773
|
-
return relative(root, path)
|
|
774
|
-
.split(/[\\/]/)
|
|
775
|
-
.some((part) => IGNORED_DIRECTORIES.has(part));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function isRelevantDiscoveredFile(path) {
|
|
779
|
-
return (
|
|
780
|
-
basename(path) === "package.json" ||
|
|
781
|
-
isStylesheetPath(path) ||
|
|
782
|
-
SOURCE_EXTENSIONS.has(extension(path))
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function isStylesheetPath(path) {
|
|
787
|
-
return STYLESHEET_SYNTAX.has(extension(path));
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function stylesheetSyntax(path) {
|
|
791
|
-
return STYLESHEET_SYNTAX.get(extension(path));
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function isStylesheetModule(path) {
|
|
795
|
-
const syntax = stylesheetSyntax(path);
|
|
796
|
-
return syntax !== undefined && path.endsWith(`.module.${syntax}`);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function sourceReferencesStyle(file, stylePath) {
|
|
800
|
-
let importPath = normalizedRelativePath(dirname(file.path), stylePath);
|
|
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
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
async function preparePackageHtml({
|
|
1505
|
-
packageRoot,
|
|
1506
|
-
sourceFiles,
|
|
1507
|
-
styleSources,
|
|
1508
|
-
snapshots,
|
|
1509
|
-
pathOwners,
|
|
1510
|
-
styleDependents,
|
|
1511
|
-
}) {
|
|
1512
|
-
const files = [];
|
|
1513
|
-
const stylePaths = new Set();
|
|
1514
|
-
const generatedPaths = new Set();
|
|
1515
|
-
const removableLinks = [];
|
|
1516
|
-
const warnings = [];
|
|
1517
|
-
|
|
1518
|
-
const htmlFiles = sourceFiles.filter((file) => extension(file.path) === ".html");
|
|
1519
|
-
// Package-owned HTML goes first so stylesheets it discovers are claimed for
|
|
1520
|
-
// this package before foreign consumers are matched against that ownership.
|
|
1521
|
-
const orderedFiles = [
|
|
1522
|
-
...htmlFiles.filter((file) => pathOwners.get(file.path) === packageRoot),
|
|
1523
|
-
...htmlFiles.filter((file) => pathOwners.get(file.path) !== packageRoot),
|
|
1524
|
-
];
|
|
1525
|
-
for (const file of orderedFiles) {
|
|
1526
|
-
const owner = pathOwners.get(file.path);
|
|
1527
|
-
// Foreign HTML is analyzed only as a consumer of this package's
|
|
1528
|
-
// stylesheets; its other links and attributes are its own package's
|
|
1529
|
-
// concern, so they never warn here.
|
|
1530
|
-
const foreign = owner !== packageRoot;
|
|
1531
|
-
if (foreign && !owner) continue;
|
|
1532
|
-
const referenceRoot = foreign ? owner : packageRoot;
|
|
1533
|
-
const parsed = parseHtmlSource(file.path, file.source);
|
|
1534
|
-
const contexts = [];
|
|
1535
|
-
let linkBase = dirname(file.path);
|
|
1536
|
-
const base = parsed.bases[0];
|
|
1537
|
-
if (base) {
|
|
1538
|
-
const baseReference = base.href.split(/[?#]/, 1)[0];
|
|
1539
|
-
const basePath =
|
|
1540
|
-
base.writable &&
|
|
1541
|
-
(baseReference === ""
|
|
1542
|
-
? file.path
|
|
1543
|
-
: localHtmlReference(referenceRoot, dirname(file.path), base.href));
|
|
1544
|
-
if (!basePath || !isWithin(referenceRoot, basePath)) {
|
|
1545
|
-
if (!foreign) {
|
|
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
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
linkBase = undefined;
|
|
1557
|
-
} else {
|
|
1558
|
-
linkBase = base.href.split(/[?#]/, 1)[0].endsWith("/") ? basePath : dirname(basePath);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
for (const link of parsed.links) {
|
|
1562
|
-
const linkedPath = linkBase && localHtmlReference(referenceRoot, linkBase, link.href);
|
|
1563
|
-
if (
|
|
1564
|
-
!linkedPath ||
|
|
1565
|
-
!isWithin(packageRoot, linkedPath) ||
|
|
1566
|
-
(foreign && pathOwners.get(linkedPath) !== packageRoot)
|
|
1567
|
-
) {
|
|
1568
|
-
if (!foreign) {
|
|
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
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
const variants = mediaVariants(link.media);
|
|
1582
|
-
if (variants === undefined) {
|
|
1583
|
-
const cssPath =
|
|
1584
|
-
(foreign
|
|
1585
|
-
? undefined
|
|
1586
|
-
: inferredPreprocessorPath({
|
|
1587
|
-
path: linkedPath,
|
|
1588
|
-
packageRoot,
|
|
1589
|
-
styleSources,
|
|
1590
|
-
pathOwners,
|
|
1591
|
-
styleDependents,
|
|
1592
|
-
})) ?? linkedPath;
|
|
1593
|
-
contexts.push({ cssPath, variants: [], direct: true, analyzable: false });
|
|
1594
|
-
if (!foreign) {
|
|
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
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
const contextStart = contexts.length;
|
|
1608
|
-
await collectHtmlStyleContexts({
|
|
1609
|
-
path: linkedPath,
|
|
1610
|
-
variants,
|
|
1611
|
-
direct: true,
|
|
1612
|
-
packageRoot,
|
|
1613
|
-
sourceFiles,
|
|
1614
|
-
styleSources,
|
|
1615
|
-
snapshots,
|
|
1616
|
-
pathOwners,
|
|
1617
|
-
styleDependents,
|
|
1618
|
-
stylePaths,
|
|
1619
|
-
generatedPaths,
|
|
1620
|
-
contexts,
|
|
1621
|
-
warnings,
|
|
1622
|
-
visited: new Set(),
|
|
1623
|
-
});
|
|
1624
|
-
const directContext = contexts[contextStart];
|
|
1625
|
-
if (directContext?.direct) {
|
|
1626
|
-
removableLinks.push({
|
|
1627
|
-
filePath: file.path,
|
|
1628
|
-
cssPath: directContext.cssPath,
|
|
1629
|
-
href: link.href,
|
|
1630
|
-
media: link.media,
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
if (foreign && contexts.length === 0) continue;
|
|
1636
|
-
if (!foreign && contexts.length > 0) {
|
|
1637
|
-
for (const attribute of parsed.dynamicAttributes) {
|
|
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
|
-
);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
files.push({
|
|
1650
|
-
...file,
|
|
1651
|
-
htmlElements: parsed.elements,
|
|
1652
|
-
htmlStylesheets: deduplicateHtmlContexts(contexts),
|
|
1653
|
-
htmlReferencesSafe: parsed.dynamicAttributes.length === 0,
|
|
1654
|
-
htmlScriptText: parsed.scriptText,
|
|
1655
|
-
});
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
return { files, stylePaths, generatedPaths, removableLinks, warnings };
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
async function collectHtmlStyleContexts(state) {
|
|
1662
|
-
const key = `${state.path}\0${state.variants.join(":")}`;
|
|
1663
|
-
if (state.visited.has(key)) return;
|
|
1664
|
-
state.visited.add(key);
|
|
1665
|
-
if (!isStylesheetPath(state.path)) return;
|
|
1666
|
-
const owner = state.pathOwners.get(state.path);
|
|
1667
|
-
if (owner && owner !== state.packageRoot) {
|
|
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
|
-
);
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
let source;
|
|
1681
|
-
try {
|
|
1682
|
-
source =
|
|
1683
|
-
state.styleSources.get(state.path) ?? (await snapshotFile(state.snapshots, state.path));
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
if (error.code === "ENOENT") {
|
|
1686
|
-
if (extension(state.path) === ".css") addInferredPreprocessorContext(state);
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
throw error;
|
|
1690
|
-
}
|
|
1691
|
-
if (!state.styleSources.has(state.path)) state.styleSources.set(state.path, source);
|
|
1692
|
-
if (!owner) state.pathOwners.set(state.path, state.packageRoot);
|
|
1693
|
-
|
|
1694
|
-
if (extension(state.path) === ".css" && addInferredPreprocessorContext(state)) return;
|
|
1695
|
-
|
|
1696
|
-
state.stylePaths.add(state.path);
|
|
1697
|
-
state.contexts.push({
|
|
1698
|
-
cssPath: state.path,
|
|
1699
|
-
variants: state.variants,
|
|
1700
|
-
direct: state.direct,
|
|
1701
|
-
analyzable: true,
|
|
1702
|
-
});
|
|
1703
|
-
if (extension(state.path) !== ".css") return;
|
|
1704
|
-
|
|
1705
|
-
for (const imported of cssImports(source)) {
|
|
1706
|
-
const variants = mediaVariants(imported.media);
|
|
1707
|
-
if (variants === undefined) {
|
|
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
|
-
);
|
|
1717
|
-
continue;
|
|
1718
|
-
}
|
|
1719
|
-
const importedPath = localHtmlReference(state.packageRoot, dirname(state.path), imported.href);
|
|
1720
|
-
if (!importedPath || !isWithin(state.packageRoot, importedPath)) continue;
|
|
1721
|
-
// Link-discovered stylesheets never went through indexStylesheetDependents,
|
|
1722
|
-
// so record their import edges here or deletion could leave this importer
|
|
1723
|
-
// pointing at a removed module.
|
|
1724
|
-
if (
|
|
1725
|
-
importedPath !== state.path &&
|
|
1726
|
-
(isStylesheetModule(importedPath) || isPreprocessorPath(importedPath))
|
|
1727
|
-
) {
|
|
1728
|
-
addStyleDependent(state.styleDependents, importedPath, state.path);
|
|
1729
|
-
}
|
|
1730
|
-
await collectHtmlStyleContexts({
|
|
1731
|
-
...state,
|
|
1732
|
-
path: importedPath,
|
|
1733
|
-
// Deduplicate so a cyclic import chain cannot grow the variant list and
|
|
1734
|
-
// mint a fresh visited key on every lap.
|
|
1735
|
-
variants: [...new Set([...state.variants, ...variants])],
|
|
1736
|
-
direct: false,
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function inferredPreprocessorPath(state) {
|
|
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,
|
|
1750
|
-
);
|
|
1751
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
function addInferredPreprocessorContext(state) {
|
|
1755
|
-
// A source importing the generated CSS pins the artifact itself: excluding
|
|
1756
|
-
// it from planning while migrating the inferred entry could delete the only
|
|
1757
|
-
// source able to rebuild the file that import depends on.
|
|
1758
|
-
if (
|
|
1759
|
-
state.styleSources.has(state.path) &&
|
|
1760
|
-
state.sourceFiles.some(
|
|
1761
|
-
(file) => extension(file.path) !== ".html" && sourceReferencesStyle(file, state.path),
|
|
1762
|
-
)
|
|
1763
|
-
) {
|
|
1764
|
-
return false;
|
|
1765
|
-
}
|
|
1766
|
-
const path = inferredPreprocessorPath(state);
|
|
1767
|
-
if (!path) return false;
|
|
1768
|
-
state.generatedPaths.add(state.path);
|
|
1769
|
-
state.stylePaths.add(path);
|
|
1770
|
-
state.contexts.push({
|
|
1771
|
-
cssPath: path,
|
|
1772
|
-
variants: state.variants,
|
|
1773
|
-
direct: state.direct,
|
|
1774
|
-
analyzable: true,
|
|
1775
|
-
});
|
|
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
|
-
);
|
|
1785
|
-
return true;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function cssImports(source) {
|
|
1789
|
-
const imports = [];
|
|
1790
|
-
const masked = maskCssComments(source);
|
|
1791
|
-
let depth = 0;
|
|
1792
|
-
let quote;
|
|
1793
|
-
// Browsers ignore @import once any block rule has appeared, so imports
|
|
1794
|
-
// after the first top-level `{` never load and must not be traversed.
|
|
1795
|
-
let importsAllowed = true;
|
|
1796
|
-
for (let index = 0; index < masked.length; index += 1) {
|
|
1797
|
-
const character = masked[index];
|
|
1798
|
-
if (quote) {
|
|
1799
|
-
if (character === "\\") index += 1;
|
|
1800
|
-
else if (character === quote) quote = undefined;
|
|
1801
|
-
continue;
|
|
1802
|
-
}
|
|
1803
|
-
if (character === '"' || character === "'") {
|
|
1804
|
-
quote = character;
|
|
1805
|
-
continue;
|
|
1806
|
-
}
|
|
1807
|
-
if (character === "{") {
|
|
1808
|
-
depth += 1;
|
|
1809
|
-
importsAllowed = false;
|
|
1810
|
-
continue;
|
|
1811
|
-
}
|
|
1812
|
-
if (character === "}") {
|
|
1813
|
-
depth = Math.max(0, depth - 1);
|
|
1814
|
-
continue;
|
|
1815
|
-
}
|
|
1816
|
-
if (
|
|
1817
|
-
!importsAllowed ||
|
|
1818
|
-
depth !== 0 ||
|
|
1819
|
-
masked.slice(index, index + 7).toLowerCase() !== "@import" ||
|
|
1820
|
-
/[-\w]/.test(masked[index + 7] ?? "")
|
|
1821
|
-
)
|
|
1822
|
-
continue;
|
|
1823
|
-
|
|
1824
|
-
let end = index + 7;
|
|
1825
|
-
let importQuote;
|
|
1826
|
-
let parentheses = 0;
|
|
1827
|
-
for (; end < masked.length; end += 1) {
|
|
1828
|
-
const next = masked[end];
|
|
1829
|
-
if (importQuote) {
|
|
1830
|
-
if (next === "\\") end += 1;
|
|
1831
|
-
else if (next === importQuote) importQuote = undefined;
|
|
1832
|
-
} else if (next === '"' || next === "'") importQuote = next;
|
|
1833
|
-
else if (next === "(") parentheses += 1;
|
|
1834
|
-
else if (next === ")") parentheses = Math.max(0, parentheses - 1);
|
|
1835
|
-
else if (next === ";" && parentheses === 0) break;
|
|
1836
|
-
}
|
|
1837
|
-
if (end >= masked.length) continue;
|
|
1838
|
-
const statement = masked.slice(index, end + 1);
|
|
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
|
-
});
|
|
1850
|
-
index = end;
|
|
1851
|
-
}
|
|
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
|
-
}));
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
function localHtmlReference(packageRoot, base, reference) {
|
|
1866
|
-
const path = reference.split(/[?#]/, 1)[0];
|
|
1867
|
-
if (!path || path.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(path)) return undefined;
|
|
1868
|
-
let decoded;
|
|
1869
|
-
try {
|
|
1870
|
-
decoded = decodeURIComponent(path);
|
|
1871
|
-
} catch {
|
|
1872
|
-
return undefined;
|
|
1873
|
-
}
|
|
1874
|
-
return decoded.startsWith("/") ? resolve(packageRoot, `.${decoded}`) : resolve(base, decoded);
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
function mediaVariants(media) {
|
|
1878
|
-
const normalized = media.trim().toLowerCase();
|
|
1879
|
-
if (!normalized || normalized === "all") return [];
|
|
1880
|
-
if (normalized === "print") return ["print"];
|
|
1881
|
-
return undefined;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function deduplicateHtmlContexts(contexts) {
|
|
1885
|
-
const unique = new Map();
|
|
1886
|
-
for (const context of contexts) {
|
|
1887
|
-
const key = `${context.cssPath}\0${context.variants.join(":")}\0${context.analyzable}`;
|
|
1888
|
-
const existing = unique.get(key);
|
|
1889
|
-
if (existing) existing.direct ||= context.direct;
|
|
1890
|
-
else unique.set(key, { ...context });
|
|
1891
|
-
}
|
|
1892
|
-
return [...unique.values()];
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
function htmlWarning(code, file, start, end, message) {
|
|
1896
|
-
return { code, file, start, end, message };
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function isProjectInput(workspaceRoot, path) {
|
|
1900
|
-
return (
|
|
1901
|
-
isWithin(workspaceRoot, path) &&
|
|
1902
|
-
!relative(workspaceRoot, path).split(/[\\/]/).includes("node_modules")
|
|
1903
|
-
);
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
async function rejectSymlinkTarget(path, root) {
|
|
1907
|
-
for (let current = path; isWithin(root, current); current = dirname(current)) {
|
|
1908
|
-
if ((await lstat(current)).isSymbolicLink()) {
|
|
1909
|
-
throw new Error(`Refusing to migrate a symbolic-link target: ${path}`);
|
|
1910
|
-
}
|
|
1911
|
-
if (current === root) break;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
async function snapshotFile(snapshots, 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
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function recordSnapshot(snapshots, path, source) {
|
|
1934
|
-
if (snapshots.has(path) && snapshots.get(path) !== source) {
|
|
1935
|
-
throw new Error(`Source changed during planning: ${path}`);
|
|
1936
|
-
}
|
|
1937
|
-
snapshots.set(path, source);
|
|
1938
|
-
return source;
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
function packageFailure(workspaceRoot, packageRoot, error) {
|
|
1942
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1943
|
-
return {
|
|
1944
|
-
package: normalizedRelativePath(workspaceRoot, packageRoot) || ".",
|
|
1945
|
-
message: message.startsWith(RECOVERABLE_INPUT_ERROR)
|
|
1946
|
-
? message.slice(RECOVERABLE_INPUT_ERROR.length)
|
|
1947
|
-
: message,
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
function isRecoverablePlanningError(error) {
|
|
1952
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1953
|
-
return message.startsWith(RECOVERABLE_INPUT_ERROR);
|
|
1954
|
-
}
|
|
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
|
-
|
|
1961
|
-
function isIntegrityError(error) {
|
|
1962
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1963
|
-
return message.startsWith("Source changed during planning:");
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
async function readSources(paths, snapshots) {
|
|
1967
|
-
return new Map(
|
|
1968
|
-
await Promise.all(paths.map(async (path) => [path, await snapshotFile(snapshots, path)])),
|
|
1969
|
-
);
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
async function collectFiles(root, include) {
|
|
1973
|
-
const files = [];
|
|
1974
|
-
async function visit(directory) {
|
|
1975
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
1976
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1977
|
-
for (const entry of entries) {
|
|
1978
|
-
const path = join(directory, entry.name);
|
|
1979
|
-
if (entry.isDirectory()) {
|
|
1980
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
|
|
1981
|
-
} else if (entry.isFile() && include(path)) {
|
|
1982
|
-
files.push(path);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
await visit(root);
|
|
1987
|
-
return files;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
function extension(path) {
|
|
1991
|
-
const match = /\.[^./]+$/.exec(path);
|
|
1992
|
-
return match?.[0] ?? "";
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
function normalizedRelativePath(root, path) {
|
|
1996
|
-
return relative(root, path).split(sep).join("/");
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
function maskCssComments(source) {
|
|
2000
|
-
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\r\n]/g, " "));
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
function indexStylesheetDependents(styleSources) {
|
|
2004
|
-
const dependents = new Map();
|
|
2005
|
-
for (const [path, rawSource] of styleSources) {
|
|
2006
|
-
const source = maskCssComments(rawSource);
|
|
2007
|
-
const references = [
|
|
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]),
|
|
2019
|
-
];
|
|
2020
|
-
for (const reference of new Set(references)) {
|
|
2021
|
-
for (const target of stylesheetReferenceTargets(path, reference, styleSources)) {
|
|
2022
|
-
if (target === path || (!isStylesheetModule(target) && !isPreprocessorPath(target)))
|
|
2023
|
-
continue;
|
|
2024
|
-
const paths = dependents.get(target) ?? [];
|
|
2025
|
-
paths.push(path);
|
|
2026
|
-
dependents.set(target, paths);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
for (const [target, paths] of dependents)
|
|
2031
|
-
dependents.set(target, [...new Set(paths)].sort(compareStrings));
|
|
2032
|
-
return dependents;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
function addStyleDependent(styleDependents, target, importer) {
|
|
2036
|
-
const paths = styleDependents.get(target) ?? [];
|
|
2037
|
-
if (paths.includes(importer)) return;
|
|
2038
|
-
paths.push(importer);
|
|
2039
|
-
paths.sort(compareStrings);
|
|
2040
|
-
styleDependents.set(target, paths);
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
function stylesheetReferenceTargets(importer, reference, styleSources) {
|
|
2044
|
-
const target = resolve(dirname(importer), reference);
|
|
2045
|
-
const candidates = STYLESHEET_SYNTAX.has(extension(target))
|
|
2046
|
-
? [target]
|
|
2047
|
-
: [...STYLESHEET_SYNTAX.keys()].flatMap((syntax) => [
|
|
2048
|
-
`${target}${syntax}`,
|
|
2049
|
-
join(dirname(target), `_${basename(target)}${syntax}`),
|
|
2050
|
-
join(target, `_index${syntax}`),
|
|
2051
|
-
join(target, `index${syntax}`),
|
|
2052
|
-
]);
|
|
2053
|
-
return candidates.filter((path) => styleSources.has(path));
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
function resolveTailwindEntry(stylePaths, styleSources, configuredPath) {
|
|
2057
|
-
const entries = stylePaths.filter((path) => {
|
|
2058
|
-
if (extension(path) !== ".css") return false;
|
|
2059
|
-
const source = maskCssComments(styleSources.get(path));
|
|
2060
|
-
return /@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(source);
|
|
2061
|
-
});
|
|
2062
|
-
if (configuredPath) return { path: configuredPath, entries };
|
|
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.");
|
|
2067
|
-
return { path: entries[0], entries };
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
async function loadTailwind(packageRoot, tailwindCss, snapshots, workspaceRoot) {
|
|
2071
|
-
const projectRequire = createRequire(join(packageRoot, "package.json"));
|
|
2072
|
-
let packagePath;
|
|
2073
|
-
try {
|
|
2074
|
-
packagePath = projectRequire.resolve("tailwindcss/package.json");
|
|
2075
|
-
} catch {
|
|
2076
|
-
throw new Error("Tailwind v4 must be installed in the target project.");
|
|
2077
|
-
}
|
|
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}.`);
|
|
2081
|
-
|
|
2082
|
-
const modulePath = projectRequire.resolve("tailwindcss");
|
|
2083
|
-
const tailwindModule = await import(pathToFileURL(modulePath));
|
|
2084
|
-
const { __unstable__loadDesignSystem: loadDesignSystem } =
|
|
2085
|
-
tailwindModule.default ?? tailwindModule;
|
|
2086
|
-
const css = await snapshotFile(snapshots, tailwindCss);
|
|
2087
|
-
const base = dirname(tailwindCss);
|
|
2088
|
-
const loadModule = createModuleLoader(snapshots, workspaceRoot);
|
|
2089
|
-
const loadStylesheet = createStylesheetLoader(
|
|
2090
|
-
projectRequire,
|
|
2091
|
-
packagePath,
|
|
2092
|
-
snapshots,
|
|
2093
|
-
workspaceRoot,
|
|
2094
|
-
);
|
|
2095
|
-
const defaultTheme = await readFile(join(dirname(packagePath), "theme.css"), "utf8");
|
|
2096
|
-
const themeTokens = {
|
|
2097
|
-
...extractThemeTokens(defaultTheme),
|
|
2098
|
-
...(await extractThemeTokensFromGraph(css, base, loadStylesheet)),
|
|
2099
|
-
};
|
|
2100
|
-
const designSystem = await loadDesignSystem(css, { base, loadModule, loadStylesheet });
|
|
2101
|
-
return { designSystem, css, path: tailwindCss, themeTokens };
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
function extractThemeTokens(css) {
|
|
2105
|
-
const tokens = {};
|
|
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();
|
|
2109
|
-
}
|
|
2110
|
-
return tokens;
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
async function extractThemeTokensFromGraph(css, base, loadStylesheet, seen = new Set()) {
|
|
2114
|
-
const tokens = {};
|
|
2115
|
-
for (const match of css.matchAll(/@import\s+["']([^"']+)["']/g)) {
|
|
2116
|
-
const key = `${base}\0${match[1]}`;
|
|
2117
|
-
if (seen.has(key)) continue;
|
|
2118
|
-
seen.add(key);
|
|
2119
|
-
const loaded = await loadStylesheet(match[1], base);
|
|
2120
|
-
Object.assign(
|
|
2121
|
-
tokens,
|
|
2122
|
-
await extractThemeTokensFromGraph(loaded.content, loaded.base, loadStylesheet, seen),
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
return Object.assign(tokens, extractThemeTokens(css));
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
function invalidCandidates(tailwind, candidates) {
|
|
2129
|
-
const generated = tailwind.designSystem.candidatesToCss(candidates);
|
|
2130
|
-
return candidates.filter((_, index) => generated[index] === null);
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
function createModuleLoader(snapshots, workspaceRoot) {
|
|
2134
|
-
return async (id, base) => {
|
|
2135
|
-
const path = createRequire(join(base, "package.json")).resolve(id);
|
|
2136
|
-
if (isProjectInput(workspaceRoot, path)) await snapshotFile(snapshots, path);
|
|
2137
|
-
const imported = await import(pathToFileURL(path));
|
|
2138
|
-
return { path, base: dirname(path), module: imported.default ?? imported };
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
function createStylesheetLoader(projectRequire, tailwindPackagePath, snapshots, workspaceRoot) {
|
|
2143
|
-
const tailwindRoot = dirname(tailwindPackagePath);
|
|
2144
|
-
return async (id, base) => {
|
|
2145
|
-
let path;
|
|
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);
|
|
2151
|
-
else path = projectRequire.resolve(id);
|
|
2152
|
-
const content = isProjectInput(workspaceRoot, path)
|
|
2153
|
-
? await snapshotFile(snapshots, path)
|
|
2154
|
-
: await readFile(path, "utf8");
|
|
2155
|
-
return { content, base: dirname(path) };
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
function unifiedDiff(path, before, after) {
|
|
2160
|
-
const oldLines = before.split("\n");
|
|
2161
|
-
const newLines = after.split("\n");
|
|
2162
|
-
return [
|
|
2163
|
-
`--- a/${path}\n`,
|
|
2164
|
-
`+++ b/${path}\n`,
|
|
2165
|
-
`@@ -1,${oldLines.length} +1,${newLines.length} @@\n`,
|
|
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("");
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
async function verifySnapshots(snapshots) {
|
|
2176
|
-
for (const [path, before] of [...snapshots].sort(([left], [right]) =>
|
|
2177
|
-
left.localeCompare(right),
|
|
2178
|
-
)) {
|
|
2179
|
-
let current;
|
|
2180
|
-
try {
|
|
2181
|
-
current = await readFile(path, "utf8");
|
|
2182
|
-
} catch (error) {
|
|
2183
|
-
throw new Error(`Source changed after planning: ${path} (${error.code ?? error.message})`);
|
|
2184
|
-
}
|
|
2185
|
-
if (current !== before) throw new Error(`Source changed after planning: ${path}`);
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
async function writeChanges(changes, deletions) {
|
|
2190
|
-
const token = `${process.pid}-${Date.now()}`;
|
|
2191
|
-
const staged = changes.map((change, index) => [
|
|
2192
|
-
join(dirname(change.path), `.${basename(change.path)}.tw-migrate-${token}-${index}`),
|
|
2193
|
-
change,
|
|
2194
|
-
]);
|
|
2195
|
-
const backups = [...changes, ...deletions].map((change, index) => [
|
|
2196
|
-
join(dirname(change.path), `.${basename(change.path)}.tw-migrate-backup-${token}-${index}`),
|
|
2197
|
-
change.path,
|
|
2198
|
-
]);
|
|
2199
|
-
const backedUp = [];
|
|
2200
|
-
let succeeded = false;
|
|
2201
|
-
try {
|
|
2202
|
-
for (const [temporaryPath, change] of staged) {
|
|
2203
|
-
const { mode } = await stat(change.path);
|
|
2204
|
-
await writeFile(temporaryPath, change.source);
|
|
2205
|
-
await chmod(temporaryPath, mode & 0o777);
|
|
2206
|
-
}
|
|
2207
|
-
for (const [backupPath, originalPath] of backups) {
|
|
2208
|
-
await rename(originalPath, backupPath);
|
|
2209
|
-
backedUp.push([backupPath, originalPath]);
|
|
2210
|
-
}
|
|
2211
|
-
for (const [temporaryPath, change] of staged) await rename(temporaryPath, change.path);
|
|
2212
|
-
succeeded = true;
|
|
2213
|
-
} finally {
|
|
2214
|
-
if (succeeded) {
|
|
2215
|
-
await Promise.all(backups.map(([backupPath]) => rm(backupPath, { force: true })));
|
|
2216
|
-
} else {
|
|
2217
|
-
for (const [backupPath, originalPath] of backedUp.reverse()) {
|
|
2218
|
-
try {
|
|
2219
|
-
await rename(backupPath, originalPath);
|
|
2220
|
-
} catch {}
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
await Promise.all(staged.map(([temporaryPath]) => rm(temporaryPath, { force: true })));
|
|
2224
|
-
}
|
|
2225
|
-
}
|