@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
@@ -0,0 +1,76 @@
1
+ const STYLE_MODULE = '@craft-ts/style';
2
+
3
+ /**
4
+ * A `class:` binding is a sheet class, never a string.
5
+ *
6
+ * Without this, the visual matrix is a fiction: a class assembled at render
7
+ * time is a visual state nothing recorded, so the matrix enumerates what the
8
+ * sheets declare while the DOM shows something else. Partial tightness here
9
+ * buys 0 % of the guarantee, not 90 %.
10
+ *
11
+ * It fires **only in files that import `@craft-ts/style`**. A component that
12
+ * has not been migrated is not claiming the guarantee, and reporting it would
13
+ * teach people to disable the rule long before the app is ready for it. The day
14
+ * a file starts using the design system, it starts being held to it.
15
+ */
16
+ module.exports = {
17
+ meta: {
18
+ type: 'problem',
19
+ docs: {
20
+ description:
21
+ 'Disallow string and computed class bindings in files that use @craft-ts/style.',
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ rawClass:
26
+ 'A class binding must be a class from a craftStyles sheet, not a string. This one is invisible to the visual matrix, so the state it produces is one nothing will ever capture. Move the rule into the sheet and bind the class it returns.',
27
+ computedClass:
28
+ 'A class binding must not be computed at render time. Every string this can produce is a visual state nobody can enumerate; make the variation an axis — when(tone.danger, [...]) — and set a data attribute instead.',
29
+ },
30
+ },
31
+ create(context) {
32
+ let usesStyle = false;
33
+ const reports = [];
34
+
35
+ const report = (node, messageId) => reports.push({ node, messageId });
36
+
37
+ const inspect = (value) => {
38
+ if (!value) return;
39
+ if (value.type === 'Literal' && typeof value.value === 'string') {
40
+ report(value, 'rawClass');
41
+ return;
42
+ }
43
+ if (value.type === 'TemplateLiteral') {
44
+ report(value, value.expressions.length ? 'computedClass' : 'rawClass');
45
+ return;
46
+ }
47
+ if (
48
+ value.type === 'FunctionExpression' ||
49
+ value.type === 'ArrowFunctionExpression'
50
+ ) {
51
+ report(value, 'computedClass');
52
+ }
53
+ };
54
+
55
+ return {
56
+ ImportDeclaration(node) {
57
+ if (node.source.value === STYLE_MODULE) usesStyle = true;
58
+ },
59
+ Property(node) {
60
+ const key = node.key;
61
+ const name =
62
+ key.type === 'Identifier'
63
+ ? key.name
64
+ : key.type === 'Literal'
65
+ ? key.value
66
+ : undefined;
67
+ if (name !== 'class') return;
68
+ inspect(node.value);
69
+ },
70
+ 'Program:exit'() {
71
+ if (!usesStyle) return;
72
+ for (const entry of reports) context.report(entry);
73
+ },
74
+ };
75
+ },
76
+ };
@@ -0,0 +1,163 @@
1
+ const STYLE_MODULE = '@craft-ts/style';
2
+
3
+ /**
4
+ * A style helper never takes a literal.
5
+ *
6
+ * The types already say so — no generated signature accepts a `string` — so
7
+ * this rule is not a second guard against the same mistake. It exists for the
8
+ * two places typing cannot reach: a value flowing in through `any`, and code
9
+ * that has not been typechecked yet while it is being written. The message
10
+ * points at the exact replacement, because a rule that only says "no" sends an
11
+ * agent looking for a way round.
12
+ *
13
+ * It extends `no-hardcoded-design-values` to style-helper arguments rather than
14
+ * duplicating it: same intent, one more position.
15
+ */
16
+ const REPLACEMENTS = {
17
+ length: 'a scale step or a unit — space(4), unit.rem(1.5), radii.md',
18
+ color: 'a palette token — palette.surface.raised',
19
+ keyword: 'the keyword member — display.flex, position.sticky',
20
+ };
21
+
22
+ const GLOBAL_KEYWORDS = new Set([
23
+ 'inherit',
24
+ 'initial',
25
+ 'unset',
26
+ 'revert',
27
+ 'revert-layer',
28
+ ]);
29
+
30
+ const LENGTH_LIKE = /^-?\d*\.?\d+(px|rem|em|%|vh|vw|ch|ex|pt|cm|mm|in)$/;
31
+ const COLOR_LIKE = /^(#[0-9a-fA-F]{3,8}|rgb|rgba|hsl|hsla|var\()/;
32
+
33
+ /**
34
+ * Helpers whose argument is legitimately a primitive.
35
+ *
36
+ * `space(4)` is how a scale step is written, `cssVars('badge', …)` takes a
37
+ * name, `unsafeLength('13px', reason)` is the marked way out. Flagging these
38
+ * would teach people to disable the rule, which costs more than the rule buys.
39
+ */
40
+ const TAKES_PRIMITIVES = new Set([
41
+ 'space',
42
+ 'num',
43
+ 'int',
44
+ 'ident',
45
+ 'cssString',
46
+ 'url',
47
+ 'unsafeLength',
48
+ 'unsafeAssume',
49
+ 'oneOf',
50
+ 'craftStyles',
51
+ 'cssVars',
52
+ 'definePalette',
53
+ 'defineStateAxis',
54
+ 'defineBreakpoints',
55
+ 'defineAxis',
56
+ ]);
57
+
58
+ /** Namespaces whose members are value constructors: `unit.px(4)`, `at.…`. */
59
+ const PRIMITIVE_NAMESPACES = new Set(['unit', 'kind', 'at']);
60
+
61
+ module.exports = {
62
+ meta: {
63
+ type: 'problem',
64
+ docs: {
65
+ description:
66
+ 'Disallow string and number literals as arguments to @craft-ts/style helpers.',
67
+ },
68
+ schema: [],
69
+ messages: {
70
+ rawValue:
71
+ "'{{value}}' is a raw CSS value, not a design-system value. Pass {{replacement}} instead. If the scale is missing the value, add it to the scale; if it genuinely cannot be proven, use unsafeLength('{{value}}', reason) so the debt is countable.",
72
+ rawGlobal:
73
+ "'{{value}}' is a CSS-wide keyword; pass it as a token — global.{{token}}(prop.<name>) — so no helper has to accept a string.",
74
+ },
75
+ },
76
+ create(context) {
77
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
78
+ const styleImports = new Set();
79
+ const namespaces = new Set();
80
+
81
+ const replacementFor = (value) => {
82
+ if (typeof value === 'number') return REPLACEMENTS.length;
83
+ if (COLOR_LIKE.test(value)) return REPLACEMENTS.color;
84
+ if (LENGTH_LIKE.test(value)) return REPLACEMENTS.length;
85
+ return REPLACEMENTS.keyword;
86
+ };
87
+
88
+ const isStyleHelper = (callee) => {
89
+ if (callee.type === 'Identifier') {
90
+ return (
91
+ styleImports.has(callee.name) && !TAKES_PRIMITIVES.has(callee.name)
92
+ );
93
+ }
94
+ if (
95
+ callee.type === 'MemberExpression' &&
96
+ callee.object.type === 'Identifier'
97
+ ) {
98
+ const object = callee.object.name;
99
+ if (PRIMITIVE_NAMESPACES.has(object)) return false;
100
+ if (namespaces.has(object)) {
101
+ // `style.space(4)` through a namespace import is still a value
102
+ // constructor; the member name is what decides.
103
+ return (
104
+ callee.property.type === 'Identifier' &&
105
+ !TAKES_PRIMITIVES.has(callee.property.name) &&
106
+ !PRIMITIVE_NAMESPACES.has(callee.property.name)
107
+ );
108
+ }
109
+ return styleImports.has(object);
110
+ }
111
+ return false;
112
+ };
113
+
114
+ return {
115
+ ImportDeclaration(node) {
116
+ if (node.source.value !== STYLE_MODULE) return;
117
+ for (const specifier of node.specifiers) {
118
+ if (specifier.type === 'ImportNamespaceSpecifier') {
119
+ namespaces.add(specifier.local.name);
120
+ } else if (specifier.type === 'ImportSpecifier') {
121
+ styleImports.add(specifier.local.name);
122
+ }
123
+ }
124
+ },
125
+ CallExpression(node) {
126
+ if (!isStyleHelper(node.callee)) return;
127
+ for (const argument of node.arguments) {
128
+ const isLiteral =
129
+ argument.type === 'Literal' &&
130
+ (typeof argument.value === 'string' ||
131
+ typeof argument.value === 'number');
132
+ const isTemplate =
133
+ argument.type === 'TemplateLiteral' && argument.quasis.length > 0;
134
+ if (!isLiteral && !isTemplate) continue;
135
+
136
+ const value = isLiteral
137
+ ? argument.value
138
+ : sourceCode.getText(argument).slice(1, -1);
139
+
140
+ if (typeof value === 'string' && GLOBAL_KEYWORDS.has(value)) {
141
+ context.report({
142
+ node: argument,
143
+ messageId: 'rawGlobal',
144
+ data: {
145
+ value,
146
+ token: value.replace(/-([a-z])/g, (_, char) =>
147
+ char.toUpperCase(),
148
+ ),
149
+ },
150
+ });
151
+ continue;
152
+ }
153
+
154
+ context.report({
155
+ node: argument,
156
+ messageId: 'rawValue',
157
+ data: { value: String(value), replacement: replacementFor(value) },
158
+ });
159
+ }
160
+ },
161
+ };
162
+ },
163
+ };
@@ -0,0 +1,114 @@
1
+ const { report } = require('./security-rule-utils.cjs');
2
+
3
+ /**
4
+ * Attributs d'URL dont le nom n'a pas d'autre sens courant en JavaScript.
5
+ * `data`, `ping`, `cite` ou `manifest` sont exclus à dessein : ce sont des
6
+ * noms de champs métier fréquents, et la règle ne verrait que le nom.
7
+ */
8
+ const URL_KEYS = /^(?:href|src|srcset|action|formaction|poster|xlink:href)$/i;
9
+ /** Helpers de la lib : leur retour est déjà validé. */
10
+ const SAFE_CALLS = new Set(['safeUrl', 'safeResourceUrl', 'safeUrlList']);
11
+
12
+ function isSafeExpression(node) {
13
+ if (!node) return true;
14
+ switch (node.type) {
15
+ case 'Literal':
16
+ case 'TemplateLiteral':
17
+ // Une URL écrite dans le source n'est pas une donnée utilisateur.
18
+ return true;
19
+ case 'CallExpression':
20
+ return (
21
+ (node.callee.type === 'Identifier' && SAFE_CALLS.has(node.callee.name)) ||
22
+ (node.callee.type === 'MemberExpression' &&
23
+ node.callee.property.type === 'Identifier' &&
24
+ SAFE_CALLS.has(node.callee.property.name))
25
+ );
26
+ case 'ConditionalExpression':
27
+ return (
28
+ isSafeExpression(node.consequent) && isSafeExpression(node.alternate)
29
+ );
30
+ case 'LogicalExpression':
31
+ return isSafeExpression(node.left) && isSafeExpression(node.right);
32
+ case 'TSAsExpression':
33
+ case 'TSNonNullExpression':
34
+ return isSafeExpression(node.expression);
35
+ case 'FunctionExpression':
36
+ case 'ArrowFunctionExpression':
37
+ // Une valeur de template Craft est un générateur ; une fonction
38
+ // ordinaire est un accesseur (`href: () => location.href`), pas une
39
+ // valeur posée dans le DOM.
40
+ return node.generator ? returnsSafely(node) : true;
41
+ default:
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function isTemplateAttributeObject(property, sourceCode) {
47
+ const ancestors = sourceCode.getAncestors
48
+ ? sourceCode.getAncestors(property)
49
+ : [];
50
+ const object = ancestors[ancestors.length - 1];
51
+ const parent = ancestors[ancestors.length - 2];
52
+ return Boolean(
53
+ object &&
54
+ object.type === 'ObjectExpression' &&
55
+ parent &&
56
+ parent.type === 'CallExpression' &&
57
+ parent.arguments.includes(object),
58
+ );
59
+ }
60
+
61
+ /** Vrai quand chaque valeur retournée par le générateur est déjà validée. */
62
+ function returnsSafely(node) {
63
+ if (node.body.type !== 'BlockStatement') return isSafeExpression(node.body);
64
+ const returns = [];
65
+ const visit = (statement) => {
66
+ if (!statement || typeof statement.type !== 'string') return;
67
+ if (statement.type === 'ReturnStatement') {
68
+ returns.push(statement.argument);
69
+ return;
70
+ }
71
+ // Les fonctions imbriquées ont leur propre valeur de retour.
72
+ if (
73
+ statement.type === 'FunctionDeclaration' ||
74
+ statement.type === 'FunctionExpression' ||
75
+ statement.type === 'ArrowFunctionExpression'
76
+ ) {
77
+ return;
78
+ }
79
+ for (const key of Object.keys(statement)) {
80
+ const value = statement[key];
81
+ if (Array.isArray(value)) value.forEach(visit);
82
+ else if (value && typeof value.type === 'string') visit(value);
83
+ }
84
+ };
85
+ node.body.body.forEach(visit);
86
+ return returns.length > 0 && returns.every(isSafeExpression);
87
+ }
88
+
89
+ module.exports = {
90
+ meta: {
91
+ type: 'problem',
92
+ schema: [],
93
+ docs: { description: 'Sanitize user-controlled DOM URLs.' },
94
+ },
95
+ create(context) {
96
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
97
+ return {
98
+ Property(node) {
99
+ const name = node.key && (node.key.name ?? node.key.value);
100
+ if (!URL_KEYS.test(String(name))) return;
101
+ // Les attributs d'un template Craft sont un objet passé en argument
102
+ // d'un helper d'élément — `a('book', { href }, …)`. Un objet affecté
103
+ // à une variable est une structure de données, pas du DOM.
104
+ if (!isTemplateAttributeObject(node, sourceCode)) return;
105
+ if (isSafeExpression(node.value)) return;
106
+ report(
107
+ context,
108
+ node,
109
+ `Wrap the dynamic "${name}" value in safeUrl(), safeResourceUrl() or safeUrlList().`,
110
+ );
111
+ },
112
+ };
113
+ },
114
+ };
@@ -0,0 +1,43 @@
1
+ const { report } = require('./security-rule-utils.cjs');
2
+
3
+ const FORWARDED = /^x-forwarded-(?:for|host|proto)$/i;
4
+
5
+ module.exports = {
6
+ meta: {
7
+ type: 'problem',
8
+ schema: [
9
+ {
10
+ type: 'object',
11
+ properties: {
12
+ // Frontière proxy de confiance : le fichier qui valide ces en-têtes
13
+ // doit pouvoir les lire, sinon la règle n'est pas tenable.
14
+ allowIn: { type: 'array', items: { type: 'string' } },
15
+ },
16
+ additionalProperties: false,
17
+ },
18
+ ],
19
+ docs: {
20
+ description:
21
+ 'Do not trust forwarded headers outside the configured proxy boundary.',
22
+ },
23
+ },
24
+ create(context) {
25
+ const allowIn = (context.options[0] && context.options[0].allowIn) || [
26
+ 'proxy-boundary',
27
+ 'trusted-proxy',
28
+ ];
29
+ const filename = context.filename ?? context.getFilename();
30
+ if (allowIn.some((fragment) => filename.includes(fragment))) return {};
31
+ return {
32
+ Literal(node) {
33
+ if (typeof node.value === 'string' && FORWARDED.test(node.value)) {
34
+ report(
35
+ context,
36
+ node,
37
+ 'Forwarded headers are untrusted input; read them only in the proxy boundary module that validates them.',
38
+ );
39
+ }
40
+ },
41
+ };
42
+ },
43
+ };
@@ -0,0 +1,93 @@
1
+ const { report } = require('./security-rule-utils.cjs');
2
+
3
+ const RAW_SINKS = /^(?:innerHTML|outerHTML|srcdoc)$/;
4
+ const DANGEROUS_CALLS = new Set([
5
+ 'insertAdjacentHTML',
6
+ 'createContextualFragment',
7
+ 'write',
8
+ 'writeln',
9
+ ]);
10
+
11
+ /** `unsafeHtml(...)` n'est acceptable qu'accompagné d'une exception datée. */
12
+ function hasRecordedException(sourceCode) {
13
+ return /allowUnsafe\s*\(/.test(sourceCode);
14
+ }
15
+
16
+ module.exports = {
17
+ meta: {
18
+ type: 'problem',
19
+ schema: [],
20
+ docs: { description: 'Require an explicit Craft HTML sanitizer.' },
21
+ },
22
+ create(context) {
23
+ const sourceCode = (context.sourceCode ?? context.getSourceCode()).getText();
24
+ return {
25
+ AssignmentExpression(node) {
26
+ if (
27
+ node.left.type === 'MemberExpression' &&
28
+ !node.left.computed &&
29
+ node.left.property.type === 'Identifier' &&
30
+ RAW_SINKS.test(node.left.property.name)
31
+ ) {
32
+ report(
33
+ context,
34
+ node,
35
+ 'Raw HTML and srcdoc assignments are forbidden; use sanitizedHtml or an audited unsafeHtml exception.',
36
+ );
37
+ }
38
+ },
39
+ CallExpression(node) {
40
+ if (
41
+ node.callee.type === 'Identifier' &&
42
+ node.callee.name === 'unsafeHtml' &&
43
+ !hasRecordedException(sourceCode)
44
+ ) {
45
+ report(
46
+ context,
47
+ node,
48
+ 'unsafeHtml requires an allowUnsafe(...) exception with an owner, a reason, a risk and an expiry date.',
49
+ );
50
+ return;
51
+ }
52
+ if (
53
+ node.callee.type === 'Identifier' &&
54
+ (node.callee.name === 'eval' || node.callee.name === 'Function')
55
+ ) {
56
+ report(context, node, 'Dynamic code evaluation is forbidden.');
57
+ return;
58
+ }
59
+ if (
60
+ node.callee.type === 'MemberExpression' &&
61
+ node.callee.property.type === 'Identifier' &&
62
+ DANGEROUS_CALLS.has(node.callee.property.name)
63
+ ) {
64
+ const owner =
65
+ node.callee.object.type === 'Identifier'
66
+ ? node.callee.object.name
67
+ : '';
68
+ if (
69
+ node.callee.property.name !== 'write' &&
70
+ node.callee.property.name !== 'writeln'
71
+ ) {
72
+ report(
73
+ context,
74
+ node,
75
+ `${node.callee.property.name} is forbidden; use the Craft sanitized HTML API.`,
76
+ );
77
+ } else if (owner === 'document') {
78
+ report(
79
+ context,
80
+ node,
81
+ 'document.write injects unparsed markup; use the Craft sanitized HTML API.',
82
+ );
83
+ }
84
+ }
85
+ },
86
+ NewExpression(node) {
87
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
88
+ report(context, node, 'Dynamic code evaluation is forbidden.');
89
+ }
90
+ },
91
+ };
92
+ },
93
+ };
@@ -0,0 +1,54 @@
1
+ const { report, isCallNamed } = require('./security-rule-utils.cjs');
2
+
3
+ function hasProperty(node, name) {
4
+ if (!node || node.type !== 'ObjectExpression') return false;
5
+ return node.properties.some((property) => {
6
+ // Un spread peut porter la propriété : on ne peut pas prouver son
7
+ // absence, et une règle qui accuse à tort finit désactivée.
8
+ if (property.type === 'SpreadElement') return true;
9
+ return (
10
+ property.type === 'Property' &&
11
+ (property.key.name ?? property.key.value) === name
12
+ );
13
+ });
14
+ }
15
+
16
+ module.exports = {
17
+ meta: {
18
+ type: 'problem',
19
+ schema: [],
20
+ docs: { description: 'Require an explicit SSR transfer policy.' },
21
+ },
22
+ create(context) {
23
+ return {
24
+ CallExpression(node) {
25
+ if (isCallNamed(node, 'captureCraftTransferSnapshot')) {
26
+ if (node.arguments.length < 2 || !hasProperty(node.arguments[1], 'policy')) {
27
+ report(
28
+ context,
29
+ node,
30
+ 'SSR transfer requires an explicit policy: captureCraftTransferSnapshot(registry, { policy }).',
31
+ );
32
+ }
33
+ return;
34
+ }
35
+ // Un rendu sans politique retombe sur le défaut fermé : le signaler
36
+ // évite de découvrir en production que rien ne s'hydrate.
37
+ if (isCallNamed(node, 'renderCraft')) {
38
+ const options = node.arguments[0];
39
+ if (
40
+ options &&
41
+ options.type === 'ObjectExpression' &&
42
+ !hasProperty(options, 'securityPolicy')
43
+ ) {
44
+ report(
45
+ context,
46
+ node,
47
+ 'renderCraft requires an explicit securityPolicy: the transfer snapshot is empty without one.',
48
+ );
49
+ }
50
+ }
51
+ },
52
+ };
53
+ },
54
+ };
@@ -121,13 +121,13 @@ module.exports = {
121
121
  schema: [],
122
122
  messages: {
123
123
  ternary:
124
- 'Do not use a ternary in a Craft template. Use ifBlock(...) for boolean visibility or matchBlock.exhaustive(...) for a discriminated union.',
124
+ 'Do not use a ternary in a Craft template. Use ifNode(...) for boolean visibility or matchNode.exhaustive(...) for a discriminated union.',
125
125
  logical:
126
126
  'Do not use a logical expression in a Craft template. Move the derivation to state, query, or craftComputed, then render it with a Craft block.',
127
127
  negation:
128
128
  'Do not use negation in a Craft template. Move the boolean derivation to state, query, or craftComputed, then bind the resulting value.',
129
129
  controlFlow:
130
- 'Do not use imperative control flow in a Craft template. Use ifBlock(...), matchBlock.exhaustive(...), each(...), or defer(...) so the render contract stays type-checkable.',
130
+ 'Do not use imperative control flow in a Craft template. Use ifNode(...), matchNode.exhaustive(...), forNode(...), or deferNode(...) so the render contract stays type-checkable.',
131
131
  },
132
132
  },
133
133
 
@@ -169,7 +169,7 @@ module.exports = {
169
169
  fix: (fixer) =>
170
170
  [
171
171
  fixer.replaceText(replacement.node, replacement.text),
172
- namedImportFix(fixer, 'ifBlock'),
172
+ namedImportFix(fixer, 'ifNode'),
173
173
  ].filter(Boolean),
174
174
  }),
175
175
  });
@@ -200,7 +200,7 @@ module.exports = {
200
200
  fix: (fixer) =>
201
201
  [
202
202
  fixer.replaceText(node, replacement),
203
- namedImportFix(fixer, 'matchBlock'),
203
+ namedImportFix(fixer, 'matchNode'),
204
204
  ].filter(Boolean),
205
205
  }),
206
206
  });
@@ -244,7 +244,7 @@ module.exports = {
244
244
  return undefined;
245
245
  }
246
246
 
247
- const text = `ifBlock(${condition}, () => ${sourceCode.getText(
247
+ const text = `ifNode(${condition}, () => ${sourceCode.getText(
248
248
  node.consequent,
249
249
  )}, () => ${sourceCode.getText(node.alternate)})`;
250
250
  const parent = node.parent;
@@ -271,7 +271,7 @@ module.exports = {
271
271
  `${formatObjectKey(key)}: () => ${expression}`,
272
272
  )
273
273
  .join(', ');
274
- return `return matchBlock.exhaustive(${match.source}, ${JSON.stringify(
274
+ return `return matchNode.exhaustive(${match.source}, ${JSON.stringify(
275
275
  match.key,
276
276
  )}, { ${handlers} });`;
277
277
  }
@@ -429,8 +429,8 @@ function isRenderableArrow(node) {
429
429
  }
430
430
 
431
431
  return !new Set([
432
- 'each',
433
- 'defer',
432
+ 'forNode',
433
+ 'deferNode',
434
434
  'content',
435
435
  'craftTemplate',
436
436
  'renderTemplate',