@sps-woodland/codemods 8.53.12 → 8.53.13

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.
@@ -41,16 +41,17 @@ const jscodeshift = jscodeshiftModule.default ?? jscodeshiftModule;
41
41
  // assume the mapping is "same name", some are exceptions.
42
42
  //
43
43
  // jsSubpath: subpath used for the bare package import, e.g.
44
- // from "@sps-woodland/cards" -> from "@sps-woodland/core/cards"
44
+ // from "@sps-woodland/tags" -> from "@sps-woodland/core/tags"
45
45
  // cssSubpath: subpath used when importing "<wrapperName>/style.css" — for
46
46
  // every package this matches jsSubpath EXCEPT "buttons", whose CSS still
47
47
  // lives under the plural "buttons" path even though the JS export is the
48
48
  // singular "button".
49
+ //
50
+ // "cards" is deliberately NOT in this map — see CARDS_EXPORT_SUBPATH below.
49
51
  const WRAPPER_TO_CORE = {
50
52
  "action-bar": { jsSubpath: "action-bar", cssSubpath: "action-bar" },
51
53
  "add-to-list": { jsSubpath: "add-to-list", cssSubpath: "add-to-list" },
52
54
  "buttons": { jsSubpath: "button", cssSubpath: "buttons" }, // exception: singular JS, plural CSS
53
- "cards": { jsSubpath: "cards", cssSubpath: "cards" },
54
55
  "column-manager": { jsSubpath: "column-manager", cssSubpath: "column-manager" },
55
56
  "content-row": { jsSubpath: "content-row", cssSubpath: "content-row" },
56
57
  "content-tiles": { jsSubpath: "content-tiles", cssSubpath: "content-tiles" },
@@ -81,26 +82,83 @@ const WRAPPER_TO_CORE = {
81
82
  // itself), @sps-woodland/tokens, @sps-woodland/illustrations,
82
83
  // @sps-woodland/codemods (these ship real, independent code, not re-exports).
83
84
 
85
+ // "cards" is a multi-family wrapper: it re-exports three independent core
86
+ // families under one legacy package name (see
87
+ // packages/@sps-woodland/cards/src/index.ts) —
88
+ // export * from "@sps-woodland/core/cards";
89
+ // export * from "@sps-woodland/core/insight-card";
90
+ // export * from "@sps-woodland/core/scrollable-container";
91
+ // — and its style.css is a concatenation of all three families' style.css
92
+ // (see that package's build:css script). A single wrapper->subpath mapping
93
+ // would route every named import and every style.css import to just one of
94
+ // the three, silently dropping the other two. Route by the specific export
95
+ // name instead, and always expand its style.css into all three families'.
96
+ const CARDS_EXPORT_SUBPATH = {
97
+ Card: "cards",
98
+ CardHeader: "cards",
99
+ CardFooter: "cards",
100
+ CardTitle: "cards",
101
+ InsightCard: "insight-card",
102
+ InsightCardSet: "insight-card",
103
+ ScrollableContainer: "scrollable-container",
104
+ };
105
+ const CARDS_CSS_SUBPATHS = ["cards", "insight-card", "scrollable-container"];
106
+
84
107
  const SCOPE = "@sps-woodland";
85
108
  const JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
86
109
  const CSS_EXTENSIONS = new Set([".css", ".scss", ".sass", ".less"]);
87
110
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "lib", "coverage"]);
88
111
 
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"
112
+ // Splits "@sps-woodland/cards/style.css" into { name: "cards", subRest:
113
+ // "/style.css" } (subRest is "" for a bare "@sps-woodland/<name>" import).
114
+ function splitScopedSpecifier(source) {
115
+ if (!source.startsWith(`${SCOPE}/`)) return null;
116
+ const rest = source.slice(SCOPE.length + 1);
92
117
  const slashIdx = rest.indexOf("/");
93
118
  const name = slashIdx === -1 ? rest : rest.slice(0, slashIdx);
94
- const subRest = slashIdx === -1 ? "" : rest.slice(slashIdx); // "" or "/style.css" or deeper
119
+ const subRest = slashIdx === -1 ? "" : rest.slice(slashIdx);
120
+ return { name, subRest };
121
+ }
122
+
123
+ function resolveNewSource(oldSource) {
124
+ const parts = splitScopedSpecifier(oldSource);
125
+ if (!parts) return null;
95
126
 
96
- const mapping = WRAPPER_TO_CORE[name];
127
+ const mapping = WRAPPER_TO_CORE[parts.name];
97
128
  if (!mapping) return null; // not a known wrapper package — leave untouched
98
129
 
99
- if (subRest === "") return `${SCOPE}/core/${mapping.jsSubpath}`;
100
- if (subRest === "/style.css") return `${SCOPE}/core/${mapping.cssSubpath}/style.css`;
130
+ if (parts.subRest === "") return `${SCOPE}/core/${mapping.jsSubpath}`;
131
+ if (parts.subRest === "/style.css") return `${SCOPE}/core/${mapping.cssSubpath}/style.css`;
101
132
  // Unknown deep subpath (rare) — map using the JS subpath and preserve
102
133
  // whatever followed the package name, best-effort.
103
- return `${SCOPE}/core/${mapping.jsSubpath}${subRest}`;
134
+ return `${SCOPE}/core/${mapping.jsSubpath}${parts.subRest}`;
135
+ }
136
+
137
+ // "cards" can't be resolved to a single new source string (see
138
+ // CARDS_EXPORT_SUBPATH above) — it needs to become several ImportDeclarations
139
+ // instead of having its source literal swapped in place. Returns the
140
+ // replacement declarations, or null if this import can't be split safely
141
+ // (a default/namespace import, an unrecognized named export, or a deep
142
+ // subpath) — callers leave those untouched and flag them for manual review.
143
+ function splitCardsImportDeclaration(j, node, subRest) {
144
+ if (subRest === "/style.css") {
145
+ return CARDS_CSS_SUBPATHS.map((sub) => j.importDeclaration([], j.literal(`${SCOPE}/core/${sub}/style.css`)));
146
+ }
147
+ if (subRest !== "") return null; // unrecognized deep subpath
148
+
149
+ const specifiers = node.specifiers;
150
+ if (specifiers.length === 0) return null; // bare side-effect import, nothing to route by
151
+
152
+ const bySubpath = new Map(); // subpath -> specifier[], insertion order
153
+ for (const spec of specifiers) {
154
+ if (spec.type !== "ImportSpecifier") return null; // default or namespace import
155
+ const sub = CARDS_EXPORT_SUBPATH[spec.imported.name];
156
+ if (!sub) return null; // not a recognized cards/insight-card/scrollable-container export
157
+ if (!bySubpath.has(sub)) bySubpath.set(sub, []);
158
+ bySubpath.get(sub).push(spec);
159
+ }
160
+
161
+ return [...bySubpath.entries()].map(([sub, specs]) => j.importDeclaration(specs, j.literal(`${SCOPE}/core/${sub}`)));
104
162
  }
105
163
 
106
164
  // Parses the file with jscodeshift (Babel/TSX parser) and rewrites only the
@@ -131,6 +189,21 @@ function transformSource(source) {
131
189
  // Static: import ... from "@sps-woodland/<name>"
132
190
  root.find(j.ImportDeclaration).forEach((p) => {
133
191
  const node = p.node.source;
192
+ const value = node.value;
193
+ const parts = typeof value === "string" ? splitScopedSpecifier(value) : null;
194
+
195
+ if (parts && parts.name === "cards") {
196
+ const newDecls = splitCardsImportDeclaration(j, p.node, parts.subRest);
197
+ if (newDecls) {
198
+ const oldSource = `${SCOPE}/cards${parts.subRest}`;
199
+ p.replace(...newDecls);
200
+ changes.push({ oldSource, newSource: newDecls.map((d) => d.source.value).join(", ") });
201
+ } else {
202
+ unknown.add("cards");
203
+ }
204
+ return;
205
+ }
206
+
134
207
  maybeRewrite(node, (newValue) => {
135
208
  node.value = newValue;
136
209
  delete node.raw;
@@ -175,7 +248,40 @@ function transformCssSource(source) {
175
248
  const changes = []; // { oldSource, newSource }
176
249
  const unknown = new Set();
177
250
 
178
- const output = source.replace(CSS_IMPORT_RE, (match, prefix, quote, name, rest) => {
251
+ const output = source.replace(CSS_IMPORT_RE, (match, prefix, quote, name, rest, offset, string) => {
252
+ if (name === "cards") {
253
+ if (rest !== "/style.css" && rest !== "/lib/style.css") {
254
+ unknown.add(`cards${rest}`);
255
+ return match;
256
+ }
257
+
258
+ // One @import can't become three without knowing what immediately
259
+ // follows the match (a plain ";", or url()'s ")" then ";") — a media
260
+ // query or anything else there would need to apply to all three
261
+ // splits, which we can't infer safely, so leave those for manual review.
262
+ const isUrl = /url\(\s*$/.test(prefix);
263
+ const tail = string.slice(offset + match.length);
264
+ if (!(isUrl ? /^\s*\)\s*;/ : /^\s*;/).test(tail)) {
265
+ unknown.add(`cards${rest} (unexpected trailing syntax after the @import — split into ${CARDS_CSS_SUBPATHS.length} imports manually)`);
266
+ return match;
267
+ }
268
+
269
+ const oldSource = `${SCOPE}/cards${rest}`;
270
+ const closeParen = isUrl ? ")" : "";
271
+ const statements = CARDS_CSS_SUBPATHS.map((sub, i) => {
272
+ const newPath = `${SCOPE}/core/${sub}/style.css`;
273
+ const isLast = i === CARDS_CSS_SUBPATHS.length - 1;
274
+ // The last split keeps whatever terminator (")" / ";" / media query)
275
+ // already follows the original match in the source untouched.
276
+ return `${prefix}${quote}${newPath}${quote}${isLast ? "" : `${closeParen};`}`;
277
+ });
278
+ changes.push({
279
+ oldSource,
280
+ newSource: CARDS_CSS_SUBPATHS.map((sub) => `${SCOPE}/core/${sub}/style.css`).join(", "),
281
+ });
282
+ return statements.join("\n");
283
+ }
284
+
179
285
  const mapping = WRAPPER_TO_CORE[name];
180
286
  if (!mapping) {
181
287
  unknown.add(name);
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.12",
4
+ "version": "8.53.13",
5
5
  "author": "SPS Commerce",
6
6
  "license": "UNLICENSED",
7
7
  "repository": "https://github.com/spscommerce/woodland/tree/master/packages/@sps-woodland/codemods",
Binary file