@mittwald/flow-codemods 1.3.0-next.19 → 1.3.0-next.20

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
@@ -184,5 +184,6 @@ npx @mittwald/flow-codemods@latest to-remote-package src
184
184
 
185
185
  `0` on success, `1` on a refusal — dirty tree, unresolvable revision, failed
186
186
  install, an unknown id, or an id whose migration has no codemod — and `1` when a
187
- codemod reports an error, is declined for every file it looked at, or found
188
- nothing under `path` to process.
187
+ codemod reports an error, found nothing under `path` to process, or declined
188
+ every file it looked at. "Every" means every: a decline among files the
189
+ 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.
@@ -191,9 +191,9 @@ export const runUpgrade = async (parsed, deps) => {
191
191
  if (result.changed > 0) {
192
192
  changedCount += 1;
193
193
  }
194
- // Same three-way distinction as the single-codemod command: "0 changed" on
195
- // its own would read as success where nothing was looked at, or where the
196
- // transform declined everything it saw.
194
+ // Same three-way distinction as the single-codemod command, and the same
195
+ // reading of "declined all": `unmodified > 0` proves the transform read
196
+ // files and handed them back, so it did not bail on the tree.
197
197
  if (result.errors > 0) {
198
198
  hadFailure = true;
199
199
  log(` ${entry.id}: ${result.errors} file(s) failed to transform`);
@@ -202,7 +202,9 @@ export const runUpgrade = async (parsed, deps) => {
202
202
  hadFailure = true;
203
203
  log(` ${entry.id}: no files under ${path} were processed`);
204
204
  }
205
- else if (result.changed === 0 && result.skipped > 0) {
205
+ else if (result.changed === 0 &&
206
+ result.unmodified === 0 &&
207
+ result.skipped > 0) {
206
208
  hadFailure = true;
207
209
  log(` ${entry.id}: declined all ${result.skipped} file(s) it looked at`);
208
210
  }
@@ -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.3.0-next.19",
3
+ "version": "1.3.0-next.20",
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": "412ba9031ad37839847167b9a9b61253a48d3f1f"
41
+ "gitHead": "89522e32233bb3f92accf31e4c338898bd13296c"
42
42
  }