@n8n/design-system 2.11.2 → 2.13.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@n8n/design-system",
4
- "version": "2.11.2",
4
+ "version": "2.13.0",
5
5
  "main": "src/index.ts",
6
6
  "import": "src/index.ts",
7
7
  "license": "SEE LICENSE IN LICENSE.md",
@@ -1,418 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Button V2 Migration Codemod
5
- *
6
- * This script migrates N8nButton components from the legacy V1 API to the V2 API.
7
- *
8
- * Transformations:
9
- * - type="primary" → variant="solid"
10
- * - type="secondary" → variant="subtle"
11
- * - type="tertiary" → variant="subtle"
12
- * - type="danger" → variant="destructive"
13
- * - type="success" → variant="solid" class="n8n-button--success"
14
- * - type="warning" → variant="solid" class="n8n-button--warning"
15
- * - type="highlight" → variant="ghost" class="n8n-button--highlight"
16
- * - type="highlightFill" → variant="subtle" class="n8n-button--highlightFill"
17
- * - outline prop → variant="outline"
18
- * - text prop → variant="ghost"
19
- * - size="xmini"|"mini" → size="xsmall"
20
- * - square → iconOnly
21
- * - nativeType → type attribute
22
- * - block → style="width: 100%"
23
- * - element="a" → (removed, href determines element)
24
- * Usage:
25
- * node migrate-button-v2.mjs [--dry-run]
26
- */
27
-
28
- import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
29
- import { join, relative } from 'path';
30
- import { fileURLToPath } from 'url';
31
- import { dirname } from 'path';
32
-
33
- const __filename = fileURLToPath(import.meta.url);
34
- const __dirname = dirname(__filename);
35
-
36
- // Configuration
37
- const FRONTEND_ROOT = join(__dirname, '../../..');
38
- const DRY_RUN = process.argv.includes('--dry-run');
39
-
40
- // Mapping from legacy type to new variant
41
- const TYPE_TO_VARIANT = {
42
- primary: 'solid',
43
- secondary: 'subtle',
44
- tertiary: 'subtle',
45
- danger: 'destructive',
46
- };
47
-
48
- // Legacy types that need override classes
49
- const LEGACY_TYPES_WITH_CLASSES = {
50
- success: { variant: 'solid', className: 'n8n-button--success' },
51
- warning: { variant: 'solid', className: 'n8n-button--warning' },
52
- highlight: { variant: 'ghost', className: 'n8n-button--highlight' },
53
- highlightFill: { variant: 'subtle', className: 'n8n-button--highlightFill' },
54
- };
55
-
56
- // Size normalization
57
- const SIZE_MAP = {
58
- xmini: 'xsmall',
59
- mini: 'xsmall',
60
- };
61
-
62
- // Stats
63
- const stats = {
64
- filesScanned: 0,
65
- filesModified: 0,
66
- transformations: {
67
- typeToVariant: 0,
68
- legacyTypeWithClass: 0,
69
- outlineToVariant: 0,
70
- textToVariant: 0,
71
- sizeNormalized: 0,
72
- squareToIconOnly: 0,
73
- nativeTypeToType: 0,
74
- blockToStyle: 0,
75
- elementRemoved: 0,
76
- },
77
- };
78
-
79
- /**
80
- * Find all .vue files recursively
81
- */
82
- function findVueFiles(dir, files = []) {
83
- const entries = readdirSync(dir);
84
-
85
- for (const entry of entries) {
86
- const fullPath = join(dir, entry);
87
-
88
- // Skip node_modules and hidden directories
89
- if (entry === 'node_modules' || entry.startsWith('.')) continue;
90
-
91
- const stat = statSync(fullPath);
92
- if (stat.isDirectory()) {
93
- findVueFiles(fullPath, files);
94
- } else if (entry.endsWith('.vue')) {
95
- files.push(fullPath);
96
- }
97
- }
98
-
99
- return files;
100
- }
101
-
102
- /**
103
- * Transform a single N8nButton tag
104
- */
105
- function transformButtonTag(fullMatch, tagContent, selfClosing, content, closingTag) {
106
- let modified = false;
107
- const changes = [];
108
-
109
- // Track what we need to add
110
- let newVariant = null;
111
- let addClass = null;
112
- let addStyle = null;
113
-
114
- // Parse current attributes (handles both static and v-bind shorthand :prop)
115
- const hasType = /\btype=["']([^"']+)["']/.exec(tagContent);
116
- const hasVariant = /\bvariant=["']/.test(tagContent);
117
- const hasOutline = /\b:?outline(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
118
- const hasText = /\b:?text(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
119
- const hasSize = /\b:?size=["']([^"']+)["']/.exec(tagContent);
120
- const hasSquare = /\b:?square(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
121
- const hasNativeType = /\b:?nativeType=["']([^"']+)["']/.exec(tagContent);
122
- const hasBlock = /\b:?block(?:=["']true["'])?(?=\s|\/?>|\s)/.test(tagContent);
123
- const hasElement = /\b:?element=["']([^"']+)["']/.exec(tagContent);
124
- const hasClass = /\bclass=["']([^"']+)["']/.exec(tagContent);
125
- const hasStyle = /\bstyle=["']([^"']+)["']/.exec(tagContent);
126
-
127
- // Skip if already using variant (already migrated)
128
- if (hasVariant) {
129
- return fullMatch;
130
- }
131
-
132
- // 1. Handle outline prop → variant="outline"
133
- if (hasOutline && !hasText) {
134
- newVariant = 'outline';
135
- tagContent = tagContent.replace(/\s*\b:?outline(?:=["']true["'])?(?=\s|\/?>)/, '');
136
- // Also remove type if present since outline takes precedence
137
- if (hasType) {
138
- tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
139
- }
140
- changes.push('outline → variant="outline"');
141
- stats.transformations.outlineToVariant++;
142
- modified = true;
143
- }
144
- // 2. Handle text prop → variant="ghost"
145
- else if (hasText) {
146
- newVariant = 'ghost';
147
- tagContent = tagContent.replace(/\s*\b:?text(?:=["']true["'])?(?=\s|\/?>)/, '');
148
- // Also remove type and outline if present
149
- if (hasType) {
150
- tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
151
- }
152
- if (hasOutline) {
153
- tagContent = tagContent.replace(/\s*\b:?outline(?:=["']true["'])?(?=\s|\/?>)/, '');
154
- }
155
- changes.push('text → variant="ghost"');
156
- stats.transformations.textToVariant++;
157
- modified = true;
158
- }
159
- // 3. Handle type prop
160
- else if (hasType) {
161
- const typeValue = hasType[1];
162
-
163
- if (TYPE_TO_VARIANT[typeValue]) {
164
- // Direct mapping
165
- newVariant = TYPE_TO_VARIANT[typeValue];
166
- tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
167
- changes.push(`type="${typeValue}" → variant="${newVariant}"`);
168
- stats.transformations.typeToVariant++;
169
- modified = true;
170
- } else if (LEGACY_TYPES_WITH_CLASSES[typeValue]) {
171
- // Legacy type with class override
172
- const mapping = LEGACY_TYPES_WITH_CLASSES[typeValue];
173
- newVariant = mapping.variant;
174
- addClass = mapping.className;
175
- tagContent = tagContent.replace(/\s*\btype=["'][^"']+["']/, '');
176
- changes.push(`type="${typeValue}" → variant="${newVariant}" + class="${addClass}"`);
177
- stats.transformations.legacyTypeWithClass++;
178
- modified = true;
179
- }
180
- }
181
-
182
- // 4. Handle size normalization
183
- if (hasSize && SIZE_MAP[hasSize[1]]) {
184
- const oldSize = hasSize[1];
185
- const newSize = SIZE_MAP[oldSize];
186
- tagContent = tagContent.replace(/\b:?size=["'][^"']+["']/, `size="${newSize}"`);
187
- changes.push(`size="${oldSize}" → size="${newSize}"`);
188
- stats.transformations.sizeNormalized++;
189
- modified = true;
190
- }
191
-
192
- // 5. Handle square → iconOnly
193
- if (hasSquare) {
194
- tagContent = tagContent.replace(/\s*\b:?square(?:=["']true["'])?(?=\s|\/?>)/, '');
195
- // Add iconOnly attribute
196
- tagContent = tagContent.replace(
197
- /(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/,
198
- '$1 iconOnly',
199
- );
200
- changes.push('square → iconOnly');
201
- stats.transformations.squareToIconOnly++;
202
- modified = true;
203
- }
204
-
205
- // 6. Handle nativeType → type
206
- if (hasNativeType) {
207
- const nativeTypeValue = hasNativeType[1];
208
- tagContent = tagContent.replace(/\s*\b:?nativeType=["'][^"']+["']/, '');
209
- tagContent = tagContent.replace(
210
- /(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/,
211
- `$1 type="${nativeTypeValue}"`,
212
- );
213
- changes.push(`nativeType="${nativeTypeValue}" → type="${nativeTypeValue}"`);
214
- stats.transformations.nativeTypeToType++;
215
- modified = true;
216
- }
217
-
218
- // 7. Handle block → style="width: 100%"
219
- if (hasBlock) {
220
- tagContent = tagContent.replace(/\s*\b:?block(?:=["']true["'])?(?=\s|\/?>)/, '');
221
- addStyle = 'width: 100%';
222
- changes.push('block → style="width: 100%"');
223
- stats.transformations.blockToStyle++;
224
- modified = true;
225
- }
226
-
227
- // 8. Handle element="a" → remove (href determines element)
228
- if (hasElement) {
229
- tagContent = tagContent.replace(/\s*\b:?element=["'][^"']+["']/, '');
230
- changes.push('element removed (href determines element)');
231
- stats.transformations.elementRemoved++;
232
- modified = true;
233
- }
234
-
235
- if (!modified) {
236
- return fullMatch;
237
- }
238
-
239
- // Now apply the collected changes
240
- const BUTTON_TAG_PATTERN = /(N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton)/;
241
-
242
- // Add variant attribute
243
- if (newVariant) {
244
- tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 variant="${newVariant}"`);
245
- }
246
-
247
- // Merge class attribute
248
- if (addClass) {
249
- if (hasClass) {
250
- // Merge with existing class
251
- tagContent = tagContent.replace(/\bclass=["']([^"']+)["']/, `class="$1 ${addClass}"`);
252
- } else {
253
- // Add new class attribute
254
- tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 class="${addClass}"`);
255
- }
256
- }
257
-
258
- // Merge style attribute
259
- if (addStyle) {
260
- if (hasStyle) {
261
- // Merge with existing style
262
- const existingStyle = hasStyle[1].trim();
263
- const separator = existingStyle.endsWith(';') ? ' ' : '; ';
264
- tagContent = tagContent.replace(
265
- /\bstyle=["']([^"']+)["']/,
266
- `style="$1${separator}${addStyle}"`,
267
- );
268
- } else {
269
- // Add new style attribute
270
- tagContent = tagContent.replace(BUTTON_TAG_PATTERN, `$1 style="${addStyle}"`);
271
- }
272
- }
273
-
274
- // Build the new tag
275
- let result;
276
- if (selfClosing) {
277
- result = `<${tagContent.trim()} />`;
278
- } else {
279
- result = `<${tagContent.trim()}>${content || ''}${closingTag}`;
280
- }
281
-
282
- // Log the transformation
283
- console.log(` ${changes.join(', ')}`);
284
-
285
- return result;
286
- }
287
-
288
- /**
289
- * Transform all N8nButton usages in a file
290
- */
291
- function transformFile(filePath) {
292
- const content = readFileSync(filePath, 'utf-8');
293
- stats.filesScanned++;
294
-
295
- // Find template section - use greedy match ([\s\S]*) to get the outermost </template>
296
- // Vue SFC files may have nested <template> tags for slots, so we need the last closing tag
297
- const templateMatch = /<template[^>]*>([\s\S]*)<\/template>/i.exec(content);
298
- if (!templateMatch) {
299
- return { modified: false };
300
- }
301
-
302
- const templateStart = templateMatch.index;
303
- const templateContent = templateMatch[1];
304
-
305
- // Match button components (both self-closing and with content)
306
- // Includes: N8nButton, n8n-button, N8nIconButton, n8n-icon-button, IconButton
307
- const BUTTON_TAGS = 'N8nButton|n8n-button|N8nIconButton|n8n-icon-button|IconButton';
308
-
309
- let newTemplateContent = templateContent;
310
- let hasChanges = false;
311
-
312
- // Process self-closing buttons first
313
- // Pattern matches: <TAG + attributes (no unquoted >) + />
314
- // This avoids matching non-self-closing tags like <N8nButton ...>content</N8nButton>
315
- newTemplateContent = newTemplateContent.replace(
316
- new RegExp(`<(${BUTTON_TAGS})((?:[^>"]|"[^"]*")*)\\s*\\/>`, 'gi'),
317
- (match, tagName, attrs) => {
318
- // Skip if no attributes
319
- if (!attrs || !attrs.trim()) return match;
320
- const result = transformButtonTag(match, `${tagName} ${attrs.trim()}`, true, null, null);
321
- if (result !== match) hasChanges = true;
322
- return result;
323
- },
324
- );
325
-
326
- // Process buttons with content (non-self-closing)
327
- // Pattern matches: <TAG + attrs (not ending with /) + > + content + </TAG>
328
- // The attrs pattern uses negative lookahead to ensure / is not followed by >
329
- newTemplateContent = newTemplateContent.replace(
330
- new RegExp(
331
- `<(${BUTTON_TAGS})((?:[^>"/]|"[^"]*"|/(?!>))*)>([\\s\\S]*?)<\\/(${BUTTON_TAGS})>`,
332
- 'gi',
333
- ),
334
- (match, tagName, attrs, content, closeTag) => {
335
- const result = transformButtonTag(
336
- match,
337
- `${tagName}${attrs || ''}`,
338
- false,
339
- content,
340
- `</${closeTag}>`,
341
- );
342
- if (result !== match) hasChanges = true;
343
- return result;
344
- },
345
- );
346
-
347
- if (!hasChanges) {
348
- return { modified: false };
349
- }
350
-
351
- // Reconstruct the file
352
- const newContent =
353
- content.slice(0, templateStart) +
354
- '<template>' +
355
- newTemplateContent +
356
- '</template>' +
357
- content.slice(templateStart + templateMatch[0].length);
358
-
359
- return { modified: true, content: newContent };
360
- }
361
-
362
- /**
363
- * Main function
364
- */
365
- async function main() {
366
- console.log('Button V2 Migration Codemod');
367
- console.log('===========================');
368
- console.log(`Mode: ${DRY_RUN ? 'DRY RUN (no files will be modified)' : 'LIVE'}`);
369
- console.log(`Scanning: ${FRONTEND_ROOT}`);
370
- console.log('');
371
-
372
- const vueFiles = findVueFiles(FRONTEND_ROOT);
373
- console.log(`Found ${vueFiles.length} Vue files\n`);
374
-
375
- for (const filePath of vueFiles) {
376
- const relativePath = relative(FRONTEND_ROOT, filePath);
377
-
378
- try {
379
- const result = transformFile(filePath);
380
-
381
- if (result.modified) {
382
- console.log(`\nModified: ${relativePath}`);
383
- stats.filesModified++;
384
-
385
- if (!DRY_RUN) {
386
- writeFileSync(filePath, result.content, 'utf-8');
387
- }
388
- }
389
- } catch (error) {
390
- console.error(`Error processing ${relativePath}:`, error.message);
391
- }
392
- }
393
-
394
- // Print summary
395
- console.log('\n===========================');
396
- console.log('Summary');
397
- console.log('===========================');
398
- console.log(`Files scanned: ${stats.filesScanned}`);
399
- console.log(`Files modified: ${stats.filesModified}`);
400
- console.log('');
401
- console.log('Transformations:');
402
- console.log(` type → variant: ${stats.transformations.typeToVariant}`);
403
- console.log(` legacy type + class: ${stats.transformations.legacyTypeWithClass}`);
404
- console.log(` outline → variant: ${stats.transformations.outlineToVariant}`);
405
- console.log(` text → variant: ${stats.transformations.textToVariant}`);
406
- console.log(` size normalized: ${stats.transformations.sizeNormalized}`);
407
- console.log(` square → iconOnly: ${stats.transformations.squareToIconOnly}`);
408
- console.log(` nativeType → type: ${stats.transformations.nativeTypeToType}`);
409
- console.log(` block → style: ${stats.transformations.blockToStyle}`);
410
- console.log(` element removed: ${stats.transformations.elementRemoved}`);
411
-
412
- if (DRY_RUN) {
413
- console.log('\nDry run complete. No files were modified.');
414
- console.log('Run without --dry-run to apply changes.');
415
- }
416
- }
417
-
418
- main().catch(console.error);