You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All line anchors in this body were verified against HEAD ddfc5547 ("chore: delete the docs and ui-website redirect-only apps (#1305)"). Corrections to the original statement are called out inline under "Verified, with corrections".
Problem
A fresh git worktree has no seeded blog database, so blog tests fail locally that CI never sees. Nothing in the output names the database as the cause, so each failure reads as a real regression in progressive enhancement, which is exactly the area a form-related or SSR change would be suspected of breaking.
examples/blog/db/dev.db is gitignored and a worktree does not carry it. npm run worktree:link links node_modules and packages/core/dist and touches the database not at all. CI never hits this because every job that boots the blog runs npm run db:migrate && npm run db:seed first.
The failures, and their exact mechanism
An empty posts table produces these, all reproduced (see "Measurements" below):
Test
Anchor at ddfc5547
Failing assertion
dynamic route: /blog/[slug] renders the post title in <head>
test/integration/blog-http.test.mjs:96
assert.ok(href, 'homepage should list at least one /blog/... link') at :99, because / renders no /blog/<slug> anchor
content reads and a display-only component renders with JS off
test/e2e/e2e.test.mjs:3027
the post-content match at :3030 (/Hello, webjs|Zero build steps|Web components first/). The display-only badge assertion at :3033 still passes
a server-rendered form submits and the response renders with JS off
test/e2e/e2e.test.mjs:3054
assert.match(html, /Found 2 results for "web"/) at :3066, because /search?q=web renders Found 0 results for "web"
progressive enhancement (JS disabled) (#183)
test/e2e/e2e.test.mjs:3014
the enclosing describe, which node:test reports as failing because two of its children failed
Verified, with corrections to the original statement
Everything below was re-read at ddfc5547. Four things in the original statement needed correcting.
CI seeds at FOUR sites, not three. The original cited .github/workflows/ci.yml:98, :368, :432. Those three npm run db:migrate lines are correct, and there is a fourth at :483. The full blocks, each named "Prepare the blog example database" with working-directory: examples/blog, are :94-99 (job unit), :364-369 (job e2e), :428-433 (the Bun-served e2e job) and :479-484 (job apps). Each also runs cp .env.example .env first, which is not required for migrating or seeding: examples/blog/drizzle.config.ts:9 and examples/blog/db/connection.server.ts:16 both fall back to db/dev.db when DATABASE_URL is unset. The .env copy is there for the app boot that follows, so the fix below does not need to copy it.
"Four failures" is three tests plus the enclosing suite, not four independent tests. Table above.
The proposed guard "when examples/blog/db/dev.db is missing" is wrong.examples/blog/package.json:20-23 declares webjs.dev.before and :36-39 declares webjs.start.before, both of which run webjs db migrate, and packages/cli/bin/webjs.js:410 (dev) and :453 (start) run those before-steps in the parent process. So booting the blog once in a worktree, which is among the first things an agent does, creates db/dev.db with an emptyposts table. A file-existence guard would then skip forever and the tests would stay red with the fix installed. The guard has to probe the rows.
test/repo-health/link-worktree-deps.test.mjs already exists (159 lines, 8 tests). The original said test/repo-health/ is "where a script-behaviour test would fit if one is warranted", which understates it. Critically, its test at :129 runs the script with cwd: process.cwd(), the real primary checkout, on every npm test. Any seed step that ran before the primary-checkout guard would therefore migrate and seed the developer's live shared database as a side effect of running the test suite.
Confirmed unchanged and correct as stated:
examples/blog/.gitignore:26 is db/dev.db (with :27db/dev.db-journal and :28db/dev.db-*).
examples/blog/package.json:14-15 is "db:migrate": "webjs db migrate" and "db:seed": "webjs db seed".
scripts/link-worktree-deps.mjs is 154 lines and is the whole of npm run worktree:link (root package.json:23).
examples/blog/db/seed.server.ts:15-31 defines exactly three posts, and two of them match the search term "web". examples/blog/app/search/page.ts:23 filters on p.title only, so Hello, webjs and Web components first match while Zero build steps does not. That is where Found 2 results for "web" comes from. The seed is insert-or-skip (onConflictDoNothing on users.email at :40 and on posts.slug at :47), so re-running it never duplicates or overwrites.
Measurements taken
All read-only against the repo. The work was done on a copy of examples/blog in a scratch directory with node_modules symlinked, which mimics a linked worktree. The repo's own examples/blog/db/dev.db was never written to and still holds its 3 posts.
Timing, migrate plus seed on an absent database:npm run db:migrate 1.50s, npm run db:seed 1.01s, 2.5s total. Invoking the CLI directly instead (node node_modules/.bin/webjs db migrate) measured 1.65s and 0.85s, so npm's wrapper overhead is inside the noise and is not a reason to bypass npm run.
Causal chain, confirmed both ways. With posts emptied, an in-process createRequestHandler({ appDir, dev: false }) renders / with no /blog/ anchor at all and /search?q=web as Found 0 results for "web", and the post-title regex /Hello, webjs|Zero build steps|Web components first/ does not match / while zero JS shipped for this badge still does. After npm run db:seed, all three match again and the post count is back to 3. That is the four listed failures, exactly.
Row probe, dependency-free.new DatabaseSync(path, { readOnly: true }) from the built-in node:sqlite returns 3 against the repo's database, throws ERR_SQLITE_ERROR: unable to open database file for a missing file, throws no such table: posts for a zero-byte file, and returns 0 for a migrated-but-unseeded database. All four cases are distinguishable and none of them writes.
Design / approach
Settled decisions
1. The seed step goes at the END of scripts/link-worktree-deps.mjs, after the summary line at :154, guarded on "the blog has no posts".
It must run after the link loops because npm run db:migrate resolves the webjs bin through the root node_modules/.bin this script just linked. It goes after the summary log rather than before it so the linking report stays contiguous and the seed's own npm output follows it rather than interleaving.
The guard is a row count, not file existence, for the reason in correction 3 above: webjs dev and webjs start both create the file with an empty table, so file existence is a proxy that goes wrong on the most common path. The probe is node:sqlite's DatabaseSync opened readOnly, which is built in on the repo's Node 24+ floor (root package.json:17) and is already how examples/blog/db/connection.server.ts:26 opens the database, so it adds no dependency and no new resolution risk.
Prior art settled the shape. Rails' db:prepare (~/Documents/Projects/frameworks/rails/activerecord/lib/active_record/railties/databases.rake:394-397) delegates to DatabaseTasks.prepare_all (~/Documents/Projects/frameworks/rails/activerecord/lib/active_record/tasks/database_tasks.rb:174-206), whose final line is load_seed if seed, where seed was set only when initialize_database reported the database was not already there. So Rails seeds exactly once, on an uninitialized database, and never touches an initialized one. That is the shape adopted here. The probe differs only because in stock Rails nothing but db:prepare creates the database, whereas here webjs dev and webjs start do. ~/Documents/Projects/frameworks/rails/railties/lib/rails/generators/rails/app/templates/bin/setup.tt confirms the placement: a fresh checkout's bootstrap script runs deps, then one idempotent bin/rails db:prepare, and nothing else about the database. npm run worktree:link is that bootstrap script here.
2. Invocation is spawnSync('npm', ['run', '<script>'], { cwd: blogDir, stdio: 'inherit' }), no shell, no .cmd handling.
spawnSync rather than execFileSync because the step must warn and continue, and execFileSync throws on a non-zero exit while spawnSync returns a status to branch on. This matches the repo's existing split: scripts/publish-npm.js:72, scripts/backfill-changelog.js:109 and scripts/run-bun-tests.js:143 use spawnSync where an exit code is inspected, while scripts/link-worktree-deps.mjs:118 and scripts/git-worktree-safe.mjs:38 use execFileSync where a throw is the desired failure mode.
npm run db:migrate rather than resolving the CLI bin, so the local step and all four CI steps stay the literally identical two commands and cannot drift. scripts/run-example-blog-browser-e2e.js:61 already spawns bare 'npm' from a repo script. The measured cost of going through npm is under 200ms across both commands.
No Windows npm.cmd handling. The repo is POSIX-only in practice: every CI job runs on ubuntu-latest, scripts/generate-favicon.mjs:65 shells out to rm, scripts/protect-main.sh is bash, and scripts/run-example-blog-browser-e2e.js:61 already spawns bare npm. The symlinkSync(src, dst, 'junction') at scripts/link-worktree-deps.mjs:108 is the single Windows nod in the file and is not a support commitment. Matching the existing convention beats inventing a platform branch nothing else in the repo has.
3. Failure is WARN-and-continue, exit code stays 0. A worktree cut for framework unit work must still link. The literal stderr text is fixed below and appears verbatim in the implementation plan.
4. Yes, also implement the self-describing fixture failure, once, in a shared fixture.
The seed step only helps an agent who runs worktree:link. An agent who cuts a worktree and goes straight to npm test gets no warning at all, because nothing ran, and that is precisely the reported failure mode (the issue reports baselining against pristine main twice to establish the failures were environmental). So the fixture guard earns its keep.
It is implemented once, in a new test/fixtures/blog-seeded.mjs helper called from both suites' before(), not as per-test preconditions. It reads the rendered homepage for a /blog/<slug> anchor rather than opening SQLite, which keeps it an assertion about the app's observable output, works against a blog served on either runtime, and keeps the remedy string in exactly one place. Throwing from before() fails the block once with a message that names the fix, instead of three cryptic assertion failures.
5. Yes, an opt-out: WEBJS_NO_WORKTREE_SEED=1. It matches the repo's established escape-hatch naming (WEBJS_NO_WORKTREE_GATE, WEBJS_NO_DOC_GATE, WEBJS_NO_WORKTREE_CLEANUP), and it is what makes the counterfactual test cheap to write: set it, run the script, assert the database was not touched.
6. The step does NOT run in the primary checkout.scripts/link-worktree-deps.mjs:127-130 already exits 0 when primary === here, before doing any work. The seed step goes after that guard and inherits it, so npm run worktree:link in the primary stays a pure no-op. This is not a style preference: test/repo-health/link-worktree-deps.test.mjs:129-138 runs the script with cwd: process.cwd() on every npm test, so a seed step placed above that guard would migrate and seed the live shared database every time anyone runs the suite.
Alternatives considered and rejected
Symlink the primary's examples/blog/db/dev.db into the worktree, the way packages/core/dist is linked. Rejected: multiple agents run worktrees concurrently, and the blog e2e suite writes (signup creates users, the comment and feedback flows insert rows), so one shared SQLite file means two agents' test runs contaminate each other's assertions and contend on the same write lock. packages/core/dist is safely shareable because it is read-only build output; a database is not.
Commit dev.db. Rejected, and the original statement already rules it out. A binary SQLite file in git is an unmergeable conflict generator, and db/migrations plus db/seed.server.ts are already the source of truth for its contents.
Seed unconditionally on every link. Rejected: test/repo-health/link-worktree-deps.test.mjs:110-111 asserts a second run reports 0 linked and changes nothing, and paying 2.5s of subprocess on every re-run to accomplish nothing contradicts the script's fast-and-idempotent contract. The row-count guard makes the common re-run path cost one read-only SQLite open.
A root pretest step. Rejected: root package.json:26 already has pretest, and it runs on everynpm test including in the primary checkout, so it would touch the shared database on every test run and add 2.5s to the inner loop for the 99% of runs that do not need it.
A postinstall hook on examples/blog, which is how ~/Documents/Projects/frameworks/next.js/examples/prisma-postgres/package.json:8 bootstraps its ORM. Rejected on its face: worktree:link exists precisely because a fresh worktree never runs npm install, so an install hook is the one place the code can never reach.
Implementation plan
Step 1. scripts/link-worktree-deps.mjs:62, widen the child-process import
Leave the node:fs and node:path imports at :60-61 alone. Do not add a top-level import { DatabaseSync } from 'node:sqlite': it is imported dynamically inside the probe so a run that never reaches the blog step never loads it (and never risks an experimental-module warning on the Node 24 floor).
Step 2. scripts/link-worktree-deps.mjs, add two functions after defaultPrimary() (which ends at :122)
Insert both between defaultPrimary() and the const here = process.cwd(); line currently at :124.
/** * How many rows the blog's `posts` table has, or `null` when the table or the * database file is not there yet. * * Read-only and dependency-free. `node:sqlite` is built in on this repo's Node * 24+ floor, and `examples/blog/db/connection.server.ts` already opens the same * database through it, so this adds nothing to install and nothing to resolve. * * @param {string} dbPath * @returns {Promise<number|null>} */asyncfunctioncountBlogPosts(dbPath){if(!existsSync(dbPath))returnnull;letdb;try{const{ DatabaseSync }=awaitimport('node:sqlite');db=newDatabaseSync(dbPath,{readOnly: true});constrow=/** @type {{ n: number }} */(db.prepare('select count(*) as n from posts').get());returnNumber(row.n);}catch{// A missing `posts` table (a file created by `webjs db migrate` before the// migrations ran, or a half-written one) reads the same as no rows for our// purposes: the blog has nothing to serve.returnnull;}finally{try{db?.close();}catch{/* already closed */}}}/** * Bring `examples/blog`'s SQLite database up to a state the blog tests can use. * * `examples/blog/db/dev.db` is gitignored, so a worktree starts with none and * three blog tests plus their enclosing suite fail on an empty `posts` table * with nothing in the output naming the database (#1323). CI never sees it, * because all four jobs that boot the blog run `db:migrate` + `db:seed` first. * * The guard is "the blog has no posts", NOT "the database file is missing". * `examples/blog/package.json` runs `webjs db migrate` as both a * `webjs.dev.before` and a `webjs.start.before` step, so booting the blog once * creates the file with an empty `posts` table and a file-existence guard would * skip forever. Rails' `db:prepare` has the same shape (seed only an * uninitialized database); only the probe differs, because there nothing but * `db:prepare` creates the file and here two other commands do. * * Never destructive. `webjs db migrate` only applies pending migrations, and * `db/seed.server.ts` is insert-or-skip on `users.email` / `posts.slug`, so a * database that already has rows is left exactly as it was. * * @param {string} blogDir absolute path to this worktree's `examples/blog` * @returns {Promise<void>} */asyncfunctionseedBlogDatabase(blogDir){// The synthetic checkouts in the repo-health tests have no blog, and neither// would a future repo layout that moved it. Nothing to do either way.if(!existsSync(join(blogDir,'package.json')))return;constposts=awaitcountBlogPosts(join(blogDir,'db','dev.db'));if(posts!==null&&posts>0){console.log(`[link-worktree-deps] blog database already has ${posts} posts, leaving it alone.`);return;}console.log('[link-worktree-deps] seeding the blog database (examples/blog)...');for(constscriptof['db:migrate','db:seed']){constr=spawnSync('npm',['run',script],{cwd: blogDir,stdio: 'inherit'});if(r.status===0)continue;constwhy=r.error ? r.error.message : `exit ${r.status??`signal ${r.signal}`}`;console.error(`[link-worktree-deps] WARNING: npm run ${script} failed in examples/blog (${why}).`);console.error('[link-worktree-deps] Linking succeeded. Three blog tests and their enclosing suite will fail until you run, from examples/blog, npm run db:migrate then npm run db:seed.');return;}console.log('[link-worktree-deps] blog database seeded.');}
Those two console.error strings are the literal warning text. Do not reword them without re-checking invariant 11 (they deliberately carry no em-dash, no space-surrounded hyphen or semicolon between words, and no colon on a code-shaped left-hand side).
Step 3. scripts/link-worktree-deps.mjs:154, append the call after the summary line
console.log(`[link-worktree-deps] ${linked} linked, ${skipped} already present.`);// LAST, after the links: `npm run db:migrate` resolves the `webjs` bin through// the root `node_modules/.bin` the loops above just linked, so this cannot run// earlier. It is also below the `primary === here` guard at the top, which is// load-bearing: `test/repo-health/link-worktree-deps.test.mjs` runs this script// against the REAL checkout on every `npm test`, and a seed step above that// guard would migrate and seed the shared database every time the suite ran.if(process.env.WEBJS_NO_WORKTREE_SEED==='1'){console.log('[link-worktree-deps] blog database seeding skipped (WEBJS_NO_WORKTREE_SEED=1).');}else{awaitseedBlogDatabase(join(here,'examples','blog'));}
Top-level await is fine: the file is .mjs and already runs as an ES module.
Step 4. New file test/fixtures/blog-seeded.mjs
test/fixtures/ is where the repo keeps non-test modules that tests import (deny-live-hosts.mjs, install-spec.mjs, jspm-double.mjs), and scripts/run-node-tests.js:32 collects only *.test.js / *.test.mjs, so a helper there is never run as a test.
/** * Precondition guard for the two suites that read real blog rows (#1323). * * `examples/blog/db/dev.db` is gitignored, so a fresh worktree has no seeded * posts and these suites fail on assertions about post links and search * results, none of which name the database. `npm run worktree:link` seeds it * automatically now, so this fires only when the suite ran without that step * (or with WEBJS_NO_WORKTREE_SEED=1), and it replaces three cryptic assertion * failures with one that names the remedy. * * It reads the rendered homepage rather than opening SQLite, so it stays a * statement about the app's observable output and holds for a blog served on * either runtime. * * @param {string} homeHtml the SSR'd HTML of the blog's `/` * @throws {Error} when the homepage lists no post */exportfunctionassertBlogSeeded(homeHtml){if(/<a[^>]+href=["']\/blog\/[^"']+["']/.test(homeHtml))return;thrownewError('The blog database has no posts, so these tests cannot pass. '+'Run `npm run worktree:link` from this worktree, or run `npm run db:migrate` '+'then `npm run db:seed` inside examples/blog.',);}
Step 5. test/integration/blog-http.test.mjs, call the guard in before()
before(async()=>{handler=awaitcreateRequestHandler({appDir: BLOG_DIR,dev: false});if(handler.warmup)awaithandler.warmup();// Fail once, naming the database, instead of leaving the slug test to fail// on a missing anchor in a fresh worktree (#1323).assertBlogSeeded(await(awaitreq('/')).text());});
Also update the file's header comment at :17-19, which currently reads "Needs the blog's seeded SQLite DB (the /api/posts + dynamic-slug cases read real rows): CI's unit job runs db:migrate + db:seed in examples/blog before this, the same setup the e2e job uses." Add one sentence: "Locally npm run worktree:link does the same for a fresh worktree, and before() fails with the remedy if neither ran."
Step 6. test/e2e/e2e.test.mjs, call the guard in before()
Today, :142:
serverProcess=awaitstartBlog(port);
After (import added alongside the existing imports at :24-29):
serverProcess=awaitstartBlog(port);// Before launching Chromium, so an unseeded worktree fails in a second// with the remedy rather than after a browser launch on three assertions// that never mention the database (#1323).assertBlogSeeded(await(awaitfetch(`${baseUrl}/`)).text());
startBlog already resolves only after the server prints ready on (test/e2e/e2e.test.mjs:102-129), so the fetch is safe at that point. Place it before the puppeteer.launch(...) call at :144.
Step 7. Docs, per the Docs section below
Tests
Unit / repo-health (extend the existing file, do not add a sibling)
test/repo-health/link-worktree-deps.test.mjs is the right home and a new sibling file is not warranted. It already owns the makePrimary() / makeWorktree() / run() harness at :24-51, already drives the script as a subprocess against a synthetic checkout pair, and the new behaviour is one more property of the same script. A sibling would duplicate the harness and split the script's contract across two files.
Two changes to the existing helpers first:
makeWorktree() at :41-46 gains an examples/blog directory with a package.json and a db/ directory when the test asks for it. Add a parameter rather than making it unconditional, because the existing eight tests must keep the blog absent so the seed step no-ops and none of them spawns npm. Suggested shape: function makeWorktree({ blog = false } = {}), which writes examples/blog/package.json and creates examples/blog/db when blog is true.
The synthetic blog's package.json gets db:migrate and db:seed scripts that are cheap, deterministic stand-ins rather than the real CLI (for example node -e "require('fs').appendFileSync('db/ran.log','migrate\n')"), so the tests assert the script's orchestration without depending on drizzle-kit, and a failure case is produced by pointing one script at node -e "process.exit(3)".
New assertions, all inside the existing describe('link-worktree-deps (#1287)', ...) at :53:
test('seeds the blog database when the worktree has no posts (#1323)'). Build a worktree with blog: true and no db/dev.db, run the script, assert stdout matches /seeding the blog database/ and that both stand-in scripts ran in order (db/ran.log is migrate\nseed\n).
test('seeds a migrated-but-empty database, not just a missing file (#1323)'). This is the counterfactual for correction 3. Create examples/blog/db/dev.db with a real but empty posts table (new DatabaseSync(path).exec('create table posts (id integer primary key)') from node:sqlite, no dependency), run the script, assert it still seeded. Reverting the row-count guard to a file-existence guard fails exactly this test.
test('leaves a database that already has posts alone (#1323)'). Same setup as 2 but insert one row first. Assert stdout matches /already has 1 posts, leaving it alone/ and that the stand-in scripts did not run (no db/ran.log). This is the counterfactual for "unconditional seeding".
test('warns and still exits 0 when seeding fails (#1323)'). Point db:migrate at a stand-in that exits non-zero. Assert the process exit code is 0, stderr matches /WARNING: npm run db:migrate failed in examples\/blog/, and stderr also carries /npm run db:migrate then npm run db:seed/. run() at :49-51 currently returns stdout only, so add a variant that captures stderr and tolerates a non-zero exit (spawnSync with encoding: 'utf8'), rather than changing run() and disturbing the eight existing tests.
test('WEBJS_NO_WORKTREE_SEED=1 skips the seed step entirely (#1323)'). Run with that env var set on a blog: true worktree with no database. Assert stdout matches /seeding skipped \(WEBJS_NO_WORKTREE_SEED=1\)/ and that no db/dev.db and no db/ran.log were created.
test('never seeds in the primary checkout (#1323)'). This is the load-bearing safety counterfactual. Run the script with the synthetic primary as its own cwd (the shape the existing :140 test uses), with a blog: true layout present, and assert the seed step never ran. Moving the seed call above the primary === here guard at :127-130 fails this test, which is what stops npm test from seeding the developer's live database via the existing :129 test.
Also extend the file's header comment at :1-13 to say the file now also covers the blog-database seeding step and why the primary-checkout case is asserted.
The end-to-end counterfactual (not automatable, state it in the PR description and verify it by hand once): in a fresh worktree, WEBJS_NO_WORKTREE_SEED=1 npm run worktree:link, then npm test -- test/integration/blog-http.test.mjs and WEBJS_E2E=1 npm run test:e2e. The three tests plus the enclosing suite must fail, and each must now fail through assertBlogSeeded with the remedy message rather than on a bare assertion. Then re-run npm run worktree:link without the env var and confirm all four go green with no manual step.
Layers that do NOT apply, and why
Browser (npm run test:browser): not applicable. The change is a Node bootstrap script and a before() precondition in two Node-driven suites. Nothing renders, hydrates, upgrades a custom element, or touches the DOM, so there is no browser-observable behaviour to assert. The blog's browser suite (scripts/run-example-blog-browser-e2e.js) is unaffected because the script's contract for it is unchanged.
E2E (test/e2e/e2e.test.mjs): no new e2e test. The suite is edited (Step 6) but the edit is a precondition guard, not an assertion about the framework. Adding an e2e that asserts "the database is seeded" would just re-assert what the three existing tests already assert.
Smoke (test/examples/*/smoke/*): not applicable. Smoke tests cover a scaffolded app produced by webjs create. Nothing in this change reaches packages/cli/lib/create.js or the templates, and a scaffolded app has neither a worktree-link script nor this repo's blog.
Bun parity (test/bun/**): not applicable, checked against the AGENTS.md runtime-sensitive list rather than skipped. That list is the serializer, the node:http versus Bun.serve listener and request path, SSR / action / CSRF dispatch, streams, node:crypto, the TypeScript stripper, and auth / session / cors. This change touches none of them. It is a repo-development script invoked as node scripts/link-worktree-deps.mjs through root package.json:23, so it never runs under Bun at all, and .claude/hooks/require-bun-parity-with-runtime-src.sh:61 only fires on staged packages/*/src or packages/cli/lib paths, none of which this touches. The one runtime-adjacent call is node:sqlite, and it is used only inside this Node-only script; examples/blog/db/connection.server.ts:21-28 keeps owning the Bun-versus-Node driver split and is not modified.
Postgres (test/pg/**, the db-postgres CI job): not applicable. The seed step targets examples/blog's SQLite dev database specifically. The row probe opens a SQLite file directly, which is correct because that is the only database a worktree is missing; nothing here changes the dialect-agnostic schema, queries, or actions.
Docs
Two surfaces, both monorepo-development documentation. Neither doc-gate hook fires for this change (.claude/hooks/require-docs-with-src.sh:59 and require-tests-with-src.sh:59 both key on packages/*/src or packages/cli/lib, and this change stages only scripts/, test/ and .md paths), so WEBJS_NO_DOC_GATE=1 is neither needed nor appropriate. Update both anyway, because both currently describe worktree:link in terms that will be wrong once it seeds.
1. AGENTS.md:58-63, the fresh-worktree section
AGENTS.md:58 currently reads:
Fix it with npm run worktree:link from inside the worktree (or a full npm install there, which is correct but slow and duplicates a large tree per worktree). Do NOT hand-symlink only the root node_modules. That is the obvious move and it produces a worktree that looks set up and then fails dozens of tests for reasons that point nowhere near the real cause. Two things beyond the root tree are needed, and the script handles both:
Change "Two things beyond the root tree are needed, and the script handles both:" to "Three things beyond the root tree are needed, and the script handles all three:" and add a third bullet after the packages/core/dist bullet at :61:
examples/blog's seeded SQLite database.db/dev.db is gitignored, so a worktree has none, and an empty posts table fails three blog tests plus their enclosing progressive-enhancement suite with nothing in the output naming the database (fix: seed the blog database in a fresh worktree so 4 tests stop failing locally #1323). The script runs the blog's db:migrate then db:seed when the posts table is missing or empty, which takes about two and a half seconds and is a no-op once there are rows. A file-existence check would not do, because webjs dev and webjs start both run webjs db migrate as a before-step and leave the file there with no rows. Seeding failures WARN and never fail the link. Skip it with WEBJS_NO_WORKTREE_SEED=1.
Then AGENTS.md:63 currently reads:
The script discovers the node_modules set from the primary checkout rather than hardcoding a list (it changes whenever a package gains a nested tree), never overwrites an existing path, and never creates a dangling link, so it is safe to re-run and safe in a worktree where you already ran a real npm install.
Append one clause so the never-overwrite claim covers the database too:
The script discovers the node_modules set from the primary checkout rather than hardcoding a list (it changes whenever a package gains a nested tree), never overwrites an existing path, and never creates a dangling link, so it is safe to re-run and safe in a worktree where you already ran a real npm install. The seed step keeps the same contract: it only ever applies pending migrations and inserts demo rows that are not there, so a database with rows in it is left untouched.
2. framework-dev.md, a new section between :96 and :98
Insert a short section immediately before the Merged worktrees are auto-removed heading at :98, so the three worktree mechanics sit together and the escape hatch lives next to WEBJS_NO_WORKTREE_CLEANUP, which is documented at :106. Keep it short and point at AGENTS.md for the linking rules rather than restating them.
Fresh worktree bootstrap seeds the blog database (#1323)
npm run worktree:link (scripts/link-worktree-deps.mjs) links the dependency trees AGENTS.md describes, and as its last step brings examples/blog's SQLite database up to a usable state. db/dev.db is gitignored, so a worktree starts with none, and an empty posts table fails three blog tests plus their enclosing progressive-enhancement suite with nothing in the output naming the cause. CI never sees this because all four jobs that boot the blog run db:migrate + db:seed first.
The guard is the row count, not the file. examples/blog/package.json runs webjs db migrate as a dev.before and a start.before step, so booting the blog once creates the file with an empty table and a file-existence check would skip forever after. The probe is a read-only node:sqlite open, which needs nothing installed. Rails' db:prepare has the same shape (seed an uninitialized database, leave an initialized one alone) and differs only in the probe, because there nothing but db:prepare creates the file.
It costs about two and a half seconds on a cold worktree and nothing once there are rows. It runs below the primary-checkout guard, so worktree:link in the primary stays a no-op and the repo-health test that drives the script against the real checkout cannot seed the shared database. A failure WARNS and leaves the link successful. Escape hatch: WEBJS_NO_WORKTREE_SEED=1. Regression test: test/repo-health/link-worktree-deps.test.mjs.
Surfaces that do NOT apply
The docs site (website/app/docs/**) and the marketing website: no. Nothing here is user-facing. worktree:link exists only in this monorepo and is meaningless to someone building an app with WebJs.
The skill (.agents/skills/webjs/** and packages/cli/templates/.agents/skills/webjs/**): no. The skill teaches how to build a WebJs app. It documents no monorepo-development script, and the scaffold copy would be actively misleading in a scaffolded app, which has no examples/blog.
Scaffold templates and generators (packages/cli/lib/create.js, packages/cli/templates/**): no. webjs create emits no worktree tooling and no blog example, so nothing generated changes.
README.md, CONVENTIONS.md, per-package AGENTS.md: no. This is not a headline capability, not a new app convention, and not a change to any package's public surface.
Acceptance criteria
In a fresh worktree, git worktree add then npm run worktree:link leaves examples/blog/db/dev.db with the three seeded posts, with no manual database step
test/integration/blog-http.test.mjs "dynamic route: /blog/[slug] renders the post title in <head>" passes in that worktree
WEBJS_E2E=1 npm run test:e2e passes the progressive enhancement (JS disabled) (#183) suite in that worktree, including "content reads and a display-only component renders with JS off" and "a server-rendered form submits and the response renders with JS off" with its Found 2 results for "web" assertion
Re-running npm run worktree:link reports the existing post count and leaves dev.db byte-identical, spawning no npm subprocess
A worktree whose db/dev.db exists with an empty posts table (the state webjs dev leaves behind) is still seeded by worktree:link
A seeding failure prints the two WARNING lines to stderr and worktree:link still exits 0 with the links in place
WEBJS_NO_WORKTREE_SEED=1 npm run worktree:link links and does not touch the database
npm run worktree:link in the primary checkout is still a pure no-op, and npm test in the primary does not migrate or seed the shared database
Running the blog suites in an unseeded worktree fails through assertBlogSeeded with the remedy message rather than on a bare assertion
test/repo-health/link-worktree-deps.test.mjs covers all six new cases, and its eight existing tests still pass unchanged
AGENTS.md's fresh-worktree section lists the database as the third thing worktree:link handles, and names WEBJS_NO_WORKTREE_SEED
framework-dev.md carries the new bootstrap section
npm test, npm run test:browser and webjs check are green
Out of scope
Committing examples/blog/db/dev.db, or any part of it, to git.
Copying examples/blog/.env.example to .env from the link script. CI does that for the app boot that follows its seed step, and neither webjs db migrate nor webjs db seed needs it (drizzle.config.ts:9 and db/connection.server.ts:16 both fall back to db/dev.db). If a worktree turns out to need .env for something else, that is a separate observation, not a widening of this change.
Seeding website/. It uses no SQLite database, so there is nothing to seed. Keep the step scoped to examples/blog.
Changing what worktree:link links, the packages/core/dist step, or the node_modules discovery walk. The linking behaviour is correct and its eight tests must keep passing untouched.
Changing examples/blog/db/seed.server.ts, its three posts, or the search behaviour in examples/blog/app/search/page.ts. Found 2 results for "web" is correct as it stands and the fix is about the database being empty, not about what the seed contains.
Adding a Windows npm.cmd branch, or any other platform support the repo does not already have.
Landmines
Open PR feat: resolve form-submitter boundness in webjs check and make the residual loud #1314 (feat/submitter-needs-bound-form) touches AGENTS.md (1 line) and test/e2e/e2e.test.mjs (+39 lines). It does not touch scripts/link-worktree-deps.mjs, test/repo-health/, or examples/blog/db/, so there is no logical conflict, but rebase on origin/main before opening this PR in case it merges first, and re-check the test/e2e/e2e.test.mjs:142 anchor for Step 6 after rebasing.
packages/core/dist is built, not committed. Keep the existing dist link step working, and note that PR feat: make a bound form submitter carry its own submission #1317 hit a related trap where a stale linked dist made an e2e counterfactual pass vacuously. When verifying the end-to-end counterfactual, build dist in the worktree rather than trusting the linked copy.
All line anchors in this body were verified against HEAD
ddfc5547("chore: delete the docs and ui-website redirect-only apps (#1305)"). Corrections to the original statement are called out inline under "Verified, with corrections".Problem
A fresh git worktree has no seeded blog database, so blog tests fail locally that CI never sees. Nothing in the output names the database as the cause, so each failure reads as a real regression in progressive enhancement, which is exactly the area a form-related or SSR change would be suspected of breaking.
examples/blog/db/dev.dbis gitignored and a worktree does not carry it.npm run worktree:linklinksnode_modulesandpackages/core/distand touches the database not at all. CI never hits this because every job that boots the blog runsnpm run db:migrate && npm run db:seedfirst.The failures, and their exact mechanism
An empty
poststable produces these, all reproduced (see "Measurements" below):ddfc5547dynamic route: /blog/[slug] renders the post title in <head>test/integration/blog-http.test.mjs:96assert.ok(href, 'homepage should list at least one /blog/... link')at:99, because/renders no/blog/<slug>anchorcontent reads and a display-only component renders with JS offtest/e2e/e2e.test.mjs:3027:3030(/Hello, webjs|Zero build steps|Web components first/). The display-only badge assertion at:3033still passesa server-rendered form submits and the response renders with JS offtest/e2e/e2e.test.mjs:3054assert.match(html, /Found 2 results for "web"/)at:3066, because/search?q=webrendersFound 0 results for "web"progressive enhancement (JS disabled) (#183)test/e2e/e2e.test.mjs:3014describe, whichnode:testreports as failing because two of its children failedVerified, with corrections to the original statement
Everything below was re-read at
ddfc5547. Four things in the original statement needed correcting.CI seeds at FOUR sites, not three. The original cited
.github/workflows/ci.yml:98,:368,:432. Those threenpm run db:migratelines are correct, and there is a fourth at:483. The full blocks, each named "Prepare the blog example database" withworking-directory: examples/blog, are:94-99(jobunit),:364-369(jobe2e),:428-433(the Bun-served e2e job) and:479-484(jobapps). Each also runscp .env.example .envfirst, which is not required for migrating or seeding:examples/blog/drizzle.config.ts:9andexamples/blog/db/connection.server.ts:16both fall back todb/dev.dbwhenDATABASE_URLis unset. The.envcopy is there for the app boot that follows, so the fix below does not need to copy it."Four failures" is three tests plus the enclosing suite, not four independent tests. Table above.
The proposed guard "when
examples/blog/db/dev.dbis missing" is wrong.examples/blog/package.json:20-23declareswebjs.dev.beforeand:36-39declareswebjs.start.before, both of which runwebjs db migrate, andpackages/cli/bin/webjs.js:410(dev) and:453(start) run those before-steps in the parent process. So booting the blog once in a worktree, which is among the first things an agent does, createsdb/dev.dbwith an emptypoststable. A file-existence guard would then skip forever and the tests would stay red with the fix installed. The guard has to probe the rows.test/repo-health/link-worktree-deps.test.mjsalready exists (159 lines, 8 tests). The original saidtest/repo-health/is "where a script-behaviour test would fit if one is warranted", which understates it. Critically, its test at:129runs the script withcwd: process.cwd(), the real primary checkout, on everynpm test. Any seed step that ran before the primary-checkout guard would therefore migrate and seed the developer's live shared database as a side effect of running the test suite.Confirmed unchanged and correct as stated:
examples/blog/.gitignore:26isdb/dev.db(with:27db/dev.db-journaland:28db/dev.db-*).examples/blog/package.json:14-15is"db:migrate": "webjs db migrate"and"db:seed": "webjs db seed".scripts/link-worktree-deps.mjsis 154 lines and is the whole ofnpm run worktree:link(rootpackage.json:23).examples/blog/db/seed.server.ts:15-31defines exactly three posts, and two of them match the search term "web".examples/blog/app/search/page.ts:23filters onp.titleonly, soHello, webjsandWeb components firstmatch whileZero build stepsdoes not. That is whereFound 2 results for "web"comes from. The seed is insert-or-skip (onConflictDoNothingonusers.emailat:40and onposts.slugat:47), so re-running it never duplicates or overwrites.Measurements taken
All read-only against the repo. The work was done on a copy of
examples/blogin a scratch directory withnode_modulessymlinked, which mimics a linked worktree. The repo's ownexamples/blog/db/dev.dbwas never written to and still holds its 3 posts.npm run db:migrate1.50s,npm run db:seed1.01s, 2.5s total. Invoking the CLI directly instead (node node_modules/.bin/webjs db migrate) measured 1.65s and 0.85s, so npm's wrapper overhead is inside the noise and is not a reason to bypassnpm run.postsemptied, an in-processcreateRequestHandler({ appDir, dev: false })renders/with no/blog/anchor at all and/search?q=webasFound 0 results for "web", and the post-title regex/Hello, webjs|Zero build steps|Web components first/does not match/whilezero JS shipped for this badgestill does. Afternpm run db:seed, all three match again and the post count is back to 3. That is the four listed failures, exactly.new DatabaseSync(path, { readOnly: true })from the built-innode:sqlitereturns 3 against the repo's database, throwsERR_SQLITE_ERROR: unable to open database filefor a missing file, throwsno such table: postsfor a zero-byte file, and returns 0 for a migrated-but-unseeded database. All four cases are distinguishable and none of them writes.Design / approach
Settled decisions
1. The seed step goes at the END of
scripts/link-worktree-deps.mjs, after the summary line at:154, guarded on "the blog has no posts".It must run after the link loops because
npm run db:migrateresolves thewebjsbin through the rootnode_modules/.binthis script just linked. It goes after the summary log rather than before it so the linking report stays contiguous and the seed's own npm output follows it rather than interleaving.The guard is a row count, not file existence, for the reason in correction 3 above:
webjs devandwebjs startboth create the file with an empty table, so file existence is a proxy that goes wrong on the most common path. The probe isnode:sqlite'sDatabaseSyncopenedreadOnly, which is built in on the repo's Node 24+ floor (rootpackage.json:17) and is already howexamples/blog/db/connection.server.ts:26opens the database, so it adds no dependency and no new resolution risk.Prior art settled the shape. Rails'
db:prepare(~/Documents/Projects/frameworks/rails/activerecord/lib/active_record/railties/databases.rake:394-397) delegates toDatabaseTasks.prepare_all(~/Documents/Projects/frameworks/rails/activerecord/lib/active_record/tasks/database_tasks.rb:174-206), whose final line isload_seed if seed, whereseedwas set only wheninitialize_databasereported the database was not already there. So Rails seeds exactly once, on an uninitialized database, and never touches an initialized one. That is the shape adopted here. The probe differs only because in stock Rails nothing butdb:preparecreates the database, whereas herewebjs devandwebjs startdo.~/Documents/Projects/frameworks/rails/railties/lib/rails/generators/rails/app/templates/bin/setup.ttconfirms the placement: a fresh checkout's bootstrap script runs deps, then one idempotentbin/rails db:prepare, and nothing else about the database.npm run worktree:linkis that bootstrap script here.2. Invocation is
spawnSync('npm', ['run', '<script>'], { cwd: blogDir, stdio: 'inherit' }), no shell, no.cmdhandling.spawnSyncrather thanexecFileSyncbecause the step must warn and continue, andexecFileSyncthrows on a non-zero exit whilespawnSyncreturns astatusto branch on. This matches the repo's existing split:scripts/publish-npm.js:72,scripts/backfill-changelog.js:109andscripts/run-bun-tests.js:143usespawnSyncwhere an exit code is inspected, whilescripts/link-worktree-deps.mjs:118andscripts/git-worktree-safe.mjs:38useexecFileSyncwhere a throw is the desired failure mode.npm run db:migraterather than resolving the CLI bin, so the local step and all four CI steps stay the literally identical two commands and cannot drift.scripts/run-example-blog-browser-e2e.js:61already spawns bare'npm'from a repo script. The measured cost of going through npm is under 200ms across both commands.No Windows
npm.cmdhandling. The repo is POSIX-only in practice: every CI job runs onubuntu-latest,scripts/generate-favicon.mjs:65shells out torm,scripts/protect-main.shis bash, andscripts/run-example-blog-browser-e2e.js:61already spawns barenpm. ThesymlinkSync(src, dst, 'junction')atscripts/link-worktree-deps.mjs:108is the single Windows nod in the file and is not a support commitment. Matching the existing convention beats inventing a platform branch nothing else in the repo has.3. Failure is WARN-and-continue, exit code stays 0. A worktree cut for framework unit work must still link. The literal stderr text is fixed below and appears verbatim in the implementation plan.
4. Yes, also implement the self-describing fixture failure, once, in a shared fixture.
The seed step only helps an agent who runs
worktree:link. An agent who cuts a worktree and goes straight tonpm testgets no warning at all, because nothing ran, and that is precisely the reported failure mode (the issue reports baselining against pristine main twice to establish the failures were environmental). So the fixture guard earns its keep.It is implemented once, in a new
test/fixtures/blog-seeded.mjshelper called from both suites'before(), not as per-test preconditions. It reads the rendered homepage for a/blog/<slug>anchor rather than opening SQLite, which keeps it an assertion about the app's observable output, works against a blog served on either runtime, and keeps the remedy string in exactly one place. Throwing frombefore()fails the block once with a message that names the fix, instead of three cryptic assertion failures.5. Yes, an opt-out:
WEBJS_NO_WORKTREE_SEED=1. It matches the repo's established escape-hatch naming (WEBJS_NO_WORKTREE_GATE,WEBJS_NO_DOC_GATE,WEBJS_NO_WORKTREE_CLEANUP), and it is what makes the counterfactual test cheap to write: set it, run the script, assert the database was not touched.6. The step does NOT run in the primary checkout.
scripts/link-worktree-deps.mjs:127-130already exits 0 whenprimary === here, before doing any work. The seed step goes after that guard and inherits it, sonpm run worktree:linkin the primary stays a pure no-op. This is not a style preference:test/repo-health/link-worktree-deps.test.mjs:129-138runs the script withcwd: process.cwd()on everynpm test, so a seed step placed above that guard would migrate and seed the live shared database every time anyone runs the suite.Alternatives considered and rejected
examples/blog/db/dev.dbinto the worktree, the waypackages/core/distis linked. Rejected: multiple agents run worktrees concurrently, and the blog e2e suite writes (signup creates users, the comment and feedback flows insert rows), so one shared SQLite file means two agents' test runs contaminate each other's assertions and contend on the same write lock.packages/core/distis safely shareable because it is read-only build output; a database is not.dev.db. Rejected, and the original statement already rules it out. A binary SQLite file in git is an unmergeable conflict generator, anddb/migrationsplusdb/seed.server.tsare already the source of truth for its contents.test/repo-health/link-worktree-deps.test.mjs:110-111asserts a second run reports0 linkedand changes nothing, and paying 2.5s of subprocess on every re-run to accomplish nothing contradicts the script's fast-and-idempotent contract. The row-count guard makes the common re-run path cost one read-only SQLite open.preteststep. Rejected: rootpackage.json:26already haspretest, and it runs on everynpm testincluding in the primary checkout, so it would touch the shared database on every test run and add 2.5s to the inner loop for the 99% of runs that do not need it.postinstallhook onexamples/blog, which is how~/Documents/Projects/frameworks/next.js/examples/prisma-postgres/package.json:8bootstraps its ORM. Rejected on its face:worktree:linkexists precisely because a fresh worktree never runsnpm install, so an install hook is the one place the code can never reach.Implementation plan
Step 1.
scripts/link-worktree-deps.mjs:62, widen the child-process importToday:
After:
Leave the
node:fsandnode:pathimports at:60-61alone. Do not add a top-levelimport { DatabaseSync } from 'node:sqlite': it is imported dynamically inside the probe so a run that never reaches the blog step never loads it (and never risks an experimental-module warning on the Node 24 floor).Step 2.
scripts/link-worktree-deps.mjs, add two functions afterdefaultPrimary()(which ends at:122)Insert both between
defaultPrimary()and theconst here = process.cwd();line currently at:124.Those two
console.errorstrings are the literal warning text. Do not reword them without re-checking invariant 11 (they deliberately carry no em-dash, no space-surrounded hyphen or semicolon between words, and no colon on a code-shaped left-hand side).Step 3.
scripts/link-worktree-deps.mjs:154, append the call after the summary lineToday the file ends with:
After:
Top-level
awaitis fine: the file is.mjsand already runs as an ES module.Step 4. New file
test/fixtures/blog-seeded.mjstest/fixtures/is where the repo keeps non-test modules that tests import (deny-live-hosts.mjs,install-spec.mjs,jspm-double.mjs), andscripts/run-node-tests.js:32collects only*.test.js/*.test.mjs, so a helper there is never run as a test.Step 5.
test/integration/blog-http.test.mjs, call the guard inbefore()Today,
:51-54:After (add the import alongside the existing ones at
:24-28):Also update the file's header comment at
:17-19, which currently reads "Needs the blog's seeded SQLite DB (the/api/posts+ dynamic-slug cases read real rows): CI'sunitjob runsdb:migrate+db:seedin examples/blog before this, the same setup the e2e job uses." Add one sentence: "Locallynpm run worktree:linkdoes the same for a fresh worktree, andbefore()fails with the remedy if neither ran."Step 6.
test/e2e/e2e.test.mjs, call the guard inbefore()Today,
:142:After (import added alongside the existing imports at
:24-29):startBlogalready resolves only after the server printsready on(test/e2e/e2e.test.mjs:102-129), so the fetch is safe at that point. Place it before thepuppeteer.launch(...)call at:144.Step 7. Docs, per the Docs section below
Tests
Unit / repo-health (extend the existing file, do not add a sibling)
test/repo-health/link-worktree-deps.test.mjsis the right home and a new sibling file is not warranted. It already owns themakePrimary()/makeWorktree()/run()harness at:24-51, already drives the script as a subprocess against a synthetic checkout pair, and the new behaviour is one more property of the same script. A sibling would duplicate the harness and split the script's contract across two files.Two changes to the existing helpers first:
makeWorktree()at:41-46gains anexamples/blogdirectory with apackage.jsonand adb/directory when the test asks for it. Add a parameter rather than making it unconditional, because the existing eight tests must keep the blog absent so the seed step no-ops and none of them spawns npm. Suggested shape:function makeWorktree({ blog = false } = {}), which writesexamples/blog/package.jsonand createsexamples/blog/dbwhenblogis true.package.jsongetsdb:migrateanddb:seedscripts that are cheap, deterministic stand-ins rather than the real CLI (for examplenode -e "require('fs').appendFileSync('db/ran.log','migrate\n')"), so the tests assert the script's orchestration without depending on drizzle-kit, and a failure case is produced by pointing one script atnode -e "process.exit(3)".New assertions, all inside the existing
describe('link-worktree-deps (#1287)', ...)at:53:test('seeds the blog database when the worktree has no posts (#1323)'). Build a worktree withblog: trueand nodb/dev.db, run the script, assert stdout matches/seeding the blog database/and that both stand-in scripts ran in order (db/ran.logismigrate\nseed\n).test('seeds a migrated-but-empty database, not just a missing file (#1323)'). This is the counterfactual for correction 3. Createexamples/blog/db/dev.dbwith a real but emptypoststable (new DatabaseSync(path).exec('create table posts (id integer primary key)')fromnode:sqlite, no dependency), run the script, assert it still seeded. Reverting the row-count guard to a file-existence guard fails exactly this test.test('leaves a database that already has posts alone (#1323)'). Same setup as 2 but insert one row first. Assert stdout matches/already has 1 posts, leaving it alone/and that the stand-in scripts did not run (nodb/ran.log). This is the counterfactual for "unconditional seeding".test('warns and still exits 0 when seeding fails (#1323)'). Pointdb:migrateat a stand-in that exits non-zero. Assert the process exit code is 0, stderr matches/WARNING: npm run db:migrate failed in examples\/blog/, and stderr also carries/npm run db:migrate then npm run db:seed/.run()at:49-51currently returns stdout only, so add a variant that captures stderr and tolerates a non-zero exit (spawnSyncwithencoding: 'utf8'), rather than changingrun()and disturbing the eight existing tests.test('WEBJS_NO_WORKTREE_SEED=1 skips the seed step entirely (#1323)'). Run with that env var set on ablog: trueworktree with no database. Assert stdout matches/seeding skipped \(WEBJS_NO_WORKTREE_SEED=1\)/and that nodb/dev.dband nodb/ran.logwere created.test('never seeds in the primary checkout (#1323)'). This is the load-bearing safety counterfactual. Run the script with the synthetic primary as its own cwd (the shape the existing:140test uses), with ablog: truelayout present, and assert the seed step never ran. Moving the seed call above theprimary === hereguard at:127-130fails this test, which is what stopsnpm testfrom seeding the developer's live database via the existing:129test.Also extend the file's header comment at
:1-13to say the file now also covers the blog-database seeding step and why the primary-checkout case is asserted.The end-to-end counterfactual (not automatable, state it in the PR description and verify it by hand once): in a fresh worktree,
WEBJS_NO_WORKTREE_SEED=1 npm run worktree:link, thennpm test -- test/integration/blog-http.test.mjsandWEBJS_E2E=1 npm run test:e2e. The three tests plus the enclosing suite must fail, and each must now fail throughassertBlogSeededwith the remedy message rather than on a bare assertion. Then re-runnpm run worktree:linkwithout the env var and confirm all four go green with no manual step.Layers that do NOT apply, and why
npm run test:browser): not applicable. The change is a Node bootstrap script and abefore()precondition in two Node-driven suites. Nothing renders, hydrates, upgrades a custom element, or touches the DOM, so there is no browser-observable behaviour to assert. The blog's browser suite (scripts/run-example-blog-browser-e2e.js) is unaffected because the script's contract for it is unchanged.test/e2e/e2e.test.mjs): no new e2e test. The suite is edited (Step 6) but the edit is a precondition guard, not an assertion about the framework. Adding an e2e that asserts "the database is seeded" would just re-assert what the three existing tests already assert.test/examples/*/smoke/*): not applicable. Smoke tests cover a scaffolded app produced bywebjs create. Nothing in this change reachespackages/cli/lib/create.jsor the templates, and a scaffolded app has neither a worktree-link script nor this repo's blog.test/bun/**): not applicable, checked against the AGENTS.md runtime-sensitive list rather than skipped. That list is the serializer, thenode:httpversusBun.servelistener and request path, SSR / action / CSRF dispatch, streams,node:crypto, the TypeScript stripper, and auth / session / cors. This change touches none of them. It is a repo-development script invoked asnode scripts/link-worktree-deps.mjsthrough rootpackage.json:23, so it never runs under Bun at all, and.claude/hooks/require-bun-parity-with-runtime-src.sh:61only fires on stagedpackages/*/srcorpackages/cli/libpaths, none of which this touches. The one runtime-adjacent call isnode:sqlite, and it is used only inside this Node-only script;examples/blog/db/connection.server.ts:21-28keeps owning the Bun-versus-Node driver split and is not modified.test/pg/**, thedb-postgresCI job): not applicable. The seed step targetsexamples/blog's SQLite dev database specifically. The row probe opens a SQLite file directly, which is correct because that is the only database a worktree is missing; nothing here changes the dialect-agnostic schema, queries, or actions.Docs
Two surfaces, both monorepo-development documentation. Neither doc-gate hook fires for this change (
.claude/hooks/require-docs-with-src.sh:59andrequire-tests-with-src.sh:59both key onpackages/*/srcorpackages/cli/lib, and this change stages onlyscripts/,test/and.mdpaths), soWEBJS_NO_DOC_GATE=1is neither needed nor appropriate. Update both anyway, because both currently describeworktree:linkin terms that will be wrong once it seeds.1.
AGENTS.md:58-63, the fresh-worktree sectionAGENTS.md:58currently reads:Change "Two things beyond the root tree are needed, and the script handles both:" to "Three things beyond the root tree are needed, and the script handles all three:" and add a third bullet after the
packages/core/distbullet at:61:Then
AGENTS.md:63currently reads:Append one clause so the never-overwrite claim covers the database too:
2.
framework-dev.md, a new section between:96and:98Insert a short section immediately before the
Merged worktrees are auto-removedheading at:98, so the three worktree mechanics sit together and the escape hatch lives next toWEBJS_NO_WORKTREE_CLEANUP, which is documented at:106. Keep it short and point at AGENTS.md for the linking rules rather than restating them.Surfaces that do NOT apply
website/app/docs/**) and the marketing website: no. Nothing here is user-facing.worktree:linkexists only in this monorepo and is meaningless to someone building an app with WebJs..agents/skills/webjs/**andpackages/cli/templates/.agents/skills/webjs/**): no. The skill teaches how to build a WebJs app. It documents no monorepo-development script, and the scaffold copy would be actively misleading in a scaffolded app, which has noexamples/blog.packages/cli/lib/create.js,packages/cli/templates/**): no.webjs createemits no worktree tooling and no blog example, so nothing generated changes.README.md,CONVENTIONS.md, per-packageAGENTS.md: no. This is not a headline capability, not a new app convention, and not a change to any package's public surface.Acceptance criteria
git worktree addthennpm run worktree:linkleavesexamples/blog/db/dev.dbwith the three seeded posts, with no manual database steptest/integration/blog-http.test.mjs"dynamic route: /blog/[slug] renders the post title in<head>" passes in that worktreeWEBJS_E2E=1 npm run test:e2epasses theprogressive enhancement (JS disabled) (#183)suite in that worktree, including "content reads and a display-only component renders with JS off" and "a server-rendered form submits and the response renders with JS off" with itsFound 2 results for "web"assertionnpm run worktree:linkreports the existing post count and leavesdev.dbbyte-identical, spawning no npm subprocessdb/dev.dbexists with an emptypoststable (the statewebjs devleaves behind) is still seeded byworktree:linkWARNINGlines to stderr andworktree:linkstill exits 0 with the links in placeWEBJS_NO_WORKTREE_SEED=1 npm run worktree:linklinks and does not touch the databasenpm run worktree:linkin the primary checkout is still a pure no-op, andnpm testin the primary does not migrate or seed the shared databaseassertBlogSeededwith the remedy message rather than on a bare assertiontest/repo-health/link-worktree-deps.test.mjscovers all six new cases, and its eight existing tests still pass unchangedAGENTS.md's fresh-worktree section lists the database as the third thingworktree:linkhandles, and namesWEBJS_NO_WORKTREE_SEEDframework-dev.mdcarries the new bootstrap sectionnpm test,npm run test:browserandwebjs checkare greenOut of scope
examples/blog/db/dev.db, or any part of it, to git.examples/blog/.env.exampleto.envfrom the link script. CI does that for the app boot that follows its seed step, and neitherwebjs db migratenorwebjs db seedneeds it (drizzle.config.ts:9anddb/connection.server.ts:16both fall back todb/dev.db). If a worktree turns out to need.envfor something else, that is a separate observation, not a widening of this change.website/. It uses no SQLite database, so there is nothing to seed. Keep the step scoped toexamples/blog.worktree:linklinks, thepackages/core/diststep, or thenode_modulesdiscovery walk. The linking behaviour is correct and its eight tests must keep passing untouched.examples/blog/db/seed.server.ts, its three posts, or the search behaviour inexamples/blog/app/search/page.ts.Found 2 results for "web"is correct as it stands and the fix is about the database being empty, not about what the seed contains.webjs doctororwebjs devabout the unseeded database. Their fresh-worktree remedy messages are about resolving@webjsdev/*(dogfood: a fresh git worktree can't resolve @webjsdev/* (no node_modules) #954) and are correct for a scaffolded app, which has no blog.npm.cmdbranch, or any other platform support the repo does not already have.Landmines
feat/submitter-needs-bound-form) touchesAGENTS.md(1 line) andtest/e2e/e2e.test.mjs(+39 lines). It does not touchscripts/link-worktree-deps.mjs,test/repo-health/, orexamples/blog/db/, so there is no logical conflict, but rebase onorigin/mainbefore opening this PR in case it merges first, and re-check thetest/e2e/e2e.test.mjs:142anchor for Step 6 after rebasing.describe-level line anchors intest/e2e/e2e.test.mjs(:3014,:3027,:3054,:3066) shift if feat: resolve form-submitter boundness in webjs check and make the residual loud #1314 lands first. They are quoted here for identification, not for editing. Step 6 is the only edit to that file and it is near the top.packages/core/distis built, not committed. Keep the existing dist link step working, and note that PR feat: make a bound form submitter carry its own submission #1317 hit a related trap where a stale linkeddistmade an e2e counterfactual pass vacuously. When verifying the end-to-end counterfactual, builddistin the worktree rather than trusting the linked copy.