@sps-woodland/codemods 8.53.7 → 8.53.9
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,269 @@
|
|
|
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. Also rewrites
|
|
14
|
+
// the matching CSS/SCSS/LESS "@sps-woodland/<wrapper>/style.css" (or
|
|
15
|
+
// "/lib/style.css") @import paths, since those are just as real a wrapper
|
|
16
|
+
// dependency as the JS imports and are otherwise invisible to a JS-only
|
|
17
|
+
// codemod. Does NOT touch anything else: no @spscommerce/ds-react, no
|
|
18
|
+
// imported-name changes, no JSX, no package.json/lockfile edits.
|
|
19
|
+
//
|
|
20
|
+
// This is intentionally NOT part of the main jscodeshift transform
|
|
21
|
+
// pipeline (main.js / src/transform.ts) — that pipeline migrates
|
|
22
|
+
// @spscommerce/ds-react -> the wrapper packages, a different step with a
|
|
23
|
+
// different data table. This script is its own bin so it can be run (and
|
|
24
|
+
// versioned/removed) independently of that pipeline.
|
|
25
|
+
//
|
|
26
|
+
// Usage (from a consuming app's repo root):
|
|
27
|
+
// pnpm dlx --package=@sps-woodland/codemods migrate-wrapper-imports [srcDir] [--write] [--verbose]
|
|
28
|
+
//
|
|
29
|
+
// Defaults to a dry run (prints what would change). Pass --write to
|
|
30
|
+
// actually modify files on disk.
|
|
31
|
+
|
|
32
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import jscodeshiftModule from "jscodeshift";
|
|
35
|
+
|
|
36
|
+
const jscodeshift = jscodeshiftModule.default ?? jscodeshiftModule;
|
|
37
|
+
|
|
38
|
+
// --- Wrapper package -> @sps-woodland/core subpath mapping -----------------
|
|
39
|
+
// Derived by reading each wrapper's own src/index.ts re-export statement in
|
|
40
|
+
// this monorepo (packages/@sps-woodland/<name>/src/index.ts) — do not
|
|
41
|
+
// assume the mapping is "same name", some are exceptions.
|
|
42
|
+
//
|
|
43
|
+
// jsSubpath: subpath used for the bare package import, e.g.
|
|
44
|
+
// from "@sps-woodland/cards" -> from "@sps-woodland/core/cards"
|
|
45
|
+
// cssSubpath: subpath used when importing "<wrapperName>/style.css" — for
|
|
46
|
+
// every package this matches jsSubpath EXCEPT "buttons", whose CSS still
|
|
47
|
+
// lives under the plural "buttons" path even though the JS export is the
|
|
48
|
+
// singular "button".
|
|
49
|
+
const WRAPPER_TO_CORE = {
|
|
50
|
+
"action-bar": { jsSubpath: "action-bar", cssSubpath: "action-bar" },
|
|
51
|
+
"add-to-list": { jsSubpath: "add-to-list", cssSubpath: "add-to-list" },
|
|
52
|
+
"buttons": { jsSubpath: "button", cssSubpath: "buttons" }, // exception: singular JS, plural CSS
|
|
53
|
+
"cards": { jsSubpath: "cards", cssSubpath: "cards" },
|
|
54
|
+
"column-manager": { jsSubpath: "column-manager", cssSubpath: "column-manager" },
|
|
55
|
+
"content-row": { jsSubpath: "content-row", cssSubpath: "content-row" },
|
|
56
|
+
"content-tiles": { jsSubpath: "content-tiles", cssSubpath: "content-tiles" },
|
|
57
|
+
"dropdowns": { jsSubpath: "dropdowns", cssSubpath: "dropdowns" },
|
|
58
|
+
"editable-table": { jsSubpath: "editable-table", cssSubpath: "editable-table" },
|
|
59
|
+
"feedback-block": { jsSubpath: "feedback-block", cssSubpath: "feedback-block" },
|
|
60
|
+
"file-upload": { jsSubpath: "file-upload", cssSubpath: "file-upload" },
|
|
61
|
+
"filter-tiles": { jsSubpath: "filter-tiles", cssSubpath: "filter-tiles" },
|
|
62
|
+
"focused-task": { jsSubpath: "focused-task", cssSubpath: "focused-task" },
|
|
63
|
+
"growler": { jsSubpath: "growler", cssSubpath: "growler" },
|
|
64
|
+
"key-value-set": { jsSubpath: "key-value-set", cssSubpath: "key-value-set" },
|
|
65
|
+
"list-bar": { jsSubpath: "list-bar", cssSubpath: "list-bar" },
|
|
66
|
+
"matrix": { jsSubpath: "matrix", cssSubpath: "matrix" },
|
|
67
|
+
"modal": { jsSubpath: "modal", cssSubpath: "modal" },
|
|
68
|
+
"page-title": { jsSubpath: "page-title", cssSubpath: "page-title" },
|
|
69
|
+
"photo": { jsSubpath: "photo", cssSubpath: "photo" },
|
|
70
|
+
"product-bar": { jsSubpath: "product-bar", cssSubpath: "product-bar" },
|
|
71
|
+
"product-bar-vertical": { jsSubpath: "product-bar-vertical", cssSubpath: "product-bar-vertical" },
|
|
72
|
+
"progress-indicators": { jsSubpath: "progress-indicators", cssSubpath: "progress-indicators" },
|
|
73
|
+
"rich-text-editor": { jsSubpath: "rich-text-editor", cssSubpath: "rich-text-editor" },
|
|
74
|
+
"side-nav": { jsSubpath: "side-nav", cssSubpath: "side-nav" },
|
|
75
|
+
"sorting-header": { jsSubpath: "sorting-header", cssSubpath: "sorting-header" },
|
|
76
|
+
"tabs": { jsSubpath: "tabs", cssSubpath: "tabs" },
|
|
77
|
+
"tags": { jsSubpath: "tags", cssSubpath: "tags" },
|
|
78
|
+
"zero-state": { jsSubpath: "zero-state", cssSubpath: "zero-state" },
|
|
79
|
+
};
|
|
80
|
+
// Explicitly NOT wrappers — leave untouched: @sps-woodland/core (the target
|
|
81
|
+
// itself), @sps-woodland/tokens, @sps-woodland/illustrations,
|
|
82
|
+
// @sps-woodland/codemods (these ship real, independent code, not re-exports).
|
|
83
|
+
|
|
84
|
+
const SCOPE = "@sps-woodland";
|
|
85
|
+
const JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
86
|
+
const CSS_EXTENSIONS = new Set([".css", ".scss", ".sass", ".less"]);
|
|
87
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "lib", "coverage"]);
|
|
88
|
+
|
|
89
|
+
function resolveNewSource(oldSource) {
|
|
90
|
+
if (!oldSource.startsWith(`${SCOPE}/`)) return null;
|
|
91
|
+
const rest = oldSource.slice(SCOPE.length + 1); // e.g. "cards" or "cards/style.css"
|
|
92
|
+
const slashIdx = rest.indexOf("/");
|
|
93
|
+
const name = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
|
|
94
|
+
const subRest = slashIdx === -1 ? "" : rest.slice(slashIdx); // "" or "/style.css" or deeper
|
|
95
|
+
|
|
96
|
+
const mapping = WRAPPER_TO_CORE[name];
|
|
97
|
+
if (!mapping) return null; // not a known wrapper package — leave untouched
|
|
98
|
+
|
|
99
|
+
if (subRest === "") return `${SCOPE}/core/${mapping.jsSubpath}`;
|
|
100
|
+
if (subRest === "/style.css") return `${SCOPE}/core/${mapping.cssSubpath}/style.css`;
|
|
101
|
+
// Unknown deep subpath (rare) — map using the JS subpath and preserve
|
|
102
|
+
// whatever followed the package name, best-effort.
|
|
103
|
+
return `${SCOPE}/core/${mapping.jsSubpath}${subRest}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parses the file with jscodeshift (Babel/TSX parser) and rewrites only the
|
|
107
|
+
// string-literal module specifier of matching nodes in place — imported
|
|
108
|
+
// names, JSX, comments, and formatting elsewhere are left to recast to
|
|
109
|
+
// reprint unchanged.
|
|
110
|
+
function transformSource(source) {
|
|
111
|
+
const j = jscodeshift.withParser("tsx");
|
|
112
|
+
const root = j(source);
|
|
113
|
+
const changes = []; // { oldSource, newSource }
|
|
114
|
+
const unknown = new Set();
|
|
115
|
+
|
|
116
|
+
function maybeRewrite(literalNode, applyNewValue) {
|
|
117
|
+
const value = literalNode.value;
|
|
118
|
+
if (typeof value !== "string" || !value.startsWith(`${SCOPE}/`)) return;
|
|
119
|
+
|
|
120
|
+
const newSource = resolveNewSource(value);
|
|
121
|
+
if (newSource === null) {
|
|
122
|
+
unknown.add(value.slice(SCOPE.length + 1).split("/")[0]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (newSource === value) return;
|
|
126
|
+
|
|
127
|
+
applyNewValue(newSource);
|
|
128
|
+
changes.push({ oldSource: value, newSource });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Static: import ... from "@sps-woodland/<name>"
|
|
132
|
+
root.find(j.ImportDeclaration).forEach((p) => {
|
|
133
|
+
const node = p.node.source;
|
|
134
|
+
maybeRewrite(node, (newValue) => {
|
|
135
|
+
node.value = newValue;
|
|
136
|
+
delete node.raw;
|
|
137
|
+
if (node.extra) delete node.extra.raw;
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// require("@sps-woodland/<name>") and dynamic import("@sps-woodland/<name>")
|
|
142
|
+
// — with the tsx parser both are CallExpressions; dynamic import's callee
|
|
143
|
+
// is a distinct "Import" node type rather than an Identifier.
|
|
144
|
+
root.find(j.CallExpression).forEach((p) => {
|
|
145
|
+
const { callee } = p.node;
|
|
146
|
+
const isRequire = callee.type === "Identifier" && callee.name === "require";
|
|
147
|
+
const isDynamicImport = callee.type === "Import";
|
|
148
|
+
if (!isRequire && !isDynamicImport) return;
|
|
149
|
+
|
|
150
|
+
const arg = p.node.arguments[0];
|
|
151
|
+
if (!arg || typeof arg.value !== "string") return;
|
|
152
|
+
maybeRewrite(arg, (newValue) => {
|
|
153
|
+
arg.value = newValue;
|
|
154
|
+
delete arg.raw;
|
|
155
|
+
if (arg.extra) delete arg.extra.raw;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
changes,
|
|
161
|
+
unknown,
|
|
162
|
+
output: changes.length > 0 ? root.toSource() : source,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Matches: @import [url(] ("|') @sps-woodland/<name><rest> (matching quote)
|
|
167
|
+
// Only the two CSS subpath forms wrapper packages actually ship
|
|
168
|
+
// ("/style.css" and "/lib/style.css") are rewritten — anything else for a
|
|
169
|
+
// known package is left alone and flagged, since guessing wrong here fails
|
|
170
|
+
// silently (no compiler to catch a bad CSS path).
|
|
171
|
+
const CSS_IMPORT_RE =
|
|
172
|
+
/(@import\s+(?:url\(\s*)?)(["'])@sps-woodland\/([a-z-]+)((?:\/[^"']*)?)\2/g;
|
|
173
|
+
|
|
174
|
+
function transformCssSource(source) {
|
|
175
|
+
const changes = []; // { oldSource, newSource }
|
|
176
|
+
const unknown = new Set();
|
|
177
|
+
|
|
178
|
+
const output = source.replace(CSS_IMPORT_RE, (match, prefix, quote, name, rest) => {
|
|
179
|
+
const mapping = WRAPPER_TO_CORE[name];
|
|
180
|
+
if (!mapping) {
|
|
181
|
+
unknown.add(name);
|
|
182
|
+
return match;
|
|
183
|
+
}
|
|
184
|
+
if (rest !== "/style.css" && rest !== "/lib/style.css") {
|
|
185
|
+
unknown.add(`${name}${rest}`);
|
|
186
|
+
return match;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const oldSource = `${SCOPE}/${name}${rest}`;
|
|
190
|
+
const newSource = `${SCOPE}/core/${mapping.cssSubpath}/style.css`;
|
|
191
|
+
changes.push({ oldSource, newSource });
|
|
192
|
+
return `${prefix}${quote}${newSource}${quote}`;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { changes, unknown, output };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function* walk(dir) {
|
|
199
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
202
|
+
const full = path.join(dir, entry.name);
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
yield* walk(full);
|
|
205
|
+
} else if (JS_EXTENSIONS.has(path.extname(entry.name)) || CSS_EXTENSIONS.has(path.extname(entry.name))) {
|
|
206
|
+
yield full;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function main() {
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
const write = args.includes("--write");
|
|
214
|
+
const verbose = args.includes("--verbose");
|
|
215
|
+
const srcDir = args.find((a) => !a.startsWith("--")) ?? "src";
|
|
216
|
+
|
|
217
|
+
let filesChanged = 0;
|
|
218
|
+
let totalReplacements = 0;
|
|
219
|
+
const unknownPackagesSeen = new Set();
|
|
220
|
+
|
|
221
|
+
for await (const file of walk(srcDir)) {
|
|
222
|
+
const original = await readFile(file, "utf8");
|
|
223
|
+
const isCss = CSS_EXTENSIONS.has(path.extname(file));
|
|
224
|
+
let result;
|
|
225
|
+
try {
|
|
226
|
+
result = isCss ? transformCssSource(original) : transformSource(original);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`skipped (parse error): ${file} — ${err.message}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const name of result.unknown) unknownPackagesSeen.add(name);
|
|
233
|
+
|
|
234
|
+
if (result.changes.length > 0) {
|
|
235
|
+
filesChanged += 1;
|
|
236
|
+
totalReplacements += result.changes.length;
|
|
237
|
+
console.log(
|
|
238
|
+
`${write ? "updated" : "would update"}: ${file} (${result.changes.length} import${result.changes.length === 1 ? "" : "s"})`,
|
|
239
|
+
);
|
|
240
|
+
if (verbose) {
|
|
241
|
+
for (const { oldSource, newSource } of result.changes) {
|
|
242
|
+
console.log(` - "${oldSource}" -> "${newSource}"`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (write) {
|
|
246
|
+
await writeFile(file, result.output, "utf8");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(`${filesChanged} file(s), ${totalReplacements} import(s) ${write ? "rewritten" : "would be rewritten"}.`);
|
|
253
|
+
if (unknownPackagesSeen.size > 0) {
|
|
254
|
+
console.log(
|
|
255
|
+
`Note: found @sps-woodland/${[...unknownPackagesSeen].join(", @sps-woodland/")} — ` +
|
|
256
|
+
`either not a wrapper package (e.g. core/tokens/illustrations, left untouched on purpose) ` +
|
|
257
|
+
`or a subpath this script doesn't recognize. Review manually, or add it to WRAPPER_TO_CORE ` +
|
|
258
|
+
`if it's a wrapper re-export this script should also handle.`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (!write) {
|
|
262
|
+
console.log("Dry run only — re-run with --write to apply.");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main().catch((err) => {
|
|
267
|
+
console.error(err);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
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.
|
|
4
|
+
"version": "8.53.9",
|
|
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":
|
|
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
|