@gilav21/shadcn-angular 0.0.20 → 0.0.22
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/README.md +101 -2
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +310 -218
- package/dist/commands/add.spec.d.ts +1 -0
- package/dist/commands/add.spec.js +140 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +108 -0
- package/dist/commands/init.js +2 -3
- package/dist/index.js +5 -0
- package/dist/registry/index.d.ts +6 -0
- package/dist/registry/index.js +13 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/package-manager.js +2 -7
- package/dist/utils/shortcut-registry.js +1 -1
- package/package.json +1 -1
- package/src/commands/add.spec.ts +170 -0
- package/src/commands/add.ts +432 -240
- package/src/commands/help.ts +131 -0
- package/src/commands/init.ts +2 -3
- package/src/index.ts +6 -0
- package/src/registry/index.ts +20 -0
- package/src/utils/config.ts +1 -1
- package/src/utils/package-manager.ts +2 -5
- package/src/utils/shortcut-registry.ts +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { resolveDependencies, promptOptionalDependencies } from './add.js';
|
|
3
|
+
import { registry } from '../registry/index.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// resolveDependencies
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
describe('resolveDependencies', () => {
|
|
8
|
+
it('resolves a single component without dependencies', () => {
|
|
9
|
+
const result = resolveDependencies(['badge']);
|
|
10
|
+
expect(result).toContain('badge');
|
|
11
|
+
expect(result.size).toBe(1);
|
|
12
|
+
});
|
|
13
|
+
it('includes transitive dependencies', () => {
|
|
14
|
+
// button depends on ripple
|
|
15
|
+
const result = resolveDependencies(['button']);
|
|
16
|
+
expect(result).toContain('button');
|
|
17
|
+
expect(result).toContain('ripple');
|
|
18
|
+
});
|
|
19
|
+
it('resolves deep transitive chains', () => {
|
|
20
|
+
// date-picker -> calendar -> button -> ripple, calendar -> select
|
|
21
|
+
const result = resolveDependencies(['date-picker']);
|
|
22
|
+
expect(result).toContain('date-picker');
|
|
23
|
+
expect(result).toContain('calendar');
|
|
24
|
+
expect(result).toContain('button');
|
|
25
|
+
expect(result).toContain('ripple');
|
|
26
|
+
expect(result).toContain('select');
|
|
27
|
+
});
|
|
28
|
+
it('deduplicates shared dependencies across multiple inputs', () => {
|
|
29
|
+
// Both button-group and speed-dial depend on button
|
|
30
|
+
const result = resolveDependencies(['button-group', 'speed-dial']);
|
|
31
|
+
expect(result).toContain('button-group');
|
|
32
|
+
expect(result).toContain('speed-dial');
|
|
33
|
+
expect(result).toContain('button');
|
|
34
|
+
expect(result).toContain('ripple');
|
|
35
|
+
// Count how many times button appears — should be exactly once (it's a Set)
|
|
36
|
+
const asArray = [...result];
|
|
37
|
+
expect(asArray.filter((n) => n === 'button')).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
it('returns a Set containing all resolved names', () => {
|
|
40
|
+
const result = resolveDependencies(['badge']);
|
|
41
|
+
expect(result).toBeInstanceOf(Set);
|
|
42
|
+
});
|
|
43
|
+
it('handles components with no registry deps gracefully', () => {
|
|
44
|
+
const result = resolveDependencies(['separator']);
|
|
45
|
+
expect(result.size).toBe(1);
|
|
46
|
+
expect(result).toContain('separator');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// promptOptionalDependencies
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
vi.mock('prompts', () => ({
|
|
53
|
+
default: vi.fn(),
|
|
54
|
+
}));
|
|
55
|
+
describe('promptOptionalDependencies', () => {
|
|
56
|
+
it('returns empty array when no components have optional deps', async () => {
|
|
57
|
+
const resolved = new Set(['badge', 'button', 'ripple']);
|
|
58
|
+
const result = await promptOptionalDependencies(resolved, { yes: false, branch: 'master' });
|
|
59
|
+
expect(result).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it('returns empty array with --yes flag (skip prompts)', async () => {
|
|
62
|
+
const resolved = new Set(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
63
|
+
const result = await promptOptionalDependencies(resolved, { yes: true, branch: 'master' });
|
|
64
|
+
expect(result).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
it('returns all optional dep names with --all flag', async () => {
|
|
67
|
+
const resolved = new Set(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
68
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
69
|
+
expect(result).toContain('context-menu');
|
|
70
|
+
});
|
|
71
|
+
it('filters out optional deps already in the resolved set', async () => {
|
|
72
|
+
// context-menu is already resolved, so it should not be offered
|
|
73
|
+
const resolved = new Set(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
74
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
75
|
+
expect(result).not.toContain('context-menu');
|
|
76
|
+
});
|
|
77
|
+
it('deduplicates optional deps across components', async () => {
|
|
78
|
+
// Both data-table and tree have context-menu as optional
|
|
79
|
+
const resolved = new Set(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
80
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
81
|
+
const contextMenuCount = result.filter((n) => n === 'context-menu').length;
|
|
82
|
+
expect(contextMenuCount).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Registry data integrity
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
describe('registry optional dependencies', () => {
|
|
89
|
+
it('data-table has context-menu as optional dependency', () => {
|
|
90
|
+
const dt = registry['data-table'];
|
|
91
|
+
expect(dt.optionalDependencies).toBeDefined();
|
|
92
|
+
const names = dt.optionalDependencies.map((d) => d.name);
|
|
93
|
+
expect(names).toContain('context-menu');
|
|
94
|
+
});
|
|
95
|
+
it('tree has context-menu as optional dependency', () => {
|
|
96
|
+
const tree = registry['tree'];
|
|
97
|
+
expect(tree.optionalDependencies).toBeDefined();
|
|
98
|
+
const names = tree.optionalDependencies.map((d) => d.name);
|
|
99
|
+
expect(names).toContain('context-menu');
|
|
100
|
+
});
|
|
101
|
+
it('every optional dependency name is a valid registry key', () => {
|
|
102
|
+
for (const [componentName, definition] of Object.entries(registry)) {
|
|
103
|
+
if (!definition.optionalDependencies)
|
|
104
|
+
continue;
|
|
105
|
+
for (const opt of definition.optionalDependencies) {
|
|
106
|
+
expect(registry[opt.name], `Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`).toBeDefined();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
it('every dependency name is a valid registry key', () => {
|
|
111
|
+
for (const [componentName, definition] of Object.entries(registry)) {
|
|
112
|
+
if (!definition.dependencies)
|
|
113
|
+
continue;
|
|
114
|
+
for (const dep of definition.dependencies) {
|
|
115
|
+
expect(registry[dep], `Dependency "${dep}" in "${componentName}" is not a valid registry key`).toBeDefined();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// help command
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
describe('help command', () => {
|
|
124
|
+
it('prints commands, optional dependencies, and component categories', async () => {
|
|
125
|
+
const { help } = await import('./help.js');
|
|
126
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
127
|
+
help();
|
|
128
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
129
|
+
const output = spy.mock.calls[0][0];
|
|
130
|
+
expect(output).toContain('Commands');
|
|
131
|
+
expect(output).toContain('init');
|
|
132
|
+
expect(output).toContain('add');
|
|
133
|
+
expect(output).toContain('Optional Dependencies');
|
|
134
|
+
expect(output).toContain('Available Components');
|
|
135
|
+
expect(output).toContain('UI');
|
|
136
|
+
expect(output).toContain('Charts');
|
|
137
|
+
expect(output).toContain('Animation');
|
|
138
|
+
spy.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function help(): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { registry } from '../registry/index.js';
|
|
3
|
+
const CATEGORY_ORDER = ['UI', 'Charts', 'Layout / Page Building', 'Animation', 'Kanban'];
|
|
4
|
+
const ANIMATION_COMPONENTS = new Set([
|
|
5
|
+
'gradient-text', 'flip-text', 'meteors', 'shine-border', 'scroll-progress',
|
|
6
|
+
'blur-fade', 'ripple', 'marquee', 'word-rotate', 'morphing-text',
|
|
7
|
+
'typing-animation', 'wobble-card', 'magnetic', 'orbit', 'stagger-children',
|
|
8
|
+
'particles', 'confetti', 'number-ticker', 'text-reveal', 'streaming-text', 'sparkles',
|
|
9
|
+
]);
|
|
10
|
+
const KANBAN_COMPONENTS = new Set(['kanban']);
|
|
11
|
+
const LAYOUT_COMPONENTS = new Set(['bento-grid', 'page-builder']);
|
|
12
|
+
function categorize(name) {
|
|
13
|
+
const def = registry[name];
|
|
14
|
+
if (!def)
|
|
15
|
+
return 'UI';
|
|
16
|
+
const hasChartFile = def.files.some(f => f.startsWith('charts/'));
|
|
17
|
+
if (hasChartFile)
|
|
18
|
+
return 'Charts';
|
|
19
|
+
if (LAYOUT_COMPONENTS.has(name))
|
|
20
|
+
return 'Layout / Page Building';
|
|
21
|
+
if (ANIMATION_COMPONENTS.has(name))
|
|
22
|
+
return 'Animation';
|
|
23
|
+
if (KANBAN_COMPONENTS.has(name))
|
|
24
|
+
return 'Kanban';
|
|
25
|
+
return 'UI';
|
|
26
|
+
}
|
|
27
|
+
function buildComponentsByCategory() {
|
|
28
|
+
const groups = new Map();
|
|
29
|
+
for (const cat of CATEGORY_ORDER) {
|
|
30
|
+
groups.set(cat, []);
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(registry)) {
|
|
33
|
+
const cat = categorize(name);
|
|
34
|
+
const list = groups.get(cat);
|
|
35
|
+
if (list)
|
|
36
|
+
list.push(name);
|
|
37
|
+
}
|
|
38
|
+
for (const list of groups.values()) {
|
|
39
|
+
list.sort();
|
|
40
|
+
}
|
|
41
|
+
return groups;
|
|
42
|
+
}
|
|
43
|
+
function formatComponentList(names, columns = 4) {
|
|
44
|
+
const colWidth = 26;
|
|
45
|
+
const lines = [];
|
|
46
|
+
for (let i = 0; i < names.length; i += columns) {
|
|
47
|
+
const row = names.slice(i, i + columns);
|
|
48
|
+
lines.push(' ' + row.map(n => n.padEnd(colWidth)).join(''));
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
function buildCommandsSection() {
|
|
53
|
+
const branchDefault = chalk.gray('(default: master)');
|
|
54
|
+
return [
|
|
55
|
+
chalk.bold('Commands'),
|
|
56
|
+
'',
|
|
57
|
+
' ' + chalk.cyan('init') + ' Initialize shadcn-angular in your project',
|
|
58
|
+
' ' + chalk.gray('-y, --yes') + ' Skip confirmation prompt',
|
|
59
|
+
' ' + chalk.gray('-d, --defaults') + ' Use default configuration',
|
|
60
|
+
' ' + chalk.gray('-b, --branch') + ' <branch> GitHub branch to fetch from ' + branchDefault,
|
|
61
|
+
'',
|
|
62
|
+
' ' + chalk.cyan('add') + ' Add component(s) to your project',
|
|
63
|
+
' ' + chalk.gray('[components...]') + ' One or more component names',
|
|
64
|
+
' ' + chalk.gray('-y, --yes') + ' Skip confirmation prompt',
|
|
65
|
+
' ' + chalk.gray('-o, --overwrite') + ' Overwrite existing files',
|
|
66
|
+
' ' + chalk.gray('-a, --all') + ' Add all available components',
|
|
67
|
+
' ' + chalk.gray('-p, --path') + ' <path> Custom install path',
|
|
68
|
+
' ' + chalk.gray('--remote') + ' Force remote fetch from GitHub registry',
|
|
69
|
+
' ' + chalk.gray('-b, --branch') + ' <branch> GitHub branch to fetch from ' + branchDefault,
|
|
70
|
+
'',
|
|
71
|
+
' ' + chalk.cyan('help') + ' Show this reference',
|
|
72
|
+
'',
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function buildOptionalDepsSection() {
|
|
76
|
+
return [
|
|
77
|
+
chalk.bold('Optional Dependencies'),
|
|
78
|
+
'',
|
|
79
|
+
' Some components offer companion packages during installation.',
|
|
80
|
+
' With ' + chalk.cyan('--yes') + ' they are skipped; with ' + chalk.cyan('--all') + ' they are included automatically.',
|
|
81
|
+
'',
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
function buildComponentsSection() {
|
|
85
|
+
const groups = buildComponentsByCategory();
|
|
86
|
+
const lines = [
|
|
87
|
+
chalk.bold('Available Components'),
|
|
88
|
+
'',
|
|
89
|
+
];
|
|
90
|
+
for (const [category, names] of groups) {
|
|
91
|
+
if (names.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
const countLabel = chalk.gray('(' + String(names.length) + ')');
|
|
94
|
+
lines.push(' ' + chalk.yellow(category) + ' ' + countLabel, formatComponentList(names), '');
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
}
|
|
98
|
+
export function help() {
|
|
99
|
+
const output = [
|
|
100
|
+
'',
|
|
101
|
+
chalk.bold.underline('shadcn-angular CLI'),
|
|
102
|
+
'',
|
|
103
|
+
...buildCommandsSection(),
|
|
104
|
+
...buildOptionalDepsSection(),
|
|
105
|
+
...buildComponentsSection(),
|
|
106
|
+
];
|
|
107
|
+
console.log(output.join('\n'));
|
|
108
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import prompts from 'prompts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
@@ -80,7 +80,6 @@ export async function init(options) {
|
|
|
80
80
|
let createShortcutRegistry = true;
|
|
81
81
|
if (options.defaults || options.yes) {
|
|
82
82
|
config = getDefaultConfig();
|
|
83
|
-
createShortcutRegistry = true;
|
|
84
83
|
}
|
|
85
84
|
else {
|
|
86
85
|
const THEME_COLORS = {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { init } from './commands/init.js';
|
|
4
4
|
import { add } from './commands/add.js';
|
|
5
|
+
import { help } from './commands/help.js';
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program
|
|
7
8
|
.name('shadcn-angular')
|
|
@@ -25,4 +26,8 @@ program
|
|
|
25
26
|
.option('--remote', 'Force remote fetch from GitHub registry')
|
|
26
27
|
.option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
|
|
27
28
|
.action(add);
|
|
29
|
+
program
|
|
30
|
+
.command('help')
|
|
31
|
+
.description('Show detailed usage information')
|
|
32
|
+
.action(help);
|
|
28
33
|
program.parse();
|
package/dist/registry/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
export interface OptionalDependency {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly description: string;
|
|
4
|
+
}
|
|
1
5
|
export interface ComponentDefinition {
|
|
2
6
|
name: string;
|
|
3
7
|
files: string[];
|
|
8
|
+
peerFiles?: string[];
|
|
4
9
|
dependencies?: string[];
|
|
10
|
+
optionalDependencies?: readonly OptionalDependency[];
|
|
5
11
|
npmDependencies?: string[];
|
|
6
12
|
libFiles?: string[];
|
|
7
13
|
shortcutDefinitions?: {
|
package/dist/registry/index.js
CHANGED
|
@@ -126,9 +126,14 @@ export const registry = {
|
|
|
126
126
|
'data-table/data-table.component.ts',
|
|
127
127
|
'data-table/data-table-column-header.component.ts',
|
|
128
128
|
'data-table/data-table-pagination.component.ts',
|
|
129
|
+
'data-table/data-table-multiselect-filter.component.ts',
|
|
129
130
|
'data-table/data-table.types.ts',
|
|
131
|
+
'data-table/data-table.utils.ts',
|
|
130
132
|
'data-table/index.ts',
|
|
131
133
|
],
|
|
134
|
+
peerFiles: [
|
|
135
|
+
'context-menu-integrations.ts',
|
|
136
|
+
],
|
|
132
137
|
dependencies: [
|
|
133
138
|
'table',
|
|
134
139
|
'input',
|
|
@@ -139,8 +144,13 @@ export const registry = {
|
|
|
139
144
|
'popover',
|
|
140
145
|
'component-outlet',
|
|
141
146
|
'icon',
|
|
147
|
+
'command',
|
|
148
|
+
'badge',
|
|
142
149
|
],
|
|
143
150
|
libFiles: ['xlsx.ts'],
|
|
151
|
+
optionalDependencies: [
|
|
152
|
+
{ name: 'context-menu', description: 'Enables right-click context menus on rows and headers' },
|
|
153
|
+
],
|
|
144
154
|
},
|
|
145
155
|
dialog: {
|
|
146
156
|
name: 'dialog',
|
|
@@ -337,6 +347,9 @@ export const registry = {
|
|
|
337
347
|
name: 'tree',
|
|
338
348
|
files: ['tree.component.ts'],
|
|
339
349
|
dependencies: ['icon'],
|
|
350
|
+
optionalDependencies: [
|
|
351
|
+
{ name: 'context-menu', description: 'Enables right-click context menus on tree nodes' },
|
|
352
|
+
],
|
|
340
353
|
},
|
|
341
354
|
'speed-dial': {
|
|
342
355
|
name: 'speed-dial',
|
package/dist/utils/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
export async function getPackageManager(cwd) {
|
|
5
5
|
const userAgent = process.env['npm_config_user_agent'];
|
|
6
6
|
if (userAgent) {
|
|
@@ -29,12 +29,7 @@ export async function installPackages(packages, options) {
|
|
|
29
29
|
if (options.dev)
|
|
30
30
|
args.push('-D');
|
|
31
31
|
}
|
|
32
|
-
else if (packageManager === 'yarn') {
|
|
33
|
-
args.push('add');
|
|
34
|
-
if (options.dev)
|
|
35
|
-
args.push('-D');
|
|
36
|
-
}
|
|
37
|
-
else if (packageManager === 'pnpm') {
|
|
32
|
+
else if (packageManager === 'yarn' || packageManager === 'pnpm') {
|
|
38
33
|
args.push('add');
|
|
39
34
|
if (options.dev)
|
|
40
35
|
args.push('-D');
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { resolveDependencies, promptOptionalDependencies } from './add.js';
|
|
3
|
+
import { registry, type ComponentName, type ComponentDefinition } from '../registry/index.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// resolveDependencies
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
describe('resolveDependencies', () => {
|
|
10
|
+
it('resolves a single component without dependencies', () => {
|
|
11
|
+
const result = resolveDependencies(['badge']);
|
|
12
|
+
expect(result).toContain('badge');
|
|
13
|
+
expect(result.size).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('includes transitive dependencies', () => {
|
|
17
|
+
// button depends on ripple
|
|
18
|
+
const result = resolveDependencies(['button']);
|
|
19
|
+
expect(result).toContain('button');
|
|
20
|
+
expect(result).toContain('ripple');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('resolves deep transitive chains', () => {
|
|
24
|
+
// date-picker -> calendar -> button -> ripple, calendar -> select
|
|
25
|
+
const result = resolveDependencies(['date-picker']);
|
|
26
|
+
expect(result).toContain('date-picker');
|
|
27
|
+
expect(result).toContain('calendar');
|
|
28
|
+
expect(result).toContain('button');
|
|
29
|
+
expect(result).toContain('ripple');
|
|
30
|
+
expect(result).toContain('select');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('deduplicates shared dependencies across multiple inputs', () => {
|
|
34
|
+
// Both button-group and speed-dial depend on button
|
|
35
|
+
const result = resolveDependencies(['button-group', 'speed-dial']);
|
|
36
|
+
expect(result).toContain('button-group');
|
|
37
|
+
expect(result).toContain('speed-dial');
|
|
38
|
+
expect(result).toContain('button');
|
|
39
|
+
expect(result).toContain('ripple');
|
|
40
|
+
|
|
41
|
+
// Count how many times button appears — should be exactly once (it's a Set)
|
|
42
|
+
const asArray = [...result];
|
|
43
|
+
expect(asArray.filter((n: string) => n === 'button')).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns a Set containing all resolved names', () => {
|
|
47
|
+
const result = resolveDependencies(['badge']);
|
|
48
|
+
expect(result).toBeInstanceOf(Set);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles components with no registry deps gracefully', () => {
|
|
52
|
+
const result = resolveDependencies(['separator']);
|
|
53
|
+
expect(result.size).toBe(1);
|
|
54
|
+
expect(result).toContain('separator');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// promptOptionalDependencies
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
vi.mock('prompts', () => ({
|
|
63
|
+
default: vi.fn(),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
describe('promptOptionalDependencies', () => {
|
|
67
|
+
it('returns empty array when no components have optional deps', async () => {
|
|
68
|
+
const resolved = new Set<ComponentName>(['badge', 'button', 'ripple']);
|
|
69
|
+
const result = await promptOptionalDependencies(resolved, { yes: false, branch: 'master' });
|
|
70
|
+
expect(result).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns empty array with --yes flag (skip prompts)', async () => {
|
|
74
|
+
const resolved = new Set<ComponentName>(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
75
|
+
const result = await promptOptionalDependencies(resolved, { yes: true, branch: 'master' });
|
|
76
|
+
expect(result).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns all optional dep names with --all flag', async () => {
|
|
80
|
+
const resolved = new Set<ComponentName>(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
81
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
82
|
+
expect(result).toContain('context-menu');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('filters out optional deps already in the resolved set', async () => {
|
|
86
|
+
// context-menu is already resolved, so it should not be offered
|
|
87
|
+
const resolved = new Set<ComponentName>(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
88
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
89
|
+
expect(result).not.toContain('context-menu');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('deduplicates optional deps across components', async () => {
|
|
93
|
+
// Both data-table and tree have context-menu as optional
|
|
94
|
+
const resolved = new Set<ComponentName>(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
95
|
+
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
96
|
+
const contextMenuCount = result.filter((n: string) => n === 'context-menu').length;
|
|
97
|
+
expect(contextMenuCount).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Registry data integrity
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe('registry optional dependencies', () => {
|
|
106
|
+
it('data-table has context-menu as optional dependency', () => {
|
|
107
|
+
const dt = registry['data-table'];
|
|
108
|
+
expect(dt.optionalDependencies).toBeDefined();
|
|
109
|
+
const names = dt.optionalDependencies!.map((d: { name: string }) => d.name);
|
|
110
|
+
expect(names).toContain('context-menu');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('tree has context-menu as optional dependency', () => {
|
|
114
|
+
const tree = registry['tree'];
|
|
115
|
+
expect(tree.optionalDependencies).toBeDefined();
|
|
116
|
+
const names = tree.optionalDependencies!.map((d: { name: string }) => d.name);
|
|
117
|
+
expect(names).toContain('context-menu');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('every optional dependency name is a valid registry key', () => {
|
|
121
|
+
for (const [componentName, definition] of Object.entries(registry) as [string, ComponentDefinition][]) {
|
|
122
|
+
if (!definition.optionalDependencies) continue;
|
|
123
|
+
for (const opt of definition.optionalDependencies) {
|
|
124
|
+
expect(
|
|
125
|
+
registry[opt.name],
|
|
126
|
+
`Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`,
|
|
127
|
+
).toBeDefined();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('every dependency name is a valid registry key', () => {
|
|
133
|
+
for (const [componentName, definition] of Object.entries(registry) as [string, ComponentDefinition][]) {
|
|
134
|
+
if (!definition.dependencies) continue;
|
|
135
|
+
for (const dep of definition.dependencies) {
|
|
136
|
+
expect(
|
|
137
|
+
registry[dep],
|
|
138
|
+
`Dependency "${dep}" in "${componentName}" is not a valid registry key`,
|
|
139
|
+
).toBeDefined();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// help command
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
describe('help command', () => {
|
|
150
|
+
it('prints commands, optional dependencies, and component categories', async () => {
|
|
151
|
+
const { help } = await import('./help.js');
|
|
152
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
153
|
+
|
|
154
|
+
help();
|
|
155
|
+
|
|
156
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
157
|
+
const output = spy.mock.calls[0][0] as string;
|
|
158
|
+
|
|
159
|
+
expect(output).toContain('Commands');
|
|
160
|
+
expect(output).toContain('init');
|
|
161
|
+
expect(output).toContain('add');
|
|
162
|
+
expect(output).toContain('Optional Dependencies');
|
|
163
|
+
expect(output).toContain('Available Components');
|
|
164
|
+
expect(output).toContain('UI');
|
|
165
|
+
expect(output).toContain('Charts');
|
|
166
|
+
expect(output).toContain('Animation');
|
|
167
|
+
|
|
168
|
+
spy.mockRestore();
|
|
169
|
+
});
|
|
170
|
+
});
|