@design.estate/wcctools 6.0.0 → 6.2.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.
Files changed (202) hide show
  1. package/changelog.md +33 -0
  2. package/dist_shell/bundle.js +405 -368
  3. package/dist_shell/bundle.js.map +1 -1
  4. package/dist_shell/bundle.js.third-party-notices.json +52 -0
  5. package/dist_shell/bundle.js.third-party-notices.txt +107 -1
  6. package/dist_shell/index.html +1 -1
  7. package/dist_ts/00_commitinfo_data.js +1 -1
  8. package/dist_ts/classes.bundlestatus.d.ts +36 -0
  9. package/dist_ts/classes.bundlestatus.js +110 -0
  10. package/dist_ts/classes.devapi.d.ts +3 -0
  11. package/dist_ts/classes.devapi.js +8 -1
  12. package/dist_ts/classes.devserver.d.ts +45 -10
  13. package/dist_ts/classes.devserver.js +123 -49
  14. package/dist_ts/cli.js +12 -3
  15. package/dist_ts/index.d.ts +0 -2
  16. package/dist_ts/index.js +1 -3
  17. package/dist_ts/plugins.d.ts +7 -2
  18. package/dist_ts/plugins.js +9 -3
  19. package/dist_ts/standards/baselines.d.ts +27 -0
  20. package/dist_ts/standards/baselines.js +83 -0
  21. package/dist_ts/standards/checker.d.ts +21 -0
  22. package/dist_ts/standards/checker.js +88 -0
  23. package/dist_ts/standards/classes.gitrepo.d.ts +13 -0
  24. package/dist_ts/standards/classes.gitrepo.js +43 -0
  25. package/dist_ts/standards/classes.sourceedits.d.ts +15 -0
  26. package/dist_ts/standards/classes.sourceedits.js +54 -0
  27. package/dist_ts/standards/classes.standardfixer.d.ts +40 -0
  28. package/dist_ts/standards/classes.standardfixer.js +210 -0
  29. package/dist_ts/standards/classes.standardproject.d.ts +80 -0
  30. package/dist_ts/standards/classes.standardproject.js +195 -0
  31. package/dist_ts/standards/classes.standardtree.d.ts +56 -0
  32. package/dist_ts/standards/classes.standardtree.js +207 -0
  33. package/dist_ts/standards/cli.d.ts +7 -0
  34. package/dist_ts/standards/cli.js +89 -0
  35. package/dist_ts/standards/config.d.ts +33 -0
  36. package/dist_ts/standards/config.js +139 -0
  37. package/dist_ts/standards/errors.d.ts +9 -0
  38. package/dist_ts/standards/errors.js +17 -0
  39. package/dist_ts/standards/helpers.ast.d.ts +56 -0
  40. package/dist_ts/standards/helpers.ast.js +172 -0
  41. package/dist_ts/standards/helpers.paths.d.ts +36 -0
  42. package/dist_ts/standards/helpers.paths.js +86 -0
  43. package/dist_ts/standards/index.d.ts +14 -0
  44. package/dist_ts/standards/index.js +15 -0
  45. package/dist_ts/standards/output.d.ts +7 -0
  46. package/dist_ts/standards/output.js +57 -0
  47. package/dist_ts/standards/ratchet.d.ts +15 -0
  48. package/dist_ts/standards/ratchet.js +45 -0
  49. package/dist_ts/standards/rule.d.ts +50 -0
  50. package/dist_ts/standards/rule.js +9 -0
  51. package/dist_ts/standards/rules/barrel.element-index.d.ts +7 -0
  52. package/dist_ts/standards/rules/barrel.element-index.js +108 -0
  53. package/dist_ts/standards/rules/barrel.no-demo-export.d.ts +3 -0
  54. package/dist_ts/standards/rules/barrel.no-demo-export.js +29 -0
  55. package/dist_ts/standards/rules/demo.fixture-not-exported.d.ts +3 -0
  56. package/dist_ts/standards/rules/demo.fixture-not-exported.js +29 -0
  57. package/dist_ts/standards/rules/demo.groups.d.ts +6 -0
  58. package/dist_ts/standards/rules/demo.groups.js +72 -0
  59. package/dist_ts/standards/rules/demo.required.d.ts +3 -0
  60. package/dist_ts/standards/rules/demo.required.js +48 -0
  61. package/dist_ts/standards/rules/demo.separate-file.d.ts +3 -0
  62. package/dist_ts/standards/rules/demo.separate-file.js +38 -0
  63. package/dist_ts/standards/rules/demo.shape.d.ts +3 -0
  64. package/dist_ts/standards/rules/demo.shape.js +38 -0
  65. package/dist_ts/standards/rules/docs.component-index.d.ts +8 -0
  66. package/dist_ts/standards/rules/docs.component-index.js +84 -0
  67. package/dist_ts/standards/rules/element.exported.d.ts +3 -0
  68. package/dist_ts/standards/rules/element.exported.js +33 -0
  69. package/dist_ts/standards/rules/element.literal-tag.d.ts +3 -0
  70. package/dist_ts/standards/rules/element.literal-tag.js +30 -0
  71. package/dist_ts/standards/rules/element.reachable.d.ts +3 -0
  72. package/dist_ts/standards/rules/element.reachable.js +48 -0
  73. package/dist_ts/standards/rules/element.tag-map.d.ts +3 -0
  74. package/dist_ts/standards/rules/element.tag-map.js +83 -0
  75. package/dist_ts/standards/rules/i18n.literals.d.ts +3 -0
  76. package/dist_ts/standards/rules/i18n.literals.js +52 -0
  77. package/dist_ts/standards/rules/index.d.ts +12 -0
  78. package/dist_ts/standards/rules/index.js +83 -0
  79. package/dist_ts/standards/rules/layout.element-folder.d.ts +3 -0
  80. package/dist_ts/standards/rules/layout.element-folder.js +47 -0
  81. package/dist_ts/standards/rules/layout.entry-file-name.d.ts +3 -0
  82. package/dist_ts/standards/rules/layout.entry-file-name.js +71 -0
  83. package/dist_ts/standards/rules/layout.entry.d.ts +3 -0
  84. package/dist_ts/standards/rules/layout.entry.js +38 -0
  85. package/dist_ts/standards/rules/layout.group-folder.d.ts +3 -0
  86. package/dist_ts/standards/rules/layout.group-folder.js +22 -0
  87. package/dist_ts/standards/rules/layout.shared-00.d.ts +3 -0
  88. package/dist_ts/standards/rules/layout.shared-00.js +36 -0
  89. package/dist_ts/standards/rules/naming.class-matches-tag.d.ts +3 -0
  90. package/dist_ts/standards/rules/naming.class-matches-tag.js +26 -0
  91. package/dist_ts/standards/rules/naming.lowercase-files.d.ts +3 -0
  92. package/dist_ts/standards/rules/naming.lowercase-files.js +26 -0
  93. package/dist_ts/standards/rules/naming.tag-prefix.d.ts +3 -0
  94. package/dist_ts/standards/rules/naming.tag-prefix.js +23 -0
  95. package/dist_ts/standards/rules/props.accessor.d.ts +3 -0
  96. package/dist_ts/standards/rules/props.accessor.js +35 -0
  97. package/dist_ts/standards/rules/props.object-no-attribute.d.ts +3 -0
  98. package/dist_ts/standards/rules/props.object-no-attribute.js +42 -0
  99. package/dist_ts/standards/rules/props.reflect-primitive.d.ts +5 -0
  100. package/dist_ts/standards/rules/props.reflect-primitive.js +41 -0
  101. package/dist_ts/standards/rules/props.typed.d.ts +6 -0
  102. package/dist_ts/standards/rules/props.typed.js +64 -0
  103. package/dist_ts/standards/rules/shared.d.ts +38 -0
  104. package/dist_ts/standards/rules/shared.js +103 -0
  105. package/dist_ts/standards/rules/style.bdtheme.d.ts +5 -0
  106. package/dist_ts/standards/rules/style.bdtheme.js +28 -0
  107. package/dist_ts_interfaces/index.d.ts +1 -0
  108. package/dist_ts_interfaces/index.js +2 -1
  109. package/dist_ts_interfaces/requests.d.ts +35 -0
  110. package/dist_ts_interfaces/standard.d.ts +93 -0
  111. package/dist_ts_interfaces/standard.js +6 -0
  112. package/dist_ts_shell/bundlestatus.d.ts +15 -0
  113. package/dist_ts_shell/bundlestatus.js +64 -0
  114. package/dist_ts_shell/elements/wcc-contextmenu.d.ts +3 -0
  115. package/dist_ts_shell/elements/wcc-contextmenu.js +18 -3
  116. package/dist_ts_shell/elements/wcc-preview-frame.d.ts +6 -0
  117. package/dist_ts_shell/elements/wcc-preview-frame.js +50 -2
  118. package/dist_ts_shell/elements/wcc-shell.d.ts +15 -0
  119. package/dist_ts_shell/elements/wcc-shell.js +45 -3
  120. package/dist_ts_shell/plugins.d.ts +2 -1
  121. package/dist_ts_shell/plugins.js +3 -2
  122. package/dist_ts_web/00_commitinfo_data.js +1 -1
  123. package/package.json +8 -3
  124. package/readme.md +40 -3
  125. package/ts/00_commitinfo_data.ts +1 -1
  126. package/ts/classes.bundlestatus.ts +123 -0
  127. package/ts/classes.devapi.ts +9 -0
  128. package/ts/classes.devserver.ts +127 -47
  129. package/ts/cli.ts +12 -2
  130. package/ts/index.ts +0 -2
  131. package/ts/plugins.ts +10 -2
  132. package/ts/standards/baselines.ts +100 -0
  133. package/ts/standards/checker.ts +114 -0
  134. package/ts/standards/classes.gitrepo.ts +48 -0
  135. package/ts/standards/classes.sourceedits.ts +58 -0
  136. package/ts/standards/classes.standardfixer.ts +234 -0
  137. package/ts/standards/classes.standardproject.ts +250 -0
  138. package/ts/standards/classes.standardtree.ts +220 -0
  139. package/ts/standards/cli.ts +86 -0
  140. package/ts/standards/config.ts +160 -0
  141. package/ts/standards/errors.ts +18 -0
  142. package/ts/standards/helpers.ast.ts +208 -0
  143. package/ts/standards/helpers.paths.ts +119 -0
  144. package/ts/standards/index.ts +22 -0
  145. package/ts/standards/output.ts +65 -0
  146. package/ts/standards/ratchet.ts +63 -0
  147. package/ts/standards/rule.ts +58 -0
  148. package/ts/standards/rules/barrel.element-index.ts +112 -0
  149. package/ts/standards/rules/barrel.no-demo-export.ts +30 -0
  150. package/ts/standards/rules/demo.fixture-not-exported.ts +30 -0
  151. package/ts/standards/rules/demo.groups.ts +74 -0
  152. package/ts/standards/rules/demo.required.ts +49 -0
  153. package/ts/standards/rules/demo.separate-file.ts +40 -0
  154. package/ts/standards/rules/demo.shape.ts +42 -0
  155. package/ts/standards/rules/docs.component-index.ts +88 -0
  156. package/ts/standards/rules/element.exported.ts +35 -0
  157. package/ts/standards/rules/element.literal-tag.ts +30 -0
  158. package/ts/standards/rules/element.reachable.ts +49 -0
  159. package/ts/standards/rules/element.tag-map.ts +87 -0
  160. package/ts/standards/rules/i18n.literals.ts +58 -0
  161. package/ts/standards/rules/index.ts +87 -0
  162. package/ts/standards/rules/layout.element-folder.ts +49 -0
  163. package/ts/standards/rules/layout.entry-file-name.ts +73 -0
  164. package/ts/standards/rules/layout.entry.ts +40 -0
  165. package/ts/standards/rules/layout.group-folder.ts +23 -0
  166. package/ts/standards/rules/layout.shared-00.ts +37 -0
  167. package/ts/standards/rules/naming.class-matches-tag.ts +27 -0
  168. package/ts/standards/rules/naming.lowercase-files.ts +27 -0
  169. package/ts/standards/rules/naming.tag-prefix.ts +24 -0
  170. package/ts/standards/rules/props.accessor.ts +37 -0
  171. package/ts/standards/rules/props.object-no-attribute.ts +45 -0
  172. package/ts/standards/rules/props.reflect-primitive.ts +44 -0
  173. package/ts/standards/rules/props.typed.ts +70 -0
  174. package/ts/standards/rules/shared.ts +127 -0
  175. package/ts/standards/rules/style.bdtheme.ts +31 -0
  176. package/ts_interfaces/bridge.ts +95 -0
  177. package/ts_interfaces/catalog.ts +37 -0
  178. package/ts_interfaces/index.ts +4 -0
  179. package/ts_interfaces/plugins.ts +3 -0
  180. package/ts_interfaces/requests.ts +65 -0
  181. package/ts_interfaces/standard.ts +103 -0
  182. package/ts_shared/index.ts +1 -0
  183. package/ts_shared/plugins.ts +3 -0
  184. package/ts_shared/previewroute.ts +58 -0
  185. package/ts_shell/bundlestatus.ts +68 -0
  186. package/ts_shell/elements/wcc-contextmenu.ts +306 -0
  187. package/ts_shell/elements/wcc-preview-frame.ts +167 -0
  188. package/ts_shell/elements/wcc-properties.ts +998 -0
  189. package/ts_shell/elements/wcc-record-button.ts +108 -0
  190. package/ts_shell/elements/wcc-recording-panel.ts +1005 -0
  191. package/ts_shell/elements/wcc-shell.ts +485 -0
  192. package/ts_shell/elements/wcc-sidebar.ts +1419 -0
  193. package/ts_shell/index.html +21 -0
  194. package/ts_shell/index.ts +4 -0
  195. package/ts_shell/plugins.ts +12 -0
  196. package/ts_shell/previewconnection.ts +103 -0
  197. package/ts_shell/services/recorder.service.ts +451 -0
  198. package/ts_shell/types/dom-mediacapture-stub/index.d.ts +12 -0
  199. package/ts_shell/types/dom-mediacapture-stub/package.json +6 -0
  200. package/ts_shell/types/dom-webcodecs-stub/index.d.ts +2 -0
  201. package/ts_shell/types/dom-webcodecs-stub/package.json +6 -0
  202. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,87 @@
1
+ import type { IStandardRule } from '../rule.js';
2
+ import { barrelElementIndexRule } from './barrel.element-index.js';
3
+ import { barrelNoDemoExportRule } from './barrel.no-demo-export.js';
4
+ import { demoFixtureNotExportedRule } from './demo.fixture-not-exported.js';
5
+ import { demoGroupsRule } from './demo.groups.js';
6
+ import { demoRequiredRule } from './demo.required.js';
7
+ import { demoSeparateFileRule } from './demo.separate-file.js';
8
+ import { demoShapeRule } from './demo.shape.js';
9
+ import { docsComponentIndexRule } from './docs.component-index.js';
10
+ import { elementExportedRule } from './element.exported.js';
11
+ import { elementLiteralTagRule } from './element.literal-tag.js';
12
+ import { elementReachableRule } from './element.reachable.js';
13
+ import { elementTagMapRule } from './element.tag-map.js';
14
+ import { i18nLiteralsRule } from './i18n.literals.js';
15
+ import { layoutElementFolderRule } from './layout.element-folder.js';
16
+ import { layoutEntryFileNameRule } from './layout.entry-file-name.js';
17
+ import { layoutEntryRule } from './layout.entry.js';
18
+ import { layoutGroupFolderRule } from './layout.group-folder.js';
19
+ import { layoutShared00Rule } from './layout.shared-00.js';
20
+ import { namingClassMatchesTagRule } from './naming.class-matches-tag.js';
21
+ import { namingLowercaseFilesRule } from './naming.lowercase-files.js';
22
+ import { namingTagPrefixRule } from './naming.tag-prefix.js';
23
+ import { propsAccessorRule } from './props.accessor.js';
24
+ import { propsObjectNoAttributeRule } from './props.object-no-attribute.js';
25
+ import { propsReflectPrimitiveRule } from './props.reflect-primitive.js';
26
+ import { propsTypedRule } from './props.typed.js';
27
+ import { styleBdThemeRule } from './style.bdtheme.js';
28
+
29
+ export { reflectExemptNames } from './props.reflect-primitive.js';
30
+ export { renderComponentIndex } from './docs.component-index.js';
31
+
32
+ /** Every rule of docs/standard.md, in the order of the document. */
33
+ export const standardRules: readonly IStandardRule[] = [
34
+ layoutEntryRule,
35
+ layoutElementFolderRule,
36
+ layoutEntryFileNameRule,
37
+ layoutGroupFolderRule,
38
+ layoutShared00Rule,
39
+ namingLowercaseFilesRule,
40
+ namingTagPrefixRule,
41
+ namingClassMatchesTagRule,
42
+ elementLiteralTagRule,
43
+ elementExportedRule,
44
+ elementTagMapRule,
45
+ elementReachableRule,
46
+ barrelElementIndexRule,
47
+ barrelNoDemoExportRule,
48
+ demoShapeRule,
49
+ demoRequiredRule,
50
+ demoSeparateFileRule,
51
+ demoGroupsRule,
52
+ demoFixtureNotExportedRule,
53
+ propsAccessorRule,
54
+ propsTypedRule,
55
+ propsReflectPrimitiveRule,
56
+ propsObjectNoAttributeRule,
57
+ styleBdThemeRule,
58
+ i18nLiteralsRule,
59
+ docsComponentIndexRule,
60
+ ];
61
+
62
+ /**
63
+ * The order `wcctools fix` applies rules in: content edits first, then file moves (which rewrite imports),
64
+ * then barrels and reachability on the final layout, then the generated docs.
65
+ */
66
+ export const fixOrder: readonly string[] = [
67
+ 'element/exported',
68
+ 'element/tag-map',
69
+ 'props/accessor',
70
+ 'props/typed',
71
+ 'props/object-no-attribute',
72
+ 'props/reflect-primitive',
73
+ 'demo/groups',
74
+ 'demo/required',
75
+ 'barrel/no-demo-export',
76
+ 'demo/fixture-not-exported',
77
+ 'naming/lowercase-files',
78
+ 'layout/shared-00',
79
+ 'layout/entry-file-name',
80
+ 'layout/element-folder',
81
+ 'barrel/element-index',
82
+ 'element/reachable',
83
+ 'docs/component-index',
84
+ ];
85
+
86
+ /** Findings the checker itself produces for the allowlist. */
87
+ export const allowlistStaleRuleId = 'allowlist/stale';
@@ -0,0 +1,49 @@
1
+ import { baseOf, dirOf } from '../helpers.paths.js';
2
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
3
+ import { catalogElements, elementKey, elementsDir, isCustomElementName, isElementParentDir, isInElementFolder } from './shared.js';
4
+
5
+ /** Every element's entry file lives in its own element folder `ts_web/elements/[00group-<area>/]<tag>/<tag>.ts`. */
6
+ export const layoutElementFolderRule: IStandardRule = {
7
+ id: 'layout/element-folder',
8
+ severity: { base: 'warn' },
9
+ fixMode: 'auto',
10
+ detect: ({ project }) => {
11
+ const findings: IRuleFinding[] = [];
12
+ const elements = catalogElements(project);
13
+ for (const element of elements) {
14
+ if (!element.tag || isInElementFolder(element)) {
15
+ continue;
16
+ }
17
+ const currentDir = dirOf(element.file);
18
+ const group = /^ts_web\/elements\/(00group-[^/]+)(\/|$)/.exec(currentDir)?.[1];
19
+ const folder = `${elementsDir}/${group ? `${group}/` : ''}${element.tag}`;
20
+ const finding: IRuleFinding = {
21
+ file: element.file,
22
+ position: element.decoratorCall.getStart(),
23
+ key: elementKey(element),
24
+ message: `<${element.tag}> is declared in ${element.file}; its element folder is ${folder}/`,
25
+ };
26
+ const elementsInFile = elements.filter((otherArg) => otherArg.file === element.file);
27
+ const folderTaken = [...project.files.keys()].some((fileArg) => fileArg.startsWith(`${folder}/`)) || project.exists(folder);
28
+ // Only flat files move mechanically: a misnamed folder holds more than this element. The folder
29
+ // is named after the tag, so only a valid custom element name is usable.
30
+ if (isCustomElementName(element.tag) && elementsInFile.length === 1 && isElementParentDir(currentDir) && !folderTaken) {
31
+ const tag = element.tag;
32
+ const companions = [...project.files.keys()].filter((fileArg) =>
33
+ fileArg !== element.file && dirOf(fileArg) === currentDir && baseOf(fileArg).startsWith(`${tag}.`) && baseOf(fileArg) !== 'index.ts');
34
+ finding.fix = (builderArg) => {
35
+ const entry = `${folder}/${tag}.ts`;
36
+ const index = `${folder}/index.ts`;
37
+ builderArg.create(index, `export * from './${tag}.js';\n`);
38
+ builderArg.move(element.file, entry);
39
+ for (const companion of companions) {
40
+ builderArg.move(companion, `${folder}/${baseOf(companion)}`);
41
+ }
42
+ builderArg.redirectBarrelExports(element.file, index);
43
+ };
44
+ }
45
+ findings.push(finding);
46
+ }
47
+ return findings;
48
+ },
49
+ };
@@ -0,0 +1,73 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import * as ast from '../helpers.ast.js';
3
+ import { baseOf, dirOf, isDemoFile, resolveSpecifier } from '../helpers.paths.js';
4
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
5
+ import { catalogElements, elementKey, isCustomElementName, isInElementFolder, rootIdentifier } from './shared.js';
6
+
7
+ const ts = plugins.typescript;
8
+
9
+ /** Inside an element folder the entry file is `<tag>.ts` and the demo file is `<tag>.demo.ts`. */
10
+ export const layoutEntryFileNameRule: IStandardRule = {
11
+ id: 'layout/entry-file-name',
12
+ severity: { base: 'warn' },
13
+ fixMode: 'auto',
14
+ detect: ({ project }) => {
15
+ const findings: IRuleFinding[] = [];
16
+ const elements = catalogElements(project);
17
+ for (const element of elements) {
18
+ if (!element.tag || !isInElementFolder(element)) {
19
+ continue;
20
+ }
21
+ const tag = element.tag;
22
+ // The expected file names come from the tag: only a valid custom element name is usable for a move.
23
+ const usableTag = isCustomElementName(tag);
24
+ const folder = dirOf(element.file);
25
+ const entry = `${folder}/${tag}.ts`;
26
+ if (element.file !== entry) {
27
+ const finding: IRuleFinding = {
28
+ file: element.file,
29
+ position: element.decoratorCall.getStart(),
30
+ key: elementKey(element),
31
+ message: `the entry file of <${tag}> is ${baseOf(element.file)}; expected ${tag}.ts`,
32
+ };
33
+ if (usableTag && elements.filter((otherArg) => otherArg.file === element.file).length === 1 && !project.exists(entry)) {
34
+ finding.fix = (builderArg) => builderArg.move(element.file, entry);
35
+ }
36
+ findings.push(finding);
37
+ }
38
+ // The demo file the static demo is imported from.
39
+ const demoMember = ast.findStaticProperty(element.classNode, 'demo');
40
+ const initializer = demoMember?.initializer;
41
+ if (!initializer) {
42
+ continue;
43
+ }
44
+ const references = ts.isArrayLiteralExpression(initializer) ? [...initializer.elements] : [initializer];
45
+ const demoFiles = new Set<string>();
46
+ for (const reference of references) {
47
+ const identifier = rootIdentifier(reference);
48
+ const source = identifier ? ast.importSourceOf(element.sourceFile, identifier) : null;
49
+ const resolved = source ? resolveSpecifier(element.file, source, (fileArg) => project.files.has(fileArg)) : null;
50
+ if (resolved && isDemoFile(resolved.target) && dirOf(resolved.target) === folder) {
51
+ demoFiles.add(resolved.target);
52
+ }
53
+ }
54
+ const expectedDemo = `${folder}/${tag}.demo.ts`;
55
+ for (const demoFile of demoFiles) {
56
+ if (demoFile === expectedDemo) {
57
+ continue;
58
+ }
59
+ const finding: IRuleFinding = {
60
+ file: element.file,
61
+ position: demoMember!.getStart(),
62
+ key: elementKey(element),
63
+ message: `the demo file of <${tag}> is ${baseOf(demoFile)}; expected ${tag}.demo.ts`,
64
+ };
65
+ if (usableTag && demoFiles.size === 1 && !project.exists(expectedDemo)) {
66
+ finding.fix = (builderArg) => builderArg.move(demoFile, expectedDemo);
67
+ }
68
+ findings.push(finding);
69
+ }
70
+ }
71
+ return findings;
72
+ },
73
+ };
@@ -0,0 +1,40 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import * as ast from '../helpers.ast.js';
3
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
4
+
5
+ const ts = plugins.typescript;
6
+
7
+ /** `html/index.ts` exists and calls `setupWccTools({ sections })`; `html/index.html` exists. */
8
+ export const layoutEntryRule: IStandardRule = {
9
+ id: 'layout/entry',
10
+ severity: { base: 'error' },
11
+ fixMode: 'none',
12
+ detect: ({ project }) => {
13
+ const findings: IRuleFinding[] = [];
14
+ const entry = 'html/index.ts';
15
+ if (!project.files.has(entry)) {
16
+ findings.push({ file: entry, position: 0, key: entry, message: 'html/index.ts is missing' });
17
+ } else {
18
+ let found = false;
19
+ const visit = (nodeArg: ast.TNode): void => {
20
+ if (ts.isCallExpression(nodeArg) && ast.calleeName(nodeArg.expression) === 'setupWccTools') {
21
+ const config = nodeArg.arguments[0];
22
+ if (config && ts.isObjectLiteralExpression(config) && config.properties.some((propertyArg) =>
23
+ (ts.isPropertyAssignment(propertyArg) || ts.isShorthandPropertyAssignment(propertyArg))
24
+ && ts.isIdentifier(propertyArg.name) && propertyArg.name.text === 'sections')) {
25
+ found = true;
26
+ }
27
+ }
28
+ ts.forEachChild(nodeArg, visit);
29
+ };
30
+ visit(project.sourceFile(entry));
31
+ if (!found) {
32
+ findings.push({ file: entry, position: 0, key: entry, message: 'html/index.ts does not call setupWccTools({ sections })' });
33
+ }
34
+ }
35
+ if (!project.files.has('html/index.html')) {
36
+ findings.push({ file: 'html/index.html', position: 0, key: 'html/index.html', message: 'html/index.html is missing' });
37
+ }
38
+ return findings;
39
+ },
40
+ };
@@ -0,0 +1,23 @@
1
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
2
+ import { catalogElements, elementKey } from './shared.js';
3
+
4
+ /** dees: element folders sit under `ts_web/elements/00group-<area>/`. */
5
+ export const layoutGroupFolderRule: IStandardRule = {
6
+ id: 'layout/group-folder',
7
+ severity: { dees: 'warn' },
8
+ fixMode: 'none',
9
+ detect: ({ project }) => {
10
+ const findings: IRuleFinding[] = [];
11
+ for (const element of catalogElements(project)) {
12
+ if (element.file.startsWith('ts_web/elements/') && !/^ts_web\/elements\/00group-[^/]+\//.test(element.file)) {
13
+ findings.push({
14
+ file: element.file,
15
+ position: element.decoratorCall.getStart(),
16
+ key: elementKey(element),
17
+ message: `<${element.tag ?? '?'}> is not inside a ts_web/elements/00group-<area>/ folder`,
18
+ });
19
+ }
20
+ }
21
+ return findings;
22
+ },
23
+ };
@@ -0,0 +1,37 @@
1
+ import { baseOf, dirOf, isDemoFile } from '../helpers.paths.js';
2
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
3
+ import { elementsDir } from './shared.js';
4
+
5
+ /** A module at the root of `ts_web/elements/` that declares no element is named `00<name>.ts`. */
6
+ export const layoutShared00Rule: IStandardRule = {
7
+ id: 'layout/shared-00',
8
+ severity: { base: 'info' },
9
+ fixMode: 'auto',
10
+ detect: ({ project }) => {
11
+ const findings: IRuleFinding[] = [];
12
+ const rootElements = project.elements.filter((elementArg) => dirOf(elementArg.file) === elementsDir);
13
+ const elementFiles = new Set(project.elements.map((elementArg) => elementArg.file));
14
+ for (const file of project.webFiles) {
15
+ const name = baseOf(file);
16
+ if (dirOf(file) !== elementsDir || name === 'index.ts' || name.startsWith('00') || isDemoFile(file) || elementFiles.has(file)) {
17
+ continue;
18
+ }
19
+ // Companion files of a flat element (`<tag>.styles.ts`) move with the element instead.
20
+ if (rootElements.some((elementArg) => elementArg.tag && name.startsWith(`${elementArg.tag}.`))) {
21
+ continue;
22
+ }
23
+ const target = `${elementsDir}/00${name}`;
24
+ const finding: IRuleFinding = {
25
+ file,
26
+ position: 0,
27
+ key: file,
28
+ message: `${name} declares no element; shared modules at the root of ts_web/elements are named 00${name}`,
29
+ };
30
+ if (!project.exists(target)) {
31
+ finding.fix = (builderArg) => builderArg.move(file, target);
32
+ }
33
+ findings.push(finding);
34
+ }
35
+ return findings;
36
+ },
37
+ };
@@ -0,0 +1,27 @@
1
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
2
+ import { catalogElements, elementKey } from './shared.js';
3
+
4
+ /** The class name, lowercased, equals the tag with hyphens removed. */
5
+ export const namingClassMatchesTagRule: IStandardRule = {
6
+ id: 'naming/class-matches-tag',
7
+ severity: { base: 'warn' },
8
+ fixMode: 'none',
9
+ allowlistCategories: ['published'],
10
+ detect: ({ project }) => {
11
+ const findings: IRuleFinding[] = [];
12
+ for (const element of catalogElements(project)) {
13
+ if (!element.tag || !element.className) {
14
+ continue;
15
+ }
16
+ if (element.className.toLowerCase() !== element.tag.replace(/-/g, '')) {
17
+ findings.push({
18
+ file: element.file,
19
+ position: element.classNode.name!.getStart(),
20
+ key: elementKey(element),
21
+ message: `class ${element.className} does not match <${element.tag}>`,
22
+ });
23
+ }
24
+ }
25
+ return findings;
26
+ },
27
+ };
@@ -0,0 +1,27 @@
1
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
2
+
3
+ /** File and folder names under `ts_web/` are lowercase. */
4
+ export const namingLowercaseFilesRule: IStandardRule = {
5
+ id: 'naming/lowercase-files',
6
+ severity: { base: 'error' },
7
+ fixMode: 'auto',
8
+ detect: ({ project }) => {
9
+ const findings: IRuleFinding[] = [];
10
+ const lowered = new Map<string, number>();
11
+ for (const file of project.files.keys()) {
12
+ lowered.set(file.toLowerCase(), (lowered.get(file.toLowerCase()) ?? 0) + 1);
13
+ }
14
+ for (const file of project.webFiles) {
15
+ if (!/[A-Z]/.test(file)) {
16
+ continue;
17
+ }
18
+ const target = file.toLowerCase();
19
+ const finding: IRuleFinding = { file, position: 0, key: file, message: `${file} is not lowercase; expected ${target}` };
20
+ if (lowered.get(target) === 1 && !project.exists(target)) {
21
+ finding.fix = (builderArg) => builderArg.move(file, target);
22
+ }
23
+ findings.push(finding);
24
+ }
25
+ return findings;
26
+ },
27
+ };
@@ -0,0 +1,24 @@
1
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
2
+ import { elementKey } from './shared.js';
3
+
4
+ /** Every tag starts with `<tagPrefix>-`. Applies to fixture elements in demo files too: they share the page. */
5
+ export const namingTagPrefixRule: IStandardRule = {
6
+ id: 'naming/tag-prefix',
7
+ severity: { base: 'error' },
8
+ fixMode: 'none',
9
+ allowlistCategories: ['published'],
10
+ detect: ({ project, config }) => {
11
+ const findings: IRuleFinding[] = [];
12
+ for (const element of project.elements) {
13
+ if (element.tag && !element.tag.startsWith(`${config.tagPrefix}-`)) {
14
+ findings.push({
15
+ file: element.file,
16
+ position: element.decoratorCall.getStart(),
17
+ key: elementKey(element),
18
+ message: `<${element.tag}> does not start with the tag prefix '${config.tagPrefix}-'`,
19
+ });
20
+ }
21
+ }
22
+ return findings;
23
+ },
24
+ };
@@ -0,0 +1,37 @@
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 { propertyKey } from './shared.js';
5
+
6
+ const ts = plugins.typescript;
7
+
8
+ /** Members decorated with `@property` or `@state` use the `accessor` keyword (standard decorators). */
9
+ export const propsAccessorRule: IStandardRule = {
10
+ id: 'props/accessor',
11
+ severity: { base: 'error' },
12
+ fixMode: 'auto',
13
+ detect: ({ project }) => {
14
+ const findings: IRuleFinding[] = [];
15
+ if (project.experimentalDecorators) {
16
+ return findings;
17
+ }
18
+ for (const property of project.properties) {
19
+ const member = property.member;
20
+ if (!ts.isPropertyDeclaration(member) || ast.hasModifier(member, ts.SyntaxKind.AccessorKeyword)) {
21
+ continue;
22
+ }
23
+ const finding: IRuleFinding = {
24
+ file: property.file,
25
+ position: member.name.getStart(),
26
+ key: propertyKey(property),
27
+ message: `@${property.decorator} ${property.name} is a field; standard decorators need 'accessor ${property.name}'`,
28
+ };
29
+ if (!ast.hasModifier(member, ts.SyntaxKind.DeclareKeyword)) {
30
+ const at = member.name.getStart();
31
+ finding.fix = (builderArg) => builderArg.edit(property.file, at, at, 'accessor ');
32
+ }
33
+ findings.push(finding);
34
+ }
35
+ return findings;
36
+ },
37
+ };
@@ -0,0 +1,45 @@
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 { addOptionFix } from './props.typed.js';
5
+ import { coreTypeOf, isArrayTypeNode, propertyKey, publicProperties } from './shared.js';
6
+
7
+ const ts = plugins.typescript;
8
+
9
+ /** An array, object literal type or `I`-prefixed interface annotation. */
10
+ const isObjectAnnotation = (typeArg: ast.TTypeNode | null): boolean => {
11
+ if (!typeArg) {
12
+ return false;
13
+ }
14
+ return isArrayTypeNode(typeArg)
15
+ || ts.isTypeLiteralNode(typeArg)
16
+ || (ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && /^I[A-Z]/.test(typeArg.typeName.text));
17
+ };
18
+
19
+ /** A public property holding an object or array declares `type: Object`/`type: Array` or `attribute: false`. */
20
+ export const propsObjectNoAttributeRule: IStandardRule = {
21
+ id: 'props/object-no-attribute',
22
+ severity: { base: 'warn' },
23
+ fixMode: 'auto',
24
+ detect: ({ project }) => {
25
+ const findings: IRuleFinding[] = [];
26
+ for (const property of publicProperties(project)) {
27
+ if (property.dynamicOptions || !isObjectAnnotation(coreTypeOf(property))) {
28
+ continue;
29
+ }
30
+ const options = property.options;
31
+ const type = options ? ast.objectPropertyText(options, 'type') : null;
32
+ if (type === 'Object' || type === 'Array' || (options && ast.objectPropertyText(options, 'attribute') === 'false')) {
33
+ continue;
34
+ }
35
+ findings.push({
36
+ file: property.file,
37
+ position: property.call.getStart(),
38
+ key: propertyKey(property),
39
+ message: `@property ${property.name} holds an object or array but declares neither type: Object/Array nor attribute: false`,
40
+ fix: addOptionFix(property, 'attribute: false', 'last'),
41
+ });
42
+ }
43
+ return findings;
44
+ },
45
+ };
@@ -0,0 +1,44 @@
1
+ import * as ast from '../helpers.ast.js';
2
+ import type { IRuleFinding, IStandardRule } from '../rule.js';
3
+ import { addOptionFix } from './props.typed.js';
4
+ import { propertyKey, publicProperties } from './shared.js';
5
+
6
+ /** Global attributes with native behaviour that stay unreflected without an allowlist entry. */
7
+ export const reflectExemptNames: readonly string[] = ['title', 'hidden', 'dir', 'lang', 'role', 'inert', 'draggable'];
8
+
9
+ const primitiveTypes = ['String', 'Number', 'Boolean'];
10
+
11
+ /** A primitive public property declares `reflect: true` or `attribute: false`. */
12
+ export const propsReflectPrimitiveRule: IStandardRule = {
13
+ id: 'props/reflect-primitive',
14
+ severity: { base: 'error' },
15
+ fixMode: 'opt-in',
16
+ allowlistCategories: ['user-data', 'object', 'large-text', 'global-attribute'],
17
+ detect: ({ project }) => {
18
+ const findings: IRuleFinding[] = [];
19
+ for (const property of publicProperties(project)) {
20
+ if (property.dynamicOptions || reflectExemptNames.includes(property.name)) {
21
+ continue;
22
+ }
23
+ const options = property.options;
24
+ const type = options ? ast.objectPropertyText(options, 'type') : null;
25
+ if (type !== null && !primitiveTypes.includes(type)) {
26
+ continue;
27
+ }
28
+ if (options && (ast.objectPropertyText(options, 'attribute') === 'false' || ast.objectPropertyText(options, 'reflect') === 'true')) {
29
+ continue;
30
+ }
31
+ const explicitReflect = options ? ast.objectProperty(options, 'reflect') : null;
32
+ findings.push({
33
+ file: property.file,
34
+ position: property.call.getStart(),
35
+ key: propertyKey(property),
36
+ message: `public primitive @property ${property.name} declares neither reflect: true nor attribute: false`,
37
+ fix: explicitReflect
38
+ ? (builderArg) => builderArg.edit(property.file, explicitReflect.initializer.getStart(), explicitReflect.initializer.end, 'true')
39
+ : addOptionFix(property, 'reflect: true', 'last'),
40
+ });
41
+ }
42
+ return findings;
43
+ },
44
+ };
@@ -0,0 +1,70 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import * as ast from '../helpers.ast.js';
3
+ import type { IStandardProperty } from '../classes.standardproject.js';
4
+ import type { IRuleFinding, IStandardRule, TFixAction } from '../rule.js';
5
+ import { coreTypeOf, isArrayTypeNode, propertyKey, publicProperties } from './shared.js';
6
+
7
+ const ts = plugins.typescript;
8
+
9
+ /** The Lit `type` that the property's annotation maps to, or null when the annotation does not decide it. */
10
+ const litTypeFor = (propertyArg: IStandardProperty): string | null => {
11
+ const typeNode = coreTypeOf(propertyArg);
12
+ if (!typeNode) {
13
+ return null;
14
+ }
15
+ switch (typeNode.kind) {
16
+ case ts.SyntaxKind.StringKeyword:
17
+ return 'String';
18
+ case ts.SyntaxKind.NumberKeyword:
19
+ return 'Number';
20
+ case ts.SyntaxKind.BooleanKeyword:
21
+ return 'Boolean';
22
+ }
23
+ if (ts.isUnionTypeNode(typeNode) && typeNode.types.every((typeArg) =>
24
+ (ts.isLiteralTypeNode(typeArg) && ts.isStringLiteral(typeArg.literal))
25
+ || typeArg.kind === ts.SyntaxKind.UndefinedKeyword
26
+ || (ts.isLiteralTypeNode(typeArg) && typeArg.literal.kind === ts.SyntaxKind.NullKeyword))) {
27
+ return 'String';
28
+ }
29
+ if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
30
+ return 'String';
31
+ }
32
+ return isArrayTypeNode(typeNode) ? 'Array' : null;
33
+ };
34
+
35
+ /** The edit that adds `propertyTextArg` to a property decorator's options, creating the options literal when absent. */
36
+ export const addOptionFix = (propertyArg: IStandardProperty, propertyTextArg: string, placementArg: 'first' | 'last'): TFixAction => (builderArg) => {
37
+ if (propertyArg.options) {
38
+ const edit = placementArg === 'first'
39
+ ? ast.prependObjectPropertyEdit(propertyArg.options, propertyTextArg)
40
+ : ast.appendObjectPropertyEdit(propertyArg.options, propertyTextArg);
41
+ builderArg.edit(propertyArg.file, edit.start, edit.end, edit.text);
42
+ return;
43
+ }
44
+ const at = propertyArg.call.arguments.pos;
45
+ builderArg.edit(propertyArg.file, at, at, `{ ${propertyTextArg} }`);
46
+ };
47
+
48
+ /** A public property declares `type`. */
49
+ export const propsTypedRule: IStandardRule = {
50
+ id: 'props/typed',
51
+ severity: { base: 'warn' },
52
+ fixMode: 'auto',
53
+ detect: ({ project }) => {
54
+ const findings: IRuleFinding[] = [];
55
+ for (const property of publicProperties(project)) {
56
+ if (property.dynamicOptions || (property.options && ast.objectProperty(property.options, 'type'))) {
57
+ continue;
58
+ }
59
+ const litType = litTypeFor(property);
60
+ findings.push({
61
+ file: property.file,
62
+ position: property.call.getStart(),
63
+ key: propertyKey(property),
64
+ message: `@property ${property.name} declares no type`,
65
+ fix: litType ? addOptionFix(property, `type: ${litType}`, 'first') : undefined,
66
+ });
67
+ }
68
+ return findings;
69
+ },
70
+ };