Skip to content

Commit bb4ea33

Browse files
authored
ci(lint): refactor & add type decl (catppuccin#335)
1 parent 1a92e07 commit bb4ea33

3 files changed

Lines changed: 274 additions & 64 deletions

File tree

scripts/lint/main.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { verifyMetadata } from "./metadata.ts";
1414
import { lint } from "./stylelint.ts";
1515

1616
const flags = parseFlags(Deno.args, { boolean: ["fix"] });
17-
const stylesheets = walk(join(REPO_ROOT, "styles"), {
17+
const subDir = flags._[0]?.toString() ?? "";
18+
const stylesheets = walk(join(REPO_ROOT, "styles", subDir), {
1819
includeFiles: true,
1920
includeDirs: false,
2021
includeSymlinks: false,
@@ -29,20 +30,7 @@ for await (const entry of stylesheets) {
2930
const content = await Deno.readTextFile(entry.path);
3031

3132
// verify the usercss metadata
32-
const { globalVars, isLess } = await verifyMetadata(entry, content, repo)
33-
.catch((e) => {
34-
const lines = content.split("\n");
35-
let startLine = -1;
36-
for (let i = 0; i < lines.length; i++) {
37-
const line = lines[i];
38-
if (e.index >= line.length) {
39-
e.index -= line.length;
40-
startLine++;
41-
} else break;
42-
}
43-
log(e.message, { file, startLine, content }, "error");
44-
throw e;
45-
});
33+
const { globalVars, isLess } = verifyMetadata(entry, content, repo);
4634
// don't attempt to compile or lint non-less files
4735
if (!isLess) continue;
4836

scripts/lint/metadata.ts

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
// TODO: remove this once types for usercss-meta are available
2-
// deno-lint-ignore-file no-explicit-any
3-
41
import chalk from "chalk";
2+
// @deno-types="../usercss-meta.d.ts";
53
import usercssMeta from "usercss-meta";
64
import { log } from "./logger.ts";
75
import { sprintf } from "std/fmt/printf.ts";
@@ -13,59 +11,60 @@ export const verifyMetadata = (
1311
entry: WalkEntry,
1412
content: string,
1513
repo: string,
16-
): Promise<{
17-
globalVars: Record<string, string>;
18-
isLess: boolean;
19-
}> => {
20-
return new Promise((resolve, reject) => {
21-
const assert = assertions(repo);
22-
const file = relative(REPO_ROOT, entry.path);
14+
) => {
15+
const assert = assertions(repo);
16+
const file = relative(REPO_ROOT, entry.path);
2317

24-
let metadata: Record<string, any> = {};
25-
try {
26-
metadata = usercssMeta.parse(content).metadata;
27-
} catch (err) {
28-
log(err, { file }, "error");
29-
reject(err);
30-
}
18+
const { metadata, errors: parsingErrors } = usercssMeta.parse(content, {
19+
allowErrors: true,
20+
});
3121

32-
Object.entries(assert).forEach(([k, v]) => {
33-
const defacto = metadata[k];
34-
if (defacto !== v) {
35-
const line = content
36-
.split("\n")
37-
.findIndex((line) => line.includes(k)) + 1;
22+
// pretty print / annotate the parsing errors
23+
parsingErrors.map((e) => {
24+
let startLine = 0;
25+
for (const line of content.split("\n")) {
26+
startLine++;
27+
e.index -= line.length + 1;
28+
if (e.index < 0) break;
29+
}
30+
log(e.message, { file, startLine, content });
31+
});
3832

39-
const message = sprintf(
40-
"Metadata %s should be %s but is %s",
41-
chalk.bold(k),
42-
chalk.green(v),
43-
chalk.red(defacto),
44-
);
33+
Object.entries(assert).forEach(([k, v]) => {
34+
const defacto = metadata[k];
35+
if (defacto !== v) {
36+
const line = content
37+
.split("\n")
38+
.findIndex((line) => line.includes(k)) + 1;
4539

46-
log(message, {
47-
file,
48-
startLine: line !== 0 ? line : undefined,
49-
content,
50-
}, "warning");
51-
}
52-
});
40+
const message = sprintf(
41+
"Metadata %s should be %s but is %s",
42+
chalk.bold(k),
43+
chalk.green(v),
44+
chalk.red(defacto),
45+
);
5346

54-
// parse the usercss variables to less global variables, e.g.
55-
// `@var select lightFlavor "Light Flavor" ["latte:Latte*", "frappe:Frappé", "macchiato:Macchiato", "mocha:Mocha"]`
56-
// gets parsed as
57-
// `lightFlavor: "latte"`
47+
log(message, {
48+
file,
49+
startLine: line !== 0 ? line : undefined,
50+
content,
51+
}, "warning");
52+
}
53+
});
5854

59-
const globalVars = Object.entries<{ default: string }>(metadata.vars)
60-
.reduce((acc, [k, v]) => {
61-
return { ...acc, [k]: v.default };
62-
}, {});
55+
// parse the usercss variables to less global variables, e.g.
56+
// `@var select lightFlavor "Light Flavor" ["latte:Latte*", "frappe:Frappé", "macchiato:Macchiato", "mocha:Mocha"]`
57+
// gets parsed as
58+
// `lightFlavor: "latte"`
59+
const globalVars = Object.entries(metadata.vars)
60+
.reduce((acc, [k, v]) => {
61+
return { ...acc, [k]: v.default };
62+
}, {});
6363

64-
resolve({
65-
globalVars,
66-
isLess: metadata.preprocessor === assert.preprocessor,
67-
});
68-
});
64+
return {
65+
globalVars,
66+
isLess: metadata.preprocessor === assert.preprocessor,
67+
};
6968
};
7069

7170
const assertions = (repo: string) => {

scripts/usercss-meta.d.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
declare namespace usercssMeta {
2+
export const ParseError: ParseError;
3+
4+
export interface ParseError extends Error {
5+
code:
6+
| "invalidCheckboxDefault"
7+
| "invalidRange"
8+
| "invalidRangeMultipleUnits"
9+
| "invalidRangeTooManyValues"
10+
| "invalidRangeValue"
11+
| "invalidRangeDefault"
12+
| "invalidRangeMin"
13+
| "invalidRangeMax"
14+
| "invalidRangeStep"
15+
| "invalidRangeUnits"
16+
| "invalidNumber"
17+
| "invalidSelect"
18+
| "invalidSelectValue"
19+
| "invalidSelectEmptyOptions"
20+
| "invalidSelectLabel"
21+
| "invalidSelectMultipleDefaults"
22+
| "invalidSelectNameDuplicated"
23+
| "invalidString"
24+
| "invalidURLProtocol"
25+
| "invalidVersion"
26+
| "invalidWord"
27+
| "missingChar"
28+
| "missingEOT"
29+
| "missingMandatory"
30+
| "missingValue"
31+
| "unknownJSONLiteral"
32+
| "unknownMeta"
33+
| "unknownVarType";
34+
35+
message: string;
36+
37+
/**
38+
* The string index where the error occurs
39+
*/
40+
index: number;
41+
42+
/**
43+
* An array of values that is used to compose the error message.
44+
* This allows other clients to generate i18n error message.
45+
*/
46+
args: unknown[];
47+
}
48+
49+
// TODO: export util types
50+
// export const util: {};
51+
52+
/**
53+
* This is a shortcut of `createParser(options).parse(text);`
54+
*/
55+
export function parse(
56+
content: string,
57+
options?: ParserOptions,
58+
): ParseResult;
59+
60+
/**
61+
* Create a metadata parser.
62+
*/
63+
export function createParser(options?: ParserOptions): Parser;
64+
65+
// TODO: export stringify types
66+
// export function stringify(
67+
// metadata: Metadata,
68+
// options: StringifierOptions,
69+
// ): string;
70+
// export function createStringifier(options: StringifierOptions): Stringifier;
71+
72+
type Parser = {
73+
/**
74+
* Parse the text (metadata header) and return the result.
75+
*/
76+
parse: typeof parse;
77+
78+
/**
79+
* Validate the value of the variable object.
80+
* This function uses the validators defined in `createParser`.
81+
*/
82+
validateVar: (varObj: VarObj) => void;
83+
};
84+
85+
type ParserOptions = {
86+
/**
87+
* `unknownKey` decides how to parse unknown keys. Possible values are:
88+
* - `ignore`: The directive is ignored. Default.
89+
* - `assign`: Assign the text value (characters before `\s*\n`) to result object.
90+
* - `throw`: Throw a `ParseError`.
91+
* @default "ignore"
92+
*/
93+
unknownKey?: "ignore" | "assign" | "throw";
94+
95+
/**
96+
* mandatoryKeys marks multiple keys as mandatory. If some keys are missing then throw a ParseError
97+
* @default ["name", "namespace", "version"]
98+
*/
99+
mandatoryKeys?: string[];
100+
101+
/**
102+
* A `key`/`parseFunction` map.
103+
* It allows users to extend the parser.
104+
*
105+
* @example
106+
* const parser = createParser({
107+
* mandatoryKeys: [],
108+
* parseKey: {
109+
* myKey: util.parseNumber
110+
* }
111+
* });
112+
* const {metadata} = parser.parse(`
113+
* /* ==UserStyle==
114+
* \@myKey 123456
115+
* ==/UserStyle==
116+
* `);
117+
* assert.equal(metadata.myKey, 123456);
118+
*/
119+
parseKey?: Record<string, unknown>;
120+
121+
/**
122+
* A `variableType`/`parseFunction` map.
123+
* It extends the parser to parse additional variable types.
124+
*
125+
* @example
126+
* const parser = createParser({
127+
* mandatoryKeys: [],
128+
* parseVar: {
129+
* myvar: util.parseNumber
130+
* }
131+
* });
132+
* const {metadata} = parser.parse(`/* ==UserStyle==
133+
* \@var myvar var-name 'Customized variable' 123456
134+
* ==/UserStyle== *\/`);
135+
* const va = metadata.vars['var-name'];
136+
* assert.equal(va.type, 'myvar');
137+
* assert.equal(va.label, 'Customized variable');
138+
* assert.equal(va.default, 123456);
139+
*/
140+
parseVar?: Record<string, unknown>;
141+
/**
142+
* A `key`/`validateFunction` map, which is used to validate the metadata value.
143+
* The function accepts a state object.
144+
*
145+
* @example
146+
* const parser = createParser({
147+
* validateKey: {
148+
* updateURL: state => {
149+
* if (/example\.com/.test(state.value)) {
150+
* throw new ParseError({
151+
* message: 'Example.com is not a good URL',
152+
* index: state.valueIndex
153+
* });
154+
* }
155+
* }
156+
* }
157+
* });
158+
*/
159+
validateKey?: Record<string, validateFn>;
160+
161+
/**
162+
* A `variableType`/`validateFunction` map, which is used to validate variables.
163+
* The function accepts a state object.
164+
*
165+
* @example
166+
* const parser = createParser({
167+
* validateVar: {
168+
* color: state => {
169+
* if (state.value === 'red') {
170+
* throw new ParseError({
171+
* message: '`red` is not allowed',
172+
* index: state.valueIndex
173+
* });
174+
* }
175+
* }
176+
* }
177+
* });
178+
*/
179+
validateVar?: Record<string, (state: StateObject) => void>;
180+
181+
/**
182+
* If allowErrors is true, the parser will collect parsing errors while
183+
* `parser.parse()` and return them as {@link ParseResult.errors}
184+
* Otherwise, the first parsing error will be thrown.
185+
* @default false
186+
*/
187+
allowErrors?: boolean;
188+
};
189+
190+
export type StateObject = {
191+
key: string;
192+
type: string;
193+
value: string;
194+
varResult: unknown;
195+
text: string;
196+
lastIndex: number;
197+
valueIndex: number;
198+
shouldIgnore: boolean;
199+
};
200+
201+
type validateFn = (state: StateObject) => void;
202+
203+
type VarObj = {
204+
label: string;
205+
name: string;
206+
value?: string;
207+
default?: string;
208+
options?: unknown;
209+
};
210+
type Metadata = {
211+
vars: VarObj[];
212+
[key: string]: unknown;
213+
};
214+
215+
type ParseResult = {
216+
metadata: Metadata;
217+
errors: ParseError[];
218+
};
219+
}
220+
221+
declare module "usercss-meta" {
222+
export = usercssMeta;
223+
}

0 commit comments

Comments
 (0)