@wallarm-org/migrator 0.0.0-stage → 0.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/README.md CHANGED
@@ -1,3 +1,84 @@
1
- # Temporary Holding Version
1
+ # @wallarm-org/migrator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Codemod CLI that migrates consumer apps across `@wallarm-org/design-system` breaking changes. Renovate runs it after a design-system upgrade; it can also be run by hand.
4
+
5
+ ## How an upgrade reaches your app
6
+
7
+ 1. The design-system releases a new version. Breaking changes always come in a major, and every breaking change ships a migration.
8
+ 2. Renovate opens one MR in your repo with the version bump, the lockfile, and the source rewrites made by the migrator. It applies every migration between the version you were on and the new one, so skipping several majors is fine.
9
+ 3. Whatever the migrator can rewrite safely is already done in the MR. Whatever it can't — a value computed at runtime, a spread that may override the prop — it leaves for you, marked in the code and listed in `wds-migration.md`. It never guesses: after the MR your app renders the same as before the upgrade.
10
+ 4. Your CI fails while any manual step is left, so the MR can't be merged half-migrated.
11
+
12
+ ## Fixing manual steps
13
+
14
+ Each spot that needs you gets a marker right after the component name, saying which migration left it and what to do:
15
+
16
+ ```tsx
17
+ // In the MR
18
+ <Avatar /* @wds-migration(avatar-size-rename): Avatar size was renamed (xs -> md, sm -> lg; new sm is 20px): non-static value, review manually */ size={big ? 'sm' : 'xs'} />
19
+
20
+ // After you fix it (old sm is the new lg, old xs is the new md)
21
+ <Avatar size={big ? 'lg' : 'md'} />
22
+ ```
23
+
24
+ `wds-migration.md` (one per package in a monorepo) lists the same spots with file and line, plus files that could not be parsed. Fix every marker, delete the comment and the report, push — CI goes green.
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ migrator --from <version> [--to <version>] [--dry-run] [paths...]
30
+ ```
31
+
32
+ - `--from` (required): the exact stable design-system version you are migrating from. A prerelease `--from` is rejected: pass the stable version your code matches.
33
+ - `--to`: target version (default: the installed `@wallarm-org/design-system`).
34
+ - `--dry-run`: print changes, write nothing.
35
+ - `--help`, `--version`.
36
+
37
+ Without paths, tracked files under the current directory are scanned. Exit code is `0` for completed runs (even with manual items) and `1` for usage/version errors. When something needs a manual change, or a file could not be parsed, `wds-migration.md` is written (its heading contains `@wds-migration`), and each manual spot also gets an in-code marker right after the tag name: `<Avatar /* @wds-migration(avatar-size-rename): … */ size={s} />`.
38
+
39
+ ## Consumer setup
40
+
41
+ Renovate repository config (or a shared preset in the GitLab Renovate runner repo):
42
+
43
+ ```json
44
+ {
45
+ "packageRules": [
46
+ {
47
+ "matchPackageNames": ["@wallarm-org/design-system"],
48
+ "minimumReleaseAge": "1 hour",
49
+ "prBodyNotes": ["⚠️ The design-system migrator may have left manual steps: search this MR for `wds-migration` comments and the `wds-migration.md` report, fix them, then delete the report."],
50
+ "postUpgradeTasks": {
51
+ "commands": ["npx --yes @wallarm-org/migrator@latest --from {{{currentVersion}}} --to {{{newVersion}}}"],
52
+ "fileFilters": ["**/*.{ts,tsx,js,jsx,mts,cts}", "**/wds-migration.md"],
53
+ "workingDirTemplate": "{{{packageFileDir}}}",
54
+ "installTools": { "node": {} }
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ - `workingDirTemplate` runs the migrator from each upgraded `package.json`'s directory, so each package's tasks run with its own `--from` and only touch files under it. From the repo root, one package's `--from` would rewrite every package (e.g. turn an already-migrated `sm` into `lg`). In a monorepo each package gets its own `wds-migration.md`, hence the recursive filter and CI check.
62
+ - `currentVersion` is the **locked** version when a lockfile exists (Renovate has no separate `lockedVersion` template). Consumers must commit their lockfile.
63
+ - `installTools.node` is needed on the default slim Renovate image (`binarySource=install`), where `npx` isn't on PATH. The `-full` image already has it. With `binarySource=global` the runner image has to provide node/npm.
64
+ - Self-hosted runner admin config: `allowedCommands: ["^npx --yes @wallarm-org/migrator@latest --from \\d+\\.\\d+\\.\\d+\\S* --to \\d+\\.\\d+\\.\\d+\\S*$"]`.
65
+ - `prBodyNotes` deliberately avoids the literal `@wds-migration`: the tracked Renovate config would otherwise trip the CI gate below.
66
+ - Consumer CI: one job
67
+
68
+ ```sh
69
+ if git grep -n '@wds-migration'; then echo 'Design-system migration left manual steps: fix the @wds-migration markers and delete wds-migration.md'; exit 1; fi
70
+ ```
71
+
72
+ Both the in-code markers and the report heading contain `@wds-migration`, so it fails (and prints the hits) until every marker is fixed and `wds-migration.md` is deleted; it works under `set -e`.
73
+ - Manual run after an upgrade: `pnpm dlx @wallarm-org/migrator --from 2.9.0` (or `yarn dlx` / `npx`).
74
+
75
+ ## Adding a migration
76
+
77
+ In Claude Code, run `/new-migration`: it walks through finding the breaking changes, deriving the mapping and writing the tests.
78
+
79
+ Every breaking-change PR to the design-system must add a migration (enforced by `require-migration` in `.github/workflows/pr-title.yml`). A breaking change that needs nothing from consumers ships a report-only migration.
80
+
81
+ 1. Create `src/migrations/<id>.ts` exporting `migration: Migration` (`{ id, title, transform(file, ctx) }`). Use `ctx.report(node, message)` for cases that need a manual change, and the JSX helpers in `src/lib/jsx.ts`.
82
+ 2. Register it in `src/migrations/index.ts`.
83
+ 3. Add before/after fixtures in `src/migrations/__fixtures__/<id>/<case>.input.tsx` + `<case>.output.tsx` (outputs include the `@wds-migration` markers; `__tests__/fixtures.test.ts` also checks idempotence). Assert report counts/messages, aliased/subpath/namespace imports and a same-named non-DS component (must not change) in `src/migrations/__tests__/` using `apply()` from `harness.ts`.
84
+ 4. Never edit `versions.json` by hand: it is stamped automatically when the design-system releases a major version.
package/dist/index.js ADDED
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env node
2
+ import node_fs from "node:fs";
3
+ import node_path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { execFileSync } from "node:child_process";
7
+ import { Node, Project, SyntaxKind, ts } from "ts-morph";
8
+ import { clean, gt, lt, lte, major, prerelease, valid } from "semver";
9
+ class UsageError extends Error {
10
+ }
11
+ const SOURCE_EXTENSIONS = [
12
+ 'ts',
13
+ 'tsx',
14
+ 'js',
15
+ 'jsx',
16
+ 'mts',
17
+ 'cts'
18
+ ];
19
+ const isSource = (f)=>SOURCE_EXTENSIONS.some((e)=>f.endsWith(`.${e}`));
20
+ function walk(abs, out) {
21
+ const stat = node_fs.statSync(abs, {
22
+ throwIfNoEntry: false
23
+ });
24
+ if (!stat) throw new UsageError(`path not found: ${abs}`);
25
+ if (stat.isFile()) {
26
+ if (isSource(abs)) out.push(abs);
27
+ return;
28
+ }
29
+ for (const entry of node_fs.readdirSync(abs, {
30
+ withFileTypes: true
31
+ }))if ('node_modules' !== entry.name && '.git' !== entry.name) walk(node_path.join(abs, entry.name), out);
32
+ }
33
+ function listCandidateFiles(cwd, paths) {
34
+ const out = [];
35
+ if (paths.length > 0) {
36
+ for (const p of paths)walk(node_path.resolve(cwd, p), out);
37
+ return out;
38
+ }
39
+ try {
40
+ const stdout = execFileSync('git', [
41
+ 'ls-files',
42
+ '-z',
43
+ '--',
44
+ ...SOURCE_EXTENSIONS.map((e)=>`*.${e}`)
45
+ ], {
46
+ cwd,
47
+ encoding: 'utf8',
48
+ stdio: [
49
+ 'ignore',
50
+ 'pipe',
51
+ 'ignore'
52
+ ],
53
+ maxBuffer: 268435456
54
+ });
55
+ return stdout.split('\0').filter(Boolean).map((f)=>node_path.resolve(cwd, f));
56
+ } catch {
57
+ throw new UsageError('not a git repository: pass explicit paths (e.g. `migrator --from X src`)');
58
+ }
59
+ }
60
+ const REPORT_FILE = 'wds-migration.md';
61
+ function renderReport(args) {
62
+ const lines = [
63
+ '# Design-system migration: manual steps (@wds-migration)',
64
+ '',
65
+ `The automatic migration from ${args.from} to ${args.to} left these items for you:`,
66
+ '',
67
+ ...args.manual.map((m)=>`- [ ] \`${m.file}:${m.line}\` — ${m.message} (${m.migrationId})`),
68
+ ''
69
+ ];
70
+ if (args.skipped.length > 0) lines.push('## Files that could not be parsed', '', ...args.skipped.map((s)=>`- \`${s.file}\` — ${s.error}`), '');
71
+ lines.push('Delete this file once every item is done.', '');
72
+ return lines.join('\n');
73
+ }
74
+ const DS_PACKAGE = '@wallarm-org/design-system';
75
+ function createProject() {
76
+ return new Project({
77
+ useInMemoryFileSystem: true,
78
+ compilerOptions: {
79
+ allowJs: true,
80
+ jsx: ts.JsxEmit.Preserve
81
+ }
82
+ });
83
+ }
84
+ function localTagNames(file, component) {
85
+ const names = new Set();
86
+ for (const decl of file.getImportDeclarations()){
87
+ const spec = decl.getModuleSpecifierValue();
88
+ if (spec !== DS_PACKAGE && spec !== `${DS_PACKAGE}/${component}`) continue;
89
+ for (const named of decl.getNamedImports())if (named.getName() === component) names.add(named.getAliasNode()?.getText() ?? component);
90
+ const ns = decl.getNamespaceImport();
91
+ if (ns) names.add(`${ns.getText()}.${component}`);
92
+ }
93
+ return names;
94
+ }
95
+ function findComponentElements(file, component) {
96
+ const names = localTagNames(file, component);
97
+ if (0 === names.size) return [];
98
+ return [
99
+ ...file.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
100
+ ...file.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
101
+ ].filter((el)=>names.has(el.getTagNameNode().getText()));
102
+ }
103
+ function getAttribute(el, name) {
104
+ const attr = el.getAttribute(name);
105
+ return attr && Node.isJsxAttribute(attr) ? attr : void 0;
106
+ }
107
+ function hasSpread(el) {
108
+ return el.getAttributes().some((a)=>Node.isJsxSpreadAttribute(a));
109
+ }
110
+ function hasSpreadAfter(el, attr) {
111
+ const attrs = el.getAttributes();
112
+ return attrs.slice(attrs.indexOf(attr) + 1).some((a)=>Node.isJsxSpreadAttribute(a));
113
+ }
114
+ function staticLiteral(attr) {
115
+ const init = attr.getInitializer();
116
+ const inner = init && Node.isJsxExpression(init) ? init.getExpression() : init;
117
+ return inner && (Node.isStringLiteral(inner) || Node.isNoSubstitutionTemplateLiteral(inner)) ? inner : void 0;
118
+ }
119
+ function readStaticString(attr) {
120
+ return staticLiteral(attr)?.getLiteralValue();
121
+ }
122
+ function replaceStaticString(attr, value) {
123
+ const lit = staticLiteral(attr);
124
+ if (!lit) return;
125
+ const delim = lit.getText()[0];
126
+ lit.replaceWithText(`${delim}${value}${delim}`);
127
+ }
128
+ const SYNTAX_ERROR = 'syntax error, file left untouched';
129
+ function markerTarget(node) {
130
+ return Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node) ? node : node.getFirstAncestor((a)=>Node.isJsxOpeningElement(a) || Node.isJsxSelfClosingElement(a));
131
+ }
132
+ function markerRegion(el) {
133
+ const end = el.getAttributes()[0]?.getStart() ?? el.getEnd();
134
+ return el.getSourceFile().getFullText().slice(el.getTagNameNode().getEnd(), end);
135
+ }
136
+ function migrateSource(args) {
137
+ const project = args.project ?? createProject();
138
+ const sourceFile = project.createSourceFile(args.fileName, args.text, {
139
+ overwrite: true
140
+ });
141
+ try {
142
+ return applyMigrations(project, sourceFile, args.migrations);
143
+ } finally{
144
+ sourceFile.forget();
145
+ }
146
+ }
147
+ function applyMigrations(project, sourceFile, migrations) {
148
+ if (project.getProgram().getSyntacticDiagnostics(sourceFile).length > 0) throw new Error(SYNTAX_ERROR);
149
+ const manual = [];
150
+ for (const migration of migrations){
151
+ const token = `@wds-migration(${migration.id})`;
152
+ const pending = new Map();
153
+ migration.transform(sourceFile, {
154
+ report: (node, message)=>{
155
+ manual.push({
156
+ line: node.getStartLineNumber(),
157
+ migrationId: migration.id,
158
+ message
159
+ });
160
+ const el = markerTarget(node);
161
+ if (el && !pending.has(el) && !markerRegion(el).includes(token)) pending.set(el, message);
162
+ }
163
+ });
164
+ const inserts = [
165
+ ...pending
166
+ ].map(([el, message])=>({
167
+ pos: el.getTagNameNode().getEnd(),
168
+ text: ` /* ${token}: ${message.replaceAll('*/', '* /').replace(/[\r\n]+/g, ' ')} */`
169
+ })).sort((x, y)=>y.pos - x.pos);
170
+ for (const { pos, text } of inserts)sourceFile.insertText(pos, text);
171
+ }
172
+ return {
173
+ code: sourceFile.getFullText(),
174
+ manual
175
+ };
176
+ }
177
+ function runMigrations(args) {
178
+ const result = {
179
+ changed: [],
180
+ manual: [],
181
+ skipped: []
182
+ };
183
+ const project = createProject();
184
+ for (const abs of args.files){
185
+ const file = node_path.relative(args.cwd, abs).split(node_path.sep).join('/');
186
+ try {
187
+ const before = node_fs.readFileSync(abs, 'utf8');
188
+ if (!before.includes(DS_PACKAGE)) continue;
189
+ const { code: after, manual } = migrateSource({
190
+ text: before,
191
+ fileName: file,
192
+ migrations: args.migrations,
193
+ project
194
+ });
195
+ result.manual.push(...manual.map((m)=>({
196
+ file,
197
+ ...m
198
+ })));
199
+ if (after !== before) result.changed.push({
200
+ file,
201
+ before,
202
+ after
203
+ });
204
+ } catch (e) {
205
+ result.skipped.push({
206
+ file,
207
+ error: e instanceof Error ? e.message : String(e)
208
+ });
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+ function parseExactVersion(input, flag) {
214
+ const version = valid(clean(input));
215
+ if (!version) throw new UsageError(`${flag} must be an exact version (e.g. 2.9.0), got "${input}"`);
216
+ if ('--from' === flag && prerelease(version)) throw new UsageError(`--from ${version} is a prerelease; pass the stable version your code matches`);
217
+ return version;
218
+ }
219
+ function selectMigrations(args) {
220
+ const { from, to, versions, migrations } = args;
221
+ if (lt(from, '1.0.0')) throw new UsageError(`--from ${from} is too old: upgrade to 1.x manually first`);
222
+ if (gt(from, to)) throw new UsageError(`--from ${from} is newer than --to ${to}`);
223
+ const isPrerelease = null !== prerelease(to);
224
+ if (!isPrerelease && major(to) > major(versions.latest)) throw new UsageError(`design-system ${to} is newer than this migrator knows; retry later or update @wallarm-org/migrator`);
225
+ const stamped = migrations.flatMap((m)=>{
226
+ const v = versions.migrations[m.id];
227
+ return void 0 === v ? [] : [
228
+ {
229
+ m,
230
+ v
231
+ }
232
+ ];
233
+ }).filter(({ v })=>gt(v, from) && lte(v, to)).sort((x, y)=>gt(x.v, y.v) ? 1 : gt(y.v, x.v) ? -1 : x.m.id.localeCompare(y.m.id)).map(({ m })=>m);
234
+ const pending = isPrerelease ? migrations.filter((m)=>!(m.id in versions.migrations)).sort((x, y)=>x.id.localeCompare(y.id)) : [];
235
+ return [
236
+ ...stamped,
237
+ ...pending
238
+ ];
239
+ }
240
+ const RENAMES = {
241
+ xs: 'md',
242
+ sm: 'lg'
243
+ };
244
+ const HINT = 'Avatar size was renamed (xs -> md, sm -> lg; new sm is 20px)';
245
+ const avatar_size_rename_migration = {
246
+ id: 'avatar-size-rename',
247
+ title: 'Avatar: xs → md, sm → lg',
248
+ transform (file, ctx) {
249
+ for (const el of findComponentElements(file, 'Avatar')){
250
+ const attr = getAttribute(el, 'size');
251
+ if (attr) {
252
+ const value = readStaticString(attr);
253
+ const next = void 0 === value ? void 0 : RENAMES[value];
254
+ if (void 0 === value) ctx.report(attr, `${HINT}: non-static value, review manually`);
255
+ else if (next && Object.hasOwn(RENAMES, value)) replaceStaticString(attr, next);
256
+ if (void 0 !== value && hasSpreadAfter(el, attr)) ctx.report(attr, `${HINT}: spread props after size may override it with an old value, review manually`);
257
+ } else if (hasSpread(el)) ctx.report(el, `${HINT}: size may come from spread props, review manually`);
258
+ }
259
+ }
260
+ };
261
+ const MAP = {
262
+ primary: [
263
+ 'secondary',
264
+ 'neutral'
265
+ ],
266
+ 'primary-alt': [
267
+ 'secondary',
268
+ 'neutral-alt'
269
+ ],
270
+ brand: [
271
+ 'solid',
272
+ 'brand'
273
+ ],
274
+ destructive: [
275
+ 'solid',
276
+ 'danger'
277
+ ],
278
+ outline: [
279
+ 'outline',
280
+ 'neutral'
281
+ ],
282
+ info: [
283
+ 'secondary',
284
+ 'info'
285
+ ]
286
+ };
287
+ const NEW_TYPES = new Set([
288
+ 'solid',
289
+ 'secondary',
290
+ 'outline'
291
+ ]);
292
+ const numeric_badge_type_color_HINT = 'NumericBadge type was split into type + color';
293
+ const numeric_badge_type_color_migration = {
294
+ id: 'numeric-badge-type-color',
295
+ title: 'NumericBadge: type splits into type + color',
296
+ transform (file, ctx) {
297
+ for (const el of findComponentElements(file, 'NumericBadge')){
298
+ const typeAttr = getAttribute(el, 'type');
299
+ if (!typeAttr) {
300
+ if (hasSpread(el)) ctx.report(el, `${numeric_badge_type_color_HINT}: type may come from spread props, review manually`);
301
+ continue;
302
+ }
303
+ const value = readStaticString(typeAttr);
304
+ if (void 0 === value) {
305
+ ctx.report(typeAttr, `${numeric_badge_type_color_HINT}: non-static type, review manually`);
306
+ continue;
307
+ }
308
+ if (hasSpreadAfter(el, typeAttr)) ctx.report(typeAttr, `${numeric_badge_type_color_HINT}: spread props after type may override it with an old value, review manually`);
309
+ if (!Object.hasOwn(MAP, value)) continue;
310
+ if (getAttribute(el, 'color')) {
311
+ if (!NEW_TYPES.has(value)) ctx.report(typeAttr, `${numeric_badge_type_color_HINT}: already has color, review manually`);
312
+ continue;
313
+ }
314
+ const [type, color] = MAP[value];
315
+ const q = typeAttr.getText().match(/['"]/)?.[0] ?? "'";
316
+ replaceStaticString(typeAttr, type);
317
+ typeAttr.replaceWithText(`${typeAttr.getText()} color=${q}${color}${q}`);
318
+ }
319
+ }
320
+ };
321
+ const migrations_migrations = [
322
+ numeric_badge_type_color_migration,
323
+ avatar_size_rename_migration
324
+ ];
325
+ const USAGE = `Usage: migrator --from <version> [--to <version>] [--dry-run] [paths...]
326
+
327
+ Migrates a consumer app across @wallarm-org/design-system breaking changes.
328
+
329
+ Options:
330
+ --from <version> design-system version you are migrating from (required, exact)
331
+ --to <version> target version (default: installed @wallarm-org/design-system)
332
+ --dry-run print changes, write nothing
333
+ --help show this help
334
+ --version show migrator version
335
+
336
+ Without paths, tracked files of the current git repository are scanned.`;
337
+ function installedVersion(cwd) {
338
+ for(let dir = cwd;; dir = node_path.dirname(dir)){
339
+ const file = node_path.join(dir, 'node_modules/@wallarm-org/design-system/package.json');
340
+ if (node_fs.existsSync(file)) return JSON.parse(node_fs.readFileSync(file, 'utf8')).version;
341
+ if (node_path.dirname(dir) === dir) throw new UsageError('could not find installed @wallarm-org/design-system: pass --to');
342
+ }
343
+ }
344
+ function lineDiff(before, after) {
345
+ const a = before.split('\n');
346
+ const b = after.split('\n');
347
+ const lines = [];
348
+ for(let i = 0; i < Math.max(a.length, b.length); i++)if (a[i] !== b[i]) lines.push(` L${i + 1}: ${a[i]?.trim() ?? ''} → ${b[i]?.trim() ?? ''}`);
349
+ return lines;
350
+ }
351
+ async function main(argv, cwd, versions, version, io = {
352
+ log: console.log,
353
+ error: console.error
354
+ }) {
355
+ try {
356
+ const { values, positionals } = parseArgs({
357
+ args: argv,
358
+ options: {
359
+ from: {
360
+ type: 'string'
361
+ },
362
+ to: {
363
+ type: 'string'
364
+ },
365
+ 'dry-run': {
366
+ type: 'boolean'
367
+ },
368
+ help: {
369
+ type: 'boolean'
370
+ },
371
+ version: {
372
+ type: 'boolean'
373
+ }
374
+ },
375
+ allowPositionals: true,
376
+ strict: true
377
+ });
378
+ if (values.help) {
379
+ io.log(USAGE);
380
+ return 0;
381
+ }
382
+ if (values.version) {
383
+ io.log(version);
384
+ return 0;
385
+ }
386
+ if (void 0 === values.from) throw new UsageError('--from is required');
387
+ const from = parseExactVersion(values.from, '--from');
388
+ const to = parseExactVersion(values.to ?? installedVersion(cwd), '--to');
389
+ const selected = selectMigrations({
390
+ from,
391
+ to,
392
+ versions,
393
+ migrations: migrations_migrations
394
+ });
395
+ const files = listCandidateFiles(cwd, positionals);
396
+ const { changed, manual, skipped } = runMigrations({
397
+ files,
398
+ cwd,
399
+ migrations: selected
400
+ });
401
+ const dry = true === values['dry-run'];
402
+ for (const c of changed)if (dry) {
403
+ io.log(c.file);
404
+ for (const l of lineDiff(c.before, c.after))io.log(l);
405
+ } else node_fs.writeFileSync(node_path.join(cwd, c.file), c.after);
406
+ for (const s of skipped)io.error(`warning: skipped ${s.file}: ${s.error}`);
407
+ const writeReport = !dry && (manual.length > 0 || skipped.length > 0);
408
+ if (writeReport) node_fs.writeFileSync(node_path.join(cwd, REPORT_FILE), renderReport({
409
+ from,
410
+ to,
411
+ manual,
412
+ skipped
413
+ }));
414
+ const s = (n, w)=>`${n} ${w}${1 === n ? '' : 's'}`;
415
+ io.log(`${dry ? '[dry-run] ' : ''}${s(changed.length, 'file')} changed, ${s(manual.length, 'manual item')}` + (skipped.length ? `, ${s(skipped.length, 'file')} skipped` : '') + (writeReport ? `; see ${REPORT_FILE}` : ''));
416
+ return 0;
417
+ } catch (e) {
418
+ if (e instanceof UsageError || e?.code?.startsWith('ERR_PARSE_ARGS')) {
419
+ io.error(e.message);
420
+ return 1;
421
+ }
422
+ throw e;
423
+ }
424
+ }
425
+ const root = node_path.join(node_path.dirname(fileURLToPath(import.meta.url)), '..');
426
+ const readJson = (f)=>JSON.parse(node_fs.readFileSync(node_path.join(root, f), 'utf8'));
427
+ const src_versions = readJson('versions.json');
428
+ process.exitCode = await main(process.argv.slice(2), process.cwd(), src_versions, readJson('package.json').version);
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@wallarm-org/migrator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Codemod CLI that migrates consumer apps across Wallarm Design System breaking changes",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/wallarm/design-system.git",
12
+ "directory": "packages/migrator"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Wallarm",
16
+ "type": "module",
17
+ "bin": {
18
+ "migrator": "./dist/index.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "versions.json"
23
+ ],
24
+ "scripts": {
25
+ "build": "rslib build",
26
+ "dev": "rslib build --watch",
27
+ "test": "rstest watch",
28
+ "test:run": "rstest run",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepublishOnly": "prepare-publish",
31
+ "postpublish": "restore-package"
32
+ },
33
+ "dependencies": {
34
+ "semver": "7.8.5",
35
+ "ts-morph": "28.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@rslib/core": "catalog:",
39
+ "@rstest/core": "catalog:",
40
+ "@types/node": "catalog:",
41
+ "@types/semver": "7.8.0",
42
+ "@wallarm-org/publish-scripts": "workspace:*",
43
+ "@wallarm-org/rstest-config": "workspace:*",
44
+ "@wallarm-org/typescript-config": "workspace:*",
45
+ "typescript": "catalog:"
46
+ }
47
+ }
package/versions.json ADDED
@@ -0,0 +1 @@
1
+ { "latest": "3.0.0", "migrations": { "numeric-badge-type-color": "2.0.0", "avatar-size-rename": "3.0.0" } }