Skip to content

Type-check test files: every app tsconfig excludes test/ #1299

Description

@vivek7405

All line anchors in this issue were verified at HEAD e5806e24 (docs: correct the CI route named in seven bun test wrappers (#1345)).

Problem

Test files are outside every app's tsconfig.json include, so webjs typecheck never reads them and a type error in a test is invisible until a human notices it.

website/tsconfig.json:16 includes app/**/*, components/**/*, lib/**/*, modules/**/* and nothing else, while 25 .ts files (and 9 .js browser tests) live under website/test/. npm run typecheck in website/ therefore reports success on a tree where a test file could be arbitrarily broken.

This is not hypothetical. During review of #1259 (PR #1295) a helper was written const quoted = (key) => ... in website/test/ssr/docs-links.test.ts, an implicitly-any parameter in a .ts file. Root AGENTS.md says plainly that "any has no carve-out at all", strict: true is set in that same tsconfig, and every sibling arrow in the file is annotated. Nothing caught it. It survived until a reviewer read the line by eye, and it would have merged otherwise.

The gap is systemic, not specific to website/. It also reaches the scaffold, which is the row that matters most: every app webjs create generates ships this gap, so an app author who follows the scaffold's own testing guidance gets no type checking on the tests they write, including the three starter tests the scaffold itself writes into test/hello/.

Corrections to the original statement (verified at HEAD)

The original body listed five configs. Two of them no longer exist, and one line anchor moved. The real surface is three rows.

Config include at HEAD Status
website/tsconfig.json:16 app/**/*, components/**/*, lib/**/*, modules/**/* Exists, as stated
examples/blog/tsconfig.json:28-35 app/**/*, components/**/*, modules/**/*, lib/**/*, middleware.js, middleware.ts Exists, as stated
docs/tsconfig.json n/a GONE. Deleted in ddfc5547 (chore: delete the docs and ui-website redirect-only apps (#1305)). docs/ now holds only node_modules/ and public/, and git ls-files docs returns zero tracked files.
packages/ui/packages/website/tsconfig.json n/a GONE. Same commit ddfc5547. The directory is empty.
packages/cli/lib/create.js:550-558 app/**/*, components/**/*, modules/**/*, lib/**/*, middleware.js, middleware.ts, .webjs/routes.d.ts Exists, but the anchor is L550-558, not L537-543. L537-543 at HEAD is the middle of the @webjsdev/intellisense comment block above plugins.

Two further claims in the original body are wrong at HEAD and must not be carried forward.

  • "website/test/fixtures/ may hold deliberately-malformed files. Check before including it wholesale; it may need an exclude entry." It holds exactly one file, website/test/fixtures/code-block-markup.js, a well-formed .js fixture. website sets checkJs: false, so it is parsed and not checked, and including test/**/* adds zero errors from it. No exclude entry is needed.
  • "The app configs set allowJs: true, checkJs: false." That is true of website only. examples/blog/tsconfig.json:14-15 sets checkJs: true, allowJs: true, and the scaffold at packages/cli/lib/create.js:511-543 sets neither, so a generated app never reads a .js file at all. The three configs disagree, and the checkJs question therefore has a different answer per row (settled below).

Design / approach

Settled: one config per app, not a second tsconfig.test.json

Decision: extend each app's single include. Nothing in the measured error inventory forces a split.

The evidence, not an assumption. The complete inventory below shows 17 errors in website/test/ and 0 in examples/blog/test/. Every one of the 17 is an ordinary type error in test code (a missing property, a never inference, an unknown assignment). Not one of them is a setting conflict. Specifically:

  • No test needs a types addition. Both configs already carry "types": ["node"] (website/tsconfig.json:8, examples/blog/tsconfig.json:12), which is what node:test / node:assert / node:fs need, and the measurement confirms zero unresolved-builtin errors.
  • No test needs looser strict. Every error has a correct strict-mode fix, spelled out per file below.
  • No test needs a different lib. The browser tests are .js and are unaffected either way.

A tsconfig.test.json extending the base would also reproduce this exact bug in a new place: a second config is a second thing to forget to run, and the whole failure here is a checker nobody ran.

Prior art, all three read at ~/Documents/Projects/frameworks/. Every mainstream generator ships one tsconfig with a whole-tree include and no test carve-out:

  • next.js/packages/create-next-app/templates/default/ts/tsconfig.json includes "**/*.ts", "**/*.tsx", "**/*.mts". Test files are in, by construction, with no second config.
  • remix-v2/templates/remix/tsconfig.json includes "**/*.ts", "**/*.tsx" plus the .server / .client variants. One config.
  • astro/examples/basics/tsconfig.json includes ".astro/types.d.ts", "**/*". One config.

Rejected alternative 1: a second tsconfig.test.json. No setting in the inventory requires it, and it recreates the "config nobody runs" failure mode. Rejected on evidence, not on taste.

Rejected alternative 2: switch to a whole-tree "**/*.ts" include, matching Next / Remix / Astro literally. It is tempting and it is the industry norm, but the WebJs app configs are deliberately directory-scoped and carry matching exclude lists (examples/blog/tsconfig.json:36-40 excludes db/migrations; the scaffold at packages/cli/lib/create.js:559 excludes .webjs/vendor). A whole-tree include would additionally pull in website/scripts/, generated output, and drizzle migration artifacts, turning a targeted fix into an unbounded one. Adding one test/**/* entry is the smallest change that closes the actual gap.

Rejected alternative 3: put the rationale in a tsconfig comment, as the original body suggested ("say which in the config comment"). Overturned on evidence. These files are strict JSON, not JSONC: test/scaffolds/scaffold-integration.test.js:361 reads the generated config with JSON.parse, which throws on //, and packages/cli/lib/create.js:510 emits it with JSON.stringify, which cannot emit a comment at all. The rationale goes in website/AGENTS.md and the skill instead.

Settled: the zero-test-app question is moot

The original body asked whether apps with zero test .ts files should still get the include change for consistency. Re-verified counts at HEAD:

App test .ts test .js
website/test 25 9
examples/blog/test 7 0
docs directory deleted (#1305) n/a
packages/ui/packages/website directory deleted (#1305) n/a

There is no zero-test app left to decide about. Both surviving apps have real test .ts files, and both get the change. The generated scaffold likewise ships three starter tests (packages/cli/lib/create.js:599-601 copies test/hello/hello.test.ts, test/hello/browser/hello.test.js, test/hello/e2e/hello.test.ts), so its include names a directory that exists at generation time and the TS18003 no-inputs hazard does not arise. Even if it did not, app/**/* already matches many files, and TS raises TS18003 only when the whole include resolves to nothing.

Settled: checkJs per row

  • website keeps checkJs: false. Its 9 browser tests under test/components/browser/ are .js by convention (web-test-runner serves them to a real browser). Including test/**/* pulls them in as parsed-but-unchecked and the measurement confirms they add zero errors. Leaving them unchecked is the status quo for every other .js in that app, so this change does not widen into them.
  • examples/blog keeps checkJs: true. It has zero .js files of its own under its include, so the flag only reaches the framework source through the workspace symlink, which is where its 3 packages/core/src/websocket-client.js errors come from. Those are genuine JSDoc gaps and are fixed in this PR rather than hidden by flipping the flag. Flipping checkJs to false would be a silent loosening dressed up as a fix.
  • The scaffold sets neither flag and stays that way. A generated app's tsconfig has no allowJs, so test/hello/browser/hello.test.js is ignored by tsc with no extra configuration.

Settled: the LayoutProps cluster gets a shared helper, not 13 annotations

13 of the 17 website errors are the same shape at 13 call sites, all of them RootLayout({ children: html\...` })missingparams, searchParams, and url`.

LayoutProps is defined at packages/core/src/routes.d.ts:136-139 and extends PageProps (packages/core/src/routes.d.ts:112-128). The three missing fields are required and correct: packages/server/src/ssr.js really does pass all four to a layout, and params / searchParams are Awaitable<T> = T & PromiseLike<T> (packages/core/src/routes.d.ts:93) because of the #848 Next 15/16 sync-plus-await parity.

Do NOT change LayoutProps to make the tests easier. Making params / searchParams / url optional would weaken a public type that every WebJs app depends on, in exchange for a test's convenience, and it would stop catching a real layout that forgets to accept them. This is explicitly out of scope.

The fix is one shared helper at website/test/helpers/layout-props.ts, imported by the three test files as #test/helpers/layout-props.ts (the app's package.json imports is the {"#*": "./*"} catch-all, so this resolves with no config change, and it matches the #app/layout.ts spelling those files already use). The runner only executes *.test.{js,ts,mjs,mts} (packages/cli/bin/webjs.js:651), so a plain .ts module in test/helpers/ is never run as a test.

This exact helper was written and compiled clean against website/tsconfig.json with test/**/* included, with no as cast anywhere:

import type { LayoutProps, TemplateResult } from '@webjsdev/core';

/**
 * Rebuild the thenable the server wraps `params` / `searchParams` in (#848),
 * so a test-built props object satisfies `LayoutProps` without a cast.
 */
function awaitable<T extends object>(value: T): T & PromiseLike<T> {
  return Object.assign(value, {
    then<A = T, B = never>(
      onfulfilled?: ((v: T) => A | PromiseLike<A>) | null,
      onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
    ): PromiseLike<A | B> {
      return Promise.resolve({ ...value }).then(onfulfilled, onrejected);
    },
  });
}

export function layoutProps(
  children: TemplateResult,
  overrides: Partial<LayoutProps> = {},
): LayoutProps {
  return {
    children,
    params: awaitable<Record<string, string>>({}),
    searchParams: awaitable<Record<string, string | string[]>>({}),
    url: 'https://webjs.dev/',
    ...overrides,
  };
}

Two notes the implementer needs. Awaitable is not re-exported from @webjsdev/core (packages/core/index.d.ts:34-42 exports Route, RouteParams, PageProps, LayoutProps, RouteHandlerContext, WebjsRoutes, RouteParamMap, and nothing else), which is why the helper derives the shape from LayoutProps rather than naming Awaitable directly. And packages/server/src/thenable-params.js exports makeThenable, but @webjsdev/server's exports map (., ./check, ./testing) does not expose it, so the helper rebuilds the six-line thenable rather than reaching into an unexported path.

Every call site then becomes RootLayout(layoutProps(html\

x`))`, which is one edit per site with no repetition of the three field names.

Implementation plan

The implementer files NO follow-up issues for anything this work turns up, and fixes every type error in the inventory below inside this same PR. That explicitly includes all 17 website test errors AND all 13 pre-existing examples/blog errors, not just the website ones. If a finding is genuinely out of reach of one PR, it is written as a note in the PR description and reported to the user, never filed as a new issue.

Step 0. Rebase on #1262 first

Issue #1262 (fix(website): docs search indexes shell comments as headings, open) is being planned in parallel and will add a new test file under website/test/. It is a different file, so git merges cleanly, but once this PR's include change lands, that new file must type-check.

#1262 lands first. This PR rebases onto it, then re-runs the website measurement in Step 1 and adds any error from #1262's new file to the inventory and to the fixes. If #1262 has not landed by the time this PR is ready, say so in the PR description and coordinate before merging, because whichever merges second inherits the other's type errors.

Step 1. Re-measure, per app

Reproduce the inventory before changing anything, so the fixes are aimed at the current tree rather than at this issue's snapshot. Non-destructive probe, leaves nothing behind:

# website
cd website && node scripts/copy-registry.mjs
node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('tsconfig.json','utf8'));c.include.push('test/**/*');fs.writeFileSync('tsconfig.probe.json',JSON.stringify(c,null,2));"
../node_modules/.bin/tsc -p tsconfig.probe.json --noEmit; rm -f tsconfig.probe.json

# examples/blog
cd ../examples/blog
../../node_modules/.bin/tsc -p tsconfig.json --noEmit            # baseline, currently 13 errors
node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('tsconfig.json','utf8'));c.include.push('test/**/*');fs.writeFileSync('tsconfig.probe.json',JSON.stringify(c,null,2));"
../../node_modules/.bin/tsc -p tsconfig.probe.json --noEmit; rm -f tsconfig.probe.json

node scripts/copy-registry.mjs is mandatory for the website. modules/ui/components/ is a gitignored mirror, and without it tsc reports 45 unrelated errors that bury the real ones (website/AGENTS.md:415-420).

Step 2. Complete error inventory (measured at HEAD e5806e24)

2a. Adding test/**/* to website surfaces 17 errors, across 5 of its 25 .ts files

File Line:Col Code Error Fix
test/ssr/docs-sitemap.test.ts 35:24 TS2345 Argument of type '"page.ts"' is not assignable to parameter of type 'never' L33 readdir(...).catch(() => []) widens to string[] | never[], so includes resolves its parameter to never. Annotate the catch: .catch((): string[] => [])
test/ssr/docs-sitemap.test.ts 35:53 TS2345 Argument of type '"page.js"' is not assignable to parameter of type 'never' Same line, same fix
test/ssr/layout-ssr.test.ts 23:47 TS2345 { children: TemplateResult } missing params, searchParams, url from LayoutProps<string> layoutProps(...) helper
test/ssr/layout-ssr.test.ts 33:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 43:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 62:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 69:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 79:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 94:47 TS2345 same helper
test/ssr/layout-ssr.test.ts 159:49 TS2345 same helper
test/ssr/layout-ssr.test.ts 169:47 TS2345 same helper
test/ssr/nav-and-routes.test.ts 30:54 TS2345 same, inside the renderLayout arrow at L30 helper
test/ssr/render-determinism.test.ts 47:53 TS2322 Type 'unknown' is not assignable to type 'TemplateResult' PAGES is declared Array<[string, () => unknown]> at L38. Every page default export returns a synchronous html result (app/page.ts:130, app/what-is-webjs/page.ts:223, app/why-webjs/page.ts:98, app/docs/getting-started/page.ts:5), so retype it Array<[string, () => TemplateResult]> and delete the two as any casts on L47-48, which exist only to paper over the unknown
test/ssr/render-determinism.test.ts 48:54 TS2322 same same
test/ssr/seo-infra.test.ts 40:47 TS2345 LayoutProps missing three fields helper
test/ssr/seo-infra.test.ts 59:47 TS2345 same helper
test/ssr/seo-infra.test.ts 69:47 TS2345 same helper

Cluster totals: 13 LayoutProps (TS2345), 2 never (TS2345), 2 unknown (TS2322). The remaining 20 .ts files and all 9 .js files under website/test/ are clean.

render-determinism.test.ts is the file that most directly proves the issue's premise. AGENTS.md says "unknown surviving into a return type, a component prop, a layout's children, or an action signature is the shape to fix". That is literally this error, sitting unnoticed with an as any next to it.

2b. Adding test/**/* to examples/blog surfaces 0 new errors

All 7 .ts files under examples/blog/test/ type-check clean today. Verified by diffing the baseline run against the probe run: the outputs are byte-identical.

2c. examples/blog/ pre-existing baseline: 13 errors, all fixed in this PR

These are red today, with no config change at all, which is why examples/blog has no typecheck script and no CI typecheck step. .github/workflows/ci.yml:544-547 records the reason verbatim: "examples/blog is genuinely red today, so adding it is a small separate change of its own." This is that change.

File Line:Col Code Error Fix
app/api/chat/route.ts 5:15 TS2595 'WebSocket' can only be imported by using a default import @types/ws@7.4.7 uses export = WebSocket. Change import type { WebSocket } from 'ws' to import type WebSocket from 'ws'. Not import type WebSocket = require('ws'), which is import = require and banned by invariant 10. Verified: the default form compiles clean under this exact tsconfig
app/api/chat/route.ts 20:21 TS7006 Parameter 'data' implicitly has an 'any' type Resolves itself once L5 uses the default import. Verified: with import type WebSocket from 'ws', sock.on('message', (data) => data.toString()) infers data with no annotation
app/api/comments/[postId]/route.ts 1:15 TS2595 same ws import same default-import fix
modules/chat/utils/clients.ts 5:15 TS2595 same ws import same default-import fix
db/columns.server.ts 26:29 TS2552 Cannot find name 'text'. Did you mean 'Text'? A real runtime bug, not a type nit. L17 is export { text, integer, real, blob } from 'drizzle-orm/sqlite-core', a re-export, which creates no local binding in ESM. So uuidPk() at L26 calls an undefined text and throws ReferenceError at runtime. Fix: add text (and real, blob if kept) to the value import at L12, then make L17 a local re-export export { text, integer, real, blob };. This is exactly what the scaffold already does correctly at packages/cli/lib/create.js:711-716, so examples/blog has drifted from its own generator
db/columns.server.ts 29:27 TS2552 same, uuid() at L29 same fix
db/connection.server.ts 22:39 TS2307 Cannot find module 'bun:sqlite' Add the same commented suppression the scaffold already ships at packages/cli/lib/create.js:825, namely // @ts-expect-error bun:sqlite is a Bun builtin with no Node typings, immediately above the dynamic import. This is not a blanket silencing: it is a genuinely absent Bun typing, narrowly scoped to one line, matching in-repo prior art
modules/chat/components/chat-box.ts 100:87 TS7006 Parameter 'e' implicitly has an 'any' type The @submit=${(e) => this.onSubmit(e)} hole at L100. Annotate (e: SubmitEvent) and match onSubmit's declared parameter
modules/comments/utils/bus.ts 12:25 TS7017 Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature Add a declare global { var __commentSubs: Map<number, Set<Subscriber>> | undefined; } block above L12. declare is erasable, so invariant 10 holds. Verified to compile clean
modules/comments/utils/bus.ts 12:54 TS7017 same (the assignment half of the ??= idiom) same fix
../../packages/core/src/websocket-client.js 85:10 TS7006 Parameter 'data' implicitly has an 'any' type The returned send(data) method. Add JSDoc @param {string | ArrayBuffer | ArrayBufferView | object} data
../../packages/core/src/websocket-client.js 94:11 TS7006 Parameter 'code' implicitly has an 'any' type The returned close(code, reason) method. Add JSDoc @param {number} [code]
../../packages/core/src/websocket-client.js 94:17 TS7006 Parameter 'reason' implicitly has an 'any' type Add JSDoc @param {string} [reason]

On the last three: they surface because examples/blog sets checkJs: true and @webjsdev/core is a workspace symlink, so tsc follows the realpath to packages/core/src/*.js, which is not under a node_modules path and therefore does get checked. Fix the JSDoc rather than flipping checkJs. They are three real annotation gaps in framework source, the public shape in packages/core/index.d.ts already declares the intended types so nothing user-facing changes, and flipping the flag would be a loosening disguised as a fix. This is a JSDoc-only edit to packages/*/src, so the commit needs WEBJS_NO_DOC_GATE=1 (no behaviour change, no public surface change) and WEBJS_BUN_VERIFIED=1 (JSDoc comments strip to nothing, so zero runtime bytes change).

Step 3. Edit the configs

  1. website/tsconfig.json:16. Append "test/**/*" to include.
  2. examples/blog/tsconfig.json:28-35. Append "test/**/*" to include.
  3. packages/cli/lib/create.js:550-558. Append 'test/**/*', to the emitted include array. Place it after 'lib/**/*', and before the middleware entries, so the generated JSON reads in the same order as the two in-repo apps. packages/cli/ is plain .js with JSDoc and stays .js.

Also update the comment block at packages/cli/lib/create.js:544-549, which currently explains only the .webjs/routes.d.ts entry, to say why test/**/* is in (the tests an app author writes are type-checked by the same one command, matching Next / Remix / Astro).

Step 4. Fix the scaffold's own e2e template, which the include change breaks

This is the landmine most likely to be missed. packages/cli/templates/test/hello/e2e/hello.test.ts:50 does puppeteer = (await import('puppeteer-core')).default;, and puppeteer-core is not in the scaffold's devDependencies (packages/cli/lib/create.js:429-449); its own header comment at L18-22 says so explicitly ("puppeteer-core is an optional dev dependency"). Once test/**/* is in the generated include, that import is TS2307 and a freshly generated app fails webjs typecheck on day one.

It will not reproduce inside this monorepo, because puppeteer-core is present at the repo-root node_modules/, so an in-repo probe passes while a real generated app fails. Verify this only in a generated app outside the repo tree.

Fix: add the same commented suppression the scaffold already uses for bun:sqlite, immediately above the dynamic import:

// @ts-expect-error puppeteer-core is an optional dev dependency; install it to run e2e.
puppeteer = (await import('puppeteer-core')).default;

Do not add puppeteer-core to the scaffold's devDependencies to dodge this. The template is deliberately written to work without it (test/hello/e2e/hello.test.ts:51 logs "Skipping: puppeteer-core not installed" and returns), and installing it for every generated app to satisfy tsc inverts that design.

While there, re-read the whole of packages/cli/templates/test/hello/hello.test.ts and packages/cli/templates/test/hello/e2e/hello.test.ts against the generated config. Both look clean otherwise (they already hand-declare structural Page / Browser types precisely to avoid any), but they have never been type-checked, so read them rather than assuming.

Step 5. Apply the website test fixes

  1. Create website/test/helpers/layout-props.ts with the helper exactly as given in Design / approach.
  2. website/test/ssr/layout-ssr.test.ts. Replace all 9 RootLayout({ children: X }) with RootLayout(layoutProps(X)) and import the helper from #test/helpers/layout-props.ts.
  3. website/test/ssr/seo-infra.test.ts. Same, 3 sites (L40, L59, L69).
  4. website/test/ssr/nav-and-routes.test.ts. Same, 1 site (the renderLayout arrow at L30).
  5. website/test/ssr/render-determinism.test.ts. Retype PAGES at L38 to Array<[string, () => TemplateResult]>, import TemplateResult type-only from @webjsdev/core, and delete both as any casts at L47-48.
  6. website/test/ssr/docs-sitemap.test.ts:33. Annotate the catch as .catch((): string[] => []).

No blanket any, no blanket @ts-expect-error. The only two suppressions this PR adds are the two named above (bun:sqlite, puppeteer-core), each one line, each with a reason, each matching existing in-repo prior art.

Step 6. Apply the examples/blog fixes

All 13 from table 2c.

Step 7. Wire examples/blog into the gate

examples/blog/package.json has no typecheck script (its scripts are dev, start, css:build, db:generate, db:migrate, db:seed, test), which is why nothing ever ran tsc there.

  1. Add "typecheck": "webjs typecheck" to examples/blog/package.json scripts, matching what the scaffold generates at packages/cli/lib/create.js:400.
  2. Add a blog typecheck step to the apps job in .github/workflows/ci.yml, next to the existing website typecheck step at L547-548:
    - name: blog typecheck
      run: npm run typecheck --workspace=@webjsdev/example-blog
  3. Rewrite the comment at .github/workflows/ci.yml:538-546, which is now factually wrong in two ways: it says the gate covers "NOT test/", and it says "examples/blog is genuinely red today".

Step 8. Add the scaffold assertion

test/scaffolds/scaffold-integration.test.js:360-366 already parses the generated tsconfig.json and asserts on compilerOptions.plugins and compilerOptions.types. Add an include assertion in the same block:

assert.ok(tsconfig.include.includes('test/**/*'),
  'generated tsconfig type-checks the app test directory (#1299)');

Tests

This is a config change whose proof is tsc itself, plus a generated-app check the diff cannot give.

Commands, per app

# website: must exit 0
cd website && npm run typecheck

# examples/blog: must exit 0 (currently non-zero even before this change)
cd examples/blog && npm run typecheck

# the two app suites must still pass (the LayoutProps helper changes real call sites)
cd website && npm test
cd website && npm run test:browser
cd examples/blog && npm test

# the scaffold assertion
node --test test/scaffolds/scaffold-integration.test.js

# framework suites, because packages/core/src/websocket-client.js is touched
npm test

Run npm run typecheck and never a bare tsc in website/. The pretypecheck half of that script (node scripts/copy-registry.mjs) mirrors in the gitignored modules/ui/components/ sources; without it tsc emits 45 phantom errors (website/AGENTS.md:415-420).

Generated-app verification (mandatory, and it must happen outside the repo tree)

Generators emit strings, so a shape or escaping bug shows only in a freshly generated app. Do this in a temp directory outside the monorepo, so the root node_modules/ cannot mask the missing puppeteer-core:

cd $(mktemp -d)
node <path-to-repo>/packages/cli/bin/webjs.js create probe-app
cd probe-app && npm install
npx webjs typecheck   # must exit 0, and must be reading test/hello/*.ts

Confirm it is genuinely reading the test directory, not silently skipping it, by breaking a starter test on purpose and re-running (see the counterfactual below).

Counterfactual

Two halves. Both are required, and neither is optional because a config change that does nothing looks exactly like a config change that works.

Half 1, the checker fires. In website/test/ssr/docs-links.test.ts, reintroduce the #1259 defect verbatim: change an annotated arrow to const quoted = (key) => .... cd website && npm run typecheck must fail with TS7006 Parameter 'key' implicitly has an 'any' type. Annotate it back and confirm it passes. Repeat the same in the generated probe-app against test/hello/hello.test.ts.

Half 2, the revert proves the gate. Remove the "test/**/*" line from website/tsconfig.json, leave the deliberately broken test file in place, and re-run npm run typecheck. It must report success on the broken tree. That is the exact bug this issue describes, and seeing it pass green is the proof that the added line is what closes it. Restore both afterwards.

CI jobs

There is exactly one typecheck job today: the apps job (In-repo app tests (website + blog)) at .github/workflows/ci.yml:508, whose website typecheck step is at L547-548. examples/blog has no typecheck step, which Step 7 adds. No other workflow runs tsc over an app.

Docs

Every surface below either shows an app include array or states what webjs typecheck covers. All must be updated.

Path What to change
website/AGENTS.md:422-434 The most wrong surface after this change. It says "test/ is deliberately outside it", enumerates the 17 errors, says "Fixing those is its own task", and says "examples/blog is genuinely red (13 errors), so gating it is a small, separate change". Rewrite: the gate now covers test/ too, both apps are gated, and record the checkJs: false decision that leaves the 9 .js browser tests parsed-but-unchecked (the tsconfig is strict JSON and cannot carry that comment itself)
.github/workflows/ci.yml:538-546 The step comment repeats the same two now-false claims. Rewrite alongside the new blog typecheck step
.agents/skills/webjs/references/typescript.md:56-77 The "Minimum tsconfig.json" block shows compilerOptions with no include at all. Add the include array including test/**/*, so an app author copying it gets type-checked tests
.agents/skills/webjs/references/testing.md Add a short statement that webjs typecheck reads test/ in a scaffolded app, so a test-side type error is a gate failure rather than a review catch. This is the skill's testing reference and it currently says nothing about it (its only typecheck mention, at L154, is about visual defects)
website/app/docs/configuration/page.ts:91 Docs-site include snippet, currently ["app/**/*", "components/**/*", "modules/**/*", "lib/**/*"]. Add test/**/*. While there, this snippet has also drifted from what the scaffold emits (it lacks middleware.js / middleware.ts / .webjs/routes.d.ts and the erasableSyntaxOnly + plugins entries); bring it in line with packages/cli/lib/create.js:510-559
website/app/docs/typescript/page.ts:36-43 Second docs-site include snippet. Add test/**/*. It is closer to the real scaffold but still lacks middleware.js and .webjs/routes.d.ts
packages/cli/lib/create.js:544-549 The generator's own comment above include, currently explaining only .webjs/routes.d.ts. Extend it to cover test/**/*
examples/blog/AGENTS.md Gains a typecheck script and a CI gate. If it documents the app's commands, add npm run typecheck

Note on the scaffold copy of the skill: it is single-source. packages/cli/lib/create.js:665-674 copies the canonical repo-root .agents/skills/webjs/ (falling back from a prepack bundle that is gitignored in the monorepo), so packages/cli/templates/.agents/skills/webjs/ does not exist at HEAD and there is exactly one copy of typescript.md / testing.md to edit.

Invoke the webjs-doc-sync skill to confirm no surface in the table is missed.

Acceptance criteria

  • website/tsconfig.json:16 include contains "test/**/*", and cd website && npm run typecheck exits 0
  • examples/blog/tsconfig.json include contains "test/**/*"
  • examples/blog/package.json has a typecheck script, and cd examples/blog && npm run typecheck exits 0 (it exits non-zero at HEAD, both with and without the include change)
  • All 17 website test errors from table 2a are fixed, none via a blanket any or blanket @ts-expect-error
  • All 13 examples/blog errors from table 2c are fixed, including the two db/columns.server.ts text references, which are a live ReferenceError and not merely a type nit
  • website/test/helpers/layout-props.ts exists, compiles with no as cast, and is used by all 13 LayoutProps call sites across layout-ssr.test.ts, seo-infra.test.ts, and nav-and-routes.test.ts
  • packages/core/src/routes.d.ts is unchanged; LayoutProps was not weakened to accommodate a test
  • The two as any casts at website/test/ssr/render-determinism.test.ts:47-48 are deleted, not preserved
  • packages/cli/lib/create.js:550-558 emits an include containing 'test/**/*'
  • packages/cli/templates/test/hello/e2e/hello.test.ts carries the @ts-expect-error for the optional puppeteer-core import, and puppeteer-core was NOT added to the scaffold's devDependencies
  • A freshly generated app, created and installed outside this repo tree, passes npx webjs typecheck with the test directory included
  • test/scaffolds/scaffold-integration.test.js asserts the generated include contains test/**/*
  • .github/workflows/ci.yml apps job runs a blog typecheck step alongside website typecheck, and the stale comment at L538-546 is rewritten
  • Counterfactual half 1: an implicitly-any parameter added to a website test file, and to a generated app's test/hello/hello.test.ts, makes typecheck fail, and it passes once annotated
  • Counterfactual half 2: reverting the "test/**/*" line makes typecheck report success on that same broken tree
  • cd website && npm test, npm run test:browser, cd examples/blog && npm test, and the root npm test all pass
  • Every doc surface in the Docs table is updated, in particular website/AGENTS.md:422-434 and .github/workflows/ci.yml:538-546, which both assert the opposite of the new behaviour
  • No follow-up issues were filed. Anything out of reach of this PR is a note in the PR description instead

Out of scope

These are true non-goals, not deferred work. Everything the inventory names is in scope and gets fixed in this PR.

  • Changing LayoutProps or PageProps in packages/core/src/routes.d.ts. Making params / searchParams / url optional would weaken a public type every WebJs app depends on, to save 13 test call sites a helper already handles.
  • Turning on checkJs for website, or turning it off for examples/blog. Each app's flag is settled above with a reason. Flipping either is a separate decision with its own blast radius.
  • Switching any config to a whole-tree "**/*.ts" include, Next / Remix / Astro style. Rejected with reasons in Design / approach.
  • Adding a tsconfig.test.json anywhere. Rejected with reasons in Design / approach.
  • Type-checking the .js browser tests under website/test/components/browser/. They enter the include as parsed-but-unchecked, exactly as every other .js in that app already is. Checking them means a checkJs decision, which is the previous bullet.
  • Resurrecting docs/ or packages/ui/packages/website/. Both were deliberately deleted in ddfc5547 (chore: delete the docs and ui-website redirect-only apps #1305).
  • Broader type hardening of website or examples/blog app source beyond the errors tsc actually reports. Do not go hunting for any in files that compile.
  • Adding a bundler, a build step, or a second compiler pass. WebJs is buildless (AGENTS.md, "Deliberately deferred").

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions