diff --git a/.agents/skills/migrate-to-vinext/references/troubleshooting.md b/.agents/skills/migrate-to-vinext/references/troubleshooting.md index d25705eb..0e1b14b4 100644 --- a/.agents/skills/migrate-to-vinext/references/troubleshooting.md +++ b/.agents/skills/migrate-to-vinext/references/troubleshooting.md @@ -24,6 +24,22 @@ When adding `"type": "module"`, any `.js` file using `module.exports` or `requir Alternatively, convert these files to ESM (`export default` syntax) and keep the `.js` extension. +## Third-Party Package ESM Resolution Errors + +**Symptom:** `Cannot find module '...'` errors in dev server when using certain npm packages. + +**Example Error:** +``` +Cannot find module '\node_modules.pnpm\validator@13.15.26\node_modules\validator\es\lib\util\assertString' +imported from \node_modules.pnpm\validator@13.15.26\node_modules\validator\es\lib\isEmail.js +``` + +**Cause:** Some ESM packages have complex internal import structures that Node.js module resolution can't handle when externalized. + +**vinext Fix:** vinext sets `noExternal: true` in all server environments (RSC and SSR), which forces all dependencies through Vite's transform pipeline. This resolves extensionless import issues automatically. + +**No configuration needed** — this is the default behavior. + ## App Router vs Pages Router Issues **Symptom:** RSC-related errors, "client/server component" boundary violations. diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 710c0d13..b6aec81c 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2059,6 +2059,8 @@ hydrate(); // Any user-provided `ssr.noExternal` is intentionally superseded // by this setting; only `ssr.external` entries escape Vite's transform. // Skip when targeting bundled runtimes (Cloudflare/Nitro bundle everything). + // This also resolves extensionless-import issues in packages like + // `validator` (see #189) by routing them through Vite's resolver. ...(hasCloudflarePlugin || hasNitroPlugin ? {} : { ssr: { external: ["react", "react-dom", "react-dom/server"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6985916..367f1e1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -654,6 +654,25 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1) + tests/fixtures/ecosystem/validator: + dependencies: + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + validator: + specifier: ^13.15.26 + version: 13.15.26 + devDependencies: + vinext: + specifier: workspace:* + version: link:../../../../packages/vinext + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1) + tests/fixtures/pages-basic: dependencies: react: @@ -4050,6 +4069,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -7369,6 +7392,8 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.15.26: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/tests/ecosystem.test.ts b/tests/ecosystem.test.ts index e0c445a9..7f18bcfc 100644 --- a/tests/ecosystem.test.ts +++ b/tests/ecosystem.test.ts @@ -406,3 +406,27 @@ describe("shadcn", () => { expect(html).toContain("Open Menu"); }); }); + +// ─── validator ────────────────────────────────────────────────────────────── + +describe("validator", () => { + let proc: ChildProcess | null = null; + let fetchPage: (pathname: string) => Promise<{ html: string; status: number }>; + + beforeAll(async () => { + const fixture = await startFixture("validator", 4405); + proc = fixture.process; + fetchPage = fixture.fetchPage; + }, 30000); + + afterAll(() => killProcess(proc)); + + it("can import and use validator/es/lib/isEmail.js in SSR", async () => { + const { html, status } = await fetchPage("/"); + expect(status).toBe(200); + expect(html).toContain("
Email: {email}
+Valid: {String(valid)}
+