@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,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
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { baseOf, isDemoFile } from '../helpers.paths.js';
|
|
2
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
3
|
+
import { removeStatement } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/** No barrel exports a demo file. */
|
|
6
|
+
export const barrelNoDemoExportRule: IStandardRule = {
|
|
7
|
+
id: 'barrel/no-demo-export',
|
|
8
|
+
severity: { base: 'error' },
|
|
9
|
+
fixMode: 'auto',
|
|
10
|
+
detect: ({ project }) => {
|
|
11
|
+
const findings: IRuleFinding[] = [];
|
|
12
|
+
for (const file of project.webFiles) {
|
|
13
|
+
if (baseOf(file) !== 'index.ts') {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
for (const reexport of project.reexports(file)) {
|
|
17
|
+
if (reexport.target && isDemoFile(reexport.target)) {
|
|
18
|
+
findings.push({
|
|
19
|
+
file,
|
|
20
|
+
position: reexport.statement.getStart(),
|
|
21
|
+
key: file,
|
|
22
|
+
message: `${file} exports the demo file ${reexport.target}`,
|
|
23
|
+
fix: (builderArg) => removeStatement(builderArg, file, reexport.statement),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { elementsBarrel } from '../classes.standardproject.js';
|
|
2
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
3
|
+
import { elementKey, removeStatement } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/** Elements declared in demo files are not reachable from `ts_web/elements/index.ts`. */
|
|
6
|
+
export const demoFixtureNotExportedRule: IStandardRule = {
|
|
7
|
+
id: 'demo/fixture-not-exported',
|
|
8
|
+
severity: { base: 'warn' },
|
|
9
|
+
fixMode: 'auto',
|
|
10
|
+
detect: ({ project }) => {
|
|
11
|
+
const findings: IRuleFinding[] = [];
|
|
12
|
+
const reached = project.reachableFrom(elementsBarrel);
|
|
13
|
+
for (const element of project.elements) {
|
|
14
|
+
if (!element.inDemoFile || !reached.has(element.file)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const exporters = [...reached].flatMap((fileArg) => project.reexports(fileArg)).filter((reexportArg) => reexportArg.target === element.file);
|
|
18
|
+
findings.push({
|
|
19
|
+
file: element.file,
|
|
20
|
+
position: element.decoratorCall.getStart(),
|
|
21
|
+
key: elementKey(element),
|
|
22
|
+
message: `fixture element <${element.tag ?? '?'}> in a demo file is reachable from ${elementsBarrel}`,
|
|
23
|
+
fix: exporters.length > 0
|
|
24
|
+
? (builderArg) => exporters.forEach((reexportArg) => removeStatement(builderArg, reexportArg.file, reexportArg.statement))
|
|
25
|
+
: undefined,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
import { catalogElements, elementKey, groupAreaOf, nextLineStart } from './shared.js';
|
|
5
|
+
|
|
6
|
+
const ts = plugins.typescript;
|
|
7
|
+
|
|
8
|
+
const quote = (textArg: string): string => `'${textArg.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An element with a demo declares `static demoGroups`, a non-empty string or array of non-empty strings.
|
|
12
|
+
* dees: the first entry is `standard.groupNames[<area>]` of the element's `00group-<area>`.
|
|
13
|
+
*/
|
|
14
|
+
export const demoGroupsRule: IStandardRule = {
|
|
15
|
+
id: 'demo/groups',
|
|
16
|
+
severity: { base: 'warn' },
|
|
17
|
+
fixMode: 'auto',
|
|
18
|
+
detect: ({ project, config, profile }) => {
|
|
19
|
+
const findings: IRuleFinding[] = [];
|
|
20
|
+
for (const element of catalogElements(project)) {
|
|
21
|
+
const demo = ast.findStaticProperty(element.classNode, 'demo');
|
|
22
|
+
if (!demo) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const area = profile === 'dees' ? groupAreaOf(element.file) : null;
|
|
26
|
+
const expected = area ? config.groupNames?.[area] ?? null : null;
|
|
27
|
+
const base = { file: element.file, key: elementKey(element) };
|
|
28
|
+
const member = ast.findStaticProperty(element.classNode, 'demoGroups');
|
|
29
|
+
const text = element.sourceFile.text;
|
|
30
|
+
|
|
31
|
+
if (!member) {
|
|
32
|
+
const finding: IRuleFinding = { ...base, position: demo.getStart(), message: `${element.className ?? '?'} has a demo but no static demoGroups` };
|
|
33
|
+
if (expected) {
|
|
34
|
+
const at = nextLineStart(text, demo.end);
|
|
35
|
+
finding.fix = (builderArg) => builderArg.edit(element.file, at, at, `${ast.indentationAt(text, demo.getStart())}public static demoGroups = [${quote(expected)}];\n`);
|
|
36
|
+
}
|
|
37
|
+
findings.push(finding);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const initializer = member.initializer;
|
|
41
|
+
let entries: string[] | null = null;
|
|
42
|
+
if (initializer && ast.isStringLiteralLike(initializer)) {
|
|
43
|
+
entries = [initializer.text];
|
|
44
|
+
} else if (initializer && ts.isArrayLiteralExpression(initializer) && initializer.elements.every(ast.isStringLiteralLike)) {
|
|
45
|
+
entries = initializer.elements.map((entryArg) => (entryArg as ast.TStringLiteralLike).text);
|
|
46
|
+
} else if (!initializer || ts.isArrayLiteralExpression(initializer) || ts.isLiteralExpression(initializer)) {
|
|
47
|
+
findings.push({ ...base, position: member.getStart(), message: `static demoGroups of ${element.className ?? '?'} is not a string or an array of strings` });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (entries && (entries.length === 0 || entries.some((entryArg) => entryArg.length === 0))) {
|
|
51
|
+
findings.push({ ...base, position: member.getStart(), message: `static demoGroups of ${element.className ?? '?'} is empty or has an empty entry` });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (profile !== 'dees' || !area || !entries) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!expected) {
|
|
58
|
+
findings.push({ ...base, position: member.getStart(), message: `standard.groupNames has no display name for the area '${area}'` });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (entries[0] !== expected) {
|
|
62
|
+
const target = initializer!;
|
|
63
|
+
const reordered = [expected, ...entries.filter((entryArg) => entryArg !== expected)];
|
|
64
|
+
findings.push({
|
|
65
|
+
...base,
|
|
66
|
+
position: member.getStart(),
|
|
67
|
+
message: `the first demo group of ${element.className ?? '?'} is ${quote(entries[0])}; expected ${quote(expected)} (00group-${area})`,
|
|
68
|
+
fix: (builderArg) => builderArg.edit(element.file, target.getStart(), target.end, `[${reordered.map(quote).join(', ')}]`),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as ast from '../helpers.ast.js';
|
|
2
|
+
import { dirOf } from '../helpers.paths.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
import { catalogElements, elementKey, isCustomElementName } from './shared.js';
|
|
5
|
+
|
|
6
|
+
/** Every exported element has `static demo`. The opt-in fix writes a stub that renders the bare tag. */
|
|
7
|
+
export const demoRequiredRule: IStandardRule = {
|
|
8
|
+
id: 'demo/required',
|
|
9
|
+
severity: { base: 'warn' },
|
|
10
|
+
fixMode: 'opt-in',
|
|
11
|
+
allowlistCategories: ['subcomponent', 'base'],
|
|
12
|
+
detect: ({ project }) => {
|
|
13
|
+
const findings: IRuleFinding[] = [];
|
|
14
|
+
const elements = catalogElements(project);
|
|
15
|
+
for (const element of elements) {
|
|
16
|
+
if (!element.exported || ast.findStaticProperty(element.classNode, 'demo')) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const finding: IRuleFinding = {
|
|
20
|
+
file: element.file,
|
|
21
|
+
position: element.decoratorCall.getStart(),
|
|
22
|
+
key: elementKey(element),
|
|
23
|
+
message: `${element.className ?? '?'} (<${element.tag ?? '?'}>) has no static demo`,
|
|
24
|
+
};
|
|
25
|
+
const sourceFile = element.sourceFile;
|
|
26
|
+
// The stub's file name and markup come from the tag: only a valid custom element name is usable.
|
|
27
|
+
const usableTag = element.tag && isCustomElementName(element.tag) ? element.tag : null;
|
|
28
|
+
const demoFile = usableTag ? `${dirOf(element.file)}/${usableTag}.demo.ts` : null;
|
|
29
|
+
const firstMember = element.classNode.members[0];
|
|
30
|
+
const alone = elements.filter((otherArg) => otherArg.file === element.file).length === 1;
|
|
31
|
+
if (usableTag && demoFile && element.className && firstMember && alone && !project.exists(demoFile) && !/\bdemoFunc\b/.test(sourceFile.text)) {
|
|
32
|
+
const tag = usableTag;
|
|
33
|
+
const customElementSource = ast.importSourceOf(sourceFile, 'customElement');
|
|
34
|
+
const htmlSource = ast.importSourceOf(sourceFile, 'html')
|
|
35
|
+
?? (customElementSource && customElementSource !== 'lit/decorators.js' ? customElementSource : 'lit');
|
|
36
|
+
const text = sourceFile.text;
|
|
37
|
+
const importAt = ast.afterLastImport(sourceFile);
|
|
38
|
+
finding.fix = (builderArg) => {
|
|
39
|
+
builderArg.create(demoFile, `import { html } from '${htmlSource}';\n\nexport const demoFunc = () => html\`<${tag}></${tag}>\`;\n`);
|
|
40
|
+
builderArg.edit(element.file, importAt, importAt, `import { demoFunc } from './${tag}.demo.js';\n`);
|
|
41
|
+
const lineStart = text.lastIndexOf('\n', firstMember.getStart() - 1) + 1;
|
|
42
|
+
builderArg.edit(element.file, lineStart, lineStart, `${ast.indentationAt(text, firstMember.getStart())}public static demo = demoFunc;\n`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
findings.push(finding);
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import { isDemoFile, resolveSpecifier } from '../helpers.paths.js';
|
|
4
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
5
|
+
import { catalogElements, elementKey, rootIdentifier } from './shared.js';
|
|
6
|
+
|
|
7
|
+
const ts = plugins.typescript;
|
|
8
|
+
|
|
9
|
+
/** `static demo` refers to an export of the element's demo file. Report-only in 6.1. */
|
|
10
|
+
export const demoSeparateFileRule: IStandardRule = {
|
|
11
|
+
id: 'demo/separate-file',
|
|
12
|
+
severity: { base: 'warn', dees: 'error' },
|
|
13
|
+
fixMode: 'none',
|
|
14
|
+
detect: ({ project }) => {
|
|
15
|
+
const findings: IRuleFinding[] = [];
|
|
16
|
+
for (const element of catalogElements(project)) {
|
|
17
|
+
const member = ast.findStaticProperty(element.classNode, 'demo');
|
|
18
|
+
if (!member?.initializer) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const initializer = member.initializer;
|
|
22
|
+
const references = ts.isArrayLiteralExpression(initializer) ? [...initializer.elements] : [initializer];
|
|
23
|
+
const separate = references.length > 0 && references.every((referenceArg) => {
|
|
24
|
+
const identifier = rootIdentifier(referenceArg);
|
|
25
|
+
const source = identifier ? ast.importSourceOf(element.sourceFile, identifier) : null;
|
|
26
|
+
const resolved = source ? resolveSpecifier(element.file, source, (fileArg) => project.files.has(fileArg)) : null;
|
|
27
|
+
return !!resolved && isDemoFile(resolved.target);
|
|
28
|
+
});
|
|
29
|
+
if (!separate) {
|
|
30
|
+
findings.push({
|
|
31
|
+
file: element.file,
|
|
32
|
+
position: member.getStart(),
|
|
33
|
+
key: elementKey(element),
|
|
34
|
+
message: `static demo of ${element.className ?? '?'} is not imported from its demo file (${element.tag ?? '<tag>'}.demo.ts)`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as ast from '../helpers.ast.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
import { catalogElements, elementKey } from './shared.js';
|
|
5
|
+
|
|
6
|
+
const ts = plugins.typescript;
|
|
7
|
+
|
|
8
|
+
/** A demo factory: a function, or a reference to one (identifier or property access). */
|
|
9
|
+
const isFactory = (expressionArg: ast.TExpression): boolean =>
|
|
10
|
+
ts.isArrowFunction(expressionArg)
|
|
11
|
+
|| ts.isFunctionExpression(expressionArg)
|
|
12
|
+
|| ts.isIdentifier(expressionArg)
|
|
13
|
+
|| ts.isPropertyAccessExpression(expressionArg);
|
|
14
|
+
|
|
15
|
+
/** `static demo` is a function or a non-empty array of functions. */
|
|
16
|
+
export const demoShapeRule: IStandardRule = {
|
|
17
|
+
id: 'demo/shape',
|
|
18
|
+
severity: { base: 'error' },
|
|
19
|
+
fixMode: 'none',
|
|
20
|
+
detect: ({ project }) => {
|
|
21
|
+
const findings: IRuleFinding[] = [];
|
|
22
|
+
for (const element of catalogElements(project)) {
|
|
23
|
+
const member = ast.findStaticProperty(element.classNode, 'demo');
|
|
24
|
+
if (!member) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const initializer = member.initializer;
|
|
28
|
+
const valid = !!initializer && (ts.isArrayLiteralExpression(initializer)
|
|
29
|
+
? initializer.elements.length > 0 && initializer.elements.every(isFactory)
|
|
30
|
+
: isFactory(initializer));
|
|
31
|
+
if (!valid) {
|
|
32
|
+
findings.push({
|
|
33
|
+
file: element.file,
|
|
34
|
+
position: member.getStart(),
|
|
35
|
+
key: elementKey(element),
|
|
36
|
+
message: `static demo of ${element.className ?? '?'} is not a function or a non-empty array of functions`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return findings;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { StandardProject } from '../classes.standardproject.js';
|
|
2
|
+
import { joinPath } from '../helpers.paths.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
import { catalogElements } from './shared.js';
|
|
5
|
+
|
|
6
|
+
export const componentIndexStart = '<!-- component-index:start -->';
|
|
7
|
+
export const componentIndexEnd = '<!-- component-index:end -->';
|
|
8
|
+
const readme = 'readme.md';
|
|
9
|
+
|
|
10
|
+
/** GitHub-style heading anchor. */
|
|
11
|
+
const slug = (headingArg: string): string => headingArg.toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, '').replace(/ /g, '-');
|
|
12
|
+
|
|
13
|
+
const headingsOf = (textArg: string): Set<string> => new Set([...textArg.matchAll(/^#{1,6}\s+(.+)$/gm)].map((matchArg) => slug(matchArg[1])));
|
|
14
|
+
|
|
15
|
+
/** The generated component index block: one row per element under `ts_web/elements`, sorted by tag. */
|
|
16
|
+
export const renderComponentIndex = (projectArg: StandardProject, readmeTextArg: string): string => {
|
|
17
|
+
const headings = headingsOf(readmeTextArg);
|
|
18
|
+
const rows = catalogElements(projectArg)
|
|
19
|
+
.filter((elementArg) => elementArg.tag && elementArg.className && elementArg.file.startsWith('ts_web/elements/'))
|
|
20
|
+
.map((elementArg) => {
|
|
21
|
+
const family = elementArg.file.split('/')[2].replace(/^00group-/, '').replace(/-/g, ' ');
|
|
22
|
+
const name = elementArg.className!;
|
|
23
|
+
const api = headings.has(name.toLowerCase()) ? `[${name}](#${name.toLowerCase()})` : '—';
|
|
24
|
+
return { tag: elementArg.tag!, row: `| \`${elementArg.tag}\` | ${family} | ${api} | [Source](${elementArg.file}) |` };
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => a.tag.localeCompare(b.tag, 'en'));
|
|
27
|
+
return `${componentIndexStart}\n| Element | Family | API | Source |\n| --- | --- | --- | --- |\n${rows.map((rowArg) => rowArg.row).join('\n')}\n${componentIndexEnd}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** dees: the readme's generated component index is current and its relative links resolve. */
|
|
31
|
+
export const docsComponentIndexRule: IStandardRule = {
|
|
32
|
+
id: 'docs/component-index',
|
|
33
|
+
severity: { dees: 'error' },
|
|
34
|
+
fixMode: 'auto',
|
|
35
|
+
detect: ({ project }) => {
|
|
36
|
+
const findings: IRuleFinding[] = [];
|
|
37
|
+
const text = project.files.get(readme);
|
|
38
|
+
if (text === undefined) {
|
|
39
|
+
return [{ file: readme, position: 0, key: readme, message: 'readme.md is missing' }];
|
|
40
|
+
}
|
|
41
|
+
const first = text.indexOf(componentIndexStart);
|
|
42
|
+
const last = text.indexOf(componentIndexEnd);
|
|
43
|
+
if (first < 0 || last < first) {
|
|
44
|
+
findings.push({ file: readme, position: 0, key: readme, message: `readme.md has no ${componentIndexStart} … ${componentIndexEnd} block` });
|
|
45
|
+
} else {
|
|
46
|
+
const generated = renderComponentIndex(project, text);
|
|
47
|
+
const end = last + componentIndexEnd.length;
|
|
48
|
+
if (text.slice(first, end) !== generated) {
|
|
49
|
+
findings.push({
|
|
50
|
+
file: readme,
|
|
51
|
+
position: first,
|
|
52
|
+
key: readme,
|
|
53
|
+
message: 'the component index is stale',
|
|
54
|
+
fix: (builderArg) => builderArg.edit(readme, first, end, generated),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const headings = headingsOf(text);
|
|
59
|
+
const withoutCode = text.replace(/^```[^\n]*\n[\s\S]*?^```/gm, (blockArg) => blockArg.replace(/[^\n]/g, ' '));
|
|
60
|
+
for (const match of withoutCode.matchAll(/\[[^\]]*\]\(([^\s)]+)\)/g)) {
|
|
61
|
+
const target = match[1];
|
|
62
|
+
if (/^[a-z]+:/i.test(target)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (target.startsWith('#')) {
|
|
66
|
+
if (!headings.has(target.slice(1))) {
|
|
67
|
+
findings.push({ file: readme, position: match.index, key: readme, message: `link to a missing section: ${target}` });
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const file = target.split('#')[0];
|
|
72
|
+
if (!file) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let decoded: string;
|
|
76
|
+
try {
|
|
77
|
+
decoded = decodeURIComponent(file);
|
|
78
|
+
} catch {
|
|
79
|
+
findings.push({ file: readme, position: match.index, key: readme, message: `malformed link: ${target}` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!project.exists(joinPath(decoded))) {
|
|
83
|
+
findings.push({ file: readme, position: match.index, key: readme, message: `link to a missing file: ${target}` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
3
|
+
import { catalogElements, elementKey } from './shared.js';
|
|
4
|
+
|
|
5
|
+
const ts = plugins.typescript;
|
|
6
|
+
|
|
7
|
+
/** The element class is exported. */
|
|
8
|
+
export const elementExportedRule: IStandardRule = {
|
|
9
|
+
id: 'element/exported',
|
|
10
|
+
severity: { base: 'error' },
|
|
11
|
+
fixMode: 'auto',
|
|
12
|
+
detect: ({ project }) => {
|
|
13
|
+
const findings: IRuleFinding[] = [];
|
|
14
|
+
for (const element of catalogElements(project)) {
|
|
15
|
+
if (element.exported) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const finding: IRuleFinding = {
|
|
19
|
+
file: element.file,
|
|
20
|
+
position: element.decoratorCall.getStart(),
|
|
21
|
+
key: elementKey(element),
|
|
22
|
+
message: `class ${element.className ?? '?'} (<${element.tag ?? '?'}>) is not exported`,
|
|
23
|
+
};
|
|
24
|
+
// `export` goes before the first keyword modifier (e.g. `abstract`) or the `class` keyword, after the decorators.
|
|
25
|
+
const firstModifier = (ts.getModifiers(element.classNode) ?? [])[0];
|
|
26
|
+
const classKeyword = element.classNode.getChildren().find((childArg) => childArg.kind === ts.SyntaxKind.ClassKeyword);
|
|
27
|
+
const position = firstModifier?.getStart() ?? classKeyword?.getStart();
|
|
28
|
+
if (position !== undefined && element.classNode.parent === element.sourceFile) {
|
|
29
|
+
finding.fix = (builderArg) => builderArg.edit(element.file, position, position, 'export ');
|
|
30
|
+
}
|
|
31
|
+
findings.push(finding);
|
|
32
|
+
}
|
|
33
|
+
return findings;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
2
|
+
import { elementKey, isCustomElementName } from './shared.js';
|
|
3
|
+
|
|
4
|
+
/** `@customElement` takes a single string literal that is a valid custom element name. */
|
|
5
|
+
export const elementLiteralTagRule: IStandardRule = {
|
|
6
|
+
id: 'element/literal-tag',
|
|
7
|
+
severity: { base: 'error' },
|
|
8
|
+
fixMode: 'none',
|
|
9
|
+
detect: ({ project }) => {
|
|
10
|
+
const findings: IRuleFinding[] = [];
|
|
11
|
+
for (const element of project.elements) {
|
|
12
|
+
if (element.tag === null) {
|
|
13
|
+
findings.push({
|
|
14
|
+
file: element.file,
|
|
15
|
+
position: element.decoratorCall.getStart(),
|
|
16
|
+
key: elementKey(element),
|
|
17
|
+
message: `@customElement of ${element.className ?? 'an anonymous class'} does not take a single string literal`,
|
|
18
|
+
});
|
|
19
|
+
} else if (!isCustomElementName(element.tag)) {
|
|
20
|
+
findings.push({
|
|
21
|
+
file: element.file,
|
|
22
|
+
position: element.decoratorCall.getStart(),
|
|
23
|
+
key: elementKey(element),
|
|
24
|
+
message: `'${element.tag}' is not a valid custom element name (a lowercase letter, then lowercase letters, digits, '-', '.' or '_', with at least one '-')`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { rootBarrel } from '../classes.standardproject.js';
|
|
2
|
+
import { dirOf } from '../helpers.paths.js';
|
|
3
|
+
import type { IRuleFinding, IStandardRule } from '../rule.js';
|
|
4
|
+
import { addReexport, catalogElements, elementKey } from './shared.js';
|
|
5
|
+
|
|
6
|
+
/** Every entry file outside demo files is reachable from `ts_web/index.ts`. */
|
|
7
|
+
export const elementReachableRule: IStandardRule = {
|
|
8
|
+
id: 'element/reachable',
|
|
9
|
+
severity: { base: 'error' },
|
|
10
|
+
fixMode: 'auto',
|
|
11
|
+
allowlistCategories: ['internal'],
|
|
12
|
+
detect: ({ project }) => {
|
|
13
|
+
const findings: IRuleFinding[] = [];
|
|
14
|
+
const reached = project.reachableFrom(rootBarrel);
|
|
15
|
+
for (const element of catalogElements(project)) {
|
|
16
|
+
if (reached.has(element.file)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const finding: IRuleFinding = {
|
|
20
|
+
file: element.file,
|
|
21
|
+
position: element.decoratorCall.getStart(),
|
|
22
|
+
key: elementKey(element),
|
|
23
|
+
message: `<${element.tag ?? '?'}> in ${element.file} is not reachable from ${rootBarrel}`,
|
|
24
|
+
};
|
|
25
|
+
// Walk up to the nearest reachable barrel; an unreachable barrel that already re-exports the file is exported instead.
|
|
26
|
+
let candidate = element.file;
|
|
27
|
+
let dir = dirOf(element.file);
|
|
28
|
+
while (dir.startsWith('ts_web')) {
|
|
29
|
+
const barrel = `${dir}/index.ts`;
|
|
30
|
+
if (barrel !== candidate && project.files.has(barrel)) {
|
|
31
|
+
if (reached.has(barrel)) {
|
|
32
|
+
const target = candidate;
|
|
33
|
+
finding.fix = (builderArg) => addReexport(builderArg, project, barrel, target);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (project.reexports(barrel).some((reexportArg) => reexportArg.target === candidate)) {
|
|
37
|
+
candidate = barrel;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (dir === 'ts_web') {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
dir = dirOf(dir);
|
|
44
|
+
}
|
|
45
|
+
findings.push(finding);
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
},
|
|
49
|
+
};
|