@gilav21/shadcn-angular 0.0.15 → 0.0.17

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.
@@ -7,10 +7,12 @@ import ora from 'ora';
7
7
  import { getConfig } from '../utils/config.js';
8
8
  import { registry } from '../registry/index.js';
9
9
  import { installPackages } from '../utils/package-manager.js';
10
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
12
13
  // Base URL for the component registry (GitHub raw content)
13
14
  const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
15
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
14
16
  // Components source directory (relative to CLI dist folder) for local dev
15
17
  function getLocalComponentsDir() {
16
18
  // From dist/commands/add.js -> packages/components/ui
@@ -25,6 +27,26 @@ function getLocalComponentsDir() {
25
27
  }
26
28
  return null;
27
29
  }
30
+ function getLocalLibDir() {
31
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
32
+ if (fs.existsSync(fromDist)) {
33
+ return fromDist;
34
+ }
35
+ return null;
36
+ }
37
+ function resolveProjectPath(cwd, inputPath) {
38
+ const resolved = path.resolve(cwd, inputPath);
39
+ const relative = path.relative(cwd, resolved);
40
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
41
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ function aliasToProjectPath(aliasOrPath) {
46
+ return aliasOrPath.startsWith('@/')
47
+ ? path.join('src', aliasOrPath.slice(2))
48
+ : aliasOrPath;
49
+ }
28
50
  async function fetchComponentContent(file, options) {
29
51
  const localDir = getLocalComponentsDir();
30
52
  // 1. Prefer local if available and not forced remote
@@ -50,6 +72,36 @@ async function fetchComponentContent(file, options) {
50
72
  throw error;
51
73
  }
52
74
  }
75
+ async function fetchLibContent(file, options) {
76
+ const localDir = getLocalLibDir();
77
+ if (localDir && !options.remote) {
78
+ const localPath = path.join(localDir, file);
79
+ if (await fs.pathExists(localPath)) {
80
+ return fs.readFile(localPath, 'utf-8');
81
+ }
82
+ }
83
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
84
+ const response = await fetch(url);
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
87
+ }
88
+ return response.text();
89
+ }
90
+ function collectInstalledShortcutEntries(targetDir) {
91
+ const entries = [];
92
+ for (const definition of Object.values(registry)) {
93
+ if (!definition.shortcutDefinitions?.length) {
94
+ continue;
95
+ }
96
+ for (const shortcutDefinition of definition.shortcutDefinitions) {
97
+ const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
98
+ if (fs.existsSync(sourcePath)) {
99
+ entries.push(shortcutDefinition);
100
+ }
101
+ }
102
+ }
103
+ return entries;
104
+ }
53
105
  export async function add(components, options) {
54
106
  const cwd = process.cwd();
55
107
  // Load config
@@ -103,9 +155,8 @@ export async function add(components, options) {
103
155
  }
104
156
  };
105
157
  componentsToAdd.forEach(c => resolveDeps(c));
106
- const targetDir = options.path
107
- ? path.join(cwd, options.path)
108
- : path.join(cwd, 'src/components/ui');
158
+ const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
159
+ const targetDir = resolveProjectPath(cwd, uiBasePath);
109
160
  // Check for existing files and diff
110
161
  const componentsToInstall = [];
111
162
  const componentsToSkip = [];
@@ -122,10 +173,10 @@ export async function add(components, options) {
122
173
  const localContent = await fs.readFile(targetPath, 'utf-8');
123
174
  try {
124
175
  let remoteContent = await fetchComponentContent(file, options);
125
- // Transform imports for comparison
126
- const utilsAlias = config.aliases.utils;
127
- remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
128
- const normalize = (str) => str.replace(/\s+/g, '').trim();
176
+ // Transform all lib/ imports for comparison
177
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
178
+ remoteContent = remoteContent.replace(/(\.\.\/)+lib\//g, libAlias + '/');
179
+ const normalize = (str) => str.replace(/\r\n/g, '\n').trim();
129
180
  if (normalize(localContent) !== normalize(remoteContent)) {
130
181
  hasChanges = true;
131
182
  }
@@ -204,9 +255,9 @@ export async function add(components, options) {
204
255
  let content = contentCache.get(file);
205
256
  if (!content) {
206
257
  content = await fetchComponentContent(file, options);
207
- // Transform imports if not already transformed (cached is transformed)
208
- const utilsAlias = config.aliases.utils;
209
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
258
+ // Transform all lib/ imports if not already transformed (cached is transformed)
259
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
260
+ content = content.replace(/(\.\.\/)+lib\//g, libAlias + '/');
210
261
  }
211
262
  await fs.ensureDir(path.dirname(targetPath));
212
263
  await fs.writeFile(targetPath, content);
@@ -232,6 +283,33 @@ export async function add(components, options) {
232
283
  else {
233
284
  spinner.info('No new components installed.');
234
285
  }
286
+ // Install required lib utility files
287
+ if (finalComponents.length > 0) {
288
+ const requiredLibFiles = new Set();
289
+ for (const name of allComponents) {
290
+ const component = registry[name];
291
+ if (component.libFiles) {
292
+ component.libFiles.forEach(f => requiredLibFiles.add(f));
293
+ }
294
+ }
295
+ if (requiredLibFiles.size > 0) {
296
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
297
+ const libDir = path.dirname(utilsPathResolved);
298
+ await fs.ensureDir(libDir);
299
+ for (const libFile of requiredLibFiles) {
300
+ const libTargetPath = path.join(libDir, libFile);
301
+ if (!await fs.pathExists(libTargetPath) || options.overwrite) {
302
+ try {
303
+ const libContent = await fetchLibContent(libFile, options);
304
+ await fs.writeFile(libTargetPath, libContent);
305
+ }
306
+ catch (err) {
307
+ console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${err.message}`));
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
235
313
  if (finalComponents.length > 0) {
236
314
  const npmDependencies = new Set();
237
315
  for (const name of finalComponents) {
@@ -252,6 +330,18 @@ export async function add(components, options) {
252
330
  }
253
331
  }
254
332
  }
333
+ const shortcutEntries = collectInstalledShortcutEntries(targetDir);
334
+ if (shortcutEntries.length > 0) {
335
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
336
+ const utilsDir = path.dirname(utilsPathResolved);
337
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
338
+ if (!await fs.pathExists(shortcutServicePath)) {
339
+ const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
340
+ await fs.ensureDir(utilsDir);
341
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
342
+ }
343
+ }
344
+ await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
255
345
  if (componentsToSkip.length > 0) {
256
346
  console.log('\n' + chalk.dim('Components skipped (up to date):'));
257
347
  componentsToSkip.forEach(name => {
@@ -1,12 +1,53 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import prompts from 'prompts';
4
5
  import chalk from 'chalk';
5
6
  import ora from 'ora';
6
- import { execa } from 'execa';
7
7
  import { getDefaultConfig } from '../utils/config.js';
8
8
  import { getStylesTemplate } from '../templates/styles.js';
9
9
  import { getUtilsTemplate } from '../templates/utils.js';
10
+ import { installPackages } from '../utils/package-manager.js';
11
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
12
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ function getLocalLibDir() {
16
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
17
+ if (fs.existsSync(fromDist)) {
18
+ return fromDist;
19
+ }
20
+ return null;
21
+ }
22
+ async function fetchLibFileContent(file) {
23
+ const localLibDir = getLocalLibDir();
24
+ if (localLibDir) {
25
+ const localPath = path.join(localLibDir, file);
26
+ if (await fs.pathExists(localPath)) {
27
+ return fs.readFile(localPath, 'utf-8');
28
+ }
29
+ }
30
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
31
+ const response = await fetch(url);
32
+ if (!response.ok) {
33
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
34
+ }
35
+ return response.text();
36
+ }
37
+ function resolveProjectPath(cwd, inputPath) {
38
+ const resolved = path.resolve(cwd, inputPath);
39
+ const relative = path.relative(cwd, resolved);
40
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
41
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ function resolveAliasOrPath(cwd, aliasOrPath) {
46
+ const normalized = aliasOrPath.startsWith('@/')
47
+ ? path.join('src', aliasOrPath.slice(2))
48
+ : aliasOrPath;
49
+ return resolveProjectPath(cwd, normalized);
50
+ }
10
51
  export async function init(options) {
11
52
  console.log(chalk.bold('\n🎨 Welcome to shadcn-angular!\n'));
12
53
  const cwd = process.cwd();
@@ -20,20 +61,24 @@ export async function init(options) {
20
61
  // Check if already initialized
21
62
  const componentsJsonPath = path.join(cwd, 'components.json');
22
63
  if (await fs.pathExists(componentsJsonPath)) {
23
- const { overwrite } = await prompts({
24
- type: 'confirm',
25
- name: 'overwrite',
26
- message: 'components.json already exists. Overwrite?',
27
- initial: false,
28
- });
64
+ const overwrite = options.yes
65
+ ? true
66
+ : (await prompts({
67
+ type: 'confirm',
68
+ name: 'overwrite',
69
+ message: 'components.json already exists. Overwrite?',
70
+ initial: false,
71
+ })).overwrite;
29
72
  if (!overwrite) {
30
73
  console.log(chalk.dim('Initialization cancelled.'));
31
74
  return;
32
75
  }
33
76
  }
34
77
  let config;
78
+ let createShortcutRegistry = true;
35
79
  if (options.defaults || options.yes) {
36
80
  config = getDefaultConfig();
81
+ createShortcutRegistry = true;
37
82
  }
38
83
  else {
39
84
  const THEME_COLORS = {
@@ -115,6 +160,12 @@ export async function init(options) {
115
160
  message: 'Where is your global styles file?',
116
161
  initial: 'src/styles.scss',
117
162
  },
163
+ {
164
+ type: 'confirm',
165
+ name: 'createShortcutRegistry',
166
+ message: 'Would you like to create a shortcut registry scaffold?',
167
+ initial: true,
168
+ },
118
169
  ]);
119
170
  config = {
120
171
  $schema: 'https://shadcn-angular.dev/schema.json',
@@ -131,6 +182,7 @@ export async function init(options) {
131
182
  ui: responses.componentsPath.replace('src/', '@/'),
132
183
  },
133
184
  };
185
+ createShortcutRegistry = responses.createShortcutRegistry ?? true;
134
186
  }
135
187
  const spinner = ora('Initializing project...').start();
136
188
  try {
@@ -143,19 +195,28 @@ export async function init(options) {
143
195
  // So we should rely on config to reconstruct the path, or better yet, if we are in 'defaults' mode, check what config is.
144
196
  // If config came from defaults, aliases are set.
145
197
  // We can reverse-map alias to path: @/ -> src/
146
- const utilsPathResolved = config.aliases.utils.replace('@/', 'src/');
147
- const utilsDir = path.dirname(path.join(cwd, utilsPathResolved + '.ts')); // utils usually ends in path/to/utils
198
+ const utilsPathResolved = resolveAliasOrPath(cwd, config.aliases.utils + '.ts');
199
+ const utilsDir = path.dirname(utilsPathResolved); // utils usually ends in path/to/utils
148
200
  await fs.ensureDir(utilsDir);
149
- await fs.writeFile(path.join(cwd, utilsPathResolved + '.ts'), getUtilsTemplate());
201
+ await fs.writeFile(utilsPathResolved, getUtilsTemplate());
150
202
  spinner.text = 'Created utils.ts';
203
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
204
+ const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts');
205
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
206
+ spinner.text = 'Created shortcut-binding.service.ts';
207
+ if (createShortcutRegistry) {
208
+ await writeShortcutRegistryIndex(cwd, config, []);
209
+ spinner.text = 'Created shortcut-registry.index.ts';
210
+ }
151
211
  // Create tailwind.css file in the same directory as the global styles
152
- const stylesDir = path.dirname(path.join(cwd, config.tailwind.css));
212
+ const userStylesPath = resolveProjectPath(cwd, config.tailwind.css);
213
+ const stylesDir = path.dirname(userStylesPath);
153
214
  const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
154
215
  // Write the tailwind.css file with all Tailwind directives
216
+ await fs.ensureDir(stylesDir);
155
217
  await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
156
218
  spinner.text = 'Created tailwind.css';
157
219
  // Add import to the user's global styles file if not already present
158
- const userStylesPath = path.join(cwd, config.tailwind.css);
159
220
  let userStyles = await fs.pathExists(userStylesPath)
160
221
  ? await fs.readFile(userStylesPath, 'utf-8')
161
222
  : '';
@@ -167,8 +228,7 @@ export async function init(options) {
167
228
  spinner.text = 'Added tailwind.css import to styles';
168
229
  }
169
230
  // Create components/ui directory
170
- const uiPathResolved = config.aliases.ui.replace('@/', 'src/');
171
- const uiDir = path.join(cwd, uiPathResolved);
231
+ const uiDir = resolveAliasOrPath(cwd, config.aliases.ui);
172
232
  await fs.ensureDir(uiDir);
173
233
  spinner.text = 'Created components directory';
174
234
  // Install dependencies
@@ -181,7 +241,7 @@ export async function init(options) {
181
241
  'postcss',
182
242
  '@tailwindcss/postcss'
183
243
  ];
184
- await execa('npm', ['install', ...dependencies], { cwd });
244
+ await installPackages(dependencies, { cwd });
185
245
  // Setup PostCSS - create .postcssrc.json which is the preferred format for Angular
186
246
  spinner.text = 'Configuring PostCSS...';
187
247
  const postcssrcPath = path.join(cwd, '.postcssrc.json');
@@ -3,6 +3,12 @@ export interface ComponentDefinition {
3
3
  files: string[];
4
4
  dependencies?: string[];
5
5
  npmDependencies?: string[];
6
+ libFiles?: string[];
7
+ shortcutDefinitions?: {
8
+ exportName: string;
9
+ componentName: string;
10
+ sourceFile: string;
11
+ }[];
6
12
  }
7
13
  export type ComponentName = keyof typeof registry;
8
14
  export declare const registry: Record<string, ComponentDefinition>;
@@ -39,6 +39,7 @@ export const registry = {
39
39
  button: {
40
40
  name: 'button',
41
41
  files: ['button.component.ts'],
42
+ dependencies: ['ripple'],
42
43
  },
43
44
  'button-group': {
44
45
  name: 'button-group',
@@ -79,6 +80,13 @@ export const registry = {
79
80
  name: 'command',
80
81
  files: ['command.component.ts'],
81
82
  dependencies: ['dialog'],
83
+ shortcutDefinitions: [
84
+ {
85
+ exportName: 'COMMAND_DIALOG_SHORTCUT_DEFINITIONS',
86
+ componentName: 'command-dialog',
87
+ sourceFile: 'command.component.ts',
88
+ },
89
+ ],
82
90
  },
83
91
  'context-menu': {
84
92
  name: 'context-menu',
@@ -132,6 +140,7 @@ export const registry = {
132
140
  'component-outlet',
133
141
  'icon',
134
142
  ],
143
+ libFiles: ['xlsx.ts'],
135
144
  },
136
145
  dialog: {
137
146
  name: 'dialog',
@@ -342,7 +351,7 @@ export const registry = {
342
351
  'emoji-picker': {
343
352
  name: 'emoji-picker',
344
353
  files: ['emoji-picker.component.ts', 'emoji-data.ts'],
345
- dependencies: ['button', 'input', 'scroll-area', 'popover'],
354
+ dependencies: ['input', 'scroll-area', 'tooltip'],
346
355
  },
347
356
  'rich-text-editor': {
348
357
  name: 'rich-text-editor',
@@ -351,18 +360,31 @@ export const registry = {
351
360
  'rich-text-toolbar.component.ts',
352
361
  'rich-text-sanitizer.service.ts',
353
362
  'rich-text-markdown.service.ts',
363
+ 'rich-text-paste-normalizer.service.ts',
364
+ 'rich-text-command-registry.service.ts',
354
365
  'rich-text-mention.component.ts',
355
366
  'rich-text-image-resizer.component.ts',
367
+ 'rich-text-locales.ts',
356
368
  ],
357
369
  dependencies: [
358
370
  'button',
359
371
  'separator',
360
372
  'popover',
361
373
  'emoji-picker',
374
+ 'autocomplete',
362
375
  'select',
363
376
  'input',
377
+ 'dialog',
364
378
  'scroll-area',
365
379
  ],
380
+ libFiles: ['pdf-parser.ts', 'image-validator.ts', 'svg-sanitizer.ts'],
381
+ shortcutDefinitions: [
382
+ {
383
+ exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
384
+ componentName: 'rich-text-editor',
385
+ sourceFile: 'rich-text-editor.component.ts',
386
+ },
387
+ ],
366
388
  },
367
389
  // Chart Components
368
390
  'pie-chart': {
@@ -464,4 +486,75 @@ export const registry = {
464
486
  files: ['split-button.component.ts'],
465
487
  dependencies: ['button', 'dropdown-menu'],
466
488
  },
489
+ // Animations
490
+ 'gradient-text': {
491
+ name: 'gradient-text',
492
+ files: ['gradient-text.component.ts'],
493
+ },
494
+ 'flip-text': {
495
+ name: 'flip-text',
496
+ files: ['flip-text.component.ts'],
497
+ },
498
+ meteors: {
499
+ name: 'meteors',
500
+ files: ['meteors.component.ts'],
501
+ },
502
+ 'shine-border': {
503
+ name: 'shine-border',
504
+ files: ['shine-border.component.ts'],
505
+ },
506
+ 'scroll-progress': {
507
+ name: 'scroll-progress',
508
+ files: ['scroll-progress.component.ts'],
509
+ },
510
+ 'blur-fade': {
511
+ name: 'blur-fade',
512
+ files: ['blur-fade.component.ts'],
513
+ },
514
+ ripple: {
515
+ name: 'ripple',
516
+ files: ['ripple.directive.ts'],
517
+ },
518
+ marquee: {
519
+ name: 'marquee',
520
+ files: ['marquee.component.ts'],
521
+ },
522
+ 'word-rotate': {
523
+ name: 'word-rotate',
524
+ files: ['word-rotate.component.ts'],
525
+ },
526
+ 'morphing-text': {
527
+ name: 'morphing-text',
528
+ files: ['morphing-text.component.ts'],
529
+ },
530
+ 'typing-animation': {
531
+ name: 'typing-animation',
532
+ files: ['typing-animation.component.ts'],
533
+ },
534
+ 'wobble-card': {
535
+ name: 'wobble-card',
536
+ files: ['wobble-card.component.ts'],
537
+ },
538
+ magnetic: {
539
+ name: 'magnetic',
540
+ files: ['magnetic.directive.ts'],
541
+ },
542
+ orbit: {
543
+ name: 'orbit',
544
+ files: ['orbit.component.ts'],
545
+ },
546
+ 'stagger-children': {
547
+ name: 'stagger-children',
548
+ files: ['stagger-children.component.ts'],
549
+ },
550
+ particles: {
551
+ name: 'particles',
552
+ files: ['particles.component.ts'],
553
+ },
554
+ // Kanban
555
+ kanban: {
556
+ name: 'kanban',
557
+ files: ['kanban.component.ts'],
558
+ dependencies: ['badge', 'avatar', 'scroll-area', 'separator'],
559
+ },
467
560
  };
@@ -6,7 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
  * Utility function for merging Tailwind CSS classes with proper precedence
7
7
  */
8
8
  export function cn(...inputs: ClassValue[]): string {
9
- return twMerge(clsx(inputs));
9
+ return twMerge(clsx(inputs));
10
10
  }
11
11
 
12
12
  /**
@@ -17,5 +17,35 @@ export function isRtl(el: HTMLElement): boolean {
17
17
  return getComputedStyle(el).direction === 'rtl';
18
18
  }
19
19
 
20
+ /**
21
+ * Returns the bounding rect of the nearest ancestor that clips overflow
22
+ * (overflow: hidden | auto | scroll | clip on either axis).
23
+ * Falls back to the full viewport rect when no such ancestor exists.
24
+ *
25
+ * Use this instead of \`window.innerWidth/innerHeight\` when calculating
26
+ * popup collision boundaries so that containers like sidebars or
27
+ * fixed-height scroll panes are respected.
28
+ */
29
+ export function getClippingRect(element: HTMLElement): DOMRect {
30
+ let parent = element.parentElement;
31
+ while (parent && parent !== document.documentElement) {
32
+ const style = window.getComputedStyle(parent);
33
+ if (
34
+ /^(hidden|auto|scroll|clip)$/.test(style.overflowX) ||
35
+ /^(hidden|auto|scroll|clip)$/.test(style.overflowY)
36
+ ) {
37
+ return parent.getBoundingClientRect();
38
+ }
39
+ parent = parent.parentElement;
40
+ }
41
+ return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
42
+ }
43
+
44
+ /**
45
+ * Check if the user prefers reduced motion via the OS-level accessibility setting.
46
+ */
47
+ export function prefersReducedMotion(): boolean {
48
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
49
+ }
20
50
  `;
21
51
  }
@@ -0,0 +1,7 @@
1
+ import type { Config } from './config.js';
2
+ export interface ShortcutRegistryEntry {
3
+ exportName: string;
4
+ componentName: string;
5
+ sourceFile: string;
6
+ }
7
+ export declare function writeShortcutRegistryIndex(cwd: string, config: Config, entries: ShortcutRegistryEntry[]): Promise<string>;
@@ -0,0 +1,58 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ function aliasToProjectPath(aliasOrPath) {
4
+ return aliasOrPath.startsWith('@/')
5
+ ? path.join('src', aliasOrPath.slice(2))
6
+ : aliasOrPath;
7
+ }
8
+ function resolveProjectPath(cwd, inputPath) {
9
+ const resolved = path.resolve(cwd, inputPath);
10
+ const relative = path.relative(cwd, resolved);
11
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
12
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
13
+ }
14
+ return resolved;
15
+ }
16
+ function getShortcutRegistryIndexPath(cwd, config) {
17
+ const utilsFilePath = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
18
+ const utilsDir = path.dirname(utilsFilePath);
19
+ return path.join(utilsDir, 'shortcut-registry.index.ts');
20
+ }
21
+ export async function writeShortcutRegistryIndex(cwd, config, entries) {
22
+ const registryPath = getShortcutRegistryIndexPath(cwd, config);
23
+ await fs.ensureDir(path.dirname(registryPath));
24
+ const uniqueEntries = Array.from(new Map(entries.map(entry => [entry.exportName, entry])).values())
25
+ .sort((a, b) => a.exportName.localeCompare(b.exportName));
26
+ const uiAlias = config.aliases.ui;
27
+ const utilsAliasDir = config.aliases.utils.includes('/')
28
+ ? config.aliases.utils.slice(0, config.aliases.utils.lastIndexOf('/'))
29
+ : config.aliases.utils;
30
+ const shortcutServiceImport = `${utilsAliasDir}/shortcut-binding.service`;
31
+ const imports = uniqueEntries
32
+ .map(entry => {
33
+ const importPath = `${uiAlias}/${entry.sourceFile.replace(/\.ts$/, '')}`;
34
+ return `import { ${entry.exportName} } from '${importPath}';`;
35
+ })
36
+ .join('\n');
37
+ const catalogItems = uniqueEntries
38
+ .map(entry => ` { componentName: '${entry.componentName}', definitions: ${entry.exportName} },`)
39
+ .join('\n');
40
+ const content = `// Auto-generated by shadcn-angular CLI. Do not edit manually.
41
+ import type { ShortcutBindingService, ShortcutDefinition } from '${shortcutServiceImport}';
42
+ ${imports ? `${imports}\n` : ''}
43
+ export const GENERATED_SHORTCUT_CATALOG: ReadonlyArray<{
44
+ componentName: string;
45
+ definitions: ReadonlyArray<ShortcutDefinition>;
46
+ }> = [
47
+ ${catalogItems}
48
+ ];
49
+
50
+ export function registerGeneratedShortcutCatalog(shortcuts: ShortcutBindingService): void {
51
+ for (const entry of GENERATED_SHORTCUT_CATALOG) {
52
+ shortcuts.defineShortcuts(entry.componentName, [...entry.definitions]);
53
+ }
54
+ }
55
+ `;
56
+ await fs.writeFile(registryPath, content);
57
+ return registryPath;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"