@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.
@@ -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 { createMasterCSSInspectionReport } from "@master/css-diagnostics";
3
- //#region src/inspect.ts
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.stdout.write(formatStylish(report));
21
- else console.log(JSON.stringify(report, null, 2));
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
- const report = await createMasterCSSInspectionReport({
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 { createCSSWithNativeDeclarations } from "@master/css-validator";
4
- import { fixMasterCSSContent, lintMasterCSSContent, resolveMasterCSSLintRules, summarizeMasterCSSLintFiles } from "@master/css-lint";
5
- import { loadProjectManifest } from "@master/css-project/manifest";
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
- //#region src/lint.ts
8
- const REPORT_VERSION = 1;
9
- const DEFAULT_SOURCE_PATTERNS = ["**/*.{html,htm,js,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php,css,scss,less}"];
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
- summary: summarizeMasterCSSLintFiles(files)
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.stdout.write(output);
84
- } else console.log(JSON.stringify(report, null, 2));
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
- return resolveSourcePaths(cwd, normalizeSourcePatterns(specifiedSourcePaths), specifiedSourcePaths.length ? [] : DEFAULT_IGNORE_PATTERNS).map((source) => {
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, css, rules) {
116
+ function lintInputs(inputs, rules, lintSession) {
101
117
  return inputs.map((input) => lintMasterCSSContent({
102
118
  content: input.content,
103
119
  filePath: input.filePath,
104
- css,
105
- rules
120
+ rules,
121
+ lintSession
106
122
  })).filter((result) => result.diagnostics.length);
107
123
  }
108
- function applyFileFixes(inputs, css, rules, includeDirectiveFixes) {
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
- includeDirectiveFixes
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 manifest;
131
- let files;
145
+ let manifestResult;
132
146
  try {
133
- const manifestResult = await loadProjectManifest(cwd);
134
- const css = createCSSWithNativeDeclarations(manifestResult.manifest);
135
- manifest = {
136
- status: "loaded",
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
- manifest = {
153
+ const report = createReport(cwd, {
148
154
  status: "error",
149
155
  entries: [],
150
156
  diagnostics: [diagnostic]
151
- };
152
- files = [createManifestFileResult(cwd, [diagnostic])];
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, manifest, files);
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 };