fix(module-builder): a module's tsconfig is not the builder's business #32

Merged
julian merged 1 commit from fix/module-builder-tsconfig into main 2026-08-11 23:07:16 +02:00
Owner

Split out of #31 at Julian's request: these are the only changed lines that
alter production build behaviour for every module, and they fix a bug that is
disabling a module in the field right now. #31 keeps the component kit and
will rebase on top of this.

The bug

Vite loads the nearest tsconfig.json when it transforms a module's .ts
files. Every module config in this repository does

{ "extends": "../../../tsconfig.svelte.json" }

and says, in a comment, that "an external module repository would carry a file
exactly like this one."
In a clone at /data/modules/src/<name>/ that target
does not exist, so the parse throws, the frontend build fails, and the host
disables the module at startup — a notification, not a red build. A module
authored to our own documented convention took itself down.

Passing tsconfigRaw as a string is what skips the lookup; an object still
reads the file and merges over it, which is the same crash.

Changes since the review

  • Options stated, not blanked. "{}" would have meant esbuild's defaults —
    class fields lowered to constructor assignment, type-only imports elided
    while check:svelte still requires import type. It now carries exactly
    what module builds were inheriting through extends, so no emitted code
    changes:

    esbuild: {
      tsconfigRaw:
        '{"compilerOptions":{"target":"ESNext","verbatimModuleSyntax":true,"useDefineForClassFields":true}}',
    },
    
  • Tests. The config is now separable from running it (moduleBuildConfig),
    so one test reads the decisions without a build. Three in total: the options
    are what we say and are a string; a module whose tsconfig cannot resolve its
    extends still builds; the import contract still rejects a bare specifier.

    Checked they fail for the right reason — rewriting the value as
    tsconfigRaw: { compilerOptions: … }, the spelling the review predicted
    someone would reach for, fails both new tests with the original
    TSConfckParseError: failed to resolve "extends".

    They run as deno task test:builder, added to ci: importing vite reads
    os.release() and esbuild spawns its service binary, so this is the one
    place needing unrestricted --allow-run, and test stays narrow (it now
    ignores this package).

  • Docs. docs/modules.md gains "Your tsconfig.json governs your editor,
    not the build"
    — that nothing in it reaches the output, why that keeps a
    module loadable, and the two safe ways to keep one (tsconfig.check.json
    passed explicitly to svelte-check, or no extends outside your own repo).

Verification

deno task check clean, deno fmt/deno lint clean, deno task test:builder
3/3. Behaviourally: the filebrowser module, cloned as an external module the
way modules actually deploy, went from external module skipped at the
frontend-build stage to loading and serving its pages.

Based on current main, so no conflicts.

Split out of #31 at Julian's request: these are the only changed lines that alter production build behaviour for every module, and they fix a bug that is disabling a module in the field right now. #31 keeps the component kit and will rebase on top of this. ## The bug Vite loads the nearest `tsconfig.json` when it transforms a module's `.ts` files. Every module config in this repository does ```jsonc { "extends": "../../../tsconfig.svelte.json" } ``` and says, in a comment, that *"an external module repository would carry a file exactly like this one."* In a clone at `/data/modules/src/<name>/` that target does not exist, so the parse throws, the frontend build fails, and the host disables the module at startup — a notification, not a red build. A module authored to our own documented convention took itself down. Passing `tsconfigRaw` as a **string** is what skips the lookup; an object still reads the file and merges over it, which is the same crash. ## Changes since the review - **Options stated, not blanked.** `"{}"` would have meant esbuild's defaults — class fields lowered to constructor assignment, type-only imports elided while `check:svelte` still requires `import type`. It now carries exactly what module builds were inheriting through `extends`, so no emitted code changes: ```ts esbuild: { tsconfigRaw: '{"compilerOptions":{"target":"ESNext","verbatimModuleSyntax":true,"useDefineForClassFields":true}}', }, ``` - **Tests.** The config is now separable from running it (`moduleBuildConfig`), so one test reads the decisions without a build. Three in total: the options are what we say and are a string; a module whose tsconfig cannot resolve its `extends` still builds; the import contract still rejects a bare specifier. Checked they fail for the right reason — rewriting the value as `tsconfigRaw: { compilerOptions: … }`, the spelling the review predicted someone would reach for, fails both new tests with the original `TSConfckParseError: failed to resolve "extends"`. They run as `deno task test:builder`, added to `ci`: importing vite reads `os.release()` and esbuild spawns its service binary, so this is the one place needing unrestricted `--allow-run`, and `test` stays narrow (it now ignores this package). - **Docs.** `docs/modules.md` gains *"Your tsconfig.json governs your editor, not the build"* — that nothing in it reaches the output, why that keeps a module loadable, and the two safe ways to keep one (`tsconfig.check.json` passed explicitly to svelte-check, or no `extends` outside your own repo). ## Verification `deno task check` clean, `deno fmt`/`deno lint` clean, `deno task test:builder` 3/3. Behaviourally: the filebrowser module, cloned as an external module the way modules actually deploy, went from `external module skipped` at the frontend-build stage to loading and serving its pages. Based on current main, so no conflicts.
fix(module-builder): a module's tsconfig is not the builder's business
All checks were successful
Build and Deploy / verify (pull_request) Successful in 1m19s
Build and Deploy / build (pull_request) Has been skipped
Dependency Check / dependencies (pull_request) Successful in 1m10s
d266e0428a
Vite loads the nearest tsconfig.json when it transforms a module's .ts files,
and a module repository carrying one is normal — an editor wants it,
svelte-check needs it. Every module config in this repository extends
"../../../tsconfig.svelte.json" and says, in a comment, that an external
module would carry a file exactly like it. That target does not exist in a
clone at /data/modules/src/<name>/, so a module authored to our own documented
convention took itself down: the parse throws, the frontend build fails, and
the host disables the module at startup. It is disabling one in the field now.

Passing tsconfigRaw as a STRING is what skips the lookup — with an object vite
reads the file first and merges over it, which is the same crash. The options
spelled out in it are the ones module builds were already inheriting through
`extends`, so no emitted code changes: an empty object would silently have
meant esbuild's defaults, lowering class fields to constructor assignment and
eliding the type-only imports check:svelte still requires be written as
`import type`.

The build config is now separable from running it, so a test can read the
decisions in it without a build. Three tests: the options are what we say they
are and are a string, a module whose tsconfig cannot resolve its `extends`
still builds, and the import contract still rejects a bare specifier. They run
as their own task — importing vite reads os.release() and esbuild spawns its
service binary, so this is the one place needing unrestricted --allow-run, and
`test` stays narrow. Without them the next person to want one compiler option
writes the object form, which reads identically at the call site and re-breaks
every external module; with them that spelling fails on the original error.

docs/modules.md now says a module's tsconfig governs its editor and not the
build, and how to keep one safely.
julian approved these changes 2026-08-11 23:07:06 +02:00
julian left a comment

Reviewed the full diff plus the claims in the description.

  • Verified the "options match what extends supplied" claim: tsconfig.svelte.json carries target: ESNext and verbatimModuleSyntax. useDefineForClassFields is not literally in that file, but it defaults to true at target ES2022+, so stating it explicitly emits identical code.
  • The remaining inherited options (module, moduleResolution, lib, strict, isolatedModules, paths) don't affect esbuild's transform output; vite does its own resolution. Nothing relevant is lost.
  • Internal modules keep their extends configs for svelte-check only — their builds are unchanged.
  • Task wiring checks out: test ignores packages/module-builder, test:builder is the single place with unrestricted --allow-run, and it runs in ci.

No findings. Ready to merge.

Reviewed the full diff plus the claims in the description. - Verified the "options match what `extends` supplied" claim: `tsconfig.svelte.json` carries `target: ESNext` and `verbatimModuleSyntax`. `useDefineForClassFields` is not literally in that file, but it defaults to true at target ES2022+, so stating it explicitly emits identical code. - The remaining inherited options (`module`, `moduleResolution`, `lib`, `strict`, `isolatedModules`, `paths`) don't affect esbuild's transform output; vite does its own resolution. Nothing relevant is lost. - Internal modules keep their `extends` configs for svelte-check only — their builds are unchanged. - Task wiring checks out: `test` ignores `packages/module-builder`, `test:builder` is the single place with unrestricted `--allow-run`, and it runs in `ci`. No findings. Ready to merge.
julian merged commit c8168747ce into main 2026-08-11 23:07:16 +02:00
julian deleted branch fix/module-builder-tsconfig 2026-08-11 23:07:16 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OpsDeck/core!32
No description provided.