@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,137 @@
|
|
|
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 { readFileSync, writeFileSync, statSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { join, extname } from 'node:path';
|
|
6
|
+
import { packageName as defaultPackageName } from './namespaceMap.js';
|
|
7
|
+
import { preflightCompatibility } from './compatibility.js';
|
|
8
|
+
|
|
9
|
+
const EXTENSIONS = new Set([
|
|
10
|
+
'.js',
|
|
11
|
+
'.jsx',
|
|
12
|
+
'.mjs',
|
|
13
|
+
'.cjs',
|
|
14
|
+
'.ts',
|
|
15
|
+
'.tsx',
|
|
16
|
+
'.mts',
|
|
17
|
+
'.cts',
|
|
18
|
+
]);
|
|
19
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.yarn']);
|
|
20
|
+
|
|
21
|
+
/** Runs one independently invocable Components codemod CLI. */
|
|
22
|
+
export function runTransformCli(argv, { command, description, transform }) {
|
|
23
|
+
const args = [...argv];
|
|
24
|
+
let check = false;
|
|
25
|
+
let packageName = defaultPackageName;
|
|
26
|
+
const paths = [];
|
|
27
|
+
|
|
28
|
+
while (args.length > 0) {
|
|
29
|
+
const arg = args.shift();
|
|
30
|
+
if (arg === '--check') check = true;
|
|
31
|
+
else if (arg === '--package') {
|
|
32
|
+
const configuredPackage = args.shift();
|
|
33
|
+
if (!configuredPackage) {
|
|
34
|
+
console.error('--package requires a package name.');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
packageName = configuredPackage;
|
|
38
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
39
|
+
printUsage(command, description);
|
|
40
|
+
return 0;
|
|
41
|
+
} else if (arg.startsWith('-')) {
|
|
42
|
+
console.error(`Unknown option: ${arg}`);
|
|
43
|
+
return 1;
|
|
44
|
+
} else paths.push(arg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (paths.length === 0) {
|
|
48
|
+
printUsage(command, description);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
preflightCompatibility({ packageName });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(
|
|
56
|
+
`Compatibility preflight failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
57
|
+
);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let files;
|
|
62
|
+
try {
|
|
63
|
+
files = paths.flatMap(collectFiles);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let changedCount = 0;
|
|
70
|
+
let diagnosticCount = 0;
|
|
71
|
+
let errorCount = 0;
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
try {
|
|
74
|
+
const original = readFileSync(file, 'utf8');
|
|
75
|
+
const result = transform(file, original, { packageName });
|
|
76
|
+
for (const diagnostic of result.diagnostics) {
|
|
77
|
+
diagnosticCount += 1;
|
|
78
|
+
console.warn(
|
|
79
|
+
`${diagnostic.file}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (result.changed) {
|
|
83
|
+
changedCount += 1;
|
|
84
|
+
if (check) console.log(`would rewrite ${file}`);
|
|
85
|
+
else {
|
|
86
|
+
writeFileSync(file, result.text, 'utf8');
|
|
87
|
+
console.log(`rewrote ${file}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
errorCount += 1;
|
|
92
|
+
console.error(
|
|
93
|
+
`${file}: failed to process: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`\n${files.length} file(s) scanned, ${changedCount} ${check ? 'would change' : 'changed'}, ${diagnosticCount} unsupported or ambiguous case(s) reported, ${errorCount} processing error(s).`,
|
|
100
|
+
);
|
|
101
|
+
if (diagnosticCount > 0 || errorCount > 0 || (check && changedCount > 0)) return 1;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printUsage(command, description) {
|
|
106
|
+
console.log(
|
|
107
|
+
[
|
|
108
|
+
`Usage: ${command} [--check] [--package <name>] <path...>`,
|
|
109
|
+
'',
|
|
110
|
+
description,
|
|
111
|
+
'',
|
|
112
|
+
'Options:',
|
|
113
|
+
' --check Report what would change without writing files; exits 1 if',
|
|
114
|
+
' anything would change or manual review is required.',
|
|
115
|
+
' --package <name> Components package name (default: @cratis/components).',
|
|
116
|
+
'',
|
|
117
|
+
'<path...> may be files or directories; directories are walked recursively for',
|
|
118
|
+
'.js/.jsx/.mjs/.cjs/.ts/.tsx/.mts/.cts files, skipping node_modules,',
|
|
119
|
+
'dist, .git, and .yarn.',
|
|
120
|
+
].join('\n'),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectFiles(inputPath) {
|
|
125
|
+
const stats = statSync(inputPath);
|
|
126
|
+
if (stats.isFile()) return EXTENSIONS.has(extname(inputPath)) ? [inputPath] : [];
|
|
127
|
+
if (!stats.isDirectory()) return [];
|
|
128
|
+
const files = [];
|
|
129
|
+
for (const entry of readdirSync(inputPath, { withFileTypes: true })) {
|
|
130
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
131
|
+
const entryPath = join(inputPath, entry.name);
|
|
132
|
+
if (entry.isDirectory()) files.push(...collectFiles(entryPath));
|
|
133
|
+
else if (entry.isFile() && EXTENSIONS.has(extname(entry.name)))
|
|
134
|
+
files.push(entryPath);
|
|
135
|
+
}
|
|
136
|
+
return files;
|
|
137
|
+
}
|
package/lib/transform.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
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 {
|
|
6
|
+
packageName as defaultPackageName,
|
|
7
|
+
namespaceSubpaths,
|
|
8
|
+
approvedRootSymbols,
|
|
9
|
+
removedRootSymbols,
|
|
10
|
+
} from './namespaceMap.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rewrites static imports of the `@cratis/components` root barrel into subpath
|
|
14
|
+
* namespace imports, using the TypeScript compiler API for exact, position-based text
|
|
15
|
+
* edits rather than a full reprint — every untouched line of the file, including
|
|
16
|
+
* comments and formatting, is left byte-identical.
|
|
17
|
+
*
|
|
18
|
+
* What it does:
|
|
19
|
+
* - `import { Canvas } from '@cratis/components'` becomes
|
|
20
|
+
* `import * as Canvas from '@cratis/components/Canvas'`.
|
|
21
|
+
* - `import type { Canvas } from '@cratis/components'` becomes
|
|
22
|
+
* `import type * as Canvas from '@cratis/components/Canvas'`; a per-specifier
|
|
23
|
+
* `type` modifier (`import { type Canvas } from ...`) is honored the same way.
|
|
24
|
+
* - Aliases are preserved: `import { Canvas as C } from '@cratis/components'` becomes
|
|
25
|
+
* `import * as C from '@cratis/components/Canvas'`.
|
|
26
|
+
* - A mixed import naming both an approved setup symbol and a namespace is split: the
|
|
27
|
+
* setup symbol stays imported from the root, each namespace becomes its own subpath
|
|
28
|
+
* import.
|
|
29
|
+
* - An import naming several namespaces produces one subpath import per namespace, in
|
|
30
|
+
* their original left-to-right order.
|
|
31
|
+
* - A named re-export follows the same rules: `export { Canvas } from '@cratis/components'`
|
|
32
|
+
* becomes `export * as Canvas from '@cratis/components/Canvas'`, with the same alias,
|
|
33
|
+
* type-only, mixed-setup-symbol, and multiple-namespace handling as the import case.
|
|
34
|
+
* - Applying the codemod again is a no-op: it only ever matches an import or named
|
|
35
|
+
* re-export whose module specifier is exactly the configured package name, so an
|
|
36
|
+
* already-migrated subpath import or re-export is never revisited.
|
|
37
|
+
*
|
|
38
|
+
* What it deliberately refuses to guess, reporting a diagnostic instead of editing:
|
|
39
|
+
* - A namespace import of the whole package (`import * as Components from '...'`) —
|
|
40
|
+
* which subpath each later `Components.X` access belongs to cannot be determined
|
|
41
|
+
* from the import alone.
|
|
42
|
+
* - A default import (`import Components from '...'`) — the package has no default
|
|
43
|
+
* export.
|
|
44
|
+
* - A side-effect-only import (`import '...'`) — there is no binding to infer a
|
|
45
|
+
* subpath from.
|
|
46
|
+
* - A named import or named re-export of a symbol that is neither an approved setup
|
|
47
|
+
* symbol nor a known namespace — the whole statement is left untouched so a
|
|
48
|
+
* partial, silently incomplete migration is never produced.
|
|
49
|
+
* - A known removed Components 3 renderer-compatibility export — it has no Components 4
|
|
50
|
+
* subpath, so the statement is left untouched with typed-parts guidance.
|
|
51
|
+
* - A dynamic `import('...')` or CommonJS `require('...')` call anywhere in the file.
|
|
52
|
+
* - A wildcard re-export of the whole package (`export * from '@cratis/components'`)
|
|
53
|
+
* or a namespace re-export of the whole package (`export * as X from
|
|
54
|
+
* '@cratis/components'`) — the same ambiguity as a whole-package namespace import:
|
|
55
|
+
* which subpath each later access belongs to cannot be determined from the
|
|
56
|
+
* re-export alone.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} fileName - Used only to select the TypeScript/TSX/JS/JSX grammar.
|
|
59
|
+
* @param {string} text - The source file's current contents.
|
|
60
|
+
* @param {{ packageName?: string }} [options]
|
|
61
|
+
* @returns {{ text: string, changed: boolean, diagnostics: Array<{ file: string, line: number, column: number, message: string }> }}
|
|
62
|
+
*/
|
|
63
|
+
export function transformSource(fileName, text, options = {}) {
|
|
64
|
+
const packageName = options.packageName ?? defaultPackageName;
|
|
65
|
+
const sourceFile = ts.createSourceFile(
|
|
66
|
+
fileName,
|
|
67
|
+
text,
|
|
68
|
+
ts.ScriptTarget.Latest,
|
|
69
|
+
true,
|
|
70
|
+
scriptKindFor(fileName),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
const edits = [];
|
|
75
|
+
|
|
76
|
+
const isRootSpecifier = (moduleSpecifier) =>
|
|
77
|
+
!!moduleSpecifier &&
|
|
78
|
+
ts.isStringLiteral(moduleSpecifier) &&
|
|
79
|
+
moduleSpecifier.text === packageName;
|
|
80
|
+
|
|
81
|
+
const report = (node, message) => {
|
|
82
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
83
|
+
node.getStart(sourceFile),
|
|
84
|
+
);
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
file: fileName,
|
|
87
|
+
line: line + 1,
|
|
88
|
+
column: character + 1,
|
|
89
|
+
message,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleImport = (node) => {
|
|
94
|
+
if (!isRootSpecifier(node.moduleSpecifier)) return;
|
|
95
|
+
|
|
96
|
+
const importClause = node.importClause;
|
|
97
|
+
if (!importClause) {
|
|
98
|
+
report(
|
|
99
|
+
node,
|
|
100
|
+
`Side-effect-only import of '${packageName}' cannot be auto-migrated: there is no named binding to infer a subpath from. Import the specific subpath directly, or remove the import if it has no effect.`,
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (importClause.name) {
|
|
105
|
+
report(
|
|
106
|
+
node,
|
|
107
|
+
`Default import of '${packageName}' cannot be auto-migrated: the package has no default export. Replace it with the subpath import(s) it actually needs.`,
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const bindings = importClause.namedBindings;
|
|
113
|
+
if (!bindings || !ts.isNamedImports(bindings)) {
|
|
114
|
+
report(
|
|
115
|
+
node,
|
|
116
|
+
`Namespace import of '${packageName}' cannot be auto-migrated: which subpath each later member access belongs to cannot be inferred from the import alone. Replace it with the specific namespace subpath import(s) it needs.`,
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const declTypeOnly = importClause.isTypeOnly;
|
|
122
|
+
const kept = [];
|
|
123
|
+
const namespaced = [];
|
|
124
|
+
let hasUnknown = false;
|
|
125
|
+
|
|
126
|
+
for (const element of bindings.elements) {
|
|
127
|
+
const importedName = (element.propertyName ?? element.name).text;
|
|
128
|
+
const localName = element.name.text;
|
|
129
|
+
|
|
130
|
+
if (approvedRootSymbols.has(importedName)) {
|
|
131
|
+
kept.push({
|
|
132
|
+
name: importedName,
|
|
133
|
+
localName,
|
|
134
|
+
elementTypeOnly: element.isTypeOnly,
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (Object.hasOwn(namespaceSubpaths, importedName)) {
|
|
139
|
+
namespaced.push({
|
|
140
|
+
localName,
|
|
141
|
+
subpath: namespaceSubpaths[importedName],
|
|
142
|
+
typeOnly: declTypeOnly || element.isTypeOnly,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (removedRootSymbols.has(importedName)) {
|
|
147
|
+
hasUnknown = true;
|
|
148
|
+
report(
|
|
149
|
+
element,
|
|
150
|
+
`'${importedName}' was a Components 3 PrimeReact compatibility export and has no Components 4 subpath. Leaving this import untouched — migrate renderer slots to typed Cratis parts and remove the compatibility export manually.`,
|
|
151
|
+
);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
hasUnknown = true;
|
|
156
|
+
report(
|
|
157
|
+
element,
|
|
158
|
+
`'${importedName}' is not a recognized '${packageName}' root export (neither an approved setup symbol nor a known namespace). Leaving this import untouched — add '${importedName}' to the migration map, or migrate it by hand.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Never guess: an unrecognized specifier means the whole statement is left as-is,
|
|
163
|
+
// even for the specifiers this codemod does recognize.
|
|
164
|
+
if (hasUnknown) return;
|
|
165
|
+
// Nothing to migrate — the import already only names approved root symbols.
|
|
166
|
+
if (namespaced.length === 0) return;
|
|
167
|
+
|
|
168
|
+
const replacement = buildSubpathReplacementText({
|
|
169
|
+
keyword: 'import',
|
|
170
|
+
packageName,
|
|
171
|
+
declTypeOnly,
|
|
172
|
+
kept,
|
|
173
|
+
namespaced,
|
|
174
|
+
});
|
|
175
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleExport = (node) => {
|
|
179
|
+
if (!isRootSpecifier(node.moduleSpecifier)) return;
|
|
180
|
+
|
|
181
|
+
if (!node.exportClause || !ts.isNamedExports(node.exportClause)) {
|
|
182
|
+
// A bare `export * from '@cratis/components'` or a namespace re-export
|
|
183
|
+
// (`export * as X from '@cratis/components'`) is exactly as ambiguous as a
|
|
184
|
+
// whole-package namespace import: there is no way to know, from the
|
|
185
|
+
// re-export alone, which subpath a later consumer's member access needs.
|
|
186
|
+
report(
|
|
187
|
+
node,
|
|
188
|
+
`A wildcard 're-export … from ${JSON.stringify(packageName)}' cannot be auto-migrated: which subpath each later member access belongs to cannot be inferred from the re-export alone. Replace it with explicit subpath re-export(s).`,
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const declTypeOnly = node.isTypeOnly;
|
|
194
|
+
const kept = [];
|
|
195
|
+
const namespaced = [];
|
|
196
|
+
let hasUnknown = false;
|
|
197
|
+
|
|
198
|
+
for (const element of node.exportClause.elements) {
|
|
199
|
+
const exportedName = (element.propertyName ?? element.name).text;
|
|
200
|
+
const localName = element.name.text;
|
|
201
|
+
|
|
202
|
+
if (approvedRootSymbols.has(exportedName)) {
|
|
203
|
+
kept.push({
|
|
204
|
+
name: exportedName,
|
|
205
|
+
localName,
|
|
206
|
+
elementTypeOnly: element.isTypeOnly,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (Object.hasOwn(namespaceSubpaths, exportedName)) {
|
|
211
|
+
namespaced.push({
|
|
212
|
+
localName,
|
|
213
|
+
subpath: namespaceSubpaths[exportedName],
|
|
214
|
+
typeOnly: declTypeOnly || element.isTypeOnly,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (removedRootSymbols.has(exportedName)) {
|
|
219
|
+
hasUnknown = true;
|
|
220
|
+
report(
|
|
221
|
+
element,
|
|
222
|
+
`'${exportedName}' was a Components 3 PrimeReact compatibility export and has no Components 4 subpath. Leaving this re-export untouched — migrate renderer slots to typed Cratis parts and remove the compatibility export manually.`,
|
|
223
|
+
);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
hasUnknown = true;
|
|
228
|
+
report(
|
|
229
|
+
element,
|
|
230
|
+
`'${exportedName}' is not a recognized '${packageName}' root export (neither an approved setup symbol nor a known namespace). Leaving this re-export untouched — add '${exportedName}' to the migration map, or migrate it by hand.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Never guess: an unrecognized specifier means the whole statement is left as-is,
|
|
235
|
+
// even for the specifiers this codemod does recognize.
|
|
236
|
+
if (hasUnknown) return;
|
|
237
|
+
// Nothing to migrate — the re-export already only names approved root symbols.
|
|
238
|
+
if (namespaced.length === 0) return;
|
|
239
|
+
|
|
240
|
+
const replacement = buildSubpathReplacementText({
|
|
241
|
+
keyword: 'export',
|
|
242
|
+
packageName,
|
|
243
|
+
declTypeOnly,
|
|
244
|
+
kept,
|
|
245
|
+
namespaced,
|
|
246
|
+
});
|
|
247
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement });
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleImportEquals = (node) => {
|
|
251
|
+
if (!ts.isExternalModuleReference(node.moduleReference)) return;
|
|
252
|
+
if (!isRootSpecifier(node.moduleReference.expression)) return;
|
|
253
|
+
|
|
254
|
+
report(
|
|
255
|
+
node,
|
|
256
|
+
`Import-assignment of '${packageName}' ('import ${node.name.text} = require(...)') cannot be auto-migrated: which subpath each later member access belongs to cannot be inferred from the import alone. Replace it with the specific namespace subpath import(s) it needs.`,
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const checkCallExpression = (node) => {
|
|
261
|
+
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
262
|
+
const isRequire =
|
|
263
|
+
ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
264
|
+
if (!isDynamicImport && !isRequire) return;
|
|
265
|
+
|
|
266
|
+
const [arg] = node.arguments;
|
|
267
|
+
if (arg && ts.isStringLiteral(arg) && arg.text === packageName) {
|
|
268
|
+
const form = isDynamicImport
|
|
269
|
+
? "dynamic 'import(...)'"
|
|
270
|
+
: "CommonJS 'require(...)'";
|
|
271
|
+
report(
|
|
272
|
+
node,
|
|
273
|
+
`A ${form} of '${packageName}' cannot be auto-migrated: this codemod only rewrites static 'import' declarations. Replace it with the specific subpath import(s) it needs.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const visit = (node) => {
|
|
279
|
+
if (ts.isImportDeclaration(node)) {
|
|
280
|
+
handleImport(node);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (ts.isExportDeclaration(node)) {
|
|
284
|
+
handleExport(node);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (ts.isImportEqualsDeclaration(node)) {
|
|
288
|
+
handleImportEquals(node);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (ts.isCallExpression(node)) {
|
|
292
|
+
checkCallExpression(node);
|
|
293
|
+
}
|
|
294
|
+
ts.forEachChild(node, visit);
|
|
295
|
+
};
|
|
296
|
+
visit(sourceFile);
|
|
297
|
+
|
|
298
|
+
if (edits.length === 0) {
|
|
299
|
+
return { text, changed: false, diagnostics };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
edits.sort((a, b) => a.start - b.start);
|
|
303
|
+
let output = text;
|
|
304
|
+
for (let i = edits.length - 1; i >= 0; i--) {
|
|
305
|
+
const { start, end, replacement } = edits[i];
|
|
306
|
+
output = output.slice(0, start) + replacement + output.slice(end);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { text: output, changed: true, diagnostics };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Builds replacement source text for both the import and named-re-export cases, which
|
|
314
|
+
* share an identical shape: an optional statement that keeps the approved root symbols
|
|
315
|
+
* (`{ name, localName, elementTypeOnly }` entries), followed by one `* as localName from
|
|
316
|
+
* '<package>/<subpath>'` statement per namespace (`{ localName, subpath, typeOnly }`
|
|
317
|
+
* entries). `keyword` is `'import'` or `'export'`.
|
|
318
|
+
*/
|
|
319
|
+
function buildSubpathReplacementText({ keyword, packageName, declTypeOnly, kept, namespaced }) {
|
|
320
|
+
const lines = [];
|
|
321
|
+
|
|
322
|
+
if (kept.length > 0) {
|
|
323
|
+
const specifiers = kept
|
|
324
|
+
.map(({ name, localName, elementTypeOnly }) => {
|
|
325
|
+
const prefix = !declTypeOnly && elementTypeOnly ? 'type ' : '';
|
|
326
|
+
const alias = localName === name ? '' : ` as ${localName}`;
|
|
327
|
+
return `${prefix}${name}${alias}`;
|
|
328
|
+
})
|
|
329
|
+
.join(', ');
|
|
330
|
+
lines.push(
|
|
331
|
+
`${keyword} ${declTypeOnly ? 'type ' : ''}{ ${specifiers} } from '${packageName}';`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const { localName, subpath, typeOnly } of namespaced) {
|
|
336
|
+
lines.push(
|
|
337
|
+
`${keyword} ${typeOnly ? 'type ' : ''}* as ${localName} from '${packageName}/${subpath}';`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function scriptKindFor(fileName) {
|
|
345
|
+
if (fileName.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
346
|
+
if (fileName.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
347
|
+
if (fileName.endsWith('.mts') || fileName.endsWith('.cts')) return ts.ScriptKind.TS;
|
|
348
|
+
if (
|
|
349
|
+
fileName.endsWith('.js') ||
|
|
350
|
+
fileName.endsWith('.mjs') ||
|
|
351
|
+
fileName.endsWith('.cjs')
|
|
352
|
+
)
|
|
353
|
+
return ts.ScriptKind.JS;
|
|
354
|
+
return ts.ScriptKind.TS;
|
|
355
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/components.migrator",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Migration tooling for updating @cratis/components consumers from Components 3 to Components 4.",
|
|
5
|
+
"author": "Cratis",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"publishConfig": {
|
|
8
9
|
"access": "public"
|
|
9
10
|
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20.0.0"
|
|
13
|
+
},
|
|
10
14
|
"files": [
|
|
11
|
-
"
|
|
12
|
-
|
|
15
|
+
"lib",
|
|
16
|
+
"scripts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"compat-manifest.json"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/Cratis/Components"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"cratis-components-button-variant-tone": "./scripts/button-variant-tone.js",
|
|
30
|
+
"cratis-components-change-handler": "./scripts/change-handler.js",
|
|
31
|
+
"cratis-components-remove-root-namespace-imports": "./scripts/remove-root-namespace-imports.js"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "yarn g:test",
|
|
35
|
+
"verify-package": "node ./test/verify-packed-package.mjs",
|
|
36
|
+
"ci": "yarn g:test"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"semver": "7.8.5",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
41
|
+
}
|
|
13
42
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
import { transformButtonVariantTone } from '../lib/buttonVariantToneTransform.js';
|
|
6
|
+
import { runTransformCli } from '../lib/runTransformCli.js';
|
|
7
|
+
|
|
8
|
+
process.exitCode = runTransformCli(process.argv.slice(2), {
|
|
9
|
+
command: 'cratis-components-button-variant-tone',
|
|
10
|
+
description:
|
|
11
|
+
'Migrates deprecated Button text/link/outlined/rounded/severity JSX props to variant/tone/shape. Uncertain cases are annotated and reported for manual review.',
|
|
12
|
+
transform: transformButtonVariantTone,
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
import { transformChangeHandlers } from '../lib/changeHandlerTransform.js';
|
|
6
|
+
import { runTransformCli } from '../lib/runTransformCli.js';
|
|
7
|
+
|
|
8
|
+
process.exitCode = runTransformCli(process.argv.slice(2), {
|
|
9
|
+
command: 'cratis-components-change-handler',
|
|
10
|
+
description:
|
|
11
|
+
'Migrates structurally-proven Components event-wrapper callbacks to semantic value callbacks. Ambiguous handlers are annotated and reported for manual review.',
|
|
12
|
+
transform: transformChangeHandlers,
|
|
13
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
|
|
5
|
+
import { runTransformCli } from '../lib/runTransformCli.js';
|
|
6
|
+
import { transformSource } from '../lib/transform.js';
|
|
7
|
+
|
|
8
|
+
process.exitCode = runTransformCli(process.argv.slice(2), {
|
|
9
|
+
command: 'cratis-components-remove-root-namespace-imports',
|
|
10
|
+
description:
|
|
11
|
+
'Rewrites Components 3 root namespace imports and named re-exports to Components 4 explicit subpaths. Unsupported whole-package and unknown forms are reported without guessing.',
|
|
12
|
+
transform: (file, text, { packageName }) =>
|
|
13
|
+
transformSource(file, text, { packageName }),
|
|
14
|
+
});
|