@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.
- package/dist/js/dialtone_health_check/deprecated_icons.cjs +105 -0
- package/dist/js/dialtone_health_check/index.cjs +82 -0
- package/dist/js/dialtone_health_check/non_dialtone_properties.cjs +44 -0
- package/dist/js/dialtone_migrate_flex_to_stack/examples-edge-cases.vue +329 -0
- package/dist/js/dialtone_migrate_flex_to_stack/index.mjs +1377 -0
- package/dist/js/dialtone_migration_helper/configs/box-shadows.mjs +19 -0
- package/dist/js/dialtone_migration_helper/configs/colors.mjs +69 -0
- package/dist/js/dialtone_migration_helper/configs/fonts.mjs +49 -0
- package/dist/js/dialtone_migration_helper/configs/size-and-space.mjs +124 -0
- package/dist/js/dialtone_migration_helper/helpers.mjs +212 -0
- package/dist/js/dialtone_migration_helper/index.mjs +135 -0
- package/dist/tokens/doc.json +46427 -46427
- package/dist/vue3/common/utils/index.cjs +1 -1
- package/dist/vue3/common/utils/index.cjs.map +1 -1
- package/dist/vue3/common/utils/index.js +45 -41
- package/dist/vue3/common/utils/index.js.map +1 -1
- package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.cjs +1 -1
- package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.cjs.map +1 -1
- package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.js +79 -75
- package/dist/vue3/lib/combobox-multi-select/combobox-multi-select.js.map +1 -1
- package/dist/vue3/types/common/utils/index.d.ts +2 -3
- package/dist/vue3/types/common/utils/index.d.ts.map +1 -1
- package/dist/vue3/types/components/collapsible/collapsible.vue.d.ts +1 -3
- package/dist/vue3/types/components/toast/layouts/toast_layout_alternate.vue.d.ts +1 -3
- package/dist/vue3/types/components/toast/layouts/toast_layout_default.vue.d.ts +1 -3
- package/dist/vue3/types/components/toast/toast.vue.d.ts +2 -6
- package/dist/vue3/types/recipes/buttons/callbar_button/callbar_button.vue.d.ts +7 -0
- package/dist/vue3/types/recipes/buttons/callbar_button/callbar_button.vue.d.ts.map +1 -1
- package/dist/vue3/types/recipes/comboboxes/combobox_multi_select/combobox_multi_select.vue.d.ts.map +1 -1
- package/dist/vue3/types/recipes/leftbar/contact_centers_row/contact_centers_row.vue.d.ts +1 -3
- package/dist/vue3/types/recipes/leftbar/contact_centers_row/contact_centers_row.vue.d.ts.map +1 -1
- package/dist/vue3/types/recipes/leftbar/contact_row/contact_row.vue.d.ts +1 -3
- package/dist/vue3/types/recipes/leftbar/general_row/general_row.vue.d.ts +1 -3
- package/dist/vue3/types/recipes/leftbar/general_row/general_row.vue.d.ts.map +1 -1
- package/dist/vue3/types/recipes/leftbar/group_row/group_row.vue.d.ts +1 -3
- package/dist/vue3/types/recipes/leftbar/group_row/group_row.vue.d.ts.map +1 -1
- 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
|
+
});
|