@konce-pt/ui 0.7.9 → 0.7.11

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.
@@ -9,13 +9,47 @@ export async function addCommand(args) {
9
9
  if (requested.length === 0)
10
10
  throw new UserError('Podaj nazwę bloku: add <nazwa...>');
11
11
  const { path: configPath, config } = requireConfig(process.cwd());
12
- const projectRoot = path.dirname(configPath);
13
- const base = flag(args, 'registry') ?? config.registry;
14
- const overwrite = hasFlag(args, 'overwrite');
12
+ const ctx = {
13
+ base: flag(args, 'registry') ?? config.registry,
14
+ config,
15
+ projectRoot: path.dirname(configPath),
16
+ targetDirRel: flag(args, 'path') ?? config.blocksDir,
17
+ overwrite: hasFlag(args, 'overwrite'),
18
+ };
15
19
  const dryRun = hasFlag(args, 'dry-run');
16
- const targetDirRel = flag(args, 'path') ?? config.blocksDir;
17
- const index = await loadIndex(base);
20
+ const index = await loadIndex(ctx.base);
18
21
  // 1) Rozwiąż zależności registry rekurencyjnie (kolejność zależnościowa, wykryj cykle).
22
+ const order = resolveOrder(requested, index);
23
+ // 2) Pomiń już zainstalowane (chyba że --overwrite).
24
+ const toInstall = order.filter((name) => ctx.overwrite || !config.installedBlocks[name]);
25
+ logAlreadyInstalled(order, ctx);
26
+ if (toInstall.length === 0) {
27
+ log.info('Nic do zrobienia.');
28
+ return;
29
+ }
30
+ // 3) Wczytaj definicje, sprawdź zależności npm.
31
+ const metas = new Map();
32
+ for (const name of toInstall)
33
+ metas.set(name, await loadBlockMeta(ctx.base, name));
34
+ checkNpmDependencies(metas, ctx.projectRoot);
35
+ // 4) Pobierz WSZYSTKIE pliki i zaplanuj zapis (instalacja atomowa — nic nie zapisujemy wcześniej).
36
+ const plan = await planWrites(toInstall, metas, ctx);
37
+ // 5) Zapis (albo raport dry-run).
38
+ if (dryRun) {
39
+ log.info('— dry-run: żadnych zapisów —');
40
+ for (const p of plan)
41
+ log.step(`${labelFor(p.action)} ${p.target}`);
42
+ }
43
+ else {
44
+ applyPlan(plan, ctx.projectRoot);
45
+ markInstalled(config, toInstall, index.libraryVersion);
46
+ writeConfig(configPath, config);
47
+ }
48
+ printSummary(toInstall, metas, dryRun);
49
+ }
50
+ // — pomocnicze —
51
+ /** Kolejność instalacji: zależności registry przed blokiem, który ich potrzebuje. Wykrywa cykle. */
52
+ function resolveOrder(requested, index) {
19
53
  const order = [];
20
54
  const done = new Set();
21
55
  const stack = new Set();
@@ -36,57 +70,49 @@ export async function addCommand(args) {
36
70
  };
37
71
  for (const name of requested)
38
72
  visit(name);
39
- // 2) Pomiń już zainstalowane (chyba że --overwrite).
40
- const toInstall = order.filter((name) => overwrite || !config.installedBlocks[name]);
41
- const skippedInstalled = order.filter((name) => !overwrite && config.installedBlocks[name]);
42
- for (const name of skippedInstalled)
43
- log.step(`${name} — już zainstalowany (pomijam; --overwrite by nadpisać)`);
44
- if (toInstall.length === 0) {
45
- log.info('Nic do zrobienia.');
73
+ return order;
74
+ }
75
+ function logAlreadyInstalled(order, ctx) {
76
+ if (ctx.overwrite)
46
77
  return;
78
+ for (const name of order) {
79
+ if (ctx.config.installedBlocks[name]) {
80
+ log.step(`${name} — już zainstalowany (pomijam; --overwrite by nadpisać)`);
81
+ }
47
82
  }
48
- // 3) Wczytaj definicje, sprawdź zależności npm.
49
- const metas = new Map();
50
- for (const name of toInstall)
51
- metas.set(name, await loadBlockMeta(base, name));
52
- checkNpmDependencies(metas, projectRoot);
53
- // 4) Pobierz WSZYSTKIE pliki i zaplanuj zapis (instalacja atomowa — nic nie zapisujemy wcześniej).
83
+ }
84
+ /** Pobiera pliki wszystkich bloków i planuje zapis — bez dotykania dysku. */
85
+ async function planWrites(toInstall, metas, ctx) {
54
86
  const plan = [];
55
87
  for (const name of toInstall) {
56
88
  const meta = metas.get(name);
57
89
  for (const file of meta.files) {
58
- const raw = await loadBlockFile(base, name, file.path);
59
- const content = rewriteImports(raw, config);
60
- const absolute = path.resolve(projectRoot, targetDirRel, name, file.path);
61
- plan.push(planWrite(name, absolute, content, projectRoot, overwrite));
90
+ const raw = await loadBlockFile(ctx.base, name, file.path);
91
+ const content = rewriteImports(raw, ctx.config);
92
+ const absolute = path.resolve(ctx.projectRoot, ctx.targetDirRel, name, file.path);
93
+ plan.push(planWrite(name, absolute, content, ctx.projectRoot, ctx.overwrite));
62
94
  }
63
95
  }
64
- // 5) Zapis (albo raport dry-run).
65
- if (dryRun) {
66
- log.info('— dry-run: żadnych zapisów —');
67
- for (const p of plan)
68
- log.step(`${labelFor(p.action)} ${p.target}`);
69
- }
70
- else {
71
- for (const p of plan) {
72
- if (p.action === 'skip-identical') {
73
- log.step(`bez zmian ${p.target}`);
74
- continue;
75
- }
76
- const dest = p.action === 'conflict-new' ? `${p.absolute}.new` : p.absolute;
77
- mkdirSync(path.dirname(dest), { recursive: true });
78
- writeFileSync(dest, p.content);
79
- log.step(`${labelFor(p.action)} ${path.relative(projectRoot, dest)}`);
80
- }
81
- const now = new Date().toISOString().slice(0, 10);
82
- for (const name of toInstall) {
83
- config.installedBlocks[name] = { version: index.libraryVersion, installedAt: now };
96
+ return plan;
97
+ }
98
+ function applyPlan(plan, projectRoot) {
99
+ for (const p of plan) {
100
+ if (p.action === 'skip-identical') {
101
+ log.step(`bez zmian ${p.target}`);
102
+ continue;
84
103
  }
85
- writeConfig(configPath, config);
104
+ const dest = p.action === 'conflict-new' ? `${p.absolute}.new` : p.absolute;
105
+ mkdirSync(path.dirname(dest), { recursive: true });
106
+ writeFileSync(dest, p.content);
107
+ log.step(`${labelFor(p.action)} ${path.relative(projectRoot, dest)}`);
108
+ }
109
+ }
110
+ function markInstalled(config, installed, libraryVersion) {
111
+ const now = new Date().toISOString().slice(0, 10);
112
+ for (const name of installed) {
113
+ config.installedBlocks[name] = { version: libraryVersion, installedAt: now };
86
114
  }
87
- printSummary(toInstall, metas, dryRun);
88
115
  }
89
- // — pomocnicze —
90
116
  function planWrite(block, absolute, content, projectRoot, overwrite) {
91
117
  const target = path.relative(projectRoot, absolute);
92
118
  if (!existsSync(absolute))
@@ -110,7 +136,7 @@ function labelFor(action) {
110
136
  function rewriteImports(content, config) {
111
137
  let out = content.replaceAll('@/', `${config.alias}/`);
112
138
  if (config.prefix !== 'kpt') {
113
- out = out.replace(/(?<![-\w])kpt-/g, `${config.prefix}-`);
139
+ out = out.replaceAll(/(?<![-\w])kpt-/g, `${config.prefix}-`);
114
140
  }
115
141
  return out;
116
142
  }
@@ -123,60 +149,80 @@ function rewriteImports(content, config) {
123
149
  * inaczej niż npm, byłby gorszy od ostrzeżenia.
124
150
  */
125
151
  function checkNpmDependencies(metas, projectRoot) {
126
- const pkgPath = path.join(projectRoot, 'package.json');
127
- const installed = {};
128
- if (existsSync(pkgPath)) {
129
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
130
- Object.assign(installed, pkg.dependencies ?? {}, pkg.devDependencies ?? {});
131
- }
152
+ const installed = readInstalledDeps(projectRoot);
132
153
  const missing = new Map();
133
154
  // Set, bo kilka bloków deklaruje zwykle ten sam peer (@angular/core) — bez tego
134
155
  // to samo ostrzeżenie poszłoby raz na blok.
135
156
  const peerNotes = new Set();
136
- for (const meta of metas.values()) {
137
- for (const [kind, deps] of [
138
- ['npm', meta.dependencies?.npm ?? {}],
139
- ['peer', meta.dependencies?.peer ?? {}],
140
- ]) {
141
- for (const [name, range] of Object.entries(deps)) {
142
- const have = installed[name];
143
- if (!have) {
144
- if (kind === 'npm')
145
- missing.set(name, range);
146
- else
147
- peerNotes.add(`${name} (peer) — brak w projekcie, blok wymaga ${range}`);
148
- continue;
149
- }
150
- // Specyfikatory bez konkretnej wersji (workspace:*, *, latest, git/file:) — nie oceniamy.
151
- if (!/\d/.test(have))
152
- continue;
153
- if (satisfies(have, range))
154
- continue;
155
- if (kind === 'npm') {
156
- throw new UserError(`Zależność ${name} jest w wersji ${have}, blok wymaga ${range}. ` +
157
- `Zaktualizuj ją samodzielnie i uruchom ponownie.`);
158
- }
159
- peerNotes.add(`${name} (peer) jest w wersji ${have}, blok wymaga ${range}`);
157
+ for (const { kind, name, range } of declaredDependencies(metas)) {
158
+ const have = installed[name];
159
+ const verdict = inspectDependency(range, have);
160
+ if (verdict === 'ok')
161
+ continue;
162
+ if (kind === 'npm') {
163
+ if (verdict === 'missing')
164
+ missing.set(name, range);
165
+ else {
166
+ throw new UserError(`Zależność ${name} jest w wersji ${have}, blok wymaga ${range}. ` +
167
+ `Zaktualizuj ją samodzielnie i uruchom ponownie.`);
160
168
  }
169
+ continue;
161
170
  }
171
+ peerNotes.add(verdict === 'missing'
172
+ ? `${name} (peer) — brak w projekcie, blok wymaga ${range}`
173
+ : `${name} (peer) jest w wersji ${have}, blok wymaga ${range}`);
162
174
  }
163
- if (missing.size > 0) {
164
- // Do instalacji podajemy same nazwy — zakres bywa złożony (">=0.6.0 <1.0.0")
165
- // i wklejony do shella by się rozpadł. Wymagania pokazujemy osobno.
166
- log.warn(`Brakujące zależności npm — zainstaluj je: ${[...missing.keys()].join(' ')}`);
167
- for (const [name, range] of missing)
168
- log.step(`${name} wymaga ${range}`);
169
- }
175
+ reportMissing(missing);
170
176
  for (const note of peerNotes)
171
177
  log.warn(note);
172
178
  }
179
+ /** `dependencies` + `devDependencies` z package.json projektu (devDeps wygrywają). */
180
+ function readInstalledDeps(projectRoot) {
181
+ const pkgPath = path.join(projectRoot, 'package.json');
182
+ if (!existsSync(pkgPath))
183
+ return {};
184
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
185
+ // Rozwinięcie `undefined` w literale obiektu jest legalne i nic nie wnosi — bez `?? {}`.
186
+ return { ...pkg.dependencies, ...pkg.devDependencies };
187
+ }
188
+ /** Płaska lista zależności zadeklarowanych przez wszystkie instalowane bloki. */
189
+ function* declaredDependencies(metas) {
190
+ for (const meta of metas.values()) {
191
+ yield* dependenciesOfKind('npm', meta.dependencies?.npm);
192
+ yield* dependenciesOfKind('peer', meta.dependencies?.peer);
193
+ }
194
+ }
195
+ function* dependenciesOfKind(kind, deps) {
196
+ if (!deps)
197
+ return;
198
+ for (const [name, range] of Object.entries(deps))
199
+ yield { kind, name, range };
200
+ }
201
+ /** Werdykt dla jednej zależności — bez rozróżnienia na npm/peer, to robi wołający. */
202
+ function inspectDependency(range, have) {
203
+ if (!have)
204
+ return 'missing';
205
+ // Specyfikatory bez konkretnej wersji (workspace:*, *, latest, git/file:) — nie oceniamy.
206
+ if (!/\d/.test(have))
207
+ return 'ok';
208
+ return satisfies(have, range) ? 'ok' : 'mismatch';
209
+ }
210
+ function reportMissing(missing) {
211
+ if (missing.size === 0)
212
+ return;
213
+ // Do instalacji podajemy same nazwy — zakres bywa złożony (">=0.6.0 <1.0.0")
214
+ // i wklejony do shella by się rozpadł. Wymagania pokazujemy osobno.
215
+ log.warn(`Brakujące zależności npm — zainstaluj je: ${[...missing.keys()].join(' ')}`);
216
+ for (const [name, range] of missing)
217
+ log.step(`${name} wymaga ${range}`);
218
+ }
173
219
  function printSummary(installed, metas, dryRun) {
174
220
  log.info(`\n${dryRun ? 'Zainstalowałbym' : 'Zainstalowano'}: ${installed.join(', ')}`);
175
221
  for (const name of installed) {
176
222
  const meta = metas.get(name);
177
223
  if (meta.usage) {
178
224
  log.info(`\n${name}:`);
179
- log.step(meta.usage.replace(/\n/g, '\n '));
225
+ log.step(meta.usage.replaceAll('\n', '\n '));
180
226
  }
181
227
  const ports = meta.api?.ports ?? [];
182
228
  for (const port of ports) {
package/dist/semver.js CHANGED
@@ -31,7 +31,9 @@ function caretUpper([maj, min, patch]) {
31
31
  }
32
32
  /** Pojedynczy komparator. `null` = nie rozumiem tego kształtu. */
33
33
  function satisfiesOne(version, comparator) {
34
- const m = /^(>=|<=|>|<|=|\^|~|)\s*(.+)$/.exec(comparator.trim());
34
+ // `\S` po `\s*` jest istotne: `\s*(.+)` pozwalałoby dopasować spację obu członom,
35
+ // co daje wykładniczy nawrót przy patologicznym wejściu.
36
+ const m = /^(>=|<=|>|<|=|\^|~|)\s*(\S.*)$/.exec(comparator.trim());
35
37
  if (!m)
36
38
  return null;
37
39
  const [, op, rest] = m;
@@ -64,7 +66,7 @@ export function satisfies(version, range) {
64
66
  if (comparators.length === 0)
65
67
  continue;
66
68
  const results = comparators.map((c) => satisfiesOne(parsed, c));
67
- if (results.some((r) => r === null))
69
+ if (results.includes(null))
68
70
  return true; // nieznany kształt — nie oceniamy
69
71
  if (results.every(Boolean))
70
72
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/ui",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "CLI Koncept UI — instaluje bloki (copy-paste) do projektu Angular: init, add, list.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",