@dialpad/dialtone-css 8.80.0-next.1 → 8.80.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build/js/dialtone_migrate_link_rendering/button-nav-test-examples.vue +92 -0
- package/lib/build/js/dialtone_migrate_link_rendering/button-nav.test.mjs +272 -0
- package/lib/build/js/dialtone_migrate_link_rendering/helpers.mjs +25 -0
- package/lib/build/js/dialtone_migrate_link_rendering/index.mjs +1041 -0
- package/lib/build/js/dialtone_migrate_link_rendering/link-nav-test-examples.vue +97 -0
- package/lib/build/js/dialtone_migrate_link_rendering/link-nav.test.mjs +194 -0
- package/lib/build/js/dialtone_migrate_link_rendering/underline-test-examples.vue +57 -0
- package/lib/build/js/dialtone_migrate_link_rendering/underline.test.mjs +161 -0
- package/lib/build/js/dialtone_migrate_props/index.mjs +794 -0
- package/lib/build/js/dialtone_migrate_props/test.mjs +959 -0
- package/lib/build/js/dialtone_migration_helper/configs/base-to-semantic.mjs +8 -8
- package/lib/build/js/dialtone_migration_helper/configs/size-to-layout.mjs +10 -0
- package/lib/build/js/dialtone_migration_helper/configs/success-to-positive.mjs +73 -0
- package/lib/build/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +92 -12
- package/lib/build/js/dialtone_migration_helper/tests/base-to-semantic-test-examples.vue +10 -10
- package/lib/build/js/dialtone_migration_helper/tests/base-to-semantic.test.mjs +8 -8
- package/lib/build/js/dialtone_migration_helper/tests/size-to-layout-test-examples.vue +16 -0
- package/lib/build/js/dialtone_migration_helper/tests/size-to-layout.test.mjs +87 -0
- package/lib/build/js/dialtone_migration_helper/tests/success-to-positive-test-examples.vue +166 -0
- package/lib/build/js/dialtone_migration_helper/tests/success-to-positive.test.mjs +287 -0
- package/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops-radius-examples.vue +66 -0
- package/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +170 -0
- package/lib/build/less/components/badge.less +1 -1
- package/lib/build/less/components/banner.less +1 -1
- package/lib/build/less/components/box.less +228 -0
- package/lib/build/less/components/description-list.less +4 -0
- package/lib/build/less/components/forms.less +4 -2
- package/lib/build/less/components/input.less +2 -2
- package/lib/build/less/components/link.less +18 -4
- package/lib/build/less/components/modal.less +1 -1
- package/lib/build/less/components/notice.less +3 -3
- package/lib/build/less/components/progress_circle.less +10 -2
- package/lib/build/less/components/prose.less +512 -0
- package/lib/build/less/components/rich-text-editor.less +7 -0
- package/lib/build/less/components/selects.less +1 -1
- package/lib/build/less/components/text.less +2 -2
- package/lib/build/less/components/toast.less +1 -1
- package/lib/build/less/dialtone.less +2 -0
- package/lib/build/less/recipes/leftbar_row.less +1 -0
- package/lib/build/less/recipes/settings_menu_button.less +1 -1
- package/lib/build/less/recipes/top_banner_info.less +1 -1
- package/lib/build/less/utilities/backgrounds.less +12 -0
- package/lib/build/less/utilities/borders.less +56 -89
- package/lib/build/less/utilities/colors.less +8 -0
- package/lib/build/less/utilities/effects.less +1 -0
- package/lib/build/less/utilities/flex.less +145 -18
- package/lib/build/less/utilities/grid.less +40 -152
- package/lib/build/less/utilities/layout.less +19 -7
- package/lib/build/less/utilities/sizing.less +148 -143
- package/lib/build/less/variables/visual-styles.less +2 -1
- package/lib/dist/dialtone-default-theme.css +4364 -1756
- package/lib/dist/dialtone-default-theme.min.css +1 -1
- package/lib/dist/dialtone-docs.json +1 -1
- package/lib/dist/dialtone.css +4271 -1705
- package/lib/dist/dialtone.min.css +1 -1
- package/lib/dist/js/dialtone_migrate_link_rendering/button-nav-test-examples.vue +92 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/button-nav.test.mjs +272 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/helpers.mjs +25 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/index.mjs +1041 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/link-nav-test-examples.vue +97 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/link-nav.test.mjs +194 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/underline-test-examples.vue +57 -0
- package/lib/dist/js/dialtone_migrate_link_rendering/underline.test.mjs +161 -0
- package/lib/dist/js/dialtone_migrate_props/index.mjs +794 -0
- package/lib/dist/js/dialtone_migrate_props/test.mjs +959 -0
- package/lib/dist/js/dialtone_migration_helper/configs/base-to-semantic.mjs +8 -8
- package/lib/dist/js/dialtone_migration_helper/configs/size-to-layout.mjs +10 -0
- package/lib/dist/js/dialtone_migration_helper/configs/success-to-positive.mjs +73 -0
- package/lib/dist/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +92 -12
- package/lib/dist/js/dialtone_migration_helper/tests/base-to-semantic-test-examples.vue +10 -10
- package/lib/dist/js/dialtone_migration_helper/tests/base-to-semantic.test.mjs +8 -8
- package/lib/dist/js/dialtone_migration_helper/tests/size-to-layout-test-examples.vue +16 -0
- package/lib/dist/js/dialtone_migration_helper/tests/size-to-layout.test.mjs +87 -0
- package/lib/dist/js/dialtone_migration_helper/tests/success-to-positive-test-examples.vue +166 -0
- package/lib/dist/js/dialtone_migration_helper/tests/success-to-positive.test.mjs +287 -0
- package/lib/dist/js/dialtone_migration_helper/tests/utility-class-to-token-stops-radius-examples.vue +66 -0
- package/lib/dist/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +170 -0
- package/lib/dist/tokens/tokens-101-dark.css +81 -45
- package/lib/dist/tokens/tokens-101-light.css +75 -39
- package/lib/dist/tokens/tokens-102-dark.css +81 -45
- package/lib/dist/tokens/tokens-102-light.css +75 -39
- package/lib/dist/tokens/tokens-103-dark.css +81 -45
- package/lib/dist/tokens/tokens-103-light.css +75 -39
- package/lib/dist/tokens/tokens-104-dark.css +81 -45
- package/lib/dist/tokens/tokens-104-light.css +75 -39
- package/lib/dist/tokens/tokens-105-dark.css +81 -45
- package/lib/dist/tokens/tokens-105-light.css +75 -39
- package/lib/dist/tokens/tokens-106-dark.css +81 -45
- package/lib/dist/tokens/tokens-106-light.css +75 -39
- package/lib/dist/tokens/tokens-107-dark.css +81 -45
- package/lib/dist/tokens/tokens-107-light.css +75 -39
- package/lib/dist/tokens/tokens-108-dark.css +81 -45
- package/lib/dist/tokens/tokens-108-light.css +75 -39
- package/lib/dist/tokens/tokens-109-dark.css +81 -45
- package/lib/dist/tokens/tokens-109-light.css +75 -39
- package/lib/dist/tokens/tokens-110-dark.css +81 -45
- package/lib/dist/tokens/tokens-110-light.css +75 -39
- package/lib/dist/tokens/tokens-111-dark.css +81 -45
- package/lib/dist/tokens/tokens-111-light.css +75 -39
- package/lib/dist/tokens/tokens-112-dark.css +81 -45
- package/lib/dist/tokens/tokens-112-light.css +75 -39
- package/lib/dist/tokens/tokens-113-dark.css +81 -45
- package/lib/dist/tokens/tokens-113-light.css +75 -39
- package/lib/dist/tokens/tokens-114-dark.css +81 -45
- package/lib/dist/tokens/tokens-114-light.css +75 -39
- package/lib/dist/tokens/tokens-115-dark.css +81 -45
- package/lib/dist/tokens/tokens-115-light.css +75 -39
- package/lib/dist/tokens/tokens-116-dark.css +81 -45
- package/lib/dist/tokens/tokens-116-light.css +75 -39
- package/lib/dist/tokens/tokens-117-dark.css +81 -45
- package/lib/dist/tokens/tokens-117-light.css +75 -39
- package/lib/dist/tokens/tokens-118-dark.css +81 -45
- package/lib/dist/tokens/tokens-118-light.css +75 -39
- package/lib/dist/tokens/tokens-119-dark.css +81 -45
- package/lib/dist/tokens/tokens-119-light.css +75 -39
- package/lib/dist/tokens/tokens-120-dark.css +81 -45
- package/lib/dist/tokens/tokens-120-light.css +75 -39
- package/lib/dist/tokens/tokens-121-dark.css +81 -45
- package/lib/dist/tokens/tokens-121-light.css +75 -39
- package/lib/dist/tokens/tokens-122-dark.css +81 -45
- package/lib/dist/tokens/tokens-122-light.css +75 -39
- package/lib/dist/tokens/tokens-123-dark.css +81 -45
- package/lib/dist/tokens/tokens-123-light.css +75 -39
- package/lib/dist/tokens/tokens-124-dark.css +81 -45
- package/lib/dist/tokens/tokens-124-light.css +75 -39
- package/lib/dist/tokens/tokens-125-dark.css +81 -45
- package/lib/dist/tokens/tokens-125-light.css +75 -39
- package/lib/dist/tokens/tokens-126-dark.css +81 -45
- package/lib/dist/tokens/tokens-126-light.css +75 -39
- package/lib/dist/tokens/tokens-127-dark.css +81 -45
- package/lib/dist/tokens/tokens-127-light.css +75 -39
- package/lib/dist/tokens/tokens-128-dark.css +81 -45
- package/lib/dist/tokens/tokens-128-light.css +75 -39
- package/lib/dist/tokens/tokens-129-dark.css +81 -45
- package/lib/dist/tokens/tokens-129-light.css +75 -39
- package/lib/dist/tokens/tokens-130-dark.css +81 -45
- package/lib/dist/tokens/tokens-130-light.css +75 -39
- package/lib/dist/tokens/tokens-131-dark.css +81 -45
- package/lib/dist/tokens/tokens-131-light.css +75 -39
- package/lib/dist/tokens/tokens-132-dark.css +81 -45
- package/lib/dist/tokens/tokens-132-light.css +75 -39
- package/lib/dist/tokens/tokens-133-dark.css +81 -45
- package/lib/dist/tokens/tokens-133-light.css +75 -39
- package/lib/dist/tokens/tokens-134-dark.css +81 -45
- package/lib/dist/tokens/tokens-134-light.css +75 -39
- package/lib/dist/tokens/tokens-135-dark.css +81 -45
- package/lib/dist/tokens/tokens-135-light.css +75 -39
- package/lib/dist/tokens/tokens-136-dark.css +81 -45
- package/lib/dist/tokens/tokens-136-light.css +75 -39
- package/lib/dist/tokens/tokens-137-dark.css +81 -45
- package/lib/dist/tokens/tokens-137-light.css +75 -39
- package/lib/dist/tokens/tokens-aegean-dark.css +81 -45
- package/lib/dist/tokens/tokens-aegean-light.css +75 -39
- package/lib/dist/tokens/tokens-base-dark.css +18 -12
- package/lib/dist/tokens/tokens-base-light.css +18 -12
- package/lib/dist/tokens/tokens-botany-dark.css +81 -45
- package/lib/dist/tokens/tokens-botany-light.css +75 -39
- package/lib/dist/tokens/tokens-buttercream-dark.css +81 -45
- package/lib/dist/tokens/tokens-buttercream-light.css +75 -39
- package/lib/dist/tokens/tokens-ceruleo-dark.css +81 -45
- package/lib/dist/tokens/tokens-ceruleo-light.css +75 -39
- package/lib/dist/tokens/tokens-debug-base.css +6 -0
- package/lib/dist/tokens/tokens-debug-dp.css +39 -3
- package/lib/dist/tokens/tokens-dp-dark.css +81 -45
- package/lib/dist/tokens/tokens-dp-light.css +75 -39
- package/lib/dist/tokens/tokens-expressive-dark.css +81 -45
- package/lib/dist/tokens/tokens-expressive-light.css +75 -39
- package/lib/dist/tokens/tokens-expressive-sm-dark.css +81 -45
- package/lib/dist/tokens/tokens-expressive-sm-light.css +75 -39
- package/lib/dist/tokens/tokens-high-desert-dark.css +81 -45
- package/lib/dist/tokens/tokens-high-desert-light.css +75 -39
- package/lib/dist/tokens/tokens-melon-dark.css +81 -45
- package/lib/dist/tokens/tokens-melon-light.css +75 -39
- package/lib/dist/tokens/tokens-plum-dark.css +81 -45
- package/lib/dist/tokens/tokens-plum-light.css +75 -39
- package/lib/dist/tokens/tokens-prota-deuter-dark.css +79 -43
- package/lib/dist/tokens/tokens-prota-deuter-light.css +74 -38
- package/lib/dist/tokens/tokens-sunflower-dark.css +81 -45
- package/lib/dist/tokens/tokens-sunflower-light.css +75 -39
- package/lib/dist/tokens/tokens-tmo-dark.css +81 -45
- package/lib/dist/tokens/tokens-tmo-light.css +75 -39
- package/lib/dist/tokens/tokens-trita-dark.css +81 -45
- package/lib/dist/tokens/tokens-trita-light.css +75 -39
- package/lib/dist/tokens/tokens-verdant-haze-dark.css +81 -45
- package/lib/dist/tokens/tokens-verdant-haze-light.css +75 -39
- package/lib/dist/tokens-docs.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable max-lines */
|
|
3
|
+
/* eslint-disable complexity */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Migration script for DtLink and DtButton anchor / router-link rendering
|
|
7
|
+
* patterns plus DtLink underline prop.
|
|
8
|
+
*
|
|
9
|
+
* Covers:
|
|
10
|
+
* DLT-3033 <a class="d-btn"> -> <dt-button href="…">
|
|
11
|
+
* <router-link class="d-btn" :to> -> <dt-button :to="…">
|
|
12
|
+
* d-btn--{size,kind,importance,…} -> corresponding props
|
|
13
|
+
*
|
|
14
|
+
* DLT-3034 <a class="d-link"> -> <dt-link href="…">
|
|
15
|
+
* <router-link class="d-link" :to> -> <dt-link :to="…">
|
|
16
|
+
* d-link--{tone} -> tone="…" (with rename)
|
|
17
|
+
* d-link--no-underline -> :underline="false"
|
|
18
|
+
*
|
|
19
|
+
* DLT-3035 <dt-link class="d-td-…"> -> closest :underline value
|
|
20
|
+
* responsive d-td-* variants -> warn (skip)
|
|
21
|
+
*
|
|
22
|
+
* Vue files only by default. `--include-markdown` opts into `.md` files.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* npx dialtone-migrate-link-rendering [options]
|
|
26
|
+
*
|
|
27
|
+
* Options:
|
|
28
|
+
* --cwd <path> Working directory (default: cwd)
|
|
29
|
+
* --dry-run Show changes without applying them
|
|
30
|
+
* --yes Apply all changes without prompting
|
|
31
|
+
* --help Show help
|
|
32
|
+
* --only=<list> Run only the named transforms; CSV of:
|
|
33
|
+
* button-nav, link-nav, underline (default: all)
|
|
34
|
+
* --include-markdown Also walk .md files (off by default)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import fs from 'fs/promises';
|
|
38
|
+
import { realpathSync } from 'node:fs';
|
|
39
|
+
import path from 'path';
|
|
40
|
+
import readline from 'readline';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Configuration
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const TRANSFORM = Object.freeze({
|
|
48
|
+
BUTTON_NAV: 'button-nav',
|
|
49
|
+
LINK_NAV: 'link-nav',
|
|
50
|
+
UNDERLINE: 'underline',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ALL_TRANSFORMS = Object.values(TRANSFORM);
|
|
54
|
+
|
|
55
|
+
// Cheap precheck pattern. Skip the masking + transform sweeps only when the file
|
|
56
|
+
// contains none of the tokens any of the three transforms or the warn-only paths
|
|
57
|
+
// could possibly hit. `<router-link` / `<RouterLink` is included because the
|
|
58
|
+
// custom-slot wrapper warning can fire even when the inner `<dt-button>` /
|
|
59
|
+
// `<dt-link>` carries no legacy class.
|
|
60
|
+
const FAST_PATH_RE = /d-btn|d-link|d-td-|<router-link|<RouterLink/;
|
|
61
|
+
|
|
62
|
+
// Tag-name alternation that accepts both kebab-case and PascalCase Vue spellings.
|
|
63
|
+
// The codemod always emits kebab-case output regardless of the source casing.
|
|
64
|
+
const TAG_NAME_ALTERNATIONS = Object.freeze({
|
|
65
|
+
'a': 'a',
|
|
66
|
+
'router-link': '(?:router-link|RouterLink)',
|
|
67
|
+
'dt-link': '(?:dt-link|DtLink)',
|
|
68
|
+
'dt-button': '(?:dt-button|DtButton)',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function tagNamePattern (sourceTag) {
|
|
72
|
+
return TAG_NAME_ALTERNATIONS[sourceTag] || escapeRe(sourceTag);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Quote-aware attribute body. Matches any sequence of characters that aren't
|
|
76
|
+
// `>`/`"`/`'`, optionally followed by a fully-quoted attribute value. Handles
|
|
77
|
+
// patterns like `:to="a > b ? x : y"` correctly by skipping `>` inside quotes.
|
|
78
|
+
const QUOTE_AWARE_ATTRS = '(?:[^>"\']*(?:"[^"]*"|\'[^\']*\'))*[^>]*';
|
|
79
|
+
|
|
80
|
+
// Tone modifier values (canonical and renamed-from forms)
|
|
81
|
+
const TONE_MODIFIER_MAP = {
|
|
82
|
+
critical: 'critical',
|
|
83
|
+
danger: 'critical', // rename
|
|
84
|
+
warning: 'warning',
|
|
85
|
+
positive: 'positive',
|
|
86
|
+
success: 'positive', // rename
|
|
87
|
+
info: 'info',
|
|
88
|
+
muted: 'muted',
|
|
89
|
+
mention: 'mention',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// DtButton: kind modifiers (with d-btn--danger -> kind="critical" rename)
|
|
93
|
+
const BUTTON_KIND_MODIFIER_MAP = {
|
|
94
|
+
muted: 'muted',
|
|
95
|
+
critical: 'critical',
|
|
96
|
+
danger: 'critical', // rename
|
|
97
|
+
positive: 'positive',
|
|
98
|
+
success: 'positive', // rename (defensive — unused in CSS but consumers may still write it)
|
|
99
|
+
inverted: 'inverted',
|
|
100
|
+
unstyled: 'unstyled',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// DtButton: importance modifiers
|
|
104
|
+
const BUTTON_IMPORTANCE_MODIFIER_MAP = {
|
|
105
|
+
outlined: 'outlined',
|
|
106
|
+
// d-btn--primary is the default; stripped without emitting a prop
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// DtButton: size modifiers (t-shirt classname -> numeric prop value)
|
|
110
|
+
const BUTTON_SIZE_MODIFIER_MAP = {
|
|
111
|
+
xs: 100,
|
|
112
|
+
sm: 200,
|
|
113
|
+
// d-btn--md is the default; stripped without emitting a prop
|
|
114
|
+
lg: 400,
|
|
115
|
+
xl: 500,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Small utilities
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function escapeRe (str) {
|
|
123
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Split a class attribute value into tokens, preserving order.
|
|
128
|
+
* Empty tokens are dropped.
|
|
129
|
+
*/
|
|
130
|
+
function splitClasses (classStr) {
|
|
131
|
+
return classStr.split(/\s+/).filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function joinClasses (tokens) {
|
|
135
|
+
return tokens.join(' ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Quote an attribute value, picking the quote style that doesn't conflict.
|
|
140
|
+
* Defaults to double quotes; falls back to single if value contains them.
|
|
141
|
+
*/
|
|
142
|
+
function quoteAttr (value) {
|
|
143
|
+
return value.includes('"') && !value.includes('\'')
|
|
144
|
+
? `'${value}'`
|
|
145
|
+
: `"${value.replace(/"/g, '"')}"`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Class-string extractors
|
|
150
|
+
//
|
|
151
|
+
// Each extractor takes a tokens[] array, mutates it (removing matched tokens),
|
|
152
|
+
// and returns the extracted value (or null if no match).
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function extractSizeModifier (tokens) {
|
|
156
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
157
|
+
const m = tokens[i].match(/^d-btn--(xs|sm|md|lg|xl)$/);
|
|
158
|
+
if (!m) continue;
|
|
159
|
+
tokens.splice(i, 1);
|
|
160
|
+
const tShirt = m[1];
|
|
161
|
+
if (tShirt === 'md') return null; // default — strip silently
|
|
162
|
+
return BUTTON_SIZE_MODIFIER_MAP[tShirt];
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractImportanceModifier (tokens) {
|
|
168
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
169
|
+
const m = tokens[i].match(/^d-btn--(outlined|primary)$/);
|
|
170
|
+
if (!m) continue;
|
|
171
|
+
tokens.splice(i, 1);
|
|
172
|
+
if (m[1] === 'primary') return null; // default — strip silently
|
|
173
|
+
return BUTTON_IMPORTANCE_MODIFIER_MAP[m[1]];
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractKindModifier (tokens) {
|
|
179
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
180
|
+
const m = tokens[i].match(/^d-btn--(muted|critical|danger|positive|success|inverted|unstyled)$/);
|
|
181
|
+
if (!m) continue;
|
|
182
|
+
tokens.splice(i, 1);
|
|
183
|
+
return BUTTON_KIND_MODIFIER_MAP[m[1]];
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function extractCircleModifier (tokens) {
|
|
189
|
+
const i = tokens.indexOf('d-btn--circle');
|
|
190
|
+
if (i === -1) return false;
|
|
191
|
+
tokens.splice(i, 1);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractActiveLoadingModifiers (tokens) {
|
|
196
|
+
let active = false;
|
|
197
|
+
let loading = false;
|
|
198
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
199
|
+
if (tokens[i] === 'd-btn--active') {
|
|
200
|
+
active = true;
|
|
201
|
+
tokens.splice(i, 1);
|
|
202
|
+
} else if (tokens[i] === 'd-btn--loading') {
|
|
203
|
+
loading = true;
|
|
204
|
+
tokens.splice(i, 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { active, loading };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* For DtLink: extract the tone modifier (d-link--{tone}) with renames applied.
|
|
212
|
+
* Skips inverted variants (handled by extractInvertedLinkModifier).
|
|
213
|
+
*/
|
|
214
|
+
function extractLinkToneModifier (tokens) {
|
|
215
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
216
|
+
const t = tokens[i];
|
|
217
|
+
if (t.startsWith('d-link--inverted')) continue;
|
|
218
|
+
const m = t.match(/^d-link--(critical|danger|warning|positive|success|info|muted|mention)$/);
|
|
219
|
+
if (!m) continue;
|
|
220
|
+
tokens.splice(i, 1);
|
|
221
|
+
return TONE_MODIFIER_MAP[m[1]];
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractNoUnderlineModifier (tokens) {
|
|
227
|
+
const i = tokens.indexOf('d-link--no-underline');
|
|
228
|
+
if (i === -1) return false;
|
|
229
|
+
tokens.splice(i, 1);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Detect inverted-* link modifiers. If a tone is bundled (`d-link--inverted-critical`),
|
|
235
|
+
* strip the class and return that tone. Otherwise (plain `d-link--inverted`), strip the
|
|
236
|
+
* class and return null. The caller emits a per-file note about v-dt-mode either way.
|
|
237
|
+
*
|
|
238
|
+
* Disabled-link modifiers (`d-link--disabled`, `d-link--inverted-disabled`) are LEFT
|
|
239
|
+
* in place — no prop equivalent, preserved on class.
|
|
240
|
+
*/
|
|
241
|
+
function extractInvertedLinkModifier (tokens) {
|
|
242
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
243
|
+
const t = tokens[i];
|
|
244
|
+
if (t === 'd-link--inverted-disabled') return { found: false, tone: null };
|
|
245
|
+
if (t === 'd-link--inverted') {
|
|
246
|
+
tokens.splice(i, 1);
|
|
247
|
+
return { found: true, tone: null };
|
|
248
|
+
}
|
|
249
|
+
const m = t.match(/^d-link--inverted-(critical|danger|warning|positive|success|info|muted|mention)$/);
|
|
250
|
+
if (m) {
|
|
251
|
+
tokens.splice(i, 1);
|
|
252
|
+
return { found: true, tone: TONE_MODIFIER_MAP[m[1]] };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { found: false, tone: null };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Attribute helpers (operate on the opening-tag string, between `<tag` and `>`)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
function detectDynamicClass (attrs) {
|
|
264
|
+
return /(^|[\s])(:|v-bind:)class\s*=/.test(attrs);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Memoized regex pairs per attribute name so per-tag calls don't recompile.
|
|
268
|
+
const EXTRACT_ATTR_RE_CACHE = new Map();
|
|
269
|
+
|
|
270
|
+
function extractAttrRegexes (attrName) {
|
|
271
|
+
let cached = EXTRACT_ATTR_RE_CACHE.get(attrName);
|
|
272
|
+
if (cached) return cached;
|
|
273
|
+
cached = {
|
|
274
|
+
dyn: new RegExp(`(?<=^|\\s)(:|v-bind:)${escapeRe(attrName)}=("([^"]*)"|'([^']*)')`),
|
|
275
|
+
stat: new RegExp(`(?<=^|\\s)${escapeRe(attrName)}=("([^"]*)"|'([^']*)')`),
|
|
276
|
+
};
|
|
277
|
+
EXTRACT_ATTR_RE_CACHE.set(attrName, cached);
|
|
278
|
+
return cached;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract a static or dynamic attribute from an attrs string.
|
|
283
|
+
* Returns { name, value, dynamic, before, after } or null.
|
|
284
|
+
*/
|
|
285
|
+
function extractAttr (attrs, attrName) {
|
|
286
|
+
const { dyn, stat } = extractAttrRegexes(attrName);
|
|
287
|
+
const dynMatch = attrs.match(dyn);
|
|
288
|
+
if (dynMatch) {
|
|
289
|
+
const value = dynMatch[3] !== undefined ? dynMatch[3] : dynMatch[4];
|
|
290
|
+
return {
|
|
291
|
+
name: attrName,
|
|
292
|
+
value,
|
|
293
|
+
dynamic: true,
|
|
294
|
+
before: attrs.slice(0, dynMatch.index),
|
|
295
|
+
after: attrs.slice(dynMatch.index + dynMatch[0].length),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const staticMatch = attrs.match(stat);
|
|
299
|
+
if (staticMatch) {
|
|
300
|
+
const value = staticMatch[2] !== undefined ? staticMatch[2] : staticMatch[3];
|
|
301
|
+
return {
|
|
302
|
+
name: attrName,
|
|
303
|
+
value,
|
|
304
|
+
dynamic: false,
|
|
305
|
+
before: attrs.slice(0, staticMatch.index),
|
|
306
|
+
after: attrs.slice(staticMatch.index + staticMatch[0].length),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Strip leading/trailing whitespace inside an attrs string while keeping
|
|
314
|
+
* a single leading space when non-empty (so `<dt-button href="...">` parses).
|
|
315
|
+
*/
|
|
316
|
+
function normalizeAttrs (attrs) {
|
|
317
|
+
// Trim only the outer separators. Don't collapse internal `\s+` because that
|
|
318
|
+
// also rewrites whitespace INSIDE quoted attribute values (e.g. `title="Hello world"`
|
|
319
|
+
// would lose its spaces).
|
|
320
|
+
const trimmed = attrs.trim();
|
|
321
|
+
return trimmed ? ` ${trimmed}` : '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Closing-tag matcher (depth-aware)
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Given the source content and the start index of an opening tag with `tagName`,
|
|
330
|
+
* find the index of the matching closing `</tagName>` (depth-aware). Returns
|
|
331
|
+
* { closeStart, closeEnd } or null if unbalanced.
|
|
332
|
+
*/
|
|
333
|
+
// Memoized open/close regex pairs per tag name. Each call needs fresh
|
|
334
|
+
// `lastIndex` state, so we clone with new RegExp() on lookup but reuse the
|
|
335
|
+
// compiled pattern source string.
|
|
336
|
+
const CLOSING_TAG_RE_CACHE = new Map();
|
|
337
|
+
|
|
338
|
+
function findClosingTag (content, openEndIndex, tagName) {
|
|
339
|
+
let cached = CLOSING_TAG_RE_CACHE.get(tagName);
|
|
340
|
+
if (!cached) {
|
|
341
|
+
cached = {
|
|
342
|
+
// Quote-aware open pattern so `>` inside attribute values doesn't terminate the tag.
|
|
343
|
+
open: `<${tagNamePattern(tagName)}\\b${QUOTE_AWARE_ATTRS}>`,
|
|
344
|
+
// Closing tags can't have attributes; tag-name alternation handles PascalCase.
|
|
345
|
+
close: `</${tagNamePattern(tagName)}\\s*>`,
|
|
346
|
+
};
|
|
347
|
+
CLOSING_TAG_RE_CACHE.set(tagName, cached);
|
|
348
|
+
}
|
|
349
|
+
const openRe = new RegExp(cached.open, 'g');
|
|
350
|
+
const closeRe = new RegExp(cached.close, 'g');
|
|
351
|
+
openRe.lastIndex = openEndIndex;
|
|
352
|
+
closeRe.lastIndex = openEndIndex;
|
|
353
|
+
|
|
354
|
+
let depth = 1;
|
|
355
|
+
while (depth > 0) {
|
|
356
|
+
const nextOpen = openRe.exec(content);
|
|
357
|
+
const nextClose = closeRe.exec(content);
|
|
358
|
+
if (!nextClose) return null;
|
|
359
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
360
|
+
depth += 1;
|
|
361
|
+
// self-closing elements don't actually open
|
|
362
|
+
if (nextOpen[0].endsWith('/>')) depth -= 1;
|
|
363
|
+
closeRe.lastIndex = nextOpen.index + nextOpen[0].length;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
depth -= 1;
|
|
367
|
+
if (depth === 0) {
|
|
368
|
+
return {
|
|
369
|
+
closeStart: nextClose.index,
|
|
370
|
+
closeEnd: nextClose.index + nextClose[0].length,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
openRe.lastIndex = nextClose.index + nextClose[0].length;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Navigation transforms (anchor / router-link → DtButton / DtLink)
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
// One ComponentConfig per target component. Holds the strings that vary
|
|
383
|
+
// between the DtButton and DtLink rewrite paths so `buildRewrittenTag`
|
|
384
|
+
// stays free of `targetTag === 'dt-button'` ternaries.
|
|
385
|
+
const COMPONENT_CONFIGS = Object.freeze({
|
|
386
|
+
'dt-button': {
|
|
387
|
+
targetTag: 'dt-button',
|
|
388
|
+
baseClass: 'd-btn',
|
|
389
|
+
warningSourceLabel: 'a/router-link.d-btn',
|
|
390
|
+
extractModifiers: extractDtButtonModifiers,
|
|
391
|
+
},
|
|
392
|
+
'dt-link': {
|
|
393
|
+
targetTag: 'dt-link',
|
|
394
|
+
baseClass: 'd-link',
|
|
395
|
+
warningSourceLabel: 'a/router-link.d-link',
|
|
396
|
+
extractModifiers: extractDtLinkModifiers,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Memoized `<sourceTag …class="…requiredClass…"…>` regexes. Keyed on
|
|
401
|
+
// `${sourceTag}|${requiredClass}` so the four (sourceTag × requiredClass)
|
|
402
|
+
// combinations the codemod uses are each compiled once.
|
|
403
|
+
const OPEN_WITH_CLASS_RE_CACHE = new Map();
|
|
404
|
+
|
|
405
|
+
function openWithClassRegex (sourceTag, requiredClass) {
|
|
406
|
+
const key = `${sourceTag}|${requiredClass}`;
|
|
407
|
+
let cached = OPEN_WITH_CLASS_RE_CACHE.get(key);
|
|
408
|
+
if (cached) return cached;
|
|
409
|
+
// The quote-aware skip on either side of `class=` prevents `>` inside other
|
|
410
|
+
// attribute values (`:to="a > b"`, `v-if="count > 0"`) from prematurely
|
|
411
|
+
// terminating the opening-tag span. The tag-name pattern accepts kebab-case
|
|
412
|
+
// and PascalCase spellings.
|
|
413
|
+
cached = new RegExp(
|
|
414
|
+
`<${tagNamePattern(sourceTag)}\\b(${QUOTE_AWARE_ATTRS}?)\\sclass=("([^"]*\\b${escapeRe(requiredClass)}\\b[^"]*)"|'([^']*\\b${escapeRe(requiredClass)}\\b[^']*)')(${QUOTE_AWARE_ATTRS}?)(/?)>`,
|
|
415
|
+
'g',
|
|
416
|
+
);
|
|
417
|
+
OPEN_WITH_CLASS_RE_CACHE.set(key, cached);
|
|
418
|
+
return cached;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function transformButtonNav (content, ctx) {
|
|
422
|
+
const config = COMPONENT_CONFIGS['dt-button'];
|
|
423
|
+
let out = rewriteAnchorOrRouterLink(content, 'a', config, ctx, /* isRouterLink */ false);
|
|
424
|
+
out = rewriteAnchorOrRouterLink(out, 'router-link', config, ctx, /* isRouterLink */ true);
|
|
425
|
+
warnRouterLinkCustomWrappers(out, config.targetTag, ctx);
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Walk the content looking for `<sourceTag …>` opening tags whose static class attr
|
|
431
|
+
* contains `config.baseClass`. For each match, find the matching closing tag and rewrite
|
|
432
|
+
* the pair to `<config.targetTag …>…</config.targetTag>` with attrs derived from the source.
|
|
433
|
+
*/
|
|
434
|
+
function rewriteAnchorOrRouterLink (content, sourceTag, config, ctx, isRouterLink) {
|
|
435
|
+
const openWithClassRe = openWithClassRegex(sourceTag, config.baseClass);
|
|
436
|
+
// Re-init lastIndex since we cache the regex object across calls.
|
|
437
|
+
openWithClassRe.lastIndex = 0;
|
|
438
|
+
|
|
439
|
+
const replacements = [];
|
|
440
|
+
let m;
|
|
441
|
+
while ((m = openWithClassRe.exec(content)) !== null) {
|
|
442
|
+
const [fullOpen, attrsBefore, , quotedDouble, quotedSingle, attrsAfter, selfClose] = m;
|
|
443
|
+
const classValue = quotedDouble !== undefined ? quotedDouble : quotedSingle;
|
|
444
|
+
// The class regex uses `\b` which lets `d-btn` match inside `d-btn--lg`. Verify
|
|
445
|
+
// the base class is present as its own token before transforming, so files that
|
|
446
|
+
// only carry modifier-only classes (`d-btn--lg` without `d-btn`) are skipped.
|
|
447
|
+
if (!splitClasses(classValue).includes(config.baseClass)) continue;
|
|
448
|
+
const openStart = m.index;
|
|
449
|
+
const openEnd = openStart + fullOpen.length;
|
|
450
|
+
|
|
451
|
+
const restAttrs = `${attrsBefore || ''} ${attrsAfter || ''}`.trim();
|
|
452
|
+
|
|
453
|
+
if (selfClose === '/') {
|
|
454
|
+
const rewritten = buildRewrittenTag({
|
|
455
|
+
config, classValue, attrs: restAttrs, isRouterLink, selfClosing: true, ctx,
|
|
456
|
+
});
|
|
457
|
+
if (rewritten == null) continue;
|
|
458
|
+
replacements.push({ start: openStart, end: openEnd, text: rewritten });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const close = findClosingTag(content, openEnd, sourceTag);
|
|
463
|
+
if (!close) {
|
|
464
|
+
ctx.warnings.push(
|
|
465
|
+
`${ctx.filePath}: <${sourceTag} class="${classValue}"> has no matching </${sourceTag}> — skipped.`,
|
|
466
|
+
);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const rewritten = buildRewrittenTag({
|
|
471
|
+
config, classValue, attrs: restAttrs, isRouterLink, selfClosing: false, ctx,
|
|
472
|
+
});
|
|
473
|
+
if (rewritten == null) continue;
|
|
474
|
+
|
|
475
|
+
replacements.push({ start: openStart, end: openEnd, text: rewritten });
|
|
476
|
+
replacements.push({ start: close.closeStart, end: close.closeEnd, text: `</${config.targetTag}>` });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
480
|
+
let out = content;
|
|
481
|
+
for (const r of replacements) {
|
|
482
|
+
out = out.slice(0, r.start) + r.text + out.slice(r.end);
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Build the rewritten opening tag. Returns null to abort the rewrite (with a warning).
|
|
489
|
+
*/
|
|
490
|
+
function buildRewrittenTag ({ config, classValue, attrs, isRouterLink, selfClosing, ctx }) {
|
|
491
|
+
if (detectDynamicClass(attrs)) {
|
|
492
|
+
ctx.warnings.push(
|
|
493
|
+
`${ctx.filePath}: <${config.warningSourceLabel}> with dynamic :class — manual review required.`,
|
|
494
|
+
);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let workingAttrs = attrs;
|
|
499
|
+
const tokens = splitClasses(classValue);
|
|
500
|
+
const newAttrs = [];
|
|
501
|
+
|
|
502
|
+
const baseIdx = tokens.indexOf(config.baseClass);
|
|
503
|
+
if (baseIdx !== -1) tokens.splice(baseIdx, 1);
|
|
504
|
+
|
|
505
|
+
if (isRouterLink) {
|
|
506
|
+
// <router-link custom> exposes vue-router internals via v-slot; rewriting it to
|
|
507
|
+
// <dt-button>/<dt-link> drops those slot semantics. Skip with a manual-review warning.
|
|
508
|
+
if (/(?:^|\s)custom(?=\s|=|\/|>|$)/.test(workingAttrs)) {
|
|
509
|
+
ctx.warnings.push(
|
|
510
|
+
`${ctx.filePath}: <router-link custom class="${classValue}"> — manual review required (custom + v-slot semantics don't transfer to <${config.targetTag}>).`,
|
|
511
|
+
);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const toAttr = extractAttr(workingAttrs, 'to');
|
|
515
|
+
if (toAttr) {
|
|
516
|
+
workingAttrs = (toAttr.before + ' ' + toAttr.after).trim();
|
|
517
|
+
const propName = toAttr.dynamic ? ':to' : 'to';
|
|
518
|
+
newAttrs.push(`${propName}=${quoteAttr(toAttr.value)}`);
|
|
519
|
+
} else {
|
|
520
|
+
ctx.warnings.push(
|
|
521
|
+
`${ctx.filePath}: <router-link class="${classValue}"> without a \`to\` attribute — skipped (likely already migrated or hand-authored).`,
|
|
522
|
+
);
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
// <a class="d-btn|d-link"> — lift href= and :href= the same way we lift to= /
|
|
527
|
+
// :to= on <router-link>. Dynamic bindings are 1:1 lifts; the consumer's
|
|
528
|
+
// expression evaluates identically on the resulting <dt-button> / <dt-link>.
|
|
529
|
+
const hrefAttr = extractAttr(workingAttrs, 'href');
|
|
530
|
+
if (hrefAttr) {
|
|
531
|
+
workingAttrs = (hrefAttr.before + ' ' + hrefAttr.after).trim();
|
|
532
|
+
const propName = hrefAttr.dynamic ? ':href' : 'href';
|
|
533
|
+
newAttrs.push(`${propName}=${quoteAttr(hrefAttr.value)}`);
|
|
534
|
+
}
|
|
535
|
+
// Anchors without href are technically invalid but we still rewrite so consumer sees the change
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
config.extractModifiers(tokens, newAttrs, ctx);
|
|
539
|
+
|
|
540
|
+
const remainingClass = joinClasses(tokens);
|
|
541
|
+
if (remainingClass) newAttrs.push(`class=${quoteAttr(remainingClass)}`);
|
|
542
|
+
|
|
543
|
+
const finalAttrs = normalizeAttrs(`${workingAttrs} ${newAttrs.join(' ')}`);
|
|
544
|
+
return `<${config.targetTag}${finalAttrs}${selfClosing ? ' />' : '>'}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function extractDtButtonModifiers (tokens, newAttrs) {
|
|
548
|
+
const size = extractSizeModifier(tokens);
|
|
549
|
+
if (size != null) newAttrs.push(`:size="${size}"`);
|
|
550
|
+
|
|
551
|
+
const importance = extractImportanceModifier(tokens);
|
|
552
|
+
if (importance != null) newAttrs.push(`importance="${importance}"`);
|
|
553
|
+
|
|
554
|
+
const kind = extractKindModifier(tokens);
|
|
555
|
+
if (kind != null) newAttrs.push(`kind="${kind}"`);
|
|
556
|
+
|
|
557
|
+
if (extractCircleModifier(tokens)) newAttrs.push('circle');
|
|
558
|
+
|
|
559
|
+
const { active, loading } = extractActiveLoadingModifiers(tokens);
|
|
560
|
+
if (active) newAttrs.push('active');
|
|
561
|
+
if (loading) newAttrs.push('loading');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function extractDtLinkModifiers (tokens, newAttrs, ctx) {
|
|
565
|
+
const inverted = extractInvertedLinkModifier(tokens);
|
|
566
|
+
let invertedTone = null;
|
|
567
|
+
if (inverted.found) {
|
|
568
|
+
invertedTone = inverted.tone;
|
|
569
|
+
ctx.notes.push({
|
|
570
|
+
kind: 'inverted-link',
|
|
571
|
+
message: `<dt-link> migrated from d-link--inverted; consider applying the v-dt-mode directive on a parent instead of the deprecated inverted styling.`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const tone = invertedTone ?? extractLinkToneModifier(tokens);
|
|
576
|
+
if (tone != null) newAttrs.push(`tone="${tone}"`);
|
|
577
|
+
|
|
578
|
+
if (extractNoUnderlineModifier(tokens)) newAttrs.push(':underline="false"');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Walk the content and warn on `<router-link custom>` wrappers around `<targetTag>`.
|
|
583
|
+
* Doesn't transform — purely informational, per Q2 resolution.
|
|
584
|
+
*/
|
|
585
|
+
function warnRouterLinkCustomWrappers (content, targetTag, ctx) {
|
|
586
|
+
// Two-step: quote-aware capture of <router-link …> attrs, then test for bare
|
|
587
|
+
// `custom` attribute. Avoids false-matching `attr="something custom"`.
|
|
588
|
+
// The body search is scoped to the matching </router-link> so a target tag
|
|
589
|
+
// that appears later in the file (outside this wrapper) doesn't false-fire.
|
|
590
|
+
const openRe = new RegExp(`<${tagNamePattern('router-link')}\\b(${QUOTE_AWARE_ATTRS}?)>`, 'g');
|
|
591
|
+
const targetRe = new RegExp(`<${tagNamePattern(targetTag)}\\b`);
|
|
592
|
+
let m;
|
|
593
|
+
while ((m = openRe.exec(content)) !== null) {
|
|
594
|
+
if (!/(?:^|\s)custom(?=\s|=|\/|>|$)/.test(m[1])) continue;
|
|
595
|
+
const close = findClosingTag(content, m.index + m[0].length, 'router-link');
|
|
596
|
+
if (!close) continue;
|
|
597
|
+
const body = content.slice(m.index + m[0].length, close.closeStart);
|
|
598
|
+
if (!targetRe.test(body)) continue;
|
|
599
|
+
ctx.warnings.push(
|
|
600
|
+
`${ctx.filePath}: <router-link custom> wrapping <${targetTag}> — manual review required (lift the to= onto <${targetTag}> directly).`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
// Stubs for V2 / V3 transforms — filled in subsequent slices
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
function transformLinkNav (content, ctx) {
|
|
610
|
+
const config = COMPONENT_CONFIGS['dt-link'];
|
|
611
|
+
let out = rewriteAnchorOrRouterLink(content, 'a', config, ctx, false);
|
|
612
|
+
out = rewriteAnchorOrRouterLink(out, 'router-link', config, ctx, true);
|
|
613
|
+
warnRouterLinkCustomWrappers(out, config.targetTag, ctx);
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
// Underline transform (d-td-* utility classes on <dt-link> → underline prop)
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
const D_TD_RECOGNIZED_TOKEN_RE = /^(?:h:)?d-td-(none|underline)$/;
|
|
622
|
+
const D_TD_ANY_TOKEN_RE = /(?:^|\s)([\w:]+:)?d-td-[\w-]+/;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Map rest+hover state to a prop emission decision.
|
|
626
|
+
* Returns { propValue, hoverDelta } where:
|
|
627
|
+
* - propValue is 'true' (default — strip classes), 'false' (set :underline="false"),
|
|
628
|
+
* or null (no change to the prop)
|
|
629
|
+
* - hoverDelta indicates whether the closest mapping changes hover behavior vs the original
|
|
630
|
+
*/
|
|
631
|
+
function mapDtdToUnderline (rest, hover) {
|
|
632
|
+
// Default DtLink behavior: rest=underline, hover=none
|
|
633
|
+
// d-link--no-underline (underline=false): rest=none, hover=underline
|
|
634
|
+
if (rest === 'underline' && hover === 'none') return { propValue: null, hoverDelta: false };
|
|
635
|
+
if (rest === 'none' && hover === 'underline') return { propValue: 'false', hoverDelta: false };
|
|
636
|
+
// "alone" / "both-same" cases — closest mapping with hover delta
|
|
637
|
+
if (rest === 'none' && hover === 'none') return { propValue: 'false', hoverDelta: true };
|
|
638
|
+
if (rest === 'underline' && hover === 'underline') return { propValue: null, hoverDelta: true };
|
|
639
|
+
return { propValue: null, hoverDelta: false };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function transformUnderline (content, ctx) {
|
|
643
|
+
// Match <dt-link …> / <DtLink …> opening tags including self-closing variants.
|
|
644
|
+
// Quote-aware so `>` in attribute values doesn't terminate the span early.
|
|
645
|
+
// Capture group 1 is the actual tag name so we preserve casing in output
|
|
646
|
+
// (matters because we don't rewrite the closing tag for non-self-closing forms).
|
|
647
|
+
const re = new RegExp(`<(${tagNamePattern('dt-link')})\\b(${QUOTE_AWARE_ATTRS}?)>`, 'g');
|
|
648
|
+
return content.replace(re, (fullTag, matchedTagName, rawAttrs) => {
|
|
649
|
+
let attrs = rawAttrs || '';
|
|
650
|
+
|
|
651
|
+
// Detect and strip a trailing `/` so we can re-emit the self-closing form
|
|
652
|
+
// (otherwise `<dt-link class="d-td-none" />` would lose its `/` and become
|
|
653
|
+
// a non-self-closing tag in the output).
|
|
654
|
+
const isSelfClosing = /\s*\/\s*$/.test(attrs);
|
|
655
|
+
if (isSelfClosing) attrs = attrs.replace(/\s*\/\s*$/, '');
|
|
656
|
+
const closer = isSelfClosing ? ' />' : '>';
|
|
657
|
+
|
|
658
|
+
// Skip if :underline is already set (idempotency)
|
|
659
|
+
if (/(^|\s)(:|v-bind:)?underline\s*=/.test(attrs)) return fullTag;
|
|
660
|
+
|
|
661
|
+
// Check dynamic :class first — even when a static class is also present,
|
|
662
|
+
// a `:class` containing d-td-* is a manual-review case (we can't merge
|
|
663
|
+
// expressions safely or invert hover behavior across runtime conditions).
|
|
664
|
+
const dynClassMatch = attrs.match(/(?:^|\s)(?::|v-bind:)class=("([^"]*)"|'([^']*)')/);
|
|
665
|
+
if (dynClassMatch) {
|
|
666
|
+
const expr = dynClassMatch[2] !== undefined ? dynClassMatch[2] : dynClassMatch[3];
|
|
667
|
+
if (/d-td-/.test(expr)) {
|
|
668
|
+
ctx.warnings.push(
|
|
669
|
+
`${ctx.filePath}: <dt-link :class="${expr}"> contains d-td-* in a dynamic binding — manual review required.`,
|
|
670
|
+
);
|
|
671
|
+
return fullTag;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Need a static class= with d-td-* tokens to do anything
|
|
676
|
+
const classMatch = attrs.match(/(?<![:\w-])class=("([^"]*)"|'([^']*)')/);
|
|
677
|
+
if (!classMatch) return fullTag;
|
|
678
|
+
|
|
679
|
+
const classValue = classMatch[2] !== undefined ? classMatch[2] : classMatch[3];
|
|
680
|
+
if (!D_TD_ANY_TOKEN_RE.test(' ' + classValue)) return fullTag;
|
|
681
|
+
|
|
682
|
+
const tokens = classValue.split(/\s+/).filter(Boolean);
|
|
683
|
+
|
|
684
|
+
// Detect unsupported variants (responsive prefix like sm:d-td-*, focus f:d-td-*, etc.)
|
|
685
|
+
const unsupported = tokens.filter(t => /^(?:[\w-]+:)?d-td-/.test(t) && !D_TD_RECOGNIZED_TOKEN_RE.test(t));
|
|
686
|
+
if (unsupported.length > 0) {
|
|
687
|
+
ctx.warnings.push(
|
|
688
|
+
`${ctx.filePath}: <dt-link class="${classValue}"> has responsive or unsupported d-td-* variant(s) (${unsupported.join(', ')}) — skipped.`,
|
|
689
|
+
);
|
|
690
|
+
return fullTag;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Compute effective rest/hover state.
|
|
694
|
+
// Bare `d-td-*` is `text-decoration: <value> !important` and applies in BOTH rest and hover
|
|
695
|
+
// because there's no :hover qualifier on the rule. An explicit `h:d-td-*` overrides hover only.
|
|
696
|
+
let restOverride = null;
|
|
697
|
+
let hoverOverride = null;
|
|
698
|
+
const remaining = [];
|
|
699
|
+
for (const token of tokens) {
|
|
700
|
+
const m = token.match(D_TD_RECOGNIZED_TOKEN_RE);
|
|
701
|
+
if (!m) {
|
|
702
|
+
remaining.push(token);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const isHover = token.startsWith('h:');
|
|
706
|
+
const value = m[1]; // 'none' or 'underline'
|
|
707
|
+
if (isHover) hoverOverride = value;
|
|
708
|
+
else restOverride = value;
|
|
709
|
+
}
|
|
710
|
+
const rest = restOverride ?? 'underline'; // DtLink default rest
|
|
711
|
+
// Hover precedence: explicit h:d-td-* > bare d-td-* (!important applies on hover too) > default
|
|
712
|
+
const hover = hoverOverride ?? restOverride ?? 'none';
|
|
713
|
+
|
|
714
|
+
const { propValue, hoverDelta } = mapDtdToUnderline(rest, hover);
|
|
715
|
+
|
|
716
|
+
// Build new class attribute
|
|
717
|
+
const newClassValue = remaining.join(' ');
|
|
718
|
+
let newAttrs = attrs.replace(classMatch[0], '').trim();
|
|
719
|
+
if (newClassValue) newAttrs += ` class=${quoteAttr(newClassValue)}`;
|
|
720
|
+
if (propValue === 'false') newAttrs += ' :underline="false"';
|
|
721
|
+
|
|
722
|
+
if (hoverDelta) {
|
|
723
|
+
ctx.notes.push({
|
|
724
|
+
kind: 'underline-hover-delta',
|
|
725
|
+
message: '<dt-link>: hover behavior may differ from the original d-td-* classes; review if hover styling matters.',
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return `<${matchedTagName}${newAttrs ? ' ' + newAttrs : ''}${closer}`;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Top-level transform entry point
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Mask HTML comments so the transformers don't match tags written as documentation
|
|
739
|
+
* inside `<!-- … -->` blocks (e.g. fixture/example files).
|
|
740
|
+
*/
|
|
741
|
+
function maskInertContent (content, filePath = '') {
|
|
742
|
+
const isMarkdown = filePath.endsWith('.md');
|
|
743
|
+
const innerRe = isMarkdown
|
|
744
|
+
? /<!--[\s\S]*?-->|```[\s\S]*?```|`[^`\n]*`/g
|
|
745
|
+
: /<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>/g;
|
|
746
|
+
const segments = [];
|
|
747
|
+
const masked = content.replace(innerRe, (match) => {
|
|
748
|
+
const placeholder = ` DT_MIGRATE_INERT_${segments.length} `;
|
|
749
|
+
segments.push(match);
|
|
750
|
+
return placeholder;
|
|
751
|
+
});
|
|
752
|
+
return { masked, segments };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function unmaskInertContent (masked, segments) {
|
|
756
|
+
return masked.replace(/ DT_MIGRATE_INERT_(\d+) /g, (_, idx) => segments[Number(idx)]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Transform a file's contents. Returns { transformed, warnings, notes }.
|
|
762
|
+
*/
|
|
763
|
+
export function transformContent (content, opts = {}) {
|
|
764
|
+
const ctx = {
|
|
765
|
+
filePath: opts.filePath || '<input>',
|
|
766
|
+
warnings: [],
|
|
767
|
+
notes: [],
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// Fast path: most consumer files contain none of the relevant tokens. Skip
|
|
771
|
+
// masking and three regex sweeps when the cheap precheck rules them out.
|
|
772
|
+
if (!FAST_PATH_RE.test(content)) {
|
|
773
|
+
return { transformed: content, warnings: ctx.warnings, notes: ctx.notes };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const enabled = opts.only && opts.only.length > 0 ? new Set(opts.only) : new Set(ALL_TRANSFORMS);
|
|
777
|
+
const { masked, segments } = maskInertContent(content, ctx.filePath);
|
|
778
|
+
let out = masked;
|
|
779
|
+
if (enabled.has(TRANSFORM.BUTTON_NAV)) out = transformButtonNav(out, ctx);
|
|
780
|
+
if (enabled.has(TRANSFORM.LINK_NAV)) out = transformLinkNav(out, ctx);
|
|
781
|
+
if (enabled.has(TRANSFORM.UNDERLINE)) out = transformUnderline(out, ctx);
|
|
782
|
+
|
|
783
|
+
return { transformed: unmaskInertContent(out, segments), warnings: ctx.warnings, notes: ctx.notes };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// File walker
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Match an ignore token against a path. Single-segment tokens (e.g. `node_modules`,
|
|
792
|
+
* `dist`) match by exact directory segment so `src/distance/` is not excluded by `dist`.
|
|
793
|
+
* Multi-segment tokens (e.g. `.vuepress/public`) match as a contiguous segment run.
|
|
794
|
+
*/
|
|
795
|
+
function isIgnoredPath (fullPath, ignore) {
|
|
796
|
+
const segments = fullPath.split(path.sep);
|
|
797
|
+
return ignore.some(ig => {
|
|
798
|
+
if (ig.includes('/')) {
|
|
799
|
+
const parts = ig.split('/');
|
|
800
|
+
for (let i = 0; i + parts.length <= segments.length; i++) {
|
|
801
|
+
if (parts.every((p, j) => segments[i + j] === p)) return true;
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
return segments.includes(ig);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function findFiles (dir, extensions, ignore = []) {
|
|
810
|
+
const results = [];
|
|
811
|
+
async function walk (currentDir) {
|
|
812
|
+
let entries;
|
|
813
|
+
try {
|
|
814
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
815
|
+
} catch {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
for (const entry of entries) {
|
|
819
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
820
|
+
if (isIgnoredPath(fullPath, ignore)) continue;
|
|
821
|
+
if (entry.isDirectory()) {
|
|
822
|
+
await walk(fullPath);
|
|
823
|
+
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
824
|
+
results.push(fullPath);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
await walk(dir);
|
|
829
|
+
return results;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
// CLI plumbing
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
function printHelp () {
|
|
837
|
+
console.log(`
|
|
838
|
+
Usage: npx dialtone-migrate-link-rendering [options]
|
|
839
|
+
|
|
840
|
+
Migrates legacy anchor / router-link patterns to <dt-button> and <dt-link>, plus
|
|
841
|
+
DtLink \`d-td-*\` utility classes to the \`underline\` prop.
|
|
842
|
+
|
|
843
|
+
Covers:
|
|
844
|
+
DLT-3033 <a class="d-btn"> -> <dt-button href="…">
|
|
845
|
+
<router-link class="d-btn" :to> -> <dt-button :to="…">
|
|
846
|
+
d-btn--{xs,sm,lg,xl} -> :size="{100,200,400,500}"
|
|
847
|
+
d-btn--outlined -> importance="outlined"
|
|
848
|
+
d-btn--{muted,critical,positive,…} -> kind="…" (with renames)
|
|
849
|
+
d-btn--{circle,active,loading} -> bare boolean attrs
|
|
850
|
+
|
|
851
|
+
DLT-3034 <a class="d-link"> -> <dt-link href="…">
|
|
852
|
+
<router-link class="d-link" :to> -> <dt-link :to="…">
|
|
853
|
+
d-link--{tone} -> tone="…" (with renames
|
|
854
|
+
danger->critical, success->positive)
|
|
855
|
+
d-link--no-underline -> :underline="false"
|
|
856
|
+
d-link--inverted* -> per-file note nudging toward v-dt-mode
|
|
857
|
+
|
|
858
|
+
DLT-3035 <dt-link class="d-td-…"> -> closest :underline value
|
|
859
|
+
(per-file note when hover delta exists)
|
|
860
|
+
|
|
861
|
+
Vendor classes (d-btn--brand, etc.) and BEM internals (d-btn__icon, etc.) are
|
|
862
|
+
preserved on the resulting tag's class attribute, not warned.
|
|
863
|
+
|
|
864
|
+
Vue files only by default. Use --include-markdown to also walk .md files.
|
|
865
|
+
|
|
866
|
+
Options:
|
|
867
|
+
--cwd <path> Working directory (default: cwd)
|
|
868
|
+
--dry-run Show changes without applying them
|
|
869
|
+
--yes Apply all changes without prompting
|
|
870
|
+
--help Show help
|
|
871
|
+
--only=<list> Run only the named transforms; CSV of:
|
|
872
|
+
button-nav, link-nav, underline
|
|
873
|
+
--include-markdown Also walk .md files
|
|
874
|
+
|
|
875
|
+
Examples:
|
|
876
|
+
npx dialtone-migrate-link-rendering
|
|
877
|
+
npx dialtone-migrate-link-rendering --dry-run
|
|
878
|
+
npx dialtone-migrate-link-rendering --cwd ./src
|
|
879
|
+
npx dialtone-migrate-link-rendering --only=button-nav,link-nav
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function parseArgs (args) {
|
|
884
|
+
const cwdIndex = args.indexOf('--cwd');
|
|
885
|
+
const onlyArg = args.find(a => a.startsWith('--only='));
|
|
886
|
+
return {
|
|
887
|
+
help: args.includes('--help'),
|
|
888
|
+
dryRun: args.includes('--dry-run'),
|
|
889
|
+
autoYes: args.includes('--yes'),
|
|
890
|
+
includeMarkdown: args.includes('--include-markdown'),
|
|
891
|
+
cwd: cwdIndex !== -1 && args[cwdIndex + 1]
|
|
892
|
+
? path.resolve(args[cwdIndex + 1])
|
|
893
|
+
: process.cwd(),
|
|
894
|
+
only: onlyArg ? onlyArg.slice('--only='.length).split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function prompt (question) {
|
|
899
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
900
|
+
return new Promise(resolve => {
|
|
901
|
+
rl.question(question, answer => {
|
|
902
|
+
rl.close();
|
|
903
|
+
resolve(answer.trim().toLowerCase());
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function scanFiles (cwd, includeMarkdown, only) {
|
|
909
|
+
const extensions = ['.vue'];
|
|
910
|
+
if (includeMarkdown) extensions.push('.md');
|
|
911
|
+
const ignore = ['node_modules', 'dist', '.git', '.vuepress/public', '.vuepress/.temp', '.vuepress/.cache'];
|
|
912
|
+
const files = await findFiles(cwd, extensions, ignore);
|
|
913
|
+
|
|
914
|
+
const changes = [];
|
|
915
|
+
const allWarnings = [];
|
|
916
|
+
const allNotes = []; // grouped per file
|
|
917
|
+
|
|
918
|
+
for (const file of files) {
|
|
919
|
+
const content = await fs.readFile(file, 'utf8');
|
|
920
|
+
const { transformed, warnings, notes } = transformContent(content, {
|
|
921
|
+
only,
|
|
922
|
+
filePath: path.relative(cwd, file),
|
|
923
|
+
});
|
|
924
|
+
if (transformed !== content) {
|
|
925
|
+
changes.push({ file, content, transformed });
|
|
926
|
+
}
|
|
927
|
+
if (warnings.length) allWarnings.push(...warnings);
|
|
928
|
+
if (notes.length) {
|
|
929
|
+
allNotes.push({
|
|
930
|
+
file: path.relative(cwd, file),
|
|
931
|
+
notes,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return { changes, allWarnings, allNotes };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function applyChanges (changes, autoYes) {
|
|
940
|
+
if (!autoYes) {
|
|
941
|
+
const answer = await prompt('\nApply changes? (y/N) ');
|
|
942
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
943
|
+
console.log('Cancelled.');
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
for (const { file, transformed } of changes) {
|
|
948
|
+
await fs.writeFile(file, transformed, 'utf8');
|
|
949
|
+
}
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function printWarnings (warnings) {
|
|
954
|
+
if (!warnings.length) return;
|
|
955
|
+
console.log('\nWarnings — manual action required:\n');
|
|
956
|
+
for (const w of warnings) console.log(` ${w}`);
|
|
957
|
+
console.log();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function printNotes (notesByFile) {
|
|
961
|
+
if (!notesByFile.length) return;
|
|
962
|
+
console.log('\nNotes — informational:\n');
|
|
963
|
+
for (const { file, notes } of notesByFile) {
|
|
964
|
+
// Group same-message notes per file into a single line with a count
|
|
965
|
+
const counts = new Map();
|
|
966
|
+
for (const n of notes) {
|
|
967
|
+
counts.set(n.message, (counts.get(n.message) || 0) + 1);
|
|
968
|
+
}
|
|
969
|
+
for (const [message, count] of counts) {
|
|
970
|
+
const prefix = count > 1 ? `${count}× ` : '';
|
|
971
|
+
console.log(` ${file}: ${prefix}${message}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
console.log();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function printChangeSummary (changes, cwd) {
|
|
978
|
+
console.log(`\nFound changes in ${changes.length} file(s):\n`);
|
|
979
|
+
for (const { file } of changes) {
|
|
980
|
+
console.log(` ${path.relative(cwd, file)}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function main () {
|
|
985
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
986
|
+
if (opts.help) {
|
|
987
|
+
printHelp();
|
|
988
|
+
process.exit(0);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Validate --only list
|
|
992
|
+
if (opts.only.length) {
|
|
993
|
+
const invalid = opts.only.filter(t => !ALL_TRANSFORMS.includes(t));
|
|
994
|
+
if (invalid.length) {
|
|
995
|
+
console.error(`Unknown transform(s) in --only: ${invalid.join(', ')}`);
|
|
996
|
+
console.error(`Valid transforms: ${ALL_TRANSFORMS.join(', ')}`);
|
|
997
|
+
process.exit(2);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
console.log(`\nScanning ${opts.cwd} for legacy link/button patterns...`);
|
|
1002
|
+
if (opts.includeMarkdown) console.log('(including .md files)');
|
|
1003
|
+
if (opts.only.length) console.log(`(only: ${opts.only.join(', ')})`);
|
|
1004
|
+
|
|
1005
|
+
const { changes, allWarnings, allNotes } = await scanFiles(opts.cwd, opts.includeMarkdown, opts.only);
|
|
1006
|
+
|
|
1007
|
+
printWarnings(allWarnings);
|
|
1008
|
+
printNotes(allNotes);
|
|
1009
|
+
|
|
1010
|
+
if (changes.length === 0) {
|
|
1011
|
+
console.log(allWarnings.length || allNotes.length
|
|
1012
|
+
? 'No automated code changes needed. See manual action items / notes above.'
|
|
1013
|
+
: 'No matching usage found. Nothing to migrate.');
|
|
1014
|
+
process.exit(0);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
printChangeSummary(changes, opts.cwd);
|
|
1018
|
+
|
|
1019
|
+
if (opts.dryRun) {
|
|
1020
|
+
console.log(`\n--dry-run: No files were modified.`);
|
|
1021
|
+
process.exit(0);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const applied = await applyChanges(changes, opts.autoYes);
|
|
1025
|
+
if (applied) console.log(`\nMigrated ${changes.length} file(s).\n`);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const isDirectRun = (() => {
|
|
1029
|
+
try {
|
|
1030
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1031
|
+
} catch {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
})();
|
|
1035
|
+
|
|
1036
|
+
if (isDirectRun) {
|
|
1037
|
+
main().catch(err => {
|
|
1038
|
+
console.error(err);
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
});
|
|
1041
|
+
}
|