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
In a freshly scaffolded app, html is typed any. So is css, TemplateResult, Suspense, repeat, connectWS, richFetch and escapeText / escapeAttr. Every component's render() therefore has an inferred return type of any, and so does every page and layout that returns a template.
Reproduced in a real npm create webjs@latest app (webjs-test/demo-app, cli 0.10.56 / core 0.7.51, TypeScript 5.9.3), asking the language service for the type at modules/components/components/theme-context.ts:29:
(method) ThemeProvider.render(): any
This is what surfaced it: LazyVim (vtsls, bundled TypeScript 5.9.3) reports 'render' implicitly has return type 'any' because it does not have a return type annotation on that component, and underlines the provider field beside it. The editor is right, and the underlying degradation is real regardless of which editor shows it.
The cause is in packages/core/index.d.ts, which re-exports seven modules from their JSDoc .js implementation while the other twenty-five entries resolve to a hand-written .d.ts sibling:
index.d.ts:69 + :73html, isTemplate, MARKER, type TemplateResult, from ./src/html.js
index.d.ts:74css, isCSS, adoptStyles, stylesToString, from ./src/css.js
index.d.ts:78escapeText, escapeAttr, from ./src/escape.js
index.d.ts:80repeat, isRepeat, from ./src/repeat.js
index.d.ts:81Suspense, isSuspense, from ./src/suspense.js
index.d.ts:82connectWS, from ./src/websocket-client.js
index.d.ts:83richFetch, from ./src/rich-fetch.js
There is no src/html.d.ts, css.d.ts, escape.d.ts, repeat.d.ts, suspense.d.ts, websocket-client.d.ts or rich-fetch.d.ts. A consumer with allowJs off, which is every scaffolded app, cannot read the JSDoc in those .js files, so each import resolves to any (TS7016), silenced by the scaffold's skipLibCheck: true.
It propagates past the direct imports, because two more overlays reach for the same untyped modules: src/component.d.ts:12-13 imports CSSResult from ./css.js and TemplateResult from ./html.js, src/routes.d.ts:26 imports TemplateResult from ./html.js, and src/directives.d.ts:1 re-exports repeat from ./repeat.js. So a component's render() return, its static styles, a page's PageProps return type and repeat from @webjsdev/core/directives are all unchecked today. Probed in the generated app: annotating a page's return as TemplateResult and returning a number, and setting static styles = 12345, both type-check silently.
The same probe found a second, unrelated break in the server overlay. packages/server/index.d.ts:122 types RequestHandler.handle as Handle, and the comment at :37-38 says the name arrives via the export * from './src/testing.d.ts' at :28. It does not: export * re-exports a name, it does not create a local binding. So that is TS2304: Cannot find name 'Handle' and handle degrades to an error type. Two TS2846s sit alongside it, from the explicit .d.ts extension in the export * specifiers at index.d.ts:11 (core) and :28 (server).
Design / approach
Add the seven missing .d.ts siblings, so the public surface is fully typed for a consumer that has allowJs off. They are pure declarations, no runtime change, and packages/core/package.jsonfiles already ships src whole, so nothing changes about packaging.
Every one of the seven already carries complete JSDoc, so the declarations can be generated rather than hand-written and then committed as normal overlays alongside their twenty-five siblings:
That was run during triage and produces correct output (html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult, with TemplateResult exported as a type). Generation is a convenience for the first draft; the committed files are hand-maintained overlays from then on, exactly like the existing twenty-five, and both drift guards then cover them.
The fix was verified against the reporter's own app by dropping the seven generated files into its node_modules/@webjsdev/core/src/:
render() went from any to TemplateResult
html went from a bare import html to function html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult
npx tsc --noEmit over the whole generated app stayed at zero errors, so the newly-real types do not red the gallery
Fix Handle in the same change with an explicit import type { Handle } from './src/testing.d.ts'; in packages/server/index.d.ts, and normalize the two export * specifiers off the explicit .d.ts extension.
Implementation notes (for the implementing agent)
Where to edit
Add packages/core/src/{html,css,escape,repeat,suspense,websocket-client,rich-fetch}.d.ts. Match the style of the existing overlays in that directory (signal.d.ts, task.d.ts, registry.d.ts are the closest models).
packages/server/index.d.ts: add the Handle import near :24 (which already imports types from @webjsdev/core), and delete the now-wrong comment at :37-38.
packages/core/index.d.ts:11 and packages/server/index.d.ts:28: the export * from './src/*.d.ts' specifiers.
test/types/dts-export-coverage.test.mjs: the guard hole, see below.
--skipLibCheck hides the whole thing in an app. Running the generated app with --skipLibCheck false shows the 15 @webjsdev/* errors (the rest of the 64 come from drizzle-orm@1.0.0-rc.3, which is why the scaffold sets the flag and should keep setting it).
Do not "fix" this by adding allowJs to the scaffold's tsconfig. That would pull the framework's .js sources into every app's program.
The .d.ts files are OVERLAYS over JSDoc .js, so the reverse guard test/types/dts-no-phantom-exports.test.mjs (Guard .d.ts overlays against .js JSDoc signature drift #1031) now applies to the seven new files too: an overlay may not declare an export the runtime does not have.
Invariants to respect
packages/ is plain .js with JSDoc, never .ts (AGENTS.md, "Working in the WebJs framework repo itself"). .d.ts overlays are the established exception and what this issue adds.
Never change a Symbol('x') to Symbol.for('x') while moving declarations.
Add a guard that would have caught this: a fixture typechecked the way an APP resolves the package, meaning allowJs OFF, asserting the public exports are not any. expectTypeOf-style assignability checks work without a new dependency: assign html to a mismatched type and require the error, or assert TemplateResult is not assignable to number. It must cover the whole public export list, not a sample, so a future untyped re-export fails.
Include the counterfactual: delete one of the seven new .d.ts and the new guard must fail naming that export.
RequestHandler.handle needs a type assertion too, or the Handle fix has no test.
Docs: no public API changes, so this is a types-only fix. references/typescript.md in .agents/skills/webjs/ is the surface to check if anything user-visible is worth stating; a bare internal type fix may correctly touch no doc surface (WEBJS_NO_DOC_GATE=1).
Acceptance criteria
packages/core/src/ has the seven missing .d.ts files, exporting the same names their .js does
In a freshly generated app, html resolves to (strings, ...values) => TemplateResult and a component's render() resolves to TemplateResult, not any
Annotating a page's return as TemplateResult and returning a number is a type error in a generated app; static styles = 12345 is a type error
repeat from @webjsdev/core/directives is typed, not any
packages/server/index.d.ts no longer references Handle without importing it, and RequestHandler.handle is a real function type
tsc --noEmit --skipLibCheck false over a generated app reports zero errors from @webjsdev/*
A new guard fails when any public export degrades to any, checked with allowJs OFF, with a counterfactual proving it fires
A freshly generated app of each template still passes webjs check and webjs typecheck with zero errors
Problem
In a freshly scaffolded app,
htmlis typedany. So iscss,TemplateResult,Suspense,repeat,connectWS,richFetchandescapeText/escapeAttr. Every component'srender()therefore has an inferred return type ofany, and so does every page and layout that returns a template.Reproduced in a real
npm create webjs@latestapp (webjs-test/demo-app, cli 0.10.56 / core 0.7.51, TypeScript 5.9.3), asking the language service for the type atmodules/components/components/theme-context.ts:29:This is what surfaced it: LazyVim (vtsls, bundled TypeScript 5.9.3) reports
'render' implicitly has return type 'any' because it does not have a return type annotationon that component, and underlines theproviderfield beside it. The editor is right, and the underlying degradation is real regardless of which editor shows it.The cause is in
packages/core/index.d.ts, which re-exports seven modules from their JSDoc.jsimplementation while the other twenty-five entries resolve to a hand-written.d.tssibling:index.d.ts:69+:73html,isTemplate,MARKER, typeTemplateResult, from./src/html.jsindex.d.ts:74css,isCSS,adoptStyles,stylesToString, from./src/css.jsindex.d.ts:78escapeText,escapeAttr, from./src/escape.jsindex.d.ts:80repeat,isRepeat, from./src/repeat.jsindex.d.ts:81Suspense,isSuspense, from./src/suspense.jsindex.d.ts:82connectWS, from./src/websocket-client.jsindex.d.ts:83richFetch, from./src/rich-fetch.jsThere is no
src/html.d.ts,css.d.ts,escape.d.ts,repeat.d.ts,suspense.d.ts,websocket-client.d.tsorrich-fetch.d.ts. A consumer withallowJsoff, which is every scaffolded app, cannot read the JSDoc in those.jsfiles, so each import resolves toany(TS7016), silenced by the scaffold'sskipLibCheck: true.It propagates past the direct imports, because two more overlays reach for the same untyped modules:
src/component.d.ts:12-13importsCSSResultfrom./css.jsandTemplateResultfrom./html.js,src/routes.d.ts:26importsTemplateResultfrom./html.js, andsrc/directives.d.ts:1re-exportsrepeatfrom./repeat.js. So a component'srender()return, itsstatic styles, a page'sPagePropsreturn type andrepeatfrom@webjsdev/core/directivesare all unchecked today. Probed in the generated app: annotating a page's return asTemplateResultand returning a number, and settingstatic styles = 12345, both type-check silently.The same probe found a second, unrelated break in the server overlay.
packages/server/index.d.ts:122typesRequestHandler.handleasHandle, and the comment at:37-38says the name arrives via theexport * from './src/testing.d.ts'at:28. It does not:export *re-exports a name, it does not create a local binding. So that isTS2304: Cannot find name 'Handle'andhandledegrades to an error type. TwoTS2846s sit alongside it, from the explicit.d.tsextension in theexport *specifiers atindex.d.ts:11(core) and:28(server).Design / approach
Add the seven missing
.d.tssiblings, so the public surface is fully typed for a consumer that hasallowJsoff. They are pure declarations, no runtime change, andpackages/core/package.jsonfilesalready shipssrcwhole, so nothing changes about packaging.Every one of the seven already carries complete JSDoc, so the declarations can be generated rather than hand-written and then committed as normal overlays alongside their twenty-five siblings:
That was run during triage and produces correct output (
html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult, withTemplateResultexported as a type). Generation is a convenience for the first draft; the committed files are hand-maintained overlays from then on, exactly like the existing twenty-five, and both drift guards then cover them.The fix was verified against the reporter's own app by dropping the seven generated files into its
node_modules/@webjsdev/core/src/:render()went fromanytoTemplateResulthtmlwent from a bareimport htmltofunction html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResultnpx tsc --noEmitover the whole generated app stayed at zero errors, so the newly-real types do not red the galleryFix
Handlein the same change with an explicitimport type { Handle } from './src/testing.d.ts';inpackages/server/index.d.ts, and normalize the twoexport *specifiers off the explicit.d.tsextension.Implementation notes (for the implementing agent)
Where to edit
packages/core/src/{html,css,escape,repeat,suspense,websocket-client,rich-fetch}.d.ts. Match the style of the existing overlays in that directory (signal.d.ts,task.d.ts,registry.d.tsare the closest models).packages/server/index.d.ts: add theHandleimport near:24(which already imports types from@webjsdev/core), and delete the now-wrong comment at:37-38.packages/core/index.d.ts:11andpackages/server/index.d.ts:28: theexport * from './src/*.d.ts'specifiers.test/types/dts-export-coverage.test.mjs: the guard hole, see below.Landmines
test/types/dts-export-coverage.test.mjs:117runs tsc with--allowJs, so the fixture reads the JSDoc out ofhtml.jsand the name resolves. The guard asserts a name is EXPORTED, never that it is notany, and--allowJsis not how an app resolves these.test/types/type-fixtures.test.mjsandtest/types/server-types.test.mjspass--allowJsfor the same stated reason. Prior art in the same family: dogfood: package .d.ts files drift from runtime exports (many import type errors) #388, dogfood: @webjsdev/core subpath exports lack a types condition (no types for /directives, /task, etc.) #389, Ship type declarations for @webjsdev/server (fix TS7016 on import) #310, Guard .d.ts overlays against .js JSDoc signature drift #1031, dts-export-coverage checks 3 of 15 overlays, not every exports subpath #1291.--skipLibCheckhides the whole thing in an app. Running the generated app with--skipLibCheck falseshows the 15@webjsdev/*errors (the rest of the 64 come fromdrizzle-orm@1.0.0-rc.3, which is why the scaffold sets the flag and should keep setting it).allowJsto the scaffold's tsconfig. That would pull the framework's.jssources into every app's program.TemplateResultis a JSDoc@typedefinhtml.js(:7), re-exported as a type atindex.d.ts:73for fix: website typecheck fails on TemplateResult import (not exported from @webjsdev/core public types) #772. The newhtml.d.tsmust export it as a type, or fix: website typecheck fails on TemplateResult import (not exported from @webjsdev/core public types) #772 regresses.MARKERis'wjm-'andhtml.js:23-33documents why the literal must stay[a-z][a-z0-9-]*(dogfood: all tier-2 ui components dead on iOS (slot hydration); tap does nothing #730, guarded bytest/rendering/marker-valid-attr-name.test.js). Declaring it asstringrather than the literal type loses nothing at runtime but do not change the value..d.tsfiles are OVERLAYS over JSDoc.js, so the reverse guardtest/types/dts-no-phantom-exports.test.mjs(Guard .d.ts overlays against .js JSDoc signature drift #1031) now applies to the seven new files too: an overlay may not declare an export the runtime does not have.Invariants to respect
packages/is plain.jswith JSDoc, never.ts(AGENTS.md, "Working in the WebJs framework repo itself")..d.tsoverlays are the established exception and what this issue adds.Symbol('x')toSymbol.for('x')while moving declarations.Tests + docs
allowJsOFF, asserting the public exports are notany.expectTypeOf-style assignability checks work without a new dependency: assignhtmlto a mismatched type and require the error, or assertTemplateResultis not assignable tonumber. It must cover the whole public export list, not a sample, so a future untyped re-export fails..d.tsand the new guard must fail naming that export.RequestHandler.handleneeds a type assertion too, or theHandlefix has no test.references/typescript.mdin.agents/skills/webjs/is the surface to check if anything user-visible is worth stating; a bare internal type fix may correctly touch no doc surface (WEBJS_NO_DOC_GATE=1).Acceptance criteria
packages/core/src/has the seven missing.d.tsfiles, exporting the same names their.jsdoeshtmlresolves to(strings, ...values) => TemplateResultand a component'srender()resolves toTemplateResult, notanyTemplateResultand returning a number is a type error in a generated app;static styles = 12345is a type errorrepeatfrom@webjsdev/core/directivesis typed, notanypackages/server/index.d.tsno longer referencesHandlewithout importing it, andRequestHandler.handleis a real function typetsc --noEmit --skipLibCheck falseover a generated app reports zero errors from@webjsdev/*any, checked withallowJsOFF, with a counterfactual proving it fireswebjs checkandwebjs typecheckwith zero errors