@webjsdev/cli 0.10.20 → 0.10.21
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/lib/create.js +5 -4
- package/lib/runtime-rewrite.js +56 -43
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +7 -0
- package/templates/.cursorrules +6 -0
- package/templates/.github/copilot-instructions.md +8 -2
- package/templates/.hooks/pre-commit +11 -0
- package/templates/AGENTS.md +24 -12
- package/templates/CONVENTIONS.md +18 -24
package/lib/create.js
CHANGED
|
@@ -17,7 +17,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
-
import { bunifyProse, bunifyDockerfile, bunifyCi } from './runtime-rewrite.js';
|
|
20
|
+
import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
|
|
@@ -532,11 +532,12 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
532
532
|
// touches npm/npx command tokens, so the test code itself is unaffected.
|
|
533
533
|
'test/hello/browser/hello.test.js', 'test/hello/e2e/hello.test.ts',
|
|
534
534
|
]);
|
|
535
|
-
// compose.yaml
|
|
536
|
-
//
|
|
537
|
-
//
|
|
535
|
+
// compose.yaml builds from the (pure oven/bun) Dockerfile and inherits its
|
|
536
|
+
// `bun --bun run start` CMD; only its healthcheck needs switching off node
|
|
537
|
+
// (the pure Bun image has no node), which bunifyCompose does.
|
|
538
538
|
const FILE_REWRITE = {
|
|
539
539
|
'Dockerfile': bunifyDockerfile,
|
|
540
|
+
'compose.yaml': bunifyCompose,
|
|
540
541
|
'.github/workflows/ci.yml': bunifyCi,
|
|
541
542
|
};
|
|
542
543
|
for (const f of templateFiles) {
|
package/lib/runtime-rewrite.js
CHANGED
|
@@ -23,14 +23,11 @@
|
|
|
23
23
|
* - the `dev` / `start` scripts force `bun --bun` (the server is Bun),
|
|
24
24
|
* - every other command stays `bun run` / `webjs ...` (runs on Node via the
|
|
25
25
|
* `webjs` bin's `#!/usr/bin/env node` shebang),
|
|
26
|
-
* - the Dockerfile
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* shell `npx drizzle-kit` for the boot `webjs db migrate`, which a pure
|
|
32
|
-
* `oven/bun` image lacks. The node base works with ANY installed CLI; a
|
|
33
|
-
* future scaffold can drop to a pure `oven/bun` base once #570 is published.
|
|
26
|
+
* - the Dockerfile is a pure `oven/bun:1` base (#595). This is safe as of
|
|
27
|
+
* `@webjsdev/cli@0.10.20` (#570): `webjs db migrate` resolves drizzle-kit
|
|
28
|
+
* and runs it under Bun (no `npx`), so a Node-less image works. (Before
|
|
29
|
+
* #570 shipped as `latest`, this stayed on `node:24-alpine` + a copied Bun
|
|
30
|
+
* binary, since the installed CLI could still shell `npx`.)
|
|
34
31
|
*/
|
|
35
32
|
|
|
36
33
|
/**
|
|
@@ -50,10 +47,17 @@
|
|
|
50
47
|
export function bunifyProse(s) {
|
|
51
48
|
return s
|
|
52
49
|
// Prose claim about the Dockerfile CMD (AGENTS.md dev-start parity section);
|
|
53
|
-
// the bun Dockerfile's CMD becomes `bun --bun run start`.
|
|
54
|
-
// node:24-alpine (+ a copied Bun binary), so the "pins node:24-alpine" prose
|
|
55
|
-
// is still accurate and is NOT rewritten.
|
|
50
|
+
// the bun Dockerfile's CMD becomes `bun --bun run start`.
|
|
56
51
|
.replaceAll('`CMD ["npm", "start"]`', '`CMD ["bun", "--bun", "run", "start"]`')
|
|
52
|
+
// The "Containerized deploy" prose describes the node template's
|
|
53
|
+
// node:24-alpine base; the bun Dockerfile is a pure oven/bun:1 image (#595),
|
|
54
|
+
// so rewrite the base claim to match what the bun app actually ships. (The
|
|
55
|
+
// trailing `npm start` is rewritten to `bun --bun run start` by the generic
|
|
56
|
+
// rule below.)
|
|
57
|
+
.replaceAll(
|
|
58
|
+
'Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs\ndeps (no build step, since Drizzle has no codegen), and starts via',
|
|
59
|
+
'Dockerfile is a pure `oven/bun:1` image (no Node, since `webjs db migrate`\nresolves drizzle-kit and runs under Bun with no `npx`, #570), installs deps\nwith `bun install` (no build step, since Drizzle has no codegen), and starts via',
|
|
60
|
+
)
|
|
57
61
|
// The "Running on Bun" section frames Bun as opt-in ("force it with --bun").
|
|
58
62
|
// In a bun-flavored app the dev/start scripts ALREADY embed --bun, so reframe
|
|
59
63
|
// it as the configured default.
|
|
@@ -92,64 +96,73 @@ export function bunifyProse(s) {
|
|
|
92
96
|
/**
|
|
93
97
|
* Rewrite the scaffolded Dockerfile for Bun.
|
|
94
98
|
*
|
|
95
|
-
* Base decision (acceptance criterion):
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* install`) uses the committed `bun.lock`, and `trustedDependencies` lets
|
|
106
|
-
* better-sqlite3's prebuild postinstall run. Once cli@#570 is the published
|
|
107
|
-
* `latest`, a future scaffold can drop to a pure `oven/bun` base.
|
|
99
|
+
* Base decision (acceptance criterion): a pure `oven/bun:1` image (no Node).
|
|
100
|
+
* Safe as of `@webjsdev/cli@0.10.20` (#570): `webjs db` / `webjs test` resolve
|
|
101
|
+
* their tools (drizzle-kit, wtr) and spawn them with the current runtime instead
|
|
102
|
+
* of `npx`, so the boot-time `webjs db migrate` runs under Bun with no Node
|
|
103
|
+
* toolchain. (Before #570 was the published `latest`, this stayed on a
|
|
104
|
+
* `node:24-alpine` base with a copied Bun binary, since the installed CLI could
|
|
105
|
+
* still shell `npx`, which a pure Bun image lacks. #595 flipped it once the
|
|
106
|
+
* npx-free CLI shipped.) `oven/bun:1` is Debian-based: `ca-certificates` ship in
|
|
107
|
+
* the image and better-sqlite3 fetches its glibc prebuild on `bun install`
|
|
108
|
+
* (gated by `trustedDependencies`), so no build toolchain is needed.
|
|
108
109
|
*
|
|
109
110
|
* @param {string} s
|
|
110
111
|
* @returns {string}
|
|
111
112
|
*/
|
|
112
113
|
export function bunifyDockerfile(s) {
|
|
113
114
|
return s
|
|
114
|
-
// Top comment: explain the
|
|
115
|
+
// Top comment: explain the pure oven/bun base.
|
|
115
116
|
.replace(
|
|
116
117
|
/# webjs serves \.ts directly[\s\S]*?since the built-in stripper and recursive fs\.watch need it\.\n/,
|
|
117
118
|
'# webjs serves .ts directly by stripping types at the runtime layer, so there is\n' +
|
|
118
119
|
'# NO JavaScript build step (webjs is buildless end to end; there is no bundler or\n' +
|
|
119
|
-
'# esbuild fallback). This image
|
|
120
|
-
'#
|
|
121
|
-
'#
|
|
122
|
-
'#
|
|
123
|
-
'#
|
|
124
|
-
'# fs.watch need it. (A pure oven/bun base, no Node, is possible once your installed\n' +
|
|
125
|
-
'# @webjsdev/cli resolves drizzle-kit without npx; the node base works regardless.)\n',
|
|
120
|
+
'# esbuild fallback). This image runs the app on **Bun**: the type-strip comes from\n' +
|
|
121
|
+
'# `amaro`, the server serves via Bun.serve, and `webjs db migrate` runs under Bun\n' +
|
|
122
|
+
'# (the CLI resolves drizzle-kit without npx, #570), so no Node is needed. webjs also\n' +
|
|
123
|
+
'# runs on Node 24+; for a Node base instead, swap to `node:24-alpine` and start with\n' +
|
|
124
|
+
'# `npm start`.\n',
|
|
126
125
|
)
|
|
127
|
-
|
|
126
|
+
.replace('FROM node:24-alpine', 'FROM oven/bun:1')
|
|
127
|
+
// Debian base: ca-certificates already present, no `apk`. Drop the alpine line.
|
|
128
128
|
.replace(
|
|
129
|
-
|
|
130
|
-
'
|
|
131
|
-
'# Bun binary, so the server serves on Bun while the Node toolchain above stays\n' +
|
|
132
|
-
'# available for the boot `webjs db migrate` (which may shell npx, depending on the\n' +
|
|
133
|
-
'# installed @webjsdev/cli version) and the shebang bins.\n' +
|
|
134
|
-
'COPY --from=oven/bun:1-alpine /usr/local/bin/bun /usr/local/bin/bun\n',
|
|
129
|
+
/# ca-certificates for outbound TLS \(e\.g\. a managed Postgres\)\. better-sqlite3\n# is a prebuilt native module, so no build toolchain is needed here\.\nRUN apk add --no-cache ca-certificates\n\n/,
|
|
130
|
+
'# The Debian-based oven/bun image ships ca-certificates for outbound TLS (e.g. a\n# managed Postgres). better-sqlite3 fetches its glibc prebuild on `bun install`\n# (gated by trustedDependencies), so no build toolchain is needed.\n\n',
|
|
135
131
|
)
|
|
136
132
|
// Lockfile + install (bun.lock, bun install).
|
|
137
133
|
.replace(
|
|
138
134
|
'# package-lock.json is optional (it\'s absent when the app was scaffolded with\n# --no-install); the glob keeps the COPY working with or without it.\nCOPY package.json package-lock.json* ./\nRUN npm install --no-audit --no-fund',
|
|
139
135
|
'# bun.lock is optional (absent when scaffolded with --no-install); the glob keeps\n# the COPY working with or without it. trustedDependencies in package.json lets\n# better-sqlite3\'s native-prebuild postinstall run (bun skips postinstalls).\nCOPY package.json bun.lock* ./\nRUN bun install',
|
|
140
136
|
)
|
|
141
|
-
//
|
|
142
|
-
//
|
|
137
|
+
// Healthcheck: the pure Bun image has no node; use `bun -e`. Keep the
|
|
138
|
+
// dependency-free-probe comment accurate (the probe runs under Bun now).
|
|
139
|
+
.replace("(Node 24's built-in fetch, no curl/wget)", "(the runtime's built-in fetch, no curl/wget)")
|
|
140
|
+
.replace('CMD ["node", "-e", "fetch(', 'CMD ["bun", "-e", "fetch(')
|
|
141
|
+
// Entrypoint: serve on Bun.
|
|
143
142
|
.replace(
|
|
144
143
|
/# `npm start` is a thin alias[\s\S]*?the migrate no longer depends on an npm `prestart` hook\.\nCMD \["npm", "start"\]/,
|
|
145
144
|
'# `bun --bun run start` runs the `start` script on Bun (the server serves via\n' +
|
|
146
145
|
'# Bun.serve). `webjs start` runs the `webjs.start.before` step (`webjs db migrate`,\n' +
|
|
147
|
-
'# which
|
|
148
|
-
'#
|
|
146
|
+
'# which resolves drizzle-kit and runs it under Bun, no npx, #570), idempotent / a\n' +
|
|
147
|
+
'# no-op with no pending migrations, then serves on $PORT.\n' +
|
|
149
148
|
'CMD ["bun", "--bun", "run", "start"]',
|
|
150
149
|
);
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Rewrite compose.yaml for the pure-Bun image (#595): its healthcheck runs in
|
|
154
|
+
* the `oven/bun:1` container (compose's healthcheck overrides the Dockerfile's),
|
|
155
|
+
* which has no `node`, so switch `node -e` to `bun -e`. compose otherwise builds
|
|
156
|
+
* from the Dockerfile and inherits its `bun --bun run start` CMD, so nothing
|
|
157
|
+
* else changes.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} s
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export function bunifyCompose(s) {
|
|
163
|
+
return s.replace('test: ["CMD", "node", "-e", "fetch(', 'test: ["CMD", "bun", "-e", "fetch(');
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Rewrite the GitHub Actions CI workflow for Bun: ADD `oven-sh/setup-bun`
|
|
155
168
|
* alongside `actions/setup-node` (kept, because the `webjs` test/db/check
|
package/package.json
CHANGED
|
@@ -34,6 +34,13 @@ FIRST, before writing any code:
|
|
|
34
34
|
- If on main/master: create a feature branch before editing.
|
|
35
35
|
- If on a feature branch: verify it matches the current task.
|
|
36
36
|
2. Sync: `git fetch origin && git rebase origin/main` if behind.
|
|
37
|
+
3. If more than one agent may work this repo at once, use a DEDICATED git
|
|
38
|
+
worktree per task (`git worktree add -b <branch> ../<repo>-<slug> origin/main`,
|
|
39
|
+
`cd` in, work there, `git worktree remove` after merge), never a shared
|
|
40
|
+
checkout. Two agents in one directory collide: a `git checkout` in one moves
|
|
41
|
+
HEAD under the other, so commits land on the wrong branch. Git enforces
|
|
42
|
+
one-branch-per-worktree, so worktrees prevent it. A lone agent in a clean
|
|
43
|
+
checkout may use a plain branch.
|
|
37
44
|
|
|
38
45
|
## Autonomous mode (sandbox / no-prompt)
|
|
39
46
|
|
package/templates/.cursorrules
CHANGED
|
@@ -35,6 +35,12 @@ FIRST, before writing any code:
|
|
|
35
35
|
2. Sync with parent: `git fetch origin && git log HEAD..origin/main --oneline`
|
|
36
36
|
- If upstream has new commits: `git rebase origin/main` before starting.
|
|
37
37
|
- Resolve any conflicts before proceeding with the task.
|
|
38
|
+
3. If more than one agent may work this repo at once, use a DEDICATED git
|
|
39
|
+
worktree per task, not a shared checkout: `git worktree add -b <branch>
|
|
40
|
+
../<repo>-<slug> origin/main`, `cd` in, work there, `git worktree remove`
|
|
41
|
+
after merge. Two agents in one directory collide (a `git checkout` in one
|
|
42
|
+
moves HEAD under the other, so commits land on the wrong branch). A lone
|
|
43
|
+
agent in a clean checkout may use a plain branch.
|
|
38
44
|
|
|
39
45
|
## Autonomous mode (sandbox / no-prompt)
|
|
40
46
|
|
|
@@ -32,6 +32,12 @@ FIRST, before writing any code:
|
|
|
32
32
|
- If on main/master: create a feature branch before editing.
|
|
33
33
|
- If on a feature branch: verify it matches the task at hand.
|
|
34
34
|
2. Sync: `git fetch origin && git rebase origin/main` if behind.
|
|
35
|
+
3. If more than one agent may work this repo at once, use a DEDICATED git
|
|
36
|
+
worktree per task (`git worktree add -b <branch> ../<repo>-<slug> origin/main`,
|
|
37
|
+
`cd` in, work there, `git worktree remove` after merge), never a shared
|
|
38
|
+
checkout. Two agents in one directory collide: a `git checkout` in one moves
|
|
39
|
+
HEAD under the other, so commits land on the wrong branch. A lone agent in a
|
|
40
|
+
clean checkout may use a plain branch.
|
|
35
41
|
|
|
36
42
|
## Autonomous mode (sandbox / no-prompt)
|
|
37
43
|
|
|
@@ -101,7 +107,7 @@ each change must include.
|
|
|
101
107
|
- **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
108
|
- Tagged template: html`<div>${value}</div>` with css`...` for styles.
|
|
103
109
|
- **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
|
-
- Components: extend WebComponent
|
|
110
|
+
- Components: extend the `WebComponent({ ... })` factory to declare reactive properties (e.g. `extends WebComponent({ count: Number })`; per-prop options via `prop(Number, { reflect: true })`), add `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. A hand-written `static properties` throws at construction (`no-static-properties`); set defaults via the `default` option or the constructor, never a class-field initializer (`reactive-props-no-class-field`).
|
|
105
111
|
- Async data in a component: prefer an `async render()` (`const u = await getUser(this.uid)`), which SSR awaits so the data is in the first paint, over prop-drilling from the page or fetching in `connectedCallback`. `renderFallback()` is the optional re-fetch loading state (never first paint); error isolation is automatic (`renderError()` customizes it); a `Task` is for genuinely client-only data.
|
|
106
112
|
- Server actions: *.server.ts files with one exported async function each.
|
|
107
113
|
- Server-only code (a DB driver like better-sqlite3/pg, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. The DB lives in db/*.server.ts; lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
|
|
@@ -109,5 +115,5 @@ each change must include.
|
|
|
109
115
|
- Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
|
|
110
116
|
- Task: import { Task, TaskStatus } from '@webjsdev/core/task'
|
|
111
117
|
- Routing: file-based under app/ (page.ts, layout.ts, route.ts, middleware.ts).
|
|
112
|
-
- Component state lives in signals from @webjsdev/core. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (
|
|
118
|
+
- Component state lives in signals from @webjsdev/core. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (declared via the `WebComponent({ ... })` factory) are for HTML attributes and .prop=${...} hydration.
|
|
113
119
|
- Don't skip tests or documentation updates.
|
|
@@ -11,9 +11,20 @@
|
|
|
11
11
|
# here, so a commit stays fast and the test gate cannot be skipped by a
|
|
12
12
|
# local --no-verify. The CI workflow runs `webjs check` + `webjs test`
|
|
13
13
|
# on every push and pull request.
|
|
14
|
+
#
|
|
15
|
+
# Running more than one AI agent on this repo at once? Give each task its own
|
|
16
|
+
# git worktree, not a shared checkout. Two agents in one working directory
|
|
17
|
+
# collide: a `git checkout` in one moves HEAD under the other, so the next
|
|
18
|
+
# commit lands on the wrong branch. Before committing, confirm the branch below
|
|
19
|
+
# is the one you intended; if it moved, you are sharing a checkout. Isolate:
|
|
20
|
+
# git worktree add -b <branch> ../<app>-<task> origin/main && cd ../<app>-<task>
|
|
14
21
|
|
|
15
22
|
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
16
23
|
|
|
24
|
+
# Surface the branch so a wrong-branch commit (a concurrent-agent HEAD move) is
|
|
25
|
+
# visible in the commit output rather than silent.
|
|
26
|
+
echo "[pre-commit] committing on branch: ${BRANCH:-<detached>}"
|
|
27
|
+
|
|
17
28
|
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
18
29
|
echo ""
|
|
19
30
|
echo "ERROR: Cannot commit directly to '$BRANCH'."
|
package/templates/AGENTS.md
CHANGED
|
@@ -163,8 +163,9 @@ entry, its own template parser. Inside `` html`…` `` templates you get:
|
|
|
163
163
|
- Binding-aware completions: reachable tag names after `<`, and
|
|
164
164
|
prefix-keyed attributes (`.prop` property names, `?bool` / plain
|
|
165
165
|
hyphenated attribute names).
|
|
166
|
-
- Diagnostics: value type-checks against
|
|
167
|
-
`@`/`.`/`?` bindings, and
|
|
166
|
+
- Diagnostics: value type-checks against the reactive props declared in
|
|
167
|
+
`WebComponent({ ... })`, unquoted `@`/`.`/`?` bindings, and
|
|
168
|
+
expressionless `.prop` bindings.
|
|
168
169
|
- Hover showing the component class / declared member type.
|
|
169
170
|
|
|
170
171
|
In VS Code / Cursor / Windsurf, the **`webjs` extension** bundles this
|
|
@@ -597,12 +598,13 @@ const secret = process.env.AUTH_SECRET; // undefined (fail-closed)
|
|
|
597
598
|
```ts
|
|
598
599
|
import { WebComponent, html, css } from '@webjsdev/core';
|
|
599
600
|
|
|
600
|
-
|
|
601
|
-
|
|
601
|
+
// Recommended declare-free base-class factory style
|
|
602
|
+
export class Counter extends WebComponent({
|
|
603
|
+
count: Number
|
|
604
|
+
}) {
|
|
602
605
|
static styles = css`button { padding: 8px 12px; }`; // shadow-DOM only
|
|
603
606
|
// static shadow = true; // opt into shadow DOM (default: light DOM)
|
|
604
607
|
// static lazy = true; // download JS only when scrolled into view
|
|
605
|
-
declare count: number; // TypeScript-only typed accessor
|
|
606
608
|
|
|
607
609
|
constructor() {
|
|
608
610
|
super();
|
|
@@ -629,8 +631,9 @@ the click handler is inert). Two consequences for how you write code:
|
|
|
629
631
|
1. **Defaults for the first paint go in `constructor()`** (after
|
|
630
632
|
`super()`), never as class-field initializers (which break
|
|
631
633
|
reactivity) and never in `connectedCallback` (which the server
|
|
632
|
-
doesn't run). For
|
|
633
|
-
default in the constructor
|
|
634
|
+
doesn't run). For reactive properties declared via the
|
|
635
|
+
`WebComponent({ ... })` factory, set the default in the constructor
|
|
636
|
+
or pass the `default` option (e.g. `prop(Number, { default: 0 })`).
|
|
634
637
|
2. **`connectedCallback` is browser-only.** Use it for
|
|
635
638
|
`localStorage`, viewport size, online status, or anything that
|
|
636
639
|
genuinely can't be known on the server. Read the value, then
|
|
@@ -659,7 +662,8 @@ See [Progressive Enhancement](https://docs.webjs.dev/docs/progressive-enhancemen
|
|
|
659
662
|
## Lit muscle-memory gotchas (read if you have written lit before)
|
|
660
663
|
|
|
661
664
|
Webjs's runtime API matches lit. The `WebComponent` base class,
|
|
662
|
-
|
|
665
|
+
reactive properties (declared via the `WebComponent({ ... })` factory),
|
|
666
|
+
the lifecycle hooks, ReactiveControllers, the
|
|
663
667
|
directive set, `html` / `css` tagged templates. The **rendering
|
|
664
668
|
model**, however, is different. Pure-lit patterns that work fine in a
|
|
665
669
|
client-only lit app break in webjs's SSR pipeline or its reactivity
|
|
@@ -714,11 +718,12 @@ Practical consequences for agents writing webjs code.
|
|
|
714
718
|
| Assuming an `async render()` always ships its module | A bare one (no other client signal) is ELIDED, so it costs zero JS and skips the on-hydration re-fetch, first paint unchanged | Rely on it for a fetch-and-display leaf. `static refresh = true` keeps the on-load refresh, `static shadow = true` always ships |
|
|
715
719
|
| `window.X` / `document.X` in constructor or `render()` | SSR crash | Move to `connectedCallback` |
|
|
716
720
|
| Top-level `import` of a browser-only library | SSR crash | Dynamic `import()` inside `connectedCallback` |
|
|
717
|
-
| Class-field initializer for a reactive property (`student: Student = {...}`) | Silently breaks reactivity (overwrites the framework accessor) | `
|
|
718
|
-
| `@property()` decorator | Banned by invariant 10 (erasable TS) |
|
|
721
|
+
| Class-field initializer for a reactive property (`student: Student = {...}`) | Silently breaks reactivity (overwrites the framework accessor) | Pass the shape to the base-class factory `WebComponent({ student: Object })` and set the default in the constructor (flagged by `reactive-props-no-class-field`) |
|
|
722
|
+
| `@property()` decorator | Banned by invariant 10 (erasable TS) | Pass the shape to the base-class factory `WebComponent({ ... })` (the only supported form) |
|
|
723
|
+
| Hand-written `static properties = { ... }` | Throws at construction (the factory owns property setup) | Pass the same shape to the base-class factory `WebComponent({ ... })` (flagged by `no-static-properties`) |
|
|
719
724
|
| Scoped `static styles = css` or an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component | Scoped block does nothing without `static shadow = true`; inline `<style>` class names leak globally | Tailwind utilities (the light-DOM default); or `static shadow = true` for genuinely scoped CSS |
|
|
720
725
|
| `willUpdate` computing SSR-visible derived state | Works (runs at SSR), but overriding it opts the component out of elision | Fine for interactive components; for display-only, derive inline in `render()` |
|
|
721
|
-
| `this.hasAttribute` / `getAttribute` in `render()` | Works (server attribute shim backs the attribute methods at SSR) | Read attributes directly, or
|
|
726
|
+
| `this.hasAttribute` / `getAttribute` in `render()` | Works (server attribute shim backs the attribute methods at SSR) | Read attributes directly, or declare a reactive prop via the base-class factory `WebComponent({ ... })` |
|
|
722
727
|
| `ContextProvider` for server-known data | Default value during SSR, content shift on hydration | Pass via props from the page function |
|
|
723
728
|
|
|
724
729
|
The full annotated catalog with code examples lives in the framework
|
|
@@ -1218,7 +1223,14 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1218
1223
|
|
|
1219
1224
|
## Workflow expectations for AI agents
|
|
1220
1225
|
|
|
1221
|
-
1. Branch before editing. Never push to `main` directly.
|
|
1226
|
+
1. Branch before editing. Never push to `main` directly. **If more than one
|
|
1227
|
+
agent may work this repo at once, give each task its own git worktree, not a
|
|
1228
|
+
shared checkout** (`git worktree add -b <branch> ../<repo>-<slug> origin/main`,
|
|
1229
|
+
`cd` in, work there, `git worktree remove` after merge). Two agents in one
|
|
1230
|
+
working directory collide: a `git checkout` in one moves `HEAD` under the
|
|
1231
|
+
other, so the next commit lands on the wrong branch. Git enforces
|
|
1232
|
+
one-branch-per-worktree, so worktrees prevent it; a lone agent in a clean
|
|
1233
|
+
checkout may use a plain branch.
|
|
1222
1234
|
2. Every code change comes with a test, AGENTS.md / docs updates if the
|
|
1223
1235
|
feature surface changed, `webjs check` passing. A unit test is not
|
|
1224
1236
|
always enough: a component, hydration, the client router, or a server
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -60,6 +60,14 @@ even if the user doesn't explicitly ask.**
|
|
|
60
60
|
3. If on a feature branch → verify it matches the current task
|
|
61
61
|
4. Sync with parent: `git fetch origin && git rebase origin/main` if behind
|
|
62
62
|
5. Don't mix unrelated work on the wrong branch
|
|
63
|
+
6. **If more than one agent may work this repo at once, use a dedicated git
|
|
64
|
+
worktree per task, never a shared checkout.** Two agents in one working
|
|
65
|
+
directory collide: a `git checkout` in one moves `HEAD` under the other, so
|
|
66
|
+
the next commit lands on the wrong branch. Isolate each task:
|
|
67
|
+
`git worktree add -b <branch> ../<repo>-<slug> origin/main`, `cd` in, work
|
|
68
|
+
there, and `git worktree remove` after the PR merges. Git enforces
|
|
69
|
+
one-branch-per-worktree, so this makes the collision impossible. A lone agent
|
|
70
|
+
in a clean checkout may use a plain branch.
|
|
63
71
|
|
|
64
72
|
### After cloning: verify the toolchain
|
|
65
73
|
|
|
@@ -635,10 +643,11 @@ Any stateful behavior with a Tier-2 element uses the element.
|
|
|
635
643
|
```ts
|
|
636
644
|
import { WebComponent, html } from '@webjsdev/core';
|
|
637
645
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
646
|
+
// Recommended declare-free base-class factory style
|
|
647
|
+
export class MyWidget extends WebComponent({
|
|
648
|
+
label: String,
|
|
649
|
+
count: Number
|
|
650
|
+
}) {
|
|
642
651
|
// Light DOM is the default; Tailwind utility classes apply directly.
|
|
643
652
|
|
|
644
653
|
constructor() {
|
|
@@ -660,16 +669,7 @@ export class MyWidget extends WebComponent {
|
|
|
660
669
|
MyWidget.register('my-widget');
|
|
661
670
|
```
|
|
662
671
|
|
|
663
|
-
`static properties`
|
|
664
|
-
attribute coercion, reflection). `declare` types the field for
|
|
665
|
-
TypeScript without emitting a class-field initializer that would
|
|
666
|
-
clobber the reactive accessor at construction time. The two
|
|
667
|
-
declarations together give you full intelligence in any tsserver-backed
|
|
668
|
-
editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense`
|
|
669
|
-
(no Lit dependency) that extends this to tag / attribute intelligence
|
|
670
|
-
inside `html\`…\`` templates (go-to-definition, binding-aware completions,
|
|
671
|
-
value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the
|
|
672
|
-
`webjs` extension bundles it automatically.
|
|
672
|
+
Reactive properties are declared one way: pass the properties shape directly to the base-class factory `WebComponent({ ... })` (e.g. `label: String`). The property types flow to `this.<prop>` with no `declare` lines needed, and the factory installs the reactive accessors so a class-field initializer can never clobber them. For per-property options use the `prop()` helper inside the shape (`count: prop(Number, { reflect: true })`, `mode: prop({ state: true })`); narrow a type with `prop<Student>(Object)`. Set defaults via the `default` option (`prop(Number, { default: 0 })`) or by assigning in the constructor after `super()`. A hand-written `static properties = { ... }` THROWS at construction (`no-static-properties`). The factory gives you full intelligence in any tsserver-backed editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense` (no Lit dependency) that extends this to tag / attribute intelligence inside `html\`…\`` templates (go-to-definition, binding-aware completions, value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the `webjs` extension bundles it automatically.
|
|
673
673
|
|
|
674
674
|
**Rules:**
|
|
675
675
|
- One component per file
|
|
@@ -680,15 +680,8 @@ value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the
|
|
|
680
680
|
- `my-widget .body`, `my-widget .title` (descendant selector)
|
|
681
681
|
- Tag name must contain a hyphen (HTML spec)
|
|
682
682
|
- Always call `Class.register('tag')`. That's the standard DOM API.
|
|
683
|
-
- **Reactive props
|
|
684
|
-
- Component state lives in signals. Import `signal` from
|
|
685
|
-
`@webjsdev/core`, read via `signal.get()` inside `render()`, write
|
|
686
|
-
via `signal.set(value)`. Module-scope signals share state across
|
|
687
|
-
components; instance signals (created in the constructor) carry
|
|
688
|
-
component-local state. Reactive properties (`static properties =
|
|
689
|
-
{ foo: { type: ... } }` with a sibling `declare foo: T`) wrap HTML
|
|
690
|
-
attributes, attribute reflection, and `.prop=${value}` SSR
|
|
691
|
-
hydration.
|
|
683
|
+
- **Reactive props are declared via the base-class factory `WebComponent({ ... })`.** A hand-written `static properties = { ... }` throws at construction (`webjs check` flags it via `no-static-properties`). Never write `propName = value` or `propName: Type = value` as a class-field initializer on a reactive prop. It compiles to `Object.defineProperty(this, …)` after `super()` and clobbers the framework's reactive accessor, silently breaking re-renders (`webjs check` flags this via `reactive-props-no-class-field`). Set defaults via the `default` option or in the constructor.
|
|
684
|
+
- Component state lives in signals. Import `signal` from `@webjsdev/core`, read via `signal.get()` inside `render()`, write via `signal.set(value)`. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (declared via the factory) wrap HTML attributes, attribute reflection, and `.prop=${value}` SSR hydration.
|
|
692
685
|
- Use lifecycle hooks (`firstUpdated`, `updated`) only when needed
|
|
693
686
|
|
|
694
687
|
---
|
|
@@ -997,7 +990,8 @@ component, applies its attributes, runs `willUpdate` and controllers'
|
|
|
997
990
|
`firstUpdated`, `updated`, or any other browser-only lifecycle hook.
|
|
998
991
|
Whatever state should appear on first paint MUST be set in the
|
|
999
992
|
constructor (after `super()`), derived in `willUpdate`, or derivable
|
|
1000
|
-
from
|
|
993
|
+
from the factory-declared reactive props + attributes on the rendered
|
|
994
|
+
tag. Reading
|
|
1001
995
|
`this.getAttribute` / `hasAttribute` in `render()` works server-side (a
|
|
1002
996
|
server attribute shim backs the attribute methods), but a `Task`'s
|
|
1003
997
|
fetch still runs only on the client.
|