Skip to content

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Sep 17, 2025

Summary

This adds a framework-agnostic Vite build plugin that prepares a Vite app for deployment to Netlify. It expects that the Vite app has an ssr environment (and no other server environments to deploy) that has exactly one bundle entry and that this entry is a server request entry point—a module with a default export containing a Web Fetch handler under the fetch property. TanStack Start meets all these conditions.

This build plugin can be opted into via the intentionally private, undocumented build.enabled option, passed into @netlify/vite-plugin.

This also adds a new @netlify/vite-plugin-tanstack-start which just wraps @netlify/vite-plugin with this enabled.

At the moment, this only supports TanStack Start alpha, but this will be released imminently (presumably v.1.132 or so). As such, it inherits its requirements:

  • Vite 7+
  • Node.js 22.12.0+

Note that by wrapping @netlify/vite-plugin, @netlify/vite-plugin-tanstack-start also inherits all the former's dev functionality.

Just in case someone were to (unnecessarily) configure both, this PR adds some logic to detect this and print an actionable warning.

To do

  • add tests
  • improve readme?
  • verify SPA mode
  • add logic to prevent duplicated @netlify/vite-plugin running twice?
  • [ ] add edge SSR support if easy

Comment on lines +50 to +53
"@tanstack/react-start": "alpha",
"@tanstack/solid-start": "alpha",
Copy link
Member Author

@serhalp serhalp Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIXME before merge (or fast follow, depending on how timing plays out)

these should be something like >=1.132.0

"@netlify/dev": "4.5.9",
"@netlify/dev-utils": "^4.1.3"
"@netlify/dev-utils": "^4.1.3",
"dedent": "^1.7.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 KB, zero dependencies, maintained: https://node-modules.dev/#install=dedent

worth it for proper syntax highlighting and syntax parsing in IDEs

@serhalp serhalp force-pushed the serhalp/frb-1993-create-netlifyvite-plugin-tanstack-start branch 6 times, most recently from af6ec5e to a2a0061 Compare September 18, 2025 21:46
This adds a framework-agnostic Vite build plugin that prepares a Vite app for deployment to Netlify. It expects that the
Vite app has an `ssr` environment (and no other server environments to deploy) that has exactly one bundle entry and
that this entry is a server request entry point, a module with a default export containing a Web Fetch handler under the
`fetch` property. TanStack Start meets all these conditions.

This build plugin can be opted into via the intentionally private, undocumented `build.enabled` option, passed into
`@netlify/vite-plugin`.

This also adds a new `@netlify/vite-plugin-tanstack-start` which just wraps `@netlify/vite-plugin` with this enabled.

At the moment, this only supports TanStack Start `alpha`, but this will be released imminently (presumably v.1.132 or
so). As such, it inherits its requirements:
- Vite 7+
- Node.js 22.12.0+
@serhalp serhalp force-pushed the serhalp/frb-1993-create-netlifyvite-plugin-tanstack-start branch from 715c821 to f148fa5 Compare September 18, 2025 23:31
"zod": "^3.24.2"
},
"devDependencies": {
"@netlify/vite-plugin-tanstack-start": "file:../../..",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝🏼 one of the only changes to the starter

}),
tanstackStart(),
viteReact(),
netlify(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝🏼 one of the only changes to the starter

Comment on lines +1 to +3
[build]
command = "npm run build"
publish = "dist/client"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝🏼 the last change to the starter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will be able to remove need for this once netlify/build#6669 ships, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, framework detection doesn't occur on netlify deploy. this uses an existing site.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File copied from remix-compute, simplified, and refactored away unnecessary dependencies

We're probably overdue for a shared helper in a package somewhere...

"description": "Vite plugin for TanStack Start on Netlify",
"type": "module",
"engines": {
"node": "^22.12.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to be TanStack Start's supported range, so no use in supporting any greater

"peerDependencies": {
"@tanstack/react-start": "alpha",
"@tanstack/solid-start": "alpha",
"vite": ">=7.0.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to be TanStack Start's supported range, so no use in supporting any greater

*/
middleware?: boolean

/** @private */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, this could be a separate export from @netlify/vite-plugin? like /internal or something. or not exposed at all, one way or another. I liked this approach, but I don't feel strongly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the intended behavior with @private, but this doesn't seem to prevent from build option from being shown to users (unless it requires some specific configuration, this was just default behavior in empty project I checked after installing npm packed version of vite plugin from this PR):

Image

I don't necessarily think we shouldn't do it like this, but maybe we could duplicate

* This is currently only supported for TanStack Start projects.

to apply to build property as well and not just build.enabled? (or maybe move it there)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm. well, I added a more explicit callout: 84225cb. I think this is fine for now? we can always refactor later. I think it's fine to change it without majors.

"prepack": "npm run build",
"test": "vitest run",
"test:dev": "vitest",
"test:ci": "npm run build && vitest run",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wasn't necessary. it was building twice in CI.

export default defineConfig({
test: {
testTimeout: 15_000,
hookTimeout: 90_000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have to bump this even more when trying to debug it on windows ... so maybe this is just still not enough ( I did see you bumped it at least once already )

But after that I did get

 FAIL  test/e2e/build.test.ts > build output when deployed to Netlify
Error: Could not extract the URL from the build logs
 ❯ deploySite test/support/netlify-deploy.ts:65:11
     63|   const [url] = new RegExp(/https:.+\.netlify\.app/gm).exec(stdout) ?? []
     64|   if (!url) {
     65|     throw new Error('Could not extract the URL from the build logs')
       |           ^
     66|   }
     67|   console.log(`🌍 Deployed site is live: ${url}`)
 ❯ test/e2e/build.test.ts:18:21

And checking _deploy.log I see CLI formatted the thing we match on like so:

╭───────────────────────── ⬥  Draft deploy is live ⬥  ─────────────────────────╮
│                                                                              │
│   Deployed draft to https://68cd7cef1a39c99933013541--phenomenal-hummingbi   │
│                            rd-d567fe.netlify.app                             │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯

... but even ignoring all that and checking deployed site - https://68cd7cef1a39c99933013541--phenomenal-hummingbird-d567fe.netlify.app/ it crashes with:

Sep 19, 05:59:36 PM: ERROR  Uncaught Exception 	{"errorType":"Error","errorMessage":"Cannot find package 'react' imported from /var/task/.netlify/v1/functions/server.mjs","code":"ERR_MODULE_NOT_FOUND","stack":["Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'react' imported from /var/task/.netlify/v1/functions/server.mjs","    at Object.getPackageJSONURL (node:internal/modules/package_json_reader:268:9)","    at packageResolve (node:internal/modules/esm/resolve:774:81)","    at moduleResolve (node:internal/modules/esm/resolve:860:18)","    at moduleResolveWithNodePath (node:internal/modules/esm/resolve:990:14)","    at defaultResolve (node:internal/modules/esm/resolve:1033:79)","    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:780:12)","    at #cachedDefaultResolve (node:internal/modules/esm/loader:704:25)","    at ModuleLoader.resolve (node:internal/modules/esm/loader:687:38)","    at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:305:38)","    at ModuleJob._link (node:internal/modules/esm/module_job:137:49)"]}
Sep 19, 05:59:36 PM: INIT_REPORT Init Duration: 271.73 ms	Phase: invoke	Status: error	Error Type: Runtime.Unknown
Sep 19, 05:59:36 PM: f47e870f Duration: 341.44 ms	Memory Usage: 106 MB

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, it looks like we don't test on Windows in remix-compute, where I copied the test helper from: https://github.com/netlify/remix-compute/tree/573bb9494e13bd1a4e858b76322ab112984db994/.github/workflows. so there are probably some latent issues there.

the url thing is easy to fix... looks like the boxen thing makes it sometimes wrap over two lines. Adding --json (but kept the rest of the logic) should fix that: d7b644e.

also bumped timeout even higher... let's see if we can at least fix the first 2 of 3 issues: 3962b51

Copy link
Member Author

@serhalp serhalp Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could switch it from pnpm to something else...? let me try changing it to npm.

or maybe one of these pnpm options could help 😕:

      --package-import-method auto      Clones/hardlinks or copies packages. The selected method depends from the file system
      --package-import-method clone     Clone (aka copy-on-write) packages from the store
      --package-import-method copy      Copy packages from the store
      --package-import-method hardlink  Hardlink packages from the store

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

welp, I did fix the first two issues but debugging the third is a rabbit hole I can't afford right now, so I skipped Windows tests for now :(

pnpm seems to be problematic on Windows CI...
We'll do this in a separate PR to be safe.
follow-up to d117f25

You can't use `overrides` for a direct dependency unless the values are identical...
Because we now have framework-specific plugins that load our generic plugin, we should make sure
nothing bad happens if a user also includes our generic plugin directly.

I went down a few different paths here and this ended up seeming like the best approach: just warn
when we detect multiple instances in dev. Build is tricky because by design our plugin will be
instantiated once per environment (e.g. client + ssr). And it ends up generally not being a problem
if it's duplicated in build anyway, just dev.
also fix technically incorrect node version check
@serhalp serhalp marked this pull request as ready for review September 21, 2025 20:38
@serhalp serhalp requested review from a team as code owners September 21, 2025 20:38
@pieh pieh merged commit fd8b2cc into main Sep 22, 2025
20 of 21 checks passed
@pieh pieh deleted the serhalp/frb-1993-create-netlifyvite-plugin-tanstack-start branch September 22, 2025 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants