@master/css-mcp 2.0.0-rc.76
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/bin/index.d.ts +1 -0
- package/dist/bin/index.js +60 -0
- package/dist/context.d.ts +60 -0
- package/dist/context.js +189 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/language.d.ts +25 -0
- package/dist/language.js +46 -0
- package/dist/lint.d.ts +56 -0
- package/dist/lint.js +132 -0
- package/dist/project.d.ts +32 -0
- package/dist/project.js +42 -0
- package/dist/result.d.ts +9 -0
- package/dist/result.js +22 -0
- package/dist/scan.d.ts +157 -0
- package/dist/scan.js +126 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +244 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Aoyue Design LLC.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Master CSS MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for Master CSS tooling. It lets AI clients inspect a Master CSS workspace, read the active project manifest, scan source files, inspect classes, render generated CSS, run lint diagnostics, request syntax suggestions, and apply reviewed fix previews.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @master/css-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You can also run it directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y @master/css-mcp@rc --root /absolute/path/to/project
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## MCP client config
|
|
18
|
+
|
|
19
|
+
Most MCP clients accept a command and arguments for stdio servers:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"master-css": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "@master/css-mcp@rc", "--root", "/absolute/path/to/project"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use an absolute `--root` path so the server resolves the intended workspace.
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | Use |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `mastercss_workspace_info` | Report workspace roots, resolved packages, and manifest status. |
|
|
39
|
+
| `mastercss_inspect_class` | Inspect one class and return generated rules and CSS text. |
|
|
40
|
+
| `mastercss_render_css` | Generate CSS from HTML or a class list. |
|
|
41
|
+
| `mastercss_scan_project` | Scan sources, check optional classes, and report scanner state, stylesheet entries, generated CSS metadata, and missing CSS diagnostics. |
|
|
42
|
+
| `mastercss_lint_project` | Run class-list diagnostics for workspace files without writing files. |
|
|
43
|
+
| `mastercss_lint_content` | Run class-list diagnostics on an in-memory source buffer without writing files. |
|
|
44
|
+
| `mastercss_suggest_syntax` | Return language-service completions and hover context. |
|
|
45
|
+
| `mastercss_preview_fixes` | Create a diff preview and confirmation token. |
|
|
46
|
+
| `mastercss_apply_preview` | Apply a preview after token, hash, and workspace checks. |
|
|
47
|
+
|
|
48
|
+
## Machine-readable results
|
|
49
|
+
|
|
50
|
+
Project scan and lint reports use `version: 1`. Tooling can rely on stable top-level fields such as `version`, `root`, `manifest`, `inputs`, `files`, `diagnostics`, and `summary` when present. Nested diagnostic `data` objects may gain additional fields over time.
|
|
51
|
+
|
|
52
|
+
## Safety
|
|
53
|
+
|
|
54
|
+
Read-only tools do not write files. File writes use a two-step preview/apply flow, validate workspace containment, and verify original file hashes before writing.
|
|
55
|
+
|
|
56
|
+
## Related docs
|
|
57
|
+
|
|
58
|
+
- [MCP Server guide](https://rc.css.master.co/guide/mcp-server)
|
|
59
|
+
- [AI Coding guide](https://rc.css.master.co/guide/ai-coding)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startStdioServer } from "../server.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/bin/index.ts
|
|
7
|
+
function readPackageVersion() {
|
|
8
|
+
const directory = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(resolve(directory, "../../package.json"), "utf8")).version || "0.0.0";
|
|
11
|
+
} catch {
|
|
12
|
+
return "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function printHelp() {
|
|
16
|
+
process.stdout.write(`Usage: master-css-mcp [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--root <dir> Workspace root. Defaults to the current working directory.
|
|
20
|
+
--preview-ttl <ms> Preview confirmation token TTL in milliseconds.
|
|
21
|
+
--version Print version.
|
|
22
|
+
--help Print help.
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const options = {};
|
|
27
|
+
for (let index = 0; index < argv.length; index++) {
|
|
28
|
+
const arg = argv[index];
|
|
29
|
+
switch (arg) {
|
|
30
|
+
case "--root":
|
|
31
|
+
options.root = argv[++index];
|
|
32
|
+
if (!options.root) throw new Error("--root requires a directory.");
|
|
33
|
+
break;
|
|
34
|
+
case "--preview-ttl": {
|
|
35
|
+
const value = Number(argv[++index]);
|
|
36
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error("--preview-ttl requires a positive number.");
|
|
37
|
+
options.previewTTL = value;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "--version":
|
|
41
|
+
process.stdout.write(`${readPackageVersion()}\n`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
break;
|
|
44
|
+
case "--help":
|
|
45
|
+
printHelp();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
break;
|
|
48
|
+
default: throw new Error(`Unknown option: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await startStdioServer(parseArgs(process.argv.slice(2)));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/context.d.ts
|
|
2
|
+
interface MasterCSSMCPContextOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
roots?: string[];
|
|
5
|
+
previewTTL?: number;
|
|
6
|
+
}
|
|
7
|
+
interface PreviewChange {
|
|
8
|
+
filePath: string;
|
|
9
|
+
beforeText?: string;
|
|
10
|
+
afterText: string;
|
|
11
|
+
}
|
|
12
|
+
interface StoredPreviewChange {
|
|
13
|
+
filePath: string;
|
|
14
|
+
beforeHash: string | null;
|
|
15
|
+
afterHash: string;
|
|
16
|
+
beforeExists: boolean;
|
|
17
|
+
beforeBytes: number;
|
|
18
|
+
afterBytes: number;
|
|
19
|
+
afterText: string;
|
|
20
|
+
diff: string;
|
|
21
|
+
}
|
|
22
|
+
interface StoredPreview {
|
|
23
|
+
confirmToken: string;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
changes: StoredPreviewChange[];
|
|
27
|
+
}
|
|
28
|
+
interface PreviewResult {
|
|
29
|
+
confirmToken?: string;
|
|
30
|
+
expiresAt?: number;
|
|
31
|
+
changes: StoredPreviewChange[];
|
|
32
|
+
summary: {
|
|
33
|
+
files: number;
|
|
34
|
+
bytesBefore: number;
|
|
35
|
+
bytesAfter: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
declare class MasterCSSMCPContext {
|
|
39
|
+
readonly root: string;
|
|
40
|
+
readonly roots: string[];
|
|
41
|
+
readonly previewTTL: number;
|
|
42
|
+
private readonly containmentRoots;
|
|
43
|
+
private previews;
|
|
44
|
+
constructor(options?: MasterCSSMCPContextOptions);
|
|
45
|
+
assertContained(filePath: string): string;
|
|
46
|
+
resolveVirtualPath(filePath: string): string;
|
|
47
|
+
resolveExistingFile(filePath: string): Promise<string>;
|
|
48
|
+
resolveWritableFile(filePath: string): Promise<string>;
|
|
49
|
+
validateGlobPatterns(patterns: string[]): void;
|
|
50
|
+
createPreview(changes: PreviewChange[], ttl?: number): Promise<PreviewResult>;
|
|
51
|
+
applyPreview(confirmToken: string): Promise<{
|
|
52
|
+
applied: boolean;
|
|
53
|
+
changes: {
|
|
54
|
+
filePath: string;
|
|
55
|
+
afterHash: string;
|
|
56
|
+
}[];
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { MasterCSSMCPContextOptions, PreviewChange, PreviewResult, StoredPreview, StoredPreviewChange, MasterCSSMCPContext as default };
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
//#region src/context.ts
|
|
6
|
+
const DEFAULT_PREVIEW_TTL = 300 * 1e3;
|
|
7
|
+
function sha256(text) {
|
|
8
|
+
return createHash("sha256").update(text).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function isContained(root, filePath) {
|
|
11
|
+
const path = relative(root, filePath);
|
|
12
|
+
return path === "" || Boolean(path && !path.startsWith("..") && !isAbsolute(path));
|
|
13
|
+
}
|
|
14
|
+
function pushUniquePath(paths, filePath) {
|
|
15
|
+
if (!paths.includes(filePath)) paths.push(filePath);
|
|
16
|
+
}
|
|
17
|
+
function collectPathAliases(filePath) {
|
|
18
|
+
const aliases = [];
|
|
19
|
+
const resolved = resolve(filePath);
|
|
20
|
+
pushUniquePath(aliases, resolved);
|
|
21
|
+
try {
|
|
22
|
+
pushUniquePath(aliases, realpathSync(resolved));
|
|
23
|
+
} catch {}
|
|
24
|
+
try {
|
|
25
|
+
pushUniquePath(aliases, realpathSync.native(resolved));
|
|
26
|
+
} catch {}
|
|
27
|
+
return aliases;
|
|
28
|
+
}
|
|
29
|
+
function assertFilePattern(pattern) {
|
|
30
|
+
if (isAbsolute(pattern)) throw new Error(`Absolute glob patterns are not allowed: ${pattern}`);
|
|
31
|
+
if (pattern.split(/[\\/]+/).includes("..")) throw new Error(`Glob patterns may not escape the workspace root: ${pattern}`);
|
|
32
|
+
}
|
|
33
|
+
function formatDiff(filePath, beforeText, afterText) {
|
|
34
|
+
if (beforeText === afterText) return "";
|
|
35
|
+
const beforeLines = beforeText.length ? beforeText.split("\n") : [];
|
|
36
|
+
const afterLines = afterText.length ? afterText.split("\n") : [];
|
|
37
|
+
return [
|
|
38
|
+
`--- ${filePath}`,
|
|
39
|
+
`+++ ${filePath}`,
|
|
40
|
+
"@@",
|
|
41
|
+
...beforeLines.map((line) => `-${line}`),
|
|
42
|
+
...afterLines.map((line) => `+${line}`)
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
var MasterCSSMCPContext = class {
|
|
46
|
+
root;
|
|
47
|
+
roots;
|
|
48
|
+
previewTTL;
|
|
49
|
+
containmentRoots;
|
|
50
|
+
previews = /* @__PURE__ */ new Map();
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
const configuredRoots = options.roots?.length ? options.roots : [options.root || process.cwd()];
|
|
53
|
+
const roots = [];
|
|
54
|
+
const containmentRoots = [];
|
|
55
|
+
for (const root of configuredRoots) {
|
|
56
|
+
const [resolved] = collectPathAliases(root);
|
|
57
|
+
if (!existsSync(resolved)) throw new Error(`Workspace root does not exist: ${resolved}`);
|
|
58
|
+
const real = realpathSync(resolved);
|
|
59
|
+
if (!statSync(real).isDirectory()) throw new Error(`Workspace root is not a directory: ${real}`);
|
|
60
|
+
roots.push(real);
|
|
61
|
+
containmentRoots.push(...collectPathAliases(resolved));
|
|
62
|
+
}
|
|
63
|
+
this.roots = roots;
|
|
64
|
+
this.root = this.roots[0];
|
|
65
|
+
this.containmentRoots = [...new Set(containmentRoots)];
|
|
66
|
+
this.previewTTL = options.previewTTL ?? DEFAULT_PREVIEW_TTL;
|
|
67
|
+
}
|
|
68
|
+
assertContained(filePath) {
|
|
69
|
+
const resolvedPath = resolve(filePath);
|
|
70
|
+
const pathAliases = collectPathAliases(resolvedPath);
|
|
71
|
+
if (!this.containmentRoots.some((root) => pathAliases.some((pathAlias) => isContained(root, pathAlias)))) throw new Error(`Path is outside the allowed workspace roots: ${filePath}`);
|
|
72
|
+
return resolvedPath;
|
|
73
|
+
}
|
|
74
|
+
resolveVirtualPath(filePath) {
|
|
75
|
+
const resolved = resolve(this.root, filePath);
|
|
76
|
+
return this.assertContained(resolved);
|
|
77
|
+
}
|
|
78
|
+
async resolveExistingFile(filePath) {
|
|
79
|
+
const realPath = await realpath$1(resolve(this.root, filePath));
|
|
80
|
+
this.assertContained(realPath);
|
|
81
|
+
if (!(await stat(realPath)).isFile()) throw new Error(`Path is not a file: ${realPath}`);
|
|
82
|
+
return realPath;
|
|
83
|
+
}
|
|
84
|
+
async resolveWritableFile(filePath) {
|
|
85
|
+
const resolved = resolve(this.root, filePath);
|
|
86
|
+
if (existsSync(resolved)) {
|
|
87
|
+
const realPath = await realpath$1(resolved);
|
|
88
|
+
this.assertContained(realPath);
|
|
89
|
+
if (!(await stat(realPath)).isFile()) throw new Error(`Path is not a file: ${realPath}`);
|
|
90
|
+
return realPath;
|
|
91
|
+
}
|
|
92
|
+
this.assertContained(resolved);
|
|
93
|
+
const parent = await realpath$1(dirname(resolved));
|
|
94
|
+
this.assertContained(parent);
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
validateGlobPatterns(patterns) {
|
|
98
|
+
for (const pattern of patterns) assertFilePattern(pattern);
|
|
99
|
+
}
|
|
100
|
+
async createPreview(changes, ttl = this.previewTTL) {
|
|
101
|
+
const storedChanges = [];
|
|
102
|
+
for (const change of changes) {
|
|
103
|
+
const filePath = await this.resolveWritableFile(change.filePath);
|
|
104
|
+
const beforeText = change.beforeText ?? await readFileIfExists(filePath);
|
|
105
|
+
if (beforeText === change.afterText) continue;
|
|
106
|
+
const beforeExists = existsSync(filePath);
|
|
107
|
+
storedChanges.push({
|
|
108
|
+
filePath,
|
|
109
|
+
beforeHash: beforeExists ? sha256(beforeText) : null,
|
|
110
|
+
afterHash: sha256(change.afterText),
|
|
111
|
+
beforeExists,
|
|
112
|
+
beforeBytes: beforeText.length,
|
|
113
|
+
afterBytes: change.afterText.length,
|
|
114
|
+
afterText: change.afterText,
|
|
115
|
+
diff: formatDiff(filePath, beforeText, change.afterText)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const summary = {
|
|
119
|
+
files: storedChanges.length,
|
|
120
|
+
bytesBefore: storedChanges.reduce((total, change) => total + change.beforeBytes, 0),
|
|
121
|
+
bytesAfter: storedChanges.reduce((total, change) => total + change.afterBytes, 0)
|
|
122
|
+
};
|
|
123
|
+
if (!storedChanges.length) return {
|
|
124
|
+
changes: [],
|
|
125
|
+
summary
|
|
126
|
+
};
|
|
127
|
+
const confirmToken = randomBytes(24).toString("base64url");
|
|
128
|
+
const createdAt = Date.now();
|
|
129
|
+
const expiresAt = createdAt + ttl;
|
|
130
|
+
this.previews.set(confirmToken, {
|
|
131
|
+
confirmToken,
|
|
132
|
+
createdAt,
|
|
133
|
+
expiresAt,
|
|
134
|
+
changes: storedChanges
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
confirmToken,
|
|
138
|
+
expiresAt,
|
|
139
|
+
changes: storedChanges,
|
|
140
|
+
summary
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async applyPreview(confirmToken) {
|
|
144
|
+
const preview = this.previews.get(confirmToken);
|
|
145
|
+
if (!preview) throw new Error("Unknown or already applied preview token.");
|
|
146
|
+
if (preview.expiresAt < Date.now()) {
|
|
147
|
+
this.previews.delete(confirmToken);
|
|
148
|
+
throw new Error("Preview token has expired.");
|
|
149
|
+
}
|
|
150
|
+
for (const change of preview.changes) {
|
|
151
|
+
const filePath = await this.resolveWritableFile(change.filePath);
|
|
152
|
+
if (filePath !== change.filePath) throw new Error(`Preview path changed while applying: ${change.filePath}`);
|
|
153
|
+
const currentText = await readFileIfExists(filePath);
|
|
154
|
+
const currentExists = existsSync(filePath);
|
|
155
|
+
if (change.beforeHash === null) {
|
|
156
|
+
if (currentExists) throw new Error(`File was created after preview generation: ${filePath}`);
|
|
157
|
+
} else if (sha256(currentText) !== change.beforeHash) throw new Error(`File changed after preview generation: ${filePath}`);
|
|
158
|
+
}
|
|
159
|
+
for (const change of preview.changes) {
|
|
160
|
+
await mkdir(dirname(change.filePath), { recursive: true });
|
|
161
|
+
await writeFile(change.filePath, change.afterText);
|
|
162
|
+
}
|
|
163
|
+
this.previews.delete(confirmToken);
|
|
164
|
+
return {
|
|
165
|
+
applied: true,
|
|
166
|
+
changes: preview.changes.map(({ filePath, afterHash }) => ({
|
|
167
|
+
filePath,
|
|
168
|
+
afterHash
|
|
169
|
+
}))
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function realpath$1(filePath) {
|
|
174
|
+
try {
|
|
175
|
+
return await realpath(filePath);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Unable to resolve path: ${filePath}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function readFileIfExists(filePath) {
|
|
181
|
+
try {
|
|
182
|
+
return await readFile(filePath, "utf8");
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") return "";
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
export { MasterCSSMCPContext as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { MasterCSSMCPServerInstance, createMasterCSSMCPServer, startStdioServer } from "./server.js";
|
|
2
|
+
import MasterCSSMCPContext, { MasterCSSMCPContextOptions } from "./context.js";
|
|
3
|
+
export { MasterCSSMCPContext, type MasterCSSMCPContextOptions, type MasterCSSMCPServerInstance, createMasterCSSMCPServer, startStdioServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import MasterCSSMCPContext from "./context.js";
|
|
2
|
+
|
|
3
|
+
//#region src/language.d.ts
|
|
4
|
+
interface SuggestSyntaxOptions {
|
|
5
|
+
content: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
position: {
|
|
8
|
+
line: number;
|
|
9
|
+
character: number;
|
|
10
|
+
};
|
|
11
|
+
triggerCharacter?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
declare function suggestSyntax(context: MasterCSSMCPContext, options: SuggestSyntaxOptions): Promise<{
|
|
15
|
+
manifest: {
|
|
16
|
+
error?: string | undefined;
|
|
17
|
+
status: "loaded" | "error";
|
|
18
|
+
entries: string[];
|
|
19
|
+
};
|
|
20
|
+
completions: import("vscode-languageserver-protocol").CompletionItem[];
|
|
21
|
+
total: number;
|
|
22
|
+
hover: import("vscode-languageserver-protocol").Hover | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { SuggestSyntaxOptions, suggestSyntax };
|
package/dist/language.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadWorkspaceManifest } from "./project.js";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import CSSLanguageService from "@master/css-language-service";
|
|
5
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
6
|
+
//#region src/language.ts
|
|
7
|
+
const languageByExtension = {
|
|
8
|
+
".astro": "astro",
|
|
9
|
+
".css": "css",
|
|
10
|
+
".htm": "html",
|
|
11
|
+
".html": "html",
|
|
12
|
+
".js": "javascript",
|
|
13
|
+
".jsx": "javascriptreact",
|
|
14
|
+
".less": "less",
|
|
15
|
+
".md": "markdown",
|
|
16
|
+
".mdx": "mdx",
|
|
17
|
+
".scss": "scss",
|
|
18
|
+
".svelte": "svelte",
|
|
19
|
+
".ts": "typescript",
|
|
20
|
+
".tsx": "typescriptreact",
|
|
21
|
+
".vue": "vue"
|
|
22
|
+
};
|
|
23
|
+
async function suggestSyntax(context, options) {
|
|
24
|
+
const filePath = context.resolveVirtualPath(options.filePath);
|
|
25
|
+
const manifest = await loadWorkspaceManifest(context);
|
|
26
|
+
const service = new CSSLanguageService(manifest.status === "loaded" ? { manifest: manifest.manifest } : void 0);
|
|
27
|
+
const document = TextDocument.create(pathToFileURL(filePath).href, languageByExtension[extname(filePath).toLowerCase()] || "html", 0, options.content);
|
|
28
|
+
const completions = service.suggestSyntax(document, options.position, {
|
|
29
|
+
triggerKind: options.triggerCharacter ? 2 : 1,
|
|
30
|
+
...options.triggerCharacter ? { triggerCharacter: options.triggerCharacter } : {}
|
|
31
|
+
}) ?? [];
|
|
32
|
+
const hover = service.inspectSyntax(document, options.position);
|
|
33
|
+
const limit = options.limit ?? 50;
|
|
34
|
+
return {
|
|
35
|
+
manifest: {
|
|
36
|
+
status: manifest.status,
|
|
37
|
+
entries: manifest.entries,
|
|
38
|
+
...manifest.status === "error" ? { error: manifest.error } : {}
|
|
39
|
+
},
|
|
40
|
+
completions: completions.slice(0, limit),
|
|
41
|
+
total: completions.length,
|
|
42
|
+
hover
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { suggestSyntax };
|
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MasterCSSLintFileResult, MasterCSSLintSourceDiagnostic } from "@master/css-lint";
|
|
2
|
+
import MasterCSSMCPContext from "./context.js";
|
|
3
|
+
|
|
4
|
+
//#region src/lint.d.ts
|
|
5
|
+
interface LintProjectOptions {
|
|
6
|
+
patterns?: string[];
|
|
7
|
+
rules?: string;
|
|
8
|
+
}
|
|
9
|
+
interface LintContentOptions {
|
|
10
|
+
content: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
rules?: string;
|
|
13
|
+
}
|
|
14
|
+
interface PreviewFixesOptions extends LintProjectOptions {
|
|
15
|
+
includeDirectiveFixes?: boolean;
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function lintProject(context: MasterCSSMCPContext, options?: LintProjectOptions): Promise<{
|
|
19
|
+
version: number;
|
|
20
|
+
root: string;
|
|
21
|
+
manifest: {
|
|
22
|
+
status: "loaded" | "error";
|
|
23
|
+
entries: string[];
|
|
24
|
+
diagnostics: MasterCSSLintSourceDiagnostic[];
|
|
25
|
+
};
|
|
26
|
+
files: MasterCSSLintFileResult[];
|
|
27
|
+
summary: import("@master/css-lint").MasterCSSLintSummary;
|
|
28
|
+
}>;
|
|
29
|
+
declare function lintContent(context: MasterCSSMCPContext, options: LintContentOptions): Promise<{
|
|
30
|
+
version: number;
|
|
31
|
+
root: string;
|
|
32
|
+
manifest: {
|
|
33
|
+
status: "loaded" | "error";
|
|
34
|
+
entries: string[];
|
|
35
|
+
diagnostics: MasterCSSLintSourceDiagnostic[];
|
|
36
|
+
};
|
|
37
|
+
files: MasterCSSLintFileResult[];
|
|
38
|
+
summary: import("@master/css-lint").MasterCSSLintSummary;
|
|
39
|
+
}>;
|
|
40
|
+
declare function previewLintFixes(context: MasterCSSMCPContext, options?: PreviewFixesOptions): Promise<{
|
|
41
|
+
mode: string;
|
|
42
|
+
preview: import("./context.js").PreviewResult;
|
|
43
|
+
lint: {
|
|
44
|
+
version: number;
|
|
45
|
+
root: string;
|
|
46
|
+
manifest: {
|
|
47
|
+
status: "loaded" | "error";
|
|
48
|
+
entries: string[];
|
|
49
|
+
diagnostics: MasterCSSLintSourceDiagnostic[];
|
|
50
|
+
};
|
|
51
|
+
files: MasterCSSLintFileResult[];
|
|
52
|
+
summary: import("@master/css-lint").MasterCSSLintSummary;
|
|
53
|
+
};
|
|
54
|
+
}>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { LintContentOptions, LintProjectOptions, PreviewFixesOptions, lintContent, lintProject, previewLintFixes };
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { loadWorkspaceManifest } from "./project.js";
|
|
2
|
+
import { resolveSourceFiles } from "./scan.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createCSSWithNativeDeclarations } from "@master/css-validator/native-declaration";
|
|
5
|
+
import { fixMasterCSSContent, lintMasterCSSContent, resolveMasterCSSLintRules, summarizeMasterCSSLintFiles } from "@master/css-lint";
|
|
6
|
+
//#region src/lint.ts
|
|
7
|
+
const DEFAULT_LINT_SOURCE_PATTERNS = ["**/*.{html,htm,js,jsx,cjs,ts,tsx,mts,cts,svelte,astro,vue,md,mdx,pug,php,css,scss,less}"];
|
|
8
|
+
const DEFAULT_IGNORE_PATTERNS = ["**/node_modules/**", "node_modules"];
|
|
9
|
+
const LINT_REPORT_VERSION = 1;
|
|
10
|
+
function createManifestDiagnostic(context, message) {
|
|
11
|
+
return {
|
|
12
|
+
ruleId: "manifest",
|
|
13
|
+
code: "manifest-loading-error",
|
|
14
|
+
severity: "error",
|
|
15
|
+
message,
|
|
16
|
+
range: {
|
|
17
|
+
start: 0,
|
|
18
|
+
end: 0
|
|
19
|
+
},
|
|
20
|
+
loc: {
|
|
21
|
+
start: {
|
|
22
|
+
line: 1,
|
|
23
|
+
column: 1
|
|
24
|
+
},
|
|
25
|
+
end: {
|
|
26
|
+
line: 1,
|
|
27
|
+
column: 1
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
source: "Master CSS",
|
|
31
|
+
sourceKind: "manifest",
|
|
32
|
+
data: { cwd: context.root }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function loadLintState(context, options = {}) {
|
|
36
|
+
const rules = resolveMasterCSSLintRules(options.rules);
|
|
37
|
+
const manifest = await loadWorkspaceManifest(context);
|
|
38
|
+
const css = manifest.status === "loaded" ? createCSSWithNativeDeclarations(manifest.manifest) : void 0;
|
|
39
|
+
const files = await resolveSourceFiles(context, options.patterns ?? DEFAULT_LINT_SOURCE_PATTERNS, options.patterns ? [] : DEFAULT_IGNORE_PATTERNS);
|
|
40
|
+
return {
|
|
41
|
+
rules,
|
|
42
|
+
manifest,
|
|
43
|
+
css,
|
|
44
|
+
inputs: await Promise.all(files.map(async (filePath) => ({
|
|
45
|
+
filePath,
|
|
46
|
+
content: await readFile(filePath, "utf8")
|
|
47
|
+
})))
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function lintInputs(inputs, css, rules) {
|
|
51
|
+
return inputs.map((input) => lintMasterCSSContent({
|
|
52
|
+
content: input.content,
|
|
53
|
+
filePath: input.filePath,
|
|
54
|
+
css,
|
|
55
|
+
rules
|
|
56
|
+
})).filter((result) => result.diagnostics.length);
|
|
57
|
+
}
|
|
58
|
+
function createManifestFileResult(context, diagnostic) {
|
|
59
|
+
return {
|
|
60
|
+
filePath: context.root,
|
|
61
|
+
languageId: "manifest",
|
|
62
|
+
sourceKind: "manifest",
|
|
63
|
+
diagnostics: [diagnostic]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function lintProject(context, options = {}) {
|
|
67
|
+
const state = await loadLintState(context, options);
|
|
68
|
+
const files = state.manifest.status === "error" || !state.css ? [createManifestFileResult(context, createManifestDiagnostic(context, `Failed to load Master CSS manifest: ${state.manifest.error}`))] : lintInputs(state.inputs, state.css, state.rules);
|
|
69
|
+
return {
|
|
70
|
+
version: LINT_REPORT_VERSION,
|
|
71
|
+
root: context.root,
|
|
72
|
+
manifest: {
|
|
73
|
+
status: state.manifest.status,
|
|
74
|
+
entries: state.manifest.entries,
|
|
75
|
+
diagnostics: state.manifest.status === "error" ? files[0].diagnostics : []
|
|
76
|
+
},
|
|
77
|
+
files,
|
|
78
|
+
summary: summarizeMasterCSSLintFiles(files)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function lintContent(context, options) {
|
|
82
|
+
const rules = resolveMasterCSSLintRules(options.rules);
|
|
83
|
+
const filePath = context.resolveVirtualPath(options.filePath);
|
|
84
|
+
const manifest = await loadWorkspaceManifest(context);
|
|
85
|
+
const files = manifest.status === "error" ? [createManifestFileResult(context, createManifestDiagnostic(context, `Failed to load Master CSS manifest: ${manifest.error}`))] : [lintMasterCSSContent({
|
|
86
|
+
content: options.content,
|
|
87
|
+
filePath,
|
|
88
|
+
css: createCSSWithNativeDeclarations(manifest.manifest),
|
|
89
|
+
rules
|
|
90
|
+
})];
|
|
91
|
+
return {
|
|
92
|
+
version: LINT_REPORT_VERSION,
|
|
93
|
+
root: context.root,
|
|
94
|
+
manifest: {
|
|
95
|
+
status: manifest.status,
|
|
96
|
+
entries: manifest.entries,
|
|
97
|
+
diagnostics: manifest.status === "error" ? files[0].diagnostics : []
|
|
98
|
+
},
|
|
99
|
+
files,
|
|
100
|
+
summary: summarizeMasterCSSLintFiles(files)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function previewLintFixes(context, options = {}) {
|
|
104
|
+
const state = await loadLintState(context, options);
|
|
105
|
+
if (state.manifest.status === "error" || !state.css) return {
|
|
106
|
+
mode: "lint-fixes",
|
|
107
|
+
preview: await context.createPreview([], options.ttlMs),
|
|
108
|
+
lint: await lintProject(context, options)
|
|
109
|
+
};
|
|
110
|
+
const changes = [];
|
|
111
|
+
for (const input of state.inputs) {
|
|
112
|
+
const fixed = fixMasterCSSContent({
|
|
113
|
+
content: input.content,
|
|
114
|
+
filePath: input.filePath,
|
|
115
|
+
css: state.css,
|
|
116
|
+
rules: state.rules,
|
|
117
|
+
includeDirectiveFixes: Boolean(options.includeDirectiveFixes)
|
|
118
|
+
});
|
|
119
|
+
if (fixed !== input.content) changes.push({
|
|
120
|
+
filePath: input.filePath,
|
|
121
|
+
beforeText: input.content,
|
|
122
|
+
afterText: fixed
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
mode: "lint-fixes",
|
|
127
|
+
preview: await context.createPreview(changes, options.ttlMs),
|
|
128
|
+
lint: await lintProject(context, options)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { lintContent, lintProject, previewLintFixes };
|