@webjsdev/cli 0.8.1
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 +71 -0
- package/bin/webjs.js +279 -0
- package/lib/create.js +898 -0
- package/lib/saas-template.js +397 -0
- package/package.json +39 -0
- package/templates/.claude/hooks/block-prose-punctuation.sh +236 -0
- package/templates/.claude/hooks/guard-branch-context.sh +39 -0
- package/templates/.claude/hooks/nudge-uncommitted.sh +46 -0
- package/templates/.claude/settings.json +35 -0
- package/templates/.claude.json +9 -0
- package/templates/.cursor/hooks/nudge-uncommitted.sh +38 -0
- package/templates/.cursor/hooks.json +8 -0
- package/templates/.cursorrules +99 -0
- package/templates/.editorconfig +18 -0
- package/templates/.env.example +27 -0
- package/templates/.gemini/hooks/nudge-uncommitted.sh +42 -0
- package/templates/.gemini/settings.json +15 -0
- package/templates/.github/copilot-instructions.md +85 -0
- package/templates/.github/pull_request_template.md +14 -0
- package/templates/.hooks/pre-commit +48 -0
- package/templates/.opencode/plugins/nudge-uncommitted.ts +62 -0
- package/templates/.windsurfrules +91 -0
- package/templates/AGENTS.md +816 -0
- package/templates/CLAUDE.md +2 -0
- package/templates/CONVENTIONS.md +901 -0
- package/templates/lib/utils/ui.ts +83 -0
- package/templates/public/tailwind-browser.js +947 -0
- package/templates/test/hello/browser/hello.test.js +40 -0
- package/templates/test/hello/e2e/hello.test.ts +87 -0
- package/templates/test/hello/hello.test.ts +24 -0
- package/templates/web-test-runner.config.js +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @webjsdev/cli
|
|
2
|
+
|
|
3
|
+
CLI for [webjs](https://github.com/vivek7405/webjs): scaffold, develop,
|
|
4
|
+
build, and run webjs apps.
|
|
5
|
+
|
|
6
|
+
Installing this package gives you the `webjs` command.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Install once, globally:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm i -g @webjsdev/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then scaffold a new app anywhere:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
webjs create my-app
|
|
20
|
+
cd my-app && npm install && npm run dev
|
|
21
|
+
# → http://localhost:3000
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
One-shot without global install:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx @webjsdev/cli create my-app
|
|
28
|
+
cd my-app && npm install && npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
webjs create <name> # scaffold a full-stack app (default)
|
|
35
|
+
webjs create <name> --template api # backend-only API app
|
|
36
|
+
webjs create <name> --template saas # auth + dashboard + Prisma User model
|
|
37
|
+
|
|
38
|
+
webjs dev # dev server with live reload
|
|
39
|
+
webjs start # production server (no build step, serves source directly)
|
|
40
|
+
webjs check # validate project conventions
|
|
41
|
+
webjs test # run server + browser tests
|
|
42
|
+
webjs db <prisma-subcommand> # prisma passthrough (saas template)
|
|
43
|
+
|
|
44
|
+
webjs ui init # initialise @webjsdev/ui in this project
|
|
45
|
+
webjs ui add <names...> # copy components from the registry (https://ui.webjs.dev/registry/<name>.json)
|
|
46
|
+
webjs ui list # list every component available in the registry
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`webjs ui` proxies to [`@webjsdev/ui`](https://www.npmjs.com/package/@webjsdev/ui),
|
|
50
|
+
an AI-first component library + CLI that copies sources into your project: class
|
|
51
|
+
helpers (`buttonClass`, `cardClass`, …) for the visual primitives and a small set
|
|
52
|
+
of stateful custom elements (`<ui-dialog>`, `<ui-tabs>`, `<ui-popover>`) where
|
|
53
|
+
state matters. The package is a hard dependency of `@webjsdev/cli`, so installing
|
|
54
|
+
the CLI gives you `webjs ui` automatically. See
|
|
55
|
+
[https://ui.webjs.dev](https://ui.webjs.dev) for the catalogue.
|
|
56
|
+
|
|
57
|
+
## Scaffolded templates
|
|
58
|
+
|
|
59
|
+
The scaffold seeds opinionated defaults so AI agents produce consistent code:
|
|
60
|
+
|
|
61
|
+
- `AGENTS.md` + `CONVENTIONS.md` (the machine-readable contract)
|
|
62
|
+
- `.claude/`, `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`
|
|
63
|
+
- `test/<feature>/` (with optional `browser/` / `e2e/` subfolders per kind) with example tests
|
|
64
|
+
- Tailwind CSS via CLI (no browser runtime at build time)
|
|
65
|
+
- TypeScript, `.editorconfig`, `.gitignore`
|
|
66
|
+
|
|
67
|
+
See the full framework docs at https://github.com/vivek7405/webjs.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/bin/webjs.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, join, dirname } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
// Exactly three scaffolds exist. Keep this list as the single source of
|
|
10
|
+
// truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
|
|
11
|
+
// .windsurfrules / .github/copilot-instructions.md mirror it.
|
|
12
|
+
const TEMPLATES = ['full-stack', 'api', 'saas'];
|
|
13
|
+
|
|
14
|
+
const USAGE = `webjs commands:
|
|
15
|
+
webjs dev [--port 3000] Start dev server with live reload
|
|
16
|
+
webjs start [--port 3000] Start production server (serves source directly, no build step)
|
|
17
|
+
webjs test [--server|--browser] Run server + browser tests
|
|
18
|
+
webjs check Validate app against conventions
|
|
19
|
+
webjs create <name> [--template full-stack|api|saas] Scaffold a new webjs app
|
|
20
|
+
(only 3 templates exist. default: full-stack with Prisma+SQLite)
|
|
21
|
+
webjs db generate Run \`prisma generate\`
|
|
22
|
+
webjs db migrate [name] Run \`prisma migrate dev\`
|
|
23
|
+
webjs db studio Run \`prisma studio\`
|
|
24
|
+
webjs ui <subcmd> AI-first component library CLI
|
|
25
|
+
(init / add / list / view / diff / info)
|
|
26
|
+
Requires @webjsdev/ui installed in the project
|
|
27
|
+
webjs help Show this help`;
|
|
28
|
+
|
|
29
|
+
/** @param {string[]} args */
|
|
30
|
+
function flag(args, name, def) {
|
|
31
|
+
const i = args.indexOf(name);
|
|
32
|
+
if (i === -1) return def;
|
|
33
|
+
return args[i + 1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
switch (cmd) {
|
|
38
|
+
case 'dev': {
|
|
39
|
+
// If we're already inside the --watch child, start the server directly.
|
|
40
|
+
if (process.env.__WEBJS_DEV_CHILD === '1') {
|
|
41
|
+
const { startServer } = await import('@webjsdev/server');
|
|
42
|
+
const port = Number(flag(rest, '--port', process.env.PORT || 3000));
|
|
43
|
+
await startServer({ appDir: process.cwd(), port, dev: true });
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Otherwise, spawn ourselves as a child with node --watch.
|
|
48
|
+
// This restarts the process on file changes, guaranteeing a fresh
|
|
49
|
+
// Node ESM module cache. Without this, edits to transitively-imported
|
|
50
|
+
// modules (actions, queries, components, utils) don't take effect
|
|
51
|
+
// because Node caches ESM by URL with no public invalidation API.
|
|
52
|
+
// Build watch paths from directories that exist in the project.
|
|
53
|
+
const { existsSync } = await import('node:fs');
|
|
54
|
+
const watchPaths = [];
|
|
55
|
+
for (const dir of ['app', 'components', 'modules', 'lib', 'actions']) {
|
|
56
|
+
if (existsSync(dir)) watchPaths.push('--watch-path', dir);
|
|
57
|
+
}
|
|
58
|
+
// Watch root middleware/config if present
|
|
59
|
+
for (const f of ['middleware.ts', 'middleware.js']) {
|
|
60
|
+
if (existsSync(f)) watchPaths.push('--watch-path', f);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const child = spawn(
|
|
64
|
+
process.execPath,
|
|
65
|
+
[
|
|
66
|
+
'--watch',
|
|
67
|
+
'--watch-preserve-output',
|
|
68
|
+
...watchPaths,
|
|
69
|
+
...process.argv.slice(1),
|
|
70
|
+
],
|
|
71
|
+
{
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'start': {
|
|
81
|
+
const { startServer } = await import('@webjsdev/server');
|
|
82
|
+
const port = Number(flag(rest, '--port', process.env.PORT || 3000));
|
|
83
|
+
await startServer({ appDir: process.cwd(), port, dev: false });
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'db': {
|
|
87
|
+
const sub = rest[0];
|
|
88
|
+
const args = rest.slice(1);
|
|
89
|
+
const map = { generate: ['generate'], migrate: ['migrate', 'dev', ...args], studio: ['studio'] };
|
|
90
|
+
const prismaArgs = map[sub];
|
|
91
|
+
if (!prismaArgs) { console.error('Unknown db subcommand.\n' + USAGE); process.exit(1); }
|
|
92
|
+
const child = spawn('npx', ['prisma', ...prismaArgs], { stdio: 'inherit', cwd: process.cwd() });
|
|
93
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'ui': {
|
|
97
|
+
// Delegate to @webjsdev/ui. Bundled as a hard dependency of
|
|
98
|
+
// @webjsdev/cli, so `npm install -g @webjsdev/cli` pulls it in
|
|
99
|
+
// automatically, and `webjs ui add button` works out of the box
|
|
100
|
+
// without an extra install in user projects.
|
|
101
|
+
const { createRequire } = await import('node:module');
|
|
102
|
+
const req = createRequire(import.meta.url);
|
|
103
|
+
let entry;
|
|
104
|
+
try {
|
|
105
|
+
entry = req.resolve('@webjsdev/ui/bin/webjsui.js');
|
|
106
|
+
} catch {
|
|
107
|
+
// Fallback: try resolving from the user's cwd in case of weird
|
|
108
|
+
// workspace setups.
|
|
109
|
+
try {
|
|
110
|
+
const userReq = createRequire(join(process.cwd(), 'package.json'));
|
|
111
|
+
entry = userReq.resolve('@webjsdev/ui/bin/webjsui.js');
|
|
112
|
+
} catch {
|
|
113
|
+
console.error('@webjsdev/ui could not be resolved.');
|
|
114
|
+
console.error('Reinstall the CLI: npm install -g @webjsdev/cli');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const child = spawn('node', [entry, ...rest], { stdio: 'inherit', cwd: process.cwd() });
|
|
119
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'test': {
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
const { existsSync } = await import('node:fs');
|
|
125
|
+
|
|
126
|
+
// Two test runners:
|
|
127
|
+
// 1. node:test for server-side tests (test/server/*.test.ts, test/unit/*.test.ts)
|
|
128
|
+
// 2. WTR + Playwright for browser tests (test/browser/*.test.js)
|
|
129
|
+
//
|
|
130
|
+
// `webjs test` → runs both
|
|
131
|
+
// `webjs test --server` → server tests only (node:test)
|
|
132
|
+
// `webjs test --browser` → browser tests only (WTR + Playwright)
|
|
133
|
+
|
|
134
|
+
const serverOnly = rest.includes('--server');
|
|
135
|
+
const browserOnly = rest.includes('--browser');
|
|
136
|
+
const runServer = !browserOnly;
|
|
137
|
+
const runBrowser = !serverOnly;
|
|
138
|
+
|
|
139
|
+
// --- Server tests (node:test) ---
|
|
140
|
+
if (runServer) {
|
|
141
|
+
const { readdir } = await import('node:fs/promises');
|
|
142
|
+
const testFiles = [];
|
|
143
|
+
|
|
144
|
+
for (const dir of ['test/server', 'test/unit', 'test']) {
|
|
145
|
+
const fullDir = join(cwd, dir);
|
|
146
|
+
if (!existsSync(fullDir)) continue;
|
|
147
|
+
const files = await readdir(fullDir);
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
if (/\.test\.(js|ts|mjs|mts)$/.test(f)) {
|
|
150
|
+
const full = join(fullDir, f);
|
|
151
|
+
if (!testFiles.includes(full)) testFiles.push(full);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (testFiles.length > 0) {
|
|
157
|
+
console.log(`webjs test: running ${testFiles.length} server test file(s)…\n`);
|
|
158
|
+
const child = spawn(process.execPath, ['--test', ...testFiles], {
|
|
159
|
+
stdio: 'inherit', cwd, env: { ...process.env },
|
|
160
|
+
});
|
|
161
|
+
const code = await new Promise(r => child.on('exit', r));
|
|
162
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Browser tests (WTR + Playwright) ---
|
|
167
|
+
if (runBrowser) {
|
|
168
|
+
const wtrConfig = join(cwd, 'web-test-runner.config.js');
|
|
169
|
+
if (existsSync(wtrConfig) || existsSync(join(cwd, 'web-test-runner.config.mjs'))) {
|
|
170
|
+
console.log(`\nwebjs test: running browser tests (WTR + Playwright)…\n`);
|
|
171
|
+
const child = spawn('npx', ['wtr'], {
|
|
172
|
+
stdio: 'inherit', cwd, env: { ...process.env },
|
|
173
|
+
});
|
|
174
|
+
const code = await new Promise(r => child.on('exit', r));
|
|
175
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
176
|
+
} else if (!serverOnly) {
|
|
177
|
+
// No WTR config, check for test/browser directory
|
|
178
|
+
const browserDir = join(cwd, 'test', 'browser');
|
|
179
|
+
if (existsSync(browserDir)) {
|
|
180
|
+
console.log(`\nwebjs test: running browser tests (WTR + Playwright)…\n`);
|
|
181
|
+
const child = spawn('npx', ['wtr', '--files', 'test/browser/**/*.test.js'], {
|
|
182
|
+
stdio: 'inherit', cwd, env: { ...process.env },
|
|
183
|
+
});
|
|
184
|
+
const code = await new Promise(r => child.on('exit', r));
|
|
185
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('\nwebjs test: done ✓');
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'check': {
|
|
194
|
+
const { checkConventions, RULES, loadConventionOverrides } = await import('@webjsdev/server/check');
|
|
195
|
+
|
|
196
|
+
if (rest.includes('--rules')) {
|
|
197
|
+
const overrides = await loadConventionOverrides(process.cwd());
|
|
198
|
+
const anyOverride = Object.keys(overrides).length > 0;
|
|
199
|
+
console.log('webjs check, available rules:');
|
|
200
|
+
console.log(' All rules are ENABLED by default. A rule is only off when');
|
|
201
|
+
console.log(' package.json "webjs": { "conventions": { ... } } sets it');
|
|
202
|
+
console.log(' to false.\n');
|
|
203
|
+
for (const r of RULES) {
|
|
204
|
+
const off = overrides[r.name] === false;
|
|
205
|
+
const status = off ? '[disabled by override]' : '[enabled]';
|
|
206
|
+
console.log(` ${r.name.padEnd(30)} ${status.padEnd(24)} ${r.description}`);
|
|
207
|
+
}
|
|
208
|
+
if (!anyOverride) {
|
|
209
|
+
console.log('\n (no overrides found; every rule above is active in this project)');
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const violations = await checkConventions(process.cwd());
|
|
215
|
+
|
|
216
|
+
if (violations.length === 0) {
|
|
217
|
+
console.log('webjs check: all conventions pass ✓');
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`webjs check: ${violations.length} violation(s) found\n`);
|
|
220
|
+
for (const v of violations) {
|
|
221
|
+
console.log(` ✗ [${v.rule}] ${v.file}`);
|
|
222
|
+
console.log(` ${v.message}`);
|
|
223
|
+
if (v.fix) console.log(` Fix: ${v.fix}`);
|
|
224
|
+
console.log();
|
|
225
|
+
}
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case 'create': {
|
|
231
|
+
const name = rest[0];
|
|
232
|
+
if (!name || name.startsWith('-')) {
|
|
233
|
+
console.error('Usage: webjs create <app-name> [--template full-stack|api|saas]');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
const template = flag(rest, '--template', 'full-stack');
|
|
237
|
+
if (!TEMPLATES.includes(template)) {
|
|
238
|
+
// AI agents sometimes hallucinate template names ("blog", "todo",
|
|
239
|
+
// "ecommerce"). Reject early with the canonical list + guidance
|
|
240
|
+
// on which scaffold to pick for which kind of app.
|
|
241
|
+
console.error(`Error: unknown template '${template}'.
|
|
242
|
+
|
|
243
|
+
Only three scaffolds exist:
|
|
244
|
+
full-stack (default): pages + components + API + Prisma/SQLite.
|
|
245
|
+
Pick this for any app the user describes in product terms
|
|
246
|
+
(todo app, blog, dashboard, marketplace, social feed, …).
|
|
247
|
+
api backend-only: route handlers + modules, no pages/SSR.
|
|
248
|
+
Pick this only if the user explicitly asks for an HTTP/JSON
|
|
249
|
+
API with no UI.
|
|
250
|
+
saas auth + login/signup + protected dashboard + Prisma User
|
|
251
|
+
model. Pick this only if the user explicitly asks for auth
|
|
252
|
+
or a SaaS-shaped product.
|
|
253
|
+
|
|
254
|
+
The scaffold is a starting point. Replace the example layout/page/
|
|
255
|
+
components/schema with the actual app the user requested. Use Prisma +
|
|
256
|
+
SQLite for persistence (already wired up). Never store app data in JSON
|
|
257
|
+
files.
|
|
258
|
+
|
|
259
|
+
Full docs: https://docs.webjs.com`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
const { scaffoldApp } = await import('../lib/create.js');
|
|
263
|
+
await scaffoldApp(name, process.cwd(), { template });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'help':
|
|
267
|
+
case undefined:
|
|
268
|
+
console.log(USAGE);
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
console.error(`Unknown command: ${cmd}\n` + USAGE);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().catch((e) => {
|
|
277
|
+
console.error(e);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|