Skip to content

default-theme dist is unusable on Vite 8 — .js import specifiers point at .jsx files #142

@bigmistqke

Description

@bigmistqke

[written w/ claude]

Summary

@kobalte/solidbase@0.6.3's published dist/default-theme/ ships .jsx files whose internal imports reference .js extensions. Vite ≤ 7 (esbuild resolver) silently substituted .js → .jsx; Vite 8 + Rolldown does not. Consumers that install SolidBase from npm and use default-theme cannot build on the stack SolidBase itself targets.

Reproduction

Install @kobalte/solidbase@0.6.3, vite@8, the pkg.pr.new SolidStart@2080 build, then:

// vite.config.ts
import { createSolidBase } from "@kobalte/solidbase/config"
import defaultTheme from "@kobalte/solidbase/default-theme"

const solidBase = createSolidBase(defaultTheme)
export default defineConfig({ plugins: [solidBase.plugin({ /* … */ }), /* … */] })

Build fails:

[UNRESOLVED_IMPORT] Could not resolve './components/Preview.js' in
  node_modules/@kobalte/solidbase/dist/default-theme/mdx-components.jsx
[UNRESOLVED_IMPORT] Could not resolve './context.js' in
  node_modules/@kobalte/solidbase/dist/default-theme/mdx-components.jsx
[UNRESOLVED_IMPORT] Could not resolve '../client/index.js' in
  node_modules/@kobalte/solidbase/dist/default-theme/Layout.jsx

The referenced files exist on disk as Preview.jsx, context.jsx, ../client/index.jsx — only the import specifiers say .js. resolve.extensionAlias: { ".js": [".js", ".jsx"] } doesn't help on Vite 8 / Rolldown.

Root cause

The mismatch is a side effect of two tsconfig choices interacting.

The TS convention. With "moduleResolution": "node16", TypeScript requires source files to write the import path as it will exist at runtime. So in src/default-theme/mdx-components.tsx, when it imports its neighbor src/default-theme/context.tsx, the import is written:

import { ... } from "./context.js"

That .js isn't referring to anything in dist/ — the source is referring to its own future post-build name. TS type-checks by looking next door at context.tsx; the .js is purely about what the file will be called after compilation.

The twist. The convention assumes context.tsx compiles to context.js. But SolidBase sets "jsx": "preserve" so TS deliberately leaves JSX intact for the downstream Solid plugin to handle. The emitted file is context.jsx, not context.js. TS doesn't rewrite import strings, so in dist/:

  • file on disk: context.jsx
  • import inside its sibling: ./context.js

Who used to paper over it. Vite ≤ 7's esbuild resolver tried .jsx automatically when .js didn't resolve, hiding the bug. Vite 8 + Rolldown doesn't, so the unresolved ./context.js becomes a hard build error.

Why this wasn't caught

There is no integration test that builds a consumer app against the published package. Every existing path bypasses dist/:

  • tests/default-theme/ has two unit specs (badges, preview) that import from source.
  • docs/ imports default-theme from ../src/default-theme (workspace .tsx).
  • examples/solid-docs is a submodule of solid-docs, which ships its own theme without extends: defaultTheme — so it never loads dist/default-theme/*.jsx.

We could prevent this class of regression by adding a smoke test that packs the tarball, installs it into a minimal Vite app fixture, and runs vite build.

Suggested fix

Switch tsconfig.json to "moduleResolution": "bundler" and update source imports from ./foo.js./foo.jsx. Under bundler, TS accepts the actual on-disk extension, so dist/ becomes self-consistent (.jsx files importing .jsx) with no codemod and no reliance on bundler extension fallback.

Workaround for downstream consumers

A small Vite plugin that intercepts .js imports originating inside @kobalte/solidbase and falls back to the sibling .jsx file:

import { existsSync } from "node:fs"
import { dirname, extname, resolve as resolvePath } from "node:path"
import { fileURLToPath } from "node:url"
import type { Plugin } from "vite"

export function solidbaseJsxFallback(): Plugin {
  return {
    name: "solidbase-jsx-fallback",
    enforce: "pre",
    resolveId(source, importer) {
      if (!importer) return null
      if (!importer.includes("@kobalte/solidbase")) return null
      if (extname(source) !== ".js") return null

      const importerPath = importer.startsWith("file://")
        ? fileURLToPath(importer)
        : importer
      const absolute = resolvePath(dirname(importerPath), source)
      const jsxCandidate = absolute.replace(/\.js$/, ".jsx")

      if (existsSync(jsxCandidate)) return jsxCandidate
      return null
    },
  }
}

Add solidbaseJsxFallback() first in your plugins array; it'll patch the resolver until the upstream tsconfig change ships.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions