@design.estate/wcctools 5.1.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/.smartconfig.json +7 -3
- package/changelog.md +28 -0
- package/cli.js +3 -0
- package/dist_shell/bundle.js +2970 -0
- package/dist_shell/bundle.js.map +1 -0
- package/dist_shell/bundle.js.third-party-notices.json +1722 -0
- package/dist_shell/bundle.js.third-party-notices.txt +4195 -0
- package/dist_shell/index.html +21 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.devapi.d.ts +11 -0
- package/dist_ts/classes.devapi.js +21 -0
- package/dist_ts/classes.devserver.d.ts +49 -0
- package/dist_ts/classes.devserver.js +142 -0
- package/dist_ts/cli.d.ts +6 -0
- package/dist_ts/cli.js +51 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/paths.d.ts +4 -0
- package/dist_ts/paths.js +6 -0
- package/dist_ts/plugins.d.ts +20 -0
- package/dist_ts/plugins.js +27 -0
- package/dist_ts/shellcontent.d.ts +9 -0
- package/dist_ts/shellcontent.js +42 -0
- 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/bridge.d.ts +88 -0
- package/dist_ts_interfaces/bridge.js +2 -0
- package/dist_ts_interfaces/catalog.d.ts +32 -0
- package/dist_ts_interfaces/catalog.js +6 -0
- package/dist_ts_interfaces/index.d.ts +4 -0
- package/dist_ts_interfaces/index.js +5 -0
- package/dist_ts_interfaces/plugins.d.ts +2 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +15 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_interfaces/standard.d.ts +93 -0
- package/dist_ts_interfaces/standard.js +6 -0
- package/dist_ts_shared/index.d.ts +1 -0
- package/dist_ts_shared/index.js +2 -0
- package/dist_ts_shared/plugins.d.ts +2 -0
- package/dist_ts_shared/plugins.js +3 -0
- package/dist_ts_shared/previewroute.d.ts +18 -0
- package/dist_ts_shared/previewroute.js +47 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.js +1 -1
- package/dist_ts_shell/elements/wcc-preview-frame.d.ts +23 -0
- package/dist_ts_shell/elements/wcc-preview-frame.js +197 -0
- package/dist_ts_shell/elements/wcc-properties.d.ts +49 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-properties.js +149 -216
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.js +1 -1
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.d.ts +2 -2
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.js +12 -16
- package/dist_ts_shell/elements/wcc-shell.d.ts +87 -0
- package/dist_ts_shell/elements/wcc-shell.js +524 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.d.ts +12 -10
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.js +137 -183
- package/dist_ts_shell/index.d.ts +4 -0
- package/dist_ts_shell/index.js +5 -0
- package/dist_ts_shell/plugins.d.ts +4 -0
- package/dist_ts_shell/plugins.js +5 -0
- package/dist_ts_shell/previewconnection.d.ts +30 -0
- package/dist_ts_shell/previewconnection.js +86 -0
- package/dist_ts_shell/services/recorder.service.js +353 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/catalog.d.ts +45 -0
- package/dist_ts_web/catalog.js +153 -0
- package/dist_ts_web/elementproperties.d.ts +15 -0
- package/dist_ts_web/elementproperties.js +89 -0
- package/dist_ts_web/elements/wcc-preview.d.ts +19 -0
- package/dist_ts_web/elements/wcc-preview.js +164 -0
- package/dist_ts_web/elements/wcc-standalone-notice.d.ts +14 -0
- package/dist_ts_web/elements/wcc-standalone-notice.js +110 -0
- package/dist_ts_web/elements/wcctools.helpers.d.ts +2 -6
- package/dist_ts_web/elements/wcctools.helpers.js +1 -7
- package/dist_ts_web/index.d.ts +2 -5
- package/dist_ts_web/index.js +38 -17
- package/dist_ts_web/previewbridge.d.ts +39 -0
- package/dist_ts_web/previewbridge.js +153 -0
- package/dist_ts_web/renderedtree.d.ts +5 -0
- package/dist_ts_web/renderedtree.js +37 -0
- package/dist_ts_web/wcctools.interfaces.d.ts +3 -6
- package/dist_ts_web/wcctools.interfaces.js +1 -1
- package/dist_ts_web/wcctools.plugins.d.ts +3 -1
- package/dist_ts_web/wcctools.plugins.js +4 -2
- package/package.json +17 -7
- package/readme.md +93 -28
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.devapi.ts +25 -0
- package/ts/classes.devserver.ts +161 -0
- package/ts/cli.ts +53 -0
- package/ts/index.ts +3 -0
- package/ts/paths.ts +7 -0
- package/ts/plugins.ts +37 -0
- package/ts/shellcontent.ts +42 -0
- 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
- package/ts_web/catalog.ts +194 -0
- package/ts_web/elementproperties.ts +96 -0
- package/ts_web/elements/wcc-preview.ts +111 -0
- package/ts_web/elements/wcc-standalone-notice.ts +65 -0
- package/ts_web/elements/wcctools.helpers.ts +2 -10
- package/ts_web/index.ts +38 -19
- package/ts_web/previewbridge.ts +189 -0
- package/ts_web/readme.md +26 -85
- package/ts_web/renderedtree.ts +43 -0
- package/ts_web/wcctools.interfaces.ts +4 -7
- package/ts_web/wcctools.plugins.ts +6 -1
- package/dist_bundle/bundle.js +0 -92419
- package/dist_bundle/bundle.js.map +0 -1
- package/dist_ts_web/elements/wcc-dashboard.d.ts +0 -119
- package/dist_ts_web/elements/wcc-dashboard.js +0 -700
- package/dist_ts_web/elements/wcc-frame.d.ts +0 -22
- package/dist_ts_web/elements/wcc-frame.js +0 -256
- package/dist_ts_web/elements/wcc-properties.d.ts +0 -43
- package/dist_ts_web/pages/index.d.ts +0 -1
- package/dist_ts_web/pages/index.js +0 -2
- package/dist_ts_web/services/recorder.service.js +0 -353
- package/ts_web/elements/wcc-contextmenu.ts +0 -291
- package/ts_web/elements/wcc-dashboard.ts +0 -672
- package/ts_web/elements/wcc-frame.ts +0 -189
- package/ts_web/elements/wcc-properties.ts +0 -1089
- package/ts_web/elements/wcc-record-button.ts +0 -108
- package/ts_web/elements/wcc-recording-panel.ts +0 -1010
- package/ts_web/elements/wcc-sidebar.ts +0 -1470
- package/ts_web/pages/index.ts +0 -1
- package/ts_web/services/recorder.service.ts +0 -451
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +0 -12
- package/ts_web/types/dom-mediacapture-stub/package.json +0 -6
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +0 -2
- package/ts_web/types/dom-webcodecs-stub/package.json +0 -6
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/services/recorder.service.d.ts +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { StandardConfigError } from './errors.js';
|
|
3
|
+
import { severityIn } from './rule.js';
|
|
4
|
+
import { standardRules } from './rules/index.js';
|
|
5
|
+
|
|
6
|
+
type IStandardConfig = plugins.interfaces.IStandardConfig;
|
|
7
|
+
type TStandardProfile = plugins.interfaces.TStandardProfile;
|
|
8
|
+
|
|
9
|
+
/** The key of wcctools' configuration in `.smartconfig.json`. */
|
|
10
|
+
export const wcctoolsConfigKey = '@design.estate/wcctools';
|
|
11
|
+
|
|
12
|
+
/** Persists the `standard` configuration of the repository it was read from. */
|
|
13
|
+
export interface IStandardConfigWriter {
|
|
14
|
+
writeStandardConfig(configArg: IStandardConfig): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ILoadedStandardConfig {
|
|
18
|
+
config: IStandardConfig;
|
|
19
|
+
profile: TStandardProfile;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A validated configuration together with the writer bound to the `.smartconfig.json` it was read from. */
|
|
23
|
+
export interface IOpenedStandardConfig {
|
|
24
|
+
loaded: ILoadedStandardConfig;
|
|
25
|
+
writer: IStandardConfigWriter;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Writes through the Smartconfig instance that read the configuration, so a write is refused
|
|
30
|
+
* (SmartconfigChangedError) when `.smartconfig.json` changed in between. Other keys of the
|
|
31
|
+
* `@design.estate/wcctools` block keep their values.
|
|
32
|
+
*/
|
|
33
|
+
export class SmartconfigStandardConfigWriter implements IStandardConfigWriter {
|
|
34
|
+
private readonly smartconfig: plugins.smartconfig.Smartconfig;
|
|
35
|
+
|
|
36
|
+
constructor(smartconfigArg: plugins.smartconfig.Smartconfig) {
|
|
37
|
+
this.smartconfig = smartconfigArg;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async writeStandardConfig(configArg: IStandardConfig): Promise<void> {
|
|
41
|
+
const block = this.smartconfig.dataFor<Record<string, unknown>>(wcctoolsConfigKey, {});
|
|
42
|
+
await this.smartconfig.writeDataFor(wcctoolsConfigKey, { ...block, standard: configArg });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isPlainObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
47
|
+
typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
|
48
|
+
|
|
49
|
+
const knownKeys = ['profile', 'tagPrefix', 'groupNames', 'allowlist', 'baselines'];
|
|
50
|
+
|
|
51
|
+
/** Validates the `standard` object; collects every problem into one StandardConfigError. */
|
|
52
|
+
export const validateStandardConfig = (valueArg: unknown): ILoadedStandardConfig => {
|
|
53
|
+
const problems: string[] = [];
|
|
54
|
+
if (valueArg === undefined) {
|
|
55
|
+
throw new StandardConfigError([`"${wcctoolsConfigKey}".standard is missing`]);
|
|
56
|
+
}
|
|
57
|
+
if (!isPlainObject(valueArg)) {
|
|
58
|
+
throw new StandardConfigError([`"${wcctoolsConfigKey}".standard must be an object`]);
|
|
59
|
+
}
|
|
60
|
+
for (const key of Object.keys(valueArg)) {
|
|
61
|
+
if (!knownKeys.includes(key)) {
|
|
62
|
+
problems.push(`standard.${key} is not a known key (${knownKeys.join(', ')})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const profile = valueArg.profile ?? 'base';
|
|
66
|
+
if (profile !== 'base' && profile !== 'dees') {
|
|
67
|
+
problems.push(`standard.profile must be 'base' or 'dees'`);
|
|
68
|
+
}
|
|
69
|
+
if (valueArg.tagPrefix === undefined) {
|
|
70
|
+
problems.push('standard.tagPrefix is missing');
|
|
71
|
+
} else if (typeof valueArg.tagPrefix !== 'string' || !/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(valueArg.tagPrefix)) {
|
|
72
|
+
problems.push('standard.tagPrefix must be a lowercase tag prefix without the trailing hyphen, e.g. "dees"');
|
|
73
|
+
}
|
|
74
|
+
if (valueArg.groupNames !== undefined) {
|
|
75
|
+
if (!isPlainObject(valueArg.groupNames)) {
|
|
76
|
+
problems.push('standard.groupNames must be an object of area → display name');
|
|
77
|
+
} else {
|
|
78
|
+
for (const [area, name] of Object.entries(valueArg.groupNames)) {
|
|
79
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
80
|
+
problems.push(`standard.groupNames.${area} must be a non-empty string`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const rulesById = new Map(standardRules.map((ruleArg) => [ruleArg.id, ruleArg]));
|
|
86
|
+
if (valueArg.allowlist !== undefined) {
|
|
87
|
+
if (!isPlainObject(valueArg.allowlist)) {
|
|
88
|
+
problems.push('standard.allowlist must be an object of rule id → entries');
|
|
89
|
+
} else {
|
|
90
|
+
for (const [ruleId, entries] of Object.entries(valueArg.allowlist)) {
|
|
91
|
+
const rule = rulesById.get(ruleId);
|
|
92
|
+
if (!rule) {
|
|
93
|
+
problems.push(`standard.allowlist: unknown rule '${ruleId}'`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!rule.allowlistCategories) {
|
|
97
|
+
problems.push(`standard.allowlist: rule '${ruleId}' takes no allowlist entries`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!isPlainObject(entries)) {
|
|
101
|
+
problems.push(`standard.allowlist['${ruleId}'] must be an object of '<file>#<member>' → '<category>: <reason>'`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
105
|
+
const match = typeof value === 'string' ? /^([a-z-]+):\s*(\S.*)$/.exec(value) : null;
|
|
106
|
+
if (!match) {
|
|
107
|
+
problems.push(`standard.allowlist['${ruleId}']['${key}'] must be '<category>: <reason>'`);
|
|
108
|
+
} else if (!rule.allowlistCategories.includes(match[1])) {
|
|
109
|
+
problems.push(`standard.allowlist['${ruleId}']['${key}']: unknown category '${match[1]}' (${rule.allowlistCategories.join(', ')})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (valueArg.baselines !== undefined) {
|
|
116
|
+
if (!isPlainObject(valueArg.baselines)) {
|
|
117
|
+
problems.push('standard.baselines must be an object of rule id → file → count');
|
|
118
|
+
} else {
|
|
119
|
+
for (const [ruleId, counts] of Object.entries(valueArg.baselines)) {
|
|
120
|
+
const rule = rulesById.get(ruleId);
|
|
121
|
+
if (!rule) {
|
|
122
|
+
problems.push(`standard.baselines: unknown rule '${ruleId}'`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if ((profile === 'base' || profile === 'dees') && severityIn(rule, profile) !== 'error') {
|
|
126
|
+
problems.push(`standard.baselines: '${ruleId}' is not an error rule of the '${profile}' profile; baselines hold error rules only`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!isPlainObject(counts)) {
|
|
130
|
+
problems.push(`standard.baselines['${ruleId}'] must be an object of file → count`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const [file, count] of Object.entries(counts)) {
|
|
134
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
|
|
135
|
+
problems.push(`standard.baselines['${ruleId}']['${file}'] must be an integer of at least 1`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (problems.length > 0) {
|
|
142
|
+
throw new StandardConfigError(problems);
|
|
143
|
+
}
|
|
144
|
+
return { config: valueArg as unknown as IStandardConfig, profile: profile as TStandardProfile };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Reads and validates `.smartconfig.json` → `@design.estate/wcctools` → `standard` of a repository. */
|
|
148
|
+
export const openStandardConfig = (rootDirArg: string): IOpenedStandardConfig => {
|
|
149
|
+
let smartconfig: plugins.smartconfig.Smartconfig;
|
|
150
|
+
try {
|
|
151
|
+
smartconfig = new plugins.smartconfig.Smartconfig(rootDirArg);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new StandardConfigError([`.smartconfig.json cannot be read: ${error instanceof Error ? error.message : String(error)}`]);
|
|
154
|
+
}
|
|
155
|
+
if (!smartconfig.smartconfigJsonExists) {
|
|
156
|
+
throw new StandardConfigError([`${rootDirArg} has no .smartconfig.json`]);
|
|
157
|
+
}
|
|
158
|
+
const wcctoolsConfig = smartconfig.dataFor<plugins.interfaces.IWccToolsConfig>(wcctoolsConfigKey, {});
|
|
159
|
+
return { loaded: validateStandardConfig(wcctoolsConfig.standard), writer: new SmartconfigStandardConfigWriter(smartconfig) };
|
|
160
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The `standard` configuration is missing or invalid; the CLI exits with code 2. */
|
|
2
|
+
export class StandardConfigError extends Error {
|
|
3
|
+
public readonly problems: string[];
|
|
4
|
+
|
|
5
|
+
constructor(problemsArg: string[]) {
|
|
6
|
+
super(`Invalid wcctools standard configuration in .smartconfig.json:\n${problemsArg.map((problemArg) => ` - ${problemArg}`).join('\n')}`);
|
|
7
|
+
this.name = 'StandardConfigError';
|
|
8
|
+
this.problems = problemsArg;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A precondition or argument of `check`/`fix` is not met; the CLI exits with code 2. */
|
|
13
|
+
export class StandardUsageError extends Error {
|
|
14
|
+
constructor(messageArg: string) {
|
|
15
|
+
super(messageArg);
|
|
16
|
+
this.name = 'StandardUsageError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -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
|
+
};
|