@xihan-ui/cli 5.1.0 → 5.3.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  import { parseArgs } from "node:util";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createInterface } from "node:readline";
7
7
  //#region src/knowledge.ts
@@ -1042,6 +1042,1275 @@ function serveStdio(options, input = process.stdin, output = process.stdout) {
1042
1042
  return new Promise((resolve) => reader.once("close", () => resolve()));
1043
1043
  }
1044
1044
  //#endregion
1045
+ //#region src/migrate.ts
1046
+ const DIALECTS = {
1047
+ ".vue": "vue",
1048
+ ".tsx": "jsx",
1049
+ ".jsx": "jsx",
1050
+ ".html": "html",
1051
+ ".htm": "html",
1052
+ ".css": "css",
1053
+ ".scss": "css",
1054
+ ".sass": "css",
1055
+ ".less": "css",
1056
+ ".ts": "script",
1057
+ ".mts": "script",
1058
+ ".cts": "script",
1059
+ ".js": "script",
1060
+ ".mjs": "script",
1061
+ ".cjs": "script"
1062
+ };
1063
+ /** 按扩展名认写法;不认识的扩展名返回 undefined,迁移时跳过。 */
1064
+ function dialectOf(file) {
1065
+ const dot = file.lastIndexOf(".");
1066
+ return dot < 0 ? void 0 : DIALECTS[file.slice(dot).toLowerCase()];
1067
+ }
1068
+ const LIBRARY = /@xihan-ui\/|\bXh[A-Z]|<xh-|['"`]xh-|--xh-|data-scope|data-xh-|xh-scope-/;
1069
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1070
+ const kebab = (name) => name.replace(/[A-Z]/g, (char, index) => `${index > 0 ? "-" : ""}${char.toLowerCase()}`);
1071
+ const camel = (name) => name.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
1072
+ const pascal = (name) => camel(name).replace(/^[a-z]/, (char) => char.toUpperCase());
1073
+ /** 改写过程的状态:当前文本、改了几处、报了哪些。 */
1074
+ var Draft = class {
1075
+ text;
1076
+ edits = 0;
1077
+ found = /* @__PURE__ */ new Map();
1078
+ constructor(text) {
1079
+ this.text = text;
1080
+ }
1081
+ /** 全文按正则替换,计数。 */
1082
+ replace(pattern, replacer) {
1083
+ let out = "";
1084
+ let last = 0;
1085
+ for (const match of this.text.matchAll(pattern)) {
1086
+ const next = replacer(match);
1087
+ if (next === void 0 || next === match[0]) continue;
1088
+ out += this.text.slice(last, match.index) + next;
1089
+ last = match.index + match[0].length;
1090
+ this.edits++;
1091
+ }
1092
+ if (last > 0) this.text = out + this.text.slice(last);
1093
+ }
1094
+ /** 按区间改:区间互不重叠,从后往前套用。 */
1095
+ splice(edits) {
1096
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
1097
+ this.text = this.text.slice(0, edit.start) + edit.text + this.text.slice(edit.end);
1098
+ this.edits++;
1099
+ }
1100
+ }
1101
+ report(index, message) {
1102
+ const line = this.text.slice(0, index).split("\n").length;
1103
+ this.found.set(`${line}\u0000${message}`, {
1104
+ line,
1105
+ message
1106
+ });
1107
+ }
1108
+ get findings() {
1109
+ return [...this.found.values()].sort((a, b) => a.line - b.line);
1110
+ }
1111
+ };
1112
+ /** 从 open 处的开括号走到配对的闭括号,跳过字符串。 */
1113
+ function skipBalanced(text, open, close, start) {
1114
+ let depth = 0;
1115
+ for (let i = open; i < text.length; i++) {
1116
+ const char = text[i];
1117
+ if (char === "\"" || char === "'" || char === "`") {
1118
+ i = text.indexOf(char, i + 1);
1119
+ if (i < 0) return text.length;
1120
+ continue;
1121
+ }
1122
+ if (char === start) depth++;
1123
+ else if (char === close && --depth === 0) return i;
1124
+ }
1125
+ return text.length;
1126
+ }
1127
+ function parseAttributes(text, from, to) {
1128
+ const out = [];
1129
+ let i = from;
1130
+ while (i < to) {
1131
+ const char = text[i];
1132
+ if (/\s|\//.test(char)) {
1133
+ i++;
1134
+ continue;
1135
+ }
1136
+ if (char === "{") {
1137
+ i = skipBalanced(text, i, "}", "{") + 1;
1138
+ continue;
1139
+ }
1140
+ const name = /^[^\s=>/'"{}]+/.exec(text.slice(i, to));
1141
+ if (!name) {
1142
+ i++;
1143
+ continue;
1144
+ }
1145
+ const start = i;
1146
+ i += name[0].length;
1147
+ let j = i;
1148
+ while (j < to && /\s/.test(text[j])) j++;
1149
+ if (text[j] !== "=") {
1150
+ out.push({
1151
+ start,
1152
+ end: i,
1153
+ name: name[0],
1154
+ quoted: false,
1155
+ braced: false
1156
+ });
1157
+ continue;
1158
+ }
1159
+ j++;
1160
+ while (j < to && /\s/.test(text[j])) j++;
1161
+ const quote = text[j];
1162
+ if (quote === "\"" || quote === "'") {
1163
+ const close = text.indexOf(quote, j + 1);
1164
+ const end = close < 0 ? to : close + 1;
1165
+ out.push({
1166
+ start,
1167
+ end,
1168
+ name: name[0],
1169
+ value: text.slice(j + 1, end - 1),
1170
+ quoted: true,
1171
+ braced: false
1172
+ });
1173
+ i = end;
1174
+ } else if (quote === "{") {
1175
+ const close = skipBalanced(text, j, "}", "{");
1176
+ out.push({
1177
+ start,
1178
+ end: close + 1,
1179
+ name: name[0],
1180
+ value: text.slice(j + 1, close).trim(),
1181
+ quoted: false,
1182
+ braced: true
1183
+ });
1184
+ i = close + 1;
1185
+ } else {
1186
+ const bare = /^[^\s>]*/.exec(text.slice(j, to))[0];
1187
+ out.push({
1188
+ start,
1189
+ end: j + bare.length,
1190
+ name: name[0],
1191
+ value: bare,
1192
+ quoted: false,
1193
+ braced: false
1194
+ });
1195
+ i = j + bare.length;
1196
+ }
1197
+ }
1198
+ return out;
1199
+ }
1200
+ /** 文本里的 Xh 组件标签与 xh- 元素标签(开标签)。 */
1201
+ function scanTags(text) {
1202
+ const out = [];
1203
+ for (const match of text.matchAll(/<(Xh[A-Z][\w.]*|xh-[a-z][\w-]*)/g)) {
1204
+ const start = match.index;
1205
+ const nameEnd = start + match[0].length;
1206
+ if (!/[\s/>]/.test(text[nameEnd] ?? "")) continue;
1207
+ let end = nameEnd;
1208
+ for (; end < text.length; end++) {
1209
+ const char = text[end];
1210
+ if (char === "\"" || char === "'") {
1211
+ const close = text.indexOf(char, end + 1);
1212
+ end = close < 0 ? text.length : close;
1213
+ } else if (char === "{") end = skipBalanced(text, end, "}", "{");
1214
+ else if (char === ">" || char === "<") break;
1215
+ }
1216
+ out.push({
1217
+ start,
1218
+ end,
1219
+ name: match[1],
1220
+ attributes: parseAttributes(text, nameEnd, end)
1221
+ });
1222
+ }
1223
+ return out;
1224
+ }
1225
+ /** 标签归到组件:Xh 组件按短横线写法、xh- 元素去掉前缀,取最长的组件标识前缀。 */
1226
+ function scopeOf(tag, scopes) {
1227
+ const words = (tag.startsWith("Xh") ? kebab(tag.slice(2)) : tag.slice(3)).split("-");
1228
+ for (let size = words.length; size > 0; size--) {
1229
+ const candidate = words.slice(0, size).join("-");
1230
+ if (scopes.has(candidate)) return candidate;
1231
+ }
1232
+ }
1233
+ /** 属性名拆成前缀(: v-bind: @ v-on:)、本名与修饰符。 */
1234
+ function splitName(name) {
1235
+ const match = /^(:|v-bind:|@|v-on:)?([^.]+)(\..*)?$/.exec(name);
1236
+ return {
1237
+ prefix: match?.[1] ?? "",
1238
+ base: match?.[2] ?? name,
1239
+ modifiers: match?.[3] ?? ""
1240
+ };
1241
+ }
1242
+ const isEventPrefix = (prefix) => prefix === "@" || prefix === "v-on:";
1243
+ /** 新属性名按原写法的风格出:原来是短横线或在模板与 HTML 里就写短横线,JSX 里写驼峰。 */
1244
+ function styled(base, to, dialect) {
1245
+ if (base.includes("-")) return kebab(to);
1246
+ if (/[A-Z]/.test(base)) return to;
1247
+ return dialect === "jsx" ? to : kebab(to);
1248
+ }
1249
+ /** 属性改名;名字对上了就返回 true(改了或报了),后面不再按删除清单报它。 */
1250
+ function rewriteProp(rule, attribute, dialect, draft, edits) {
1251
+ const { prefix, base } = splitName(attribute.name);
1252
+ if (isEventPrefix(prefix) || camel(base) !== rule.from) return false;
1253
+ const name = styled(base, rule.to, dialect);
1254
+ if (rule.booleanTo === void 0) {
1255
+ edits.push({
1256
+ start: attribute.start,
1257
+ end: attribute.start + prefix.length + base.length,
1258
+ text: prefix + name
1259
+ });
1260
+ return true;
1261
+ }
1262
+ const raw = attribute.value?.trim();
1263
+ const truthy = raw === void 0 || attribute.braced && raw === "true" || (prefix === ":" || prefix === "v-bind:" ? raw === "true" : !attribute.braced);
1264
+ if ((attribute.braced || prefix !== "") && raw === "false") {
1265
+ const start = /\s/.test(draft.text[attribute.start - 1] ?? "") ? attribute.start - 1 : attribute.start;
1266
+ edits.push({
1267
+ start,
1268
+ end: attribute.end,
1269
+ text: ""
1270
+ });
1271
+ return true;
1272
+ }
1273
+ if (!truthy) {
1274
+ draft.report(attribute.start, `${rule.from} 写了表达式:改成 ${rule.to},开着时取 ${JSON.stringify(rule.booleanTo)},关着时不写`);
1275
+ return true;
1276
+ }
1277
+ const value = rule.booleanTo;
1278
+ const text = typeof value === "string" ? `${name}="${value}"` : dialect === "jsx" ? `${name}={${value}}` : dialect === "vue" ? `:${name}="${value}"` : `${name}="${value}"`;
1279
+ edits.push({
1280
+ start: attribute.start,
1281
+ end: attribute.end,
1282
+ text
1283
+ });
1284
+ return true;
1285
+ }
1286
+ /** 事件改名;名字对上了就返回 true。 */
1287
+ function rewriteEvent(rule, attribute, edits) {
1288
+ const { prefix, base, modifiers } = splitName(attribute.name);
1289
+ if (isEventPrefix(prefix) && kebab(camel(base)) === rule.from) {
1290
+ edits.push({
1291
+ start: attribute.start,
1292
+ end: attribute.start + attribute.name.length,
1293
+ text: `${prefix}${base.includes("-") || base === base.toLowerCase() ? rule.to : camel(rule.to)}${modifiers}`
1294
+ });
1295
+ return true;
1296
+ }
1297
+ if (prefix === "" && base === `on${pascal(rule.from)}`) {
1298
+ edits.push({
1299
+ start: attribute.start,
1300
+ end: attribute.start + base.length,
1301
+ text: `on${pascal(rule.to)}`
1302
+ });
1303
+ return true;
1304
+ }
1305
+ return false;
1306
+ }
1307
+ /** 标签里的属性与事件改名,顺带报出本 major 删掉的属性。 */
1308
+ function rewriteTags(draft, dialect, rules, surface) {
1309
+ const scopes = new Set(surface.scopes);
1310
+ const elementTags = new Set(surface.tags);
1311
+ const edits = [];
1312
+ for (const tag of scanTags(draft.text)) {
1313
+ const scope = scopeOf(tag.name, scopes);
1314
+ if (!scope) continue;
1315
+ const isElement = elementTags.has(tag.name);
1316
+ const tagDialect = tag.name.startsWith("xh-") && dialect !== "vue" ? "html" : dialect;
1317
+ const removedProps = new Set(surface.removed.props[scope] ?? []);
1318
+ const removedAttributes = new Set(isElement ? surface.removed.elementAttributes[tag.name] ?? [] : []);
1319
+ for (const attribute of tag.attributes) {
1320
+ let handled = false;
1321
+ for (const rule of rules.props ?? []) {
1322
+ if (!rule.components.includes(scope)) continue;
1323
+ if (rule.only === "web-components" && !(isElement && dialect !== "vue")) continue;
1324
+ handled = rewriteProp(rule, attribute, tagDialect, draft, edits) || handled;
1325
+ }
1326
+ for (const rule of rules.events ?? []) if (rule.components.includes(scope)) handled = rewriteEvent(rule, attribute, edits) || handled;
1327
+ if (handled) continue;
1328
+ const { prefix, base } = splitName(attribute.name);
1329
+ const asProp = isEventPrefix(prefix) ? `on${pascal(base)}` : camel(base);
1330
+ if (removedProps.has(asProp) || removedAttributes.has(base)) draft.report(attribute.start, `${scope} 的 ${attribute.name} 在 ${rules.major}.0 删除了,按更新日志改写`);
1331
+ }
1332
+ }
1333
+ draft.splice(edits);
1334
+ }
1335
+ function parseSpecifiers(list) {
1336
+ return list.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
1337
+ const match = /^(type\s+)?([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(part);
1338
+ return match ? {
1339
+ type: Boolean(match[1]),
1340
+ name: match[2],
1341
+ alias: match[3]
1342
+ } : {
1343
+ type: false,
1344
+ name: part
1345
+ };
1346
+ });
1347
+ }
1348
+ const printSpecifier = (spec) => `${spec.type ? "type " : ""}${spec.name}${spec.alias ? ` as ${spec.alias}` : ""}`;
1349
+ /** 一组名字从一个模块搬到另一个模块:改导入语句,没起别名的顺带改文件里的用处。 */
1350
+ function moveImports(draft, from, to, names) {
1351
+ const locals = /* @__PURE__ */ new Map();
1352
+ draft.replace(/import\s+(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\3(\s*;)?/g, (match) => {
1353
+ if (match[4] !== from) return void 0;
1354
+ const specifiers = parseSpecifiers(match[2]);
1355
+ const moving = specifiers.filter((spec) => Object.hasOwn(names, spec.name));
1356
+ if (moving.length === 0) return void 0;
1357
+ const staying = specifiers.filter((spec) => !moving.includes(spec));
1358
+ const moved = moving.map((spec) => {
1359
+ const renamed = names[spec.name] === true ? spec.name : names[spec.name];
1360
+ if (!spec.alias && renamed !== spec.name) locals.set(spec.name, renamed);
1361
+ return {
1362
+ ...spec,
1363
+ name: renamed
1364
+ };
1365
+ });
1366
+ const keyword = match[1] ?? "";
1367
+ const quote = match[3];
1368
+ const semi = match[5] ?? "";
1369
+ const statement = (specs, module) => `import ${keyword}{ ${specs.map(printSpecifier).join(", ")} } from ${quote}${module}${quote}${semi}`;
1370
+ return staying.length > 0 ? `${statement(staying, from)}\n${statement(moved, to)}` : statement(moved, to);
1371
+ });
1372
+ for (const [name, renamed] of locals) draft.replace(new RegExp(`(?<![\\w$.])${escape(name)}(?![\\w$])`, "g"), () => renamed);
1373
+ }
1374
+ /** 从 @xihan-ui 各包导入的名字(导入时的原名)。 */
1375
+ function libraryImports(text) {
1376
+ const out = /* @__PURE__ */ new Set();
1377
+ for (const match of text.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]@xihan-ui\/[^'"]+['"]/g)) for (const spec of parseSpecifiers(match[1])) out.add(spec.name);
1378
+ return out;
1379
+ }
1380
+ function migrateMajor(draft, dialect, { rules, surface }) {
1381
+ for (const check of rules.manual ?? []) for (const match of draft.text.matchAll(new RegExp(check.pattern.source, `${check.pattern.flags.replace("g", "")}g`))) draft.report(match.index, check.message);
1382
+ const modules = Object.entries(rules.modules ?? {}).sort(([a], [b]) => b.length - a.length);
1383
+ for (const [from, to] of modules) draft.replace(new RegExp(`(['"\`])${escape(from)}(?=(?:/[^'"\`]*)?\\1)`, "g"), (match) => `${match[1]}${to}`);
1384
+ for (const moved of rules.imports ?? []) moveImports(draft, moved.from, moved.to, moved.names);
1385
+ if (dialect !== "css") rewriteTags(draft, dialect, rules, surface);
1386
+ const identifiers = {
1387
+ ...surface.renamed.identifiers,
1388
+ ...rules.identifiers
1389
+ };
1390
+ const imported = libraryImports(draft.text);
1391
+ for (const [from, rename] of Object.entries(identifiers)) {
1392
+ const to = typeof rename === "string" ? rename : rename.to;
1393
+ const attrs = typeof rename === "string" ? void 0 : rename.attrs;
1394
+ if (!from.startsWith("Xh") && !imported.has(from)) continue;
1395
+ if (from.startsWith("Xh")) for (const [old, next] of [[from, to], [`xh-${kebab(from.slice(2))}`, `xh-${kebab(to.slice(2))}`]]) {
1396
+ draft.replace(new RegExp(`<${escape(old)}(?=[\\s/>])`, "g"), () => `<${next}${attrs ? ` ${attrs}` : ""}`);
1397
+ draft.replace(new RegExp(`</${escape(old)}(?=\\s*>)`, "g"), () => `</${next}`);
1398
+ }
1399
+ draft.replace(new RegExp(`(?<![\\w$])${escape(from)}(?![\\w$])`, "g"), () => to);
1400
+ }
1401
+ const elements = [...Object.entries(surface.renamed.elements).map(([from, to]) => ({
1402
+ from,
1403
+ to
1404
+ })), ...rules.elements ?? []];
1405
+ for (const element of elements) renameElement(draft, dialect, element);
1406
+ for (const rename of rules.parts ?? []) renamePart(draft, rename);
1407
+ for (const rule of rules.events ?? []) rewriteListeners(draft, rule);
1408
+ const cssVariables = {
1409
+ ...surface.renamed.cssVariables,
1410
+ ...rules.cssVariables
1411
+ };
1412
+ for (const [from, to] of Object.entries(cssVariables)) draft.replace(new RegExp(`${escape(from)}(?![\\w-])`, "g"), () => to);
1413
+ for (const [from, to] of [...Object.entries(rules.keyframes ?? {}), ...Object.entries(rules.dataAttributes ?? {})]) draft.replace(new RegExp(`(?<![\\w-])${escape(from)}(?![\\w-])`, "g"), () => to);
1414
+ reportRemoved(draft, rules.major, surface);
1415
+ }
1416
+ /** 元素改名:先改元素里的部件,再改开闭标签、字符串里的元素名与样式表里的元素选择器。 */
1417
+ function renameElement(draft, dialect, { from, to, attrs, parts }) {
1418
+ if (parts) renamePartsInside(draft, from, parts);
1419
+ draft.replace(new RegExp(`<${escape(from)}(?=[\\s/>])`, "g"), () => `<${to}${attrs ? ` ${attrs}` : ""}`);
1420
+ draft.replace(new RegExp(`</${escape(from)}(?=\\s*>)`, "g"), () => `</${to}`);
1421
+ draft.replace(new RegExp(`(['"\`])${escape(from)}\\1`, "g"), (match) => `${match[1]}${to}${match[1]}`);
1422
+ if (dialect === "css" || dialect === "vue" || dialect === "html") draft.replace(new RegExp(`(?<![\\w-])${escape(from)}(?=[\\s.:#\\[{,>+~)])`, "g"), () => to);
1423
+ }
1424
+ /** 元素块(开标签到同名闭标签)里的 data-xh-part 改名。 */
1425
+ function renamePartsInside(draft, tag, parts) {
1426
+ const block = new RegExp(`<${escape(tag)}(?=[\\s/>])[\\s\\S]*?</${escape(tag)}\\s*>`, "g");
1427
+ draft.replace(block, (match) => match[0].replace(/data-xh-part=(["'])([\w-]+)\1/g, (whole, quote, part) => {
1428
+ const next = parts[part];
1429
+ return next ? `data-xh-part=${quote}${next}${quote}` : whole;
1430
+ }));
1431
+ }
1432
+ /** 部件改名:自写皮肤里 [data-scope][data-part] 与挂载类的写法,Web Components 元素里的 data-xh-part。 */
1433
+ function renamePart(draft, { components, from, to }) {
1434
+ for (const scope of components) {
1435
+ const part = `(\\[data-part=(['"]?))${escape(from)}(\\2\\])`;
1436
+ draft.replace(new RegExp(`(\\[data-scope=(['"]?)${escape(scope)}\\2\\]|\\.xh-scope-${escape(scope)}(?![\\w-]))${part}`, "g"), (match) => `${match[1]}${match[3]}${to}${match[5]}`);
1437
+ renamePartsInside(draft, `xh-${scope}`, { [from]: to });
1438
+ }
1439
+ }
1440
+ /** Web Components 的事件监听:够独特的事件直接改名,与原生事件同名的只报出来。 */
1441
+ function rewriteListeners(draft, rule) {
1442
+ const pattern = new RegExp(`((?:add|remove)EventListener\\(\\s*)(['"\`])${escape(rule.from)}\\2`, "g");
1443
+ if (rule.distinctive) {
1444
+ draft.replace(pattern, (match) => `${match[1]}${match[2]}${rule.to}${match[2]}`);
1445
+ return;
1446
+ }
1447
+ if (!new RegExp(rule.components.map((scope) => `<xh-${escape(scope)}(?=[\\s/>])|['"\`]xh-${escape(scope)}['"\`]|Xh${pascal(scope)}Element`).join("|")).test(draft.text)) return;
1448
+ for (const match of draft.text.matchAll(pattern)) draft.report(match.index, `${rule.components.join("、")} 的 ${rule.from} 事件改名 ${rule.to};这里监听的若是这些元素,改成 '${rule.to}'`);
1449
+ }
1450
+ /** 本 major 删掉、迁移后仍出现的名字逐个报出来。 */
1451
+ function reportRemoved(draft, major, surface) {
1452
+ const { removed } = surface;
1453
+ const text = draft.text;
1454
+ const words = /* @__PURE__ */ new Map();
1455
+ for (const match of text.matchAll(/[A-Z_$][\w$]*/gi)) if (!words.has(match[0])) words.set(match[0], match.index);
1456
+ const importedFrom = /* @__PURE__ */ new Map();
1457
+ for (const match of text.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) for (const spec of parseSpecifiers(match[1])) importedFrom.set(spec.name, [...importedFrom.get(spec.name) ?? [], match[2]]);
1458
+ for (const [pkg, names] of Object.entries(removed.exports)) for (const name of names) {
1459
+ const at = words.get(name);
1460
+ const modules = importedFrom.get(name);
1461
+ const fromPackage = modules?.some((module) => module === pkg || module.startsWith(`${pkg}/`));
1462
+ if (at !== void 0 && (fromPackage || !modules && name.startsWith("Xh"))) draft.report(at, `${name}(${pkg})在 ${major}.0 删除了,按更新日志改写`);
1463
+ }
1464
+ for (const subpath of removed.subpaths) {
1465
+ const at = text.search(new RegExp(`['"]${escape(subpath)}['"]`));
1466
+ if (at >= 0) draft.report(at, `子入口 ${subpath} 在 ${major}.0 删除了`);
1467
+ }
1468
+ const tokens = (pattern) => {
1469
+ const out = /* @__PURE__ */ new Map();
1470
+ for (const match of text.matchAll(pattern)) if (!out.has(match[1])) out.set(match[1], match.index);
1471
+ return out;
1472
+ };
1473
+ const variables = tokens(/(--xh-[\w-]+)/g);
1474
+ for (const name of removed.cssVariables) {
1475
+ const at = variables.get(name);
1476
+ if (at !== void 0) draft.report(at, `${name} 在 ${major}.0 删除了,按更新日志改写`);
1477
+ }
1478
+ const kebabWords = tokens(/(?<![\w-])((?:xh|data)-[\w-]+)/g);
1479
+ for (const name of [...removed.keyframes, ...removed.dataAttributes]) {
1480
+ const at = kebabWords.get(name);
1481
+ if (at !== void 0) draft.report(at, `${name} 在 ${major}.0 删除了,按更新日志改写`);
1482
+ }
1483
+ for (const element of removed.elements) {
1484
+ const at = text.search(new RegExp(`<${escape(element)}(?=[\\s/>])|['"\`]${escape(element)}['"\`]`));
1485
+ if (at >= 0) draft.report(at, `<${element}> 在 ${major}.0 删除了,按更新日志改写`);
1486
+ }
1487
+ for (const [scope, parts] of Object.entries(removed.parts)) for (const part of parts) {
1488
+ const at = text.search(new RegExp(`(?:\\[data-scope=(['"]?)${escape(scope)}\\1\\]|\\.xh-scope-${escape(scope)}(?![\\w-]))\\[data-part=(['"]?)${escape(part)}\\2\\]`));
1489
+ if (at >= 0) draft.report(at, `${scope} 的部件 ${part} 在 ${major}.0 删除了,按更新日志改写选择器`);
1490
+ }
1491
+ }
1492
+ /** 按从低到高的几套规则改一份源码。 */
1493
+ function migrateSource(text, dialect, migrations) {
1494
+ if (!LIBRARY.test(text)) return {
1495
+ text,
1496
+ edits: 0,
1497
+ findings: []
1498
+ };
1499
+ const draft = new Draft(text);
1500
+ for (const migration of migrations) migrateMajor(draft, dialect, migration);
1501
+ return {
1502
+ text: draft.text,
1503
+ edits: draft.edits,
1504
+ findings: draft.findings
1505
+ };
1506
+ }
1507
+ /** 不进去的目录:依赖、产物与点开头的目录(.git、.nuxt 一类)。 */
1508
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
1509
+ "node_modules",
1510
+ "dist",
1511
+ "build",
1512
+ "coverage",
1513
+ "out"
1514
+ ]);
1515
+ function* sourceFiles(entry) {
1516
+ let stat;
1517
+ try {
1518
+ stat = statSync(entry);
1519
+ } catch {
1520
+ return;
1521
+ }
1522
+ if (stat.isFile()) {
1523
+ if (dialectOf(entry)) yield entry;
1524
+ return;
1525
+ }
1526
+ for (const dirent of readdirSync(entry, { withFileTypes: true })) {
1527
+ if (dirent.name.startsWith(".") || SKIP_DIRS.has(dirent.name)) continue;
1528
+ yield* sourceFiles(join(entry, dirent.name));
1529
+ }
1530
+ }
1531
+ /** 迁移一组文件或目录;write 为假时只算不写。 */
1532
+ function migrateProject(options) {
1533
+ const files = [];
1534
+ for (const path of options.paths) for (const file of sourceFiles(resolve(options.cwd, path))) {
1535
+ const text = readFileSync(file, "utf8");
1536
+ const result = migrateSource(text, dialectOf(file), options.migrations);
1537
+ if (result.edits === 0 && result.findings.length === 0) continue;
1538
+ if (options.write && result.text !== text) writeFileSync(file, result.text);
1539
+ files.push({
1540
+ file: relative(options.cwd, file).split(sep).join("/"),
1541
+ edits: result.edits,
1542
+ findings: result.findings
1543
+ });
1544
+ }
1545
+ return {
1546
+ from: options.from,
1547
+ to: options.to,
1548
+ majors: options.migrations.map(({ rules }) => rules.major),
1549
+ files: files.sort((a, b) => a.file.localeCompare(b.file)),
1550
+ notes: options.migrations.map(({ rules }) => ({
1551
+ major: rules.major,
1552
+ notes: rules.notes
1553
+ })),
1554
+ written: options.write
1555
+ };
1556
+ }
1557
+ /** 迁移结果写成给人看的 markdown。 */
1558
+ function renderMigration(report) {
1559
+ const changed = report.files.filter((file) => file.edits > 0);
1560
+ const edits = changed.reduce((sum, file) => sum + file.edits, 0);
1561
+ const lines = [
1562
+ `# 迁移 ${report.from}.x → ${report.to}.0`,
1563
+ "",
1564
+ `依次套用 ${report.majors.map((major) => `${major}.0`).join("、")} 的规则:${changed.length > 0 ? `改写 ${changed.length} 个文件,共 ${edits} 处` : "没有要改的地方"}${report.written || changed.length === 0 ? "" : "(未写回)"}。`
1565
+ ];
1566
+ if (changed.length > 0) lines.push("", ...changed.map((file) => `- ${file.file}:${file.edits} 处`));
1567
+ const findings = report.files.flatMap((file) => file.findings.map((finding) => `- ${file.file}:${finding.line} ${finding.message}`));
1568
+ if (findings.length > 0) lines.push("", `## 要人工处理(${findings.length} 处)`, "", ...findings);
1569
+ for (const { major, notes } of report.notes) if (notes.length > 0) lines.push("", `## ${major}.0 升级须知里改不了代码的`, "", ...notes.map((note) => `- ${note}`));
1570
+ return lines.join("\n");
1571
+ }
1572
+ //#endregion
1573
+ //#region src/migrations/index.ts
1574
+ const RULES = [
1575
+ {
1576
+ major: 2,
1577
+ modules: {
1578
+ "@xihan-ui/kernel/metadata": "@xihan-ui/core/metadata",
1579
+ "@xihan-ui/kernel/skin-check": "@xihan-ui/core/skin-check",
1580
+ "@xihan-ui/kernel/vite": "@xihan-ui/core/vite",
1581
+ "@xihan-ui/machine/vanilla": "@xihan-ui/core/vanilla",
1582
+ "@xihan-ui/behavior/presence": "@xihan-ui/core/presence",
1583
+ "@xihan-ui/kernel": "@xihan-ui/core",
1584
+ "@xihan-ui/machine": "@xihan-ui/core",
1585
+ "@xihan-ui/behavior": "@xihan-ui/core"
1586
+ },
1587
+ components: {
1588
+ Ellipsis: "Truncate",
1589
+ DynamicInput: "FieldArray",
1590
+ Time: "Timestamp",
1591
+ QrCode: "MatrixCode",
1592
+ Calendar: "CalendarPicker",
1593
+ Result: "EmptyState",
1594
+ Space: "Flex",
1595
+ Countdown: "Timer"
1596
+ },
1597
+ props: [
1598
+ {
1599
+ components: ["password-input"],
1600
+ from: "visible",
1601
+ to: "revealed"
1602
+ },
1603
+ {
1604
+ components: ["password-input"],
1605
+ from: "defaultVisible",
1606
+ to: "defaultRevealed"
1607
+ },
1608
+ {
1609
+ components: ["resizable", "floating-panel"],
1610
+ from: "size",
1611
+ to: "dimensions"
1612
+ },
1613
+ {
1614
+ components: ["resizable", "floating-panel"],
1615
+ from: "defaultSize",
1616
+ to: "defaultDimensions"
1617
+ },
1618
+ {
1619
+ components: ["floating-panel"],
1620
+ from: "stage",
1621
+ to: "windowState"
1622
+ },
1623
+ {
1624
+ components: ["floating-panel"],
1625
+ from: "defaultStage",
1626
+ to: "defaultWindowState"
1627
+ },
1628
+ {
1629
+ components: ["table"],
1630
+ from: "expanded",
1631
+ to: "expandedValue"
1632
+ },
1633
+ {
1634
+ components: ["table"],
1635
+ from: "defaultExpanded",
1636
+ to: "defaultExpandedValue"
1637
+ },
1638
+ {
1639
+ components: ["steps", "tour"],
1640
+ from: "step",
1641
+ to: "value"
1642
+ },
1643
+ {
1644
+ components: ["steps", "tour"],
1645
+ from: "defaultStep",
1646
+ to: "defaultValue"
1647
+ },
1648
+ {
1649
+ components: ["mention"],
1650
+ from: "prefix",
1651
+ to: "triggerPrefix"
1652
+ },
1653
+ {
1654
+ components: ["image-viewer"],
1655
+ from: "items",
1656
+ to: "collection"
1657
+ },
1658
+ {
1659
+ components: ["toast"],
1660
+ from: "type",
1661
+ to: "tone"
1662
+ },
1663
+ {
1664
+ components: ["listbox"],
1665
+ from: "multiple",
1666
+ to: "selectionMode",
1667
+ booleanTo: "multiple"
1668
+ }
1669
+ ],
1670
+ events: [
1671
+ {
1672
+ components: ["password-input"],
1673
+ from: "visibility-change",
1674
+ to: "revealed-change",
1675
+ distinctive: true
1676
+ },
1677
+ {
1678
+ components: ["back-top"],
1679
+ from: "visible-change",
1680
+ to: "visibility-change",
1681
+ distinctive: true
1682
+ },
1683
+ {
1684
+ components: ["resizable", "floating-panel"],
1685
+ from: "size-change",
1686
+ to: "dimensions-change",
1687
+ distinctive: true
1688
+ },
1689
+ {
1690
+ components: ["resizable"],
1691
+ from: "size-change-end",
1692
+ to: "dimensions-change-end",
1693
+ distinctive: true
1694
+ },
1695
+ {
1696
+ components: ["floating-panel"],
1697
+ from: "stage-change",
1698
+ to: "window-state-change",
1699
+ distinctive: true
1700
+ },
1701
+ {
1702
+ components: [
1703
+ "json-viewer",
1704
+ "side-nav",
1705
+ "table",
1706
+ "tree",
1707
+ "tree-select"
1708
+ ],
1709
+ from: "expanded-change",
1710
+ to: "expanded-value-change",
1711
+ distinctive: true
1712
+ },
1713
+ {
1714
+ components: ["steps", "tour"],
1715
+ from: "step-change",
1716
+ to: "value-change"
1717
+ },
1718
+ {
1719
+ components: ["number-animation"],
1720
+ from: "finish",
1721
+ to: "complete"
1722
+ },
1723
+ {
1724
+ components: ["virtualizer"],
1725
+ from: "change",
1726
+ to: "range-change"
1727
+ }
1728
+ ],
1729
+ parts: [
1730
+ {
1731
+ components: ["alert", "empty-state"],
1732
+ from: "icon",
1733
+ to: "indicator"
1734
+ },
1735
+ {
1736
+ components: ["avatar-group"],
1737
+ from: "overflow",
1738
+ to: "overflow-item"
1739
+ },
1740
+ {
1741
+ components: ["checkbox-group"],
1742
+ from: "trigger",
1743
+ to: "select-all-trigger"
1744
+ },
1745
+ {
1746
+ components: ["clipboard"],
1747
+ from: "trigger",
1748
+ to: "copy-trigger"
1749
+ },
1750
+ {
1751
+ components: ["combobox", "listbox"],
1752
+ from: "item-group",
1753
+ to: "group"
1754
+ },
1755
+ {
1756
+ components: ["combobox", "listbox"],
1757
+ from: "item-group-label",
1758
+ to: "group-label"
1759
+ },
1760
+ {
1761
+ components: ["date-picker", "time-picker"],
1762
+ from: "presets",
1763
+ to: "preset-group"
1764
+ },
1765
+ {
1766
+ components: ["fieldset"],
1767
+ from: "helper-text",
1768
+ to: "description"
1769
+ },
1770
+ {
1771
+ components: ["file-upload"],
1772
+ from: "item-group",
1773
+ to: "list"
1774
+ },
1775
+ {
1776
+ components: ["floating-panel"],
1777
+ from: "stage-trigger",
1778
+ to: "window-state-trigger"
1779
+ },
1780
+ {
1781
+ components: ["heatmap"],
1782
+ from: "week-day-label",
1783
+ to: "week-day"
1784
+ },
1785
+ {
1786
+ components: ["page-header"],
1787
+ from: "subtitle",
1788
+ to: "description"
1789
+ },
1790
+ {
1791
+ components: ["pagination"],
1792
+ from: "ellipsis",
1793
+ to: "ellipsis-trigger"
1794
+ },
1795
+ {
1796
+ components: ["signature-pad"],
1797
+ from: "segment",
1798
+ to: "path"
1799
+ },
1800
+ {
1801
+ components: ["skeleton"],
1802
+ from: "bone",
1803
+ to: "item"
1804
+ },
1805
+ {
1806
+ components: ["slider"],
1807
+ from: "mark",
1808
+ to: "tick"
1809
+ },
1810
+ {
1811
+ components: ["slider"],
1812
+ from: "mark-label",
1813
+ to: "tick-label"
1814
+ },
1815
+ {
1816
+ components: ["slider"],
1817
+ from: "marks",
1818
+ to: "tick-group"
1819
+ },
1820
+ {
1821
+ components: ["sortable"],
1822
+ from: "item-handle",
1823
+ to: "item-drag-trigger"
1824
+ },
1825
+ {
1826
+ components: ["table"],
1827
+ from: "loading-state",
1828
+ to: "loading"
1829
+ },
1830
+ {
1831
+ components: ["timer"],
1832
+ from: "area",
1833
+ to: "display"
1834
+ },
1835
+ {
1836
+ components: ["card"],
1837
+ from: "body",
1838
+ to: "content"
1839
+ }
1840
+ ],
1841
+ dataAttributes: {
1842
+ "data-affixed": "data-fixed",
1843
+ "data-stage": "data-window-state",
1844
+ "data-target-stage": "data-target-window-state"
1845
+ },
1846
+ manual: [
1847
+ {
1848
+ pattern: /\bdata-borderless\b/,
1849
+ message: "data-borderless 删除,改认 data-bordered,取值反转"
1850
+ },
1851
+ {
1852
+ pattern: /\bdata-type\b/,
1853
+ message: "data-type 拆分为 data-value-type、data-severity、data-select-mode、data-reveal-mode、data-format,按组件改认"
1854
+ },
1855
+ {
1856
+ pattern: /\bdata-(?:phase|status)\b/,
1857
+ message: "data-phase 与六个组件的生命周期 data-status 并入 data-state"
1858
+ },
1859
+ {
1860
+ pattern: /\b(?:XhThread|XhComposer|XhCodeBlock)\w*|<xh-(?:thread|composer|code-block)\b/,
1861
+ message: "thread、composer、code-block 删除,分别改用 message-feed(配合 log)、prompt-input、code-view"
1862
+ },
1863
+ {
1864
+ pattern: /\bXhPopselect\w*|<xh-popselect\b/,
1865
+ message: "popselect 退役:随表单提交的场景改用 select,仅切换视图参数的场景改用 popover 组合 listbox"
1866
+ },
1867
+ {
1868
+ pattern: /\bXhSpace\b|<xh-space\b/,
1869
+ message: "space 并入 flex:XhSpace 未写 gap 时缺省有间距,XhFlex 缺省为 0,迁移后显式写 gap=\"md\""
1870
+ },
1871
+ {
1872
+ pattern: /\bXhResult\w*|<xh-result\b/,
1873
+ message: "result 并入 empty-state:live 缺省为 polite、缺省尺寸小一号,保持原样写 live=\"off\" 与 size=\"lg\""
1874
+ },
1875
+ {
1876
+ pattern: /(?:\bXhFlex|<xh-flex\b)[^>]*\s:?direction=/,
1877
+ message: "flex 的 direction 别名删除,只保留 orientation"
1878
+ },
1879
+ {
1880
+ pattern: /(?:\bXh(?:Descriptions|List|Layout|PageHeader)|<xh-(?:descriptions|list|layout|page-header)\b)[^>]*\s:?bordered\b|(?:\bXhTable|<xh-table\b)[^>]*\s:?borderless\b/,
1881
+ message: "descriptions、list、layout、page-header 的 bordered 与 table 的 borderless 改为 variant 形态轴"
1882
+ },
1883
+ {
1884
+ pattern: /\b(?:XhConfig|xhConfig)\b[\s\S]{1,80}\bmotion\b/,
1885
+ message: "XhConfig.motion 的隐式全局覆盖移除,视觉环境统一由 createVisualEnvironmentController 管理"
1886
+ },
1887
+ {
1888
+ pattern: /\bXhFormFieldGroup\b[^>]*\b:?value=/,
1889
+ message: "XhFormFieldGroup 的 value 改名为 name(表单字段标识统一为 FormPath)"
1890
+ }
1891
+ ],
1892
+ notes: [
1893
+ "@xihan-ui/kernel、machine、behavior 合并为 @xihan-ui/core,旧包不再发布:导入路径已改,package.json 里的依赖换成 @xihan-ui/core",
1894
+ "@xihan-ui/code-highlight 改为可选 peer 依赖,用代码着色的项目自行安装",
1895
+ "select、tags-input、tag-group 的标签统一改用 tag 组件渲染:item-delete-trigger、item-preview、item-text 对应 tag 的 root / label / close-trigger",
1896
+ "部件上同名事件处理器统一为使用者的先执行;pin-input 按顺序录入;combobox 等的原生表单提交改为每个值一个同名隐藏字段",
1897
+ "读屏文案只接受函数;kbd 的单键与组合键统一为 keys 数组;grid 的 cols / span / offset 增加取值范围校验"
1898
+ ]
1899
+ },
1900
+ {
1901
+ major: 3,
1902
+ identifiers: {
1903
+ XhToastRoot: {
1904
+ to: "XhNotificationItem",
1905
+ attrs: "preset=\"toast\""
1906
+ },
1907
+ XhToastRootProps: "XhNotificationItemProps",
1908
+ XhToastIndicator: "XhNotificationItemIndicator",
1909
+ XhToastIndicatorProps: "XhNotificationItemIndicatorProps",
1910
+ XhToastContent: "XhNotificationItemContent",
1911
+ XhToastContentProps: "XhNotificationItemContentProps",
1912
+ XhToastTitle: "XhNotificationItemTitle",
1913
+ XhToastTitleProps: "XhNotificationItemTitleProps",
1914
+ XhToastDescription: "XhNotificationItemDescription",
1915
+ XhToastDescriptionProps: "XhNotificationItemDescriptionProps",
1916
+ XhToastActionTrigger: "XhNotificationItemActionTrigger",
1917
+ XhToastActionTriggerProps: "XhNotificationItemActionTriggerProps",
1918
+ XhToastProgress: "XhNotificationItemProgress",
1919
+ XhToastProgressProps: "XhNotificationItemProgressProps",
1920
+ XhToastCloseTrigger: "XhNotificationItemCloseTrigger",
1921
+ XhToastCloseTriggerProps: "XhNotificationItemCloseTriggerProps",
1922
+ useToast: "useNotificationItem",
1923
+ ToastContext: "NotificationItemContext",
1924
+ useToastContext: "useNotificationItemContext",
1925
+ ToastRootSlotProps: "NotificationItemSlotProps",
1926
+ createToastService: "createNotificationService",
1927
+ ToastService: "NotificationService",
1928
+ ToastServiceOptions: "NotificationServiceOptions",
1929
+ ToastCreateOptions: "NotificationCreateOptions",
1930
+ ToastMessageOptions: "NotificationMessageOptions",
1931
+ ToastPromiseOptions: "NotificationPromiseOptions",
1932
+ withToastSound: "withNotificationSound",
1933
+ ToastSoundOptions: "NotificationSoundOptions",
1934
+ withToastSoundService: "withNotificationSoundService",
1935
+ ToastSoundServicePort: "NotificationSoundServicePort",
1936
+ ToastSoundServiceOptions: "NotificationSoundServiceOptions",
1937
+ ToastSoundKey: "NotificationSoundKey",
1938
+ toastMachine: "notificationItemMachine",
1939
+ ToastSchema: "NotificationItemSchema",
1940
+ ToastApi: "NotificationItemApi",
1941
+ connectToast: "connectNotificationItem",
1942
+ toastAnatomy: "notificationAnatomy",
1943
+ toastKeyboard: "notificationKeyboard",
1944
+ toastMeta: "notificationMeta",
1945
+ resolveToastId: "resolveNotificationItemId",
1946
+ ToastTone: "NotificationTone",
1947
+ ToastRecord: "NotificationRecord",
1948
+ ToastTranslations: "NotificationTranslations",
1949
+ ToastActionDetails: "NotificationActionDetails",
1950
+ ToastPauseSource: "NotificationPauseSource",
1951
+ ToastPressedPart: "NotificationPressedPart",
1952
+ ToastStatusChangeDetails: "NotificationStatusChangeDetails",
1953
+ XhSegmentedRoot: {
1954
+ to: "XhRadioGroupRoot",
1955
+ attrs: "variant=\"segmented\""
1956
+ },
1957
+ XhSegmentedRootProps: "XhRadioGroupRootProps",
1958
+ XhSegmentedIndicator: "XhRadioGroupThumb",
1959
+ XhSegmentedIndicatorProps: "XhRadioGroupThumbProps",
1960
+ XhSegmentedItem: "XhRadioGroupItem",
1961
+ XhSegmentedItemProps: "XhRadioGroupItemProps",
1962
+ XhSegmentedItemText: "XhRadioGroupItemText",
1963
+ XhSegmentedItemTextProps: "XhRadioGroupItemTextProps",
1964
+ useSegmented: "useRadioGroup",
1965
+ useSegmentedContext: "useRadioGroupContext",
1966
+ useSegmentedItemContext: "useRadioGroupItemContext",
1967
+ SegmentedContext: "RadioGroupContext",
1968
+ timePickerColumns: "timeColumns",
1969
+ timePickerColumnsFor: "timeColumnsFor",
1970
+ timePickerItemValue: "timeItemValue",
1971
+ TimePickerColumnsOptions: "TimeColumnsOptions"
1972
+ },
1973
+ elements: [{
1974
+ from: "xh-toast",
1975
+ to: "xh-notification-item",
1976
+ attrs: "preset=\"toast\"",
1977
+ parts: {
1978
+ "root": "item",
1979
+ "indicator": "item-indicator",
1980
+ "content": "item-content",
1981
+ "title": "item-title",
1982
+ "description": "item-description",
1983
+ "action-trigger": "item-action-trigger",
1984
+ "progress": "item-progress",
1985
+ "close-trigger": "item-close-trigger"
1986
+ }
1987
+ }, {
1988
+ from: "xh-segmented",
1989
+ to: "xh-radio-group",
1990
+ attrs: "variant=\"segmented\"",
1991
+ parts: { indicator: "thumb" }
1992
+ }],
1993
+ cssVariables: {
1994
+ "--xh-spin-duration": "--xh-motion-loop-spin",
1995
+ "--xh-caret-duration": "--xh-motion-loop-caret",
1996
+ "--xh-shimmer-duration": "--xh-motion-loop-shimmer",
1997
+ "--xh-toast-icon-fg": "--xh-notification-indicator-fg",
1998
+ "--xh-toast-scale-collapsed": "--xh-notification-stack-scale",
1999
+ "--xh-segmented-bg": "--xh-radio-group-track-bg",
2000
+ "--xh-segmented-bg-disabled": "--xh-radio-group-track-bg-disabled",
2001
+ "--xh-segmented-border": "--xh-radio-group-track-border",
2002
+ "--xh-segmented-border-invalid": "--xh-radio-group-track-border-invalid",
2003
+ "--xh-segmented-radius": "--xh-radio-group-track-radius",
2004
+ "--xh-segmented-track-padding": "--xh-radio-group-track-padding",
2005
+ "--xh-segmented-item-bg-hover": "--xh-radio-group-segment-bg-hover",
2006
+ "--xh-segmented-item-bg-pressed": "--xh-radio-group-segment-bg-pressed",
2007
+ "--xh-segmented-item-fg": "--xh-radio-group-segment-fg",
2008
+ "--xh-segmented-item-fg-checked": "--xh-radio-group-segment-fg-checked",
2009
+ "--xh-segmented-item-fg-checked-disabled": "--xh-radio-group-segment-fg-checked-disabled",
2010
+ "--xh-segmented-item-fg-hover": "--xh-radio-group-segment-fg-hover",
2011
+ "--xh-segmented-item-font-weight": "--xh-radio-group-segment-font-weight",
2012
+ "--xh-segmented-item-h": "--xh-radio-group-segment-h",
2013
+ "--xh-segmented-item-px": "--xh-radio-group-segment-px",
2014
+ "--xh-segmented-item-radius": "--xh-radio-group-segment-radius",
2015
+ "--xh-segmented-indicator-bg": "--xh-radio-group-thumb-bg",
2016
+ "--xh-segmented-indicator-border": "--xh-radio-group-thumb-border",
2017
+ "--xh-segmented-indicator-radius": "--xh-radio-group-thumb-radius",
2018
+ "--xh-segmented-indicator-shadow": "--xh-radio-group-thumb-shadow",
2019
+ "--xh-segmented-indicator-shadow-disabled": "--xh-radio-group-thumb-shadow-disabled",
2020
+ "--xh-icon-wrapper-size": "--xh-icon-frame-size",
2021
+ "--xh-icon-wrapper-glyph-size": "--xh-icon-frame-glyph-size",
2022
+ "--xh-icon-wrapper-radius": "--xh-icon-frame-radius",
2023
+ "--xh-icon-wrapper-bg": "--xh-icon-frame-bg",
2024
+ "--xh-icon-wrapper-shadow": "--xh-icon-frame-shadow",
2025
+ "--xh-gradient-text-from": "--xh-typography-gradient-from",
2026
+ "--xh-gradient-text-to": "--xh-typography-gradient-to"
2027
+ },
2028
+ keyframes: {
2029
+ "xh-toast-in": "xh-notification-stack-in",
2030
+ "xh-toast-out": "xh-notification-stack-out",
2031
+ "xh-spinner-rotate": "xh-spin",
2032
+ "xh-popconfirm-rotate": "xh-spin",
2033
+ "xh-switch-rotate": "xh-spin",
2034
+ "xh-toast-spin": "xh-spin",
2035
+ "xh-approval-rotate": "xh-spin",
2036
+ "xh-clipboard-rotate": "xh-spin",
2037
+ "xh-download-trigger-rotate": "xh-spin",
2038
+ "xh-notification-spin": "xh-spin",
2039
+ "xh-reasoning-shimmer": "xh-shimmer",
2040
+ "xh-tool-call-shimmer": "xh-shimmer",
2041
+ "xh-skeleton-shimmer": "xh-shimmer",
2042
+ "xh-approval-in": "xh-item-in",
2043
+ "xh-message-feed-item-in": "xh-item-in",
2044
+ "xh-question-flow-in": "xh-item-in",
2045
+ "xh-tool-call-enter": "xh-item-in",
2046
+ "xh-approval-result-in": "xh-pop-in",
2047
+ "xh-message-feed-button-in": "xh-pop-in",
2048
+ "xh-question-flow-result-in": "xh-pop-in",
2049
+ "xh-log-button-in": "xh-pop-in",
2050
+ "xh-reasoning-fade-in": "xh-fade-in",
2051
+ "xh-clipboard-loading-reveal": "xh-fade-in",
2052
+ "xh-download-trigger-loading-reveal": "xh-fade-in",
2053
+ "xh-clipboard-loading-hide": "xh-fade-out",
2054
+ "xh-diff-view-reveal": "xh-drop-in",
2055
+ "xh-form-summary-enter": "xh-drop-in",
2056
+ "xh-dialog-in": "xh-sheet-in",
2057
+ "xh-dialog-out": "xh-sheet-out",
2058
+ "xh-notification-in": "xh-sheet-in",
2059
+ "xh-notification-out": "xh-sheet-out",
2060
+ "xh-drawer-in-top": "xh-slide-in",
2061
+ "xh-drawer-in-right": "xh-slide-in",
2062
+ "xh-drawer-in-bottom": "xh-slide-in",
2063
+ "xh-drawer-in-left": "xh-slide-in",
2064
+ "xh-drawer-out-top": "xh-slide-out",
2065
+ "xh-drawer-out-right": "xh-slide-out",
2066
+ "xh-drawer-out-bottom": "xh-slide-out",
2067
+ "xh-drawer-out-left": "xh-slide-out"
2068
+ },
2069
+ manual: [
2070
+ {
2071
+ pattern: /\bcreateToastService\s*\(/,
2072
+ message: "createToastService(options) 改为 createNotificationService({ preset: 'toast', ...options }):名字已改,补上 preset: 'toast',否则是两层卡片"
2073
+ },
2074
+ {
2075
+ pattern: /\btoastTranslations\b/,
2076
+ message: "通知服务的选项 toastTranslations 改为 translations;全局配置的文案桶 translations.toast 改为 translations.notification"
2077
+ },
2078
+ {
2079
+ pattern: /\bresolveToastDuration\b/,
2080
+ message: "resolveToastDuration(loading, duration) 改为 resolveNotificationDuration(loading, duration, preset)"
2081
+ },
2082
+ {
2083
+ pattern: /\b(?:removeDelay|remove-delay|TOAST_REMOVE_DELAY)\b/,
2084
+ message: "Toast 与 Notification 删除了固定退场窗口 removeDelay,改为等退场动画播完再卸载;要调时长覆盖 --xh-motion-duration-exit"
2085
+ },
2086
+ {
2087
+ pattern: /\bXhIconWrapper\b|<xh-icon-wrapper\b/,
2088
+ message: "IconWrapper 并入 Icon:底框写在图标自己身上,variant 改名 frame,如 <XhIcon :icon=\"FolderIcon\" frame=\"subtle\" tone=\"brand\" size=\"lg\" />"
2089
+ },
2090
+ {
2091
+ pattern: /\bXhGradientText\b|<xh-gradient-text\b/,
2092
+ message: "GradientText 并入 Typography:写成 XhTypographyRoot 里的 XhTypographyText variant=\"gradient\",from / to / direction 改为 --xh-typography-gradient-* 覆盖槽"
2093
+ },
2094
+ {
2095
+ pattern: /\bXhSegmentedHiddenInput\b/,
2096
+ message: "分段控件并入单选组后每个条目一份原生 radio,不再有整组的 hidden-input,删掉这个部件"
2097
+ },
2098
+ {
2099
+ pattern: /\bscrollElement\b|\bscroll-element\b|\bgetScrollEl\b/,
2100
+ message: "Anchor 的滚动容器 scrollElement 改名 target(与 Affix、BackTop 一致),Headless refs getScrollEl 改为 getTargetEl"
2101
+ },
2102
+ {
2103
+ pattern: /(?:\bXhTime(?:Range)?Picker|<xh-time(?:-range)?-picker\b)[^>]*\s:?step=/,
2104
+ message: "TimePicker / TimeRangePicker 删除 step,改为按单位的 timeStep:step={15} 改为 timeStep={{ minute: 15 }}(Web Components time-step='{\"minute\":15}')"
2105
+ },
2106
+ {
2107
+ pattern: /\bisTimeUnavailable\b/,
2108
+ message: "isTimeUnavailable 改为 (value, unit, context):时列的 value 恒按 24 小时制,TimeRangePicker 原第三个参数 index 挪进 context.index"
2109
+ },
2110
+ {
2111
+ pattern: /\b(?:TOAST|NOTIFICATION)_(?:DURATION|GAP|MAX|PLACEMENT)\b/,
2112
+ message: "轻提示与通知的缺省常量改读 NOTIFICATION_PRESETS.<preset> 或 notificationPresetOf(preset)"
2113
+ },
2114
+ {
2115
+ pattern: /\[data-scope=['"]?toast['"]?\]/,
2116
+ message: "[data-scope='toast'][data-part='root'] 改认 [data-scope='notification'][data-part='item'][data-preset='toast'],其余部件改认 item-<部件>"
2117
+ },
2118
+ {
2119
+ pattern: /\[data-scope=['"]?segmented['"]?\]/,
2120
+ message: "分段控件的选择器改认 [data-scope='radio-group'][data-variant='segmented'],指示块改认 thumb 部件"
2121
+ }
2122
+ ],
2123
+ notes: [
2124
+ "Notification、Toast 的叠放测量槽(--xh-toast-offset-collapsed、--xh-toast-height 等)不再公开,叠放收拢比例改由 --xh-motion-scale-stack 逐层计算",
2125
+ "键盘统一为 APG 单选组:segmented 形态按 Enter 不再选中,Home / End 不再跳到首末段",
2126
+ "缺省值与外观:减弱动效改为去掉位移、保留淡变;Select、Combobox、TreeSelect 的候选面板与字段盒等宽;Tooltip 新增接替窗口 skipDelayDuration",
2127
+ "Headless 的 Badge、EmptyState、Marquee、Skeleton、Spinner、Timestamp、Watermark 改由状态机驱动:connectX(props, normalize) 改为 connectX(service, normalize)",
2128
+ "@xihan-ui/motion 的缓动表删除 decelerate 与 accelerate,resolveEasing 认不出写法时抛 TypeError"
2129
+ ]
2130
+ },
2131
+ {
2132
+ major: 4,
2133
+ notes: [
2134
+ "缺省值:Accordion variant 改为 outline、Tree variant 改为 ghost、Badge tone 改为 danger,日历的 weekdayFormat 改为 narrow;要保持原样请显式传值",
2135
+ "Fieldset 的禁用会压到整棵子树;Web Components 的字段族宿主缺省 display: contents,写在宿主上的宽度与外边距改写到 root 或用 --xh-control-w",
2136
+ "令牌:控件高 sm / md / lg 为 28 / 32 / 36px,圆角 2 / 4 / 8px,--xh-motion-scale-press 为 1(主题写回 0.97 可恢复按下回弹)",
2137
+ "删掉的组件覆盖槽集中在字段聚焦环、Dialog、Slider 与日期时间面板,下面逐个报出写到它们的地方"
2138
+ ]
2139
+ },
2140
+ {
2141
+ major: 5,
2142
+ imports: [
2143
+ {
2144
+ from: "@xihan-ui/headless",
2145
+ to: "@xihan-ui/core/color",
2146
+ names: {
2147
+ colorCss: true,
2148
+ colorHexToRgba: true,
2149
+ colorHslaToRgba: true,
2150
+ colorHsvaToRgba: true,
2151
+ colorNormalizeHsva: true,
2152
+ colorNormalizeRgba: true,
2153
+ colorOklchToRgba: true,
2154
+ colorParse: true,
2155
+ colorRgbaToHex: true,
2156
+ colorRgbaToHsla: true,
2157
+ colorRgbaToHsva: true,
2158
+ colorRgbaToOklch: true,
2159
+ colorSameColor: true,
2160
+ colorSameRgba: true,
2161
+ colorToString: true,
2162
+ ColorFormat: true,
2163
+ ColorHsla: true,
2164
+ ColorHsva: true,
2165
+ ColorOklch: true,
2166
+ ColorRgba: true
2167
+ }
2168
+ },
2169
+ {
2170
+ from: "@xihan-ui/headless",
2171
+ to: "@xihan-ui/core/hotkey",
2172
+ names: {
2173
+ detectKbdPlatform: true,
2174
+ formatHotkey: true,
2175
+ matchesHotkey: true,
2176
+ resolveKbdPlatform: true,
2177
+ HotkeySegment: true,
2178
+ KbdPlatform: true,
2179
+ KbdResolvedPlatform: true
2180
+ }
2181
+ },
2182
+ {
2183
+ from: "@xihan-ui/tokens",
2184
+ to: "@xihan-ui/core/color",
2185
+ names: {
2186
+ srgbToLinear: "colorSrgbToLinear",
2187
+ linearToSrgb: "colorLinearToSrgb",
2188
+ linearRgbToOklch: "colorLinearRgbToOklch",
2189
+ oklchToLinearRgb: "colorOklchToLinearRgb",
2190
+ parseColorToOklch: "colorParseOklch",
2191
+ inSrgbGamut: "colorInSrgbGamut",
2192
+ clampChroma: "colorClampChroma",
2193
+ formatOklch: "colorFormatOklch",
2194
+ Oklch: "ColorOklch"
2195
+ }
2196
+ }
2197
+ ],
2198
+ identifiers: {
2199
+ HeatmapCellDetails: "HeatmapDatumDetails",
2200
+ MenuSelectDetails: "MenuItemSelectDetails",
2201
+ ContextMenuSelectDetails: "ContextMenuItemSelectDetails",
2202
+ MenubarSelectDetails: "MenubarItemSelectDetails",
2203
+ CommandSelectDetails: "CommandItemSelectDetails",
2204
+ MentionSelectDetails: "MentionItemSelectDetails"
2205
+ },
2206
+ props: [
2207
+ {
2208
+ components: ["tree"],
2209
+ from: "multiple",
2210
+ to: "selectionMode",
2211
+ booleanTo: "multiple"
2212
+ },
2213
+ {
2214
+ components: ["table"],
2215
+ from: "footer",
2216
+ to: "footerRows",
2217
+ booleanTo: 1
2218
+ },
2219
+ {
2220
+ components: ["dialog", "drawer"],
2221
+ from: "role",
2222
+ to: "dialogRole",
2223
+ only: "web-components"
2224
+ },
2225
+ {
2226
+ components: ["notification"],
2227
+ from: "title",
2228
+ to: "titleText",
2229
+ only: "web-components"
2230
+ }
2231
+ ],
2232
+ events: [
2233
+ {
2234
+ components: ["heatmap"],
2235
+ from: "cell-press",
2236
+ to: "datum-press",
2237
+ distinctive: true
2238
+ },
2239
+ {
2240
+ components: ["heatmap"],
2241
+ from: "cell-active",
2242
+ to: "datum-active",
2243
+ distinctive: true
2244
+ },
2245
+ {
2246
+ components: [
2247
+ "menu",
2248
+ "context-menu",
2249
+ "menubar",
2250
+ "command",
2251
+ "mention"
2252
+ ],
2253
+ from: "select",
2254
+ to: "item-select"
2255
+ },
2256
+ {
2257
+ components: ["popconfirm"],
2258
+ from: "cancel",
2259
+ to: "cancel-press"
2260
+ }
2261
+ ],
2262
+ manual: [
2263
+ {
2264
+ pattern: /\bbuildPeriodGrid\b[\s\S]{0,200}?\.cells\b/,
2265
+ message: "日历族的周、月、季度、年视图改为按行渲染:buildPeriodGrid 的 cells 改为 rows,自备格子列表用 calendarPeriodRows(periods, view) 切行"
2266
+ },
2267
+ {
2268
+ pattern: /\bgetFooterRowProps\(\s*\)/,
2269
+ message: "表格的 getFooterRowProps() 改为 getFooterRowProps({ level }),多行表尾每行写 level"
2270
+ },
2271
+ {
2272
+ pattern: /\b(?:getTitleEl|getDescriptionEl|setTitleEl|setDescriptionEl)\b/,
2273
+ message: "HoverCard 删去了标题、说明的 refs:自写的标题、说明部件改为经 parts 报告在场"
2274
+ },
2275
+ {
2276
+ pattern: /\bopenSource\b/,
2277
+ message: "Citation 的语言包删除了 openSource、source,覆盖过的直接去掉"
2278
+ }
2279
+ ],
2280
+ notes: [
2281
+ "菜单族的 item-indicator 只当对号、只在勾选与单选条目上出现;当图标用的改写 item-icon(ItemIcon)",
2282
+ "Web Components FileUpload 的条目改按 allFiles 对应(远程附件在前、本地文件在后),用了 remoteFiles 的改按 allFiles 铺行",
2283
+ "表单校验:只含空白的字符串与元素全为空的数组算空,min / max 对字符串改按字素计数",
2284
+ "React Card:XhCardRoot 的 title、content 改为卡片的标题与正文,不再作为原生属性写到根上",
2285
+ "Web Components 布尔属性改为三态:显式写 =\"false\" 为假,不写交给组件缺省",
2286
+ "缺省结构:根组件不写子内容时铺出整套部件;渲染成 <button> 的部件清掉了浏览器缺省样式",
2287
+ "ColorPicker 单选给空数组改为未选,要黑色的改写 ['#000000']",
2288
+ "headless 直接用状态机的:菜单族的 onSelect 改为 onItemSelect,Popconfirm 的 onCancel 改为 onCancelPress"
2289
+ ]
2290
+ }
2291
+ ];
2292
+ /** 有迁移规则的最新 major。 */
2293
+ const LATEST_MAJOR = RULES.at(-1).major;
2294
+ /**
2295
+ * 公开面差异有一百多 kB,不打进包:源码里它与本文件同目录,构建时 tsdown 把它拷到 dist/,
2296
+ * 与打包后的本模块同目录。只在跑 migrate 时读一次。
2297
+ */
2298
+ const SURFACE_FILE = "surface.generated.json";
2299
+ let diffs;
2300
+ function surfaceOf(major) {
2301
+ diffs ??= JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), SURFACE_FILE), "utf8"));
2302
+ const surface = diffs.find((diff) => diff.major === major);
2303
+ if (!surface) throw new Error(`${SURFACE_FILE} 里没有 ${major} 的差异,先跑 node build/gen-migrations.mjs`);
2304
+ return surface;
2305
+ }
2306
+ /** 从 from 迁到 to 要依次套用的几套规则:from 之后、to 及之前的每个 major。 */
2307
+ function migrationsBetween(from, to) {
2308
+ return RULES.filter((rules) => rules.major > from && rules.major <= to).map((rules) => ({
2309
+ rules,
2310
+ surface: surfaceOf(rules.major)
2311
+ }));
2312
+ }
2313
+ //#endregion
1045
2314
  //#region src/project.ts
1046
2315
  const ADAPTER_PACKAGES = [
1047
2316
  ["vue", "@xihan-ui/vue"],
@@ -1284,6 +2553,11 @@ const HELP = `xihan-ui —— XiHan.UI 的离线知识库与 MCP 服务端
1284
2553
  design.md 设计语言文档
1285
2554
  changelog [<版本> [<版本>]] [--keyword <词>] 更新日志,两个版本时取区间
1286
2555
 
2556
+ 迁移
2557
+ migrate <从版本> <到版本> [<路径>...] 按每个 major 的规则改写源码:导入、组件、属性、事件、部件、元素、
2558
+ [--dry-run] [--check] CSS 变量与关键帧的改名;改不了的报出位置。路径缺省为当前目录,
2559
+ --dry-run 只列出不写回,--check 有要改的就以 1 退出
2560
+
1287
2561
  集成
1288
2562
  mcp 以 stdio 启动 MCP 服务端
1289
2563
  setup --client <${CLIENTS.join("|")}> 写入 MCP 配置与技能
@@ -1413,6 +2687,30 @@ function query(command, positionals, values, knowledge, framework) {
1413
2687
  default: throw new UsageError(`未知命令:${command}。xihan-ui --help 查看全部命令`);
1414
2688
  }
1415
2689
  }
2690
+ /** 版本写法(4、4.2.0、v4、^4.2.0)取 major。 */
2691
+ function majorOf(version, usage) {
2692
+ const match = /^\D*(\d+)/.exec(requireArg(version, usage));
2693
+ if (!match) throw new UsageError(`版本写法认不出:${version}。用法:xihan-ui ${usage}`);
2694
+ return Number(match[1]);
2695
+ }
2696
+ function runMigrate(positionals, values, out) {
2697
+ const usage = "migrate <从版本> <到版本> [<路径>...]";
2698
+ const from = majorOf(positionals[0], usage);
2699
+ const to = majorOf(positionals[1], usage);
2700
+ const latest = LATEST_MAJOR;
2701
+ if (to <= from) throw new UsageError(`到版本(${to})要比从版本(${from})新`);
2702
+ if (to > latest) throw new UsageError(`最新的一套迁移规则是 ${latest}.0,没有到 ${to}.0 的规则`);
2703
+ const report = migrateProject({
2704
+ migrations: migrationsBetween(from, to),
2705
+ from,
2706
+ to,
2707
+ paths: positionals.length > 2 ? positionals.slice(2) : ["."],
2708
+ cwd: out.cwd ?? process.cwd(),
2709
+ write: !values["dry-run"] && !values.check
2710
+ });
2711
+ out.stdout(values.format === "json" ? JSON.stringify(report, null, 2) : renderMigration(report));
2712
+ return values.check && report.files.some((file) => file.edits > 0) ? 1 : 0;
2713
+ }
1416
2714
  function runSetup(values, knowledge, out) {
1417
2715
  const client = values.client;
1418
2716
  if (typeof client !== "string" || !CLIENTS.includes(client)) throw new UsageError(`--client 只能是 ${CLIENTS.join(" / ")}`);
@@ -1488,6 +2786,10 @@ async function runCli(argv, environment = {}) {
1488
2786
  return 0;
1489
2787
  }
1490
2788
  try {
2789
+ if (command === "migrate") {
2790
+ if (values.format !== void 0 && values.format !== "markdown" && values.format !== "json") throw new UsageError(`--format 只能是 markdown / json,收到 ${values.format}`);
2791
+ return runMigrate(rest, values, out);
2792
+ }
1491
2793
  const knowledge = loadKnowledge(out.dataDir);
1492
2794
  if (values.version) {
1493
2795
  out.stdout(knowledge.meta.version);