Skip to content

feat: improve core to support vercel #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 83 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
22648f9
feat: possibility to override dev env
magne4000 May 28, 2025
2aa7ff2
fix: help vite importer
magne4000 May 28, 2025
7010e73
chore: add photon logo to tests
magne4000 Jun 3, 2025
4c02cd4
refactor: better Error management
magne4000 Jun 3, 2025
14e6ab5
feat: UM entries can be standalone
magne4000 Jun 4, 2025
d0134cf
refactor: virtual apply
magne4000 Jun 4, 2025
52bfe83
feat: apply for a unique handler
magne4000 Jun 4, 2025
f12b1f7
feat: support for server entry with exclusive handler
magne4000 Jun 4, 2025
53eee26
fix: ensure proper resolution of handlers in adapters
magne4000 Jun 4, 2025
8cf747d
fix: early resolve photon meta
magne4000 Jun 4, 2025
33dc8ab
feat: new getPhotonServerEntryId util
magne4000 Jun 4, 2025
8c4f465
fix: dead-lock
magne4000 Jun 4, 2025
39ad723
chore: cleanup
magne4000 Jun 4, 2025
9fc38a1
refactor
magne4000 Jun 4, 2025
065ae75
chore: simplify early meta resolution
magne4000 Jun 4, 2025
8351422
chore: remove direct rollup references + cloudflare deps upgrade
magne4000 Jun 4, 2025
970eb25
chore: lint
magne4000 Jun 4, 2025
2e83f04
fix: bun and deno conditions
magne4000 Jun 10, 2025
4c79c26
test: bun
magne4000 Jun 10, 2025
093266e
feat: add support for dev env emulation per handler
magne4000 Jun 10, 2025
bb913d9
chore: cleanup
magne4000 Jun 10, 2025
1e738e4
fix: upgrade deps
magne4000 Jun 10, 2025
8809dfe
fix: PhotonMeta type
magne4000 Jun 11, 2025
7b2e48f
fix: handle query parameter checks in Photon server entry resolution
magne4000 Jun 12, 2025
d1228ad
feat: support resolving Photon server entry with handler configuration
magne4000 Jun 12, 2025
d7447e7
fix: module resolution
magne4000 Jun 12, 2025
7a3e429
fix: refine Photon server and handler resolution logic
magne4000 Jun 12, 2025
8e70263
feat: introduce `getPhotonMeta` utility for streamlined Photon meta r…
magne4000 Jun 12, 2025
1b7874a
fix: update Cloudflare plugin to use `getPhotonMeta`
magne4000 Jun 12, 2025
ba56522
chore: simplify fallback server entry
magne4000 Jun 12, 2025
94b889d
chore: replace arktype with zod
magne4000 Jun 16, 2025
87e2d23
chore
magne4000 Jun 16, 2025
d764f2f
refactor: centralize mergeable namespace
magne4000 Jun 16, 2025
ce2d17d
chore: add name prop onto Photon entries
magne4000 Jun 16, 2025
6a5750f
chore
magne4000 Jun 16, 2025
c3136c8
chore
magne4000 Jun 16, 2025
98d1a09
feat: add additionalServerConfigs config
magne4000 Jun 18, 2025
f00ba48
chore
magne4000 Jun 18, 2025
ea1bd26
chore
magne4000 Jun 18, 2025
334eb35
feat: new api helpers
magne4000 Jun 18, 2025
f52bc7a
feat: new api helper ensureUniqueEntry
magne4000 Jun 18, 2025
8b823d9
feat: new api helper stripUniqueEntry
magne4000 Jun 18, 2025
2cc5060
feat: new api helper stripUniqueEntry
magne4000 Jun 18, 2025
7835dc8
chore
magne4000 Jun 18, 2025
da2d121
chore: introduce server-with-config entries
magne4000 Jun 20, 2025
760668c
chore: server-entry-with-config loader
magne4000 Jun 25, 2025
5f3addf
chore
magne4000 Jun 25, 2025
39dd7c2
fix: ensure uniq ref to photon config
magne4000 Jun 25, 2025
c75e3df
fix: define entries as loose objects
magne4000 Jun 25, 2025
38ae054
fix: define entries as loose objects
magne4000 Jun 25, 2025
7c37631
chore: setting moduleSideEffects in virtual modules
magne4000 Jun 25, 2025
e1225e5
feat: new defaultBuildEnv config
magne4000 Jun 30, 2025
cb283b0
chore: filter injected handlers
magne4000 Jun 30, 2025
cb6dcd2
fix: HMR should work when server is running in any env
magne4000 Jun 30, 2025
b80ebde
chore: run devServer plugin later on
magne4000 Jun 30, 2025
a85dffb
revert: always inject middlewares in server instead of vite
magne4000 Jul 2, 2025
624855d
fix: remove enforced moduleSideEffects
magne4000 Jul 3, 2025
c3868c6
feat: trying a new plugin deduplication implementation
magne4000 Jul 7, 2025
565538a
refactor: replace codegen by actual files
magne4000 Jul 8, 2025
8c23b3e
chore: merge virtual-apply and get-middleware
magne4000 Jul 8, 2025
af62306
fix: transform plugins
magne4000 Jul 8, 2025
aeee351
feat: add static context onto handlers
magne4000 Jul 22, 2025
0f0a572
refactor: merging additionalServerConfig and handlers under entries
magne4000 Jul 23, 2025
5568a12
chore
magne4000 Jul 24, 2025
1e0ef1b
chore: forbid server-config entries in non-code splitted projects
magne4000 Jul 24, 2025
e71a55b
refactor: only merge photon configs once configResolved is called
magne4000 Jul 28, 2025
1c82c37
fix: split photon:resolve-config plugin
magne4000 Jul 28, 2025
c55905f
chore: minor optimization
magne4000 Jul 28, 2025
1720ddc
fix: ensure that addPhotonEntry is not called after buildStart hook
magne4000 Jul 28, 2025
bfec832
fix: prefer restart hmr mode if Bun or Deno
magne4000 Jul 28, 2025
e4118f3
feat: export { targetLoader } plugin
magne4000 Jul 29, 2025
7f05771
chore: escapeStringRegexp util
magne4000 Jul 29, 2025
a71e9de
refactor: merge entry resolvers into 1 plugin
magne4000 Jul 29, 2025
d46102f
chore: resolved defaultBuildEnv is not optional anymore
magne4000 Jul 29, 2025
787d1c7
doc(example): comments
magne4000 Jul 29, 2025
97513ea
fix: types
magne4000 Jul 29, 2025
5cb31c9
chore: extend targetLoader
magne4000 Jul 29, 2025
10804cc
chore: make resolvedPhotonConfig global
magne4000 Jul 29, 2025
ed36e70
chore: emit entries that have explicit target defined
magne4000 Jul 29, 2025
a27932b
fix: do not try to code split too early to not loose information
magne4000 Jul 30, 2025
c7d7ab1
chore: more comprehensive codeSplitting option
magne4000 Jul 30, 2025
761b2ce
fix: isTargetEntry
magne4000 Jul 31, 2025
877032a
fix: isTargetEntry
magne4000 Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions example/app-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"@photonjs/core": "workspace:",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"wrangler": "^4.15.1"
"wrangler": "^4.19.1"
},
"dependencies": {
"@universal-middleware/core": "^0.4.7",
"@universal-middleware/core": "^0.4.9",
"awesome-framework": "workspace:",
"hono": "^4.7.8"
}
Expand Down
5 changes: 5 additions & 0 deletions example/app-cloudflare/testRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ function testRun(cmd: `pnpm run ${string}`) {
expect(await response.text()).toBe('The API Route')
})

test('/standalone', async () => {
const response: Response = await fetch(`${getServerUrl()}/standalone`)
expect(await response.text()).toBe('The /standalone Route')
})

test('/bar', async () => {
const response: Response = await fetch(`${getServerUrl()}/bar`)
expect(await response.text()).toBe('bar')
Expand Down
4 changes: 2 additions & 2 deletions example/app-cloudflare/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference types="@photonjs/core/api" />
/// <reference types="@photonjs/core" />
/* The Vite plugin cloudflare() will be replaced by this:
import cloudflare from '@photonjs/cloudflare'
*/
Expand All @@ -10,7 +10,7 @@ export default defineConfig(({ mode }) => {
return {
// No photon server entry is defined, it will fallback to a virtual entry
photon: {
handlers: {
entries: {
// foo entry declares its route with `enhance` directly inside the file
foo: 'src/middlewares/foo.ts',
// bar entry route is declared here, and `enhance` is not used
Expand Down
2 changes: 1 addition & 1 deletion example/app-hono-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@photonjs/core": "workspace:",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"wrangler": "^4.15.1"
"wrangler": "^4.19.1"
},
"dependencies": {
"awesome-framework": "workspace:",
Expand Down
8 changes: 2 additions & 6 deletions example/app-hono-cloudflare/server.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// Will be moved to @photonjs/hono
import { apply, serve } from '@photonjs/core/hono'
import awesomeFramework from 'awesome-framework/universal-middleware'
import { Hono } from 'hono'

async function startServer() {
const app = new Hono()

apply(
app,
// Adds the framework's middlewares
awesomeFramework,
)
// awesomeFramework will automatically be injected by apply
apply(app)

return serve(app)
}
Expand Down
1 change: 0 additions & 1 deletion example/app-hono-cloudflare/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// <reference types="@photonjs/core/api" />
/* The Vite plugin cloudflare() will be replaced by this:
import cloudflare from '@photonjs/cloudflare'
*/
Expand Down
3 changes: 2 additions & 1 deletion example/awesome-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"dependencies": {
"@photonjs/core": "workspace:",
"@universal-middleware/core": "^0.4.7",
"@universal-middleware/core": "^0.4.9",
"@universal-middleware/sirv": "^0.1.20"
},
"devDependencies": {
Expand All @@ -21,6 +21,7 @@
"./entry-client": "./dist/entry-client.js",
"./render": "./dist/render.js",
"./vite": "./dist/vite/index.js",
"./standalone": "./dist/photon/entries/standalone.js",
"./universal-middleware": {
"types": "./dist/photon/entries/prod.d.ts",
"development": {
Expand Down
21 changes: 21 additions & 0 deletions example/awesome-framework/src/photon/entries/standalone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { enhance } from '@universal-middleware/core'

const standaloneMiddleware = enhance(
async () => {
return new Response('The /standalone Route', {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
})
},
// enhance() adds meta data (a Universal Middleware in itself is just a Request => Response function)
{
name: 'awesome-framework:standalone',
path: '/standalone',
method: 'GET',
},
)

// This middleware will be used as a standalone handler, which means it is not included by `./universal-middleware` export
export default standaloneMiddleware
4 changes: 2 additions & 2 deletions example/awesome-framework/src/photon/middlewares/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { enhance, MiddlewareOrder } from '@universal-middleware/core'

export const loggerMiddleware = enhance(
(request: Request) => {
console.log('Request:', request.url)
(request: Request, ctx: Universal.Context) => {
console.log('Request:', request.url, 'Context:', ctx)
},
// enhance() adds meta data (a Universal Middleware in itself is just a Request => Response function)
{
Expand Down
20 changes: 20 additions & 0 deletions example/awesome-framework/src/vite/photonPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,29 @@ import type { Plugin } from 'vite'

export function photonPlugin(): Plugin[] {
return installPhoton('awesome-framework', {
// Enables full installation of all Photon plugins
fullInstall: true,

// Disables code-splitting functionality for testing purposes
codeSplitting: {
framework: false,
},

// Always use those middlewares for all entries defined by Photon
resolveMiddlewares() {
return 'awesome-framework/universal-middleware'
},

// Configuration for entry points used by Photon
entries: {
standalone: {
// Unique identifier for this entry point. Is resolved by Vite
id: 'awesome-framework/standalone',

// Sets composition mode to 'isolated', preventing it from
// being composed with middlewares defined in `resolveMiddlewares`
compositionMode: 'isolated',
},
},
})
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"========= Build": "",
"build": "pnpm run build:photon && pnpm run build:awesome-framework",
"build:photon": "pnpm --recursive --filter {packages/*} run build",
"build:awesome-framework": "cd example/awesome-framework/ && pnpm run build",
"build:awesome-framework": "pnpm --filter {example/awesome-framework} run build",
"========= Test": "",
"test": "test-e2e",
"========= Formatting": "",
Expand All @@ -22,5 +22,10 @@
"playwright": "^1.52.0",
"prettier": "^3.2.5"
},
"pnpm": {
"overrides": {
"@types/node": "^20.17.32"
}
},
"packageManager": "[email protected]"
}
12 changes: 6 additions & 6 deletions packages/adapter-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
"release:commit": "release-me commit"
},
"dependencies": {
"@cloudflare/vite-plugin": "^1.0.12",
"@cloudflare/vite-plugin": "^1.5.0",
"@photonjs/core": "workspace:^",
"@universal-middleware/cloudflare": "^0.4.8"
"@universal-middleware/cloudflare": "^0.4.10"
},
"devDependencies": {
"@brillout/release-me": "^0.4.3",
"@cloudflare/workers-types": "^4.20250515.0",
"@universal-middleware/core": "^0.4.7",
"@universal-middleware/h3": "^0.4.9",
"@universal-middleware/hono": "^0.4.10",
"@cloudflare/workers-types": "^4.20250604.0",
"@universal-middleware/core": "^0.4.9",
"@universal-middleware/h3": "^0.4.12",
"@universal-middleware/hono": "^0.4.15",
"crossws": "^0.3.5",
"h3": "^1.15.1",
"tsup": "^8.4.0",
Expand Down
9 changes: 4 additions & 5 deletions packages/adapter-cloudflare/src/adapters/dev.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { ExportedHandlerFetchHandler } from '@cloudflare/workers-types'
import { createIncompatibleServerError, createMissingApplyError, createMissingExportError } from '../utils/errors.js'

export function asFetch(app: unknown, id: string): ExportedHandlerFetchHandler {
if (!app) {
throw new Error(`[photon] Missing export default in ${JSON.stringify(id)}`)
throw createMissingExportError(id)
}

const server = (app as Record<symbol, string | undefined>)[Symbol.for('photon:server')]

if (!server) {
throw new Error('[photon] { apply } function needs to be called before export')
throw createMissingApplyError()
}

switch (server) {
Expand All @@ -30,7 +31,5 @@ export function asFetch(app: unknown, id: string): ExportedHandlerFetchHandler {
}
}

throw new Error(
`[photon] Clouflare target is not compatible with server "${server}". We recommend using "hono" instead.`,
)
throw createIncompatibleServerError(server)
}
3 changes: 2 additions & 1 deletion packages/adapter-cloudflare/src/adapters/h3.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ExportedHandlerFetchHandler, Response } from '@cloudflare/workers-types'
import type { apply as applyAdapter } from '@universal-middleware/h3'
import { toWebHandler } from 'h3'
import { createMissingDependencyError } from '../utils/errors.js'

async function tryImportCrossws() {
try {
const { default: wsAdapter } = await import('crossws/adapters/cloudflare')
return wsAdapter
} catch (e) {
throw new Error('crossws is not installed', { cause: e })
throw createMissingDependencyError('crossws', e)
}
}

Expand Down
94 changes: 27 additions & 67 deletions packages/adapter-cloudflare/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
/// <reference types="@photonjs/core/api" />
import { cloudflare as cloudflareVitePlugins, type PluginConfig } from '@cloudflare/vite-plugin'
import { isPhotonMeta } from '@photonjs/core/api'
import { supportedTargetServers } from '@photonjs/core/vite'
import { supportedTargetServers, targetLoader } from '@photonjs/core/vite'
import type { Plugin } from 'vite'

const moduleId = 'photon:cloudflare'
const virtualModuleId = `\0${moduleId}`

// TODO: create actual virtual Target Entries for each server
export function cloudflare(config?: Omit<PluginConfig, 'viteEnvironment'>): Plugin[] {
return [
{
Expand All @@ -19,88 +15,52 @@ export function cloudflare(config?: Omit<PluginConfig, 'viteEnvironment'>): Plug
photon: {
// @cloudflare/vite-plugin has its own dev server
devServer: false,
codeSplitting: {
target: false,
},
// Should be set to the value of cloudflareVitePlugins -> viteEnvironment.name
// defaultBuildEnv: 'cloudflare',
},
}
},
},
},
supportedTargetServers('cloudflare', ['hono', 'h3']),
{
name: `${moduleId}:resolver`,

async resolveId(id, importer, opts) {
if (id.startsWith(moduleId)) {
const resolved = await this.resolve(id.replace(/^photon:cloudflare:/, ''), importer, opts)

if (!resolved) {
return this.error(`[photon][cloudflare] Cannot resolve ${id}`)
}

return {
...resolved,
id: `${virtualModuleId}:${resolved.id}`,
}
}
},

// TODO add tests
async load(id) {
if (!id.startsWith(virtualModuleId)) return
const actualId = id.slice(virtualModuleId.length + 1)

const info = this.getModuleInfo(id)

if (!isPhotonMeta(info?.meta)) {
return this.error(`[photon][cloudflare] ${actualId} is not a Photon entry`)
}

...targetLoader('cloudflare', {
async load(id, { meta }) {
const isDev = this.environment.config.command === 'serve'

if (info.meta.photon.type === 'server') {
// `server` usually exists only during build time
if (info.meta.photon.server) {
return {
// language=ts
code: `import serverEntry from ${JSON.stringify(actualId)};
import { asFetch } from "@photonjs/cloudflare/${info.meta.photon.server}";
// `server` usually exists only during build time
if (meta.server) {
return {
// language=ts
code: `import serverEntry from ${JSON.stringify(id)};
import { asFetch } from "@photonjs/cloudflare/${meta.server}";

export const fetch = asFetch(serverEntry);
export default { fetch };
`,
map: { mappings: '' },
}
export const fetch = asFetch(serverEntry);
export default { fetch };
`,
map: { mappings: '' },
}
}

if (isDev) {
return {
// language=ts
code: `import serverEntry from ${JSON.stringify(actualId)};
import { asFetch } from "@photonjs/cloudflare/dev";

export const fetch = asFetch(serverEntry, ${JSON.stringify(actualId)});
export default { fetch };
`,
map: { mappings: '' },
}
}
} else {
if (isDev) {
return {
// language=ts
code: `import handler from ${JSON.stringify(actualId)};
import { getRuntime } from "@universal-middleware/cloudflare";
code: `import serverEntry from ${JSON.stringify(id)};
import { asFetch } from "@photonjs/cloudflare/dev";

export const fetch = (request, env, ctx) => {
return handler(request, {}, getRuntime(env, ctx))
};
export const fetch = asFetch(serverEntry, ${JSON.stringify(id)});
export default { fetch };
`,
map: { mappings: '' },
}
}

return this.error(`[photon][cloudflare] Unable to load ${actualId}`)
return this.error(`[photon][cloudflare] Unable to load ${id}`)
},
},
}),
supportedTargetServers('cloudflare', ['hono', 'h3']),
// FIXME do not enforce ssr env?
...cloudflareVitePlugins({ ...config, viteEnvironment: { name: 'ssr' } }),
]
}
28 changes: 28 additions & 0 deletions packages/adapter-cloudflare/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PhotonBugError, PhotonConfigError, PhotonDependencyError, PhotonUsageError } from '@photonjs/core/errors'

// Utility functions for common error scenarios
export function createMissingExportError(id: string): PhotonUsageError {
return new PhotonUsageError(`Missing export default in ${JSON.stringify(id)}`)
}

export function createMissingApplyError(): PhotonUsageError {
return new PhotonUsageError('{ apply } function needs to be called before export')
}

export function createIncompatibleServerError(server: string): PhotonConfigError {
return new PhotonConfigError(
`Cloudflare target is not compatible with server "${server}". We recommend using "hono" instead.`,
)
}

export function createMissingDependencyError(dependency: string, cause?: unknown): PhotonDependencyError {
return new PhotonDependencyError(
`${dependency} is not installed. Please install ${dependency} to use this functionality with Cloudflare.`,
{ cause },
)
}

export function assert(condition: unknown): asserts condition {
if (condition) return
throw new PhotonBugError()
}
Loading