@gilav21/shadcn-angular 0.0.14 → 0.0.16

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.
@@ -4,18 +4,20 @@ import { fileURLToPath } from 'url';
4
4
  import prompts from 'prompts';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
7
- import { getConfig } from '../utils/config.js';
8
- import { registry, type ComponentName } from '../registry/index.js';
9
- import { installPackages } from '../utils/package-manager.js';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { registry, type ComponentName } from '../registry/index.js';
9
+ import { installPackages } from '../utils/package-manager.js';
10
+ import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
13
14
 
14
15
  // Base URL for the component registry (GitHub raw content)
15
- const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
16
+ const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
17
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
16
18
 
17
19
  // Components source directory (relative to CLI dist folder) for local dev
18
- function getLocalComponentsDir(): string | null {
20
+ function getLocalComponentsDir(): string | null {
19
21
  // From dist/commands/add.js -> packages/components/ui
20
22
  const fromDist = path.resolve(__dirname, '../../../components/ui');
21
23
  if (fs.existsSync(fromDist)) {
@@ -29,15 +31,38 @@ function getLocalComponentsDir(): string | null {
29
31
  return null;
30
32
  }
31
33
 
32
- interface AddOptions {
34
+ interface AddOptions {
33
35
  yes?: boolean;
34
36
  overwrite?: boolean;
35
37
  all?: boolean;
36
38
  path?: string;
37
- remote?: boolean; // Force remote fetch
38
- }
39
-
40
- async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
39
+ remote?: boolean; // Force remote fetch
40
+ }
41
+
42
+ function getLocalLibDir(): string | null {
43
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
44
+ if (fs.existsSync(fromDist)) {
45
+ return fromDist;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function resolveProjectPath(cwd: string, inputPath: string): string {
51
+ const resolved = path.resolve(cwd, inputPath);
52
+ const relative = path.relative(cwd, resolved);
53
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
54
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
55
+ }
56
+ return resolved;
57
+ }
58
+
59
+ function aliasToProjectPath(aliasOrPath: string): string {
60
+ return aliasOrPath.startsWith('@/')
61
+ ? path.join('src', aliasOrPath.slice(2))
62
+ : aliasOrPath;
63
+ }
64
+
65
+ async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
41
66
  const localDir = getLocalComponentsDir();
42
67
 
43
68
  // 1. Prefer local if available and not forced remote
@@ -62,7 +87,41 @@ async function fetchComponentContent(file: string, options: AddOptions): Promise
62
87
  }
63
88
  throw error;
64
89
  }
65
- }
90
+ }
91
+
92
+ async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
93
+ const localDir = getLocalLibDir();
94
+
95
+ if (localDir && !options.remote) {
96
+ const localPath = path.join(localDir, file);
97
+ if (await fs.pathExists(localPath)) {
98
+ return fs.readFile(localPath, 'utf-8');
99
+ }
100
+ }
101
+
102
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
103
+ const response = await fetch(url);
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
106
+ }
107
+ return response.text();
108
+ }
109
+
110
+ function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEntry[] {
111
+ const entries: ShortcutRegistryEntry[] = [];
112
+ for (const definition of Object.values(registry)) {
113
+ if (!definition.shortcutDefinitions?.length) {
114
+ continue;
115
+ }
116
+ for (const shortcutDefinition of definition.shortcutDefinitions) {
117
+ const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
118
+ if (fs.existsSync(sourcePath)) {
119
+ entries.push(shortcutDefinition);
120
+ }
121
+ }
122
+ }
123
+ return entries;
124
+ }
66
125
 
67
126
  export async function add(components: string[], options: AddOptions) {
68
127
  const cwd = process.cwd();
@@ -121,9 +180,8 @@ export async function add(components: string[], options: AddOptions) {
121
180
  };
122
181
  componentsToAdd.forEach(c => resolveDeps(c));
123
182
 
124
- const targetDir = options.path
125
- ? path.join(cwd, options.path)
126
- : path.join(cwd, 'src/components/ui');
183
+ const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
184
+ const targetDir = resolveProjectPath(cwd, uiBasePath);
127
185
 
128
186
  // Check for existing files and diff
129
187
  const componentsToInstall: ComponentName[] = [];
@@ -149,7 +207,7 @@ export async function add(components: string[], options: AddOptions) {
149
207
  const utilsAlias = config.aliases.utils;
150
208
  remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
151
209
 
152
- const normalize = (str: string) => str.replace(/\s+/g, '').trim();
210
+ const normalize = (str: string) => str.replace(/\r\n/g, '\n').trim();
153
211
  if (normalize(localContent) !== normalize(remoteContent)) {
154
212
  hasChanges = true;
155
213
  }
@@ -265,8 +323,8 @@ export async function add(components: string[], options: AddOptions) {
265
323
  spinner.info('No new components installed.');
266
324
  }
267
325
 
268
- if (finalComponents.length > 0) {
269
- const npmDependencies = new Set<string>();
326
+ if (finalComponents.length > 0) {
327
+ const npmDependencies = new Set<string>();
270
328
  for (const name of finalComponents) {
271
329
  const component = registry[name];
272
330
  if (component.npmDependencies) {
@@ -283,11 +341,26 @@ export async function add(components: string[], options: AddOptions) {
283
341
  depSpinner.fail('Failed to install dependencies.');
284
342
  console.error(e);
285
343
  }
286
- }
287
- }
288
-
289
- if (componentsToSkip.length > 0) {
290
- console.log('\n' + chalk.dim('Components skipped (up to date):'));
344
+ }
345
+ }
346
+
347
+ const shortcutEntries = collectInstalledShortcutEntries(targetDir);
348
+ if (shortcutEntries.length > 0) {
349
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
350
+ const utilsDir = path.dirname(utilsPathResolved);
351
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
352
+
353
+ if (!await fs.pathExists(shortcutServicePath)) {
354
+ const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
355
+ await fs.ensureDir(utilsDir);
356
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
357
+ }
358
+ }
359
+
360
+ await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
361
+
362
+ if (componentsToSkip.length > 0) {
363
+ console.log('\n' + chalk.dim('Components skipped (up to date):'));
291
364
  componentsToSkip.forEach(name => {
292
365
  console.log(chalk.dim(' - ') + chalk.gray(name));
293
366
  });
@@ -1,19 +1,66 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import prompts from 'prompts';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { execa } from 'execa';
7
- import { getDefaultConfig, type Config } from '../utils/config.js';
8
- import { getStylesTemplate } from '../templates/styles.js';
9
- import { getUtilsTemplate } from '../templates/utils.js';
10
-
11
- interface InitOptions {
12
- yes?: boolean;
13
- defaults?: boolean;
14
- }
15
-
16
- export async function init(options: InitOptions) {
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import prompts from 'prompts';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { getDefaultConfig, type Config } from '../utils/config.js';
8
+ import { getStylesTemplate } from '../templates/styles.js';
9
+ import { getUtilsTemplate } from '../templates/utils.js';
10
+ import { installPackages } from '../utils/package-manager.js';
11
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
12
+
13
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ function getLocalLibDir(): string | null {
18
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
19
+ if (fs.existsSync(fromDist)) {
20
+ return fromDist;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ async function fetchLibFileContent(file: string): Promise<string> {
26
+ const localLibDir = getLocalLibDir();
27
+ if (localLibDir) {
28
+ const localPath = path.join(localLibDir, file);
29
+ if (await fs.pathExists(localPath)) {
30
+ return fs.readFile(localPath, 'utf-8');
31
+ }
32
+ }
33
+
34
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
35
+ const response = await fetch(url);
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
38
+ }
39
+ return response.text();
40
+ }
41
+
42
+ interface InitOptions {
43
+ yes?: boolean;
44
+ defaults?: boolean;
45
+ }
46
+
47
+ function resolveProjectPath(cwd: string, inputPath: string): string {
48
+ const resolved = path.resolve(cwd, inputPath);
49
+ const relative = path.relative(cwd, resolved);
50
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
51
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
52
+ }
53
+ return resolved;
54
+ }
55
+
56
+ function resolveAliasOrPath(cwd: string, aliasOrPath: string): string {
57
+ const normalized = aliasOrPath.startsWith('@/')
58
+ ? path.join('src', aliasOrPath.slice(2))
59
+ : aliasOrPath;
60
+ return resolveProjectPath(cwd, normalized);
61
+ }
62
+
63
+ export async function init(options: InitOptions) {
17
64
  console.log(chalk.bold('\n🎨 Welcome to shadcn-angular!\n'));
18
65
 
19
66
  const cwd = process.cwd();
@@ -26,26 +73,30 @@ export async function init(options: InitOptions) {
26
73
  process.exit(1);
27
74
  }
28
75
 
29
- // Check if already initialized
30
- const componentsJsonPath = path.join(cwd, 'components.json');
31
- if (await fs.pathExists(componentsJsonPath)) {
32
- const { overwrite } = await prompts({
33
- type: 'confirm',
34
- name: 'overwrite',
35
- message: 'components.json already exists. Overwrite?',
36
- initial: false,
37
- });
38
- if (!overwrite) {
39
- console.log(chalk.dim('Initialization cancelled.'));
40
- return;
76
+ // Check if already initialized
77
+ const componentsJsonPath = path.join(cwd, 'components.json');
78
+ if (await fs.pathExists(componentsJsonPath)) {
79
+ const overwrite = options.yes
80
+ ? true
81
+ : (await prompts({
82
+ type: 'confirm',
83
+ name: 'overwrite',
84
+ message: 'components.json already exists. Overwrite?',
85
+ initial: false,
86
+ })).overwrite;
87
+ if (!overwrite) {
88
+ console.log(chalk.dim('Initialization cancelled.'));
89
+ return;
41
90
  }
42
91
  }
43
92
 
44
- let config: Config;
93
+ let config: Config;
94
+ let createShortcutRegistry = true;
45
95
 
46
- if (options.defaults || options.yes) {
47
- config = getDefaultConfig();
48
- } else {
96
+ if (options.defaults || options.yes) {
97
+ config = getDefaultConfig();
98
+ createShortcutRegistry = true;
99
+ } else {
49
100
  const THEME_COLORS: Record<string, string> = {
50
101
  zinc: '#71717a',
51
102
  slate: '#64748b',
@@ -123,16 +174,22 @@ export async function init(options: InitOptions) {
123
174
  message: 'Where would you like to install utils?',
124
175
  initial: 'src/components/lib',
125
176
  },
126
- {
127
- type: 'text',
128
- name: 'globalCss',
129
- message: 'Where is your global styles file?',
130
- initial: 'src/styles.scss',
131
- },
132
- ]);
133
-
134
- config = {
135
- $schema: 'https://shadcn-angular.dev/schema.json',
177
+ {
178
+ type: 'text',
179
+ name: 'globalCss',
180
+ message: 'Where is your global styles file?',
181
+ initial: 'src/styles.scss',
182
+ },
183
+ {
184
+ type: 'confirm',
185
+ name: 'createShortcutRegistry',
186
+ message: 'Would you like to create a shortcut registry scaffold?',
187
+ initial: true,
188
+ },
189
+ ]);
190
+
191
+ config = {
192
+ $schema: 'https://shadcn-angular.dev/schema.json',
136
193
  style: 'default',
137
194
  tailwind: {
138
195
  css: responses.globalCss,
@@ -144,10 +201,10 @@ export async function init(options: InitOptions) {
144
201
  components: responses.componentsPath.replace('src/', '@/'), // Basic heuristic
145
202
  utils: responses.utilsPath.replace('src/', '@/').replace('.ts', ''),
146
203
  ui: responses.componentsPath.replace('src/', '@/'),
147
- },
148
- iconLibrary: 'lucide-angular',
149
- };
150
- }
204
+ },
205
+ };
206
+ createShortcutRegistry = responses.createShortcutRegistry ?? true;
207
+ }
151
208
 
152
209
  const spinner = ora('Initializing project...').start();
153
210
 
@@ -163,26 +220,37 @@ export async function init(options: InitOptions) {
163
220
  // If config came from defaults, aliases are set.
164
221
  // We can reverse-map alias to path: @/ -> src/
165
222
 
166
- const utilsPathResolved = config.aliases.utils.replace('@/', 'src/');
167
- const utilsDir = path.dirname(path.join(cwd, utilsPathResolved + '.ts')); // utils usually ends in path/to/utils
168
-
169
- await fs.ensureDir(utilsDir);
170
- await fs.writeFile(path.join(cwd, utilsPathResolved + '.ts'), getUtilsTemplate());
171
- spinner.text = 'Created utils.ts';
172
-
173
- // Create tailwind.css file in the same directory as the global styles
174
- const stylesDir = path.dirname(path.join(cwd, config.tailwind.css));
175
- const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
176
-
177
- // Write the tailwind.css file with all Tailwind directives
178
- await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
179
- spinner.text = 'Created tailwind.css';
180
-
181
- // Add import to the user's global styles file if not already present
182
- const userStylesPath = path.join(cwd, config.tailwind.css);
183
- let userStyles = await fs.pathExists(userStylesPath)
184
- ? await fs.readFile(userStylesPath, 'utf-8')
185
- : '';
223
+ const utilsPathResolved = resolveAliasOrPath(cwd, config.aliases.utils + '.ts');
224
+ const utilsDir = path.dirname(utilsPathResolved); // utils usually ends in path/to/utils
225
+
226
+ await fs.ensureDir(utilsDir);
227
+ await fs.writeFile(utilsPathResolved, getUtilsTemplate());
228
+ spinner.text = 'Created utils.ts';
229
+
230
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
231
+ const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts');
232
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
233
+ spinner.text = 'Created shortcut-binding.service.ts';
234
+
235
+ if (createShortcutRegistry) {
236
+ await writeShortcutRegistryIndex(cwd, config, []);
237
+ spinner.text = 'Created shortcut-registry.index.ts';
238
+ }
239
+
240
+ // Create tailwind.css file in the same directory as the global styles
241
+ const userStylesPath = resolveProjectPath(cwd, config.tailwind.css);
242
+ const stylesDir = path.dirname(userStylesPath);
243
+ const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
244
+
245
+ // Write the tailwind.css file with all Tailwind directives
246
+ await fs.ensureDir(stylesDir);
247
+ await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
248
+ spinner.text = 'Created tailwind.css';
249
+
250
+ // Add import to the user's global styles file if not already present
251
+ let userStyles = await fs.pathExists(userStylesPath)
252
+ ? await fs.readFile(userStylesPath, 'utf-8')
253
+ : '';
186
254
 
187
255
  const tailwindImport = '@import "./tailwind.css";';
188
256
  if (!userStyles.includes('tailwind.css')) {
@@ -190,26 +258,24 @@ export async function init(options: InitOptions) {
190
258
  userStyles = tailwindImport + '\n\n' + userStyles;
191
259
  await fs.writeFile(userStylesPath, userStyles);
192
260
  spinner.text = 'Added tailwind.css import to styles';
193
- }
194
-
195
- // Create components/ui directory
196
- const uiPathResolved = config.aliases.ui.replace('@/', 'src/');
197
- const uiDir = path.join(cwd, uiPathResolved);
198
- await fs.ensureDir(uiDir);
199
- spinner.text = 'Created components directory';
261
+ }
262
+
263
+ // Create components/ui directory
264
+ const uiDir = resolveAliasOrPath(cwd, config.aliases.ui);
265
+ await fs.ensureDir(uiDir);
266
+ spinner.text = 'Created components directory';
200
267
 
201
268
  // Install dependencies
202
269
  spinner.text = 'Installing dependencies...';
203
- const dependencies = [
204
- 'clsx',
205
- 'tailwind-merge',
270
+ const dependencies = [
271
+ 'clsx',
272
+ 'tailwind-merge',
206
273
  'class-variance-authority',
207
- 'lucide-angular',
208
274
  'tailwindcss',
209
- 'postcss',
210
- '@tailwindcss/postcss'
211
- ];
212
- await execa('npm', ['install', ...dependencies], { cwd });
275
+ 'postcss',
276
+ '@tailwindcss/postcss'
277
+ ];
278
+ await installPackages(dependencies, { cwd });
213
279
 
214
280
  // Setup PostCSS - create .postcssrc.json which is the preferred format for Angular
215
281
  spinner.text = 'Configuring PostCSS...';
@@ -224,43 +290,6 @@ export async function init(options: InitOptions) {
224
290
  await fs.writeJson(postcssrcPath, configContent, { spaces: 4 });
225
291
  }
226
292
 
227
- // Configure app.config.ts with Lucide icons
228
- spinner.text = 'Configuring icons in app.config.ts...';
229
- const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
230
-
231
- if (await fs.pathExists(appConfigPath)) {
232
- let appConfigContent = await fs.readFile(appConfigPath, 'utf-8');
233
-
234
- // Add imports
235
- if (!appConfigContent.includes('LucideAngularModule')) {
236
- const iconImports = "import { LucideAngularModule, ArrowDown, ArrowUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-angular';";
237
- appConfigContent = iconImports + '\n' + appConfigContent;
238
- }
239
-
240
- if (!appConfigContent.includes('importProvidersFrom')) {
241
- appConfigContent = "import { importProvidersFrom } from '@angular/core';\n" + appConfigContent;
242
- }
243
-
244
- // Add provider
245
- const providerCode = `
246
- importProvidersFrom(LucideAngularModule.pick({
247
- ArrowDown,
248
- ArrowUp,
249
- ChevronsUpDown,
250
- ChevronLeft,
251
- ChevronRight,
252
- ChevronsLeft,
253
- ChevronsRight
254
- }))`;
255
-
256
- if (!appConfigContent.includes('LucideAngularModule.pick')) {
257
- appConfigContent = appConfigContent.replace(
258
- /providers:\s*\[/,
259
- `providers: [${providerCode},`
260
- );
261
- await fs.writeFile(appConfigPath, appConfigContent);
262
- }
263
- }
264
293
 
265
294
  spinner.succeed(chalk.green('Project initialized successfully!'));
266
295