@cratis/components.migrator 0.0.0 → 4.1.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/LICENSE +21 -0
- package/README.md +258 -1
- package/compat-manifest.json +305 -0
- package/lib/buttonVariantToneTransform.js +380 -0
- package/lib/changeHandlerTransform.js +380 -0
- package/lib/compatibility.js +164 -0
- package/lib/namespaceMap.js +87 -0
- package/lib/runTransformCli.js +137 -0
- package/lib/transform.js +355 -0
- package/package.json +33 -4
- package/scripts/button-variant-tone.js +13 -0
- package/scripts/change-handler.js +13 -0
- package/scripts/remove-root-namespace-imports.js +14 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { packageName as defaultPackageName } from './namespaceMap.js';
|
|
6
|
+
|
|
7
|
+
const TODO = 'TODO(cratis-codemod)';
|
|
8
|
+
const affectedComponents = {
|
|
9
|
+
CommandForm: {
|
|
10
|
+
InputTextField: new Set(['target.value', 'currentTarget.value']),
|
|
11
|
+
PasswordField: new Set(['target.value', 'currentTarget.value']),
|
|
12
|
+
TextAreaField: new Set(['target.value', 'currentTarget.value']),
|
|
13
|
+
ColorPickerField: new Set(['target.value', 'currentTarget.value']),
|
|
14
|
+
CheckboxField: new Set(['target.checked', 'currentTarget.checked']),
|
|
15
|
+
ToggleSwitchField: new Set(['target.checked', 'currentTarget.checked']),
|
|
16
|
+
NumberField: new Set(['target.valueAsNumber', 'currentTarget.valueAsNumber']),
|
|
17
|
+
SliderField: new Set(['target.valueAsNumber', 'currentTarget.valueAsNumber']),
|
|
18
|
+
DropdownField: new Set(['value']),
|
|
19
|
+
MultiSelectField: new Set(['value']),
|
|
20
|
+
},
|
|
21
|
+
Dropdown: {
|
|
22
|
+
Dropdown: new Set(['value']),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Rewrites structurally-proven legacy event-wrapper JSX callbacks to semantic values.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} fileName
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @param {{ packageName?: string }} [options]
|
|
32
|
+
*/
|
|
33
|
+
export function transformChangeHandlers(fileName, text, options = {}) {
|
|
34
|
+
const packageName = options.packageName ?? defaultPackageName;
|
|
35
|
+
const sourceFile = ts.createSourceFile(
|
|
36
|
+
fileName,
|
|
37
|
+
text,
|
|
38
|
+
ts.ScriptTarget.Latest,
|
|
39
|
+
true,
|
|
40
|
+
scriptKindFor(fileName),
|
|
41
|
+
);
|
|
42
|
+
const named = new Map();
|
|
43
|
+
const namespaces = new Map();
|
|
44
|
+
const diagnostics = [];
|
|
45
|
+
const edits = [];
|
|
46
|
+
|
|
47
|
+
for (const statement of sourceFile.statements) {
|
|
48
|
+
if (
|
|
49
|
+
!ts.isImportDeclaration(statement) ||
|
|
50
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
51
|
+
!statement.importClause ||
|
|
52
|
+
statement.importClause.isTypeOnly
|
|
53
|
+
)
|
|
54
|
+
continue;
|
|
55
|
+
const group = groupForSpecifier(statement.moduleSpecifier.text, packageName);
|
|
56
|
+
if (!group) continue;
|
|
57
|
+
const bindings = statement.importClause.namedBindings;
|
|
58
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
59
|
+
namespaces.set(bindings.name.text, group);
|
|
60
|
+
} else if (bindings && ts.isNamedImports(bindings)) {
|
|
61
|
+
for (const element of bindings.elements) {
|
|
62
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
63
|
+
if (
|
|
64
|
+
!ts.isTypeOnlyImportOrExportDeclaration(element) &&
|
|
65
|
+
affectedComponents[group][imported]
|
|
66
|
+
) {
|
|
67
|
+
named.set(element.name.text, { group, component: imported });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const report = (node, component, reason) => {
|
|
74
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
75
|
+
node.getStart(sourceFile),
|
|
76
|
+
);
|
|
77
|
+
diagnostics.push({
|
|
78
|
+
file: fileName,
|
|
79
|
+
line: line + 1,
|
|
80
|
+
column: character + 1,
|
|
81
|
+
message: `${component} change-handler migration requires manual review: ${reason}. The callback was left unchanged.`,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const visit = (node) => {
|
|
86
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
87
|
+
const target = resolveTarget(node.tagName, named, namespaces);
|
|
88
|
+
if (target) migrateCallbacks(node, target);
|
|
89
|
+
}
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const migrateCallbacks = (opening, target) => {
|
|
94
|
+
const allowed = affectedComponents[target.group][target.component];
|
|
95
|
+
for (const attribute of opening.attributes.properties) {
|
|
96
|
+
if (!ts.isJsxAttribute(attribute)) continue;
|
|
97
|
+
const propName = attribute.name.getText(sourceFile);
|
|
98
|
+
if (propName !== 'onChange' && propName !== 'onValueChange') continue;
|
|
99
|
+
if (
|
|
100
|
+
!attribute.initializer ||
|
|
101
|
+
!ts.isJsxExpression(attribute.initializer) ||
|
|
102
|
+
!attribute.initializer.expression
|
|
103
|
+
)
|
|
104
|
+
continue;
|
|
105
|
+
|
|
106
|
+
const callback = unwrap(attribute.initializer.expression);
|
|
107
|
+
if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))
|
|
108
|
+
continue;
|
|
109
|
+
const result = analyzeCallback(callback, allowed);
|
|
110
|
+
if (result.kind === 'already-semantic' || result.kind === 'irrelevant')
|
|
111
|
+
continue;
|
|
112
|
+
if (result.kind === 'unsupported') {
|
|
113
|
+
report(attribute, target.component, result.reason);
|
|
114
|
+
annotate(attribute);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
edits.push({
|
|
119
|
+
start: result.parameter.getStart(sourceFile),
|
|
120
|
+
end: result.parameter.getEnd(),
|
|
121
|
+
replacement: result.name,
|
|
122
|
+
});
|
|
123
|
+
edits.push({
|
|
124
|
+
start: result.argument.getStart(sourceFile),
|
|
125
|
+
end: result.argument.getEnd(),
|
|
126
|
+
replacement: result.name,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const annotate = (attribute) => {
|
|
132
|
+
if (attribute.getText(sourceFile).includes(TODO)) return;
|
|
133
|
+
const expression = attribute.initializer?.expression;
|
|
134
|
+
if (!expression) return;
|
|
135
|
+
edits.push({
|
|
136
|
+
start: expression.getStart(sourceFile),
|
|
137
|
+
end: expression.getStart(sourceFile),
|
|
138
|
+
replacement: `/* ${TODO}: review ambiguous Components change handler. */ `,
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
visit(sourceFile);
|
|
143
|
+
return applyEdits(text, edits, diagnostics);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function groupForSpecifier(specifier, packageName) {
|
|
147
|
+
if (
|
|
148
|
+
specifier === `${packageName}/CommandForm` ||
|
|
149
|
+
specifier === `${packageName}/CommandForm/fields`
|
|
150
|
+
)
|
|
151
|
+
return 'CommandForm';
|
|
152
|
+
if (specifier === `${packageName}/Dropdown`) return 'Dropdown';
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveTarget(tag, named, namespaces) {
|
|
157
|
+
if (ts.isIdentifier(tag)) return named.get(tag.text);
|
|
158
|
+
if (!ts.isPropertyAccessExpression(tag) || !ts.isIdentifier(tag.expression))
|
|
159
|
+
return undefined;
|
|
160
|
+
const group = namespaces.get(tag.expression.text);
|
|
161
|
+
if (!group || !affectedComponents[group][tag.name.text]) return undefined;
|
|
162
|
+
return { group, component: tag.name.text };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function analyzeCallback(callback, allowed) {
|
|
166
|
+
if (callback.parameters.length !== 1) {
|
|
167
|
+
return hasLegacyLookingAccess(callback, callback.parameters[0])
|
|
168
|
+
? {
|
|
169
|
+
kind: 'unsupported',
|
|
170
|
+
reason: 'the callback has multiple parameters or an unsupported parameter shape',
|
|
171
|
+
}
|
|
172
|
+
: { kind: 'irrelevant' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const parameter = callback.parameters[0];
|
|
176
|
+
if (parameter.dotDotDotToken || parameter.initializer) {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'unsupported',
|
|
179
|
+
reason: 'rest/default callback parameters are not statically safe',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const binding = bindingAccess(parameter.name);
|
|
184
|
+
if (!binding) {
|
|
185
|
+
return {
|
|
186
|
+
kind: 'unsupported',
|
|
187
|
+
reason: 'the destructured callback parameter is not a single event value field',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const call = singleCall(callback.body);
|
|
192
|
+
if (!call || call.arguments.length !== 1) {
|
|
193
|
+
return callbackUsesBinding(callback, binding)
|
|
194
|
+
? {
|
|
195
|
+
kind: 'unsupported',
|
|
196
|
+
reason: 'the callback is not a single one-argument forwarding call',
|
|
197
|
+
}
|
|
198
|
+
: { kind: 'irrelevant' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const argument = unwrap(call.arguments[0]);
|
|
202
|
+
const access = accessFromArgument(argument, binding);
|
|
203
|
+
const references = bindingReferences(callback, binding);
|
|
204
|
+
|
|
205
|
+
if (access && allowed.has(access) && references === 1) {
|
|
206
|
+
const name = containsIdentifier(call.expression, 'value') ? 'nextValue' : 'value';
|
|
207
|
+
return { kind: 'rewrite', parameter, argument: call.arguments[0], name };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (access || references > 0 || hasLegacyLookingAccess(callback, parameter)) {
|
|
211
|
+
if (
|
|
212
|
+
binding.kind === 'identifier' &&
|
|
213
|
+
references === 1 &&
|
|
214
|
+
ts.isIdentifier(argument) &&
|
|
215
|
+
argument.text === binding.localName
|
|
216
|
+
) {
|
|
217
|
+
return /^(e|event|evt|changeEvent)$/iu.test(binding.localName)
|
|
218
|
+
? {
|
|
219
|
+
kind: 'unsupported',
|
|
220
|
+
reason: 'the callback forwards an event-looking parameter without proving its semantic value',
|
|
221
|
+
}
|
|
222
|
+
: { kind: 'already-semantic' };
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
kind: 'unsupported',
|
|
226
|
+
reason:
|
|
227
|
+
access && !allowed.has(access)
|
|
228
|
+
? `it reads '${access}', which is not the legacy payload shape for this component`
|
|
229
|
+
: 'the event parameter is used more than once or the callback depends on native-event details',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { kind: 'irrelevant' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function bindingAccess(name) {
|
|
237
|
+
if (ts.isIdentifier(name)) return { kind: 'identifier', localName: name.text };
|
|
238
|
+
if (!ts.isObjectBindingPattern(name) || name.elements.length !== 1) return undefined;
|
|
239
|
+
const element = name.elements[0];
|
|
240
|
+
if (element.dotDotDotToken || element.initializer) return undefined;
|
|
241
|
+
const first = (element.propertyName ?? element.name).getText();
|
|
242
|
+
if (
|
|
243
|
+
ts.isIdentifier(element.name) &&
|
|
244
|
+
(first === 'value' || first === 'checked' || first === 'valueAsNumber')
|
|
245
|
+
) {
|
|
246
|
+
return { kind: 'destructured', access: first, localName: element.name.text };
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
(first === 'target' || first === 'currentTarget') &&
|
|
250
|
+
ts.isObjectBindingPattern(element.name) &&
|
|
251
|
+
element.name.elements.length === 1
|
|
252
|
+
) {
|
|
253
|
+
const nested = element.name.elements[0];
|
|
254
|
+
if (nested.dotDotDotToken || nested.initializer || !ts.isIdentifier(nested.name))
|
|
255
|
+
return undefined;
|
|
256
|
+
const property = (nested.propertyName ?? nested.name).getText();
|
|
257
|
+
if (
|
|
258
|
+
property === 'value' ||
|
|
259
|
+
property === 'checked' ||
|
|
260
|
+
property === 'valueAsNumber'
|
|
261
|
+
) {
|
|
262
|
+
return {
|
|
263
|
+
kind: 'destructured',
|
|
264
|
+
access: `${first}.${property}`,
|
|
265
|
+
localName: nested.name.text,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function singleCall(body) {
|
|
273
|
+
if (!ts.isBlock(body)) {
|
|
274
|
+
const expression = unwrap(body);
|
|
275
|
+
return ts.isCallExpression(expression) ? expression : undefined;
|
|
276
|
+
}
|
|
277
|
+
if (body.statements.length !== 1) return undefined;
|
|
278
|
+
const statement = body.statements[0];
|
|
279
|
+
if (ts.isExpressionStatement(statement)) {
|
|
280
|
+
const expression = unwrap(statement.expression);
|
|
281
|
+
return ts.isCallExpression(expression) ? expression : undefined;
|
|
282
|
+
}
|
|
283
|
+
if (ts.isReturnStatement(statement) && statement.expression) {
|
|
284
|
+
const expression = unwrap(statement.expression);
|
|
285
|
+
return ts.isCallExpression(expression) ? expression : undefined;
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function accessFromArgument(argument, binding) {
|
|
291
|
+
if (binding.kind === 'destructured') {
|
|
292
|
+
return ts.isIdentifier(argument) && argument.text === binding.localName
|
|
293
|
+
? binding.access
|
|
294
|
+
: undefined;
|
|
295
|
+
}
|
|
296
|
+
const parts = [];
|
|
297
|
+
let current = argument;
|
|
298
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
299
|
+
parts.unshift(current.name.text);
|
|
300
|
+
current = unwrap(current.expression);
|
|
301
|
+
}
|
|
302
|
+
return ts.isIdentifier(current) && current.text === binding.localName
|
|
303
|
+
? parts.join('.')
|
|
304
|
+
: undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function bindingReferences(callback, binding) {
|
|
308
|
+
let count = 0;
|
|
309
|
+
const visit = (node) => {
|
|
310
|
+
if (node === callback.parameters[0]) return;
|
|
311
|
+
if (ts.isIdentifier(node) && node.text === binding.localName) count += 1;
|
|
312
|
+
ts.forEachChild(node, visit);
|
|
313
|
+
};
|
|
314
|
+
ts.forEachChild(callback.body, visit);
|
|
315
|
+
return count;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function callbackUsesBinding(callback, binding) {
|
|
319
|
+
return bindingReferences(callback, binding) > 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function hasLegacyLookingAccess(callback, parameter) {
|
|
323
|
+
if (!parameter || !ts.isIdentifier(parameter.name)) return false;
|
|
324
|
+
const root = parameter.name.text;
|
|
325
|
+
let found = false;
|
|
326
|
+
const visit = (node) => {
|
|
327
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
328
|
+
let current = node;
|
|
329
|
+
while (ts.isPropertyAccessExpression(current))
|
|
330
|
+
current = unwrap(current.expression);
|
|
331
|
+
if (ts.isIdentifier(current) && current.text === root) found = true;
|
|
332
|
+
}
|
|
333
|
+
if (!found) ts.forEachChild(node, visit);
|
|
334
|
+
};
|
|
335
|
+
ts.forEachChild(callback.body, visit);
|
|
336
|
+
return found;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function containsIdentifier(node, name) {
|
|
340
|
+
let found = false;
|
|
341
|
+
const visit = (child) => {
|
|
342
|
+
if (ts.isIdentifier(child) && child.text === name) found = true;
|
|
343
|
+
if (!found) ts.forEachChild(child, visit);
|
|
344
|
+
};
|
|
345
|
+
visit(node);
|
|
346
|
+
return found;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function unwrap(node) {
|
|
350
|
+
while (
|
|
351
|
+
ts.isParenthesizedExpression(node) ||
|
|
352
|
+
ts.isAsExpression(node) ||
|
|
353
|
+
ts.isTypeAssertionExpression(node)
|
|
354
|
+
)
|
|
355
|
+
node = node.expression;
|
|
356
|
+
return node;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function applyEdits(text, edits, diagnostics) {
|
|
360
|
+
if (edits.length === 0) return { text, changed: false, diagnostics };
|
|
361
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
362
|
+
let output = text;
|
|
363
|
+
for (let index = edits.length - 1; index >= 0; index--) {
|
|
364
|
+
const edit = edits[index];
|
|
365
|
+
output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
|
|
366
|
+
}
|
|
367
|
+
return { text: output, changed: output !== text, diagnostics };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function scriptKindFor(fileName) {
|
|
371
|
+
if (fileName.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
372
|
+
if (fileName.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
373
|
+
if (
|
|
374
|
+
fileName.endsWith('.js') ||
|
|
375
|
+
fileName.endsWith('.mjs') ||
|
|
376
|
+
fileName.endsWith('.cjs')
|
|
377
|
+
)
|
|
378
|
+
return ts.ScriptKind.JS;
|
|
379
|
+
return ts.ScriptKind.TS;
|
|
380
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import semver from 'semver';
|
|
9
|
+
|
|
10
|
+
const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const bundledManifestPath = path.join(packageDirectory, 'compat-manifest.json');
|
|
12
|
+
const migratorPackagePath = path.join(packageDirectory, 'package.json');
|
|
13
|
+
|
|
14
|
+
/** Validates the bundled release contract and the installed Components migration boundary. */
|
|
15
|
+
export function preflightCompatibility({
|
|
16
|
+
cwd = process.cwd(),
|
|
17
|
+
packageName = '@cratis/components',
|
|
18
|
+
} = {}) {
|
|
19
|
+
const manifest = readJson(bundledManifestPath, 'bundled compatibility manifest');
|
|
20
|
+
const migratorPackage = readJson(migratorPackagePath, 'migrator package manifest');
|
|
21
|
+
validateBundledManifest(manifest, migratorPackage.version);
|
|
22
|
+
|
|
23
|
+
const installedPackagePath = resolvePackageManifest(packageName, cwd);
|
|
24
|
+
const installedPackage = readJson(
|
|
25
|
+
installedPackagePath,
|
|
26
|
+
`installed ${packageName} package manifest`,
|
|
27
|
+
);
|
|
28
|
+
if (installedPackage.name !== packageName) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Resolved ${packageName} package manifest declares unexpected name '${installedPackage.name ?? '<missing>'}'.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (semver.valid(installedPackage.version) !== installedPackage.version) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Installed ${packageName} has invalid version '${installedPackage.version ?? '<missing>'}'.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const supportedWindow = Object.values(manifest.supportWindows).find(
|
|
40
|
+
(window) =>
|
|
41
|
+
['source', 'target'].includes(window.migrationRole) &&
|
|
42
|
+
semver.satisfies(installedPackage.version, window.components),
|
|
43
|
+
);
|
|
44
|
+
if (!supportedWindow) {
|
|
45
|
+
const ranges = Object.values(manifest.supportWindows)
|
|
46
|
+
.filter((window) => ['source', 'target'].includes(window.migrationRole))
|
|
47
|
+
.map((window) => window.components)
|
|
48
|
+
.join(' or ');
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Installed ${packageName}@${installedPackage.version} is unsupported by this codemod. ` +
|
|
51
|
+
`Allowed migration Components ranges: ${ranges}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const windowMigratorRange =
|
|
55
|
+
typeof supportedWindow.tooling === 'string'
|
|
56
|
+
? supportedWindow.tooling
|
|
57
|
+
: supportedWindow.tooling?.migrator;
|
|
58
|
+
if (
|
|
59
|
+
!semver.validRange(windowMigratorRange) ||
|
|
60
|
+
!semver.satisfies(migratorPackage.version, windowMigratorRange)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Migrator ${migratorPackage.version} is incompatible with the ${supportedWindow.migrationRole} support window tooling range '${windowMigratorRange ?? '<missing>'}'.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
manifest,
|
|
69
|
+
migratorVersion: migratorPackage.version,
|
|
70
|
+
componentsVersion: installedPackage.version,
|
|
71
|
+
migrationRole: supportedWindow.migrationRole,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Validates data that must remain true for the packed migrator to run safely. */
|
|
76
|
+
export function validateBundledManifest(manifest, migratorVersion) {
|
|
77
|
+
if (!manifest || manifest.schemaVersion !== 2) {
|
|
78
|
+
throw new Error('Bundled compatibility manifest has an unsupported schema.');
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
!['source-candidate', 'publication-authorized'].includes(manifest.releaseStatus)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('Bundled compatibility manifest has an invalid releaseStatus.');
|
|
84
|
+
}
|
|
85
|
+
const migratorRange = manifest.toolingCompatibility?.migrator;
|
|
86
|
+
if (!semver.validRange(migratorRange)) {
|
|
87
|
+
throw new Error('Bundled compatibility manifest has an invalid migrator range.');
|
|
88
|
+
}
|
|
89
|
+
if (semver.valid(migratorVersion) !== migratorVersion) {
|
|
90
|
+
throw new Error(`Migrator package has invalid version '${migratorVersion}'.`);
|
|
91
|
+
}
|
|
92
|
+
if (!semver.satisfies(migratorVersion, migratorRange)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Migrator package version ${migratorVersion} is outside bundled range '${migratorRange}'.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const packageEntry = manifest.packages?.find(
|
|
99
|
+
({ name }) => name === '@cratis/components.migrator',
|
|
100
|
+
);
|
|
101
|
+
if (
|
|
102
|
+
packageEntry?.version !== migratorVersion ||
|
|
103
|
+
packageEntry?.independentRelease !== false ||
|
|
104
|
+
!semver.satisfies(migratorVersion, packageEntry?.releaseMajorRange ?? '')
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Bundled compatibility manifest has stale migrator package metadata.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const windows = Object.values(manifest.supportWindows ?? {});
|
|
112
|
+
const sourceWindows = windows.filter(
|
|
113
|
+
({ migrationRole }) => migrationRole === 'source',
|
|
114
|
+
);
|
|
115
|
+
const targetWindows = windows.filter(
|
|
116
|
+
({ migrationRole }) => migrationRole === 'target',
|
|
117
|
+
);
|
|
118
|
+
if (
|
|
119
|
+
windows.length !== 2 ||
|
|
120
|
+
sourceWindows.length !== 1 ||
|
|
121
|
+
targetWindows.length !== 1 ||
|
|
122
|
+
sourceWindows[0].components !== '>=3 <4' ||
|
|
123
|
+
sourceWindows[0].migrationTarget !== '>=4 <5' ||
|
|
124
|
+
targetWindows[0].components !== '>=4 <5' ||
|
|
125
|
+
manifest.toolingCompatibility?.componentsCore !== '>=4 <5' ||
|
|
126
|
+
windows.some((window) => {
|
|
127
|
+
const toolingRange =
|
|
128
|
+
typeof window.tooling === 'string'
|
|
129
|
+
? window.tooling
|
|
130
|
+
: window.tooling?.migrator;
|
|
131
|
+
return (
|
|
132
|
+
!semver.validRange(window.components) || !semver.validRange(toolingRange)
|
|
133
|
+
);
|
|
134
|
+
})
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Bundled compatibility manifest has invalid migration support windows.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolvePackageManifest(packageName, cwd) {
|
|
143
|
+
try {
|
|
144
|
+
const requireFromCwd = createRequire(
|
|
145
|
+
path.join(path.resolve(cwd), 'package.json'),
|
|
146
|
+
);
|
|
147
|
+
return requireFromCwd.resolve(`${packageName}/package.json`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Could not resolve installed ${packageName} from '${path.resolve(cwd)}'. ` +
|
|
152
|
+
`Install a supported Components 3 or Components 4 package before running the codemod. ${detail}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readJson(filePath, description) {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
162
|
+
throw new Error(`Could not read ${description}: ${detail}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single source of truth for the root-namespace-removal migration: every component
|
|
6
|
+
* namespace removed from the Components 3 package root, and the small setup surface
|
|
7
|
+
* that remains supported at the Components 4 root.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `Source/index.ts` and the `exports` map in `Source/package.json`. When a
|
|
10
|
+
* namespace subpath is added, renamed, or removed there, update `namespaceSubpaths`
|
|
11
|
+
* here (and the matching copy in `ESLint/lib/rootNamespaceMap.js`) in the same change.
|
|
12
|
+
* Neither file imports the other: this package and `@cratis/eslint-plugin-components`
|
|
13
|
+
* ship and publish independently, so the map is intentionally duplicated rather than
|
|
14
|
+
* shared across a workspace boundary.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The package this migration applies to by default. */
|
|
18
|
+
export const packageName = '@cratis/components';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every retained namespace removed from the Components 3 root, mapped to the Components 4
|
|
22
|
+
* subpath it becomes: `import { Canvas } from '@cratis/components'` migrates to
|
|
23
|
+
* `import * as Canvas from '@cratis/components/Canvas'`. `Types` is the one case where
|
|
24
|
+
* the namespace name and its subpath differ in casing (`./types`, lowercase).
|
|
25
|
+
*/
|
|
26
|
+
export const namespaceSubpaths = {
|
|
27
|
+
Canvas: 'Canvas',
|
|
28
|
+
Chat: 'Chat',
|
|
29
|
+
CommandDialog: 'CommandDialog',
|
|
30
|
+
// The former root CommandStepper namespace aliased the entire CommandDialog module.
|
|
31
|
+
// Preserve that module identity; the narrower ./CommandStepper subpath does not.
|
|
32
|
+
CommandStepper: 'CommandDialog',
|
|
33
|
+
CommandForm: 'CommandForm',
|
|
34
|
+
Common: 'Common',
|
|
35
|
+
DataPage: 'DataPage',
|
|
36
|
+
DataTables: 'DataTables',
|
|
37
|
+
Dialogs: 'Dialogs',
|
|
38
|
+
Display: 'Display',
|
|
39
|
+
Dropdown: 'Dropdown',
|
|
40
|
+
Filter: 'Filter',
|
|
41
|
+
Notifications: 'Notifications',
|
|
42
|
+
ObjectContentEditor: 'ObjectContentEditor',
|
|
43
|
+
ObjectNavigationalBar: 'ObjectNavigationalBar',
|
|
44
|
+
PivotViewer: 'PivotViewer',
|
|
45
|
+
SchemaEditor: 'SchemaEditor',
|
|
46
|
+
TimeMachine: 'TimeMachine',
|
|
47
|
+
Toolbar: 'Toolbar',
|
|
48
|
+
Types: 'types',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Components 3 root exports that have no Components 4 root or subpath equivalent.
|
|
53
|
+
* These are known migration boundaries rather than unknown symbols: transforms must
|
|
54
|
+
* refuse them with actionable renderer-removal guidance instead of suggesting that the
|
|
55
|
+
* namespace map is incomplete.
|
|
56
|
+
*/
|
|
57
|
+
export const removedRootSymbols = new Set([
|
|
58
|
+
'Compatibility',
|
|
59
|
+
'assertPrimeReact11PassThroughCompatibility',
|
|
60
|
+
'components3PrimeReact11PassThroughContract',
|
|
61
|
+
'primeReact11PassThroughSentinelAttribute',
|
|
62
|
+
'primeReact11PassThroughSentinelPreset',
|
|
63
|
+
'PrimeReact11PassThroughComponent',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The primary setup surface Source/index.ts re-exports directly from the root so a
|
|
68
|
+
* consumer that only wants provider setup does not need to know about the `Common`
|
|
69
|
+
* subpath. These names are never rewritten to a namespace import.
|
|
70
|
+
*/
|
|
71
|
+
export const approvedRootSymbols = new Set([
|
|
72
|
+
'CratisComponentsProvider',
|
|
73
|
+
'useCratisComponentsConfig',
|
|
74
|
+
'cratisDefaults',
|
|
75
|
+
'mergeCratisComponentsConfig',
|
|
76
|
+
'CratisComponentsConfig',
|
|
77
|
+
'CratisComponentsProviderProps',
|
|
78
|
+
'CratisComponentsMessages',
|
|
79
|
+
'CratisPaginatorMessages',
|
|
80
|
+
'CratisDatePickerMessages',
|
|
81
|
+
'CratisDropdownMessages',
|
|
82
|
+
'CratisDialogMessages',
|
|
83
|
+
'CratisStepperMessages',
|
|
84
|
+
'CratisNotificationsMessages',
|
|
85
|
+
'CratisDataTableMessages',
|
|
86
|
+
'CratisColumnFilterMessages',
|
|
87
|
+
]);
|