uipkge-ng 0.1.1 → 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 +12 -0
- package/README.md +12 -5
- package/dist/commands/add.js +19 -1
- package/dist/commands/diff.js +27 -6
- package/dist/commands/doctor.js +35 -7
- 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/project.js +46 -17
- package/dist/setup.js +41 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
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
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
|
+
|
|
5
17
|
## 0.1.1 — 2026-09-26
|
|
6
18
|
|
|
7
19
|
### Added
|
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
|
|
@@ -191,7 +191,7 @@ Components
|
|
|
191
191
|
→ Run `npm install class-variance-authority`.
|
|
192
192
|
```
|
|
193
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, 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.
|
|
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
195
|
|
|
196
196
|
## Existing projects
|
|
197
197
|
|
|
@@ -199,6 +199,7 @@ uipkge-ng is built to be run on apps that already have code in them:
|
|
|
199
199
|
|
|
200
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`.
|
|
201
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.
|
|
202
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.
|
|
203
204
|
- **Your `utils.ts` stays yours.** If `src/lib/utils.ts` exists, `init` keeps it and warns if it doesn't export `cn()`.
|
|
204
205
|
- **Try first.** `add --dry-run` shows exactly what would change, `add --diff` shows how.
|
|
@@ -216,7 +217,7 @@ Each app gets its own `components.json`, tokens and paths — written to its `ts
|
|
|
216
217
|
|
|
217
218
|
**npm, pnpm, yarn and bun workspaces**: run it in the app (`apps/web`). The package manager comes from the workspace's lockfile.
|
|
218
219
|
|
|
219
|
-
**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`.
|
|
220
221
|
|
|
221
222
|
## components.json
|
|
222
223
|
|
|
@@ -244,6 +245,7 @@ Each app gets its own `components.json`, tokens and paths — written to its `ts
|
|
|
244
245
|
| `tokens` | the file uipkge owns for design tokens and item CSS |
|
|
245
246
|
| `aliases` | where components, helpers and blocks go — see [Custom folders](#custom-folders) |
|
|
246
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 |
|
|
247
249
|
|
|
248
250
|
## Custom folders
|
|
249
251
|
|
|
@@ -300,7 +302,7 @@ uipkge-ng view @internal/data-table
|
|
|
300
302
|
- `{name}` becomes the item name; `list` reads the index at `{name}` = `registry`.
|
|
301
303
|
- `${VAR}` in headers comes from the environment, then `.env.local`, then `.env`. A missing variable is an error before any request is made.
|
|
302
304
|
- Headers are only ever sent to that registry's own URLs.
|
|
303
|
-
- `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.
|
|
304
306
|
|
|
305
307
|
## Publishing your own registry
|
|
306
308
|
|
|
@@ -374,6 +376,12 @@ It never writes files — installing stays a command you run. It reads the `comp
|
|
|
374
376
|
| `--debug` | stack traces on errors |
|
|
375
377
|
| `-h, --help` / `-v, --version` | |
|
|
376
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
|
+
|
|
377
385
|
## Troubleshooting
|
|
378
386
|
|
|
379
387
|
| Message | Fix |
|
|
@@ -383,7 +391,6 @@ It never writes files — installing stays a command you run. It reads the `comp
|
|
|
383
391
|
| `N files already exist with different content` | keep yours and use `--path`, or replace with `--overwrite` |
|
|
384
392
|
| `The "ui" alias "…" is not mapped in tsconfig.json "paths"` | add the mapping, or reset `aliases` to the defaults |
|
|
385
393
|
| `Registry "@acme" needs the environment variable ACME_TOKEN` | set it in your shell, `.env.local` or `.env` |
|
|
386
|
-
| `Nx workspaces are not supported yet` | use a standalone Angular CLI app for now |
|
|
387
394
|
|
|
388
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).
|
|
389
396
|
|
package/dist/commands/add.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { assertWritable, conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
|
|
@@ -9,6 +9,7 @@ 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,7 +67,23 @@ 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
|
}
|
|
@@ -121,6 +138,7 @@ export async function runAdd(options) {
|
|
|
121
138
|
const summary = targetedFiles(item).length ? plural(result.written.length, 'file') : 'bundle';
|
|
122
139
|
log.success(`${item.name} ${color.dim(`(${[summary, ...extras].join(', ')})`)}`);
|
|
123
140
|
}
|
|
141
|
+
await recordSources();
|
|
124
142
|
log.blank();
|
|
125
143
|
log.info(`${color.green('✔')} Added ${plural(plan.install.length, 'component')}.`);
|
|
126
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
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { readConfig, registryFor } from '../config.js';
|
|
3
|
+
import { fromSource, readConfig, registryFor } from '../config.js';
|
|
4
4
|
import { targetedFiles } from '../files.js';
|
|
5
5
|
import { adaptItem, resolveLayout } from '../layout.js';
|
|
6
6
|
import { color } from '../output.js';
|
|
@@ -8,7 +8,7 @@ import { installArgs, missingPackages } from '../packages.js';
|
|
|
8
8
|
import { angularApps, declaredDependencies, detectPackageManager, findApp, readPackageJson, resolveTarget } from '../project.js';
|
|
9
9
|
import { FOUNDATION_ITEMS, namedRegistryFrom } from '../registry.js';
|
|
10
10
|
import { projectEnv } from '../env.js';
|
|
11
|
-
import { TAILWIND_POSTCSS, readPostcssConfig, relativeImport } from '../setup.js';
|
|
11
|
+
import { TAILWIND_POSTCSS, readPostcssConfig, relativeImport, unlayeredResets } from '../setup.js';
|
|
12
12
|
const MIN_NODE = [20, 12];
|
|
13
13
|
/** `3: a, b, c` or `202: a, b, c, d, e, f … and 196 more`. */
|
|
14
14
|
const summarize = (names, shown = 6) => `${names.length}: ${names.slice(0, shown).join(', ')}${names.length > shown ? ` … and ${names.length - shown} more` : ''}`;
|
|
@@ -66,9 +66,8 @@ export async function runChecks(cwd) {
|
|
|
66
66
|
});
|
|
67
67
|
if (!angular)
|
|
68
68
|
return checks;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
push({ section: 'Project', title: 'Nx', status: 'fail', detail: 'Nx workspaces are not supported yet' });
|
|
69
|
+
if (existsSync(path.join(packageRoot, 'nx.json')))
|
|
70
|
+
push({ section: 'Project', title: 'Workspace', status: 'pass', detail: 'Nx' });
|
|
72
71
|
const pm = detectPackageManager(packageRoot, pkg);
|
|
73
72
|
push({ section: 'Project', title: 'Package manager', status: 'pass', detail: pm });
|
|
74
73
|
// ── Configuration ──
|
|
@@ -135,7 +134,7 @@ export async function runChecks(cwd) {
|
|
|
135
134
|
detail: tailwind ?? 'not installed',
|
|
136
135
|
fix: tailwindMajor >= 4 ? undefined : tailwind ? 'uipkge components need Tailwind CSS v4.' : 'Run `uipkge-ng init`.',
|
|
137
136
|
});
|
|
138
|
-
const analog = Boolean(deps['@analogjs/platform']
|
|
137
|
+
const analog = Boolean(deps['@analogjs/platform']);
|
|
139
138
|
if (analog) {
|
|
140
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." });
|
|
141
140
|
}
|
|
@@ -169,6 +168,19 @@ export async function runChecks(cwd) {
|
|
|
169
168
|
fix: imported || registered ? undefined : `Add \`@import '${importPath}';\` to ${config.styles}, or list ${config.tokens} under "styles" in angular.json.`,
|
|
170
169
|
});
|
|
171
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
|
+
}
|
|
172
184
|
if (layout) {
|
|
173
185
|
const utils = path.join(root, layout.dirs.lib, 'utils.ts');
|
|
174
186
|
const hasCn = existsSync(utils) && /export\s+(?:function|const|let)\s+cn\b|export\s*\{[^}]*\bcn\b[^}]*\}/.test(readFileSync(utils, 'utf8'));
|
|
@@ -185,9 +197,25 @@ export async function runChecks(cwd) {
|
|
|
185
197
|
push({ section: 'Components', title: 'Installed components', status: 'skip', detail: 'needs the registry and folders above' });
|
|
186
198
|
return checks;
|
|
187
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
|
+
}
|
|
188
216
|
const installed = [];
|
|
189
217
|
const partial = [];
|
|
190
|
-
for (const item of
|
|
218
|
+
for (const item of candidates) {
|
|
191
219
|
const files = targetedFiles(adaptItem(item, layout));
|
|
192
220
|
if (!files.length)
|
|
193
221
|
continue;
|
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/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/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
|
+
}
|