@dialpad/dialtone-css 8.81.0-next.1 → 8.81.0-next.2
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/lib/build/js/dialtone_migrate/index.mjs +2 -0
- package/lib/build/js/dialtone_migrate_flex_to_stack/index.mjs +87 -5
- package/lib/build/js/dialtone_migrate_typography/index.mjs +116 -17
- package/lib/build/js/dialtone_migrate_typography/test.mjs +170 -0
- package/lib/dist/js/dialtone_migrate/index.mjs +2 -0
- package/lib/dist/js/dialtone_migrate_flex_to_stack/index.mjs +87 -5
- package/lib/dist/js/dialtone_migrate_typography/index.mjs +116 -17
- package/lib/dist/js/dialtone_migrate_typography/test.mjs +170 -0
- package/package.json +2 -2
|
@@ -363,6 +363,7 @@ function parseArgs (args) {
|
|
|
363
363
|
help: args.includes('--help'),
|
|
364
364
|
dryRun: args.includes('--dry-run'),
|
|
365
365
|
autoYes: args.includes('--yes'),
|
|
366
|
+
noImport: args.includes('--no-import'),
|
|
366
367
|
healthCheck: args.includes('--health-check'),
|
|
367
368
|
all: args.includes('--all'),
|
|
368
369
|
cwd: cwdIndex !== -1 && args[cwdIndex + 1]
|
|
@@ -778,6 +779,7 @@ async function runStandaloneMigration (migration, opts) {
|
|
|
778
779
|
const args = ['--cwd', opts.cwd];
|
|
779
780
|
if (opts.dryRun) args.push('--dry-run');
|
|
780
781
|
if (opts.autoYes) args.push('--yes');
|
|
782
|
+
if (opts.noImport) args.push('--no-import');
|
|
781
783
|
|
|
782
784
|
return new Promise((resolve, reject) => {
|
|
783
785
|
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
@@ -1028,15 +1028,28 @@ async function processFile(filePath, options) {
|
|
|
1028
1028
|
newContent = newContent.slice(0, r.start) + r.replacement + newContent.slice(r.end);
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1031
|
+
// Auto-inject DtStack import before writing; fall back to manual instructions if needed.
|
|
1032
|
+
// Skipped entirely when --no-import is set (e.g. DtStack is globally registered).
|
|
1033
|
+
const importCheck = options.noImport ? null : detectMissingStackImport(newContent, changes > 0);
|
|
1034
|
+
let finalContent = newContent;
|
|
1035
|
+
if (importCheck?.needsImport) {
|
|
1036
|
+
const injected = injectComponentImport(newContent, 'DtStack', importCheck.suggestedPath);
|
|
1037
|
+
if (injected) finalContent = injected;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1033
1041
|
|
|
1034
|
-
// Check if file needs DtStack import
|
|
1035
|
-
const importCheck = detectMissingStackImport(newContent, changes > 0);
|
|
1036
1042
|
if (importCheck?.needsImport) {
|
|
1043
|
+
if (finalContent !== newContent) {
|
|
1044
|
+
console.log(log.green(` ✓ Saved ${changes} change(s) + added DtStack import`));
|
|
1045
|
+
return { changes, skipped, needsImport: false };
|
|
1046
|
+
}
|
|
1047
|
+
console.log(log.green(` ✓ Saved ${changes} change(s)`));
|
|
1037
1048
|
printImportInstructions(filePath, importCheck);
|
|
1038
1049
|
return { changes, skipped, needsImport: true };
|
|
1039
1050
|
}
|
|
1051
|
+
|
|
1052
|
+
console.log(log.green(` ✓ Saved ${changes} change(s)`));
|
|
1040
1053
|
}
|
|
1041
1054
|
|
|
1042
1055
|
return { changes, skipped, needsImport: false };
|
|
@@ -1133,6 +1146,70 @@ function detectImportPattern(content) {
|
|
|
1133
1146
|
return '@/components/stack';
|
|
1134
1147
|
}
|
|
1135
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
|
|
1151
|
+
* Returns updated content on success, or null when the insertion can't be made safely
|
|
1152
|
+
* (caller should fall back to printing manual instructions).
|
|
1153
|
+
*
|
|
1154
|
+
* Handles:
|
|
1155
|
+
* - <script setup>: inserts import after the last existing import; no components
|
|
1156
|
+
* registration needed (auto-registered when imported in setup context).
|
|
1157
|
+
* - Options API with existing `components: {}`: inserts import + adds to the object.
|
|
1158
|
+
* - Options API without a components object: returns null (manual step required).
|
|
1159
|
+
*/
|
|
1160
|
+
function injectComponentImport(content, componentName, importPath) {
|
|
1161
|
+
if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
|
|
1162
|
+
return null; // already imported
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
|
|
1166
|
+
const scriptBlockRe = isScriptSetup
|
|
1167
|
+
? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
|
|
1168
|
+
: /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
|
|
1169
|
+
const scriptMatch = scriptBlockRe.exec(content);
|
|
1170
|
+
if (!scriptMatch) return null;
|
|
1171
|
+
|
|
1172
|
+
const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
|
|
1173
|
+
const scriptInner = scriptMatch[1];
|
|
1174
|
+
|
|
1175
|
+
const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
|
|
1176
|
+
let lastImportMatch = null;
|
|
1177
|
+
let m;
|
|
1178
|
+
while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
|
|
1179
|
+
|
|
1180
|
+
const insertOffset = lastImportMatch
|
|
1181
|
+
? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
|
|
1182
|
+
: scriptInnerStart;
|
|
1183
|
+
|
|
1184
|
+
const importLine = `\nimport { ${componentName} } from '${importPath}';`;
|
|
1185
|
+
let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
|
|
1186
|
+
|
|
1187
|
+
if (isScriptSetup) return out; // import alone is sufficient for <script setup>
|
|
1188
|
+
|
|
1189
|
+
// Options API: also register in components: {}
|
|
1190
|
+
// Re-exec against the updated content to get current positions, then restrict
|
|
1191
|
+
// the components: { search to within the script block and after export default
|
|
1192
|
+
// to avoid matching template bindings or helper objects that appear earlier.
|
|
1193
|
+
const scriptMatchUpdated = scriptBlockRe.exec(out);
|
|
1194
|
+
if (!scriptMatchUpdated) return null;
|
|
1195
|
+
const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
|
|
1196
|
+
const scriptBodyText = scriptMatchUpdated[1];
|
|
1197
|
+
const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
|
|
1198
|
+
if (!exportDefaultMatch) return null;
|
|
1199
|
+
const searchFrom = scriptBodyStart + exportDefaultMatch.index;
|
|
1200
|
+
const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
|
|
1201
|
+
if (!compMatchInSlice) return null; // no components object — can't safely auto-register
|
|
1202
|
+
const compAbsIndex = searchFrom + compMatchInSlice.index;
|
|
1203
|
+
|
|
1204
|
+
const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
|
|
1205
|
+
const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
|
|
1206
|
+
const memberIndent = compIndent + ' ';
|
|
1207
|
+
const insertAt = compAbsIndex + compMatchInSlice[0].length;
|
|
1208
|
+
out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
|
|
1209
|
+
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1136
1213
|
/**
|
|
1137
1214
|
* Print instructions for adding DtStack import and registration
|
|
1138
1215
|
* @param {string} filePath - Path to the file
|
|
@@ -1179,6 +1256,7 @@ function parseArgs() {
|
|
|
1179
1256
|
files: [], // Explicit file list via --file flag
|
|
1180
1257
|
showOutline: false, // Add migration marker for visual debugging
|
|
1181
1258
|
removeOutline: false, // Remove migration markers (cleanup mode)
|
|
1259
|
+
noImport: false, // Skip import injection (for apps that globally register DtStack)
|
|
1182
1260
|
};
|
|
1183
1261
|
|
|
1184
1262
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1206,11 +1284,12 @@ Options:
|
|
|
1206
1284
|
--yes, -y Apply all changes without prompting
|
|
1207
1285
|
--show-outline Add data-migrate-outline attribute for visual debugging
|
|
1208
1286
|
--remove-outline Remove data-migrate-outline attributes after review
|
|
1287
|
+
--no-import Skip import injection (use when DtStack is globally registered)
|
|
1209
1288
|
--help, -h Show help
|
|
1210
1289
|
|
|
1211
1290
|
Post-Migration Steps:
|
|
1212
1291
|
1. Review template changes with data-migrate-outline markers
|
|
1213
|
-
2. Add DtStack imports as instructed by the script
|
|
1292
|
+
2. Add DtStack imports as instructed by the script (skipped with --no-import)
|
|
1214
1293
|
3. Test your application
|
|
1215
1294
|
4. Run with --remove-outline to clean up markers
|
|
1216
1295
|
|
|
@@ -1252,6 +1331,8 @@ Examples:
|
|
|
1252
1331
|
options.showOutline = true;
|
|
1253
1332
|
} else if (arg === '--remove-outline') {
|
|
1254
1333
|
options.removeOutline = true;
|
|
1334
|
+
} else if (arg === '--no-import') {
|
|
1335
|
+
options.noImport = true;
|
|
1255
1336
|
} else if (arg === '--file' && args[i + 1]) {
|
|
1256
1337
|
const filePath = args[++i];
|
|
1257
1338
|
options.files.push(filePath);
|
|
@@ -1348,6 +1429,7 @@ async function main() {
|
|
|
1348
1429
|
yes: options.yes,
|
|
1349
1430
|
showOutline: options.showOutline,
|
|
1350
1431
|
validate: options.validate,
|
|
1432
|
+
noImport: options.noImport,
|
|
1351
1433
|
});
|
|
1352
1434
|
}
|
|
1353
1435
|
|
|
@@ -1002,17 +1002,31 @@ function flagDynamicClasses (content) {
|
|
|
1002
1002
|
const re = new RegExp(DYNAMIC_CLASS_PATTERN);
|
|
1003
1003
|
if (!re.test(content)) return content;
|
|
1004
1004
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
return `${indent}<!-- dt-text-migrate: review dynamic class -->\n${line}`;
|
|
1013
|
-
});
|
|
1005
|
+
// Walk whole opening tags (quote-aware) so the marker is never injected between
|
|
1006
|
+
// attributes of a multi-line element. The old line-by-line approach would insert
|
|
1007
|
+
// the comment before the :class= line, which lands it between attributes when the
|
|
1008
|
+
// opening tag spans multiple lines.
|
|
1009
|
+
const tagRe = new RegExp(`<[a-zA-Z][\\w-]*(${ATTR_BODY})>`, 'g');
|
|
1010
|
+
const insertions = [];
|
|
1011
|
+
let m;
|
|
1014
1012
|
|
|
1015
|
-
|
|
1013
|
+
while ((m = tagRe.exec(content)) !== null) {
|
|
1014
|
+
if (!new RegExp(DYNAMIC_CLASS_PATTERN).test(m[0])) continue;
|
|
1015
|
+
const before = content.slice(Math.max(0, m.index - 80), m.index);
|
|
1016
|
+
if (/<!--\s*dt-text-migrate:\s*review dynamic class\s*-->/.test(before)) continue;
|
|
1017
|
+
insertions.push(m.index);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (insertions.length === 0) return content;
|
|
1021
|
+
|
|
1022
|
+
let out = content;
|
|
1023
|
+
for (const idx of insertions.reverse()) {
|
|
1024
|
+
const lineStart = out.lastIndexOf('\n', idx - 1) + 1;
|
|
1025
|
+
const indent = out.slice(lineStart, idx).match(/^[ \t]*/)[0];
|
|
1026
|
+
const marker = `${indent}<!-- dt-text-migrate: review dynamic class -->\n`;
|
|
1027
|
+
out = out.slice(0, lineStart) + marker + out.slice(lineStart);
|
|
1028
|
+
}
|
|
1029
|
+
return out;
|
|
1016
1030
|
}
|
|
1017
1031
|
|
|
1018
1032
|
// Find composed typography classes on tags outside the rewrite scope
|
|
@@ -1245,6 +1259,70 @@ function detectMissingDtTextImport (content, usesText) {
|
|
|
1245
1259
|
};
|
|
1246
1260
|
}
|
|
1247
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
|
|
1264
|
+
* Returns updated content on success, or null when the insertion can't be made safely
|
|
1265
|
+
* (caller should fall back to printing manual instructions).
|
|
1266
|
+
*
|
|
1267
|
+
* Handles:
|
|
1268
|
+
* - <script setup>: inserts import after the last existing import; no components
|
|
1269
|
+
* registration needed (auto-registered when imported in setup context).
|
|
1270
|
+
* - Options API with existing `components: {}`: inserts import + adds to the object.
|
|
1271
|
+
* - Options API without a components object: returns null (manual step required).
|
|
1272
|
+
*/
|
|
1273
|
+
export function injectComponentImport (content, componentName, importPath) {
|
|
1274
|
+
if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
|
|
1275
|
+
return null; // already imported
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
|
|
1279
|
+
const scriptBlockRe = isScriptSetup
|
|
1280
|
+
? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
|
|
1281
|
+
: /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
|
|
1282
|
+
const scriptMatch = scriptBlockRe.exec(content);
|
|
1283
|
+
if (!scriptMatch) return null;
|
|
1284
|
+
|
|
1285
|
+
const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
|
|
1286
|
+
const scriptInner = scriptMatch[1];
|
|
1287
|
+
|
|
1288
|
+
const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
|
|
1289
|
+
let lastImportMatch = null;
|
|
1290
|
+
let m;
|
|
1291
|
+
while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
|
|
1292
|
+
|
|
1293
|
+
const insertOffset = lastImportMatch
|
|
1294
|
+
? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
|
|
1295
|
+
: scriptInnerStart;
|
|
1296
|
+
|
|
1297
|
+
const importLine = `\nimport { ${componentName} } from '${importPath}';`;
|
|
1298
|
+
let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
|
|
1299
|
+
|
|
1300
|
+
if (isScriptSetup) return out; // import alone is sufficient for <script setup>
|
|
1301
|
+
|
|
1302
|
+
// Options API: also register in components: {}
|
|
1303
|
+
// Re-exec against the updated content to get current positions, then restrict
|
|
1304
|
+
// the components: { search to within the script block and after export default
|
|
1305
|
+
// to avoid matching template bindings or helper objects that appear earlier.
|
|
1306
|
+
const scriptMatchUpdated = scriptBlockRe.exec(out);
|
|
1307
|
+
if (!scriptMatchUpdated) return null;
|
|
1308
|
+
const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
|
|
1309
|
+
const scriptBodyText = scriptMatchUpdated[1];
|
|
1310
|
+
const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
|
|
1311
|
+
if (!exportDefaultMatch) return null;
|
|
1312
|
+
const searchFrom = scriptBodyStart + exportDefaultMatch.index;
|
|
1313
|
+
const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
|
|
1314
|
+
if (!compMatchInSlice) return null; // no components object — can't safely auto-register
|
|
1315
|
+
const compAbsIndex = searchFrom + compMatchInSlice.index;
|
|
1316
|
+
|
|
1317
|
+
const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
|
|
1318
|
+
const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
|
|
1319
|
+
const memberIndent = compIndent + ' ';
|
|
1320
|
+
const insertAt = compAbsIndex + compMatchInSlice[0].length;
|
|
1321
|
+
out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
|
|
1322
|
+
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1248
1326
|
function printImportInstructions (filePath, importCheck) {
|
|
1249
1327
|
console.log(log.yellow('\n⚠️ ACTION REQUIRED: Add DtText import and registration'));
|
|
1250
1328
|
log.cyan(` File: ${filePath}`);
|
|
@@ -1450,9 +1528,6 @@ async function processFile (filePath, options) {
|
|
|
1450
1528
|
|
|
1451
1529
|
if (!shouldApply) return { changes: 0, needsImport: false };
|
|
1452
1530
|
|
|
1453
|
-
await fs.writeFile(filePath, transformed, 'utf-8');
|
|
1454
|
-
console.log(log.green(' ✓ Saved'));
|
|
1455
|
-
|
|
1456
1531
|
// Only warn about missing DtText import when we actually INSERTED new <dt-text> elements
|
|
1457
1532
|
// — review-marker-only changes (eyebrow/d-code--sm/d-fs-* flags) don't require an import.
|
|
1458
1533
|
// Use a count delta (not boolean presence) so partial migrations on a file that already
|
|
@@ -1460,14 +1535,34 @@ async function processFile (filePath, options) {
|
|
|
1460
1535
|
const beforeCount = (content.match(/<dt-text\b/g) || []).length;
|
|
1461
1536
|
const afterCount = (transformed.match(/<dt-text\b/g) || []).length;
|
|
1462
1537
|
const addedDtText = afterCount > beforeCount;
|
|
1463
|
-
|
|
1464
|
-
|
|
1538
|
+
// Auto-inject DtText import before writing; fall back to manual instructions if needed.
|
|
1539
|
+
// Skipped entirely when --no-import is set (e.g. DtText is globally registered).
|
|
1540
|
+
const importCheck = options.noImport ? null : detectMissingDtTextImport(transformed, addedDtText);
|
|
1541
|
+
|
|
1542
|
+
let finalContent = transformed;
|
|
1543
|
+
if (importCheck?.needsImport) {
|
|
1544
|
+
const injected = injectComponentImport(transformed, 'DtText', importCheck.suggestedPath);
|
|
1545
|
+
if (injected) finalContent = injected;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1549
|
+
|
|
1550
|
+
if (importCheck?.needsImport) {
|
|
1551
|
+
if (finalContent !== transformed) {
|
|
1552
|
+
console.log(log.green(' ✓ Saved + added DtText import'));
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(log.green(' ✓ Saved'));
|
|
1555
|
+
printImportInstructions(filePath, importCheck);
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
console.log(log.green(' ✓ Saved'));
|
|
1559
|
+
}
|
|
1465
1560
|
|
|
1466
1561
|
if (notes.length > 0) {
|
|
1467
1562
|
for (const note of notes) log.gray(` ℹ ${note}`);
|
|
1468
1563
|
}
|
|
1469
1564
|
|
|
1470
|
-
return { changes: 1, needsImport:
|
|
1565
|
+
return { changes: 1, needsImport: importCheck?.needsImport && finalContent === transformed };
|
|
1471
1566
|
}
|
|
1472
1567
|
|
|
1473
1568
|
//------------------------------------------------------------------------------
|
|
@@ -1484,6 +1579,7 @@ function parseArgs () {
|
|
|
1484
1579
|
files: [],
|
|
1485
1580
|
removeMarkers: false,
|
|
1486
1581
|
validate: false,
|
|
1582
|
+
noImport: false, // Skip import injection (for apps that globally register DtText)
|
|
1487
1583
|
};
|
|
1488
1584
|
|
|
1489
1585
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1503,6 +1599,7 @@ Options:
|
|
|
1503
1599
|
--remove-markers Strip all <!-- dt-text-migrate: review ... --> comments
|
|
1504
1600
|
--validate Read-only mode: scan existing <dt-text> for prop bugs
|
|
1505
1601
|
(object syntax, invalid values, mixed CSS classes)
|
|
1602
|
+
--no-import Skip import injection (use when DtText is globally registered)
|
|
1506
1603
|
--help, -h Show help
|
|
1507
1604
|
|
|
1508
1605
|
Examples:
|
|
@@ -1512,7 +1609,7 @@ Examples:
|
|
|
1512
1609
|
npx dialtone-migrate-typography --remove-markers --cwd ./src
|
|
1513
1610
|
|
|
1514
1611
|
Post-Migration Steps:
|
|
1515
|
-
1. Add DtText imports as instructed by the script
|
|
1612
|
+
1. Add DtText imports as instructed by the script (skipped with --no-import)
|
|
1516
1613
|
2. Review files marked with <!-- dt-text-migrate: review --> comments
|
|
1517
1614
|
3. Run with --remove-markers to clean up all markers after manual review
|
|
1518
1615
|
`);
|
|
@@ -1531,6 +1628,8 @@ Post-Migration Steps:
|
|
|
1531
1628
|
options.removeMarkers = true;
|
|
1532
1629
|
} else if (arg === '--validate') {
|
|
1533
1630
|
options.validate = true;
|
|
1631
|
+
} else if (arg === '--no-import') {
|
|
1632
|
+
options.noImport = true;
|
|
1534
1633
|
}
|
|
1535
1634
|
}
|
|
1536
1635
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
detectImportPathFor,
|
|
13
13
|
removeMarkersForTest,
|
|
14
14
|
validateDtTextProps,
|
|
15
|
+
injectComponentImport,
|
|
15
16
|
} from './index.mjs';
|
|
16
17
|
|
|
17
18
|
function run (input) {
|
|
@@ -987,6 +988,26 @@ describe('wrapper safety — positive cases still convert (no false positives)',
|
|
|
987
988
|
});
|
|
988
989
|
});
|
|
989
990
|
|
|
991
|
+
describe('dynamic :class flagging — multi-line element', () => {
|
|
992
|
+
it('marker goes before the opening < of a multi-line element, not between attributes', () => {
|
|
993
|
+
const input = [
|
|
994
|
+
'<div',
|
|
995
|
+
' v-if="condition"',
|
|
996
|
+
' :class="{ \'d-headline--md\': isHeading }"',
|
|
997
|
+
'>x</div>',
|
|
998
|
+
].join('\n');
|
|
999
|
+
const out = run(input);
|
|
1000
|
+
// Marker must appear on its own line before <div, never inside the tag
|
|
1001
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review dynamic class -->'), 'should emit marker');
|
|
1002
|
+
const markerIdx = out.indexOf('<!-- dt-text-migrate: review dynamic class -->');
|
|
1003
|
+
const divIdx = out.indexOf('<div');
|
|
1004
|
+
assert.ok(markerIdx < divIdx, 'marker must precede the opening <div');
|
|
1005
|
+
// The tag itself must remain structurally intact (comment not injected mid-tag)
|
|
1006
|
+
assert.ok(out.includes(' :class='), ':class line must be unchanged');
|
|
1007
|
+
assert.ok(out.includes(' v-if='), 'v-if line must be unchanged');
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
|
|
990
1011
|
describe('wrapper safety — override path (d-fw-*, d-fc-*, etc.) with component children', () => {
|
|
991
1012
|
// Override path mirrors the composed-path safety: if the rewriteable tag
|
|
992
1013
|
// wraps a component/block child, skip auto-conversion. Behavior here is a
|
|
@@ -1018,3 +1039,152 @@ describe('wrapper safety — override path (d-fw-*, d-fc-*, etc.) with component
|
|
|
1018
1039
|
);
|
|
1019
1040
|
});
|
|
1020
1041
|
});
|
|
1042
|
+
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
1044
|
+
// injectComponentImport — auto import insertion
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
|
|
1047
|
+
describe('injectComponentImport — <script setup>', () => {
|
|
1048
|
+
it('inserts import after the last existing import', () => {
|
|
1049
|
+
const content = [
|
|
1050
|
+
'<script setup>',
|
|
1051
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1052
|
+
'</script>',
|
|
1053
|
+
'<template><p>x</p></template>',
|
|
1054
|
+
].join('\n');
|
|
1055
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1056
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1057
|
+
const stackIdx = out.indexOf('DtStack');
|
|
1058
|
+
const textIdx = out.indexOf('DtText');
|
|
1059
|
+
assert.ok(textIdx > stackIdx, 'DtText import comes after DtStack import');
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('inserts import when no existing imports present', () => {
|
|
1063
|
+
const content = [
|
|
1064
|
+
'<script setup>',
|
|
1065
|
+
'const x = 1;',
|
|
1066
|
+
'</script>',
|
|
1067
|
+
].join('\n');
|
|
1068
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1069
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it('returns null when component is already imported', () => {
|
|
1073
|
+
const content = [
|
|
1074
|
+
'<script setup>',
|
|
1075
|
+
'import { DtText } from \'@dialpad/dialtone-vue\';',
|
|
1076
|
+
'</script>',
|
|
1077
|
+
].join('\n');
|
|
1078
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('does not add to components object (not needed for script setup)', () => {
|
|
1082
|
+
const content = [
|
|
1083
|
+
'<script setup>',
|
|
1084
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1085
|
+
'</script>',
|
|
1086
|
+
].join('\n');
|
|
1087
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1088
|
+
assert.ok(!out.includes('components:'), 'no components object added for script setup');
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('handles <script setup lang="ts">', () => {
|
|
1092
|
+
const content = [
|
|
1093
|
+
'<script setup lang="ts">',
|
|
1094
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1095
|
+
'</script>',
|
|
1096
|
+
].join('\n');
|
|
1097
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1098
|
+
assert.ok(out !== null, 'should succeed');
|
|
1099
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'));
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
describe('injectComponentImport — Options API', () => {
|
|
1104
|
+
it('inserts import and adds to existing components object', () => {
|
|
1105
|
+
const content = [
|
|
1106
|
+
'<script>',
|
|
1107
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1108
|
+
'export default {',
|
|
1109
|
+
' components: {',
|
|
1110
|
+
' DtStack,',
|
|
1111
|
+
' },',
|
|
1112
|
+
'};',
|
|
1113
|
+
'</script>',
|
|
1114
|
+
].join('\n');
|
|
1115
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1116
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1117
|
+
assert.ok(out.includes('DtText,'), 'DtText added to components');
|
|
1118
|
+
assert.ok(out.includes('DtStack,'), 'existing DtStack preserved');
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it('returns null when no components object exists', () => {
|
|
1122
|
+
const content = [
|
|
1123
|
+
'<script>',
|
|
1124
|
+
'export default {',
|
|
1125
|
+
' data () { return {}; },',
|
|
1126
|
+
'};',
|
|
1127
|
+
'</script>',
|
|
1128
|
+
].join('\n');
|
|
1129
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('returns null when no script block found', () => {
|
|
1133
|
+
const content = '<template><p>x</p></template>';
|
|
1134
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('returns null when component is already imported', () => {
|
|
1138
|
+
const content = [
|
|
1139
|
+
'<script>',
|
|
1140
|
+
'import { DtText } from \'@dialpad/dialtone-vue\';',
|
|
1141
|
+
'export default { components: { DtText } };',
|
|
1142
|
+
'</script>',
|
|
1143
|
+
].join('\n');
|
|
1144
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('does not corrupt a helper object with components: { before export default', () => {
|
|
1148
|
+
const content = [
|
|
1149
|
+
'<script>',
|
|
1150
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1151
|
+
'const editorConfig = { components: { toolbar: true } };',
|
|
1152
|
+
'export default {',
|
|
1153
|
+
' components: {',
|
|
1154
|
+
' DtStack,',
|
|
1155
|
+
' },',
|
|
1156
|
+
'};',
|
|
1157
|
+
'</script>',
|
|
1158
|
+
].join('\n');
|
|
1159
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1160
|
+
assert.ok(out, 'should return updated content');
|
|
1161
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1162
|
+
// DtText must be added to export default components, not to the editorConfig object
|
|
1163
|
+
const editorConfigIdx = out.indexOf('editorConfig');
|
|
1164
|
+
const dtTextIdx = out.indexOf('DtText,');
|
|
1165
|
+
const exportDefaultIdx = out.indexOf('export default');
|
|
1166
|
+
assert.ok(dtTextIdx > exportDefaultIdx, 'DtText registered after export default');
|
|
1167
|
+
assert.ok(dtTextIdx > editorConfigIdx, 'DtText not inserted into editorConfig helper');
|
|
1168
|
+
assert.ok(!out.slice(0, exportDefaultIdx).includes('DtText,'), 'DtText not in pre-export-default scope');
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('returns null when components: { appears only in a template binding, not in export default', () => {
|
|
1172
|
+
// Simulate a file where the script has no components option but the template
|
|
1173
|
+
// has a :config="{ components: { ... } }" binding — the template is masked
|
|
1174
|
+
// during injectComponentImport (which operates on the full SFC), so the
|
|
1175
|
+
// components: { in the template must not be matched.
|
|
1176
|
+
const content = [
|
|
1177
|
+
'<template>',
|
|
1178
|
+
' <some-editor :config="{ components: { toolbar: MyBar } }" />',
|
|
1179
|
+
'</template>',
|
|
1180
|
+
'<script>',
|
|
1181
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1182
|
+
'export default {',
|
|
1183
|
+
' data () { return {}; },',
|
|
1184
|
+
'};',
|
|
1185
|
+
'</script>',
|
|
1186
|
+
].join('\n');
|
|
1187
|
+
// No components: { in export default → should return null (can't auto-register)
|
|
1188
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
@@ -363,6 +363,7 @@ function parseArgs (args) {
|
|
|
363
363
|
help: args.includes('--help'),
|
|
364
364
|
dryRun: args.includes('--dry-run'),
|
|
365
365
|
autoYes: args.includes('--yes'),
|
|
366
|
+
noImport: args.includes('--no-import'),
|
|
366
367
|
healthCheck: args.includes('--health-check'),
|
|
367
368
|
all: args.includes('--all'),
|
|
368
369
|
cwd: cwdIndex !== -1 && args[cwdIndex + 1]
|
|
@@ -778,6 +779,7 @@ async function runStandaloneMigration (migration, opts) {
|
|
|
778
779
|
const args = ['--cwd', opts.cwd];
|
|
779
780
|
if (opts.dryRun) args.push('--dry-run');
|
|
780
781
|
if (opts.autoYes) args.push('--yes');
|
|
782
|
+
if (opts.noImport) args.push('--no-import');
|
|
781
783
|
|
|
782
784
|
return new Promise((resolve, reject) => {
|
|
783
785
|
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
@@ -1028,15 +1028,28 @@ async function processFile(filePath, options) {
|
|
|
1028
1028
|
newContent = newContent.slice(0, r.start) + r.replacement + newContent.slice(r.end);
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1031
|
+
// Auto-inject DtStack import before writing; fall back to manual instructions if needed.
|
|
1032
|
+
// Skipped entirely when --no-import is set (e.g. DtStack is globally registered).
|
|
1033
|
+
const importCheck = options.noImport ? null : detectMissingStackImport(newContent, changes > 0);
|
|
1034
|
+
let finalContent = newContent;
|
|
1035
|
+
if (importCheck?.needsImport) {
|
|
1036
|
+
const injected = injectComponentImport(newContent, 'DtStack', importCheck.suggestedPath);
|
|
1037
|
+
if (injected) finalContent = injected;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1033
1041
|
|
|
1034
|
-
// Check if file needs DtStack import
|
|
1035
|
-
const importCheck = detectMissingStackImport(newContent, changes > 0);
|
|
1036
1042
|
if (importCheck?.needsImport) {
|
|
1043
|
+
if (finalContent !== newContent) {
|
|
1044
|
+
console.log(log.green(` ✓ Saved ${changes} change(s) + added DtStack import`));
|
|
1045
|
+
return { changes, skipped, needsImport: false };
|
|
1046
|
+
}
|
|
1047
|
+
console.log(log.green(` ✓ Saved ${changes} change(s)`));
|
|
1037
1048
|
printImportInstructions(filePath, importCheck);
|
|
1038
1049
|
return { changes, skipped, needsImport: true };
|
|
1039
1050
|
}
|
|
1051
|
+
|
|
1052
|
+
console.log(log.green(` ✓ Saved ${changes} change(s)`));
|
|
1040
1053
|
}
|
|
1041
1054
|
|
|
1042
1055
|
return { changes, skipped, needsImport: false };
|
|
@@ -1133,6 +1146,70 @@ function detectImportPattern(content) {
|
|
|
1133
1146
|
return '@/components/stack';
|
|
1134
1147
|
}
|
|
1135
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
|
|
1151
|
+
* Returns updated content on success, or null when the insertion can't be made safely
|
|
1152
|
+
* (caller should fall back to printing manual instructions).
|
|
1153
|
+
*
|
|
1154
|
+
* Handles:
|
|
1155
|
+
* - <script setup>: inserts import after the last existing import; no components
|
|
1156
|
+
* registration needed (auto-registered when imported in setup context).
|
|
1157
|
+
* - Options API with existing `components: {}`: inserts import + adds to the object.
|
|
1158
|
+
* - Options API without a components object: returns null (manual step required).
|
|
1159
|
+
*/
|
|
1160
|
+
function injectComponentImport(content, componentName, importPath) {
|
|
1161
|
+
if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
|
|
1162
|
+
return null; // already imported
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
|
|
1166
|
+
const scriptBlockRe = isScriptSetup
|
|
1167
|
+
? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
|
|
1168
|
+
: /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
|
|
1169
|
+
const scriptMatch = scriptBlockRe.exec(content);
|
|
1170
|
+
if (!scriptMatch) return null;
|
|
1171
|
+
|
|
1172
|
+
const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
|
|
1173
|
+
const scriptInner = scriptMatch[1];
|
|
1174
|
+
|
|
1175
|
+
const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
|
|
1176
|
+
let lastImportMatch = null;
|
|
1177
|
+
let m;
|
|
1178
|
+
while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
|
|
1179
|
+
|
|
1180
|
+
const insertOffset = lastImportMatch
|
|
1181
|
+
? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
|
|
1182
|
+
: scriptInnerStart;
|
|
1183
|
+
|
|
1184
|
+
const importLine = `\nimport { ${componentName} } from '${importPath}';`;
|
|
1185
|
+
let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
|
|
1186
|
+
|
|
1187
|
+
if (isScriptSetup) return out; // import alone is sufficient for <script setup>
|
|
1188
|
+
|
|
1189
|
+
// Options API: also register in components: {}
|
|
1190
|
+
// Re-exec against the updated content to get current positions, then restrict
|
|
1191
|
+
// the components: { search to within the script block and after export default
|
|
1192
|
+
// to avoid matching template bindings or helper objects that appear earlier.
|
|
1193
|
+
const scriptMatchUpdated = scriptBlockRe.exec(out);
|
|
1194
|
+
if (!scriptMatchUpdated) return null;
|
|
1195
|
+
const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
|
|
1196
|
+
const scriptBodyText = scriptMatchUpdated[1];
|
|
1197
|
+
const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
|
|
1198
|
+
if (!exportDefaultMatch) return null;
|
|
1199
|
+
const searchFrom = scriptBodyStart + exportDefaultMatch.index;
|
|
1200
|
+
const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
|
|
1201
|
+
if (!compMatchInSlice) return null; // no components object — can't safely auto-register
|
|
1202
|
+
const compAbsIndex = searchFrom + compMatchInSlice.index;
|
|
1203
|
+
|
|
1204
|
+
const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
|
|
1205
|
+
const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
|
|
1206
|
+
const memberIndent = compIndent + ' ';
|
|
1207
|
+
const insertAt = compAbsIndex + compMatchInSlice[0].length;
|
|
1208
|
+
out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
|
|
1209
|
+
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1136
1213
|
/**
|
|
1137
1214
|
* Print instructions for adding DtStack import and registration
|
|
1138
1215
|
* @param {string} filePath - Path to the file
|
|
@@ -1179,6 +1256,7 @@ function parseArgs() {
|
|
|
1179
1256
|
files: [], // Explicit file list via --file flag
|
|
1180
1257
|
showOutline: false, // Add migration marker for visual debugging
|
|
1181
1258
|
removeOutline: false, // Remove migration markers (cleanup mode)
|
|
1259
|
+
noImport: false, // Skip import injection (for apps that globally register DtStack)
|
|
1182
1260
|
};
|
|
1183
1261
|
|
|
1184
1262
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1206,11 +1284,12 @@ Options:
|
|
|
1206
1284
|
--yes, -y Apply all changes without prompting
|
|
1207
1285
|
--show-outline Add data-migrate-outline attribute for visual debugging
|
|
1208
1286
|
--remove-outline Remove data-migrate-outline attributes after review
|
|
1287
|
+
--no-import Skip import injection (use when DtStack is globally registered)
|
|
1209
1288
|
--help, -h Show help
|
|
1210
1289
|
|
|
1211
1290
|
Post-Migration Steps:
|
|
1212
1291
|
1. Review template changes with data-migrate-outline markers
|
|
1213
|
-
2. Add DtStack imports as instructed by the script
|
|
1292
|
+
2. Add DtStack imports as instructed by the script (skipped with --no-import)
|
|
1214
1293
|
3. Test your application
|
|
1215
1294
|
4. Run with --remove-outline to clean up markers
|
|
1216
1295
|
|
|
@@ -1252,6 +1331,8 @@ Examples:
|
|
|
1252
1331
|
options.showOutline = true;
|
|
1253
1332
|
} else if (arg === '--remove-outline') {
|
|
1254
1333
|
options.removeOutline = true;
|
|
1334
|
+
} else if (arg === '--no-import') {
|
|
1335
|
+
options.noImport = true;
|
|
1255
1336
|
} else if (arg === '--file' && args[i + 1]) {
|
|
1256
1337
|
const filePath = args[++i];
|
|
1257
1338
|
options.files.push(filePath);
|
|
@@ -1348,6 +1429,7 @@ async function main() {
|
|
|
1348
1429
|
yes: options.yes,
|
|
1349
1430
|
showOutline: options.showOutline,
|
|
1350
1431
|
validate: options.validate,
|
|
1432
|
+
noImport: options.noImport,
|
|
1351
1433
|
});
|
|
1352
1434
|
}
|
|
1353
1435
|
|
|
@@ -1002,17 +1002,31 @@ function flagDynamicClasses (content) {
|
|
|
1002
1002
|
const re = new RegExp(DYNAMIC_CLASS_PATTERN);
|
|
1003
1003
|
if (!re.test(content)) return content;
|
|
1004
1004
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
return `${indent}<!-- dt-text-migrate: review dynamic class -->\n${line}`;
|
|
1013
|
-
});
|
|
1005
|
+
// Walk whole opening tags (quote-aware) so the marker is never injected between
|
|
1006
|
+
// attributes of a multi-line element. The old line-by-line approach would insert
|
|
1007
|
+
// the comment before the :class= line, which lands it between attributes when the
|
|
1008
|
+
// opening tag spans multiple lines.
|
|
1009
|
+
const tagRe = new RegExp(`<[a-zA-Z][\\w-]*(${ATTR_BODY})>`, 'g');
|
|
1010
|
+
const insertions = [];
|
|
1011
|
+
let m;
|
|
1014
1012
|
|
|
1015
|
-
|
|
1013
|
+
while ((m = tagRe.exec(content)) !== null) {
|
|
1014
|
+
if (!new RegExp(DYNAMIC_CLASS_PATTERN).test(m[0])) continue;
|
|
1015
|
+
const before = content.slice(Math.max(0, m.index - 80), m.index);
|
|
1016
|
+
if (/<!--\s*dt-text-migrate:\s*review dynamic class\s*-->/.test(before)) continue;
|
|
1017
|
+
insertions.push(m.index);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (insertions.length === 0) return content;
|
|
1021
|
+
|
|
1022
|
+
let out = content;
|
|
1023
|
+
for (const idx of insertions.reverse()) {
|
|
1024
|
+
const lineStart = out.lastIndexOf('\n', idx - 1) + 1;
|
|
1025
|
+
const indent = out.slice(lineStart, idx).match(/^[ \t]*/)[0];
|
|
1026
|
+
const marker = `${indent}<!-- dt-text-migrate: review dynamic class -->\n`;
|
|
1027
|
+
out = out.slice(0, lineStart) + marker + out.slice(lineStart);
|
|
1028
|
+
}
|
|
1029
|
+
return out;
|
|
1016
1030
|
}
|
|
1017
1031
|
|
|
1018
1032
|
// Find composed typography classes on tags outside the rewrite scope
|
|
@@ -1245,6 +1259,70 @@ function detectMissingDtTextImport (content, usesText) {
|
|
|
1245
1259
|
};
|
|
1246
1260
|
}
|
|
1247
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
|
|
1264
|
+
* Returns updated content on success, or null when the insertion can't be made safely
|
|
1265
|
+
* (caller should fall back to printing manual instructions).
|
|
1266
|
+
*
|
|
1267
|
+
* Handles:
|
|
1268
|
+
* - <script setup>: inserts import after the last existing import; no components
|
|
1269
|
+
* registration needed (auto-registered when imported in setup context).
|
|
1270
|
+
* - Options API with existing `components: {}`: inserts import + adds to the object.
|
|
1271
|
+
* - Options API without a components object: returns null (manual step required).
|
|
1272
|
+
*/
|
|
1273
|
+
export function injectComponentImport (content, componentName, importPath) {
|
|
1274
|
+
if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
|
|
1275
|
+
return null; // already imported
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
|
|
1279
|
+
const scriptBlockRe = isScriptSetup
|
|
1280
|
+
? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
|
|
1281
|
+
: /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
|
|
1282
|
+
const scriptMatch = scriptBlockRe.exec(content);
|
|
1283
|
+
if (!scriptMatch) return null;
|
|
1284
|
+
|
|
1285
|
+
const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
|
|
1286
|
+
const scriptInner = scriptMatch[1];
|
|
1287
|
+
|
|
1288
|
+
const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
|
|
1289
|
+
let lastImportMatch = null;
|
|
1290
|
+
let m;
|
|
1291
|
+
while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
|
|
1292
|
+
|
|
1293
|
+
const insertOffset = lastImportMatch
|
|
1294
|
+
? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
|
|
1295
|
+
: scriptInnerStart;
|
|
1296
|
+
|
|
1297
|
+
const importLine = `\nimport { ${componentName} } from '${importPath}';`;
|
|
1298
|
+
let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
|
|
1299
|
+
|
|
1300
|
+
if (isScriptSetup) return out; // import alone is sufficient for <script setup>
|
|
1301
|
+
|
|
1302
|
+
// Options API: also register in components: {}
|
|
1303
|
+
// Re-exec against the updated content to get current positions, then restrict
|
|
1304
|
+
// the components: { search to within the script block and after export default
|
|
1305
|
+
// to avoid matching template bindings or helper objects that appear earlier.
|
|
1306
|
+
const scriptMatchUpdated = scriptBlockRe.exec(out);
|
|
1307
|
+
if (!scriptMatchUpdated) return null;
|
|
1308
|
+
const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
|
|
1309
|
+
const scriptBodyText = scriptMatchUpdated[1];
|
|
1310
|
+
const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
|
|
1311
|
+
if (!exportDefaultMatch) return null;
|
|
1312
|
+
const searchFrom = scriptBodyStart + exportDefaultMatch.index;
|
|
1313
|
+
const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
|
|
1314
|
+
if (!compMatchInSlice) return null; // no components object — can't safely auto-register
|
|
1315
|
+
const compAbsIndex = searchFrom + compMatchInSlice.index;
|
|
1316
|
+
|
|
1317
|
+
const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
|
|
1318
|
+
const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
|
|
1319
|
+
const memberIndent = compIndent + ' ';
|
|
1320
|
+
const insertAt = compAbsIndex + compMatchInSlice[0].length;
|
|
1321
|
+
out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
|
|
1322
|
+
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1248
1326
|
function printImportInstructions (filePath, importCheck) {
|
|
1249
1327
|
console.log(log.yellow('\n⚠️ ACTION REQUIRED: Add DtText import and registration'));
|
|
1250
1328
|
log.cyan(` File: ${filePath}`);
|
|
@@ -1450,9 +1528,6 @@ async function processFile (filePath, options) {
|
|
|
1450
1528
|
|
|
1451
1529
|
if (!shouldApply) return { changes: 0, needsImport: false };
|
|
1452
1530
|
|
|
1453
|
-
await fs.writeFile(filePath, transformed, 'utf-8');
|
|
1454
|
-
console.log(log.green(' ✓ Saved'));
|
|
1455
|
-
|
|
1456
1531
|
// Only warn about missing DtText import when we actually INSERTED new <dt-text> elements
|
|
1457
1532
|
// — review-marker-only changes (eyebrow/d-code--sm/d-fs-* flags) don't require an import.
|
|
1458
1533
|
// Use a count delta (not boolean presence) so partial migrations on a file that already
|
|
@@ -1460,14 +1535,34 @@ async function processFile (filePath, options) {
|
|
|
1460
1535
|
const beforeCount = (content.match(/<dt-text\b/g) || []).length;
|
|
1461
1536
|
const afterCount = (transformed.match(/<dt-text\b/g) || []).length;
|
|
1462
1537
|
const addedDtText = afterCount > beforeCount;
|
|
1463
|
-
|
|
1464
|
-
|
|
1538
|
+
// Auto-inject DtText import before writing; fall back to manual instructions if needed.
|
|
1539
|
+
// Skipped entirely when --no-import is set (e.g. DtText is globally registered).
|
|
1540
|
+
const importCheck = options.noImport ? null : detectMissingDtTextImport(transformed, addedDtText);
|
|
1541
|
+
|
|
1542
|
+
let finalContent = transformed;
|
|
1543
|
+
if (importCheck?.needsImport) {
|
|
1544
|
+
const injected = injectComponentImport(transformed, 'DtText', importCheck.suggestedPath);
|
|
1545
|
+
if (injected) finalContent = injected;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1549
|
+
|
|
1550
|
+
if (importCheck?.needsImport) {
|
|
1551
|
+
if (finalContent !== transformed) {
|
|
1552
|
+
console.log(log.green(' ✓ Saved + added DtText import'));
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(log.green(' ✓ Saved'));
|
|
1555
|
+
printImportInstructions(filePath, importCheck);
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
console.log(log.green(' ✓ Saved'));
|
|
1559
|
+
}
|
|
1465
1560
|
|
|
1466
1561
|
if (notes.length > 0) {
|
|
1467
1562
|
for (const note of notes) log.gray(` ℹ ${note}`);
|
|
1468
1563
|
}
|
|
1469
1564
|
|
|
1470
|
-
return { changes: 1, needsImport:
|
|
1565
|
+
return { changes: 1, needsImport: importCheck?.needsImport && finalContent === transformed };
|
|
1471
1566
|
}
|
|
1472
1567
|
|
|
1473
1568
|
//------------------------------------------------------------------------------
|
|
@@ -1484,6 +1579,7 @@ function parseArgs () {
|
|
|
1484
1579
|
files: [],
|
|
1485
1580
|
removeMarkers: false,
|
|
1486
1581
|
validate: false,
|
|
1582
|
+
noImport: false, // Skip import injection (for apps that globally register DtText)
|
|
1487
1583
|
};
|
|
1488
1584
|
|
|
1489
1585
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1503,6 +1599,7 @@ Options:
|
|
|
1503
1599
|
--remove-markers Strip all <!-- dt-text-migrate: review ... --> comments
|
|
1504
1600
|
--validate Read-only mode: scan existing <dt-text> for prop bugs
|
|
1505
1601
|
(object syntax, invalid values, mixed CSS classes)
|
|
1602
|
+
--no-import Skip import injection (use when DtText is globally registered)
|
|
1506
1603
|
--help, -h Show help
|
|
1507
1604
|
|
|
1508
1605
|
Examples:
|
|
@@ -1512,7 +1609,7 @@ Examples:
|
|
|
1512
1609
|
npx dialtone-migrate-typography --remove-markers --cwd ./src
|
|
1513
1610
|
|
|
1514
1611
|
Post-Migration Steps:
|
|
1515
|
-
1. Add DtText imports as instructed by the script
|
|
1612
|
+
1. Add DtText imports as instructed by the script (skipped with --no-import)
|
|
1516
1613
|
2. Review files marked with <!-- dt-text-migrate: review --> comments
|
|
1517
1614
|
3. Run with --remove-markers to clean up all markers after manual review
|
|
1518
1615
|
`);
|
|
@@ -1531,6 +1628,8 @@ Post-Migration Steps:
|
|
|
1531
1628
|
options.removeMarkers = true;
|
|
1532
1629
|
} else if (arg === '--validate') {
|
|
1533
1630
|
options.validate = true;
|
|
1631
|
+
} else if (arg === '--no-import') {
|
|
1632
|
+
options.noImport = true;
|
|
1534
1633
|
}
|
|
1535
1634
|
}
|
|
1536
1635
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
detectImportPathFor,
|
|
13
13
|
removeMarkersForTest,
|
|
14
14
|
validateDtTextProps,
|
|
15
|
+
injectComponentImport,
|
|
15
16
|
} from './index.mjs';
|
|
16
17
|
|
|
17
18
|
function run (input) {
|
|
@@ -987,6 +988,26 @@ describe('wrapper safety — positive cases still convert (no false positives)',
|
|
|
987
988
|
});
|
|
988
989
|
});
|
|
989
990
|
|
|
991
|
+
describe('dynamic :class flagging — multi-line element', () => {
|
|
992
|
+
it('marker goes before the opening < of a multi-line element, not between attributes', () => {
|
|
993
|
+
const input = [
|
|
994
|
+
'<div',
|
|
995
|
+
' v-if="condition"',
|
|
996
|
+
' :class="{ \'d-headline--md\': isHeading }"',
|
|
997
|
+
'>x</div>',
|
|
998
|
+
].join('\n');
|
|
999
|
+
const out = run(input);
|
|
1000
|
+
// Marker must appear on its own line before <div, never inside the tag
|
|
1001
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review dynamic class -->'), 'should emit marker');
|
|
1002
|
+
const markerIdx = out.indexOf('<!-- dt-text-migrate: review dynamic class -->');
|
|
1003
|
+
const divIdx = out.indexOf('<div');
|
|
1004
|
+
assert.ok(markerIdx < divIdx, 'marker must precede the opening <div');
|
|
1005
|
+
// The tag itself must remain structurally intact (comment not injected mid-tag)
|
|
1006
|
+
assert.ok(out.includes(' :class='), ':class line must be unchanged');
|
|
1007
|
+
assert.ok(out.includes(' v-if='), 'v-if line must be unchanged');
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
|
|
990
1011
|
describe('wrapper safety — override path (d-fw-*, d-fc-*, etc.) with component children', () => {
|
|
991
1012
|
// Override path mirrors the composed-path safety: if the rewriteable tag
|
|
992
1013
|
// wraps a component/block child, skip auto-conversion. Behavior here is a
|
|
@@ -1018,3 +1039,152 @@ describe('wrapper safety — override path (d-fw-*, d-fc-*, etc.) with component
|
|
|
1018
1039
|
);
|
|
1019
1040
|
});
|
|
1020
1041
|
});
|
|
1042
|
+
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
1044
|
+
// injectComponentImport — auto import insertion
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
|
|
1047
|
+
describe('injectComponentImport — <script setup>', () => {
|
|
1048
|
+
it('inserts import after the last existing import', () => {
|
|
1049
|
+
const content = [
|
|
1050
|
+
'<script setup>',
|
|
1051
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1052
|
+
'</script>',
|
|
1053
|
+
'<template><p>x</p></template>',
|
|
1054
|
+
].join('\n');
|
|
1055
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1056
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1057
|
+
const stackIdx = out.indexOf('DtStack');
|
|
1058
|
+
const textIdx = out.indexOf('DtText');
|
|
1059
|
+
assert.ok(textIdx > stackIdx, 'DtText import comes after DtStack import');
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('inserts import when no existing imports present', () => {
|
|
1063
|
+
const content = [
|
|
1064
|
+
'<script setup>',
|
|
1065
|
+
'const x = 1;',
|
|
1066
|
+
'</script>',
|
|
1067
|
+
].join('\n');
|
|
1068
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1069
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it('returns null when component is already imported', () => {
|
|
1073
|
+
const content = [
|
|
1074
|
+
'<script setup>',
|
|
1075
|
+
'import { DtText } from \'@dialpad/dialtone-vue\';',
|
|
1076
|
+
'</script>',
|
|
1077
|
+
].join('\n');
|
|
1078
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('does not add to components object (not needed for script setup)', () => {
|
|
1082
|
+
const content = [
|
|
1083
|
+
'<script setup>',
|
|
1084
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1085
|
+
'</script>',
|
|
1086
|
+
].join('\n');
|
|
1087
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1088
|
+
assert.ok(!out.includes('components:'), 'no components object added for script setup');
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('handles <script setup lang="ts">', () => {
|
|
1092
|
+
const content = [
|
|
1093
|
+
'<script setup lang="ts">',
|
|
1094
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1095
|
+
'</script>',
|
|
1096
|
+
].join('\n');
|
|
1097
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1098
|
+
assert.ok(out !== null, 'should succeed');
|
|
1099
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'));
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
describe('injectComponentImport — Options API', () => {
|
|
1104
|
+
it('inserts import and adds to existing components object', () => {
|
|
1105
|
+
const content = [
|
|
1106
|
+
'<script>',
|
|
1107
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1108
|
+
'export default {',
|
|
1109
|
+
' components: {',
|
|
1110
|
+
' DtStack,',
|
|
1111
|
+
' },',
|
|
1112
|
+
'};',
|
|
1113
|
+
'</script>',
|
|
1114
|
+
].join('\n');
|
|
1115
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1116
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1117
|
+
assert.ok(out.includes('DtText,'), 'DtText added to components');
|
|
1118
|
+
assert.ok(out.includes('DtStack,'), 'existing DtStack preserved');
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it('returns null when no components object exists', () => {
|
|
1122
|
+
const content = [
|
|
1123
|
+
'<script>',
|
|
1124
|
+
'export default {',
|
|
1125
|
+
' data () { return {}; },',
|
|
1126
|
+
'};',
|
|
1127
|
+
'</script>',
|
|
1128
|
+
].join('\n');
|
|
1129
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('returns null when no script block found', () => {
|
|
1133
|
+
const content = '<template><p>x</p></template>';
|
|
1134
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('returns null when component is already imported', () => {
|
|
1138
|
+
const content = [
|
|
1139
|
+
'<script>',
|
|
1140
|
+
'import { DtText } from \'@dialpad/dialtone-vue\';',
|
|
1141
|
+
'export default { components: { DtText } };',
|
|
1142
|
+
'</script>',
|
|
1143
|
+
].join('\n');
|
|
1144
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('does not corrupt a helper object with components: { before export default', () => {
|
|
1148
|
+
const content = [
|
|
1149
|
+
'<script>',
|
|
1150
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1151
|
+
'const editorConfig = { components: { toolbar: true } };',
|
|
1152
|
+
'export default {',
|
|
1153
|
+
' components: {',
|
|
1154
|
+
' DtStack,',
|
|
1155
|
+
' },',
|
|
1156
|
+
'};',
|
|
1157
|
+
'</script>',
|
|
1158
|
+
].join('\n');
|
|
1159
|
+
const out = injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue');
|
|
1160
|
+
assert.ok(out, 'should return updated content');
|
|
1161
|
+
assert.ok(out.includes('import { DtText } from \'@dialpad/dialtone-vue\';'), 'import inserted');
|
|
1162
|
+
// DtText must be added to export default components, not to the editorConfig object
|
|
1163
|
+
const editorConfigIdx = out.indexOf('editorConfig');
|
|
1164
|
+
const dtTextIdx = out.indexOf('DtText,');
|
|
1165
|
+
const exportDefaultIdx = out.indexOf('export default');
|
|
1166
|
+
assert.ok(dtTextIdx > exportDefaultIdx, 'DtText registered after export default');
|
|
1167
|
+
assert.ok(dtTextIdx > editorConfigIdx, 'DtText not inserted into editorConfig helper');
|
|
1168
|
+
assert.ok(!out.slice(0, exportDefaultIdx).includes('DtText,'), 'DtText not in pre-export-default scope');
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('returns null when components: { appears only in a template binding, not in export default', () => {
|
|
1172
|
+
// Simulate a file where the script has no components option but the template
|
|
1173
|
+
// has a :config="{ components: { ... } }" binding — the template is masked
|
|
1174
|
+
// during injectComponentImport (which operates on the full SFC), so the
|
|
1175
|
+
// components: { in the template must not be matched.
|
|
1176
|
+
const content = [
|
|
1177
|
+
'<template>',
|
|
1178
|
+
' <some-editor :config="{ components: { toolbar: MyBar } }" />',
|
|
1179
|
+
'</template>',
|
|
1180
|
+
'<script>',
|
|
1181
|
+
'import { DtStack } from \'@dialpad/dialtone-vue\';',
|
|
1182
|
+
'export default {',
|
|
1183
|
+
' data () { return {}; },',
|
|
1184
|
+
'};',
|
|
1185
|
+
'</script>',
|
|
1186
|
+
].join('\n');
|
|
1187
|
+
// No components: { in export default → should return null (can't auto-register)
|
|
1188
|
+
assert.equal(injectComponentImport(content, 'DtText', '@dialpad/dialtone-vue'), null);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dialpad/dialtone-css",
|
|
3
|
-
"version": "8.81.0-next.
|
|
3
|
+
"version": "8.81.0-next.2",
|
|
4
4
|
"description": "Dialpad's design system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Dialpad",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"less": "^4.2.0",
|
|
85
85
|
"oslllo-svg-fixer": "^2.2.0",
|
|
86
86
|
"through2": "^4.0.2",
|
|
87
|
-
"@dialpad/dialtone-tokens": "1.48.0-next.
|
|
87
|
+
"@dialpad/dialtone-tokens": "1.48.0-next.20",
|
|
88
88
|
"@dialpad/postcss-responsive-variations": "1.2.4-next.1"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|