@master/css-cli 2.0.0-rc.88 → 2.0.1
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 +22 -135
- package/dist/asset-ownership.js +150 -0
- package/dist/bin/index.js +1 -2
- package/dist/constants.js +1 -2
- package/dist/core.js +18 -21
- package/dist/dependency-rebuild.js +43 -0
- package/dist/generate.js +388 -0
- package/dist/inspect.js +51 -7
- package/dist/lint.js +59 -37
- package/dist/publication-lock.js +103 -0
- package/dist/publication.js +109 -0
- package/dist/source-watch.js +41 -0
- package/dist/suggest.js +70 -0
- package/package.json +55 -59
- package/LICENSE +0 -21
- package/dist/scan.js +0 -199
package/dist/generate.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { createDependencyRebuild } from "./dependency-rebuild.js";
|
|
2
|
+
import { publishOwnedStylesheet, stylesheetStatePath } from "./asset-ownership.js";
|
|
3
|
+
import { createSourceWatchPlan } from "./source-watch.js";
|
|
4
|
+
import { DEFAULT_SCAN_OUTPUT } from "./constants.js";
|
|
5
|
+
import { withStylesheetPublicationLock } from "./publication-lock.js";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import defaultManifestJSON from "@master/css-preset/default-manifest.json" with { type: "json" };
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { once } from "node:events";
|
|
12
|
+
// src/generate.ts
|
|
13
|
+
const DEFAULT_SOURCE_PATTERNS = ["**/*.{html,htm,js,mjs,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php}"];
|
|
14
|
+
let scannerModulePromise;
|
|
15
|
+
let stylesheetModulePromise;
|
|
16
|
+
let projectModulePromise;
|
|
17
|
+
let fastGlobModulePromise;
|
|
18
|
+
let chokidarModulePromise;
|
|
19
|
+
let bytesPromise;
|
|
20
|
+
const defaultManifest = defaultManifestJSON;
|
|
21
|
+
async function registerManagedCSSEntries(scanner, stylesheets, delivery) {
|
|
22
|
+
const [_stylesheet, { discoverManifestEntries }] = await Promise.all([loadStylesheetModule(), loadProjectModule()]);
|
|
23
|
+
stylesheets.clear();
|
|
24
|
+
for (const entry of await discoverManifestEntries({ root: scanner.cwd })) {
|
|
25
|
+
delivery?.onDependency?.(entry);
|
|
26
|
+
await stylesheets.register(scanner, entry, fs.readFileSync(entry, "utf8"), {
|
|
27
|
+
baseManifest: defaultManifest,
|
|
28
|
+
projectDir: scanner.cwd,
|
|
29
|
+
delivery
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
scanner.resetDependencies = [...stylesheets.snapshot().dependencies];
|
|
33
|
+
}
|
|
34
|
+
function normalizeSourcePatterns(specifiedSourcePaths) {
|
|
35
|
+
return specifiedSourcePaths?.length ? specifiedSourcePaths : DEFAULT_SOURCE_PATTERNS;
|
|
36
|
+
}
|
|
37
|
+
function normalizeGlobPatterns(patterns) {
|
|
38
|
+
return patterns.map((pattern) => pattern.replace(/\\/g, "/"));
|
|
39
|
+
}
|
|
40
|
+
function filesystemSourceIdentity(cwd, source) {
|
|
41
|
+
return path.relative(cwd, path.resolve(cwd, source)).split(path.sep).join("/");
|
|
42
|
+
}
|
|
43
|
+
function resolveSourcePaths(scanner, fg, sourcePatterns, ignore = [], explicit = false) {
|
|
44
|
+
const sources = fg.sync(normalizeGlobPatterns(sourcePatterns), {
|
|
45
|
+
cwd: scanner.cwd,
|
|
46
|
+
ignore: normalizeGlobPatterns(ignore)
|
|
47
|
+
}).filter((source) => source && scanner.isSourceAllowed(source, { explicit }));
|
|
48
|
+
return [...new Set(sources.map((source) => filesystemSourceIdentity(scanner.cwd, source)))];
|
|
49
|
+
}
|
|
50
|
+
async function scanSourceFile(scanner, source) {
|
|
51
|
+
const filepath = path.resolve(scanner.cwd, source);
|
|
52
|
+
await scanner.scan(source, fs.readFileSync(filepath, "utf8"));
|
|
53
|
+
}
|
|
54
|
+
async function scanSourceFiles(scanner, sourcePaths) {
|
|
55
|
+
await scanner.reconcileSources("project", sourcePaths.map((source) => ({
|
|
56
|
+
source,
|
|
57
|
+
content: fs.readFileSync(path.resolve(scanner.cwd, source), "utf8")
|
|
58
|
+
})));
|
|
59
|
+
}
|
|
60
|
+
async function prepareScanner(scanner, stylesheets, sourcePaths, delivery) {
|
|
61
|
+
await registerManagedCSSEntries(scanner, stylesheets, delivery);
|
|
62
|
+
await scanSourceFiles(scanner, sourcePaths);
|
|
63
|
+
}
|
|
64
|
+
async function reportExport(scanner, css, filename = DEFAULT_SCAN_OUTPUT) {
|
|
65
|
+
const filepath = path.resolve(scanner.cwd, filename);
|
|
66
|
+
if (scanner.options.verbose) {
|
|
67
|
+
const bytes = await loadBytes();
|
|
68
|
+
process.stderr.write(`${filename} exported ${bytes(css.length)}\n`);
|
|
69
|
+
}
|
|
70
|
+
scanner.emit("export", filename, filepath);
|
|
71
|
+
}
|
|
72
|
+
function formatWatchedPath(cwd, file) {
|
|
73
|
+
return path.isAbsolute(file) ? path.relative(cwd, file) : file;
|
|
74
|
+
}
|
|
75
|
+
function createDependencyWatchPlan(dependencies) {
|
|
76
|
+
const contains = (parent, file) => {
|
|
77
|
+
const relative = path.relative(parent, file);
|
|
78
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
79
|
+
};
|
|
80
|
+
const directories = [...dependencies].filter((file) => fs.statSync(file, { throwIfNoEntry: false })?.isDirectory());
|
|
81
|
+
const matches = (file) => dependencies.has(path.resolve(file)) || directories.some((directory) => contains(directory, file));
|
|
82
|
+
const roots = [...new Set([...dependencies].map((file) => {
|
|
83
|
+
let directory = path.dirname(file);
|
|
84
|
+
while (!fs.statSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
|
|
85
|
+
const parent = path.dirname(directory);
|
|
86
|
+
if (parent === directory) break;
|
|
87
|
+
directory = parent;
|
|
88
|
+
}
|
|
89
|
+
return directory;
|
|
90
|
+
}))];
|
|
91
|
+
return {
|
|
92
|
+
roots: roots.filter((root) => !roots.some((other) => root !== other && contains(other, root))),
|
|
93
|
+
matches,
|
|
94
|
+
ignored(file, stats) {
|
|
95
|
+
if (stats?.isDirectory()) return !matches(file) && ![...dependencies].some((target) => contains(file, target));
|
|
96
|
+
return stats?.isFile() ? !matches(file) : false;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function waitForWatcherReady(watcher) {
|
|
101
|
+
await once(watcher, "ready");
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
103
|
+
}
|
|
104
|
+
async function runGenerate(specifiedSourcePaths = [], options = {}) {
|
|
105
|
+
const pipelineModules = Promise.all([loadStylesheetModule(), loadProjectModule()]);
|
|
106
|
+
const [MasterCSSScanner, fg] = await Promise.all([loadCSSScanner(), loadFastGlob()]);
|
|
107
|
+
const { watch, output, verbose, cwd } = options;
|
|
108
|
+
const scanner = new MasterCSSScanner({
|
|
109
|
+
manifest: defaultManifest,
|
|
110
|
+
binding: options.binding,
|
|
111
|
+
verbose: verbose ? +verbose : void 0
|
|
112
|
+
}, cwd);
|
|
113
|
+
const { createStylesheetCollection } = await loadStylesheetModule();
|
|
114
|
+
const stylesheets = createStylesheetCollection();
|
|
115
|
+
const sourcePatterns = normalizeSourcePatterns(specifiedSourcePaths);
|
|
116
|
+
const outputPath = path.resolve(scanner.cwd, output || "master.css");
|
|
117
|
+
const outputFiles = /* @__PURE__ */ new Set([outputPath, stylesheetStatePath(outputPath)]);
|
|
118
|
+
const attemptedDependencies = /* @__PURE__ */ new Set();
|
|
119
|
+
const missingDependencies = /* @__PURE__ */ new Set();
|
|
120
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
121
|
+
const assetPrefix = `master-${hash(path.relative(scanner.cwd, outputPath))}`;
|
|
122
|
+
let revision = "";
|
|
123
|
+
let reportedOutput = false;
|
|
124
|
+
let resourceBytes;
|
|
125
|
+
const readResource = (file) => {
|
|
126
|
+
const bytes = resourceBytes?.get(file) ?? fs.readFileSync(file);
|
|
127
|
+
resourceBytes?.set(file, bytes);
|
|
128
|
+
return bytes;
|
|
129
|
+
};
|
|
130
|
+
const delivery = options.export ? {
|
|
131
|
+
entryURL: `./${encodeURIComponent(path.basename(outputPath))}`,
|
|
132
|
+
onDependency: watch ? (file) => {
|
|
133
|
+
const absolute = path.resolve(file);
|
|
134
|
+
attemptedDependencies.add(absolute);
|
|
135
|
+
if (!fs.existsSync(absolute)) missingDependencies.add(absolute);
|
|
136
|
+
} : void 0,
|
|
137
|
+
stylesheetURL: (file, variant) => `./${assetPrefix}-${revision}${hash(path.relative(scanner.cwd, file) + (variant?.slice(file.length) ?? ""))}.css`,
|
|
138
|
+
resourceURL: (file) => `./${assetPrefix}-${hash(readResource(file))}${encodeURIComponent(path.extname(file))}`,
|
|
139
|
+
relativeResourceURLs: true,
|
|
140
|
+
inlineImports: true
|
|
141
|
+
} : void 0;
|
|
142
|
+
const writeOutput = async () => {
|
|
143
|
+
const compose = () => stylesheets.compose({
|
|
144
|
+
scanner,
|
|
145
|
+
baseManifest: defaultManifest,
|
|
146
|
+
projectDir: scanner.cwd,
|
|
147
|
+
pruneNativeCSS: options.pruneNativeCSS,
|
|
148
|
+
validation: options.strict ? "error" : "report",
|
|
149
|
+
onDiagnostic: (diagnostic) => {
|
|
150
|
+
if (diagnostic.severity === "error") process.stderr.write(`${diagnostic.code}: ${diagnostic.message}\n`);
|
|
151
|
+
},
|
|
152
|
+
delivery
|
|
153
|
+
});
|
|
154
|
+
revision = "";
|
|
155
|
+
resourceBytes = options.export ? /* @__PURE__ */ new Map() : void 0;
|
|
156
|
+
try {
|
|
157
|
+
let composition = await compose();
|
|
158
|
+
if (options.export) {
|
|
159
|
+
revision = `${hash(JSON.stringify({
|
|
160
|
+
css: composition.css,
|
|
161
|
+
stylesheets: composition.stylesheets?.map(({ href, css }) => ({
|
|
162
|
+
href,
|
|
163
|
+
css
|
|
164
|
+
}))
|
|
165
|
+
}))}-`;
|
|
166
|
+
composition = await compose();
|
|
167
|
+
const assets = /* @__PURE__ */ new Map();
|
|
168
|
+
const addAsset = (href, bytes) => {
|
|
169
|
+
const target = fileURLToPath(new URL(href, pathToFileURL(outputPath)));
|
|
170
|
+
if (assets.has(target) && !assets.get(target).equals(bytes)) throw new Error(`Conflicting stylesheet assets: ${target}`);
|
|
171
|
+
assets.set(target, bytes);
|
|
172
|
+
};
|
|
173
|
+
for (const asset of composition.stylesheets || []) addAsset(asset.href, Buffer.from(asset.css));
|
|
174
|
+
for (const asset of composition.resources || []) addAsset(asset.href, readResource(asset.file));
|
|
175
|
+
const published = await withStylesheetPublicationLock(() => publishOwnedStylesheet(outputPath, composition.css, assets, outputFiles));
|
|
176
|
+
scanner.resetDependencies = [.../* @__PURE__ */ new Set([...scanner.resetDependencies, ...composition.dependencies || []])];
|
|
177
|
+
if (!watch || published || !reportedOutput) {
|
|
178
|
+
await reportExport(scanner, composition.css, output);
|
|
179
|
+
reportedOutput = true;
|
|
180
|
+
}
|
|
181
|
+
} else process.stdout.write(`${composition.css}\n`);
|
|
182
|
+
} finally {
|
|
183
|
+
revision = "";
|
|
184
|
+
resourceBytes = void 0;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
await scanner.init();
|
|
188
|
+
const scanPaths = () => resolveSourcePaths(scanner, fg, sourcePatterns, specifiedSourcePaths.length ? [] : scanner.options.exclude, specifiedSourcePaths.length > 0).filter((file) => !options.export || !outputFiles.has(path.resolve(scanner.cwd, file)));
|
|
189
|
+
if (watch) {
|
|
190
|
+
const chokidar = await loadChokidar();
|
|
191
|
+
const watchers = [];
|
|
192
|
+
let pipelineReady = false;
|
|
193
|
+
let stopped = false;
|
|
194
|
+
let writing = Promise.resolve();
|
|
195
|
+
let planWatcher;
|
|
196
|
+
let sourceWatcher;
|
|
197
|
+
let sourceRefreshQueued = false;
|
|
198
|
+
let sourceRefreshRequested = false;
|
|
199
|
+
let watchedDependencies = /* @__PURE__ */ new Set();
|
|
200
|
+
const closeWatcher = async (watcher) => {
|
|
201
|
+
const index = watchers.indexOf(watcher);
|
|
202
|
+
if (index >= 0) watchers.splice(index, 1);
|
|
203
|
+
await watcher.close();
|
|
204
|
+
};
|
|
205
|
+
const closeWatchers = async () => {
|
|
206
|
+
await Promise.all(watchers.splice(0).map((watcher) => watcher.close()));
|
|
207
|
+
};
|
|
208
|
+
const shutdown = async () => {
|
|
209
|
+
stopped = true;
|
|
210
|
+
await writing;
|
|
211
|
+
await closeWatchers();
|
|
212
|
+
await scanner.dispose();
|
|
213
|
+
stylesheets.dispose();
|
|
214
|
+
};
|
|
215
|
+
const syncDependencies = async (dependencies) => {
|
|
216
|
+
if (stopped) return;
|
|
217
|
+
const next = new Set([...dependencies].map((file) => path.resolve(scanner.cwd, file)));
|
|
218
|
+
if (next.size === watchedDependencies.size && [...next].every((file) => watchedDependencies.has(file))) return;
|
|
219
|
+
const previous = planWatcher;
|
|
220
|
+
if (next.size) {
|
|
221
|
+
const plan = createDependencyWatchPlan(next);
|
|
222
|
+
planWatcher = chokidar.watch(plan.roots, {
|
|
223
|
+
ignoreInitial: true,
|
|
224
|
+
ignored: (file, stats) => options.export && outputFiles.has(path.resolve(scanner.cwd, file)) || plan.ignored(file, stats)
|
|
225
|
+
});
|
|
226
|
+
for (const event of [
|
|
227
|
+
"add",
|
|
228
|
+
"change",
|
|
229
|
+
"unlink"
|
|
230
|
+
]) planWatcher.on(event, (file) => {
|
|
231
|
+
if (!plan.matches(file) || options.export && outputFiles.has(path.resolve(scanner.cwd, file))) return;
|
|
232
|
+
if (scanner.options.verbose) process.stderr.write(`\n[change] ${formatWatchedPath(scanner.cwd, file)}\n`);
|
|
233
|
+
enqueue(() => rebuildDependency(file));
|
|
234
|
+
});
|
|
235
|
+
watchers.push(planWatcher);
|
|
236
|
+
await waitForWatcherReady(planWatcher);
|
|
237
|
+
} else planWatcher = void 0;
|
|
238
|
+
if (previous) {
|
|
239
|
+
watchers.splice(watchers.indexOf(previous), 1);
|
|
240
|
+
await previous.close();
|
|
241
|
+
}
|
|
242
|
+
watchedDependencies = next;
|
|
243
|
+
};
|
|
244
|
+
const rebuild = async (initial = false) => {
|
|
245
|
+
pipelineReady = false;
|
|
246
|
+
attemptedDependencies.clear();
|
|
247
|
+
missingDependencies.clear();
|
|
248
|
+
await prepareScanner(scanner, stylesheets, scanPaths(), delivery);
|
|
249
|
+
await writeOutput();
|
|
250
|
+
await syncDependencies([
|
|
251
|
+
...scanner.resetDependencies,
|
|
252
|
+
...scanner.sourcePolicyDependencies,
|
|
253
|
+
...attemptedDependencies
|
|
254
|
+
]);
|
|
255
|
+
pipelineReady = true;
|
|
256
|
+
missingDependencies.clear();
|
|
257
|
+
process.stderr.write(initial ? "\nStart watching source changes\n" : "\nRestart watching source changes\n");
|
|
258
|
+
if (!initial) scanner.emit("resetDependencyChange");
|
|
259
|
+
};
|
|
260
|
+
const enqueue = (operation) => {
|
|
261
|
+
writing = writing.then(async () => {
|
|
262
|
+
if (!stopped) await operation();
|
|
263
|
+
}).catch(async (error) => {
|
|
264
|
+
pipelineReady = false;
|
|
265
|
+
const missing = error?.path;
|
|
266
|
+
if (missing) attemptedDependencies.add(path.resolve(scanner.cwd, missing));
|
|
267
|
+
await syncDependencies([
|
|
268
|
+
...watchedDependencies,
|
|
269
|
+
...scanner.resetDependencies,
|
|
270
|
+
...attemptedDependencies
|
|
271
|
+
]);
|
|
272
|
+
process.stderr.write(`Cannot rebuild CSS: ${error}\n`);
|
|
273
|
+
if (!stopped && [...missingDependencies].some((file) => fs.existsSync(file))) enqueue(() => rebuild());
|
|
274
|
+
});
|
|
275
|
+
return writing;
|
|
276
|
+
};
|
|
277
|
+
const rebuildDependency = createDependencyRebuild(() => rebuild(), () => pipelineReady);
|
|
278
|
+
const sourcePlan = createSourceWatchPlan(fg, scanner.cwd, sourcePatterns, specifiedSourcePaths.length ? [] : scanner.options.exclude);
|
|
279
|
+
const startSourceWatcher = async (reconcile = false) => {
|
|
280
|
+
if (stopped) return;
|
|
281
|
+
if (!sourcePlan.roots.length) return;
|
|
282
|
+
const previous = sourceWatcher;
|
|
283
|
+
const next = chokidar.watch(sourcePlan.roots, {
|
|
284
|
+
cwd: scanner.cwd,
|
|
285
|
+
ignoreInitial: true,
|
|
286
|
+
ignored: (file, stats) => options.export && outputFiles.has(path.resolve(scanner.cwd, file)) || sourcePlan.ignored(file, stats)
|
|
287
|
+
});
|
|
288
|
+
const scanChangedSource = (file) => {
|
|
289
|
+
if (!sourcePlan.matches(file)) return;
|
|
290
|
+
const source = filesystemSourceIdentity(scanner.cwd, file);
|
|
291
|
+
enqueue(async () => {
|
|
292
|
+
if (!pipelineReady) return rebuild();
|
|
293
|
+
if (scanner.isSourceAllowed(source, { explicit: specifiedSourcePaths.length > 0 })) await scanSourceFile(scanner, source);
|
|
294
|
+
else scanner.removeSource(source);
|
|
295
|
+
await writeOutput();
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
next.on("add", scanChangedSource);
|
|
299
|
+
next.on("change", scanChangedSource);
|
|
300
|
+
next.on("unlink", (file) => {
|
|
301
|
+
if (!sourcePlan.matches(file)) return;
|
|
302
|
+
const source = filesystemSourceIdentity(scanner.cwd, file);
|
|
303
|
+
enqueue(async () => {
|
|
304
|
+
scanner.removeSource(source);
|
|
305
|
+
await writeOutput();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
next.on("addDir", () => {
|
|
309
|
+
if (stopped || !sourceWatcher) return;
|
|
310
|
+
sourceRefreshRequested = true;
|
|
311
|
+
if (sourceRefreshQueued) return;
|
|
312
|
+
sourceRefreshQueued = true;
|
|
313
|
+
enqueue(async () => {
|
|
314
|
+
try {
|
|
315
|
+
while (sourceRefreshRequested && !stopped) {
|
|
316
|
+
sourceRefreshRequested = false;
|
|
317
|
+
await startSourceWatcher(true);
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
sourceRefreshQueued = false;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
watchers.push(next);
|
|
325
|
+
try {
|
|
326
|
+
await waitForWatcherReady(next);
|
|
327
|
+
if (stopped) return;
|
|
328
|
+
sourceWatcher = next;
|
|
329
|
+
if (reconcile) {
|
|
330
|
+
if (!pipelineReady) await rebuild();
|
|
331
|
+
else {
|
|
332
|
+
await scanSourceFiles(scanner, scanPaths());
|
|
333
|
+
await writeOutput();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
if (sourceWatcher !== next) await closeWatcher(next);
|
|
338
|
+
else if (previous) await closeWatcher(previous);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
process.once("SIGTERM", () => {
|
|
342
|
+
shutdown().finally(() => process.exit(0));
|
|
343
|
+
});
|
|
344
|
+
process.once("SIGINT", () => {
|
|
345
|
+
shutdown().finally(() => process.exit(0));
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
await pipelineModules;
|
|
349
|
+
await startSourceWatcher();
|
|
350
|
+
await enqueue(() => rebuild(true));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
await shutdown();
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
} else try {
|
|
356
|
+
await pipelineModules;
|
|
357
|
+
await prepareScanner(scanner, stylesheets, scanPaths(), delivery);
|
|
358
|
+
await writeOutput();
|
|
359
|
+
} finally {
|
|
360
|
+
await scanner.dispose();
|
|
361
|
+
stylesheets.dispose();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function loadCSSScanner() {
|
|
365
|
+
scannerModulePromise ||= import("@master/css-tooling/scanner/node").then((mod) => mod.MasterCSSScanner);
|
|
366
|
+
return scannerModulePromise;
|
|
367
|
+
}
|
|
368
|
+
function loadStylesheetModule() {
|
|
369
|
+
stylesheetModulePromise ||= import("@master/css-compiler/stylesheet");
|
|
370
|
+
return stylesheetModulePromise;
|
|
371
|
+
}
|
|
372
|
+
function loadProjectModule() {
|
|
373
|
+
projectModulePromise ||= import("@master/css-compiler/project");
|
|
374
|
+
return projectModulePromise;
|
|
375
|
+
}
|
|
376
|
+
function loadFastGlob() {
|
|
377
|
+
fastGlobModulePromise ||= import("fast-glob").then((mod) => mod.default || mod);
|
|
378
|
+
return fastGlobModulePromise;
|
|
379
|
+
}
|
|
380
|
+
function loadChokidar() {
|
|
381
|
+
chokidarModulePromise ||= import("chokidar").then((mod) => mod.default);
|
|
382
|
+
return chokidarModulePromise;
|
|
383
|
+
}
|
|
384
|
+
function loadBytes() {
|
|
385
|
+
bytesPromise ||= import("bytes").then((mod) => mod.default || mod);
|
|
386
|
+
return bytesPromise;
|
|
387
|
+
}
|
|
388
|
+
export { runGenerate as default };
|
package/dist/inspect.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import defaultManifestJSON from "@master/css-preset/default-manifest.json" with { type: "json" };
|
|
3
|
+
import { captureMasterCSSProject, compareProjectSnapshots, createMasterCSSDesignSystemReport, createMasterCSSInspectionReport } from "@master/css-compiler/diagnostics";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
// src/inspect.ts
|
|
7
|
+
const defaultManifest = defaultManifestJSON;
|
|
4
8
|
function parseMaxWarnings(value) {
|
|
5
9
|
if (value === void 0) return Number.POSITIVE_INFINITY;
|
|
6
10
|
const maxWarnings = Number(value);
|
|
@@ -17,22 +21,62 @@ function formatStylish(report) {
|
|
|
17
21
|
return `${lines.join("\n")}\n`;
|
|
18
22
|
}
|
|
19
23
|
function outputReport(report, format) {
|
|
20
|
-
if (format === "stylish") process.
|
|
21
|
-
else
|
|
24
|
+
if (format === "stylish") process.stderr.write(formatStylish(report));
|
|
25
|
+
else process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
22
26
|
}
|
|
23
27
|
async function runInspect(specifiedSourcePaths = [], options = {}) {
|
|
24
28
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
25
29
|
const format = options.format || "json";
|
|
26
30
|
const exitCode = options.exitCode || "diagnostics";
|
|
27
|
-
|
|
31
|
+
if (options.designSystemOptions && !options.designSystem) throw new Error("--design-system-options requires --design-system.");
|
|
32
|
+
if (options.designSystem) {
|
|
33
|
+
if (options.snapshot || options.compare || options.classes || options.includeCss) throw new Error("--design-system inspects the managed CSS graph; do not combine it with --classes, --include-css, --snapshot or --compare.");
|
|
34
|
+
const routeStylesheets = specifiedSourcePaths.length ? (await fg([...specifiedSourcePaths], {
|
|
35
|
+
cwd,
|
|
36
|
+
absolute: true,
|
|
37
|
+
onlyFiles: true,
|
|
38
|
+
ignore: ["**/node_modules/**"]
|
|
39
|
+
})).sort() : [];
|
|
40
|
+
if (specifiedSourcePaths.length && !routeStylesheets.length) throw new Error(`No route stylesheet matched: ${specifiedSourcePaths.join(", ")}`);
|
|
41
|
+
const nonCSS = routeStylesheets.filter((file) => path.extname(file) !== ".css");
|
|
42
|
+
if (nonCSS.length) throw new Error(`--design-system source paths must be stylesheets: ${nonCSS.map((file) => path.relative(cwd, file)).join(", ")}`);
|
|
43
|
+
const designSystem = options.designSystemOptions ? JSON.parse(await readFile(path.resolve(cwd, options.designSystemOptions), "utf8")) : void 0;
|
|
44
|
+
if (options.designSystemOptions && (!designSystem || typeof designSystem !== "object" || Array.isArray(designSystem))) throw new TypeError("Design-system options must be a versioned JSON object.");
|
|
45
|
+
const report = await createMasterCSSDesignSystemReport({
|
|
46
|
+
manifest: defaultManifest,
|
|
47
|
+
cwd,
|
|
48
|
+
designSystem,
|
|
49
|
+
routeStylesheets
|
|
50
|
+
});
|
|
51
|
+
if (format === "stylish") {
|
|
52
|
+
const lines = [`Inspected ${report.summary.stylesheets} stylesheets (${report.project.routeStylesheets.length} route), ${report.summary.references} token references.`];
|
|
53
|
+
for (const item of report.diagnostics) lines.push(`${item.severity.toUpperCase()} ${item.code}${item.source ? ` ${item.source}` : ""}: ${item.message}`);
|
|
54
|
+
lines.push("Coverage: supplied project CSS; browser behavior not checked.");
|
|
55
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
56
|
+
} else process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
57
|
+
if (exitCode !== "never" && (report.summary.errors || report.summary.warnings > parseMaxWarnings(options.maxWarnings))) process.exitCode = 1;
|
|
58
|
+
return report;
|
|
59
|
+
}
|
|
60
|
+
const inspectionOptions = {
|
|
61
|
+
manifest: defaultManifest,
|
|
28
62
|
cwd,
|
|
29
63
|
patterns: specifiedSourcePaths.length ? specifiedSourcePaths : void 0,
|
|
30
64
|
classes: options.classes,
|
|
31
65
|
includeCss: Boolean(options.includeCss)
|
|
32
|
-
}
|
|
66
|
+
};
|
|
67
|
+
if (options.snapshot || options.compare) {
|
|
68
|
+
if (options.snapshot && options.compare) throw new Error("Choose either --snapshot or --compare.");
|
|
69
|
+
const { snapshot } = await captureMasterCSSProject(inspectionOptions);
|
|
70
|
+
const result = options.compare ? await compareProjectSnapshots({
|
|
71
|
+
before: JSON.parse(await readFile(path.resolve(cwd, options.compare), "utf8")),
|
|
72
|
+
after: snapshot
|
|
73
|
+
}) : snapshot;
|
|
74
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
const report = await createMasterCSSInspectionReport(inspectionOptions);
|
|
33
78
|
outputReport(report, format);
|
|
34
79
|
if (exitCode !== "never" && (report.summary.errors || report.summary.warnings > parseMaxWarnings(options.maxWarnings))) process.exitCode = 1;
|
|
35
80
|
return report;
|
|
36
81
|
}
|
|
37
|
-
//#endregion
|
|
38
82
|
export { runInspect as default };
|
package/dist/lint.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
3
|
+
import { loadProjectManifest } from "@master/css-compiler/project";
|
|
4
|
+
import { createToolingSessionSync } from "@master/css-tooling/node";
|
|
5
|
+
import defaultManifestJSON from "@master/css-preset/default-manifest.json" with { type: "json" };
|
|
6
6
|
import fg from "fast-glob";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { fixMasterCSSContent, lintMasterCSSContent, resolveMasterCSSLintRules, summarizeMasterCSSLintFiles } from "@master/css-tooling/lint";
|
|
8
|
+
import { SourcePolicy } from "@master/css-tooling/scanner/node";
|
|
9
|
+
// src/lint.ts
|
|
10
|
+
const REPORT_VERSION = 5;
|
|
11
|
+
const defaultManifest = defaultManifestJSON;
|
|
12
|
+
const DEFAULT_SOURCE_PATTERNS = ["**/*.{html,htm,js,mjs,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php,css,scss,less}"];
|
|
10
13
|
const DEFAULT_IGNORE_PATTERNS = ["**/node_modules/**", "node_modules"];
|
|
11
14
|
function normalizeSourcePatterns(specifiedSourcePaths) {
|
|
12
15
|
return specifiedSourcePaths?.length ? specifiedSourcePaths : DEFAULT_SOURCE_PATTERNS;
|
|
@@ -59,17 +62,27 @@ function createManifestFileResult(cwd, diagnostics) {
|
|
|
59
62
|
diagnostics
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
|
-
function createReport(cwd, manifest, files) {
|
|
65
|
+
function createReport(cwd, manifest, files, checkedFiles) {
|
|
66
|
+
const { files: _files, ...summary } = summarizeMasterCSSLintFiles(files);
|
|
63
67
|
return {
|
|
64
68
|
version: REPORT_VERSION,
|
|
65
69
|
cwd,
|
|
66
70
|
manifest,
|
|
67
71
|
files,
|
|
68
|
-
|
|
72
|
+
coverage: {
|
|
73
|
+
status: manifest.status === "error" ? "not-run" : checkedFiles.length ? "complete" : "no-inputs",
|
|
74
|
+
checkedFiles,
|
|
75
|
+
description: manifest.status === "error" ? "Source lint did not run because manifest loading failed. Manifest diagnostics are not checked source files." : checkedFiles.length ? "Source lint ran on every path in checkedFiles with the enabled rules. The files array lists only diagnostic results; an empty files array means those inputs produced no diagnostics. manifest.entries lists stylesheet entries used to load the manifest, not linted source inputs. Browser behavior is not checked." : "No source inputs matched, so source lint did not run. Zero diagnostics does not verify any source file or browser behavior."
|
|
76
|
+
},
|
|
77
|
+
summary: {
|
|
78
|
+
...summary,
|
|
79
|
+
checkedFiles: checkedFiles.length,
|
|
80
|
+
filesWithDiagnostics: files.filter((file) => file.sourceKind !== "manifest" && file.diagnostics.length).length
|
|
81
|
+
}
|
|
69
82
|
};
|
|
70
83
|
}
|
|
71
84
|
function formatStylish(report) {
|
|
72
|
-
const lines = [];
|
|
85
|
+
const lines = [report.coverage.status === "not-run" ? "Source lint checks did not run because the manifest could not be loaded." : report.coverage.status === "no-inputs" ? "No source inputs matched; no source lint checks ran." : `Checked ${report.summary.checkedFiles} source inputs; ${report.summary.filesWithDiagnostics} with diagnostics.`];
|
|
73
86
|
for (const result of report.files) {
|
|
74
87
|
if (!result.diagnostics.length) continue;
|
|
75
88
|
lines.push(result.filePath);
|
|
@@ -80,8 +93,8 @@ function formatStylish(report) {
|
|
|
80
93
|
function outputReport(report, format) {
|
|
81
94
|
if (format === "stylish") {
|
|
82
95
|
const output = formatStylish(report);
|
|
83
|
-
if (output) process.
|
|
84
|
-
} else
|
|
96
|
+
if (output) process.stderr.write(output);
|
|
97
|
+
} else process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
85
98
|
}
|
|
86
99
|
function resolveSourceInputs(cwd, specifiedSourcePaths, options) {
|
|
87
100
|
if (options.stdin) return [{
|
|
@@ -89,7 +102,10 @@ function resolveSourceInputs(cwd, specifiedSourcePaths, options) {
|
|
|
89
102
|
content: fs.readFileSync(0, "utf8"),
|
|
90
103
|
stdin: true
|
|
91
104
|
}];
|
|
92
|
-
|
|
105
|
+
const sourcePatterns = normalizeSourcePatterns(specifiedSourcePaths);
|
|
106
|
+
const explicit = specifiedSourcePaths.length > 0;
|
|
107
|
+
const policy = new SourcePolicy(cwd);
|
|
108
|
+
return resolveSourcePaths(cwd, sourcePatterns, explicit ? [] : DEFAULT_IGNORE_PATTERNS).filter((source) => policy.accepts(source, explicit)).map((source) => {
|
|
93
109
|
const filePath = path.resolve(cwd, source);
|
|
94
110
|
return {
|
|
95
111
|
filePath,
|
|
@@ -97,23 +113,22 @@ function resolveSourceInputs(cwd, specifiedSourcePaths, options) {
|
|
|
97
113
|
};
|
|
98
114
|
});
|
|
99
115
|
}
|
|
100
|
-
function lintInputs(inputs,
|
|
116
|
+
function lintInputs(inputs, rules, lintSession) {
|
|
101
117
|
return inputs.map((input) => lintMasterCSSContent({
|
|
102
118
|
content: input.content,
|
|
103
119
|
filePath: input.filePath,
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
rules,
|
|
121
|
+
lintSession
|
|
106
122
|
})).filter((result) => result.diagnostics.length);
|
|
107
123
|
}
|
|
108
|
-
function applyFileFixes(inputs,
|
|
124
|
+
function applyFileFixes(inputs, rules, lintSession) {
|
|
109
125
|
for (const input of inputs) {
|
|
110
126
|
if (input.stdin) continue;
|
|
111
127
|
const fixed = fixMasterCSSContent({
|
|
112
128
|
content: input.content,
|
|
113
129
|
filePath: input.filePath,
|
|
114
|
-
css,
|
|
115
130
|
rules,
|
|
116
|
-
|
|
131
|
+
lintSession
|
|
117
132
|
});
|
|
118
133
|
if (fixed !== input.content) {
|
|
119
134
|
fs.writeFileSync(input.filePath, fixed);
|
|
@@ -127,34 +142,41 @@ async function runLint(specifiedSourcePaths = [], options = {}) {
|
|
|
127
142
|
const exitCode = options.exitCode || "diagnostics";
|
|
128
143
|
const rules = resolveMasterCSSLintRules(options.rules);
|
|
129
144
|
const inputs = resolveSourceInputs(cwd, specifiedSourcePaths, options);
|
|
130
|
-
let
|
|
131
|
-
let files;
|
|
145
|
+
let manifestResult;
|
|
132
146
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
entries: manifestResult.entries,
|
|
138
|
-
diagnostics: []
|
|
139
|
-
};
|
|
140
|
-
files = lintInputs(inputs, css, rules);
|
|
141
|
-
if (options.fix && !options.fixDryRun) {
|
|
142
|
-
applyFileFixes(inputs, css, rules, Boolean(options.fixDirectives));
|
|
143
|
-
files = lintInputs(inputs, css, rules);
|
|
144
|
-
}
|
|
147
|
+
manifestResult = await loadProjectManifest({
|
|
148
|
+
root: cwd,
|
|
149
|
+
baseManifest: defaultManifest
|
|
150
|
+
});
|
|
145
151
|
} catch (error) {
|
|
146
152
|
const diagnostic = createManifestDiagnostic(cwd, error);
|
|
147
|
-
|
|
153
|
+
const report = createReport(cwd, {
|
|
148
154
|
status: "error",
|
|
149
155
|
entries: [],
|
|
150
156
|
diagnostics: [diagnostic]
|
|
151
|
-
};
|
|
152
|
-
|
|
157
|
+
}, [createManifestFileResult(cwd, [diagnostic])], []);
|
|
158
|
+
outputReport(report, format);
|
|
159
|
+
if (exitCode !== "never") process.exitCode = 1;
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
162
|
+
const lintSession = createToolingSessionSync({ manifest: manifestResult.manifest });
|
|
163
|
+
let files;
|
|
164
|
+
try {
|
|
165
|
+
files = lintInputs(inputs, rules, lintSession);
|
|
166
|
+
if (options.fix && !options.fixDryRun) {
|
|
167
|
+
applyFileFixes(inputs, rules, lintSession);
|
|
168
|
+
files = lintInputs(inputs, rules, lintSession);
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
lintSession.dispose();
|
|
153
172
|
}
|
|
154
|
-
const report = createReport(cwd,
|
|
173
|
+
const report = createReport(cwd, {
|
|
174
|
+
status: "loaded",
|
|
175
|
+
entries: [...manifestResult.entries],
|
|
176
|
+
diagnostics: []
|
|
177
|
+
}, files, inputs.map((input) => input.filePath));
|
|
155
178
|
outputReport(report, format);
|
|
156
179
|
if (exitCode !== "never" && (report.summary.errors || report.summary.warnings > parseMaxWarnings(options.maxWarnings))) process.exitCode = 1;
|
|
157
180
|
return report;
|
|
158
181
|
}
|
|
159
|
-
//#endregion
|
|
160
182
|
export { runLint as default };
|