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
{{ message }}
Repository navigation
Type-check test files: every app tsconfig excludes test/ #1299
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.jsoninclude, 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.
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.
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:
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.jsonimports 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:
importtype{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. */functionawaitable<Textendsobject>(value: T): T&PromiseLike<T>{returnObject.assign(value,{then<A=T,B=never>(onfulfilled?: ((v: T)=>A|PromiseLike<A>)|null,onrejected?: ((reason: unknown)=>B|PromiseLike<B>)|null,): PromiseLike<A|B>{returnPromise.resolve({ ...value}).then(onfulfilled,onrejected);},});}exportfunctionlayoutProps(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.
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:
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'. Notimport 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
website/tsconfig.json:16. Append "test/**/*" to include.
examples/blog/tsconfig.json:28-35. Append "test/**/*" to include.
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=(awaitimport('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
Create website/test/helpers/layout-props.ts with the helper exactly as given in Design / approach.
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.
website/test/ssr/nav-and-routes.test.ts. Same, 1 site (the renderLayout arrow at L30).
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.
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 notypecheck script (its scripts are dev, start, css:build, db:generate, db:migrate, db:seed, test), which is why nothing ever ran tsc there.
Add "typecheck": "webjs typecheck" to examples/blog/package.json scripts, matching what the scaffold generates at packages/cli/lib/create.js:400.
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 typecheckrun: npm run typecheck --workspace=@webjsdev/example-blog
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 0cd 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 testcd 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
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:16include contains "test/**/*", and cd website && npm run typecheck exits 0
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.tstext 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.ymlapps 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.
Problem
Test files are outside every app's
tsconfig.jsoninclude, sowebjs typechecknever reads them and a type error in a test is invisible until a human notices it.website/tsconfig.json:16includesapp/**/*,components/**/*,lib/**/*,modules/**/*and nothing else, while 25.tsfiles (and 9.jsbrowser tests) live underwebsite/test/.npm run typecheckinwebsite/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) => ...inwebsite/test/ssr/docs-links.test.ts, an implicitly-anyparameter in a.tsfile. RootAGENTS.mdsays plainly that "anyhas no carve-out at all",strict: trueis 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 appwebjs creategenerates 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 intotest/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.
includeat HEADwebsite/tsconfig.json:16app/**/*,components/**/*,lib/**/*,modules/**/*examples/blog/tsconfig.json:28-35app/**/*,components/**/*,modules/**/*,lib/**/*,middleware.js,middleware.tsdocs/tsconfig.jsonddfc5547(chore: delete the docs and ui-website redirect-only apps (#1305)).docs/now holds onlynode_modules/andpublic/, andgit ls-files docsreturns zero tracked files.packages/ui/packages/website/tsconfig.jsonddfc5547. The directory is empty.packages/cli/lib/create.js:550-558app/**/*,components/**/*,modules/**/*,lib/**/*,middleware.js,middleware.ts,.webjs/routes.d.ts@webjsdev/intellisensecomment block aboveplugins.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 anexcludeentry." It holds exactly one file,website/test/fixtures/code-block-markup.js, a well-formed.jsfixture.websitesetscheckJs: false, so it is parsed and not checked, and includingtest/**/*adds zero errors from it. Noexcludeentry is needed.allowJs: true, checkJs: false." That is true ofwebsiteonly.examples/blog/tsconfig.json:14-15setscheckJs: true, allowJs: true, and the scaffold atpackages/cli/lib/create.js:511-543sets neither, so a generated app never reads a.jsfile at all. The three configs disagree, and thecheckJsquestion therefore has a different answer per row (settled below).Design / approach
Settled: one config per app, not a second
tsconfig.test.jsonDecision: 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 inexamples/blog/test/. Every one of the 17 is an ordinary type error in test code (a missing property, aneverinference, anunknownassignment). Not one of them is a setting conflict. Specifically:typesaddition. Both configs already carry"types": ["node"](website/tsconfig.json:8,examples/blog/tsconfig.json:12), which is whatnode:test/node:assert/node:fsneed, and the measurement confirms zero unresolved-builtin errors.strict. Every error has a correct strict-mode fix, spelled out per file below.lib. The browser tests are.jsand are unaffected either way.A
tsconfig.test.jsonextending 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.jsonincludes"**/*.ts","**/*.tsx","**/*.mts". Test files are in, by construction, with no second config.remix-v2/templates/remix/tsconfig.jsonincludes"**/*.ts","**/*.tsx"plus the.server/.clientvariants. One config.astro/examples/basics/tsconfig.jsonincludes".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 matchingexcludelists (examples/blog/tsconfig.json:36-40excludesdb/migrations; the scaffold atpackages/cli/lib/create.js:559excludes.webjs/vendor). A whole-tree include would additionally pull inwebsite/scripts/, generated output, and drizzle migration artifacts, turning a targeted fix into an unbounded one. Adding onetest/**/*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:361reads the generated config withJSON.parse, which throws on//, andpackages/cli/lib/create.js:510emits it withJSON.stringify, which cannot emit a comment at all. The rationale goes inwebsite/AGENTS.mdand the skill instead.Settled: the zero-test-app question is moot
The original body asked whether apps with zero test
.tsfiles should still get theincludechange for consistency. Re-verified counts at HEAD:.ts.jswebsite/testexamples/blog/testdocspackages/ui/packages/websiteThere is no zero-test app left to decide about. Both surviving apps have real test
.tsfiles, and both get the change. The generated scaffold likewise ships three starter tests (packages/cli/lib/create.js:599-601copiestest/hello/hello.test.ts,test/hello/browser/hello.test.js,test/hello/e2e/hello.test.ts), so itsincludenames a directory that exists at generation time and theTS18003 no-inputshazard 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:
checkJsper rowwebsitekeepscheckJs: false. Its 9 browser tests undertest/components/browser/are.jsby convention (web-test-runner serves them to a real browser). Includingtest/**/*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.jsin that app, so this change does not widen into them.examples/blogkeepscheckJs: true. It has zero.jsfiles of its own under its include, so the flag only reaches the framework source through the workspace symlink, which is where its 3packages/core/src/websocket-client.jserrors come from. Those are genuine JSDoc gaps and are fixed in this PR rather than hidden by flipping the flag. FlippingcheckJstofalsewould be a silent loosening dressed up as a fix.allowJs, sotest/hello/browser/hello.test.jsis ignored bytscwith no extra configuration.Settled: the
LayoutPropscluster gets a shared helper, not 13 annotations13 of the 17 website errors are the same shape at 13 call sites, all of them
RootLayout({ children: html\...` })missingparams,searchParams, andurl`.LayoutPropsis defined atpackages/core/src/routes.d.ts:136-139and extendsPageProps(packages/core/src/routes.d.ts:112-128). The three missing fields are required and correct:packages/server/src/ssr.jsreally does pass all four to a layout, andparams/searchParamsareAwaitable<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
LayoutPropsto make the tests easier. Makingparams/searchParams/urloptional 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'spackage.jsonimportsis the{"#*": "./*"}catch-all, so this resolves with no config change, and it matches the#app/layout.tsspelling those files already use). The runner only executes*.test.{js,ts,mjs,mts}(packages/cli/bin/webjs.js:651), so a plain.tsmodule intest/helpers/is never run as a test.This exact helper was written and compiled clean against
website/tsconfig.jsonwithtest/**/*included, with noascast anywhere:Two notes the implementer needs.
Awaitableis not re-exported from@webjsdev/core(packages/core/index.d.ts:34-42exportsRoute,RouteParams,PageProps,LayoutProps,RouteHandlerContext,WebjsRoutes,RouteParamMap, and nothing else), which is why the helper derives the shape fromLayoutPropsrather than namingAwaitabledirectly. Andpackages/server/src/thenable-params.jsexportsmakeThenable, but@webjsdev/server'sexportsmap (.,./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
x`))`, which is one edit per site with no repetition of the three field names.RootLayout(layoutProps(html\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/blogerrors, 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 underwebsite/test/. It is a different file, so git merges cleanly, but once this PR'sincludechange 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:
node scripts/copy-registry.mjsis mandatory for the website.modules/ui/components/is a gitignored mirror, and without ittscreports 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/**/*towebsitesurfaces 17 errors, across 5 of its 25.tsfilestest/ssr/docs-sitemap.test.tsArgument of type '"page.ts"' is not assignable to parameter of type 'never'readdir(...).catch(() => [])widens tostring[] | never[], soincludesresolves its parameter tonever. Annotate the catch:.catch((): string[] => [])test/ssr/docs-sitemap.test.tsArgument of type '"page.js"' is not assignable to parameter of type 'never'test/ssr/layout-ssr.test.ts{ children: TemplateResult }missingparams,searchParams,urlfromLayoutProps<string>layoutProps(...)helpertest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/layout-ssr.test.tstest/ssr/nav-and-routes.test.tsrenderLayoutarrow at L30test/ssr/render-determinism.test.tsType 'unknown' is not assignable to type 'TemplateResult'PAGESis declaredArray<[string, () => unknown]>at L38. Every page default export returns a synchronoushtmlresult (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 itArray<[string, () => TemplateResult]>and delete the twoas anycasts on L47-48, which exist only to paper over theunknowntest/ssr/render-determinism.test.tstest/ssr/seo-infra.test.tsLayoutPropsmissing three fieldstest/ssr/seo-infra.test.tstest/ssr/seo-infra.test.tsCluster totals: 13
LayoutProps(TS2345), 2never(TS2345), 2unknown(TS2322). The remaining 20.tsfiles and all 9.jsfiles underwebsite/test/are clean.render-determinism.test.tsis the file that most directly proves the issue's premise.AGENTS.mdsays "unknownsurviving into a return type, a component prop, a layout'schildren, or an action signature is the shape to fix". That is literally this error, sitting unnoticed with anas anynext to it.2b. Adding
test/**/*toexamples/blogsurfaces 0 new errorsAll 7
.tsfiles underexamples/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 PRThese are red today, with no config change at all, which is why
examples/bloghas notypecheckscript and no CI typecheck step..github/workflows/ci.yml:544-547records the reason verbatim: "examples/blog is genuinely red today, so adding it is a small separate change of its own." This is that change.app/api/chat/route.ts'WebSocket' can only be imported by using a default import@types/ws@7.4.7usesexport = WebSocket. Changeimport type { WebSocket } from 'ws'toimport type WebSocket from 'ws'. Notimport type WebSocket = require('ws'), which isimport = requireand banned by invariant 10. Verified: the default form compiles clean under this exact tsconfigapp/api/chat/route.tsParameter 'data' implicitly has an 'any' typeimport type WebSocket from 'ws',sock.on('message', (data) => data.toString())infersdatawith no annotationapp/api/comments/[postId]/route.tswsimportmodules/chat/utils/clients.tswsimportdb/columns.server.tsCannot find name 'text'. Did you mean 'Text'?export { text, integer, real, blob } from 'drizzle-orm/sqlite-core', a re-export, which creates no local binding in ESM. SouuidPk()at L26 calls an undefinedtextand throwsReferenceErrorat runtime. Fix: addtext(andreal,blobif kept) to the value import at L12, then make L17 a local re-exportexport { text, integer, real, blob };. This is exactly what the scaffold already does correctly atpackages/cli/lib/create.js:711-716, soexamples/bloghas drifted from its own generatordb/columns.server.tsuuid()at L29db/connection.server.tsCannot find module 'bun:sqlite'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 artmodules/chat/components/chat-box.tsParameter 'e' implicitly has an 'any' type@submit=${(e) => this.onSubmit(e)}hole at L100. Annotate(e: SubmitEvent)and matchonSubmit's declared parametermodules/comments/utils/bus.tsElement implicitly has an 'any' type because type 'typeof globalThis' has no index signaturedeclare global { var __commentSubs: Map<number, Set<Subscriber>> | undefined; }block above L12.declareis erasable, so invariant 10 holds. Verified to compile cleanmodules/comments/utils/bus.ts??=idiom)../../packages/core/src/websocket-client.jsParameter 'data' implicitly has an 'any' typesend(data)method. Add JSDoc@param {string | ArrayBuffer | ArrayBufferView | object} data../../packages/core/src/websocket-client.jsParameter 'code' implicitly has an 'any' typeclose(code, reason)method. Add JSDoc@param {number} [code]../../packages/core/src/websocket-client.jsParameter 'reason' implicitly has an 'any' type@param {string} [reason]On the last three: they surface because
examples/blogsetscheckJs: trueand@webjsdev/coreis a workspace symlink, sotscfollows the realpath topackages/core/src/*.js, which is not under anode_modulespath and therefore does get checked. Fix the JSDoc rather than flippingcheckJs. They are three real annotation gaps in framework source, the public shape inpackages/core/index.d.tsalready 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 topackages/*/src, so the commit needsWEBJS_NO_DOC_GATE=1(no behaviour change, no public surface change) andWEBJS_BUN_VERIFIED=1(JSDoc comments strip to nothing, so zero runtime bytes change).Step 3. Edit the configs
website/tsconfig.json:16. Append"test/**/*"toinclude.examples/blog/tsconfig.json:28-35. Append"test/**/*"toinclude.packages/cli/lib/create.js:550-558. Append'test/**/*',to the emittedincludearray. Place it after'lib/**/*',and before themiddlewareentries, so the generated JSON reads in the same order as the two in-repo apps.packages/cli/is plain.jswith 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.tsentry, to say whytest/**/*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:50doespuppeteer = (await import('puppeteer-core')).default;, andpuppeteer-coreis not in the scaffold'sdevDependencies(packages/cli/lib/create.js:429-449); its own header comment at L18-22 says so explicitly ("puppeteer-core is an optional dev dependency"). Oncetest/**/*is in the generatedinclude, that import isTS2307and a freshly generated app failswebjs typecheckon day one.It will not reproduce inside this monorepo, because
puppeteer-coreis present at the repo-rootnode_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:Do not add
puppeteer-coreto the scaffold'sdevDependenciesto dodge this. The template is deliberately written to work without it (test/hello/e2e/hello.test.ts:51logs "Skipping: puppeteer-core not installed" and returns), and installing it for every generated app to satisfytscinverts that design.While there, re-read the whole of
packages/cli/templates/test/hello/hello.test.tsandpackages/cli/templates/test/hello/e2e/hello.test.tsagainst the generated config. Both look clean otherwise (they already hand-declare structuralPage/Browsertypes precisely to avoidany), but they have never been type-checked, so read them rather than assuming.Step 5. Apply the website test fixes
website/test/helpers/layout-props.tswith the helper exactly as given in Design / approach.website/test/ssr/layout-ssr.test.ts. Replace all 9RootLayout({ children: X })withRootLayout(layoutProps(X))and import the helper from#test/helpers/layout-props.ts.website/test/ssr/seo-infra.test.ts. Same, 3 sites (L40, L59, L69).website/test/ssr/nav-and-routes.test.ts. Same, 1 site (therenderLayoutarrow at L30).website/test/ssr/render-determinism.test.ts. RetypePAGESat L38 toArray<[string, () => TemplateResult]>, importTemplateResulttype-only from@webjsdev/core, and delete bothas anycasts at L47-48.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/blogfixesAll 13 from table 2c.
Step 7. Wire
examples/bloginto the gateexamples/blog/package.jsonhas notypecheckscript (its scripts aredev,start,css:build,db:generate,db:migrate,db:seed,test), which is why nothing ever rantscthere."typecheck": "webjs typecheck"toexamples/blog/package.jsonscripts, matching what the scaffold generates atpackages/cli/lib/create.js:400.blog typecheckstep to theappsjob in.github/workflows/ci.yml, next to the existingwebsite typecheckstep at L547-548:.github/workflows/ci.yml:538-546, which is now factually wrong in two ways: it says the gate covers "NOTtest/", and it says "examples/blog is genuinely red today".Step 8. Add the scaffold assertion
test/scaffolds/scaffold-integration.test.js:360-366already parses the generatedtsconfig.jsonand asserts oncompilerOptions.pluginsandcompilerOptions.types. Add anincludeassertion in the same block:Tests
This is a config change whose proof is
tscitself, plus a generated-app check the diff cannot give.Commands, per app
Run
npm run typecheckand never a baretscinwebsite/. Thepretypecheckhalf of that script (node scripts/copy-registry.mjs) mirrors in the gitignoredmodules/ui/components/sources; without ittscemits 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 missingpuppeteer-core: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 toconst quoted = (key) => ....cd website && npm run typecheckmust fail withTS7006 Parameter 'key' implicitly has an 'any' type. Annotate it back and confirm it passes. Repeat the same in the generatedprobe-appagainsttest/hello/hello.test.ts.Half 2, the revert proves the gate. Remove the
"test/**/*"line fromwebsite/tsconfig.json, leave the deliberately broken test file in place, and re-runnpm 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
appsjob (In-repo app tests (website + blog)) at.github/workflows/ci.yml:508, whosewebsite typecheckstep is at L547-548.examples/bloghas no typecheck step, which Step 7 adds. No other workflow runstscover an app.Docs
Every surface below either shows an app
includearray or states whatwebjs typecheckcovers. All must be updated.website/AGENTS.md:422-434test/is deliberately outside it", enumerates the 17 errors, says "Fixing those is its own task", and says "examples/blogis genuinely red (13 errors), so gating it is a small, separate change". Rewrite: the gate now coverstest/too, both apps are gated, and record thecheckJs: falsedecision that leaves the 9.jsbrowser tests parsed-but-unchecked (the tsconfig is strict JSON and cannot carry that comment itself).github/workflows/ci.yml:538-546blog typecheckstep.agents/skills/webjs/references/typescript.md:56-77tsconfig.json" block showscompilerOptionswith noincludeat all. Add theincludearray includingtest/**/*, so an app author copying it gets type-checked tests.agents/skills/webjs/references/testing.mdwebjs typecheckreadstest/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 onlytypecheckmention, at L154, is about visual defects)website/app/docs/configuration/page.ts:91includesnippet, currently["app/**/*", "components/**/*", "modules/**/*", "lib/**/*"]. Addtest/**/*. While there, this snippet has also drifted from what the scaffold emits (it lacksmiddleware.js/middleware.ts/.webjs/routes.d.tsand theerasableSyntaxOnly+pluginsentries); bring it in line withpackages/cli/lib/create.js:510-559website/app/docs/typescript/page.ts:36-43includesnippet. Addtest/**/*. It is closer to the real scaffold but still lacksmiddleware.jsand.webjs/routes.d.tspackages/cli/lib/create.js:544-549include, currently explaining only.webjs/routes.d.ts. Extend it to covertest/**/*examples/blog/AGENTS.mdtypecheckscript and a CI gate. If it documents the app's commands, addnpm run typecheckNote on the scaffold copy of the skill: it is single-source.
packages/cli/lib/create.js:665-674copies the canonical repo-root.agents/skills/webjs/(falling back from a prepack bundle that is gitignored in the monorepo), sopackages/cli/templates/.agents/skills/webjs/does not exist at HEAD and there is exactly one copy oftypescript.md/testing.mdto edit.Invoke the
webjs-doc-syncskill to confirm no surface in the table is missed.Acceptance criteria
website/tsconfig.json:16includecontains"test/**/*", andcd website && npm run typecheckexits 0examples/blog/tsconfig.jsonincludecontains"test/**/*"examples/blog/package.jsonhas atypecheckscript, andcd examples/blog && npm run typecheckexits 0 (it exits non-zero at HEAD, both with and without the include change)anyor blanket@ts-expect-errorexamples/blogerrors from table 2c are fixed, including the twodb/columns.server.tstextreferences, which are a liveReferenceErrorand not merely a type nitwebsite/test/helpers/layout-props.tsexists, compiles with noascast, and is used by all 13LayoutPropscall sites acrosslayout-ssr.test.ts,seo-infra.test.ts, andnav-and-routes.test.tspackages/core/src/routes.d.tsis unchanged;LayoutPropswas not weakened to accommodate a testas anycasts atwebsite/test/ssr/render-determinism.test.ts:47-48are deleted, not preservedpackages/cli/lib/create.js:550-558emits anincludecontaining'test/**/*'packages/cli/templates/test/hello/e2e/hello.test.tscarries the@ts-expect-errorfor the optionalpuppeteer-coreimport, andpuppeteer-corewas NOT added to the scaffold'sdevDependenciesnpx webjs typecheckwith the test directory includedtest/scaffolds/scaffold-integration.test.jsasserts the generatedincludecontainstest/**/*.github/workflows/ci.ymlappsjob runs ablog typecheckstep alongsidewebsite typecheck, and the stale comment at L538-546 is rewrittenanyparameter added to a website test file, and to a generated app'stest/hello/hello.test.ts, makestypecheckfail, and it passes once annotated"test/**/*"line makestypecheckreport success on that same broken treecd website && npm test,npm run test:browser,cd examples/blog && npm test, and the rootnpm testall passwebsite/AGENTS.md:422-434and.github/workflows/ci.yml:538-546, which both assert the opposite of the new behaviourOut of scope
These are true non-goals, not deferred work. Everything the inventory names is in scope and gets fixed in this PR.
LayoutPropsorPagePropsinpackages/core/src/routes.d.ts. Makingparams/searchParams/urloptional would weaken a public type every WebJs app depends on, to save 13 test call sites a helper already handles.checkJsforwebsite, or turning it off forexamples/blog. Each app's flag is settled above with a reason. Flipping either is a separate decision with its own blast radius."**/*.ts"include, Next / Remix / Astro style. Rejected with reasons in Design / approach.tsconfig.test.jsonanywhere. Rejected with reasons in Design / approach..jsbrowser tests underwebsite/test/components/browser/. They enter the include as parsed-but-unchecked, exactly as every other.jsin that app already is. Checking them means acheckJsdecision, which is the previous bullet.docs/orpackages/ui/packages/website/. Both were deliberately deleted inddfc5547(chore: delete the docs and ui-website redirect-only apps #1305).websiteorexamples/blogapp source beyond the errorstscactually reports. Do not go hunting foranyin files that compile.AGENTS.md, "Deliberately deferred").