@sps-woodland/codemods 8.53.7 → 8.53.8

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.
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // -----------------------------------------------------------------------
3
+ // TEMPORARY TOOLING — slated for removal once consuming apps have migrated
4
+ // off the @sps-woodland/<name> wrapper packages (buttons, cards,
5
+ // content-row, etc.) and those wrapper packages are deleted from the
6
+ // monorepo. Do not build new features on top of this script; it exists
7
+ // purely to make that one-time migration mechanical. Delete this file (and
8
+ // its "bin" entry in package.json) once the wrapper packages are gone.
9
+ // -----------------------------------------------------------------------
10
+ //
11
+ // AST-based find-and-replace (via jscodeshift): rewrite imports of
12
+ // @sps-woodland/<wrapper> packages (thin re-export shims) to import
13
+ // directly from the @sps-woodland/core subpath they wrap. Does NOT touch
14
+ // anything else: no @spscommerce/ds-react, no imported-name changes, no
15
+ // JSX, no package.json/lockfile edits.
16
+ //
17
+ // This is intentionally NOT part of the main jscodeshift transform
18
+ // pipeline (main.js / src/transform.ts) — that pipeline migrates
19
+ // @spscommerce/ds-react -> the wrapper packages, a different step with a
20
+ // different data table. This script is its own bin so it can be run (and
21
+ // versioned/removed) independently of that pipeline.
22
+ //
23
+ // Usage (from a consuming app's repo root):
24
+ // pnpm dlx --package=@sps-woodland/codemods migrate-wrapper-imports [srcDir] [--write] [--verbose]
25
+ //
26
+ // Defaults to a dry run (prints what would change). Pass --write to
27
+ // actually modify files on disk.
28
+
29
+ import { readdir, readFile, writeFile } from "node:fs/promises";
30
+ import path from "node:path";
31
+ import jscodeshiftModule from "jscodeshift";
32
+
33
+ const jscodeshift = jscodeshiftModule.default ?? jscodeshiftModule;
34
+
35
+ // --- Wrapper package -> @sps-woodland/core subpath mapping -----------------
36
+ // Derived by reading each wrapper's own src/index.ts re-export statement in
37
+ // this monorepo (packages/@sps-woodland/<name>/src/index.ts) — do not
38
+ // assume the mapping is "same name", some are exceptions.
39
+ //
40
+ // jsSubpath: subpath used for the bare package import, e.g.
41
+ // from "@sps-woodland/cards" -> from "@sps-woodland/core/cards"
42
+ // cssSubpath: subpath used when importing "<wrapperName>/style.css" — for
43
+ // every package this matches jsSubpath EXCEPT "buttons", whose CSS still
44
+ // lives under the plural "buttons" path even though the JS export is the
45
+ // singular "button".
46
+ const WRAPPER_TO_CORE = {
47
+ "action-bar": { jsSubpath: "action-bar", cssSubpath: "action-bar" },
48
+ "add-to-list": { jsSubpath: "add-to-list", cssSubpath: "add-to-list" },
49
+ "buttons": { jsSubpath: "button", cssSubpath: "buttons" }, // exception: singular JS, plural CSS
50
+ "cards": { jsSubpath: "cards", cssSubpath: "cards" },
51
+ "column-manager": { jsSubpath: "column-manager", cssSubpath: "column-manager" },
52
+ "content-row": { jsSubpath: "content-row", cssSubpath: "content-row" },
53
+ "content-tiles": { jsSubpath: "content-tiles", cssSubpath: "content-tiles" },
54
+ "dropdowns": { jsSubpath: "dropdowns", cssSubpath: "dropdowns" },
55
+ "editable-table": { jsSubpath: "editable-table", cssSubpath: "editable-table" },
56
+ "feedback-block": { jsSubpath: "feedback-block", cssSubpath: "feedback-block" },
57
+ "file-upload": { jsSubpath: "file-upload", cssSubpath: "file-upload" },
58
+ "filter-tiles": { jsSubpath: "filter-tiles", cssSubpath: "filter-tiles" },
59
+ "focused-task": { jsSubpath: "focused-task", cssSubpath: "focused-task" },
60
+ "growler": { jsSubpath: "growler", cssSubpath: "growler" },
61
+ "key-value-set": { jsSubpath: "key-value-set", cssSubpath: "key-value-set" },
62
+ "list-bar": { jsSubpath: "list-bar", cssSubpath: "list-bar" },
63
+ "matrix": { jsSubpath: "matrix", cssSubpath: "matrix" },
64
+ "modal": { jsSubpath: "modal", cssSubpath: "modal" },
65
+ "page-title": { jsSubpath: "page-title", cssSubpath: "page-title" },
66
+ "photo": { jsSubpath: "photo", cssSubpath: "photo" },
67
+ "product-bar": { jsSubpath: "product-bar", cssSubpath: "product-bar" },
68
+ "product-bar-vertical": { jsSubpath: "product-bar-vertical", cssSubpath: "product-bar-vertical" },
69
+ "progress-indicators": { jsSubpath: "progress-indicators", cssSubpath: "progress-indicators" },
70
+ "rich-text-editor": { jsSubpath: "rich-text-editor", cssSubpath: "rich-text-editor" },
71
+ "side-nav": { jsSubpath: "side-nav", cssSubpath: "side-nav" },
72
+ "sorting-header": { jsSubpath: "sorting-header", cssSubpath: "sorting-header" },
73
+ "tabs": { jsSubpath: "tabs", cssSubpath: "tabs" },
74
+ "tags": { jsSubpath: "tags", cssSubpath: "tags" },
75
+ "zero-state": { jsSubpath: "zero-state", cssSubpath: "zero-state" },
76
+ };
77
+ // Explicitly NOT wrappers — leave untouched: @sps-woodland/core (the target
78
+ // itself), @sps-woodland/tokens, @sps-woodland/illustrations,
79
+ // @sps-woodland/codemods (these ship real, independent code, not re-exports).
80
+
81
+ const SCOPE = "@sps-woodland";
82
+ const EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
83
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "lib", "coverage"]);
84
+
85
+ function resolveNewSource(oldSource) {
86
+ if (!oldSource.startsWith(`${SCOPE}/`)) return null;
87
+ const rest = oldSource.slice(SCOPE.length + 1); // e.g. "cards" or "cards/style.css"
88
+ const slashIdx = rest.indexOf("/");
89
+ const name = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
90
+ const subRest = slashIdx === -1 ? "" : rest.slice(slashIdx); // "" or "/style.css" or deeper
91
+
92
+ const mapping = WRAPPER_TO_CORE[name];
93
+ if (!mapping) return null; // not a known wrapper package — leave untouched
94
+
95
+ if (subRest === "") return `${SCOPE}/core/${mapping.jsSubpath}`;
96
+ if (subRest === "/style.css") return `${SCOPE}/core/${mapping.cssSubpath}/style.css`;
97
+ // Unknown deep subpath (rare) — map using the JS subpath and preserve
98
+ // whatever followed the package name, best-effort.
99
+ return `${SCOPE}/core/${mapping.jsSubpath}${subRest}`;
100
+ }
101
+
102
+ // Parses the file with jscodeshift (Babel/TSX parser) and rewrites only the
103
+ // string-literal module specifier of matching nodes in place — imported
104
+ // names, JSX, comments, and formatting elsewhere are left to recast to
105
+ // reprint unchanged.
106
+ function transformSource(source) {
107
+ const j = jscodeshift.withParser("tsx");
108
+ const root = j(source);
109
+ const changes = []; // { oldSource, newSource }
110
+ const unknown = new Set();
111
+
112
+ function maybeRewrite(literalNode, applyNewValue) {
113
+ const value = literalNode.value;
114
+ if (typeof value !== "string" || !value.startsWith(`${SCOPE}/`)) return;
115
+
116
+ const newSource = resolveNewSource(value);
117
+ if (newSource === null) {
118
+ unknown.add(value.slice(SCOPE.length + 1).split("/")[0]);
119
+ return;
120
+ }
121
+ if (newSource === value) return;
122
+
123
+ applyNewValue(newSource);
124
+ changes.push({ oldSource: value, newSource });
125
+ }
126
+
127
+ // Static: import ... from "@sps-woodland/<name>"
128
+ root.find(j.ImportDeclaration).forEach((p) => {
129
+ const node = p.node.source;
130
+ maybeRewrite(node, (newValue) => {
131
+ node.value = newValue;
132
+ delete node.raw;
133
+ if (node.extra) delete node.extra.raw;
134
+ });
135
+ });
136
+
137
+ // require("@sps-woodland/<name>") and dynamic import("@sps-woodland/<name>")
138
+ // — with the tsx parser both are CallExpressions; dynamic import's callee
139
+ // is a distinct "Import" node type rather than an Identifier.
140
+ root.find(j.CallExpression).forEach((p) => {
141
+ const { callee } = p.node;
142
+ const isRequire = callee.type === "Identifier" && callee.name === "require";
143
+ const isDynamicImport = callee.type === "Import";
144
+ if (!isRequire && !isDynamicImport) return;
145
+
146
+ const arg = p.node.arguments[0];
147
+ if (!arg || typeof arg.value !== "string") return;
148
+ maybeRewrite(arg, (newValue) => {
149
+ arg.value = newValue;
150
+ delete arg.raw;
151
+ if (arg.extra) delete arg.extra.raw;
152
+ });
153
+ });
154
+
155
+ return {
156
+ changes,
157
+ unknown,
158
+ output: changes.length > 0 ? root.toSource() : source,
159
+ };
160
+ }
161
+
162
+ async function* walk(dir) {
163
+ const entries = await readdir(dir, { withFileTypes: true });
164
+ for (const entry of entries) {
165
+ if (SKIP_DIRS.has(entry.name)) continue;
166
+ const full = path.join(dir, entry.name);
167
+ if (entry.isDirectory()) {
168
+ yield* walk(full);
169
+ } else if (EXTENSIONS.has(path.extname(entry.name))) {
170
+ yield full;
171
+ }
172
+ }
173
+ }
174
+
175
+ async function main() {
176
+ const args = process.argv.slice(2);
177
+ const write = args.includes("--write");
178
+ const verbose = args.includes("--verbose");
179
+ const srcDir = args.find((a) => !a.startsWith("--")) ?? "src";
180
+
181
+ let filesChanged = 0;
182
+ let totalReplacements = 0;
183
+ const unknownPackagesSeen = new Set();
184
+
185
+ for await (const file of walk(srcDir)) {
186
+ const original = await readFile(file, "utf8");
187
+ let result;
188
+ try {
189
+ result = transformSource(original);
190
+ } catch (err) {
191
+ console.error(`skipped (parse error): ${file} — ${err.message}`);
192
+ continue;
193
+ }
194
+
195
+ for (const name of result.unknown) unknownPackagesSeen.add(name);
196
+
197
+ if (result.changes.length > 0) {
198
+ filesChanged += 1;
199
+ totalReplacements += result.changes.length;
200
+ console.log(
201
+ `${write ? "updated" : "would update"}: ${file} (${result.changes.length} import${result.changes.length === 1 ? "" : "s"})`,
202
+ );
203
+ if (verbose) {
204
+ for (const { oldSource, newSource } of result.changes) {
205
+ console.log(` - "${oldSource}" -> "${newSource}"`);
206
+ }
207
+ }
208
+ if (write) {
209
+ await writeFile(file, result.output, "utf8");
210
+ }
211
+ }
212
+ }
213
+
214
+ console.log("");
215
+ console.log(`${filesChanged} file(s), ${totalReplacements} import(s) ${write ? "rewritten" : "would be rewritten"}.`);
216
+ if (unknownPackagesSeen.size > 0) {
217
+ console.log(
218
+ `Note: found imports from @sps-woodland/${[...unknownPackagesSeen].join(", @sps-woodland/")} — ` +
219
+ `not in the wrapper map, left untouched. Add them to WRAPPER_TO_CORE if they're also re-export shims.`,
220
+ );
221
+ }
222
+ if (!write) {
223
+ console.log("Dry run only — re-run with --write to apply.");
224
+ }
225
+ }
226
+
227
+ main().catch((err) => {
228
+ console.error(err);
229
+ process.exit(1);
230
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sps-woodland/codemods",
3
3
  "description": "SPS Woodland Design System codemods",
4
- "version": "8.53.7",
4
+ "version": "8.53.8",
5
5
  "author": "SPS Commerce",
6
6
  "license": "UNLICENSED",
7
7
  "repository": "https://github.com/spscommerce/woodland/tree/master/packages/@sps-woodland/codemods",
@@ -12,7 +12,10 @@
12
12
  "pub": "pnpm pack && npm publish"
13
13
  },
14
14
  "main": "./main.js",
15
- "bin": "./main.js",
15
+ "bin": {
16
+ "codemods": "./main.js",
17
+ "migrate-wrapper-imports": "./migrate-wrapper-imports.mjs"
18
+ },
16
19
  "publishConfig": {
17
20
  "access": "public"
18
21
  },
Binary file
Binary file