Head propagation (#5511)
* Head propagation * Adding a changeset * Fix broken build * Self review stuff * Use compiler prerelease exact version * new compiler version * Update packages/astro/src/vite-plugin-head-propagation/index.ts Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * Use getAstroMetadata * add .js * make relative lookup work on win * Use compiler@0.30.0 * PR review comments * Make renderHead an alias for a better named function Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
This commit is contained in:
parent
b137657699
commit
05915fec01
36 changed files with 804 additions and 279 deletions
7
.changeset/cool-jobs-draw.md
Normal file
7
.changeset/cool-jobs-draw.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Low-level head propagation
|
||||||
|
|
||||||
|
This adds low-level head propagation ability within the Astro runtime. This is not really useable within an Astro app at the moment, but provides the APIs necessary for `renderEntry` to do head propagation.
|
|
@ -100,7 +100,7 @@
|
||||||
"test:e2e:match": "playwright test -g"
|
"test:e2e:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^0.29.15",
|
"@astrojs/compiler": "^0.30.0",
|
||||||
"@astrojs/language-server": "^0.28.3",
|
"@astrojs/language-server": "^0.28.3",
|
||||||
"@astrojs/markdown-remark": "^1.1.3",
|
"@astrojs/markdown-remark": "^1.1.3",
|
||||||
"@astrojs/telemetry": "^1.0.1",
|
"@astrojs/telemetry": "^1.0.1",
|
||||||
|
@ -111,11 +111,11 @@
|
||||||
"@babel/plugin-transform-react-jsx": "^7.17.12",
|
"@babel/plugin-transform-react-jsx": "^7.17.12",
|
||||||
"@babel/traverse": "^7.18.2",
|
"@babel/traverse": "^7.18.2",
|
||||||
"@babel/types": "^7.18.4",
|
"@babel/types": "^7.18.4",
|
||||||
|
"@proload/core": "^0.3.3",
|
||||||
|
"@proload/plugin-tsm": "^0.2.1",
|
||||||
"@types/babel__core": "^7.1.19",
|
"@types/babel__core": "^7.1.19",
|
||||||
"@types/html-escaper": "^3.0.0",
|
"@types/html-escaper": "^3.0.0",
|
||||||
"@types/yargs-parser": "^21.0.0",
|
"@types/yargs-parser": "^21.0.0",
|
||||||
"@proload/core": "^0.3.3",
|
|
||||||
"@proload/plugin-tsm": "^0.2.1",
|
|
||||||
"boxen": "^6.2.1",
|
"boxen": "^6.2.1",
|
||||||
"ci-info": "^3.3.1",
|
"ci-info": "^3.3.1",
|
||||||
"common-ancestor-path": "^1.0.1",
|
"common-ancestor-path": "^1.0.1",
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type { SerializedSSRManifest } from '../core/app/types';
|
||||||
import type { PageBuildData } from '../core/build/types';
|
import type { PageBuildData } from '../core/build/types';
|
||||||
import type { AstroConfigSchema } from '../core/config';
|
import type { AstroConfigSchema } from '../core/config';
|
||||||
import type { AstroCookies } from '../core/cookies';
|
import type { AstroCookies } from '../core/cookies';
|
||||||
import type { AstroComponentFactory } from '../runtime/server';
|
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
||||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||||
export type {
|
export type {
|
||||||
MarkdownHeading,
|
MarkdownHeading,
|
||||||
|
@ -1398,10 +1398,25 @@ export interface SSRMetadata {
|
||||||
hasRenderedHead: boolean;
|
hasRenderedHead: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hint on whether the Astro runtime needs to wait on a component to render head
|
||||||
|
* content. The meanings:
|
||||||
|
*
|
||||||
|
* - __none__ (default) The component does not propagation head content.
|
||||||
|
* - __self__ The component appends head content.
|
||||||
|
* - __in-tree__ Another component within this component's dependency tree appends head content.
|
||||||
|
*
|
||||||
|
* These are used within the runtime to know whether or not a component should be waited on.
|
||||||
|
*/
|
||||||
|
export type PropagationHint = 'none' | 'self' | 'in-tree';
|
||||||
|
|
||||||
export interface SSRResult {
|
export interface SSRResult {
|
||||||
styles: Set<SSRElement>;
|
styles: Set<SSRElement>;
|
||||||
scripts: Set<SSRElement>;
|
scripts: Set<SSRElement>;
|
||||||
links: Set<SSRElement>;
|
links: Set<SSRElement>;
|
||||||
|
propagation: Map<string, PropagationHint>;
|
||||||
|
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
|
||||||
|
extraHead: Array<any>;
|
||||||
cookies: AstroCookies | undefined;
|
cookies: AstroCookies | undefined;
|
||||||
createAstro(
|
createAstro(
|
||||||
Astro: AstroGlobalPartial,
|
Astro: AstroGlobalPartial,
|
||||||
|
|
|
@ -12,6 +12,7 @@ export interface CompileProps {
|
||||||
astroConfig: AstroConfig;
|
astroConfig: AstroConfig;
|
||||||
viteConfig: ResolvedConfig;
|
viteConfig: ResolvedConfig;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
id: string | undefined;
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ export async function compile({
|
||||||
astroConfig,
|
astroConfig,
|
||||||
viteConfig,
|
viteConfig,
|
||||||
filename,
|
filename,
|
||||||
|
id: moduleId,
|
||||||
source,
|
source,
|
||||||
}: CompileProps): Promise<CompileResult> {
|
}: CompileProps): Promise<CompileResult> {
|
||||||
const cssDeps = new Set<string>();
|
const cssDeps = new Set<string>();
|
||||||
|
@ -35,6 +37,7 @@ export async function compile({
|
||||||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||||
// result passed to esbuild, but also available in the catch handler.
|
// result passed to esbuild, but also available in the catch handler.
|
||||||
transformResult = await transform(source, {
|
transformResult = await transform(source, {
|
||||||
|
moduleId,
|
||||||
pathname: filename,
|
pathname: filename,
|
||||||
projectRoot: astroConfig.root.toString(),
|
projectRoot: astroConfig.root.toString(),
|
||||||
site: astroConfig.site?.toString(),
|
site: astroConfig.site?.toString(),
|
||||||
|
|
|
@ -18,6 +18,7 @@ import legacyMarkdownVitePlugin from '../vite-plugin-markdown-legacy/index.js';
|
||||||
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||||
|
import astroHeadPropagationPlugin from '../vite-plugin-head-propagation/index.js';
|
||||||
import { createCustomViteLogger } from './errors/dev/index.js';
|
import { createCustomViteLogger } from './errors/dev/index.js';
|
||||||
import { resolveDependency } from './util.js';
|
import { resolveDependency } from './util.js';
|
||||||
|
|
||||||
|
@ -112,6 +113,7 @@ export async function createVite(
|
||||||
astroPostprocessVitePlugin({ settings }),
|
astroPostprocessVitePlugin({ settings }),
|
||||||
astroIntegrationsContainerPlugin({ settings, logging }),
|
astroIntegrationsContainerPlugin({ settings, logging }),
|
||||||
astroScriptsPageSSRPlugin({ settings }),
|
astroScriptsPageSSRPlugin({ settings }),
|
||||||
|
astroHeadPropagationPlugin({ settings }),
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(settings.config.publicDir),
|
publicDir: fileURLToPath(settings.config.publicDir),
|
||||||
root: fileURLToPath(settings.config.root),
|
root: fileURLToPath(settings.config.root),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { RouteData, SSRElement } from '../../@types/astro';
|
import type { RouteData, SSRElement, SSRResult } from '../../@types/astro';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The RenderContext represents the parts of rendering that are specific to one request.
|
* The RenderContext represents the parts of rendering that are specific to one request.
|
||||||
|
@ -11,6 +11,7 @@ export interface RenderContext {
|
||||||
scripts?: Set<SSRElement>;
|
scripts?: Set<SSRElement>;
|
||||||
links?: Set<SSRElement>;
|
links?: Set<SSRElement>;
|
||||||
styles?: Set<SSRElement>;
|
styles?: Set<SSRElement>;
|
||||||
|
propagation?: SSRResult['propagation'];
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
|
||||||
params,
|
params,
|
||||||
props: pageProps,
|
props: pageProps,
|
||||||
pathname: ctx.pathname,
|
pathname: ctx.pathname,
|
||||||
|
propagation: ctx.propagation,
|
||||||
resolve: env.resolve,
|
resolve: env.resolve,
|
||||||
renderers: env.renderers,
|
renderers: env.renderers,
|
||||||
request: ctx.request,
|
request: ctx.request,
|
||||||
|
|
34
packages/astro/src/core/render/dev/head.ts
Normal file
34
packages/astro/src/core/render/dev/head.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import type { SSRResult } from '../../../@types/astro';
|
||||||
|
|
||||||
|
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
|
||||||
|
|
||||||
|
import { viteID } from '../../util.js';
|
||||||
|
import { getAstroMetadata } from '../../../vite-plugin-astro/index.js';
|
||||||
|
import { crawlGraph } from './vite.js';
|
||||||
|
|
||||||
|
export async function getPropagationMap(
|
||||||
|
filePath: URL,
|
||||||
|
loader: ModuleLoader
|
||||||
|
): Promise<SSRResult['propagation']> {
|
||||||
|
const map: SSRResult['propagation'] = new Map();
|
||||||
|
|
||||||
|
const rootID = viteID(filePath);
|
||||||
|
addInjection(map, loader.getModuleInfo(rootID))
|
||||||
|
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
|
||||||
|
const id = moduleNode.id;
|
||||||
|
if (id) {
|
||||||
|
addInjection(map, loader.getModuleInfo(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInjection(map: SSRResult['propagation'], modInfo: ModuleInfo | null) {
|
||||||
|
if(modInfo) {
|
||||||
|
const astro = getAstroMetadata(modInfo);
|
||||||
|
if(astro && astro.propagation) {
|
||||||
|
map.set(modInfo.id, astro.propagation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,48 +3,23 @@ import type {
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
RouteData,
|
RouteData,
|
||||||
RuntimeMode,
|
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||||
import { LogOptions } from '../../logger/core.js';
|
|
||||||
import type { ModuleLoader } from '../../module-loader/index';
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
import { isPage, resolveIdToUrl } from '../../util.js';
|
import { isPage, resolveIdToUrl } from '../../util.js';
|
||||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||||
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
|
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
|
||||||
import { RouteCache } from '../route-cache.js';
|
|
||||||
import { getStylesForURL } from './css.js';
|
import { getStylesForURL } from './css.js';
|
||||||
import type { DevelopmentEnvironment } from './environment';
|
import type { DevelopmentEnvironment } from './environment';
|
||||||
import { getScriptsForURL } from './scripts.js';
|
import { getScriptsForURL } from './scripts.js';
|
||||||
|
import { getPropagationMap } from './head.js';
|
||||||
export { createDevelopmentEnvironment } from './environment.js';
|
export { createDevelopmentEnvironment } from './environment.js';
|
||||||
export type { DevelopmentEnvironment };
|
export type { DevelopmentEnvironment };
|
||||||
|
|
||||||
export interface SSROptionsOld {
|
|
||||||
/** an instance of the AstroSettings */
|
|
||||||
settings: AstroSettings;
|
|
||||||
/** location of file on disk */
|
|
||||||
filePath: URL;
|
|
||||||
/** logging options */
|
|
||||||
logging: LogOptions;
|
|
||||||
/** "development" or "production" */
|
|
||||||
mode: RuntimeMode;
|
|
||||||
/** production website */
|
|
||||||
origin: string;
|
|
||||||
/** the web request (needed for dynamic routes) */
|
|
||||||
pathname: string;
|
|
||||||
/** optional, in case we need to render something outside of a dev server */
|
|
||||||
route?: RouteData;
|
|
||||||
/** pass in route cache because SSR can’t manage cache-busting */
|
|
||||||
routeCache: RouteCache;
|
|
||||||
/** Module loader (Vite) */
|
|
||||||
loader: ModuleLoader;
|
|
||||||
/** Request */
|
|
||||||
request: Request;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSROptions {
|
export interface SSROptions {
|
||||||
/** The environment instance */
|
/** The environment instance */
|
||||||
env: DevelopmentEnvironment;
|
env: DevelopmentEnvironment;
|
||||||
|
@ -163,7 +138,9 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { scripts, styles, links };
|
const propagationMap = await getPropagationMap(filePath, env.loader);
|
||||||
|
|
||||||
|
return { scripts, styles, links, propagationMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderPage(options: SSROptions): Promise<Response> {
|
export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
|
@ -173,7 +150,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
// The new instances are passed through.
|
// The new instances are passed through.
|
||||||
options.env.renderers = renderers;
|
options.env.renderers = renderers;
|
||||||
|
|
||||||
const { scripts, links, styles } = await getScriptsAndStyles({
|
const { scripts, links, styles, propagationMap } = await getScriptsAndStyles({
|
||||||
env: options.env,
|
env: options.env,
|
||||||
filePath: options.filePath,
|
filePath: options.filePath,
|
||||||
});
|
});
|
||||||
|
@ -185,6 +162,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
scripts,
|
scripts,
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
|
propagation: propagationMap,
|
||||||
route: options.route,
|
route: options.route,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ export interface CreateResultArgs {
|
||||||
links?: Set<SSRElement>;
|
links?: Set<SSRElement>;
|
||||||
scripts?: Set<SSRElement>;
|
scripts?: Set<SSRElement>;
|
||||||
styles?: Set<SSRElement>;
|
styles?: Set<SSRElement>;
|
||||||
|
propagation?: SSRResult['propagation'];
|
||||||
request: Request;
|
request: Request;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
@ -154,6 +155,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
styles: args.styles ?? new Set<SSRElement>(),
|
styles: args.styles ?? new Set<SSRElement>(),
|
||||||
scripts: args.scripts ?? new Set<SSRElement>(),
|
scripts: args.scripts ?? new Set<SSRElement>(),
|
||||||
links: args.links ?? new Set<SSRElement>(),
|
links: args.links ?? new Set<SSRElement>(),
|
||||||
|
propagation: args.propagation ?? new Map(),
|
||||||
|
propagators: new Map(),
|
||||||
|
extraHead: [],
|
||||||
cookies,
|
cookies,
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro(
|
createAstro(
|
||||||
|
|
|
@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj {
|
||||||
clientOnlyComponents: [],
|
clientOnlyComponents: [],
|
||||||
hydratedComponents: [],
|
hydratedComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
|
propagation: 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
path.node.body.splice(
|
path.node.body.splice(
|
||||||
|
|
29
packages/astro/src/runtime/server/astro-component.ts
Normal file
29
packages/astro/src/runtime/server/astro-component.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { PropagationHint } from '../../@types/astro';
|
||||||
|
import type { AstroComponentFactory } from './render/index.js';
|
||||||
|
|
||||||
|
function baseCreateComponent(cb: AstroComponentFactory, moduleId?: string) {
|
||||||
|
// Add a flag to this callback to mark it as an Astro component
|
||||||
|
cb.isAstroComponentFactory = true;
|
||||||
|
cb.moduleId = moduleId;
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateComponentOptions {
|
||||||
|
factory: AstroComponentFactory;
|
||||||
|
moduleId?: string;
|
||||||
|
propagation?: PropagationHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createComponentWithOptions(opts: CreateComponentOptions) {
|
||||||
|
const cb = baseCreateComponent(opts.factory, opts.moduleId);
|
||||||
|
cb.propagation = opts.propagation;
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
// Used in creating the component. aka the main export.
|
||||||
|
export function createComponent(arg1: AstroComponentFactory, moduleId: string) {
|
||||||
|
if(typeof arg1 === 'function') {
|
||||||
|
return baseCreateComponent(arg1, moduleId);
|
||||||
|
} else {
|
||||||
|
return createComponentWithOptions(arg1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ export function createAstro(
|
||||||
fetchContent: createDeprecatedFetchContentFn(),
|
fetchContent: createDeprecatedFetchContentFn(),
|
||||||
glob: createAstroGlobFn(),
|
glob: createAstroGlobFn(),
|
||||||
// INVESTIGATE is there a use-case for multi args?
|
// INVESTIGATE is there a use-case for multi args?
|
||||||
|
// TODO remove in 2.0
|
||||||
resolve(...segments: string[]) {
|
resolve(...segments: string[]) {
|
||||||
let resolved = segments.reduce((u, segment) => new URL(segment, u), referenceURL).pathname;
|
let resolved = segments.reduce((u, segment) => new URL(segment, u), referenceURL).pathname;
|
||||||
// When inside of project root, remove the leading path so you are
|
// When inside of project root, remove the leading path so you are
|
||||||
|
|
|
@ -4,11 +4,13 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from
|
||||||
export { renderJSX } from './jsx.js';
|
export { renderJSX } from './jsx.js';
|
||||||
export {
|
export {
|
||||||
addAttribute,
|
addAttribute,
|
||||||
|
createHeadAndContent,
|
||||||
defineScriptVars,
|
defineScriptVars,
|
||||||
Fragment,
|
Fragment,
|
||||||
maybeRenderHead,
|
maybeRenderHead,
|
||||||
renderAstroComponent,
|
renderAstroTemplateResult as renderAstroComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
|
renderComponentToIterable,
|
||||||
Renderer as Renderer,
|
Renderer as Renderer,
|
||||||
renderHead,
|
renderHead,
|
||||||
renderHTMLElement,
|
renderHTMLElement,
|
||||||
|
@ -16,26 +18,18 @@ export {
|
||||||
renderSlot,
|
renderSlot,
|
||||||
renderTemplate as render,
|
renderTemplate as render,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
|
renderUniqueStylesheet,
|
||||||
renderToString,
|
renderToString,
|
||||||
stringifyChunk,
|
stringifyChunk,
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
} from './render/index.js';
|
} from './render/index.js';
|
||||||
export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
|
export { createComponent } from './astro-component.js';
|
||||||
import type { AstroComponentFactory } from './render/index.js';
|
export type { AstroComponentFactory, AstroComponentInstance, RenderInstruction } from './render/index.js';
|
||||||
|
|
||||||
import { markHTMLString } from './escape.js';
|
import { markHTMLString } from './escape.js';
|
||||||
import { Renderer } from './render/index.js';
|
import { Renderer } from './render/index.js';
|
||||||
|
|
||||||
import { addAttribute } from './render/index.js';
|
import { addAttribute } from './render/index.js';
|
||||||
|
|
||||||
// Used in creating the component. aka the main export.
|
|
||||||
export function createComponent(cb: AstroComponentFactory) {
|
|
||||||
// Add a flag to this callback to mark it as an Astro component
|
|
||||||
// INVESTIGATE does this need to cast
|
|
||||||
(cb as any).isAstroComponentFactory = true;
|
|
||||||
return cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeSlots(...slotted: unknown[]) {
|
export function mergeSlots(...slotted: unknown[]) {
|
||||||
const slots: Record<string, () => any> = {};
|
const slots: Record<string, () => any> = {};
|
||||||
for (const slot of slotted) {
|
for (const slot of slotted) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
HTMLString,
|
HTMLString,
|
||||||
markHTMLString,
|
markHTMLString,
|
||||||
renderComponent,
|
renderComponentToIterable,
|
||||||
renderToString,
|
renderToString,
|
||||||
spreadAttributes,
|
spreadAttributes,
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
|
@ -177,7 +177,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
props[Skip.symbol] = skip;
|
props[Skip.symbol] = skip;
|
||||||
let output: ComponentIterable;
|
let output: ComponentIterable;
|
||||||
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
|
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
|
||||||
output = await renderComponent(
|
output = await renderComponentToIterable(
|
||||||
result,
|
result,
|
||||||
vnode.props['client:display-name'] ?? '',
|
vnode.props['client:display-name'] ?? '',
|
||||||
null,
|
null,
|
||||||
|
@ -185,7 +185,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
slots
|
slots
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
output = await renderComponent(
|
output = await renderComponentToIterable(
|
||||||
result,
|
result,
|
||||||
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
|
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
|
||||||
vnode.type,
|
vnode.type,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
|
import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
|
||||||
import { AstroComponent, renderAstroComponent } from './astro.js';
|
import { isRenderTemplateResult, renderAstroTemplateResult } from './astro/index.js';
|
||||||
|
import { isAstroComponentInstance } from './astro/index.js';
|
||||||
import { SlotString } from './slot.js';
|
import { SlotString } from './slot.js';
|
||||||
|
|
||||||
export async function* renderChild(child: any): AsyncIterable<any> {
|
export async function* renderChild(child: any): AsyncIterable<any> {
|
||||||
|
@ -25,13 +26,10 @@ export async function* renderChild(child: any): AsyncIterable<any> {
|
||||||
} else if (!child && child !== 0) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
}
|
}
|
||||||
// Add a comment explaining why each of these are needed.
|
else if(isRenderTemplateResult(child)) {
|
||||||
// Maybe create clearly named function for what this is doing.
|
yield* renderAstroTemplateResult(child);
|
||||||
else if (
|
} else if(isAstroComponentInstance(child)) {
|
||||||
child instanceof AstroComponent ||
|
yield* child.render();
|
||||||
Object.prototype.toString.call(child) === '[object AstroComponent]'
|
|
||||||
) {
|
|
||||||
yield* renderAstroComponent(child);
|
|
||||||
} else if (ArrayBuffer.isView(child)) {
|
} else if (ArrayBuffer.isView(child)) {
|
||||||
yield child;
|
yield child;
|
||||||
} else if (
|
} else if (
|
||||||
|
|
|
@ -1,146 +0,0 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
|
||||||
import type { AstroComponentFactory } from './index';
|
|
||||||
import type { RenderInstruction } from './types';
|
|
||||||
|
|
||||||
import { HTMLBytes, markHTMLString } from '../escape.js';
|
|
||||||
import { HydrationDirectiveProps } from '../hydration.js';
|
|
||||||
import { isPromise } from '../util.js';
|
|
||||||
import { renderChild } from './any.js';
|
|
||||||
import { HTMLParts } from './common.js';
|
|
||||||
|
|
||||||
// Issue warnings for invalid props for Astro components
|
|
||||||
function validateComponentProps(props: any, displayName: string) {
|
|
||||||
if (props != null) {
|
|
||||||
for (const prop of Object.keys(props)) {
|
|
||||||
if (HydrationDirectiveProps.has(prop)) {
|
|
||||||
// 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.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The return value when rendering a component.
|
|
||||||
// This is the result of calling render(), should this be named to RenderResult or...?
|
|
||||||
export class AstroComponent {
|
|
||||||
private htmlParts: TemplateStringsArray;
|
|
||||||
private expressions: any[];
|
|
||||||
private error: Error | undefined;
|
|
||||||
|
|
||||||
constructor(htmlParts: TemplateStringsArray, expressions: any[]) {
|
|
||||||
this.htmlParts = htmlParts;
|
|
||||||
this.error = undefined;
|
|
||||||
this.expressions = expressions.map((expression) => {
|
|
||||||
// Wrap Promise expressions so we can catch errors
|
|
||||||
// There can only be 1 error that we rethrow from an Astro component,
|
|
||||||
// so this keeps track of whether or not we have already done so.
|
|
||||||
if (isPromise(expression)) {
|
|
||||||
return Promise.resolve(expression).catch((err) => {
|
|
||||||
if (!this.error) {
|
|
||||||
this.error = err;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return expression;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
|
||||||
return 'AstroComponent';
|
|
||||||
}
|
|
||||||
|
|
||||||
async *[Symbol.asyncIterator]() {
|
|
||||||
const { htmlParts, expressions } = this;
|
|
||||||
|
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
|
||||||
const html = htmlParts[i];
|
|
||||||
const expression = expressions[i];
|
|
||||||
|
|
||||||
yield markHTMLString(html);
|
|
||||||
yield* renderChild(expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if a component is an .astro component
|
|
||||||
export function isAstroComponent(obj: any): obj is AstroComponent {
|
|
||||||
return (
|
|
||||||
typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAstroComponentFactory(obj: any): obj is AstroComponentFactory {
|
|
||||||
return obj == null ? false : obj.isAstroComponentFactory === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* renderAstroComponent(
|
|
||||||
component: InstanceType<typeof AstroComponent>
|
|
||||||
): AsyncIterable<string | HTMLBytes | RenderInstruction> {
|
|
||||||
for await (const value of component) {
|
|
||||||
if (value || value === 0) {
|
|
||||||
for await (const chunk of renderChild(value)) {
|
|
||||||
switch (chunk.type) {
|
|
||||||
case 'directive': {
|
|
||||||
yield chunk;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
yield markHTMLString(chunk);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls a component and renders it into a string of HTML
|
|
||||||
export async function renderToString(
|
|
||||||
result: SSRResult,
|
|
||||||
componentFactory: AstroComponentFactory,
|
|
||||||
props: any,
|
|
||||||
children: any
|
|
||||||
): Promise<string> {
|
|
||||||
const Component = await componentFactory(result, props, children);
|
|
||||||
|
|
||||||
if (!isAstroComponent(Component)) {
|
|
||||||
const response: Response = Component;
|
|
||||||
throw response;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts = new HTMLParts();
|
|
||||||
for await (const chunk of renderAstroComponent(Component)) {
|
|
||||||
parts.append(chunk, result);
|
|
||||||
}
|
|
||||||
return parts.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderToIterable(
|
|
||||||
result: SSRResult,
|
|
||||||
componentFactory: AstroComponentFactory,
|
|
||||||
displayName: string,
|
|
||||||
props: any,
|
|
||||||
children: any
|
|
||||||
): Promise<AsyncIterable<string | HTMLBytes | RenderInstruction>> {
|
|
||||||
validateComponentProps(props, displayName);
|
|
||||||
const Component = await componentFactory(result, props, children);
|
|
||||||
|
|
||||||
if (!isAstroComponent(Component)) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
`Returning a Response is only supported inside of page components. Consider refactoring this logic into something like a function that can be used in the page.`
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = Component;
|
|
||||||
throw response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderAstroComponent(Component);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
|
||||||
return new AstroComponent(htmlParts, expressions);
|
|
||||||
}
|
|
53
packages/astro/src/runtime/server/render/astro/factory.ts
Normal file
53
packages/astro/src/runtime/server/render/astro/factory.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import type { SSRResult, PropagationHint } from '../../../../@types/astro';
|
||||||
|
import type { HeadAndContent } from './head-and-content';
|
||||||
|
import type { RenderTemplateResult } from './render-template';
|
||||||
|
|
||||||
|
import { renderAstroTemplateResult } from './render-template.js';
|
||||||
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
|
import { HTMLParts } from '../common.js';
|
||||||
|
|
||||||
|
export type AstroFactoryReturnValue = RenderTemplateResult | Response | HeadAndContent;
|
||||||
|
|
||||||
|
// The callback passed to to $$createComponent
|
||||||
|
export interface AstroComponentFactory {
|
||||||
|
(result: any, props: any, slots: any): AstroFactoryReturnValue;
|
||||||
|
isAstroComponentFactory?: boolean;
|
||||||
|
moduleId: string | undefined;
|
||||||
|
propagation?: PropagationHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAstroComponentFactory(obj: any): obj is AstroComponentFactory {
|
||||||
|
return obj == null ? false : obj.isAstroComponentFactory === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calls a component and renders it into a string of HTML
|
||||||
|
export async function renderToString(
|
||||||
|
result: SSRResult,
|
||||||
|
componentFactory: AstroComponentFactory,
|
||||||
|
props: any,
|
||||||
|
children: any
|
||||||
|
): Promise<string> {
|
||||||
|
const factoryResult = await componentFactory(result, props, children);
|
||||||
|
|
||||||
|
if (factoryResult instanceof Response) {
|
||||||
|
const response = factoryResult;
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = new HTMLParts();
|
||||||
|
const templateResult = isHeadAndContent(factoryResult) ? factoryResult.content : factoryResult;
|
||||||
|
for await (const chunk of renderAstroTemplateResult(templateResult)) {
|
||||||
|
parts.append(chunk, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return parts.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAPropagatingComponent(result: SSRResult, factory: AstroComponentFactory): boolean {
|
||||||
|
let hint: PropagationHint = factory.propagation || 'none';
|
||||||
|
if(factory.moduleId && result.propagation.has(factory.moduleId) && hint === 'none') {
|
||||||
|
hint = result.propagation.get(factory.moduleId)!;
|
||||||
|
}
|
||||||
|
return hint === 'in-tree' || hint === 'self';
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { RenderTemplateResult } from './render-template';
|
||||||
|
|
||||||
|
const headAndContentSym = Symbol.for('astro.headAndContent');
|
||||||
|
|
||||||
|
export type HeadAndContent = {
|
||||||
|
[headAndContentSym]: true;
|
||||||
|
head: string | RenderTemplateResult;
|
||||||
|
content: RenderTemplateResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHeadAndContent(obj: unknown): obj is HeadAndContent {
|
||||||
|
return typeof obj === 'object' && !!((obj as any)[headAndContentSym]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHeadAndContent(
|
||||||
|
head: string | RenderTemplateResult,
|
||||||
|
content: RenderTemplateResult
|
||||||
|
): HeadAndContent {
|
||||||
|
return {
|
||||||
|
[headAndContentSym]: true,
|
||||||
|
head,
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
25
packages/astro/src/runtime/server/render/astro/index.ts
Normal file
25
packages/astro/src/runtime/server/render/astro/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
export {
|
||||||
|
createAstroComponentInstance,
|
||||||
|
isAstroComponentInstance
|
||||||
|
} from './instance.js';
|
||||||
|
export {
|
||||||
|
isAstroComponentFactory,
|
||||||
|
renderToString
|
||||||
|
} from './factory.js';
|
||||||
|
export {
|
||||||
|
isRenderTemplateResult,
|
||||||
|
renderAstroTemplateResult,
|
||||||
|
renderTemplate
|
||||||
|
} from './render-template.js';
|
||||||
|
export {
|
||||||
|
isHeadAndContent,
|
||||||
|
createHeadAndContent
|
||||||
|
} from './head-and-content.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AstroComponentFactory
|
||||||
|
} from './factory';
|
||||||
|
export type {
|
||||||
|
AstroComponentInstance
|
||||||
|
} from './instance';
|
82
packages/astro/src/runtime/server/render/astro/instance.ts
Normal file
82
packages/astro/src/runtime/server/render/astro/instance.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import type { SSRResult } from '../../../../@types/astro';
|
||||||
|
import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js';
|
||||||
|
|
||||||
|
import { HydrationDirectiveProps } from '../../hydration.js';
|
||||||
|
import { renderChild } from '../any.js';
|
||||||
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
|
import { isAPropagatingComponent } from './factory.js';
|
||||||
|
import { isPromise } from '../../util.js';
|
||||||
|
|
||||||
|
type ComponentProps = Record<string | number, any>;
|
||||||
|
|
||||||
|
const astroComponentInstanceSym = Symbol.for('astro.componentInstance');
|
||||||
|
|
||||||
|
export class AstroComponentInstance {
|
||||||
|
[astroComponentInstanceSym] = true;
|
||||||
|
|
||||||
|
private readonly result: SSRResult;
|
||||||
|
private readonly props: ComponentProps;
|
||||||
|
private readonly slots: any;
|
||||||
|
private readonly factory: AstroComponentFactory;
|
||||||
|
private returnValue: ReturnType<AstroComponentFactory> | undefined;
|
||||||
|
constructor(result: SSRResult, props: ComponentProps, slots: any, factory: AstroComponentFactory) {
|
||||||
|
this.result = result;
|
||||||
|
this.props = props;
|
||||||
|
this.slots = slots;
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.returnValue = this.factory(this.result, this.props, this.slots);
|
||||||
|
return this.returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *render() {
|
||||||
|
if(this.returnValue === undefined) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: AstroFactoryReturnValue | undefined = this.returnValue;
|
||||||
|
if(isPromise(value)) {
|
||||||
|
value = await value;
|
||||||
|
}
|
||||||
|
if(isHeadAndContent(value)) {
|
||||||
|
yield * value.content;
|
||||||
|
} else {
|
||||||
|
yield * renderChild(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue warnings for invalid props for Astro components
|
||||||
|
function validateComponentProps(props: any, displayName: string) {
|
||||||
|
if (props != null) {
|
||||||
|
for (const prop of Object.keys(props)) {
|
||||||
|
if (HydrationDirectiveProps.has(prop)) {
|
||||||
|
// 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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAstroComponentInstance(
|
||||||
|
result: SSRResult,
|
||||||
|
displayName: string,
|
||||||
|
factory: AstroComponentFactory,
|
||||||
|
props: ComponentProps,
|
||||||
|
slots: any = {}
|
||||||
|
) {
|
||||||
|
validateComponentProps(props, displayName);
|
||||||
|
const instance = new AstroComponentInstance(result, props, slots, factory);
|
||||||
|
if(isAPropagatingComponent(result, factory) && !result.propagators.has(factory)) {
|
||||||
|
result.propagators.set(factory, instance);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAstroComponentInstance(obj: unknown): obj is AstroComponentInstance {
|
||||||
|
return typeof obj === 'object' && !!((obj as any)[astroComponentInstanceSym]);
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { RenderInstruction } from '../types';
|
||||||
|
|
||||||
|
import { HTMLBytes, markHTMLString } from '../../escape.js';
|
||||||
|
import { isPromise } from '../../util.js';
|
||||||
|
import { renderChild } from '../any.js';
|
||||||
|
|
||||||
|
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
|
||||||
|
|
||||||
|
// The return value when rendering a component.
|
||||||
|
// This is the result of calling render(), should this be named to RenderResult or...?
|
||||||
|
export class RenderTemplateResult {
|
||||||
|
public [renderTemplateResultSym] = true;
|
||||||
|
private htmlParts: TemplateStringsArray;
|
||||||
|
private expressions: any[];
|
||||||
|
private error: Error | undefined;
|
||||||
|
constructor(htmlParts: TemplateStringsArray, expressions: unknown[]) {
|
||||||
|
this.htmlParts = htmlParts;
|
||||||
|
this.error = undefined;
|
||||||
|
this.expressions = expressions.map((expression) => {
|
||||||
|
// Wrap Promise expressions so we can catch errors
|
||||||
|
// There can only be 1 error that we rethrow from an Astro component,
|
||||||
|
// so this keeps track of whether or not we have already done so.
|
||||||
|
if (isPromise(expression)) {
|
||||||
|
return Promise.resolve(expression).catch((err) => {
|
||||||
|
if (!this.error) {
|
||||||
|
this.error = err;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return expression;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this is legacy and should be removed in 2.0
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return 'AstroComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
const { htmlParts, expressions } = this;
|
||||||
|
|
||||||
|
for (let i = 0; i < htmlParts.length; i++) {
|
||||||
|
const html = htmlParts[i];
|
||||||
|
const expression = expressions[i];
|
||||||
|
|
||||||
|
yield markHTMLString(html);
|
||||||
|
yield* renderChild(expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if a component is an .astro component
|
||||||
|
export function isRenderTemplateResult(obj: unknown): obj is RenderTemplateResult {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' && !!((obj as any)[renderTemplateResultSym])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* renderAstroTemplateResult(
|
||||||
|
component: RenderTemplateResult
|
||||||
|
): AsyncIterable<string | HTMLBytes | RenderInstruction> {
|
||||||
|
for await (const value of component) {
|
||||||
|
if (value || value === 0) {
|
||||||
|
for await (const chunk of renderChild(value)) {
|
||||||
|
switch (chunk.type) {
|
||||||
|
case 'directive': {
|
||||||
|
yield chunk;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
yield markHTMLString(chunk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
||||||
|
return new RenderTemplateResult(htmlParts, expressions);
|
||||||
|
}
|
|
@ -12,15 +12,18 @@ import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
||||||
import { serializeProps } from '../serialize.js';
|
import { serializeProps } from '../serialize.js';
|
||||||
import { shorthash } from '../shorthash.js';
|
import { shorthash } from '../shorthash.js';
|
||||||
import {
|
import {
|
||||||
|
createAstroComponentInstance,
|
||||||
isAstroComponentFactory,
|
isAstroComponentFactory,
|
||||||
renderAstroComponent,
|
isAstroComponentInstance,
|
||||||
|
renderAstroTemplateResult,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
renderToIterable,
|
type AstroComponentInstance
|
||||||
} from './astro.js';
|
} from './astro/index.js';
|
||||||
import { Fragment, Renderer, stringifyChunk } from './common.js';
|
import { Fragment, Renderer, stringifyChunk } from './common.js';
|
||||||
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
||||||
import { renderSlot, renderSlots } from './slot.js';
|
import { renderSlot, renderSlots } from './slot.js';
|
||||||
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
||||||
|
import { isPromise } from '../util.js';
|
||||||
|
|
||||||
const rendererAliases = new Map([['solid', 'solid-js']]);
|
const rendererAliases = new Map([['solid', 'solid-js']]);
|
||||||
|
|
||||||
|
@ -45,65 +48,25 @@ function guessRenderers(componentUrl?: string): string[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComponentType = 'fragment' | 'html' | 'astro-factory' | 'unknown';
|
|
||||||
export type ComponentIterable = AsyncIterable<string | HTMLBytes | RenderInstruction>;
|
export type ComponentIterable = AsyncIterable<string | HTMLBytes | RenderInstruction>;
|
||||||
|
|
||||||
function getComponentType(Component: unknown): ComponentType {
|
function isFragmentComponent(Component: unknown) {
|
||||||
if (Component === Fragment) {
|
return Component === Fragment;
|
||||||
return 'fragment';
|
|
||||||
}
|
|
||||||
if (Component && typeof Component === 'object' && (Component as any)['astro:html']) {
|
|
||||||
return 'html';
|
|
||||||
}
|
|
||||||
if (isAstroComponentFactory(Component)) {
|
|
||||||
return 'astro-factory';
|
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderComponent(
|
function isHTMLComponent(Component: unknown) {
|
||||||
|
return (
|
||||||
|
Component && typeof Component === 'object' && (Component as any)['astro:html']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderFrameworkComponent(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
_props: Record<string | number, any>,
|
_props: Record<string | number, any>,
|
||||||
slots: any = {},
|
slots: any = {},
|
||||||
route?: RouteData | undefined
|
|
||||||
): Promise<ComponentIterable> {
|
): Promise<ComponentIterable> {
|
||||||
Component = (await Component) ?? Component;
|
|
||||||
|
|
||||||
switch (getComponentType(Component)) {
|
|
||||||
case 'fragment': {
|
|
||||||
const children = await renderSlot(result, slots?.default);
|
|
||||||
if (children == null) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
return markHTMLString(children);
|
|
||||||
}
|
|
||||||
|
|
||||||
// .html components
|
|
||||||
case 'html': {
|
|
||||||
const { slotInstructions, children } = await renderSlots(result, slots);
|
|
||||||
const html = (Component as any).render({ slots: children });
|
|
||||||
const hydrationHtml = slotInstructions
|
|
||||||
? slotInstructions.map((instr) => stringifyChunk(result, instr)).join('')
|
|
||||||
: '';
|
|
||||||
return markHTMLString(hydrationHtml + html);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'astro-factory': {
|
|
||||||
async function* renderAstroComponentInline(): AsyncGenerator<
|
|
||||||
string | HTMLBytes | RenderInstruction,
|
|
||||||
void,
|
|
||||||
undefined
|
|
||||||
> {
|
|
||||||
let iterable = await renderToIterable(result, Component as any, displayName, _props, slots);
|
|
||||||
yield* iterable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderAstroComponentInline();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Component && !_props['client:only']) {
|
if (!Component && !_props['client:only']) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
|
`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
|
||||||
|
@ -284,7 +247,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
// as a string and the user is responsible for adding a script tag for the component definition.
|
// as a string and the user is responsible for adding a script tag for the component definition.
|
||||||
if (!html && typeof Component === 'string') {
|
if (!html && typeof Component === 'string') {
|
||||||
const childSlots = Object.values(children).join('');
|
const childSlots = Object.values(children).join('');
|
||||||
const iterable = renderAstroComponent(
|
const iterable = renderAstroTemplateResult(
|
||||||
await renderTemplate`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
|
await renderTemplate`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
|
||||||
childSlots === '' && voidElementNames.test(Component)
|
childSlots === '' && voidElementNames.test(Component)
|
||||||
? `/>`
|
? `/>`
|
||||||
|
@ -365,3 +328,68 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
|
|
||||||
return renderAll();
|
return renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderFragmentComponent(result: SSRResult, slots: any = {}) {
|
||||||
|
const children = await renderSlot(result, slots?.default);
|
||||||
|
if (children == null) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return markHTMLString(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderHTMLComponent(
|
||||||
|
result: SSRResult,
|
||||||
|
Component: unknown,
|
||||||
|
_props: Record<string | number, any>,
|
||||||
|
slots: any = {}
|
||||||
|
) {
|
||||||
|
const { slotInstructions, children } = await renderSlots(result, slots);
|
||||||
|
const html = (Component as any).render({ slots: children });
|
||||||
|
const hydrationHtml = slotInstructions
|
||||||
|
? slotInstructions.map((instr) => stringifyChunk(result, instr)).join('')
|
||||||
|
: '';
|
||||||
|
return markHTMLString(hydrationHtml + html);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderComponent(
|
||||||
|
result: SSRResult,
|
||||||
|
displayName: string,
|
||||||
|
Component: unknown,
|
||||||
|
props: Record<string | number, any>,
|
||||||
|
slots: any = {}
|
||||||
|
): Promise<ComponentIterable> | ComponentIterable | AstroComponentInstance {
|
||||||
|
if(isPromise(Component)) {
|
||||||
|
return Promise.resolve(Component).then(Unwrapped => {
|
||||||
|
return renderComponent(result, displayName, Unwrapped, props, slots) as any;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isFragmentComponent(Component)) {
|
||||||
|
return renderFragmentComponent(result, slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
// .html components
|
||||||
|
if(isHTMLComponent(Component)) {
|
||||||
|
return renderHTMLComponent(result, Component, props, slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isAstroComponentFactory(Component)) {
|
||||||
|
return createAstroComponentInstance(result, displayName, Component, props, slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderFrameworkComponent(result, displayName, Component, props, slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderComponentToIterable(
|
||||||
|
result: SSRResult,
|
||||||
|
displayName: string,
|
||||||
|
Component: unknown,
|
||||||
|
props: Record<string | number, any>,
|
||||||
|
slots: any = {}
|
||||||
|
): Promise<ComponentIterable> | ComponentIterable {
|
||||||
|
const renderResult = renderComponent(result, displayName, Component, props, slots);
|
||||||
|
if(isAstroComponentInstance(renderResult)) {
|
||||||
|
return renderResult.render();
|
||||||
|
}
|
||||||
|
return renderResult;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { SSRResult } from '../../../@types/astro';
|
||||||
|
|
||||||
import { markHTMLString } from '../escape.js';
|
import { markHTMLString } from '../escape.js';
|
||||||
import { renderElement } from './util.js';
|
import { renderElement } from './util.js';
|
||||||
|
import { renderChild } from './any.js';
|
||||||
|
|
||||||
// Filter out duplicate elements in our set
|
// Filter out duplicate elements in our set
|
||||||
const uniqueElements = (item: any, index: number, all: any[]) => {
|
const uniqueElements = (item: any, index: number, all: any[]) => {
|
||||||
|
@ -12,8 +13,14 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderHead(result: SSRResult): Promise<string> {
|
async function * renderExtraHead(result: SSRResult, base: string) {
|
||||||
result._metadata.hasRenderedHead = true;
|
yield base;
|
||||||
|
for(const part of result.extraHead) {
|
||||||
|
yield * renderChild(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAllHeadContent(result: SSRResult) {
|
||||||
const styles = Array.from(result.styles)
|
const styles = Array.from(result.styles)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((style) => renderElement('style', style));
|
.map((style) => renderElement('style', style));
|
||||||
|
@ -27,16 +34,30 @@ export function renderHead(result: SSRResult): Promise<string> {
|
||||||
const links = Array.from(result.links)
|
const links = Array.from(result.links)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((link) => renderElement('link', link, false));
|
.map((link) => renderElement('link', link, false));
|
||||||
return markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n'));
|
|
||||||
|
const baseHeadContent = markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n'))
|
||||||
|
|
||||||
|
if(result.extraHead.length > 0) {
|
||||||
|
return renderExtraHead(result, baseHeadContent);
|
||||||
|
} else {
|
||||||
|
return baseHeadContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRenderHead(result: SSRResult) {
|
||||||
|
result._metadata.hasRenderedHead = true;
|
||||||
|
return renderAllHeadContent.bind(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderHead = createRenderHead;
|
||||||
|
|
||||||
// This function is called by Astro components that do not contain a <head> component
|
// This function is called by Astro components that do not contain a <head> component
|
||||||
// This accommodates the fact that using a <head> is optional in Astro, so this
|
// This accommodates the fact that using a <head> is optional in Astro, so this
|
||||||
// is called before a component's first non-head HTML element. If the head was
|
// is called before a component's first non-head HTML element. If the head was
|
||||||
// already injected it is a noop.
|
// already injected it is a noop.
|
||||||
export async function* maybeRenderHead(result: SSRResult): AsyncIterable<string> {
|
export async function* maybeRenderHead(result: SSRResult) {
|
||||||
if (result._metadata.hasRenderedHead) {
|
if (result._metadata.hasRenderedHead) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
yield renderHead(result);
|
yield createRenderHead(result)();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
import { renderTemplate } from './astro.js';
|
export type { RenderInstruction } from './types';
|
||||||
|
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
|
||||||
|
|
||||||
export { renderAstroComponent, renderTemplate, renderToString } from './astro.js';
|
export { createHeadAndContent, renderAstroTemplateResult, renderToString, renderTemplate } from './astro/index.js';
|
||||||
export { Fragment, Renderer, stringifyChunk } from './common.js';
|
export { Fragment, Renderer, stringifyChunk } from './common.js';
|
||||||
export { renderComponent } from './component.js';
|
export { renderComponent, renderComponentToIterable } from './component.js';
|
||||||
export { renderHTMLElement } from './dom.js';
|
export { renderHTMLElement } from './dom.js';
|
||||||
export { maybeRenderHead, renderHead } from './head.js';
|
export { maybeRenderHead, renderHead } from './head.js';
|
||||||
export { renderPage } from './page.js';
|
export { renderPage } from './page.js';
|
||||||
export { renderSlot } from './slot.js';
|
export { renderSlot } from './slot.js';
|
||||||
export type { RenderInstruction } from './types';
|
|
||||||
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
||||||
|
export { renderUniqueStylesheet } from './stylesheet.js';
|
||||||
// The callback passed to to $$createComponent
|
|
||||||
export interface AstroComponentFactory {
|
|
||||||
(result: any, props: any, slots: any): ReturnType<typeof renderTemplate> | Response;
|
|
||||||
isAstroComponentFactory?: boolean;
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,13 @@ import type { AstroComponentFactory } from './index';
|
||||||
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
import { isHTMLString } from '../escape.js';
|
import { isHTMLString } from '../escape.js';
|
||||||
import { createResponse } from '../response.js';
|
import { createResponse } from '../response.js';
|
||||||
import { isAstroComponent, isAstroComponentFactory, renderAstroComponent } from './astro.js';
|
import {
|
||||||
|
isAstroComponentFactory,
|
||||||
|
isAstroComponentInstance,
|
||||||
|
isRenderTemplateResult,
|
||||||
|
isHeadAndContent,
|
||||||
|
renderAstroTemplateResult
|
||||||
|
} from './astro/index.js';
|
||||||
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
|
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
|
||||||
import { renderComponent } from './component.js';
|
import { renderComponent } from './component.js';
|
||||||
import { maybeRenderHead } from './head.js';
|
import { maybeRenderHead } from './head.js';
|
||||||
|
@ -45,6 +51,22 @@ async function iterableToHTMLBytes(
|
||||||
return parts.toArrayBuffer();
|
return parts.toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively calls component instances that might have head content
|
||||||
|
// to be propagated up.
|
||||||
|
async function bufferHeadContent(result: SSRResult) {
|
||||||
|
const iterator = result.propagators.values();
|
||||||
|
while(true) {
|
||||||
|
const { value, done } = iterator.next();
|
||||||
|
if(done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const returnValue = await value.init();
|
||||||
|
if(isHeadAndContent(returnValue)) {
|
||||||
|
result.extraHead.push(returnValue.head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderPage(
|
export async function renderPage(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
componentFactory: AstroComponentFactory | NonAstroPageComponent,
|
componentFactory: AstroComponentFactory | NonAstroPageComponent,
|
||||||
|
@ -57,16 +79,19 @@ export async function renderPage(
|
||||||
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
||||||
|
|
||||||
let output: ComponentIterable;
|
let output: ComponentIterable;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
output = await renderComponent(
|
const renderResult = await renderComponent(
|
||||||
result,
|
result,
|
||||||
componentFactory.name,
|
componentFactory.name,
|
||||||
componentFactory,
|
componentFactory,
|
||||||
pageProps,
|
pageProps,
|
||||||
null,
|
null,
|
||||||
route
|
|
||||||
);
|
);
|
||||||
|
if(isAstroComponentInstance(renderResult)) {
|
||||||
|
output = renderResult.render();
|
||||||
|
} else {
|
||||||
|
output = renderResult;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (AstroError.is(e) && !e.loc) {
|
if (AstroError.is(e) && !e.loc) {
|
||||||
e.setLocation({
|
e.setLocation({
|
||||||
|
@ -94,9 +119,13 @@ export async function renderPage(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const factoryReturnValue = await componentFactory(result, props, children);
|
const factoryReturnValue = await componentFactory(result, props, children);
|
||||||
|
const factoryIsHeadAndContent = isHeadAndContent(factoryReturnValue);
|
||||||
|
if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) {
|
||||||
|
// Wait for head content to be buffered up
|
||||||
|
await bufferHeadContent(result);
|
||||||
|
const templateResult = factoryIsHeadAndContent ? factoryReturnValue.content : factoryReturnValue;
|
||||||
|
|
||||||
if (isAstroComponent(factoryReturnValue)) {
|
let iterable = renderAstroTemplateResult(templateResult);
|
||||||
let iterable = renderAstroComponent(factoryReturnValue);
|
|
||||||
let init = result.response;
|
let init = result.response;
|
||||||
let headers = new Headers(init.headers);
|
let headers = new Headers(init.headers);
|
||||||
let body: BodyInit;
|
let body: BodyInit;
|
||||||
|
|
25
packages/astro/src/runtime/server/render/stylesheet.ts
Normal file
25
packages/astro/src/runtime/server/render/stylesheet.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { SSRResult } from '../../../@types/astro';
|
||||||
|
import { renderElement } from './util.js';
|
||||||
|
import { markHTMLString } from '../escape.js';
|
||||||
|
|
||||||
|
const stylesheetRel = 'stylesheet';
|
||||||
|
|
||||||
|
export function renderStylesheet({ href }: { href: string }) {
|
||||||
|
return markHTMLString(renderElement('link', {
|
||||||
|
props: {
|
||||||
|
rel: stylesheetRel,
|
||||||
|
href
|
||||||
|
},
|
||||||
|
children: ''
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderUniqueStylesheet(result: SSRResult, link: { href: string }) {
|
||||||
|
for (const existingLink of result.links) {
|
||||||
|
if(existingLink.props.rel === stylesheetRel && existingLink.props.href === link.href) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderStylesheet(link);
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ import { normalizeFilename } from '../vite-plugin-utils/index.js';
|
||||||
import { cachedFullCompilation } from './compile.js';
|
import { cachedFullCompilation } from './compile.js';
|
||||||
import { handleHotUpdate } from './hmr.js';
|
import { handleHotUpdate } from './hmr.js';
|
||||||
import { parseAstroRequest, ParsedRequestResult } from './query.js';
|
import { parseAstroRequest, ParsedRequestResult } from './query.js';
|
||||||
|
export type { AstroPluginMetadata };
|
||||||
|
export { getAstroMetadata } from './metadata.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -108,6 +110,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
if (!compileResult) {
|
if (!compileResult) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (query.type) {
|
switch (query.type) {
|
||||||
case 'style': {
|
case 'style': {
|
||||||
if (typeof query.index === 'undefined') {
|
if (typeof query.index === 'undefined') {
|
||||||
|
@ -198,6 +201,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
astroConfig: config,
|
astroConfig: config,
|
||||||
viteConfig: resolvedConfig,
|
viteConfig: resolvedConfig,
|
||||||
filename,
|
filename,
|
||||||
|
id,
|
||||||
source,
|
source,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -215,6 +219,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
clientOnlyComponents: transformResult.clientOnlyComponents,
|
clientOnlyComponents: transformResult.clientOnlyComponents,
|
||||||
hydratedComponents: transformResult.hydratedComponents,
|
hydratedComponents: transformResult.hydratedComponents,
|
||||||
scripts: transformResult.scripts,
|
scripts: transformResult.scripts,
|
||||||
|
propagation: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -236,6 +241,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
astroConfig: config,
|
astroConfig: config,
|
||||||
viteConfig: resolvedConfig,
|
viteConfig: resolvedConfig,
|
||||||
filename: context.file,
|
filename: context.file,
|
||||||
|
id: context.modules[0]?.id ?? undefined,
|
||||||
source: await context.read(),
|
source: await context.read(),
|
||||||
};
|
};
|
||||||
const compile = () => cachedCompilation(compileProps);
|
const compile = () => cachedCompilation(compileProps);
|
||||||
|
|
9
packages/astro/src/vite-plugin-astro/metadata.ts
Normal file
9
packages/astro/src/vite-plugin-astro/metadata.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { PluginMetadata } from './types';
|
||||||
|
import type { ModuleInfo } from '../core/module-loader';
|
||||||
|
|
||||||
|
export function getAstroMetadata(modInfo: ModuleInfo): PluginMetadata['astro'] | undefined {
|
||||||
|
if(modInfo.meta?.astro) {
|
||||||
|
return modInfo.meta.astro as PluginMetadata['astro'];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import type { TransformResult } from '@astrojs/compiler';
|
import type { TransformResult } from '@astrojs/compiler';
|
||||||
|
import type { PropagationHint } from '../@types/astro';
|
||||||
|
|
||||||
export interface PluginMetadata {
|
export interface PluginMetadata {
|
||||||
astro: {
|
astro: {
|
||||||
hydratedComponents: TransformResult['hydratedComponents'];
|
hydratedComponents: TransformResult['hydratedComponents'];
|
||||||
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
||||||
scripts: TransformResult['scripts'];
|
scripts: TransformResult['scripts'];
|
||||||
|
propagation: PropagationHint;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
54
packages/astro/src/vite-plugin-head-propagation/index.ts
Normal file
54
packages/astro/src/vite-plugin-head-propagation/index.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import type { AstroSettings } from '../@types/astro';
|
||||||
|
import type { ModuleInfo } from 'rollup';
|
||||||
|
|
||||||
|
import * as vite from 'vite';
|
||||||
|
import { getAstroMetadata } from '../vite-plugin-astro/index.js';
|
||||||
|
|
||||||
|
const injectExp = /^\/\/\s*astro-head-inject/;
|
||||||
|
/**
|
||||||
|
* If any component is marked as doing head injection, walk up the tree
|
||||||
|
* and mark parent Astro components as having head injection in the tree.
|
||||||
|
* This is used at runtime to determine if we should wait for head content
|
||||||
|
* to be be populated before rendering the entire tree.
|
||||||
|
*/
|
||||||
|
export default function configHeadPropagationVitePlugin({
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
settings: AstroSettings;
|
||||||
|
}): vite.Plugin {
|
||||||
|
function addHeadInjectionInTree(graph: vite.ModuleGraph, id: string, getInfo: (id: string) => ModuleInfo | null, seen: Set<string> = new Set()) {
|
||||||
|
const mod = server.moduleGraph.getModuleById(id);
|
||||||
|
for(const parent of mod?.importers || []) {
|
||||||
|
if(parent.id) {
|
||||||
|
if(seen.has(parent.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const info = getInfo(parent.id);
|
||||||
|
if(info?.meta.astro) {
|
||||||
|
const astroMetadata = getAstroMetadata(info);
|
||||||
|
if(astroMetadata) {
|
||||||
|
astroMetadata.propagation = 'in-tree';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addHeadInjectionInTree(graph, parent.id, getInfo, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: vite.ViteDevServer;
|
||||||
|
return {
|
||||||
|
name: 'astro:head-propagation',
|
||||||
|
configureServer(_server) {
|
||||||
|
server = _server;
|
||||||
|
},
|
||||||
|
transform(source, id) {
|
||||||
|
if(!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(injectExp.test(source)) {
|
||||||
|
addHeadInjectionInTree(server.moduleGraph, id, (child) => this.getModuleInfo(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ export default function loadFallbackPlugin({
|
||||||
async resolveId(id, parent) {
|
async resolveId(id, parent) {
|
||||||
// See if this can be loaded from our fs
|
// See if this can be loaded from our fs
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const candidateId = npath.posix.join(npath.posix.dirname(parent), id);
|
const candidateId = npath.posix.join(npath.posix.dirname(slashify(parent)), id);
|
||||||
try {
|
try {
|
||||||
// Check to see if this file exists and is not a directory.
|
// Check to see if this file exists and is not a directory.
|
||||||
const stats = await fs.promises.stat(candidateId);
|
const stats = await fs.promises.stat(candidateId);
|
||||||
|
|
|
@ -207,6 +207,7 @@ ${setup}`.trim();
|
||||||
viteConfig: resolvedConfig,
|
viteConfig: resolvedConfig,
|
||||||
filename,
|
filename,
|
||||||
source: astroResult,
|
source: astroResult,
|
||||||
|
id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let transformResult = await cachedCompilation(compileProps);
|
let transformResult = await cachedCompilation(compileProps);
|
||||||
|
@ -232,6 +233,7 @@ ${tsResult}`;
|
||||||
clientOnlyComponents: transformResult.clientOnlyComponents,
|
clientOnlyComponents: transformResult.clientOnlyComponents,
|
||||||
hydratedComponents: transformResult.hydratedComponents,
|
hydratedComponents: transformResult.hydratedComponents,
|
||||||
scripts: transformResult.scripts,
|
scripts: transformResult.scripts,
|
||||||
|
propagation: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -157,6 +157,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
||||||
hydratedComponents: [],
|
hydratedComponents: [],
|
||||||
clientOnlyComponents: [],
|
clientOnlyComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
|
propagation: 'none',
|
||||||
} as PluginMetadata['astro'],
|
} as PluginMetadata['astro'],
|
||||||
vite: {
|
vite: {
|
||||||
lang: 'ts',
|
lang: 'ts',
|
||||||
|
|
160
packages/astro/test/units/dev/head-injection.test.js
Normal file
160
packages/astro/test/units/dev/head-injection.test.js
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
import { runInContainer } from '../../../dist/core/dev/index.js';
|
||||||
|
import { createFs, createRequestAndResponse } from '../test-utils.js';
|
||||||
|
|
||||||
|
const root = new URL('../../fixtures/alias/', import.meta.url);
|
||||||
|
|
||||||
|
describe('head injection', () => {
|
||||||
|
it('Dynamic injection from component created in the page frontmatter', async () => {
|
||||||
|
const fs = createFs(
|
||||||
|
{
|
||||||
|
'/src/components/Other.astro': `
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="other">Other</div>
|
||||||
|
`,
|
||||||
|
'/src/common/head.js': `
|
||||||
|
// astro-head-inject
|
||||||
|
import Other from '../components/Other.astro';
|
||||||
|
import {
|
||||||
|
createComponent,
|
||||||
|
createHeadAndContent,
|
||||||
|
renderComponent,
|
||||||
|
renderTemplate,
|
||||||
|
renderUniqueStylesheet,
|
||||||
|
} from 'astro/runtime/server/index.js';
|
||||||
|
|
||||||
|
export function renderEntry() {
|
||||||
|
return createComponent({
|
||||||
|
factory(result, props, slots) {
|
||||||
|
return createHeadAndContent(
|
||||||
|
renderUniqueStylesheet(result, {
|
||||||
|
href: '/some/fake/styles.css'
|
||||||
|
}),
|
||||||
|
renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
propagation: 'self'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`.trim(),
|
||||||
|
'/src/pages/index.astro': `
|
||||||
|
---
|
||||||
|
import { renderEntry } from '../common/head.js';
|
||||||
|
const Head = renderEntry();
|
||||||
|
---
|
||||||
|
<h1>testing</h1>
|
||||||
|
<Head />
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
root
|
||||||
|
);
|
||||||
|
|
||||||
|
await runInContainer({
|
||||||
|
fs, root,
|
||||||
|
userConfig: {
|
||||||
|
vite: { server: { middlewareMode: true } }
|
||||||
|
}
|
||||||
|
}, async (container) => {
|
||||||
|
const { req, res, done, text } = createRequestAndResponse({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
container.handle(req, res);
|
||||||
|
await done;
|
||||||
|
const html = await text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
expect($('link[rel=stylesheet][href="/some/fake/styles.css"]')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#other')).to.have.a.lengthOf(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Dynamic injection from a layout component', async () => {
|
||||||
|
const fs = createFs(
|
||||||
|
{
|
||||||
|
'/src/components/Other.astro': `
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="other">Other</div>
|
||||||
|
`,
|
||||||
|
'/src/common/head.js': `
|
||||||
|
// astro-head-inject
|
||||||
|
import Other from '../components/Other.astro';
|
||||||
|
import {
|
||||||
|
createComponent,
|
||||||
|
createHeadAndContent,
|
||||||
|
renderComponent,
|
||||||
|
renderTemplate,
|
||||||
|
renderUniqueStylesheet,
|
||||||
|
} from 'astro/runtime/server/index.js';
|
||||||
|
|
||||||
|
export function renderEntry() {
|
||||||
|
return createComponent({
|
||||||
|
factory(result, props, slots) {
|
||||||
|
return createHeadAndContent(
|
||||||
|
renderUniqueStylesheet(result, {
|
||||||
|
href: '/some/fake/styles.css'
|
||||||
|
}),
|
||||||
|
renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
propagation: 'self'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`.trim(),
|
||||||
|
'/src/components/Layout.astro': `
|
||||||
|
---
|
||||||
|
import { renderEntry } from '../common/head.js';
|
||||||
|
const ExtraHead = renderEntry();
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Normal head stuff</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
<ExtraHead />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
'/src/pages/index.astro': `
|
||||||
|
---
|
||||||
|
import Layout from '../components/Layout.astro';
|
||||||
|
---
|
||||||
|
<Layout>
|
||||||
|
<h1>Test page</h1>
|
||||||
|
</Layout>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
root
|
||||||
|
);
|
||||||
|
|
||||||
|
await runInContainer({
|
||||||
|
fs, root,
|
||||||
|
userConfig: {
|
||||||
|
vite: { server: { middlewareMode: true } }
|
||||||
|
}
|
||||||
|
}, async (container) => {
|
||||||
|
const { req, res, done, text } = createRequestAndResponse({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
container.handle(req, res);
|
||||||
|
await done;
|
||||||
|
const html = await text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
expect($('link[rel=stylesheet][href="/some/fake/styles.css"]')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#other')).to.have.a.lengthOf(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -373,7 +373,7 @@ importers:
|
||||||
|
|
||||||
packages/astro:
|
packages/astro:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/compiler': ^0.29.15
|
'@astrojs/compiler': ^0.30.0
|
||||||
'@astrojs/language-server': ^0.28.3
|
'@astrojs/language-server': ^0.28.3
|
||||||
'@astrojs/markdown-remark': ^1.1.3
|
'@astrojs/markdown-remark': ^1.1.3
|
||||||
'@astrojs/telemetry': ^1.0.1
|
'@astrojs/telemetry': ^1.0.1
|
||||||
|
@ -471,7 +471,7 @@ importers:
|
||||||
yargs-parser: ^21.0.1
|
yargs-parser: ^21.0.1
|
||||||
zod: ^3.17.3
|
zod: ^3.17.3
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 0.29.15
|
'@astrojs/compiler': 0.30.0
|
||||||
'@astrojs/language-server': 0.28.3
|
'@astrojs/language-server': 0.28.3
|
||||||
'@astrojs/markdown-remark': link:../markdown/remark
|
'@astrojs/markdown-remark': link:../markdown/remark
|
||||||
'@astrojs/telemetry': link:../telemetry
|
'@astrojs/telemetry': link:../telemetry
|
||||||
|
@ -3893,6 +3893,10 @@ packages:
|
||||||
/@astrojs/compiler/0.29.15:
|
/@astrojs/compiler/0.29.15:
|
||||||
resolution: {integrity: sha512-vicPD8oOPNkcFZvz71Uz/nJcadovurUQ3L0yMZNPb6Nn6T1nHhlSHt5nAKaurB2pYU9DrxOFWZS2/RdV+JsWmQ==}
|
resolution: {integrity: sha512-vicPD8oOPNkcFZvz71Uz/nJcadovurUQ3L0yMZNPb6Nn6T1nHhlSHt5nAKaurB2pYU9DrxOFWZS2/RdV+JsWmQ==}
|
||||||
|
|
||||||
|
/@astrojs/compiler/0.30.0:
|
||||||
|
resolution: {integrity: sha512-av2HV5NuyzI5E12hpn4k7XNEHbfF81/JUISPu6CclC5yKCxTS7z64hRU68tA8k7dYLATcxvjQtvN2H/dnxHaMw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@astrojs/language-server/0.28.3:
|
/@astrojs/language-server/0.28.3:
|
||||||
resolution: {integrity: sha512-fPovAX/X46eE2w03jNRMpQ7W9m2mAvNt4Ay65lD9wl1Z5vIQYxlg7Enp9qP225muTr4jSVB5QiLumFJmZMAaVA==}
|
resolution: {integrity: sha512-fPovAX/X46eE2w03jNRMpQ7W9m2mAvNt4Ay65lD9wl1Z5vIQYxlg7Enp9qP225muTr4jSVB5QiLumFJmZMAaVA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
Loading…
Reference in a new issue