Skip to content

Type an entry's default export; stop rx-tsc crashing on .d.rx - #65

Open
brenelz wants to merge 2 commits into
solidjs:mainfrom
brenelz:fix/projection-default-export
Open

brenelz wants to merge 2 commits into
solidjs:mainfrom
brenelz:fix/projection-default-export

Conversation

@brenelz

@brenelz brenelz commented Nov 14, 2026

Copy link
Copy Markdown

A TypeScript entry written as the README shows (import main from "./app.rx"; app.mount(main(app))) fails rx-tsc --noEmit with TS1192: Module './app.rx' has no default export. Separately, with "include": ["src"] and a .d.rx in src/, rx-tsc crashes with languageId not found for …/users.d.rx and then Error: !!sourceScript from Volar's proxyCreateProgram.

Root cause

The TSX projection (spec/13 TY-1c) emits the default component as export function App(props: any) plus export const __view, and nothing as a default export. At run time the compiled entry's default export is main(__rt) (spec/11 CP-4a), so TypeScript sees no default where JavaScript gets one. For the crash: .rx is a program extension, so include globs also match .d.rx. The language plugin returned no language id for those files, and Volar's tsc asserts that every program file has a source script.

What changed

  • The prelude gains export type Main = (app: object) => () => View;, a function of either build's createApp app that gives the view app.mount (or the server's app.stream) takes. A module with export default component now ends its projection with declare const __main: __.Main; export default __main;. The line is appended after the existing output, so the component's own mappings are unchanged. The type is in the prelude rather than taken from @reactivescript/runtime, because the prelude must resolve without the runtime installed (ref's type runner has no copy), and createApp's declared return type does not list the __rt helpers anyway.
  • The crate (compiler/src/project.rs, prelude.ts) and ref (ref/types/project.ts) make the same change, since crate.ts --project compares them text for text.
  • The language plugin gives .d.rx its own language id and an empty TypeScript module (export {}), so a .d.rx that include picks up no longer stops the program.
  • compiler/src/prelude.ts line 12 still had the \`` escapes copied from ref's template literal, so crate.ts --projectfailed onmainwithFAIL prelude`. The fix changes only a comment.

Verification

  • cargo test --release in compiler/: a new test checks that an entry's projection has exactly one default export, that App keeps its mapping, and that a dependency without a default component gets no default export.
  • node test/tsc.js in tooling/rx-language-server/ (now also needs npm run build in runtime/). New checks:
    • the README's src/main.js snippet, read from README.md, type-checks against the built runtime's types;
    • app.stream(main(app), sink) type-checks with the server build;
    • app.mount(main), with main uncalled, fails with TS2345;
    • a .d.rx and the JS module it declares pass with "include": ["."].
  • npm test in tooling/rx-language-server/: 16/16 and 8/8.
  • In ref/:
    • npm run crate: 364/364.
    • npm run crate -- --project: 209/209, with 12,421 mappings holding.
    • npm run types and RX_CRATE=1 npm run types: 176 pass each.
    • npm test: no failures.
  • A copy of a Vite template using 0.3.1, with its @ts-expect-error lines and its "exclude": ["src/**/*.d.rx"] removed, passes rx-tsc --noEmit with this branch. It crashes with the published 0.3.1.

Left open

  • The default export is emitted wherever a default component exists, dependencies included, because the editor analyzes every file as its own entry. This is safe while the checker refuses default imports between .rx files (NOT_EXPORTED, MO-6), but MO-1c says a dependency may have a default component. If that import is ever allowed, the importer's projection should name the component (import { Badge as Tag }).
  • A compiled entry exports only default, yet the projection still exports export component Foo and other named exports, so JavaScript can import names that do not exist at run time.
  • An entry with no default component still compiles to a main default, but its projection has none.
  • Main takes any object; a narrower type needs an app type exported from the runtime.
  • The new default export line has no mapping onto the .rx, so go-to-definition on the imported main lands nowhere.
  • A .d.rx's own types are not checked; projecting it through the crate's declaration_file would check them, but with no mappings back.

🤖 Generated with Claude Code

The TSX projection of a module with `export default component` now ends
with a default export typed as the prelude's `Main`, a function of the
app giving the view `app.mount` takes, as the compiled `main(__rt)` is.
A `.d.rx` that a tsconfig's `include` picks up becomes an empty module
in the language plugin instead of a file with no script.
@brenelz
brenelz marked this pull request as ready for review November 14, 2026 02:58

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant