netlify / netlify/framework-adapters

vite-plugin: SSR function import path is wrong when Vite root is a subdirectory (relative build.outDir resolved against cwd)

Open Beginner friendly
#68 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
1
Forks
3
Avg merge
1d 7h
Merged PRs (30d)
9

Description

Environment

  • @netlify/vite-plugin-tanstack-start 1.3.19 (bundles @netlify/vite-plugin 3.0.1)
  • @tanstack/react-start 1.168.54
  • vite 8.3.0
  • Node 24, pnpm

Summary

When the Vite root is a subdirectory of the repository and the build runs from the repository root, the generated SSR function gets an extra ../ in its import specifier.

# web/.netlify/v1/functions/server.mjs (generated)
import serverEntrypoint from "../../../../dist/server/server.js";

# expected
import serverEntrypoint from "../../../dist/server/server.js";

Netlify's Functions bundling then fails:

◈ Functions bundling
◈ Packaging Functions from web/.netlify/v1/functions directory:
 - server.mjs
X [ERROR] Could not resolve "../../../../dist/server/server.js"
    web/.netlify/v1/functions/server.mjs:1:29

This makes the SSR/API function undeployable for this layout. The same wrong specifier is produced regardless of whether the directory is discovered as a framework function (<base>/<package>/.netlify/v1/functions) or via [functions] directory.

How to reproduce

Repo layout where Vite root is a subdirectory but the build is invoked from the repo root:

repo/
├─ netlify.toml           # [build] command = "pnpm run build"
├─ package.json           # "build": "vite build --config web/vite.config.ts"
└─ web/
   ├─ vite.config.ts      # root: <web/>
   └─ src/...

web/vite.config.ts:

export default defineConfig({
  root: rootDir, // web/
  plugins: [tanstackStart(), netlify(), react()],
});

Then:

pnpm run build           # from repo root, process.cwd() !== config.root
cat web/.netlify/v1/functions/server.mjs

Cause

@netlify/vite-plugin dist/main.js (build plugin):

async writeBundle(_, bundle) {
  const functionsDirectory = join(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR);
  const serverEntrypoint = getServerEntrypoint(bundle, this.environment.config.build.outDir);
  const serverEntrypointRelativePath = toPosixPath(relative(functionsDirectory, serverEntrypoint));
  // ...
}

this.environment.config.build.outDir is a relative value here (dist/server): TanStack Start's plugin derives the per-environment output directory as join(userConfig.build?.outDir ?? 'dist', 'server') (@tanstack/start-plugin-core vite/output-directory.ts), so the environment config keeps it relative.

path.relative(from, to) resolves a relative to against process.cwd(). When process.cwd() !== config.root, the entry path resolves to <repo>/dist/server/server.js instead of <repo>/web/dist/server/server.js, producing one extra ../.

In a project where cwd === root this happens to work, which is why it only shows up with a subdirectory root.

Suggested fix

Resolve the output directory against the Vite root before computing the relative path:

const outDir = isAbsolute(this.environment.config.build.outDir)
  ? this.environment.config.build.outDir
  : resolve(resolvedConfig.root, this.environment.config.build.outDir);
const serverEntrypoint = getServerEntrypoint(bundle, outDir);

Workaround

Setting an absolute build.outDir in vite.config.ts avoids the issue:

build: { outDir: path.join(rootDir, "dist") }

but it would be good not to require that.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in dist/main.js around the writeBundle logic that uses the environment build.outDir, then run the reported reproduction with pnpm run build from the repository root. Done means web/.netlify/v1/functions/server.mjs uses the correct relative import and Netlify Functions bundling resolves server.js for a subdirectory Vite root.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript, vite
Domain
build-system
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.