@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,274 @@
|
|
|
1
|
+
const DEFAULT_PRIMITIVES = [
|
|
2
|
+
'state',
|
|
3
|
+
'query',
|
|
4
|
+
'mutation',
|
|
5
|
+
'asyncProcess',
|
|
6
|
+
'queryParam',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const FACTORY_HOST_CALLEES = new Set([
|
|
10
|
+
'craftComponent',
|
|
11
|
+
'craftService',
|
|
12
|
+
'toCraftService',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
docs: {
|
|
19
|
+
description:
|
|
20
|
+
'Require craft primitive invocations (which return a generator) to be consumed with `yield*` inside a generator factory or `craftUse(...)` elsewhere.',
|
|
21
|
+
},
|
|
22
|
+
fixable: 'code',
|
|
23
|
+
schema: [
|
|
24
|
+
{
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
primitives: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
messages: {
|
|
36
|
+
requireUnwrap:
|
|
37
|
+
"'{{name}}(...)' returns a primitive generator that must be consumed: use `yield* {{name}}(...)` inside a generator factory, or `craftUse({{name}}(...))` elsewhere.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] ?? {};
|
|
42
|
+
const primitives = new Set(options.primitives ?? DEFAULT_PRIMITIVES);
|
|
43
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
if (
|
|
48
|
+
node.callee.type !== 'Identifier' ||
|
|
49
|
+
!primitives.has(node.callee.name) ||
|
|
50
|
+
// Creation calls always take a config; destructured readers such as
|
|
51
|
+
// `({ state }) => state().page` are argument-less.
|
|
52
|
+
node.arguments.length === 0
|
|
53
|
+
) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!resolvesToImport(node.callee, sourceCode, context)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
isConsumed(node) ||
|
|
63
|
+
isDirectFactoryReturn(node) ||
|
|
64
|
+
isTrackedTypeOnlyPosition(node)
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const primitiveName = node.callee.name;
|
|
70
|
+
|
|
71
|
+
context.report({
|
|
72
|
+
node,
|
|
73
|
+
messageId: 'requireUnwrap',
|
|
74
|
+
data: { name: primitiveName },
|
|
75
|
+
fix(fixer) {
|
|
76
|
+
const text = sourceCode.getText(node);
|
|
77
|
+
|
|
78
|
+
if (isInsideGeneratorFunction(node)) {
|
|
79
|
+
const wrapped = needsYieldParens(node)
|
|
80
|
+
? `(yield* ${text})`
|
|
81
|
+
: `yield* ${text}`;
|
|
82
|
+
return [fixer.replaceText(node, wrapped)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fixes = [fixer.replaceText(node, `craftUse(${text})`)];
|
|
86
|
+
const importFix = createCraftUseImportFix(fixer, sourceCode);
|
|
87
|
+
if (importFix) {
|
|
88
|
+
fixes.push(importFix);
|
|
89
|
+
}
|
|
90
|
+
return fixes;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function resolvesToImport(identifier, sourceCode, context) {
|
|
99
|
+
const scope = sourceCode.getScope
|
|
100
|
+
? sourceCode.getScope(identifier)
|
|
101
|
+
: context.getScope();
|
|
102
|
+
let current = scope;
|
|
103
|
+
while (current) {
|
|
104
|
+
const variable = current.variables.find(
|
|
105
|
+
(candidate) => candidate.name === identifier.name,
|
|
106
|
+
);
|
|
107
|
+
if (variable) {
|
|
108
|
+
if (variable.defs.length === 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return variable.defs.every((def) => def.type === 'ImportBinding');
|
|
112
|
+
}
|
|
113
|
+
current = current.upper;
|
|
114
|
+
}
|
|
115
|
+
// Unresolved (globals) — not an import of the primitive.
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isConsumed(node) {
|
|
120
|
+
const parent = skipParens(node.parent);
|
|
121
|
+
if (!parent) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (parent.type === 'YieldExpression') {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
parent.type === 'CallExpression' &&
|
|
129
|
+
parent.callee.type === 'Identifier' &&
|
|
130
|
+
parent.callee.name === 'craftUse' &&
|
|
131
|
+
skipParensDown(parent.arguments[0]) === node
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isDirectFactoryReturn(node) {
|
|
136
|
+
const arrow = skipParens(node.parent);
|
|
137
|
+
if (!arrow || arrow.type !== 'ArrowFunctionExpression') {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (skipParensDown(arrow.body) !== node) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return isFactoryHostArrow(arrow);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isFactoryHostArrow(arrow) {
|
|
147
|
+
const parent = arrow.parent;
|
|
148
|
+
if (
|
|
149
|
+
parent &&
|
|
150
|
+
parent.type === 'CallExpression' &&
|
|
151
|
+
parent.callee.type === 'Identifier' &&
|
|
152
|
+
FACTORY_HOST_CALLEES.has(parent.callee.name)
|
|
153
|
+
) {
|
|
154
|
+
if (parent.callee.name === 'craftComponent') {
|
|
155
|
+
return parent.arguments[2] === arrow;
|
|
156
|
+
}
|
|
157
|
+
return parent.arguments.includes(arrow);
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
parent &&
|
|
161
|
+
parent.type === 'Property' &&
|
|
162
|
+
parent.key &&
|
|
163
|
+
((parent.key.type === 'Identifier' && parent.key.name === 'queryParams') ||
|
|
164
|
+
(parent.key.type === 'Literal' && parent.key.value === 'queryParams')) &&
|
|
165
|
+
parent.value === arrow
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// `mockHttpRequestForRoute` / type-only fixtures sometimes reference a
|
|
170
|
+
// primitive purely for inference through `typeof`; those never run.
|
|
171
|
+
function isTrackedTypeOnlyPosition(node) {
|
|
172
|
+
let current = node.parent;
|
|
173
|
+
while (current) {
|
|
174
|
+
if (
|
|
175
|
+
current.type === 'TSTypeQuery' ||
|
|
176
|
+
current.type === 'TSTypeReference' ||
|
|
177
|
+
current.type === 'TSTypeAnnotation'
|
|
178
|
+
) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
current = current.parent;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isInsideGeneratorFunction(node) {
|
|
187
|
+
let current = node.parent;
|
|
188
|
+
while (current) {
|
|
189
|
+
if (current.type === 'ArrowFunctionExpression') {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
current.type === 'FunctionDeclaration' ||
|
|
194
|
+
current.type === 'FunctionExpression'
|
|
195
|
+
) {
|
|
196
|
+
return current.generator === true;
|
|
197
|
+
}
|
|
198
|
+
current = current.parent;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function needsYieldParens(node) {
|
|
204
|
+
const parent = node.parent;
|
|
205
|
+
if (!parent) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return (
|
|
209
|
+
parent.type === 'SpreadElement' ||
|
|
210
|
+
parent.type === 'MemberExpression' ||
|
|
211
|
+
parent.type === 'BinaryExpression' ||
|
|
212
|
+
parent.type === 'ConditionalExpression' ||
|
|
213
|
+
parent.type === 'TemplateLiteral' ||
|
|
214
|
+
(parent.type === 'CallExpression' && parent.callee === node)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function skipParens(node) {
|
|
219
|
+
let current = node;
|
|
220
|
+
while (current && current.type === 'ParenthesizedExpression') {
|
|
221
|
+
current = current.parent;
|
|
222
|
+
}
|
|
223
|
+
return current;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function skipParensDown(node) {
|
|
227
|
+
let current = node;
|
|
228
|
+
while (current && current.type === 'ParenthesizedExpression') {
|
|
229
|
+
current = current.expression;
|
|
230
|
+
}
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createCraftUseImportFix(fixer, sourceCode) {
|
|
235
|
+
const program = sourceCode.ast;
|
|
236
|
+
let craftImport;
|
|
237
|
+
|
|
238
|
+
for (const statement of program.body) {
|
|
239
|
+
if (
|
|
240
|
+
statement.type === 'ImportDeclaration' &&
|
|
241
|
+
statement.source.value === '@craft-ts/core'
|
|
242
|
+
) {
|
|
243
|
+
const alreadyImported = statement.specifiers.some(
|
|
244
|
+
(specifier) =>
|
|
245
|
+
specifier.type === 'ImportSpecifier' &&
|
|
246
|
+
specifier.imported.type === 'Identifier' &&
|
|
247
|
+
specifier.imported.name === 'craftUse',
|
|
248
|
+
);
|
|
249
|
+
if (alreadyImported) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
craftImport = statement;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!craftImport) {
|
|
258
|
+
return fixer.insertTextBefore(
|
|
259
|
+
program.body[0] ?? program,
|
|
260
|
+
"import { craftUse } from '@craft-ts/core';\n",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const namedSpecifiers = craftImport.specifiers.filter(
|
|
265
|
+
(specifier) => specifier.type === 'ImportSpecifier',
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (namedSpecifiers.length === 0) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
273
|
+
return fixer.insertTextAfter(lastSpecifier, ', craftUse');
|
|
274
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const REACTIVE_BRANDS = ['SIGNAL', 'YIELDABLE_VALUE', 'INPUT_BRAND'];
|
|
2
|
+
const { NAMED_HTML_HELPERS } = require('./html-helpers.cjs');
|
|
3
|
+
const STRUCTURAL_HELPERS = new Set([
|
|
4
|
+
'catchBlock',
|
|
5
|
+
'content',
|
|
6
|
+
'defer',
|
|
7
|
+
'each',
|
|
8
|
+
'heading',
|
|
9
|
+
'headingSection',
|
|
10
|
+
'ifBlock',
|
|
11
|
+
'liveRegion',
|
|
12
|
+
'matchBlock',
|
|
13
|
+
'renderContent',
|
|
14
|
+
'renderTemplate',
|
|
15
|
+
]);
|
|
16
|
+
const RENDER_HELPERS = new Set(['customElement', 'h', ...NAMED_HTML_HELPERS]);
|
|
17
|
+
const EVENT_NAMES = new Set([
|
|
18
|
+
'blur',
|
|
19
|
+
'change',
|
|
20
|
+
'click',
|
|
21
|
+
'dblclick',
|
|
22
|
+
'focus',
|
|
23
|
+
'input',
|
|
24
|
+
'keydown',
|
|
25
|
+
'keypress',
|
|
26
|
+
'keyup',
|
|
27
|
+
'mousedown',
|
|
28
|
+
'mouseup',
|
|
29
|
+
'pointerdown',
|
|
30
|
+
'pointerup',
|
|
31
|
+
'reset',
|
|
32
|
+
'scroll',
|
|
33
|
+
'submit',
|
|
34
|
+
]);
|
|
35
|
+
const EAGER_CALLBACK_METHODS = new Set([
|
|
36
|
+
'every',
|
|
37
|
+
'filter',
|
|
38
|
+
'find',
|
|
39
|
+
'findIndex',
|
|
40
|
+
'findLast',
|
|
41
|
+
'findLastIndex',
|
|
42
|
+
'flatMap',
|
|
43
|
+
'forEach',
|
|
44
|
+
'map',
|
|
45
|
+
'reduce',
|
|
46
|
+
'reduceRight',
|
|
47
|
+
'some',
|
|
48
|
+
'sort',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description:
|
|
56
|
+
'Require reactive values rendered by Craft components to be read inside granular binding callbacks.',
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
messages: {
|
|
60
|
+
directRead:
|
|
61
|
+
'Do not read a reactive value directly while building a Craft template. Wrap the rendered expression in a binding callback, for example `() => value()`.',
|
|
62
|
+
missingTypeInfo:
|
|
63
|
+
'This rule requires TypeScript type information to identify reactive template values.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
create(context) {
|
|
68
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
69
|
+
const parserServices = sourceCode.parserServices ?? context.parserServices;
|
|
70
|
+
const checker = parserServices?.program?.getTypeChecker?.();
|
|
71
|
+
const esTreeNodeToTSNodeMap = parserServices?.esTreeNodeToTSNodeMap;
|
|
72
|
+
let reportedMissingTypeInfo = false;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
Program() {
|
|
76
|
+
if (!checker || !esTreeNodeToTSNodeMap) {
|
|
77
|
+
context.report({
|
|
78
|
+
node: sourceCode.ast,
|
|
79
|
+
messageId: 'missingTypeInfo',
|
|
80
|
+
});
|
|
81
|
+
reportedMissingTypeInfo = true;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
CallExpression(node) {
|
|
86
|
+
if (
|
|
87
|
+
reportedMissingTypeInfo ||
|
|
88
|
+
!checker ||
|
|
89
|
+
!esTreeNodeToTSNodeMap ||
|
|
90
|
+
node.callee.type !== 'Identifier' ||
|
|
91
|
+
node.callee.name !== 'craftComponent' ||
|
|
92
|
+
node.arguments.length < 4
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
inspectTemplate(node.arguments[3]);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function inspectTemplate(template) {
|
|
101
|
+
walk(template, (node) => {
|
|
102
|
+
if (node !== template && isNestedCraftComponent(node)) {
|
|
103
|
+
return 'skip';
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
node.type !== 'CallExpression' ||
|
|
107
|
+
node.arguments.length !== 0 ||
|
|
108
|
+
!isReactiveRead(node) ||
|
|
109
|
+
isInsideBindingOrAction(node, template)
|
|
110
|
+
) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
context.report({ node, messageId: 'directRead' });
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isReactiveRead(node) {
|
|
118
|
+
const tsNode = esTreeNodeToTSNodeMap.get(node.callee);
|
|
119
|
+
if (!tsNode) return false;
|
|
120
|
+
return hasReactiveBrand(checker.getTypeAtLocation(tsNode), new Set());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hasReactiveBrand(type, seen) {
|
|
124
|
+
if (!type || seen.has(type)) return false;
|
|
125
|
+
seen.add(type);
|
|
126
|
+
if (type.isUnion?.() || type.isIntersection?.()) {
|
|
127
|
+
return type.types.some((part) => hasReactiveBrand(part, seen));
|
|
128
|
+
}
|
|
129
|
+
return checker
|
|
130
|
+
.getPropertiesOfType(type)
|
|
131
|
+
.some((property) =>
|
|
132
|
+
REACTIVE_BRANDS.some((brand) =>
|
|
133
|
+
String(property.escapedName ?? property.name).includes(brand),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isInsideBindingOrAction(node, template) {
|
|
139
|
+
let current = node.parent;
|
|
140
|
+
while (current && current !== template) {
|
|
141
|
+
if (isFunctionNode(current)) {
|
|
142
|
+
if (isEventOrOutputCallback(current)) return true;
|
|
143
|
+
if (isReactiveBindingFunction(current)) return true;
|
|
144
|
+
}
|
|
145
|
+
current = current.parent;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isReactiveBindingFunction(fn) {
|
|
151
|
+
const renderCall = enclosingRenderCall(fn);
|
|
152
|
+
if (!renderCall) return !isEagerCollectionCallback(fn);
|
|
153
|
+
const name = callName(renderCall);
|
|
154
|
+
return !isStructuralTemplateFunction(fn, renderCall, name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isEagerCollectionCallback(fn) {
|
|
158
|
+
const call = fn.parent;
|
|
159
|
+
return Boolean(
|
|
160
|
+
call?.type === 'CallExpression' &&
|
|
161
|
+
call.callee.type === 'MemberExpression' &&
|
|
162
|
+
!call.callee.computed &&
|
|
163
|
+
call.callee.property.type === 'Identifier' &&
|
|
164
|
+
EAGER_CALLBACK_METHODS.has(call.callee.property.name),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isStructuralTemplateFunction(fn, call, name) {
|
|
169
|
+
const argument = directArgumentContaining(fn, call);
|
|
170
|
+
const argumentIndex = argument ? call.arguments.indexOf(argument) : -1;
|
|
171
|
+
const key = enclosingPropertyName(fn, call);
|
|
172
|
+
|
|
173
|
+
if (name === 'each') {
|
|
174
|
+
return argumentIndex === 2 || key === 'empty';
|
|
175
|
+
}
|
|
176
|
+
if (name === 'ifBlock') {
|
|
177
|
+
return argumentIndex === 1 || argumentIndex === 2;
|
|
178
|
+
}
|
|
179
|
+
if (name === 'defer') {
|
|
180
|
+
return ['resolve', 'placeholder', 'loading', 'error'].includes(key);
|
|
181
|
+
}
|
|
182
|
+
if (name === 'exhaustive') {
|
|
183
|
+
return argumentIndex !== 0;
|
|
184
|
+
}
|
|
185
|
+
return Boolean(name && STRUCTURAL_HELPERS.has(name));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function directArgumentContaining(node, call) {
|
|
189
|
+
let current = node;
|
|
190
|
+
while (current?.parent && current.parent !== call) {
|
|
191
|
+
current = current.parent;
|
|
192
|
+
}
|
|
193
|
+
return current?.parent === call ? current : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function enclosingPropertyName(node, call) {
|
|
197
|
+
let current = node.parent;
|
|
198
|
+
while (current && current !== call) {
|
|
199
|
+
if (current.type === 'Property') return propertyName(current);
|
|
200
|
+
current = current.parent;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function enclosingRenderCall(node) {
|
|
206
|
+
let current = node.parent;
|
|
207
|
+
while (current) {
|
|
208
|
+
if (current.type === 'CallExpression') {
|
|
209
|
+
return isRenderCall(current) ? current : undefined;
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
current.type !== 'Property' &&
|
|
213
|
+
current.type !== 'ObjectExpression' &&
|
|
214
|
+
current.type !== 'ArrayExpression' &&
|
|
215
|
+
current.type !== 'SpreadElement' &&
|
|
216
|
+
current.type !== 'TSAsExpression' &&
|
|
217
|
+
current.type !== 'TSSatisfiesExpression' &&
|
|
218
|
+
current.type !== 'ChainExpression'
|
|
219
|
+
) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
current = current.parent;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isRenderCall(node) {
|
|
228
|
+
const name = callName(node);
|
|
229
|
+
if (name && (RENDER_HELPERS.has(name) || STRUCTURAL_HELPERS.has(name))) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (name === 'exhaustive') return true;
|
|
233
|
+
return (
|
|
234
|
+
node.callee.type === 'Identifier' &&
|
|
235
|
+
/^[A-Z]/.test(node.callee.name) &&
|
|
236
|
+
![
|
|
237
|
+
'Array',
|
|
238
|
+
'BigInt',
|
|
239
|
+
'Boolean',
|
|
240
|
+
'Date',
|
|
241
|
+
'Number',
|
|
242
|
+
'Object',
|
|
243
|
+
'String',
|
|
244
|
+
].includes(node.callee.name)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function callName(node) {
|
|
249
|
+
if (node.callee.type === 'Identifier') return node.callee.name;
|
|
250
|
+
if (
|
|
251
|
+
node.callee.type === 'MemberExpression' &&
|
|
252
|
+
!node.callee.computed &&
|
|
253
|
+
node.callee.property.type === 'Identifier'
|
|
254
|
+
) {
|
|
255
|
+
return node.callee.property.name;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isEventOrOutputCallback(fn) {
|
|
261
|
+
const property = fn.parent;
|
|
262
|
+
if (property?.type !== 'Property' || property.value !== fn) return false;
|
|
263
|
+
const name = propertyName(property);
|
|
264
|
+
return Boolean(name && (EVENT_NAMES.has(name) || /^on[A-Z]/.test(name)));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function propertyName(property) {
|
|
268
|
+
if (property.computed) return undefined;
|
|
269
|
+
if (property.key.type === 'Identifier') return property.key.name;
|
|
270
|
+
return property.key.type === 'Literal' &&
|
|
271
|
+
typeof property.key.value === 'string'
|
|
272
|
+
? property.key.value
|
|
273
|
+
: undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isNestedCraftComponent(node) {
|
|
277
|
+
return (
|
|
278
|
+
node.type === 'CallExpression' &&
|
|
279
|
+
node.callee.type === 'Identifier' &&
|
|
280
|
+
node.callee.name === 'craftComponent'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isFunctionNode(node) {
|
|
285
|
+
return (
|
|
286
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
287
|
+
node?.type === 'FunctionExpression'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function walk(node, visitor) {
|
|
292
|
+
if (!node || typeof node.type !== 'string') return;
|
|
293
|
+
if (visitor(node) === 'skip') return;
|
|
294
|
+
const keys = sourceCode.visitorKeys[node.type] ?? [];
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
const child = node[key];
|
|
297
|
+
if (Array.isArray(child)) {
|
|
298
|
+
child.forEach((entry) => walk(entry, visitor));
|
|
299
|
+
} else {
|
|
300
|
+
walk(child, visitor);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { componentInfo } = require('./css-rule-utils.cjs');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
'Require @media (prefers-reduced-motion) when component styles use @keyframes or transition.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missing:
|
|
15
|
+
'Component styles use {{feature}} but never gate motion with prefers-reduced-motion.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
const info = componentInfo(context, sourceCode, node);
|
|
23
|
+
if (!info?.css) return;
|
|
24
|
+
const hasKeyframes = /@keyframes\b/.test(info.css);
|
|
25
|
+
const hasTransition = /\btransition(?:-duration|-property|-timing-function)?\s*:/.test(
|
|
26
|
+
info.css,
|
|
27
|
+
);
|
|
28
|
+
if (!hasKeyframes && !hasTransition) return;
|
|
29
|
+
if (/prefers-reduced-motion/.test(info.css)) return;
|
|
30
|
+
context.report({
|
|
31
|
+
node: info.styles?.value ?? info.stylesUrl.value,
|
|
32
|
+
messageId: 'missing',
|
|
33
|
+
data: { feature: hasKeyframes ? '@keyframes' : 'transition' },
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|