refactor how client directives work
This commit is contained in:
parent
005d53145f
commit
dd6c06e229
12 changed files with 128 additions and 14 deletions
|
@ -881,6 +881,7 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
|
|||
adapter: AstroAdapter | undefined;
|
||||
renderers: AstroRenderer[];
|
||||
scripts: { stage: InjectedScriptStage; content: string }[];
|
||||
clientDirectives: ClientDirectiveMap;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1200,6 +1201,7 @@ export interface SSRElement {
|
|||
|
||||
export interface SSRMetadata {
|
||||
renderers: SSRLoadedRenderer[];
|
||||
clientDirectives: ClientDirectiveMap;
|
||||
pathname: string;
|
||||
hasHydrationScript: boolean;
|
||||
hasDirectives: Set<string>;
|
||||
|
@ -1220,3 +1222,18 @@ export interface SSRResult {
|
|||
}
|
||||
|
||||
export type MarkdownAstroData = { frontmatter: object };
|
||||
|
||||
/* Client Directives */
|
||||
export type ClientDirectiveMap = Map<string, { type: 'inline' | 'external', src: string }>;
|
||||
type Hydrate = () => Promise<void>;
|
||||
type Load = () => Promise<Hydrate>;
|
||||
|
||||
type DirectiveOptions = {
|
||||
// The component displayName
|
||||
name: string;
|
||||
// The attribute value provided,
|
||||
// for ex `client:interactive="click"
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ClientDirective = (load: Load, opts: DirectiveOptions, element: HTMLElement) => void;
|
||||
|
|
|
@ -353,6 +353,12 @@ export async function validateConfig(
|
|||
renderers: [jsxRenderer],
|
||||
injectedRoutes: [],
|
||||
adapter: undefined,
|
||||
clientDirectives: new Map([
|
||||
['visible', {
|
||||
type: 'external',
|
||||
src: 'astro/runtime/client/visible.js'
|
||||
}]
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import legacyMarkdownVitePlugin from '../vite-plugin-markdown-legacy/index.js';
|
|||
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||
import astroClientDirective from '../vite-plugin-client-directive/index.js';
|
||||
import { createCustomViteLogger } from './errors.js';
|
||||
import { resolveDependency } from './util.js';
|
||||
|
||||
|
@ -86,6 +87,7 @@ export async function createVite(
|
|||
astroPostprocessVitePlugin({ config: astroConfig }),
|
||||
astroIntegrationsContainerPlugin({ config: astroConfig, logging }),
|
||||
astroScriptsPageSSRPlugin({ config: astroConfig }),
|
||||
astroClientDirective({ config: astroConfig, logging }),
|
||||
],
|
||||
publicDir: fileURLToPath(astroConfig.publicDir),
|
||||
root: fileURLToPath(astroConfig.root),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||
import type {
|
||||
ComponentInstance,
|
||||
ClientDirectiveMap,
|
||||
Params,
|
||||
Props,
|
||||
RouteData,
|
||||
|
@ -79,6 +80,7 @@ export interface RenderOptions {
|
|||
scripts: Set<SSRElement>;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
clientDirectives: ClientDirectiveMap;
|
||||
route?: RouteData;
|
||||
routeCache: RouteCache;
|
||||
site?: string;
|
||||
|
@ -91,6 +93,7 @@ export interface RenderOptions {
|
|||
export async function render(opts: RenderOptions): Promise<Response> {
|
||||
const {
|
||||
adapterName,
|
||||
clientDirectives,
|
||||
links,
|
||||
styles,
|
||||
logging,
|
||||
|
@ -145,6 +148,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
|
|||
pathname,
|
||||
resolve,
|
||||
renderers,
|
||||
clientDirectives,
|
||||
request,
|
||||
site,
|
||||
scripts,
|
||||
|
|
|
@ -168,6 +168,7 @@ export async function render(
|
|||
|
||||
let response = await coreRender({
|
||||
adapterName: astroConfig.adapter?.name,
|
||||
clientDirectives: astroConfig._ctx.clientDirectives,
|
||||
links,
|
||||
styles,
|
||||
logging,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { bold } from 'kleur/colors';
|
|||
import type {
|
||||
AstroGlobal,
|
||||
AstroGlobalPartial,
|
||||
ClientDirectiveMap,
|
||||
Params,
|
||||
Props,
|
||||
RuntimeMode,
|
||||
|
@ -36,6 +37,7 @@ export interface CreateResultArgs {
|
|||
pathname: string;
|
||||
props: Props;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
clientDirectives: ClientDirectiveMap;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
site: string | undefined;
|
||||
links?: Set<SSRElement>;
|
||||
|
@ -122,7 +124,7 @@ class Slots {
|
|||
let renderMarkdown: any = null;
|
||||
|
||||
export function createResult(args: CreateResultArgs): SSRResult {
|
||||
const { markdown, params, pathname, props: pageProps, renderers, request, resolve } = args;
|
||||
const { clientDirectives, markdown, params, pathname, props: pageProps, renderers, request, resolve } = args;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const headers = new Headers();
|
||||
|
@ -274,6 +276,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
|||
resolve,
|
||||
_metadata: {
|
||||
renderers,
|
||||
clientDirectives,
|
||||
pathname,
|
||||
hasHydrationScript: false,
|
||||
hasDirectives: new Set(),
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { 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 visible: ClientDirective = (load, _opts, root) => {
|
||||
const cb = async () => {
|
||||
let hydrate = await getHydrateCallback();
|
||||
let hydrate = await load();
|
||||
await hydrate();
|
||||
};
|
||||
|
||||
|
@ -24,4 +26,5 @@
|
|||
io.observe(child);
|
||||
}
|
||||
};
|
||||
window.dispatchEvent(new Event('astro:visible'));
|
||||
|
||||
export default visible;
|
||||
|
|
|
@ -62,7 +62,7 @@ declare const Astro: {
|
|||
start() {
|
||||
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
|
||||
const directive = this.getAttribute('client') as directiveAstroKeys;
|
||||
if (Astro[directive] === undefined) {
|
||||
if (typeof Astro === 'undefined' || Astro[directive] === undefined) {
|
||||
window.addEventListener(`astro:${directive}`, () => this.start(), { once: true });
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export const Renderer = Symbol.for('astro:renderer');
|
|||
// Rendering produces either marked strings of HTML or instructions for hydration.
|
||||
// These directive instructions bubble all the way up to renderPage so that we
|
||||
// can ensure they are added only once, and as soon as possible.
|
||||
export function stringifyChunk(result: SSRResult, chunk: string | RenderInstruction) {
|
||||
export function stringifyChunk(result: SSRResult, chunk: string | RenderInstruction): string | Promise<string> {
|
||||
switch ((chunk as any).type) {
|
||||
case 'directive': {
|
||||
const { hydration } = chunk as RenderInstruction;
|
||||
|
@ -29,7 +29,7 @@ export function stringifyChunk(result: SSRResult, chunk: string | RenderInstruct
|
|||
? 'directive'
|
||||
: null;
|
||||
if (prescriptType) {
|
||||
let prescripts = getPrescripts(prescriptType, hydration.directive);
|
||||
let prescripts = getPrescripts(result, prescriptType, hydration.directive);
|
||||
return markHTMLString(prescripts);
|
||||
} else {
|
||||
return '';
|
||||
|
|
|
@ -6,6 +6,7 @@ import { isAstroComponent, isAstroComponentFactory, renderAstroComponent } from
|
|||
import { stringifyChunk } from './common.js';
|
||||
import { renderComponent } from './component.js';
|
||||
import { maybeRenderHead } from './head.js';
|
||||
import { string } from 'zod';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
||||
|
@ -72,7 +73,13 @@ export async function renderPage(
|
|||
let i = 0;
|
||||
try {
|
||||
for await (const chunk of iterable) {
|
||||
let html = stringifyChunk(result, chunk);
|
||||
let html: string;
|
||||
let stringChunk = stringifyChunk(result, chunk);
|
||||
if((stringChunk as any).then !== undefined) {
|
||||
html = await stringChunk;
|
||||
} else {
|
||||
html = (stringChunk as string);
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
|
@ -94,7 +101,13 @@ export async function renderPage(
|
|||
body = '';
|
||||
let i = 0;
|
||||
for await (const chunk of iterable) {
|
||||
let html = stringifyChunk(result, chunk);
|
||||
let html: string;
|
||||
let stringChunk = stringifyChunk(result, chunk);
|
||||
if((stringChunk as any).then !== undefined) {
|
||||
html = await stringChunk;
|
||||
} else {
|
||||
html = (stringChunk as string);
|
||||
}
|
||||
if (i === 0) {
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
body += '<!DOCTYPE html>\n';
|
||||
|
|
|
@ -14,6 +14,8 @@ export function determineIfNeedsHydrationScript(result: SSRResult): boolean {
|
|||
return (result._metadata.hasHydrationScript = true);
|
||||
}
|
||||
|
||||
const ISLAND_STYLES = `<style>astro-island,astro-slot{display:contents}</style>`;
|
||||
|
||||
export const hydrationScripts: Record<string, string> = {
|
||||
idle: idlePrebuilt,
|
||||
load: loadPrebuilt,
|
||||
|
@ -40,18 +42,47 @@ function getDirectiveScriptText(directive: string): string {
|
|||
return directiveScriptText;
|
||||
}
|
||||
|
||||
export function getPrescripts(type: PrescriptType, directive: string): string {
|
||||
function getDirectiveScript(result: SSRResult, directive: string): string | Promise<string> {
|
||||
if(!result._metadata.clientDirectives.has(directive)) {
|
||||
// TODO better error message
|
||||
throw new Error(`Unable to find directive ${directive}`);
|
||||
}
|
||||
let { type, src } = result._metadata.clientDirectives.get(directive)!;
|
||||
switch(type) {
|
||||
case 'external': {
|
||||
return result.resolve(`${src}?astro-client-directive=${directive}`).then(value => {
|
||||
return `<script type="module" src="${value}"></script>`;
|
||||
});
|
||||
}
|
||||
case 'inline': {
|
||||
throw new Error(`Inline not yet supported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPromise<T>(value: any): value is Promise<T> {
|
||||
if(typeof value.then === 'function') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPrescripts(result: SSRResult, type: PrescriptType, directive: string): string | Promise<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>`;
|
||||
let directiveScript = getDirectiveScript(result, directive);
|
||||
if(isPromise<string>(directiveScript)) {
|
||||
return directiveScript.then(scriptText => {
|
||||
return `${ISLAND_STYLES}${scriptText}<script>${islandScript}</script>`;
|
||||
});
|
||||
}
|
||||
return `${ISLAND_STYLES}${directiveScript}<script>${islandScript}</script>`
|
||||
case 'directive':
|
||||
return `<script>${getDirectiveScriptText(directive)}</script>`;
|
||||
return getDirectiveScript(result, directive);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
34
packages/astro/src/vite-plugin-client-directive/index.ts
Normal file
34
packages/astro/src/vite-plugin-client-directive/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import type * as vite from 'vite';
|
||||
import type { AstroConfig, ManifestData } from '../@types/astro';
|
||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
||||
|
||||
interface AstroPluginOptions {
|
||||
config: AstroConfig;
|
||||
logging: LogOptions;
|
||||
}
|
||||
|
||||
export default function createPlugin({ config, logging }: AstroPluginOptions): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:client-directive',
|
||||
transform(code, id, opts = {}) {
|
||||
let idx = id.indexOf('?astro-client-directive');
|
||||
if(idx !== -1) {
|
||||
let entrypoint = id.slice(0, idx);
|
||||
let params = new URLSearchParams(id.slice(idx));
|
||||
let directive = params.get('astro-client-directive');
|
||||
return `
|
||||
import directive from '${entrypoint}';
|
||||
|
||||
(self.Astro = self.Astro || {}).${directive} = directive;
|
||||
window.dispatchEvent(new Event('astro:${directive}'));
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/*if (opts.ssr) return;
|
||||
if (!id.includes('vite/dist/client/client.mjs')) return;
|
||||
return code
|
||||
.replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
|
||||
.replace(/\[vite\]/g, '[astro]');*/
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue