@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,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
|
|
5
|
+
const SERVICE_NAME = /(?:Service|Provider)$/;
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description:
|
|
12
|
+
'Disallow manually enumerating application providers in RouteCheckedDI.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
18
|
+
const manualAliases = new Map();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
TSTypeAliasDeclaration(node) {
|
|
22
|
+
const names = stringLiteralUnion(node.typeAnnotation);
|
|
23
|
+
if (names.length > 0 && names.some((name) => SERVICE_NAME.test(name))) {
|
|
24
|
+
manualAliases.set(node.id.name, node);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
'Program:exit'(program) {
|
|
28
|
+
for (const node of program.body) {
|
|
29
|
+
if (node.type !== 'TSTypeAliasDeclaration') continue;
|
|
30
|
+
const aliases = routeCheckedDiAliases(node.typeAnnotation);
|
|
31
|
+
if (aliases.length === 0) continue;
|
|
32
|
+
const availableNames = aliases[0];
|
|
33
|
+
const names = stringLiteralUnion(availableNames);
|
|
34
|
+
if (names.some((name) => SERVICE_NAME.test(name))) {
|
|
35
|
+
report(node, availableNames);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const node of manualAliases.values()) {
|
|
40
|
+
const usedByRouteCheck = sourceCode.ast.body.some(
|
|
41
|
+
(statement) =>
|
|
42
|
+
statement.type === 'TSTypeAliasDeclaration' &&
|
|
43
|
+
routeCheckedDiAliases(statement.typeAnnotation).some(
|
|
44
|
+
(availableNames) =>
|
|
45
|
+
availableNames.type === 'TSTypeReference' &&
|
|
46
|
+
availableNames.typeName.type === 'Identifier' &&
|
|
47
|
+
availableNames.typeName.name === node.id.name,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
if (usedByRouteCheck) report(node, node.typeAnnotation);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function report(node, providerType) {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
message:
|
|
59
|
+
'Do not maintain a manual provider-name list for RouteCheckedDI. Derive application providers from craftAppConfig (AppProvidedServiceNamesOf<typeof appConfig>) or keep only the route context providers.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function routeCheckedDiAliases(typeNode) {
|
|
66
|
+
if (!typeNode || typeNode.type !== 'TSTypeReference') return [];
|
|
67
|
+
if (
|
|
68
|
+
typeNode.typeName.type !== 'Identifier' ||
|
|
69
|
+
typeNode.typeName.name !== 'RouteCheckedDI'
|
|
70
|
+
) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return typeNode.typeArguments?.params?.slice(1, 2) ?? [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stringLiteralUnion(node) {
|
|
77
|
+
if (!node) return [];
|
|
78
|
+
if (node.type === 'TSLiteralType' && typeof node.literal.value === 'string') {
|
|
79
|
+
return [node.literal.value];
|
|
80
|
+
}
|
|
81
|
+
if (node.type === 'TSUnionType') {
|
|
82
|
+
return node.types.flatMap(stringLiteralUnion);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NONINTERACTIVE_TAGS, INTERACTIVE_ROLES } = require('./html-helpers.cjs');
|
|
4
|
+
const { parseHyperscriptCall, hasProp, staticPropString, property } = require('./hyperscript-walk.cjs');
|
|
5
|
+
|
|
6
|
+
const INTERACTION_PROPS = new Set([
|
|
7
|
+
'click',
|
|
8
|
+
'onClick',
|
|
9
|
+
'keydown',
|
|
10
|
+
'onKeydown',
|
|
11
|
+
'keyup',
|
|
12
|
+
'onKeyup',
|
|
13
|
+
'keypress',
|
|
14
|
+
'onKeypress',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function hasInteraction(props) {
|
|
18
|
+
if (!props || props.type !== 'ObjectExpression') return false;
|
|
19
|
+
return props.properties.some((entry) => {
|
|
20
|
+
if (entry.type !== 'Property' || entry.computed) return false;
|
|
21
|
+
const name =
|
|
22
|
+
entry.key.type === 'Identifier'
|
|
23
|
+
? entry.key.name
|
|
24
|
+
: entry.key.type === 'Literal'
|
|
25
|
+
? String(entry.key.value)
|
|
26
|
+
: '';
|
|
27
|
+
return INTERACTION_PROPS.has(name);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'problem',
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
'Forbid click/keydown on non-interactive elements unless they have an interactive role and tabIndex.',
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
messages: {
|
|
40
|
+
noninteractive:
|
|
41
|
+
'Do not add {{handler}} to <{{tag}}>. Use a button/a, or add an interactive role plus tabIndex.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
const call = parseHyperscriptCall(node);
|
|
48
|
+
if (!call || !call.tag || !NONINTERACTIVE_TAGS.has(call.tag)) return;
|
|
49
|
+
if (!hasInteraction(call.props)) return;
|
|
50
|
+
const role = staticPropString(call.props, 'role');
|
|
51
|
+
const hasTabIndex = hasProp(call.props, 'tabIndex') || hasProp(call.props, 'tabindex');
|
|
52
|
+
if (role && INTERACTIVE_ROLES.has(role) && hasTabIndex) return;
|
|
53
|
+
const handler = property(call.props, 'click')
|
|
54
|
+
? 'click'
|
|
55
|
+
: property(call.props, 'keydown')
|
|
56
|
+
? 'keydown'
|
|
57
|
+
: 'keyboard/pointer handlers';
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: 'noninteractive',
|
|
61
|
+
data: { tag: call.tag, handler },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseHyperscriptCall, property, stringLiteralValue } = require('./hyperscript-walk.cjs');
|
|
4
|
+
|
|
5
|
+
function tabIndexValue(props) {
|
|
6
|
+
const entry = property(props, 'tabIndex') ?? property(props, 'tabindex');
|
|
7
|
+
if (!entry) return undefined;
|
|
8
|
+
if (entry.value.type === 'Literal' && typeof entry.value.value === 'number') {
|
|
9
|
+
return entry.value.value;
|
|
10
|
+
}
|
|
11
|
+
if (entry.value.type === 'UnaryExpression' && entry.value.operator === '-' &&
|
|
12
|
+
entry.value.argument.type === 'Literal' && typeof entry.value.argument.value === 'number') {
|
|
13
|
+
return -entry.value.argument.value;
|
|
14
|
+
}
|
|
15
|
+
const asString = stringLiteralValue(entry.value);
|
|
16
|
+
if (asString !== undefined && asString !== '') return Number(asString);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: {
|
|
24
|
+
description: 'Forbid positive tabIndex values that scramble focus order.',
|
|
25
|
+
},
|
|
26
|
+
schema: [],
|
|
27
|
+
messages: {
|
|
28
|
+
positive:
|
|
29
|
+
'tabIndex must not be greater than 0. Use 0 (in order) or -1 (programmatic).',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
CallExpression(node) {
|
|
35
|
+
const call = parseHyperscriptCall(node);
|
|
36
|
+
if (!call) return;
|
|
37
|
+
const value = tabIndexValue(call.props);
|
|
38
|
+
if (typeof value === 'number' && value > 0) {
|
|
39
|
+
context.report({ node, messageId: 'positive' });
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const CORE_PACKAGE = '@craft-ts/core';
|
|
2
|
+
const EFFECT_PACKAGE = '@craft-ts/effect';
|
|
3
|
+
const ANGULAR_PACKAGE = '@angular/core';
|
|
4
|
+
|
|
5
|
+
const PRIMITIVES = new Set([
|
|
6
|
+
'asyncProcess',
|
|
7
|
+
'asyncProcessEffect',
|
|
8
|
+
'computedEffect',
|
|
9
|
+
'mutation',
|
|
10
|
+
'mutationEffect',
|
|
11
|
+
'query',
|
|
12
|
+
'queryEffect',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const INSERTION_PIPES = new Set([
|
|
16
|
+
'insertAsyncProcessPipe',
|
|
17
|
+
'insertMutationPipe',
|
|
18
|
+
'insertQueryPipe',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const NATIVE_PROPERTIES = new Set(['hasValue', 'isLoading', 'status', 'value']);
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
'Disallow insertion properties that only duplicate a primitive resource property.',
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: {
|
|
32
|
+
redundant:
|
|
33
|
+
"'{{property}}' is already provided by the primitive; do not re-declare it in an insertion without additional logic.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
const primitiveBindings = new Map();
|
|
39
|
+
const pipeBindings = new Map();
|
|
40
|
+
const computedBindings = new Map();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ImportDeclaration(node) {
|
|
44
|
+
if (node.importKind === 'type') return;
|
|
45
|
+
|
|
46
|
+
const source = node.source.value;
|
|
47
|
+
if (
|
|
48
|
+
source !== CORE_PACKAGE &&
|
|
49
|
+
source !== EFFECT_PACKAGE &&
|
|
50
|
+
source !== ANGULAR_PACKAGE
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const specifier of node.specifiers) {
|
|
56
|
+
if (
|
|
57
|
+
specifier.type !== 'ImportSpecifier' ||
|
|
58
|
+
specifier.importKind === 'type'
|
|
59
|
+
) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const importedName = getIdentifierName(specifier.imported);
|
|
64
|
+
if (!importedName) continue;
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
(source === CORE_PACKAGE || source === EFFECT_PACKAGE) &&
|
|
68
|
+
PRIMITIVES.has(importedName)
|
|
69
|
+
) {
|
|
70
|
+
primitiveBindings.set(specifier.local.name, importedName);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (source === CORE_PACKAGE && INSERTION_PIPES.has(importedName)) {
|
|
74
|
+
pipeBindings.set(specifier.local.name, importedName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
(source === CORE_PACKAGE && importedName === 'craftComputed') ||
|
|
79
|
+
(source === ANGULAR_PACKAGE && importedName === 'computed')
|
|
80
|
+
) {
|
|
81
|
+
computedBindings.set(specifier.local.name, importedName);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
FunctionExpression(node) {
|
|
87
|
+
inspectInsertionCallback(node);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
ArrowFunctionExpression(node) {
|
|
91
|
+
inspectInsertionCallback(node);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function inspectInsertionCallback(node) {
|
|
96
|
+
if (!isInsertionCallback(node, primitiveBindings, pipeBindings)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const resourceParameter = getResourceParameter(node);
|
|
101
|
+
if (!resourceParameter) return;
|
|
102
|
+
|
|
103
|
+
const object = getReturnedObject(node);
|
|
104
|
+
if (!object) return;
|
|
105
|
+
|
|
106
|
+
for (const property of object.properties) {
|
|
107
|
+
if (
|
|
108
|
+
property.type !== 'Property' ||
|
|
109
|
+
property.computed ||
|
|
110
|
+
property.kind !== 'init'
|
|
111
|
+
) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const propertyName = getPropertyName(property.key);
|
|
116
|
+
if (!propertyName || !NATIVE_PROPERTIES.has(propertyName)) continue;
|
|
117
|
+
|
|
118
|
+
const computed = getComputedCall(property.value, computedBindings);
|
|
119
|
+
if (
|
|
120
|
+
!computed ||
|
|
121
|
+
(computed.name !== undefined && computed.name !== propertyName)
|
|
122
|
+
) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const read = getReturnedResourceRead(
|
|
127
|
+
computed.callback,
|
|
128
|
+
resourceParameter,
|
|
129
|
+
);
|
|
130
|
+
if (read === propertyName) {
|
|
131
|
+
context.report({
|
|
132
|
+
node: property,
|
|
133
|
+
messageId: 'redundant',
|
|
134
|
+
data: { property: propertyName },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function isInsertionCallback(node, primitiveBindings, pipeBindings) {
|
|
143
|
+
const parent = node.parent;
|
|
144
|
+
if (!parent || parent.type !== 'CallExpression') return false;
|
|
145
|
+
|
|
146
|
+
const callee = parent.callee;
|
|
147
|
+
if (callee.type !== 'Identifier') return false;
|
|
148
|
+
|
|
149
|
+
return primitiveBindings.has(callee.name) || pipeBindings.has(callee.name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getResourceParameter(node) {
|
|
153
|
+
for (const parameter of node.params) {
|
|
154
|
+
if (parameter.type !== 'ObjectPattern') continue;
|
|
155
|
+
|
|
156
|
+
for (const property of parameter.properties) {
|
|
157
|
+
if (
|
|
158
|
+
property.type !== 'Property' ||
|
|
159
|
+
property.computed ||
|
|
160
|
+
getPropertyName(property.key) !== 'resource'
|
|
161
|
+
) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const value = property.value;
|
|
166
|
+
if (value.type === 'Identifier') return value.name;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getReturnedObject(node) {
|
|
174
|
+
const body = unwrap(node.body);
|
|
175
|
+
if (body.type === 'ObjectExpression') return body;
|
|
176
|
+
if (body.type !== 'BlockStatement') return undefined;
|
|
177
|
+
|
|
178
|
+
const returnStatement = body.body.find(
|
|
179
|
+
(statement) => statement.type === 'ReturnStatement',
|
|
180
|
+
);
|
|
181
|
+
return returnStatement?.argument
|
|
182
|
+
? getObjectExpression(returnStatement.argument)
|
|
183
|
+
: undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getObjectExpression(node) {
|
|
187
|
+
const expression = unwrap(node);
|
|
188
|
+
return expression.type === 'ObjectExpression' ? expression : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getComputedCall(node, computedBindings) {
|
|
192
|
+
const call = unwrap(node);
|
|
193
|
+
if (call.type !== 'CallExpression' || call.callee.type !== 'Identifier') {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const importedName = computedBindings.get(call.callee.name);
|
|
198
|
+
if (importedName === 'craftComputed') {
|
|
199
|
+
const name = getStringLiteral(call.arguments[0]);
|
|
200
|
+
const callback = call.arguments[call.arguments.length - 1];
|
|
201
|
+
return name && callback && isFunction(callback)
|
|
202
|
+
? { name, callback }
|
|
203
|
+
: undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (importedName === 'computed') {
|
|
207
|
+
const callback = call.arguments[0];
|
|
208
|
+
return callback && isFunction(callback)
|
|
209
|
+
? { name: undefined, callback }
|
|
210
|
+
: undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getReturnedResourceRead(node, resourceParameter) {
|
|
217
|
+
const body = unwrap(node.body);
|
|
218
|
+
if (body.type === 'BlockStatement') {
|
|
219
|
+
const returnStatement = body.body.find(
|
|
220
|
+
(statement) => statement.type === 'ReturnStatement',
|
|
221
|
+
);
|
|
222
|
+
if (!returnStatement?.argument) return undefined;
|
|
223
|
+
|
|
224
|
+
const directRead = getResourceRead(
|
|
225
|
+
returnStatement.argument,
|
|
226
|
+
resourceParameter,
|
|
227
|
+
);
|
|
228
|
+
if (directRead) return directRead;
|
|
229
|
+
|
|
230
|
+
const statusVariable = getStatusVariable(body, resourceParameter);
|
|
231
|
+
return isCanonicalIsLoading(returnStatement.argument, statusVariable)
|
|
232
|
+
? 'isLoading'
|
|
233
|
+
: undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return getResourceRead(body, resourceParameter);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getStatusVariable(body, resourceParameter) {
|
|
240
|
+
for (const statement of body.body) {
|
|
241
|
+
if (
|
|
242
|
+
statement.type !== 'VariableDeclaration' ||
|
|
243
|
+
statement.declarations.length !== 1
|
|
244
|
+
) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const declaration = statement.declarations[0];
|
|
249
|
+
if (declaration.id.type !== 'Identifier') continue;
|
|
250
|
+
if (getResourceRead(declaration.init, resourceParameter) !== 'status') {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return declaration.id.name;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getResourceRead(node, resourceParameter) {
|
|
261
|
+
let expression = unwrap(node);
|
|
262
|
+
if (!expression) return undefined;
|
|
263
|
+
if (expression.type === 'YieldExpression') {
|
|
264
|
+
expression = unwrap(expression.argument);
|
|
265
|
+
}
|
|
266
|
+
if (!expression) return undefined;
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
expression.type !== 'CallExpression' ||
|
|
270
|
+
expression.callee.type !== 'MemberExpression' ||
|
|
271
|
+
expression.callee.computed ||
|
|
272
|
+
expression.callee.object.type !== 'Identifier' ||
|
|
273
|
+
expression.callee.object.name !== resourceParameter ||
|
|
274
|
+
expression.callee.property.type !== 'Identifier' ||
|
|
275
|
+
expression.arguments.length !== 0
|
|
276
|
+
) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return expression.callee.property.name;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isCanonicalIsLoading(node, statusVariable) {
|
|
284
|
+
if (!statusVariable) return false;
|
|
285
|
+
|
|
286
|
+
const expression = unwrap(node);
|
|
287
|
+
if (expression.type !== 'LogicalExpression' || expression.operator !== '||') {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const values = [
|
|
292
|
+
getStatusComparison(expression.left, statusVariable),
|
|
293
|
+
getStatusComparison(expression.right, statusVariable),
|
|
294
|
+
];
|
|
295
|
+
return values.includes('loading') && values.includes('reloading');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function getStatusComparison(node, statusVariable) {
|
|
299
|
+
const expression = unwrap(node);
|
|
300
|
+
if (
|
|
301
|
+
expression.type !== 'BinaryExpression' ||
|
|
302
|
+
!['===', '=='].includes(expression.operator)
|
|
303
|
+
) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
expression.left.type === 'Identifier' &&
|
|
309
|
+
expression.left.name === statusVariable
|
|
310
|
+
) {
|
|
311
|
+
return getStringLiteral(expression.right);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
expression.right.type === 'Identifier' &&
|
|
316
|
+
expression.right.name === statusVariable
|
|
317
|
+
) {
|
|
318
|
+
return getStringLiteral(expression.left);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isFunction(node) {
|
|
325
|
+
return (
|
|
326
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
327
|
+
node.type === 'FunctionExpression'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getPropertyName(node) {
|
|
332
|
+
if (node.type === 'Identifier') return node.name;
|
|
333
|
+
return getStringLiteral(node);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getStringLiteral(node) {
|
|
337
|
+
return node?.type === 'Literal' && typeof node.value === 'string'
|
|
338
|
+
? node.value
|
|
339
|
+
: undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getIdentifierName(node) {
|
|
343
|
+
return node.type === 'Identifier' ? node.name : undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function unwrap(node) {
|
|
347
|
+
let current = node;
|
|
348
|
+
while (
|
|
349
|
+
current &&
|
|
350
|
+
[
|
|
351
|
+
'ChainExpression',
|
|
352
|
+
'TSAsExpression',
|
|
353
|
+
'TSNonNullExpression',
|
|
354
|
+
'TSSatisfiesExpression',
|
|
355
|
+
'TSTypeAssertion',
|
|
356
|
+
].includes(current.type)
|
|
357
|
+
) {
|
|
358
|
+
current = current.expression;
|
|
359
|
+
}
|
|
360
|
+
return current;
|
|
361
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const WRITE_METHODS = new Set(['set', 'update', 'mutate']);
|
|
2
|
+
const DOM_EVENTS = new Set([
|
|
3
|
+
'change',
|
|
4
|
+
'click',
|
|
5
|
+
'input',
|
|
6
|
+
'keydown',
|
|
7
|
+
'keypress',
|
|
8
|
+
'keyup',
|
|
9
|
+
'submit',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isFunctionNode(node) {
|
|
13
|
+
return (
|
|
14
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
15
|
+
node?.type === 'FunctionExpression'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
docs: {
|
|
23
|
+
description:
|
|
24
|
+
'Disallow signal writes from Craft component templates and render bindings.',
|
|
25
|
+
},
|
|
26
|
+
schema: [],
|
|
27
|
+
messages: {
|
|
28
|
+
renderWrite:
|
|
29
|
+
'Do not call {{method}}() while rendering a Craft template. Move the write to a DOM event, output, mutation, or explicit business effect.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
create(context) {
|
|
34
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
if (
|
|
39
|
+
node.callee.type !== 'Identifier' ||
|
|
40
|
+
node.callee.name !== 'craftComponent' ||
|
|
41
|
+
node.arguments.length < 4 ||
|
|
42
|
+
!isFunctionNode(node.arguments[3])
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
inspect(node.arguments[3].body);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function inspect(root) {
|
|
51
|
+
walk(root, (node) => {
|
|
52
|
+
if (isNestedCraftComponent(node)) return 'skip';
|
|
53
|
+
if (node.type === 'Property' && isAllowedCallbackProperty(node)) {
|
|
54
|
+
return 'skip';
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
node.type !== 'CallExpression' ||
|
|
58
|
+
node.callee.type !== 'MemberExpression' ||
|
|
59
|
+
node.callee.computed ||
|
|
60
|
+
node.callee.property.type !== 'Identifier' ||
|
|
61
|
+
!WRITE_METHODS.has(node.callee.property.name)
|
|
62
|
+
) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: 'renderWrite',
|
|
68
|
+
data: { method: node.callee.property.name },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isAllowedCallbackProperty(node) {
|
|
74
|
+
if (node.computed || !isFunctionNode(node.value)) return false;
|
|
75
|
+
const name =
|
|
76
|
+
node.key.type === 'Identifier'
|
|
77
|
+
? node.key.name
|
|
78
|
+
: node.key.type === 'Literal' && typeof node.key.value === 'string'
|
|
79
|
+
? node.key.value
|
|
80
|
+
: undefined;
|
|
81
|
+
return Boolean(name && (DOM_EVENTS.has(name) || /^on[A-Z]/.test(name)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isNestedCraftComponent(node) {
|
|
85
|
+
return (
|
|
86
|
+
node.type === 'CallExpression' &&
|
|
87
|
+
node.callee.type === 'Identifier' &&
|
|
88
|
+
node.callee.name === 'craftComponent'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function walk(node, visitor) {
|
|
93
|
+
if (!node || typeof node.type !== 'string') return;
|
|
94
|
+
if (visitor(node) === 'skip') return;
|
|
95
|
+
const keys = sourceCode.visitorKeys[node.type] ?? [];
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const child = node[key];
|
|
98
|
+
if (Array.isArray(child)) {
|
|
99
|
+
child.forEach((entry) => walk(entry, visitor));
|
|
100
|
+
} else {
|
|
101
|
+
walk(child, visitor);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|