@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.
Files changed (89) hide show
  1. package/README.md +64 -10
  2. package/package.json +8 -2
  3. package/src/bin/agent-selector.d.ts +22 -0
  4. package/src/bin/agent-selector.js +100 -0
  5. package/src/bin/agent-selector.js.map +1 -0
  6. package/src/bin/craft-graph.js +74 -8
  7. package/src/bin/craft-graph.js.map +1 -1
  8. package/src/bin/craft.d.ts +23 -0
  9. package/src/bin/craft.js +245 -28
  10. package/src/bin/craft.js.map +1 -1
  11. package/src/eslint-rules/anchor-has-href.cjs +37 -3
  12. package/src/eslint-rules/craft-computed-name-match.cjs +26 -5
  13. package/src/eslint-rules/craft-name-match-utils.cjs +18 -9
  14. package/src/eslint-rules/hyperscript-walk.cjs +3 -3
  15. package/src/eslint-rules/index.cjs +46 -1
  16. package/src/eslint-rules/no-auth-token-in-local-storage.cjs +17 -0
  17. package/src/eslint-rules/no-craft-computed-side-effects.cjs +12 -5
  18. package/src/eslint-rules/no-craft-service-component-same-file.cjs +138 -0
  19. package/src/eslint-rules/no-effect-import-in-frontend.cjs +52 -0
  20. package/src/eslint-rules/no-effect-in-params.cjs +31 -1
  21. package/src/eslint-rules/no-explicit-effect-type.cjs +137 -0
  22. package/src/eslint-rules/no-free-has.cjs +47 -0
  23. package/src/eslint-rules/no-heading-level-skip.cjs +6 -6
  24. package/src/eslint-rules/no-raw-class.cjs +76 -0
  25. package/src/eslint-rules/no-raw-css-value.cjs +163 -0
  26. package/src/eslint-rules/no-raw-user-url.cjs +114 -0
  27. package/src/eslint-rules/no-trust-forwarded-headers.cjs +43 -0
  28. package/src/eslint-rules/no-unsafe-html.cjs +93 -0
  29. package/src/eslint-rules/no-unsafe-transfer-state.cjs +54 -0
  30. package/src/eslint-rules/prefer-craft-template-blocks.cjs +8 -8
  31. package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +57 -14
  32. package/src/eslint-rules/prefer-inline-effect-insertion.cjs +263 -0
  33. package/src/eslint-rules/prefer-inline-route-providers.cjs +138 -0
  34. package/src/eslint-rules/recommended-config.cjs +6 -0
  35. package/src/eslint-rules/require-reactive-template-bindings.cjs +8 -8
  36. package/src/eslint-rules/require-route-security-policy.cjs +87 -0
  37. package/src/eslint-rules/require-server-function-timeout.cjs +39 -0
  38. package/src/eslint-rules/security-config.cjs +14 -0
  39. package/src/eslint-rules/security-rule-utils.cjs +21 -0
  40. package/src/eslint-rules/style-file-boundary.cjs +75 -0
  41. package/src/eslint-rules/sync-effect-body.cjs +343 -0
  42. package/src/index.d.ts +4 -0
  43. package/src/index.js +4 -0
  44. package/src/index.js.map +1 -1
  45. package/src/scripts/architecture-graph.js +8 -2
  46. package/src/scripts/architecture-graph.js.map +1 -1
  47. package/src/scripts/create/create-project.d.ts +71 -1
  48. package/src/scripts/create/create-project.js +1937 -176
  49. package/src/scripts/create/create-project.js.map +1 -1
  50. package/src/scripts/data-flow-graph.d.ts +1 -1
  51. package/src/scripts/effect-dependency-graph.d.ts +1 -1
  52. package/src/scripts/security-check.d.ts +30 -0
  53. package/src/scripts/security-check.js +223 -0
  54. package/src/scripts/security-check.js.map +1 -0
  55. package/src/scripts/style-architecture.d.ts +56 -0
  56. package/src/scripts/style-architecture.js +142 -0
  57. package/src/scripts/style-architecture.js.map +1 -0
  58. package/src/scripts/style-graph.d.ts +99 -0
  59. package/src/scripts/style-graph.js +179 -0
  60. package/src/scripts/style-graph.js.map +1 -0
  61. package/src/scripts/style-report.d.ts +55 -0
  62. package/src/scripts/style-report.js +67 -0
  63. package/src/scripts/style-report.js.map +1 -0
  64. package/src/scripts/template/migrate-template.js +1 -1
  65. package/src/scripts/template/migrate-template.js.map +1 -1
  66. package/src/scripts/angular-brand-codemod.ts +0 -3985
  67. package/src/scripts/architecture/migrate-architecture.ts +0 -653
  68. package/src/scripts/architecture-graph.ts +0 -3136
  69. package/src/scripts/components/migrate-components.ts +0 -227
  70. package/src/scripts/components/migration-diagnostic.ts +0 -8
  71. package/src/scripts/create/create-project.ts +0 -849
  72. package/src/scripts/data-flow-graph.ts +0 -240
  73. package/src/scripts/dependency-graph.ts +0 -5908
  74. package/src/scripts/effect-dependency-graph.ts +0 -630
  75. package/src/scripts/migrate.ts +0 -162
  76. package/src/scripts/migration-workspace.ts +0 -93
  77. package/src/scripts/primitives/migrate-named-primitives.ts +0 -627
  78. package/src/scripts/primitives/migrate-primitive-generators.ts +0 -410
  79. package/src/scripts/primitives/migrate-primitives.ts +0 -655
  80. package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +0 -499
  81. package/src/scripts/primitives/migration-diagnostic.ts +0 -15
  82. package/src/scripts/routes/migrate-routes.ts +0 -895
  83. package/src/scripts/routes/migration-diagnostic.ts +0 -25
  84. package/src/scripts/routes/route-command.ts +0 -1807
  85. package/src/scripts/routes/verify-routes.ts +0 -1511
  86. package/src/scripts/services/config.ts +0 -60
  87. package/src/scripts/services/migrate-services.ts +0 -1348
  88. package/src/scripts/services/migration-diagnostic.ts +0 -28
  89. 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.body.body[0].argument.argument;
48
- return fixer.replaceText(
49
- node,
50
- sourceCode.getText(delegatedCall.callee),
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 statement = node.body.body[0];
70
- const delegatedCall = statement.argument?.argument;
75
+ const delegatedCall = getDelegatedCall(node);
71
76
 
72
77
  return Boolean(
73
- statement.type === 'ReturnStatement' &&
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.type === 'Identifier',
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
- 'catchBlock',
4
+ 'catchNode',
5
5
  'content',
6
- 'defer',
7
- 'each',
6
+ 'deferNode',
7
+ 'forNode',
8
8
  'heading',
9
9
  'headingSection',
10
- 'ifBlock',
10
+ 'ifNode',
11
11
  'liveRegion',
12
- 'matchBlock',
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 === 'each') {
173
+ if (name === 'forNode') {
174
174
  return argumentIndex === 2 || key === 'empty';
175
175
  }
176
- if (name === 'ifBlock') {
176
+ if (name === 'ifNode') {
177
177
  return argumentIndex === 1 || argumentIndex === 2;
178
178
  }
179
- if (name === 'defer') {
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
+ };