@dialpad/dialtone 9.150.1 → 9.150.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.
Files changed (37) hide show
  1. package/dist/js/dialtone_health_check/deprecated_icons.cjs +105 -0
  2. package/dist/js/dialtone_health_check/index.cjs +82 -0
  3. package/dist/js/dialtone_health_check/non_dialtone_properties.cjs +44 -0
  4. package/dist/js/dialtone_migrate_flex_to_stack/examples-edge-cases.vue +329 -0
  5. package/dist/js/dialtone_migrate_flex_to_stack/index.mjs +1377 -0
  6. package/dist/js/dialtone_migration_helper/configs/box-shadows.mjs +19 -0
  7. package/dist/js/dialtone_migration_helper/configs/colors.mjs +69 -0
  8. package/dist/js/dialtone_migration_helper/configs/fonts.mjs +49 -0
  9. package/dist/js/dialtone_migration_helper/configs/size-and-space.mjs +124 -0
  10. package/dist/js/dialtone_migration_helper/helpers.mjs +212 -0
  11. package/dist/js/dialtone_migration_helper/index.mjs +135 -0
  12. package/dist/tokens/doc.json +46427 -46427
  13. package/dist/vue3/common/utils/index.cjs +1 -1
  14. package/dist/vue3/common/utils/index.cjs.map +1 -1
  15. package/dist/vue3/common/utils/index.js +45 -41
  16. package/dist/vue3/common/utils/index.js.map +1 -1
  17. package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.cjs +1 -1
  18. package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.cjs.map +1 -1
  19. package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.js +79 -75
  20. package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.js.map +1 -1
  21. package/dist/vue3/types/common/utils/index.d.ts +2 -3
  22. package/dist/vue3/types/common/utils/index.d.ts.map +1 -1
  23. package/dist/vue3/types/components/collapsible/collapsible.vue.d.ts +1 -3
  24. package/dist/vue3/types/components/toast/layouts/toast_layout_alternate.vue.d.ts +1 -3
  25. package/dist/vue3/types/components/toast/layouts/toast_layout_default.vue.d.ts +1 -3
  26. package/dist/vue3/types/components/toast/toast.vue.d.ts +2 -6
  27. package/dist/vue3/types/recipes/buttons/callbar_button/callbar_button.vue.d.ts +7 -0
  28. package/dist/vue3/types/recipes/buttons/callbar_button/callbar_button.vue.d.ts.map +1 -1
  29. package/dist/vue3/types/recipes/comboboxes/combobox_multi_select/combobox_multi_select.vue.d.ts.map +1 -1
  30. package/dist/vue3/types/recipes/leftbar/contact_centers_row/contact_centers_row.vue.d.ts +1 -3
  31. package/dist/vue3/types/recipes/leftbar/contact_centers_row/contact_centers_row.vue.d.ts.map +1 -1
  32. package/dist/vue3/types/recipes/leftbar/contact_row/contact_row.vue.d.ts +1 -3
  33. package/dist/vue3/types/recipes/leftbar/general_row/general_row.vue.d.ts +1 -3
  34. package/dist/vue3/types/recipes/leftbar/general_row/general_row.vue.d.ts.map +1 -1
  35. package/dist/vue3/types/recipes/leftbar/group_row/group_row.vue.d.ts +1 -3
  36. package/dist/vue3/types/recipes/leftbar/group_row/group_row.vue.d.ts.map +1 -1
  37. package/package.json +5 -2
@@ -0,0 +1,1377 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable max-lines */
3
+ /* eslint-disable complexity */
4
+
5
+ /**
6
+ * @fileoverview Migration script to convert d-d-flex patterns to <dt-stack> components
7
+ *
8
+ * Usage:
9
+ * npx dialtone-migrate-flex-to-stack [options]
10
+ *
11
+ * Options:
12
+ * --cwd <path> Working directory (default: current directory)
13
+ * --dry-run Show changes without applying them
14
+ * --yes Apply all changes without prompting
15
+ * --help Show help
16
+ *
17
+ * Examples:
18
+ * npx dialtone-migrate-flex-to-stack
19
+ * npx dialtone-migrate-flex-to-stack --dry-run
20
+ * npx dialtone-migrate-flex-to-stack --cwd ./src
21
+ * npx dialtone-migrate-flex-to-stack --yes
22
+ */
23
+
24
+ import fs from 'fs/promises';
25
+ import path from 'path';
26
+ import readline from 'readline';
27
+
28
+ /**
29
+ * Simple recursive file finder (replaces glob)
30
+ */
31
+ async function findFiles(dir, extensions, ignore = []) {
32
+ const results = [];
33
+
34
+ async function walk(currentDir) {
35
+ try {
36
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = path.join(currentDir, entry.name);
40
+
41
+ // Skip ignored directories
42
+ if (ignore.some(ig => fullPath.includes(ig))) continue;
43
+
44
+ if (entry.isDirectory()) {
45
+ await walk(fullPath);
46
+ } else if (entry.isFile()) {
47
+ const matchesExtension = extensions.some(ext => entry.name.endsWith(ext));
48
+ if (matchesExtension) {
49
+ results.push(fullPath);
50
+ }
51
+ }
52
+ }
53
+ } catch {
54
+ // Skip directories we can't read
55
+ }
56
+ }
57
+
58
+ await walk(dir);
59
+ return results;
60
+ }
61
+
62
+ /**
63
+ * Validate and resolve explicitly specified files
64
+ * @param {string[]} filePaths - Array of file paths (relative or absolute)
65
+ * @param {string[]} extensions - Expected file extensions
66
+ * @returns {Promise<string[]>} - Array of validated absolute paths
67
+ */
68
+ async function validateAndResolveFiles(filePaths, extensions) {
69
+ const resolvedFiles = [];
70
+ const errors = [];
71
+
72
+ for (const filePath of filePaths) {
73
+ // Resolve to absolute path
74
+ const absolutePath = path.isAbsolute(filePath)
75
+ ? filePath
76
+ : path.resolve(process.cwd(), filePath);
77
+
78
+ // Check if file exists and is a file
79
+ try {
80
+ const stat = await fs.stat(absolutePath);
81
+
82
+ if (!stat.isFile()) {
83
+ errors.push(`Not a file: ${filePath}`);
84
+ continue;
85
+ }
86
+
87
+ // Check extension
88
+ const hasValidExtension = extensions.some(ext => absolutePath.endsWith(ext));
89
+ if (!hasValidExtension) {
90
+ errors.push(`Invalid extension for ${filePath}. Expected: ${extensions.join(', ')}`);
91
+ continue;
92
+ }
93
+
94
+ resolvedFiles.push(absolutePath);
95
+ } catch (err) {
96
+ if (err.code === 'ENOENT') {
97
+ errors.push(`File not found: ${filePath}`);
98
+ } else {
99
+ errors.push(`Error accessing ${filePath}: ${err.message}`);
100
+ }
101
+ }
102
+ }
103
+
104
+ // Report errors but continue with valid files
105
+ if (errors.length > 0) {
106
+ console.log(log.yellow('\n⚠ File validation issues:'));
107
+ errors.forEach(err => console.log(log.yellow(` ${err}`)));
108
+ console.log();
109
+ }
110
+
111
+ if (resolvedFiles.length === 0 && filePaths.length > 0) {
112
+ throw new Error('No valid files to process. All specified files had errors.');
113
+ }
114
+
115
+ return resolvedFiles;
116
+ }
117
+
118
+ //------------------------------------------------------------------------------
119
+ // Conversion Mappings
120
+ //------------------------------------------------------------------------------
121
+
122
+ const FLEX_TO_PROP = {
123
+ // Align mappings (d-ai-* → align prop)
124
+ 'd-ai-flex-start': { prop: 'align', value: 'start' },
125
+ 'd-ai-center': { prop: 'align', value: 'center' },
126
+ 'd-ai-flex-end': { prop: 'align', value: 'end' },
127
+ 'd-ai-stretch': { prop: 'align', value: 'stretch' },
128
+ 'd-ai-baseline': { prop: 'align', value: 'baseline' },
129
+ 'd-ai-normal': { prop: 'align', value: 'normal' },
130
+
131
+ // Justify mappings (d-jc-* → justify prop)
132
+ 'd-jc-flex-start': { prop: 'justify', value: 'start' },
133
+ 'd-jc-center': { prop: 'justify', value: 'center' },
134
+ 'd-jc-flex-end': { prop: 'justify', value: 'end' },
135
+ 'd-jc-space-around': { prop: 'justify', value: 'around' },
136
+ 'd-jc-space-between': { prop: 'justify', value: 'between' },
137
+ 'd-jc-space-evenly': { prop: 'justify', value: 'evenly' },
138
+
139
+ // Direction mappings (d-fd-* → direction prop)
140
+ 'd-fd-row': { prop: 'direction', value: 'row' },
141
+ 'd-fd-column': { prop: 'direction', value: 'column' },
142
+ 'd-fd-row-reverse': { prop: 'direction', value: 'row-reverse' },
143
+ 'd-fd-column-reverse': { prop: 'direction', value: 'column-reverse' },
144
+
145
+ // Gap mappings (d-g* → gap prop)
146
+ 'd-g0': { prop: 'gap', value: '0' },
147
+ 'd-g8': { prop: 'gap', value: '400' },
148
+ 'd-g16': { prop: 'gap', value: '500' },
149
+ 'd-g24': { prop: 'gap', value: '550' },
150
+ 'd-g32': { prop: 'gap', value: '600' },
151
+ 'd-g48': { prop: 'gap', value: '650' },
152
+ 'd-g64': { prop: 'gap', value: '700' },
153
+
154
+ // Grid-gap mappings (d-gg* → gap prop) - deprecated utilities, same as d-g*
155
+ 'd-gg0': { prop: 'gap', value: '0' },
156
+ 'd-gg8': { prop: 'gap', value: '400' },
157
+ 'd-gg16': { prop: 'gap', value: '500' },
158
+ 'd-gg24': { prop: 'gap', value: '550' },
159
+ 'd-gg32': { prop: 'gap', value: '600' },
160
+ 'd-gg48': { prop: 'gap', value: '650' },
161
+ 'd-gg64': { prop: 'gap', value: '700' },
162
+ };
163
+
164
+ // Classes to remove (redundant on dt-stack)
165
+ const CLASSES_TO_REMOVE = ['d-d-flex', 'd-fl-center'];
166
+
167
+ // Classes that have no prop equivalent - retain as classes on dt-stack
168
+ const RETAIN_PATTERNS = [
169
+ /^d-fw-/, // flex-wrap
170
+ /^d-fl-/, // flex-grow, flex-shrink, flex-basis (Note: d-fl-center handled separately in CLASSES_TO_REMOVE)
171
+ /^d-as-/, // align-self
172
+ /^d-order/, // order
173
+ /^d-ac-/, // align-content
174
+ /^d-flow\d+$/, // flow gap
175
+ /^d-gg?(80|96|112|128|144|160|176|192|208)$/, // large gaps without prop equivalent (d-g* and d-gg*)
176
+ /^d-flg/, // deprecated flex gap (custom property based) - retain with info message
177
+ /^d-ji-/, // justify-items (grid/flex hybrid)
178
+ /^d-js-/, // justify-self (grid/flex hybrid)
179
+ /^d-plc-/, // place-content (grid shorthand)
180
+ /^d-pli-/, // place-items (grid shorthand)
181
+ /^d-pls-/, // place-self (grid shorthand)
182
+ ];
183
+
184
+ // Native HTML elements that are safe to convert to dt-stack
185
+ // Custom Vue components (anything with hyphens or PascalCase) should NOT be converted
186
+ const NATIVE_HTML_ELEMENTS = new Set([
187
+ 'div', 'span', 'section', 'article', 'aside', 'nav', 'main',
188
+ 'header', 'footer', 'ul', 'ol', 'li', 'form', 'fieldset',
189
+ 'label', 'p', 'figure', 'figcaption', 'details', 'summary',
190
+ 'address', 'blockquote', 'dialog', 'menu', 'a', 'button',
191
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
192
+ ]);
193
+
194
+ // DOM API patterns that indicate ref is used for direct DOM manipulation
195
+ // If a ref is used with these patterns, the element should NOT be converted to a component
196
+ const REF_DOM_PATTERNS = [
197
+ /\.addEventListener\(/,
198
+ /\.removeEventListener\(/,
199
+ /\.querySelector\(/,
200
+ /\.querySelectorAll\(/,
201
+ /\.getBoundingClientRect\(/,
202
+ /\.focus\(/,
203
+ /\.blur\(/,
204
+ /\.click\(/,
205
+ /\.scrollIntoView\(/,
206
+ /\.scrollTo\(/,
207
+ /\.classList\./,
208
+ /\.setAttribute\(/,
209
+ /\.removeAttribute\(/,
210
+ /\.getAttribute\(/,
211
+ /\.style\./,
212
+ /\.offsetWidth/,
213
+ /\.offsetHeight/,
214
+ /\.clientWidth/,
215
+ /\.clientHeight/,
216
+ /\.scrollWidth/,
217
+ /\.scrollHeight/,
218
+ /\.parentNode/,
219
+ /\.parentElement/,
220
+ /\.children/,
221
+ /\.firstChild/,
222
+ /\.lastChild/,
223
+ /\.nextSibling/,
224
+ /\.previousSibling/,
225
+ /\.contains\(/,
226
+ /\.closest\(/,
227
+ ];
228
+
229
+ // Thresholds and limits used throughout the script
230
+ const THRESHOLDS = {
231
+ MAX_TAG_GAP_BYTES: 10000, // Beyond this suggests wrong tag match (~10KB)
232
+ REF_USAGE_CONTEXT_LENGTH: 100, // Chars to check after ref usage for DOM APIs
233
+ ELEMENT_PREVIEW_LENGTH: 70, // Chars to show in skip summary
234
+ DEFAULT_CONTEXT_LINES: 2, // Lines before/after for error context
235
+ };
236
+
237
+ //------------------------------------------------------------------------------
238
+ // Pattern Detection
239
+ //------------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Regex to match elements with d-d-flex or d-fl-center in class attribute
243
+ * Captures: tag name (including hyphenated), attributes before class, class value, attributes after class, self-closing
244
+ * Uses [\w-]+ to capture hyphenated tag names like 'code-well-header'
245
+ */
246
+ const ELEMENT_REGEX = /<([\w-]+)([^>]*?)\bclass="([^"]*\b(?:d-d-flex|d-fl-center)\b[^"]*)"([^>]*?)(\/?)>/g;
247
+
248
+ /**
249
+ * Find all elements with d-d-flex or d-fl-center in a template string
250
+ */
251
+ function findFlexElements(content) {
252
+ const matches = [];
253
+ let match;
254
+
255
+ while ((match = ELEMENT_REGEX.exec(content)) !== null) {
256
+ const [fullMatch, tagName, attrsBefore, classValue, attrsAfter, selfClosing] = match;
257
+
258
+ // Skip if already dt-stack
259
+ if (tagName === 'dt-stack' || tagName === 'DtStack') continue;
260
+
261
+ // Skip custom Vue components - only convert native HTML elements
262
+ // Custom components have their own behavior and shouldn't be replaced with dt-stack
263
+ if (!NATIVE_HTML_ELEMENTS.has(tagName.toLowerCase())) continue;
264
+
265
+ // Skip if d-d-flex only appears with responsive prefix (e.g., lg:d-d-flex)
266
+ // Check if there's a bare d-d-flex or d-fl-center (not preceded by breakpoint prefix)
267
+ const classes = classValue.split(/\s+/);
268
+ const hasBareFlexClass = classes.includes('d-d-flex') || classes.includes('d-fl-center');
269
+ if (!hasBareFlexClass) continue;
270
+
271
+ matches.push({
272
+ fullMatch,
273
+ tagName,
274
+ attrsBefore: attrsBefore.trim(),
275
+ classValue,
276
+ attrsAfter: attrsAfter.trim(),
277
+ selfClosing: selfClosing === '/',
278
+ index: match.index,
279
+ endIndex: match.index + fullMatch.length,
280
+ });
281
+ }
282
+
283
+ return matches;
284
+ }
285
+
286
+ /**
287
+ * Find the matching closing tag for an element, accounting for nesting
288
+ * @param {string} content - The file content
289
+ * @param {number} startPos - Position after the opening tag ends
290
+ * @param {string} tagName - The tag name to find closing tag for
291
+ * @returns {object|null} - { index, length } of the closing tag, or null if not found
292
+ */
293
+ function findMatchingClosingTag(content, startPos, tagName) {
294
+ let depth = 1;
295
+ let pos = startPos;
296
+
297
+ // Compile regex patterns once (performance optimization)
298
+ // Opening tag: <tagName followed by whitespace, >, or />
299
+ const openPattern = new RegExp(`<${tagName}(?:\\s[^>]*?)?>`);
300
+ const selfClosePattern = new RegExp(`<${tagName}(?:\\s[^>]*?)?/>`);
301
+ const closePattern = new RegExp(`</${tagName}>`);
302
+
303
+ while (depth > 0 && pos < content.length) {
304
+ const slice = content.slice(pos);
305
+
306
+ // Find next opening tag (non-self-closing)
307
+ const openMatch = slice.match(openPattern);
308
+ // Find next self-closing tag (doesn't affect depth)
309
+ const selfCloseMatch = slice.match(selfClosePattern);
310
+ // Find next closing tag
311
+ const closeMatch = slice.match(closePattern);
312
+
313
+ if (!closeMatch) {
314
+ // No closing tag found - malformed HTML
315
+ return null;
316
+ }
317
+
318
+ const closePos = pos + closeMatch.index;
319
+
320
+ // Check if there's an opening tag before this closing tag
321
+ let openPos = openMatch ? pos + openMatch.index : Infinity;
322
+
323
+ // If the opening tag is actually a self-closing tag, it doesn't count
324
+ if (selfCloseMatch && openMatch && selfCloseMatch.index === openMatch.index) {
325
+ openPos = Infinity; // Treat as no opening tag
326
+ }
327
+
328
+ if (openPos < closePos) {
329
+ // Found an opening tag before the closing tag - increase depth
330
+ depth++;
331
+ pos = openPos + openMatch[0].length;
332
+ } else {
333
+ // Found a closing tag
334
+ depth--;
335
+ if (depth === 0) {
336
+ return {
337
+ index: closePos,
338
+ length: closeMatch[0].length,
339
+ };
340
+ }
341
+ pos = closePos + closeMatch[0].length;
342
+ }
343
+ }
344
+
345
+ return null; // No matching closing tag found
346
+ }
347
+
348
+ //------------------------------------------------------------------------------
349
+ // Transformation Logic
350
+ //------------------------------------------------------------------------------
351
+
352
+ /**
353
+ * Check if an element should be skipped (not migrated)
354
+ * @param {object} element - Element with classValue and fullMatch properties
355
+ * @param {string} fileContent - Full file content for ref usage analysis
356
+ * @returns {object|null} - Returns skip info object if should skip, null if should migrate
357
+ */
358
+ function shouldSkipElement(element, fileContent = '') {
359
+ const classes = element.classValue.split(/\s+/).filter(Boolean);
360
+
361
+ // Skip grid containers (not flexbox)
362
+ if (classes.includes('d-d-grid') || classes.includes('d-d-inline-grid')) {
363
+ return {
364
+ reason: 'Grid container detected (not flexbox)',
365
+ severity: 'info',
366
+ message: `Skipping <${element.tagName}> - uses CSS Grid (d-d-grid/d-d-inline-grid), not flexbox`,
367
+ };
368
+ }
369
+
370
+ // Skip inline-flex (DtStack is block-level only)
371
+ if (classes.includes('d-d-inline-flex')) {
372
+ return {
373
+ reason: 'Inline-flex not supported by DtStack',
374
+ severity: 'info',
375
+ message: `Skipping <${element.tagName}> - d-d-inline-flex not supported (DtStack is block-level)`,
376
+ };
377
+ }
378
+
379
+ // Skip d-d-contents (layout tree manipulation)
380
+ if (classes.includes('d-d-contents')) {
381
+ return {
382
+ reason: 'Display: contents detected',
383
+ severity: 'warning',
384
+ message: `Skipping <${element.tagName}> - d-d-contents manipulates layout tree, verify layout after migration if converted manually`,
385
+ };
386
+ }
387
+
388
+ // Skip deprecated flex column system (complex child selectors)
389
+ if (classes.some(cls => /^d-fl-col\d+$/.test(cls))) {
390
+ return {
391
+ reason: 'Deprecated flex column system (d-fl-col*)',
392
+ severity: 'warning',
393
+ message: `Skipping <${element.tagName}> - d-fl-col* uses complex child selectors, requires manual migration (utility deprecated, see DLT-1763)`,
394
+ };
395
+ }
396
+
397
+ // Skip auto-spacing utilities (margin-based, incompatible with gap)
398
+ const autoSpacingClass = classes.find(cls => /^d-stack\d+$/.test(cls) || /^d-flow\d+$/.test(cls));
399
+ if (autoSpacingClass) {
400
+ return {
401
+ reason: 'Auto-spacing utility (margin-based)',
402
+ severity: 'warning',
403
+ message: `Skipping <${element.tagName}> - ${autoSpacingClass} uses margin-based spacing, incompatible with gap-based DtStack`,
404
+ };
405
+ }
406
+
407
+ // Skip elements with ref attributes used for DOM manipulation
408
+ // When converted to a component, refs return component instances instead of DOM elements
409
+ const refMatch = element.fullMatch.match(/\bref="([^"]+)"/);
410
+ if (refMatch && fileContent) {
411
+ const refName = refMatch[1];
412
+ // Check for DOM API usage patterns with this ref
413
+ // Common patterns: refName.value.addEventListener, refName.value?.focus(), etc.
414
+ const refUsagePatterns = [
415
+ new RegExp(`${refName}\\.value\\.`), // refName.value.something
416
+ new RegExp(`${refName}\\.value\\?\\.`), // refName.value?.something (optional chaining)
417
+ new RegExp(`\\$refs\\.${refName}\\.`), // this.$refs.refName.something (Options API)
418
+ new RegExp(`\\$refs\\['${refName}'\\]\\.`), // this.$refs['refName'].something
419
+ new RegExp(`\\$refs\\["${refName}"\\]\\.`), // this.$refs["refName"].something
420
+ ];
421
+
422
+ // Check if any ref usage pattern matches a DOM API pattern
423
+ for (const refPattern of refUsagePatterns) {
424
+ const refUsageMatch = fileContent.match(refPattern);
425
+ if (refUsageMatch) {
426
+ // Found ref usage, now check if it's used with DOM APIs
427
+ const usageContext = fileContent.slice(
428
+ Math.max(0, refUsageMatch.index),
429
+ Math.min(fileContent.length, refUsageMatch.index + THRESHOLDS.REF_USAGE_CONTEXT_LENGTH),
430
+ );
431
+
432
+ for (const domPattern of REF_DOM_PATTERNS) {
433
+ if (domPattern.test(usageContext)) {
434
+ return {
435
+ reason: 'Ref used for DOM manipulation',
436
+ severity: 'warning',
437
+ message: `Skipping <${element.tagName}> - ref="${refName}" is used for DOM API calls. Converting to component would break this. Use .$el accessor or keep as native element.`,
438
+ };
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ // Skip elements with dynamic :class bindings containing flex utilities
446
+ // These bindings would be corrupted by the transformation
447
+ const dynamicClassMatch = element.fullMatch.match(/:class="([^"]+)"/);
448
+ if (dynamicClassMatch) {
449
+ const bindingContent = dynamicClassMatch[1];
450
+ const flexUtilityPattern = /d-d-flex|d-fl-center|d-ai-|d-jc-|d-fd-|d-gg?\d/;
451
+
452
+ if (flexUtilityPattern.test(bindingContent)) {
453
+ return {
454
+ reason: 'Dynamic :class with flex utilities',
455
+ severity: 'warning',
456
+ message: `Skipping <${element.tagName}> - dynamic :class binding contains flex utilities. Convert to dynamic DtStack props instead.`,
457
+ };
458
+ }
459
+ }
460
+
461
+ return null; // No skip reason, proceed with migration
462
+ }
463
+
464
+ /**
465
+ * Transform a flex element to dt-stack
466
+ * @param {object} element - Element to transform
467
+ * @param {boolean} showOutline - Whether to add migration markers
468
+ * @param {string} fileContent - Full file content for ref usage analysis
469
+ * @returns {object|null} - Transformation object or null if element should be skipped
470
+ */
471
+ function transformElement(element, showOutline = false, fileContent = '') {
472
+ // Check if element should be skipped
473
+ const skipInfo = shouldSkipElement(element, fileContent);
474
+ if (skipInfo) {
475
+ return { skip: true, ...skipInfo };
476
+ }
477
+
478
+ const classes = element.classValue.split(/\s+/).filter(Boolean);
479
+ const props = [];
480
+ const retainedClasses = [];
481
+ const directionClasses = ['d-fd-row', 'd-fd-column', 'd-fd-row-reverse', 'd-fd-column-reverse'];
482
+
483
+ // Check for d-fl-center (combination utility - sets display:flex + align:center + justify:center)
484
+ const hasFlCenter = classes.includes('d-fl-center');
485
+
486
+ // Find ALL direction utilities present
487
+ const foundDirectionClasses = classes.filter(cls => directionClasses.includes(cls));
488
+ const directionCount = foundDirectionClasses.length;
489
+
490
+ for (const cls of classes) {
491
+ // Check if class should be removed
492
+ if (CLASSES_TO_REMOVE.includes(cls)) continue;
493
+
494
+ // Special handling for direction utilities
495
+ if (FLEX_TO_PROP[cls] && FLEX_TO_PROP[cls].prop === 'direction') {
496
+ if (directionCount === 1) {
497
+ // Single direction utility - safe to convert
498
+ const { prop, value } = FLEX_TO_PROP[cls];
499
+
500
+ // Skip d-fd-column since DtStack defaults to column
501
+ if (value !== 'column') {
502
+ props.push({ prop, value });
503
+ }
504
+ // Don't add to retainedClasses - it's been converted (or omitted as redundant)
505
+ continue;
506
+ } else if (directionCount > 1) {
507
+ // Multiple direction utilities - retain all, let CSS cascade decide
508
+ retainedClasses.push(cls);
509
+ continue;
510
+ }
511
+ }
512
+
513
+ // Check if class converts to a prop (non-direction)
514
+ if (FLEX_TO_PROP[cls]) {
515
+ const { prop, value } = FLEX_TO_PROP[cls];
516
+ // Avoid duplicate props
517
+ if (!props.some(p => p.prop === prop)) {
518
+ props.push({ prop, value });
519
+ }
520
+ continue;
521
+ }
522
+
523
+ // Check if class should be retained
524
+ if (RETAIN_PATTERNS.some(pattern => pattern.test(cls))) {
525
+ retainedClasses.push(cls);
526
+ continue;
527
+ }
528
+
529
+ // Keep other classes (non-flex utilities like d-p16, d-mb8, etc.)
530
+ retainedClasses.push(cls);
531
+ }
532
+
533
+ // Handle d-fl-center: extract align="center" and justify="center" props
534
+ // d-fl-center sets: display:flex + align-items:center + justify-content:center
535
+ if (hasFlCenter) {
536
+ // Only add if not already present (avoid duplicates)
537
+ if (!props.some(p => p.prop === 'align')) {
538
+ props.push({ prop: 'align', value: 'center' });
539
+ }
540
+ if (!props.some(p => p.prop === 'justify')) {
541
+ props.push({ prop: 'justify', value: 'center' });
542
+ }
543
+ }
544
+
545
+ // Add default direction="row" if no direction utilities found OR multiple found
546
+ if (directionCount === 0 || directionCount > 1) {
547
+ props.unshift({ prop: 'direction', value: 'row' });
548
+ }
549
+
550
+ // Build the new element
551
+ let newElement = '<dt-stack';
552
+
553
+ // Add `as` prop for non-div elements to preserve semantic HTML
554
+ const tagLower = element.tagName.toLowerCase();
555
+ if (tagLower !== 'div') {
556
+ newElement += ` as="${element.tagName}"`;
557
+ }
558
+
559
+ // Add converted props
560
+ for (const { prop, value } of props) {
561
+ newElement += ` ${prop}="${value}"`;
562
+ }
563
+
564
+ // Add retained classes if any
565
+ if (retainedClasses.length > 0) {
566
+ newElement += ` class="${retainedClasses.join(' ')}"`;
567
+ }
568
+
569
+ // Add other attributes (before and after class)
570
+ if (element.attrsBefore) {
571
+ newElement += ` ${element.attrsBefore}`;
572
+ }
573
+ if (element.attrsAfter) {
574
+ newElement += ` ${element.attrsAfter}`;
575
+ }
576
+
577
+ // Add migration marker for visual debugging (if flag is set)
578
+ if (showOutline) {
579
+ newElement += ' data-migrate-outline';
580
+ }
581
+
582
+ // Close tag
583
+ newElement += element.selfClosing ? ' />' : '>';
584
+
585
+ return {
586
+ original: element.fullMatch,
587
+ transformed: newElement,
588
+ tagName: element.tagName,
589
+ props,
590
+ retainedClasses,
591
+ };
592
+ }
593
+
594
+ //------------------------------------------------------------------------------
595
+ // Validation Helpers
596
+ //------------------------------------------------------------------------------
597
+
598
+ /**
599
+ * Get line number for a character position in content
600
+ * @param {string} content - File content
601
+ * @param {number} position - Character position
602
+ * @returns {number} - Line number (1-indexed)
603
+ */
604
+ function getLineNumber(content, position) {
605
+ const lines = content.slice(0, position).split('\n');
606
+ return lines.length;
607
+ }
608
+
609
+ /**
610
+ * Get context lines around a position
611
+ * @param {string} content - File content
612
+ * @param {number} position - Character position
613
+ * @param {number} contextLines - Number of lines before and after
614
+ * @returns {string} - Context with line numbers
615
+ */
616
+ function getContext(content, position, contextLines = THRESHOLDS.DEFAULT_CONTEXT_LINES) {
617
+ const lines = content.split('\n');
618
+ const lineNum = getLineNumber(content, position);
619
+ const startLine = Math.max(0, lineNum - contextLines - 1);
620
+ const endLine = Math.min(lines.length, lineNum + contextLines);
621
+
622
+ let result = '';
623
+ for (let i = startLine; i < endLine; i++) {
624
+ const prefix = i === lineNum - 1 ? '>' : ' ';
625
+ result += `${prefix} ${String(i + 1).padStart(4)}: ${lines[i]}\n`;
626
+ }
627
+ return result;
628
+ }
629
+
630
+ /**
631
+ * Validate transformations before applying
632
+ * @param {object[]} transformations - Array of transformation objects
633
+ * @param {string} content - Original file content
634
+ * @returns {object} - { valid: boolean, errors: array, warnings: array }
635
+ */
636
+ function validateTransformations(transformations, content) {
637
+ const errors = [];
638
+ const warnings = [];
639
+
640
+ for (const t of transformations) {
641
+ // Check: Non-self-closing elements must have a matching closing tag
642
+ if (!t.selfClosing && t.closeStart === null) {
643
+ errors.push({
644
+ line: getLineNumber(content, t.openStart),
645
+ message: `No matching closing tag found for transformed element`,
646
+ context: getContext(content, t.openStart),
647
+ severity: 'error',
648
+ });
649
+ }
650
+
651
+ // Check: Closing tag position must be after opening tag
652
+ if (!t.selfClosing && t.closeStart !== null && t.closeStart <= t.openEnd) {
653
+ errors.push({
654
+ line: getLineNumber(content, t.openStart),
655
+ message: `Closing tag position (${t.closeStart}) is before or at opening tag end (${t.openEnd})`,
656
+ context: getContext(content, t.openStart),
657
+ severity: 'error',
658
+ });
659
+ }
660
+
661
+ // Warning: Very large gap between opening and closing tags (might indicate wrong match)
662
+ if (!t.selfClosing && t.closeStart !== null) {
663
+ const gap = t.closeStart - t.openEnd;
664
+ if (gap > THRESHOLDS.MAX_TAG_GAP_BYTES) {
665
+ warnings.push({
666
+ line: getLineNumber(content, t.openStart),
667
+ message: `Large gap (${gap} chars) between opening and closing tags - verify correct match`,
668
+ severity: 'warning',
669
+ });
670
+ }
671
+ }
672
+ }
673
+
674
+ // Check for overlapping transformations
675
+ const sortedByStart = [...transformations].sort((a, b) => a.openStart - b.openStart);
676
+ for (let i = 0; i < sortedByStart.length - 1; i++) {
677
+ const current = sortedByStart[i];
678
+ const next = sortedByStart[i + 1];
679
+
680
+ // Check if opening tags overlap
681
+ if (current.openEnd > next.openStart) {
682
+ errors.push({
683
+ line: getLineNumber(content, current.openStart),
684
+ message: `Overlapping transformations detected`,
685
+ severity: 'error',
686
+ });
687
+ }
688
+ }
689
+
690
+ return {
691
+ valid: errors.length === 0,
692
+ errors,
693
+ warnings,
694
+ };
695
+ }
696
+
697
+ //------------------------------------------------------------------------------
698
+ // Skip Summary Tracking
699
+ //------------------------------------------------------------------------------
700
+
701
+ /**
702
+ * Track skipped elements by reason for grouped summary
703
+ * @type {Map<string, Array<{file: string, line: number, element: string}>>}
704
+ */
705
+ const skippedByReason = new Map();
706
+
707
+ /**
708
+ * Add a skipped element to the tracking map
709
+ * @param {string} reason - The skip reason category
710
+ * @param {string} file - File path
711
+ * @param {number} line - Line number
712
+ * @param {string} element - Element snippet
713
+ */
714
+ function trackSkippedElement(reason, file, line, element) {
715
+ if (!skippedByReason.has(reason)) {
716
+ skippedByReason.set(reason, []);
717
+ }
718
+ skippedByReason.get(reason).push({ file, line, element });
719
+ }
720
+
721
+ /**
722
+ * Print grouped summary of all skipped elements at end of migration
723
+ */
724
+ function printSkippedSummary() {
725
+ if (skippedByReason.size === 0) return;
726
+
727
+ console.log(`\n${colors.bold}⚠️ Elements Requiring Manual Review${colors.reset}\n`);
728
+
729
+ for (const [reason, elements] of skippedByReason) {
730
+ // Header with count
731
+ console.log(`${colors.yellow}${reason} (${elements.length} element${elements.length === 1 ? '' : 's'})${colors.reset}`);
732
+
733
+ // Show first 3 examples
734
+ const examples = elements.slice(0, 3);
735
+ for (const { file, line, element } of examples) {
736
+ console.log(`${colors.gray} ${file}:${line}${colors.reset}`);
737
+ // Truncate element preview to 70 chars
738
+ const preview = element.length > THRESHOLDS.ELEMENT_PREVIEW_LENGTH
739
+ ? element.substring(0, THRESHOLDS.ELEMENT_PREVIEW_LENGTH) + '...'
740
+ : element;
741
+ console.log(`${colors.gray} ${preview}${colors.reset}`);
742
+ }
743
+
744
+ // Show count of remaining if more than 3
745
+ if (elements.length > 3) {
746
+ console.log(`${colors.gray} ... and ${elements.length - 3} more${colors.reset}`);
747
+ }
748
+ console.log();
749
+ }
750
+
751
+ // Provide helpful guidance
752
+ console.log(`${colors.cyan}📚 Manual Migration Guide:${colors.reset}`);
753
+ console.log(`${colors.cyan} https://dialtone.dialpad.com/about/whats-new/posts/2025-12-2.html#manual-migration${colors.reset}`);
754
+ console.log();
755
+ }
756
+
757
+ //------------------------------------------------------------------------------
758
+ // Console Helpers (replace chalk)
759
+ //------------------------------------------------------------------------------
760
+
761
+ const colors = {
762
+ reset: '\x1b[0m',
763
+ red: '\x1b[31m',
764
+ green: '\x1b[32m',
765
+ yellow: '\x1b[33m',
766
+ cyan: '\x1b[36m',
767
+ gray: '\x1b[90m',
768
+ bold: '\x1b[1m',
769
+ };
770
+
771
+ const log = {
772
+ cyan: (msg) => console.log(`${colors.cyan}${msg}${colors.reset}`),
773
+ gray: (msg) => console.log(`${colors.gray}${msg}${colors.reset}`),
774
+ red: (msg) => `${colors.red}${msg}${colors.reset}`,
775
+ green: (msg) => `${colors.green}${msg}${colors.reset}`,
776
+ yellow: (msg) => `${colors.yellow}${msg}${colors.reset}`,
777
+ bold: (msg) => console.log(`${colors.bold}${msg}${colors.reset}`),
778
+ };
779
+
780
+ //------------------------------------------------------------------------------
781
+ // Interactive Prompt (replace inquirer)
782
+ //------------------------------------------------------------------------------
783
+
784
+ async function prompt(question) {
785
+ const rl = readline.createInterface({
786
+ input: process.stdin,
787
+ output: process.stdout,
788
+ });
789
+
790
+ return new Promise((resolve) => {
791
+ rl.question(question, (answer) => {
792
+ rl.close();
793
+ resolve(answer.toLowerCase().trim());
794
+ });
795
+ });
796
+ }
797
+
798
+ //------------------------------------------------------------------------------
799
+ // File Processing
800
+ //------------------------------------------------------------------------------
801
+
802
+ /**
803
+ * Process a single file
804
+ */
805
+ async function processFile(filePath, options) {
806
+ const content = await fs.readFile(filePath, 'utf-8');
807
+
808
+ // Find elements with d-d-flex
809
+ const elements = findFlexElements(content);
810
+
811
+ if (elements.length === 0) return { changes: 0, skipped: 0 };
812
+
813
+ log.cyan(`\n📄 ${filePath}`);
814
+ log.gray(` Found ${elements.length} element(s) with d-d-flex\n`);
815
+
816
+ // Check for dynamic :class bindings with flex utilities (standalone, not on flex elements)
817
+ // Note: Dynamic bindings ON flex elements are handled by shouldSkipElement()
818
+ const dynamicClassRegex = /:(class|v-bind:class)="([^"]*)"/g;
819
+ let dynamicMatch;
820
+ const flexUtilityPattern = /d-d-flex|d-fl-center|d-ai-|d-jc-|d-fd-|d-gg?\d/;
821
+
822
+ while ((dynamicMatch = dynamicClassRegex.exec(content)) !== null) {
823
+ const bindingContent = dynamicMatch[2];
824
+ if (flexUtilityPattern.test(bindingContent)) {
825
+ const lineNum = getLineNumber(content, dynamicMatch.index);
826
+ trackSkippedElement(
827
+ 'Dynamic :class with flex utilities',
828
+ filePath,
829
+ lineNum,
830
+ `:class="${bindingContent.length > 50 ? bindingContent.substring(0, 50) + '...' : bindingContent}"`,
831
+ );
832
+ }
833
+ }
834
+
835
+ let changes = 0;
836
+ let skipped = 0;
837
+ let applyAll = options.yes || false;
838
+
839
+ // Collect all transformations with their positions first
840
+ // We need to process all elements and find their closing tags BEFORE making any changes
841
+ const transformations = [];
842
+
843
+ for (const element of elements) {
844
+ const transformation = transformElement(element, options.showOutline, content);
845
+
846
+ // Handle skipped elements - track for grouped summary instead of inline output
847
+ if (transformation.skip) {
848
+ const lineNum = getLineNumber(content, element.index);
849
+ trackSkippedElement(
850
+ transformation.reason,
851
+ filePath,
852
+ lineNum,
853
+ element.fullMatch,
854
+ );
855
+ skipped++;
856
+ continue;
857
+ }
858
+
859
+ // Find the matching closing tag position (in original content)
860
+ let closingTag = null;
861
+ if (!element.selfClosing) {
862
+ closingTag = findMatchingClosingTag(content, element.endIndex, element.tagName);
863
+ }
864
+
865
+ // Show before/after
866
+ console.log(log.red(' - ') + transformation.original);
867
+ console.log(log.green(' + ') + transformation.transformed);
868
+
869
+ if (transformation.retainedClasses.length > 0) {
870
+ console.log(log.yellow(` ⚠ Retained classes: ${transformation.retainedClasses.join(', ')}`));
871
+
872
+ // Add specific info for edge case utilities
873
+ const hasFlg = transformation.retainedClasses.some(cls => /^d-flg/.test(cls));
874
+ const hasGridHybrid = transformation.retainedClasses.some(cls => /^d-(ji-|js-|plc-|pli-|pls-)/.test(cls));
875
+
876
+ if (hasFlg) {
877
+ log.gray(` ℹ d-flg* is deprecated - consider replacing with DtStack gap prop or at least d-g* gap utilities`);
878
+ }
879
+ if (hasGridHybrid) {
880
+ log.gray(` ℹ Grid/flex hybrid utilities (d-ji-*, d-js-*, d-plc-*, etc.) retained - no DtStack prop equivalent`);
881
+ }
882
+ }
883
+ console.log();
884
+
885
+ if (options.dryRun) {
886
+ changes++;
887
+ continue;
888
+ }
889
+
890
+ let shouldApply = applyAll;
891
+
892
+ if (!applyAll) {
893
+ const answer = await prompt(' Apply? [y]es / [n]o / [a]ll / [q]uit: ');
894
+
895
+ if (answer === 'q' || answer === 'quit') break;
896
+ if (answer === 'a' || answer === 'all') {
897
+ applyAll = true;
898
+ shouldApply = true;
899
+ }
900
+ if (answer === 'y' || answer === 'yes') shouldApply = true;
901
+ }
902
+
903
+ if (shouldApply) {
904
+ transformations.push({
905
+ // Opening tag replacement
906
+ openStart: element.index,
907
+ openEnd: element.endIndex,
908
+ openReplacement: transformation.transformed,
909
+ // Closing tag replacement (if not self-closing)
910
+ closeStart: closingTag ? closingTag.index : null,
911
+ closeEnd: closingTag ? closingTag.index + closingTag.length : null,
912
+ closeReplacement: '</dt-stack>',
913
+ selfClosing: element.selfClosing,
914
+ });
915
+ changes++;
916
+ } else {
917
+ skipped++;
918
+ }
919
+ }
920
+
921
+ // Run validation if --validate flag is set
922
+ if (options.validate && transformations.length > 0) {
923
+ const validation = validateTransformations(transformations, content);
924
+
925
+ if (validation.errors.length > 0) {
926
+ console.log(log.red(`\n ❌ Validation FAILED: ${validation.errors.length} error(s) found`));
927
+ for (const err of validation.errors) {
928
+ console.log(log.red(`\n Line ${err.line}: ${err.message}`));
929
+ if (err.context) {
930
+ console.log(log.gray(err.context));
931
+ }
932
+ }
933
+ }
934
+
935
+ if (validation.warnings.length > 0) {
936
+ console.log(log.yellow(`\n ⚠ ${validation.warnings.length} warning(s):`));
937
+ for (const warn of validation.warnings) {
938
+ console.log(log.yellow(` Line ${warn.line}: ${warn.message}`));
939
+ }
940
+ }
941
+
942
+ if (validation.valid && validation.warnings.length === 0) {
943
+ console.log(log.green(` ✓ Validation passed - ${transformations.length} transformation(s) look safe`));
944
+ } else if (validation.valid) {
945
+ console.log(log.yellow(` ⚠ Validation passed with warnings - review before applying`));
946
+ }
947
+
948
+ return {
949
+ changes,
950
+ skipped,
951
+ needsImport: false,
952
+ validationErrors: validation.errors.length,
953
+ validationWarnings: validation.warnings.length,
954
+ };
955
+ }
956
+
957
+ // Apply all transformations in reverse order (end to start) to preserve positions
958
+ if (!options.dryRun && transformations.length > 0) {
959
+ // Sort by position descending (process from end of file to start)
960
+ // We need to handle both opening and closing tags, so collect all replacements
961
+ const allReplacements = [];
962
+
963
+ for (const t of transformations) {
964
+ // Add opening tag replacement
965
+ allReplacements.push({
966
+ start: t.openStart,
967
+ end: t.openEnd,
968
+ replacement: t.openReplacement,
969
+ });
970
+
971
+ // Add closing tag replacement if not self-closing
972
+ if (!t.selfClosing && t.closeStart !== null) {
973
+ allReplacements.push({
974
+ start: t.closeStart,
975
+ end: t.closeEnd,
976
+ replacement: t.closeReplacement,
977
+ });
978
+ }
979
+ }
980
+
981
+ // Sort by start position descending
982
+ allReplacements.sort((a, b) => b.start - a.start);
983
+
984
+ // Apply replacements from end to start
985
+ let newContent = content;
986
+ for (const r of allReplacements) {
987
+ newContent = newContent.slice(0, r.start) + r.replacement + newContent.slice(r.end);
988
+ }
989
+
990
+ await fs.writeFile(filePath, newContent, 'utf-8');
991
+ console.log(log.green(` ✓ Saved ${changes} change(s)`));
992
+
993
+ // Check if file needs DtStack import
994
+ const importCheck = detectMissingStackImport(newContent, changes > 0);
995
+ if (importCheck?.needsImport) {
996
+ printImportInstructions(filePath, importCheck);
997
+ return { changes, skipped, needsImport: true };
998
+ }
999
+ }
1000
+
1001
+ return { changes, skipped, needsImport: false };
1002
+ }
1003
+
1004
+ /**
1005
+ * Remove data-migrate-outline attributes from files
1006
+ * @param {string} filePath - Path to file to clean
1007
+ * @param {object} options - Options object with dryRun, yes flags
1008
+ * @returns {object} - { changes, skipped }
1009
+ */
1010
+ async function cleanupMarkers(filePath, options) {
1011
+ const content = await fs.readFile(filePath, 'utf8');
1012
+
1013
+ // Find all data-migrate-outline attributes
1014
+ const markerPattern = /\s+data-migrate-outline(?:="[^"]*")?/g;
1015
+ const matches = [...content.matchAll(markerPattern)];
1016
+
1017
+ if (matches.length === 0) {
1018
+ return { changes: 0, skipped: 0 };
1019
+ }
1020
+
1021
+ // Show what we found
1022
+ console.log(log.cyan(`\n📄 ${filePath}`));
1023
+ console.log(log.gray(` Found ${matches.length} marker(s)\n`));
1024
+
1025
+ if (options.dryRun) {
1026
+ // Preview only
1027
+ matches.forEach((match, idx) => {
1028
+ const start = Math.max(0, match.index - 50);
1029
+ const end = Math.min(content.length, match.index + match[0].length + 50);
1030
+ const context = content.slice(start, end);
1031
+ console.log(log.yellow(` ${idx + 1}. ${context.replace(/\n/g, ' ')}`));
1032
+ });
1033
+ return { changes: matches.length, skipped: 0 };
1034
+ }
1035
+
1036
+ // Remove all markers
1037
+ const newContent = content.replace(markerPattern, '');
1038
+
1039
+ // Write back
1040
+ await fs.writeFile(filePath, newContent, 'utf8');
1041
+
1042
+ console.log(log.green(` ✓ Removed ${matches.length} marker(s)`));
1043
+
1044
+ return { changes: matches.length, skipped: 0 };
1045
+ }
1046
+
1047
+ /**
1048
+ * Check if a file needs DtStack import
1049
+ * @param {string} content - Full file content
1050
+ * @param {boolean} usesStack - Whether file has <dt-stack> in template
1051
+ * @returns {object|null} - Detection result with suggested import path, or null if import exists
1052
+ */
1053
+ function detectMissingStackImport(content, usesStack) {
1054
+ if (!usesStack) return null;
1055
+
1056
+ // Check if DtStack is already imported
1057
+ const hasImport = /import\s+(?:\{[^}]*\bDtStack\b[^}]*\}|DtStack)\s+from/.test(content);
1058
+ if (hasImport) return null;
1059
+
1060
+ // Analyze existing imports to suggest appropriate path
1061
+ const importPath = detectImportPattern(content);
1062
+
1063
+ return {
1064
+ needsImport: true,
1065
+ suggestedPath: importPath,
1066
+ hasComponentsObject: /components:\s*\{/.test(content),
1067
+ };
1068
+ }
1069
+
1070
+ /**
1071
+ * Detect import pattern from existing imports in file
1072
+ * @param {string} content - File content
1073
+ * @returns {string} - Suggested import path
1074
+ */
1075
+ function detectImportPattern(content) {
1076
+ // Check for @/ alias (absolute from package root)
1077
+ if (content.includes('from \'@/components/')) {
1078
+ return '@/components/stack';
1079
+ }
1080
+
1081
+ // Check for relative barrel imports
1082
+ if (content.includes('from \'./\'')) {
1083
+ return './'; // User should adjust based on context
1084
+ }
1085
+
1086
+ // Check for external package imports
1087
+ if (content.includes('from \'@dialpad/dialtone-vue') || content.includes('from \'@dialpad/dialtone-icons')) {
1088
+ return '@dialpad/dialtone-vue3';
1089
+ }
1090
+
1091
+ // Default suggestion
1092
+ return '@/components/stack';
1093
+ }
1094
+
1095
+ /**
1096
+ * Print instructions for adding DtStack import and registration
1097
+ * @param {string} filePath - Path to the file
1098
+ * @param {object} importCheck - Result from detectMissingStackImport
1099
+ */
1100
+ function printImportInstructions(filePath, importCheck) {
1101
+ console.log(log.yellow('\n⚠️ ACTION REQUIRED: Add DtStack import and registration'));
1102
+ console.log(log.cyan(` File: ${filePath}`));
1103
+ console.log();
1104
+ console.log(log.gray(' Add this import to your <script> block:'));
1105
+ console.log(log.green(` import { DtStack } from '${importCheck.suggestedPath}';`));
1106
+ console.log();
1107
+
1108
+ if (importCheck.hasComponentsObject) {
1109
+ console.log(log.gray(' Add to your components object:'));
1110
+ console.log(log.green(' components: {'));
1111
+ console.log(log.green(' // ... existing components'));
1112
+ console.log(log.green(' DtStack,'));
1113
+ console.log(log.green(' },'));
1114
+ } else {
1115
+ console.log(log.gray(' Create or update your components object:'));
1116
+ console.log(log.green(' export default {'));
1117
+ console.log(log.green(' components: { DtStack },'));
1118
+ console.log(log.green(' // ... rest of your component'));
1119
+ console.log(log.green(' };'));
1120
+ }
1121
+ console.log();
1122
+ }
1123
+
1124
+ //------------------------------------------------------------------------------
1125
+ // Argument Parsing (simple, no yargs)
1126
+ //------------------------------------------------------------------------------
1127
+
1128
+ function parseArgs() {
1129
+ const args = process.argv.slice(2);
1130
+ const options = {
1131
+ cwd: process.cwd(),
1132
+ dryRun: false,
1133
+ yes: false,
1134
+ validate: false, // Validation mode - check for issues without modifying
1135
+ extensions: ['.vue'],
1136
+ patterns: [],
1137
+ hasExtFlag: false, // Track if --ext was used
1138
+ files: [], // Explicit file list via --file flag
1139
+ showOutline: false, // Add migration marker for visual debugging
1140
+ removeOutline: false, // Remove migration markers (cleanup mode)
1141
+ };
1142
+
1143
+ for (let i = 0; i < args.length; i++) {
1144
+ const arg = args[i];
1145
+
1146
+ if (arg === '--help' || arg === '-h') {
1147
+ console.log(`
1148
+ Usage: npx dialtone-migrate-flex-to-stack [options]
1149
+
1150
+ Migrates d-d-flex utility patterns to <dt-stack> components.
1151
+
1152
+ After migration, you'll need to add DtStack imports manually.
1153
+ The script will print detailed instructions for each file.
1154
+
1155
+ Options:
1156
+ --cwd <path> Working directory (default: current directory)
1157
+ --ext <ext> File extension to process (default: .vue)
1158
+ Can be specified multiple times (e.g., --ext .vue --ext .md)
1159
+ --file <path> Specific file to process (can be specified multiple times)
1160
+ Relative or absolute paths supported
1161
+ When used, --cwd is ignored for file discovery
1162
+ --dry-run Show changes without applying them
1163
+ --validate Validate transformations and report potential issues
1164
+ (like --dry-run but with additional validation checks)
1165
+ --yes, -y Apply all changes without prompting
1166
+ --show-outline Add data-migrate-outline attribute for visual debugging
1167
+ --remove-outline Remove data-migrate-outline attributes after review
1168
+ --help, -h Show help
1169
+
1170
+ Post-Migration Steps:
1171
+ 1. Review template changes with data-migrate-outline markers
1172
+ 2. Add DtStack imports as instructed by the script
1173
+ 3. Test your application
1174
+ 4. Run with --remove-outline to clean up markers
1175
+
1176
+ Examples:
1177
+ npx dialtone-migrate-flex-to-stack # Process .vue files
1178
+ npx dialtone-migrate-flex-to-stack --ext .md # Process .md files only
1179
+ npx dialtone-migrate-flex-to-stack --ext .vue --ext .md # Process both
1180
+ npx dialtone-migrate-flex-to-stack --ext .md --cwd ./docs # Process .md in docs/
1181
+ npx dialtone-migrate-flex-to-stack --dry-run # Preview changes
1182
+ npx dialtone-migrate-flex-to-stack --yes # Auto-apply all changes
1183
+
1184
+ # Target specific files:
1185
+ npx dialtone-migrate-flex-to-stack --file src/App.vue --dry-run
1186
+ npx dialtone-migrate-flex-to-stack --file ./component1.vue --file ./component2.vue --yes
1187
+ npx dialtone-migrate-flex-to-stack --file /absolute/path/to/file.vue
1188
+ `);
1189
+ process.exit(0);
1190
+ }
1191
+
1192
+ if (arg === '--cwd' && args[i + 1]) {
1193
+ options.cwd = path.resolve(args[++i]);
1194
+ } else if (arg === '--ext' && args[i + 1]) {
1195
+ // First --ext call clears the default
1196
+ if (!options.hasExtFlag) {
1197
+ options.extensions = [];
1198
+ options.hasExtFlag = true;
1199
+ }
1200
+ const ext = args[++i];
1201
+ // Add leading dot if not present
1202
+ options.extensions.push(ext.startsWith('.') ? ext : `.${ext}`);
1203
+ } else if (arg === '--dry-run') {
1204
+ options.dryRun = true;
1205
+ } else if (arg === '--validate') {
1206
+ options.validate = true;
1207
+ options.dryRun = true; // Validate mode implies dry-run (no file modifications)
1208
+ } else if (arg === '--yes' || arg === '-y') {
1209
+ options.yes = true;
1210
+ } else if (arg === '--show-outline') {
1211
+ options.showOutline = true;
1212
+ } else if (arg === '--remove-outline') {
1213
+ options.removeOutline = true;
1214
+ } else if (arg === '--file' && args[i + 1]) {
1215
+ const filePath = args[++i];
1216
+ options.files.push(filePath);
1217
+ } else if (!arg.startsWith('-')) {
1218
+ options.patterns.push(arg);
1219
+ }
1220
+ }
1221
+
1222
+ // Validate mutually exclusive flags - CRITICAL SAFETY CHECK
1223
+ if (options.showOutline && options.removeOutline) {
1224
+ throw new Error('Cannot use --show-outline and --remove-outline together');
1225
+ }
1226
+
1227
+ // Display mode warning for clarity
1228
+ if (options.removeOutline) {
1229
+ console.log(log.yellow('\n⚠️ CLEANUP MODE: Will remove data-migrate-outline attributes only'));
1230
+ console.log(log.yellow(' No flex-to-stack transformations will be performed\n'));
1231
+ }
1232
+
1233
+ return options;
1234
+ }
1235
+
1236
+ //------------------------------------------------------------------------------
1237
+ // Main
1238
+ //------------------------------------------------------------------------------
1239
+
1240
+ async function main() {
1241
+ // Reset global state for fresh run (important for testing)
1242
+ skippedByReason.clear();
1243
+
1244
+ const options = parseArgs();
1245
+
1246
+ log.bold('\n🔄 Flex to Stack Migration Tool\n');
1247
+
1248
+ // Show mode
1249
+ if (options.files.length > 0) {
1250
+ log.gray(`Mode: Targeted files (${options.files.length} specified)`);
1251
+ } else {
1252
+ log.gray(`Mode: Directory scan`);
1253
+ log.gray(`Working directory: ${options.cwd}`);
1254
+ log.gray(`Extensions: ${options.extensions.join(', ')}`);
1255
+ }
1256
+
1257
+ if (options.validate) {
1258
+ console.log(log.yellow('VALIDATE MODE - checking for potential issues'));
1259
+ } else if (options.dryRun) {
1260
+ console.log(log.yellow('DRY RUN - no files will be modified'));
1261
+ }
1262
+ if (options.yes) {
1263
+ console.log(log.yellow('AUTO-APPLY - all changes will be applied without prompts'));
1264
+ }
1265
+
1266
+ // Find files - conditional based on --file flag
1267
+ let files;
1268
+ if (options.files.length > 0) {
1269
+ // Use explicitly specified files
1270
+ files = await validateAndResolveFiles(options.files, options.extensions);
1271
+ } else {
1272
+ // Use directory scanning (current behavior)
1273
+ files = await findFiles(options.cwd, options.extensions, ['node_modules', 'dist', 'coverage']);
1274
+ }
1275
+
1276
+ log.gray(`Found ${files.length} file(s) to scan\n`);
1277
+
1278
+ if (files.length === 0) {
1279
+ console.log(log.yellow('No files found matching the patterns.'));
1280
+ return;
1281
+ }
1282
+
1283
+ // Process files
1284
+ let totalChanges = 0;
1285
+ let totalSkipped = 0;
1286
+ let filesModified = 0;
1287
+ let filesNeedingImports = 0;
1288
+ let totalValidationErrors = 0;
1289
+ let totalValidationWarnings = 0;
1290
+ const fileList = [];
1291
+
1292
+ for (const file of files) {
1293
+ let result;
1294
+
1295
+ if (options.removeOutline) {
1296
+ // CLEANUP MODE ONLY - No transformations will happen
1297
+ // Only removes data-migrate-outline attributes
1298
+ result = await cleanupMarkers(file, {
1299
+ dryRun: options.dryRun,
1300
+ yes: options.yes,
1301
+ });
1302
+ } else {
1303
+ // MIGRATION MODE - Normal flex-to-stack transformation
1304
+ // Can optionally add markers with --show-outline
1305
+ result = await processFile(file, {
1306
+ dryRun: options.dryRun,
1307
+ yes: options.yes,
1308
+ showOutline: options.showOutline,
1309
+ validate: options.validate,
1310
+ });
1311
+ }
1312
+
1313
+ totalChanges += result.changes;
1314
+ totalSkipped += result.skipped;
1315
+ if (result.changes > 0) filesModified++;
1316
+
1317
+ // Track files that need imports
1318
+ if (result.needsImport) {
1319
+ filesNeedingImports++;
1320
+ fileList.push(file);
1321
+ }
1322
+
1323
+ // Track validation results
1324
+ if (result.validationErrors) totalValidationErrors += result.validationErrors;
1325
+ if (result.validationWarnings) totalValidationWarnings += result.validationWarnings;
1326
+ }
1327
+
1328
+ // Summary
1329
+ log.bold('\n📊 Summary\n');
1330
+ console.log(` Files scanned: ${files.length}`);
1331
+ console.log(` Files modified: ${filesModified}`);
1332
+
1333
+ if (options.removeOutline) {
1334
+ console.log(` Markers removed: ${totalChanges}`);
1335
+ } else if (options.validate) {
1336
+ console.log(` Transformations checked: ${totalChanges}`);
1337
+ console.log(` Elements skipped: ${totalSkipped}`);
1338
+ if (totalValidationErrors > 0) {
1339
+ console.log(log.red(` Validation errors: ${totalValidationErrors}`));
1340
+ }
1341
+ if (totalValidationWarnings > 0) {
1342
+ console.log(log.yellow(` Validation warnings: ${totalValidationWarnings}`));
1343
+ }
1344
+ if (totalValidationErrors === 0 && totalValidationWarnings === 0 && totalChanges > 0) {
1345
+ console.log(log.green(` ✓ All transformations validated successfully`));
1346
+ }
1347
+ } else {
1348
+ console.log(` Changes applied: ${totalChanges}`);
1349
+ console.log(` Changes skipped: ${totalSkipped}`);
1350
+ }
1351
+
1352
+ if (filesNeedingImports > 0 && !options.removeOutline && !options.dryRun) {
1353
+ console.log(log.yellow(`\n⚠️ ${filesNeedingImports} file(s) need DtStack import/registration`));
1354
+ console.log(log.gray(' See instructions above for each file.'));
1355
+ console.log();
1356
+ console.log(log.gray(' Quick checklist:'));
1357
+ fileList.forEach(file => {
1358
+ console.log(log.gray(` [ ] ${file}`));
1359
+ });
1360
+ }
1361
+
1362
+ if (options.dryRun && totalChanges > 0) {
1363
+ console.log(log.yellow('\n Run without --dry-run to apply changes.'));
1364
+ }
1365
+
1366
+ // Print grouped summary of all skipped elements
1367
+ if (!options.removeOutline) {
1368
+ printSkippedSummary();
1369
+ }
1370
+
1371
+ console.log();
1372
+ }
1373
+
1374
+ main().catch((error) => {
1375
+ console.error(`${colors.red}Error:${colors.reset}`, error.message);
1376
+ process.exit(1);
1377
+ });