@craft-ts/dev-tools 0.7.0-beta.13 → 0.7.0-beta.16

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 +60 -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 +22 -0
  9. package/src/bin/craft.js +231 -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 +60 -1
  48. package/src/scripts/create/create-project.js +2082 -184
  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
@@ -1,28 +0,0 @@
1
- export type ServiceMigrationDiagnosticCode =
2
- | 'CONFLICTING_SCOPE'
3
- | 'UNSUPPORTED_PROVIDED_IN'
4
- | 'COMPLEX_PROVIDER'
5
- | 'RUNTIME_TOKEN_USAGE'
6
- | 'NON_CONVERTIBLE_CLASS'
7
- | 'DI_CYCLE'
8
- | 'LIFECYCLE_HOOK'
9
- | 'INJECTION_FLAGS'
10
- | 'FRAMEWORK_CONTRACT'
11
- | 'INJECT_ASYNC'
12
- | 'THIRD_PARTY_SCOPE_REQUIRED'
13
- | 'HTTP_CLIENT_REWRITE_REQUIRED'
14
- | 'MUTATION_SUBSCRIBE_REQUIRES_REWRITE'
15
- | 'EXPLICIT_SUBSCRIBE_REQUIRES_REVIEW'
16
- | 'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW'
17
- | 'RESOURCE_CHAIN_REQUIRES_REWRITE'
18
- | 'DIVERGENT_ROOTS'
19
- | 'GEN_DEPS_OVERRIDE_REQUIRED'
20
- | 'COMPLEX_TEST';
21
-
22
- export type ServiceMigrationDiagnostic = {
23
- code: ServiceMigrationDiagnosticCode;
24
- filePath: string;
25
- symbol?: string;
26
- message: string;
27
- manual: true;
28
- };
@@ -1,418 +0,0 @@
1
- export type TemplateMigrationDiagnostic = {
2
- readonly code: 'UNSUPPORTED_ANGULAR_SYNTAX' | 'MALFORMED_TEMPLATE';
3
- readonly message: string;
4
- readonly offset?: number;
5
- };
6
-
7
- export type TemplateMigrationOptions = {
8
- readonly componentName?: string;
9
- readonly includeImport?: boolean;
10
- };
11
-
12
- export type TemplateMigrationResult = {
13
- readonly code: string;
14
- readonly imports: readonly string[];
15
- readonly diagnostics: readonly TemplateMigrationDiagnostic[];
16
- };
17
-
18
- type HtmlNode = HtmlElement | HtmlText;
19
- type HtmlElement = {
20
- readonly kind: 'element';
21
- readonly tag: string;
22
- readonly attributes: readonly HtmlAttribute[];
23
- readonly children: readonly HtmlNode[];
24
- };
25
- type HtmlText = { readonly kind: 'text'; readonly value: string };
26
- type HtmlAttribute = { readonly name: string; readonly value: string | true };
27
-
28
- const nativeHelpers = new Set([
29
- 'a',
30
- 'article',
31
- 'aside',
32
- 'button',
33
- 'div',
34
- 'footer',
35
- 'form',
36
- 'h1',
37
- 'h2',
38
- 'h3',
39
- 'h4',
40
- 'h5',
41
- 'h6',
42
- 'header',
43
- 'iframe',
44
- 'img',
45
- 'input',
46
- 'label',
47
- 'legend',
48
- 'li',
49
- 'main',
50
- 'nav',
51
- 'ol',
52
- 'option',
53
- 'p',
54
- 'section',
55
- 'select',
56
- 'small',
57
- 'span',
58
- 'strong',
59
- 'svg',
60
- 'table',
61
- 'tbody',
62
- 'td',
63
- 'textarea',
64
- 'th',
65
- 'thead',
66
- 'tr',
67
- 'ul',
68
- 'dialog',
69
- 'fieldset',
70
- 'figure',
71
- 'figcaption',
72
- 'caption',
73
- 'area',
74
- ]);
75
-
76
- const voidTags = new Set([
77
- 'area',
78
- 'base',
79
- 'br',
80
- 'col',
81
- 'embed',
82
- 'hr',
83
- 'img',
84
- 'input',
85
- 'link',
86
- 'meta',
87
- 'param',
88
- 'source',
89
- 'track',
90
- 'wbr',
91
- ]);
92
-
93
- /** Converts an HTML/Web Component snippet into a Craft template expression. */
94
- export function migrateTemplateToCraft(
95
- source: string,
96
- options: TemplateMigrationOptions = {},
97
- ): TemplateMigrationResult {
98
- const diagnostics: TemplateMigrationDiagnostic[] = [];
99
- const roots = new HtmlSnippetParser(source, diagnostics).parse();
100
- const imports = new Set<string>();
101
- const expression = renderNodes(roots, imports, diagnostics, 0);
102
- const componentName = options.componentName?.trim();
103
- const body = componentName
104
- ? renderComponent(componentName, expression, imports)
105
- : `({}) => ${expression}`;
106
- const importLine =
107
- options.includeImport !== false && imports.size > 0
108
- ? `import { ${[...imports].sort().join(', ')} } from '@craft-ts/component';\n\n`
109
- : '';
110
- return {
111
- code: `${importLine}${body}\n`,
112
- imports: [...imports].sort(),
113
- diagnostics,
114
- };
115
- }
116
-
117
- function renderComponent(
118
- name: string,
119
- expression: string,
120
- imports: Set<string>,
121
- ): string {
122
- imports.add('craftComponent');
123
- const identifier = safeIdentifier(name, 'PastedTemplate');
124
- return `export const ${identifier} = craftComponent(\n '${escapeSingleQuoted(name)}',\n {},\n () => ({}),\n () => ${expression},\n);`;
125
- }
126
-
127
- function renderNodes(
128
- nodes: readonly HtmlNode[],
129
- imports: Set<string>,
130
- diagnostics: TemplateMigrationDiagnostic[],
131
- level: number,
132
- ): string {
133
- const meaningful = nodes.filter(
134
- (node) => node.kind !== 'text' || node.value.trim().length > 0,
135
- );
136
- if (meaningful.length === 0) return '[]';
137
- if (meaningful.length === 1) {
138
- return renderNode(meaningful[0], imports, diagnostics, level);
139
- }
140
- const indent = ' '.repeat(level);
141
- const childIndent = ' '.repeat(level + 1);
142
- return `[\n${meaningful
143
- .map(
144
- (node) =>
145
- `${childIndent}${renderNode(node, imports, diagnostics, level + 1)},`,
146
- )
147
- .join('\n')}\n${indent}]`;
148
- }
149
-
150
- function renderNode(
151
- node: HtmlNode,
152
- imports: Set<string>,
153
- diagnostics: TemplateMigrationDiagnostic[],
154
- level: number,
155
- ): string {
156
- if (node.kind === 'text') return renderText(node.value);
157
- const tag = node.tag.toLowerCase();
158
- const helper = nativeHelpers.has(tag) ? tag : 'customElement';
159
- imports.add(helper);
160
- const props = renderAttributes(node.attributes, diagnostics);
161
- const children = renderNodes(node.children, imports, diagnostics, level + 1);
162
- const hasChildren = node.children.some(
163
- (child) => child.kind !== 'text' || child.value.trim().length > 0,
164
- );
165
- if (helper === 'customElement') {
166
- const tagArgument = `'${escapeSingleQuoted(tag)}'`;
167
- return hasChildren
168
- ? `customElement(${tagArgument}${props ? `, ${props}` : ', {}'}, ${children})`
169
- : `customElement(${tagArgument}${props ? `, ${props}` : ''})`;
170
- }
171
- return hasChildren
172
- ? `${helper}(${props ? `${props}, ` : ''}${children})`
173
- : `${helper}(${props || ''})`;
174
- }
175
-
176
- function renderAttributes(
177
- attributes: readonly HtmlAttribute[],
178
- diagnostics: TemplateMigrationDiagnostic[],
179
- ): string {
180
- if (attributes.length === 0) return '';
181
- const entries: string[] = [];
182
- for (const attribute of attributes) {
183
- const name = attribute.name;
184
- if (name.startsWith('*') || name.startsWith('@')) {
185
- diagnostics.push({
186
- code: 'UNSUPPORTED_ANGULAR_SYNTAX',
187
- message: `Structural Angular syntax '${name}' needs a manual ifBlock/each conversion.`,
188
- });
189
- continue;
190
- }
191
- const bracketed = /^\[([^\]]+)\]$/.exec(name);
192
- const event = /^\(([^)]+)\)$/.exec(name);
193
- const key = bracketed?.[1] ?? event?.[1] ?? name;
194
- const outputKey = quoteObjectKey(key);
195
- if (event) {
196
- const expression =
197
- attribute.value === true ? 'undefined' : String(attribute.value);
198
- entries.push(`${outputKey}: (event) => ${expression}`);
199
- } else if (bracketed) {
200
- const expression =
201
- attribute.value === true ? 'undefined' : String(attribute.value);
202
- entries.push(`${outputKey}: () => ${expression}`);
203
- } else if (attribute.value === true) {
204
- entries.push(`${outputKey}: true`);
205
- } else {
206
- const value = String(attribute.value);
207
- entries.push(
208
- `${outputKey}: ${interpolationExpression(value) ?? quote(value)}`,
209
- );
210
- }
211
- }
212
- return entries.length > 0 ? `{ ${entries.join(', ')} }` : '';
213
- }
214
-
215
- function renderText(value: string): string {
216
- const decoded = decodeEntities(value);
217
- return interpolationExpression(decoded) ?? quote(decoded);
218
- }
219
-
220
- function interpolationExpression(value: string): string | undefined {
221
- const matches = [...value.matchAll(/{{\s*([\s\S]*?)\s*}}/g)];
222
- if (matches.length === 0) return undefined;
223
- if (matches.length === 1 && matches[0][0] === value) {
224
- return `() => ${matches[0][1].trim()}`;
225
- }
226
- let template = value.replace(/`/g, '\\`');
227
- for (const match of matches) {
228
- template = template.replace(match[0], `\${${match[1].trim()}}`);
229
- }
230
- return `() => \`${template}\``;
231
- }
232
-
233
- function quote(value: string): string {
234
- return `'${escapeSingleQuoted(value)}'`;
235
- }
236
-
237
- function quoteObjectKey(value: string): string {
238
- return /^[A-Za-z_$][\w$]*$/.test(value) ? value : quote(value);
239
- }
240
-
241
- function safeIdentifier(value: string, fallback: string): string {
242
- const words = value
243
- .replace(/[^A-Za-z0-9_$]/g, ' ')
244
- .trim()
245
- .split(/\s+/)
246
- .filter(Boolean);
247
- const result = words
248
- .map((word, index) =>
249
- index === 0 ? word : `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`,
250
- )
251
- .join('');
252
- return /^[A-Za-z_$]/.test(result) ? result : fallback;
253
- }
254
-
255
- function escapeSingleQuoted(value: string): string {
256
- return value
257
- .replace(/\\/g, '\\\\')
258
- .replace(/'/g, "\\'")
259
- .replace(/\r?\n/g, '\\n');
260
- }
261
-
262
- function decodeEntities(value: string): string {
263
- return value
264
- .replace(/&amp;/gi, '&')
265
- .replace(/&lt;/gi, '<')
266
- .replace(/&gt;/gi, '>')
267
- .replace(/&quot;/gi, '"')
268
- .replace(/&#39;/gi, "'");
269
- }
270
-
271
- class HtmlSnippetParser {
272
- private index = 0;
273
-
274
- constructor(
275
- private readonly source: string,
276
- private readonly diagnostics: TemplateMigrationDiagnostic[],
277
- ) {}
278
-
279
- parse(): HtmlNode[] {
280
- return this.parseNodes();
281
- }
282
-
283
- private parseNodes(stopTag?: string): HtmlNode[] {
284
- const nodes: HtmlNode[] = [];
285
- let textStart = this.index;
286
- while (this.index < this.source.length) {
287
- if (this.source.startsWith('<!--', this.index)) {
288
- this.flushText(nodes, textStart);
289
- const end = this.source.indexOf('-->', this.index + 4);
290
- this.index = end < 0 ? this.source.length : end + 3;
291
- textStart = this.index;
292
- continue;
293
- }
294
- if (this.source[this.index] !== '<') {
295
- this.index += 1;
296
- continue;
297
- }
298
- const closing = /^<\/\s*([\w:-]+)[^>]*>/.exec(
299
- this.source.slice(this.index),
300
- );
301
- if (closing) {
302
- this.flushText(nodes, textStart);
303
- this.index += closing[0].length;
304
- if (stopTag && closing[1].toLowerCase() !== stopTag) {
305
- this.diagnostics.push({
306
- code: 'MALFORMED_TEMPLATE',
307
- message: `Unexpected closing tag </${closing[1]}>.`,
308
- offset: this.index - closing[0].length,
309
- });
310
- }
311
- return nodes;
312
- }
313
- this.flushText(nodes, textStart);
314
- const opening = this.readOpeningTag();
315
- if (!opening) {
316
- this.index += 1;
317
- continue;
318
- }
319
- const children =
320
- opening.selfClosing || voidTags.has(opening.tag)
321
- ? []
322
- : this.parseNodes(opening.tag);
323
- nodes.push({
324
- kind: 'element',
325
- tag: opening.tag,
326
- attributes: opening.attributes,
327
- children,
328
- });
329
- textStart = this.index;
330
- }
331
- this.flushText(nodes, textStart);
332
- if (stopTag) {
333
- this.diagnostics.push({
334
- code: 'MALFORMED_TEMPLATE',
335
- message: `Missing closing tag </${stopTag}>.`,
336
- });
337
- }
338
- return nodes;
339
- }
340
-
341
- private flushText(nodes: HtmlNode[], start: number): void {
342
- if (this.index > start)
343
- nodes.push({ kind: 'text', value: this.source.slice(start, this.index) });
344
- }
345
-
346
- private readOpeningTag():
347
- | {
348
- readonly tag: string;
349
- readonly attributes: readonly HtmlAttribute[];
350
- readonly selfClosing: boolean;
351
- }
352
- | undefined {
353
- const start = this.index;
354
- if (!/^<[A-Za-z][\w:-]*/.test(this.source.slice(start))) return undefined;
355
- let cursor = start + 1;
356
- while (/[\w:-]/.test(this.source[cursor] ?? '')) cursor += 1;
357
- const tag = this.source.slice(start + 1, cursor).toLowerCase();
358
- const end = findTagEnd(this.source, cursor);
359
- if (end < 0) {
360
- this.diagnostics.push({
361
- code: 'MALFORMED_TEMPLATE',
362
- message: `Missing closing '>' for <${tag}>.`,
363
- offset: start,
364
- });
365
- this.index = this.source.length;
366
- return undefined;
367
- }
368
- const rawAttributes = this.source.slice(cursor, end);
369
- this.index = end + 1;
370
- return {
371
- tag,
372
- attributes: parseAttributes(rawAttributes),
373
- selfClosing: /\/\s*$/.test(rawAttributes),
374
- };
375
- }
376
- }
377
-
378
- function findTagEnd(source: string, start: number): number {
379
- let quoteCharacter = '';
380
- for (let index = start; index < source.length; index += 1) {
381
- const character = source[index];
382
- if (quoteCharacter) {
383
- if (character === quoteCharacter) quoteCharacter = '';
384
- } else if (character === '"' || character === "'")
385
- quoteCharacter = character;
386
- else if (character === '>') return index;
387
- }
388
- return -1;
389
- }
390
-
391
- function parseAttributes(source: string): HtmlAttribute[] {
392
- const attributes: HtmlAttribute[] = [];
393
- let index = 0;
394
- while (index < source.length) {
395
- while (/\s/.test(source[index] ?? '') || source[index] === '/') index += 1;
396
- if (index >= source.length) break;
397
- const nameStart = index;
398
- while (!/[\s=/>]/.test(source[index] ?? '')) index += 1;
399
- const name = source.slice(nameStart, index);
400
- while (/\s/.test(source[index] ?? '')) index += 1;
401
- if (source[index] !== '=') {
402
- attributes.push({ name, value: true });
403
- continue;
404
- }
405
- index += 1;
406
- while (/\s/.test(source[index] ?? '')) index += 1;
407
- const quoteCharacter =
408
- source[index] === '"' || source[index] === "'" ? source[index++] : '';
409
- const valueStart = index;
410
- if (quoteCharacter)
411
- while (index < source.length && source[index] !== quoteCharacter)
412
- index += 1;
413
- else while (!/[\s>]/.test(source[index] ?? '')) index += 1;
414
- attributes.push({ name, value: source.slice(valueStart, index) });
415
- if (quoteCharacter && source[index] === quoteCharacter) index += 1;
416
- }
417
- return attributes;
418
- }