diff options
32 files changed, 222 insertions, 158 deletions
diff --git a/.changeset/better-carrots-attend.md b/.changeset/better-carrots-attend.md new file mode 100644 index 000000000..055de3c8f --- /dev/null +++ b/.changeset/better-carrots-attend.md @@ -0,0 +1,7 @@ +--- +'@astrojs/svelte': patch +'@astrojs/react': patch +'@astrojs/vue': patch +--- + +Improves type-safety of renderers diff --git a/packages/integrations/react/env.d.ts b/packages/integrations/react/env.d.ts new file mode 100644 index 000000000..21e68d939 --- /dev/null +++ b/packages/integrations/react/env.d.ts @@ -0,0 +1,8 @@ +declare module 'astro:react:opts' { + type Options = Pick< + import('./src/index.js').ReactIntegrationOptions, + 'experimentalDisableStreaming' | 'experimentalReactChildren' + >; + const options: Options; + export = options; +}
\ No newline at end of file diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index 77a897a7e..ddae8825a 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -22,25 +22,15 @@ "exports": { ".": "./dist/index.js", "./actions": "./dist/actions.js", - "./client.js": "./client.js", - "./client-v17.js": "./client-v17.js", - "./server.js": "./server.js", - "./server-v17.js": "./server-v17.js", + "./client.js": "./dist/client.js", + "./client-v17.js": "./dist/client-v17.js", + "./server.js": "./dist/server.js", + "./server-v17.js": "./dist/server-v17.js", "./package.json": "./package.json", - "./jsx-runtime": "./jsx-runtime.js" + "./jsx-runtime": "./dist/jsx-runtime.js" }, "files": [ - "dist", - "client.js", - "client-v17.js", - "context.js", - "jsx-runtime.js", - "server.js", - "server.d.ts", - "server-v17.js", - "server-v17.d.ts", - "static-html.js", - "vnode-children.js" + "dist" ], "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc", diff --git a/packages/integrations/react/server.d.ts b/packages/integrations/react/server.d.ts deleted file mode 100644 index 75cc3eb64..000000000 --- a/packages/integrations/react/server.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { NamedSSRLoadedRendererValue } from 'astro'; - -declare const renderer: NamedSSRLoadedRendererValue; -export default renderer; diff --git a/packages/integrations/react/server17.d.ts b/packages/integrations/react/server17.d.ts deleted file mode 100644 index bb2f29556..000000000 --- a/packages/integrations/react/server17.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { NamedSSRLoadedRendererValue } from 'astro'; -export default NamedSSRLoadedRendererValue; diff --git a/packages/integrations/react/client-v17.js b/packages/integrations/react/src/client-v17.ts index bd17050ea..4ba4bcf60 100644 --- a/packages/integrations/react/client-v17.js +++ b/packages/integrations/react/src/client-v17.ts @@ -2,8 +2,13 @@ import { createElement } from 'react'; import { hydrate, render, unmountComponentAtNode } from 'react-dom'; import StaticHtml from './static-html.js'; -export default (element) => - (Component, props, { default: children, ...slotted }, { client }) => { +export default (element: HTMLElement) => + ( + Component: any, + props: Record<string, any>, + { default: children, ...slotted }: Record<string, any>, + { client }: Record<string, string>, + ) => { for (const [key, value] of Object.entries(slotted)) { props[key] = createElement(StaticHtml, { value, name: key }); } diff --git a/packages/integrations/react/client.js b/packages/integrations/react/src/client.ts index 044eaf26f..ea187a1a1 100644 --- a/packages/integrations/react/client.js +++ b/packages/integrations/react/src/client.ts @@ -1,17 +1,17 @@ import { createElement, startTransition } from 'react'; -import { createRoot, hydrateRoot } from 'react-dom/client'; +import { type Root, createRoot, hydrateRoot } from 'react-dom/client'; import StaticHtml from './static-html.js'; -function isAlreadyHydrated(element) { +function isAlreadyHydrated(element: HTMLElement) { for (const key in element) { if (key.startsWith('__reactContainer')) { - return key; + return key as keyof HTMLElement; } } } -function createReactElementFromDOMElement(element) { - let attrs = {}; +function createReactElementFromDOMElement(element: any): any { + let attrs: Record<string, string> = {}; for (const attr of element.attributes) { attrs[attr.name] = attr.value; } @@ -24,7 +24,7 @@ function createReactElementFromDOMElement(element) { element.localName, attrs, Array.from(element.childNodes) - .map((c) => { + .map((c: any) => { if (c.nodeType === Node.TEXT_NODE) { return c.data; } else if (c.nodeType === Node.ELEMENT_NODE) { @@ -37,7 +37,7 @@ function createReactElementFromDOMElement(element) { ); } -function getChildren(childString, experimentalReactChildren) { +function getChildren(childString: string, experimentalReactChildren: boolean) { if (experimentalReactChildren && childString) { let children = []; let template = document.createElement('template'); @@ -54,8 +54,8 @@ function getChildren(childString, experimentalReactChildren) { } // Keep a map of roots so we can reuse them on re-renders -let rootMap = new WeakMap(); -const getOrCreateRoot = (element, creator) => { +let rootMap = new WeakMap<HTMLElement, Root>(); +const getOrCreateRoot = (element: HTMLElement, creator: () => Root) => { let root = rootMap.get(element); if (!root) { root = creator(); @@ -64,8 +64,13 @@ const getOrCreateRoot = (element, creator) => { return root; }; -export default (element) => - (Component, props, { default: children, ...slotted }, { client }) => { +export default (element: HTMLElement) => + ( + Component: any, + props: Record<string, any>, + { default: children, ...slotted }: Record<string, any>, + { client }: Record<string, string>, + ) => { if (!element.hasAttribute('ssr')) return; const actionKey = element.getAttribute('data-action-key'); @@ -107,7 +112,7 @@ export default (element) => } startTransition(() => { const root = getOrCreateRoot(element, () => { - const r = hydrateRoot(element, componentEl, renderOptions); + const r = hydrateRoot(element, componentEl, renderOptions as any); element.addEventListener('astro:unmount', () => r.unmount(), { once: true }); return r; }); diff --git a/packages/integrations/react/context.js b/packages/integrations/react/src/context.ts index 2e3e37fd5..953c35c6a 100644 --- a/packages/integrations/react/context.js +++ b/packages/integrations/react/src/context.ts @@ -1,8 +1,10 @@ -const contexts = new WeakMap(); +import type { SSRResult } from 'astro'; + +const contexts = new WeakMap<SSRResult, { currentIndex: number; readonly id: string }>(); const ID_PREFIX = 'r'; -function getContext(rendererContextResult) { +function getContext(rendererContextResult: SSRResult) { if (contexts.has(rendererContextResult)) { return contexts.get(rendererContextResult); } @@ -16,8 +18,8 @@ function getContext(rendererContextResult) { return ctx; } -export function incrementId(rendererContextResult) { - const ctx = getContext(rendererContextResult); +export function incrementId(rendererContextResult: SSRResult) { + const ctx = getContext(rendererContextResult)!; const id = ctx.id; ctx.currentIndex++; return id; diff --git a/packages/integrations/react/jsx-runtime.js b/packages/integrations/react/src/jsx-runtime.ts index d86f698b9..3f0e51c65 100644 --- a/packages/integrations/react/jsx-runtime.js +++ b/packages/integrations/react/src/jsx-runtime.ts @@ -2,7 +2,7 @@ // it can run in Node ESM. 'react' doesn't declare this module as an export map // So we have to use the .js. The .js is not added via the babel automatic JSX transform // hence this module as a workaround. -import jsxr from 'react/jsx-runtime.js'; +import jsxr from 'react/jsx-runtime'; const { jsx, jsxs, Fragment } = jsxr; export { jsx, jsxs, Fragment }; diff --git a/packages/integrations/react/server-v17.js b/packages/integrations/react/src/server-v17.ts index 4e577883a..a91b6e6d5 100644 --- a/packages/integrations/react/server-v17.js +++ b/packages/integrations/react/src/server-v17.ts @@ -1,11 +1,12 @@ +import type { AstroComponentMetadata } from 'astro'; import React from 'react'; -import ReactDOM from 'react-dom/server.js'; +import ReactDOM from 'react-dom/server'; import StaticHtml from './static-html.js'; -const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const reactTypeof = Symbol.for('react.element'); -function check(Component, props, children) { +function check(Component: any, props: Record<string, any>, children: any) { // Note: there are packages that do some unholy things to create "components". // Checking the $$typeof property catches most of these patterns. if (typeof Component === 'object') { @@ -19,7 +20,7 @@ function check(Component, props, children) { } let isReactComponent = false; - function Tester(...args) { + function Tester(...args: Array<any>) { try { const vnode = Component(...args); if (vnode && vnode['$$typeof'] === reactTypeof) { @@ -30,14 +31,19 @@ function check(Component, props, children) { return React.createElement('div'); } - renderToStaticMarkup(Tester, props, children, {}); + renderToStaticMarkup(Tester, props, children, {} as any); return isReactComponent; } -function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) { +function renderToStaticMarkup( + Component: any, + props: Record<string, any>, + { default: children, ...slotted }: Record<string, any>, + metadata: AstroComponentMetadata, +) { delete props['class']; - const slots = {}; + const slots: Record<string, any> = {}; for (const [key, value] of Object.entries(slotted)) { const name = slotName(key); slots[name] = React.createElement(StaticHtml, { value, name }); diff --git a/packages/integrations/react/server.js b/packages/integrations/react/src/server.ts index 67d9e9386..5581a95db 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/src/server.ts @@ -1,14 +1,21 @@ import opts from 'astro:react:opts'; +import type { AstroComponentMetadata } from 'astro'; import React from 'react'; import ReactDOM from 'react-dom/server'; import { incrementId } from './context.js'; import StaticHtml from './static-html.js'; +import type { RendererContext } from './types.js'; -const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const reactTypeof = Symbol.for('react.element'); const reactTransitionalTypeof = Symbol.for('react.transitional.element'); -async function check(Component, props, children) { +async function check( + this: RendererContext, + Component: any, + props: Record<string, any>, + children: any, +) { // Note: there are packages that do some unholy things to create "components". // Checking the $$typeof property catches most of these patterns. if (typeof Component === 'object') { @@ -26,7 +33,7 @@ async function check(Component, props, children) { } let isReactComponent = false; - function Tester(...args) { + function Tester(...args: Array<any>) { try { const vnode = Component(...args); if ( @@ -40,31 +47,37 @@ async function check(Component, props, children) { return React.createElement('div'); } - await renderToStaticMarkup(Tester, props, children, {}); + await renderToStaticMarkup.call(this, Tester, props, children, {} as any); return isReactComponent; } -async function getNodeWritable() { +async function getNodeWritable(): Promise<typeof import('node:stream').Writable> { let nodeStreamBuiltinModuleName = 'node:stream'; let { Writable } = await import(/* @vite-ignore */ nodeStreamBuiltinModuleName); return Writable; } -function needsHydration(metadata) { +function needsHydration(metadata: AstroComponentMetadata) { // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` return metadata.astroStaticSlot ? !!metadata.hydrate : true; } -async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) { +async function renderToStaticMarkup( + this: RendererContext, + Component: any, + props: Record<string, any>, + { default: children, ...slotted }: Record<string, any>, + metadata: AstroComponentMetadata, +) { let prefix; if (this && this.result) { prefix = incrementId(this.result); } - const attrs = { prefix }; + const attrs: Record<string, any> = { prefix }; delete props['class']; - const slots = {}; + const slots: Record<string, any> = {}; for (const [key, value] of Object.entries(slotted)) { const name = slotName(key); slots[name] = React.createElement(StaticHtml, { @@ -111,10 +124,11 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl return { html, attrs }; } -/** - * @returns {Promise<[actionResult: any, actionKey: string, actionName: string] | undefined>} - */ -async function getFormState({ result }) { +async function getFormState({ + result, +}: RendererContext): Promise< + [actionResult: any, actionKey: string, actionName: string] | undefined +> { const { request, actionResult } = result; if (!actionResult) return undefined; @@ -139,7 +153,7 @@ async function getFormState({ result }) { return [actionResult, actionKey, actionName]; } -async function renderToPipeableStreamAsync(vnode, options) { +async function renderToPipeableStreamAsync(vnode: any, options: Record<string, any>) { const Writable = await getNodeWritable(); let html = ''; return new Promise((resolve, reject) => { @@ -171,7 +185,7 @@ async function renderToPipeableStreamAsync(vnode, options) { * Use a while loop instead of "for await" due to cloudflare and Vercel Edge issues * See https://github.com/facebook/react/issues/24169 */ -async function readResult(stream) { +async function readResult(stream: ReactDOM.ReactDOMServerReadableStream) { const reader = stream.getReader(); let result = ''; const decoder = new TextDecoder('utf-8'); @@ -191,13 +205,13 @@ async function readResult(stream) { } } -async function renderToReadableStreamAsync(vnode, options) { +async function renderToReadableStreamAsync(vnode: any, options: Record<string, any>) { return await readResult(await ReactDOM.renderToReadableStream(vnode, options)); } const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; -function isFormRequest(contentType) { +function isFormRequest(contentType: string | null) { // Split off parameters like charset or boundary // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms const type = contentType?.split(';')[0].toLowerCase(); diff --git a/packages/integrations/react/static-html.js b/packages/integrations/react/src/static-html.ts index e319a40c7..010896196 100644 --- a/packages/integrations/react/static-html.js +++ b/packages/integrations/react/src/static-html.ts @@ -7,7 +7,11 @@ import { createElement as h } from 'react'; * As a bonus, we can signal to React that this subtree is * entirely static and will never change via `shouldComponentUpdate`. */ -const StaticHtml = ({ value, name, hydrate = true }) => { +const StaticHtml = ({ + value, + name, + hydrate = true, +}: { value: string | null; name?: string; hydrate?: boolean }) => { if (!value) return null; const tagName = hydrate ? 'astro-slot' : 'astro-static-slot'; return h(tagName, { diff --git a/packages/integrations/react/src/types.ts b/packages/integrations/react/src/types.ts new file mode 100644 index 000000000..5dff5b0b4 --- /dev/null +++ b/packages/integrations/react/src/types.ts @@ -0,0 +1,4 @@ +import type { SSRResult } from 'astro'; +export type RendererContext = { + result: SSRResult; +}; diff --git a/packages/integrations/react/vnode-children.js b/packages/integrations/react/src/vnode-children.ts index e0751c95a..6aa9724c6 100644 --- a/packages/integrations/react/vnode-children.js +++ b/packages/integrations/react/src/vnode-children.ts @@ -2,15 +2,15 @@ import { Fragment, createElement } from 'react'; import { DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE, parse } from 'ultrahtml'; let ids = 0; -export default function convert(children) { +export default function convert(children: any) { let doc = parse(children.toString().trim()); let id = ids++; let key = 0; - function createReactElementFromNode(node) { + function createReactElementFromNode(node: any) { const childVnodes = Array.isArray(node.children) && node.children.length - ? node.children.map((child) => createReactElementFromNode(child)).filter(Boolean) + ? node.children.map((child: any) => createReactElementFromNode(child)).filter(Boolean) : undefined; if (node.type === DOCUMENT_NODE) { diff --git a/packages/integrations/react/test/parsed-react-children.test.js b/packages/integrations/react/test/parsed-react-children.test.js index 75604e5d3..7c81357b7 100644 --- a/packages/integrations/react/test/parsed-react-children.test.js +++ b/packages/integrations/react/test/parsed-react-children.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import convert from '../vnode-children.js'; +import convert from '../dist/vnode-children.js'; describe('experimental react children', () => { it('has undefined as children for direct children', () => { diff --git a/packages/integrations/react/tsconfig.json b/packages/integrations/react/tsconfig.json index 1504b4b6d..c152f18f8 100644 --- a/packages/integrations/react/tsconfig.json +++ b/packages/integrations/react/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], + "include": ["src", "env.d.ts"], "compilerOptions": { "outDir": "./dist" } diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index 08233a060..6a5a1b17b 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -22,20 +22,15 @@ "exports": { ".": "./dist/index.js", "./editor": "./dist/editor.cjs", - "./*": "./*", - "./client.js": "./client.svelte.js", - "./server.js": "./server.js", + "./client.js": "./dist/client.svelte.js", + "./server.js": "./dist/server.js", "./package.json": "./package.json" }, "files": [ - "dist", - "client.svelte.js", - "server.js", - "server.d.ts", - "context.js" + "dist" ], "scripts": { - "build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", + "build": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist", "dev": "astro-scripts dev \"src/**/*.ts\"" }, diff --git a/packages/integrations/svelte/server.d.ts b/packages/integrations/svelte/server.d.ts deleted file mode 100644 index 75cc3eb64..000000000 --- a/packages/integrations/svelte/server.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { NamedSSRLoadedRendererValue } from 'astro'; - -declare const renderer: NamedSSRLoadedRendererValue; -export default renderer; diff --git a/packages/integrations/svelte/client.svelte.js b/packages/integrations/svelte/src/client.svelte.ts index 3bc9369f8..102f99490 100644 --- a/packages/integrations/svelte/client.svelte.js +++ b/packages/integrations/svelte/src/client.svelte.ts @@ -1,15 +1,19 @@ import { createRawSnippet, hydrate, mount, unmount } from 'svelte'; -/** @type {WeakMap<any, ReturnType<typeof createComponent>} */ -const existingApplications = new WeakMap(); +const existingApplications = new WeakMap<HTMLElement, ReturnType<typeof createComponent>>(); -export default (element) => { - return async (Component, props, slotted, { client }) => { +export default (element: HTMLElement) => { + return async ( + Component: any, + props: Record<string, any>, + slotted: Record<string, any>, + { client }: Record<string, string>, + ) => { if (!element.hasAttribute('ssr')) return; let children = undefined; - let _$$slots = undefined; - let renderFns = {}; + let _$$slots: Record<string, any> | undefined = undefined; + let renderFns: Record<string, any> = {}; for (const [key, value] of Object.entries(slotted)) { // Legacy slot support @@ -43,7 +47,7 @@ export default (element) => { ...renderFns, }; if (existingApplications.has(element)) { - existingApplications.get(element).setProps(resolvedProps); + existingApplications.get(element)!.setProps(resolvedProps); } else { const component = createComponent(Component, element, resolvedProps, client !== 'only'); existingApplications.set(element, component); @@ -52,13 +56,13 @@ export default (element) => { }; }; -/** - * @param {any} Component - * @param {HTMLElement} target - * @param {Record<string, any>} props - * @param {boolean} shouldHydrate - */ -function createComponent(Component, target, props, shouldHydrate) { + +function createComponent( + Component: any, + target: HTMLElement, + props: Record<string, any>, + shouldHydrate: boolean, +) { let propsState = $state(props); const bootstrap = shouldHydrate ? hydrate : mount; if (!shouldHydrate) { @@ -66,7 +70,7 @@ function createComponent(Component, target, props, shouldHydrate) { } const component = bootstrap(Component, { target, props: propsState }); return { - setProps(newProps) { + setProps(newProps: Record<string, any>) { Object.assign(propsState, newProps); // Remove props in `propsState` but not in `newProps` for (const key in propsState) { diff --git a/packages/integrations/svelte/context.js b/packages/integrations/svelte/src/context.ts index faec823a0..833755044 100644 --- a/packages/integrations/svelte/context.js +++ b/packages/integrations/svelte/src/context.ts @@ -1,8 +1,10 @@ -const contexts = new WeakMap(); +import type { SSRResult } from 'astro'; + +const contexts = new WeakMap<SSRResult, { currentIndex: number; readonly id: string }>(); const ID_PREFIX = 's'; -function getContext(rendererContextResult) { +function getContext(rendererContextResult: SSRResult) { if (contexts.has(rendererContextResult)) { return contexts.get(rendererContextResult); } @@ -16,8 +18,8 @@ function getContext(rendererContextResult) { return ctx; } -export function incrementId(rendererContextResult) { - const ctx = getContext(rendererContextResult); +export function incrementId(rendererContextResult: SSRResult) { + const ctx = getContext(rendererContextResult)!; const id = ctx.id; ctx.currentIndex++; return id; diff --git a/packages/integrations/svelte/server.js b/packages/integrations/svelte/src/server.ts index 6ebb62adc..4b0fccb3d 100644 --- a/packages/integrations/svelte/server.js +++ b/packages/integrations/svelte/src/server.ts @@ -1,8 +1,10 @@ +import type { AstroComponentMetadata } from 'astro'; import { createRawSnippet } from 'svelte'; import { render } from 'svelte/server'; import { incrementId } from './context.js'; +import type { RendererContext } from './types.js'; -function check(Component) { +function check(Component: any) { if (typeof Component !== 'function') return false; // Svelte 5 generated components always accept a `$$payload` prop. // This assumes that the SSR build does not minify it (which Astro enforces by default). @@ -11,21 +13,27 @@ function check(Component) { return Component.toString().includes('$$payload'); } -function needsHydration(metadata) { +function needsHydration(metadata: AstroComponentMetadata) { // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` return metadata.astroStaticSlot ? !!metadata.hydrate : true; } -async function renderToStaticMarkup(Component, props, slotted, metadata) { +async function renderToStaticMarkup( + this: RendererContext, + Component: any, + props: Record<string, any>, + slotted: Record<string, any>, + metadata: AstroComponentMetadata, +) { const tagName = needsHydration(metadata) ? 'astro-slot' : 'astro-static-slot'; let children = undefined; - let $$slots = undefined; + let $$slots: Record<string, any> | undefined = undefined; let idPrefix; if (this && this.result) { idPrefix = incrementId(this.result); } - const renderProps = {}; + const renderProps: Record<string, any> = {}; for (const [key, value] of Object.entries(slotted)) { // Legacy slot support $$slots ??= {}; diff --git a/packages/integrations/svelte/src/types.ts b/packages/integrations/svelte/src/types.ts new file mode 100644 index 000000000..86834336e --- /dev/null +++ b/packages/integrations/svelte/src/types.ts @@ -0,0 +1,4 @@ +import type { SSRResult } from 'astro'; +export type RendererContext = { + result: SSRResult; +};
\ No newline at end of file diff --git a/packages/integrations/vue/context.js b/packages/integrations/vue/context.js deleted file mode 100644 index 80a569ce6..000000000 --- a/packages/integrations/vue/context.js +++ /dev/null @@ -1,24 +0,0 @@ -const contexts = new WeakMap(); - -const ID_PREFIX = 'v'; - -function getContext(rendererContextResult) { - if (contexts.has(rendererContextResult)) { - return contexts.get(rendererContextResult); - } - const ctx = { - currentIndex: 0, - get id() { - return ID_PREFIX + this.currentIndex.toString(); - }, - }; - contexts.set(rendererContextResult, ctx); - return ctx; -} - -export function incrementId(rendererContextResult) { - const ctx = getContext(rendererContextResult); - const id = ctx.id; - ctx.currentIndex++; - return id; -} diff --git a/packages/integrations/vue/env.d.ts b/packages/integrations/vue/env.d.ts new file mode 100644 index 000000000..3a0052dca --- /dev/null +++ b/packages/integrations/vue/env.d.ts @@ -0,0 +1,3 @@ +declare module 'virtual:@astrojs/vue/app' { + export const setup: (app: import('vue').App<Element>) => void | Promise<void>; +}
\ No newline at end of file diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json index 6b05637de..e113443b4 100644 --- a/packages/integrations/vue/package.json +++ b/packages/integrations/vue/package.json @@ -22,21 +22,15 @@ "exports": { ".": "./dist/index.js", "./editor": "./dist/editor.cjs", - "./*": "./*", - "./client.js": "./client.js", - "./server.js": "./server.js", + "./client.js": "./dist/client.js", + "./server.js": "./dist/server.js", "./package.json": "./package.json" }, "files": [ - "dist", - "client.js", - "context.js", - "server.js", - "server.d.ts", - "static-html.js" + "dist" ], "scripts": { - "build": "astro-scripts build \"src/index.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", + "build": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist", "dev": "astro-scripts dev \"src/**/*.ts\"", "test": "astro-scripts test \"test/**/*.test.js\"" diff --git a/packages/integrations/vue/server.d.ts b/packages/integrations/vue/server.d.ts deleted file mode 100644 index 75cc3eb64..000000000 --- a/packages/integrations/vue/server.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { NamedSSRLoadedRendererValue } from 'astro'; - -declare const renderer: NamedSSRLoadedRendererValue; -export default renderer; diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/src/client.ts index 4ec2b9e68..8f02d534e 100644 --- a/packages/integrations/vue/client.js +++ b/packages/integrations/vue/src/client.ts @@ -3,15 +3,23 @@ import { Suspense, createApp, createSSRApp, h } from 'vue'; import StaticHtml from './static-html.js'; // keep track of already initialized apps, so we don't hydrate again for view transitions -let appMap = new WeakMap(); +let appMap = new WeakMap< + HTMLElement, + { props: Record<string, any>; slots: Record<string, any>; component?: any } +>(); -export default (element) => - async (Component, props, slotted, { client }) => { +export default (element: HTMLElement) => + async ( + Component: any, + props: Record<string, any>, + slotted: Record<string, any>, + { client }: Record<string, string>, + ) => { if (!element.hasAttribute('ssr')) return; // Expose name on host component for Vue devtools const name = Component.name ? `${Component.name} Host` : undefined; - const slots = {}; + const slots: Record<string, any> = {}; for (const [key, value] of Object.entries(slotted)) { slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key }); } @@ -30,8 +38,9 @@ export default (element) => const app = bootstrap({ name, render() { - let content = h(Component, appInstance.props, appInstance.slots); - appInstance.component = this; + // At this point, appInstance has been set so it's safe to use a non-null assertion + let content = h(Component, appInstance!.props, appInstance!.slots); + appInstance!.component = this; // related to https://github.com/withastro/astro/issues/6549 // if the component is async, wrap it in a Suspense component if (isAsync(Component.setup)) { @@ -40,7 +49,7 @@ export default (element) => return content; }, }); - app.config.idPrefix = element.getAttribute('prefix'); + app.config.idPrefix = element.getAttribute('prefix') ?? undefined; await setup(app); app.mount(element, isHydrate); appMap.set(element, appInstance); @@ -52,7 +61,7 @@ export default (element) => } }; -function isAsync(fn) { +function isAsync(fn: () => any) { const constructor = fn?.constructor; return constructor && constructor.name === 'AsyncFunction'; } diff --git a/packages/integrations/vue/src/context.ts b/packages/integrations/vue/src/context.ts new file mode 100644 index 000000000..833755044 --- /dev/null +++ b/packages/integrations/vue/src/context.ts @@ -0,0 +1,26 @@ +import type { SSRResult } from 'astro'; + +const contexts = new WeakMap<SSRResult, { currentIndex: number; readonly id: string }>(); + +const ID_PREFIX = 's'; + +function getContext(rendererContextResult: SSRResult) { + if (contexts.has(rendererContextResult)) { + return contexts.get(rendererContextResult); + } + const ctx = { + currentIndex: 0, + get id() { + return ID_PREFIX + this.currentIndex.toString(); + }, + }; + contexts.set(rendererContextResult, ctx); + return ctx; +} + +export function incrementId(rendererContextResult: SSRResult) { + const ctx = getContext(rendererContextResult)!; + const id = ctx.id; + ctx.currentIndex++; + return id; +} diff --git a/packages/integrations/vue/server.js b/packages/integrations/vue/src/server.ts index 315909087..6b4c2a3f4 100644 --- a/packages/integrations/vue/server.js +++ b/packages/integrations/vue/src/server.ts @@ -1,21 +1,29 @@ import { setup } from 'virtual:@astrojs/vue/app'; +import type { AstroComponentMetadata } from 'astro'; import { createSSRApp, h } from 'vue'; import { renderToString } from 'vue/server-renderer'; import { incrementId } from './context.js'; import StaticHtml from './static-html.js'; +import type { RendererContext } from './types.js'; -function check(Component) { +function check(Component: any) { return !!Component['ssrRender'] || !!Component['__ssrInlineRender']; } -async function renderToStaticMarkup(Component, inputProps, slotted, metadata) { +async function renderToStaticMarkup( + this: RendererContext, + Component: any, + inputProps: Record<string, any>, + slotted: Record<string, any>, + metadata: AstroComponentMetadata, +) { let prefix; if (this && this.result) { prefix = incrementId(this.result); } const attrs = { prefix }; - const slots = {}; + const slots: Record<string, any> = {}; const props = { ...inputProps }; delete props.slot; for (const [key, value] of Object.entries(slotted)) { diff --git a/packages/integrations/vue/static-html.js b/packages/integrations/vue/src/static-html.ts index 689b56a70..689b56a70 100644 --- a/packages/integrations/vue/static-html.js +++ b/packages/integrations/vue/src/static-html.ts diff --git a/packages/integrations/vue/src/types.ts b/packages/integrations/vue/src/types.ts new file mode 100644 index 000000000..5dff5b0b4 --- /dev/null +++ b/packages/integrations/vue/src/types.ts @@ -0,0 +1,4 @@ +import type { SSRResult } from 'astro'; +export type RendererContext = { + result: SSRResult; +}; diff --git a/packages/integrations/vue/tsconfig.json b/packages/integrations/vue/tsconfig.json index 5742d1f6e..100f3c93b 100644 --- a/packages/integrations/vue/tsconfig.json +++ b/packages/integrations/vue/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], + "include": ["src", "env.d.ts"], "compilerOptions": { "outDir": "./dist", "verbatimModuleSyntax": false |