@craft-ng/dev-tools 0.5.0-beta.4 → 0.5.0-beta.5
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/package.json +1 -1
- package/src/eslint-rules/global-exception-registry-match.cjs +4 -4
- package/src/eslint-rules/index.cjs +4 -0
- package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +258 -0
- package/src/eslint-rules/require-pending-component-di-check.cjs +3 -3
package/package.json
CHANGED
|
@@ -176,7 +176,7 @@ function getCallExpressionName(callExpression) {
|
|
|
176
176
|
if (Node.isIdentifier(expression)) {
|
|
177
177
|
return expression.getText();
|
|
178
178
|
}
|
|
179
|
-
// `
|
|
179
|
+
// `craftRoute(...).withProviders` etc. — the leftmost call's callee.
|
|
180
180
|
if (Node.isPropertyAccessExpression(expression)) {
|
|
181
181
|
return expression.getName();
|
|
182
182
|
}
|
|
@@ -208,7 +208,7 @@ function resolveCollectionVar(craftRoutesCall) {
|
|
|
208
208
|
return undefined;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// A route array element is either `
|
|
211
|
+
// A route array element is either `craftRoute('path', { … }, { … })` (optionally
|
|
212
212
|
// followed by `.withProviders(…)`) or a plain `{ path: '…', handleExceptions: { … } }`.
|
|
213
213
|
function extractRouteDefinition(element) {
|
|
214
214
|
if (Node.isObjectLiteralExpression(element)) {
|
|
@@ -245,11 +245,11 @@ function extractRouteDefinition(element) {
|
|
|
245
245
|
return undefined;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// Walks `
|
|
248
|
+
// Walks `craftRoute(...).withProviders(...)` (any depth) down to the `craftRoute(` call.
|
|
249
249
|
function findRouteCall(node) {
|
|
250
250
|
let current = node;
|
|
251
251
|
while (Node.isCallExpression(current)) {
|
|
252
|
-
if (getCallExpressionName(current) === '
|
|
252
|
+
if (getCallExpressionName(current) === 'craftRoute') {
|
|
253
253
|
return current;
|
|
254
254
|
}
|
|
255
255
|
const expression = current.getExpression();
|
|
@@ -19,6 +19,8 @@ const requireTrackOnDependentPrimitives = require('./require-track-on-dependent-
|
|
|
19
19
|
const requireAssertExhaustiveRouteExceptions = require('./require-assert-exhaustive-route-exceptions.cjs');
|
|
20
20
|
const preferCraftRouterOutlet = require('./prefer-craft-router-outlet.cjs');
|
|
21
21
|
const requirePendingComponentDiCheck = require('./require-pending-component-di-check.cjs');
|
|
22
|
+
const requireCraftExceptionHandler = require('./require-craft-exception-handler.cjs');
|
|
23
|
+
const requireExceptionComponentDiCheck = require('./require-exception-component-di-check.cjs');
|
|
22
24
|
const requireChildRouteMountCheck = require('./require-child-route-mount-check.cjs');
|
|
23
25
|
|
|
24
26
|
module.exports = {
|
|
@@ -45,6 +47,8 @@ module.exports = {
|
|
|
45
47
|
requireAssertExhaustiveRouteExceptions,
|
|
46
48
|
'prefer-craft-router-outlet': preferCraftRouterOutlet,
|
|
47
49
|
'require-pending-component-di-check': requirePendingComponentDiCheck,
|
|
50
|
+
'require-craft-exception-handler': requireCraftExceptionHandler,
|
|
51
|
+
'require-exception-component-di-check': requireExceptionComponentDiCheck,
|
|
48
52
|
'require-child-route-mount-check': requireChildRouteMountCheck,
|
|
49
53
|
},
|
|
50
54
|
};
|
|
@@ -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,258 @@
|
|
|
1
|
+
/** Generates O(1) DI checks for renderComponent/errorComponent/withErrorComponent. */
|
|
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 (!/renderComponent|errorComponent|withErrorComponent/.test(text))
|
|
10
|
+
return;
|
|
11
|
+
|
|
12
|
+
const cascade = readCascadeContext(text);
|
|
13
|
+
const checks = [];
|
|
14
|
+
walk(program, (node) => {
|
|
15
|
+
if (isCall(node, 'craftRoutes')) {
|
|
16
|
+
collectRouteChecks(node, source, cascade, checks);
|
|
17
|
+
} else if (isCall(node, 'withErrorComponent')) {
|
|
18
|
+
const descriptor = node.arguments[0];
|
|
19
|
+
const deps = readProperty(descriptor, 'componentDeps', source);
|
|
20
|
+
if (deps) {
|
|
21
|
+
checks.push({
|
|
22
|
+
deps,
|
|
23
|
+
names: joinNames(cascade.names, ['CraftGlobalError']),
|
|
24
|
+
values: cascade.values,
|
|
25
|
+
label: 'global error component',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const missing = checks.filter(
|
|
32
|
+
(check) =>
|
|
33
|
+
!text.includes(
|
|
34
|
+
`RouteExceptionComponentCheckedDI<\n ${check.deps},`,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
if (missing.length === 0) return;
|
|
38
|
+
|
|
39
|
+
let fixed = ensureImports(text);
|
|
40
|
+
fixed += missing.map(renderCheck).join('');
|
|
41
|
+
context.report({
|
|
42
|
+
node: program,
|
|
43
|
+
message: `${missing.length} exception component(s) must be checked with RouteExceptionComponentCheckedDI.`,
|
|
44
|
+
fix: (fixer) => fixer.replaceTextRange([0, text.length], fixed),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function collectRouteChecks(call, source, cascade, checks) {
|
|
52
|
+
const collection = literal(call.arguments[0]);
|
|
53
|
+
const array = call.arguments[1];
|
|
54
|
+
if (!collection || array?.type !== 'ArrayExpression') return;
|
|
55
|
+
|
|
56
|
+
for (const entry of array.elements) {
|
|
57
|
+
const routeCall = unwrapRouteCall(entry);
|
|
58
|
+
const def = routeCall ? routeCall.arguments[1] : entry;
|
|
59
|
+
const path = routeCall
|
|
60
|
+
? literal(routeCall.arguments[0])
|
|
61
|
+
: literal(propertyValue(def, 'path'));
|
|
62
|
+
if (path === undefined || def?.type !== 'ObjectExpression') continue;
|
|
63
|
+
const baseNames = routeNames(collection, path, def);
|
|
64
|
+
if (routeCall && routeCall !== entry) {
|
|
65
|
+
walk(entry.arguments[0], (node) => {
|
|
66
|
+
if (
|
|
67
|
+
node?.type === 'CallExpression' &&
|
|
68
|
+
node.callee?.type === 'Identifier' &&
|
|
69
|
+
node.callee.name.startsWith('provide')
|
|
70
|
+
) {
|
|
71
|
+
baseNames.push(node.callee.name.slice('provide'.length));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const routeError = propertyValue(def, 'errorComponent');
|
|
77
|
+
const routeDeps = readProperty(routeError, 'componentDeps', source);
|
|
78
|
+
if (routeDeps) {
|
|
79
|
+
checks.push({
|
|
80
|
+
deps: routeDeps,
|
|
81
|
+
names: joinNames(cascade.names, [...baseNames, 'CraftGlobalError']),
|
|
82
|
+
values: cascade.values,
|
|
83
|
+
label: `error component: ${path}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handlers = routeCall
|
|
88
|
+
? routeCall.arguments[2]
|
|
89
|
+
: propertyValue(def, 'handleExceptions');
|
|
90
|
+
if (handlers?.type !== 'ObjectExpression') continue;
|
|
91
|
+
for (const handler of handlers.properties) {
|
|
92
|
+
if (handler.type !== 'Property') continue;
|
|
93
|
+
const code = propertyName(handler);
|
|
94
|
+
walk(handler.value, (node) => {
|
|
95
|
+
if (!isCall(node, 'renderComponent')) return;
|
|
96
|
+
const deps = readProperty(node.arguments[0], 'componentDeps', source);
|
|
97
|
+
if (!deps) return;
|
|
98
|
+
const helper = `${pascal(collection)}${routeBase(path)}${pascal(code)}Exception`;
|
|
99
|
+
checks.push({
|
|
100
|
+
deps,
|
|
101
|
+
names: joinNames(cascade.names, [...baseNames, helper]),
|
|
102
|
+
values: cascade.values,
|
|
103
|
+
label: `render component: ${path}#${code}`,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function unwrapRouteCall(entry) {
|
|
111
|
+
if (isCall(entry, 'craftRoute')) return entry;
|
|
112
|
+
if (
|
|
113
|
+
entry?.type === 'CallExpression' &&
|
|
114
|
+
entry.callee?.type === 'MemberExpression' &&
|
|
115
|
+
entry.callee.property?.type === 'Identifier' &&
|
|
116
|
+
entry.callee.property.name === 'withProviders' &&
|
|
117
|
+
isCall(entry.callee.object, 'craftRoute')
|
|
118
|
+
) {
|
|
119
|
+
return entry.callee.object;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function routeNames(collection, path, def) {
|
|
125
|
+
const prefix = pascal(collection);
|
|
126
|
+
const base = routeBase(path);
|
|
127
|
+
const names = path
|
|
128
|
+
.split('/')
|
|
129
|
+
.filter((x) => x.startsWith(':'))
|
|
130
|
+
.map((x) => `${prefix}${pascal(x.slice(1).replace(/\?$/, ''))}Params`);
|
|
131
|
+
if (propertyValue(def, 'data')) names.push(`${prefix}${base}Data`);
|
|
132
|
+
if (propertyValue(def, 'queryParams'))
|
|
133
|
+
names.push(`${prefix}${base}QueryParams`);
|
|
134
|
+
if (propertyValue(def, 'withLoaderViewTransitionImage'))
|
|
135
|
+
names.push(`${prefix}${base}ViewTransition`);
|
|
136
|
+
const providers = propertyValue(def, 'providers');
|
|
137
|
+
if (providers?.type === 'ArrayExpression') {
|
|
138
|
+
for (const provider of providers.elements) {
|
|
139
|
+
if (
|
|
140
|
+
provider?.type === 'CallExpression' &&
|
|
141
|
+
provider.callee?.type === 'Identifier' &&
|
|
142
|
+
provider.callee.name.startsWith('provide')
|
|
143
|
+
) {
|
|
144
|
+
names.push(provider.callee.name.slice('provide'.length));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return names;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderCheck(check, index) {
|
|
152
|
+
const id = check.label
|
|
153
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
154
|
+
.trim()
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.map(pascal)
|
|
157
|
+
.join('');
|
|
158
|
+
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>;`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readCascadeContext(text) {
|
|
162
|
+
const match = text.match(
|
|
163
|
+
/ValidateCascadeRoutesFile<\s*([^,]+),\s*([^,]+),\s*typeof\s+\w+\s*>/m,
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
names: match?.[1]?.trim() ?? 'never',
|
|
167
|
+
values: match?.[2]?.trim() ?? 'never',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function joinNames(parent, names) {
|
|
172
|
+
return (
|
|
173
|
+
[parent === 'never' ? '' : parent, ...names.map((name) => `'${name}'`)]
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join(' | ') || 'never'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ensureImports(text) {
|
|
180
|
+
const names = ['CanRun', 'RouteExceptionComponentCheckedDI'];
|
|
181
|
+
const match = text.match(
|
|
182
|
+
/import\s*\{([\s\S]*?)\}\s*from\s*(['"]@craft-ng\/core['"])/,
|
|
183
|
+
);
|
|
184
|
+
if (!match)
|
|
185
|
+
return `import type { ${names.join(', ')} } from '@craft-ng/core';\n${text}`;
|
|
186
|
+
const missing = names.filter(
|
|
187
|
+
(name) => !new RegExp(`\\b${name}\\b`).test(match[1]),
|
|
188
|
+
);
|
|
189
|
+
if (!missing.length) return text;
|
|
190
|
+
return text.replace(
|
|
191
|
+
match[0],
|
|
192
|
+
`import {${match[1].trimEnd()},\n type ${missing.join(',\n type ')},\n} from ${match[2]}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readProperty(node, name, source) {
|
|
197
|
+
const value = propertyValue(node, name);
|
|
198
|
+
return value ? source.getText(value) : undefined;
|
|
199
|
+
}
|
|
200
|
+
function propertyValue(node, name) {
|
|
201
|
+
if (node?.type !== 'ObjectExpression') return undefined;
|
|
202
|
+
return node.properties.find(
|
|
203
|
+
(p) => p.type === 'Property' && propertyName(p) === name,
|
|
204
|
+
)?.value;
|
|
205
|
+
}
|
|
206
|
+
function propertyName(property) {
|
|
207
|
+
return property.key?.type === 'Identifier'
|
|
208
|
+
? property.key.name
|
|
209
|
+
: (literal(property.key) ?? '');
|
|
210
|
+
}
|
|
211
|
+
function literal(node) {
|
|
212
|
+
return node?.type === 'Literal'
|
|
213
|
+
? node.value
|
|
214
|
+
: node?.type === 'StringLiteral'
|
|
215
|
+
? node.value
|
|
216
|
+
: undefined;
|
|
217
|
+
}
|
|
218
|
+
function isCall(node, name) {
|
|
219
|
+
return (
|
|
220
|
+
node?.type === 'CallExpression' &&
|
|
221
|
+
node.callee?.type === 'Identifier' &&
|
|
222
|
+
node.callee.name === name
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
function pascal(value) {
|
|
226
|
+
return String(value)
|
|
227
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.map(
|
|
230
|
+
(x) =>
|
|
231
|
+
x[0].toUpperCase() +
|
|
232
|
+
(x === x.toUpperCase() ? x.slice(1).toLowerCase() : x.slice(1)),
|
|
233
|
+
)
|
|
234
|
+
.join('');
|
|
235
|
+
}
|
|
236
|
+
function routeBase(path) {
|
|
237
|
+
return (
|
|
238
|
+
String(path)
|
|
239
|
+
.split('/')
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.map((x) => pascal(x.replace(/^:/, '').replace(/\?$/, '')))
|
|
242
|
+
.join('') || 'Root'
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function walk(node, visit) {
|
|
246
|
+
if (!node || typeof node !== 'object') return;
|
|
247
|
+
visit(node);
|
|
248
|
+
for (const [key, value] of Object.entries(node)) {
|
|
249
|
+
if (key === 'parent' || key === 'range' || key === 'loc') continue;
|
|
250
|
+
if (Array.isArray(value)) value.forEach((child) => walk(child, visit));
|
|
251
|
+
else if (
|
|
252
|
+
value &&
|
|
253
|
+
typeof value === 'object' &&
|
|
254
|
+
typeof value.type === 'string'
|
|
255
|
+
)
|
|
256
|
+
walk(value, visit);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -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) {
|