Skip to content

feat: built-in +server support (via Universal Deploy, deprecating vike-photon)#3106

Open
magne4000 wants to merge 190 commits intomainfrom
magne4000/dev
Open

feat: built-in +server support (via Universal Deploy, deprecating vike-photon)#3106
magne4000 wants to merge 190 commits intomainfrom
magne4000/dev

Conversation

@magne4000
Copy link
Member

@magne4000 magne4000 commented Feb 18, 2026

TODO doc

  • doc: /migration/universal-deploy
  • doc: /server
  • doc: any reference to photon
  • Complete test/vite-plugin-vercel tests
  • Hide UM usage

@magne4000 magne4000 changed the title Magne4000/dev feat: native support for +middleware files Feb 18, 2026
Copy link
Member Author

@magne4000 magne4000 left a comment

Choose a reason for hiding this comment

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

@brillout I think we can have a first pass of review

@brillout
Copy link
Member

@brillout I think we can have a first pass of review

What's the scope of the current state? Asking because I ain't sure what is temporary or not.

@magne4000
Copy link
Member Author

It's feature complete. So the only temporary thing is the remaining TODO comment regarding HttpMethod typing

@brillout
Copy link
Member

E.g. the res object returned by UM isn't exposed and instead is merely used to generate a pageContext.httpResponse — I guess that's temporary?

I guess the goal is to have to distinct response types, either:

  1. The barebone httpResponse for backwards compatibility, and for fully manual integration (without UM)
  2. The full res standard HttpResponse object that the user integrates in his server entry (either via UM's apply() or manually via custom integration)

I think we can either return only 1 or only 2. Not sure whether returning both would make sense.

@brillout
Copy link
Member

Actually, I'm realizing maybe it could make sense: using UM internally to add support for +middlewares with all the current renderPage() integrations 👀

@magne4000 magne4000 force-pushed the magne4000/dev branch 2 times, most recently from 6238f15 to 560107a Compare February 18, 2026 15:54
@brillout
Copy link
Member

FYI did some minor refactoring 234f499 — should be equivalent. Bonus: the PR diff is now a lot more readable.

@brillout
Copy link
Member

Let me do a final round of reviewing and then I'm good on my side.

@brillout
Copy link
Member

LGTM (except #3106 (comment))

@magne4000 magne4000 changed the title feat: native support for +middleware files feat: native support for Universal Deploy Feb 26, 2026
@brillout brillout changed the title feat: native support for Universal Deploy feat: built-in vike-photon features (via Universal Deploy) Feb 26, 2026
magne4000 and others added 7 commits February 27, 2026 11:51
# Conflicts:
#	packages/vike/package.json
#	pnpm-lock.yaml
# Conflicts:
#	pnpm-lock.yaml
@magne4000
Copy link
Member Author

I wonder if we can/should make it completely zero-config. AFAICT it's possible, but not sure if it's what we want. I'll think about it.

I like it and IMO we should do it, with one exception: I think entries should still be added via addEntry() imported from @universal-deploy/store. It's slightly less zero-config but it shows/preserves how flexible UD is.

WDYT?

I think we should keep it as it is for now. At the moment, the main goal of UD isn't to be zero-config, but rather to support as many use cases as possible with as little code as possible. Adding configuration options like prependToGlobalEntry could give the impression that we're shaping UD specifically for Vike rather than considering broader use cases.

That might make it harder to present UD to developers from other frameworks, and in practice it doesn't really change anything for us.

@magne4000
Copy link
Member Author

Am I understanding it correctly that Netlify/Fastly/EdgeOne adopting UD simply means using @universal-deploy/store and that's the only UD dependency for them?

// node_modules/@{netlify,fastly,edgeone}/vite

// The only UD dependency:
import { store } from '@universal-deploy/store'

// The only API used by Vite deployment plugins:
store.entries.forEach(entry => { /* ... */ })

Or am I missing something?

Yes, it's just that, or even just using the catchAll plugin instead if they do not support code splitting for even less code.

@magne4000
Copy link
Member Author

How about:

- virtual:ud:catch-all
+ virtual:global-entry

In UD, I'll keep the virtual:ud prefix to follow Vite's recommendations and namespacing.

@brillout
Copy link
Member

I'd frame UD (e.g. in the README) as:

  • @universal-deploy/store => the store (basically just a globalThis array wrapped in a package)
  • @universal-deploy/vite => utility for easy integration for Vite-based frameworks, a swiss army knife

The key word here is swiss army knife. From that perspective, I think it makes sense to add conveniences to UD to make it zero-config.

If we position it as zero-config that makes UD more appealing and more trustworthy, because we signal that we're open to add features to ease the life of framework developers.

As a framework author, I don't care that much whether UD is 10% more or less code, but I will very much appreciate an easy integration.

Another way to frame UD:

  • Adopts standards like fetch and Netlify's RFC
  • Utilities to make the life of framework authors easier

I think that's an appealing "sales pitch". It revolves around making the life of the framework author easier.

The fact that UD is 10% more or less code doesn't impact the sales pitch. That's how I see it.

I don't think adding options to @universal-deploy/vite will make it seem bloated. If anything, I'd say that making it not zero-config will require more brain power for its users and make it feel more "bloated".

UD doesn't have to be zero-config for every framework (we can tell them what to do), but it should at least be zero-config for UD's flagship integration example. Showing how easy it was to add UD to Vike makes it very appealing, I think.

@brillout
Copy link
Member

How about:

- virtual:ud:catch-all
+ virtual:global-entry

In UD, I'll keep the virtual:ud prefix to follow Vite's recommendations and namespacing.

I agree internally the more verbose the virtual IDs the better for debuggability.

But for public facing virtual IDs, I think it's important we avoid users to think "what the heck is this cryptic ID". 99,9% of users won't know what UD is.

That said, it only affects Cloudflare users, so it's not that important either.

@magne4000
Copy link
Member Author

@brillout good for a new review 👍

@brillout
Copy link
Member

brillout commented Mar 18, 2026

👍 👀 A bit busy at the moment, but let me know if it's blocking you.

@rtritto
Copy link
Contributor

rtritto commented Mar 22, 2026

The migration/universal-deploy page should also include the migration of photon settings

+config.ts

export default {
-  photon: {
-    standalone: {
-      bundle: true,
-      minify: true
-    },
-    target: 'my_target'
-  }
}

I don't know where to migrate them.
It seems that standaloner will not be used.
Perhaps photon settings should be migrated to vite.config.ts.

@rtritto
Copy link
Contributor

rtritto commented Mar 22, 2026

Having two server entries, the migration should be:

+config.ts

export default {
-  photon: {
-    server: process.env.NODE_ENV === 'production'
-      ? 'server/index.ts'
-      : 'server/entry.node.ts'
-  }
}

/server/index.ts

import { Hono } from 'hono'

-import { apply } from '@photonjs/hono'
+import { addVikeMiddleware } from '@vikejs/hono'

const app = new Hono()

-apply(app)
+addVikeMiddleware(app)

export default app

/server/entry.node.ts

-import { serve } from '@photonjs/hono'

-import app from './index'

-const port = +(process.env.PORT || 3000)

-export default serve(app, { port })

/pages/+server.ts

+import app from '../server/index'

+export default {
+  fetch: app.fetch
+}

How can I change the server entry?
Pseudo code: serverEntry: process.env.NODE_ENV === 'production' ? '/server/index.ts' : '/pages/+server.ts'

@rtritto
Copy link
Contributor

rtritto commented Mar 22, 2026

Pseudo code: serverEntry: process.env.NODE_ENV === 'production' ? '/server/index.ts' : '/pages/+server.ts'

Perhaps this option should be added in +config.ts.

@brillout
Copy link
Member

@rtritto Thanks for the feedback! Although keep it mind that this isn't the official API yet. E.g. we'll remove import { addVikeMiddleware } from '@vikejs/hono' with import vike from '@vikejs/hono'.

@magne4000
Copy link
Member Author

magne4000 commented Mar 23, 2026

@rtritto good catch, thanks for letting us know. I'll complete the migration doc.

FYI, you should be able to point to different server entries doing something like this:

// +config.ts
import devEntry from '/pages/+server'
import prodEntry from '/server/index'

export default {
  server: process.env.NODE_ENV === 'production' ? prodEntry : devEntry
}

I will not document this particular use case though, as we do not recommend having 2 server entries. Could you share why you currently have those 2 servers entries?

we'll remove import { addVikeMiddleware } from '@vikejs/hono' with import vike from '@vikejs/hono'.

Done.

@rtritto
Copy link
Contributor

rtritto commented Mar 23, 2026

I will not document this particular use case though, as we do not recommend having 2 server entries. Could you share why you currently have those 2 servers entries?

In production (deployed at Vercel as serverless SSR), in the entry point (/dist/server/index.mjs), I only export the app (Hono) without serve function (app listen) and without using any vercel package.
I have to add /api/ssr.js

import app from '../dist/server/index.mjs'

export const GET = app.fetch
export const POST = app.fetch

Instead, the serve function (app.listen) is needed only in development.

@rtritto
Copy link
Contributor

rtritto commented Mar 23, 2026

FYI, you should be able to point to different server entries doing something like this:

// +config.ts
import devEntry from '/pages/+server'
import prodEntry from '/server/index'

export default {
  server: process.env.NODE_ENV === 'production' ? prodEntry : devEntry
}

In the packages/vike/src/types/Config.ts of this PR, the server option is a boolean.
I will expect the string type support:

export default {
  server: process.env.NODE_ENV === 'production' ? '/server/index.ts' : '/pages/+server.ts'
}

@magne4000
Copy link
Member Author

FYI, you should be able to point to different server entries doing something like this:

// +config.ts
import devEntry from '/pages/+server'
import prodEntry from '/server/index'

export default {
  server: process.env.NODE_ENV === 'production' ? prodEntry : devEntry
}

In the packages/vike/src/types/Config.ts of this PR, the server option is a boolean. I will expect the string type support:

export default {
  server: process.env.NODE_ENV === 'production' ? '/server/index.ts' : '/pages/+server.ts'
}

I've updated the typing. And actually, I just tested, there correct migration will be:

export default {
  server: process.env.NODE_ENV === 'production' ? 'import:./server/index.ts:default' : 'import:./pages/+server.ts:default'
}

@rtritto
Copy link
Contributor

rtritto commented Mar 23, 2026

I've updated the typing. And actually, I just tested, there correct migration will be:

export default {
  server: process.env.NODE_ENV === 'production' ? 'import:./server/index.ts:default' : 'import:./pages/+server.ts:default'
}

WDYT about to add a robust check to be smart?

if (!server.startsWith('import:')) {
  server = `import:${server}:default`
}

By this way, we can use:

export default {
  server: process.env.NODE_ENV === 'production' ? '/server/index.ts' : '/pages/+server.ts'
}

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