@il4mb/css-tokenizer 1.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/index.test.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { Registry } from "@/registry";
2
+ import { Tokenizer } from "@/tokenizer";
3
+ import { readWhile } from "@/tools";
4
+
5
+ const registry = new Registry();
6
+ registry.add({
7
+ type: "var",
8
+ kind: "class",
9
+ regex: /^--[a-z][a-z0-9-_]+/i,
10
+ priority: 10,
11
+ reader({ index, content }) {
12
+ let nextIndex = readWhile(content, index + 2, /[a-z0-9-_]/);
13
+ return [index, nextIndex];
14
+ },
15
+ });
16
+
17
+ const tokenizer = new Tokenizer(registry);
18
+
19
+
20
+ const content = `--primary`;
21
+ const result = tokenizer.tokenize(content);
22
+
23
+ console.log(result.toArray());
24
+ console.log(JSON.stringify(result.toTokenTree(content), null, 2));
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@il4mb/css-tokenizer",
3
+ "version": "1.0.0",
4
+ "description": "Modular, extensible CSS tokenizer for parsing CSS-like strings into tuple lists and token trees.",
5
+ "keywords": [
6
+ "css",
7
+ "tokenizer",
8
+ "modular"
9
+ ],
10
+ "homepage": "https://github.com/il4mb/css-tokenizer#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/il4mb/css-tokenizer/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/il4mb/css-tokenizer.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "il4mb",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/index.ts"
23
+ },
24
+ "main": "index.test.ts",
25
+ "scripts": {
26
+ "test": "vitest index.test.ts"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^26.4.1",
30
+ "typescript": "7.0.2",
31
+ "vitest": "^5.0.0"
32
+ }
33
+ }
package/readme.md ADDED
@@ -0,0 +1,87 @@
1
+ # css-tokenizer
2
+
3
+ Modular, extensible CSS tokenizer for parsing CSS-like strings into tuple lists and token trees.
4
+
5
+ ## Get started
6
+
7
+ Install:
8
+
9
+ ```bash
10
+ npm install @il4mb/css-tokenizer
11
+ ```
12
+
13
+ ## Quick usage
14
+
15
+ ```ts
16
+ import { tokenize } from "@il4mb/css-tokenizer";
17
+
18
+ const content = "width: 10px";
19
+ const result = tokenize(content);
20
+
21
+ // Tuple list entries are [type, start, end]
22
+ console.log(result.toArray());
23
+
24
+ // Convert tuples to a structured token tree (useful for walking semantic tokens)
25
+ console.log(JSON.stringify(result.toTokenTree(content), null, 2));
26
+ ```
27
+
28
+ Example token tree (for the value `10px`):
29
+
30
+ ```json
31
+ [
32
+ {
33
+ "type": "dimension",
34
+ "start": 7,
35
+ "end": 11,
36
+ "value": "10px",
37
+ "children": [
38
+ { "type": "number", "start": 7, "end": 9, "value": "10" },
39
+ { "type": "word", "start": 9, "end": 11, "value": "px" }
40
+ ]
41
+ }
42
+ ]
43
+ ```
44
+
45
+ ## Custom token types
46
+
47
+ You can register custom token readers with a `Registry` and use a `Tokenizer` built from that registry.
48
+
49
+ ```ts
50
+ import { Registry, Tokenizer, readWhile } from "@il4mb/css-tokenizer";
51
+
52
+ const registry = new Registry();
53
+
54
+ registry.add({
55
+ type: "var", // token name
56
+ kind: "class", // category hint (class|char|keyword)
57
+ regex: /^--[a-z][a-z0-9-_]+/i,
58
+ priority: 10,
59
+ reader({ index, content }) {
60
+ // advance past the leading `--`
61
+ const nextIndex = readWhile(content, index + 2, /[a-z0-9-_]/i);
62
+ return [index, nextIndex];
63
+ },
64
+ });
65
+
66
+ const tokenizer = new Tokenizer(registry);
67
+ const content = `--primary`;
68
+ const result = tokenizer.tokenize(content);
69
+
70
+ console.log(result.toArray());
71
+ console.log(JSON.stringify(result.toTokenTree(content), null, 2));
72
+ ```
73
+
74
+ ## API (high level)
75
+
76
+ - `tokenize(content: string)` — tokenize using the default registry
77
+ - `Registry` — create and customize token definitions
78
+ - `Tokenizer` — tokenizer instance that uses a `Registry`
79
+ - helpers: `readWhile`, `findFunctionEnd` and others from the `tools` export
80
+
81
+ ## Contributing
82
+
83
+ PRs and issues welcome. Please add tests for new token readers.
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,20 @@
1
+ export declare global {
2
+
3
+ export type Exp = string | RegExp;
4
+
5
+ export interface TokenPlain {
6
+ id: string;
7
+ type: string;
8
+ value: string;
9
+ start: number;
10
+ end: number;
11
+ number?: number;
12
+ unit?: string;
13
+ children?: TokenPlain[]
14
+ }
15
+
16
+ export type Tuple = [type: number, start: number, end: number];
17
+ export type TRange = [start: number, end: number];
18
+ }
19
+
20
+ export { };
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /// <reference path="./global.d.ts" />
2
+
3
+ import { Registry } from "./registry";
4
+ import { Tokenizer } from "./tokenizer";
5
+
6
+ export * from "./type";
7
+
8
+ export { Registry } from "./registry";
9
+ export { Tokenizer } from "./tokenizer";
10
+ export * from "./tools";
11
+ export * from "./tupleList";
12
+
13
+
14
+ const tokenizer = new Tokenizer(new Registry());
15
+ export const tokenize = (content: string) => tokenizer.tokenize(content);
@@ -0,0 +1,174 @@
1
+ import { findFunctionEnd, readWhile } from "./tools";
2
+ import { ModelDefinition } from "./type";
3
+
4
+ const CSS_NAMED_COLORS = [
5
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
6
+ "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet",
7
+ "brown", "burlywood", "cadetblue", "chartreuse", "chocolate",
8
+ "coral", "cornflowerblue", "cornsilk", "crimson", "cyan",
9
+ "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgrey",
10
+ "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
11
+ "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
12
+ "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise",
13
+ "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey",
14
+ "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia",
15
+ "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "grey",
16
+ "green", "greenyellow", "honeydew", "hotpink", "indianred",
17
+ "indigo", "ivory", "khaki", "lavender", "lavenderblush",
18
+ "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan",
19
+ "lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen",
20
+ "lightpink", "lightsalmon", "lightseagreen", "lightskyblue",
21
+ "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow",
22
+ "lime", "limegreen", "linen", "magenta", "maroon",
23
+ "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
24
+ "mediumseagreen", "mediumslateblue", "mediumspringgreen",
25
+ "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream",
26
+ "mistyrose", "moccasin", "navajowhite", "navy", "oldlace",
27
+ "olive", "olivedrab", "orange", "orangered", "orchid",
28
+ "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
29
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
30
+ "purple", "rebeccapurple", "red", "rosybrown", "royalblue",
31
+ "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell",
32
+ "sienna", "silver", "skyblue", "slateblue", "slategray",
33
+ "slategrey", "snow", "springgreen", "steelblue", "tan", "teal",
34
+ "thistle", "tomato", "turquoise", "violet", "wheat", "white",
35
+ "whitesmoke", "yellow", "yellowgreen", "transparent", "currentcolor",
36
+ ];
37
+
38
+ const COLOR_FUNCTIONS = [
39
+ /^rgba?\(/i, /^hsla?\(/i, /^hwb\(/i, /^lab\(/i, /^lch\(/i,
40
+ /^oklab\(/i, /^oklch\(/i, /^color\(/i, /^color-mix\(/i
41
+ ];
42
+
43
+ const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i;
44
+
45
+
46
+
47
+ export class Registry implements Iterable<ModelDefinition> {
48
+
49
+ private items: ModelDefinition[] = [
50
+ {
51
+ type: "symbol",
52
+ kind: "char",
53
+ regex: /[\s\S]/,
54
+ priority: 0
55
+ },
56
+ {
57
+ type: "dimension",
58
+ kind: "keyword",
59
+ exp: [/^[+-]?[0-9]+[a-z]+/],
60
+ priority: 110,
61
+ reader({ index, content, deepReader }) {
62
+ let end = index;
63
+ if (content[end] === "-" || content[end] === "+") end++;
64
+ while (end < content.length && /[0-9]/.test(content[end])) end++;
65
+
66
+ if (content[end] === "." && /[0-9]/.test(content[end + 1] ?? "")) {
67
+ end++;
68
+ while (end < content.length && /[0-9]/.test(content[end])) end++;
69
+ }
70
+
71
+ while (end < content.length && /[a-zA-Z%]/.test(content[end])) end++;
72
+ return deepReader([index, end]);
73
+ }
74
+ },
75
+ {
76
+ type: "number",
77
+ kind: "class",
78
+ regex: /^[0-9]/,
79
+ priority: 5,
80
+ reader({ index, content }) {
81
+ let nextIndex = index;
82
+ while (/^[0-9]$/.test(content[nextIndex])) {
83
+ nextIndex++;
84
+ }
85
+ return [index, nextIndex];
86
+ }
87
+ },
88
+ {
89
+ type: "word",
90
+ kind: "class",
91
+ regex: /^[a-z]/i, // Constrained to single char match
92
+ priority: 5,
93
+ reader({ index, content }) {
94
+ let nextIndex = readWhile(content, index, /[a-z0-9_-]+/);
95
+ return [index, nextIndex];
96
+ },
97
+ },
98
+ {
99
+ type: "hash",
100
+ kind: "class",
101
+ regex: /^#[a-z0-9]+/i,
102
+ priority: 50,
103
+ reader({ index, content }) {
104
+ let nextIndex = index + 1;
105
+ while (nextIndex < content.length && /^[a-z0-9]+/i.test(content[nextIndex])) {
106
+ nextIndex++
107
+ }
108
+ return [index, nextIndex];
109
+ },
110
+ },
111
+ {
112
+ type: "color",
113
+ kind: "keyword",
114
+ priority: 120,
115
+ exp: [...CSS_NAMED_COLORS, HEX_COLOR, ...COLOR_FUNCTIONS],
116
+ reader({ index, content, matched, deepReader }) {
117
+ if (matched instanceof RegExp) {
118
+ const match = matched.exec(content.slice(index));
119
+ if (!match) return [index, index + 1];
120
+ const value = match[0];
121
+ if (value.endsWith('(')) {
122
+ const end = findFunctionEnd(content, index);
123
+ const fnName = value.slice(0, -1);
124
+ const innerStart = index + value.length;
125
+ const innerEnd = Math.max(innerStart, end);
126
+ return deepReader([index, end]);
127
+ }
128
+ }
129
+ const nextIndex = readWhile(content, index + 1, /[a-zA-Z0-9_-]/);
130
+ return [index, nextIndex];
131
+ },
132
+ }
133
+ ];
134
+
135
+ constructor(models: ModelDefinition[] = []) {
136
+ this.items.push(...models);
137
+ this.sort();
138
+ }
139
+
140
+ add(def: ModelDefinition) {
141
+ this.items.push({ priority: 0, ...def });
142
+ this.sort();
143
+ }
144
+
145
+ get(index: number) {
146
+ return this.items[index];
147
+ }
148
+
149
+ get length() {
150
+ return this.items.length;
151
+ }
152
+
153
+ [Symbol.iterator]() {
154
+ return this.items[Symbol.iterator]();
155
+ }
156
+
157
+ indexOf(item: ModelDefinition) {
158
+ return this.items.indexOf(item);
159
+ }
160
+
161
+ sort() {
162
+ this.items.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
163
+ }
164
+
165
+ sortExps(exps: Exp[]): Exp[] {
166
+ return [...exps].sort((a, b) => {
167
+ const aIsRegex = a instanceof RegExp;
168
+ const bIsRegex = b instanceof RegExp;
169
+ if (aIsRegex !== bIsRegex) return aIsRegex ? -1 : 1;
170
+ if (aIsRegex && bIsRegex) return 0;
171
+ return (b as string).length - (a as string).length;
172
+ });
173
+ }
174
+ }
@@ -0,0 +1,144 @@
1
+ import { Registry } from "./registry";
2
+ import { TupleList } from "./tupleList";
3
+ import { ModelDefinition } from "./type";
4
+
5
+ export class Tokenizer {
6
+
7
+ constructor(public registry: Registry) { }
8
+
9
+ getReader(def: ModelDefinition, match?: Exp) {
10
+ const type = this.registry.indexOf(def);
11
+ return ((content: string, index: number): Tuple[] => {
12
+ if (def.kind === "char") {
13
+ // char only single character so just + 1 for nextIndex
14
+ return [[type, index, index + 1]];
15
+ }
16
+
17
+ const deepReader = (range: TRange): Tuple[] => {
18
+ const tuples: Tuple[] = [[type, range[0], range[1]]];
19
+ const rangeContent = content.slice(range[0], range[1]);
20
+ const others = this.tokenize(rangeContent, { ignoreTypes: [type] })
21
+ .toArray()
22
+ .map(([type, start, end]) => [type, start + range[0], end + range[0]] as Tuple);
23
+
24
+ tuples.push(...others);
25
+ return tuples;
26
+ }
27
+
28
+ if (def.kind === "class") {
29
+ if ('reader' in def && def.reader) {
30
+ let ranges = def.reader({ content, index, deepReader });
31
+ if (ranges.length > 0 && Array.isArray(ranges[0])) {
32
+ return ranges.map((range: any) => range.length === 2 ? [type, ...range] : range) as Tuple[];
33
+ }
34
+ return [[type, ...(ranges as TRange)]];
35
+ }
36
+
37
+ const start = index;
38
+ let nextIndex = start + 1;
39
+
40
+ // Added length check and optional chaining to prevent infinite loops and crashes
41
+ while (nextIndex < content.length) {
42
+ const nextMatch = this.findMatch(content, nextIndex);
43
+ if (nextMatch && type === nextMatch[0]) {
44
+ nextIndex++;
45
+ } else {
46
+ break;
47
+ }
48
+ }
49
+
50
+ return [[type, start, nextIndex]];
51
+ }
52
+
53
+ // Keyword fallback check (in case reader is undefined)
54
+ if (!def.reader) {
55
+ const length = typeof match === "string" ? match.length : 1;
56
+ return [[type, index, index + length]];
57
+ }
58
+
59
+ const ranges = def.reader({ content, index, matched: match, deepReader });
60
+ if (ranges.length > 0 && Array.isArray(ranges[0])) {
61
+ return ranges.map((range: any) => range.length === 2 ? [type, ...range] : range) as Tuple[];
62
+ }
63
+ return [[type, ...(ranges as TRange)]];
64
+ });
65
+ }
66
+
67
+ findMatch(content: string, index: number, ignoreTypes?: number[]): [number, (c: string, i: number) => Tuple[]] | undefined {
68
+ for (let i = 0; i < this.registry.length; i++) {
69
+ // Bypass ignored rules so fallback tokens get a chance
70
+ if (ignoreTypes && ignoreTypes.includes(i)) {
71
+ continue;
72
+ }
73
+
74
+ const match = this.tryMatch(this.registry.get(i), content, index);
75
+ if (match) return [i, match];
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ tryMatch(def: ModelDefinition, content: string, index: number) {
81
+ // Guard against out-of-bounds indexing
82
+ if (index >= content.length) return false;
83
+
84
+ if (def.kind === "keyword") {
85
+ const right = content.slice(index);
86
+ const matched = this.registry.sortExps(def.exp).find(exp =>
87
+ exp instanceof RegExp ? exp.test(right) : right.startsWith(exp as string)
88
+ );
89
+
90
+ return matched ? this.getReader(def, matched) : false;
91
+ }
92
+
93
+ if (def.kind === "class") {
94
+ if (!def.regex.test(content.slice(index))) return false;
95
+ return this.getReader(def);
96
+ }
97
+
98
+ if (def.kind === "char") {
99
+ if ('regex' in def) {
100
+ if (!def.regex.test(content.slice(index))) return false;
101
+ return this.getReader(def);
102
+ }
103
+ return def.char === content[index] ? this.getReader(def) : false;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ tokenize(content: string, options?: { ignoreTypes?: number[] }) {
109
+ const tupleList = new TupleList(this.registry);
110
+ let i = 0;
111
+
112
+ while (i < content.length) {
113
+ let hit = this.findMatch(content, i, options?.ignoreTypes);
114
+ if (hit) {
115
+ const results = hit[1](content, i);
116
+ if (results && results.length > 0) {
117
+ const nextIndex = Math.max(i, ...results.map(tuple => tuple[2]));
118
+ if (nextIndex <= i) {
119
+ tupleList.push([-1, i, i + 1]);
120
+ i++;
121
+ continue;
122
+ }
123
+
124
+ tupleList.push(...results);
125
+ i = nextIndex;
126
+ continue;
127
+ }
128
+ }
129
+
130
+ tupleList.push([-1, i, i + 1]);
131
+ i++;
132
+ }
133
+
134
+ return tupleList;
135
+ }
136
+
137
+ isRange(data: any): data is TRange {
138
+ return Array.isArray(data) && data.length === 2 && data.every(t => typeof t === "number");
139
+ }
140
+
141
+ isTuple(data: any): data is Tuple {
142
+ return Array.isArray(data) && data.length === 3 && data.every(t => typeof t === "number");
143
+ }
144
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,37 @@
1
+ export function findFunctionEnd(source: string, index: number): number {
2
+ let depth = 0;
3
+ let cursor = index;
4
+ let inString = false;
5
+ let stringChar = '';
6
+
7
+ while (cursor < source.length) {
8
+ const char = source[cursor];
9
+
10
+ // Handle strings
11
+ if (!inString && (char === '"' || char === "'")) {
12
+ inString = true;
13
+ stringChar = char;
14
+ } else if (inString && char === stringChar) {
15
+ inString = false;
16
+ }
17
+
18
+ // Only count parentheses outside of strings
19
+ if (!inString) {
20
+ if (char === '(') depth++;
21
+ else if (char === ')') {
22
+ depth--;
23
+ if (depth === 0) return cursor + 1;
24
+ }
25
+ }
26
+
27
+ cursor++;
28
+ }
29
+ return source.length;
30
+ }
31
+
32
+
33
+ export const readWhile = (content: string, index: number, test: RegExp) => {
34
+ let nextIndex = index;
35
+ while (nextIndex < content.length && test.test(content[nextIndex])) nextIndex++;
36
+ return nextIndex;
37
+ }
@@ -0,0 +1,73 @@
1
+ import { Registry } from "./registry";
2
+
3
+ export type TokenTree = {
4
+ type: string;
5
+ start: number;
6
+ end: number
7
+ value: string;
8
+ children?: TokenTree[]
9
+ }
10
+
11
+ export class TupleList {
12
+ protected items: Tuple[] = [];
13
+
14
+ constructor(protected registry: Registry) { }
15
+
16
+ push(...items: Tuple[]) {
17
+ items.forEach(item => {
18
+ this.items.push(item);
19
+ });
20
+ }
21
+
22
+ toArray() {
23
+ return [...this.items];
24
+ }
25
+
26
+ toTokenList(content: string): TokenTree[] {
27
+ return this.items.map(([type, start, end]) => {
28
+ const model = this.registry.get(type);
29
+ return {
30
+ type: model?.type ?? "unknown",
31
+ start,
32
+ end,
33
+ value: content.slice(start, end),
34
+ };
35
+ });
36
+ }
37
+
38
+ toTokenTree(content: string): TokenTree[] {
39
+ const tokens = this.toTokenList(content);
40
+
41
+ tokens.sort((a, b) => {
42
+ if (a.start !== b.start) {
43
+ return a.start - b.start;
44
+ }
45
+ return b.end - a.end;
46
+ });
47
+
48
+ const roots: TokenTree[] = [];
49
+ const stack: TokenTree[] = [];
50
+
51
+ for (const token of tokens) {
52
+ /*
53
+ * FIXED: Robust parent exit check.
54
+ * Pop parents whose physical range strictly ends before or exactly where the new token starts.
55
+ * (e.g. `parent.end <= token.start`)
56
+ */
57
+ while (stack.length > 0 && stack[stack.length - 1].end <= token.start) {
58
+ stack.pop();
59
+ }
60
+ const parent = stack[stack.length - 1];
61
+
62
+ if (parent) {
63
+ if (!parent.children) parent.children = [];
64
+ parent.children.push(token);
65
+ } else {
66
+ roots.push(token);
67
+ }
68
+ stack.push(token);
69
+ }
70
+
71
+ return roots;
72
+ }
73
+ }
package/src/type.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ export type ReaderContext = {
2
+ content: string;
3
+ index: number;
4
+ deepReader: (ranges: TRange) => Tuple[]
5
+ }
6
+
7
+
8
+
9
+ export interface CharModel {
10
+ kind: "char";
11
+ char: string;
12
+ }
13
+ export interface CharRegexModel {
14
+ kind: "char";
15
+ regex: RegExp;
16
+ }
17
+
18
+ export interface KeywordModel {
19
+ kind: "keyword";
20
+ exp: Exp[];
21
+ reader: (ctx: ReaderContext & { matched: Exp }) => TRange[] | TRange | Tuple[];
22
+ }
23
+
24
+ export interface ClassModel {
25
+ kind: "class";
26
+ regex: RegExp;
27
+ reader?: (ctx: ReaderContext) => TRange[] | TRange | Tuple[];
28
+ }
29
+
30
+ export type Model = CharModel | CharRegexModel | KeywordModel | ClassModel;
31
+
32
+ export type ModelDefinition<T extends Model = Model> = {
33
+ type: string;
34
+ priority?: number;
35
+ } & T;
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": false,
8
+ "incremental": true,
9
+ "module": "esnext",
10
+ "esModuleInterop": true,
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "paths": {
16
+ "@/*": ["./src/*"]
17
+ }
18
+ },
19
+ "include": ["**/*.ts", "src/**/*.ts", "src/**/*.tsx", "src/global.d.ts"],
20
+ "exclude": ["node_modules"]
21
+ }