@webjsdev/ui 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,6 +111,13 @@ it on demand with `npx @webjsdev/ui view <name>`, which is what the pointer it
111
111
  leaves behind says too). It also self-heals the theme tokens if
112
112
  they are missing.
113
113
 
114
+ The install runs with your project's package manager. `add` looks for a
115
+ lockfile in the current directory and then each parent, stopping at the
116
+ workspace root, so an app inside a monorepo uses the root's lockfile:
117
+ `pnpm-lock.yaml` means pnpm, `yarn.lock` yarn, `bun.lock` or `bun.lockb` bun,
118
+ `package-lock.json` npm. With no lockfile it reads `npm_config_user_agent`
119
+ (set by `bunx`, `pnpm dlx` and friends), and falls back to npm.
120
+
114
121
  Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
115
122
  that ships inside the installed `@webjsdev/ui` package, so they work with no
116
123
  network. Point at a custom registry with `--registry <url>`; `webjsui diff`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/ui",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "type": "module",
5
5
  "description": "The AI-first component library for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Class-helper functions for visuals, custom elements only where state matters, source-copied into your repo so you own it.",
6
6
  "bin": {
@@ -10,6 +10,7 @@ import { DEFAULT_REGISTRY_URL } from '../registry/fetcher.js';
10
10
  import { isCustomElementSource } from '../registry/local.js';
11
11
  import { stripExample } from '../registry/example.js';
12
12
  import { ensureTheme } from '../utils/theme.js';
13
+ import { detectPackageManager } from '../utils/package-manager.js';
13
14
 
14
15
  export const add = new Command()
15
16
  .name('add')
@@ -247,7 +248,7 @@ function ensureDir(d) {
247
248
  }
248
249
 
249
250
  async function installDeps(cwd, deps, dev) {
250
- const manager = detectPackageManager(cwd);
251
+ const manager = detectAddCommand(cwd);
251
252
  const flag = dev ? '-D' : '';
252
253
  const cmd = `${manager.exec} ${manager.add} ${flag} ${deps.join(' ')}`.replace(/\s+/g, ' ').trim();
253
254
  logger.info(`${logger.dim('$')} ${cmd}`);
@@ -258,9 +259,21 @@ async function installDeps(cwd, deps, dev) {
258
259
  }
259
260
  }
260
261
 
261
- function detectPackageManager(cwd) {
262
- if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return { exec: 'pnpm', add: 'add' };
263
- if (existsSync(join(cwd, 'yarn.lock'))) return { exec: 'yarn', add: 'add' };
264
- if (existsSync(join(cwd, 'bun.lockb'))) return { exec: 'bun', add: 'add' };
265
- return { exec: 'npm', add: 'install' };
262
+ /** Install command per manager (`-D` marks a dev dependency for all four). */
263
+ const ADD_COMMANDS = {
264
+ npm: { exec: 'npm', add: 'install' },
265
+ pnpm: { exec: 'pnpm', add: 'add' },
266
+ yarn: { exec: 'yarn', add: 'add' },
267
+ bun: { exec: 'bun', add: 'add' },
268
+ };
269
+
270
+ /**
271
+ * Resolve the install command for the project at `cwd` (#1494): the lockfile
272
+ * walk-up (so `bun.lock` and a workspace-root lockfile both count), then the
273
+ * invoking tool's user agent, then npm.
274
+ * @param {string} cwd
275
+ * @param {Record<string, string | undefined>} [env]
276
+ */
277
+ export function detectAddCommand(cwd, env = process.env) {
278
+ return ADD_COMMANDS[detectPackageManager({ cwd, env })];
266
279
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Package-manager detection for `webjs ui add` (#1494).
3
+ *
4
+ * KEEP IN SYNC with `packages/cli/lib/package-manager.js`. The two published
5
+ * packages carry the same small module rather than one importing the other,
6
+ * so a `@webjsdev/cli` release can never fail at import time against an older
7
+ * `@webjsdev/ui` that lacks the export. `packages/cli/test/package-manager.test.mjs`
8
+ * asserts both copies agree on every fixture.
9
+ *
10
+ * Detection order (with `prefer: 'lockfile'`, the default):
11
+ * 1. A lockfile in `cwd` or any ancestor. The walk stops at the first
12
+ * workspace root (a package.json declaring `workspaces`, or a
13
+ * `pnpm-workspace.yaml`) or at the filesystem root, so an app nested in a
14
+ * workspace finds the root's lockfile. Within one directory the order is
15
+ * pnpm, yarn, bun (`bun.lock`, the text lockfile Bun writes since 1.2,
16
+ * and the older binary `bun.lockb`), then npm.
17
+ * 2. `npm_config_user_agent`, which npm, pnpm, yarn and bun set when they
18
+ * run a script or a `dlx` / `bunx` / `npx` binary.
19
+ * 3. `npm`.
20
+ * With `prefer: 'agent'` steps 1 and 2 swap, which suits `webjs create`: the
21
+ * tool that invoked it is the strongest signal for a directory that has no
22
+ * lockfile yet.
23
+ */
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import { dirname, join, resolve } from 'node:path';
26
+
27
+ /** @typedef {'npm'|'pnpm'|'yarn'|'bun'} PackageManager */
28
+
29
+ /** Lockfile name to manager, in per-directory precedence order. */
30
+ const LOCKFILES = /** @type {const} */ ([
31
+ ['pnpm-lock.yaml', 'pnpm'],
32
+ ['yarn.lock', 'yarn'],
33
+ ['bun.lock', 'bun'],
34
+ ['bun.lockb', 'bun'],
35
+ ['package-lock.json', 'npm'],
36
+ ['npm-shrinkwrap.json', 'npm'],
37
+ ]);
38
+
39
+ /**
40
+ * Whether `dir` is a workspace root, where the lockfile walk stops.
41
+ * @param {string} dir
42
+ * @returns {boolean}
43
+ */
44
+ function isWorkspaceRoot(dir) {
45
+ if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return true;
46
+ const pkgPath = join(dir, 'package.json');
47
+ if (!existsSync(pkgPath)) return false;
48
+ try {
49
+ return Boolean(JSON.parse(readFileSync(pkgPath, 'utf8')).workspaces);
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Walk up from `cwd` looking for a lockfile.
57
+ * @param {string} cwd
58
+ * @returns {PackageManager | null}
59
+ */
60
+ export function managerFromLockfile(cwd) {
61
+ let dir = resolve(cwd);
62
+ for (;;) {
63
+ for (const [file, manager] of LOCKFILES) {
64
+ if (existsSync(join(dir, file))) return manager;
65
+ }
66
+ if (isWorkspaceRoot(dir)) return null;
67
+ const parent = dirname(dir);
68
+ if (parent === dir) return null;
69
+ dir = parent;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Read the manager from an `npm_config_user_agent` value such as
75
+ * `bun/1.3.14 npm/? node/v24.0.0 linux x64`.
76
+ * @param {string | undefined} ua
77
+ * @returns {PackageManager | null}
78
+ */
79
+ export function managerFromUserAgent(ua) {
80
+ const name = String(ua || '').split('/')[0];
81
+ return name === 'pnpm' || name === 'yarn' || name === 'bun' || name === 'npm' ? name : null;
82
+ }
83
+
84
+ /**
85
+ * @param {{ cwd?: string | null, env?: Record<string, string | undefined>, prefer?: 'lockfile' | 'agent' }} [opts]
86
+ * @returns {PackageManager}
87
+ */
88
+ export function detectPackageManager({ cwd = null, env = process.env, prefer = 'lockfile' } = {}) {
89
+ const fromLock = () => (cwd ? managerFromLockfile(cwd) : null);
90
+ const fromAgent = () => managerFromUserAgent(env.npm_config_user_agent);
91
+ return (prefer === 'agent' ? fromAgent() ?? fromLock() : fromLock() ?? fromAgent()) ?? 'npm';
92
+ }