summaryrefslogtreecommitdiff
path: root/packages/astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src')
-rw-r--r--packages/astro/src/@types/astro.ts58
-rw-r--r--packages/astro/src/core/app/common.ts2
-rw-r--r--packages/astro/src/core/app/index.ts1
-rw-r--r--packages/astro/src/core/app/types.ts10
-rw-r--r--packages/astro/src/core/build/generate.ts1
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts1
-rw-r--r--packages/astro/src/core/client-directive/build.ts33
-rw-r--r--packages/astro/src/core/client-directive/default.ts15
-rw-r--r--packages/astro/src/core/client-directive/index.ts2
-rw-r--r--packages/astro/src/core/config/schema.ts5
-rw-r--r--packages/astro/src/core/config/settings.ts2
-rw-r--r--packages/astro/src/core/render/core.ts1
-rw-r--r--packages/astro/src/core/render/dev/environment.ts1
-rw-r--r--packages/astro/src/core/render/environment.ts3
-rw-r--r--packages/astro/src/core/render/result.ts5
-rw-r--r--packages/astro/src/integrations/index.ts20
-rw-r--r--packages/astro/src/jsx/babel.ts3
-rw-r--r--packages/astro/src/runtime/client/idle.ts10
-rw-r--r--packages/astro/src/runtime/client/load.ts13
-rw-r--r--packages/astro/src/runtime/client/media.ts9
-rw-r--r--packages/astro/src/runtime/client/only.ts13
-rw-r--r--packages/astro/src/runtime/client/visible.ts15
-rw-r--r--packages/astro/src/runtime/server/hydration.ts17
-rw-r--r--packages/astro/src/runtime/server/render/astro/instance.ts3
-rw-r--r--packages/astro/src/runtime/server/render/common.ts2
-rw-r--r--packages/astro/src/runtime/server/render/component.ts4
-rw-r--r--packages/astro/src/runtime/server/scripts.ts36
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 '';
}