@webjsdev/ui 0.3.14 → 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`
@@ -127,6 +134,7 @@ always compares against the live upstream.
127
134
  | `webjsui diff [name]` | Show diff between your local copy and the live registry |
128
135
  | `webjsui info` | Print project diagnostics |
129
136
  | `webjsui build` | (For registry authors) Compile a custom registry |
137
+ | `webjsui lint` | Opt-in design-system linter. Reports raw palette colors, arbitrary values and classes composed over a kit helper, at the line, with a message built from your own theme tokens and helper variants. Configured by a `lint` block in `components.json`; with no block it reports nothing and exits 0. `--json` for an agent loop, `--max-warnings <n>` to pin a count. |
130
138
 
131
139
  ## Tag convention
132
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/ui",
3
- "version": "0.3.14",
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,93 @@
1
+ import { Command } from 'commander';
2
+ import { getConfig } from '../utils/get-config.js';
3
+ import { lintApp } from '../lint/index.js';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ /**
7
+ * Run the linter and shape its report, with no printing and no exit, so the
8
+ * command test calls this directly. `code` is 0 or 1 only, matching `webjs
9
+ * check`'s posture that an agent gates on non-zero: 1 when any error-level
10
+ * violation exists, when the warning count exceeds `maxWarnings` (-1 is no
11
+ * cap), or when the command cannot run at all (no `components.json`, or a
12
+ * config that fails the schema).
13
+ *
14
+ * @param {{ cwd?: string, json?: boolean, maxWarnings?: number|string }} [opts]
15
+ * @returns {{ lines: string[], code: number, report: null | { violations: any[], summary: any, warnings: string[] } }}
16
+ */
17
+ export function runLint(opts = {}) {
18
+ const cwd = opts.cwd ?? process.cwd();
19
+ const maxWarnings = Number(opts.maxWarnings ?? -1);
20
+ /** @type {string[]} */
21
+ const lines = [];
22
+ // A run that cannot start still answers in the requested shape, so an agent
23
+ // loop parsing stdout under --json receives an error document, never text.
24
+ const refuse = (error) => {
25
+ if (opts.json) {
26
+ lines.push(JSON.stringify({ error, violations: [], summary: { count: 0, errors: 0, warnings: 0, byRule: {} }, warnings: [], configured: false }, null, 2));
27
+ } else {
28
+ lines.push(`webjsui lint: ${error}`);
29
+ }
30
+ return { lines, code: 1, report: null };
31
+ };
32
+ if (!Number.isInteger(maxWarnings) || maxWarnings < -1) {
33
+ return refuse(`--max-warnings expects an integer (-1 for no cap), got ${JSON.stringify(String(opts.maxWarnings))}`);
34
+ }
35
+ let config;
36
+ try {
37
+ config = getConfig(cwd);
38
+ } catch (e) {
39
+ return refuse(`components.json is invalid: ${firstIssue(e)}`);
40
+ }
41
+ if (!config) return refuse('components.json not found (run `npx @webjsdev/ui init`)');
42
+
43
+ const { violations, warnings, configured } = lintApp(cwd, config);
44
+ const errors = violations.filter((v) => v.severity === 'error').length;
45
+ const warns = violations.length - errors;
46
+ /** @type {Record<string, number>} */
47
+ const byRule = {};
48
+ for (const v of violations) byRule[v.rule] = (byRule[v.rule] ?? 0) + 1;
49
+ const report = {
50
+ violations,
51
+ summary: { count: violations.length, errors, warnings: warns, byRule },
52
+ warnings,
53
+ configured,
54
+ };
55
+
56
+ if (opts.json) {
57
+ lines.push(JSON.stringify(report, null, 2));
58
+ } else if (!configured) {
59
+ lines.push('webjsui lint: no rules configured (add a "lint" block to components.json)');
60
+ } else {
61
+ for (const w of warnings) lines.push(`⚠ ${w}`);
62
+ if (violations.length === 0) {
63
+ lines.push('webjsui lint: all checks pass ✓');
64
+ } else {
65
+ lines.push(`webjsui lint: ${violations.length} problem(s) found (${errors} error${errors === 1 ? '' : 's'}, ${warns} warning${warns === 1 ? '' : 's'})`);
66
+ for (const v of violations) {
67
+ lines.push('');
68
+ lines.push(` ${v.severity === 'error' ? '✗' : '⚠'} [${v.rule}] ${v.file}:${v.line}:${v.column}`);
69
+ lines.push(` ${v.message}`);
70
+ }
71
+ }
72
+ }
73
+ const code = errors > 0 || (maxWarnings >= 0 && warns > maxWarnings) ? 1 : 0;
74
+ return { lines, code, report };
75
+ }
76
+
77
+ function firstIssue(e) {
78
+ const issue = e?.issues?.[0];
79
+ if (issue) return `${issue.path?.join('.') || '(root)'}: ${issue.message}`;
80
+ return String(e?.message ?? e);
81
+ }
82
+
83
+ export const lint = new Command()
84
+ .name('lint')
85
+ .description('Check the app against its design system (opt-in, configured in components.json)')
86
+ .option('-c, --cwd <cwd>', 'the working directory', process.cwd())
87
+ .option('--json', 'emit structured violations for an agent loop')
88
+ .option('--max-warnings <n>', 'fail when warnings exceed this count', '-1')
89
+ .action((opts) => {
90
+ const { lines, code } = runLint(opts);
91
+ for (const l of lines) logger.info(l);
92
+ if (code !== 0) process.exitCode = code;
93
+ });
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import { view } from './commands/view.js';
9
9
  import { diff } from './commands/diff.js';
10
10
  import { info } from './commands/info.js';
11
11
  import { build } from './commands/build.js';
12
+ import { lint } from './commands/lint.js';
12
13
 
13
14
  const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
14
15
 
@@ -27,6 +28,7 @@ program
27
28
  .addCommand(view)
28
29
  .addCommand(diff)
29
30
  .addCommand(info)
30
- .addCommand(build);
31
+ .addCommand(build)
32
+ .addCommand(lint);
31
33
 
32
34
  program.parse();