Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .changeset/new-taxis-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@phoria/phoria": patch
"@phoria/phoria-svelte": patch
"@phoria/phoria-react": patch
"@phoria/phoria-vue": patch
---

Improve type narrowing of Phoria Islands
2 changes: 1 addition & 1 deletion docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ import "./components/register"
import type { PhoriaIsland } from "@phoria/phoria/server"

async function renderPhoriaIsland(island: PhoriaIsland) {
return island.render()
return await island.render()
}

export { renderPhoriaIsland }
Expand Down
2 changes: 1 addition & 1 deletion e2e/test-app/WebApp/ui/src/entry-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "./components/register"
import type { PhoriaIsland } from "@phoria/phoria/server"

async function renderPhoriaIsland(island: PhoriaIsland) {
return island.render()
return await island.render()
}

export { renderPhoriaIsland }
4 changes: 2 additions & 2 deletions packages/phoria-islands/src/client/csr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ interface PhoriaIslandCsrOptions {
mode: PhoriaIslandCsrMountMode
}

interface PhoriaIslandComponentCsrService<T> {
interface PhoriaIslandComponentCsrService<F extends string, T> {
mount: (
island: HTMLElement,
component: PhoriaIslandComponentEntry<PhoriaIslandComponentModule, T>,
component: PhoriaIslandComponentEntry<F, PhoriaIslandComponentModule, T>,
props: PhoriaIslandProps,
options?: Partial<PhoriaIslandCsrOptions>
) => Promise<void>
Expand Down
14 changes: 7 additions & 7 deletions packages/phoria-islands/src/phoria-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ type PhoriaIslandComponentLoader<M extends PhoriaIslandComponentModule, T> =
| PhoriaIslandComponentModuleLoader<M, T>
| PhoriaIslandComponentDefaultModuleLoader<T>

interface PhoriaIslandComponentEntry<M extends PhoriaIslandComponentModule, T> {
interface PhoriaIslandComponentEntry<F extends string, M extends PhoriaIslandComponentModule, T> {
name: string
framework: string
framework: F
loader: PhoriaIslandComponentLoader<M, T>
}

interface PhoriaIslandComponent<T> {
interface PhoriaIslandComponent<F extends string, T> {
component: T
componentName: string
framework: string
framework: F
componentPath?: string
}

async function importComponent<T>(
componentEntry: PhoriaIslandComponentEntry<PhoriaIslandComponentModule, T>
): Promise<PhoriaIslandComponent<T>> {
async function importComponent<F extends string, T>(
componentEntry: PhoriaIslandComponentEntry<F, PhoriaIslandComponentModule, T>
): Promise<PhoriaIslandComponent<F, T>> {
if (typeof componentEntry.loader === "function") {
const defaultExportModule = await componentEntry.loader()

Expand Down
10 changes: 5 additions & 5 deletions packages/phoria-islands/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ function getFrameworks() {
}

// biome-ignore lint/suspicious/noExplicitAny: The registry must be able to store any type of service
const ssrServiceRegistry = new Map<string, PhoriaIslandComponentSsrService<any>>()
const ssrServiceRegistry = new Map<string, PhoriaIslandComponentSsrService<any, any>>()

function registerSsrService<T>(framework: string, service: PhoriaIslandComponentSsrService<T>) {
function registerSsrService<F extends string, T>(framework: string, service: PhoriaIslandComponentSsrService<F, T>) {
const frameworkName = registerFramework(framework)

ssrServiceRegistry.set(frameworkName, service)
Expand All @@ -52,9 +52,9 @@ function getSsrService(framework: string) {
}

// biome-ignore lint/suspicious/noExplicitAny: The registry must be able to store any type of service
const csrServiceRegistry = new Map<string, PhoriaIslandComponentCsrService<any>>()
const csrServiceRegistry = new Map<string, PhoriaIslandComponentCsrService<any, any>>()

function registerCsrService<T>(framework: string, service: PhoriaIslandComponentCsrService<T>) {
function registerCsrService<F extends string, T>(framework: string, service: PhoriaIslandComponentCsrService<F, T>) {
const frameworkName = registerFramework(framework)

csrServiceRegistry.set(frameworkName, service)
Expand All @@ -71,7 +71,7 @@ function getCsrService(framework: string) {
}

// biome-ignore lint/suspicious/noExplicitAny: The registry must be able to store any type of component
const componentRegistry = new Map<string, PhoriaIslandComponentEntry<PhoriaIslandComponentModule, any>>()
const componentRegistry = new Map<string, PhoriaIslandComponentEntry<any, PhoriaIslandComponentModule, any>>()

interface PhoriaIslandComponentOptions<M extends PhoriaIslandComponentModule, T> {
loader: PhoriaIslandComponentLoader<M, T>
Expand Down
15 changes: 7 additions & 8 deletions packages/phoria-islands/src/server/phoria-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import type { PhoriaIslandComponentEntry, PhoriaIslandComponentModule, PhoriaIsl
import { getComponent, getSsrService } from "~/register"
import type { PhoriaIslandComponentSsrService, RenderPhoriaIslandComponentOptions } from "./ssr"

// biome-ignore lint/suspicious/noExplicitAny: The island can be any type of component
class PhoriaIsland<C = any, P extends PhoriaIslandProps = PhoriaIslandProps> {
private component: PhoriaIslandComponentEntry<PhoriaIslandComponentModule, C>
private ssr: PhoriaIslandComponentSsrService<C>
class PhoriaIsland<F extends string = string, C = unknown, P extends PhoriaIslandProps = PhoriaIslandProps> {
private component: PhoriaIslandComponentEntry<F, PhoriaIslandComponentModule, C>
private ssr: PhoriaIslandComponentSsrService<F, C>

componentName: string
props: P
framework: string
framework: F

constructor(
component: PhoriaIslandComponentEntry<PhoriaIslandComponentModule, C>,
component: PhoriaIslandComponentEntry<F, PhoriaIslandComponentModule, C>,
props: P,
ssr: PhoriaIslandComponentSsrService<C>
ssr: PhoriaIslandComponentSsrService<F, C>
) {
this.component = component
this.ssr = ssr
Expand All @@ -25,7 +24,7 @@ class PhoriaIsland<C = any, P extends PhoriaIslandProps = PhoriaIslandProps> {
this.framework = component.framework
}

async render(options?: Partial<RenderPhoriaIslandComponentOptions<C>>) {
async render(options?: Partial<RenderPhoriaIslandComponentOptions<F, C>>) {
return await this.ssr.render(this.component, this.props, options)
}

Expand Down
16 changes: 8 additions & 8 deletions packages/phoria-islands/src/server/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ interface PhoriaIslandSsrResult {
html: string | ReadableStream
}

interface PhoriaIslandComponentSsrService<T> {
interface PhoriaIslandComponentSsrService<F extends string, T> {
render: (
component: PhoriaIslandComponentEntry<PhoriaIslandComponentModule, T>,
component: PhoriaIslandComponentEntry<F, PhoriaIslandComponentModule, T>,
props: PhoriaIslandProps,
options?: Partial<RenderPhoriaIslandComponentOptions<T>>
options?: Partial<RenderPhoriaIslandComponentOptions<F, T>>
) => Promise<PhoriaIslandSsrResult>
}

type RenderPhoriaIslandComponent<C, P extends PhoriaIslandProps = PhoriaIslandProps> = (
island: PhoriaIslandComponent<C>,
type RenderPhoriaIslandComponent<F extends string, C, P extends PhoriaIslandProps = PhoriaIslandProps> = (
island: PhoriaIslandComponent<F, C>,
props?: P
) => string | Promise<string | ReadableStream>

interface RenderPhoriaIslandComponentOptions<C> {
renderComponent: RenderPhoriaIslandComponent<C>
interface RenderPhoriaIslandComponentOptions<F extends string, C> {
renderComponent: RenderPhoriaIslandComponent<F, C>
}

interface PhoriaServerEntry {
renderPhoriaIsland: (island: PhoriaIsland<unknown>) => Promise<PhoriaIslandSsrResult>
renderPhoriaIsland: (island: PhoriaIsland) => Promise<PhoriaIslandSsrResult>
}

export type {
Expand Down
4 changes: 2 additions & 2 deletions packages/phoria-react/src/client/csr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type PhoriaIslandComponentCsrService, csrMountMode } from "@phoria/phor
import type { FunctionComponent } from "react"
import { framework } from "~/main"

const service: PhoriaIslandComponentCsrService<FunctionComponent> = {
const service: PhoriaIslandComponentCsrService<typeof framework.name, FunctionComponent> = {
mount: async (island, component, props, options) => {
if (component.framework !== framework.name) {
throw new Error(`${framework.name} cannot render the ${component.framework} component named "${component.name}".`)
Expand All @@ -14,7 +14,7 @@ const service: PhoriaIslandComponentCsrService<FunctionComponent> = {
Promise.all([
import("react").then((m) => m.default),
import("react-dom/client").then((m) => m.default),
importComponent<FunctionComponent>(component)
importComponent<typeof framework.name, FunctionComponent>(component)
]).then(([React, ReactDOM, Island]) => {
if (mode === csrMountMode.hydrate) {
ReactDOM.hydrateRoot(
Expand Down
13 changes: 10 additions & 3 deletions packages/phoria-react/src/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { registerSsrService } from "@phoria/phoria"
import { framework } from "~/main"
import { type RenderReactPhoriaIslandComponent, renderComponentToStream, renderComponentToString, service } from "./ssr"
import {
type ReactPhoriaIsland,
type RenderReactPhoriaIslandComponent,
isReactIsland,
renderComponentToStream,
renderComponentToString,
service
} from "./ssr"

registerSsrService(framework.name, service)

export { renderComponentToStream, renderComponentToString }
export { isReactIsland, renderComponentToStream, renderComponentToString }

export type { RenderReactPhoriaIslandComponent }
export type { ReactPhoriaIsland, RenderReactPhoriaIslandComponent }
17 changes: 12 additions & 5 deletions packages/phoria-react/src/server/ssr.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { type PhoriaIslandProps, importComponent } from "@phoria/phoria"
import type { PhoriaIslandComponentSsrService, RenderPhoriaIslandComponent } from "@phoria/phoria/server"
import type { PhoriaIsland, PhoriaIslandComponentSsrService, RenderPhoriaIslandComponent } from "@phoria/phoria/server"
import { type FunctionComponent, StrictMode } from "react"
import { renderToString } from "react-dom/server"
import { renderToReadableStream } from "react-dom/server.edge"
import { framework } from "~/main"

type RenderReactPhoriaIslandComponent<P extends PhoriaIslandProps = PhoriaIslandProps> = RenderPhoriaIslandComponent<
typeof framework.name,
FunctionComponent,
P
>
Expand All @@ -28,14 +29,20 @@ const renderComponentToStream: RenderReactPhoriaIslandComponent = async (island,
)
}

const service: PhoriaIslandComponentSsrService<FunctionComponent> = {
type ReactPhoriaIsland = PhoriaIsland<typeof framework.name, FunctionComponent>

function isReactIsland(island: PhoriaIsland): island is ReactPhoriaIsland {
return island.framework === framework.name
}

const service: PhoriaIslandComponentSsrService<typeof framework.name, FunctionComponent> = {
render: async (component, props, options) => {
if (component.framework !== framework.name) {
throw new Error(`${framework.name} cannot render the ${component.framework} component named "${component.name}".`)
}

// TODO: Can "cache" the imported component? Maybe only in production?
const island = await importComponent<FunctionComponent>(component)
const island = await importComponent<typeof framework.name, FunctionComponent>(component)

const renderComponent = options?.renderComponent ?? renderComponentToStream

Expand All @@ -49,6 +56,6 @@ const service: PhoriaIslandComponentSsrService<FunctionComponent> = {
}
}

export { service, renderComponentToStream, renderComponentToString }
export { isReactIsland, renderComponentToStream, renderComponentToString, service }

export type { RenderReactPhoriaIslandComponent }
export type { ReactPhoriaIsland, RenderReactPhoriaIslandComponent }
22 changes: 12 additions & 10 deletions packages/phoria-svelte/src/client/csr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ import { type PhoriaIslandComponentCsrService, csrMountMode } from "@phoria/phor
import type { Component } from "svelte"
import { framework } from "~/main"

const service: PhoriaIslandComponentCsrService<Component> = {
const service: PhoriaIslandComponentCsrService<typeof framework.name, Component> = {
mount: async (island, component, props, options) => {
if (component.framework !== framework.name) {
throw new Error(`${framework.name} cannot render the ${component.framework} component named "${component.name}".`)
}

const mode = options?.mode ?? csrMountMode.hydrate

Promise.all([import("svelte"), importComponent<Component>(component)]).then(([Svelte, Island]) => {
// biome-ignore lint/complexity/noBannedTypes: Must match expected props type
const svelteProps = typeof props === "object" ? (props as {}) : undefined
Promise.all([import("svelte"), importComponent<typeof framework.name, Component>(component)]).then(
([Svelte, Island]) => {
// biome-ignore lint/complexity/noBannedTypes: Must match expected props type
const svelteProps = typeof props === "object" ? (props as {}) : undefined

if (mode === csrMountMode.hydrate) {
Svelte.hydrate(Island.component, { target: island, props: svelteProps })
return
}
if (mode === csrMountMode.hydrate) {
Svelte.hydrate(Island.component, { target: island, props: svelteProps })
return
}

Svelte.mount(Island.component, { target: island, props: svelteProps })
})
Svelte.mount(Island.component, { target: island, props: svelteProps })
}
)
}
}

Expand Down
12 changes: 9 additions & 3 deletions packages/phoria-svelte/src/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { registerSsrService } from "@phoria/phoria"
import { framework } from "~/main"
import { type RenderSveltePhoriaIslandComponent, renderComponentToString, service } from "./ssr"
import {
type RenderSveltePhoriaIslandComponent,
type SveltePhoriaIsland,
isSvelteIsland,
renderComponentToString,
service
} from "./ssr"

registerSsrService(framework.name, service)

export { renderComponentToString }
export { isSvelteIsland, renderComponentToString }

export type { RenderSveltePhoriaIslandComponent }
export type { RenderSveltePhoriaIslandComponent, SveltePhoriaIsland }
17 changes: 12 additions & 5 deletions packages/phoria-svelte/src/server/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { type PhoriaIslandProps, importComponent } from "@phoria/phoria"
import type { PhoriaIslandComponentSsrService, RenderPhoriaIslandComponent } from "@phoria/phoria/server"
import type { PhoriaIsland, PhoriaIslandComponentSsrService, RenderPhoriaIslandComponent } from "@phoria/phoria/server"
import type { Component, ComponentProps } from "svelte"
import { render } from "svelte/server"
import { framework } from "~/main"

type RenderSveltePhoriaIslandComponent<P extends PhoriaIslandProps = PhoriaIslandProps> = RenderPhoriaIslandComponent<
typeof framework.name,
Component,
P
>
Expand All @@ -19,14 +20,20 @@ const renderComponentToString: RenderSveltePhoriaIslandComponent = (island, prop
return html.body
}

const service: PhoriaIslandComponentSsrService<Component> = {
type SveltePhoriaIsland = PhoriaIsland<typeof framework.name, Component>

function isSvelteIsland(island: PhoriaIsland): island is SveltePhoriaIsland {
return island.framework === framework.name
}

const service: PhoriaIslandComponentSsrService<typeof framework.name, Component> = {
render: async (component, props, options) => {
if (component.framework !== framework.name) {
throw new Error(`${framework.name} cannot render the ${component.framework} component named "${component.name}".`)
}

// TODO: Can "cache" the imported component? Maybe only in production?
const island = await importComponent<Component>(component)
const island = await importComponent<typeof framework.name, Component>(component)

const renderComponent = options?.renderComponent ?? renderComponentToString

Expand All @@ -40,6 +47,6 @@ const service: PhoriaIslandComponentSsrService<Component> = {
}
}

export { service, renderComponentToString }
export { isSvelteIsland, renderComponentToString, service }

export type { RenderSveltePhoriaIslandComponent }
export type { RenderSveltePhoriaIslandComponent, SveltePhoriaIsland }
4 changes: 2 additions & 2 deletions packages/phoria-vue/src/client/csr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import type { PhoriaIslandComponentCsrService } from "@phoria/phoria/client"
import type { Component } from "vue"
import { framework } from "~/main"

const service: PhoriaIslandComponentCsrService<Component> = {
const service: PhoriaIslandComponentCsrService<typeof framework.name, Component> = {
mount: async (island, component, props) => {
if (component.framework !== framework.name) {
throw new Error(`${framework.name} cannot render the ${component.framework} component named "${component.name}".`)
}

Promise.all([import("vue"), importComponent<Component>(component)]).then(([Vue, Island]) => {
Promise.all([import("vue"), importComponent<typeof framework.name, Component>(component)]).then(([Vue, Island]) => {
const app = Vue.createApp(Island.component, props)
app.mount(island)
})
Expand Down
13 changes: 10 additions & 3 deletions packages/phoria-vue/src/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { registerSsrService } from "@phoria/phoria"
import { framework } from "~/main"
import { type RenderVuePhoriaIslandComponent, renderComponentToStream, renderComponentToString, service } from "./ssr"
import {
type RenderVuePhoriaIslandComponent,
type VuePhoriaIsland,
isVueIsland,
renderComponentToStream,
renderComponentToString,
service
} from "./ssr"

registerSsrService(framework.name, service)

export { renderComponentToStream, renderComponentToString }
export { isVueIsland, renderComponentToStream, renderComponentToString }

export type { RenderVuePhoriaIslandComponent }
export type { RenderVuePhoriaIslandComponent, VuePhoriaIsland }
Loading
Loading