uipkge-ng 0.1.0 → 0.1.2
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/CHANGELOG.md +44 -0
- package/README.md +41 -7
- package/dist/commands/add.js +25 -3
- package/dist/commands/diff.js +27 -6
- package/dist/commands/doctor.js +279 -0
- package/dist/commands/info.js +10 -2
- package/dist/commands/init.js +16 -6
- package/dist/commands/list.js +21 -3
- package/dist/config.js +9 -1
- package/dist/files.js +16 -7
- package/dist/index.js +7 -3
- package/dist/project.js +46 -17
- package/dist/resolve.js +16 -4
- package/dist/setup.js +41 -10
- package/package.json +7 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `uipkge-ng` (and its aliases `@uipkge/ng` and `uipkge`). This project follows [Semantic Versioning](https://semver.org); while it's 0.x, minor versions may change behaviour.
|
|
4
|
+
|
|
5
|
+
## 0.1.2 — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Nx workspaces: Angular apps are found from their `project.json` (Node apps and libraries are skipped), `@/*` and `@/ui/*` go into `tsconfig.base.json` beside your library paths, and a Sass app gets the tokens in its `project.json`. Works from the workspace root or inside an app.
|
|
10
|
+
- Components added from a URL or a local file are recorded in `components.json` under `sources`, so `list`, `diff`, `info` and `doctor` include them like registry components.
|
|
11
|
+
- `doctor` and `init` warn about a global `* { margin: 0; padding: 0 }` outside a CSS layer — it overrides Tailwind v4's layered utilities and strips every component's spacing — and tell you to move it into `@layer base`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- A project with `@analogjs/vite-plugin-angular` (used for Vitest in Nx and some Angular CLI setups) is no longer mistaken for an Analog app, which pointed Tailwind setup at Vite instead of PostCSS.
|
|
16
|
+
|
|
17
|
+
## 0.1.1 — 2026-09-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `doctor`: checks the setup (Node, Angular app, `components.json`, registries and their tokens, aliases, Tailwind v4 and its build step, design tokens actually loaded, `cn()`, incomplete components, undeclared component packages) and prints the fix for each problem. Exits with 1 on failure; `--json`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `add` no longer stops half-way on a bundle item — an item with no files of its own, only dependencies, such as `charts`. Bundles now install their dependencies.
|
|
26
|
+
- `add` checks every item before writing anything, so a broken item can't leave a project half-installed.
|
|
27
|
+
- `init` works when the default registry (`registryUrl`) ships only its own components: `utils` and `tailwind` come from uipkge instead, and it says so.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Resolving dependencies fetches in parallel: adding all 200 uipkge components resolves in about 3 seconds instead of about 70.
|
|
32
|
+
- `build` writes bare names in `registryDependencies` as uipkge URLs, so they resolve the same in every project whatever its default registry is. A bare name that matches one of the registry's own items is now an error — use `@your-registry/name` or a URL for those.
|
|
33
|
+
|
|
34
|
+
## 0.1.0 — 2026-09-25
|
|
35
|
+
|
|
36
|
+
First release.
|
|
37
|
+
|
|
38
|
+
- `init`: Tailwind CSS v4, the `cn()` helper, design tokens and tsconfig paths; Sass/Less apps get the tokens registered in `angular.json`.
|
|
39
|
+
- `add`: components with their dependencies, `@angular/*` pinned to the app's version; `--dry-run`, `--view`, `--diff`, `--path`, `--overwrite`; stops before writing when a file you have differs.
|
|
40
|
+
- `list`, `view`, `diff`, `info`.
|
|
41
|
+
- Custom folders through `aliases`, with import rewriting.
|
|
42
|
+
- Named and private registries (`@name/item`, `{name}` URLs, `${ENV}` headers from the environment, `.env.local` and `.env`).
|
|
43
|
+
- `build` for publishing your own registry; `mcp` server for AI assistants.
|
|
44
|
+
- Multi-project Angular workspaces and npm/pnpm/yarn/bun monorepos.
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ import { UiButtonComponent } from '@/ui/button';
|
|
|
41
41
|
## Requirements
|
|
42
42
|
|
|
43
43
|
- Node.js **20.12** or newer
|
|
44
|
-
- An Angular CLI application with standalone components (Analog/Vite supported
|
|
44
|
+
- An Angular CLI application with standalone components — including multi-project workspaces and Nx (Analog/Vite supported)
|
|
45
45
|
- npm, pnpm, yarn or bun — detected from your lockfile
|
|
46
46
|
|
|
47
47
|
## Getting started
|
|
@@ -92,6 +92,7 @@ Dark mode follows a `dark` class on `<html>`.
|
|
|
92
92
|
| [`view <items...>`](#view) | show a component's details and source |
|
|
93
93
|
| [`diff [item]`](#diff) | compare your copies with the registry |
|
|
94
94
|
| [`info`](#info) | project, config and registry details |
|
|
95
|
+
| [`doctor`](#doctor) | check the setup and explain how to fix problems |
|
|
95
96
|
| [`build [registry.json]`](#publishing-your-own-registry) | build your own registry |
|
|
96
97
|
| [`mcp`](#ai-assistants-mcp) | MCP server for AI assistants |
|
|
97
98
|
|
|
@@ -168,12 +169,37 @@ Checks what you installed from the default registry and from every named one. Ta
|
|
|
168
169
|
|
|
169
170
|
Prints the CLI and Node versions, the project (app, Angular, package manager, Tailwind, PostCSS), `components.json`, the folders your aliases resolve to, whether the registry is reachable and what's installed. Paste it into bug reports; `--json` for machines.
|
|
170
171
|
|
|
172
|
+
### doctor
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx uipkge-ng doctor
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Checks the whole setup — like `react-native doctor` — and says how to fix anything wrong:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
Styling
|
|
182
|
+
✔ Tailwind CSS ^4.3.3
|
|
183
|
+
✔ Tailwind build .postcssrc.json uses @tailwindcss/postcss
|
|
184
|
+
✖ Design tokens src/uipkge.css isn't loaded
|
|
185
|
+
→ Add `@import './uipkge.css';` to src/styles.css, or list src/uipkge.css under "styles" in angular.json.
|
|
186
|
+
|
|
187
|
+
Components
|
|
188
|
+
! Incomplete components files missing from 1: card
|
|
189
|
+
→ Reinstall with `uipkge-ng add card --overwrite`.
|
|
190
|
+
✖ Component packages not in package.json: class-variance-authority
|
|
191
|
+
→ Run `npm install class-variance-authority`.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
It checks Node, the Angular app (and workspace), `components.json`, that the registry and every named registry answer (and their tokens are set), that your aliases resolve, Tailwind v4 and its build step, that the design tokens are actually loaded, the `cn()` helper, global CSS resets that would override Tailwind (`* { padding: 0 }` outside a layer), components with missing files, and npm packages your components need but `package.json` doesn't list. It changes nothing. It exits with 1 when something fails, so it can run in CI; `--json` for scripts.
|
|
195
|
+
|
|
171
196
|
## Existing projects
|
|
172
197
|
|
|
173
198
|
uipkge-ng is built to be run on apps that already have code in them:
|
|
174
199
|
|
|
175
200
|
- **Your files are never overwritten silently.** If a file `add` would write already exists with different content — say your own `button.component.ts` — it stops before writing anything. Interactively it asks; otherwise re-run with `--overwrite`, or install elsewhere with `--path src/app/uipkge`.
|
|
176
201
|
- **Your styles stay yours.** Tokens live in their own file (`src/uipkge.css`); your stylesheet only gains one import line.
|
|
202
|
+
- **Your global resets are checked.** Tailwind v4 keeps its utilities in a CSS layer, and CSS outside a layer always wins — so a `* { margin: 0; padding: 0 }` reset (common in older apps and in some starters) strips every component's spacing. `init` and `doctor` point it out; wrap it in `@layer base { … }` and everything spaces correctly again.
|
|
177
203
|
- **Your tsconfig stays yours.** Existing paths are kept. If `@/*` already points somewhere else, uipkge's path is added as a fallback and the helper goes where your mapping resolves.
|
|
178
204
|
- **Your `utils.ts` stays yours.** If `src/lib/utils.ts` exists, `init` keeps it and warns if it doesn't export `cn()`.
|
|
179
205
|
- **Try first.** `add --dry-run` shows exactly what would change, `add --diff` shows how.
|
|
@@ -191,7 +217,7 @@ Each app gets its own `components.json`, tokens and paths — written to its `ts
|
|
|
191
217
|
|
|
192
218
|
**npm, pnpm, yarn and bun workspaces**: run it in the app (`apps/web`). The package manager comes from the workspace's lockfile.
|
|
193
219
|
|
|
194
|
-
**Nx**
|
|
220
|
+
**Nx** workspaces work from the workspace root or inside an app (`apps/shop`). Angular apps are found from their `project.json` — Node apps and libraries are skipped — and `@/*` / `@/ui/*` go into `tsconfig.base.json` next to your library paths, where Nx keeps them. A Sass app gets the tokens in its `project.json` `styles`.
|
|
195
221
|
|
|
196
222
|
## components.json
|
|
197
223
|
|
|
@@ -219,6 +245,7 @@ Each app gets its own `components.json`, tokens and paths — written to its `ts
|
|
|
219
245
|
| `tokens` | the file uipkge owns for design tokens and item CSS |
|
|
220
246
|
| `aliases` | where components, helpers and blocks go — see [Custom folders](#custom-folders) |
|
|
221
247
|
| `registries` | named and private registries — see [Registries](#registries) |
|
|
248
|
+
| `sources` | written by `add`: where components added from a URL or a local file came from, so `list`, `diff`, `info` and `doctor` include them |
|
|
222
249
|
|
|
223
250
|
## Custom folders
|
|
224
251
|
|
|
@@ -237,7 +264,7 @@ Every command then uses those folders, and the `@/ui/…` and `@/lib/…` import
|
|
|
237
264
|
|
|
238
265
|
## Registries
|
|
239
266
|
|
|
240
|
-
|
|
267
|
+
Out of the box everything comes from the uipkge registry, `https://uipkge.dev/r/angular` — no setup needed. Any shadcn-style registry works — a public one, your company's private one, or one you [build yourself](#publishing-your-own-registry).
|
|
241
268
|
|
|
242
269
|
### Changing the default registry
|
|
243
270
|
|
|
@@ -275,7 +302,7 @@ uipkge-ng view @internal/data-table
|
|
|
275
302
|
- `{name}` becomes the item name; `list` reads the index at `{name}` = `registry`.
|
|
276
303
|
- `${VAR}` in headers comes from the environment, then `.env.local`, then `.env`. A missing variable is an error before any request is made.
|
|
277
304
|
- Headers are only ever sent to that registry's own URLs.
|
|
278
|
-
- `list @name`, `info` and `diff` show which of its items you have.
|
|
305
|
+
- `list @name`, `info` and `diff` show which of its items you have. Components added from a URL or a local file are recorded under `sources`, so `list` (in their own group), `diff`, `info` and `doctor` include them too.
|
|
279
306
|
|
|
280
307
|
## Publishing your own registry
|
|
281
308
|
|
|
@@ -349,6 +376,12 @@ It never writes files — installing stays a command you run. It reads the `comp
|
|
|
349
376
|
| `--debug` | stack traces on errors |
|
|
350
377
|
| `-h, --help` / `-v, --version` | |
|
|
351
378
|
|
|
379
|
+
## Known limits
|
|
380
|
+
|
|
381
|
+
- **Tested by hand on macOS.** CI runs Linux, macOS and Windows on every push (Windows doesn't block yet).
|
|
382
|
+
- **Analog** apps build with Vite: `init` installs `@tailwindcss/vite`, but you add `tailwindcss()` to `vite.config.ts` yourself.
|
|
383
|
+
- `--path` moves only the components you name; their dependencies stay in the default folder so imports keep resolving.
|
|
384
|
+
|
|
352
385
|
## Troubleshooting
|
|
353
386
|
|
|
354
387
|
| Message | Fix |
|
|
@@ -358,9 +391,8 @@ It never writes files — installing stays a command you run. It reads the `comp
|
|
|
358
391
|
| `N files already exist with different content` | keep yours and use `--path`, or replace with `--overwrite` |
|
|
359
392
|
| `The "ui" alias "…" is not mapped in tsconfig.json "paths"` | add the mapping, or reset `aliases` to the defaults |
|
|
360
393
|
| `Registry "@acme" needs the environment variable ACME_TOKEN` | set it in your shell, `.env.local` or `.env` |
|
|
361
|
-
| `Nx workspaces are not supported yet` | use a standalone Angular CLI app for now |
|
|
362
394
|
|
|
363
|
-
Still stuck? Include the output of `npx uipkge-ng info` when you [open an issue](https://github.com/uday-a/uipkge-cli/issues).
|
|
395
|
+
Start with `npx uipkge-ng doctor` — it finds most setup problems and tells you the fix. Still stuck? Include the output of `npx uipkge-ng info` when you [open an issue](https://github.com/uday-a/uipkge-cli/issues).
|
|
364
396
|
|
|
365
397
|
## Credits
|
|
366
398
|
|
|
@@ -368,4 +400,6 @@ Created by **Uday Adaka** as part of **[uipkge](https://uipkge.dev)**. It uses t
|
|
|
368
400
|
|
|
369
401
|
## License
|
|
370
402
|
|
|
371
|
-
[MIT](./LICENSE) © 2026 Uday Adaka — uipkge.
|
|
403
|
+
[MIT](./LICENSE) © 2026 Uday Adaka — uipkge.
|
|
404
|
+
|
|
405
|
+
In plain words: you can use it for anything — personal or commercial — fork it, change it, and sell or redistribute it, including your modified version. The one condition: keep the copyright line (Uday Adaka, uipkge) and the license text in every copy.
|
package/dist/commands/add.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { registryFor, requireConfig } from '../config.js';
|
|
3
|
+
import { registryFor, requireConfig, toSource, writeConfig } from '../config.js';
|
|
4
4
|
import { UipkgeError } from '../errors.js';
|
|
5
5
|
import { applyCss, applyEnv, envKeys, hasCss } from '../extras.js';
|
|
6
|
-
import { conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
|
|
6
|
+
import { assertWritable, conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
|
|
7
7
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
8
8
|
import { color, log, plural } from '../output.js';
|
|
9
9
|
import { installPackages, missingPackages, pinAngular } from '../packages.js';
|
|
10
10
|
import { declaredDependencies, loadProject, resolveTarget } from '../project.js';
|
|
11
11
|
import { confirm, isInteractive, pickComponents } from '../prompts.js';
|
|
12
|
+
import { refKind } from '../registry.js';
|
|
12
13
|
import { planInstall } from '../resolve.js';
|
|
13
14
|
import { colorPatch, compareItem } from './diff.js';
|
|
14
15
|
import { firstSentence, isListable } from './list.js';
|
|
@@ -66,10 +67,29 @@ export async function runAdd(options) {
|
|
|
66
67
|
return printDiff(root, previewPlan, options.diff);
|
|
67
68
|
if (options.dryRun)
|
|
68
69
|
return printDryRun(root, config.tokens, plan, packages, devPackages, options.overwrite);
|
|
70
|
+
/** Remember where URL/file components came from, so diff/info/doctor can find them later. */
|
|
71
|
+
const recordSources = async () => {
|
|
72
|
+
const cwd = path.resolve(options.cwd);
|
|
73
|
+
let changed = false;
|
|
74
|
+
for (const ref of names.filter(n => refKind(n) === 'url' || refKind(n) === 'file')) {
|
|
75
|
+
const { name } = await registry.item(ref);
|
|
76
|
+
const source = toSource(root, cwd, ref);
|
|
77
|
+
if (config.sources?.[name] !== source) {
|
|
78
|
+
config.sources = { ...config.sources, [name]: source };
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (changed)
|
|
83
|
+
await writeConfig(root, config);
|
|
84
|
+
};
|
|
69
85
|
if (!plan.install.length) {
|
|
86
|
+
await recordSources();
|
|
70
87
|
log.info(`Already installed: ${plan.skipped.join(', ')}. ${color.dim('Use --overwrite to reinstall.')}`);
|
|
71
88
|
return;
|
|
72
89
|
}
|
|
90
|
+
// Every item must be writable before anything is written or installed.
|
|
91
|
+
for (const item of plan.install)
|
|
92
|
+
assertWritable(root, item);
|
|
73
93
|
// Existing files that differ: never write the rest of an item around them.
|
|
74
94
|
let overwrite = options.overwrite;
|
|
75
95
|
const conflicts = overwrite ? [] : plan.install.flatMap(item => conflictingFiles(root, item).map(file => `${file} (${item.name})`));
|
|
@@ -115,8 +135,10 @@ export async function runAdd(options) {
|
|
|
115
135
|
log.warn(`${item.name} ships a Tailwind v3 config; Tailwind v4 reads config from CSS, so it was not applied.`);
|
|
116
136
|
if (item.docs)
|
|
117
137
|
docs.push([item.name, item.docs]);
|
|
118
|
-
|
|
138
|
+
const summary = targetedFiles(item).length ? plural(result.written.length, 'file') : 'bundle';
|
|
139
|
+
log.success(`${item.name} ${color.dim(`(${[summary, ...extras].join(', ')})`)}`);
|
|
119
140
|
}
|
|
141
|
+
await recordSources();
|
|
120
142
|
log.blank();
|
|
121
143
|
log.info(`${color.green('✔')} Added ${plural(plan.install.length, 'component')}.`);
|
|
122
144
|
if (plan.skipped.length)
|
package/dist/commands/diff.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { createTwoFilesPatch } from 'diff';
|
|
5
|
-
import { registryFor, requireConfig } from '../config.js';
|
|
5
|
+
import { fromSource, registryFor, requireConfig } from '../config.js';
|
|
6
6
|
import { isItemInstalled, targetedFiles, normalizeEol } from '../files.js';
|
|
7
7
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
8
8
|
import { color, log, plural } from '../output.js';
|
|
@@ -59,7 +59,7 @@ export async function runDiff(options) {
|
|
|
59
59
|
await diffOne(root, adaptItem(await registry.item(options.name), layout), options.name);
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
await diffAll(root, registry, Object.keys(config.registries ?? {}), layout);
|
|
62
|
+
await diffAll(root, registry, Object.keys(config.registries ?? {}), layout, config.sources ?? {});
|
|
63
63
|
}
|
|
64
64
|
async function diffOne(root, item, ref) {
|
|
65
65
|
const files = await compareItem(root, item);
|
|
@@ -110,8 +110,29 @@ export async function installedRefs(root, registry, namespaces, layout) {
|
|
|
110
110
|
}));
|
|
111
111
|
return refs.flat();
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Components added from a URL or file (components.json `sources`) that are still installed,
|
|
115
|
+
* as loadable refs mapped to how they're shown (the stored source). A source that can't be
|
|
116
|
+
* loaded any more is reported and skipped.
|
|
117
|
+
*/
|
|
118
|
+
export async function installedSources(root, registry, sources, layout) {
|
|
119
|
+
const found = new Map();
|
|
120
|
+
for (const source of Object.values(sources)) {
|
|
121
|
+
const ref = fromSource(root, source);
|
|
122
|
+
try {
|
|
123
|
+
if (isItemInstalled(root, adaptItem(await registry.item(ref), layout)))
|
|
124
|
+
found.set(ref, source);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
log.warn(`Skipped ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
async function diffAll(root, registry, namespaces, layout, sources) {
|
|
133
|
+
const external = await installedSources(root, registry, sources, layout);
|
|
134
|
+
const installed = [...(await installedRefs(root, registry, namespaces, layout)), ...external.keys()];
|
|
135
|
+
const label = (ref) => external.get(ref) ?? ref;
|
|
115
136
|
if (!installed.length) {
|
|
116
137
|
log.info('No uipkge components are installed yet. Add one with `uipkge-ng add <name>`.');
|
|
117
138
|
return;
|
|
@@ -133,8 +154,8 @@ async function diffAll(root, registry, namespaces, layout) {
|
|
|
133
154
|
}
|
|
134
155
|
outdated.sort();
|
|
135
156
|
log.info(`${plural(outdated.length, 'component')} ${outdated.length === 1 ? 'differs' : 'differ'} from the registry:`);
|
|
136
|
-
for (const
|
|
137
|
-
log.info(` ${color.yellow('•')} ${
|
|
157
|
+
for (const ref of outdated)
|
|
158
|
+
log.info(` ${color.yellow('•')} ${label(ref)}`);
|
|
138
159
|
log.blank();
|
|
139
160
|
log.info(color.dim('See the changes with `uipkge-ng diff <name>`.'));
|
|
140
161
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fromSource, readConfig, registryFor } from '../config.js';
|
|
4
|
+
import { targetedFiles } from '../files.js';
|
|
5
|
+
import { adaptItem, resolveLayout } from '../layout.js';
|
|
6
|
+
import { color } from '../output.js';
|
|
7
|
+
import { installArgs, missingPackages } from '../packages.js';
|
|
8
|
+
import { angularApps, declaredDependencies, detectPackageManager, findApp, readPackageJson, resolveTarget } from '../project.js';
|
|
9
|
+
import { FOUNDATION_ITEMS, namedRegistryFrom } from '../registry.js';
|
|
10
|
+
import { projectEnv } from '../env.js';
|
|
11
|
+
import { TAILWIND_POSTCSS, readPostcssConfig, relativeImport, unlayeredResets } from '../setup.js';
|
|
12
|
+
const MIN_NODE = [20, 12];
|
|
13
|
+
/** `3: a, b, c` or `202: a, b, c, d, e, f … and 196 more`. */
|
|
14
|
+
const summarize = (names, shown = 6) => `${names.length}: ${names.slice(0, shown).join(', ')}${names.length > shown ? ` … and ${names.length - shown} more` : ''}`;
|
|
15
|
+
/**
|
|
16
|
+
* Checks a project's uipkge setup, like `react-native doctor`. Never throws:
|
|
17
|
+
* every problem becomes a failed check with a fix, and later checks that
|
|
18
|
+
* depend on it are skipped rather than failing with noise.
|
|
19
|
+
*/
|
|
20
|
+
export async function runChecks(cwd) {
|
|
21
|
+
const checks = [];
|
|
22
|
+
const push = (c) => (checks.push(c), c);
|
|
23
|
+
// ── Environment ──
|
|
24
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
25
|
+
const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
|
|
26
|
+
push({
|
|
27
|
+
section: 'Environment',
|
|
28
|
+
title: 'Node.js',
|
|
29
|
+
status: nodeOk ? 'pass' : 'fail',
|
|
30
|
+
detail: process.version,
|
|
31
|
+
fix: nodeOk ? undefined : `uipkge-ng needs Node ${MIN_NODE.join('.')} or newer.`,
|
|
32
|
+
});
|
|
33
|
+
// ── Project ──
|
|
34
|
+
const location = findApp(path.resolve(cwd));
|
|
35
|
+
if (!location) {
|
|
36
|
+
push({ section: 'Project', title: 'package.json', status: 'fail', detail: 'none found', fix: 'Run this inside your Angular project.' });
|
|
37
|
+
return checks;
|
|
38
|
+
}
|
|
39
|
+
if (location.ambiguous) {
|
|
40
|
+
push({
|
|
41
|
+
section: 'Project',
|
|
42
|
+
title: 'Angular app',
|
|
43
|
+
status: 'fail',
|
|
44
|
+
detail: `this workspace has ${location.ambiguous.length} apps: ${location.ambiguous.map(a => a.name).join(', ')}`,
|
|
45
|
+
fix: `Run it inside one app's folder, or pass it with \`-c ${path.relative(cwd, location.ambiguous[0].root) || '.'}\`.`,
|
|
46
|
+
});
|
|
47
|
+
return checks;
|
|
48
|
+
}
|
|
49
|
+
const { root, packageRoot } = location;
|
|
50
|
+
let pkg;
|
|
51
|
+
try {
|
|
52
|
+
pkg = await readPackageJson(packageRoot);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
push({ section: 'Project', title: 'package.json', status: 'fail', detail: error.message });
|
|
56
|
+
return checks;
|
|
57
|
+
}
|
|
58
|
+
const deps = declaredDependencies(pkg);
|
|
59
|
+
const angular = deps['@angular/core'];
|
|
60
|
+
push({
|
|
61
|
+
section: 'Project',
|
|
62
|
+
title: 'Angular',
|
|
63
|
+
status: angular ? 'pass' : 'fail',
|
|
64
|
+
detail: angular ? `${angular}${location.app ? ` (app: ${location.app.name})` : ''}` : 'no @angular/core in package.json',
|
|
65
|
+
fix: angular ? undefined : 'uipkge-ng works with Angular CLI apps.',
|
|
66
|
+
});
|
|
67
|
+
if (!angular)
|
|
68
|
+
return checks;
|
|
69
|
+
if (existsSync(path.join(packageRoot, 'nx.json')))
|
|
70
|
+
push({ section: 'Project', title: 'Workspace', status: 'pass', detail: 'Nx' });
|
|
71
|
+
const pm = detectPackageManager(packageRoot, pkg);
|
|
72
|
+
push({ section: 'Project', title: 'Package manager', status: 'pass', detail: pm });
|
|
73
|
+
// ── Configuration ──
|
|
74
|
+
let config = null;
|
|
75
|
+
try {
|
|
76
|
+
config = await readConfig(root);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
push({ section: 'Configuration', title: 'components.json', status: 'fail', detail: error.message, fix: 'Fix the JSON, or run `uipkge-ng init --overwrite`.' });
|
|
80
|
+
return checks;
|
|
81
|
+
}
|
|
82
|
+
if (!config) {
|
|
83
|
+
push({ section: 'Configuration', title: 'components.json', status: 'fail', detail: `not found in ${root}`, fix: 'Run `uipkge-ng init`.' });
|
|
84
|
+
return checks;
|
|
85
|
+
}
|
|
86
|
+
push({ section: 'Configuration', title: 'components.json', status: 'pass', detail: path.relative(cwd, path.join(root, 'components.json')) || 'components.json' });
|
|
87
|
+
const registry = registryFor(root, config, cwd);
|
|
88
|
+
let index = null;
|
|
89
|
+
try {
|
|
90
|
+
index = (await registry.index()).items;
|
|
91
|
+
push({ section: 'Configuration', title: 'Registry', status: 'pass', detail: `${registry.baseUrl} (${index.length} items)` });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
push({
|
|
95
|
+
section: 'Configuration',
|
|
96
|
+
title: 'Registry',
|
|
97
|
+
status: 'fail',
|
|
98
|
+
detail: `${registry.baseUrl}: ${error.message}`,
|
|
99
|
+
fix: 'Check your connection, "registryUrl" in components.json, or UIPKGE_REGISTRY_URL.',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const env = projectEnv(root);
|
|
103
|
+
for (const [ns, raw] of Object.entries(config.registries ?? {})) {
|
|
104
|
+
try {
|
|
105
|
+
namedRegistryFrom(ns, raw, env);
|
|
106
|
+
await registry.index(ns);
|
|
107
|
+
push({ section: 'Configuration', title: `Registry ${ns}`, status: 'pass' });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
push({
|
|
111
|
+
section: 'Configuration',
|
|
112
|
+
title: `Registry ${ns}`,
|
|
113
|
+
status: 'fail',
|
|
114
|
+
detail: error.message,
|
|
115
|
+
fix: `Check "${ns}" under "registries" in components.json; tokens come from the environment, .env.local or .env.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let layout = null;
|
|
120
|
+
try {
|
|
121
|
+
layout = resolveLayout(root, config.aliases);
|
|
122
|
+
push({ section: 'Configuration', title: 'Folders (aliases)', status: 'pass', detail: `ui → ${layout.dirs.ui}, lib → ${layout.dirs.lib}` });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
push({ section: 'Configuration', title: 'Folders (aliases)', status: 'fail', detail: error.message, fix: 'Map the aliases in tsconfig "paths", or run `uipkge-ng init --overwrite`.' });
|
|
126
|
+
}
|
|
127
|
+
// ── Styling ──
|
|
128
|
+
const tailwind = deps['tailwindcss'];
|
|
129
|
+
const tailwindMajor = Number(tailwind?.match(/\d+/)?.[0] ?? 0);
|
|
130
|
+
push({
|
|
131
|
+
section: 'Styling',
|
|
132
|
+
title: 'Tailwind CSS',
|
|
133
|
+
status: tailwindMajor >= 4 ? 'pass' : 'fail',
|
|
134
|
+
detail: tailwind ?? 'not installed',
|
|
135
|
+
fix: tailwindMajor >= 4 ? undefined : tailwind ? 'uipkge components need Tailwind CSS v4.' : 'Run `uipkge-ng init`.',
|
|
136
|
+
});
|
|
137
|
+
const analog = Boolean(deps['@analogjs/platform']);
|
|
138
|
+
if (analog) {
|
|
139
|
+
push({ section: 'Styling', title: 'Tailwind build', status: 'warn', detail: 'Analog builds with Vite', fix: "Check vite.config.ts has `tailwindcss()` from '@tailwindcss/vite' in plugins." });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const postcss = await readPostcssConfig(packageRoot);
|
|
143
|
+
const wired = postcss?.content.includes(TAILWIND_POSTCSS);
|
|
144
|
+
push({
|
|
145
|
+
section: 'Styling',
|
|
146
|
+
title: 'Tailwind build',
|
|
147
|
+
status: wired ? 'pass' : 'fail',
|
|
148
|
+
detail: wired ? `${postcss.file} uses ${TAILWIND_POSTCSS}` : postcss ? `${postcss.file} doesn't use ${TAILWIND_POSTCSS}` : 'no PostCSS config',
|
|
149
|
+
fix: wired ? undefined : `Add "${TAILWIND_POSTCSS}" to your PostCSS plugins, or run \`uipkge-ng init --overwrite\`.`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const tokensFile = path.join(root, config.tokens);
|
|
153
|
+
if (!existsSync(tokensFile)) {
|
|
154
|
+
push({ section: 'Styling', title: 'Design tokens', status: 'fail', detail: `${config.tokens} is missing`, fix: 'Run `uipkge-ng init --overwrite`.' });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Loaded either by an @import in the global stylesheet or as its own entry in angular.json "styles".
|
|
158
|
+
const importPath = relativeImport(config.styles, config.tokens);
|
|
159
|
+
const stylesFile = path.join(root, config.styles);
|
|
160
|
+
const imported = existsSync(stylesFile) && new RegExp(`@import\\s+(url\\()?['"]${importPath.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}['"]`).test(readFileSync(stylesFile, 'utf8'));
|
|
161
|
+
const app = location.app ?? angularApps(packageRoot).find(a => a.root === root);
|
|
162
|
+
const registered = Boolean(app?.styles.some(s => path.resolve(root, s) === tokensFile));
|
|
163
|
+
push({
|
|
164
|
+
section: 'Styling',
|
|
165
|
+
title: 'Design tokens',
|
|
166
|
+
status: imported || registered ? 'pass' : 'fail',
|
|
167
|
+
detail: imported ? `${config.styles} imports ${importPath}` : registered ? `${config.tokens} is in angular.json "styles"` : `${config.tokens} isn't loaded`,
|
|
168
|
+
fix: imported || registered ? undefined : `Add \`@import '${importPath}';\` to ${config.styles}, or list ${config.tokens} under "styles" in angular.json.`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const globalStyles = path.join(root, config.styles);
|
|
172
|
+
if (existsSync(globalStyles)) {
|
|
173
|
+
const resets = unlayeredResets(readFileSync(globalStyles, 'utf8'));
|
|
174
|
+
push({
|
|
175
|
+
section: 'Styling',
|
|
176
|
+
title: 'Global resets',
|
|
177
|
+
status: resets.length ? 'warn' : 'pass',
|
|
178
|
+
detail: resets.length
|
|
179
|
+
? `${config.styles} zeroes margin/padding on \`${resets[0]}\` outside a CSS layer, which overrides Tailwind's utilities (components lose their spacing)`
|
|
180
|
+
: 'none that override Tailwind',
|
|
181
|
+
fix: resets.length ? `Move that rule into \`@layer base { … }\` in ${config.styles}.` : undefined,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (layout) {
|
|
185
|
+
const utils = path.join(root, layout.dirs.lib, 'utils.ts');
|
|
186
|
+
const hasCn = existsSync(utils) && /export\s+(?:function|const|let)\s+cn\b|export\s*\{[^}]*\bcn\b[^}]*\}/.test(readFileSync(utils, 'utf8'));
|
|
187
|
+
push({
|
|
188
|
+
section: 'Styling',
|
|
189
|
+
title: 'cn() helper',
|
|
190
|
+
status: hasCn ? 'pass' : 'fail',
|
|
191
|
+
detail: hasCn ? path.relative(root, utils) : existsSync(utils) ? `${path.relative(root, utils)} doesn't export cn()` : `${path.relative(root, utils)} is missing`,
|
|
192
|
+
fix: hasCn ? undefined : 'Every component imports it. Run `uipkge-ng init --overwrite`, or add it (`uipkge-ng view utils`).',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// ── Components ──
|
|
196
|
+
if (!index || !layout) {
|
|
197
|
+
push({ section: 'Components', title: 'Installed components', status: 'skip', detail: 'needs the registry and folders above' });
|
|
198
|
+
return checks;
|
|
199
|
+
}
|
|
200
|
+
// Registry components, plus those added from a URL or file (recorded in `sources`).
|
|
201
|
+
const candidates = index.filter(i => !FOUNDATION_ITEMS.has(i.name));
|
|
202
|
+
for (const [name, source] of Object.entries(config.sources ?? {})) {
|
|
203
|
+
try {
|
|
204
|
+
candidates.push(await registry.item(fromSource(root, source)));
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
push({
|
|
208
|
+
section: 'Components',
|
|
209
|
+
title: `Source of ${name}`,
|
|
210
|
+
status: 'warn',
|
|
211
|
+
detail: `${source}: ${error.message}`,
|
|
212
|
+
fix: `Fix or remove "${name}" under "sources" in components.json.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const installed = [];
|
|
217
|
+
const partial = [];
|
|
218
|
+
for (const item of candidates) {
|
|
219
|
+
const files = targetedFiles(adaptItem(item, layout));
|
|
220
|
+
if (!files.length)
|
|
221
|
+
continue;
|
|
222
|
+
const present = files.filter(f => existsSync(resolveTarget(root, f.target))).length;
|
|
223
|
+
if (present === files.length)
|
|
224
|
+
installed.push(item);
|
|
225
|
+
else if (present > 0)
|
|
226
|
+
partial.push(item.name);
|
|
227
|
+
}
|
|
228
|
+
push({ section: 'Components', title: 'Installed components', status: 'pass', detail: installed.length ? summarize(installed.map(i => i.name)) : 'none yet' });
|
|
229
|
+
if (partial.length) {
|
|
230
|
+
push({
|
|
231
|
+
section: 'Components',
|
|
232
|
+
title: 'Incomplete components',
|
|
233
|
+
status: 'warn',
|
|
234
|
+
detail: `files missing from ${summarize(partial)}`,
|
|
235
|
+
fix: `Reinstall with \`uipkge-ng add ${partial.join(' ')} --overwrite\`.`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const missing = missingPackages(installed.flatMap(i => i.dependencies ?? []), deps);
|
|
239
|
+
push({
|
|
240
|
+
section: 'Components',
|
|
241
|
+
title: 'Component packages',
|
|
242
|
+
status: missing.length ? 'fail' : 'pass',
|
|
243
|
+
detail: missing.length ? `not in package.json: ${missing.join(', ')}` : 'all declared',
|
|
244
|
+
fix: missing.length ? `Run \`${pm} ${installArgs(pm, missing, false).join(' ')}\`.` : undefined,
|
|
245
|
+
});
|
|
246
|
+
return checks;
|
|
247
|
+
}
|
|
248
|
+
const MARK = {
|
|
249
|
+
pass: color.green('✔'),
|
|
250
|
+
warn: color.yellow('!'),
|
|
251
|
+
fail: color.red('✖'),
|
|
252
|
+
skip: color.dim('-'),
|
|
253
|
+
};
|
|
254
|
+
export async function runDoctor(options) {
|
|
255
|
+
const checks = await runChecks(options.cwd);
|
|
256
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
257
|
+
const warned = checks.filter(c => c.status === 'warn').length;
|
|
258
|
+
if (failed)
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
if (options.json) {
|
|
261
|
+
process.stdout.write(`${JSON.stringify({ ok: failed === 0, checks }, null, 2)}\n`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const width = Math.max(...checks.map(c => c.title.length)) + 2;
|
|
265
|
+
const out = [color.bold('uipkge-ng doctor')];
|
|
266
|
+
let section = '';
|
|
267
|
+
for (const c of checks) {
|
|
268
|
+
if (c.section !== section)
|
|
269
|
+
out.push('', color.bold(c.section)), (section = c.section);
|
|
270
|
+
out.push(` ${MARK[c.status]} ${c.title.padEnd(width)}${c.detail ? color.dim(c.detail) : ''}`);
|
|
271
|
+
if (c.fix && (c.status === 'fail' || c.status === 'warn'))
|
|
272
|
+
out.push(` ${' '.repeat(width)}${color.cyan('→')} ${c.fix}`);
|
|
273
|
+
}
|
|
274
|
+
const passed = checks.filter(c => c.status === 'pass').length;
|
|
275
|
+
out.push('', failed
|
|
276
|
+
? color.red(`${failed} problem${failed === 1 ? '' : 's'} found`) + color.dim(` · ${passed} passed${warned ? ` · ${warned} warning${warned === 1 ? '' : 's'}` : ''}`)
|
|
277
|
+
: color.green('Everything looks good.') + color.dim(` ${passed} checks passed${warned ? ` · ${warned} warning${warned === 1 ? '' : 's'}` : ''}`), '');
|
|
278
|
+
process.stdout.write(out.join('\n'));
|
|
279
|
+
}
|
package/dist/commands/info.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { readConfig, registryFor } from '../config.js';
|
|
4
4
|
import { isItemInstalled } from '../files.js';
|
|
5
|
+
import { installedSources } from './diff.js';
|
|
5
6
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
6
7
|
import { color } from '../output.js';
|
|
7
8
|
import { declaredDependencies, detectPackageManager, findApp, readPackageJson } from '../project.js';
|
|
@@ -26,7 +27,7 @@ export async function collectInfo(cwd, version) {
|
|
|
26
27
|
app: location?.app?.name ?? null,
|
|
27
28
|
apps: location?.ambiguous?.map(a => a.name) ?? null,
|
|
28
29
|
angular: deps['@angular/core'] ?? null,
|
|
29
|
-
analog: Boolean(deps['@analogjs/platform']
|
|
30
|
+
analog: Boolean(deps['@analogjs/platform']),
|
|
30
31
|
nx: Boolean(deps['nx'] || (packageRoot && existsSync(path.join(packageRoot, 'nx.json')))),
|
|
31
32
|
packageManager: packageRoot && pkg ? detectPackageManager(packageRoot, pkg) : null,
|
|
32
33
|
tailwind: deps['tailwindcss'] ?? null,
|
|
@@ -61,6 +62,13 @@ export async function collectInfo(cwd, version) {
|
|
|
61
62
|
catch (error) {
|
|
62
63
|
info.registry.error = error instanceof Error ? error.message : String(error);
|
|
63
64
|
}
|
|
65
|
+
// Components added from a URL or file, recorded by `add`.
|
|
66
|
+
if (root && config?.sources && layout) {
|
|
67
|
+
const found = new Set((await installedSources(root, registryFor(root, config, cwd), config.sources, layout)).values());
|
|
68
|
+
for (const [name, source] of Object.entries(config.sources))
|
|
69
|
+
if (found.has(source))
|
|
70
|
+
info.installed.push(`${name} (${source})`);
|
|
71
|
+
}
|
|
64
72
|
// Named registries: best effort, an unreachable one just adds nothing.
|
|
65
73
|
if (root && config?.registries) {
|
|
66
74
|
const registry = registryFor(root, config, cwd);
|
|
@@ -98,7 +106,7 @@ export async function runInfo(options) {
|
|
|
98
106
|
row('package manager', info.project.packageManager ?? '—'),
|
|
99
107
|
row('tailwind', yes(info.project.tailwind)),
|
|
100
108
|
row('postcss config', yes(info.project.postcssConfig)),
|
|
101
|
-
row('analog / nx', `${info.project.analog ? 'analog' : 'no'} / ${info.project.nx ?
|
|
109
|
+
row('analog / nx', `${info.project.analog ? 'analog' : 'no'} / ${info.project.nx ? 'nx' : 'no'}`),
|
|
102
110
|
'',
|
|
103
111
|
color.bold('components.json'),
|
|
104
112
|
info.config ? JSON.stringify(info.config, null, 2).split('\n').map(l => ` ${l}`).join('\n') : row('status', color.yellow('not initialized — run `uipkge-ng init`')),
|
package/dist/commands/init.js
CHANGED
|
@@ -7,10 +7,10 @@ import { writeItemFiles } from '../files.js';
|
|
|
7
7
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
8
8
|
import { color, log } from '../output.js';
|
|
9
9
|
import { installPackages, missingPackages } from '../packages.js';
|
|
10
|
-
import { declaredDependencies, loadProject } from '../project.js';
|
|
10
|
+
import { declaredDependencies, isNxWorkspace, loadProject } from '../project.js';
|
|
11
11
|
import { confirm, isInteractive } from '../prompts.js';
|
|
12
12
|
import { createRegistry, DEFAULT_REGISTRY_URL, NotFoundError, resolveRegistryUrl } from '../registry.js';
|
|
13
|
-
import { addGlobalStyle, appTsconfigEdit, planTailwind, readPostcssConfig, relativeImport, withTailwindPlugin, tsconfigsToEdit, withTokensImport, writeIfChanged, } from '../setup.js';
|
|
13
|
+
import { addGlobalStyle, appTsconfigEdit, planTailwind, readPostcssConfig, relativeImport, withTailwindPlugin, tsconfigsToEdit, unlayeredResets, withTokensImport, writeIfChanged, } from '../setup.js';
|
|
14
14
|
export async function runInit(options) {
|
|
15
15
|
const project = await loadProject(options.cwd);
|
|
16
16
|
const { root, packageRoot } = project;
|
|
@@ -74,14 +74,17 @@ export async function runInit(options) {
|
|
|
74
74
|
done.push('Tailwind CSS v4 installed (add the Vite plugin)');
|
|
75
75
|
}
|
|
76
76
|
// 3. tsconfig aliases the component sources import.
|
|
77
|
-
|
|
77
|
+
// Nx keeps every path in the shared tsconfig.base.json: an app tsconfig with its own `paths`
|
|
78
|
+
// would stop seeing libraries added there later.
|
|
79
|
+
const nxBase = path.join(packageRoot, 'tsconfig.base.json');
|
|
80
|
+
const tsconfigs = isNxWorkspace(packageRoot) && existsSync(nxBase) ? [path.relative(root, nxBase)] : tsconfigsToEdit(root);
|
|
78
81
|
if (!tsconfigs.length)
|
|
79
82
|
throw new UipkgeError(`No tsconfig.json in ${root}.`);
|
|
80
83
|
const edited = [];
|
|
81
84
|
for (const file of tsconfigs) {
|
|
82
85
|
const abs = path.join(root, file);
|
|
83
86
|
if (await writeIfChanged(abs, appTsconfigEdit(root, file, await readFile(abs, 'utf8'))))
|
|
84
|
-
edited.push(
|
|
87
|
+
edited.push(path.relative(packageRoot, abs).replace(/\\/g, '/'));
|
|
85
88
|
}
|
|
86
89
|
if (edited.length)
|
|
87
90
|
done.push(`tsconfig paths \`@/*\` and \`@/ui/*\` added (${edited.join(', ')})`);
|
|
@@ -106,6 +109,12 @@ export async function runInit(options) {
|
|
|
106
109
|
await linkTokens(project, config.styles, config.tokens, done);
|
|
107
110
|
await writeConfig(root, config);
|
|
108
111
|
done.push('components.json written');
|
|
112
|
+
// An unlayered `* { padding: 0 }` beats Tailwind v4's layered utilities and strips every component's spacing.
|
|
113
|
+
const stylesPath = path.join(root, config.styles);
|
|
114
|
+
const resets = existsSync(stylesPath) ? unlayeredResets(await readFile(stylesPath, 'utf8')) : [];
|
|
115
|
+
if (resets.length) {
|
|
116
|
+
log.warn(`${config.styles} resets margin/padding on \`${resets[0]}\` outside a CSS layer; it will override Tailwind's utilities. Move it into \`@layer base { … }\`.`);
|
|
117
|
+
}
|
|
109
118
|
log.blank();
|
|
110
119
|
log.success(color.bold('uipkge is ready.'));
|
|
111
120
|
for (const line of done)
|
|
@@ -134,8 +143,9 @@ async function linkTokens(project, styles, tokens, done) {
|
|
|
134
143
|
// Sass/Less: a CSS import there doesn't carry Tailwind v4's syntax reliably, so the tokens become their own global stylesheet.
|
|
135
144
|
if (!styles.endsWith('.css') && project.app) {
|
|
136
145
|
const entry = path.relative(project.packageRoot, path.join(root, tokens)).replace(/\\/g, '/');
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
const where = path.relative(project.packageRoot, project.app.config.file).replace(/\\/g, '/');
|
|
147
|
+
if (await addGlobalStyle(project.app.config, entry))
|
|
148
|
+
done.push(`${where} styles include ${entry}`);
|
|
139
149
|
return;
|
|
140
150
|
}
|
|
141
151
|
if (!existsSync(stylesFile)) {
|
package/dist/commands/list.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import { readConfig, registryFor } from '../config.js';
|
|
2
|
+
import { fromSource, readConfig, registryFor } from '../config.js';
|
|
3
3
|
import { UipkgeError } from '../errors.js';
|
|
4
4
|
import { isItemInstalled } from '../files.js';
|
|
5
5
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
@@ -17,6 +17,8 @@ export function firstSentence(text) {
|
|
|
17
17
|
return end === -1 ? flat : flat.slice(0, end);
|
|
18
18
|
}
|
|
19
19
|
const OTHER = 'other';
|
|
20
|
+
/** Group for components added from a URL or local file (components.json `sources`). */
|
|
21
|
+
export const SOURCES_GROUP = 'added from a url or file';
|
|
20
22
|
/** Filters, then groups by first category (alphabetical, `other` last), rows sorted by name. */
|
|
21
23
|
export function groupRows(items, filter) {
|
|
22
24
|
const query = filter.query?.trim().toLowerCase();
|
|
@@ -43,9 +45,11 @@ export function groupRows(items, filter) {
|
|
|
43
45
|
for (const row of rows)
|
|
44
46
|
byCategory.set(row.category, [...(byCategory.get(row.category) ?? []), row]);
|
|
45
47
|
return [...byCategory]
|
|
46
|
-
|
|
48
|
+
// Registry categories alphabetically, then `other`, then components from URLs/files.
|
|
49
|
+
.sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
|
|
47
50
|
.map(([cat, list]) => ({ category: cat, rows: list.sort((x, y) => x.name.localeCompare(y.name)) }));
|
|
48
51
|
}
|
|
52
|
+
const rank = (category) => (category === SOURCES_GROUP ? 2 : category === OTHER ? 1 : 0);
|
|
49
53
|
export const ellipsize = (text, width) => width < 2 ? '' : text.length <= width ? text : `${text.slice(0, width - 1).trimEnd()}…`;
|
|
50
54
|
export async function runList(options) {
|
|
51
55
|
const cwd = path.resolve(options.cwd);
|
|
@@ -66,7 +70,21 @@ export async function runList(options) {
|
|
|
66
70
|
layout = undefined;
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
|
-
|
|
73
|
+
// Components added from a URL or file (recorded by `add`) aren't in any index; list them in their own group.
|
|
74
|
+
const fromSources = [];
|
|
75
|
+
if (!options.namespace && root && config?.sources) {
|
|
76
|
+
for (const source of Object.values(config.sources)) {
|
|
77
|
+
try {
|
|
78
|
+
const item = await registry.item(fromSource(root, source));
|
|
79
|
+
fromSources.push({ ...item, categories: [SOURCES_GROUP], description: `${source}${item.description ? ` — ${item.description}` : ''}` });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// An unreachable source is reported by `doctor`; browsing shouldn't fail on it.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const items = options.namespace ? index.items.map(i => ({ ...i, name: `${options.namespace}/${i.name}` })) : [...index.items, ...fromSources];
|
|
87
|
+
const groups = groupRows(items, {
|
|
70
88
|
query: options.query,
|
|
71
89
|
category: options.category,
|
|
72
90
|
installedOnly: options.installedOnly,
|
package/dist/config.js
CHANGED
|
@@ -3,9 +3,17 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { projectEnv } from './env.js';
|
|
5
5
|
import { UipkgeError } from './errors.js';
|
|
6
|
-
import { createRegistry, resolveRegistryUrl } from './registry.js';
|
|
6
|
+
import { createRegistry, refKind, resolveRegistryUrl } from './registry.js';
|
|
7
7
|
export const CONFIG_FILE = 'components.json';
|
|
8
8
|
export const DEFAULT_ALIASES = { ui: '@/ui', lib: '@/lib', blocks: '@/app/components/blocks' };
|
|
9
|
+
/** How `sources` stores a ref: URLs as they are, local files relative to the app so the entry stays portable. */
|
|
10
|
+
export function toSource(root, cwd, ref) {
|
|
11
|
+
return refKind(ref) === 'file' ? path.relative(root, path.resolve(cwd, ref)).replace(/\\/g, '/') : ref;
|
|
12
|
+
}
|
|
13
|
+
/** A `sources` entry as a ref the registry client can load from anywhere. */
|
|
14
|
+
export function fromSource(root, source) {
|
|
15
|
+
return refKind(source) === 'url' ? source : path.resolve(root, source);
|
|
16
|
+
}
|
|
9
17
|
export function defaultConfig(registryUrl) {
|
|
10
18
|
return {
|
|
11
19
|
framework: 'angular',
|
package/dist/files.js
CHANGED
|
@@ -26,15 +26,24 @@ export function conflictingFiles(root, item) {
|
|
|
26
26
|
return normalizeEol(readFileSync(abs, 'utf8')) === normalizeEol(f.content) ? [] : [path.relative(root, abs)];
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Throws if the item can't be written as-is. `add` checks every item before
|
|
31
|
+
* writing any, so a bad item never leaves a project half-installed. An item
|
|
32
|
+
* without files (a bundle such as `charts`) is fine: it only brings its
|
|
33
|
+
* dependencies.
|
|
34
|
+
*/
|
|
35
|
+
export function assertWritable(root, item) {
|
|
36
|
+
for (const file of targetedFiles(item)) {
|
|
35
37
|
if (typeof file.content !== 'string') {
|
|
36
|
-
throw new UipkgeError(`Registry item \`${item.name}\` is missing the content of ${file.target}
|
|
38
|
+
throw new UipkgeError(`Registry item \`${item.name}\` is missing the content of ${file.target}.`, 'Nothing was installed.');
|
|
37
39
|
}
|
|
40
|
+
resolveTarget(root, file.target);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function writeItemFiles(root, item, overwrite) {
|
|
44
|
+
const result = { written: [], kept: [] };
|
|
45
|
+
assertWritable(root, item);
|
|
46
|
+
for (const file of targetedFiles(item)) {
|
|
38
47
|
const abs = resolveTarget(root, file.target);
|
|
39
48
|
const rel = path.relative(root, abs);
|
|
40
49
|
if (existsSync(abs) && !overwrite) {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { normalizeOptionalValues } from './args.js';
|
|
|
6
6
|
import { runAdd } from './commands/add.js';
|
|
7
7
|
import { runBuild } from './commands/build.js';
|
|
8
8
|
import { runDiff } from './commands/diff.js';
|
|
9
|
+
import { runDoctor } from './commands/doctor.js';
|
|
9
10
|
import { runInfo } from './commands/info.js';
|
|
10
11
|
import { runInit } from './commands/init.js';
|
|
11
12
|
import { runList } from './commands/list.js';
|
|
@@ -21,6 +22,7 @@ const COMMAND_HELP = [
|
|
|
21
22
|
['view <items...>', "show a component's details and files"],
|
|
22
23
|
['diff [item]', 'compare installed components with the registry'],
|
|
23
24
|
['info', 'show project, config and registry details'],
|
|
25
|
+
['doctor', 'check the setup and explain how to fix problems'],
|
|
24
26
|
['build [file]', 'build your own registry (registry.json → public/r)'],
|
|
25
27
|
['mcp', 'run an MCP server so AI assistants can browse components'],
|
|
26
28
|
];
|
|
@@ -47,7 +49,7 @@ Options
|
|
|
47
49
|
-s, --silent only print errors
|
|
48
50
|
--category <c> only one category (list)
|
|
49
51
|
-i, --installed only installed components (list)
|
|
50
|
-
--json machine-readable output (list, view, info)
|
|
52
|
+
--json machine-readable output (list, view, info, doctor)
|
|
51
53
|
--output <dir> where build writes the registry (default public/r)
|
|
52
54
|
--debug show stack traces on errors
|
|
53
55
|
-h, --help show this help
|
|
@@ -55,7 +57,7 @@ Options
|
|
|
55
57
|
|
|
56
58
|
Registry: https://uipkge.dev/r/angular (override with components.json "registryUrl" or UIPKGE_REGISTRY_URL)
|
|
57
59
|
`;
|
|
58
|
-
const COMMANDS = new Set(['init', 'add', 'list', 'ls', 'view', 'diff', 'info', 'build', 'mcp']);
|
|
60
|
+
const COMMANDS = new Set(['init', 'add', 'list', 'ls', 'view', 'diff', 'info', 'doctor', 'build', 'mcp']);
|
|
59
61
|
const OPTION_COMMANDS = {
|
|
60
62
|
overwrite: ['init', 'add'],
|
|
61
63
|
all: ['add'],
|
|
@@ -65,7 +67,7 @@ const OPTION_COMMANDS = {
|
|
|
65
67
|
diff: ['add'],
|
|
66
68
|
category: ['list', 'ls'],
|
|
67
69
|
installed: ['list', 'ls'],
|
|
68
|
-
json: ['list', 'ls', 'view', 'info'],
|
|
70
|
+
json: ['list', 'ls', 'view', 'info', 'doctor'],
|
|
69
71
|
output: ['build'],
|
|
70
72
|
};
|
|
71
73
|
async function main(argv) {
|
|
@@ -135,6 +137,8 @@ async function main(argv) {
|
|
|
135
137
|
return runDiff({ cwd, name: rest[0] });
|
|
136
138
|
case 'info':
|
|
137
139
|
return runInfo({ cwd, json: values.json, version: VERSION });
|
|
140
|
+
case 'doctor':
|
|
141
|
+
return runDoctor({ cwd, json: values.json });
|
|
138
142
|
case 'build':
|
|
139
143
|
return runBuild({ cwd, registry: rest[0] ?? 'registry.json', output: values.output ?? 'public/r' });
|
|
140
144
|
case 'mcp':
|
package/dist/project.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { parse } from 'jsonc-parser';
|
|
@@ -52,23 +52,53 @@ const isInside = (child, parent) => {
|
|
|
52
52
|
const rel = path.relative(parent, child);
|
|
53
53
|
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
54
54
|
};
|
|
55
|
-
|
|
55
|
+
function toApp(packageRoot, name, p, config, rootDir) {
|
|
56
|
+
const root = rootDir ?? path.resolve(packageRoot, p.root ?? '');
|
|
57
|
+
const styles = ((p.architect ?? p.targets)?.build?.options?.styles ?? [])
|
|
58
|
+
.map(s => (typeof s === 'string' ? s : s.input))
|
|
59
|
+
.filter((s) => Boolean(s))
|
|
60
|
+
.map(s => path.relative(root, path.resolve(packageRoot, s)).replace(/\\/g, '/'));
|
|
61
|
+
return { name, root, styles, config };
|
|
62
|
+
}
|
|
63
|
+
/** project.json files of an Nx workspace (a few levels deep, skipping build output and dependencies). */
|
|
64
|
+
function nxProjectFiles(dir, depth = 0, out = []) {
|
|
65
|
+
if (depth > 4)
|
|
66
|
+
return out;
|
|
67
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
if (entry.isFile() && entry.name === 'project.json')
|
|
69
|
+
out.push(path.join(dir, entry.name));
|
|
70
|
+
else if (entry.isDirectory() && !/^(node_modules|dist|tmp|coverage|\..*)$/.test(entry.name))
|
|
71
|
+
nxProjectFiles(path.join(dir, entry.name), depth + 1, out);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Angular application projects (libraries have no global styles to theme): from
|
|
77
|
+
* angular.json, or in an Nx workspace from each app's project.json, keeping only
|
|
78
|
+
* apps whose build target is Angular's.
|
|
79
|
+
*/
|
|
56
80
|
export function angularApps(packageRoot) {
|
|
57
81
|
const file = path.join(packageRoot, 'angular.json');
|
|
58
|
-
if (
|
|
82
|
+
if (existsSync(file)) {
|
|
83
|
+
const doc = (parse(readFileSync(file, 'utf8')) ?? {});
|
|
84
|
+
return Object.entries(doc.projects ?? {})
|
|
85
|
+
.filter(([, p]) => (p.projectType ?? 'application') === 'application')
|
|
86
|
+
.map(([name, p]) => toApp(packageRoot, name, p, { file, path: ['projects', name] }));
|
|
87
|
+
}
|
|
88
|
+
if (!existsSync(path.join(packageRoot, 'nx.json')))
|
|
59
89
|
return [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.map(s => path.relative(root, path.resolve(packageRoot, s)).replace(/\\/g, '/'));
|
|
69
|
-
return { name, root, styles };
|
|
90
|
+
return nxProjectFiles(packageRoot).flatMap(projectFile => {
|
|
91
|
+
const p = (parse(readFileSync(projectFile, 'utf8')) ?? {});
|
|
92
|
+
const build = p.targets?.build;
|
|
93
|
+
const angularBuild = /angular/.test(build?.executor ?? build?.builder ?? '');
|
|
94
|
+
if (p.projectType !== 'application' || !angularBuild)
|
|
95
|
+
return [];
|
|
96
|
+
const root = path.dirname(projectFile);
|
|
97
|
+
return [toApp(packageRoot, p.name ?? path.basename(root), p, { file: projectFile, path: [] }, root)];
|
|
70
98
|
});
|
|
71
99
|
}
|
|
100
|
+
/** An Nx workspace: its tsconfig paths live in the shared tsconfig.base.json. */
|
|
101
|
+
export const isNxWorkspace = (packageRoot) => existsSync(path.join(packageRoot, 'nx.json'));
|
|
72
102
|
/**
|
|
73
103
|
* Which Angular app `cwd` belongs to. An existing components.json between cwd
|
|
74
104
|
* and the package root wins; otherwise the angular.json project containing
|
|
@@ -112,16 +142,15 @@ export async function loadProject(cwd) {
|
|
|
112
142
|
if (!angularCore) {
|
|
113
143
|
throw new UipkgeError(`${root} is not an Angular project (no @angular/core in package.json).`);
|
|
114
144
|
}
|
|
115
|
-
if (deps['nx'] || existsSync(path.join(packageRoot, 'nx.json'))) {
|
|
116
|
-
throw new UipkgeError('Nx workspaces are not supported yet.', 'Run uipkge-ng inside a standalone Angular CLI app.');
|
|
117
|
-
}
|
|
118
145
|
return {
|
|
119
146
|
root,
|
|
120
147
|
packageRoot,
|
|
121
148
|
app: location.app,
|
|
122
149
|
packageJson,
|
|
123
150
|
angularCore,
|
|
124
|
-
|
|
151
|
+
// The Analog framework itself. @analogjs/vite-plugin-angular alone is common in Angular CLI and
|
|
152
|
+
// Nx workspaces (Vitest), and those still build with the Angular CLI, not Vite.
|
|
153
|
+
isAnalog: Boolean(deps['@analogjs/platform']),
|
|
125
154
|
hasTailwind: Boolean(deps['tailwindcss']),
|
|
126
155
|
packageManager: detectPackageManager(packageRoot, packageJson),
|
|
127
156
|
};
|
package/dist/resolve.js
CHANGED
|
@@ -28,17 +28,28 @@ transform = item => item) {
|
|
|
28
28
|
const skipped = [];
|
|
29
29
|
const dependencies = new Set();
|
|
30
30
|
const devDependencies = new Set();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// The default registry's foundation is init's job; a component depending on
|
|
32
|
+
// `utils` must not reinstall it. `@acme/utils` is someone else's item.
|
|
33
|
+
const isFoundation = (ref) => {
|
|
34
34
|
const kind = refKind(ref);
|
|
35
35
|
const ownRegistry = kind === 'name' || (kind === 'url' && ref.startsWith(`${registry.baseUrl}/`));
|
|
36
|
-
|
|
36
|
+
return ownRegistry && FOUNDATION_ITEMS.has(itemNameFromRef(ref));
|
|
37
|
+
};
|
|
38
|
+
// Start fetches early and in parallel; the registry caches them, so the ordered
|
|
39
|
+
// walk below just awaits ones already in flight. Errors surface there, not here.
|
|
40
|
+
const prefetch = (refs) => {
|
|
41
|
+
for (const ref of refs)
|
|
42
|
+
if (!isFoundation(ref))
|
|
43
|
+
registry.item(ref).catch(() => undefined);
|
|
44
|
+
};
|
|
45
|
+
async function visit(ref, direct) {
|
|
46
|
+
if (isFoundation(ref))
|
|
37
47
|
return undefined;
|
|
38
48
|
const item = transform(await registry.item(ref), direct);
|
|
39
49
|
if (visited.has(item.name))
|
|
40
50
|
return item.name;
|
|
41
51
|
visited.add(item.name);
|
|
52
|
+
prefetch(item.registryDependencies ?? []);
|
|
42
53
|
for (const dep of item.registryDependencies ?? [])
|
|
43
54
|
await visit(dep, false);
|
|
44
55
|
if (isInstalled(item) && !overwrite) {
|
|
@@ -51,6 +62,7 @@ transform = item => item) {
|
|
|
51
62
|
return item.name;
|
|
52
63
|
}
|
|
53
64
|
const requested = [];
|
|
65
|
+
prefetch(names);
|
|
54
66
|
for (const name of names) {
|
|
55
67
|
const resolved = await visit(name, true);
|
|
56
68
|
if (resolved && !requested.includes(resolved))
|
package/dist/setup.js
CHANGED
|
@@ -142,21 +142,23 @@ export async function writeIfChanged(file, next) {
|
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
144
|
* Puts `entry` (relative to the workspace root) first in the app's global
|
|
145
|
-
* `styles` for every target that has them (build, test),
|
|
145
|
+
* `styles` for every target that has them (build, test), in whichever file
|
|
146
|
+
* configures the app — angular.json or an Nx project.json — keeping its
|
|
146
147
|
* formatting. Returns whether anything changed.
|
|
147
148
|
*/
|
|
148
|
-
export async function addGlobalStyle(
|
|
149
|
-
|
|
150
|
-
let
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
149
|
+
export async function addGlobalStyle(config, entry) {
|
|
150
|
+
let text = await readFile(config.file, 'utf8');
|
|
151
|
+
let node = parse(text);
|
|
152
|
+
for (const key of config.path)
|
|
153
|
+
node = node?.[key];
|
|
154
|
+
const project = node;
|
|
155
|
+
const key = project?.architect ? 'architect' : 'targets';
|
|
154
156
|
let changed = false;
|
|
155
|
-
for (const [name, target] of Object.entries(
|
|
157
|
+
for (const [name, target] of Object.entries(project?.[key] ?? {})) {
|
|
156
158
|
const styles = target.options?.styles;
|
|
157
159
|
if (!Array.isArray(styles) || styles.some(s => (typeof s === 'string' ? s : s.input) === entry))
|
|
158
160
|
continue;
|
|
159
|
-
const edits = modify(text, [
|
|
161
|
+
const edits = modify(text, [...config.path, key, name, 'options', 'styles', 0], entry, {
|
|
160
162
|
isArrayInsertion: true,
|
|
161
163
|
formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' },
|
|
162
164
|
});
|
|
@@ -164,6 +166,35 @@ export async function addGlobalStyle(packageRoot, project, entry) {
|
|
|
164
166
|
changed = true;
|
|
165
167
|
}
|
|
166
168
|
if (changed)
|
|
167
|
-
await writeFile(file, text, 'utf8');
|
|
169
|
+
await writeFile(config.file, text, 'utf8');
|
|
168
170
|
return changed;
|
|
169
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Universal rules outside any `@layer` that zero margin or padding (`* { padding: 0 }`). Tailwind v4 puts utilities in a cascade layer, and unlayered CSS
|
|
174
|
+
* always wins over layered CSS, so a reset like this strips the spacing from every component.
|
|
175
|
+
* Returns the offending selectors.
|
|
176
|
+
*/
|
|
177
|
+
export function unlayeredResets(css) {
|
|
178
|
+
let text = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
179
|
+
// Drop @layer blocks (brace-matched) — rules inside a layer are fine.
|
|
180
|
+
for (let i = text.search(/@layer\b[^;{]*\{/); i !== -1; i = text.search(/@layer\b[^;{]*\{/)) {
|
|
181
|
+
let depth = 0;
|
|
182
|
+
let j = text.indexOf('{', i);
|
|
183
|
+
for (; j < text.length; j++) {
|
|
184
|
+
if (text[j] === '{')
|
|
185
|
+
depth++;
|
|
186
|
+
else if (text[j] === '}' && --depth === 0)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
text = text.slice(0, i) + text.slice(j + 1);
|
|
190
|
+
}
|
|
191
|
+
const found = [];
|
|
192
|
+
for (const m of text.matchAll(/(^|[};])\s*([^{};@]+?)\s*\{([^{}]*)\}/g)) {
|
|
193
|
+
const selector = m[2].trim().replace(/\s+/g, ' ');
|
|
194
|
+
// Only the universal selector reaches component elements; `html, body { margin: 0 }` is harmless.
|
|
195
|
+
const universal = selector.split(',').some(part => /^\*(::?[a-z-]+)?$/.test(part.trim()));
|
|
196
|
+
if (universal && /(^|[;\s])(margin|padding)\s*:\s*0(px)?\s*(;|$)/.test(m[3]))
|
|
197
|
+
found.push(selector);
|
|
198
|
+
}
|
|
199
|
+
return found;
|
|
200
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uipkge-ng",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Add uipkge Angular components to your project: copy the source, own the code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uday Adaka (https://uipkge.dev)",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"uipkge-ng": "dist/index.js"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"CHANGELOG.md"
|
|
29
30
|
],
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=20.12"
|
|
@@ -34,7 +35,9 @@
|
|
|
34
35
|
"build": "tsc -p tsconfig.build.json",
|
|
35
36
|
"dev": "tsx src/index.ts",
|
|
36
37
|
"test": "vitest run",
|
|
37
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
40
|
+
"test:coverage": "vitest run --coverage"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@clack/prompts": "^1.8.1",
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/node": "^22.0.0",
|
|
49
|
+
"@vitest/coverage-v8": "3.2.7",
|
|
46
50
|
"tsx": "^4.20.0",
|
|
47
51
|
"typescript": "^5.9.3",
|
|
48
52
|
"vitest": "^3.2.7"
|