@expressivecss/mcp-server 0.2.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/.claude-plugin/plugin.json +14 -0
- package/.cursor-plugin/plugin.json +25 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +101 -0
- package/README.md +240 -0
- package/capability-roadmap.json +3109 -0
- package/component-decisions.json +6950 -0
- package/component-guides.json +171 -0
- package/contract.json +27 -0
- package/lint.mjs +68 -0
- package/mcp.json +14 -0
- package/package.json +49 -0
- package/sample-hermes-config.yaml +13 -0
- package/scripts/resolve-version.mjs +736 -0
- package/semantics-data.json +1166 -0
- package/server.js +2380 -0
- package/smoke.mjs +1237 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// Generated from scripts/lib/resolve-expressivecss-version.mjs. Do not edit.
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const PACKAGE_NAME = '@expressivecss/expressive';
|
|
8
|
+
const MAX_MANIFEST_BYTES = 1 * 1024 * 1024;
|
|
9
|
+
const MAX_LOCKFILE_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const MAX_FRAMEWORK_MARKER_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
function readLimit(filePath) {
|
|
13
|
+
const name = path.basename(filePath);
|
|
14
|
+
if (['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'].includes(name)) return MAX_LOCKFILE_BYTES;
|
|
15
|
+
if (['llm.md', 'semantics.json'].includes(name)) return MAX_FRAMEWORK_MARKER_BYTES;
|
|
16
|
+
return MAX_MANIFEST_BYTES;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sameFile(left, right) {
|
|
20
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
21
|
+
&& left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readBounded(filePath) {
|
|
25
|
+
let handle;
|
|
26
|
+
try {
|
|
27
|
+
const pathBefore = await lstat(filePath, { bigint: true });
|
|
28
|
+
if (pathBefore.isSymbolicLink()) return { error: 'symbolic-link' };
|
|
29
|
+
if (!pathBefore.isFile()) return { error: 'not-regular-file' };
|
|
30
|
+
const noFollow = fsConstants.O_NOFOLLOW ?? 0;
|
|
31
|
+
handle = await open(filePath, fsConstants.O_RDONLY | noFollow);
|
|
32
|
+
const before = await handle.stat({ bigint: true });
|
|
33
|
+
if (!before.isFile() || !sameFile(before, pathBefore)) return { error: 'identity-changed' };
|
|
34
|
+
const limit = readLimit(filePath);
|
|
35
|
+
if (before.size > BigInt(limit)) return { error: 'oversized' };
|
|
36
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
37
|
+
let offset = 0;
|
|
38
|
+
while (offset < buffer.length) {
|
|
39
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
40
|
+
if (bytesRead === 0) break;
|
|
41
|
+
offset += bytesRead;
|
|
42
|
+
}
|
|
43
|
+
if (offset > limit) return { error: 'oversized' };
|
|
44
|
+
const after = await handle.stat({ bigint: true });
|
|
45
|
+
const pathAfter = await lstat(filePath, { bigint: true });
|
|
46
|
+
const currentResolved = await realpath(filePath);
|
|
47
|
+
const openedResolved = await realpath(`/proc/self/fd/${handle.fd}`).catch(() => currentResolved);
|
|
48
|
+
if (!sameFile(before, after) || !sameFile(after, pathAfter) || currentResolved !== openedResolved) {
|
|
49
|
+
return { error: 'identity-changed' };
|
|
50
|
+
}
|
|
51
|
+
return { text: buffer.subarray(0, offset).toString('utf8') };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code === 'ENOENT') return { missing: true };
|
|
54
|
+
if (error?.code === 'ELOOP' || error?.code === 'EMLINK') return { error: 'symbolic-link' };
|
|
55
|
+
return { error: 'unreadable' };
|
|
56
|
+
} finally {
|
|
57
|
+
await handle?.close().catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readText(filePath) {
|
|
62
|
+
const result = await readBounded(filePath);
|
|
63
|
+
return result.text ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function readJson(filePath) {
|
|
67
|
+
const text = await readText(filePath);
|
|
68
|
+
if (text === null) return null;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanVersion(value) {
|
|
77
|
+
if (typeof value !== 'string') return null;
|
|
78
|
+
const cleaned = value.trim().replace(/^['"]|['"]$/gu, '').replace(/\(.+\)$/u, '');
|
|
79
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(cleaned)
|
|
80
|
+
? cleaned
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseSemver(value) {
|
|
85
|
+
const cleaned = cleanVersion(value);
|
|
86
|
+
if (!cleaned) return null;
|
|
87
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(cleaned);
|
|
88
|
+
return {
|
|
89
|
+
major: Number(match[1]),
|
|
90
|
+
minor: Number(match[2]),
|
|
91
|
+
patch: Number(match[3]),
|
|
92
|
+
prerelease: match[4]?.split('.') ?? [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function versionsHaveEqualPrecedence(left, right) {
|
|
97
|
+
const parsedLeft = parseSemver(left);
|
|
98
|
+
const parsedRight = parseSemver(right);
|
|
99
|
+
return Boolean(parsedLeft && parsedRight && compareSemver(parsedLeft, parsedRight) === 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function compareSemver(left, right) {
|
|
103
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
104
|
+
if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
|
|
105
|
+
}
|
|
106
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
107
|
+
return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length ? -1 : 1;
|
|
108
|
+
}
|
|
109
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
|
110
|
+
for (let index = 0; index < length; index += 1) {
|
|
111
|
+
const leftPart = left.prerelease[index];
|
|
112
|
+
const rightPart = right.prerelease[index];
|
|
113
|
+
if (leftPart === undefined || rightPart === undefined) return leftPart === undefined ? -1 : 1;
|
|
114
|
+
if (leftPart === rightPart) continue;
|
|
115
|
+
const leftNumber = /^\d+$/u.test(leftPart) ? BigInt(leftPart) : null;
|
|
116
|
+
const rightNumber = /^\d+$/u.test(rightPart) ? BigInt(rightPart) : null;
|
|
117
|
+
if (leftNumber !== null && rightNumber !== null) return leftNumber < rightNumber ? -1 : 1;
|
|
118
|
+
if (leftNumber !== null || rightNumber !== null) return leftNumber !== null ? -1 : 1;
|
|
119
|
+
return leftPart < rightPart ? -1 : 1;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function satisfiesComparator(version, operator, boundary) {
|
|
125
|
+
const comparison = compareSemver(version, boundary);
|
|
126
|
+
if (operator === '>') return comparison > 0;
|
|
127
|
+
if (operator === '>=') return comparison >= 0;
|
|
128
|
+
if (operator === '<') return comparison < 0;
|
|
129
|
+
if (operator === '<=') return comparison <= 0;
|
|
130
|
+
return comparison === 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function satisfiesSimpleRange(version, range) {
|
|
134
|
+
if (/^(?:\*|x)$/iu.test(range)) return true;
|
|
135
|
+
const wildcard = /^(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/iu.exec(range);
|
|
136
|
+
if (wildcard) {
|
|
137
|
+
const [, major, minor, patch] = wildcard;
|
|
138
|
+
return version.major === Number(major)
|
|
139
|
+
&& (minor === undefined || /^(?:x|\*)$/iu.test(minor) || version.minor === Number(minor))
|
|
140
|
+
&& (patch === undefined || /^(?:x|\*)$/iu.test(patch) || version.patch === Number(patch))
|
|
141
|
+
&& version.prerelease.length === 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const shorthand = /^(\^|~)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(range);
|
|
145
|
+
if (shorthand) {
|
|
146
|
+
const lower = parseSemver(shorthand[2]);
|
|
147
|
+
if (!lower) return null;
|
|
148
|
+
const upper = shorthand[1] === '~'
|
|
149
|
+
? { major: lower.major, minor: lower.minor + 1, patch: 0, prerelease: [] }
|
|
150
|
+
: lower.major > 0
|
|
151
|
+
? { major: lower.major + 1, minor: 0, patch: 0, prerelease: [] }
|
|
152
|
+
: lower.minor > 0
|
|
153
|
+
? { major: 0, minor: lower.minor + 1, patch: 0, prerelease: [] }
|
|
154
|
+
: { major: 0, minor: 0, patch: lower.patch + 1, prerelease: [] };
|
|
155
|
+
return compareSemver(version, lower) >= 0 && compareSemver(version, upper) < 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const comparator = /^(>=|<=|>|<|=)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(range);
|
|
159
|
+
if (!comparator) return null;
|
|
160
|
+
const boundary = parseSemver(comparator[2]);
|
|
161
|
+
return boundary ? satisfiesComparator(version, comparator[1] ?? '=', boundary) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function versionSatisfiesRange(versionValue, rangeValue) {
|
|
165
|
+
const version = parseSemver(versionValue);
|
|
166
|
+
if (!version || typeof rangeValue !== 'string' || !rangeValue.trim()) return null;
|
|
167
|
+
const alternatives = rangeValue.trim().split(/\s*\|\|\s*/u);
|
|
168
|
+
let unsupported = false;
|
|
169
|
+
for (const alternative of alternatives) {
|
|
170
|
+
const comparators = alternative.trim().split(/\s+/u).filter(Boolean);
|
|
171
|
+
if (comparators.length === 0) {
|
|
172
|
+
unsupported = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const prereleaseCore = `${version.major}.${version.minor}.${version.patch}-`;
|
|
176
|
+
if (version.prerelease.length > 0 && !alternative.includes(prereleaseCore)) continue;
|
|
177
|
+
const results = comparators.map((comparator) => satisfiesSimpleRange(version, comparator));
|
|
178
|
+
if (results.every((result) => result === true)) return true;
|
|
179
|
+
if (results.some((result) => result === null)) unsupported = true;
|
|
180
|
+
}
|
|
181
|
+
return unsupported ? null : false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function manifestRange(manifest) {
|
|
185
|
+
return manifest?.dependencies?.[PACKAGE_NAME]
|
|
186
|
+
?? manifest?.devDependencies?.[PACKAGE_NAME]
|
|
187
|
+
?? manifest?.peerDependencies?.[PACKAGE_NAME]
|
|
188
|
+
?? manifest?.optionalDependencies?.[PACKAGE_NAME]
|
|
189
|
+
?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function verifiedLockVersion(version, declaredRange, source, file) {
|
|
193
|
+
const satisfies = versionSatisfiesRange(version, declaredRange);
|
|
194
|
+
if (satisfies === true) return { version, diagnostics: [] };
|
|
195
|
+
return {
|
|
196
|
+
version: null,
|
|
197
|
+
diagnostics: [{
|
|
198
|
+
code: satisfies === false ? 'lockfile-version-outside-manifest-range' : 'unsupported-manifest-range',
|
|
199
|
+
severity: 'blocked',
|
|
200
|
+
source,
|
|
201
|
+
path: file,
|
|
202
|
+
message: satisfies === false
|
|
203
|
+
? `${file} resolves ${version}, outside manifest range ${JSON.stringify(declaredRange)}.`
|
|
204
|
+
: `Cannot verify ${file} version ${version} against unsupported manifest range ${JSON.stringify(declaredRange)}.`,
|
|
205
|
+
}],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function npmLockResolution(lock, declaredRange) {
|
|
210
|
+
if (lock?.lockfileVersion === 1) {
|
|
211
|
+
const entry = lock?.dependencies?.[PACKAGE_NAME];
|
|
212
|
+
const version = cleanVersion(entry?.version);
|
|
213
|
+
if (version) return verifiedLockVersion(version, declaredRange, 'npm', 'package-lock.json');
|
|
214
|
+
return entry
|
|
215
|
+
? { version: null, diagnostics: [malformedLockVersion('npm', 'package-lock.json')] }
|
|
216
|
+
: { version: null, diagnostics: [directEntryMissing('npm', 'package-lock.json')] };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lockedRange = manifestRange(lock?.packages?.['']);
|
|
220
|
+
if (!lockedRange) {
|
|
221
|
+
return { version: null, diagnostics: [directEntryMissing('npm', 'package-lock.json')] };
|
|
222
|
+
}
|
|
223
|
+
if (lockedRange !== declaredRange) {
|
|
224
|
+
return {
|
|
225
|
+
version: null,
|
|
226
|
+
diagnostics: [{
|
|
227
|
+
code: 'lockfile-declaration-mismatch',
|
|
228
|
+
severity: 'warning',
|
|
229
|
+
source: 'npm',
|
|
230
|
+
path: 'package-lock.json',
|
|
231
|
+
message: `package-lock.json declares ${JSON.stringify(lockedRange)}, not manifest range ${JSON.stringify(declaredRange)}.`,
|
|
232
|
+
}],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const version = cleanVersion(lock?.packages?.[`node_modules/${PACKAGE_NAME}`]?.version);
|
|
236
|
+
return version
|
|
237
|
+
? verifiedLockVersion(version, declaredRange, 'npm', 'package-lock.json')
|
|
238
|
+
: { version: null, diagnostics: [malformedLockVersion('npm', 'package-lock.json')] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function directEntryMissing(source, file) {
|
|
242
|
+
return {
|
|
243
|
+
code: 'direct-lock-entry-missing',
|
|
244
|
+
severity: 'warning',
|
|
245
|
+
source,
|
|
246
|
+
path: file,
|
|
247
|
+
message: `${file} has no matching direct ${PACKAGE_NAME} entry.`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function malformedLockVersion(source, file) {
|
|
252
|
+
return {
|
|
253
|
+
code: 'malformed-lockfile',
|
|
254
|
+
severity: 'blocked',
|
|
255
|
+
source,
|
|
256
|
+
path: file,
|
|
257
|
+
message: `${file} has no valid resolved ExpressiveCSS version for its direct entry.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function pnpmLockResolution(text, declaredRange) {
|
|
262
|
+
const lines = text.split(/\r?\n/u);
|
|
263
|
+
if (lines.some((line) => /^\t|^ +\t/u.test(line))) {
|
|
264
|
+
return {
|
|
265
|
+
version: null,
|
|
266
|
+
diagnostics: [{
|
|
267
|
+
code: 'malformed-lockfile',
|
|
268
|
+
severity: 'blocked',
|
|
269
|
+
source: 'pnpm',
|
|
270
|
+
path: 'pnpm-lock.yaml',
|
|
271
|
+
message: 'pnpm-lock.yaml uses tab indentation.',
|
|
272
|
+
}],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const lockfileVersion = /^lockfileVersion:\s*['"]?([^'"\s]+)['"]?\s*$/u.exec(lines.find((line) => line.startsWith('lockfileVersion:')) ?? '')?.[1];
|
|
276
|
+
const major = Number.parseInt(lockfileVersion ?? '', 10);
|
|
277
|
+
if (!Number.isInteger(major) || major < 6 || major > 9) {
|
|
278
|
+
return {
|
|
279
|
+
version: null,
|
|
280
|
+
diagnostics: [{
|
|
281
|
+
code: lockfileVersion ? 'unsupported-lockfile-version' : 'malformed-lockfile',
|
|
282
|
+
severity: 'blocked',
|
|
283
|
+
source: 'pnpm',
|
|
284
|
+
path: 'pnpm-lock.yaml',
|
|
285
|
+
message: lockfileVersion
|
|
286
|
+
? `Unsupported pnpm lockfile version ${JSON.stringify(lockfileVersion)}.`
|
|
287
|
+
: 'pnpm-lock.yaml has no valid lockfileVersion.',
|
|
288
|
+
}],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const importersIndex = lines.findIndex((line) => line === 'importers:');
|
|
293
|
+
const targetIndex = lines.findIndex((line, index) => index > importersIndex && /^ {2}(?:['"]\.['"]|\.):\s*$/u.test(line));
|
|
294
|
+
if (importersIndex < 0 || targetIndex < 0) {
|
|
295
|
+
return { version: null, diagnostics: [directEntryMissing('pnpm', 'pnpm-lock.yaml')] };
|
|
296
|
+
}
|
|
297
|
+
let targetEnd = lines.length;
|
|
298
|
+
for (let index = targetIndex + 1; index < lines.length; index += 1) {
|
|
299
|
+
if (lines[index].trim() && /^\s*/u.exec(lines[index])[0].length <= 2) {
|
|
300
|
+
targetEnd = index;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const targetLines = lines.slice(targetIndex + 1, targetEnd);
|
|
305
|
+
const dependencyIndex = targetLines.findIndex((line) => /^ {6}['"]?@expressivecss\/expressive['"]?:\s*$/u.test(line));
|
|
306
|
+
if (dependencyIndex < 0) {
|
|
307
|
+
return { version: null, diagnostics: [directEntryMissing('pnpm', 'pnpm-lock.yaml')] };
|
|
308
|
+
}
|
|
309
|
+
const dependencyLines = [];
|
|
310
|
+
for (let index = dependencyIndex + 1; index < targetLines.length; index += 1) {
|
|
311
|
+
if (targetLines[index].trim() && /^\s*/u.exec(targetLines[index])[0].length <= 6) break;
|
|
312
|
+
dependencyLines.push(targetLines[index]);
|
|
313
|
+
}
|
|
314
|
+
const specifier = /^\s+specifier:\s*['"]?([^'"\s]+)['"]?\s*$/mu.exec(dependencyLines.join('\n'))?.[1] ?? null;
|
|
315
|
+
const rawVersion = /^\s+version:\s*['"]?([^'"\s]+)['"]?\s*$/mu.exec(dependencyLines.join('\n'))?.[1] ?? null;
|
|
316
|
+
if (specifier !== declaredRange) {
|
|
317
|
+
return {
|
|
318
|
+
version: null,
|
|
319
|
+
diagnostics: [{
|
|
320
|
+
code: 'lockfile-declaration-mismatch',
|
|
321
|
+
severity: 'warning',
|
|
322
|
+
source: 'pnpm',
|
|
323
|
+
path: 'pnpm-lock.yaml',
|
|
324
|
+
message: `pnpm-lock.yaml declares ${JSON.stringify(specifier)}, not manifest range ${JSON.stringify(declaredRange)}.`,
|
|
325
|
+
}],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const version = cleanVersion(rawVersion);
|
|
329
|
+
return version
|
|
330
|
+
? verifiedLockVersion(version, declaredRange, 'pnpm', 'pnpm-lock.yaml')
|
|
331
|
+
: {
|
|
332
|
+
version: null,
|
|
333
|
+
diagnostics: [{
|
|
334
|
+
code: 'malformed-lockfile',
|
|
335
|
+
severity: 'blocked',
|
|
336
|
+
source: 'pnpm',
|
|
337
|
+
path: 'pnpm-lock.yaml',
|
|
338
|
+
message: 'The target pnpm importer has no valid resolved ExpressiveCSS version.',
|
|
339
|
+
}],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function yarnLockResolution(text, declaredRange) {
|
|
344
|
+
const classic = /^# yarn lockfile v1\s*$/mu.test(text);
|
|
345
|
+
const berry = /^__metadata:\s*$/mu.test(text);
|
|
346
|
+
if (!classic && !berry) {
|
|
347
|
+
return {
|
|
348
|
+
version: null,
|
|
349
|
+
diagnostics: [{
|
|
350
|
+
code: 'malformed-lockfile',
|
|
351
|
+
severity: 'blocked',
|
|
352
|
+
source: 'yarn',
|
|
353
|
+
path: 'yarn.lock',
|
|
354
|
+
message: 'yarn.lock has no supported Classic or Berry format marker.',
|
|
355
|
+
}],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const expected = new Set([
|
|
360
|
+
`${PACKAGE_NAME}@${declaredRange}`,
|
|
361
|
+
`${PACKAGE_NAME}@npm:${declaredRange}`,
|
|
362
|
+
]);
|
|
363
|
+
const lines = text.split(/\r?\n/u);
|
|
364
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
365
|
+
const header = lines[index];
|
|
366
|
+
if (!header.trim() || /^\s|#/u.test(header) || !header.endsWith(':')) continue;
|
|
367
|
+
const descriptors = [...header.slice(0, -1).matchAll(/"([^"]+)"|'([^']+)'|([^,\s]+)/gu)]
|
|
368
|
+
.map((match) => match[1] ?? match[2] ?? match[3]);
|
|
369
|
+
if (!descriptors.some((descriptor) => expected.has(descriptor))) continue;
|
|
370
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
371
|
+
const line = lines[cursor];
|
|
372
|
+
if (line.trim() && !/^\s/u.test(line)) break;
|
|
373
|
+
const rawVersion = /^\s+version(?:\s+|:\s*)["']?([^\s"']+)["']?\s*$/u.exec(line)?.[1];
|
|
374
|
+
if (!rawVersion) continue;
|
|
375
|
+
const version = cleanVersion(rawVersion);
|
|
376
|
+
if (version) return verifiedLockVersion(version, declaredRange, 'yarn', 'yarn.lock');
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
version: null,
|
|
381
|
+
diagnostics: [{
|
|
382
|
+
code: 'malformed-lockfile',
|
|
383
|
+
severity: 'blocked',
|
|
384
|
+
source: 'yarn',
|
|
385
|
+
path: 'yarn.lock',
|
|
386
|
+
message: 'The matching Yarn descriptor has no valid resolved ExpressiveCSS version.',
|
|
387
|
+
}],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { version: null, diagnostics: [directEntryMissing('yarn', 'yarn.lock')] };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function resolveLockfile(projectRoot, packageManagerField = null, declaredRange = null) {
|
|
394
|
+
const declaredManager = typeof packageManagerField === 'string'
|
|
395
|
+
? /^(npm|pnpm|yarn)@/u.exec(packageManagerField)?.[1] ?? null
|
|
396
|
+
: null;
|
|
397
|
+
const lockfiles = [
|
|
398
|
+
{ manager: 'npm', file: 'package-lock.json' },
|
|
399
|
+
{ manager: 'pnpm', file: 'pnpm-lock.yaml' },
|
|
400
|
+
{ manager: 'yarn', file: 'yarn.lock' },
|
|
401
|
+
];
|
|
402
|
+
const reads = {};
|
|
403
|
+
for (const entry of lockfiles) {
|
|
404
|
+
reads[entry.manager] = { ...entry, ...(await readBounded(path.join(projectRoot, entry.file))) };
|
|
405
|
+
}
|
|
406
|
+
const unreadable = lockfiles
|
|
407
|
+
.map((entry) => reads[entry.manager])
|
|
408
|
+
.filter((entry) => entry.error);
|
|
409
|
+
if (unreadable.length > 0) {
|
|
410
|
+
return {
|
|
411
|
+
packageManager: declaredManager ?? unreadable[0].manager,
|
|
412
|
+
version: null,
|
|
413
|
+
file: unreadable[0].file,
|
|
414
|
+
diagnostics: unreadable.map((entry) => ({
|
|
415
|
+
code: 'unreadable-lockfile',
|
|
416
|
+
severity: 'blocked',
|
|
417
|
+
source: entry.manager,
|
|
418
|
+
path: entry.file,
|
|
419
|
+
message: `${entry.file} could not be read as a regular file (${entry.error}).`,
|
|
420
|
+
})),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const npmText = reads.npm.text ?? null;
|
|
424
|
+
let npmLock = null;
|
|
425
|
+
if (npmText !== null) {
|
|
426
|
+
try {
|
|
427
|
+
npmLock = JSON.parse(npmText);
|
|
428
|
+
} catch {
|
|
429
|
+
npmLock = null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const pnpmText = reads.pnpm.text ?? null;
|
|
433
|
+
const yarnText = reads.yarn.text ?? null;
|
|
434
|
+
const existing = [
|
|
435
|
+
npmText !== null ? 'npm' : null,
|
|
436
|
+
pnpmText !== null ? 'pnpm' : null,
|
|
437
|
+
yarnText !== null ? 'yarn' : null,
|
|
438
|
+
].filter(Boolean);
|
|
439
|
+
|
|
440
|
+
if (existing.length > 1 && !declaredManager) {
|
|
441
|
+
return {
|
|
442
|
+
packageManager: 'unknown',
|
|
443
|
+
version: null,
|
|
444
|
+
file: null,
|
|
445
|
+
diagnostics: [{
|
|
446
|
+
code: 'ambiguous-lockfiles',
|
|
447
|
+
severity: 'blocked',
|
|
448
|
+
source: 'lockfile',
|
|
449
|
+
path: null,
|
|
450
|
+
message: 'Multiple lockfiles exist and package.json does not select one.',
|
|
451
|
+
}],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const selectedManager = declaredManager ?? existing[0] ?? null;
|
|
456
|
+
if (selectedManager === 'npm' && npmText !== null && !npmLock) {
|
|
457
|
+
return {
|
|
458
|
+
packageManager: 'npm',
|
|
459
|
+
version: null,
|
|
460
|
+
file: 'package-lock.json',
|
|
461
|
+
diagnostics: [{
|
|
462
|
+
code: 'malformed-lockfile',
|
|
463
|
+
severity: 'blocked',
|
|
464
|
+
source: 'npm',
|
|
465
|
+
path: 'package-lock.json',
|
|
466
|
+
message: 'package-lock.json is not valid JSON.',
|
|
467
|
+
}],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (selectedManager === 'npm' && ![1, 2, 3].includes(npmLock?.lockfileVersion)) {
|
|
471
|
+
return {
|
|
472
|
+
packageManager: 'npm',
|
|
473
|
+
version: null,
|
|
474
|
+
file: 'package-lock.json',
|
|
475
|
+
diagnostics: [{
|
|
476
|
+
code: 'unsupported-lockfile-version',
|
|
477
|
+
severity: 'blocked',
|
|
478
|
+
source: 'npm',
|
|
479
|
+
path: 'package-lock.json',
|
|
480
|
+
message: `Unsupported npm lockfile version ${JSON.stringify(npmLock?.lockfileVersion)}.`,
|
|
481
|
+
}],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (declaredManager && !existing.includes(declaredManager) && existing.length > 0) {
|
|
485
|
+
const selectedFile = {
|
|
486
|
+
npm: 'package-lock.json',
|
|
487
|
+
pnpm: 'pnpm-lock.yaml',
|
|
488
|
+
yarn: 'yarn.lock',
|
|
489
|
+
}[declaredManager];
|
|
490
|
+
return {
|
|
491
|
+
packageManager: declaredManager,
|
|
492
|
+
version: null,
|
|
493
|
+
file: selectedFile,
|
|
494
|
+
diagnostics: [{
|
|
495
|
+
code: 'selected-lockfile-missing',
|
|
496
|
+
severity: 'blocked',
|
|
497
|
+
source: declaredManager,
|
|
498
|
+
path: selectedFile,
|
|
499
|
+
message: `package.json selects ${declaredManager}, but ${selectedFile} is missing.`,
|
|
500
|
+
}],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (declaredManager === 'pnpm' && pnpmText !== null) {
|
|
505
|
+
return { packageManager: 'pnpm', file: 'pnpm-lock.yaml', ...pnpmLockResolution(pnpmText, declaredRange) };
|
|
506
|
+
}
|
|
507
|
+
if (declaredManager === 'yarn' && yarnText !== null) {
|
|
508
|
+
return { packageManager: 'yarn', file: 'yarn.lock', ...yarnLockResolution(yarnText, declaredRange) };
|
|
509
|
+
}
|
|
510
|
+
if (declaredManager === 'npm' && npmLock) {
|
|
511
|
+
return { packageManager: 'npm', file: 'package-lock.json', ...npmLockResolution(npmLock, declaredRange) };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (npmLock) {
|
|
515
|
+
return { packageManager: 'npm', file: 'package-lock.json', ...npmLockResolution(npmLock, declaredRange) };
|
|
516
|
+
}
|
|
517
|
+
if (pnpmText !== null) {
|
|
518
|
+
return { packageManager: 'pnpm', file: 'pnpm-lock.yaml', ...pnpmLockResolution(pnpmText, declaredRange) };
|
|
519
|
+
}
|
|
520
|
+
if (yarnText !== null) {
|
|
521
|
+
return { packageManager: 'yarn', file: 'yarn.lock', ...yarnLockResolution(yarnText, declaredRange) };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { packageManager: declaredManager ?? 'unknown', version: null, file: null };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function bundledContract(explicitPath = null) {
|
|
528
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
529
|
+
const candidates = [
|
|
530
|
+
explicitPath,
|
|
531
|
+
path.resolve(scriptDirectory, '../references/contract.json'),
|
|
532
|
+
path.resolve(scriptDirectory, '../contract.json'),
|
|
533
|
+
].filter(Boolean);
|
|
534
|
+
for (const candidate of candidates) {
|
|
535
|
+
const contract = await readJson(candidate);
|
|
536
|
+
if (contract?.frameworkVersion) return contract;
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function resolveExpressiveVersion({
|
|
542
|
+
projectRoot = process.cwd(),
|
|
543
|
+
contractVersion = null,
|
|
544
|
+
skillVersion = null,
|
|
545
|
+
contractManifestPath = null,
|
|
546
|
+
} = {}) {
|
|
547
|
+
const root = path.resolve(projectRoot);
|
|
548
|
+
const manifest = await readJson(path.join(root, 'package.json'));
|
|
549
|
+
const declaredRange = manifestRange(manifest);
|
|
550
|
+
const lock = await resolveLockfile(root, manifest?.packageManager, declaredRange);
|
|
551
|
+
const isFrameworkSource = manifest?.name === PACKAGE_NAME
|
|
552
|
+
&& cleanVersion(manifest.version)
|
|
553
|
+
&& (await Promise.all([
|
|
554
|
+
readText(path.join(root, 'llm.md')),
|
|
555
|
+
readText(path.join(root, 'semantics.json')),
|
|
556
|
+
readText(path.join(root, 'docs', 'src', 'data', 'nav.ts')),
|
|
557
|
+
])).every((source) => source !== null);
|
|
558
|
+
const warnings = [];
|
|
559
|
+
const diagnostics = isFrameworkSource || !declaredRange ? [] : [...(lock.diagnostics ?? [])];
|
|
560
|
+
const candidates = [];
|
|
561
|
+
const conflicts = [];
|
|
562
|
+
|
|
563
|
+
let resolvedVersion = null;
|
|
564
|
+
let resolutionSource = 'none';
|
|
565
|
+
|
|
566
|
+
if (isFrameworkSource) {
|
|
567
|
+
resolvedVersion = cleanVersion(manifest.version);
|
|
568
|
+
resolutionSource = 'framework-source';
|
|
569
|
+
} else if (declaredRange) {
|
|
570
|
+
const installedRelativePath = 'node_modules/@expressivecss/expressive/package.json';
|
|
571
|
+
const installedText = await readText(path.join(root, installedRelativePath));
|
|
572
|
+
let installed = null;
|
|
573
|
+
if (installedText !== null) {
|
|
574
|
+
try {
|
|
575
|
+
installed = JSON.parse(installedText);
|
|
576
|
+
} catch {
|
|
577
|
+
installed = null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const installedVersion = installed?.name === PACKAGE_NAME ? cleanVersion(installed.version) : null;
|
|
581
|
+
if (installedText !== null && !installedVersion) {
|
|
582
|
+
resolutionSource = 'manifest-only';
|
|
583
|
+
diagnostics.push({
|
|
584
|
+
code: 'malformed-installed-metadata',
|
|
585
|
+
severity: 'blocked',
|
|
586
|
+
source: 'installed-package',
|
|
587
|
+
path: installedRelativePath,
|
|
588
|
+
message: 'Installed ExpressiveCSS package metadata has no valid package name and version.',
|
|
589
|
+
});
|
|
590
|
+
} else if (installedVersion) {
|
|
591
|
+
resolvedVersion = installedVersion;
|
|
592
|
+
resolutionSource = 'installed-package';
|
|
593
|
+
const installedCandidate = {
|
|
594
|
+
source: 'installed-package',
|
|
595
|
+
version: installedVersion,
|
|
596
|
+
path: installedRelativePath,
|
|
597
|
+
};
|
|
598
|
+
candidates.push(installedCandidate);
|
|
599
|
+
const installedSatisfiesRange = versionSatisfiesRange(installedVersion, declaredRange);
|
|
600
|
+
if (installedSatisfiesRange !== true) {
|
|
601
|
+
diagnostics.push({
|
|
602
|
+
code: installedSatisfiesRange === false
|
|
603
|
+
? 'installed-version-outside-manifest-range'
|
|
604
|
+
: 'unsupported-manifest-range',
|
|
605
|
+
severity: 'blocked',
|
|
606
|
+
source: 'installed-package',
|
|
607
|
+
path: installedRelativePath,
|
|
608
|
+
message: installedSatisfiesRange === false
|
|
609
|
+
? `Installed ExpressiveCSS ${installedVersion} does not satisfy the direct manifest range ${declaredRange}.`
|
|
610
|
+
: `Cannot verify installed ExpressiveCSS ${installedVersion} against unsupported manifest range ${declaredRange}.`,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (lock.version && lock.version !== installedVersion) {
|
|
614
|
+
const lockCandidate = { source: 'lockfile', version: lock.version, path: lock.file };
|
|
615
|
+
candidates.push(lockCandidate);
|
|
616
|
+
conflicts.push({
|
|
617
|
+
code: 'installed-lockfile-version-conflict',
|
|
618
|
+
selected: installedCandidate,
|
|
619
|
+
other: lockCandidate,
|
|
620
|
+
});
|
|
621
|
+
diagnostics.push({
|
|
622
|
+
code: 'installed-lockfile-version-conflict',
|
|
623
|
+
severity: 'blocked',
|
|
624
|
+
source: 'resolution',
|
|
625
|
+
path: lock.file,
|
|
626
|
+
message: `Installed ExpressiveCSS ${installedVersion} differs from ${lock.file} version ${lock.version}.`,
|
|
627
|
+
});
|
|
628
|
+
warnings.push(`Installed ExpressiveCSS ${installedVersion} differs from ${lock.file} version ${lock.version}.`);
|
|
629
|
+
}
|
|
630
|
+
} else if (lock.version) {
|
|
631
|
+
resolvedVersion = lock.version;
|
|
632
|
+
resolutionSource = 'lockfile';
|
|
633
|
+
candidates.push({ source: 'lockfile', version: lock.version, path: lock.file });
|
|
634
|
+
} else {
|
|
635
|
+
resolutionSource = 'manifest-only';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const contract = await bundledContract(contractManifestPath);
|
|
640
|
+
const bundledVersion = cleanVersion(contract?.frameworkVersion ?? '');
|
|
641
|
+
const normalizedContract = cleanVersion(contractVersion ?? contract?.frameworkVersion ?? '');
|
|
642
|
+
const normalizedSkillVersion = cleanVersion(skillVersion ?? contract?.skillVersion ?? '');
|
|
643
|
+
const hasBlockingDiagnostic = diagnostics.some((diagnostic) => diagnostic.severity === 'blocked');
|
|
644
|
+
const status = hasBlockingDiagnostic || !resolvedVersion || !normalizedContract
|
|
645
|
+
? 'unresolved'
|
|
646
|
+
: versionsHaveEqualPrecedence(resolvedVersion, normalizedContract)
|
|
647
|
+
? 'match'
|
|
648
|
+
: 'mismatch';
|
|
649
|
+
const matchingTag = resolvedVersion && Array.isArray(contract?.releaseTags)
|
|
650
|
+
&& contract.releaseTags.includes(`v${resolvedVersion}`)
|
|
651
|
+
? `v${resolvedVersion}`
|
|
652
|
+
: null;
|
|
653
|
+
const bundledContractSafe = status === 'match'
|
|
654
|
+
&& versionsHaveEqualPrecedence(normalizedContract, bundledVersion);
|
|
655
|
+
const documentationMode = bundledContractSafe
|
|
656
|
+
? 'bundled'
|
|
657
|
+
: matchingTag
|
|
658
|
+
? 'matching-tag'
|
|
659
|
+
: resolutionSource === 'installed-package'
|
|
660
|
+
? 'installed-package'
|
|
661
|
+
: 'unavailable';
|
|
662
|
+
const documentationSources = {
|
|
663
|
+
bundled: {
|
|
664
|
+
available: bundledContractSafe,
|
|
665
|
+
frameworkVersion: bundledVersion,
|
|
666
|
+
sourceHash: contract?.sourceHash ?? null,
|
|
667
|
+
},
|
|
668
|
+
current: {
|
|
669
|
+
// Local version agreement does not verify what the public site currently serves.
|
|
670
|
+
available: false,
|
|
671
|
+
url: 'https://www.expressivecss.com',
|
|
672
|
+
},
|
|
673
|
+
matchingTag: {
|
|
674
|
+
available: Boolean(matchingTag),
|
|
675
|
+
url: matchingTag ? `https://github.com/BaezFJ/ExpressiveCSS/tree/${matchingTag}` : null,
|
|
676
|
+
},
|
|
677
|
+
installedPackage: {
|
|
678
|
+
available: resolutionSource === 'installed-package',
|
|
679
|
+
path: resolutionSource === 'installed-package' ? 'node_modules/@expressivecss/expressive' : null,
|
|
680
|
+
componentDocumentationAvailable: false,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
diagnostics.sort((left, right) => `${left.code}\0${left.path ?? ''}\0${left.message}`
|
|
684
|
+
.localeCompare(`${right.code}\0${right.path ?? ''}\0${right.message}`));
|
|
685
|
+
warnings.sort();
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
projectRoot: root,
|
|
689
|
+
packageManager: lock.packageManager,
|
|
690
|
+
declaredRange,
|
|
691
|
+
declaredVersion: declaredRange,
|
|
692
|
+
resolvedVersion,
|
|
693
|
+
exactInstalledVersion: resolutionSource === 'installed-package' ? resolvedVersion : null,
|
|
694
|
+
resolutionSource,
|
|
695
|
+
skillVersion: normalizedSkillVersion,
|
|
696
|
+
skillContractVersion: normalizedContract,
|
|
697
|
+
contractVersion: normalizedContract,
|
|
698
|
+
contractSourceHash: contract?.sourceHash ?? null,
|
|
699
|
+
matchingTag,
|
|
700
|
+
matchingCommit: null,
|
|
701
|
+
status,
|
|
702
|
+
contractStatus: status,
|
|
703
|
+
documentationMode,
|
|
704
|
+
documentationSources,
|
|
705
|
+
bundledContractSafe,
|
|
706
|
+
currentDocsSafe: false,
|
|
707
|
+
warnings,
|
|
708
|
+
diagnostics,
|
|
709
|
+
candidates,
|
|
710
|
+
conflicts,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function cliArguments(argv) {
|
|
715
|
+
const values = {};
|
|
716
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
717
|
+
const argument = argv[index];
|
|
718
|
+
if (!argument.startsWith('--')) continue;
|
|
719
|
+
const [key, ...rest] = argument.slice(2).split('=');
|
|
720
|
+
if (rest.length) values[key] = rest.join('=');
|
|
721
|
+
else if (argv[index + 1] && !argv[index + 1].startsWith('--')) values[key] = argv[index += 1];
|
|
722
|
+
else values[key] = true;
|
|
723
|
+
}
|
|
724
|
+
return values;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
728
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
729
|
+
const args = cliArguments(process.argv.slice(2));
|
|
730
|
+
const result = await resolveExpressiveVersion({
|
|
731
|
+
projectRoot: typeof args['project-root'] === 'string' ? args['project-root'] : process.cwd(),
|
|
732
|
+
contractVersion: typeof args['contract-version'] === 'string' ? args['contract-version'] : null,
|
|
733
|
+
skillVersion: typeof args['skill-version'] === 'string' ? args['skill-version'] : null,
|
|
734
|
+
});
|
|
735
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
736
|
+
}
|