@nexub/tokens 2.8.0 → 2.9.0
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/bin/README.md +1 -0
- package/bin/nexub-migrate.mjs +127 -2
- package/package.json +1 -1
package/bin/README.md
CHANGED
|
@@ -16,6 +16,7 @@ npx nexub-migrate --rules logical-classes --ignore 'src/legacy/**'
|
|
|
16
16
|
| `logical-classes` | `ml/mr` `pl/pr` `border-l/r` `rounded-l/r/tl/tr/bl/br` `text-left/right` → `ms/me` `ps/pe` `border-s/e` `rounded-s/e/ss/se/es/ee` `text-start/end`, keeping variants, `!` and `-`. `left-*`/`right-*` positions are reported only. |
|
|
17
17
|
| `tw-animate` | Reports tailwindcss-animate classes (`animate-in`, `fade-in-0`, `zoom-in-95`, `slide-in-from-*` …). Use `nx-motion` / `nx-pop` / `nx-dialog` from `@nexub/themes/base`. |
|
|
18
18
|
| `opacity-modifier` | Reports `bg-[var(--x)]/10` (Tailwind 3 emits nothing). `--fix` rewrites to `bg-[color-mix(in_srgb,var(--x)_10%,transparent)]`. |
|
|
19
|
+
| `deprecated-components` | `@nexub/react` components removed in 3.0, in files that import from `@nexub/react`: `Banner`/`Callout` → `Alert`; `Tag` → `<Badge variant="soft">`; `StatusPill label="x"` → `<Badge variant="soft">x</Badge>`; `*Props` types renamed; imports deduped; aliases (`Tag as T`) keep their local name. Reported only: `StatusPill dot`, `*Variants`, and uses outside JSX (`typeof Banner`, `styled(Tag)`). |
|
|
19
20
|
|
|
20
21
|
**Where classes are looked for.** `.html`/`.mdx`: `class=`/`className=` attributes. `.css`/`.scss`: `@apply`.
|
|
21
22
|
JS/TS: string and template literals (static parts only) that are either in an explicit class context —
|
package/bin/nexub-migrate.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
|
|
10
|
-
export const RULES = ['legacy-tokens', 'logical-classes', 'tw-animate', 'opacity-modifier'];
|
|
10
|
+
export const RULES = ['legacy-tokens', 'logical-classes', 'tw-animate', 'opacity-modifier', 'deprecated-components'];
|
|
11
11
|
const EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.css', '.scss', '.html', '.mdx']);
|
|
12
12
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.next', 'out', 'coverage']);
|
|
13
13
|
const MAX_BYTES = 1024 * 1024;
|
|
@@ -261,6 +261,122 @@ function rewriteClassText(text, seg, offset, rules, fix, report) {
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// ─── deprecated-components: @nexub/react components removed in 3.0 ───────────
|
|
265
|
+
const REPLACED = { Banner: 'Alert', Callout: 'Alert', Tag: 'Badge', StatusPill: 'Badge' };
|
|
266
|
+
const SOFT = new Set(['Tag', 'StatusPill']); // container-coloured, so Badge variant="soft", not its solid default
|
|
267
|
+
const TYPES = { BannerProps: 'AlertProps', CalloutProps: 'AlertProps', TagProps: 'BadgeProps', StatusPillProps: 'BadgeProps' };
|
|
268
|
+
const VARIANTS = new Set(['bannerVariants', 'calloutVariants', 'tagVariants', 'statusPillVariants']);
|
|
269
|
+
const COMPONENT_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mdx']);
|
|
270
|
+
|
|
271
|
+
const keepLines = (s) => s.replace(/[^\n]/g, ''); // removals keep line numbers stable
|
|
272
|
+
|
|
273
|
+
/** Index just past the `{…}` or quoted value starting at i. ponytail: braces inside strings in the value count. */
|
|
274
|
+
function skipValue(src, i) {
|
|
275
|
+
if (src[i] === '"' || src[i] === "'") return src.indexOf(src[i], i + 1) + 1;
|
|
276
|
+
let depth = 0;
|
|
277
|
+
for (let j = i; j < src.length; j++) {
|
|
278
|
+
if (src[j] === '{') depth++;
|
|
279
|
+
else if (src[j] === '}' && --depth === 0) return j + 1;
|
|
280
|
+
}
|
|
281
|
+
return src.length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Parse a JSX opening tag's attributes from i (just past the name) to its `>` or `/>`. */
|
|
285
|
+
function readTag(src, i) {
|
|
286
|
+
const attrs = [];
|
|
287
|
+
while (i < src.length) {
|
|
288
|
+
const ws = i;
|
|
289
|
+
while (/\s/.test(src[i])) i++;
|
|
290
|
+
if (src.startsWith('/>', i)) return { attrs, close: ws, end: i + 2, selfClosing: true };
|
|
291
|
+
if (src[i] === '>') return { attrs, close: ws, end: i + 1, selfClosing: false };
|
|
292
|
+
if (src[i] === '{') { i = skipValue(src, i); continue; } // {...spread}
|
|
293
|
+
const name = /^[\w:-]+/.exec(src.slice(i))?.[0];
|
|
294
|
+
if (!name) return null;
|
|
295
|
+
let j = i + name.length;
|
|
296
|
+
let value = null;
|
|
297
|
+
const eq = /^\s*=\s*/.exec(src.slice(j));
|
|
298
|
+
if (eq) {
|
|
299
|
+
const v = j + eq[0].length;
|
|
300
|
+
j = skipValue(src, v);
|
|
301
|
+
value = src.slice(v, j);
|
|
302
|
+
}
|
|
303
|
+
attrs.push({ name, start: ws, end: j, value });
|
|
304
|
+
i = j;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Rewrite deprecated @nexub/react components. Returns { output, items: [{ line, rule, message }] }. */
|
|
310
|
+
function migrateComponents(src) {
|
|
311
|
+
const edits = [];
|
|
312
|
+
const items = [];
|
|
313
|
+
const lineOf = (at) => src.slice(0, at).split('\n').length;
|
|
314
|
+
const report = (at, message) => items.push({ line: lineOf(at), rule: 'deprecated-components', message });
|
|
315
|
+
const locals = new Map(); // local JSX name -> { from, to, aliased }
|
|
316
|
+
const types = new Set();
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
const imports = [];
|
|
319
|
+
for (const m of src.matchAll(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])@nexub\/react\2/dg)) {
|
|
320
|
+
imports.push(m.indices[0]);
|
|
321
|
+
const base = m.indices[1][0];
|
|
322
|
+
for (const s of m[1].matchAll(/\s*(?:\btype\s+)?\b(\w+)(?:\s+as\s+(\w+))?(?:\s*,)?/dg)) {
|
|
323
|
+
const [spec, name, alias] = s;
|
|
324
|
+
const next = REPLACED[name] ?? TYPES[name] ?? name;
|
|
325
|
+
if (VARIANTS.has(name)) report(base + s.index, `${name} is removed in 3.0; use alertVariants / badgeVariants or Alert / Badge`);
|
|
326
|
+
if (REPLACED[name]) locals.set(alias ?? name, { from: name, to: REPLACED[name], aliased: !!alias });
|
|
327
|
+
if (TYPES[name] && !alias) types.add(name);
|
|
328
|
+
const out = alias ? `${next} as ${alias}` : next;
|
|
329
|
+
if (seen.has(out)) { edits.push({ start: base + s.index, end: base + s.index + spec.length, text: keepLines(spec) }); continue; }
|
|
330
|
+
seen.add(out);
|
|
331
|
+
if (next !== name) edits.push({ start: base + s.indices[1][0], end: base + s.indices[1][1], text: next });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const [local, { from, to, aliased }] of locals) {
|
|
335
|
+
const tagName = aliased ? local : to;
|
|
336
|
+
for (const m of src.matchAll(new RegExp(`<(/?)${local}(?![\\w.$-])`, 'g'))) {
|
|
337
|
+
const nameEnd = m.index + m[0].length;
|
|
338
|
+
edits.push({ start: m.index + 1 + m[1].length, end: nameEnd, text: tagName });
|
|
339
|
+
if (m[1]) continue;
|
|
340
|
+
const tag = readTag(src, nameEnd);
|
|
341
|
+
if (!tag) { report(m.index, `<${local}>: could not parse the tag; migrate it to <${to}> by hand`); continue; }
|
|
342
|
+
const attr = Object.fromEntries(tag.attrs.map((a) => [a.name, a]));
|
|
343
|
+
if (SOFT.has(from) && !attr.variant) edits.push({ start: nameEnd, end: nameEnd, text: ' variant="soft"' });
|
|
344
|
+
if (from !== 'StatusPill') continue;
|
|
345
|
+
// StatusPill label -> children; dot has no Badge equivalent.
|
|
346
|
+
const drop = (a) => edits.push({ start: a.start, end: a.end, text: keepLines(src.slice(a.start, a.end)) });
|
|
347
|
+
if (attr.dot) {
|
|
348
|
+
drop(attr.dot);
|
|
349
|
+
if (attr.dot.value !== '{false}') report(m.index, `<${local} dot>: Badge has no dot; pass leadingIcon={<StatusDot />} or similar`);
|
|
350
|
+
}
|
|
351
|
+
const label = attr.label;
|
|
352
|
+
if (!label?.value || label.value.includes('\n') || !tag.selfClosing) {
|
|
353
|
+
report(m.index, `<${local}>: move label into <${tagName}>children</${tagName}> by hand`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const q = label.value[0];
|
|
357
|
+
const inner = label.value.slice(1, -1);
|
|
358
|
+
const children = q === '{' ? label.value : /[{}<>]/.test(inner) ? `{${label.value}}` : inner;
|
|
359
|
+
drop(label);
|
|
360
|
+
edits.push({ start: tag.close, end: tag.end, text: `${keepLines(src.slice(tag.close, tag.end))}>${children}</${tagName}>` });
|
|
361
|
+
}
|
|
362
|
+
// Non-JSX references (typeof X, styled(X), as={X}) keep the old name unless it was aliased.
|
|
363
|
+
if (!aliased || SOFT.has(from)) {
|
|
364
|
+
for (const m of src.matchAll(new RegExp(`(?:\\btypeof\\s+${local}\\b|[({]\\s*${local}\\s*[,)}])`, 'g'))) {
|
|
365
|
+
if (!imports.some(([a, b]) => a <= m.index && m.index < b)) report(m.index, `${local} is used outside JSX; it becomes ${to} in 3.0 (check props)`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (types.size) {
|
|
370
|
+
const re = new RegExp(`\\b(${[...types].join('|')})\\b`, 'g');
|
|
371
|
+
for (const m of src.matchAll(re)) {
|
|
372
|
+
if (!edits.some((e) => e.start <= m.index && m.index < e.end)) edits.push({ start: m.index, end: m.index + m[0].length, text: TYPES[m[0]] });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
let output = src;
|
|
376
|
+
for (const e of edits.sort((a, b) => b.start - a.start || b.end - a.end)) output = output.slice(0, e.start) + e.text + output.slice(e.end);
|
|
377
|
+
return { output, items };
|
|
378
|
+
}
|
|
379
|
+
|
|
264
380
|
// ─── per-file migration ──────────────────────────────────────────────────────
|
|
265
381
|
/**
|
|
266
382
|
* Migrate one file's text. Returns { output, manual: [{ line, rule, message }] }.
|
|
@@ -287,9 +403,14 @@ export function migrateSource(src, { ext, rules = new Set(RULES), fix = false, a
|
|
|
287
403
|
return name;
|
|
288
404
|
});
|
|
289
405
|
}
|
|
406
|
+
if (rules.has('deprecated-components') && COMPONENT_EXTS.has(ext) && /@nexub\/react['"]/.test(text)) {
|
|
407
|
+
const r = migrateComponents(text);
|
|
408
|
+
text = r.output;
|
|
409
|
+
items.push(...r.items);
|
|
410
|
+
}
|
|
290
411
|
// Rewrites never add or remove newlines, so offsets in src and text share line numbers.
|
|
291
412
|
const lineOf = (at) => src.slice(0, at).split('\n').length;
|
|
292
|
-
const manual = items.map(({ at, rule, message }) => ({ line: lineOf(at), rule, message })).sort((a, b) => a.line - b.line);
|
|
413
|
+
const manual = items.map(({ at, line, rule, message }) => ({ line: line ?? lineOf(at), rule, message })).sort((a, b) => a.line - b.line);
|
|
293
414
|
return { output: text, manual };
|
|
294
415
|
}
|
|
295
416
|
|
|
@@ -342,6 +463,10 @@ Rules:
|
|
|
342
463
|
left-/right- positions reported
|
|
343
464
|
tw-animate report tailwindcss-animate enter/exit classes (use nx-motion/nx-pop/nx-dialog)
|
|
344
465
|
opacity-modifier report [var(--x)]/NN (emits nothing in Tailwind 3); --fix rewrites to color-mix
|
|
466
|
+
deprecated-components
|
|
467
|
+
@nexub/react components removed in 3.0: Banner, Callout -> Alert;
|
|
468
|
+
Tag -> <Badge variant="soft">; StatusPill label -> Badge children
|
|
469
|
+
(dot and non-JSX uses reported); *Props types renamed
|
|
345
470
|
|
|
346
471
|
Ignored: node_modules, dist, build, .next, out, coverage, files over 1 MB.
|
|
347
472
|
Exit codes: 0 ok, 1 --check found something, 2 usage error.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexub/tokens",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Design tokens for Nexub Design System — colours, typography, spacing, radii, shadows, and motion as CSS variables, Tailwind presets and React Native objects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|