@craft-ts/dev-tools 0.7.0-beta.13
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/LICENSE +21 -0
- package/README.md +154 -0
- package/generators.json +26 -0
- package/package.json +119 -0
- package/src/bin/craft-architecture-check.d.ts +2 -0
- package/src/bin/craft-architecture-check.js +51 -0
- package/src/bin/craft-architecture-check.js.map +1 -0
- package/src/bin/craft-brand.d.ts +2 -0
- package/src/bin/craft-brand.js +9 -0
- package/src/bin/craft-brand.js.map +1 -0
- package/src/bin/craft-graph.d.ts +2 -0
- package/src/bin/craft-graph.js +72 -0
- package/src/bin/craft-graph.js.map +1 -0
- package/src/bin/craft-migrate-architecture.d.ts +2 -0
- package/src/bin/craft-migrate-architecture.js +64 -0
- package/src/bin/craft-migrate-architecture.js.map +1 -0
- package/src/bin/craft-migrate-components.d.ts +2 -0
- package/src/bin/craft-migrate-components.js +67 -0
- package/src/bin/craft-migrate-components.js.map +1 -0
- package/src/bin/craft-migrate-primitives.d.ts +2 -0
- package/src/bin/craft-migrate-primitives.js +71 -0
- package/src/bin/craft-migrate-primitives.js.map +1 -0
- package/src/bin/craft-migrate-routes.d.ts +2 -0
- package/src/bin/craft-migrate-routes.js +77 -0
- package/src/bin/craft-migrate-routes.js.map +1 -0
- package/src/bin/craft-migrate-services.d.ts +2 -0
- package/src/bin/craft-migrate-services.js +75 -0
- package/src/bin/craft-migrate-services.js.map +1 -0
- package/src/bin/craft-migrate-template.d.ts +2 -0
- package/src/bin/craft-migrate-template.js +68 -0
- package/src/bin/craft-migrate-template.js.map +1 -0
- package/src/bin/craft-migrate.d.ts +2 -0
- package/src/bin/craft-migrate.js +91 -0
- package/src/bin/craft-migrate.js.map +1 -0
- package/src/bin/craft.d.ts +2 -0
- package/src/bin/craft.js +318 -0
- package/src/bin/craft.js.map +1 -0
- package/src/eslint-rules/anchor-has-href.cjs +35 -0
- package/src/eslint-rules/app-start-registry-match.cjs +355 -0
- package/src/eslint-rules/button-has-type.cjs +50 -0
- package/src/eslint-rules/control-has-accessible-name.cjs +56 -0
- package/src/eslint-rules/craft-component-name-match.cjs +8 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +63 -0
- package/src/eslint-rules/craft-css-token-registry.cjs +17 -0
- package/src/eslint-rules/craft-css-var-naming.cjs +18 -0
- package/src/eslint-rules/craft-css-vars-contract.cjs +34 -0
- package/src/eslint-rules/craft-directive-name-match.cjs +8 -0
- package/src/eslint-rules/craft-method-name-match.cjs +8 -0
- package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
- package/src/eslint-rules/craft-primitive-context.cjs +46 -0
- package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-styles-scope-safe.cjs +25 -0
- package/src/eslint-rules/css-rule-utils.cjs +109 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +562 -0
- package/src/eslint-rules/heading-has-content.cjs +31 -0
- package/src/eslint-rules/html-helpers.cjs +115 -0
- package/src/eslint-rules/hyperscript-walk.cjs +249 -0
- package/src/eslint-rules/iframe-has-title.cjs +26 -0
- package/src/eslint-rules/img-has-alt.cjs +28 -0
- package/src/eslint-rules/index.cjs +209 -0
- package/src/eslint-rules/index.d.cts +5 -0
- package/src/eslint-rules/label-has-associated-control.cjs +67 -0
- package/src/eslint-rules/no-async-await.cjs +46 -0
- package/src/eslint-rules/no-craft-computed-side-effects.cjs +301 -0
- package/src/eslint-rules/no-craft-use-in-template.cjs +89 -0
- package/src/eslint-rules/no-craft-use.cjs +42 -0
- package/src/eslint-rules/no-direct-temporal-globals.cjs +73 -0
- package/src/eslint-rules/no-effect-in-params.cjs +265 -0
- package/src/eslint-rules/no-effect-outside-loaders.cjs +3 -0
- package/src/eslint-rules/no-ephemeral-template-form-state.cjs +131 -0
- package/src/eslint-rules/no-hardcoded-design-values.cjs +15 -0
- package/src/eslint-rules/no-heading-level-skip.cjs +101 -0
- package/src/eslint-rules/no-imperative-craft-resource-trigger.cjs +291 -0
- package/src/eslint-rules/no-important-in-component-styles.cjs +12 -0
- package/src/eslint-rules/no-injection-token.cjs +173 -0
- package/src/eslint-rules/no-invalid-insertion-pipe.cjs +162 -0
- package/src/eslint-rules/no-manual-route-provider-list.cjs +85 -0
- package/src/eslint-rules/no-noninteractive-element-interactions.cjs +66 -0
- package/src/eslint-rules/no-positive-tabindex.cjs +44 -0
- package/src/eslint-rules/no-redundant-primitive-insertion.cjs +361 -0
- package/src/eslint-rules/no-render-writes.cjs +106 -0
- package/src/eslint-rules/no-throw.cjs +132 -0
- package/src/eslint-rules/no-transition-actions.cjs +133 -0
- package/src/eslint-rules/no-type-assertions-in-template.cjs +211 -0
- package/src/eslint-rules/no-widened-route-provider-context.cjs +39 -0
- package/src/eslint-rules/prefer-browser-boundaries.cjs +92 -0
- package/src/eslint-rules/prefer-craft-http-transport.cjs +103 -0
- package/src/eslint-rules/prefer-craft-template-blocks.cjs +492 -0
- package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +105 -0
- package/src/eslint-rules/prefer-named-html-helpers.cjs +33 -0
- package/src/eslint-rules/prefer-query-method-over-state-trigger.cjs +282 -0
- package/src/eslint-rules/prefer-relative-heading.cjs +43 -0
- package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
- package/src/eslint-rules/recommended-config.cjs +77 -0
- package/src/eslint-rules/require-assert-exhaustive-route-exceptions.cjs +259 -0
- package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
- package/src/eslint-rules/require-child-route-mount-check.cjs +224 -0
- package/src/eslint-rules/require-component-monitoring.cjs +212 -0
- package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
- package/src/eslint-rules/require-craft-method-for-yieldable-callback.cjs +236 -0
- package/src/eslint-rules/require-craft-resource-trigger-yield.cjs +209 -0
- package/src/eslint-rules/require-effect-adapters.cjs +86 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
- package/src/eslint-rules/require-focus-visible.cjs +36 -0
- package/src/eslint-rules/require-interactive-local-name.cjs +105 -0
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-outlet-heading-section.cjs +70 -0
- package/src/eslint-rules/require-pending-component-di-check.cjs +490 -0
- package/src/eslint-rules/require-primitive-context.cjs +156 -0
- package/src/eslint-rules/require-primitive-derived-property.cjs +598 -0
- package/src/eslint-rules/require-primitive-generator-unwrap.cjs +274 -0
- package/src/eslint-rules/require-reactive-template-bindings.cjs +305 -0
- package/src/eslint-rules/require-reduced-motion.cjs +38 -0
- package/src/eslint-rules/require-route-heading-outline.cjs +166 -0
- package/src/eslint-rules/require-yieldable-insertion-write.cjs +98 -0
- package/src/eslint-rules/require-yieldable-reactive-read.cjs +102 -0
- package/src/eslint-rules/require-yieldable-template-method.cjs +424 -0
- package/src/eslint-rules/role-has-required-aria.cjs +38 -0
- package/src/eslint-rules/server-function-client-match.cjs +283 -0
- package/src/eslint-rules/target-blank-noopener.cjs +31 -0
- package/src/eslint-rules/template-element-name-unique.cjs +124 -0
- package/src/eslint-rules/valid-aria.cjs +38 -0
- package/src/generators/route/compat.d.ts +3 -0
- package/src/generators/route/compat.js +6 -0
- package/src/generators/route/compat.js.map +1 -0
- package/src/generators/route/generator.d.ts +31 -0
- package/src/generators/route/generator.js +402 -0
- package/src/generators/route/generator.js.map +1 -0
- package/src/generators/route/schema.json +50 -0
- package/src/generators/route/split-schema.json +19 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +23 -0
- package/src/index.js.map +1 -0
- package/src/scripts/angular-brand-codemod.d.ts +139 -0
- package/src/scripts/angular-brand-codemod.js +2318 -0
- package/src/scripts/angular-brand-codemod.js.map +1 -0
- package/src/scripts/angular-brand-codemod.ts +3985 -0
- package/src/scripts/architecture/migrate-architecture.d.ts +14 -0
- package/src/scripts/architecture/migrate-architecture.js +534 -0
- package/src/scripts/architecture/migrate-architecture.js.map +1 -0
- package/src/scripts/architecture/migrate-architecture.ts +653 -0
- package/src/scripts/architecture-graph.d.ts +368 -0
- package/src/scripts/architecture-graph.js +2191 -0
- package/src/scripts/architecture-graph.js.map +1 -0
- package/src/scripts/architecture-graph.ts +3136 -0
- package/src/scripts/components/migrate-components.d.ts +20 -0
- package/src/scripts/components/migrate-components.js +163 -0
- package/src/scripts/components/migrate-components.js.map +1 -0
- package/src/scripts/components/migrate-components.ts +227 -0
- package/src/scripts/components/migration-diagnostic.d.ts +7 -0
- package/src/scripts/components/migration-diagnostic.js +2 -0
- package/src/scripts/components/migration-diagnostic.js.map +1 -0
- package/src/scripts/components/migration-diagnostic.ts +8 -0
- package/src/scripts/create/create-project.d.ts +20 -0
- package/src/scripts/create/create-project.js +783 -0
- package/src/scripts/create/create-project.js.map +1 -0
- package/src/scripts/create/create-project.ts +849 -0
- package/src/scripts/data-flow-graph.d.ts +36 -0
- package/src/scripts/data-flow-graph.js +186 -0
- package/src/scripts/data-flow-graph.js.map +1 -0
- package/src/scripts/data-flow-graph.ts +240 -0
- package/src/scripts/dependency-graph.d.ts +152 -0
- package/src/scripts/dependency-graph.js +4456 -0
- package/src/scripts/dependency-graph.js.map +1 -0
- package/src/scripts/dependency-graph.ts +5908 -0
- package/src/scripts/effect-dependency-graph.d.ts +89 -0
- package/src/scripts/effect-dependency-graph.js +487 -0
- package/src/scripts/effect-dependency-graph.js.map +1 -0
- package/src/scripts/effect-dependency-graph.ts +630 -0
- package/src/scripts/migrate.d.ts +37 -0
- package/src/scripts/migrate.js +98 -0
- package/src/scripts/migrate.js.map +1 -0
- package/src/scripts/migrate.ts +162 -0
- package/src/scripts/migration-workspace.d.ts +2 -0
- package/src/scripts/migration-workspace.js +67 -0
- package/src/scripts/migration-workspace.js.map +1 -0
- package/src/scripts/migration-workspace.ts +93 -0
- package/src/scripts/primitives/migrate-named-primitives.d.ts +50 -0
- package/src/scripts/primitives/migrate-named-primitives.js +487 -0
- package/src/scripts/primitives/migrate-named-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-named-primitives.ts +627 -0
- package/src/scripts/primitives/migrate-primitive-generators.d.ts +30 -0
- package/src/scripts/primitives/migrate-primitive-generators.js +309 -0
- package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
- package/src/scripts/primitives/migrate-primitive-generators.ts +410 -0
- package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +427 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.ts +655 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.d.ts +23 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.js +382 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.js.map +1 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +499 -0
- package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
- package/src/scripts/primitives/migration-diagnostic.js +2 -0
- package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
- package/src/scripts/primitives/migration-diagnostic.ts +15 -0
- package/src/scripts/routes/migrate-routes.d.ts +34 -0
- package/src/scripts/routes/migrate-routes.js +657 -0
- package/src/scripts/routes/migrate-routes.js.map +1 -0
- package/src/scripts/routes/migrate-routes.ts +895 -0
- package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
- package/src/scripts/routes/migration-diagnostic.js +2 -0
- package/src/scripts/routes/migration-diagnostic.js.map +1 -0
- package/src/scripts/routes/migration-diagnostic.ts +25 -0
- package/src/scripts/routes/route-command.d.ts +75 -0
- package/src/scripts/routes/route-command.js +1169 -0
- package/src/scripts/routes/route-command.js.map +1 -0
- package/src/scripts/routes/route-command.ts +1807 -0
- package/src/scripts/routes/verify-routes.d.ts +60 -0
- package/src/scripts/routes/verify-routes.js +1121 -0
- package/src/scripts/routes/verify-routes.js.map +1 -0
- package/src/scripts/routes/verify-routes.ts +1511 -0
- package/src/scripts/services/config.d.ts +2 -0
- package/src/scripts/services/config.js +38 -0
- package/src/scripts/services/config.js.map +1 -0
- package/src/scripts/services/config.ts +60 -0
- package/src/scripts/services/migrate-services.d.ts +29 -0
- package/src/scripts/services/migrate-services.js +924 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.ts +1348 -0
- package/src/scripts/services/migration-diagnostic.d.ts +8 -0
- package/src/scripts/services/migration-diagnostic.js +2 -0
- package/src/scripts/services/migration-diagnostic.js.map +1 -0
- package/src/scripts/services/migration-diagnostic.ts +28 -0
- package/src/scripts/template/migrate-template.d.ts +16 -0
- package/src/scripts/template/migrate-template.js +349 -0
- package/src/scripts/template/migrate-template.js.map +1 -0
- package/src/scripts/template/migrate-template.ts +418 -0
- package/src/template-migration.d.ts +1 -0
- package/src/template-migration.js +2 -0
- package/src/template-migration.js.map +1 -0
- package/src/testing.d.ts +19 -0
- package/src/testing.js +2 -0
- package/src/testing.js.map +1 -0
- package/src/tsconfig.codemod.json +13 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
IndentationText,
|
|
5
|
+
Node,
|
|
6
|
+
Project,
|
|
7
|
+
QuoteKind,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
} = require('ts-morph');
|
|
10
|
+
|
|
11
|
+
const projectCache = new Map();
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
'Ensure craftService definitions using onAppStart are registered in CraftAppStartRegistry.',
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
'Program:exit'() {
|
|
26
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
27
|
+
const filePath = getFilePath(context);
|
|
28
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const text = sourceCode.getText();
|
|
33
|
+
if (!text.includes('onAppStart')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sourceFile = getProjectSourceFile(
|
|
38
|
+
getProject(getCwd(context)),
|
|
39
|
+
filePath,
|
|
40
|
+
text,
|
|
41
|
+
);
|
|
42
|
+
const requiredRegistrations = collectRequiredRegistrations(sourceFile);
|
|
43
|
+
if (requiredRegistrations.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const registryState = analyzeRegistryState(
|
|
48
|
+
sourceFile,
|
|
49
|
+
requiredRegistrations,
|
|
50
|
+
);
|
|
51
|
+
if (
|
|
52
|
+
registryState.missing.length === 0 &&
|
|
53
|
+
registryState.outOfDate.length === 0
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ensureRegistryEntries(sourceFile, requiredRegistrations);
|
|
59
|
+
const fixedText = sourceFile.getFullText();
|
|
60
|
+
if (fixedText === text) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
context.report({
|
|
65
|
+
loc: getNodeLoc(sourceCode, registryState.reportNode),
|
|
66
|
+
message: formatRegistryMessage(registryState),
|
|
67
|
+
fix(fixer) {
|
|
68
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function getProject(cwd) {
|
|
77
|
+
let project = projectCache.get(cwd);
|
|
78
|
+
if (project) {
|
|
79
|
+
return project;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const manipulationSettings = {
|
|
83
|
+
indentationText: IndentationText.TwoSpaces,
|
|
84
|
+
quoteKind: QuoteKind.Single,
|
|
85
|
+
};
|
|
86
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
87
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
88
|
+
? new Project({
|
|
89
|
+
tsConfigFilePath,
|
|
90
|
+
manipulationSettings,
|
|
91
|
+
})
|
|
92
|
+
: new Project({
|
|
93
|
+
compilerOptions: {
|
|
94
|
+
experimentalDecorators: true,
|
|
95
|
+
target: 9,
|
|
96
|
+
},
|
|
97
|
+
manipulationSettings,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
projectCache.set(cwd, project);
|
|
101
|
+
return project;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
105
|
+
const normalizedPath = path.resolve(filePath);
|
|
106
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
107
|
+
if (existingSourceFile) {
|
|
108
|
+
existingSourceFile.replaceWithText(text);
|
|
109
|
+
return existingSourceFile;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
113
|
+
if (sourceFile) {
|
|
114
|
+
sourceFile.replaceWithText(text);
|
|
115
|
+
return sourceFile;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getFilePath(context) {
|
|
122
|
+
const filePath = context.filename ?? context.getFilename();
|
|
123
|
+
if (!filePath || filePath === '<input>') {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return filePath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getCwd(context) {
|
|
131
|
+
return context.cwd ?? process.cwd();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function collectRequiredRegistrations(sourceFile) {
|
|
135
|
+
const registrations = new Map();
|
|
136
|
+
|
|
137
|
+
for (const declaration of sourceFile.getVariableDeclarations()) {
|
|
138
|
+
const initializer = declaration.getInitializerIfKind(
|
|
139
|
+
SyntaxKind.CallExpression,
|
|
140
|
+
);
|
|
141
|
+
if (!initializer || getCallExpressionName(initializer) !== 'craftService') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!containsOnAppStartCall(initializer)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const tag = readCraftServiceName(initializer);
|
|
150
|
+
const serviceReference = readServiceReferenceName(declaration, tag);
|
|
151
|
+
if (!tag || !serviceReference || registrations.has(tag)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
registrations.set(tag, {
|
|
156
|
+
tag,
|
|
157
|
+
serviceReference,
|
|
158
|
+
reportNode: initializer,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [...registrations.values()];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getCallExpressionName(callExpression) {
|
|
166
|
+
const expression = callExpression.getExpression();
|
|
167
|
+
return Node.isIdentifier(expression) ? expression.getText() : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function containsOnAppStartCall(callExpression) {
|
|
171
|
+
return callExpression
|
|
172
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
173
|
+
.some((innerCall) => getCallExpressionName(innerCall) === 'onAppStart');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readCraftServiceName(callExpression) {
|
|
177
|
+
const [definitionArgument] = callExpression.getArguments();
|
|
178
|
+
if (
|
|
179
|
+
!definitionArgument ||
|
|
180
|
+
!Node.isObjectLiteralExpression(definitionArgument)
|
|
181
|
+
) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const nameProperty = definitionArgument
|
|
186
|
+
.getProperties()
|
|
187
|
+
.find(
|
|
188
|
+
(property) =>
|
|
189
|
+
Node.isPropertyAssignment(property) && property.getName() === 'name',
|
|
190
|
+
);
|
|
191
|
+
if (!nameProperty || !Node.isPropertyAssignment(nameProperty)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const initializer = nameProperty.getInitializer();
|
|
196
|
+
if (
|
|
197
|
+
!initializer ||
|
|
198
|
+
(!Node.isStringLiteral(initializer) &&
|
|
199
|
+
!Node.isNoSubstitutionTemplateLiteral(initializer))
|
|
200
|
+
) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return initializer.getLiteralText();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readServiceReferenceName(declaration, serviceName) {
|
|
208
|
+
const nameNode = declaration.getNameNode();
|
|
209
|
+
if (!Node.isObjectBindingPattern(nameNode)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const bindingElement =
|
|
214
|
+
nameNode
|
|
215
|
+
.getElements()
|
|
216
|
+
.find((element) => element.getNameNode().getText() === serviceName) ??
|
|
217
|
+
(nameNode.getElements().length === 1
|
|
218
|
+
? nameNode.getElements()[0]
|
|
219
|
+
: undefined);
|
|
220
|
+
|
|
221
|
+
return bindingElement?.getNameNode().getText();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function analyzeRegistryState(sourceFile, requiredRegistrations) {
|
|
225
|
+
const moduleDeclaration = getCraftCoreModuleDeclaration(sourceFile);
|
|
226
|
+
const registryInterface = moduleDeclaration?.getInterface(
|
|
227
|
+
'CraftAppStartRegistry',
|
|
228
|
+
);
|
|
229
|
+
const propertiesByName = new Map(
|
|
230
|
+
(registryInterface?.getProperties() ?? []).map((property) => [
|
|
231
|
+
readPropertyName(property),
|
|
232
|
+
property,
|
|
233
|
+
]),
|
|
234
|
+
);
|
|
235
|
+
const missing = [];
|
|
236
|
+
const outOfDate = [];
|
|
237
|
+
|
|
238
|
+
for (const registration of requiredRegistrations) {
|
|
239
|
+
const property = propertiesByName.get(registration.tag);
|
|
240
|
+
if (!property) {
|
|
241
|
+
missing.push(registration.tag);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const currentType = normalizeText(property.getTypeNode()?.getText() ?? '');
|
|
246
|
+
const expectedType = normalizeText(
|
|
247
|
+
`typeof ${registration.serviceReference}`,
|
|
248
|
+
);
|
|
249
|
+
if (currentType !== expectedType) {
|
|
250
|
+
outOfDate.push(registration.tag);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
missing,
|
|
256
|
+
outOfDate,
|
|
257
|
+
reportNode: requiredRegistrations[0].reportNode,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getCraftCoreModuleDeclaration(sourceFile) {
|
|
262
|
+
return sourceFile.getModules().find((moduleDeclaration) => {
|
|
263
|
+
const nameNode = moduleDeclaration.getNameNode();
|
|
264
|
+
return (
|
|
265
|
+
Node.isStringLiteral(nameNode) &&
|
|
266
|
+
nameNode.getLiteralText() === '@craft-ts/core'
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readPropertyName(property) {
|
|
272
|
+
const nameNode = property.getNameNode();
|
|
273
|
+
if (Node.isIdentifier(nameNode) || Node.isPrivateIdentifier(nameNode)) {
|
|
274
|
+
return nameNode.getText();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (Node.isStringLiteral(nameNode) || Node.isNumericLiteral(nameNode)) {
|
|
278
|
+
return nameNode.getLiteralText();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return property.getName();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function ensureRegistryEntries(sourceFile, requiredRegistrations) {
|
|
285
|
+
const moduleDeclaration =
|
|
286
|
+
getCraftCoreModuleDeclaration(sourceFile) ??
|
|
287
|
+
sourceFile.addModule({
|
|
288
|
+
name: "'@craft-ts/core'",
|
|
289
|
+
hasDeclareKeyword: true,
|
|
290
|
+
});
|
|
291
|
+
const registryInterface =
|
|
292
|
+
moduleDeclaration.getInterface('CraftAppStartRegistry') ??
|
|
293
|
+
moduleDeclaration.addInterface({
|
|
294
|
+
name: 'CraftAppStartRegistry',
|
|
295
|
+
});
|
|
296
|
+
const propertiesByName = new Map(
|
|
297
|
+
registryInterface
|
|
298
|
+
.getProperties()
|
|
299
|
+
.map((property) => [readPropertyName(property), property]),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
for (const registration of requiredRegistrations) {
|
|
303
|
+
const expectedType = `typeof ${registration.serviceReference}`;
|
|
304
|
+
const property = propertiesByName.get(registration.tag);
|
|
305
|
+
if (!property) {
|
|
306
|
+
registryInterface.addProperty({
|
|
307
|
+
name: formatPropertyName(registration.tag),
|
|
308
|
+
type: expectedType,
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
property.setType(expectedType);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatPropertyName(name) {
|
|
318
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : `'${name}'`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getNodeLoc(sourceCode, node) {
|
|
322
|
+
return {
|
|
323
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
324
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatRegistryMessage({ missing, outOfDate }) {
|
|
329
|
+
const segments = [];
|
|
330
|
+
if (missing.length > 0) {
|
|
331
|
+
segments.push(`missing ${formatNameList(missing)}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (outOfDate.length > 0) {
|
|
335
|
+
segments.push(`out of date for ${formatNameList(outOfDate)}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return `CraftAppStartRegistry is ${segments.join(' and ')}. Run ESLint --fix on this file to register app-start services.`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function formatNameList(names) {
|
|
342
|
+
if (names.length === 1) {
|
|
343
|
+
return names[0];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (names.length === 2) {
|
|
347
|
+
return `${names[0]} and ${names[1]}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeText(text) {
|
|
354
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
355
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseHyperscriptCall, hasProp } = require('./hyperscript-walk.cjs');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
'Require an explicit type on button() so it cannot silently submit a parent form.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
fixable: 'code',
|
|
14
|
+
messages: {
|
|
15
|
+
missingType:
|
|
16
|
+
'button() must set type ("button", "submit", or "reset"). Craft does not default it.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
const call = parseHyperscriptCall(node);
|
|
23
|
+
if (!call || call.tag !== 'button') return;
|
|
24
|
+
if (hasProp(call.props, 'type')) return;
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: 'missingType',
|
|
28
|
+
fix(fixer) {
|
|
29
|
+
if (call.props && call.props.type === 'ObjectExpression') {
|
|
30
|
+
const open = call.props.range[0];
|
|
31
|
+
return fixer.insertTextAfterRange(
|
|
32
|
+
[open, open + 1],
|
|
33
|
+
" type: 'button',",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const firstArg = node.arguments[0];
|
|
37
|
+
if (firstArg) {
|
|
38
|
+
return fixer.insertTextBefore(firstArg, "{ type: 'button' }, ");
|
|
39
|
+
}
|
|
40
|
+
const paren = node.range[1] - 1;
|
|
41
|
+
return fixer.insertTextBeforeRange(
|
|
42
|
+
[paren, paren],
|
|
43
|
+
"{ type: 'button' }",
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseHyperscriptCall, hasAccessibleName, staticPropString } = require('./hyperscript-walk.cjs');
|
|
4
|
+
|
|
5
|
+
const NAMED_CONTROLS = new Set(['button', 'a', 'textarea', 'select', 'svg']);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description:
|
|
12
|
+
'Require an accessible name on buttons, links, labelled form controls, and interactive SVG.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
missingName:
|
|
17
|
+
'<{{tag}}> must have an accessible name (text children, aria-label, aria-labelledby, or a <label>).',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
const call = parseHyperscriptCall(node);
|
|
24
|
+
if (!call || !call.tag) return;
|
|
25
|
+
if (call.tag === 'input') {
|
|
26
|
+
const type = staticPropString(call.props, 'type') ?? 'text';
|
|
27
|
+
if (type === 'hidden') return;
|
|
28
|
+
if (hasAccessibleName(call)) return;
|
|
29
|
+
// An input may be named by a sibling label[htmlFor] — checked by label-has-associated-control.
|
|
30
|
+
// Still require a name on the control itself when it is a button-like type.
|
|
31
|
+
if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
|
|
32
|
+
context.report({ node, messageId: 'missingName', data: { tag: `input[type=${type}]` } });
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!NAMED_CONTROLS.has(call.tag)) return;
|
|
37
|
+
if (call.tag === 'svg') {
|
|
38
|
+
const role = staticPropString(call.props, 'role');
|
|
39
|
+
const hasHandler =
|
|
40
|
+
call.props &&
|
|
41
|
+
call.props.properties.some(
|
|
42
|
+
(entry) =>
|
|
43
|
+
entry.type === 'Property' &&
|
|
44
|
+
((entry.key.type === 'Identifier' &&
|
|
45
|
+
(entry.key.name === 'click' || entry.key.name === 'onClick')) ||
|
|
46
|
+
(entry.key.type === 'Literal' &&
|
|
47
|
+
(entry.key.value === 'click' || entry.key.value === 'onClick'))),
|
|
48
|
+
);
|
|
49
|
+
if (role !== 'button' && role !== 'img' && !hasHandler) return;
|
|
50
|
+
}
|
|
51
|
+
if (hasAccessibleName(call)) return;
|
|
52
|
+
context.report({ node, messageId: 'missingName', data: { tag: call.tag } });
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const { createNameMatchRule } = require('./craft-name-match-utils.cjs');
|
|
2
|
+
|
|
3
|
+
module.exports = createNameMatchRule({
|
|
4
|
+
calleeName: 'craftComponent',
|
|
5
|
+
description:
|
|
6
|
+
'Ensure craftComponent(name, ...) is called with a string literal name matching the declared component.',
|
|
7
|
+
supportsObjectConfigForm: false,
|
|
8
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { createNameMatchRule } = require('./craft-name-match-utils.cjs');
|
|
2
|
+
|
|
3
|
+
const namedRule = createNameMatchRule({
|
|
4
|
+
calleeName: 'craftComputed',
|
|
5
|
+
description:
|
|
6
|
+
'Require a matching craftComputed name outside insertion result objects; insertion keys provide the runtime name automatically.',
|
|
7
|
+
supportsObjectConfigForm: false,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
...namedRule,
|
|
12
|
+
create(context) {
|
|
13
|
+
const listeners = namedRule.create(context);
|
|
14
|
+
return {
|
|
15
|
+
...listeners,
|
|
16
|
+
CallExpression(node) {
|
|
17
|
+
if (isInsertionResultProperty(node)) return;
|
|
18
|
+
listeners.CallExpression?.(node);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function isInsertionResultProperty(node) {
|
|
25
|
+
const property = node.parent;
|
|
26
|
+
if (
|
|
27
|
+
property?.type !== 'Property' ||
|
|
28
|
+
property.value !== node ||
|
|
29
|
+
property.parent?.type !== 'ObjectExpression'
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let current = property.parent;
|
|
35
|
+
while (current) {
|
|
36
|
+
if (
|
|
37
|
+
(current.type === 'ArrowFunctionExpression' ||
|
|
38
|
+
current.type === 'FunctionExpression') &&
|
|
39
|
+
current.parent?.type === 'CallExpression'
|
|
40
|
+
) {
|
|
41
|
+
const callee = current.parent.callee;
|
|
42
|
+
const name =
|
|
43
|
+
callee.type === 'Identifier'
|
|
44
|
+
? callee.name
|
|
45
|
+
: callee.type === 'MemberExpression' &&
|
|
46
|
+
!callee.computed &&
|
|
47
|
+
callee.property.type === 'Identifier'
|
|
48
|
+
? callee.property.name
|
|
49
|
+
: undefined;
|
|
50
|
+
return Boolean(
|
|
51
|
+
name &&
|
|
52
|
+
(name.startsWith('insert') ||
|
|
53
|
+
name === 'craftPipe' ||
|
|
54
|
+
['state', 'query', 'mutation', 'asyncProcess', 'queryParams'].includes(
|
|
55
|
+
name,
|
|
56
|
+
)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (current.type === 'Program') break;
|
|
60
|
+
current = current.parent;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { componentInfo, cssFacts } = require('./css-rule-utils.cjs');
|
|
2
|
+
const registrations = new Map();
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: { type: 'problem', schema: [], messages: { duplicate: '@property {{name}} is also registered by {{owner}}. A custom property may have only one owner.' } },
|
|
5
|
+
create(context) {
|
|
6
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
7
|
+
return { CallExpression(node) {
|
|
8
|
+
const info = componentInfo(context, sourceCode, node); if (!info?.css) return;
|
|
9
|
+
cssFacts(info.css).registered.forEach((name) => {
|
|
10
|
+
const previous = registrations.get(name);
|
|
11
|
+
if (previous && previous !== info.name) context.report({ node: info.styles?.value ?? info.stylesUrl.value, messageId: 'duplicate', data: { name, owner: previous } });
|
|
12
|
+
else registrations.set(name, info.name);
|
|
13
|
+
});
|
|
14
|
+
} };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { componentInfo, cssFacts, prefixes } = require('./css-rule-utils.cjs');
|
|
2
|
+
module.exports = {
|
|
3
|
+
meta: { type: 'suggestion', schema: [], messages: { naming: '{{name}} must use the component namespace ({{expected}}).' } },
|
|
4
|
+
create(context) {
|
|
5
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
6
|
+
return { CallExpression(node) {
|
|
7
|
+
const info = componentInfo(context, sourceCode, node); if (!info?.css) return;
|
|
8
|
+
const accepted = prefixes(info.name); const facts = cssFacts(info.css);
|
|
9
|
+
const templateText = node.arguments[3] ? sourceCode.getText(node.arguments[3]) : '';
|
|
10
|
+
facts.declared.forEach((name) => {
|
|
11
|
+
const inheritedChildVariable = new RegExp(
|
|
12
|
+
`['\"]${name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}['\"]\\s*:\\s*inherit\\b`,
|
|
13
|
+
).test(templateText);
|
|
14
|
+
if (!inheritedChildVariable && ![...accepted].some((prefix) => name.startsWith(prefix))) context.report({ node: info.styles?.value ?? info.stylesUrl.value, messageId: 'naming', data: { name, expected: [...accepted].join(' or ') } });
|
|
15
|
+
});
|
|
16
|
+
} };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { componentInfo, cssFacts } = require('./css-rule-utils.cjs');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem', schema: [],
|
|
6
|
+
messages: {
|
|
7
|
+
opaque: 'Craft component styles are dynamic and their CSS variable contract cannot be checked.',
|
|
8
|
+
missing: 'External styles use {{name}} but ComponentMeta.cssVars does not declare it.',
|
|
9
|
+
absent: 'External styles use CSS variables but ComponentMeta.cssVars is missing.',
|
|
10
|
+
unused: 'ComponentMeta.cssVars declares {{name}}, but the stylesheet never declares or consumes it.',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
15
|
+
return { CallExpression(node) {
|
|
16
|
+
const info = componentInfo(context, sourceCode, node);
|
|
17
|
+
if (!info || (!info.styles && !info.stylesUrl)) return;
|
|
18
|
+
if (info.opaque) return context.report({ node: info.styles?.value ?? info.stylesUrl.value, messageId: 'opaque' });
|
|
19
|
+
const facts = cssFacts(info.css);
|
|
20
|
+
if (info.external && facts.used.size && !info.cssVars) {
|
|
21
|
+
context.report({ node: info.stylesUrl.value, messageId: 'absent' });
|
|
22
|
+
}
|
|
23
|
+
if (info.cssVars) {
|
|
24
|
+
facts.used.forEach((name) => {
|
|
25
|
+
if (!info.contract.has(name)) context.report({ node: info.cssVars.value, messageId: 'missing', data: { name } });
|
|
26
|
+
});
|
|
27
|
+
info.contract.forEach((name) => {
|
|
28
|
+
if (!facts.used.has(name) && !facts.declared.has(name)) context.report({ node: info.cssVars.value, messageId: 'unused', data: { name } });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
} };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const { createNameMatchRule } = require('./craft-name-match-utils.cjs');
|
|
2
|
+
|
|
3
|
+
module.exports = createNameMatchRule({
|
|
4
|
+
calleeName: 'craftDirective',
|
|
5
|
+
description:
|
|
6
|
+
'Ensure craftDirective(name, ...) is called with a string literal name matching the declared directive.',
|
|
7
|
+
supportsObjectConfigForm: false,
|
|
8
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const { createNameMatchRule } = require('./craft-name-match-utils.cjs');
|
|
2
|
+
|
|
3
|
+
module.exports = createNameMatchRule({
|
|
4
|
+
calleeName: 'craftMethod',
|
|
5
|
+
description:
|
|
6
|
+
"Ensure craftMethod(name, ...) is called with a string literal first argument (or { name } object) that matches the declared variable or class property name.",
|
|
7
|
+
supportsObjectConfigForm: true,
|
|
8
|
+
});
|