bestax-migrate 2.31.1 → 2.31.3

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
@@ -93,6 +93,17 @@ Run it from your app root. `--param paths="src lib"` (default `src`) and `--para
93
93
  | `--css <mode>` | Stylesheet target: `bestax`, `bulma`, or `keep` (default `bestax`; `keep` for `bulma-classes`) |
94
94
  | `--no-deps` | Skip updating package.json dependencies |
95
95
 
96
+ ## Exit codes
97
+
98
+ | Code | Meaning |
99
+ | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
100
+ | `0` | The run finished. TODOs are expected output and leave it at 0 |
101
+ | `1` | A file could not be parsed or transformed. It is marked `✖`, listed in the report and left as it was, and every other file is still migrated. Fix it and run the codemod on it again. A run like that sends no telemetry |
102
+ | `1` | The command was refused before the run: an unknown source, flag or `--css` mode, a path that isn't there, an HTML page for a source that reads none, or Node older than 22 |
103
+ | `130` | Ctrl-C at the telemetry question, which comes after the run |
104
+
105
+ `--help` lists them too. With `--dry`, a file that fails to parse still makes the exit code 1, so a preview in CI catches it. A file the run can't read at all stops it there instead, with Node's error and exit code 1, before `package.json` is updated.
106
+
96
107
  ## After the codemod
97
108
 
98
109
  1. Run your package manager's install (the codemod rewrote `package.json` but never installs)
package/dist/cli.d.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  * .html/.htm → a new module beside the page, for a source that reads HTML),
11
11
  * updates the nearest package.json (unless --no-deps), writes results back
12
12
  * (unless --dry), and prints the run report. TODO annotations are expected
13
- * output — the exit code is 0 whenever the run itself succeeds.
13
+ * output, and leave the exit code at 0. A file that fails to parse or
14
+ * transform makes it 1, once every other file has been migrated: see
15
+ * `EXIT_CODES`.
14
16
  */
15
17
  import { createInterface } from 'node:readline/promises';
16
18
  import type { Readable, Writable } from 'node:stream';
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAIV,eAAe,EAEhB,MAAM,YAAY,CAAC;AA2BpB;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,EAAE,CAwBV;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAkB5D;AAED,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgDlD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBvD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAEpE;AA2QD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,KAAK,EACT,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,EAGjC,aAAa,GAAE,OAAO,eAAiC,GACtD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAyBzB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,OAAO,GAAG,SAAS,EACzB,EAAE,EAAE,KAAK,EACT,aAAa,GAAE,CAAC,EAAE,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAA0B,GAC7E,OAAO,CAAC,IAAI,CAAC,CAqBf;AAYD,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAkD,GACrD,OAAO,CAmJT"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAIV,eAAe,EAEhB,MAAM,YAAY,CAAC;AAyCpB;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,EAAE,CAwBV;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAkB5D;AAED,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgDlD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsBvD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAEpE;AA2RD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,KAAK,EACT,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,EAGjC,aAAa,GAAE,OAAO,eAAiC,GACtD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAyBzB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,OAAO,GAAG,SAAS,EACzB,EAAE,EAAE,KAAK,EACT,aAAa,GAAE,CAAC,EAAE,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAA0B,GAC7E,OAAO,CAAC,IAAI,CAAC,CAqBf;AAYD,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAkD,GACrD,OAAO,CA+JT"}
package/dist/cli.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * .html/.htm → a new module beside the page, for a source that reads HTML),
11
11
  * updates the nearest package.json (unless --no-deps), writes results back
12
12
  * (unless --dry), and prints the run report. TODO annotations are expected
13
- * output — the exit code is 0 whenever the run itself succeeds.
13
+ * output, and leave the exit code at 0. A file that fails to parse or
14
+ * transform makes it 1, once every other file has been migrated: see
15
+ * `EXIT_CODES`.
14
16
  */
15
17
  import fs from 'node:fs';
16
18
  import path from 'node:path';
@@ -31,6 +33,19 @@ const SKIP_DIRS = new Set([
31
33
  ]);
32
34
  const STYLE_EXTENSIONS = new Set(['scss', 'sass']);
33
35
  const CSS_MODES = ['bestax', 'bulma', 'keep'];
36
+ /**
37
+ * The exit codes, for `--help` and the README's "Exit codes", which says the
38
+ * same. Commander exits 1 on a command line it refuses, and so does the bin
39
+ * entry on a Node older than it supports.
40
+ */
41
+ const EXIT_CODES = [
42
+ 'Exit codes:',
43
+ ' 0 the run finished; TODO(bestax-migrate) comments are expected output',
44
+ ' 1 a file could not be parsed or transformed (each is marked ✖ and',
45
+ ' listed in the report, and the other files are still migrated), or',
46
+ ' the command was refused before the run',
47
+ ' 130 Ctrl-C at the telemetry question, after the run',
48
+ ].join('\n');
34
49
  /** HTML pages, which only a source with `htmlExtensions` reads. */
35
50
  const HTML_EXTENSIONS = ['html', 'htm'];
36
51
  /**
@@ -230,11 +245,19 @@ function migrateFiles(source, files, reporter, io, options, project) {
230
245
  const pkg = sourcePackage(source);
231
246
  const sourceImportRe = pkg === null ? null : makeSourceImportRe(pkg);
232
247
  for (const file of files) {
248
+ // Outside the failure handling on purpose: a file the run can't read at
249
+ // all stops it here, before the manifest pass, which could not tell
250
+ // whether that file still imports the source.
233
251
  const sourceText = fs.readFileSync(file, 'utf8');
234
252
  const collector = reporter.startFile();
235
253
  const extension = extensionOf(file);
236
254
  if (source.transformHtml && source.htmlExtensions?.includes(extension)) {
237
- const written = migrateHtmlPage(source.transformHtml, file, sourceText, collector, io, { ...options, project });
255
+ const page = migrateHtmlPage(source.transformHtml, file, sourceText, collector, io, { ...options, project });
256
+ if ('error' in page) {
257
+ reporter.failFile(file, page.error, collector.entries);
258
+ continue;
259
+ }
260
+ const written = page.written;
238
261
  // The module is what the app will import from, as a JSX file's output
239
262
  // is: one that imports bulma/… needs `bulma` declared.
240
263
  if (written !== null && /['"](?:~?bulma\/)/.test(written)) {
@@ -261,7 +284,8 @@ function migrateFiles(source, files, reporter, io, options, project) {
261
284
  }
262
285
  }
263
286
  catch (error) {
264
- io.error(chalk.red(`✖ ${file}: ${error.message}`));
287
+ const message = error.message;
288
+ io.error(chalk.red(`✖ ${file}: ${message}`));
265
289
  // Read the signals off the ORIGINAL text before bailing. A parse
266
290
  // failure in the only file importing the source library would
267
291
  // otherwise let the manifest pass remove the package with no warning —
@@ -270,7 +294,7 @@ function migrateFiles(source, files, reporter, io, options, project) {
270
294
  bulmaReferenced = true;
271
295
  if (importsSource(sourceImportRe, sourceText))
272
296
  sourceStillImported = true;
273
- reporter.finishFile(file, false, collector.entries);
297
+ reporter.failFile(file, message, collector.entries);
274
298
  continue;
275
299
  }
276
300
  if (/['"](?:~?bulma\/)/.test(output ?? sourceText)) {
@@ -292,7 +316,7 @@ function migrateFiles(source, files, reporter, io, options, project) {
292
316
  /**
293
317
  * One HTML page: its module is written beside it (never over a file already
294
318
  * there; the source checks), and the page itself is left as it was. Returns
295
- * the module, or null when there is none to write.
319
+ * the module, null when there is none to write, or why the page failed.
296
320
  */
297
321
  function migrateHtmlPage(transformHtml, file, sourceText, collector, io, options) {
298
322
  let result;
@@ -303,11 +327,12 @@ function migrateHtmlPage(transformHtml, file, sourceText, collector, io, options
303
327
  });
304
328
  }
305
329
  catch (error) {
306
- io.error(chalk.red(`✖ ${file}: ${error.message}`));
307
- return null;
330
+ const message = error.message;
331
+ io.error(chalk.red(`✖ ${file}: ${message}`));
332
+ return { error: message };
308
333
  }
309
334
  if (result.output === null)
310
- return null;
335
+ return { written: null };
311
336
  if (options.print)
312
337
  io.log(result.output);
313
338
  if (options.dry) {
@@ -317,7 +342,7 @@ function migrateHtmlPage(transformHtml, file, sourceText, collector, io, options
317
342
  fs.writeFileSync(result.outputPath, result.output);
318
343
  io.log(chalk.green(`Wrote ${result.outputPath} from ${file}.`));
319
344
  }
320
- return result.output;
345
+ return { written: result.output };
321
346
  }
322
347
  /**
323
348
  * HTML pages a folder walk found but `--extensions` left out, reported when
@@ -402,6 +427,7 @@ function migrateDependencies(source, targets, reporter, io, options, deps) {
402
427
  for (const pkgPath of findPackageJsons(targets)) {
403
428
  const collector = reporter.startFile();
404
429
  let changed = false;
430
+ let failure;
405
431
  try {
406
432
  const raw = fs.readFileSync(pkgPath, 'utf8');
407
433
  const pkg = JSON.parse(raw);
@@ -421,7 +447,12 @@ function migrateDependencies(source, targets, reporter, io, options, deps) {
421
447
  }
422
448
  }
423
449
  catch (error) {
424
- io.error(chalk.red(`✖ ${pkgPath}: ${error.message}`));
450
+ failure = error.message;
451
+ io.error(chalk.red(`✖ ${pkgPath}: ${failure}`));
452
+ }
453
+ if (failure !== undefined) {
454
+ reporter.failFile(pkgPath, failure, collector.entries);
455
+ continue;
425
456
  }
426
457
  reporter.finishFile(pkgPath, changed, collector.entries);
427
458
  if (changed) {
@@ -530,6 +561,7 @@ export function createCLI(io = { log: console.log, error: console.error }) {
530
561
  .option('--no-deps', 'skip updating package.json dependencies')
531
562
  .option('--telemetry', 'enable anonymous usage telemetry (https://bestax.io/docs/guides/telemetry)')
532
563
  .option('--no-telemetry', 'disable anonymous usage telemetry')
564
+ .addHelpText('after', `\n${EXIT_CODES}`)
533
565
  .action(async (sourceName, targets, options) => {
534
566
  const source = getSource(sourceName);
535
567
  if (!source) {
@@ -584,6 +616,16 @@ export function createCLI(io = { log: console.log, error: console.error }) {
584
616
  depSignals.sourceStillImported = true;
585
617
  migrateDependencies(source, targets, reporter, io, runOptions, depSignals);
586
618
  io.log(reporter.render(source.label + (runOptions.dry ? ' (dry run)' : '')));
619
+ // A file that failed leaves the migration unfinished, whatever the
620
+ // rest did, so the exit code says so: a script or CI step that runs
621
+ // the codemod must not read it as success. Every other file has been
622
+ // migrated by now. Not `program.error`, which exits at once and could
623
+ // cut the report short on its way down a pipe. The run is not a
624
+ // successful one, so it ends here, with no nudge and no telemetry.
625
+ if (reporter.failures.length > 0) {
626
+ process.exitCode = 1;
627
+ return;
628
+ }
587
629
  displayStarNudge(io);
588
630
  // Success path only: rule names and counts, never file/line/message.
589
631
  const stats = {
package/dist/report.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Run reporting: collects per-file results and prints the end-of-run summary.
3
3
  * TODO annotations are expected output, not failures — the CLI exits 0 when
4
- * files were transformed and TODOs remain.
4
+ * files were transformed and TODOs remain. A file that could not be parsed or
5
+ * transformed is a failure, and the CLI exits 1 for it.
5
6
  */
6
7
  import type { FileResult, TodoCollector, TodoEntry } from './types.js';
7
8
  export declare class Reporter {
@@ -11,8 +12,11 @@ export declare class Reporter {
11
12
  entries: TodoEntry[];
12
13
  };
13
14
  finishFile(file: string, changed: boolean, todos: TodoEntry[]): void;
15
+ /** A file that could not be parsed or transformed, and why. */
16
+ failFile(file: string, error: string, todos: TodoEntry[]): void;
14
17
  get files(): FileResult[];
15
18
  get changedCount(): number;
19
+ get failures(): FileResult[];
16
20
  get todos(): TodoEntry[];
17
21
  /** TODO counts grouped by rule, highest first. */
18
22
  todosByRule(): Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvE,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAoB;IAEnC,2EAA2E;IAC3E,SAAS,IAAI,aAAa,GAAG;QAAE,OAAO,EAAE,SAAS,EAAE,CAAA;KAAE;IAUrD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI;IAIpE,IAAI,KAAK,IAAI,UAAU,EAAE,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,KAAK,IAAI,SAAS,EAAE,CAEvB;IAED,kDAAkD;IAClD,WAAW,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IAY5D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CA8B9B"}
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvE,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAoB;IAEnC,2EAA2E;IAC3E,SAAS,IAAI,aAAa,GAAG;QAAE,OAAO,EAAE,SAAS,EAAE,CAAA;KAAE;IAUrD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI;IAIpE,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI;IAI/D,IAAI,KAAK,IAAI,UAAU,EAAE,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,QAAQ,IAAI,UAAU,EAAE,CAE3B;IAED,IAAI,KAAK,IAAI,SAAS,EAAE,CAEvB;IAED,kDAAkD;IAClD,WAAW,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IAY5D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAiD9B"}
package/dist/report.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Run reporting: collects per-file results and prints the end-of-run summary.
3
3
  * TODO annotations are expected output, not failures — the CLI exits 0 when
4
- * files were transformed and TODOs remain.
4
+ * files were transformed and TODOs remain. A file that could not be parsed or
5
+ * transformed is a failure, and the CLI exits 1 for it.
5
6
  */
6
7
  import chalk from 'chalk';
7
8
  export class Reporter {
@@ -19,12 +20,19 @@ export class Reporter {
19
20
  finishFile(file, changed, todos) {
20
21
  this.results.push({ file, changed, todos });
21
22
  }
23
+ /** A file that could not be parsed or transformed, and why. */
24
+ failFile(file, error, todos) {
25
+ this.results.push({ file, changed: false, todos, error });
26
+ }
22
27
  get files() {
23
28
  return this.results;
24
29
  }
25
30
  get changedCount() {
26
31
  return this.results.filter(r => r.changed).length;
27
32
  }
33
+ get failures() {
34
+ return this.results.filter(r => r.error !== undefined);
35
+ }
28
36
  get todos() {
29
37
  return this.results.flatMap(r => r.todos);
30
38
  }
@@ -44,9 +52,20 @@ export class Reporter {
44
52
  const lines = [];
45
53
  lines.push('');
46
54
  lines.push(chalk.bold(`bestax-migrate — ${label}`));
55
+ const failures = this.failures;
47
56
  lines.push(`${this.results.length} file(s) scanned, ` +
48
57
  `${chalk.green(String(this.changedCount))} transformed, ` +
58
+ (failures.length > 0
59
+ ? `${chalk.red(String(failures.length))} failed, `
60
+ : '') +
49
61
  `${chalk.yellow(String(this.todos.length))} TODO(s) left`);
62
+ if (failures.length > 0) {
63
+ lines.push('');
64
+ lines.push(chalk.red(` failed (${failures.length})`));
65
+ for (const { file, error } of failures) {
66
+ lines.push(` ${file} — ${error}`);
67
+ }
68
+ }
50
69
  for (const { rule, entries } of this.todosByRule()) {
51
70
  lines.push('');
52
71
  lines.push(chalk.yellow(` ${rule} (${entries.length})`));
@@ -60,6 +79,10 @@ export class Reporter {
60
79
  lines.push(`Search for ${chalk.bold('TODO(bestax-migrate)')} comments to finish the migration by hand,`);
61
80
  lines.push('or let the bestax-migrate skill walk them: https://bestax.io/docs/skills/intro');
62
81
  }
82
+ if (failures.length > 0) {
83
+ lines.push('');
84
+ lines.push(chalk.red(`${failures.length} file(s) failed and were left as they were: fix each one and run the codemod on it again.`));
85
+ }
63
86
  lines.push('');
64
87
  return lines.join('\n');
65
88
  }
@@ -25,6 +25,23 @@ export interface HtmlParseError {
25
25
  code: string;
26
26
  line: number;
27
27
  col: number;
28
+ /**
29
+ * For a `duplicate-attribute` on an element's start tag, which attribute
30
+ * it is. Absent where the tag made no element (an end tag, a second
31
+ * `<body>`), or the name can't be read back out of the source.
32
+ */
33
+ attribute?: DuplicateAttribute;
34
+ }
35
+ /**
36
+ * An attribute a start tag writes twice. A browser keeps the first and drops
37
+ * the rest, so `kept` is what the element has and `dropped` is the copy this
38
+ * error is about, each as the page writes it (`placeholder="Email"`).
39
+ */
40
+ export interface DuplicateAttribute {
41
+ /** The name, lowercased as the parser reads it. */
42
+ name: string;
43
+ kept: string;
44
+ dropped: string;
28
45
  }
29
46
  export interface ParsedHtml {
30
47
  kind: 'document' | 'fragment';
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/sources/_shared/html/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,KAAK,EACX,MAAM,QAAQ,CAAC;AAEhB,MAAM,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;AAC3D,MAAM,MAAM,OAAO,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;AACzD,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;AACzD,MAAM,MAAM,WAAW,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;AAC/D,MAAM,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B;;;OAGG;IACH,IAAI,EAAE,OAAO,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;IAC1D,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,cAAc,EAAE,SAAS,EAAE,CAAC;IAC5B,cAAc,EAAE,SAAS,EAAE,CAAC;IAC5B;;;OAGG;IACH,eAAe,EAAE,WAAW,EAAE,CAAC;IAC/B,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAQD,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,OAAO,CAErD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,QAAQ,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,WAAW,CAEzD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,EAAE,CAIxD;AAQD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CA6CpD"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/sources/_shared/html/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,KAAK,EACX,MAAM,QAAQ,CAAC;AAEhB,MAAM,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;AAC3D,MAAM,MAAM,OAAO,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;AACzD,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;AACzD,MAAM,MAAM,WAAW,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;AAC/D,MAAM,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B;;;OAGG;IACH,IAAI,EAAE,OAAO,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;IAC1D,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,cAAc,EAAE,SAAS,EAAE,CAAC;IAC5B,cAAc,EAAE,SAAS,EAAE,CAAC;IAC5B;;;OAGG;IACH,eAAe,EAAE,WAAW,EAAE,CAAC;IAC/B,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAQD,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,OAAO,CAErD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,QAAQ,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,WAAW,CAEzD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,EAAE,CAIxD;AA2ED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CA0DpD"}
@@ -39,8 +39,64 @@ export function childrenOf(node) {
39
39
  function find(nodes, tag) {
40
40
  return nodes.find((node) => isElement(node) && node.tagName === tag);
41
41
  }
42
+ /** The element whose start tag holds `offset`, anywhere under `node`. */
43
+ function elementAt(node, offset) {
44
+ for (const child of childrenOf(node)) {
45
+ if (!isElement(child))
46
+ continue;
47
+ const tag = child.sourceCodeLocation?.startTag;
48
+ if (tag && tag.startOffset < offset && offset < tag.endOffset) {
49
+ return child;
50
+ }
51
+ const inside = elementAt(child, offset);
52
+ if (inside)
53
+ return inside;
54
+ }
55
+ return undefined;
56
+ }
57
+ /** What may sit just before an attribute's name in a start tag. */
58
+ const BEFORE_NAME = /[\t\n\f\r /"']/;
59
+ /**
60
+ * The value written after an attribute's name, if any: `="a"`, `='a'`, `=a`,
61
+ * with the whitespace the tokenizer allows around the `=`.
62
+ */
63
+ const VALUE_AFTER_NAME = /[\t\n\f\r ]*=[\t\n\f\r ]*(?:"[^"]*"?|'[^']*'?|[^\t\n\f\r >]*)/y;
64
+ const asciiLower = (text) => text.replace(/[A-Z]/g, letter => letter.toLowerCase());
65
+ /**
66
+ * An attribute as the page writes it, from where its name starts. Read from
67
+ * the source rather than parse5's end offset, which stops at the name when
68
+ * the next attribute follows a quoted value with no space between them.
69
+ */
70
+ function writtenAt(source, start, name) {
71
+ VALUE_AFTER_NAME.lastIndex = start + name.length;
72
+ const end = VALUE_AFTER_NAME.exec(source)
73
+ ? VALUE_AFTER_NAME.lastIndex
74
+ : start + name.length;
75
+ return source.slice(start, end).trimEnd();
76
+ }
77
+ /**
78
+ * Which attribute a `duplicate-attribute` error is about. parse5 reports it
79
+ * where the second copy's name ends, and the tag keeps only the first copy,
80
+ * whose name and place it records; so the name is the longest of the
81
+ * element's that the source spells just before the error.
82
+ */
83
+ function duplicateAt(source, tree, offset) {
84
+ const attrs = elementAt(tree, offset)?.sourceCodeLocation?.attrs ?? {};
85
+ const name = Object.keys(attrs)
86
+ .filter(key => asciiLower(source.slice(offset - key.length, offset)) === key &&
87
+ BEFORE_NAME.test(source.charAt(offset - key.length - 1)))
88
+ .sort((a, b) => b.length - a.length)[0];
89
+ if (name === undefined)
90
+ return undefined;
91
+ return {
92
+ name,
93
+ kept: writtenAt(source, attrs[name].startOffset, name),
94
+ dropped: writtenAt(source, offset - name.length, name),
95
+ };
96
+ }
42
97
  export function parseHtml(source) {
43
98
  const errors = [];
99
+ const offsets = [];
44
100
  const options = {
45
101
  sourceCodeLocationInfo: true,
46
102
  scriptingEnabled: false,
@@ -50,12 +106,25 @@ export function parseHtml(source) {
50
106
  line: error.startLine,
51
107
  col: error.startCol,
52
108
  });
109
+ offsets.push(error.startOffset);
53
110
  },
54
111
  };
112
+ // Each duplicate is named once the tree is built, since its element is.
113
+ const nameDuplicates = (tree) => {
114
+ errors.forEach((error, index) => {
115
+ if (error.code !== 'duplicate-attribute')
116
+ return;
117
+ const attribute = duplicateAt(source, tree, offsets[index]);
118
+ if (attribute)
119
+ error.attribute = attribute;
120
+ });
121
+ };
55
122
  if (!isDocument(source)) {
123
+ const fragment = parseFragment(source, options);
124
+ nameDuplicates(fragment);
56
125
  return {
57
126
  kind: 'fragment',
58
- root: parseFragment(source, options),
127
+ root: fragment,
59
128
  head: [],
60
129
  htmlAttributes: [],
61
130
  bodyAttributes: [],
@@ -64,6 +133,7 @@ export function parseHtml(source) {
64
133
  };
65
134
  }
66
135
  const document = parse(source, options);
136
+ nameDuplicates(document);
67
137
  // The parser always builds `<html>`, and inside it `<head>` and `<body>`
68
138
  // (a `<frameset>` page has no body, and gets an empty one here).
69
139
  const html = find(document.childNodes, 'html');
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EAEV,UAAU,EACV,aAAa,EACb,SAAS,EACT,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAexB,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,+BAA+B,CAAC;AAiEvC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAYjE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAoB3B,CAAC;AAsBF;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAgBnE;AAYD,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAQR;AAsBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU3E;AA4KD,+EAA+E;AAC/E,eAAO,MAAM,YAAY,wCAAsC,CAAC;AAyxBhE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4BjD;AAID,+EAA+E;AAC/E,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAWD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,GAAG,SAAS,EACpC,OAAO,GAAE,gBAAqB,GAC7B,aAAa,CAsHf;AAgCD,yCAAyC;AACzC,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,GAAG,SAAS,EACpC,OAAO,GAAE,gBAAqB,GAC7B,UAAU,CAGZ"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EAEV,UAAU,EACV,aAAa,EACb,SAAS,EACT,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAexB,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,+BAA+B,CAAC;AAiEvC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAYjE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAoB3B,CAAC;AAsBF;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAgBnE;AAYD,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAQR;AAsBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU3E;AA4KD,+EAA+E;AAC/E,eAAO,MAAM,YAAY,wCAAsC,CAAC;AAwyBhE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4BjD;AAID,+EAA+E;AAC/E,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAWD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,GAAG,SAAS,EACpC,OAAO,GAAE,gBAAqB,GAC7B,aAAa,CAsHf;AAgCD,yCAAyC;AACzC,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,GAAG,SAAS,EACpC,OAAO,GAAE,gBAAqB,GAC7B,UAAU,CAGZ"}
@@ -278,7 +278,7 @@ const SCRIPT_PATTERNS = [
278
278
  [
279
279
  'carousel',
280
280
  /carousel/i,
281
- "drives a carousel: bestax's `Carousel` takes its slides as `CarouselItem` children and renders its own arrows and indicators (`autoplay` for a timed one); its styles ship in bestax's `extras.css` and `bestax.css`, not in Bulma's stylesheet",
281
+ "drives a carousel: bestax's `Carousel` takes its slides as `CarouselItem` children and renders its own arrows and indicators (`autoplay` for a timed one); for several slides in view it takes `itemsToShow`, with a variant per viewport such as `itemsToShowMobile`, and `itemsToScroll`, bulma-carousel's `slidesToShow` and `slidesToScroll`; it loops unless `repeat={false}`, where bulma-carousel stops at the ends unless `loop` or `infinite` is set; its styles ship in bestax's `extras.css` and `bestax.css`, not in Bulma's stylesheet",
282
282
  ],
283
283
  [
284
284
  'theme',
@@ -952,9 +952,24 @@ function relativeUrlNotes(root, dir, notes) {
952
952
  relativeUrlNotes(child, dir, notes);
953
953
  }
954
954
  }
955
+ /**
956
+ * The parse errors, once per code with how many more there are, except a
957
+ * duplicate attribute whose name is known: each of those is its own note,
958
+ * naming the attribute and the copy the element keeps, since no two are
959
+ * about the same thing and reading the DOM is the only other way to tell.
960
+ */
955
961
  function parseNotes(parsed, notes) {
956
962
  const byCode = new Map();
957
963
  for (const error of parsed.errors) {
964
+ const duplicate = error.attribute;
965
+ if (duplicate) {
966
+ notes.push({
967
+ rule: htmlRuleId('html-parse', error.code),
968
+ message: `the HTML writes \`${duplicate.name}\` more than once on one element, and a browser keeps the first, \`${quote(duplicate.kept)}\`, and drops this one, \`${quote(duplicate.dropped)}\`: the component keeps the first too, so if the page meant the other, change it there`,
969
+ line: error.line,
970
+ });
971
+ continue;
972
+ }
958
973
  const seen = byCode.get(error.code);
959
974
  if (seen)
960
975
  seen.count++;
package/dist/types.d.ts CHANGED
@@ -96,6 +96,8 @@ export interface FileResult {
96
96
  file: string;
97
97
  changed: boolean;
98
98
  todos: TodoEntry[];
99
+ /** Why the file could not be parsed or transformed, when it couldn't. */
100
+ error?: string;
99
101
  }
100
102
  /** Collects TODO entries for the file currently being transformed. */
101
103
  export interface TodoCollector {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bestax-migrate",
3
- "version": "2.31.1",
3
+ "version": "2.31.3",
4
4
  "description": "Codemods for migrating existing React apps from other Bulma libraries to @allxsmith/bestax-bulma",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,27 +34,27 @@
34
34
  "url": "https://github.com/allxsmith/bestax/issues"
35
35
  },
36
36
  "dependencies": {
37
- "@babel/parser": "^7.29.8",
38
- "chalk": "^6.0.0",
37
+ "@babel/parser": "^7.29.9",
38
+ "chalk": "^6.0.1",
39
39
  "commander": "^15.0.0",
40
40
  "jscodeshift": "^17.3.0",
41
41
  "parse5": "^7.3.0",
42
42
  "property-information": "^7.2.0",
43
- "@allxsmith/bestax-bulma": "^5.41.0"
43
+ "@allxsmith/bestax-bulma": "^5.44.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@jest/globals": "^30.0.0",
47
47
  "@types/jest": "^30.0.0",
48
48
  "@types/jscodeshift": "^17.3.0",
49
- "@types/node": "^26.6.4",
50
- "@types/react": "^19.2.18",
51
- "@types/react-dom": "^19.2.5",
49
+ "@types/node": "^26.6.5",
50
+ "@types/react": "^19.3.0",
51
+ "@types/react-dom": "^19.3.0",
52
52
  "classnames": "^2.5.1",
53
53
  "clsx": "^2.1.1",
54
54
  "jest": "^30.2.0",
55
- "react": "^19.2.8",
56
- "react-dom": "^19.2.8",
57
- "ts-jest": "^29.4.14",
55
+ "react": "^19.3.1",
56
+ "react-dom": "^19.3.1",
57
+ "ts-jest": "^29.4.15",
58
58
  "typescript": "^6.0.3"
59
59
  },
60
60
  "publishConfig": {
@@ -78,5 +78,5 @@
78
78
  "clean": "rimraf dist",
79
79
  "release": "npx semantic-release"
80
80
  },
81
- "readme": "# bestax-migrate\n\n[![npm version](https://img.shields.io/npm/v/bestax-migrate.svg)](https://www.npmjs.com/package/bestax-migrate)\n[![npm downloads](https://img.shields.io/npm/dm/bestax-migrate.svg)](https://www.npmjs.com/package/bestax-migrate)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Socket Badge](https://badge.socket.dev/npm/package/bestax-migrate/latest)](https://socket.dev/npm/package/bestax-migrate/overview)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/allxsmith/bestax/badge)](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14361/badge)](https://www.bestpractices.dev/projects/14361)\n[![npm provenance](https://img.shields.io/badge/npm-provenance-3fb950.svg)](https://www.npmjs.com/package/bestax-migrate#provenance)\n[![Security policy](https://img.shields.io/badge/security-policy-blue.svg)](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported sources:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n| [`rbx`](https://github.com/dfee/rbx) (v2) | ✅ Full export surface mapped (a few patterns are flagged as TODOs) |\n| [`bloomer`](https://github.com/AlgusDark/bloomer) (0.6) | ✅ All 108 exports mapped (Font Awesome 4 icons and Bulma 0.4's `Nav` are flagged) |\n| `bulma-classes`: Bulma's classes on plain JSX, no library | ✅ Converts an element only when bestax renders the same markup; the rest is flagged |\n| `bulma-classes` on HTML: static pages and templates styled with Bulma | ✅ Writes a React component beside each page; its head and scripts are flagged |\n\nPass the source as the first argument: `bestax-migrate <source> <paths…>`.\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n# …or, for an rbx or bloomer app\npnpm dlx bestax-migrate rbx src/ --dry\npnpm dlx bestax-migrate bloomer src/ --dry\n# …or, for an app that writes Bulma's classes on plain JSX\npnpm dlx bestax-migrate bulma-classes src/ --dry\n# …or, for static HTML pages styled with Bulma: name a page, or walk a folder with -e html\n# (-e html,htm takes .htm pages too)\npnpm dlx bestax-migrate bulma-classes src/pages/login/index.html --dry\npnpm dlx bestax-migrate bulma-classes -e html src/pages/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite the source library's imports to `@allxsmith/bestax-bulma` (including destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …) — and, for bloomer, its flat names onto bestax's compounds (`CardHeaderTitle` → `Card.Header.Title`)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`) and bloomer's `isDisplay`/`isHidden` strings, arrays and objects (`isHidden=\"touch\"` → `visibilityTouch=\"hidden\"`)\n- for `bulma-classes`, turn raw Bulma markup into components and props (`<a className=\"button is-primary\">` → `<Button as=\"a\" color=\"primary\">`, `<p className=\"has-text-centered mt-4\">` → `<Paragraph textAlign=\"centered\" mt=\"4\">`), converting an element only when bestax renders exactly the same markup\n- for `bulma-classes` on an HTML page, write a React module beside it (`index.html` → `index.tsx`, or `.jsx` when the package isn't TypeScript) whose default export is the page's body as a component (`LoginPage`), converted as the same markup written as JSX would be. The page is never changed and an existing module is never overwritten; the head, the `<html>` and `<body>` attributes, the stylesheets and scripts it loads, and inline handlers are left as `html-*` TODOs, except that under `--css bestax` or `--css bulma` the module imports the stylesheet in place of the page's Bulma `<link>`\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove the source library, add `@allxsmith/bestax-bulma`, raise a declared pre-1.0 `bulma` to `^1` (adding it only when your sources import `bulma/…` directly — otherwise it arrives transitively), and swap the dead `node-sass` for dart `sass` (no install is ever run). Migrating **rbx** is what frees an app from Bulma 0.7.5: rbx pinned it as a _direct_ dependency, so the app could not choose its own Bulma version at all. The four Bulma extensions rbx also pulled in (`bulma-badge`, `bulma-divider`, `bulma-pageloader`, `bulma-tooltip`) are **reported rather than removed** — bestax replaces all four, but a manifest entry is a deliberate declaration and your own Sass may import them. A **bloomer** app declared its own Bulma 0.6, which is what gets bumped. For **bulma-classes** there is no library to remove, and by default (`--css keep`) the app's Bulma version, Sass and stylesheet imports all stay: its own Bulma already styles every class a converted element renders. `--css bestax` or `--css bulma` moves it to Bulma v1 like the other sources\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the migration guide ([react-bulma-components](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), [rbx](https://bestax.io/docs/guides/getting-started/migration/rbx), [bloomer](https://bestax.io/docs/guides/getting-started/migration/bloomer), [Bulma classes](https://bestax.io/docs/guides/getting-started/migration/bulma-classes)), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n### Through the Codemod Registry\n\nbestax-migrate is also in the [Codemod Registry](https://app.codemod.com/registry) as `bestax-migrate`, for teams that run their migrations through Codemod. That package runs this CLI from npm at a pinned release, which can trail the newest one:\n\n```bash\n# Preview (no writes)\nnpx codemod bestax-migrate --workflow preview --param source=react-bulma-components\n# Apply\nnpx codemod bestax-migrate --param source=react-bulma-components\n```\n\nRun it from your app root. `--param paths=\"src lib\"` (default `src`) and `--param options=\"--css keep\"` pass through to the CLI. Codemod's own `--dry-run` skips the shell step this package runs, so use the `preview` workflow for a dry run.\n\n## Options\n\n| Flag | Description |\n| -------------------- | ---------------------------------------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout, still writing them unless `--dry` is set |\n| `--extensions`, `-e` | File extensions to include in a folder (default `js,jsx,ts,tsx,scss,sass`; `html` for pages) |\n| `--css <mode>` | Stylesheet target: `bestax`, `bulma`, or `keep` (default `bestax`; `keep` for `bulma-classes`) |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components) · [rbx migration guide](https://bestax.io/docs/guides/getting-started/migration/rbx) · [bloomer migration guide](https://bestax.io/docs/guides/getting-started/migration/bloomer) · [Bulma classes migration guide](https://bestax.io/docs/guides/getting-started/migration/bulma-classes).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## Telemetry\n\n`bestax-migrate` can send one **anonymous** usage event after a successful\nrun — only if you opt in when asked (once, on an interactive terminal). The\nevent is just the run's shape — source library, CSS mode, the dry/deps flags, a\ncapped changed-file count, and per-rule TODO counts — plus the CLI version,\nNode major version, and OS platform name. Never file paths, file contents,\ncode, names, IPs, or machine IDs, and no identifier exists that could link two\nevents together. Opt out any time with `--no-telemetry`,\n`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.\n\nFull disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)\n\n## License\n\nSource code licensed MIT\n"
81
+ "readme": "# bestax-migrate\n\n[![npm version](https://img.shields.io/npm/v/bestax-migrate.svg)](https://www.npmjs.com/package/bestax-migrate)\n[![npm downloads](https://img.shields.io/npm/dm/bestax-migrate.svg)](https://www.npmjs.com/package/bestax-migrate)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Socket Badge](https://badge.socket.dev/npm/package/bestax-migrate/latest)](https://socket.dev/npm/package/bestax-migrate/overview)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/allxsmith/bestax/badge)](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14361/badge)](https://www.bestpractices.dev/projects/14361)\n[![npm provenance](https://img.shields.io/badge/npm-provenance-3fb950.svg)](https://www.npmjs.com/package/bestax-migrate#provenance)\n[![Security policy](https://img.shields.io/badge/security-policy-blue.svg)](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported sources:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n| [`rbx`](https://github.com/dfee/rbx) (v2) | ✅ Full export surface mapped (a few patterns are flagged as TODOs) |\n| [`bloomer`](https://github.com/AlgusDark/bloomer) (0.6) | ✅ All 108 exports mapped (Font Awesome 4 icons and Bulma 0.4's `Nav` are flagged) |\n| `bulma-classes`: Bulma's classes on plain JSX, no library | ✅ Converts an element only when bestax renders the same markup; the rest is flagged |\n| `bulma-classes` on HTML: static pages and templates styled with Bulma | ✅ Writes a React component beside each page; its head and scripts are flagged |\n\nPass the source as the first argument: `bestax-migrate <source> <paths…>`.\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n# …or, for an rbx or bloomer app\npnpm dlx bestax-migrate rbx src/ --dry\npnpm dlx bestax-migrate bloomer src/ --dry\n# …or, for an app that writes Bulma's classes on plain JSX\npnpm dlx bestax-migrate bulma-classes src/ --dry\n# …or, for static HTML pages styled with Bulma: name a page, or walk a folder with -e html\n# (-e html,htm takes .htm pages too)\npnpm dlx bestax-migrate bulma-classes src/pages/login/index.html --dry\npnpm dlx bestax-migrate bulma-classes -e html src/pages/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite the source library's imports to `@allxsmith/bestax-bulma` (including destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …) — and, for bloomer, its flat names onto bestax's compounds (`CardHeaderTitle` → `Card.Header.Title`)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`) and bloomer's `isDisplay`/`isHidden` strings, arrays and objects (`isHidden=\"touch\"` → `visibilityTouch=\"hidden\"`)\n- for `bulma-classes`, turn raw Bulma markup into components and props (`<a className=\"button is-primary\">` → `<Button as=\"a\" color=\"primary\">`, `<p className=\"has-text-centered mt-4\">` → `<Paragraph textAlign=\"centered\" mt=\"4\">`), converting an element only when bestax renders exactly the same markup\n- for `bulma-classes` on an HTML page, write a React module beside it (`index.html` → `index.tsx`, or `.jsx` when the package isn't TypeScript) whose default export is the page's body as a component (`LoginPage`), converted as the same markup written as JSX would be. The page is never changed and an existing module is never overwritten; the head, the `<html>` and `<body>` attributes, the stylesheets and scripts it loads, and inline handlers are left as `html-*` TODOs, except that under `--css bestax` or `--css bulma` the module imports the stylesheet in place of the page's Bulma `<link>`\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove the source library, add `@allxsmith/bestax-bulma`, raise a declared pre-1.0 `bulma` to `^1` (adding it only when your sources import `bulma/…` directly — otherwise it arrives transitively), and swap the dead `node-sass` for dart `sass` (no install is ever run). Migrating **rbx** is what frees an app from Bulma 0.7.5: rbx pinned it as a _direct_ dependency, so the app could not choose its own Bulma version at all. The four Bulma extensions rbx also pulled in (`bulma-badge`, `bulma-divider`, `bulma-pageloader`, `bulma-tooltip`) are **reported rather than removed** — bestax replaces all four, but a manifest entry is a deliberate declaration and your own Sass may import them. A **bloomer** app declared its own Bulma 0.6, which is what gets bumped. For **bulma-classes** there is no library to remove, and by default (`--css keep`) the app's Bulma version, Sass and stylesheet imports all stay: its own Bulma already styles every class a converted element renders. `--css bestax` or `--css bulma` moves it to Bulma v1 like the other sources\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the migration guide ([react-bulma-components](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), [rbx](https://bestax.io/docs/guides/getting-started/migration/rbx), [bloomer](https://bestax.io/docs/guides/getting-started/migration/bloomer), [Bulma classes](https://bestax.io/docs/guides/getting-started/migration/bulma-classes)), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n### Through the Codemod Registry\n\nbestax-migrate is also in the [Codemod Registry](https://app.codemod.com/registry) as `bestax-migrate`, for teams that run their migrations through Codemod. That package runs this CLI from npm at a pinned release, which can trail the newest one:\n\n```bash\n# Preview (no writes)\nnpx codemod bestax-migrate --workflow preview --param source=react-bulma-components\n# Apply\nnpx codemod bestax-migrate --param source=react-bulma-components\n```\n\nRun it from your app root. `--param paths=\"src lib\"` (default `src`) and `--param options=\"--css keep\"` pass through to the CLI. Codemod's own `--dry-run` skips the shell step this package runs, so use the `preview` workflow for a dry run.\n\n## Options\n\n| Flag | Description |\n| -------------------- | ---------------------------------------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout, still writing them unless `--dry` is set |\n| `--extensions`, `-e` | File extensions to include in a folder (default `js,jsx,ts,tsx,scss,sass`; `html` for pages) |\n| `--css <mode>` | Stylesheet target: `bestax`, `bulma`, or `keep` (default `bestax`; `keep` for `bulma-classes`) |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## Exit codes\n\n| Code | Meaning |\n| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `0` | The run finished. TODOs are expected output and leave it at 0 |\n| `1` | A file could not be parsed or transformed. It is marked `✖`, listed in the report and left as it was, and every other file is still migrated. Fix it and run the codemod on it again. A run like that sends no telemetry |\n| `1` | The command was refused before the run: an unknown source, flag or `--css` mode, a path that isn't there, an HTML page for a source that reads none, or Node older than 22 |\n| `130` | Ctrl-C at the telemetry question, which comes after the run |\n\n`--help` lists them too. With `--dry`, a file that fails to parse still makes the exit code 1, so a preview in CI catches it. A file the run can't read at all stops it there instead, with Node's error and exit code 1, before `package.json` is updated.\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components) · [rbx migration guide](https://bestax.io/docs/guides/getting-started/migration/rbx) · [bloomer migration guide](https://bestax.io/docs/guides/getting-started/migration/bloomer) · [Bulma classes migration guide](https://bestax.io/docs/guides/getting-started/migration/bulma-classes).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## Telemetry\n\n`bestax-migrate` can send one **anonymous** usage event after a successful\nrun — only if you opt in when asked (once, on an interactive terminal). The\nevent is just the run's shape — source library, CSS mode, the dry/deps flags, a\ncapped changed-file count, and per-rule TODO counts — plus the CLI version,\nNode major version, and OS platform name. Never file paths, file contents,\ncode, names, IPs, or machine IDs, and no identifier exists that could link two\nevents together. Opt out any time with `--no-telemetry`,\n`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.\n\nFull disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)\n\n## License\n\nSource code licensed MIT\n"
82
82
  }