@master/css-cli 2.0.0-rc.87 → 2.0.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 +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/README.md
CHANGED
|
@@ -1,155 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
<div align="center">
|
|
1
|
+
# @master/css-cli
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
<a href="https://css.master.co">
|
|
6
|
-
<picture>
|
|
7
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/33840671/201701649-3bb7d698-abec-4d5f-ac30-ccc4d7bafcd4.svg">
|
|
8
|
-
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/33840671/201703010-77bf2373-9899-40cc-98f5-30cf9b546941.svg">
|
|
9
|
-
<img alt="Master CSS" src="https://user-images.githubusercontent.com/33840671/201703010-77bf2373-9899-40cc-98f5-30cf9b546941.svg" width="100%">
|
|
10
|
-
</picture>
|
|
11
|
-
</a>
|
|
12
|
-
</p>
|
|
13
|
-
<p align="center">Master CSS CLI</p>
|
|
14
|
-
|
|
15
|
-
<p align="center">
|
|
16
|
-
<a aria-label="GitHub release (latest by date including pre-releases)" href="https://github.com/master-co/css/releases">
|
|
17
|
-
<picture>
|
|
18
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=212022&label=&style=for-the-badge&logo=github&logoColor=fff">
|
|
19
|
-
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=f6f7f8&label=&style=for-the-badge&logo=github&logoColor=%23000">
|
|
20
|
-
<img alt="NPM Version" src="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=f6f7f8&label=&style=for-the-badge&logo=github">
|
|
21
|
-
</picture>
|
|
22
|
-
</a>
|
|
23
|
-
<a aria-label="NPM Package" href="https://www.npmjs.com/package/@master/css-cli">
|
|
24
|
-
<picture>
|
|
25
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/dm/@master/css-cli?color=212022&label=%20&logo=npm&style=for-the-badge">
|
|
26
|
-
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/npm/dm/@master/css-cli?color=f6f7f8&label=%20&logo=npm&style=for-the-badge">
|
|
27
|
-
<img alt="NPM package ( download / month )" src="https://img.shields.io/npm/dm/@master/css-cli?color=f6f7f8&label=%20&logo=npm&style=for-the-badge">
|
|
28
|
-
</picture>
|
|
29
|
-
</a>
|
|
30
|
-
<a aria-label="Discord Community" href="https://discord.gg/sZNKpAAAw6">
|
|
31
|
-
<picture>
|
|
32
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/discord/917780624314613760?color=212022&label=%20&logo=discord&style=for-the-badge">
|
|
33
|
-
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/discord/917780624314613760?color=f6f7f8&label=%20&logo=discord&style=for-the-badge">
|
|
34
|
-
<img alt="Discord online" src="https://img.shields.io/discord/917780624314613760?color=f6f7f8&label=%20&logo=discord&style=for-the-badge">
|
|
35
|
-
</picture>
|
|
36
|
-
</a>
|
|
37
|
-
<a aria-label="Follow @mastercorg" href="https://twitter.com/mastercorg">
|
|
38
|
-
<picture>
|
|
39
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/static/v1?label=%20&message=twitter&color=212022&logo=twitter&style=for-the-badge">
|
|
40
|
-
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/static/v1?label=%20&message=twitter&color=f6f7f8&logo=twitter&style=for-the-badge">
|
|
41
|
-
<img alt="Follow @mastercorg" src="https://img.shields.io/static/v1?label=%20&message=twitter&color=f6f7f8&logo=twitter&style=for-the-badge">
|
|
42
|
-
</picture>
|
|
43
|
-
</a>
|
|
44
|
-
<a aria-label="Github Actions" href="https://github.com/master-co/css/actions/workflows/ci-release.yml">
|
|
45
|
-
<picture>
|
|
46
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=212022&logo=githubactions&style=for-the-badge">
|
|
47
|
-
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=f6f7f8&logo=githubactions&style=for-the-badge&logoColor=%23000">
|
|
48
|
-
<img alt="Github release actions" src="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=f6f7f8&logo=githubactions&style=for-the-badge&logoColor=%23000">
|
|
49
|
-
</picture>
|
|
50
|
-
</a>
|
|
51
|
-
</p>
|
|
52
|
-
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
## Installation
|
|
3
|
+
Generate and inspect Master CSS with the project's installed compiler.
|
|
56
4
|
|
|
57
5
|
```bash
|
|
58
|
-
npm install -
|
|
6
|
+
npm install --save-dev @master/css-cli
|
|
7
|
+
npx @master/css-cli generate
|
|
59
8
|
```
|
|
60
9
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Commands
|
|
10
|
+
Use the explicit `generate`, `lint`, `inspect`, or `suggest` command. The app CSS entry starts with `@import '@master/css';`, loading the complete preset. Static generation emits CSS without a Master browser runtime. Native CSS is preserved unless pruning is explicitly enabled.
|
|
64
11
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Scan source files, detect Master CSS classes, generate CSS rules, and write the generated CSS file.
|
|
12
|
+
`generate --output` flattens the import graph into the output file in cascade order and writes referenced images and fonts beside it. External imports at the top of an imported stylesheet move up with it when only other external imports precede it. An imported stylesheet that keeps a namespace or an import under a media, supports or layer qualifier, or whose external imports would follow inlined rules, stays a sibling CSS file, as do the imports before it; deploy the output directory as a whole.
|
|
68
13
|
|
|
69
14
|
```bash
|
|
70
|
-
npx @master/css-cli
|
|
15
|
+
npx @master/css-cli generate 'src/**/*.{html,tsx}' --output public/master.css
|
|
16
|
+
npx @master/css-cli generate --watch
|
|
17
|
+
npx @master/css-cli lint --format json
|
|
18
|
+
npx @master/css-cli inspect --include-css --format json
|
|
19
|
+
npx @master/css-cli suggest 'display: flex; align-items: center;'
|
|
71
20
|
```
|
|
72
21
|
|
|
73
|
-
|
|
22
|
+
Without source paths, `lint` follows the scanner source policy and skips generated output and `.gitignore` matches. Pass a path or glob to lint an ignored artifact explicitly.
|
|
74
23
|
|
|
75
|
-
|
|
76
|
-
npx @master/css-cli
|
|
77
|
-
```
|
|
24
|
+
`suggest` proposes project-aware classes for an isolated CSS declaration block without writing files. It checks declaration output; review the surrounding cascade and browser rendering before applying a suggestion.
|
|
78
25
|
|
|
79
|
-
|
|
26
|
+
Inspection reports distinguish class matching, CSS syntax, CSS values and browser support. Generated CSS is not visual verification. `--exit-code never` suppresses the diagnostic failure exit code; `--max-warnings` sets the warning threshold.
|
|
80
27
|
|
|
81
|
-
|
|
82
|
-
| --- | --- | --- | --- |
|
|
83
|
-
| `-o, --output <path>` | `string` | `master.css` | Generated CSS output path. |
|
|
84
|
-
| `-v, --verbose <level>` | `number` | `1` | Logging level. Use `0` to silence terminal output. |
|
|
85
|
-
| `-w, --watch` | `boolean` | `false` | Watch files and rebuild continuously. |
|
|
28
|
+
## Compare project changes
|
|
86
29
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Run machine-readable Master CSS diagnostics for agents, MCP servers, editor tools, and native CI checks. JavaScript and VS Code projects should still use ESLint as the main human lint workflow; this command is a native preflight surface that shares the same class policy engine.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
npx @master/css-cli lint index.html main.js
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Use JSON output for tools:
|
|
30
|
+
Capture the known source classes, independently resolved manifest, native stylesheets and output assets before changing a token, mixin or stylesheet:
|
|
96
31
|
|
|
97
32
|
```bash
|
|
98
|
-
|
|
33
|
+
npx @master/css-cli inspect --snapshot > before.json
|
|
34
|
+
# Edit the project, then compare against the saved snapshot.
|
|
35
|
+
npx @master/css-cli inspect --compare before.json > impact.json
|
|
99
36
|
```
|
|
100
37
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| Option | Type | Default | Description |
|
|
104
|
-
| --- | --- | --- | --- |
|
|
105
|
-
| `--stdin` | `boolean` | `false` | Read one source buffer from stdin. |
|
|
106
|
-
| `--stdin-filepath <path>` | `string` | `stdin.html` | File path used to infer the stdin language. |
|
|
107
|
-
| `--fix` | `boolean` | `false` | Apply safe class-list fixes to files. |
|
|
108
|
-
| `--fix-dry-run` | `boolean` | `false` | Return fix proposals without writing files. |
|
|
109
|
-
| `--fix-directives` | `boolean` | `false` | Allow structural directive fixes when `--fix` writes files. |
|
|
110
|
-
| `--format <format>` | `json` or `stylish` | `json` | Print machine-readable diagnostics or human-readable output. |
|
|
111
|
-
| `--rules <rules>` | comma list | recommended | Select lint rules, `recommended`, or `all`. |
|
|
112
|
-
| `--exit-code <mode>` | `diagnostics` or `never` | `diagnostics` | Control whether diagnostics set a non-zero exit code. |
|
|
113
|
-
| `--max-warnings <number>` | `number` | none | Exit with a non-zero status when warnings exceed this count. |
|
|
114
|
-
|
|
115
|
-
The JSON report uses `version: 1`. Tooling can rely on the top-level `version`, `cwd`, `manifest`, `files`, and `summary` fields. Diagnostic `data` objects may gain additional fields as Master CSS reports become more precise.
|
|
116
|
-
|
|
117
|
-
### `npx @master/css-cli inspect [source paths]`
|
|
118
|
-
|
|
119
|
-
Inspect scanner state, managed stylesheet entries, generated CSS size, and explicit missing CSS checks. This command is for MCP servers, AI agents, build diagnostics, and support tooling that need machine-readable extraction state, not class policy linting.
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
npx @master/css-cli inspect --classes "btn card" --format json --exit-code never
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Use `--classes` to ask whether specific class names are present in the generated CSS path. The report explains each checked class as `generated`, `native-css`, `safelist`, `invalid`, `blocklisted`, or `not-detected`.
|
|
126
|
-
|
|
127
|
-
The JSON report includes:
|
|
128
|
-
|
|
129
|
-
| Field | Description |
|
|
130
|
-
| --- | --- |
|
|
131
|
-
| `inputs` | Source patterns, resolved files, and explicit classes checked by this run. |
|
|
132
|
-
| `scanner` | Global scanner class sets, counts, safelist/blocklist counts, and reset dependencies. |
|
|
133
|
-
| `stylesheets` | Managed CSS entries, dependency metadata, warnings, and entry loading errors. |
|
|
134
|
-
| `css` | Generated CSS byte size, emitted global counts, and optional CSS text. |
|
|
135
|
-
| `missingCSS` | Present and missing results for `--classes`. |
|
|
136
|
-
| `files` | Per-file scanner discoveries for latent, valid, invalid, and native CSS classes. |
|
|
137
|
-
| `diagnostics` | Machine-readable `scanner`, `stylesheet`, and `missing-css` diagnostics. |
|
|
138
|
-
| `summary` | File, stylesheet, diagnostic, error, warning, missing CSS, and invalid class totals. |
|
|
139
|
-
|
|
140
|
-
The JSON report uses `version: 1`. Tooling can rely on the top-level `version`, `cwd`, `inputs`, `scanner`, `stylesheets`, `css`, `missingCSS`, `files`, `diagnostics`, and `summary` fields. Nested diagnostic `data` and stylesheet details may evolve unless documented for a specific diagnostic code.
|
|
141
|
-
|
|
142
|
-
Options:
|
|
143
|
-
|
|
144
|
-
| Option | Type | Default | Description |
|
|
145
|
-
| --- | --- | --- | --- |
|
|
146
|
-
| `--classes <classes>` | whitespace list | none | Class names to verify against scanner state and generated CSS. |
|
|
147
|
-
| `--include-css` | `boolean` | `false` | Include generated CSS text in the JSON report. |
|
|
148
|
-
| `--format <format>` | `json` or `stylish` | `json` | Print machine-readable diagnostics or human-readable output. |
|
|
149
|
-
| `--exit-code <mode>` | `diagnostics` or `never` | `diagnostics` | Control whether diagnostics set a non-zero exit code. |
|
|
150
|
-
| `--max-warnings <number>` | `number` | none | Exit with a non-zero status when warnings exceed this count. |
|
|
38
|
+
Both options produce JSON on stdout and are mutually exclusive. Snapshots and comparison reports have independent `version: 1` contracts. `--compare` resolves its path relative to the current working directory. Invalid snapshots or failed project resolution, stylesheet compilation or extraction fail the command. Class validation errors remain comparable, so deleting a token can reveal consumers that lose their generated rules. Differences alone do not fail; run ordinary `inspect` for diagnostic exit codes.
|
|
151
39
|
|
|
152
|
-
|
|
40
|
+
The impact report includes changed definitions, known consumers and files, before/after rules, transitive dependencies and asset/order changes. Read `coverage` for excluded and unresolved sources. Native references are conservative at stylesheet granularity; the report does not evaluate the DOM cascade, remote stylesheets or binary resource contents. Capture uses local file URLs for asset identities.
|
|
153
41
|
|
|
154
|
-
|
|
155
|
-
- [Scanner package](../scanner)
|
|
42
|
+
See [CLI installation](https://css.master.co/guide/installation#cli) and [AI review workflow](https://css.master.co/guide/ai-coding) for project setup and review.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { publishStylesheet, replaceStylesheetFile, resolveEntryTarget } from "./publication.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
// src/asset-ownership.ts
|
|
6
|
+
const ASSET_RETENTION_MS = 864e5;
|
|
7
|
+
const suffix = ".master-css.json";
|
|
8
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
const stylesheetStatePath = (entry) => path.join(path.dirname(entry), `.${path.basename(entry)}${suffix}`);
|
|
10
|
+
const name = (value) => typeof value === "string" && value !== "." && value !== ".." && value.length > 0 && !/[\\/\0]/.test(value);
|
|
11
|
+
const assetName = (value) => name(value) && /^master-[a-f0-9]+-/.test(value);
|
|
12
|
+
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
13
|
+
const timestamp = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
14
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
const identityKeys = [
|
|
16
|
+
"dev",
|
|
17
|
+
"ino",
|
|
18
|
+
"birthtimeNs",
|
|
19
|
+
"mtimeNs",
|
|
20
|
+
"mode"
|
|
21
|
+
];
|
|
22
|
+
const temporaryName = (value) => typeof value === "string" && /^\.master-css-[a-f0-9-]+\.tmp$/.test(value);
|
|
23
|
+
const validIdentity = (value) => object(value) && identityKeys.every((key) => typeof value[key] === "string" && /^\d+$/.test(value[key]));
|
|
24
|
+
const generation = (value) => object(value) && hash(value.hash) && Array.isArray(value.assets) && value.assets.every(assetName);
|
|
25
|
+
function identity(file) {
|
|
26
|
+
const stat = fs.lstatSync(file, {
|
|
27
|
+
bigint: true,
|
|
28
|
+
throwIfNoEntry: false
|
|
29
|
+
});
|
|
30
|
+
if (!stat?.isFile()) return void 0;
|
|
31
|
+
return {
|
|
32
|
+
dev: String(stat.dev),
|
|
33
|
+
ino: String(stat.ino),
|
|
34
|
+
birthtimeNs: String(stat.birthtimeNs),
|
|
35
|
+
mtimeNs: String(stat.mtimeNs),
|
|
36
|
+
mode: String(stat.mode & 511n)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function sameIdentity(left, right) {
|
|
40
|
+
return left && identityKeys.every((key) => left[key] === right[key]);
|
|
41
|
+
}
|
|
42
|
+
function readState(file, entry) {
|
|
43
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
44
|
+
if (!stat) return void 0;
|
|
45
|
+
if (!stat.isFile()) throw new Error(`Invalid stylesheet ownership file: ${file}`);
|
|
46
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
+
if (!object(value) || value.version !== 1 || !name(value.entry) || entry !== void 0 && value.entry !== entry || value.current !== void 0 && !generation(value.current) || !Array.isArray(value.retained) || !value.retained.every((item) => generation(item) && timestamp(item.retiredAt)) || !object(value.owned) || !Object.entries(value.owned).every(([file, item]) => assetName(file) && validIdentity(item) && object(item) && hash(item.hash) && timestamp(item.createdAt)) || value.pending !== void 0 && (!object(value.pending) || !generation(value.pending.generation) || !validIdentity(value.pending.entryIdentity) || !temporaryName(value.pending.entryTemporary) || !object(value.pending.temporaries) || !Object.entries(value.pending.temporaries).every(([file, item]) => temporaryName(file) && validIdentity(item)))) throw new Error(`Invalid stylesheet ownership file: ${file}`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function promote(state, next, now) {
|
|
51
|
+
if (state.current && (state.current.hash !== next.hash || JSON.stringify(state.current.assets) !== JSON.stringify(next.assets))) state.retained.unshift({
|
|
52
|
+
...state.current,
|
|
53
|
+
retiredAt: now
|
|
54
|
+
});
|
|
55
|
+
state.current = next;
|
|
56
|
+
delete state.pending;
|
|
57
|
+
}
|
|
58
|
+
function referenced(state) {
|
|
59
|
+
return [
|
|
60
|
+
state.current,
|
|
61
|
+
...state.retained,
|
|
62
|
+
state.pending?.generation
|
|
63
|
+
].flatMap((item) => item?.assets ?? []);
|
|
64
|
+
}
|
|
65
|
+
/** Called inside the cooperating CLI publication gate. */
|
|
66
|
+
function publishOwnedStylesheet(entry, css, assets, outputFiles, now = Date.now()) {
|
|
67
|
+
entry = path.resolve(entry);
|
|
68
|
+
const directory = path.dirname(entry);
|
|
69
|
+
const stateFile = stylesheetStatePath(entry);
|
|
70
|
+
outputFiles.add(stateFile);
|
|
71
|
+
const state = readState(stateFile, path.basename(entry)) ?? {
|
|
72
|
+
version: 1,
|
|
73
|
+
entry: path.basename(entry),
|
|
74
|
+
retained: [],
|
|
75
|
+
owned: {}
|
|
76
|
+
};
|
|
77
|
+
const save = () => {
|
|
78
|
+
const stat = fs.lstatSync(stateFile, { throwIfNoEntry: false });
|
|
79
|
+
if (stat) {
|
|
80
|
+
if (!stat.isFile()) throw new Error(`Invalid stylesheet ownership file: ${stateFile}`);
|
|
81
|
+
fs.accessSync(stateFile, fs.constants.W_OK);
|
|
82
|
+
}
|
|
83
|
+
replaceStylesheetFile(stateFile, Buffer.from(JSON.stringify(state, null, 2) + "\n"), outputFiles, 384);
|
|
84
|
+
};
|
|
85
|
+
const target = resolveEntryTarget(entry);
|
|
86
|
+
if (state.pending) {
|
|
87
|
+
const entryTemporary = path.join(path.dirname(target), state.pending.entryTemporary);
|
|
88
|
+
if (sameIdentity(identity(entryTemporary), state.pending.entryIdentity)) fs.unlinkSync(entryTemporary);
|
|
89
|
+
for (const [file, expected] of Object.entries(state.pending.temporaries)) {
|
|
90
|
+
const temporary = path.join(directory, file);
|
|
91
|
+
if (sameIdentity(identity(temporary), expected)) fs.unlinkSync(temporary);
|
|
92
|
+
}
|
|
93
|
+
if (sameIdentity(identity(target), state.pending.entryIdentity) && digest(fs.readFileSync(target)) === state.pending.generation.hash) promote(state, state.pending.generation, now);
|
|
94
|
+
else delete state.pending;
|
|
95
|
+
save();
|
|
96
|
+
}
|
|
97
|
+
const names = [...assets.keys()].map((file) => {
|
|
98
|
+
if (path.dirname(file) !== directory || !assetName(path.basename(file))) throw new Error(`Invalid stylesheet asset path: ${file}`);
|
|
99
|
+
outputFiles.add(file);
|
|
100
|
+
return path.basename(file);
|
|
101
|
+
}).sort();
|
|
102
|
+
const next = {
|
|
103
|
+
hash: digest(css),
|
|
104
|
+
assets: names
|
|
105
|
+
};
|
|
106
|
+
const unchanged = state.current?.hash === next.hash && JSON.stringify(state.current.assets) === JSON.stringify(names) && fs.existsSync(target) && digest(fs.readFileSync(target)) === next.hash && [...assets].every(([file, bytes]) => identity(file) && fs.readFileSync(file).equals(bytes));
|
|
107
|
+
if (!unchanged) publishStylesheet(entry, css, assets, outputFiles, {
|
|
108
|
+
beforePublish(created, temporary) {
|
|
109
|
+
for (const [file, prepared] of created) state.owned[path.basename(file)] = {
|
|
110
|
+
...identity(prepared),
|
|
111
|
+
hash: digest(assets.get(file)),
|
|
112
|
+
createdAt: now
|
|
113
|
+
};
|
|
114
|
+
state.pending = {
|
|
115
|
+
generation: next,
|
|
116
|
+
entryIdentity: identity(temporary),
|
|
117
|
+
entryTemporary: path.basename(temporary),
|
|
118
|
+
temporaries: Object.fromEntries([...created.values()].map((file) => [path.basename(file), identity(file)]))
|
|
119
|
+
};
|
|
120
|
+
save();
|
|
121
|
+
},
|
|
122
|
+
afterPublish() {
|
|
123
|
+
promote(state, next, now);
|
|
124
|
+
save();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const beforeCleanup = JSON.stringify(state);
|
|
128
|
+
state.retained = state.retained.filter((item, index) => index === 0 || now - item.retiredAt < 864e5);
|
|
129
|
+
const protectedFiles = new Set(referenced(state));
|
|
130
|
+
try {
|
|
131
|
+
for (const file of fs.readdirSync(directory)) {
|
|
132
|
+
if (!file.startsWith(".") || !file.endsWith(suffix) || path.join(directory, file) === stateFile) continue;
|
|
133
|
+
const other = readState(path.join(directory, file));
|
|
134
|
+
if (other) for (const asset of referenced(other)) protectedFiles.add(asset);
|
|
135
|
+
}
|
|
136
|
+
for (const [file, owned] of Object.entries(state.owned)) {
|
|
137
|
+
const target = path.join(directory, file);
|
|
138
|
+
if (!sameIdentity(identity(target), owned) || digest(fs.readFileSync(target)) !== owned.hash) delete state.owned[file];
|
|
139
|
+
else if (!protectedFiles.has(file) && now - owned.createdAt >= 864e5) {
|
|
140
|
+
fs.unlinkSync(target);
|
|
141
|
+
delete state.owned[file];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (JSON.stringify(state) !== beforeCleanup) save();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
process.stderr.write(`Cannot clean stylesheet assets: ${error}\n`);
|
|
147
|
+
}
|
|
148
|
+
return !unchanged;
|
|
149
|
+
}
|
|
150
|
+
export { ASSET_RETENTION_MS, publishOwnedStylesheet, stylesheetStatePath };
|
package/dist/bin/index.js
CHANGED
package/dist/constants.js
CHANGED
package/dist/core.js
CHANGED
|
@@ -2,38 +2,35 @@ import { DEFAULT_SCAN_OUTPUT } from "./constants.js";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
// src/core.ts
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
8
9
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
9
|
-
const removedCommands = /* @__PURE__ */ new Set([
|
|
10
|
-
"extract",
|
|
11
|
-
"render",
|
|
12
|
-
"scan"
|
|
13
|
-
]);
|
|
14
|
-
function errorRemovedCommand(program, command) {
|
|
15
|
-
program.error(`The "${command}" command was removed. Use "${pkg.name}" directly, for example "${pkg.name} -w".`);
|
|
16
|
-
}
|
|
17
10
|
async function runProgram(argv = process.argv) {
|
|
18
11
|
const program = new Command();
|
|
19
|
-
program.name(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
program.name("master-css").description(pkg.description).version(pkg.version || "0.0.0");
|
|
13
|
+
program.command("generate").description("Generate CSS from project source files.").argument("[source paths...]", "The glob pattern paths to scan sources").option("-w, --watch", "Watch file changes and generate CSS rules.").option("-o, --output <path>", "Specify your CSS file output path", DEFAULT_SCAN_OUTPUT).option("-v, --verbose <level>", "Verbose logging 0~N", "1").addOption(new Option("--binding <binding>", "Execution binding: auto, native, or wasm.").choices([
|
|
14
|
+
"auto",
|
|
15
|
+
"native",
|
|
16
|
+
"wasm"
|
|
17
|
+
]).default("auto")).option("--no-export", "Print only CSS results.").option("--strict", "Fail CSS syntax or value validation before publishing any generated assets.").option("--prune-native-css", "Prune unused native CSS only in project-owned source files.").action(async (sourcePaths, options) => {
|
|
18
|
+
const { default: runGenerate } = await import("./generate.js");
|
|
19
|
+
return runGenerate(sourcePaths, options);
|
|
24
20
|
});
|
|
25
|
-
program.command("lint").description("Run machine-readable Master CSS diagnostics for source files.").argument("[source paths...]", "The glob pattern paths to lint sources").option("--stdin", "Read source content from stdin.").option("--stdin-filepath <path>", "File path used to infer the stdin source language.").option("--fix", "Apply safe class-list fixes to files.").option("--fix-dry-run", "Return fix proposals without writing files.").option("--
|
|
21
|
+
program.command("lint").description("Run machine-readable Master CSS diagnostics for source files.").argument("[source paths...]", "The glob pattern paths to lint sources").option("--stdin", "Read source content from stdin.").option("--stdin-filepath <path>", "File path used to infer the stdin source language.").option("--fix", "Apply safe class-list fixes to files.").option("--fix-dry-run", "Return fix proposals without writing files.").option("--format <format>", "Diagnostic output format: json or stylish.", "json").option("--rules <rules>", "Comma-separated lint rules, \"recommended\", or \"all\".").option("--exit-code <mode>", "Exit code behavior: diagnostics or never.", "diagnostics").option("--max-warnings <number>", "Exit with a non-zero status if warnings exceed this count.").action(async (sourcePaths, options) => {
|
|
26
22
|
const { default: runLint } = await import("./lint.js");
|
|
27
23
|
await runLint(sourcePaths, options);
|
|
28
24
|
});
|
|
29
|
-
program.command("inspect").description("Inspect scanner state, stylesheet entries, generated CSS, and missing CSS.").argument("[source paths...]", "The glob pattern paths to inspect").option("--classes <classes>", "Whitespace-separated class names to verify in generated CSS.").option("--include-css", "Include generated CSS text in the JSON report.").option("--format <format>", "Diagnostic output format: json or stylish.", "json").option("--exit-code <mode>", "Exit code behavior: diagnostics or never.", "diagnostics").option("--max-warnings <number>", "Exit with a non-zero status if warnings exceed this count.").action(async (sourcePaths, options) => {
|
|
25
|
+
program.command("inspect").description("Inspect scanner state, stylesheet entries, generated CSS, and missing CSS.").argument("[source paths...]", "The glob pattern paths to inspect; with --design-system, route and component stylesheets to add").option("--classes <classes>", "Whitespace-separated class names to verify in generated CSS.").option("--include-css", "Include generated CSS text in the JSON report.").option("--design-system", "Inspect token references, definition metadata, policy and collisions in managed and route stylesheets.").option("--design-system-options <path>", "JSON file containing version: 1, optional metadata and rawValuePolicy.").option("--snapshot", "Capture a resolved project snapshot as JSON.").option("--compare <snapshot>", "Compare a saved JSON snapshot with the current project.").option("--format <format>", "Diagnostic output format: json or stylish.", "json").option("--exit-code <mode>", "Exit code behavior: diagnostics or never.", "diagnostics").option("--max-warnings <number>", "Exit with a non-zero status if warnings exceed this count.").action(async (sourcePaths, options) => {
|
|
30
26
|
const { default: runInspect } = await import("./inspect.js");
|
|
31
27
|
await runInspect(sourcePaths, options);
|
|
32
28
|
});
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
program.command("suggest").description("Suggest classes for a CSS declaration block without editing files.").argument("[declarations]", "A quoted CSS declaration block, without a selector or braces.").option("--stdin", "Read the declaration block from stdin instead of an argument.").option("--cwd <path>", "Project root used to load the active manifest.", ".").action(async (declarations, options) => {
|
|
30
|
+
const { default: runSuggest } = await import("./suggest.js");
|
|
31
|
+
await runSuggest(declarations, options);
|
|
32
|
+
});
|
|
35
33
|
await program.parseAsync(argv);
|
|
36
34
|
return program;
|
|
37
35
|
}
|
|
38
|
-
//#endregion
|
|
39
36
|
export { runProgram as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
// src/dependency-rebuild.ts
|
|
5
|
+
const identity = (stat) => [
|
|
6
|
+
stat.dev,
|
|
7
|
+
stat.ino,
|
|
8
|
+
stat.mode,
|
|
9
|
+
stat.size,
|
|
10
|
+
stat.mtimeNs,
|
|
11
|
+
stat.ctimeNs
|
|
12
|
+
].join(":");
|
|
13
|
+
function fileRevision(file) {
|
|
14
|
+
try {
|
|
15
|
+
const stat = statSync(file, {
|
|
16
|
+
bigint: true,
|
|
17
|
+
throwIfNoEntry: false
|
|
18
|
+
});
|
|
19
|
+
if (!stat?.isFile()) return void 0;
|
|
20
|
+
const hash = createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
21
|
+
const after = statSync(file, {
|
|
22
|
+
bigint: true,
|
|
23
|
+
throwIfNoEntry: false
|
|
24
|
+
});
|
|
25
|
+
return after && identity(stat) === identity(after) ? `${identity(stat)}:${hash}` : void 0;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return void 0;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Called inside the watch queue, so each comparison follows the previous rebuild. */
|
|
32
|
+
function createDependencyRebuild(rebuild, ready) {
|
|
33
|
+
const completed = /* @__PURE__ */ new Map();
|
|
34
|
+
return async (file) => {
|
|
35
|
+
const absolute = resolve(file);
|
|
36
|
+
const revision = fileRevision(absolute);
|
|
37
|
+
if (ready() && revision !== void 0 && completed.get(absolute) === revision) return;
|
|
38
|
+
completed.delete(absolute);
|
|
39
|
+
await rebuild();
|
|
40
|
+
if (revision !== void 0) completed.set(absolute, revision);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export { createDependencyRebuild };
|