@mittwald/flow-codemods 1.2.17 → 1.2.18

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
@@ -131,5 +131,6 @@ npx @mittwald/flow-codemods@latest to-remote-package src
131
131
 
132
132
  `0` on success, `1` on a refusal — dirty tree, unresolvable revision, failed
133
133
  install, an unknown id, or an id whose migration has no codemod — and `1` when a
134
- codemod reports an error, is declined for every file it looked at, or found
135
- nothing under `path` to process.
134
+ codemod reports an error, found nothing under `path` to process, or declined
135
+ every file it looked at. "Every" means every: a decline among files the
136
+ transform did read is a per-file fact, not a failed run.
@@ -78,14 +78,19 @@ export const runSingleCodemod = async (parsed, { cwd, log, run = runCodemod }) =
78
78
  }
79
79
  // The same trap as `processedNothing`, one field over: a transform that
80
80
  // declines a file by returning nothing counts as `skipped`, not `unmodified`.
81
- // If every file was skipped and none changed, "0 file(s) changed" would read
81
+ // If every file was declined and none changed, "0 file(s) changed" would read
82
82
  // as a clean no-op run when in fact the transform bailed on everything.
83
- if (result.changed === 0 && result.skipped > 0) {
83
+ //
84
+ // "All" means all: `unmodified > 0` says the transform read those files and
85
+ // handed them back, so a decline among them is a per-file fact, not a failed
86
+ // run.
87
+ if (result.changed === 0 && result.unmodified === 0 && result.skipped > 0) {
84
88
  log(`${id}: the transform declined all ${result.skipped} file(s) it looked at, and changed none.`);
85
89
  return 1;
86
90
  }
87
91
  const skipped = result.skipped > 0 ? `, ${result.skipped} skipped` : "";
88
- const summary = `${id}: ${result.changed} file(s) changed, ${result.unmodified} unchanged${skipped}.`;
92
+ const empty = result.empty > 0 ? `, ${result.empty} empty` : "";
93
+ const summary = `${id}: ${result.changed} file(s) changed, ${result.unmodified} unchanged${skipped}${empty}.`;
89
94
  // Only a catalogued id has a migration guide entry to point at — a transform
90
95
  // like `to-remote-package` with no catalogue entry has no anchor in
91
96
  // `MIGRATION.md` to link, so pointing there would be a dead link.
@@ -168,9 +168,9 @@ export const runUpgrade = async (parsed, deps) => {
168
168
  if (result.changed > 0) {
169
169
  changedCount += 1;
170
170
  }
171
- // Same three-way distinction as the single-codemod command: "0 changed" on
172
- // its own would read as success where nothing was looked at, or where the
173
- // transform declined everything it saw.
171
+ // Same three-way distinction as the single-codemod command, and the same
172
+ // reading of "declined all": `unmodified > 0` proves the transform read
173
+ // files and handed them back, so it did not bail on the tree.
174
174
  if (result.errors > 0) {
175
175
  hadFailure = true;
176
176
  log(` ${entry.id}: ${result.errors} file(s) failed to transform`);
@@ -179,7 +179,9 @@ export const runUpgrade = async (parsed, deps) => {
179
179
  hadFailure = true;
180
180
  log(` ${entry.id}: no files under ${path} were processed`);
181
181
  }
182
- else if (result.changed === 0 && result.skipped > 0) {
182
+ else if (result.changed === 0 &&
183
+ result.unmodified === 0 &&
184
+ result.skipped > 0) {
183
185
  hadFailure = true;
184
186
  log(` ${entry.id}: declined all ${result.skipped} file(s) it looked at`);
185
187
  }
@@ -16,8 +16,15 @@ export interface CodemodOptions {
16
16
  export interface CodemodResult {
17
17
  changed: number;
18
18
  unmodified: number;
19
- /** Files the transform declined by returning nothing. */
19
+ /**
20
+ * Files the transform declined by returning nothing — empty ones excluded.
21
+ *
22
+ * Both come back from jscodeshift as `skip`. A caller may report this field
23
+ * as "the transform bailed on these"; it must not report `empty` that way.
24
+ */
20
25
  skipped: number;
26
+ /** Files with no content, which no transform can do anything with. */
27
+ empty: number;
21
28
  errors: number;
22
29
  /** True when jscodeshift accounted for no file at all — see below. */
23
30
  processedNothing: boolean;
@@ -1,4 +1,5 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { extname, join } from "node:path";
2
3
  import { fileURLToPath, pathToFileURL } from "node:url";
3
4
  import { unknownCodemodMessage } from "../catalog/entries.js";
4
5
  // jscodeshift ships no types for its Runner — `allowJs` in this repo's shared
@@ -54,6 +55,61 @@ const transformPath = (id) => findTransform(id) ?? `${packageRoot}src/tools/${id
54
55
  * module's own `existsSync` check below.
55
56
  */
56
57
  export const transformExists = (id) => existsSync(transformPath(id));
58
+ /**
59
+ * The extensions the walk takes, and the directories it stays out of.
60
+ *
61
+ * Both are handed to jscodeshift below — without them its walk takes every file
62
+ * under the path, and since `upgrade` runs codemods after the install,
63
+ * `node_modules` is freshly populated underneath; non-JS files then fail to
64
+ * parse, which shows up as `errors > 0` and hides the real change count.
65
+ *
66
+ * `emptySourceFiles` reads the same two constants so that its walk and
67
+ * jscodeshift's agree by construction.
68
+ */
69
+ const extensions = ["js", "jsx", "ts", "tsx", "cjs", "mjs", "cts", "mts"];
70
+ const ignoredDirectories = ["node_modules", "dist", ".git"];
71
+ const sourceExtensions = new Set(extensions.map((extension) => `.${extension}`));
72
+ const ignoredDirectorySet = new Set(ignoredDirectories);
73
+ /**
74
+ * How many source files under `path` hold nothing at all.
75
+ *
76
+ * Jscodeshift's worker tests a transform's result for truthiness
77
+ * (`updateStatus(out ? 'nochange' : 'skip', file)`), and on an empty file both
78
+ * `root.toSource()` and `fileInfo.source` are `""` — so an empty file lands in
79
+ * `skip`, indistinguishable there from a transform that declined it.
80
+ *
81
+ * Counted with a walk of our own rather than filtered out of jscodeshift's
82
+ * input: this number only ever explains a count. If the two walks disagree, the
83
+ * worst outcome is a slightly wrong explanation, never a file that should have
84
+ * been transformed and was not.
85
+ */
86
+ const emptySourceFiles = (path) => {
87
+ let found = 0;
88
+ const visit = (candidate) => {
89
+ let stats;
90
+ try {
91
+ stats = statSync(candidate);
92
+ }
93
+ catch {
94
+ // A broken symlink or a file removed mid-walk. jscodeshift skips these
95
+ // too ("Skipping path … which does not exist").
96
+ return;
97
+ }
98
+ if (stats.isDirectory()) {
99
+ for (const child of readdirSync(candidate)) {
100
+ if (!ignoredDirectorySet.has(child)) {
101
+ visit(join(candidate, child));
102
+ }
103
+ }
104
+ return;
105
+ }
106
+ if (stats.size === 0 && sourceExtensions.has(extname(candidate))) {
107
+ found += 1;
108
+ }
109
+ };
110
+ visit(path);
111
+ return found;
112
+ };
57
113
  /**
58
114
  * Runs one codemod over a path.
59
115
  *
@@ -105,21 +161,22 @@ export const runCodemod = async ({ id, path, dry = false, print = false, }) => {
105
161
  silent: true,
106
162
  dry,
107
163
  print,
108
- // Without these the walk takes every file under the path. `upgrade` runs
109
- // codemods after the install, so `node_modules` is freshly populated
110
- // underneath — and non-JS files fail to parse, which shows up as
111
- // `errors > 0` and hides the real change count.
112
- extensions: "js,jsx,ts,tsx,cjs,mjs,cts,mts",
113
- ignorePattern: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
164
+ extensions: extensions.join(","),
165
+ ignorePattern: ignoredDirectories.map((name) => `**/${name}/**`),
114
166
  }));
115
167
  }
116
168
  catch (error) {
117
169
  throw new Error(`Running ${id} failed: ${error instanceof Error ? error.message : error}`, { cause: error });
118
170
  }
171
+ // Clamped rather than subtracted outright: the two walks are independent, so
172
+ // a file that appeared or emptied between them must not push `skipped` below
173
+ // zero.
174
+ const empty = Math.min(emptySourceFiles(path), stats.skip);
119
175
  return {
120
176
  changed: stats.ok,
121
177
  unmodified: stats.nochange,
122
- skipped: stats.skip,
178
+ skipped: stats.skip - empty,
179
+ empty,
123
180
  errors: stats.error,
124
181
  processedNothing: stats.ok + stats.nochange + stats.skip + stats.error === 0,
125
182
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/flow-codemods",
3
- "version": "1.2.17",
3
+ "version": "1.2.18",
4
4
  "type": "module",
5
5
  "description": "Codemods and an upgrade CLI for consumers of Flow, mittwald's design system",
6
6
  "homepage": "https://flow.mittwald.de",
@@ -38,5 +38,5 @@
38
38
  "vitest": "^4.1.11",
39
39
  "yaml": "^2.8.1"
40
40
  },
41
- "gitHead": "069b32e72bac3917e27c54e9df65046f6c91ddeb"
41
+ "gitHead": "f5f05b9f490f10b0f4b1298efc7c8bf8fa8be555"
42
42
  }