@craft-ng/dev-tools 0.5.0-beta.5 → 0.5.1-beta.0

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 (64) hide show
  1. package/README.md +132 -23
  2. package/package.json +6 -2
  3. package/src/bin/craft-migrate-primitives.d.ts +2 -0
  4. package/src/bin/craft-migrate-primitives.js +71 -0
  5. package/src/bin/craft-migrate-primitives.js.map +1 -0
  6. package/src/bin/craft-migrate-routes.d.ts +2 -0
  7. package/src/bin/craft-migrate-routes.js +77 -0
  8. package/src/bin/craft-migrate-routes.js.map +1 -0
  9. package/src/bin/craft-migrate-services.d.ts +2 -0
  10. package/src/bin/craft-migrate-services.js +75 -0
  11. package/src/bin/craft-migrate-services.js.map +1 -0
  12. package/src/bin/craft-migrate.d.ts +2 -0
  13. package/src/bin/craft-migrate.js +90 -0
  14. package/src/bin/craft-migrate.js.map +1 -0
  15. package/src/eslint-rules/index.cjs +2 -0
  16. package/src/eslint-rules/require-exception-component-di-check.cjs +66 -10
  17. package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
  18. package/src/index.d.ts +8 -0
  19. package/src/index.js +8 -0
  20. package/src/index.js.map +1 -1
  21. package/src/scripts/angular-brand-codemod.d.ts +17 -0
  22. package/src/scripts/angular-brand-codemod.js +44 -8
  23. package/src/scripts/angular-brand-codemod.js.map +1 -1
  24. package/src/scripts/angular-brand-codemod.spec.ts +3 -3
  25. package/src/scripts/angular-brand-codemod.ts +78 -7
  26. package/src/scripts/migrate.d.ts +32 -0
  27. package/src/scripts/migrate.js +67 -0
  28. package/src/scripts/migrate.js.map +1 -0
  29. package/src/scripts/migrate.ts +128 -0
  30. package/src/scripts/migration-workspace.d.ts +2 -0
  31. package/src/scripts/migration-workspace.js +72 -0
  32. package/src/scripts/migration-workspace.js.map +1 -0
  33. package/src/scripts/migration-workspace.ts +93 -0
  34. package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
  35. package/src/scripts/primitives/migrate-primitives.js +354 -0
  36. package/src/scripts/primitives/migrate-primitives.js.map +1 -0
  37. package/src/scripts/primitives/migrate-primitives.spec.ts +279 -0
  38. package/src/scripts/primitives/migrate-primitives.ts +543 -0
  39. package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
  40. package/src/scripts/primitives/migration-diagnostic.js +2 -0
  41. package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
  42. package/src/scripts/primitives/migration-diagnostic.ts +13 -0
  43. package/src/scripts/routes/migrate-routes.d.ts +34 -0
  44. package/src/scripts/routes/migrate-routes.js +669 -0
  45. package/src/scripts/routes/migrate-routes.js.map +1 -0
  46. package/src/scripts/routes/migrate-routes.spec.ts +264 -0
  47. package/src/scripts/routes/migrate-routes.ts +897 -0
  48. package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
  49. package/src/scripts/routes/migration-diagnostic.js +2 -0
  50. package/src/scripts/routes/migration-diagnostic.js.map +1 -0
  51. package/src/scripts/routes/migration-diagnostic.ts +25 -0
  52. package/src/scripts/services/config.d.ts +2 -0
  53. package/src/scripts/services/config.js +39 -0
  54. package/src/scripts/services/config.js.map +1 -0
  55. package/src/scripts/services/config.ts +60 -0
  56. package/src/scripts/services/migrate-services.d.ts +29 -0
  57. package/src/scripts/services/migrate-services.js +895 -0
  58. package/src/scripts/services/migrate-services.js.map +1 -0
  59. package/src/scripts/services/migrate-services.spec.ts +719 -0
  60. package/src/scripts/services/migrate-services.ts +1282 -0
  61. package/src/scripts/services/migration-diagnostic.d.ts +8 -0
  62. package/src/scripts/services/migration-diagnostic.js +2 -0
  63. package/src/scripts/services/migration-diagnostic.js.map +1 -0
  64. package/src/scripts/services/migration-diagnostic.ts +27 -0
@@ -1,4 +1,4 @@
1
- /** Generates O(1) DI checks for renderComponent/errorComponent/withErrorComponent. */
1
+ /** Generates O(1) DI checks for route, exception, and route-load error components. */
2
2
  module.exports = {
3
3
  meta: { type: 'problem', fixable: 'code', schema: [] },
4
4
  create(context) {
@@ -6,7 +6,11 @@ module.exports = {
6
6
  return {
7
7
  'Program:exit'(program) {
8
8
  const text = source.getText();
9
- if (!/renderComponent|errorComponent|withErrorComponent/.test(text))
9
+ if (
10
+ !/renderComponent|errorComponent|withErrorComponent|withRouteLoadError|provideRouteLoadErrorComponent/.test(
11
+ text,
12
+ )
13
+ )
10
14
  return;
11
15
 
12
16
  const cascade = readCascadeContext(text);
@@ -14,25 +18,34 @@ module.exports = {
14
18
  walk(program, (node) => {
15
19
  if (isCall(node, 'craftRoutes')) {
16
20
  collectRouteChecks(node, source, cascade, checks);
17
- } else if (isCall(node, 'withErrorComponent')) {
21
+ } else if (
22
+ isCall(node, 'withErrorComponent') ||
23
+ isCall(node, 'withRouteLoadError')
24
+ ) {
18
25
  const descriptor = node.arguments[0];
19
26
  const deps = readProperty(descriptor, 'componentDeps', source);
20
27
  if (deps) {
28
+ const routeLoad = isCall(node, 'withRouteLoadError');
21
29
  checks.push({
22
30
  deps,
23
- names: joinNames(cascade.names, ['CraftGlobalError']),
31
+ names: joinNames(
32
+ cascade.names,
33
+ routeLoad
34
+ ? ['CraftRouteLoadError', 'CraftRouteLoadRecovery']
35
+ : ['CraftGlobalError'],
36
+ ),
24
37
  values: cascade.values,
25
- label: 'global error component',
38
+ label: routeLoad
39
+ ? 'global route load error component'
40
+ : 'global error component',
26
41
  });
27
42
  }
28
43
  }
29
44
  });
30
45
 
46
+ const existingChecks = readExistingCheckDeps(text);
31
47
  const missing = checks.filter(
32
- (check) =>
33
- !text.includes(
34
- `RouteExceptionComponentCheckedDI<\n ${check.deps},`,
35
- ),
48
+ (check) => !existingChecks.has(normalizeTypeText(check.deps)),
36
49
  );
37
50
  if (missing.length === 0) return;
38
51
 
@@ -84,6 +97,29 @@ function collectRouteChecks(call, source, cascade, checks) {
84
97
  });
85
98
  }
86
99
 
100
+ const providers = propertyValue(def, 'providers');
101
+ if (providers?.type === 'ArrayExpression') {
102
+ for (const provider of providers.elements) {
103
+ if (!isCall(provider, 'provideRouteLoadErrorComponent')) continue;
104
+ const deps = readProperty(
105
+ provider.arguments[0],
106
+ 'componentDeps',
107
+ source,
108
+ );
109
+ if (!deps) continue;
110
+ checks.push({
111
+ deps,
112
+ names: joinNames(cascade.names, [
113
+ ...baseNames,
114
+ 'CraftRouteLoadError',
115
+ 'CraftRouteLoadRecovery',
116
+ ]),
117
+ values: cascade.values,
118
+ label: `route load error component: ${path}`,
119
+ });
120
+ }
121
+ }
122
+
87
123
  const handlers = routeCall
88
124
  ? routeCall.arguments[2]
89
125
  : propertyValue(def, 'handleExceptions');
@@ -193,10 +229,30 @@ function ensureImports(text) {
193
229
  );
194
230
  }
195
231
 
232
+ function readExistingCheckDeps(text) {
233
+ const deps = new Set();
234
+ const regex = /RouteExceptionComponentCheckedDI<\s*([\s\S]*?),\s*(?:'[^']*'|never)/g;
235
+ let match;
236
+ while ((match = regex.exec(text))) {
237
+ deps.add(normalizeTypeText(match[1]));
238
+ }
239
+ return deps;
240
+ }
241
+
242
+ function normalizeTypeText(text) {
243
+ return String(text).replace(/\s+/g, '');
244
+ }
245
+
196
246
  function readProperty(node, name, source) {
197
247
  const value = propertyValue(node, name);
198
- return value ? source.getText(value) : undefined;
248
+ return value ? readTypeText(value, source) : undefined;
249
+ }
250
+
251
+ function readTypeText(node, source) {
252
+ if (node?.type === 'TSAsExpression') return source.getText(node.typeAnnotation);
253
+ return source.getText(node);
199
254
  }
255
+
200
256
  function propertyValue(node, name) {
201
257
  if (node?.type !== 'ObjectExpression') return undefined;
202
258
  return node.properties.find(
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const LAZY_PROPERTIES = new Set([
4
+ 'loadComponent',
5
+ 'loadChildren',
6
+ ]);
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: {
12
+ description:
13
+ 'Require lazy route imports to use the withRetry helper so browser-cached module failures can be retried.',
14
+ },
15
+ fixable: 'code',
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
20
+ const importsByLoader = new Map();
21
+
22
+ return {
23
+ ImportExpression(node) {
24
+ const loader = enclosingLazyLoader(node);
25
+ if (!loader || isWrappedWithRetry(node)) return;
26
+
27
+ const imports = importsByLoader.get(loader) ?? [];
28
+ imports.push(node);
29
+ importsByLoader.set(loader, imports);
30
+ },
31
+ 'Program:exit'() {
32
+ for (const [loader, imports] of importsByLoader) {
33
+ context.report({
34
+ node: imports[0],
35
+ message:
36
+ 'Lazy route imports must be wrapped with withRetry(import(...)).',
37
+ fix(fixer) {
38
+ const parameterFixes = addWithRetryParameter(
39
+ fixer,
40
+ sourceCode,
41
+ loader,
42
+ );
43
+ if (!parameterFixes) return null;
44
+
45
+ return [
46
+ ...parameterFixes,
47
+ ...imports.flatMap((moduleImport) => [
48
+ fixer.insertTextBefore(moduleImport, 'withRetry('),
49
+ fixer.insertTextAfter(moduleImport, ')'),
50
+ ]),
51
+ ];
52
+ },
53
+ });
54
+ }
55
+ },
56
+ };
57
+ },
58
+ };
59
+
60
+ function enclosingLazyLoader(node) {
61
+ let current = node.parent;
62
+ while (current) {
63
+ if (
64
+ (current.type === 'ArrowFunctionExpression' ||
65
+ current.type === 'FunctionExpression') &&
66
+ current.parent?.type === 'Property' &&
67
+ LAZY_PROPERTIES.has(propertyName(current.parent)) &&
68
+ isInsideCraftRoute(current.parent)
69
+ ) {
70
+ return current;
71
+ }
72
+ if (current.type === 'Property' || current.type === 'Program') return null;
73
+ current = current.parent;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function isInsideCraftRoute(node) {
79
+ let current = node.parent;
80
+ while (current) {
81
+ if (
82
+ current.type === 'CallExpression' &&
83
+ current.callee.type === 'Identifier' &&
84
+ (current.callee.name === 'craftRoute' ||
85
+ current.callee.name === 'craftRoutes')
86
+ ) {
87
+ return true;
88
+ }
89
+ if (current.type === 'Program') return false;
90
+ current = current.parent;
91
+ }
92
+ return false;
93
+ }
94
+
95
+ function propertyName(property) {
96
+ if (property.computed) return undefined;
97
+ if (property.key.type === 'Identifier') return property.key.name;
98
+ if (property.key.type === 'Literal') return property.key.value;
99
+ return undefined;
100
+ }
101
+
102
+ function isWrappedWithRetry(node) {
103
+ return (
104
+ node.parent?.type === 'CallExpression' &&
105
+ node.parent.callee.type === 'Identifier' &&
106
+ node.parent.callee.name === 'withRetry' &&
107
+ node.parent.arguments[0] === node
108
+ );
109
+ }
110
+
111
+ function addWithRetryParameter(fixer, sourceCode, loader) {
112
+ if (loader.params.length === 0) {
113
+ const arrow = sourceCode.getTokenBefore(loader.body, {
114
+ filter: (token) => token.value === '=>',
115
+ });
116
+ if (!arrow) return null;
117
+
118
+ const closeParen = sourceCode.getTokenBefore(arrow);
119
+ const openParen = closeParen && sourceCode.getTokenBefore(closeParen);
120
+ if (openParen?.value !== '(' || closeParen?.value !== ')') return null;
121
+ return [
122
+ fixer.replaceTextRange(
123
+ [openParen.range[1], closeParen.range[0]],
124
+ '{ withRetry }',
125
+ ),
126
+ ];
127
+ }
128
+
129
+ if (loader.params.length !== 1 || loader.params[0].type !== 'ObjectPattern') {
130
+ return null;
131
+ }
132
+
133
+ const pattern = loader.params[0];
134
+ if (
135
+ pattern.properties.some(
136
+ (property) =>
137
+ property.type === 'Property' &&
138
+ property.key.type === 'Identifier' &&
139
+ property.key.name === 'withRetry',
140
+ )
141
+ ) {
142
+ return [];
143
+ }
144
+
145
+ const closingBrace = sourceCode.getLastToken(pattern);
146
+ const prefix = pattern.properties.length > 0 ? ', ' : '';
147
+ return [fixer.insertTextBefore(closingBrace, `${prefix}withRetry`)];
148
+ }
package/src/index.d.ts CHANGED
@@ -1 +1,9 @@
1
1
  export * from './scripts/angular-brand-codemod';
2
+ export * from './scripts/primitives/migrate-primitives';
3
+ export * from './scripts/migrate';
4
+ export * from './scripts/primitives/migration-diagnostic';
5
+ export * from './scripts/routes/migrate-routes';
6
+ export * from './scripts/routes/migration-diagnostic';
7
+ export * from './scripts/services/config';
8
+ export * from './scripts/services/migrate-services';
9
+ export * from './scripts/services/migration-diagnostic';
package/src/index.js CHANGED
@@ -1,3 +1,11 @@
1
1
  // Export programmatic API for codemod
2
2
  export * from './scripts/angular-brand-codemod';
3
+ export * from './scripts/primitives/migrate-primitives';
4
+ export * from './scripts/migrate';
5
+ export * from './scripts/primitives/migration-diagnostic';
6
+ export * from './scripts/routes/migrate-routes';
7
+ export * from './scripts/routes/migration-diagnostic';
8
+ export * from './scripts/services/config';
9
+ export * from './scripts/services/migrate-services';
10
+ export * from './scripts/services/migration-diagnostic';
3
11
  //# sourceMappingURL=index.js.map
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/dev-tools/src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,cAAc,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/dev-tools/src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,mBAAmB,CAAC;AAClC,cAAc,2CAA2C,CAAC;AAC1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,uCAAuC,CAAC;AACtD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC"}
@@ -19,6 +19,22 @@ export type AngularBrandImportAugmentationRule = {
19
19
  export type AngularBrandConfig = {
20
20
  importAugmentations?: readonly AngularBrandImportAugmentationRule[];
21
21
  };
22
+ export type ServiceMigrationScope = 'global' | 'toProvide' | 'manuallyProvidedAtRoot' | 'function' | 'abstract';
23
+ export type ServiceMigrationStrategy = 'craftService' | 'toCraftService' | 'companion' | 'ignore';
24
+ export type ServiceMigrationOverride = {
25
+ file?: string;
26
+ module?: string;
27
+ symbol?: string;
28
+ name?: string;
29
+ scope?: ServiceMigrationScope;
30
+ strategy?: ServiceMigrationStrategy;
31
+ };
32
+ export type CraftDevToolsConfig = {
33
+ brand?: AngularBrandConfig;
34
+ serviceMigration?: {
35
+ overrides?: readonly ServiceMigrationOverride[];
36
+ };
37
+ };
22
38
  export type TransformResult = {
23
39
  changed: boolean;
24
40
  skipped: boolean;
@@ -97,6 +113,7 @@ export type RunSummary = {
97
113
  files: RunFileReport[];
98
114
  };
99
115
  export declare function defineAngularBrandConfig<Config extends AngularBrandConfig>(config: Config): Config;
116
+ export declare function defineCraftDevToolsConfig<Config extends CraftDevToolsConfig>(config: Config): Config;
100
117
  export declare function discoverAngularBrandConfigFilePath(searchFromDir: string, stopDir?: string): string | undefined;
101
118
  export declare function loadAngularBrandConfigFromFile(configFilePath: string): AngularBrandConfig;
102
119
  export declare function transformSourceFile(sourceFile: SourceFile, options?: AngularBrandCodemodOptions): TransformResult;
@@ -18,6 +18,7 @@ const DEFAULT_OPTIONS = {
18
18
  config: undefined,
19
19
  configFilePath: undefined,
20
20
  };
21
+ const CRAFT_DEV_TOOLS_CONFIG_FILE_NAME = 'craft-dev-tools.config.ts';
21
22
  const ANGULAR_BRAND_CONFIG_FILE_NAME = 'craft-brand.config.ts';
22
23
  const DEFAULT_ANGULAR_BRAND_METADATA_CONTEXTS = [
23
24
  'imports',
@@ -54,7 +55,22 @@ const DEFAULT_ANGULAR_BRAND_CONFIG = defineAngularBrandConfig({
54
55
  {
55
56
  key: 'FormField',
56
57
  symbol: 'FormField',
57
- typeText: 'FormField<unknown>',
58
+ typeText: 'FormField<never>',
59
+ module: '@angular/forms/signals',
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ match: {
65
+ module: '@angular/forms/signals',
66
+ symbols: ['FormRoot'],
67
+ metadata: ['imports'],
68
+ },
69
+ deps: [
70
+ {
71
+ key: 'FormRoot',
72
+ symbol: 'FormRoot',
73
+ typeText: 'FormRoot<unknown>',
58
74
  module: '@angular/forms/signals',
59
75
  },
60
76
  ],
@@ -168,13 +184,26 @@ const IGNORED_DIRECTORIES = new Set([
168
184
  export function defineAngularBrandConfig(config) {
169
185
  return config;
170
186
  }
187
+ export function defineCraftDevToolsConfig(config) {
188
+ return config;
189
+ }
190
+ function isCraftDevToolsConfig(value) {
191
+ return Boolean(value &&
192
+ typeof value === 'object' &&
193
+ ('brand' in value || 'serviceMigration' in value));
194
+ }
171
195
  export function discoverAngularBrandConfigFilePath(searchFromDir, stopDir) {
172
196
  const resolvedStopDir = stopDir ? resolve(stopDir) : undefined;
173
197
  let currentDir = resolve(searchFromDir);
174
198
  while (true) {
175
- const candidatePath = join(currentDir, ANGULAR_BRAND_CONFIG_FILE_NAME);
176
- if (existsSync(candidatePath) && statSync(candidatePath).isFile()) {
177
- return candidatePath;
199
+ for (const fileName of [
200
+ CRAFT_DEV_TOOLS_CONFIG_FILE_NAME,
201
+ ANGULAR_BRAND_CONFIG_FILE_NAME,
202
+ ]) {
203
+ const candidatePath = join(currentDir, fileName);
204
+ if (existsSync(candidatePath) && statSync(candidatePath).isFile()) {
205
+ return candidatePath;
206
+ }
178
207
  }
179
208
  if (resolvedStopDir && currentDir === resolvedStopDir) {
180
209
  return undefined;
@@ -187,7 +216,7 @@ export function discoverAngularBrandConfigFilePath(searchFromDir, stopDir) {
187
216
  }
188
217
  }
189
218
  export function loadAngularBrandConfigFromFile(configFilePath) {
190
- var _a;
219
+ var _a, _b;
191
220
  const resolvedConfigFilePath = resolve(configFilePath);
192
221
  const cachedConfig = angularBrandConfigCache.get(resolvedConfigFilePath);
193
222
  if (cachedConfig) {
@@ -213,14 +242,17 @@ export function loadAngularBrandConfigFromFile(configFilePath) {
213
242
  const compiledModule = new Function('exports', 'require', 'module', '__filename', '__dirname', transpiled.outputText);
214
243
  compiledModule(module.exports, (specifier) => {
215
244
  if (specifier === '@craft-ng/dev-tools') {
216
- return { defineAngularBrandConfig };
245
+ return { defineAngularBrandConfig, defineCraftDevToolsConfig };
217
246
  }
218
247
  return moduleRequire(specifier);
219
248
  }, module, resolvedConfigFilePath, dirname(resolvedConfigFilePath));
220
249
  const exportedConfig = (_a = module.exports['default']) !== null && _a !== void 0 ? _a : (module.exports['__esModule']
221
250
  ? module.exports['default']
222
251
  : module.exports);
223
- const validatedConfig = validateAngularBrandConfig(exportedConfig, resolvedConfigFilePath);
252
+ const brandConfig = isCraftDevToolsConfig(exportedConfig)
253
+ ? ((_b = exportedConfig.brand) !== null && _b !== void 0 ? _b : {})
254
+ : exportedConfig;
255
+ const validatedConfig = validateAngularBrandConfig(brandConfig, resolvedConfigFilePath);
224
256
  angularBrandConfigCache.set(resolvedConfigFilePath, validatedConfig);
225
257
  return validatedConfig;
226
258
  }
@@ -1065,7 +1097,11 @@ function ensureGeneratedDependencyTypeImports(sourceFile, generatedDependencyGro
1065
1097
  if (namesToAdd.length === 0) {
1066
1098
  continue;
1067
1099
  }
1068
- existingImport.addNamedImports(namesToAdd.map((name) => ({ name, isTypeOnly: true })));
1100
+ const declarationIsTypeOnly = existingImport.isTypeOnly();
1101
+ existingImport.addNamedImports(namesToAdd.map((name) => ({
1102
+ name,
1103
+ isTypeOnly: !declarationIsTypeOnly,
1104
+ })));
1069
1105
  continue;
1070
1106
  }
1071
1107
  sourceFile.addImportDeclaration({