@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
|
@@ -7,6 +7,11 @@ const craftSignalSourceNameMatch = require('./craft-signal-source-name-match.cjs
|
|
|
7
7
|
const noCraftComputedSideEffects = require('./no-craft-computed-side-effects.cjs');
|
|
8
8
|
const provideHostNameMatchComponent = require('./provide-host-name-match-component.cjs');
|
|
9
9
|
const preferCraftHttpTransport = require('./prefer-craft-http-transport.cjs');
|
|
10
|
+
const noCraftServiceComponentSameFile = require('./no-craft-service-component-same-file.cjs');
|
|
11
|
+
const noRawCssValue = require('./no-raw-css-value.cjs');
|
|
12
|
+
const noRawClass = require('./no-raw-class.cjs');
|
|
13
|
+
const noFreeHas = require('./no-free-has.cjs');
|
|
14
|
+
const styleFileBoundary = require('./style-file-boundary.cjs');
|
|
10
15
|
const preferBrowserBoundaries = require('./prefer-browser-boundaries.cjs');
|
|
11
16
|
const requireComponentMonitoring = require('./require-component-monitoring.cjs');
|
|
12
17
|
const requirePrimitiveGeneratorUnwrap = require('./require-primitive-generator-unwrap.cjs');
|
|
@@ -67,15 +72,28 @@ const targetBlankNoopener = require('./target-blank-noopener.cjs');
|
|
|
67
72
|
const requireFocusVisible = require('./require-focus-visible.cjs');
|
|
68
73
|
const requireReducedMotion = require('./require-reduced-motion.cjs');
|
|
69
74
|
const requireEffectAdapters = require('./require-effect-adapters.cjs');
|
|
75
|
+
const syncEffectBody = require('./sync-effect-body.cjs');
|
|
70
76
|
const serverFunctionClientMatch = require('./server-function-client-match.cjs');
|
|
71
77
|
const noInvalidInsertionPipe = require('./no-invalid-insertion-pipe.cjs');
|
|
72
78
|
const noRedundantPrimitiveInsertion = require('./no-redundant-primitive-insertion.cjs');
|
|
73
79
|
const noEffectOutsideLoaders = require('./no-effect-outside-loaders.cjs');
|
|
80
|
+
const noExplicitEffectType = require('./no-explicit-effect-type.cjs');
|
|
81
|
+
const preferInlineEffectInsertion = require('./prefer-inline-effect-insertion.cjs');
|
|
82
|
+
const preferInlineRouteProviders = require('./prefer-inline-route-providers.cjs');
|
|
74
83
|
const preferQueryMethodOverStateTrigger = require('./prefer-query-method-over-state-trigger.cjs');
|
|
75
84
|
const noInjectionToken = require('./no-injection-token.cjs');
|
|
76
85
|
const noManualRouteProviderList = require('./no-manual-route-provider-list.cjs');
|
|
77
86
|
const noWidenedRouteProviderContext = require('./no-widened-route-provider-context.cjs');
|
|
87
|
+
const requireRouteSecurityPolicy = require('./require-route-security-policy.cjs');
|
|
88
|
+
const requireServerFunctionTimeout = require('./require-server-function-timeout.cjs');
|
|
89
|
+
const noAuthTokenInLocalStorage = require('./no-auth-token-in-local-storage.cjs');
|
|
90
|
+
const noRawUserUrl = require('./no-raw-user-url.cjs');
|
|
91
|
+
const noUnsafeTransferState = require('./no-unsafe-transfer-state.cjs');
|
|
92
|
+
const noUnsafeHtml = require('./no-unsafe-html.cjs');
|
|
93
|
+
const noTrustForwardedHeaders = require('./no-trust-forwarded-headers.cjs');
|
|
94
|
+
const noEffectImportInFrontend = require('./no-effect-import-in-frontend.cjs');
|
|
78
95
|
const recommendedRules = require('./recommended-config.cjs');
|
|
96
|
+
const securityRules = require('./security-config.cjs');
|
|
79
97
|
|
|
80
98
|
const plugin = {
|
|
81
99
|
rules: {
|
|
@@ -88,6 +106,11 @@ const plugin = {
|
|
|
88
106
|
'no-craft-computed-side-effects': noCraftComputedSideEffects,
|
|
89
107
|
'provide-host-name-match-component': provideHostNameMatchComponent,
|
|
90
108
|
'prefer-craft-http-transport': preferCraftHttpTransport,
|
|
109
|
+
'no-craft-service-component-same-file': noCraftServiceComponentSameFile,
|
|
110
|
+
'no-raw-css-value': noRawCssValue,
|
|
111
|
+
'no-raw-class': noRawClass,
|
|
112
|
+
'no-free-has': noFreeHas,
|
|
113
|
+
'style-file-boundary': styleFileBoundary,
|
|
91
114
|
'prefer-browser-boundaries': preferBrowserBoundaries,
|
|
92
115
|
'require-component-monitoring': requireComponentMonitoring,
|
|
93
116
|
'require-primitive-generator-unwrap': requirePrimitiveGeneratorUnwrap,
|
|
@@ -151,16 +174,28 @@ const plugin = {
|
|
|
151
174
|
'require-focus-visible': requireFocusVisible,
|
|
152
175
|
'require-reduced-motion': requireReducedMotion,
|
|
153
176
|
'require-effect-adapters': requireEffectAdapters,
|
|
177
|
+
'sync-effect-body': syncEffectBody,
|
|
154
178
|
'server-function-client-match': serverFunctionClientMatch,
|
|
155
179
|
'no-invalid-insertion-pipe': noInvalidInsertionPipe,
|
|
156
180
|
'no-redundant-primitive-insertion': noRedundantPrimitiveInsertion,
|
|
157
181
|
'no-effect-outside-loaders': noEffectOutsideLoaders,
|
|
182
|
+
'no-explicit-effect-type': noExplicitEffectType,
|
|
183
|
+
'prefer-inline-effect-insertion': preferInlineEffectInsertion,
|
|
184
|
+
'prefer-inline-route-providers': preferInlineRouteProviders,
|
|
158
185
|
// Backwards-compatible alias; use no-effect-outside-loaders in new configs.
|
|
159
186
|
'no-effect-in-params': noEffectOutsideLoaders,
|
|
160
187
|
'prefer-query-method-over-state-trigger': preferQueryMethodOverStateTrigger,
|
|
161
188
|
'no-injection-token': noInjectionToken,
|
|
162
189
|
'no-manual-route-provider-list': noManualRouteProviderList,
|
|
163
190
|
'no-widened-route-provider-context': noWidenedRouteProviderContext,
|
|
191
|
+
'require-route-security-policy': requireRouteSecurityPolicy,
|
|
192
|
+
'require-server-function-timeout': requireServerFunctionTimeout,
|
|
193
|
+
'no-auth-token-in-local-storage': noAuthTokenInLocalStorage,
|
|
194
|
+
'no-raw-user-url': noRawUserUrl,
|
|
195
|
+
'no-unsafe-transfer-state': noUnsafeTransferState,
|
|
196
|
+
'no-unsafe-html': noUnsafeHtml,
|
|
197
|
+
'no-trust-forwarded-headers': noTrustForwardedHeaders,
|
|
198
|
+
'no-effect-import-in-frontend': noEffectImportInFrontend,
|
|
164
199
|
},
|
|
165
200
|
};
|
|
166
201
|
|
|
@@ -190,20 +225,30 @@ const a11yRuleSeverity = {
|
|
|
190
225
|
plugin.configs = {
|
|
191
226
|
recommended: {
|
|
192
227
|
plugins: { 'craft-ts': plugin },
|
|
193
|
-
rules: recommendedRules,
|
|
228
|
+
rules: { ...recommendedRules, ...securityRules },
|
|
194
229
|
},
|
|
195
230
|
effect: {
|
|
196
231
|
plugins: { 'craft-ts': plugin },
|
|
197
232
|
rules: {
|
|
198
233
|
...recommendedRules,
|
|
234
|
+
...securityRules,
|
|
199
235
|
'craft-ts/require-effect-adapters': 'error',
|
|
236
|
+
'craft-ts/sync-effect-body': 'error',
|
|
200
237
|
'craft-ts/no-effect-outside-loaders': 'error',
|
|
238
|
+
'craft-ts/no-explicit-effect-type': 'error',
|
|
239
|
+
'craft-ts/prefer-inline-effect-insertion': 'error',
|
|
201
240
|
},
|
|
202
241
|
},
|
|
203
242
|
a11y: {
|
|
204
243
|
plugins: { 'craft-ts': plugin },
|
|
205
244
|
rules: a11yRuleSeverity,
|
|
206
245
|
},
|
|
246
|
+
// Conservé pour activer le préréglage seul, sur un projet qui ne prend pas
|
|
247
|
+
// `recommended` (une lib partagée, un dossier d'outillage).
|
|
248
|
+
security: {
|
|
249
|
+
plugins: { 'craft-ts': plugin },
|
|
250
|
+
rules: securityRules,
|
|
251
|
+
},
|
|
207
252
|
};
|
|
208
253
|
|
|
209
254
|
module.exports = plugin;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { report } = require('./security-rule-utils.cjs');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: { type: 'problem', schema: [], docs: { description: 'Do not persist authentication material in browser storage.' } },
|
|
5
|
+
create(context) {
|
|
6
|
+
return { CallExpression(node) {
|
|
7
|
+
const callee = node.callee;
|
|
8
|
+
if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier' || callee.property.name !== 'setItem') return;
|
|
9
|
+
const owner = callee.object && callee.object.type === 'Identifier' ? callee.object.name : '';
|
|
10
|
+
if (!/^(?:localStorage|sessionStorage)$/.test(owner)) return;
|
|
11
|
+
const key = node.arguments[0];
|
|
12
|
+
if (key && key.type === 'Literal' && /token|auth|jwt|session|refresh|password/i.test(String(key.value))) {
|
|
13
|
+
report(context, node, 'Authentication tokens must not be stored in localStorage or sessionStorage.');
|
|
14
|
+
}
|
|
15
|
+
} };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -22,17 +22,24 @@ const WRITE_METHODS = new Set([
|
|
|
22
22
|
'update',
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
+
// `settled(...)` reads an already-resolved async source; `syncEffect(...)` runs
|
|
26
|
+
// an Effect whose `R` carries `SyncOp`, so the type already proved it cannot
|
|
27
|
+
// suspend. Both are reads, not asynchronous work.
|
|
28
|
+
// Module scope on purpose: the visitors below run before any `const` declared
|
|
29
|
+
// inside `create()` past its `return` would be initialised.
|
|
30
|
+
const ALLOWED_COMPUTED_CALLS = new Set(['settled', 'syncEffect']);
|
|
31
|
+
|
|
25
32
|
module.exports = {
|
|
26
33
|
meta: {
|
|
27
34
|
type: 'problem',
|
|
28
35
|
docs: {
|
|
29
36
|
description:
|
|
30
|
-
'Disallow writes and asynchronous work inside craftComputed callbacks; only reactive reads and
|
|
37
|
+
'Disallow writes and asynchronous work inside craftComputed callbacks; only reactive reads, settled(...) and syncEffect(...) are allowed.',
|
|
31
38
|
},
|
|
32
39
|
schema: [],
|
|
33
40
|
messages: {
|
|
34
41
|
forbidden:
|
|
35
|
-
'Craft computed callbacks may only read reactive Craft values or use `settled(...)`; `{{call}}` is not allowed because it can write or perform asynchronous work.',
|
|
42
|
+
'Craft computed callbacks may only read reactive Craft values or use `settled(...)` / `syncEffect(...)`; `{{call}}` is not allowed because it can write or perform asynchronous work.',
|
|
36
43
|
missingTypeInfo:
|
|
37
44
|
'This rule requires TypeScript type information to identify reactive reads and asynchronous Craft calls.',
|
|
38
45
|
},
|
|
@@ -124,10 +131,10 @@ module.exports = {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
function isAllowedSettledCall(node) {
|
|
134
|
+
if (node.callee.type !== 'Identifier') return false;
|
|
127
135
|
return (
|
|
128
|
-
node.callee.
|
|
129
|
-
(node.callee.name
|
|
130
|
-
importedNames.get(node.callee.name) === 'settled')
|
|
136
|
+
ALLOWED_COMPUTED_CALLS.has(node.callee.name) ||
|
|
137
|
+
ALLOWED_COMPUTED_CALLS.has(importedNames.get(node.callee.name))
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const CORE_MODULE = '@craft-ts/core';
|
|
2
|
+
const COMPONENT_MODULE = '@craft-ts/component';
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
'Disallow declaring craftService and craftComponent in the same file so route-level providers do not break lazy loading.',
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
separateFiles:
|
|
14
|
+
'Do not declare craftService and craftComponent in the same file. If the service is provided at route level while the component is lazy-loaded, co-location breaks lazy loading; keep them in separate files.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
19
|
+
const calls = [];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
const kind = getCraftHostKind(node.callee, sourceCode);
|
|
24
|
+
if (kind) {
|
|
25
|
+
calls.push({ kind, node });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
'Program:exit'() {
|
|
29
|
+
const hasService = calls.some(({ kind }) => kind === 'service');
|
|
30
|
+
const hasComponent = calls.some(({ kind }) => kind === 'component');
|
|
31
|
+
|
|
32
|
+
if (!hasService || !hasComponent) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const { node } of calls) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: 'separateFiles',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function getCraftHostKind(callee, sourceCode) {
|
|
48
|
+
if (callee.type === 'Identifier') {
|
|
49
|
+
if (
|
|
50
|
+
resolvesToImportedName(callee, sourceCode, 'craftService', CORE_MODULE)
|
|
51
|
+
) {
|
|
52
|
+
return 'service';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
resolvesToImportedName(
|
|
57
|
+
callee,
|
|
58
|
+
sourceCode,
|
|
59
|
+
'craftComponent',
|
|
60
|
+
COMPONENT_MODULE,
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
63
|
+
return 'component';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
callee.type !== 'MemberExpression' ||
|
|
71
|
+
callee.computed ||
|
|
72
|
+
callee.object.type !== 'Identifier' ||
|
|
73
|
+
callee.property.type !== 'Identifier'
|
|
74
|
+
) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
callee.property.name === 'craftService' &&
|
|
80
|
+
resolvesToNamespaceImport(callee.object, sourceCode, CORE_MODULE)
|
|
81
|
+
) {
|
|
82
|
+
return 'service';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
callee.property.name === 'craftComponent' &&
|
|
87
|
+
resolvesToNamespaceImport(callee.object, sourceCode, COMPONENT_MODULE)
|
|
88
|
+
) {
|
|
89
|
+
return 'component';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolvesToImportedName(identifier, sourceCode, importedName, module) {
|
|
96
|
+
const variable = findVariable(identifier, sourceCode);
|
|
97
|
+
return variable?.defs.some(
|
|
98
|
+
(definition) =>
|
|
99
|
+
definition.type === 'ImportBinding' &&
|
|
100
|
+
definition.node?.type === 'ImportSpecifier' &&
|
|
101
|
+
getImportedName(definition.node) === importedName &&
|
|
102
|
+
definition.parent?.source?.value === module,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolvesToNamespaceImport(identifier, sourceCode, module) {
|
|
107
|
+
const variable = findVariable(identifier, sourceCode);
|
|
108
|
+
return variable?.defs.some(
|
|
109
|
+
(definition) =>
|
|
110
|
+
definition.type === 'ImportBinding' &&
|
|
111
|
+
definition.node?.type === 'ImportNamespaceSpecifier' &&
|
|
112
|
+
definition.parent?.source?.value === module,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findVariable(identifier, sourceCode) {
|
|
117
|
+
let scope = sourceCode.getScope(identifier);
|
|
118
|
+
|
|
119
|
+
while (scope) {
|
|
120
|
+
const variable = scope.variables.find(
|
|
121
|
+
(candidate) => candidate.name === identifier.name,
|
|
122
|
+
);
|
|
123
|
+
if (variable) {
|
|
124
|
+
return variable;
|
|
125
|
+
}
|
|
126
|
+
scope = scope.upper;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getImportedName(specifier) {
|
|
133
|
+
if (specifier.imported.type === 'Identifier') {
|
|
134
|
+
return specifier.imported.name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return specifier.imported.value;
|
|
138
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EFFECT_IMPORT_PREFIXES = [
|
|
4
|
+
'effect',
|
|
5
|
+
'@effect/',
|
|
6
|
+
'@craft-ts/effect',
|
|
7
|
+
'@craft-ts/i18n-effect',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function isEffectImport(source) {
|
|
11
|
+
return EFFECT_IMPORT_PREFIXES.some(
|
|
12
|
+
(prefix) => prefix.endsWith('/')
|
|
13
|
+
? source.startsWith(prefix)
|
|
14
|
+
: source === prefix || source.startsWith(prefix + '/'),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description:
|
|
23
|
+
'Prevent EffectTS imports from crossing into a plain frontend boundary.',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
forbidden:
|
|
28
|
+
'EffectTS imports are not allowed in the plain frontend. Keep EffectTS in server files or choose the Effect frontend runtime.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
const reportIfEffectImport = (node) => {
|
|
34
|
+
const source = node.source?.value;
|
|
35
|
+
if (typeof source === 'string' && isEffectImport(source)) {
|
|
36
|
+
context.report({ node: node.source, messageId: 'forbidden' });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ImportDeclaration: reportIfEffectImport,
|
|
42
|
+
ExportAllDeclaration: reportIfEffectImport,
|
|
43
|
+
ExportNamedDeclaration: reportIfEffectImport,
|
|
44
|
+
ImportExpression(node) {
|
|
45
|
+
const source = node.source;
|
|
46
|
+
if (source?.type === 'Literal' && typeof source.value === 'string' && isEffectImport(source.value)) {
|
|
47
|
+
context.report({ node: source, messageId: 'forbidden' });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -22,7 +22,7 @@ module.exports = {
|
|
|
22
22
|
schema: [],
|
|
23
23
|
messages: {
|
|
24
24
|
effect:
|
|
25
|
-
'Effect values and Effect service reads are only allowed in an Effect loader. Keep params, methods, craftComputed(...) and craftEffect(...) synchronous.',
|
|
25
|
+
'Effect values and Effect service reads are only allowed in an Effect loader. Keep params, methods, craftComputed(...) and craftEffect(...) synchronous, or run a declared-synchronous Effect (SyncOp) through syncEffect(...).',
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
28
|
|
|
@@ -35,6 +35,11 @@ module.exports = {
|
|
|
35
35
|
const reportedNodes = new WeakSet();
|
|
36
36
|
const primitiveBindings = new Map();
|
|
37
37
|
const effectBindings = new Set();
|
|
38
|
+
// `syncEffect(...)` is the one sanctioned way through: its argument is an
|
|
39
|
+
// Effect whose `R` carries `SyncOp`, so the type already proved someone
|
|
40
|
+
// declared it synchronous. Reporting it would ban the very API that makes
|
|
41
|
+
// a business calculation reusable from a computation.
|
|
42
|
+
const syncEffectBindings = new Set();
|
|
38
43
|
|
|
39
44
|
return {
|
|
40
45
|
ImportDeclaration(node) {
|
|
@@ -61,6 +66,10 @@ module.exports = {
|
|
|
61
66
|
if (source === EFFECT_RUNTIME_PACKAGE && imported === 'Effect') {
|
|
62
67
|
effectBindings.add(specifier.local.name);
|
|
63
68
|
}
|
|
69
|
+
|
|
70
|
+
if (source === EFFECT_PACKAGE && imported === 'syncEffect') {
|
|
71
|
+
syncEffectBindings.add(specifier.local.name);
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
for (const specifier of node.specifiers) {
|
|
@@ -213,8 +222,29 @@ module.exports = {
|
|
|
213
222
|
return tsNode ? checker.getTypeAtLocation(tsNode) : undefined;
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
function isInsideSyncEffectCall(node) {
|
|
226
|
+
// Starts at the node itself: `syncEffect(...)` is exempt, and so is
|
|
227
|
+
// everything it wraps.
|
|
228
|
+
for (let current = node; current; current = current.parent) {
|
|
229
|
+
if (
|
|
230
|
+
current.type === 'CallExpression' &&
|
|
231
|
+
current.callee.type === 'Identifier' &&
|
|
232
|
+
syncEffectBindings.has(current.callee.name)
|
|
233
|
+
) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
['FunctionDeclaration', 'Program'].includes(current.type)
|
|
238
|
+
) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
216
245
|
function report(node) {
|
|
217
246
|
if (!node || reportedNodes.has(node)) return;
|
|
247
|
+
if (isInsideSyncEffectCall(node)) return;
|
|
218
248
|
reportedNodes.add(node);
|
|
219
249
|
context.report({ node, messageId: 'effect' });
|
|
220
250
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EFFECT_PACKAGE = 'effect';
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
'Prefer the type inferred by Effect.gen over an explicit Effect return annotation.',
|
|
11
|
+
},
|
|
12
|
+
fixable: 'code',
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
explicit:
|
|
16
|
+
'Effect.gen infers the complete Effect type. Remove this explicit type annotation.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
const effectBindings = new Set();
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
if (node.source.value !== EFFECT_PACKAGE) return;
|
|
26
|
+
|
|
27
|
+
for (const specifier of node.specifiers) {
|
|
28
|
+
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
29
|
+
effectBindings.add(specifier.local.name);
|
|
30
|
+
} else if (
|
|
31
|
+
specifier.type === 'ImportSpecifier' &&
|
|
32
|
+
getIdentifierName(specifier.imported) === 'Effect'
|
|
33
|
+
) {
|
|
34
|
+
effectBindings.add(specifier.local.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
VariableDeclarator(node) {
|
|
40
|
+
if (!node.id.typeAnnotation || !isEffectGenExpression(node.init)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
report(node.id.typeAnnotation);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
FunctionDeclaration(node) {
|
|
48
|
+
if (node.returnType && hasDirectEffectGenReturn(node.body)) {
|
|
49
|
+
report(node.returnType);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
ArrowFunctionExpression(node) {
|
|
54
|
+
if (node.returnType && hasEffectGenBody(node.body)) {
|
|
55
|
+
report(node.returnType);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
FunctionExpression(node) {
|
|
60
|
+
if (node.returnType && hasEffectGenBody(node.body)) {
|
|
61
|
+
report(node.returnType);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function report(annotation) {
|
|
67
|
+
context.report({
|
|
68
|
+
node: annotation,
|
|
69
|
+
messageId: 'explicit',
|
|
70
|
+
fix(fixer) {
|
|
71
|
+
return fixer.remove(annotation);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasEffectGenBody(body) {
|
|
77
|
+
if (isEffectGenExpression(body)) return true;
|
|
78
|
+
return body?.type === 'BlockStatement' && hasDirectEffectGenReturn(body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasDirectEffectGenReturn(body) {
|
|
82
|
+
return Boolean(
|
|
83
|
+
body?.body?.some(
|
|
84
|
+
(statement) =>
|
|
85
|
+
statement.type === 'ReturnStatement' &&
|
|
86
|
+
isEffectGenExpression(statement.argument),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isEffectGenExpression(node) {
|
|
92
|
+
let current = unwrap(node);
|
|
93
|
+
while (current?.type === 'CallExpression') {
|
|
94
|
+
if (isEffectGenMember(current.callee)) return true;
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
current.callee.type === 'MemberExpression' &&
|
|
98
|
+
!current.callee.computed
|
|
99
|
+
) {
|
|
100
|
+
current = unwrap(current.callee.object);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isEffectGenMember(node) {
|
|
110
|
+
return (
|
|
111
|
+
node?.type === 'MemberExpression' &&
|
|
112
|
+
!node.computed &&
|
|
113
|
+
node.property.type === 'Identifier' &&
|
|
114
|
+
node.property.name === 'gen' &&
|
|
115
|
+
node.object.type === 'Identifier' &&
|
|
116
|
+
effectBindings.has(node.object.name)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function getIdentifierName(node) {
|
|
123
|
+
return node.type === 'Identifier' ? node.name : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unwrap(node) {
|
|
127
|
+
let current = node;
|
|
128
|
+
while (
|
|
129
|
+
current &&
|
|
130
|
+
['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression'].includes(
|
|
131
|
+
current.type,
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
current = current.expression;
|
|
135
|
+
}
|
|
136
|
+
return current;
|
|
137
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const HAS = /:has\(/;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `:has()` in hand-written CSS reaches across a component boundary.
|
|
5
|
+
*
|
|
6
|
+
* A component whose appearance depends on markup it does not own has visual
|
|
7
|
+
* states nobody can enumerate — the matrix would have to know what every caller
|
|
8
|
+
* puts inside it. `descendant.userInvalid` and its siblings are the closed set
|
|
9
|
+
* that can be enumerated, and each one carries the driver that reaches it.
|
|
10
|
+
*
|
|
11
|
+
* The combinators that stay inside a component — `+`, `~`, `:nth-child` — are
|
|
12
|
+
* deliberately not restricted: they cannot see past the subtree the component
|
|
13
|
+
* renders itself.
|
|
14
|
+
*
|
|
15
|
+
* This is the net under raw CSS, not the mechanism: the generated property
|
|
16
|
+
* table has no way to spell a selector at all.
|
|
17
|
+
*/
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description:
|
|
23
|
+
'Disallow free-form :has() in styles; use the descendant.* axis instead.',
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
freeHas:
|
|
28
|
+
':has() here reaches across the component boundary, so what this component looks like depends on markup it does not own — a state the visual matrix cannot enumerate. Use the descendant axis instead: when(descendant.userInvalid, [...]), which is a closed set and carries its own test driver.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
const check = (node, text) => {
|
|
33
|
+
if (typeof text === 'string' && HAS.test(text)) {
|
|
34
|
+
context.report({ node, messageId: 'freeHas' });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
Literal(node) {
|
|
40
|
+
if (typeof node.value === 'string') check(node, node.value);
|
|
41
|
+
},
|
|
42
|
+
TemplateElement(node) {
|
|
43
|
+
check(node, node.value.cooked ?? node.value.raw);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
const { parseHyperscriptCall } = require('./hyperscript-walk.cjs');
|
|
4
4
|
|
|
5
5
|
const TRANSPARENT = new Set([
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
6
|
+
'ifNode',
|
|
7
|
+
'forNode',
|
|
8
|
+
'matchNode',
|
|
9
|
+
'catchNode',
|
|
10
|
+
'pendingNode',
|
|
11
|
+
'deferNode',
|
|
12
12
|
'craftTemplate',
|
|
13
13
|
]);
|
|
14
14
|
|