diff --git a/.changeset/new-taxis-camp.md b/.changeset/new-taxis-camp.md new file mode 100644 index 0000000..b51abde --- /dev/null +++ b/.changeset/new-taxis-camp.md @@ -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 diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 9ed0d87..05bd82f 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -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 } diff --git a/e2e/test-app/WebApp/ui/src/entry-server.ts b/e2e/test-app/WebApp/ui/src/entry-server.ts index f32a441..575f785 100644 --- a/e2e/test-app/WebApp/ui/src/entry-server.ts +++ b/e2e/test-app/WebApp/ui/src/entry-server.ts @@ -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 } diff --git a/packages/phoria-islands/src/client/csr.ts b/packages/phoria-islands/src/client/csr.ts index 6828123..e7b775f 100644 --- a/packages/phoria-islands/src/client/csr.ts +++ b/packages/phoria-islands/src/client/csr.ts @@ -11,10 +11,10 @@ interface PhoriaIslandCsrOptions { mode: PhoriaIslandCsrMountMode } -interface PhoriaIslandComponentCsrService { +interface PhoriaIslandComponentCsrService { mount: ( island: HTMLElement, - component: PhoriaIslandComponentEntry, + component: PhoriaIslandComponentEntry, props: PhoriaIslandProps, options?: Partial ) => Promise diff --git a/packages/phoria-islands/src/phoria-island.ts b/packages/phoria-islands/src/phoria-island.ts index 94935f8..3b936b4 100644 --- a/packages/phoria-islands/src/phoria-island.ts +++ b/packages/phoria-islands/src/phoria-island.ts @@ -20,22 +20,22 @@ type PhoriaIslandComponentLoader = | PhoriaIslandComponentModuleLoader | PhoriaIslandComponentDefaultModuleLoader -interface PhoriaIslandComponentEntry { +interface PhoriaIslandComponentEntry { name: string - framework: string + framework: F loader: PhoriaIslandComponentLoader } -interface PhoriaIslandComponent { +interface PhoriaIslandComponent { component: T componentName: string - framework: string + framework: F componentPath?: string } -async function importComponent( - componentEntry: PhoriaIslandComponentEntry -): Promise> { +async function importComponent( + componentEntry: PhoriaIslandComponentEntry +): Promise> { if (typeof componentEntry.loader === "function") { const defaultExportModule = await componentEntry.loader() diff --git a/packages/phoria-islands/src/register.ts b/packages/phoria-islands/src/register.ts index c8f7876..404e441 100644 --- a/packages/phoria-islands/src/register.ts +++ b/packages/phoria-islands/src/register.ts @@ -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>() +const ssrServiceRegistry = new Map>() -function registerSsrService(framework: string, service: PhoriaIslandComponentSsrService) { +function registerSsrService(framework: string, service: PhoriaIslandComponentSsrService) { const frameworkName = registerFramework(framework) ssrServiceRegistry.set(frameworkName, service) @@ -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>() +const csrServiceRegistry = new Map>() -function registerCsrService(framework: string, service: PhoriaIslandComponentCsrService) { +function registerCsrService(framework: string, service: PhoriaIslandComponentCsrService) { const frameworkName = registerFramework(framework) csrServiceRegistry.set(frameworkName, service) @@ -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>() +const componentRegistry = new Map>() interface PhoriaIslandComponentOptions { loader: PhoriaIslandComponentLoader diff --git a/packages/phoria-islands/src/server/phoria-island.ts b/packages/phoria-islands/src/server/phoria-island.ts index 9b1387f..5d90ef1 100644 --- a/packages/phoria-islands/src/server/phoria-island.ts +++ b/packages/phoria-islands/src/server/phoria-island.ts @@ -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 { - private component: PhoriaIslandComponentEntry - private ssr: PhoriaIslandComponentSsrService +class PhoriaIsland { + private component: PhoriaIslandComponentEntry + private ssr: PhoriaIslandComponentSsrService componentName: string props: P - framework: string + framework: F constructor( - component: PhoriaIslandComponentEntry, + component: PhoriaIslandComponentEntry, props: P, - ssr: PhoriaIslandComponentSsrService + ssr: PhoriaIslandComponentSsrService ) { this.component = component this.ssr = ssr @@ -25,7 +24,7 @@ class PhoriaIsland { this.framework = component.framework } - async render(options?: Partial>) { + async render(options?: Partial>) { return await this.ssr.render(this.component, this.props, options) } diff --git a/packages/phoria-islands/src/server/ssr.ts b/packages/phoria-islands/src/server/ssr.ts index e19f472..53a0436 100644 --- a/packages/phoria-islands/src/server/ssr.ts +++ b/packages/phoria-islands/src/server/ssr.ts @@ -12,25 +12,25 @@ interface PhoriaIslandSsrResult { html: string | ReadableStream } -interface PhoriaIslandComponentSsrService { +interface PhoriaIslandComponentSsrService { render: ( - component: PhoriaIslandComponentEntry, + component: PhoriaIslandComponentEntry, props: PhoriaIslandProps, - options?: Partial> + options?: Partial> ) => Promise } -type RenderPhoriaIslandComponent = ( - island: PhoriaIslandComponent, +type RenderPhoriaIslandComponent = ( + island: PhoriaIslandComponent, props?: P ) => string | Promise -interface RenderPhoriaIslandComponentOptions { - renderComponent: RenderPhoriaIslandComponent +interface RenderPhoriaIslandComponentOptions { + renderComponent: RenderPhoriaIslandComponent } interface PhoriaServerEntry { - renderPhoriaIsland: (island: PhoriaIsland) => Promise + renderPhoriaIsland: (island: PhoriaIsland) => Promise } export type { diff --git a/packages/phoria-react/src/client/csr.tsx b/packages/phoria-react/src/client/csr.tsx index 97eb1be..6cdd690 100644 --- a/packages/phoria-react/src/client/csr.tsx +++ b/packages/phoria-react/src/client/csr.tsx @@ -3,7 +3,7 @@ import { type PhoriaIslandComponentCsrService, csrMountMode } from "@phoria/phor import type { FunctionComponent } from "react" import { framework } from "~/main" -const service: PhoriaIslandComponentCsrService = { +const service: PhoriaIslandComponentCsrService = { 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}".`) @@ -14,7 +14,7 @@ const service: PhoriaIslandComponentCsrService = { Promise.all([ import("react").then((m) => m.default), import("react-dom/client").then((m) => m.default), - importComponent(component) + importComponent(component) ]).then(([React, ReactDOM, Island]) => { if (mode === csrMountMode.hydrate) { ReactDOM.hydrateRoot( diff --git a/packages/phoria-react/src/server/main.ts b/packages/phoria-react/src/server/main.ts index e85e64f..7022762 100644 --- a/packages/phoria-react/src/server/main.ts +++ b/packages/phoria-react/src/server/main.ts @@ -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 } diff --git a/packages/phoria-react/src/server/ssr.tsx b/packages/phoria-react/src/server/ssr.tsx index 7418c5e..aa8633a 100644 --- a/packages/phoria-react/src/server/ssr.tsx +++ b/packages/phoria-react/src/server/ssr.tsx @@ -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

= RenderPhoriaIslandComponent< + typeof framework.name, FunctionComponent, P > @@ -28,14 +29,20 @@ const renderComponentToStream: RenderReactPhoriaIslandComponent = async (island, ) } -const service: PhoriaIslandComponentSsrService = { +type ReactPhoriaIsland = PhoriaIsland + +function isReactIsland(island: PhoriaIsland): island is ReactPhoriaIsland { + return island.framework === framework.name +} + +const service: PhoriaIslandComponentSsrService = { 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) + const island = await importComponent(component) const renderComponent = options?.renderComponent ?? renderComponentToStream @@ -49,6 +56,6 @@ const service: PhoriaIslandComponentSsrService = { } } -export { service, renderComponentToStream, renderComponentToString } +export { isReactIsland, renderComponentToStream, renderComponentToString, service } -export type { RenderReactPhoriaIslandComponent } +export type { ReactPhoriaIsland, RenderReactPhoriaIslandComponent } diff --git a/packages/phoria-svelte/src/client/csr.ts b/packages/phoria-svelte/src/client/csr.ts index 6fbbcc3..6fcd6b7 100644 --- a/packages/phoria-svelte/src/client/csr.ts +++ b/packages/phoria-svelte/src/client/csr.ts @@ -3,7 +3,7 @@ import { type PhoriaIslandComponentCsrService, csrMountMode } from "@phoria/phor import type { Component } from "svelte" import { framework } from "~/main" -const service: PhoriaIslandComponentCsrService = { +const service: PhoriaIslandComponentCsrService = { 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}".`) @@ -11,17 +11,19 @@ const service: PhoriaIslandComponentCsrService = { const mode = options?.mode ?? csrMountMode.hydrate - Promise.all([import("svelte"), importComponent(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(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 }) + } + ) } } diff --git a/packages/phoria-svelte/src/server/main.ts b/packages/phoria-svelte/src/server/main.ts index f8e5fe0..b8e5153 100644 --- a/packages/phoria-svelte/src/server/main.ts +++ b/packages/phoria-svelte/src/server/main.ts @@ -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 } diff --git a/packages/phoria-svelte/src/server/ssr.ts b/packages/phoria-svelte/src/server/ssr.ts index dd31406..0bda243 100644 --- a/packages/phoria-svelte/src/server/ssr.ts +++ b/packages/phoria-svelte/src/server/ssr.ts @@ -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

= RenderPhoriaIslandComponent< + typeof framework.name, Component, P > @@ -19,14 +20,20 @@ const renderComponentToString: RenderSveltePhoriaIslandComponent = (island, prop return html.body } -const service: PhoriaIslandComponentSsrService = { +type SveltePhoriaIsland = PhoriaIsland + +function isSvelteIsland(island: PhoriaIsland): island is SveltePhoriaIsland { + return island.framework === framework.name +} + +const service: PhoriaIslandComponentSsrService = { 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) + const island = await importComponent(component) const renderComponent = options?.renderComponent ?? renderComponentToString @@ -40,6 +47,6 @@ const service: PhoriaIslandComponentSsrService = { } } -export { service, renderComponentToString } +export { isSvelteIsland, renderComponentToString, service } -export type { RenderSveltePhoriaIslandComponent } +export type { RenderSveltePhoriaIslandComponent, SveltePhoriaIsland } diff --git a/packages/phoria-vue/src/client/csr.ts b/packages/phoria-vue/src/client/csr.ts index 9cdf859..8c488b8 100644 --- a/packages/phoria-vue/src/client/csr.ts +++ b/packages/phoria-vue/src/client/csr.ts @@ -3,13 +3,13 @@ import type { PhoriaIslandComponentCsrService } from "@phoria/phoria/client" import type { Component } from "vue" import { framework } from "~/main" -const service: PhoriaIslandComponentCsrService = { +const service: PhoriaIslandComponentCsrService = { 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)]).then(([Vue, Island]) => { + Promise.all([import("vue"), importComponent(component)]).then(([Vue, Island]) => { const app = Vue.createApp(Island.component, props) app.mount(island) }) diff --git a/packages/phoria-vue/src/server/main.ts b/packages/phoria-vue/src/server/main.ts index 4630955..633674d 100644 --- a/packages/phoria-vue/src/server/main.ts +++ b/packages/phoria-vue/src/server/main.ts @@ -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 } diff --git a/packages/phoria-vue/src/server/ssr.ts b/packages/phoria-vue/src/server/ssr.ts index 6a87179..2a68c0e 100644 --- a/packages/phoria-vue/src/server/ssr.ts +++ b/packages/phoria-vue/src/server/ssr.ts @@ -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, createSSRApp } from "vue" import { renderToString, renderToWebStream } from "vue/server-renderer" import { framework } from "~/main" type RenderVuePhoriaIslandComponent

= RenderPhoriaIslandComponent< + typeof framework.name, Component, P > @@ -21,14 +22,20 @@ const renderComponentToStream: RenderVuePhoriaIslandComponent = async (island, p return renderToWebStream(app, ctx) } -const service: PhoriaIslandComponentSsrService = { +type VuePhoriaIsland = PhoriaIsland + +function isVueIsland(island: PhoriaIsland): island is VuePhoriaIsland { + return island.framework === framework.name +} + +const service: PhoriaIslandComponentSsrService = { 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) + const island = await importComponent(component) const renderComponent = options?.renderComponent ?? renderComponentToStream @@ -43,6 +50,6 @@ const service: PhoriaIslandComponentSsrService = { } } -export { service, renderComponentToStream, renderComponentToString } +export { isVueIsland, renderComponentToStream, renderComponentToString, service } -export type { RenderVuePhoriaIslandComponent } +export type { RenderVuePhoriaIslandComponent, VuePhoriaIsland }