@craft-ng/dev-tools 0.5.0-beta.4 → 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.
- package/README.md +132 -23
- package/package.json +6 -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/eslint-rules/global-exception-registry-match.cjs +4 -4
- package/src/eslint-rules/index.cjs +6 -0
- package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-pending-component-di-check.cjs +3 -3
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -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-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +354 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.spec.ts +279 -0
- package/src/scripts/primitives/migrate-primitives.ts +543 -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 +13 -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 +897 -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/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 +895 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.spec.ts +719 -0
- package/src/scripts/services/migrate-services.ts +1282 -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 +27 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** Enforces craftExceptionHandler(function* (...) {}) in craftRoute() handler maps. */
|
|
2
|
+
module.exports = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'problem',
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
schema: [],
|
|
7
|
+
messages: {
|
|
8
|
+
wrap: 'Route exception handlers must use craftExceptionHandler(function* (...) {}).',
|
|
9
|
+
redirect:
|
|
10
|
+
'Raw redirect(...) is ambiguous: migrate internal routes to redirectTo(...) and opaque URLs to redirectUrl(...).',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
const source = context.sourceCode ?? context.getSourceCode();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
'Program:exit'(program) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
|
|
20
|
+
for (const statement of program.body) {
|
|
21
|
+
walk(statement, (node) => {
|
|
22
|
+
if (
|
|
23
|
+
node.type !== 'CallExpression' ||
|
|
24
|
+
node.callee?.type !== 'Identifier' ||
|
|
25
|
+
node.callee.name !== 'craftRoute' ||
|
|
26
|
+
node.arguments.length < 3
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handlers = node.arguments[2];
|
|
32
|
+
if (handlers?.type !== 'ObjectExpression') return;
|
|
33
|
+
|
|
34
|
+
for (const property of handlers.properties) {
|
|
35
|
+
if (property.type !== 'Property') continue;
|
|
36
|
+
const value = property.value;
|
|
37
|
+
if (
|
|
38
|
+
value.type === 'CallExpression' &&
|
|
39
|
+
value.callee?.type === 'Identifier' &&
|
|
40
|
+
value.callee.name === 'craftExceptionHandler'
|
|
41
|
+
) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
value.type !== 'ArrowFunctionExpression' &&
|
|
47
|
+
value.type !== 'FunctionExpression'
|
|
48
|
+
) {
|
|
49
|
+
issues.push({ node: value, manual: true });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const text = source.getText(value);
|
|
54
|
+
issues.push({
|
|
55
|
+
node: value,
|
|
56
|
+
manual: /\bredirect\s*\(/.test(text),
|
|
57
|
+
replacement: wrapHandler(value, source),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (issues.length === 0) return;
|
|
64
|
+
const manual = issues.filter((issue) => issue.manual);
|
|
65
|
+
if (manual.length > 0) {
|
|
66
|
+
for (const issue of manual) {
|
|
67
|
+
context.report({ node: issue.node, messageId: 'redirect' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const fixable = issues.filter((item) => !item.manual);
|
|
72
|
+
if (fixable.length === 0) return;
|
|
73
|
+
const original = source.getText();
|
|
74
|
+
const replacements = fixable
|
|
75
|
+
.map((issue) => ({
|
|
76
|
+
range: issue.node.range,
|
|
77
|
+
text: issue.replacement,
|
|
78
|
+
}))
|
|
79
|
+
.sort((a, b) => b.range[0] - a.range[0]);
|
|
80
|
+
let fixed = original;
|
|
81
|
+
for (const replacement of replacements) {
|
|
82
|
+
fixed =
|
|
83
|
+
fixed.slice(0, replacement.range[0]) +
|
|
84
|
+
replacement.text +
|
|
85
|
+
fixed.slice(replacement.range[1]);
|
|
86
|
+
}
|
|
87
|
+
fixed = ensureImport(fixed);
|
|
88
|
+
|
|
89
|
+
context.report({
|
|
90
|
+
node: fixable[0].node,
|
|
91
|
+
messageId: 'wrap',
|
|
92
|
+
fix: (fixer) => fixer.replaceTextRange([0, original.length], fixed),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function wrapHandler(node, source) {
|
|
100
|
+
const params = node.params.map((param) => source.getText(param)).join(', ');
|
|
101
|
+
const body =
|
|
102
|
+
node.body.type === 'BlockStatement'
|
|
103
|
+
? source.getText(node.body)
|
|
104
|
+
: `{ return ${source.getText(node.body)}; }`;
|
|
105
|
+
return `craftExceptionHandler(function* (${params}) ${body})`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ensureImport(text) {
|
|
109
|
+
if (
|
|
110
|
+
/\bcraftExceptionHandler\b/.test(
|
|
111
|
+
text.match(/import[\s\S]*?from\s+['"]@craft-ng\/core['"]/g)?.join('\n') ??
|
|
112
|
+
'',
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
const pattern = /import\s*\{([\s\S]*?)\}\s*from\s*(['"]@craft-ng\/core['"])/;
|
|
118
|
+
if (pattern.test(text)) {
|
|
119
|
+
return text.replace(pattern, (_all, names, source) => {
|
|
120
|
+
const separator = names.includes('\n') ? '\n ' : ' ';
|
|
121
|
+
return `import {${names.trimEnd()},${separator}craftExceptionHandler\n} from ${source}`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return `import { craftExceptionHandler } from '@craft-ng/core';\n${text}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function walk(node, visit) {
|
|
128
|
+
if (!node || typeof node !== 'object') return;
|
|
129
|
+
visit(node);
|
|
130
|
+
for (const [key, value] of Object.entries(node)) {
|
|
131
|
+
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
for (const child of value) walk(child, visit);
|
|
134
|
+
} else if (
|
|
135
|
+
value &&
|
|
136
|
+
typeof value === 'object' &&
|
|
137
|
+
typeof value.type === 'string'
|
|
138
|
+
) {
|
|
139
|
+
walk(value, visit);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/** Generates O(1) DI checks for route, exception, and route-load error components. */
|
|
2
|
+
module.exports = {
|
|
3
|
+
meta: { type: 'problem', fixable: 'code', schema: [] },
|
|
4
|
+
create(context) {
|
|
5
|
+
const source = context.sourceCode ?? context.getSourceCode();
|
|
6
|
+
return {
|
|
7
|
+
'Program:exit'(program) {
|
|
8
|
+
const text = source.getText();
|
|
9
|
+
if (
|
|
10
|
+
!/renderComponent|errorComponent|withErrorComponent|withRouteLoadError|provideRouteLoadErrorComponent/.test(
|
|
11
|
+
text,
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
return;
|
|
15
|
+
|
|
16
|
+
const cascade = readCascadeContext(text);
|
|
17
|
+
const checks = [];
|
|
18
|
+
walk(program, (node) => {
|
|
19
|
+
if (isCall(node, 'craftRoutes')) {
|
|
20
|
+
collectRouteChecks(node, source, cascade, checks);
|
|
21
|
+
} else if (
|
|
22
|
+
isCall(node, 'withErrorComponent') ||
|
|
23
|
+
isCall(node, 'withRouteLoadError')
|
|
24
|
+
) {
|
|
25
|
+
const descriptor = node.arguments[0];
|
|
26
|
+
const deps = readProperty(descriptor, 'componentDeps', source);
|
|
27
|
+
if (deps) {
|
|
28
|
+
const routeLoad = isCall(node, 'withRouteLoadError');
|
|
29
|
+
checks.push({
|
|
30
|
+
deps,
|
|
31
|
+
names: joinNames(
|
|
32
|
+
cascade.names,
|
|
33
|
+
routeLoad
|
|
34
|
+
? ['CraftRouteLoadError', 'CraftRouteLoadRecovery']
|
|
35
|
+
: ['CraftGlobalError'],
|
|
36
|
+
),
|
|
37
|
+
values: cascade.values,
|
|
38
|
+
label: routeLoad
|
|
39
|
+
? 'global route load error component'
|
|
40
|
+
: 'global error component',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const existingChecks = readExistingCheckDeps(text);
|
|
47
|
+
const missing = checks.filter(
|
|
48
|
+
(check) => !existingChecks.has(normalizeTypeText(check.deps)),
|
|
49
|
+
);
|
|
50
|
+
if (missing.length === 0) return;
|
|
51
|
+
|
|
52
|
+
let fixed = ensureImports(text);
|
|
53
|
+
fixed += missing.map(renderCheck).join('');
|
|
54
|
+
context.report({
|
|
55
|
+
node: program,
|
|
56
|
+
message: `${missing.length} exception component(s) must be checked with RouteExceptionComponentCheckedDI.`,
|
|
57
|
+
fix: (fixer) => fixer.replaceTextRange([0, text.length], fixed),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function collectRouteChecks(call, source, cascade, checks) {
|
|
65
|
+
const collection = literal(call.arguments[0]);
|
|
66
|
+
const array = call.arguments[1];
|
|
67
|
+
if (!collection || array?.type !== 'ArrayExpression') return;
|
|
68
|
+
|
|
69
|
+
for (const entry of array.elements) {
|
|
70
|
+
const routeCall = unwrapRouteCall(entry);
|
|
71
|
+
const def = routeCall ? routeCall.arguments[1] : entry;
|
|
72
|
+
const path = routeCall
|
|
73
|
+
? literal(routeCall.arguments[0])
|
|
74
|
+
: literal(propertyValue(def, 'path'));
|
|
75
|
+
if (path === undefined || def?.type !== 'ObjectExpression') continue;
|
|
76
|
+
const baseNames = routeNames(collection, path, def);
|
|
77
|
+
if (routeCall && routeCall !== entry) {
|
|
78
|
+
walk(entry.arguments[0], (node) => {
|
|
79
|
+
if (
|
|
80
|
+
node?.type === 'CallExpression' &&
|
|
81
|
+
node.callee?.type === 'Identifier' &&
|
|
82
|
+
node.callee.name.startsWith('provide')
|
|
83
|
+
) {
|
|
84
|
+
baseNames.push(node.callee.name.slice('provide'.length));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const routeError = propertyValue(def, 'errorComponent');
|
|
90
|
+
const routeDeps = readProperty(routeError, 'componentDeps', source);
|
|
91
|
+
if (routeDeps) {
|
|
92
|
+
checks.push({
|
|
93
|
+
deps: routeDeps,
|
|
94
|
+
names: joinNames(cascade.names, [...baseNames, 'CraftGlobalError']),
|
|
95
|
+
values: cascade.values,
|
|
96
|
+
label: `error component: ${path}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
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
|
+
|
|
123
|
+
const handlers = routeCall
|
|
124
|
+
? routeCall.arguments[2]
|
|
125
|
+
: propertyValue(def, 'handleExceptions');
|
|
126
|
+
if (handlers?.type !== 'ObjectExpression') continue;
|
|
127
|
+
for (const handler of handlers.properties) {
|
|
128
|
+
if (handler.type !== 'Property') continue;
|
|
129
|
+
const code = propertyName(handler);
|
|
130
|
+
walk(handler.value, (node) => {
|
|
131
|
+
if (!isCall(node, 'renderComponent')) return;
|
|
132
|
+
const deps = readProperty(node.arguments[0], 'componentDeps', source);
|
|
133
|
+
if (!deps) return;
|
|
134
|
+
const helper = `${pascal(collection)}${routeBase(path)}${pascal(code)}Exception`;
|
|
135
|
+
checks.push({
|
|
136
|
+
deps,
|
|
137
|
+
names: joinNames(cascade.names, [...baseNames, helper]),
|
|
138
|
+
values: cascade.values,
|
|
139
|
+
label: `render component: ${path}#${code}`,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function unwrapRouteCall(entry) {
|
|
147
|
+
if (isCall(entry, 'craftRoute')) return entry;
|
|
148
|
+
if (
|
|
149
|
+
entry?.type === 'CallExpression' &&
|
|
150
|
+
entry.callee?.type === 'MemberExpression' &&
|
|
151
|
+
entry.callee.property?.type === 'Identifier' &&
|
|
152
|
+
entry.callee.property.name === 'withProviders' &&
|
|
153
|
+
isCall(entry.callee.object, 'craftRoute')
|
|
154
|
+
) {
|
|
155
|
+
return entry.callee.object;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function routeNames(collection, path, def) {
|
|
161
|
+
const prefix = pascal(collection);
|
|
162
|
+
const base = routeBase(path);
|
|
163
|
+
const names = path
|
|
164
|
+
.split('/')
|
|
165
|
+
.filter((x) => x.startsWith(':'))
|
|
166
|
+
.map((x) => `${prefix}${pascal(x.slice(1).replace(/\?$/, ''))}Params`);
|
|
167
|
+
if (propertyValue(def, 'data')) names.push(`${prefix}${base}Data`);
|
|
168
|
+
if (propertyValue(def, 'queryParams'))
|
|
169
|
+
names.push(`${prefix}${base}QueryParams`);
|
|
170
|
+
if (propertyValue(def, 'withLoaderViewTransitionImage'))
|
|
171
|
+
names.push(`${prefix}${base}ViewTransition`);
|
|
172
|
+
const providers = propertyValue(def, 'providers');
|
|
173
|
+
if (providers?.type === 'ArrayExpression') {
|
|
174
|
+
for (const provider of providers.elements) {
|
|
175
|
+
if (
|
|
176
|
+
provider?.type === 'CallExpression' &&
|
|
177
|
+
provider.callee?.type === 'Identifier' &&
|
|
178
|
+
provider.callee.name.startsWith('provide')
|
|
179
|
+
) {
|
|
180
|
+
names.push(provider.callee.name.slice('provide'.length));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return names;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderCheck(check, index) {
|
|
188
|
+
const id = check.label
|
|
189
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
190
|
+
.trim()
|
|
191
|
+
.split(/\s+/)
|
|
192
|
+
.map(pascal)
|
|
193
|
+
.join('');
|
|
194
|
+
return `\n\ntype _Check${id}${index ?? ''}DI = RouteExceptionComponentCheckedDI<\n ${check.deps},\n ${check.names || 'never'},\n ${check.values},\n '${check.label}',\n>;\ntype _CanRun${id}${index ?? ''} = CanRun<_Check${id}${index ?? ''}DI>;`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readCascadeContext(text) {
|
|
198
|
+
const match = text.match(
|
|
199
|
+
/ValidateCascadeRoutesFile<\s*([^,]+),\s*([^,]+),\s*typeof\s+\w+\s*>/m,
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
names: match?.[1]?.trim() ?? 'never',
|
|
203
|
+
values: match?.[2]?.trim() ?? 'never',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function joinNames(parent, names) {
|
|
208
|
+
return (
|
|
209
|
+
[parent === 'never' ? '' : parent, ...names.map((name) => `'${name}'`)]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.join(' | ') || 'never'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function ensureImports(text) {
|
|
216
|
+
const names = ['CanRun', 'RouteExceptionComponentCheckedDI'];
|
|
217
|
+
const match = text.match(
|
|
218
|
+
/import\s*\{([\s\S]*?)\}\s*from\s*(['"]@craft-ng\/core['"])/,
|
|
219
|
+
);
|
|
220
|
+
if (!match)
|
|
221
|
+
return `import type { ${names.join(', ')} } from '@craft-ng/core';\n${text}`;
|
|
222
|
+
const missing = names.filter(
|
|
223
|
+
(name) => !new RegExp(`\\b${name}\\b`).test(match[1]),
|
|
224
|
+
);
|
|
225
|
+
if (!missing.length) return text;
|
|
226
|
+
return text.replace(
|
|
227
|
+
match[0],
|
|
228
|
+
`import {${match[1].trimEnd()},\n type ${missing.join(',\n type ')},\n} from ${match[2]}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
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
|
+
|
|
246
|
+
function readProperty(node, name, source) {
|
|
247
|
+
const value = propertyValue(node, name);
|
|
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);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function propertyValue(node, name) {
|
|
257
|
+
if (node?.type !== 'ObjectExpression') return undefined;
|
|
258
|
+
return node.properties.find(
|
|
259
|
+
(p) => p.type === 'Property' && propertyName(p) === name,
|
|
260
|
+
)?.value;
|
|
261
|
+
}
|
|
262
|
+
function propertyName(property) {
|
|
263
|
+
return property.key?.type === 'Identifier'
|
|
264
|
+
? property.key.name
|
|
265
|
+
: (literal(property.key) ?? '');
|
|
266
|
+
}
|
|
267
|
+
function literal(node) {
|
|
268
|
+
return node?.type === 'Literal'
|
|
269
|
+
? node.value
|
|
270
|
+
: node?.type === 'StringLiteral'
|
|
271
|
+
? node.value
|
|
272
|
+
: undefined;
|
|
273
|
+
}
|
|
274
|
+
function isCall(node, name) {
|
|
275
|
+
return (
|
|
276
|
+
node?.type === 'CallExpression' &&
|
|
277
|
+
node.callee?.type === 'Identifier' &&
|
|
278
|
+
node.callee.name === name
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function pascal(value) {
|
|
282
|
+
return String(value)
|
|
283
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.map(
|
|
286
|
+
(x) =>
|
|
287
|
+
x[0].toUpperCase() +
|
|
288
|
+
(x === x.toUpperCase() ? x.slice(1).toLowerCase() : x.slice(1)),
|
|
289
|
+
)
|
|
290
|
+
.join('');
|
|
291
|
+
}
|
|
292
|
+
function routeBase(path) {
|
|
293
|
+
return (
|
|
294
|
+
String(path)
|
|
295
|
+
.split('/')
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.map((x) => pascal(x.replace(/^:/, '').replace(/\?$/, '')))
|
|
298
|
+
.join('') || 'Root'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
function walk(node, visit) {
|
|
302
|
+
if (!node || typeof node !== 'object') return;
|
|
303
|
+
visit(node);
|
|
304
|
+
for (const [key, value] of Object.entries(node)) {
|
|
305
|
+
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
306
|
+
if (Array.isArray(value)) value.forEach((child) => walk(child, visit));
|
|
307
|
+
else if (
|
|
308
|
+
value &&
|
|
309
|
+
typeof value === 'object' &&
|
|
310
|
+
typeof value.type === 'string'
|
|
311
|
+
)
|
|
312
|
+
walk(value, visit);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -5,7 +5,7 @@ const { IndentationText, Node, Project, QuoteKind, SyntaxKind } = require('ts-mo
|
|
|
5
5
|
const projectCache = new Map();
|
|
6
6
|
|
|
7
7
|
const ROUTES_FACTORY = 'craftRoutes';
|
|
8
|
-
const ROUTE_FN = '
|
|
8
|
+
const ROUTE_FN = 'craftRoute';
|
|
9
9
|
const PENDING_PROP = 'pendingComponent';
|
|
10
10
|
const VT_PROP = 'withLoaderViewTransitionImage';
|
|
11
11
|
const CHECK_TYPE = 'RouteCheckedDI';
|
|
@@ -99,7 +99,7 @@ module.exports = {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const fixedText = sourceFile.getFullText();
|
|
102
|
-
const message = `
|
|
102
|
+
const message = `craftRoute(s) with a pendingComponent must be verified with ${CHECK_TYPE}(): ${issues
|
|
103
103
|
.map((i) => i.path)
|
|
104
104
|
.join(', ')}`;
|
|
105
105
|
|
|
@@ -178,7 +178,7 @@ function collectCollections(sourceFile, filePath) {
|
|
|
178
178
|
return collections;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// A route element is either `
|
|
181
|
+
// A route element is either `craftRoute('<path>', { … })` or a `{ path: '<path>', … }`
|
|
182
182
|
// object literal. Returns the path string and the route's object literal.
|
|
183
183
|
function readRoute(element) {
|
|
184
184
|
if (Node.isCallExpression(element) && element.getExpression().getText() === ROUTE_FN) {
|
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;
|