@dialpad/dialtone-css 8.81.0-next.14 → 8.81.0-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/lib/build/js/dialtone_migrate/index.mjs +54 -7
  2. package/lib/build/js/dialtone_migrate/test.mjs +148 -0
  3. package/lib/dist/js/dialtone_migrate/index.mjs +54 -7
  4. package/lib/dist/js/dialtone_migrate/test.mjs +148 -0
  5. package/lib/dist/tokens/tokens-raincloud-dark.css +1050 -0
  6. package/lib/dist/tokens-docs.json +1 -1
  7. package/package.json +2 -2
  8. package/lib/dist/tokens/tokens-103-dark.css +0 -1050
  9. package/lib/dist/tokens/tokens-ceruleo-dark.css +0 -1050
  10. package/lib/dist/tokens/tokens-ceruleo-light.css +0 -1050
  11. /package/lib/dist/tokens/{tokens-131-dark.css → tokens-alpine-dark.css} +0 -0
  12. /package/lib/dist/tokens/{tokens-131-light.css → tokens-alpine-light.css} +0 -0
  13. /package/lib/dist/tokens/{tokens-136-dark.css → tokens-arctic-dark.css} +0 -0
  14. /package/lib/dist/tokens/{tokens-136-light.css → tokens-arctic-light.css} +0 -0
  15. /package/lib/dist/tokens/{tokens-118-dark.css → tokens-aurora-dark.css} +0 -0
  16. /package/lib/dist/tokens/{tokens-118-light.css → tokens-aurora-light.css} +0 -0
  17. /package/lib/dist/tokens/{tokens-112-dark.css → tokens-autumn-dark.css} +0 -0
  18. /package/lib/dist/tokens/{tokens-112-light.css → tokens-autumn-light.css} +0 -0
  19. /package/lib/dist/tokens/{tokens-110-dark.css → tokens-blue-hour-dark.css} +0 -0
  20. /package/lib/dist/tokens/{tokens-110-light.css → tokens-blue-hour-light.css} +0 -0
  21. /package/lib/dist/tokens/{tokens-109-dark.css → tokens-brick-dark.css} +0 -0
  22. /package/lib/dist/tokens/{tokens-109-light.css → tokens-brick-light.css} +0 -0
  23. /package/lib/dist/tokens/{tokens-117-dark.css → tokens-cactus-bloom-dark.css} +0 -0
  24. /package/lib/dist/tokens/{tokens-117-light.css → tokens-cactus-bloom-light.css} +0 -0
  25. /package/lib/dist/tokens/{tokens-115-dark.css → tokens-cayenne-dark.css} +0 -0
  26. /package/lib/dist/tokens/{tokens-115-light.css → tokens-cayenne-light.css} +0 -0
  27. /package/lib/dist/tokens/{tokens-106-dark.css → tokens-cedar-grove-dark.css} +0 -0
  28. /package/lib/dist/tokens/{tokens-106-light.css → tokens-cedar-grove-light.css} +0 -0
  29. /package/lib/dist/tokens/{tokens-113-dark.css → tokens-cobalt-dark.css} +0 -0
  30. /package/lib/dist/tokens/{tokens-113-light.css → tokens-cobalt-light.css} +0 -0
  31. /package/lib/dist/tokens/{tokens-116-dark.css → tokens-copper-dark.css} +0 -0
  32. /package/lib/dist/tokens/{tokens-116-light.css → tokens-copper-light.css} +0 -0
  33. /package/lib/dist/tokens/{tokens-127-dark.css → tokens-coral-reef-dark.css} +0 -0
  34. /package/lib/dist/tokens/{tokens-127-light.css → tokens-coral-reef-light.css} +0 -0
  35. /package/lib/dist/tokens/{tokens-121-dark.css → tokens-dragonfruit-dark.css} +0 -0
  36. /package/lib/dist/tokens/{tokens-121-light.css → tokens-dragonfruit-light.css} +0 -0
  37. /package/lib/dist/tokens/{tokens-102-dark.css → tokens-eucalyptus-dark.css} +0 -0
  38. /package/lib/dist/tokens/{tokens-103-light.css → tokens-eucalyptus-light.css} +0 -0
  39. /package/lib/dist/tokens/{tokens-105-dark.css → tokens-fjord-dark.css} +0 -0
  40. /package/lib/dist/tokens/{tokens-105-light.css → tokens-fjord-light.css} +0 -0
  41. /package/lib/dist/tokens/{tokens-124-dark.css → tokens-inkberry-dark.css} +0 -0
  42. /package/lib/dist/tokens/{tokens-124-light.css → tokens-inkberry-light.css} +0 -0
  43. /package/lib/dist/tokens/{tokens-114-dark.css → tokens-kiln-dark.css} +0 -0
  44. /package/lib/dist/tokens/{tokens-114-light.css → tokens-kiln-light.css} +0 -0
  45. /package/lib/dist/tokens/{tokens-134-dark.css → tokens-lavender-dark.css} +0 -0
  46. /package/lib/dist/tokens/{tokens-134-light.css → tokens-lavender-light.css} +0 -0
  47. /package/lib/dist/tokens/{tokens-133-dark.css → tokens-marigold-dark.css} +0 -0
  48. /package/lib/dist/tokens/{tokens-133-light.css → tokens-marigold-light.css} +0 -0
  49. /package/lib/dist/tokens/{tokens-137-dark.css → tokens-mulberry-dark.css} +0 -0
  50. /package/lib/dist/tokens/{tokens-137-light.css → tokens-mulberry-light.css} +0 -0
  51. /package/lib/dist/tokens/{tokens-104-dark.css → tokens-mushroom-dark.css} +0 -0
  52. /package/lib/dist/tokens/{tokens-104-light.css → tokens-mushroom-light.css} +0 -0
  53. /package/lib/dist/tokens/{tokens-130-dark.css → tokens-nightshade-dark.css} +0 -0
  54. /package/lib/dist/tokens/{tokens-130-light.css → tokens-nightshade-light.css} +0 -0
  55. /package/lib/dist/tokens/{tokens-101-dark.css → tokens-paprika-dark.css} +0 -0
  56. /package/lib/dist/tokens/{tokens-101-light.css → tokens-paprika-light.css} +0 -0
  57. /package/lib/dist/tokens/{tokens-107-dark.css → tokens-peach-blossom-dark.css} +0 -0
  58. /package/lib/dist/tokens/{tokens-107-light.css → tokens-peach-blossom-light.css} +0 -0
  59. /package/lib/dist/tokens/{tokens-108-dark.css → tokens-poppy-field-dark.css} +0 -0
  60. /package/lib/dist/tokens/{tokens-108-light.css → tokens-poppy-field-light.css} +0 -0
  61. /package/lib/dist/tokens/{tokens-102-light.css → tokens-raincloud-light.css} +0 -0
  62. /package/lib/dist/tokens/{tokens-123-dark.css → tokens-rhubarb-dark.css} +0 -0
  63. /package/lib/dist/tokens/{tokens-123-light.css → tokens-rhubarb-light.css} +0 -0
  64. /package/lib/dist/tokens/{tokens-119-dark.css → tokens-rust-harbor-dark.css} +0 -0
  65. /package/lib/dist/tokens/{tokens-119-light.css → tokens-rust-harbor-light.css} +0 -0
  66. /package/lib/dist/tokens/{tokens-128-dark.css → tokens-sea-glow-dark.css} +0 -0
  67. /package/lib/dist/tokens/{tokens-128-light.css → tokens-sea-glow-light.css} +0 -0
  68. /package/lib/dist/tokens/{tokens-122-dark.css → tokens-seashell-dark.css} +0 -0
  69. /package/lib/dist/tokens/{tokens-122-light.css → tokens-seashell-light.css} +0 -0
  70. /package/lib/dist/tokens/{tokens-129-dark.css → tokens-solstice-dark.css} +0 -0
  71. /package/lib/dist/tokens/{tokens-129-light.css → tokens-solstice-light.css} +0 -0
  72. /package/lib/dist/tokens/{tokens-120-dark.css → tokens-storm-dark.css} +0 -0
  73. /package/lib/dist/tokens/{tokens-120-light.css → tokens-storm-light.css} +0 -0
  74. /package/lib/dist/tokens/{tokens-135-dark.css → tokens-tropical-night-dark.css} +0 -0
  75. /package/lib/dist/tokens/{tokens-135-light.css → tokens-tropical-night-light.css} +0 -0
  76. /package/lib/dist/tokens/{tokens-111-dark.css → tokens-wildflower-dark.css} +0 -0
  77. /package/lib/dist/tokens/{tokens-111-light.css → tokens-wildflower-light.css} +0 -0
  78. /package/lib/dist/tokens/{tokens-132-dark.css → tokens-wineberry-dark.css} +0 -0
  79. /package/lib/dist/tokens/{tokens-132-light.css → tokens-wineberry-light.css} +0 -0
  80. /package/lib/dist/tokens/{tokens-126-dark.css → tokens-winter-gold-dark.css} +0 -0
  81. /package/lib/dist/tokens/{tokens-126-light.css → tokens-winter-gold-light.css} +0 -0
  82. /package/lib/dist/tokens/{tokens-125-dark.css → tokens-woodland-dark.css} +0 -0
  83. /package/lib/dist/tokens/{tokens-125-light.css → tokens-woodland-light.css} +0 -0
@@ -56,11 +56,29 @@ const __dirname = path.dirname(__filename);
56
56
  * @property {string} [scriptDir] - Directory name for standalone scripts
57
57
  * @property {boolean} [supportsPackageArg] - Forward --package to this standalone script
58
58
  * @property {string[]} [extraArgs] - Extra CLI args to forward
59
- * @property {RegExp[]} detectPatterns - Patterns that indicate migration is still needed
59
+ * @property {RegExp[]} detectPatterns - Patterns that unambiguously indicate migration is
60
+ * still needed. Always evaluated, regardless of skipIfPatterns.
61
+ * @property {RegExp[]} [ambiguousDetectPatterns] - Patterns that overlap with valid
62
+ * post-migration values. Only evaluated when skipIfPatterns does not match, so a file
63
+ * already containing a new-scale-only marker isn't flagged on these alone — but an
64
+ * unambiguous detectPatterns match elsewhere in the same (partially migrated) file
65
+ * still counts.
66
+ * @property {RegExp[]} [skipIfPatterns] - New-scale-only markers that gate
67
+ * ambiguousDetectPatterns — their presence means the file has already been migrated.
60
68
  * @property {string[]} fileExtensions - File extensions to scan during health check
61
69
  */
62
70
 
63
71
  /** @type {Migration[]} */
72
+ // Consumes a Dialtone tag's attribute soup, treating quoted attribute values as opaque so a
73
+ // literal > inside one (e.g. :disabled="count > 0") doesn't truncate the match early.
74
+ const DT_TAG_ATTRS = '(?:[^>"\']|"[^"]*"|\'[^\']*\')*';
75
+
76
+ // Allowlist (not denylist) of what may immediately precede "gap": whitespace (bare gap=),
77
+ // whitespace+":" (shorthand :gap=), or whitespace+"v-bind:" (long form). This rejects
78
+ // data-gap, @gap, v-on:gap, v-model:gap, v-slot:gap, and any other directive namespace,
79
+ // without having to enumerate every prefix that isn't the real gap prop/attribute.
80
+ const GAP_ATTR_LEAD = '(?:(?<=\\s)|(?<=\\s:)|(?<=\\sv-bind:))';
81
+
64
82
  const MIGRATIONS = [
65
83
  // ── Required (breaking) ────────────────────────────────────────────
66
84
  {
@@ -162,9 +180,13 @@ const MIGRATIONS = [
162
180
  type: 'standalone',
163
181
  scriptDir: 'dialtone_migrate_props',
164
182
  detectPatterns: [
165
- /(?:show|hide-close|hide-icon|label-visible|selected-values)(?:=|[\s>])/,
166
- /<(?:dt-(?:banner|notice|toast|modal)|Dt(?:Banner|Notice|Toast|Modal))\b[^>]*\btitle(?:=|[\s>])/,
167
- /<(?:dt-[\w-]+|Dt\w+)\b[^>]*@(?:input|change)(?:=|\.)/,
183
+ // (?<!-) blocks a hyphen immediately before the prop name — \b alone still matches
184
+ // there (- is a non-word char), which would treat native v-show as the show prop.
185
+ new RegExp(`<(?:dt-[\\w-]+|Dt\\w+)\\b${DT_TAG_ATTRS}\\b(?<!-)(?:show|hide-close|hide-icon|label-visible|selected-values)(?:=|[\\s>])`),
186
+ // (?<!-) blocks a hyphen immediately before "title" so data-title (and any other
187
+ // custom -title attribute) isn't mistaken for the title prop.
188
+ new RegExp(`<(?:dt-(?:banner|notice|toast|modal)|Dt(?:Banner|Notice|Toast|Modal))\\b${DT_TAG_ATTRS}\\b(?<!-)title(?:=|[\\s>])`),
189
+ new RegExp(`<(?:dt-[\\w-]+|Dt\\w+)\\b${DT_TAG_ATTRS}@(?:input|change)(?:=|\\.)`),
168
190
  /kind="(?:danger|error)"/,
169
191
  /validation-state="(?:error|success)"/,
170
192
  ],
@@ -242,9 +264,27 @@ const MIGRATIONS = [
242
264
  category: 'opt-in',
243
265
  type: 'config',
244
266
  configName: 'stack-gap-to-spacing',
267
+ // Old-only stops (never valid post-migration values) — always flagged as pending.
245
268
  detectPatterns: [
246
- /gap="(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)"/,
247
- /d-stack--gap-(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)/,
269
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:350|450|500|550|625|650|700)"`),
270
+ /d-stack--gap-(?:350|450|500|550|625|650|700)/,
271
+ ],
272
+ // Old and new gap scales share some numeric stops (e.g. "100"), so a bare match on
273
+ // these doesn't prove pending work. Only counted when the file has no new-scale-only
274
+ // marker (skipIfPatterns) — an unambiguous detectPatterns match elsewhere in the same
275
+ // (partially migrated) file is still flagged regardless.
276
+ ambiguousDetectPatterns: [
277
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:50|100|200|300|400|525|600)"`),
278
+ /d-stack--gap-(?:50|100|200|300|400|525|600)/,
279
+ ],
280
+ // Mirrors dialtone_migration_helper/configs/stack-gap-to-spacing.mjs's isAlreadyMigrated
281
+ // check: new-scale-only stops indicate the file was already migrated.
282
+ skipIfPatterns: [
283
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:25|75|150|250|525)"`),
284
+ new RegExp(`${GAP_ATTR_LEAD}gap='(?:25|75|150|250|525)'`),
285
+ new RegExp(`${GAP_ATTR_LEAD}gap="'(?:25|75|150|250|525)'"`),
286
+ /d-stack--gap-(?:25|75|150|250|525)/,
287
+ /d-description-list--gap-(?:25|75|150|250|525)/,
248
288
  ],
249
289
  fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.md'],
250
290
  },
@@ -546,7 +586,14 @@ async function healthCheck (cwd) {
546
586
 
547
587
  for (const [file, content] of fileContents) {
548
588
  if (!migration.fileExtensions.some(ext => file.endsWith(ext))) continue;
549
- for (const pattern of migration.detectPatterns) {
589
+
590
+ let patterns = migration.detectPatterns;
591
+ const alreadyMigrated = migration.skipIfPatterns?.some(pattern => pattern.test(content));
592
+ if (!alreadyMigrated && migration.ambiguousDetectPatterns) {
593
+ patterns = [...patterns, ...migration.ambiguousDetectPatterns];
594
+ }
595
+
596
+ for (const pattern of patterns) {
550
597
  // Reset lastIndex for global patterns
551
598
  const re = new RegExp(pattern.source, pattern.flags.replace('g', ''));
552
599
  if (re.test(content)) {
@@ -86,3 +86,151 @@ describe('--package validation', () => {
86
86
  assert.equal(status, 0);
87
87
  });
88
88
  });
89
+
90
+ describe('--health-check', () => {
91
+ function runHealthCheck (files) {
92
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dlt-health-'));
93
+ try {
94
+ for (const [name, contents] of Object.entries(files)) {
95
+ fs.writeFileSync(path.join(tmp, name), contents);
96
+ }
97
+ return execFileSync(process.execPath, [cli, '--health-check', '--cwd', tmp],
98
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
99
+ } finally {
100
+ fs.rmSync(tmp, { recursive: true, force: true });
101
+ }
102
+ }
103
+
104
+ it('does not flag native v-show on non-Dialtone elements as component-props', () => {
105
+ const output = runHealthCheck({
106
+ 'App.vue': '<template><div v-show="isReady">Ready</div></template>',
107
+ });
108
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
109
+ });
110
+
111
+ it('does not flag native v-show on a Dialtone element as component-props', () => {
112
+ const output = runHealthCheck({
113
+ 'App.vue': '<template><dt-modal v-show="isOpen"></dt-modal></template>',
114
+ });
115
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
116
+ });
117
+
118
+ it('does not flag v-show on dt-button as component-props', () => {
119
+ const output = runHealthCheck({
120
+ 'App.vue': '<template><dt-button v-show="visible">Go</dt-button></template>',
121
+ });
122
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
123
+ });
124
+
125
+ it('does not flag data-title or v-title on a Dialtone element as component-props', () => {
126
+ const output = runHealthCheck({
127
+ 'App.vue': '<template><dt-modal data-title="tooltip" v-title="x"></dt-modal></template>',
128
+ });
129
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
130
+ });
131
+
132
+ it('still flags show= on a Dialtone component as component-props', () => {
133
+ const output = runHealthCheck({
134
+ 'App.vue': '<template><dt-modal show="isOpen"></dt-modal></template>',
135
+ });
136
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
137
+ });
138
+
139
+ it('still flags title= on a Dialtone component as component-props', () => {
140
+ const output = runHealthCheck({
141
+ 'App.vue': '<template><dt-modal title="isOpen"></dt-modal></template>',
142
+ });
143
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
144
+ });
145
+
146
+ it('still flags :show= on a Dialtone component when a quoted attribute earlier in the tag contains a literal >', () => {
147
+ const output = runHealthCheck({
148
+ 'App.vue': '<template><dt-modal :disabled="count > 0" :show="isOpen"></dt-modal></template>',
149
+ });
150
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
151
+ });
152
+
153
+ it('does not flag an already-migrated file for stack-gap-to-spacing', () => {
154
+ const output = runHealthCheck({
155
+ 'App.vue': '<template><dt-stack gap="100"></dt-stack><dt-stack gap="25"></dt-stack></template>',
156
+ });
157
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
158
+ });
159
+
160
+ it('still flags an unmigrated file for stack-gap-to-spacing', () => {
161
+ const output = runHealthCheck({
162
+ 'App.vue': '<template><dt-stack gap="625"></dt-stack></template>',
163
+ });
164
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
165
+ });
166
+
167
+ it('does not flag an unrelated data-gap attribute for stack-gap-to-spacing', () => {
168
+ const output = runHealthCheck({
169
+ 'App.vue': '<template><div data-gap="625"></div></template>',
170
+ });
171
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
172
+ });
173
+
174
+ it('does not let an unrelated data-gap new-scale value suppress a real ambiguous pending gap', () => {
175
+ const output = runHealthCheck({
176
+ 'App.vue': '<template><div data-gap="25"></div><dt-stack gap="100"></dt-stack></template>',
177
+ });
178
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
179
+ });
180
+
181
+ it('does not flag an @gap event-listener value as a pending gap prop', () => {
182
+ const output = runHealthCheck({
183
+ 'App.vue': '<template><dt-stack @gap="625"></dt-stack></template>',
184
+ });
185
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
186
+ });
187
+
188
+ it('does not let an @gap or v-on:gap event-listener value suppress a real ambiguous pending gap', () => {
189
+ const output = runHealthCheck({
190
+ 'App.vue': '<template><dt-stack @gap="25"></dt-stack><dt-stack v-on:gap="75"></dt-stack><dt-stack gap="100"></dt-stack></template>',
191
+ });
192
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
193
+ });
194
+
195
+ it('still flags a bound :gap= and v-bind:gap= as pending', () => {
196
+ const output = runHealthCheck({
197
+ 'App.vue': '<template><dt-stack :gap="625"></dt-stack><dt-stack v-bind:gap="625"></dt-stack></template>',
198
+ });
199
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
200
+ });
201
+
202
+ it('does not flag v-model:gap and still detects a real ambiguous gap alongside it', () => {
203
+ const output = runHealthCheck({
204
+ 'App.vue': '<template><dt-stack v-model:gap="someRef"></dt-stack><dt-stack gap="100"></dt-stack></template>',
205
+ });
206
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
207
+ });
208
+
209
+ it('does not flag gap="525" for stack-gap-to-spacing', () => {
210
+ const output = runHealthCheck({
211
+ 'App.vue': '<template><dt-stack gap="525"></dt-stack></template>',
212
+ });
213
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
214
+ });
215
+
216
+ it('does not flag a mix of single-quoted gap=\'25\' and double-quoted gap="100" for stack-gap-to-spacing', () => {
217
+ const output = runHealthCheck({
218
+ 'App.vue': '<template><dt-stack gap=\'25\'></dt-stack><dt-stack gap="100"></dt-stack></template>',
219
+ });
220
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
221
+ });
222
+
223
+ it('still flags an unmigrated stop in a partially migrated (mixed) file for stack-gap-to-spacing', () => {
224
+ const output = runHealthCheck({
225
+ 'App.vue': '<template><dt-stack gap="25"></dt-stack><dt-stack gap="625"></dt-stack></template>',
226
+ });
227
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
228
+ });
229
+
230
+ it('does not flag an old stack class alongside a bound gap="\'25\'" for stack-gap-to-spacing', () => {
231
+ const output = runHealthCheck({
232
+ 'App.vue': '<template><dt-stack class="d-stack--gap-100" :gap="\'25\'"></dt-stack></template>',
233
+ });
234
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
235
+ });
236
+ });
@@ -56,11 +56,29 @@ const __dirname = path.dirname(__filename);
56
56
  * @property {string} [scriptDir] - Directory name for standalone scripts
57
57
  * @property {boolean} [supportsPackageArg] - Forward --package to this standalone script
58
58
  * @property {string[]} [extraArgs] - Extra CLI args to forward
59
- * @property {RegExp[]} detectPatterns - Patterns that indicate migration is still needed
59
+ * @property {RegExp[]} detectPatterns - Patterns that unambiguously indicate migration is
60
+ * still needed. Always evaluated, regardless of skipIfPatterns.
61
+ * @property {RegExp[]} [ambiguousDetectPatterns] - Patterns that overlap with valid
62
+ * post-migration values. Only evaluated when skipIfPatterns does not match, so a file
63
+ * already containing a new-scale-only marker isn't flagged on these alone — but an
64
+ * unambiguous detectPatterns match elsewhere in the same (partially migrated) file
65
+ * still counts.
66
+ * @property {RegExp[]} [skipIfPatterns] - New-scale-only markers that gate
67
+ * ambiguousDetectPatterns — their presence means the file has already been migrated.
60
68
  * @property {string[]} fileExtensions - File extensions to scan during health check
61
69
  */
62
70
 
63
71
  /** @type {Migration[]} */
72
+ // Consumes a Dialtone tag's attribute soup, treating quoted attribute values as opaque so a
73
+ // literal > inside one (e.g. :disabled="count > 0") doesn't truncate the match early.
74
+ const DT_TAG_ATTRS = '(?:[^>"\']|"[^"]*"|\'[^\']*\')*';
75
+
76
+ // Allowlist (not denylist) of what may immediately precede "gap": whitespace (bare gap=),
77
+ // whitespace+":" (shorthand :gap=), or whitespace+"v-bind:" (long form). This rejects
78
+ // data-gap, @gap, v-on:gap, v-model:gap, v-slot:gap, and any other directive namespace,
79
+ // without having to enumerate every prefix that isn't the real gap prop/attribute.
80
+ const GAP_ATTR_LEAD = '(?:(?<=\\s)|(?<=\\s:)|(?<=\\sv-bind:))';
81
+
64
82
  const MIGRATIONS = [
65
83
  // ── Required (breaking) ────────────────────────────────────────────
66
84
  {
@@ -162,9 +180,13 @@ const MIGRATIONS = [
162
180
  type: 'standalone',
163
181
  scriptDir: 'dialtone_migrate_props',
164
182
  detectPatterns: [
165
- /(?:show|hide-close|hide-icon|label-visible|selected-values)(?:=|[\s>])/,
166
- /<(?:dt-(?:banner|notice|toast|modal)|Dt(?:Banner|Notice|Toast|Modal))\b[^>]*\btitle(?:=|[\s>])/,
167
- /<(?:dt-[\w-]+|Dt\w+)\b[^>]*@(?:input|change)(?:=|\.)/,
183
+ // (?<!-) blocks a hyphen immediately before the prop name — \b alone still matches
184
+ // there (- is a non-word char), which would treat native v-show as the show prop.
185
+ new RegExp(`<(?:dt-[\\w-]+|Dt\\w+)\\b${DT_TAG_ATTRS}\\b(?<!-)(?:show|hide-close|hide-icon|label-visible|selected-values)(?:=|[\\s>])`),
186
+ // (?<!-) blocks a hyphen immediately before "title" so data-title (and any other
187
+ // custom -title attribute) isn't mistaken for the title prop.
188
+ new RegExp(`<(?:dt-(?:banner|notice|toast|modal)|Dt(?:Banner|Notice|Toast|Modal))\\b${DT_TAG_ATTRS}\\b(?<!-)title(?:=|[\\s>])`),
189
+ new RegExp(`<(?:dt-[\\w-]+|Dt\\w+)\\b${DT_TAG_ATTRS}@(?:input|change)(?:=|\\.)`),
168
190
  /kind="(?:danger|error)"/,
169
191
  /validation-state="(?:error|success)"/,
170
192
  ],
@@ -242,9 +264,27 @@ const MIGRATIONS = [
242
264
  category: 'opt-in',
243
265
  type: 'config',
244
266
  configName: 'stack-gap-to-spacing',
267
+ // Old-only stops (never valid post-migration values) — always flagged as pending.
245
268
  detectPatterns: [
246
- /gap="(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)"/,
247
- /d-stack--gap-(?:50|100|200|300|350|400|450|500|525|550|600|625|650|700)/,
269
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:350|450|500|550|625|650|700)"`),
270
+ /d-stack--gap-(?:350|450|500|550|625|650|700)/,
271
+ ],
272
+ // Old and new gap scales share some numeric stops (e.g. "100"), so a bare match on
273
+ // these doesn't prove pending work. Only counted when the file has no new-scale-only
274
+ // marker (skipIfPatterns) — an unambiguous detectPatterns match elsewhere in the same
275
+ // (partially migrated) file is still flagged regardless.
276
+ ambiguousDetectPatterns: [
277
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:50|100|200|300|400|525|600)"`),
278
+ /d-stack--gap-(?:50|100|200|300|400|525|600)/,
279
+ ],
280
+ // Mirrors dialtone_migration_helper/configs/stack-gap-to-spacing.mjs's isAlreadyMigrated
281
+ // check: new-scale-only stops indicate the file was already migrated.
282
+ skipIfPatterns: [
283
+ new RegExp(`${GAP_ATTR_LEAD}gap="(?:25|75|150|250|525)"`),
284
+ new RegExp(`${GAP_ATTR_LEAD}gap='(?:25|75|150|250|525)'`),
285
+ new RegExp(`${GAP_ATTR_LEAD}gap="'(?:25|75|150|250|525)'"`),
286
+ /d-stack--gap-(?:25|75|150|250|525)/,
287
+ /d-description-list--gap-(?:25|75|150|250|525)/,
248
288
  ],
249
289
  fileExtensions: ['.vue', '.html', '.js', '.ts', '.jsx', '.tsx', '.md'],
250
290
  },
@@ -546,7 +586,14 @@ async function healthCheck (cwd) {
546
586
 
547
587
  for (const [file, content] of fileContents) {
548
588
  if (!migration.fileExtensions.some(ext => file.endsWith(ext))) continue;
549
- for (const pattern of migration.detectPatterns) {
589
+
590
+ let patterns = migration.detectPatterns;
591
+ const alreadyMigrated = migration.skipIfPatterns?.some(pattern => pattern.test(content));
592
+ if (!alreadyMigrated && migration.ambiguousDetectPatterns) {
593
+ patterns = [...patterns, ...migration.ambiguousDetectPatterns];
594
+ }
595
+
596
+ for (const pattern of patterns) {
550
597
  // Reset lastIndex for global patterns
551
598
  const re = new RegExp(pattern.source, pattern.flags.replace('g', ''));
552
599
  if (re.test(content)) {
@@ -86,3 +86,151 @@ describe('--package validation', () => {
86
86
  assert.equal(status, 0);
87
87
  });
88
88
  });
89
+
90
+ describe('--health-check', () => {
91
+ function runHealthCheck (files) {
92
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dlt-health-'));
93
+ try {
94
+ for (const [name, contents] of Object.entries(files)) {
95
+ fs.writeFileSync(path.join(tmp, name), contents);
96
+ }
97
+ return execFileSync(process.execPath, [cli, '--health-check', '--cwd', tmp],
98
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
99
+ } finally {
100
+ fs.rmSync(tmp, { recursive: true, force: true });
101
+ }
102
+ }
103
+
104
+ it('does not flag native v-show on non-Dialtone elements as component-props', () => {
105
+ const output = runHealthCheck({
106
+ 'App.vue': '<template><div v-show="isReady">Ready</div></template>',
107
+ });
108
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
109
+ });
110
+
111
+ it('does not flag native v-show on a Dialtone element as component-props', () => {
112
+ const output = runHealthCheck({
113
+ 'App.vue': '<template><dt-modal v-show="isOpen"></dt-modal></template>',
114
+ });
115
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
116
+ });
117
+
118
+ it('does not flag v-show on dt-button as component-props', () => {
119
+ const output = runHealthCheck({
120
+ 'App.vue': '<template><dt-button v-show="visible">Go</dt-button></template>',
121
+ });
122
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
123
+ });
124
+
125
+ it('does not flag data-title or v-title on a Dialtone element as component-props', () => {
126
+ const output = runHealthCheck({
127
+ 'App.vue': '<template><dt-modal data-title="tooltip" v-title="x"></dt-modal></template>',
128
+ });
129
+ assert.match(output, /\[DONE\].*Component Props & Events \(component-props\)/);
130
+ });
131
+
132
+ it('still flags show= on a Dialtone component as component-props', () => {
133
+ const output = runHealthCheck({
134
+ 'App.vue': '<template><dt-modal show="isOpen"></dt-modal></template>',
135
+ });
136
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
137
+ });
138
+
139
+ it('still flags title= on a Dialtone component as component-props', () => {
140
+ const output = runHealthCheck({
141
+ 'App.vue': '<template><dt-modal title="isOpen"></dt-modal></template>',
142
+ });
143
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
144
+ });
145
+
146
+ it('still flags :show= on a Dialtone component when a quoted attribute earlier in the tag contains a literal >', () => {
147
+ const output = runHealthCheck({
148
+ 'App.vue': '<template><dt-modal :disabled="count > 0" :show="isOpen"></dt-modal></template>',
149
+ });
150
+ assert.match(output, /\[PENDING\].*Component Props & Events \(component-props\)/);
151
+ });
152
+
153
+ it('does not flag an already-migrated file for stack-gap-to-spacing', () => {
154
+ const output = runHealthCheck({
155
+ 'App.vue': '<template><dt-stack gap="100"></dt-stack><dt-stack gap="25"></dt-stack></template>',
156
+ });
157
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
158
+ });
159
+
160
+ it('still flags an unmigrated file for stack-gap-to-spacing', () => {
161
+ const output = runHealthCheck({
162
+ 'App.vue': '<template><dt-stack gap="625"></dt-stack></template>',
163
+ });
164
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
165
+ });
166
+
167
+ it('does not flag an unrelated data-gap attribute for stack-gap-to-spacing', () => {
168
+ const output = runHealthCheck({
169
+ 'App.vue': '<template><div data-gap="625"></div></template>',
170
+ });
171
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
172
+ });
173
+
174
+ it('does not let an unrelated data-gap new-scale value suppress a real ambiguous pending gap', () => {
175
+ const output = runHealthCheck({
176
+ 'App.vue': '<template><div data-gap="25"></div><dt-stack gap="100"></dt-stack></template>',
177
+ });
178
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
179
+ });
180
+
181
+ it('does not flag an @gap event-listener value as a pending gap prop', () => {
182
+ const output = runHealthCheck({
183
+ 'App.vue': '<template><dt-stack @gap="625"></dt-stack></template>',
184
+ });
185
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
186
+ });
187
+
188
+ it('does not let an @gap or v-on:gap event-listener value suppress a real ambiguous pending gap', () => {
189
+ const output = runHealthCheck({
190
+ 'App.vue': '<template><dt-stack @gap="25"></dt-stack><dt-stack v-on:gap="75"></dt-stack><dt-stack gap="100"></dt-stack></template>',
191
+ });
192
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
193
+ });
194
+
195
+ it('still flags a bound :gap= and v-bind:gap= as pending', () => {
196
+ const output = runHealthCheck({
197
+ 'App.vue': '<template><dt-stack :gap="625"></dt-stack><dt-stack v-bind:gap="625"></dt-stack></template>',
198
+ });
199
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
200
+ });
201
+
202
+ it('does not flag v-model:gap and still detects a real ambiguous gap alongside it', () => {
203
+ const output = runHealthCheck({
204
+ 'App.vue': '<template><dt-stack v-model:gap="someRef"></dt-stack><dt-stack gap="100"></dt-stack></template>',
205
+ });
206
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
207
+ });
208
+
209
+ it('does not flag gap="525" for stack-gap-to-spacing', () => {
210
+ const output = runHealthCheck({
211
+ 'App.vue': '<template><dt-stack gap="525"></dt-stack></template>',
212
+ });
213
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
214
+ });
215
+
216
+ it('does not flag a mix of single-quoted gap=\'25\' and double-quoted gap="100" for stack-gap-to-spacing', () => {
217
+ const output = runHealthCheck({
218
+ 'App.vue': '<template><dt-stack gap=\'25\'></dt-stack><dt-stack gap="100"></dt-stack></template>',
219
+ });
220
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
221
+ });
222
+
223
+ it('still flags an unmigrated stop in a partially migrated (mixed) file for stack-gap-to-spacing', () => {
224
+ const output = runHealthCheck({
225
+ 'App.vue': '<template><dt-stack gap="25"></dt-stack><dt-stack gap="625"></dt-stack></template>',
226
+ });
227
+ assert.match(output, /\[PENDING\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
228
+ });
229
+
230
+ it('does not flag an old stack class alongside a bound gap="\'25\'" for stack-gap-to-spacing', () => {
231
+ const output = runHealthCheck({
232
+ 'App.vue': '<template><dt-stack class="d-stack--gap-100" :gap="\'25\'"></dt-stack></template>',
233
+ });
234
+ assert.match(output, /\[DONE\].*Stack Gap to Spacing \(stack-gap-to-spacing\)/);
235
+ });
236
+ });