@craft-ng/dev-tools 0.5.0-beta.5 → 0.6.0-beta.1
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 +198 -23
- package/generators.json +26 -0
- package/package.json +23 -2
- package/src/bin/craft-migrate-primitives.d.ts +2 -0
- package/src/bin/craft-migrate-primitives.js +71 -0
- package/src/bin/craft-migrate-primitives.js.map +1 -0
- package/src/bin/craft-migrate-routes.d.ts +2 -0
- package/src/bin/craft-migrate-routes.js +77 -0
- package/src/bin/craft-migrate-routes.js.map +1 -0
- package/src/bin/craft-migrate-services.d.ts +2 -0
- package/src/bin/craft-migrate-services.js +75 -0
- package/src/bin/craft-migrate-services.js.map +1 -0
- package/src/bin/craft-migrate.d.ts +2 -0
- package/src/bin/craft-migrate.js +90 -0
- package/src/bin/craft-migrate.js.map +1 -0
- package/src/bin/craft.d.ts +2 -0
- package/src/bin/craft.js +166 -0
- package/src/bin/craft.js.map +1 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +8 -125
- package/src/eslint-rules/craft-method-name-match.cjs +8 -166
- package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
- package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-source-name-match.cjs +8 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +78 -13
- package/src/eslint-rules/index.cjs +10 -2
- package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +66 -10
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-primitive-generator-unwrap.cjs +267 -0
- package/src/generators/route/compat.d.ts +3 -0
- package/src/generators/route/compat.js +6 -0
- package/src/generators/route/compat.js.map +1 -0
- package/src/generators/route/generator.d.ts +31 -0
- package/src/generators/route/generator.js +434 -0
- package/src/generators/route/generator.js.map +1 -0
- package/src/generators/route/schema.json +50 -0
- package/src/generators/route/split-schema.json +19 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +9 -0
- package/src/index.js.map +1 -1
- package/src/scripts/angular-brand-codemod.d.ts +17 -0
- package/src/scripts/angular-brand-codemod.js +44 -8
- package/src/scripts/angular-brand-codemod.js.map +1 -1
- package/src/scripts/angular-brand-codemod.spec.ts +3 -3
- package/src/scripts/angular-brand-codemod.ts +78 -7
- package/src/scripts/migrate.d.ts +32 -0
- package/src/scripts/migrate.js +67 -0
- package/src/scripts/migrate.js.map +1 -0
- package/src/scripts/migrate.ts +128 -0
- package/src/scripts/migration-workspace.d.ts +2 -0
- package/src/scripts/migration-workspace.js +72 -0
- package/src/scripts/migration-workspace.js.map +1 -0
- package/src/scripts/migration-workspace.ts +93 -0
- package/src/scripts/primitives/migrate-primitive-generators.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitive-generators.js +315 -0
- package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
- package/src/scripts/primitives/migrate-primitive-generators.ts +402 -0
- package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +386 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.spec.ts +322 -0
- package/src/scripts/primitives/migrate-primitives.ts +601 -0
- package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
- package/src/scripts/primitives/migration-diagnostic.js +2 -0
- package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
- package/src/scripts/primitives/migration-diagnostic.ts +14 -0
- package/src/scripts/routes/migrate-routes.d.ts +34 -0
- package/src/scripts/routes/migrate-routes.js +669 -0
- package/src/scripts/routes/migrate-routes.js.map +1 -0
- package/src/scripts/routes/migrate-routes.spec.ts +264 -0
- package/src/scripts/routes/migrate-routes.ts +895 -0
- package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
- package/src/scripts/routes/migration-diagnostic.js +2 -0
- package/src/scripts/routes/migration-diagnostic.js.map +1 -0
- package/src/scripts/routes/migration-diagnostic.ts +25 -0
- package/src/scripts/routes/route-command.d.ts +75 -0
- package/src/scripts/routes/route-command.js +1187 -0
- package/src/scripts/routes/route-command.js.map +1 -0
- package/src/scripts/routes/route-command.spec.ts +553 -0
- package/src/scripts/routes/route-command.ts +1790 -0
- package/src/scripts/services/config.d.ts +2 -0
- package/src/scripts/services/config.js +39 -0
- package/src/scripts/services/config.js.map +1 -0
- package/src/scripts/services/config.ts +60 -0
- package/src/scripts/services/migrate-services.d.ts +29 -0
- package/src/scripts/services/migrate-services.js +929 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.spec.ts +761 -0
- package/src/scripts/services/migrate-services.ts +1333 -0
- package/src/scripts/services/migration-diagnostic.d.ts +8 -0
- package/src/scripts/services/migration-diagnostic.js +2 -0
- package/src/scripts/services/migration-diagnostic.js.map +1 -0
- package/src/scripts/services/migration-diagnostic.ts +28 -0
- package/src/eslint-rules/require-track-on-dependent-primitives.cjs +0 -219
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { runPrimitivesMigration } from './migrate-primitives';
|
|
6
|
+
|
|
7
|
+
const temporaryDirectories: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(
|
|
11
|
+
temporaryDirectories
|
|
12
|
+
.splice(0)
|
|
13
|
+
.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function fixture(files: Record<string, string>): Promise<string> {
|
|
18
|
+
const root = await mkdtemp(join(tmpdir(), 'craft-primitives-'));
|
|
19
|
+
temporaryDirectories.push(root);
|
|
20
|
+
await Promise.all(
|
|
21
|
+
Object.entries(files).map(async ([path, contents]) => {
|
|
22
|
+
const fullPath = join(root, path);
|
|
23
|
+
const { mkdir } = await import('node:fs/promises');
|
|
24
|
+
await mkdir(join(fullPath, '..'), { recursive: true });
|
|
25
|
+
await writeFile(fullPath, contents, 'utf8');
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('primitives migration', () => {
|
|
32
|
+
it('converts Angular signal calls to craft state', async () => {
|
|
33
|
+
const root = await fixture({
|
|
34
|
+
'tsconfig.json': JSON.stringify({
|
|
35
|
+
compilerOptions: { experimentalDecorators: true },
|
|
36
|
+
}),
|
|
37
|
+
'wizard.ts': `
|
|
38
|
+
import { computed, signal } from '@angular/core';
|
|
39
|
+
export class Wizard {
|
|
40
|
+
readonly activeStep = signal('delivery');
|
|
41
|
+
readonly total = computed(() => this.activeStep());
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = await runPrimitivesMigration({
|
|
47
|
+
rootDir: root,
|
|
48
|
+
write: true,
|
|
49
|
+
eslint: false,
|
|
50
|
+
log: () => undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const output = await readFile(join(root, 'wizard.ts'), 'utf8');
|
|
54
|
+
expect(result.diagnostics).toEqual([]);
|
|
55
|
+
expect(output).toContain("import { computed } from '@angular/core'");
|
|
56
|
+
expect(output).toContain("import { state } from '@craft-ng/core'");
|
|
57
|
+
expect(output).toContain(
|
|
58
|
+
"activeStep = state('delivery', ({ set, update }) => ({ set, update }))",
|
|
59
|
+
);
|
|
60
|
+
expect(output).toContain(
|
|
61
|
+
'// CRAFT_IMPERATIVE_CODE_DETECTED: imperative code detected, prefer a declarative approach.',
|
|
62
|
+
);
|
|
63
|
+
expect(output).not.toContain('signal(');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('replaces explicit signal generics with satisfies on the state value', async () => {
|
|
67
|
+
const root = await fixture({
|
|
68
|
+
'tsconfig.json': '{}',
|
|
69
|
+
'wizard.ts': `
|
|
70
|
+
import { signal } from '@angular/core';
|
|
71
|
+
type WizardStep = 'delivery' | 'schedule' | 'review';
|
|
72
|
+
export const activeStep = signal<WizardStep>('delivery');
|
|
73
|
+
export const stepStatus = signal<Record<WizardStep, 'success' | 'error' | null>>({
|
|
74
|
+
delivery: null,
|
|
75
|
+
schedule: null,
|
|
76
|
+
review: null,
|
|
77
|
+
});
|
|
78
|
+
`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await runPrimitivesMigration({
|
|
82
|
+
rootDir: root,
|
|
83
|
+
write: true,
|
|
84
|
+
eslint: false,
|
|
85
|
+
log: () => undefined,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const output = await readFile(join(root, 'wizard.ts'), 'utf8');
|
|
89
|
+
expect(output).toContain(
|
|
90
|
+
"state('delivery' as WizardStep satisfies WizardStep, ({ set, update }) => ({ set, update }))",
|
|
91
|
+
);
|
|
92
|
+
expect(output).toContain(
|
|
93
|
+
"} as Record<WizardStep, 'success' | 'error' | null> satisfies Record<WizardStep, 'success' | 'error' | null>, ({ set, update }) => ({ set, update }))",
|
|
94
|
+
);
|
|
95
|
+
expect(output).not.toContain('state<WizardStep>');
|
|
96
|
+
expect(output).not.toContain('state<Record<');
|
|
97
|
+
expect(
|
|
98
|
+
output.match(/CRAFT_IMPERATIVE_CODE_DETECTED/g),
|
|
99
|
+
).toHaveLength(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('annotates an imperative workflow in an already migrated craftService', async () => {
|
|
103
|
+
const root = await fixture({
|
|
104
|
+
'tsconfig.json': '{}',
|
|
105
|
+
'wizard.ts': `
|
|
106
|
+
import { craftService, state } from '@craft-ng/core';
|
|
107
|
+
export const { injectWizard } = craftService(
|
|
108
|
+
{ name: 'Wizard', scope: 'function' },
|
|
109
|
+
() => {
|
|
110
|
+
const activeStep = state('delivery');
|
|
111
|
+
const stepStatus = state({ delivery: null });
|
|
112
|
+
const checkoutForm = { form: { delivery: { submit: async () => true } } };
|
|
113
|
+
const router = { navigate: async (_commands: unknown[]) => true };
|
|
114
|
+
|
|
115
|
+
async function validateStep(step: 'delivery') {
|
|
116
|
+
const success = await checkoutForm.form[step].submit();
|
|
117
|
+
if (!success) return;
|
|
118
|
+
stepStatus.update((status) => ({ ...status, [step]: 'success' }));
|
|
119
|
+
activeStep.set(step);
|
|
120
|
+
await router.navigate(['/checkout', step]);
|
|
121
|
+
}
|
|
122
|
+
return { validateStep };
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
`,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const first = await runPrimitivesMigration({
|
|
129
|
+
rootDir: root,
|
|
130
|
+
write: true,
|
|
131
|
+
eslint: false,
|
|
132
|
+
log: () => undefined,
|
|
133
|
+
});
|
|
134
|
+
const output = await readFile(join(root, 'wizard.ts'), 'utf8');
|
|
135
|
+
expect(output).toContain(
|
|
136
|
+
'// CRAFT_REACTIVE_WORKFLOW_RECOMMENDED: workflow impératif détecté...',
|
|
137
|
+
);
|
|
138
|
+
expect(first.diagnostics.map((diagnostic) => diagnostic.code)).toContain(
|
|
139
|
+
'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await runPrimitivesMigration({
|
|
143
|
+
rootDir: root,
|
|
144
|
+
write: true,
|
|
145
|
+
eslint: false,
|
|
146
|
+
log: () => undefined,
|
|
147
|
+
});
|
|
148
|
+
const secondOutput = await readFile(join(root, 'wizard.ts'), 'utf8');
|
|
149
|
+
expect(
|
|
150
|
+
secondOutput.match(/CRAFT_REACTIVE_WORKFLOW_RECOMMENDED/g),
|
|
151
|
+
).toHaveLength(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('reports signal forms and rxResource async validators for insertForm rewrite', async () => {
|
|
155
|
+
const root = await fixture({
|
|
156
|
+
'tsconfig.json': JSON.stringify({
|
|
157
|
+
compilerOptions: { experimentalDecorators: true },
|
|
158
|
+
}),
|
|
159
|
+
'checkout-wizard.ts': `
|
|
160
|
+
import { signal } from '@angular/core';
|
|
161
|
+
import { rxResource } from '@angular/core/rxjs-interop';
|
|
162
|
+
import { form, required, validateAsync } from '@angular/forms/signals';
|
|
163
|
+
export class CheckoutWizard {
|
|
164
|
+
private readonly discount = signal(0);
|
|
165
|
+
readonly checkoutForm = form(signal({ coupon: { code: '' } }), (schema) => {
|
|
166
|
+
required(schema.coupon.code);
|
|
167
|
+
validateAsync(schema.coupon.code, {
|
|
168
|
+
params: ({ fieldTreeOf }) => ({ fieldTreeOf }),
|
|
169
|
+
factory: (params) => rxResource({
|
|
170
|
+
params,
|
|
171
|
+
stream: ({ params }) => params.fieldTreeOf(schema.coupon.code)().value(),
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
`,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await runPrimitivesMigration({
|
|
180
|
+
rootDir: root,
|
|
181
|
+
write: true,
|
|
182
|
+
eslint: false,
|
|
183
|
+
log: () => undefined,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const output = await readFile(join(root, 'checkout-wizard.ts'), 'utf8');
|
|
187
|
+
expect(output).toContain(
|
|
188
|
+
'state(0, ({ set, update }) => ({ set, update }))',
|
|
189
|
+
);
|
|
190
|
+
expect(output).toContain(
|
|
191
|
+
'state({ coupon: { code:',
|
|
192
|
+
);
|
|
193
|
+
expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
|
|
194
|
+
'SIGNAL_FORM_REQUIRES_INSERT_FORM',
|
|
195
|
+
'ASYNC_VALIDATOR_REQUIRES_QUERY',
|
|
196
|
+
'RX_RESOURCE_REQUIRES_QUERY',
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('converts a debounced single-emission rxResource to query', async () => {
|
|
201
|
+
const root = await fixture({
|
|
202
|
+
'tsconfig.json': '{}',
|
|
203
|
+
'location-field.ts': `
|
|
204
|
+
import { signal } from '@angular/core';
|
|
205
|
+
import { rxResource, toObservable, toSignal } from '@angular/core/rxjs-interop';
|
|
206
|
+
import { debounceTime, distinctUntilChanged, from, map, of } from 'rxjs';
|
|
207
|
+
import { switchMap } from 'rxjs/operators';
|
|
208
|
+
|
|
209
|
+
export class LocationField {
|
|
210
|
+
private readonly api = Promise.resolve({
|
|
211
|
+
search: (params: { q: string }) => of([{ label: params.q }]),
|
|
212
|
+
});
|
|
213
|
+
private readonly searchInput = signal('');
|
|
214
|
+
private readonly debouncedSearch = toSignal(
|
|
215
|
+
toObservable(this.searchInput).pipe(
|
|
216
|
+
debounceTime(280),
|
|
217
|
+
map((value) => value.trim()),
|
|
218
|
+
distinctUntilChanged(),
|
|
219
|
+
),
|
|
220
|
+
{ initialValue: '' },
|
|
221
|
+
);
|
|
222
|
+
readonly suggestions = rxResource({
|
|
223
|
+
params: () => {
|
|
224
|
+
const query = this.debouncedSearch();
|
|
225
|
+
return query.length < 2 ? undefined : { q: query };
|
|
226
|
+
},
|
|
227
|
+
stream: ({ params }) => {
|
|
228
|
+
if (!params) return of([]);
|
|
229
|
+
return from(this.api).pipe(switchMap((service) => service.search(params)));
|
|
230
|
+
},
|
|
231
|
+
defaultValue: [],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
`,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await runPrimitivesMigration({
|
|
238
|
+
rootDir: root,
|
|
239
|
+
write: true,
|
|
240
|
+
eslint: false,
|
|
241
|
+
log: () => undefined,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const output = await readFile(join(root, 'location-field.ts'), 'utf8');
|
|
245
|
+
expect(output).toContain('readonly suggestions = query({');
|
|
246
|
+
expect(output).toContain('loader: async ({ params }) =>');
|
|
247
|
+
expect(output).toContain('return [];');
|
|
248
|
+
expect(output).toContain(
|
|
249
|
+
'// CRAFT_FIRST_VALUE_FROM_REVIEW: firstValueFrom bridges an Observable temporarily; prefer a Promise-native Craft API when possible.',
|
|
250
|
+
);
|
|
251
|
+
expect(output).toContain(
|
|
252
|
+
'return firstValueFrom((await this.api).search(params));',
|
|
253
|
+
);
|
|
254
|
+
expect(output).not.toContain('rxResource');
|
|
255
|
+
expect(output).not.toContain("from 'rxjs/operators'");
|
|
256
|
+
expect(result.diagnostics).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('supports check mode for remaining signal forms', async () => {
|
|
260
|
+
const root = await fixture({
|
|
261
|
+
'tsconfig.json': '{}',
|
|
262
|
+
'form.ts': `
|
|
263
|
+
import { form } from '@angular/forms/signals';
|
|
264
|
+
export const x = form;
|
|
265
|
+
`,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await runPrimitivesMigration({
|
|
269
|
+
rootDir: root,
|
|
270
|
+
write: false,
|
|
271
|
+
check: true,
|
|
272
|
+
eslint: false,
|
|
273
|
+
log: () => undefined,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(result.exitCode).toBe(1);
|
|
277
|
+
expect(result.remainingSignalForms).toBe(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('suggests inlining a local makeFormTreeInsert into insertForm', async () => {
|
|
281
|
+
const root = await fixture({
|
|
282
|
+
'tsconfig.json': '{}',
|
|
283
|
+
'checkout.ts': `
|
|
284
|
+
import { formTreeNeed, insertForm, insertSelectFormTree, makeFormTreeInsert, state } from '@craft-ng/core';
|
|
285
|
+
type CheckoutForm = { coupon: { code: string } };
|
|
286
|
+
const { insertCouponTree } = makeFormTreeInsert(
|
|
287
|
+
'Coupon',
|
|
288
|
+
formTreeNeed<CheckoutForm['coupon']>(),
|
|
289
|
+
() => ({}),
|
|
290
|
+
);
|
|
291
|
+
export const checkout = state(
|
|
292
|
+
{ coupon: { code: '' } },
|
|
293
|
+
insertForm(insertSelectFormTree('coupon', insertCouponTree())),
|
|
294
|
+
);
|
|
295
|
+
`,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const first = await runPrimitivesMigration({
|
|
299
|
+
rootDir: root,
|
|
300
|
+
write: true,
|
|
301
|
+
eslint: false,
|
|
302
|
+
log: () => undefined,
|
|
303
|
+
});
|
|
304
|
+
await runPrimitivesMigration({
|
|
305
|
+
rootDir: root,
|
|
306
|
+
write: true,
|
|
307
|
+
eslint: false,
|
|
308
|
+
log: () => undefined,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const output = await readFile(join(root, 'checkout.ts'), 'utf8');
|
|
312
|
+
expect(first.diagnostics.map((diagnostic) => diagnostic.code)).toContain(
|
|
313
|
+
'FORM_TREE_INSERT_EXTRACTION_REQUIRES_REVIEW',
|
|
314
|
+
);
|
|
315
|
+
expect(output).toContain(
|
|
316
|
+
'// CRAFT_FORM_TREE_INSERT_EXTRACTION_REVIEW: makeFormTreeInsert sert surtout à extraire et découper une logique de formulaire; si cet insert n’est utilisé qu’ici, envisager de le placer directement dans insertForm.',
|
|
317
|
+
);
|
|
318
|
+
expect(
|
|
319
|
+
output.match(/CRAFT_FORM_TREE_INSERT_EXTRACTION_REVIEW/g),
|
|
320
|
+
).toHaveLength(1);
|
|
321
|
+
});
|
|
322
|
+
});
|