@design.estate/wcctools 6.0.0 → 6.1.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/changelog.md +13 -0
- package/dist_shell/bundle.js +3 -3
- package/dist_shell/bundle.js.map +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/cli.js +8 -2
- package/dist_ts/plugins.d.ts +7 -2
- package/dist_ts/plugins.js +9 -3
- package/dist_ts/standards/baselines.d.ts +27 -0
- package/dist_ts/standards/baselines.js +83 -0
- package/dist_ts/standards/checker.d.ts +21 -0
- package/dist_ts/standards/checker.js +88 -0
- package/dist_ts/standards/classes.gitrepo.d.ts +13 -0
- package/dist_ts/standards/classes.gitrepo.js +43 -0
- package/dist_ts/standards/classes.sourceedits.d.ts +15 -0
- package/dist_ts/standards/classes.sourceedits.js +54 -0
- package/dist_ts/standards/classes.standardfixer.d.ts +40 -0
- package/dist_ts/standards/classes.standardfixer.js +210 -0
- package/dist_ts/standards/classes.standardproject.d.ts +80 -0
- package/dist_ts/standards/classes.standardproject.js +195 -0
- package/dist_ts/standards/classes.standardtree.d.ts +56 -0
- package/dist_ts/standards/classes.standardtree.js +207 -0
- package/dist_ts/standards/cli.d.ts +7 -0
- package/dist_ts/standards/cli.js +89 -0
- package/dist_ts/standards/config.d.ts +33 -0
- package/dist_ts/standards/config.js +139 -0
- package/dist_ts/standards/errors.d.ts +9 -0
- package/dist_ts/standards/errors.js +17 -0
- package/dist_ts/standards/helpers.ast.d.ts +56 -0
- package/dist_ts/standards/helpers.ast.js +172 -0
- package/dist_ts/standards/helpers.paths.d.ts +36 -0
- package/dist_ts/standards/helpers.paths.js +86 -0
- package/dist_ts/standards/index.d.ts +14 -0
- package/dist_ts/standards/index.js +15 -0
- package/dist_ts/standards/output.d.ts +7 -0
- package/dist_ts/standards/output.js +57 -0
- package/dist_ts/standards/ratchet.d.ts +15 -0
- package/dist_ts/standards/ratchet.js +45 -0
- package/dist_ts/standards/rule.d.ts +50 -0
- package/dist_ts/standards/rule.js +9 -0
- package/dist_ts/standards/rules/barrel.element-index.d.ts +7 -0
- package/dist_ts/standards/rules/barrel.element-index.js +108 -0
- package/dist_ts/standards/rules/barrel.no-demo-export.d.ts +3 -0
- package/dist_ts/standards/rules/barrel.no-demo-export.js +29 -0
- package/dist_ts/standards/rules/demo.fixture-not-exported.d.ts +3 -0
- package/dist_ts/standards/rules/demo.fixture-not-exported.js +29 -0
- package/dist_ts/standards/rules/demo.groups.d.ts +6 -0
- package/dist_ts/standards/rules/demo.groups.js +72 -0
- package/dist_ts/standards/rules/demo.required.d.ts +3 -0
- package/dist_ts/standards/rules/demo.required.js +48 -0
- package/dist_ts/standards/rules/demo.separate-file.d.ts +3 -0
- package/dist_ts/standards/rules/demo.separate-file.js +38 -0
- package/dist_ts/standards/rules/demo.shape.d.ts +3 -0
- package/dist_ts/standards/rules/demo.shape.js +38 -0
- package/dist_ts/standards/rules/docs.component-index.d.ts +8 -0
- package/dist_ts/standards/rules/docs.component-index.js +84 -0
- package/dist_ts/standards/rules/element.exported.d.ts +3 -0
- package/dist_ts/standards/rules/element.exported.js +33 -0
- package/dist_ts/standards/rules/element.literal-tag.d.ts +3 -0
- package/dist_ts/standards/rules/element.literal-tag.js +30 -0
- package/dist_ts/standards/rules/element.reachable.d.ts +3 -0
- package/dist_ts/standards/rules/element.reachable.js +48 -0
- package/dist_ts/standards/rules/element.tag-map.d.ts +3 -0
- package/dist_ts/standards/rules/element.tag-map.js +83 -0
- package/dist_ts/standards/rules/i18n.literals.d.ts +3 -0
- package/dist_ts/standards/rules/i18n.literals.js +52 -0
- package/dist_ts/standards/rules/index.d.ts +12 -0
- package/dist_ts/standards/rules/index.js +83 -0
- package/dist_ts/standards/rules/layout.element-folder.d.ts +3 -0
- package/dist_ts/standards/rules/layout.element-folder.js +47 -0
- package/dist_ts/standards/rules/layout.entry-file-name.d.ts +3 -0
- package/dist_ts/standards/rules/layout.entry-file-name.js +71 -0
- package/dist_ts/standards/rules/layout.entry.d.ts +3 -0
- package/dist_ts/standards/rules/layout.entry.js +38 -0
- package/dist_ts/standards/rules/layout.group-folder.d.ts +3 -0
- package/dist_ts/standards/rules/layout.group-folder.js +22 -0
- package/dist_ts/standards/rules/layout.shared-00.d.ts +3 -0
- package/dist_ts/standards/rules/layout.shared-00.js +36 -0
- package/dist_ts/standards/rules/naming.class-matches-tag.d.ts +3 -0
- package/dist_ts/standards/rules/naming.class-matches-tag.js +26 -0
- package/dist_ts/standards/rules/naming.lowercase-files.d.ts +3 -0
- package/dist_ts/standards/rules/naming.lowercase-files.js +26 -0
- package/dist_ts/standards/rules/naming.tag-prefix.d.ts +3 -0
- package/dist_ts/standards/rules/naming.tag-prefix.js +23 -0
- package/dist_ts/standards/rules/props.accessor.d.ts +3 -0
- package/dist_ts/standards/rules/props.accessor.js +35 -0
- package/dist_ts/standards/rules/props.object-no-attribute.d.ts +3 -0
- package/dist_ts/standards/rules/props.object-no-attribute.js +42 -0
- package/dist_ts/standards/rules/props.reflect-primitive.d.ts +5 -0
- package/dist_ts/standards/rules/props.reflect-primitive.js +41 -0
- package/dist_ts/standards/rules/props.typed.d.ts +6 -0
- package/dist_ts/standards/rules/props.typed.js +64 -0
- package/dist_ts/standards/rules/shared.d.ts +38 -0
- package/dist_ts/standards/rules/shared.js +103 -0
- package/dist_ts/standards/rules/style.bdtheme.d.ts +5 -0
- package/dist_ts/standards/rules/style.bdtheme.js +28 -0
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/standard.d.ts +93 -0
- package/dist_ts_interfaces/standard.js +6 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +4 -2
- package/readme.md +34 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cli.ts +8 -1
- package/ts/plugins.ts +10 -2
- package/ts/standards/baselines.ts +100 -0
- package/ts/standards/checker.ts +114 -0
- package/ts/standards/classes.gitrepo.ts +48 -0
- package/ts/standards/classes.sourceedits.ts +58 -0
- package/ts/standards/classes.standardfixer.ts +234 -0
- package/ts/standards/classes.standardproject.ts +250 -0
- package/ts/standards/classes.standardtree.ts +220 -0
- package/ts/standards/cli.ts +86 -0
- package/ts/standards/config.ts +160 -0
- package/ts/standards/errors.ts +18 -0
- package/ts/standards/helpers.ast.ts +208 -0
- package/ts/standards/helpers.paths.ts +119 -0
- package/ts/standards/index.ts +22 -0
- package/ts/standards/output.ts +65 -0
- package/ts/standards/ratchet.ts +63 -0
- package/ts/standards/rule.ts +58 -0
- package/ts/standards/rules/barrel.element-index.ts +112 -0
- package/ts/standards/rules/barrel.no-demo-export.ts +30 -0
- package/ts/standards/rules/demo.fixture-not-exported.ts +30 -0
- package/ts/standards/rules/demo.groups.ts +74 -0
- package/ts/standards/rules/demo.required.ts +49 -0
- package/ts/standards/rules/demo.separate-file.ts +40 -0
- package/ts/standards/rules/demo.shape.ts +42 -0
- package/ts/standards/rules/docs.component-index.ts +88 -0
- package/ts/standards/rules/element.exported.ts +35 -0
- package/ts/standards/rules/element.literal-tag.ts +30 -0
- package/ts/standards/rules/element.reachable.ts +49 -0
- package/ts/standards/rules/element.tag-map.ts +87 -0
- package/ts/standards/rules/i18n.literals.ts +58 -0
- package/ts/standards/rules/index.ts +87 -0
- package/ts/standards/rules/layout.element-folder.ts +49 -0
- package/ts/standards/rules/layout.entry-file-name.ts +73 -0
- package/ts/standards/rules/layout.entry.ts +40 -0
- package/ts/standards/rules/layout.group-folder.ts +23 -0
- package/ts/standards/rules/layout.shared-00.ts +37 -0
- package/ts/standards/rules/naming.class-matches-tag.ts +27 -0
- package/ts/standards/rules/naming.lowercase-files.ts +27 -0
- package/ts/standards/rules/naming.tag-prefix.ts +24 -0
- package/ts/standards/rules/props.accessor.ts +37 -0
- package/ts/standards/rules/props.object-no-attribute.ts +45 -0
- package/ts/standards/rules/props.reflect-primitive.ts +44 -0
- package/ts/standards/rules/props.typed.ts +70 -0
- package/ts/standards/rules/shared.ts +127 -0
- package/ts/standards/rules/style.bdtheme.ts +31 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
const ts = plugins.typescript;
|
|
4
|
+
|
|
5
|
+
export type TNode = plugins.typescript.Node;
|
|
6
|
+
export type TSourceFile = plugins.typescript.SourceFile;
|
|
7
|
+
export type TClassDeclaration = plugins.typescript.ClassDeclaration;
|
|
8
|
+
export type TClassElement = plugins.typescript.ClassElement;
|
|
9
|
+
export type TPropertyDeclaration = plugins.typescript.PropertyDeclaration;
|
|
10
|
+
export type TObjectLiteralExpression = plugins.typescript.ObjectLiteralExpression;
|
|
11
|
+
export type TCallExpression = plugins.typescript.CallExpression;
|
|
12
|
+
export type TExpression = plugins.typescript.Expression;
|
|
13
|
+
export type TTypeNode = plugins.typescript.TypeNode;
|
|
14
|
+
export type TStringLiteralLike = plugins.typescript.StringLiteralLike;
|
|
15
|
+
|
|
16
|
+
export const parseSource = (fileArg: string, textArg: string): TSourceFile =>
|
|
17
|
+
ts.createSourceFile(fileArg, textArg, ts.ScriptTarget.Latest, true);
|
|
18
|
+
|
|
19
|
+
/** 1-based line and column of a position. */
|
|
20
|
+
export const lineAndColumn = (sourceFileArg: TSourceFile, positionArg: number): { line: number; column: number } => {
|
|
21
|
+
const location = sourceFileArg.getLineAndCharacterOfPosition(positionArg);
|
|
22
|
+
return { line: location.line + 1, column: location.character + 1 };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** The called name of `name(...)` or `a.b.name(...)`. */
|
|
26
|
+
export const calleeName = (expressionArg: TExpression): string | null => {
|
|
27
|
+
if (ts.isIdentifier(expressionArg)) {
|
|
28
|
+
return expressionArg.text;
|
|
29
|
+
}
|
|
30
|
+
if (ts.isPropertyAccessExpression(expressionArg)) {
|
|
31
|
+
return expressionArg.name.text;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The decorator call named `nameArg` on a node, e.g. `@customElement('x')`. */
|
|
37
|
+
export const findDecoratorCall = (nodeArg: TNode, namesArg: readonly string[]): { name: string; call: TCallExpression } | null => {
|
|
38
|
+
if (!ts.canHaveDecorators(nodeArg)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
for (const decorator of ts.getDecorators(nodeArg) ?? []) {
|
|
42
|
+
if (ts.isCallExpression(decorator.expression)) {
|
|
43
|
+
const name = calleeName(decorator.expression.expression);
|
|
44
|
+
if (name && namesArg.includes(name)) {
|
|
45
|
+
return { name, call: decorator.expression };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const hasModifier = (nodeArg: TNode, kindArg: plugins.typescript.SyntaxKind): boolean =>
|
|
53
|
+
ts.canHaveModifiers(nodeArg) && (ts.getModifiers(nodeArg) ?? []).some((modifierArg) => modifierArg.kind === kindArg);
|
|
54
|
+
|
|
55
|
+
/** The text of a member name: identifier, private identifier (with '#'), string or numeric literal. */
|
|
56
|
+
export const memberNameText = (memberArg: TClassElement): string | null => {
|
|
57
|
+
const name = memberArg.name;
|
|
58
|
+
if (!name) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
62
|
+
return name.text;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isPrivateIdentifier(name)) {
|
|
65
|
+
return name.text;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const isStatic = (memberArg: TNode): boolean => hasModifier(memberArg, ts.SyntaxKind.StaticKeyword);
|
|
71
|
+
|
|
72
|
+
/** The static property declaration `static <name> = …` of a class. */
|
|
73
|
+
export const findStaticProperty = (classArg: TClassDeclaration, nameArg: string): TPropertyDeclaration | null => {
|
|
74
|
+
for (const member of classArg.members) {
|
|
75
|
+
if (ts.isPropertyDeclaration(member) && isStatic(member) && memberNameText(member) === nameArg) {
|
|
76
|
+
return member;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const isStringLiteralLike = (nodeArg: TNode | undefined): nodeArg is TStringLiteralLike =>
|
|
83
|
+
!!nodeArg && (ts.isStringLiteral(nodeArg) || ts.isNoSubstitutionTemplateLiteral(nodeArg));
|
|
84
|
+
|
|
85
|
+
/** A property assignment `key: value` of an object literal, by key. */
|
|
86
|
+
export const objectProperty = (objectArg: TObjectLiteralExpression, keyArg: string): plugins.typescript.PropertyAssignment | null => {
|
|
87
|
+
for (const property of objectArg.properties) {
|
|
88
|
+
if (ts.isPropertyAssignment(property)) {
|
|
89
|
+
const name = property.name;
|
|
90
|
+
if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === keyArg) {
|
|
91
|
+
return property;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The source text of an object literal property's value, or null. */
|
|
99
|
+
export const objectPropertyText = (objectArg: TObjectLiteralExpression, keyArg: string): string | null =>
|
|
100
|
+
objectProperty(objectArg, keyArg)?.initializer.getText() ?? null;
|
|
101
|
+
|
|
102
|
+
/** The indentation (leading whitespace) of the line a position is on. */
|
|
103
|
+
export const indentationAt = (textArg: string, positionArg: number): string => {
|
|
104
|
+
const lineStart = textArg.lastIndexOf('\n', positionArg - 1) + 1;
|
|
105
|
+
return /^[ \t]*/.exec(textArg.slice(lineStart))![0];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export interface ITextEdit {
|
|
109
|
+
start: number;
|
|
110
|
+
end: number;
|
|
111
|
+
text: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The edit that adds `propertyTextArg` (e.g. `reflect: true`) as the last property of an object
|
|
116
|
+
* literal, following the literal's own layout: inline literals stay on one line, multi-line
|
|
117
|
+
* literals get a new line with the indentation of their last property.
|
|
118
|
+
*/
|
|
119
|
+
export const appendObjectPropertyEdit = (objectArg: TObjectLiteralExpression, propertyTextArg: string): ITextEdit => {
|
|
120
|
+
const sourceText = objectArg.getSourceFile().text;
|
|
121
|
+
const properties = objectArg.properties;
|
|
122
|
+
if (properties.length === 0) {
|
|
123
|
+
return { start: objectArg.getStart(), end: objectArg.end, text: `{ ${propertyTextArg} }` };
|
|
124
|
+
}
|
|
125
|
+
const last = properties[properties.length - 1];
|
|
126
|
+
const closeBrace = objectArg.end - 1;
|
|
127
|
+
const between = sourceText.slice(last.end, closeBrace);
|
|
128
|
+
const multiLine = sourceText.slice(objectArg.getStart(), objectArg.end).includes('\n');
|
|
129
|
+
if (multiLine) {
|
|
130
|
+
const indentation = indentationAt(sourceText, last.getStart());
|
|
131
|
+
const commaIndex = between.indexOf(',');
|
|
132
|
+
if (commaIndex !== -1) {
|
|
133
|
+
const at = last.end + commaIndex + 1;
|
|
134
|
+
return { start: at, end: at, text: `\n${indentation}${propertyTextArg},` };
|
|
135
|
+
}
|
|
136
|
+
return { start: last.end, end: last.end, text: `,\n${indentation}${propertyTextArg}` };
|
|
137
|
+
}
|
|
138
|
+
return { start: last.end, end: last.end, text: `, ${propertyTextArg}` };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** The edit that adds `propertyTextArg` (e.g. `type: String`) as the first property of an object literal. */
|
|
142
|
+
export const prependObjectPropertyEdit = (objectArg: TObjectLiteralExpression, propertyTextArg: string): ITextEdit => {
|
|
143
|
+
const sourceText = objectArg.getSourceFile().text;
|
|
144
|
+
const properties = objectArg.properties;
|
|
145
|
+
if (properties.length === 0) {
|
|
146
|
+
return { start: objectArg.getStart(), end: objectArg.end, text: `{ ${propertyTextArg} }` };
|
|
147
|
+
}
|
|
148
|
+
const first = properties[0];
|
|
149
|
+
const multiLine = sourceText.slice(objectArg.getStart(), objectArg.end).includes('\n');
|
|
150
|
+
const start = first.getStart();
|
|
151
|
+
if (multiLine && sourceText.slice(objectArg.getStart() + 1, start).includes('\n')) {
|
|
152
|
+
return { start, end: start, text: `${propertyTextArg},\n${indentationAt(sourceText, start)}` };
|
|
153
|
+
}
|
|
154
|
+
return { start, end: start, text: `${propertyTextArg}, ` };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** The position after the last import declaration, at the start of the following line; 0 without imports. */
|
|
158
|
+
export const afterLastImport = (sourceFileArg: TSourceFile): number => {
|
|
159
|
+
let position = 0;
|
|
160
|
+
for (const statement of sourceFileArg.statements) {
|
|
161
|
+
if (ts.isImportDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) {
|
|
162
|
+
position = statement.end;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (position === 0) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
const newline = sourceFileArg.text.indexOf('\n', position);
|
|
169
|
+
return newline === -1 ? sourceFileArg.text.length : newline + 1;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** The module an identifier is imported from in a file, e.g. `html` → `lit`. */
|
|
173
|
+
export const importSourceOf = (sourceFileArg: TSourceFile, identifierArg: string): string | null => {
|
|
174
|
+
for (const statement of sourceFileArg.statements) {
|
|
175
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const clause = statement.importClause;
|
|
179
|
+
if (clause.name?.text === identifierArg) {
|
|
180
|
+
return statement.moduleSpecifier.text;
|
|
181
|
+
}
|
|
182
|
+
const bindings = clause.namedBindings;
|
|
183
|
+
if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === identifierArg) {
|
|
184
|
+
return statement.moduleSpecifier.text;
|
|
185
|
+
}
|
|
186
|
+
if (bindings && ts.isNamedImports(bindings) && bindings.elements.some((elementArg) => elementArg.name.text === identifierArg)) {
|
|
187
|
+
return statement.moduleSpecifier.text;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Every relative-capable module specifier literal of a file: imports, re-exports, dynamic `import()` and `import('…')` types. */
|
|
194
|
+
export const moduleSpecifierLiterals = (sourceFileArg: TSourceFile): TStringLiteralLike[] => {
|
|
195
|
+
const found: TStringLiteralLike[] = [];
|
|
196
|
+
const visit = (nodeArg: TNode): void => {
|
|
197
|
+
if ((ts.isImportDeclaration(nodeArg) || ts.isExportDeclaration(nodeArg)) && nodeArg.moduleSpecifier && isStringLiteralLike(nodeArg.moduleSpecifier)) {
|
|
198
|
+
found.push(nodeArg.moduleSpecifier);
|
|
199
|
+
} else if (ts.isCallExpression(nodeArg) && nodeArg.expression.kind === ts.SyntaxKind.ImportKeyword && isStringLiteralLike(nodeArg.arguments[0])) {
|
|
200
|
+
found.push(nodeArg.arguments[0]);
|
|
201
|
+
} else if (ts.isImportTypeNode(nodeArg) && ts.isLiteralTypeNode(nodeArg.argument) && isStringLiteralLike(nodeArg.argument.literal)) {
|
|
202
|
+
found.push(nodeArg.argument.literal);
|
|
203
|
+
}
|
|
204
|
+
ts.forEachChild(nodeArg, visit);
|
|
205
|
+
};
|
|
206
|
+
visit(sourceFileArg);
|
|
207
|
+
return found;
|
|
208
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repository paths in the standard are relative to the repository root with '/' separators.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const posix = plugins.path.posix;
|
|
8
|
+
|
|
9
|
+
export const toPosix = (pathArg: string): string => pathArg.split(plugins.path.sep).join('/');
|
|
10
|
+
|
|
11
|
+
export const dirOf = (fileArg: string): string => posix.dirname(fileArg);
|
|
12
|
+
|
|
13
|
+
export const baseOf = (fileArg: string): string => posix.basename(fileArg);
|
|
14
|
+
|
|
15
|
+
export const joinPath = (...partsArg: string[]): string => posix.normalize(posix.join(...partsArg));
|
|
16
|
+
|
|
17
|
+
/** A demo file: `<name>.demo.ts`, `demo.ts`, `<name>.demodata.ts` (and similar), or any file under `ts_web/demos/`. */
|
|
18
|
+
export const isDemoFile = (fileArg: string): boolean =>
|
|
19
|
+
/(^|[./-])demo(data)?\.ts$/.test(baseOf(fileArg)) || fileArg.startsWith('ts_web/demos/');
|
|
20
|
+
|
|
21
|
+
export const isTypeScriptSource = (fileArg: string): boolean =>
|
|
22
|
+
/\.(ts|tsx|mts|cts)$/.test(fileArg) && !/\.d\.(ts|mts|cts)$/.test(fileArg);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* True for a path the fixer may write: relative to the repository root, '/' separators, already
|
|
26
|
+
* normalised, and without '.' or '..' segments, so it cannot leave the root lexically.
|
|
27
|
+
*/
|
|
28
|
+
export const isRepoPath = (pathArg: string): boolean =>
|
|
29
|
+
pathArg.length > 0
|
|
30
|
+
&& !pathArg.includes('\\')
|
|
31
|
+
&& !pathArg.includes('\0')
|
|
32
|
+
&& posix.normalize(pathArg) === pathArg
|
|
33
|
+
&& pathArg.split('/').every((segmentArg) => segmentArg !== '' && segmentArg !== '.' && segmentArg !== '..');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How a relative module specifier spells its target: `exact` names the file itself (`./x.ts`,
|
|
37
|
+
* `./data.json`, `./x.d.ts`), `js` names a TypeScript file by its emitted extension (`./x.js` for
|
|
38
|
+
* `x.ts` or `x.d.ts`), `none` leaves the extension out (`./x`, or `./folder` for its index file).
|
|
39
|
+
*/
|
|
40
|
+
type TSpecifierStyle = 'exact' | 'js' | 'none';
|
|
41
|
+
|
|
42
|
+
export interface IResolvedSpecifier {
|
|
43
|
+
/** The resolved repository path. */
|
|
44
|
+
target: string;
|
|
45
|
+
style: TSpecifierStyle;
|
|
46
|
+
/** True when the specifier names a folder and resolved to its index file. */
|
|
47
|
+
viaFolder: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The files an emitted extension stands for, in NodeNext resolution order. */
|
|
51
|
+
const sourcesOfEmittedExtension: Record<string, string[]> = {
|
|
52
|
+
'.js': ['.ts', '.tsx', '.d.ts'],
|
|
53
|
+
'.jsx': ['.tsx'],
|
|
54
|
+
'.mjs': ['.mts', '.d.mts'],
|
|
55
|
+
'.cjs': ['.cts', '.d.cts'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** The files an extensionless specifier stands for, in resolution order. */
|
|
59
|
+
const extensionlessSuffixes = ['.ts', '.tsx', '.d.ts'];
|
|
60
|
+
const folderIndexSuffixes = ['/index.ts', '/index.tsx', '/index.d.ts'];
|
|
61
|
+
|
|
62
|
+
const typeScriptExtension = /(\.d)?\.(ts|tsx|mts|cts)$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a relative module specifier against the importing file the way NodeNext resolution
|
|
66
|
+
* does: an emitted `.js` specifier maps back to its `.ts`, `.tsx` or `.d.ts` source, any other
|
|
67
|
+
* specifier that names a known file exactly resolves to it (`.ts`, `.json`, `.css`, `.d.ts`, …), and
|
|
68
|
+
* an extensionless one to a source or a folder's index file. Returns null for package specifiers
|
|
69
|
+
* and for targets that are not among the known files.
|
|
70
|
+
*/
|
|
71
|
+
export const resolveSpecifier = (
|
|
72
|
+
fromFileArg: string,
|
|
73
|
+
specifierArg: string,
|
|
74
|
+
existsArg: (fileArg: string) => boolean,
|
|
75
|
+
): IResolvedSpecifier | null => {
|
|
76
|
+
if (!specifierArg.startsWith('./') && !specifierArg.startsWith('../')) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const raw = joinPath(dirOf(fromFileArg), specifierArg);
|
|
80
|
+
const extension = posix.extname(raw);
|
|
81
|
+
for (const sourceExtension of sourcesOfEmittedExtension[extension] ?? []) {
|
|
82
|
+
const target = raw.slice(0, -extension.length) + sourceExtension;
|
|
83
|
+
if (existsArg(target)) {
|
|
84
|
+
return { target, style: 'js', viaFolder: false };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (existsArg(raw)) {
|
|
88
|
+
return { target: raw, style: 'exact', viaFolder: false };
|
|
89
|
+
}
|
|
90
|
+
for (const suffix of extensionlessSuffixes) {
|
|
91
|
+
if (existsArg(`${raw}${suffix}`)) {
|
|
92
|
+
return { target: `${raw}${suffix}`, style: 'none', viaFolder: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const suffix of folderIndexSuffixes) {
|
|
96
|
+
if (existsArg(`${raw}${suffix}`)) {
|
|
97
|
+
return { target: `${raw}${suffix}`, style: 'none', viaFolder: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** The relative specifier from a file to a target, spelled in the given style. */
|
|
104
|
+
export const specifierFor = (fromFileArg: string, targetArg: string, styleArg: TSpecifierStyle, viaFolderArg = false): string => {
|
|
105
|
+
let target = targetArg;
|
|
106
|
+
if (styleArg === 'none' && viaFolderArg && /^index(\.d)?\.(ts|tsx)$/.test(baseOf(target))) {
|
|
107
|
+
target = dirOf(target);
|
|
108
|
+
} else if (styleArg === 'none') {
|
|
109
|
+
target = target.replace(typeScriptExtension, '');
|
|
110
|
+
} else if (styleArg === 'js') {
|
|
111
|
+
const emitted: Record<string, string> = { ts: '.js', tsx: '.jsx', mts: '.mjs', cts: '.cjs' };
|
|
112
|
+
target = target.replace(typeScriptExtension, (_matchArg, _declarationArg, extensionArg: string) => emitted[extensionArg]);
|
|
113
|
+
}
|
|
114
|
+
let relative = posix.relative(dirOf(fromFileArg), target);
|
|
115
|
+
if (!relative.startsWith('.')) {
|
|
116
|
+
relative = `./${relative}`;
|
|
117
|
+
}
|
|
118
|
+
return relative;
|
|
119
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { initialBaselines, rekeyStandardConfig, updatedBaselines, type IBaselineRaise } from './baselines.js';
|
|
2
|
+
export { checkStandard, describeStandardRules, detectFindings, rulesOfProfile, type IDetectedFinding } from './checker.js';
|
|
3
|
+
export { GitRepo } from './classes.gitrepo.js';
|
|
4
|
+
export { SourceEdits } from './classes.sourceedits.js';
|
|
5
|
+
export { StandardFixer, type IStandardFixOptions } from './classes.standardfixer.js';
|
|
6
|
+
export { StandardProject, loadStandardProject, type IStandardElement, type IStandardProperty } from './classes.standardproject.js';
|
|
7
|
+
export { FixBatch, StandardTree } from './classes.standardtree.js';
|
|
8
|
+
export {
|
|
9
|
+
SmartconfigStandardConfigWriter,
|
|
10
|
+
openStandardConfig,
|
|
11
|
+
validateStandardConfig,
|
|
12
|
+
wcctoolsConfigKey,
|
|
13
|
+
type ILoadedStandardConfig,
|
|
14
|
+
type IOpenedStandardConfig,
|
|
15
|
+
type IStandardConfigWriter,
|
|
16
|
+
} from './config.js';
|
|
17
|
+
export { StandardConfigError, StandardUsageError } from './errors.js';
|
|
18
|
+
export { isDemoFile, resolveSpecifier, specifierFor } from './helpers.paths.js';
|
|
19
|
+
export { registerStandardCommands } from './cli.js';
|
|
20
|
+
export { renderFixResult, renderStandardReport } from './output.js';
|
|
21
|
+
export { initBaselines, updateBaselines, type IBaselineRunResult } from './ratchet.js';
|
|
22
|
+
export { allowlistStaleRuleId, fixOrder, standardRules } from './rules/index.js';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
type IStandardReport = plugins.interfaces.IStandardReport;
|
|
4
|
+
type IStandardFinding = plugins.interfaces.IStandardFinding;
|
|
5
|
+
|
|
6
|
+
const isShown = (findingArg: IStandardFinding, verboseArg: boolean): boolean =>
|
|
7
|
+
verboseArg || (!findingArg.covered && findingArg.severity !== 'info');
|
|
8
|
+
|
|
9
|
+
/** The text form of a check report: findings grouped by file, then a summary. */
|
|
10
|
+
export const renderStandardReport = (reportArg: IStandardReport, verboseArg: boolean): string[] => {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
let currentFile: string | null = null;
|
|
13
|
+
for (const finding of reportArg.findings.filter((findingArg) => isShown(findingArg, verboseArg))) {
|
|
14
|
+
if (finding.file !== currentFile) {
|
|
15
|
+
if (currentFile !== null) {
|
|
16
|
+
lines.push('');
|
|
17
|
+
}
|
|
18
|
+
lines.push(finding.file);
|
|
19
|
+
currentFile = finding.file;
|
|
20
|
+
}
|
|
21
|
+
const notes = [finding.fixable ? 'fixable' : '', finding.covered ? `covered by the ${finding.covered}` : ''].filter(Boolean);
|
|
22
|
+
lines.push(` ${finding.line}:${finding.column} ${finding.severity} ${finding.ruleId} ${finding.message}${notes.length ? ` (${notes.join(', ')})` : ''}`);
|
|
23
|
+
}
|
|
24
|
+
if (verboseArg && reportArg.baselineSlack.length > 0) {
|
|
25
|
+
lines.push('', 'Below their baseline:');
|
|
26
|
+
for (const slack of reportArg.baselineSlack) {
|
|
27
|
+
lines.push(` ${slack.ruleId} ${slack.file} ${slack.baseline} → ${slack.current}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const count = (predicateArg: (findingArg: IStandardFinding) => boolean): number => reportArg.findings.filter(predicateArg).length;
|
|
31
|
+
const failing = count((findingArg) => findingArg.severity === 'error' && !findingArg.covered);
|
|
32
|
+
const warnings = count((findingArg) => findingArg.severity === 'warn' && !findingArg.covered);
|
|
33
|
+
const infos = count((findingArg) => findingArg.severity === 'info' && !findingArg.covered);
|
|
34
|
+
const allowlisted = count((findingArg) => findingArg.covered === 'allowlist');
|
|
35
|
+
const baselined = count((findingArg) => findingArg.covered === 'baseline');
|
|
36
|
+
if (lines.length > 0) {
|
|
37
|
+
lines.push('');
|
|
38
|
+
}
|
|
39
|
+
lines.push(
|
|
40
|
+
`wcctools check (${reportArg.profile}): ${failing} error(s), ${warnings} warning(s), ${infos} info; `
|
|
41
|
+
+ `${allowlisted} allowlisted, ${baselined} within baselines, ${reportArg.baselineSlack.length} file(s) below their baseline — ${reportArg.ok ? 'ok' : 'failed'}`,
|
|
42
|
+
);
|
|
43
|
+
return lines;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The text form of a fix result: the changes, then the check that remains. */
|
|
47
|
+
export const renderFixResult = (resultArg: plugins.interfaces.IStandardFixResult, verboseArg: boolean): string[] => {
|
|
48
|
+
const { plan } = resultArg;
|
|
49
|
+
const lines: string[] = [resultArg.dryRun ? 'wcctools fix --dry-run would make these changes:' : 'wcctools fix made these changes:'];
|
|
50
|
+
for (const move of plan.moves) {
|
|
51
|
+
lines.push(` git mv ${move.from} ${move.to}`);
|
|
52
|
+
}
|
|
53
|
+
for (const file of plan.created) {
|
|
54
|
+
lines.push(` create ${file}`);
|
|
55
|
+
}
|
|
56
|
+
for (const edited of plan.edited) {
|
|
57
|
+
lines.push(` edit ${edited.file} (${edited.ruleIds.join(', ')})`);
|
|
58
|
+
}
|
|
59
|
+
if (plan.moves.length + plan.created.length + plan.edited.length === 0) {
|
|
60
|
+
lines.push(' nothing to fix');
|
|
61
|
+
}
|
|
62
|
+
lines.push('', resultArg.dryRun ? 'Remaining after the fix:' : 'Remaining:');
|
|
63
|
+
lines.push(...renderStandardReport(resultArg.remaining, verboseArg));
|
|
64
|
+
return lines;
|
|
65
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { initialBaselines, updatedBaselines, type IBaselineRaise } from './baselines.js';
|
|
3
|
+
import { checkStandard } from './checker.js';
|
|
4
|
+
import { loadStandardProject } from './classes.standardproject.js';
|
|
5
|
+
import type { IOpenedStandardConfig } from './config.js';
|
|
6
|
+
import { StandardUsageError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
type IStandardConfig = plugins.interfaces.IStandardConfig;
|
|
9
|
+
|
|
10
|
+
export interface IBaselineRunResult {
|
|
11
|
+
/** The check with the configuration as it is after the run. */
|
|
12
|
+
report: plugins.interfaces.IStandardReport;
|
|
13
|
+
/** Files per rule written to the baselines; 0 when nothing was written. */
|
|
14
|
+
writtenFiles: number;
|
|
15
|
+
/** For --update-baseline: counts that would have to grow; the update was refused when not empty. */
|
|
16
|
+
raised: IBaselineRaise[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hasBaselines = (configArg: IStandardConfig): boolean =>
|
|
20
|
+
Object.values(configArg.baselines ?? {}).some((countsArg) => Object.keys(countsArg).length > 0);
|
|
21
|
+
|
|
22
|
+
const withBaselines = (configArg: IStandardConfig, baselinesArg: NonNullable<IStandardConfig['baselines']>): IStandardConfig => {
|
|
23
|
+
const config: IStandardConfig = { ...configArg };
|
|
24
|
+
delete config.baselines;
|
|
25
|
+
return Object.keys(baselinesArg).length > 0 ? { ...config, baselines: baselinesArg } : config;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const countFiles = (baselinesArg: NonNullable<IStandardConfig['baselines']>): number =>
|
|
29
|
+
Object.values(baselinesArg).reduce((sumArg, countsArg) => sumArg + Object.keys(countsArg).length, 0);
|
|
30
|
+
|
|
31
|
+
/** `wcctools check --init-baseline`: records the current error counts; only while no baseline exists. */
|
|
32
|
+
export const initBaselines = async (rootDirArg: string, openedArg: IOpenedStandardConfig): Promise<IBaselineRunResult> => {
|
|
33
|
+
const { loaded, writer } = openedArg;
|
|
34
|
+
if (hasBaselines(loaded.config)) {
|
|
35
|
+
throw new StandardUsageError('standard.baselines already exist; lower them with --update-baseline');
|
|
36
|
+
}
|
|
37
|
+
const project = await loadStandardProject(rootDirArg);
|
|
38
|
+
const baselines = initialBaselines(checkStandard(project, loaded));
|
|
39
|
+
const config = withBaselines(loaded.config, baselines);
|
|
40
|
+
if (countFiles(baselines) > 0) {
|
|
41
|
+
await writer.writeStandardConfig(config);
|
|
42
|
+
}
|
|
43
|
+
return { report: checkStandard(project, { ...loaded, config }), writtenFiles: countFiles(baselines), raised: [] };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** `wcctools check --update-baseline`: lowers counts to the current ones, removes zeros, refuses to raise any. */
|
|
47
|
+
export const updateBaselines = async (rootDirArg: string, openedArg: IOpenedStandardConfig): Promise<IBaselineRunResult> => {
|
|
48
|
+
const { loaded, writer } = openedArg;
|
|
49
|
+
if (!hasBaselines(loaded.config)) {
|
|
50
|
+
throw new StandardUsageError('standard.baselines do not exist; record them with --init-baseline');
|
|
51
|
+
}
|
|
52
|
+
const project = await loadStandardProject(rootDirArg);
|
|
53
|
+
const report = checkStandard(project, loaded);
|
|
54
|
+
const { baselines, raised } = updatedBaselines(report, loaded.config.baselines!);
|
|
55
|
+
if (raised.length > 0) {
|
|
56
|
+
return { report, writtenFiles: 0, raised };
|
|
57
|
+
}
|
|
58
|
+
const config = withBaselines(loaded.config, baselines);
|
|
59
|
+
if (JSON.stringify(config) !== JSON.stringify(loaded.config)) {
|
|
60
|
+
await writer.writeStandardConfig(config);
|
|
61
|
+
}
|
|
62
|
+
return { report: checkStandard(project, { ...loaded, config }), writtenFiles: countFiles(baselines), raised };
|
|
63
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { StandardProject } from './classes.standardproject.js';
|
|
3
|
+
|
|
4
|
+
type TStandardProfile = plugins.interfaces.TStandardProfile;
|
|
5
|
+
type TStandardSeverity = plugins.interfaces.TStandardSeverity;
|
|
6
|
+
|
|
7
|
+
/** Collects the changes of one fix run of one rule; the fixer applies them to its in-memory tree. */
|
|
8
|
+
export interface IFixBuilder {
|
|
9
|
+
/** Replaces the span [start, end) of a file's current text. Identical edits are applied once. */
|
|
10
|
+
edit(fileArg: string, startArg: number, endArg: number, textArg: string): void;
|
|
11
|
+
/** Creates a file; skipped when the path exists. */
|
|
12
|
+
create(fileArg: string, textArg: string): void;
|
|
13
|
+
/** Moves a file (`git mv`) and rewrites every relative module specifier that resolves to it. */
|
|
14
|
+
move(fromArg: string, toArg: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* In this run's moves, `export … from` declarations of barrels (index.ts) that resolve to `targetArg`
|
|
17
|
+
* point at `barrelTargetArg` instead, e.g. at the new element folder's index.ts.
|
|
18
|
+
*/
|
|
19
|
+
redirectBarrelExports(targetArg: string, barrelTargetArg: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TFixAction = (builderArg: IFixBuilder) => void;
|
|
23
|
+
|
|
24
|
+
export interface IRuleFinding {
|
|
25
|
+
file: string;
|
|
26
|
+
/** Offset in the file's text; 0 for file-level findings and for files that do not exist. */
|
|
27
|
+
position: number;
|
|
28
|
+
message: string;
|
|
29
|
+
/** Allowlist key: '<file>#<member>' or '<file>'. */
|
|
30
|
+
key: string;
|
|
31
|
+
/** Set when `wcctools fix` resolves the finding. */
|
|
32
|
+
fix?: TFixAction;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IRuleContext {
|
|
36
|
+
project: StandardProject;
|
|
37
|
+
config: plugins.interfaces.IStandardConfig;
|
|
38
|
+
profile: TStandardProfile;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One rule of docs/standard.md. */
|
|
42
|
+
export interface IStandardRule {
|
|
43
|
+
id: string;
|
|
44
|
+
/** Severity per profile; a rule without a `base` severity belongs to `dees` only. `dees` falls back to `base`. */
|
|
45
|
+
severity: { base?: TStandardSeverity; dees?: TStandardSeverity };
|
|
46
|
+
fixMode: plugins.interfaces.TStandardFixMode;
|
|
47
|
+
/** Categories an allowlist entry may use; absent when the rule takes no allowlist. */
|
|
48
|
+
allowlistCategories?: readonly string[];
|
|
49
|
+
detect(contextArg: IRuleContext): IRuleFinding[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The rule's severity in a profile, or null when the rule is not part of it. */
|
|
53
|
+
export const severityIn = (ruleArg: IStandardRule, profileArg: TStandardProfile): TStandardSeverity | null => {
|
|
54
|
+
if (profileArg === 'dees') {
|
|
55
|
+
return ruleArg.severity.dees ?? ruleArg.severity.base ?? null;
|
|
56
|
+
}
|
|
57
|
+
return ruleArg.severity.base ?? null;
|
|
58
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { baseOf, dirOf, specifierFor } from '../helpers.paths.js';
|
|
2
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
3
|
+
import { addReexport, catalogElements, elementsDir, isInElementFolder } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An element folder has `index.ts` re-exporting the entry file (and `types.ts`); a group folder's
|
|
7
|
+
* `index.ts` re-exports each element folder's `index.ts`; `ts_web/elements/index.ts` re-exports each
|
|
8
|
+
* group or element folder.
|
|
9
|
+
*/
|
|
10
|
+
export const barrelElementIndexRule: IStandardRule = {
|
|
11
|
+
id: 'barrel/element-index',
|
|
12
|
+
severity: { base: 'warn' },
|
|
13
|
+
fixMode: 'auto',
|
|
14
|
+
detect: ({ project }) => {
|
|
15
|
+
const findings = new Map<string, IRuleFinding>();
|
|
16
|
+
const add = (findingArg: IRuleFinding): void => {
|
|
17
|
+
const id = `${findingArg.file}|${findingArg.message}`;
|
|
18
|
+
if (!findings.has(id)) {
|
|
19
|
+
findings.set(id, findingArg);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const folderElements = catalogElements(project).filter(isInElementFolder);
|
|
23
|
+
const folders = [...new Set(folderElements.map((elementArg) => dirOf(elementArg.file)))].sort();
|
|
24
|
+
const groupsWithFolders = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const element of folderElements) {
|
|
27
|
+
const folder = dirOf(element.file);
|
|
28
|
+
const index = `${folder}/index.ts`;
|
|
29
|
+
const types = `${folder}/types.ts`;
|
|
30
|
+
if (!project.files.has(index)) {
|
|
31
|
+
const exported = [element.file, ...(project.files.has(types) ? [types] : [])];
|
|
32
|
+
add({
|
|
33
|
+
file: index,
|
|
34
|
+
position: 0,
|
|
35
|
+
key: index,
|
|
36
|
+
message: `${folder}/ has no index.ts`,
|
|
37
|
+
fix: (builderArg) => builderArg.create(index, exported.map((fileArg) => `export * from '${specifierFor(index, fileArg, 'js')}';\n`).join('')),
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const required of [element.file, types]) {
|
|
42
|
+
if (!project.files.has(required) || project.reexports(index).some((reexportArg) => reexportArg.target === required)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
add({
|
|
46
|
+
file: index,
|
|
47
|
+
position: 0,
|
|
48
|
+
key: index,
|
|
49
|
+
message: `${index} does not re-export ./${baseOf(required)}`,
|
|
50
|
+
fix: (builderArg) => addReexport(builderArg, project, index, required),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parent = dirOf(folder);
|
|
55
|
+
const parentIndex = `${parent}/index.ts`;
|
|
56
|
+
if (parent !== elementsDir) {
|
|
57
|
+
groupsWithFolders.add(parent);
|
|
58
|
+
}
|
|
59
|
+
if (!project.files.has(parentIndex)) {
|
|
60
|
+
if (parent === elementsDir) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const groupFolders = folders.filter((folderArg) => dirOf(folderArg) === parent);
|
|
64
|
+
add({
|
|
65
|
+
file: parentIndex,
|
|
66
|
+
position: 0,
|
|
67
|
+
key: parentIndex,
|
|
68
|
+
message: `${parent}/ has no index.ts`,
|
|
69
|
+
fix: (builderArg) => builderArg.create(parentIndex, groupFolders.map((folderArg) =>
|
|
70
|
+
`export * from '${specifierFor(parentIndex, `${folderArg}/index.ts`, 'js')}';\n`).join('')),
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const parentReexports = project.reexports(parentIndex);
|
|
75
|
+
if (parentReexports.some((reexportArg) => reexportArg.target === index)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const inside = parentReexports.filter((reexportArg) => reexportArg.target?.startsWith(`${folder}/`));
|
|
79
|
+
const finding: IRuleFinding = {
|
|
80
|
+
file: parentIndex,
|
|
81
|
+
position: inside[0]?.statement.getStart() ?? 0,
|
|
82
|
+
key: parentIndex,
|
|
83
|
+
message: `${parentIndex} does not re-export ${baseOf(folder)}/index.ts`,
|
|
84
|
+
};
|
|
85
|
+
if (inside.length === 0) {
|
|
86
|
+
finding.fix = (builderArg) => addReexport(builderArg, project, parentIndex, index);
|
|
87
|
+
} else if (inside.length === 1) {
|
|
88
|
+
const literal = inside[0].statement.moduleSpecifier!;
|
|
89
|
+
finding.fix = (builderArg) => builderArg.edit(parentIndex, literal.getStart() + 1, literal.end - 1, specifierFor(parentIndex, index, 'js'));
|
|
90
|
+
}
|
|
91
|
+
add(finding);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const elementsIndex = `${elementsDir}/index.ts`;
|
|
95
|
+
if (project.files.has(elementsIndex)) {
|
|
96
|
+
for (const group of [...groupsWithFolders].sort()) {
|
|
97
|
+
const groupIndex = `${group}/index.ts`;
|
|
98
|
+
if (!project.files.has(groupIndex) || project.reexports(elementsIndex).some((reexportArg) => reexportArg.target === groupIndex)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
add({
|
|
102
|
+
file: elementsIndex,
|
|
103
|
+
position: 0,
|
|
104
|
+
key: elementsIndex,
|
|
105
|
+
message: `${elementsIndex} does not re-export ${baseOf(group)}/index.ts`,
|
|
106
|
+
fix: (builderArg) => addReexport(builderArg, project, elementsIndex, groupIndex),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...findings.values()];
|
|
111
|
+
},
|
|
112
|
+
};
|