@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 +8 -0
- package/package.json +1 -1
- package/src/commands/add.js +19 -6
- package/src/commands/lint.js +93 -0
- package/src/index.js +3 -1
- package/src/lint/grammar.js +839 -0
- package/src/lint/index.js +205 -0
- package/src/lint/rules/index.js +12 -0
- package/src/lint/rules/no-arbitrary-values.js +33 -0
- package/src/lint/rules/no-raw-colors.js +86 -0
- package/src/lint/rules/no-restyle.js +66 -0
- package/src/lint/scan.js +446 -0
- package/src/lint/theme-tokens.js +75 -0
- package/src/registry/extract.js +166 -0
- package/src/registry/schema.js +38 -0
- package/src/utils/package-manager.js +92 -0
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.
|
|
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": {
|
package/src/commands/add.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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();
|