Skip to content

Commit

Permalink
feat: improve type narrowing of Phoria Islands (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
CMeeg authored Jan 10, 2025
1 parent f2324ac commit edff1eb
Show file tree
Hide file tree
Showing 17 changed files with 120 additions and 70 deletions.
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

0 comments on commit edff1eb

Please sign in to comment.