Skip to content

fix: seed the blog database in a fresh worktree so 4 tests stop failing locally #1323

Description

@vivek7405

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.

  1. 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.

  2. "Four failures" is three tests plus the enclosing suite, not four independent tests. Table above.

  3. 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 empty posts 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.

  4. 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 :27 db/dev.db-journal and :28 db/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 every npm 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

Today:

import { execFileSync } from 'node:child_process';

After:

import { execFileSync, spawnSync } from 'node:child_process';

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>}
 */
async function countBlogPosts(dbPath) {
  if (!existsSync(dbPath)) return null;
  let db;
  try {
    const { DatabaseSync } = await import('node:sqlite');
    db = new DatabaseSync(dbPath, { readOnly: true });
    const row = /** @type {{ n: number }} */ (db.prepare('select count(*) as n from posts').get());
    return Number(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.
    return null;
  } 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>}
 */
async function seedBlogDatabase(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;

  const posts = await countBlogPosts(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 (const script of ['db:migrate', 'db:seed']) {
    const r = spawnSync('npm', ['run', script], { cwd: blogDir, stdio: 'inherit' });
    if (r.status === 0) continue;
    const why = 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

Today the file ends with:

console.log(`[link-worktree-deps] ${linked} linked, ${skipped} already present.`);

After:

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 {
  await seedBlogDatabase(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
 */
export function assertBlogSeeded(homeHtml) {
  if (/<a[^>]+href=["']\/blog\/[^"']+["']/.test(homeHtml)) return;
  throw new Error(
    '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()

Today, :51-54:

  before(async () => {
    handler = await createRequestHandler({ appDir: BLOG_DIR, dev: false });
    if (handler.warmup) await handler.warmup();
  });

After (add the import alongside the existing ones at :24-28):

import { assertBlogSeeded } from '../fixtures/blog-seeded.mjs';
  before(async () => {
    handler = await createRequestHandler({ appDir: BLOG_DIR, dev: false });
    if (handler.warmup) await handler.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 (await req('/')).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 = await startBlog(port);

After (import added alongside the existing imports at :24-29):

import { assertBlogSeeded } from '../fixtures/blog-seeded.mjs';
    serverProcess = await startBlog(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 (await fetch(`${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:

  1. 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).
  2. 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.
  3. 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".
  4. 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.
  5. 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.
  6. 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.
  • Teaching webjs doctor or webjs dev about 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.
  • Adding a Windows npm.cmd branch, or any other platform support the repo does not already have.

Landmines

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions