Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions