@gilav21/shadcn-angular 0.0.24 → 0.0.26
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/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +169 -114
- package/dist/commands/add.spec.js +17 -23
- package/dist/commands/diff.d.ts +8 -0
- package/dist/commands/diff.js +99 -0
- package/dist/commands/help.js +15 -6
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +171 -185
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +50 -0
- package/dist/index.js +21 -1
- package/dist/registry/index.d.ts +122 -12
- package/dist/registry/index.js +59 -158
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +22 -2
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/shortcut-registry.js +1 -13
- package/package.json +1 -1
- package/scripts/sync-registry.ts +347 -0
- package/src/commands/add.spec.ts +22 -32
- package/src/commands/add.ts +211 -137
- package/src/commands/diff.ts +133 -0
- package/src/commands/help.ts +15 -6
- package/src/commands/init.ts +329 -314
- package/src/commands/list.ts +66 -0
- package/src/index.ts +24 -1
- package/src/registry/index.ts +73 -169
- package/src/utils/config.ts +22 -3
- package/src/utils/paths.ts +52 -0
- package/src/utils/shortcut-registry.ts +1 -15
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
export function validateBranch(branch) {
|
|
7
|
+
if (!/^[\w.\-/]+$/.test(branch)) {
|
|
8
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function getDefaultRegistryBaseUrl(branch) {
|
|
12
|
+
validateBranch(branch);
|
|
13
|
+
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components`;
|
|
14
|
+
}
|
|
15
|
+
export function getRegistryBaseUrl(branch, customRegistry) {
|
|
16
|
+
const base = customRegistry ?? getDefaultRegistryBaseUrl(branch);
|
|
17
|
+
return `${base}/ui`;
|
|
18
|
+
}
|
|
19
|
+
export function getLibRegistryBaseUrl(branch, customRegistry) {
|
|
20
|
+
const base = customRegistry ?? getDefaultRegistryBaseUrl(branch);
|
|
21
|
+
return `${base}/lib`;
|
|
22
|
+
}
|
|
23
|
+
export function getLocalComponentsDir() {
|
|
24
|
+
const localPath = path.resolve(__dirname, '../../../../components/ui');
|
|
25
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
26
|
+
}
|
|
27
|
+
export function getLocalLibDir() {
|
|
28
|
+
const localPath = path.resolve(__dirname, '../../../../components/lib');
|
|
29
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
30
|
+
}
|
|
31
|
+
export function resolveProjectPath(cwd, inputPath) {
|
|
32
|
+
const resolved = path.resolve(cwd, inputPath);
|
|
33
|
+
const relative = path.relative(cwd, resolved);
|
|
34
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
35
|
+
throw new Error(`Path must stay inside the project directory: ${inputPath}`);
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export function aliasToProjectPath(aliasOrPath) {
|
|
40
|
+
return aliasOrPath.startsWith('@/')
|
|
41
|
+
? path.join('src', aliasOrPath.slice(2))
|
|
42
|
+
: aliasOrPath;
|
|
43
|
+
}
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
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
|
-
}
|
|
3
|
+
import { resolveProjectPath, aliasToProjectPath } from './paths.js';
|
|
16
4
|
function getShortcutRegistryIndexPath(cwd, config) {
|
|
17
5
|
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
18
6
|
return path.join(libDir, 'shortcut-registry.index.ts');
|
package/package.json
CHANGED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Recursively walks imports from each component's entry file,
|
|
4
|
+
* stopping at other component boundaries to discover only the
|
|
5
|
+
* files that belong to THIS component's tree.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx packages/cli/scripts/sync-registry.ts # report only
|
|
9
|
+
* npx tsx packages/cli/scripts/sync-registry.ts --fix # update registry
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const CLI_SRC = path.resolve(SCRIPT_DIR, '../src');
|
|
18
|
+
const COMPONENTS_ROOT = path.resolve(SCRIPT_DIR, '../../components');
|
|
19
|
+
const REGISTRY_PATH = path.join(CLI_SRC, 'registry/index.ts');
|
|
20
|
+
|
|
21
|
+
const IMPORT_REGEX = /from\s+['"](\.[^'"]+)['"]/g;
|
|
22
|
+
|
|
23
|
+
// utils.ts is always installed during init — not a component libFile
|
|
24
|
+
const BASELINE_LIB_FILES = new Set(['utils.ts']);
|
|
25
|
+
|
|
26
|
+
// ── Parse registry ──────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
interface RegistryEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
files: string[];
|
|
31
|
+
libFiles: string[];
|
|
32
|
+
dependencies: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseRegistry(): RegistryEntry[] {
|
|
36
|
+
const source = readFileSync(REGISTRY_PATH, 'utf-8');
|
|
37
|
+
const entries: RegistryEntry[] = [];
|
|
38
|
+
|
|
39
|
+
const blockRegex = /['"]?([\w-]+)['"]?\s*:\s*\{[^}]*?name:\s*['"]([^'"]+)['"][^}]*?files:\s*\[([\s\S]*?)\]/g;
|
|
40
|
+
let match: RegExpExecArray | null;
|
|
41
|
+
|
|
42
|
+
while ((match = blockRegex.exec(source)) !== null) {
|
|
43
|
+
const name = match[2];
|
|
44
|
+
const filesRaw = match[3];
|
|
45
|
+
const files = [...filesRaw.matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
46
|
+
|
|
47
|
+
const blockEnd = source.indexOf('},', match.index + match[0].length);
|
|
48
|
+
const fullBlock = source.slice(match.index, blockEnd === -1 ? undefined : blockEnd);
|
|
49
|
+
const libFilesMatch = /libFiles:\s*\[([\s\S]*?)\]/.exec(fullBlock);
|
|
50
|
+
const libFiles = libFilesMatch
|
|
51
|
+
? [...libFilesMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1])
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
const depsMatch = /dependencies:\s*\[([\s\S]*?)\]/.exec(fullBlock);
|
|
55
|
+
const dependencies = depsMatch
|
|
56
|
+
? [...depsMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1])
|
|
57
|
+
: [];
|
|
58
|
+
|
|
59
|
+
entries.push({ name, files, libFiles, dependencies });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Resolve imports ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function resolveImport(importPath: string, fromFile: string): string | null {
|
|
68
|
+
const fromDir = path.dirname(path.join(COMPONENTS_ROOT, fromFile));
|
|
69
|
+
let resolved = path.resolve(fromDir, importPath);
|
|
70
|
+
|
|
71
|
+
if (!existsSync(resolved)) resolved += '.ts';
|
|
72
|
+
if (!existsSync(resolved)) return null;
|
|
73
|
+
|
|
74
|
+
return path.relative(COMPONENTS_ROOT, resolved).replaceAll('\\', '/');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Tree walker with component boundaries ───────────────────────────────
|
|
78
|
+
|
|
79
|
+
interface WalkResult {
|
|
80
|
+
ownFiles: Set<string>;
|
|
81
|
+
discoveredDeps: Set<string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function walkTree(
|
|
85
|
+
entryFile: string,
|
|
86
|
+
componentName: string,
|
|
87
|
+
entryFileToComponent: Map<string, string>,
|
|
88
|
+
): WalkResult {
|
|
89
|
+
const ownFiles = new Set<string>();
|
|
90
|
+
const discoveredDeps = new Set<string>();
|
|
91
|
+
|
|
92
|
+
function collect(file: string): void {
|
|
93
|
+
if (ownFiles.has(file)) return;
|
|
94
|
+
|
|
95
|
+
// If this file is the entry point of ANOTHER component → dependency, stop
|
|
96
|
+
const ownerComponent = entryFileToComponent.get(file);
|
|
97
|
+
if (ownerComponent && ownerComponent !== componentName) {
|
|
98
|
+
discoveredDeps.add(ownerComponent);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ownFiles.add(file);
|
|
103
|
+
|
|
104
|
+
const fullPath = path.join(COMPONENTS_ROOT, file);
|
|
105
|
+
if (!existsSync(fullPath)) return;
|
|
106
|
+
|
|
107
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
108
|
+
const regex = new RegExp(IMPORT_REGEX.source, IMPORT_REGEX.flags);
|
|
109
|
+
let match: RegExpExecArray | null;
|
|
110
|
+
|
|
111
|
+
while ((match = regex.exec(content)) !== null) {
|
|
112
|
+
const resolved = resolveImport(match[1], file);
|
|
113
|
+
if (resolved) collect(resolved);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
collect(entryFile);
|
|
118
|
+
return { ownFiles, discoveredDeps };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Split files into ui/ and lib/ ───────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function splitFiles(allFiles: Set<string>): { uiFiles: string[]; libFiles: string[] } {
|
|
124
|
+
const uiFiles: string[] = [];
|
|
125
|
+
const libFiles: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (const file of allFiles) {
|
|
128
|
+
if (file.startsWith('ui/')) {
|
|
129
|
+
uiFiles.push(file.slice(3));
|
|
130
|
+
} else if (file.startsWith('lib/')) {
|
|
131
|
+
const libName = file.slice(4);
|
|
132
|
+
if (!BASELINE_LIB_FILES.has(libName)) {
|
|
133
|
+
libFiles.push(libName);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
uiFiles.sort((a, b) => a.localeCompare(b));
|
|
139
|
+
libFiles.sort((a, b) => a.localeCompare(b));
|
|
140
|
+
return { uiFiles, libFiles };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function getEntryFile(entry: RegistryEntry): string {
|
|
146
|
+
// Barrel index.ts re-exports everything — use it when available
|
|
147
|
+
const indexFile = entry.files.find(f => f.endsWith('index.ts'));
|
|
148
|
+
if (indexFile) return indexFile;
|
|
149
|
+
|
|
150
|
+
// Convention: main file is {name}.component.ts or {name}.directive.ts
|
|
151
|
+
const baseName = entry.name.split('/').at(-1) ?? entry.name;
|
|
152
|
+
const conventionFile = entry.files.find(f =>
|
|
153
|
+
f.endsWith(`${baseName}.component.ts`) || f.endsWith(`${baseName}.directive.ts`),
|
|
154
|
+
);
|
|
155
|
+
return conventionFile ?? entry.files[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface ComponentUpdate {
|
|
159
|
+
name: string;
|
|
160
|
+
files: string[];
|
|
161
|
+
libFiles: string[];
|
|
162
|
+
dependencies: string[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildBoundaryMap(entries: RegistryEntry[]): Map<string, string> {
|
|
166
|
+
const map = new Map<string, string>();
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
map.set('ui/' + getEntryFile(entry), entry.name);
|
|
169
|
+
}
|
|
170
|
+
return map;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function analyzeComponent(
|
|
174
|
+
entry: RegistryEntry,
|
|
175
|
+
entryFileToComponent: Map<string, string>,
|
|
176
|
+
): { update: ComponentUpdate; changed: boolean } {
|
|
177
|
+
const entryFile = 'ui/' + getEntryFile(entry);
|
|
178
|
+
const { ownFiles, discoveredDeps } = walkTree(entryFile, entry.name, entryFileToComponent);
|
|
179
|
+
const { uiFiles, libFiles: discoveredLibs } = splitFiles(ownFiles);
|
|
180
|
+
|
|
181
|
+
const mergedLibFiles = [...new Set([...entry.libFiles, ...discoveredLibs])];
|
|
182
|
+
mergedLibFiles.sort((a, b) => a.localeCompare(b));
|
|
183
|
+
|
|
184
|
+
const finalDeps = [...discoveredDeps].sort((a, b) => a.localeCompare(b));
|
|
185
|
+
|
|
186
|
+
const addedFiles = uiFiles.filter(f => !entry.files.includes(f));
|
|
187
|
+
const removedFiles = entry.files.filter(f => !uiFiles.includes(f));
|
|
188
|
+
const addedLibs = discoveredLibs.filter(f => !entry.libFiles.includes(f));
|
|
189
|
+
const addedDeps = finalDeps.filter(d => !entry.dependencies.includes(d));
|
|
190
|
+
const removedDeps = entry.dependencies.filter(d => !finalDeps.includes(d));
|
|
191
|
+
const changed = addedFiles.length > 0 || removedFiles.length > 0
|
|
192
|
+
|| addedLibs.length > 0 || addedDeps.length > 0 || removedDeps.length > 0;
|
|
193
|
+
|
|
194
|
+
if (changed) {
|
|
195
|
+
console.log(` ${entry.name}:`);
|
|
196
|
+
for (const f of addedFiles) console.log(` + files: ${f}`);
|
|
197
|
+
for (const f of removedFiles) console.log(` - files: ${f}`);
|
|
198
|
+
for (const f of addedLibs) console.log(` + libFiles: ${f}`);
|
|
199
|
+
for (const d of addedDeps) console.log(` + dependencies: ${d}`);
|
|
200
|
+
for (const d of removedDeps) console.log(` - dependencies: ${d}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
update: { name: entry.name, files: uiFiles, libFiles: mergedLibFiles, dependencies: finalDeps },
|
|
205
|
+
changed,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function findNamePos(source: string, name: string): number {
|
|
210
|
+
const singleQuote = source.indexOf(`name: '${name}'`);
|
|
211
|
+
if (singleQuote >= 0) return singleQuote;
|
|
212
|
+
return source.indexOf(`name: "${name}"`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function replaceFilesArray(source: string, name: string, filesArrayStr: string): string {
|
|
216
|
+
const namePos = findNamePos(source, name);
|
|
217
|
+
if (namePos === -1) return source;
|
|
218
|
+
|
|
219
|
+
const filesStart = source.indexOf('files: [', namePos);
|
|
220
|
+
if (filesStart === -1) return source;
|
|
221
|
+
const filesEnd = source.indexOf(']', filesStart + 8);
|
|
222
|
+
if (filesEnd === -1) return source;
|
|
223
|
+
|
|
224
|
+
return source.slice(0, filesStart + 8) + filesArrayStr + source.slice(filesEnd);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function updateLibFiles(source: string, name: string, libArrayStr: string): string {
|
|
228
|
+
const updatedNamePos = findNamePos(source, name);
|
|
229
|
+
if (updatedNamePos === -1) return source;
|
|
230
|
+
const nextNamePos = source.indexOf('name: ', updatedNamePos + name.length + 8);
|
|
231
|
+
const blockEnd = nextNamePos === -1 ? source.length : nextNamePos;
|
|
232
|
+
const blockSlice = source.slice(updatedNamePos, blockEnd);
|
|
233
|
+
|
|
234
|
+
const libOffset = blockSlice.indexOf('libFiles: [');
|
|
235
|
+
if (libOffset >= 0) {
|
|
236
|
+
const absLibStart = updatedNamePos + libOffset;
|
|
237
|
+
const libEnd = source.indexOf(']', absLibStart + 11);
|
|
238
|
+
return source.slice(0, absLibStart + 11) + libArrayStr + source.slice(libEnd);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const absFilesStart = source.indexOf('files: [', updatedNamePos);
|
|
242
|
+
const absFilesEnd = source.indexOf(']', absFilesStart + 8);
|
|
243
|
+
return source.slice(0, absFilesEnd + 1) + `,\n libFiles: [${libArrayStr}]` + source.slice(absFilesEnd + 1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateDependencies(source: string, name: string, depsArrayStr: string): string {
|
|
247
|
+
const updatedNamePos = findNamePos(source, name);
|
|
248
|
+
if (updatedNamePos === -1) return source;
|
|
249
|
+
const nextNamePos = source.indexOf('name: ', updatedNamePos + name.length + 8);
|
|
250
|
+
const blockEnd = nextNamePos === -1 ? source.length : nextNamePos;
|
|
251
|
+
const blockSlice = source.slice(updatedNamePos, blockEnd);
|
|
252
|
+
|
|
253
|
+
const depsOffset = blockSlice.indexOf('dependencies: [');
|
|
254
|
+
if (depsOffset >= 0) {
|
|
255
|
+
const absDepsStart = updatedNamePos + depsOffset;
|
|
256
|
+
const depsEnd = source.indexOf(']', absDepsStart + 15);
|
|
257
|
+
return source.slice(0, absDepsStart + 15) + depsArrayStr + source.slice(depsEnd);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Insert after files array (or after libFiles if present)
|
|
261
|
+
const libOffset = blockSlice.indexOf('libFiles: [');
|
|
262
|
+
if (libOffset >= 0) {
|
|
263
|
+
const absLibStart = updatedNamePos + libOffset;
|
|
264
|
+
const libEnd = source.indexOf(']', absLibStart + 11);
|
|
265
|
+
return source.slice(0, libEnd + 1) + `,\n dependencies: [${depsArrayStr}]` + source.slice(libEnd + 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const absFilesStart = source.indexOf('files: [', updatedNamePos);
|
|
269
|
+
const absFilesEnd = source.indexOf(']', absFilesStart + 8);
|
|
270
|
+
return source.slice(0, absFilesEnd + 1) + `,\n dependencies: [${depsArrayStr}]` + source.slice(absFilesEnd + 1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function removeDependencies(source: string, name: string): string {
|
|
274
|
+
const namePos = findNamePos(source, name);
|
|
275
|
+
if (namePos === -1) return source;
|
|
276
|
+
const nextNamePos = source.indexOf('name: ', namePos + name.length + 8);
|
|
277
|
+
const blockEnd = nextNamePos === -1 ? source.length : nextNamePos;
|
|
278
|
+
const blockSlice = source.slice(namePos, blockEnd);
|
|
279
|
+
|
|
280
|
+
// Match leading comma + whitespace + dependencies: [...] but NOT trailing comma
|
|
281
|
+
const depsRegex = /,?\s*dependencies:\s*\[[\s\S]*?\]/;
|
|
282
|
+
const depsMatch = depsRegex.exec(blockSlice);
|
|
283
|
+
if (!depsMatch) return source;
|
|
284
|
+
|
|
285
|
+
const absStart = namePos + depsMatch.index;
|
|
286
|
+
const absEnd = absStart + depsMatch[0].length;
|
|
287
|
+
return source.slice(0, absStart) + source.slice(absEnd);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyUpdates(updates: ComponentUpdate[]): void {
|
|
291
|
+
let source = readFileSync(REGISTRY_PATH, 'utf-8');
|
|
292
|
+
|
|
293
|
+
for (const update of updates) {
|
|
294
|
+
const filesArrayStr = update.files.map(f => `'${f}'`).join(', ');
|
|
295
|
+
source = replaceFilesArray(source, update.name, filesArrayStr);
|
|
296
|
+
|
|
297
|
+
if (update.libFiles.length > 0) {
|
|
298
|
+
const libArrayStr = update.libFiles.map(f => `'${f}'`).join(', ');
|
|
299
|
+
source = updateLibFiles(source, update.name, libArrayStr);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (update.dependencies.length > 0) {
|
|
303
|
+
const depsStr = update.dependencies.map(d => `'${d}'`).join(', ');
|
|
304
|
+
source = updateDependencies(source, update.name, depsStr);
|
|
305
|
+
} else {
|
|
306
|
+
source = removeDependencies(source, update.name);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
writeFileSync(REGISTRY_PATH, source);
|
|
311
|
+
console.log('Registry updated.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Main ────────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function main(): void {
|
|
317
|
+
const fix = process.argv.includes('--fix');
|
|
318
|
+
const entries = parseRegistry();
|
|
319
|
+
const entryFileToComponent = buildBoundaryMap(entries);
|
|
320
|
+
|
|
321
|
+
console.log(`Scanning ${entries.length} components...\n`);
|
|
322
|
+
|
|
323
|
+
let hasChanges = false;
|
|
324
|
+
const updates: ComponentUpdate[] = [];
|
|
325
|
+
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
const { update, changed } = analyzeComponent(entry, entryFileToComponent);
|
|
328
|
+
if (changed) hasChanges = true;
|
|
329
|
+
updates.push(update);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!hasChanges) {
|
|
333
|
+
console.log('All components are in sync.');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log('');
|
|
338
|
+
|
|
339
|
+
if (fix) {
|
|
340
|
+
applyUpdates(updates);
|
|
341
|
+
} else {
|
|
342
|
+
console.log('Run with --fix to update the registry.');
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
main();
|
package/src/commands/add.spec.ts
CHANGED
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
checkFileConflict,
|
|
8
8
|
classifyComponent,
|
|
9
9
|
detectConflicts,
|
|
10
|
-
type AddOptions,
|
|
11
10
|
} from './add.js';
|
|
12
|
-
import { registry, type ComponentName
|
|
11
|
+
import { registry, type ComponentName } from '../registry/index.js';
|
|
13
12
|
import fs from 'fs-extra';
|
|
14
13
|
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
@@ -353,22 +352,22 @@ describe('detectConflicts', () => {
|
|
|
353
352
|
describe('peer file filtering logic', () => {
|
|
354
353
|
it('removes peer files of declined components from peerFilesToUpdate', () => {
|
|
355
354
|
const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts', 'shared/peer-c.ts']);
|
|
356
|
-
const conflicting
|
|
357
|
-
const toOverwrite
|
|
358
|
-
const finalComponents
|
|
355
|
+
const conflicting = new Set(['compA', 'compB']);
|
|
356
|
+
const toOverwrite = new Set(['compA']);
|
|
357
|
+
const finalComponents = new Set(['compA']);
|
|
359
358
|
|
|
360
359
|
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
361
360
|
compA: ['shared/peer-a.ts'],
|
|
362
361
|
compB: ['shared/peer-b.ts', 'shared/peer-c.ts'],
|
|
363
362
|
};
|
|
364
363
|
|
|
365
|
-
const declined = conflicting.filter(c => !toOverwrite.
|
|
364
|
+
const declined = [...conflicting].filter(c => !toOverwrite.has(c));
|
|
366
365
|
|
|
367
366
|
for (const name of declined) {
|
|
368
367
|
const peerFiles = mockPeerFiles[name];
|
|
369
368
|
if (!peerFiles) continue;
|
|
370
369
|
for (const file of peerFiles) {
|
|
371
|
-
const stillNeeded = finalComponents.some(fc =>
|
|
370
|
+
const stillNeeded = [...finalComponents].some(fc =>
|
|
372
371
|
mockPeerFiles[fc]?.includes(file),
|
|
373
372
|
);
|
|
374
373
|
if (!stillNeeded) {
|
|
@@ -384,22 +383,22 @@ describe('peer file filtering logic', () => {
|
|
|
384
383
|
|
|
385
384
|
it('keeps shared peer files when another final component still needs them', () => {
|
|
386
385
|
const peerFilesToUpdate = new Set(['shared/common.ts']);
|
|
387
|
-
const conflicting
|
|
388
|
-
const toOverwrite
|
|
389
|
-
const finalComponents
|
|
386
|
+
const conflicting = new Set(['compA', 'compB']);
|
|
387
|
+
const toOverwrite = new Set(['compA']);
|
|
388
|
+
const finalComponents = new Set(['compA']);
|
|
390
389
|
|
|
391
390
|
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
392
391
|
compA: ['shared/common.ts'],
|
|
393
392
|
compB: ['shared/common.ts'],
|
|
394
393
|
};
|
|
395
394
|
|
|
396
|
-
const declined = conflicting.filter(c => !toOverwrite.
|
|
395
|
+
const declined = [...conflicting].filter(c => !toOverwrite.has(c));
|
|
397
396
|
|
|
398
397
|
for (const name of declined) {
|
|
399
398
|
const peerFiles = mockPeerFiles[name];
|
|
400
399
|
if (!peerFiles) continue;
|
|
401
400
|
for (const file of peerFiles) {
|
|
402
|
-
const stillNeeded = finalComponents.some(fc =>
|
|
401
|
+
const stillNeeded = [...finalComponents].some(fc =>
|
|
403
402
|
mockPeerFiles[fc]?.includes(file),
|
|
404
403
|
);
|
|
405
404
|
if (!stillNeeded) {
|
|
@@ -413,27 +412,18 @@ describe('peer file filtering logic', () => {
|
|
|
413
412
|
|
|
414
413
|
it('removes all peer files when all components are declined', () => {
|
|
415
414
|
const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts']);
|
|
416
|
-
const
|
|
417
|
-
const toOverwrite: string[] = [];
|
|
418
|
-
const finalComponents: string[] = [];
|
|
415
|
+
const declined = ['compA', 'compB'];
|
|
419
416
|
|
|
420
417
|
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
421
418
|
compA: ['shared/peer-a.ts'],
|
|
422
419
|
compB: ['shared/peer-b.ts'],
|
|
423
420
|
};
|
|
424
421
|
|
|
425
|
-
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
426
|
-
|
|
427
422
|
for (const name of declined) {
|
|
428
423
|
const peerFiles = mockPeerFiles[name];
|
|
429
424
|
if (!peerFiles) continue;
|
|
430
425
|
for (const file of peerFiles) {
|
|
431
|
-
|
|
432
|
-
mockPeerFiles[fc]?.includes(file),
|
|
433
|
-
);
|
|
434
|
-
if (!stillNeeded) {
|
|
435
|
-
peerFilesToUpdate.delete(file);
|
|
436
|
-
}
|
|
426
|
+
peerFilesToUpdate.delete(file);
|
|
437
427
|
}
|
|
438
428
|
}
|
|
439
429
|
|
|
@@ -468,9 +458,9 @@ describe('resolveDependencies', () => {
|
|
|
468
458
|
});
|
|
469
459
|
|
|
470
460
|
it('deduplicates shared dependencies across multiple inputs', () => {
|
|
471
|
-
const result = resolveDependencies(['
|
|
472
|
-
expect(result).toContain('
|
|
473
|
-
expect(result).toContain('
|
|
461
|
+
const result = resolveDependencies(['date-picker', 'sparkles']);
|
|
462
|
+
expect(result).toContain('date-picker');
|
|
463
|
+
expect(result).toContain('sparkles');
|
|
474
464
|
expect(result).toContain('button');
|
|
475
465
|
expect(result).toContain('ripple');
|
|
476
466
|
|
|
@@ -547,25 +537,25 @@ describe('registry optional dependencies', () => {
|
|
|
547
537
|
});
|
|
548
538
|
|
|
549
539
|
it('every optional dependency name is a valid registry key', () => {
|
|
550
|
-
for (const [componentName, definition] of Object.entries(registry)
|
|
540
|
+
for (const [componentName, definition] of Object.entries(registry)) {
|
|
551
541
|
if (!definition.optionalDependencies) continue;
|
|
552
542
|
for (const opt of definition.optionalDependencies) {
|
|
553
543
|
expect(
|
|
554
|
-
|
|
544
|
+
opt.name in registry,
|
|
555
545
|
`Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`,
|
|
556
|
-
).
|
|
546
|
+
).toBe(true);
|
|
557
547
|
}
|
|
558
548
|
}
|
|
559
549
|
});
|
|
560
550
|
|
|
561
551
|
it('every dependency name is a valid registry key', () => {
|
|
562
|
-
for (const [componentName, definition] of Object.entries(registry)
|
|
552
|
+
for (const [componentName, definition] of Object.entries(registry)) {
|
|
563
553
|
if (!definition.dependencies) continue;
|
|
564
554
|
for (const dep of definition.dependencies) {
|
|
565
555
|
expect(
|
|
566
|
-
registry
|
|
556
|
+
dep in registry,
|
|
567
557
|
`Dependency "${dep}" in "${componentName}" is not a valid registry key`,
|
|
568
|
-
).
|
|
558
|
+
).toBe(true);
|
|
569
559
|
}
|
|
570
560
|
}
|
|
571
561
|
});
|