@conductor-by-89soone/tokens 0.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.
@@ -0,0 +1,348 @@
1
+ import { readFileSync, statSync, readdirSync } from 'fs';
2
+ import { join, relative, sep } from 'path';
3
+
4
+ // src/build/errors.ts
5
+ var EXIT_CODES = {
6
+ "TOK-CYCLE": 1,
7
+ "TOK-UNKNOWN-REF": 1,
8
+ "TOK-DEPTH": 1,
9
+ "TOK-TIER": 1,
10
+ "TOK-TIER-MISSING": 1,
11
+ "TOK-DUPLICATE-KEY": 1,
12
+ "TOK-ICON": 1,
13
+ "TOK-GROUP-SIZE": 1,
14
+ "TOK-USAGE": 1,
15
+ "TOK-VALUE": 1,
16
+ "TOK-Z-DUPLICATE": 1,
17
+ "TOK-PREFIX": 2,
18
+ "TOK-NAME-COLLISION": 2,
19
+ "TOK-MEDIA-VAR": 2,
20
+ "TOK-CP-DUPLICATE-ID": 1,
21
+ "TOK-CP-UNKNOWN-KEY": 1,
22
+ "TOK-CP-DECORATIVE": 1,
23
+ "TOK-CP-USAGE": 1,
24
+ "TOK-CP-COLOR": 1,
25
+ "TOK-CP-ALPHA": 1,
26
+ "TOK-LITERAL": 1,
27
+ "TOK-ARG": 3
28
+ };
29
+ var TokenBuildError = class extends Error {
30
+ code;
31
+ exitCode;
32
+ /** Indented lines printed under the summary: offending keys, cycle paths, hints. */
33
+ details;
34
+ constructor(code, summary, details = []) {
35
+ super(`error[${code}]: ${summary}`);
36
+ this.name = "TokenBuildError";
37
+ this.code = code;
38
+ this.exitCode = EXIT_CODES[code];
39
+ this.details = details;
40
+ }
41
+ /** The full stderr rendering, summary line first. */
42
+ format() {
43
+ return [this.message, ...this.details.map((line) => ` ${line}`)].join("\n");
44
+ }
45
+ };
46
+
47
+ // src/lint/source.ts
48
+ var ALLOW_DIRECTIVE = "cdt-allow-literal";
49
+ var ALLOW_PATTERN = new RegExp(`(?:/\\*|//)\\s*${ALLOW_DIRECTIVE}\\s*:\\s*([^*\\n]*?)\\s*(?:\\*/|$)`);
50
+ var BLOCK_COMMENT_END = "*/";
51
+ function blank(characters, start, end) {
52
+ for (let index = start; index < end; index += 1) {
53
+ if (characters[index] !== "\n") characters[index] = " ";
54
+ }
55
+ }
56
+ function maskComments(text) {
57
+ const characters = text.split("");
58
+ let index = 0;
59
+ let quote;
60
+ while (index < characters.length) {
61
+ const character = characters[index];
62
+ const next = characters[index + 1];
63
+ if (quote !== void 0) {
64
+ if (character === "\\") index += 2;
65
+ else {
66
+ if (character === quote) quote = void 0;
67
+ index += 1;
68
+ }
69
+ continue;
70
+ }
71
+ if (character === '"' || character === "'" || character === "`") {
72
+ quote = character;
73
+ index += 1;
74
+ continue;
75
+ }
76
+ if (character === "/" && next === "*") {
77
+ const end = text.indexOf(BLOCK_COMMENT_END, index + 2);
78
+ const stop = end === -1 ? characters.length : end + BLOCK_COMMENT_END.length;
79
+ blank(characters, index, stop);
80
+ index = stop;
81
+ continue;
82
+ }
83
+ if (character === "/" && next === "/") {
84
+ let end = index;
85
+ while (end < characters.length && characters[end] !== "\n") end += 1;
86
+ blank(characters, index, end);
87
+ index = end;
88
+ continue;
89
+ }
90
+ index += 1;
91
+ }
92
+ return characters.join("");
93
+ }
94
+ function headerEndLine(masked) {
95
+ const lines = masked.split("\n");
96
+ for (const [index, line] of lines.entries()) {
97
+ if (line.trim() !== "") return index;
98
+ }
99
+ return lines.length;
100
+ }
101
+ function findAllowances(file, text) {
102
+ const header = headerEndLine(maskComments(text));
103
+ return text.split("\n").flatMap((line, index) => {
104
+ const match = ALLOW_PATTERN.exec(line);
105
+ if (!match) return [];
106
+ const reason = match[1].trim();
107
+ return [
108
+ {
109
+ file,
110
+ line: index + 1,
111
+ scope: index < header ? "file" : "line",
112
+ reason: reason === "" ? "(no reason given)" : reason
113
+ }
114
+ ];
115
+ });
116
+ }
117
+ function isAllowed(allowances, line) {
118
+ return allowances.some(
119
+ (allowance) => allowance.scope === "file" || allowance.line === line || allowance.line === line - 1
120
+ );
121
+ }
122
+
123
+ // src/lint/rules.ts
124
+ var HEX_COLOR = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})\b/gi;
125
+ var FUNCTIONAL_COLOR = /\b(?:rgba?|hsla?)\([^)]*\)/gi;
126
+ var PX_LITERAL = /(?<![\w#.])(\d+(?:\.\d+)?)px\b/g;
127
+ var MS_LITERAL = /(?<![\w#.])(\d+(?:\.\d+)?)ms\b/g;
128
+ var Z_INDEX = /\b(?:z-index|zIndex)\s*:\s*["'`]?\s*(-?\d+)/g;
129
+ var FONT_SIZE_PX = /\b(?:font-size|fontSize)\s*:\s*["'`]?\s*(\d+(?:\.\d+)?px)/g;
130
+ var MEDIA_PRELUDE = /@media[^{;]*/g;
131
+ var DECLARATION_BLOCK = /\{[^{}]*\}/g;
132
+ var TEXT_FAINT = "--cdt-text-faint";
133
+ var SURFACE_ELEVATED = "--cdt-surface-elevated";
134
+ function matchesOf(masked, pattern, rule, message) {
135
+ return [...masked.matchAll(pattern)].map((match) => {
136
+ const whole = match[0];
137
+ return {
138
+ index: match.index,
139
+ length: whole.length,
140
+ rule,
141
+ message: message(match[1] ?? whole),
142
+ snippet: whole
143
+ };
144
+ });
145
+ }
146
+ function mediaRanges(masked) {
147
+ return [...masked.matchAll(MEDIA_PRELUDE)].map((match) => [
148
+ match.index,
149
+ match.index + match[0].length
150
+ ]);
151
+ }
152
+ var within = (ranges, index) => ranges.some(([start, end]) => index >= start && index < end);
153
+ function colorMatches(masked) {
154
+ return [
155
+ ...matchesOf(
156
+ masked,
157
+ FUNCTIONAL_COLOR,
158
+ "color-literal",
159
+ (captured) => `colour literal \`${captured}\``
160
+ ),
161
+ ...matchesOf(masked, HEX_COLOR, "color-literal", (captured) => `colour literal \`${captured}\``)
162
+ ];
163
+ }
164
+ function sizeMatches(masked) {
165
+ const fontSizes = matchesOf(
166
+ masked,
167
+ FONT_SIZE_PX,
168
+ "font-size-px",
169
+ (captured) => `\`font-size\` literal \`${captured}\`; use a \`--cdt-font-size-*\` token`
170
+ );
171
+ const covered = fontSizes.map((match) => [match.index, match.index + match.length]);
172
+ const media = mediaRanges(masked);
173
+ const pixels = matchesOf(
174
+ masked,
175
+ PX_LITERAL,
176
+ "px-literal",
177
+ (captured) => `px literal \`${captured}px\`; use a spacing, radius or size token`
178
+ ).filter((match) => !within(covered, match.index) && !within(media, match.index));
179
+ return [...fontSizes, ...pixels];
180
+ }
181
+ function faintOnElevatedMatches(masked) {
182
+ const violations = [];
183
+ for (const block of masked.matchAll(DECLARATION_BLOCK)) {
184
+ const body = block[0];
185
+ const declarations = body.split(/[;\n]/);
186
+ const paintsFaintText = declarations.some(
187
+ (declaration) => /(?:^|[\s{,"'`])(?:color|Color)\s*:/.test(declaration) && declaration.includes(TEXT_FAINT)
188
+ );
189
+ const paintsElevatedBackground = declarations.some(
190
+ (declaration) => /(?:^|[\s{,"'`])(?:background|background-color|backgroundColor)\s*:/.test(declaration) && declaration.includes(SURFACE_ELEVATED)
191
+ );
192
+ if (!paintsFaintText || !paintsElevatedBackground) continue;
193
+ const offset = body.indexOf(TEXT_FAINT);
194
+ violations.push({
195
+ index: block.index + offset,
196
+ length: TEXT_FAINT.length,
197
+ rule: "text-faint-on-elevated",
198
+ message: `\`${TEXT_FAINT}\` is painted on \`${SURFACE_ELEVATED}\` (2.94:1). \`text.faint\` is \`decorative\` and forbidden on that surface (FR-THM-005 AC-3)`,
199
+ snippet: TEXT_FAINT
200
+ });
201
+ }
202
+ return violations;
203
+ }
204
+ function zIndexMatches(masked) {
205
+ return matchesOf(
206
+ masked,
207
+ Z_INDEX,
208
+ "z-index-literal",
209
+ (captured) => `numeric \`z-index\` \`${captured}\`; use a \`--cdt-z-*\` token (FR-TOK-008 AC-2)`
210
+ );
211
+ }
212
+ function positionOf(text, index) {
213
+ const before = text.slice(0, index);
214
+ const line = before.split("\n").length;
215
+ const column = index - (before.lastIndexOf("\n") + 1) + 1;
216
+ return { line, column };
217
+ }
218
+ function lintSource(file, text) {
219
+ const masked = maskComments(text);
220
+ const matches = [
221
+ ...colorMatches(masked),
222
+ ...sizeMatches(masked),
223
+ ...matchesOf(
224
+ masked,
225
+ MS_LITERAL,
226
+ "ms-literal",
227
+ (captured) => `ms literal \`${captured}ms\`; use a \`--cdt-motion-*\` token`
228
+ ),
229
+ ...zIndexMatches(masked),
230
+ ...faintOnElevatedMatches(masked)
231
+ ];
232
+ return matches.map(({ index, rule, message, snippet }) => ({ ...positionOf(text, index), file, rule, message, snippet })).sort((one, other) => one.line - other.line || one.column - other.column || one.rule.localeCompare(other.rule));
233
+ }
234
+
235
+ // src/lint/index.ts
236
+ var LINTED_EXTENSIONS = [".css", ".ts", ".tsx", ".mts", ".cts"];
237
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git"]);
238
+ function isLintable(path) {
239
+ return LINTED_EXTENSIONS.some((extension) => path.endsWith(extension));
240
+ }
241
+ function walk(directory, found) {
242
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
243
+ (one, other) => one.name.localeCompare(other.name)
244
+ )) {
245
+ const path = join(directory, entry.name);
246
+ if (entry.isDirectory()) {
247
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) walk(path, found);
248
+ } else if (entry.isFile() && isLintable(path)) {
249
+ found.push(path);
250
+ }
251
+ }
252
+ }
253
+ function collectFiles(targets, root = process.cwd()) {
254
+ const found = [];
255
+ for (const target of targets) {
256
+ const absolute = join(root, target);
257
+ let stats;
258
+ try {
259
+ stats = statSync(absolute);
260
+ } catch {
261
+ throw new TokenBuildError("TOK-ARG", `lint target \`${target}\` does not exist`, [
262
+ `resolved to: ${absolute}`,
263
+ "hint: pass a path relative to the repository root."
264
+ ]);
265
+ }
266
+ if (stats.isDirectory()) walk(absolute, found);
267
+ else if (isLintable(absolute)) found.push(absolute);
268
+ }
269
+ return found.map((path) => relative(root, path).split(sep).join("/")).sort();
270
+ }
271
+ function lintTokens(targets, root = process.cwd()) {
272
+ const files = collectFiles(targets, root);
273
+ const violations = [];
274
+ const allowances = [];
275
+ for (const file of files) {
276
+ const text = readFileSync(join(root, file), "utf8");
277
+ const fileAllowances = findAllowances(file, text);
278
+ allowances.push(...fileAllowances);
279
+ violations.push(
280
+ ...lintSource(file, text).filter((violation) => !isAllowed(fileAllowances, violation.line))
281
+ );
282
+ }
283
+ return { files, violations, allowances };
284
+ }
285
+ function formatViolation(violation) {
286
+ const where = `${violation.file}:${violation.line}:${violation.column}`;
287
+ return `${where} ${violation.rule} ${violation.message}`;
288
+ }
289
+ function formatAllowances(allowances) {
290
+ const lines = [`[lint:tokens] ${allowances.length} allowance(s)`];
291
+ for (const allowance of allowances) {
292
+ lines.push(` ${allowance.file}:${allowance.line} scope=${allowance.scope} reason: ${allowance.reason}`);
293
+ }
294
+ return lines;
295
+ }
296
+ function assertNoViolations(result) {
297
+ if (result.violations.length === 0) return;
298
+ throw new TokenBuildError(
299
+ "TOK-LITERAL",
300
+ `${result.violations.length} hardcoded value(s) outside the token source`,
301
+ [
302
+ ...result.violations.map(formatViolation),
303
+ "hint: read the value from a `--cdt-` custom property, or annotate the line with `/* cdt-allow-literal: <reason> */`."
304
+ ]
305
+ );
306
+ }
307
+
308
+ // src/lint-cli.ts
309
+ var USAGE = "usage: conductor-lint-tokens <path...> [--report]";
310
+ function parseLintArgs(argv) {
311
+ const targets = [];
312
+ let report = false;
313
+ for (const arg of argv) {
314
+ if (arg === "--report") report = true;
315
+ else if (arg.startsWith("--")) throw new TokenBuildError("TOK-ARG", `unrecognised argument \`${arg}\``, [USAGE]);
316
+ else targets.push(arg);
317
+ }
318
+ if (targets.length === 0) {
319
+ throw new TokenBuildError("TOK-ARG", "no lint targets given", [USAGE]);
320
+ }
321
+ return { targets, report };
322
+ }
323
+ function runOnce(options) {
324
+ const result = lintTokens(options.targets);
325
+ if (options.report) {
326
+ for (const line of formatAllowances(result.allowances)) console.log(line);
327
+ }
328
+ console.log(
329
+ `[lint:tokens] scanned ${result.files.length} file(s), ${result.violations.length} violation(s), ${result.allowances.length} allowance(s)`
330
+ );
331
+ assertNoViolations(result);
332
+ return 0;
333
+ }
334
+ function main(argv) {
335
+ try {
336
+ return runOnce(parseLintArgs(argv));
337
+ } catch (error) {
338
+ if (error instanceof TokenBuildError) {
339
+ console.error(error.format());
340
+ return error.exitCode;
341
+ }
342
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
343
+ return 1;
344
+ }
345
+ }
346
+ process.exitCode = main(process.argv.slice(2));
347
+
348
+ export { parseLintArgs };