diff options
Diffstat (limited to 'packages/astro/src')
27 files changed, 218 insertions, 67 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0f8cf4240..e20e0e5a8 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -55,6 +55,10 @@ export interface AstroBuiltinProps { 'client:only'?: boolean | string; } +// Allow users to extend this for astro-jsx.d.ts +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AstroClientDirectives {} + export interface AstroBuiltinAttributes { 'class:list'?: | Record<string, boolean> @@ -1077,6 +1081,28 @@ export interface AstroUserConfig { /** * @docs + * @name experimental.customClientDirectives + * @type {boolean} + * @default `false` + * @version 2.5.0 + * @description + * Allow integrations to use the [experimental `addClientDirective` API](/en/reference/integrations-reference/#addclientdirective-option) in the `astro:config:setup` hook + * to add custom client directives in Astro files. + * + * To enable this feature, set `experimental.customClientDirectives` to `true` in your Astro config: + * + * ```js + * { + * experimental: { + * customClientDirectives: true, + * }, + * } + * ``` + */ + customClientDirectives?: boolean; + + /** + * @docs * @name experimental.middleware * @type {boolean} * @default `false` @@ -1206,6 +1232,10 @@ export interface AstroSettings { stage: InjectedScriptStage; content: string; }[]; + /** + * Map of directive name (e.g. `load`) to the directive script code + */ + clientDirectives: Map<string, string>; tsConfig: TsConfigJson | undefined; tsConfigPath: string | undefined; watchFiles: string[]; @@ -1654,6 +1684,7 @@ export interface AstroIntegration { addWatchFile: (path: URL | string) => void; injectScript: (stage: InjectedScriptStage, content: string) => void; injectRoute: (injectRoute: InjectedRoute) => void; + addClientDirective: (directive: ClientDirectiveConfig) => void; // TODO: Add support for `injectElement()` for full HTML element injection, not just scripts. // This may require some refactoring of `scripts`, `styles`, and `links` into something // more generalized. Consider the SSR use-case as well. @@ -1750,6 +1781,7 @@ export interface SSRMetadata { hasDirectives: Set<string>; hasRenderedHead: boolean; headInTree: boolean; + clientDirectives: Map<string, string>; } /** @@ -1815,3 +1847,29 @@ export type CreatePreviewServer = ( export interface PreviewModule { default: CreatePreviewServer; } + +/* Client Directives */ +type DirectiveHydrate = () => Promise<void>; +type DirectiveLoad = () => Promise<DirectiveHydrate>; + +type DirectiveOptions = { + /** + * The component displayName + */ + name: string; + /** + * The attribute value provided + */ + value: string; +}; + +export type ClientDirective = ( + load: DirectiveLoad, + options: DirectiveOptions, + el: HTMLElement +) => void; + +export interface ClientDirectiveConfig { + name: string; + entrypoint: string; +} diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 6fd13d9b9..58898b2fe 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -15,11 +15,13 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): const assets = new Set<string>(serializedManifest.assets); const componentMetadata = new Map(serializedManifest.componentMetadata); + const clientDirectives = new Map(serializedManifest.clientDirectives); return { ...serializedManifest, assets, componentMetadata, + clientDirectives, routes, }; } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 0451b4ed5..546d45975 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -65,6 +65,7 @@ export class App { markdown: manifest.markdown, mode: 'production', renderers: manifest.renderers, + clientDirectives: manifest.clientDirectives, async resolve(specifier: string) { if (!(specifier in manifest.entryModules)) { throw new Error(`Unable to resolve [${specifier}]`); diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index ab6a50b9c..89c5bad37 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -41,16 +41,24 @@ export interface SSRManifest { markdown: MarkdownRenderingOptions; pageMap: Map<ComponentPath, ComponentInstance>; renderers: SSRLoadedRenderer[]; + /** + * Map of directive name (e.g. `load`) to the directive script code + */ + clientDirectives: Map<string, string>; entryModules: Record<string, string>; assets: Set<string>; componentMetadata: SSRResult['componentMetadata']; middleware?: AstroMiddlewareInstance<unknown>; } -export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & { +export type SerializedSSRManifest = Omit< + SSRManifest, + 'routes' | 'assets' | 'componentMetadata' | 'clientDirectives' +> & { routes: SerializedRouteInfo[]; assets: string[]; componentMetadata: [string, SSRComponentMetadata][]; + clientDirectives: [string, string][]; }; export type AdapterCreateExports<T = any> = ( diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 26e53d367..5d2cb09ca 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -417,6 +417,7 @@ async function generatePath( markdown: settings.config.markdown, mode: opts.mode, renderers, + clientDirectives: settings.clientDirectives, async resolve(specifier: string) { const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); if (typeof hashedFilePath !== 'string') { diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 8259e5e15..935e7b380 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -237,6 +237,7 @@ function buildManifest( pageMap: null as any, componentMetadata: Array.from(internals.componentMetadata), renderers: [], + clientDirectives: Array.from(settings.clientDirectives), entryModules, assets: staticFiles.map(prefixAssetPath), }; diff --git a/packages/astro/src/core/client-directive/build.ts b/packages/astro/src/core/client-directive/build.ts new file mode 100644 index 000000000..591c0c437 --- /dev/null +++ b/packages/astro/src/core/client-directive/build.ts @@ -0,0 +1,33 @@ +import { build } from 'esbuild'; + +/** + * Build a client directive entrypoint into code that can directly run in a `<script>` tag. + */ +export async function buildClientDirectiveEntrypoint(name: string, entrypoint: string) { + const stringifiedName = JSON.stringify(name); + const stringifiedEntrypoint = JSON.stringify(entrypoint); + + // NOTE: when updating this stdin code, make sure to also update `packages/astro/scripts/prebuild.ts` + // that prebuilds the client directive with a similar code too. + const output = await build({ + stdin: { + contents: `\ +import directive from ${stringifiedEntrypoint}; + +(self.Astro || (self.Astro = {}))[${stringifiedName}] = directive; + +window.dispatchEvent(new Event('astro:' + ${stringifiedName}));`, + resolveDir: process.cwd(), + }, + absWorkingDir: process.cwd(), + format: 'iife', + minify: true, + bundle: true, + write: false, + }); + + const outputFile = output.outputFiles?.[0]; + if (!outputFile) return ''; + + return outputFile.text; +} diff --git a/packages/astro/src/core/client-directive/default.ts b/packages/astro/src/core/client-directive/default.ts new file mode 100644 index 000000000..352763ba6 --- /dev/null +++ b/packages/astro/src/core/client-directive/default.ts @@ -0,0 +1,15 @@ +import idlePrebuilt from '../../runtime/client/idle.prebuilt.js'; +import loadPrebuilt from '../../runtime/client/load.prebuilt.js'; +import mediaPrebuilt from '../../runtime/client/media.prebuilt.js'; +import onlyPrebuilt from '../../runtime/client/only.prebuilt.js'; +import visiblePrebuilt from '../../runtime/client/visible.prebuilt.js'; + +export function getDefaultClientDirectives() { + return new Map([ + ['idle', idlePrebuilt], + ['load', loadPrebuilt], + ['media', mediaPrebuilt], + ['only', onlyPrebuilt], + ['visible', visiblePrebuilt], + ]); +} diff --git a/packages/astro/src/core/client-directive/index.ts b/packages/astro/src/core/client-directive/index.ts new file mode 100644 index 000000000..7c1a9a71c --- /dev/null +++ b/packages/astro/src/core/client-directive/index.ts @@ -0,0 +1,2 @@ +export { buildClientDirectiveEntrypoint } from './build.js'; +export { getDefaultClientDirectives } from './default.js'; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index fd8d88c4d..54640b19f 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -38,6 +38,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { legacy: {}, experimental: { assets: false, + customClientDirecives: false, inlineStylesheets: 'never', middleware: false, }, @@ -195,6 +196,10 @@ export const AstroConfigSchema = z.object({ experimental: z .object({ assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets), + customClientDirectives: z + .boolean() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.experimental.customClientDirecives), inlineStylesheets: z .enum(['always', 'auto', 'never']) .optional() diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts index 4d8278b80..fa90af4c0 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -7,6 +7,7 @@ import { markdownContentEntryType } from '../../vite-plugin-markdown/content-ent import { createDefaultDevConfig } from './config.js'; import { AstroTimer } from './timer.js'; import { loadTSConfig } from './tsconfig.js'; +import { getDefaultClientDirectives } from '../client-directive/index.js'; export function createBaseSettings(config: AstroConfig): AstroSettings { return { @@ -23,6 +24,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings { contentEntryTypes: [markdownContentEntryType], renderers: [jsxRenderer], scripts: [], + clientDirectives: getDefaultClientDirectives(), watchFiles: [], forceDisableTelemetry: false, timer: new AstroTimer(), diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index fd57ad8bc..1c12a1a8d 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -140,6 +140,7 @@ export async function renderPage({ mod, renderContext, env, apiContext }: Render componentMetadata: renderContext.componentMetadata, resolve: env.resolve, renderers: env.renderers, + clientDirectives: env.clientDirectives, request: renderContext.request, site: env.site, scripts: renderContext.scripts, diff --git a/packages/astro/src/core/render/dev/environment.ts b/packages/astro/src/core/render/dev/environment.ts index 5aa3688dd..6a45f9c36 100644 --- a/packages/astro/src/core/render/dev/environment.ts +++ b/packages/astro/src/core/render/dev/environment.ts @@ -25,6 +25,7 @@ export function createDevelopmentEnvironment( mode, // This will be overridden in the dev server renderers: [], + clientDirectives: settings.clientDirectives, resolve: createResolve(loader, settings.config.root), routeCache: new RouteCache(logging, mode), site: settings.config.site, diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts index 4c5f6bace..d4a1cc38e 100644 --- a/packages/astro/src/core/render/environment.ts +++ b/packages/astro/src/core/render/environment.ts @@ -2,6 +2,7 @@ import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { RuntimeMode, SSRLoadedRenderer } from '../../@types/astro'; import type { LogOptions } from '../logger/core.js'; import { RouteCache } from './route-cache.js'; +import { getDefaultClientDirectives } from '../client-directive/default.js'; /** * An environment represents the static parts of rendering that do not change @@ -16,6 +17,7 @@ export interface Environment { /** "development" or "production" */ mode: RuntimeMode; renderers: SSRLoadedRenderer[]; + clientDirectives: Map<string, string>; resolve: (s: string) => Promise<string>; routeCache: RouteCache; site?: string; @@ -46,6 +48,7 @@ export function createBasicEnvironment(options: CreateBasicEnvironmentArgs): Env }, mode, renderers: options.renderers ?? [], + clientDirectives: getDefaultClientDirectives(), resolve: options.resolve ?? ((s: string) => Promise.resolve(s)), routeCache: new RouteCache(options.logging, mode), ssr: options.ssr ?? true, diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 598ec116f..e18ed7eb9 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -42,6 +42,7 @@ export interface CreateResultArgs { pathname: string; props: Props; renderers: SSRLoadedRenderer[]; + clientDirectives: Map<string, string>; resolve: (s: string) => Promise<string>; site: string | undefined; links?: Set<SSRElement>; @@ -132,7 +133,8 @@ class Slots { let renderMarkdown: any = null; export function createResult(args: CreateResultArgs): SSRResult { - const { markdown, params, pathname, renderers, request, resolve, locals } = args; + const { markdown, params, pathname, renderers, clientDirectives, request, resolve, locals } = + args; const url = new URL(request.url); const headers = new Headers(); @@ -260,6 +262,7 @@ export function createResult(args: CreateResultArgs): SSRResult { hasRenderedHead: false, hasDirectives: new Set(), headInTree: false, + clientDirectives, }, response, }; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index d306e7be3..f833d94a1 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -17,6 +17,7 @@ import type { PageBuildData } from '../core/build/types'; import { mergeConfig } from '../core/config/config.js'; import { info, type LogOptions } from '../core/logger/core.js'; import { mdxContentEntryType } from '../vite-plugin-markdown/content-entry-type.js'; +import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; async function withTakingALongTimeMsg<T>({ name, @@ -55,6 +56,7 @@ export async function runHookConfigSetup({ let updatedConfig: AstroConfig = { ...settings.config }; let updatedSettings: AstroSettings = { ...settings, config: updatedConfig }; + let addedClientDirectives = new Map<string, Promise<string>>(); for (const integration of settings.config.integrations) { /** @@ -97,6 +99,19 @@ export async function runHookConfigSetup({ addWatchFile: (path) => { updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path); }, + addClientDirective: ({ name, entrypoint }) => { + if (!settings.config.experimental.customClientDirectives) { + throw new Error( + `The "${integration.name}" integration is trying to add the "${name}" client directive, but the \`experimental.customClientDirectives\` config is not enabled.` + ); + } + if (updatedSettings.clientDirectives.has(name) || addedClientDirectives.has(name)) { + throw new Error( + `The "${integration.name}" integration is trying to add the "${name}" client directive, but it already exists.` + ); + } + addedClientDirectives.set(name, buildClientDirectiveEntrypoint(name, entrypoint)); + }, }; // --- @@ -138,6 +153,11 @@ export async function runHookConfigSetup({ ) { addContentEntryType(mdxContentEntryType); } + + // Add custom client directives to settings, waiting for compiled code by esbuild + for (const [name, compiled] of addedClientDirectives) { + updatedSettings.clientDirectives.set(name, await compiled); + } } } diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index 861914336..9caf42aaf 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -3,7 +3,6 @@ import * as t from '@babel/types'; import { AstroErrorData } from '../core/errors/errors-data.js'; import { AstroError } from '../core/errors/errors.js'; import { resolvePath } from '../core/util.js'; -import { HydrationDirectiveProps } from '../runtime/server/hydration.js'; import type { PluginMetadata } from '../vite-plugin-astro/types'; const ClientOnlyPlaceholder = 'astro-client-only'; @@ -285,7 +284,7 @@ export default function astroJSX(): PluginObj { for (const attr of parentNode.openingElement.attributes) { if (t.isJSXAttribute(attr)) { const name = jsxAttributeToString(attr); - if (HydrationDirectiveProps.has(name)) { + if (name.startsWith('client:')) { // eslint-disable-next-line console.warn( `You are attempting to render <${displayName} ${name} />, but ${displayName} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.` diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 4af28bd46..48aa9dc1f 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -1,13 +1,15 @@ -(self.Astro = self.Astro || {}).idle = (getHydrateCallback) => { +import type { ClientDirective } from '../../@types/astro'; + +const idleDirective: ClientDirective = (load) => { const cb = async () => { - let hydrate = await getHydrateCallback(); + const hydrate = await load(); await hydrate(); }; - if ('requestIdleCallback' in window) { (window as any).requestIdleCallback(cb); } else { setTimeout(cb, 200); } }; -window.dispatchEvent(new Event('astro:idle')); + +export default idleDirective; diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index 426c6c68a..15a2f1dcb 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -1,7 +1,8 @@ -(self.Astro = self.Astro || {}).load = (getHydrateCallback) => { - (async () => { - let hydrate = await getHydrateCallback(); - await hydrate(); - })(); +import type { ClientDirective } from '../../@types/astro'; + +const loadDirective: ClientDirective = async (load) => { + const hydrate = await load(); + await hydrate(); }; -window.dispatchEvent(new Event('astro:load')); + +export default loadDirective; diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index c180d396a..3d92d3713 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -1,9 +1,11 @@ +import type { ClientDirective } from '../../@types/astro'; + /** * Hydrate this component when a matching media query is found */ -(self.Astro = self.Astro || {}).media = (getHydrateCallback, options) => { +const mediaDirective: ClientDirective = (load, options) => { const cb = async () => { - let hydrate = await getHydrateCallback(); + const hydrate = await load(); await hydrate(); }; @@ -16,4 +18,5 @@ } } }; -window.dispatchEvent(new Event('astro:media')); + +export default mediaDirective; diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index e8272edbb..f67ae3ace 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -1,10 +1,11 @@ +import type { ClientDirective } from '../../@types/astro'; + /** * Hydrate this component only on the client */ -(self.Astro = self.Astro || {}).only = (getHydrateCallback) => { - (async () => { - let hydrate = await getHydrateCallback(); - await hydrate(); - })(); +const onlyDirective: ClientDirective = async (load) => { + const hydrate = await load(); + await hydrate(); }; -window.dispatchEvent(new Event('astro:only')); + +export default onlyDirective; diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index 28975040c..e42b04339 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -1,15 +1,17 @@ +import type { ClientDirective } from '../../@types/astro'; + /** * Hydrate this component when one of it's children becomes visible * We target the children because `astro-island` is set to `display: contents` * which doesn't work with IntersectionObserver */ -(self.Astro = self.Astro || {}).visible = (getHydrateCallback, _opts, root) => { +const visibleDirective: ClientDirective = (load, _options, el) => { const cb = async () => { - let hydrate = await getHydrateCallback(); + const hydrate = await load(); await hydrate(); }; - let io = new IntersectionObserver((entries) => { + const io = new IntersectionObserver((entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue; // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island` @@ -19,9 +21,10 @@ } }); - for (let i = 0; i < root.children.length; i++) { - const child = root.children[i]; + for (let i = 0; i < el.children.length; i++) { + const child = el.children[i]; io.observe(child); } }; -window.dispatchEvent(new Event('astro:visible')); + +export default visibleDirective; diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index 4729708e7..9394be581 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -9,10 +9,6 @@ import { escapeHTML } from './escape.js'; import { serializeProps } from './serialize.js'; import { serializeListValue } from './util.js'; -const HydrationDirectivesRaw = ['load', 'idle', 'media', 'visible', 'only']; -const HydrationDirectives = new Set(HydrationDirectivesRaw); -export const HydrationDirectiveProps = new Set(HydrationDirectivesRaw.map((n) => `client:${n}`)); - export interface HydrationMetadata { directive: string; value: string; @@ -29,8 +25,8 @@ interface ExtractedProps { // Used to extract the directives, aka `client:load` information about a component. // Finds these special props and removes them from what gets passed into the component. export function extractDirectives( - displayName: string, - inputProps: Record<string | number | symbol, any> + inputProps: Record<string | number | symbol, any>, + clientDirectives: SSRResult['_metadata']['clientDirectives'] ): ExtractedProps { let extracted: ExtractedProps = { isPage: false, @@ -74,11 +70,12 @@ export function extractDirectives( extracted.hydration.value = value; // throw an error if an invalid hydration directive was provided - if (!HydrationDirectives.has(extracted.hydration.directive)) { + if (!clientDirectives.has(extracted.hydration.directive)) { + const hydrationMethods = Array.from(clientDirectives.keys()) + .map((d) => `client:${d}`) + .join(', '); throw new Error( - `Error: invalid hydration directive "${key}". Supported hydration methods: ${Array.from( - HydrationDirectiveProps - ).join(', ')}` + `Error: invalid hydration directive "${key}". Supported hydration methods: ${hydrationMethods}` ); } diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts index 47ce7f495..ed5044575 100644 --- a/packages/astro/src/runtime/server/render/astro/instance.ts +++ b/packages/astro/src/runtime/server/render/astro/instance.ts @@ -2,7 +2,6 @@ import type { SSRResult } from '../../../../@types/astro'; import type { ComponentSlots } from '../slot.js'; import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js'; -import { HydrationDirectiveProps } from '../../hydration.js'; import { isPromise } from '../../util.js'; import { renderChild } from '../any.js'; import { isAPropagatingComponent } from './factory.js'; @@ -62,7 +61,7 @@ export class AstroComponentInstance { function validateComponentProps(props: any, displayName: string) { if (props != null) { for (const prop of Object.keys(props)) { - if (HydrationDirectiveProps.has(prop)) { + if (prop.startsWith('client:')) { // eslint-disable-next-line console.warn( `You are attempting to render <${displayName} ${prop} />, but ${displayName} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.` diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index e9e74f9fa..e9be3bf8b 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -39,7 +39,7 @@ export function stringifyChunk( ? 'directive' : null; if (prescriptType) { - let prescripts = getPrescripts(prescriptType, hydration.directive); + let prescripts = getPrescripts(result, prescriptType, hydration.directive); return markHTMLString(prescripts); } else { return ''; diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index cc8851522..afedd8858 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -67,10 +67,10 @@ async function renderFrameworkComponent( ); } - const { renderers } = result._metadata; + const { renderers, clientDirectives } = result._metadata; const metadata: AstroComponentMetadata = { displayName }; - const { hydration, isPage, props } = extractDirectives(displayName, _props); + const { hydration, isPage, props } = extractDirectives(_props, clientDirectives); let html = ''; let attrs: Record<string, string> | undefined = undefined; diff --git a/packages/astro/src/runtime/server/scripts.ts b/packages/astro/src/runtime/server/scripts.ts index 1d57c07e9..b466d1df3 100644 --- a/packages/astro/src/runtime/server/scripts.ts +++ b/packages/astro/src/runtime/server/scripts.ts @@ -1,12 +1,8 @@ import type { SSRResult } from '../../@types/astro'; - -import idlePrebuilt from '../client/idle.prebuilt.js'; -import loadPrebuilt from '../client/load.prebuilt.js'; -import mediaPrebuilt from '../client/media.prebuilt.js'; -import onlyPrebuilt from '../client/only.prebuilt.js'; -import visiblePrebuilt from '../client/visible.prebuilt.js'; import islandScript from './astro-island.prebuilt.js'; +const ISLAND_STYLES = `<style>astro-island,astro-slot{display:contents}</style>`; + export function determineIfNeedsHydrationScript(result: SSRResult): boolean { if (result._metadata.hasHydrationScript) { return false; @@ -14,14 +10,6 @@ export function determineIfNeedsHydrationScript(result: SSRResult): boolean { return (result._metadata.hasHydrationScript = true); } -export const hydrationScripts: Record<string, string> = { - idle: idlePrebuilt, - load: loadPrebuilt, - only: onlyPrebuilt, - media: mediaPrebuilt, - visible: visiblePrebuilt, -}; - export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: string): boolean { if (result._metadata.hasDirectives.has(directive)) { return false; @@ -32,26 +20,28 @@ export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: s export type PrescriptType = null | 'both' | 'directive'; -function getDirectiveScriptText(directive: string): string { - if (!(directive in hydrationScripts)) { +function getDirectiveScriptText(result: SSRResult, directive: string): string { + const clientDirectives = result._metadata.clientDirectives; + const clientDirective = clientDirectives.get(directive); + if (!clientDirective) { throw new Error(`Unknown directive: ${directive}`); } - const directiveScriptText = hydrationScripts[directive]; - return directiveScriptText; + return clientDirective; } -export function getPrescripts(type: PrescriptType, directive: string): string { +export function getPrescripts(result: SSRResult, type: PrescriptType, directive: string): string { // Note that this is a classic script, not a module script. // This is so that it executes immediate, and when the browser encounters // an astro-island element the callbacks will fire immediately, causing the JS // deps to be loaded immediately. switch (type) { case 'both': - return `<style>astro-island,astro-slot{display:contents}</style><script>${ - getDirectiveScriptText(directive) + islandScript - }</script>`; + return `${ISLAND_STYLES}<script>${getDirectiveScriptText( + result, + directive + )};${islandScript}</script>`; case 'directive': - return `<script>${getDirectiveScriptText(directive)}</script>`; + return `<script>${getDirectiveScriptText(result, directive)}</script>`; } return ''; } |