@webjsdev/cli 0.10.15 → 0.10.17
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/bin/webjs.js +27 -29
- package/lib/dev-supervisor.js +61 -0
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +1 -1
- package/templates/.cursorrules +1 -1
- package/templates/.github/copilot-instructions.md +1 -1
- package/templates/AGENTS.md +22 -4
- package/templates/CONVENTIONS.md +1 -1
- package/templates/Dockerfile +8 -5
package/bin/webjs.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
|
|
6
6
|
import { loadAppEnv, resolvePort } from '../lib/port.js';
|
|
7
|
+
import { planDevSupervisor } from '../lib/dev-supervisor.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -39,7 +40,8 @@ if (cmd !== 'help' && cmd !== undefined) {
|
|
|
39
40
|
const TEMPLATES = ['full-stack', 'api', 'saas'];
|
|
40
41
|
|
|
41
42
|
const USAGE = `webjs commands:
|
|
42
|
-
webjs dev [--port 8080]
|
|
43
|
+
webjs dev [--port 8080] [--no-hot] Start dev server with live reload
|
|
44
|
+
(--no-hot: run in-process, no hot-reload supervisor)
|
|
43
45
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
44
46
|
webjs test [--server|--browser] Run server + browser tests
|
|
45
47
|
webjs check [--json] Run correctness checks on the app (--json emits structured violations)
|
|
@@ -83,7 +85,8 @@ async function main() {
|
|
|
83
85
|
}
|
|
84
86
|
switch (cmd) {
|
|
85
87
|
case 'dev': {
|
|
86
|
-
// If we're already inside the
|
|
88
|
+
// If we're already inside the reload child (node --watch or bun --hot),
|
|
89
|
+
// start the server directly.
|
|
87
90
|
if (process.env.__WEBJS_DEV_CHILD === '1') {
|
|
88
91
|
const { startServer } = await import('@webjsdev/server');
|
|
89
92
|
// Load `.env` BEFORE resolving the port so a `PORT` set there is in
|
|
@@ -104,36 +107,31 @@ async function main() {
|
|
|
104
107
|
const hint = prismaDevHint(process.cwd());
|
|
105
108
|
if (hint) console.error(hint);
|
|
106
109
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// because Node caches ESM by URL with no public invalidation API.
|
|
112
|
-
// Build watch paths from directories that exist in the project.
|
|
110
|
+
// Decide how to run: in-process (`--no-hot`), or re-exec'd under the host
|
|
111
|
+
// runtime's hot-reload supervisor (`node --watch` on Node, `bun --hot` on
|
|
112
|
+
// Bun, #514). The branch logic lives in the pure `planDevSupervisor` so it
|
|
113
|
+
// is unit-testable without spawning a process.
|
|
113
114
|
const { existsSync } = await import('node:fs');
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
const plan = planDevSupervisor({
|
|
116
|
+
isBun: !!process.versions.bun,
|
|
117
|
+
argv: process.argv.slice(1),
|
|
118
|
+
noHot: rest.includes('--no-hot'),
|
|
119
|
+
exists: (p) => existsSync(p),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (plan.mode === 'inline') {
|
|
123
|
+
const { startServer } = await import('@webjsdev/server');
|
|
124
|
+
loadAppEnv(process.cwd());
|
|
125
|
+
const port = resolvePort(flag(rest, '--port'));
|
|
126
|
+
await startServer({ appDir: process.cwd(), port, dev: true });
|
|
127
|
+
break;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
const child = spawn(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
...watchPaths,
|
|
129
|
-
...process.argv.slice(1),
|
|
130
|
-
],
|
|
131
|
-
{
|
|
132
|
-
stdio: 'inherit',
|
|
133
|
-
cwd: process.cwd(),
|
|
134
|
-
env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
|
|
135
|
-
},
|
|
136
|
-
);
|
|
130
|
+
const child = spawn(process.execPath, plan.args, {
|
|
131
|
+
stdio: 'inherit',
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
|
|
134
|
+
});
|
|
137
135
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
138
136
|
break;
|
|
139
137
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-server reload supervisor planning for `webjs dev` (issue #514).
|
|
3
|
+
*
|
|
4
|
+
* `webjs dev` re-execs itself under the host runtime's hot-reload supervisor so
|
|
5
|
+
* an edit to a transitively-imported module (an action, query, component, util)
|
|
6
|
+
* takes effect without a manual restart. Both runtimes cache ES modules by
|
|
7
|
+
* resolved URL with no public invalidation API, so the dev re-import in
|
|
8
|
+
* `@webjsdev/server`'s `dev.js` relies on the runtime's own file-watching cache
|
|
9
|
+
* invalidation:
|
|
10
|
+
*
|
|
11
|
+
* - **Node** has no in-place module-cache eviction, so it re-execs under
|
|
12
|
+
* `node --watch`, which RESTARTS the process on a file change (a fresh ESM
|
|
13
|
+
* cache each time). The dev re-import additionally appends a `?t=` cache-bust
|
|
14
|
+
* query that Node honours between restarts.
|
|
15
|
+
* - **Bun** keys its module cache by path and IGNORES that `?t=` query, so the
|
|
16
|
+
* `node --watch` model does not transfer: without help a re-imported module
|
|
17
|
+
* stays STALE on Bun (the #514 bug). Bun's `--hot` invalidates loaded modules
|
|
18
|
+
* on a file change WITHOUT restarting the process, which is exactly what the
|
|
19
|
+
* dev re-import needs; `Bun.serve` is reused across hot reloads, so the
|
|
20
|
+
* listener is not duplicated. `--hot` auto-watches every loaded file, so the
|
|
21
|
+
* node `--watch-path` flags do not apply (and are not Bun flags).
|
|
22
|
+
*
|
|
23
|
+
* This pure planner returns the spawn decision so the bin stays a thin shell and
|
|
24
|
+
* the branch logic is unit-testable without spawning a process.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Plan how `webjs dev` runs its server.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} opts
|
|
31
|
+
* @param {boolean} opts.isBun Whether the host runtime is Bun (`process.versions.bun`).
|
|
32
|
+
* @param {string[]} opts.argv `process.argv.slice(1)` (the script path followed by its args), forwarded to the child verbatim.
|
|
33
|
+
* @param {boolean} opts.noHot Whether `--no-hot` was passed (opt out of the supervisor entirely).
|
|
34
|
+
* @param {(path: string) => boolean} opts.exists Existence check for the Node `--watch-path` targets (relative to cwd). Unused on Bun.
|
|
35
|
+
* @returns {{ mode: 'inline' } | { mode: 'spawn', args: string[] }}
|
|
36
|
+
* `inline` runs the server in this process (no reload watcher); `spawn`
|
|
37
|
+
* re-execs `process.execPath` with `args` and `__WEBJS_DEV_CHILD=1`.
|
|
38
|
+
*/
|
|
39
|
+
export function planDevSupervisor({ isBun, argv, noHot, exists }) {
|
|
40
|
+
// `--no-hot` opts out of the reload supervisor on either runtime: run the dev
|
|
41
|
+
// server in THIS process with no watcher. Degraded dev (a deep-import edit
|
|
42
|
+
// needs a manual restart) but useful under an external process manager or a
|
|
43
|
+
// debugger that wants a single, un-re-exec'd process.
|
|
44
|
+
if (noHot) return { mode: 'inline' };
|
|
45
|
+
|
|
46
|
+
if (isBun) return { mode: 'spawn', args: ['--hot', ...argv] };
|
|
47
|
+
|
|
48
|
+
// Node: re-exec under `node --watch`, watching the project dirs/files that
|
|
49
|
+
// exist. `--watch-preserve-output` keeps prior logs across a restart.
|
|
50
|
+
const watchPaths = [];
|
|
51
|
+
for (const dir of ['app', 'components', 'modules', 'lib', 'actions']) {
|
|
52
|
+
if (exists(dir)) watchPaths.push('--watch-path', dir);
|
|
53
|
+
}
|
|
54
|
+
for (const f of ['middleware.ts', 'middleware.js']) {
|
|
55
|
+
if (exists(f)) watchPaths.push('--watch-path', f);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
mode: 'spawn',
|
|
59
|
+
args: ['--watch', '--watch-preserve-output', ...watchPaths, ...argv],
|
|
60
|
+
};
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -96,7 +96,7 @@ self-review loop.
|
|
|
96
96
|
## Framework rules
|
|
97
97
|
|
|
98
98
|
- No build step: ES modules served directly.
|
|
99
|
-
- **Erasable TypeScript only.** Node 24+ strips types via
|
|
99
|
+
- **Erasable TypeScript only.** The runtime (Node 24+ or Bun) strips types via
|
|
100
100
|
`module.stripTypeScriptTypes` (whitespace replacement, byte-exact position
|
|
101
101
|
preservation, no sourcemap). The scaffold's `tsconfig.json` sets
|
|
102
102
|
`erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace`
|
package/templates/.cursorrules
CHANGED
|
@@ -102,7 +102,7 @@ self-review loop.
|
|
|
102
102
|
## Framework rules
|
|
103
103
|
|
|
104
104
|
- No build step: source files are served as ES modules
|
|
105
|
-
- **Erasable TypeScript only.**
|
|
105
|
+
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
|
|
106
106
|
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css`) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag.
|
|
107
107
|
- Shadow-DOM components opt in with `static shadow = true` and use `static styles = css` for scoped CSS, not inline styles. That is the right home for scoped CSS.
|
|
108
108
|
- One function per server action file (*.server.ts)
|
|
@@ -98,7 +98,7 @@ each change must include.
|
|
|
98
98
|
|
|
99
99
|
- No build step: source files are served as ES modules. Don't introduce
|
|
100
100
|
build tools or bundlers in the critical path.
|
|
101
|
-
- **Erasable TypeScript only.**
|
|
101
|
+
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
|
|
102
102
|
- Tagged template: html`<div>${value}</div>` with css`...` for styles.
|
|
103
103
|
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css\`...\``) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag. Shadow-DOM components (`static shadow = true`) legitimately author `static styles = css\`...\`` for scoped CSS; don't use inline `style="..."` there.
|
|
104
104
|
- Components: extend WebComponent, declare `static properties` (and `static styles` for shadow-DOM components), call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field.
|
package/templates/AGENTS.md
CHANGED
|
@@ -393,6 +393,23 @@ In Docker / Railway, prefer `npm start` (or `node node_modules/.bin/npm
|
|
|
393
393
|
start`) as the CMD over `node ... webjs.js start ...`. The npm form
|
|
394
394
|
fires `prestart`; the direct binary form skips it.
|
|
395
395
|
|
|
396
|
+
### Running on Bun instead of Node
|
|
397
|
+
|
|
398
|
+
webjs runs on **Node 24+ or Bun**. The same `package.json` scripts work on
|
|
399
|
+
either; to run under Bun, force it with `--bun` so the server executes on Bun
|
|
400
|
+
rather than the `webjs` bin's Node shebang:
|
|
401
|
+
|
|
402
|
+
```sh
|
|
403
|
+
bun install
|
|
404
|
+
bun --bun run dev # or: bun --bun run start
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
|
|
408
|
+
on Bun (which has no built-in) it comes from `amaro` automatically, so the same
|
|
409
|
+
source serves identically. SSR action-result seeding (an internal hydration
|
|
410
|
+
optimization) is off on Bun (it needs `module.registerHooks`), which only means
|
|
411
|
+
an async-render component re-fetches once on hydration, no behavior change.
|
|
412
|
+
|
|
396
413
|
**Containerized deploy ships with the scaffold.** `Dockerfile`,
|
|
397
414
|
`compose.yaml`, and `.dockerignore` are scaffolded at the app root. The
|
|
398
415
|
Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs
|
|
@@ -1126,10 +1143,11 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1126
1143
|
browser-only). Never fetch initial data in `connectedCallback` /
|
|
1127
1144
|
`firstUpdated`. Fetch in the page function (server) and pass it as
|
|
1128
1145
|
a prop. See *Component pattern* above.
|
|
1129
|
-
8. **Erasable TypeScript only.**
|
|
1130
|
-
`module.stripTypeScriptTypes
|
|
1131
|
-
|
|
1132
|
-
|
|
1146
|
+
8. **Erasable TypeScript only.** The runtime strips types at the runtime
|
|
1147
|
+
layer (Node 24+'s built-in `module.stripTypeScriptTypes`, or `amaro`
|
|
1148
|
+
on Bun, which is byte-identical), with whitespace replacement so
|
|
1149
|
+
line and column positions are byte-exact and no sourcemap ships to
|
|
1150
|
+
the browser. Your `tsconfig.json` sets `erasableSyntaxOnly: true`, so
|
|
1133
1151
|
the TS compiler rejects: `enum`, `namespace` with values,
|
|
1134
1152
|
constructor parameter properties, legacy decorators with
|
|
1135
1153
|
`emitDecoratorMetadata`, and `import = require`. Use the erasable
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -1054,7 +1054,7 @@ export async function createPost(input: {
|
|
|
1054
1054
|
|
|
1055
1055
|
<!-- OVERRIDE -->
|
|
1056
1056
|
- TypeScript with explicit `.ts` extensions in imports
|
|
1057
|
-
- **Erasable TypeScript only.** The
|
|
1057
|
+
- **Erasable TypeScript only.** The runtime strips types at the runtime layer (Node 24+'s built-in `module.stripTypeScriptTypes`, or `amaro` on Bun, byte-identical) with whitespace replacement, so line + column positions are byte-exact and no sourcemap ships. Your `tsconfig.json` sets `erasableSyntaxOnly: true`, so the compiler rejects: `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Write the erasable equivalents:
|
|
1058
1058
|
```ts
|
|
1059
1059
|
// Not allowed
|
|
1060
1060
|
enum Color { Red, Green, Blue }
|
package/templates/Dockerfile
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
# Works with a plain `docker build` / `docker compose up`, and is the same
|
|
4
4
|
# artifact the webdeploy hosting tool (ubicloud + uncloud) builds and ships.
|
|
5
5
|
#
|
|
6
|
-
# webjs serves .ts directly
|
|
7
|
-
# NO JavaScript build step
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# webjs serves .ts directly by stripping types at the runtime layer, so there is
|
|
7
|
+
# NO JavaScript build step (webjs is buildless end to end; there is no bundler or
|
|
8
|
+
# esbuild fallback). This image runs the app on **Node 24+**, where the strip is
|
|
9
|
+
# the built-in `module.stripTypeScriptTypes`. webjs ALSO runs on **Bun** (where
|
|
10
|
+
# the strip comes from `amaro` automatically), so you can swap this base for an
|
|
11
|
+
# `oven/bun` image and start the app with `bun --bun run start`. Do not lower the
|
|
12
|
+
# Node base below 24 (the floor the CI workflow and the framework pin enforce),
|
|
13
|
+
# since the built-in stripper and recursive fs.watch need it.
|
|
11
14
|
#
|
|
12
15
|
# Security headers are set by the framework, not the proxy. webjs emits
|
|
13
16
|
# X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and
|