@craft-ts/dev-tools 0.7.0-beta.13 → 0.7.0-beta.15
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/README.md +64 -10
- package/package.json +8 -2
- package/src/bin/agent-selector.d.ts +22 -0
- package/src/bin/agent-selector.js +100 -0
- package/src/bin/agent-selector.js.map +1 -0
- package/src/bin/craft-graph.js +74 -8
- package/src/bin/craft-graph.js.map +1 -1
- package/src/bin/craft.d.ts +23 -0
- package/src/bin/craft.js +245 -28
- package/src/bin/craft.js.map +1 -1
- package/src/eslint-rules/anchor-has-href.cjs +37 -3
- package/src/eslint-rules/craft-computed-name-match.cjs +26 -5
- package/src/eslint-rules/craft-name-match-utils.cjs +18 -9
- package/src/eslint-rules/hyperscript-walk.cjs +3 -3
- package/src/eslint-rules/index.cjs +46 -1
- package/src/eslint-rules/no-auth-token-in-local-storage.cjs +17 -0
- package/src/eslint-rules/no-craft-computed-side-effects.cjs +12 -5
- package/src/eslint-rules/no-craft-service-component-same-file.cjs +138 -0
- package/src/eslint-rules/no-effect-import-in-frontend.cjs +52 -0
- package/src/eslint-rules/no-effect-in-params.cjs +31 -1
- package/src/eslint-rules/no-explicit-effect-type.cjs +137 -0
- package/src/eslint-rules/no-free-has.cjs +47 -0
- package/src/eslint-rules/no-heading-level-skip.cjs +6 -6
- package/src/eslint-rules/no-raw-class.cjs +76 -0
- package/src/eslint-rules/no-raw-css-value.cjs +163 -0
- package/src/eslint-rules/no-raw-user-url.cjs +114 -0
- package/src/eslint-rules/no-trust-forwarded-headers.cjs +43 -0
- package/src/eslint-rules/no-unsafe-html.cjs +93 -0
- package/src/eslint-rules/no-unsafe-transfer-state.cjs +54 -0
- package/src/eslint-rules/prefer-craft-template-blocks.cjs +8 -8
- package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +57 -14
- package/src/eslint-rules/prefer-inline-effect-insertion.cjs +263 -0
- package/src/eslint-rules/prefer-inline-route-providers.cjs +138 -0
- package/src/eslint-rules/recommended-config.cjs +6 -0
- package/src/eslint-rules/require-reactive-template-bindings.cjs +8 -8
- package/src/eslint-rules/require-route-security-policy.cjs +87 -0
- package/src/eslint-rules/require-server-function-timeout.cjs +39 -0
- package/src/eslint-rules/security-config.cjs +14 -0
- package/src/eslint-rules/security-rule-utils.cjs +21 -0
- package/src/eslint-rules/style-file-boundary.cjs +75 -0
- package/src/eslint-rules/sync-effect-body.cjs +343 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/scripts/architecture-graph.js +8 -2
- package/src/scripts/architecture-graph.js.map +1 -1
- package/src/scripts/create/create-project.d.ts +71 -1
- package/src/scripts/create/create-project.js +1937 -176
- package/src/scripts/create/create-project.js.map +1 -1
- package/src/scripts/data-flow-graph.d.ts +1 -1
- package/src/scripts/effect-dependency-graph.d.ts +1 -1
- package/src/scripts/security-check.d.ts +30 -0
- package/src/scripts/security-check.js +223 -0
- package/src/scripts/security-check.js.map +1 -0
- package/src/scripts/style-architecture.d.ts +56 -0
- package/src/scripts/style-architecture.js +142 -0
- package/src/scripts/style-architecture.js.map +1 -0
- package/src/scripts/style-graph.d.ts +99 -0
- package/src/scripts/style-graph.js +179 -0
- package/src/scripts/style-graph.js.map +1 -0
- package/src/scripts/style-report.d.ts +55 -0
- package/src/scripts/style-report.js +67 -0
- package/src/scripts/style-report.js.map +1 -0
- package/src/scripts/template/migrate-template.js +1 -1
- package/src/scripts/template/migrate-template.js.map +1 -1
- package/src/scripts/angular-brand-codemod.ts +0 -3985
- package/src/scripts/architecture/migrate-architecture.ts +0 -653
- package/src/scripts/architecture-graph.ts +0 -3136
- package/src/scripts/components/migrate-components.ts +0 -227
- package/src/scripts/components/migration-diagnostic.ts +0 -8
- package/src/scripts/create/create-project.ts +0 -849
- package/src/scripts/data-flow-graph.ts +0 -240
- package/src/scripts/dependency-graph.ts +0 -5908
- package/src/scripts/effect-dependency-graph.ts +0 -630
- package/src/scripts/migrate.ts +0 -162
- package/src/scripts/migration-workspace.ts +0 -93
- package/src/scripts/primitives/migrate-named-primitives.ts +0 -627
- package/src/scripts/primitives/migrate-primitive-generators.ts +0 -410
- package/src/scripts/primitives/migrate-primitives.ts +0 -655
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +0 -499
- package/src/scripts/primitives/migration-diagnostic.ts +0 -15
- package/src/scripts/routes/migrate-routes.ts +0 -895
- package/src/scripts/routes/migration-diagnostic.ts +0 -25
- package/src/scripts/routes/route-command.ts +0 -1807
- package/src/scripts/routes/verify-routes.ts +0 -1511
- package/src/scripts/services/config.ts +0 -60
- package/src/scripts/services/migrate-services.ts +0 -1348
- package/src/scripts/services/migration-diagnostic.ts +0 -28
- package/src/scripts/template/migrate-template.ts +0 -418
|
@@ -3,7 +3,7 @@ module.exports = {
|
|
|
3
3
|
type: 'suggestion',
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
'Prefer passing a yieldable callback directly when a template generator only delegates to it.',
|
|
6
|
+
'Prefer passing a yieldable callback directly when a template generator or method only delegates to it.',
|
|
7
7
|
},
|
|
8
8
|
fixable: 'code',
|
|
9
9
|
schema: [],
|
|
@@ -41,14 +41,20 @@ module.exports = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
context.report({
|
|
44
|
-
node,
|
|
44
|
+
node: getReportNode(node),
|
|
45
45
|
messageId: 'preferDirect',
|
|
46
46
|
fix(fixer) {
|
|
47
|
-
const delegatedCall = node
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const delegatedCall = getDelegatedCall(node);
|
|
48
|
+
const directCallback = sourceCode.getText(delegatedCall.callee);
|
|
49
|
+
|
|
50
|
+
if (isMethodProperty(node)) {
|
|
51
|
+
return fixer.replaceText(
|
|
52
|
+
node.parent,
|
|
53
|
+
`${sourceCode.getText(node.parent.key)}: ${directCallback}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fixer.replaceText(node, directCallback);
|
|
52
58
|
},
|
|
53
59
|
});
|
|
54
60
|
});
|
|
@@ -66,19 +72,56 @@ module.exports = {
|
|
|
66
72
|
return false;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
const
|
|
70
|
-
const delegatedCall = statement.argument?.argument;
|
|
75
|
+
const delegatedCall = getDelegatedCall(node);
|
|
71
76
|
|
|
72
77
|
return Boolean(
|
|
73
|
-
|
|
74
|
-
statement.argument?.type === 'YieldExpression' &&
|
|
75
|
-
statement.argument.delegate &&
|
|
76
|
-
delegatedCall?.type === 'CallExpression' &&
|
|
78
|
+
delegatedCall?.type === 'CallExpression' &&
|
|
77
79
|
delegatedCall.arguments.length === 0 &&
|
|
78
|
-
delegatedCall.callee
|
|
80
|
+
isDirectCallbackCallee(delegatedCall.callee),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDirectCallbackCallee(callee) {
|
|
85
|
+
if (callee.type === 'Identifier') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
callee.type === 'MemberExpression' &&
|
|
91
|
+
!callee.computed &&
|
|
92
|
+
!callee.optional &&
|
|
93
|
+
callee.property.type === 'Identifier' &&
|
|
94
|
+
isDirectCallbackCallee(callee.object)
|
|
79
95
|
);
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
function getDelegatedCall(node) {
|
|
99
|
+
const statement = node.body.body[0];
|
|
100
|
+
const yieldExpression =
|
|
101
|
+
statement.type === 'ReturnStatement'
|
|
102
|
+
? statement.argument
|
|
103
|
+
: statement.type === 'ExpressionStatement'
|
|
104
|
+
? statement.expression
|
|
105
|
+
: undefined;
|
|
106
|
+
|
|
107
|
+
return yieldExpression?.type === 'YieldExpression' &&
|
|
108
|
+
yieldExpression.delegate
|
|
109
|
+
? yieldExpression.argument
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isMethodProperty(node) {
|
|
114
|
+
return (
|
|
115
|
+
node.parent?.type === 'Property' &&
|
|
116
|
+
node.parent.value === node &&
|
|
117
|
+
node.parent.method
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getReportNode(node) {
|
|
122
|
+
return isMethodProperty(node) ? node.parent : node;
|
|
123
|
+
}
|
|
124
|
+
|
|
82
125
|
function isNestedCraftComponent(node) {
|
|
83
126
|
return (
|
|
84
127
|
node.type === 'CallExpression' &&
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EFFECT_PACKAGE = '@craft-ts/effect';
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
'Keep queryEffect insertion callbacks inline so their types are inferred.',
|
|
11
|
+
},
|
|
12
|
+
fixable: 'code',
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
inline:
|
|
16
|
+
'Keep the queryEffect insertion callback inline and let its types be inferred; do not extract a typed helper.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
22
|
+
const queryEffectBindings = new Set();
|
|
23
|
+
const visitorKeys = sourceCode.visitorKeys ?? {};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ImportDeclaration(node) {
|
|
27
|
+
if (node.source.value !== EFFECT_PACKAGE) return;
|
|
28
|
+
for (const specifier of node.specifiers) {
|
|
29
|
+
if (
|
|
30
|
+
specifier.type === 'ImportSpecifier' &&
|
|
31
|
+
getIdentifierName(specifier.imported) === 'queryEffect'
|
|
32
|
+
) {
|
|
33
|
+
queryEffectBindings.add(specifier.local.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
'Program:exit'(program) {
|
|
39
|
+
walk(program, (node) => {
|
|
40
|
+
if (
|
|
41
|
+
node.type !== 'CallExpression' ||
|
|
42
|
+
node.callee.type !== 'Identifier' ||
|
|
43
|
+
!queryEffectBindings.has(node.callee.name)
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const insertion = node.arguments[2];
|
|
49
|
+
if (!insertion || insertion.type !== 'Identifier') return;
|
|
50
|
+
|
|
51
|
+
const helper = findInsertionHelper(program, insertion.name);
|
|
52
|
+
if (!helper) return;
|
|
53
|
+
|
|
54
|
+
const callback = helper.init;
|
|
55
|
+
const callbackText = withoutParameterType(callback, sourceCode);
|
|
56
|
+
const fixes = [
|
|
57
|
+
fixerReplace(sourceCode, insertion, callbackText),
|
|
58
|
+
removeDeclaration(helper),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const typeName = getParameterTypeName(callback);
|
|
62
|
+
const typeAlias = typeName && findTypeAlias(program, typeName);
|
|
63
|
+
if (
|
|
64
|
+
typeAlias &&
|
|
65
|
+
!hasOtherReference(program, typeName, [
|
|
66
|
+
typeAlias,
|
|
67
|
+
callback.params[0]?.typeAnnotation,
|
|
68
|
+
])
|
|
69
|
+
) {
|
|
70
|
+
fixes.push(removeDeclaration(typeAlias));
|
|
71
|
+
|
|
72
|
+
const insertionParamsImport = findImportSpecifier(
|
|
73
|
+
program,
|
|
74
|
+
'InsertionParams',
|
|
75
|
+
);
|
|
76
|
+
if (
|
|
77
|
+
insertionParamsImport &&
|
|
78
|
+
!hasOtherReference(program, 'InsertionParams', [
|
|
79
|
+
typeAlias,
|
|
80
|
+
insertionParamsImport,
|
|
81
|
+
])
|
|
82
|
+
) {
|
|
83
|
+
fixes.push(removeImportSpecifier(insertionParamsImport));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
context.report({
|
|
88
|
+
node: insertion,
|
|
89
|
+
messageId: 'inline',
|
|
90
|
+
fix(fixer) {
|
|
91
|
+
return fixes.map((fix) => fix(fixer));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}, visitorKeys);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function fixerReplace(_sourceCode, node, text) {
|
|
99
|
+
return (fixer) => fixer.replaceText(node, text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function removeDeclaration(node) {
|
|
103
|
+
return (fixer) => {
|
|
104
|
+
const declaration = node.parent;
|
|
105
|
+
if (declaration.type === 'TSTypeAliasDeclaration') {
|
|
106
|
+
const container = declaration.parent;
|
|
107
|
+
return container.type === 'ExportNamedDeclaration'
|
|
108
|
+
? fixer.remove(container)
|
|
109
|
+
: fixer.remove(declaration);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (declaration.type !== 'VariableDeclaration') {
|
|
113
|
+
return fixer.remove(node);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const statement =
|
|
117
|
+
declaration.parent.type === 'ExportNamedDeclaration'
|
|
118
|
+
? declaration.parent
|
|
119
|
+
: declaration;
|
|
120
|
+
if (
|
|
121
|
+
declaration.declarations.length === 1
|
|
122
|
+
) {
|
|
123
|
+
return fixer.remove(statement);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const index = declaration.declarations.indexOf(node);
|
|
127
|
+
const next = declaration.declarations[index + 1];
|
|
128
|
+
const previous = declaration.declarations[index - 1];
|
|
129
|
+
return next
|
|
130
|
+
? fixer.removeRange([node.range[0], next.range[0]])
|
|
131
|
+
: fixer.removeRange([previous.range[1], node.range[1]]);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function removeImportSpecifier(specifier) {
|
|
136
|
+
return (fixer) => {
|
|
137
|
+
const declaration = specifier.parent;
|
|
138
|
+
if (declaration.specifiers.length === 1) {
|
|
139
|
+
return fixer.remove(declaration);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const index = declaration.specifiers.indexOf(specifier);
|
|
143
|
+
const next = declaration.specifiers[index + 1];
|
|
144
|
+
const previous = declaration.specifiers[index - 1];
|
|
145
|
+
return next
|
|
146
|
+
? fixer.removeRange([specifier.range[0], next.range[0]])
|
|
147
|
+
: fixer.removeRange([previous.range[1], specifier.range[1]]);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findInsertionHelper(program, name) {
|
|
152
|
+
let result;
|
|
153
|
+
walk(program, (node) => {
|
|
154
|
+
if (
|
|
155
|
+
result ||
|
|
156
|
+
node.type !== 'VariableDeclarator' ||
|
|
157
|
+
node.id.type !== 'Identifier' ||
|
|
158
|
+
node.id.name !== name ||
|
|
159
|
+
!isFunction(node.init)
|
|
160
|
+
) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
result = node;
|
|
164
|
+
}, visitorKeys);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findTypeAlias(program, name) {
|
|
169
|
+
let result;
|
|
170
|
+
walk(program, (node) => {
|
|
171
|
+
if (
|
|
172
|
+
!result &&
|
|
173
|
+
node.type === 'TSTypeAliasDeclaration' &&
|
|
174
|
+
node.id.name === name
|
|
175
|
+
) {
|
|
176
|
+
result = node;
|
|
177
|
+
}
|
|
178
|
+
}, visitorKeys);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findImportSpecifier(program, name) {
|
|
183
|
+
let result;
|
|
184
|
+
walk(program, (node) => {
|
|
185
|
+
if (
|
|
186
|
+
!result &&
|
|
187
|
+
node.type === 'ImportSpecifier' &&
|
|
188
|
+
getIdentifierName(node.imported) === name
|
|
189
|
+
) {
|
|
190
|
+
result = node;
|
|
191
|
+
}
|
|
192
|
+
}, visitorKeys);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function hasOtherReference(program, name, excluded) {
|
|
197
|
+
let found = false;
|
|
198
|
+
walk(program, (node) => {
|
|
199
|
+
if (
|
|
200
|
+
!found &&
|
|
201
|
+
node.type === 'Identifier' &&
|
|
202
|
+
node.name === name &&
|
|
203
|
+
!excluded.some((parent) => parent && isDescendant(node, parent))
|
|
204
|
+
) {
|
|
205
|
+
found = true;
|
|
206
|
+
}
|
|
207
|
+
}, visitorKeys);
|
|
208
|
+
return found;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isDescendant(node, ancestor) {
|
|
212
|
+
let current = node;
|
|
213
|
+
while (current) {
|
|
214
|
+
if (current === ancestor) return true;
|
|
215
|
+
current = current.parent;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function getParameterTypeName(callback) {
|
|
223
|
+
const annotation = callback.params[0]?.typeAnnotation;
|
|
224
|
+
const typeName = annotation?.typeAnnotation;
|
|
225
|
+
return typeName?.type === 'TSTypeReference' &&
|
|
226
|
+
typeName.typeName.type === 'Identifier'
|
|
227
|
+
? typeName.typeName.name
|
|
228
|
+
: undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function withoutParameterType(callback, sourceCode) {
|
|
232
|
+
const text = sourceCode.getText(callback);
|
|
233
|
+
const annotation = callback.params[0]?.typeAnnotation;
|
|
234
|
+
if (!annotation) return text;
|
|
235
|
+
|
|
236
|
+
const start = annotation.range[0] - callback.range[0];
|
|
237
|
+
const end = annotation.range[1] - callback.range[0];
|
|
238
|
+
return text.slice(0, start) + text.slice(end);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isFunction(node) {
|
|
242
|
+
return (
|
|
243
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
244
|
+
node?.type === 'FunctionExpression'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getIdentifierName(node) {
|
|
249
|
+
return node.type === 'Identifier' ? node.name : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function walk(node, visit, visitorKeys) {
|
|
253
|
+
if (!node || typeof node.type !== 'string') return;
|
|
254
|
+
visit(node);
|
|
255
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
256
|
+
const child = node[key];
|
|
257
|
+
if (Array.isArray(child)) {
|
|
258
|
+
child.forEach((item) => walk(item, visit, visitorKeys));
|
|
259
|
+
} else {
|
|
260
|
+
walk(child, visit, visitorKeys);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const COMPONENT_PACKAGE = '@craft-ts/component';
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
'Prefer inlining route providers that are used only once by loadCraftComponent.',
|
|
11
|
+
},
|
|
12
|
+
fixable: 'code',
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
inline:
|
|
16
|
+
'Inline this route provider tuple in loadCraftComponent(...); it is only used once.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
22
|
+
const loadCraftComponentBindings = new Set();
|
|
23
|
+
const visitorKeys = sourceCode.visitorKeys ?? {};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ImportDeclaration(node) {
|
|
27
|
+
if (node.source.value !== COMPONENT_PACKAGE) return;
|
|
28
|
+
for (const specifier of node.specifiers) {
|
|
29
|
+
if (
|
|
30
|
+
specifier.type === 'ImportSpecifier' &&
|
|
31
|
+
getIdentifierName(specifier.imported) === 'loadCraftComponent'
|
|
32
|
+
) {
|
|
33
|
+
loadCraftComponentBindings.add(specifier.local.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
'Program:exit'(program) {
|
|
39
|
+
walk(program, (node) => {
|
|
40
|
+
if (node.type !== 'VariableDeclarator') return;
|
|
41
|
+
if (
|
|
42
|
+
node.id.type !== 'Identifier' ||
|
|
43
|
+
!isProviderArray(node.init)
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const references = findReferences(program, node.id.name, node);
|
|
49
|
+
if (references.length !== 1) return;
|
|
50
|
+
|
|
51
|
+
const reference = references[0];
|
|
52
|
+
const call = reference.parent;
|
|
53
|
+
if (
|
|
54
|
+
call?.type !== 'CallExpression' ||
|
|
55
|
+
call.arguments[1] !== reference ||
|
|
56
|
+
call.callee.type !== 'Identifier' ||
|
|
57
|
+
!loadCraftComponentBindings.has(call.callee.name)
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
context.report({
|
|
63
|
+
node: reference,
|
|
64
|
+
messageId: 'inline',
|
|
65
|
+
fix(fixer) {
|
|
66
|
+
return [
|
|
67
|
+
fixer.replaceText(reference, sourceCode.getText(node.init)),
|
|
68
|
+
removeDeclaration(fixer, node),
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}, visitorKeys);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function findReferences(program, name, declaration) {
|
|
77
|
+
const references = [];
|
|
78
|
+
walk(program, (node) => {
|
|
79
|
+
if (
|
|
80
|
+
node.type === 'Identifier' &&
|
|
81
|
+
node.name === name &&
|
|
82
|
+
node !== declaration.id
|
|
83
|
+
) {
|
|
84
|
+
references.push(node);
|
|
85
|
+
}
|
|
86
|
+
}, visitorKeys);
|
|
87
|
+
return references;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function isProviderArray(node) {
|
|
93
|
+
let current = node;
|
|
94
|
+
while (
|
|
95
|
+
current &&
|
|
96
|
+
['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression'].includes(
|
|
97
|
+
current.type,
|
|
98
|
+
)
|
|
99
|
+
) {
|
|
100
|
+
current = current.expression;
|
|
101
|
+
}
|
|
102
|
+
return current?.type === 'ArrayExpression';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function removeDeclaration(fixer, node) {
|
|
106
|
+
const declaration = node.parent;
|
|
107
|
+
if (declaration.type !== 'VariableDeclaration') {
|
|
108
|
+
return fixer.remove(node);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (declaration.declarations.length === 1) {
|
|
112
|
+
return fixer.remove(declaration);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const index = declaration.declarations.indexOf(node);
|
|
116
|
+
const next = declaration.declarations[index + 1];
|
|
117
|
+
const previous = declaration.declarations[index - 1];
|
|
118
|
+
return next
|
|
119
|
+
? fixer.removeRange([node.range[0], next.range[0]])
|
|
120
|
+
: fixer.removeRange([previous.range[1], node.range[1]]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getIdentifierName(node) {
|
|
124
|
+
return node.type === 'Identifier' ? node.name : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function walk(node, visit, visitorKeys) {
|
|
128
|
+
if (!node || typeof node.type !== 'string') return;
|
|
129
|
+
visit(node);
|
|
130
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
131
|
+
const child = node[key];
|
|
132
|
+
if (Array.isArray(child)) {
|
|
133
|
+
child.forEach((item) => walk(item, visit, visitorKeys));
|
|
134
|
+
} else {
|
|
135
|
+
walk(child, visit, visitorKeys);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -18,6 +18,11 @@ module.exports = {
|
|
|
18
18
|
'craft-ts/no-craft-computed-side-effects': 'error',
|
|
19
19
|
'craft-ts/provide-host-name-match-component': 'warn',
|
|
20
20
|
'craft-ts/prefer-craft-http-transport': 'error',
|
|
21
|
+
'craft-ts/no-craft-service-component-same-file': 'warn',
|
|
22
|
+
'craft-ts/no-raw-css-value': 'error',
|
|
23
|
+
'craft-ts/no-raw-class': 'error',
|
|
24
|
+
'craft-ts/no-free-has': 'error',
|
|
25
|
+
'craft-ts/style-file-boundary': 'error',
|
|
21
26
|
'craft-ts/no-imperative-craft-resource-trigger': 'error',
|
|
22
27
|
'craft-ts/no-transition-actions': 'error',
|
|
23
28
|
'craft-ts/require-craft-resource-trigger-yield': 'error',
|
|
@@ -43,6 +48,7 @@ module.exports = {
|
|
|
43
48
|
'craft-ts/no-injection-token': 'error',
|
|
44
49
|
'craft-ts/no-manual-route-provider-list': 'error',
|
|
45
50
|
'craft-ts/no-widened-route-provider-context': 'error',
|
|
51
|
+
'craft-ts/prefer-inline-route-providers': 'error',
|
|
46
52
|
'craft-ts/no-async-await': 'error',
|
|
47
53
|
'craft-ts/no-throw': 'error',
|
|
48
54
|
'craft-ts/require-assert-exhaustive-route-exceptions': 'warn',
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
const REACTIVE_BRANDS = ['SIGNAL', 'YIELDABLE_VALUE', 'INPUT_BRAND'];
|
|
2
2
|
const { NAMED_HTML_HELPERS } = require('./html-helpers.cjs');
|
|
3
3
|
const STRUCTURAL_HELPERS = new Set([
|
|
4
|
-
'
|
|
4
|
+
'catchNode',
|
|
5
5
|
'content',
|
|
6
|
-
'
|
|
7
|
-
'
|
|
6
|
+
'deferNode',
|
|
7
|
+
'forNode',
|
|
8
8
|
'heading',
|
|
9
9
|
'headingSection',
|
|
10
|
-
'
|
|
10
|
+
'ifNode',
|
|
11
11
|
'liveRegion',
|
|
12
|
-
'
|
|
12
|
+
'matchNode',
|
|
13
13
|
'renderContent',
|
|
14
14
|
'renderTemplate',
|
|
15
15
|
]);
|
|
@@ -170,13 +170,13 @@ module.exports = {
|
|
|
170
170
|
const argumentIndex = argument ? call.arguments.indexOf(argument) : -1;
|
|
171
171
|
const key = enclosingPropertyName(fn, call);
|
|
172
172
|
|
|
173
|
-
if (name === '
|
|
173
|
+
if (name === 'forNode') {
|
|
174
174
|
return argumentIndex === 2 || key === 'empty';
|
|
175
175
|
}
|
|
176
|
-
if (name === '
|
|
176
|
+
if (name === 'ifNode') {
|
|
177
177
|
return argumentIndex === 1 || argumentIndex === 2;
|
|
178
178
|
}
|
|
179
|
-
if (name === '
|
|
179
|
+
if (name === 'deferNode') {
|
|
180
180
|
return ['resolve', 'placeholder', 'loading', 'error'].includes(key);
|
|
181
181
|
}
|
|
182
182
|
if (name === 'exhaustive') {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { report, isCallNamed } = require('./security-rule-utils.cjs');
|
|
2
|
+
|
|
3
|
+
function nameOf(property) {
|
|
4
|
+
return String(property.key.name ?? property.key.value ?? '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** `component:`, `loadComponent:` ou le spread `...loadCraftComponent(...)`. */
|
|
8
|
+
function rendersComponent(route) {
|
|
9
|
+
return route.properties.some((property) => {
|
|
10
|
+
if (property.type === 'Property') {
|
|
11
|
+
const name = nameOf(property);
|
|
12
|
+
return name === 'component' || name === 'loadComponent';
|
|
13
|
+
}
|
|
14
|
+
if (property.type === 'SpreadElement') {
|
|
15
|
+
return isCallNamed(property.argument, 'loadCraftComponent');
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function declaresPolicy(route) {
|
|
22
|
+
return route.properties.some(
|
|
23
|
+
(property) =>
|
|
24
|
+
property.type === 'Property' &&
|
|
25
|
+
(nameOf(property) === 'ssr' || nameOf(property) === 'security'),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
schema: [
|
|
33
|
+
{
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
/**
|
|
37
|
+
* `auto` (défaut) : n'exige la déclaration que dans un fichier qui
|
|
38
|
+
* en contient déjà une, c'est-à-dire une application qui rend côté
|
|
39
|
+
* serveur. Une route oubliée y est un vrai trou ; une SPA qui ne
|
|
40
|
+
* déclare rien nulle part n'a rien à déclarer.
|
|
41
|
+
* `required` : exige la politique sur toute route rendue.
|
|
42
|
+
* `off` : désactive la vérification.
|
|
43
|
+
*/
|
|
44
|
+
mode: { enum: ['auto', 'required', 'off'] },
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
docs: {
|
|
50
|
+
description: 'Require an explicit SSR policy on every rendered route.',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
create(context) {
|
|
54
|
+
const mode = (context.options[0] && context.options[0].mode) || 'auto';
|
|
55
|
+
if (mode === 'off') return {};
|
|
56
|
+
return {
|
|
57
|
+
CallExpression(node) {
|
|
58
|
+
if (!isCallNamed(node, 'craftRoutes')) return;
|
|
59
|
+
const list = node.arguments.find(
|
|
60
|
+
(argument) => argument.type === 'ArrayExpression',
|
|
61
|
+
);
|
|
62
|
+
if (!list) return;
|
|
63
|
+
const routes = list.elements.filter(
|
|
64
|
+
(element) => element && element.type === 'ObjectExpression',
|
|
65
|
+
);
|
|
66
|
+
const rendered = routes.filter(rendersComponent);
|
|
67
|
+
if (mode === 'auto' && !rendered.some(declaresPolicy)) return;
|
|
68
|
+
for (const route of rendered) {
|
|
69
|
+
if (declaresPolicy(route)) continue;
|
|
70
|
+
const path = route.properties.find(
|
|
71
|
+
(property) =>
|
|
72
|
+
property.type === 'Property' && nameOf(property) === 'path',
|
|
73
|
+
);
|
|
74
|
+
const label =
|
|
75
|
+
path && path.value.type === 'Literal'
|
|
76
|
+
? `"${path.value.value}"`
|
|
77
|
+
: 'a route';
|
|
78
|
+
report(
|
|
79
|
+
context,
|
|
80
|
+
route,
|
|
81
|
+
`Route ${label} must declare an explicit ssr policy (mode: 'block' | 'skip' | 'stream').`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { report, isCallNamed } = require('./security-rule-utils.cjs');
|
|
2
|
+
|
|
3
|
+
function propertyOf(node, name) {
|
|
4
|
+
if (!node || node.type !== 'ObjectExpression') return undefined;
|
|
5
|
+
return node.properties.find(
|
|
6
|
+
(property) =>
|
|
7
|
+
property.type === 'Property' &&
|
|
8
|
+
(property.key.name ?? property.key.value) === name,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
schema: [],
|
|
16
|
+
docs: { description: 'Require body and timeout limits for server functions.' },
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
if (!isCallNamed(node, 'createServer')) return;
|
|
22
|
+
const options = node.arguments[0];
|
|
23
|
+
if (!options || options.type !== 'ObjectExpression') return;
|
|
24
|
+
// `createServer` de node:http prend un callback : ne rien dire.
|
|
25
|
+
if (!propertyOf(options, 'functions')) return;
|
|
26
|
+
const runtime = propertyOf(options, 'runtimeOptions');
|
|
27
|
+
const value = runtime && runtime.value;
|
|
28
|
+
const has = (name) => Boolean(propertyOf(value, name));
|
|
29
|
+
if (!has('timeoutMs') || !has('maxBodyBytes')) {
|
|
30
|
+
report(
|
|
31
|
+
context,
|
|
32
|
+
node,
|
|
33
|
+
'createServer requires runtimeOptions.timeoutMs and runtimeOptions.maxBodyBytes.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Préréglage sécurité, inclus dans `recommended` et dans `effect` : ces
|
|
3
|
+
* garde-fous portent sur des vecteurs d'attaque, pas sur du style, donc ils
|
|
4
|
+
* s'appliquent par défaut plutôt que sur adhésion.
|
|
5
|
+
*/
|
|
6
|
+
module.exports = {
|
|
7
|
+
'craft-ts/require-route-security-policy': 'error',
|
|
8
|
+
'craft-ts/require-server-function-timeout': 'error',
|
|
9
|
+
'craft-ts/no-auth-token-in-local-storage': 'error',
|
|
10
|
+
'craft-ts/no-raw-user-url': 'error',
|
|
11
|
+
'craft-ts/no-unsafe-transfer-state': 'error',
|
|
12
|
+
'craft-ts/no-unsafe-html': 'error',
|
|
13
|
+
'craft-ts/no-trust-forwarded-headers': 'error',
|
|
14
|
+
};
|