Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Head propagation #5511

Merged
merged 13 commits into from
Dec 6, 2022
7 changes: 7 additions & 0 deletions .changeset/cool-jobs-draw.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^0.29.15",
"@astrojs/compiler": "^0.30.0",
"@astrojs/language-server": "^0.28.3",
"@astrojs/markdown-remark": "^1.1.3",
"@astrojs/telemetry": "^1.0.1",
Expand All @@ -111,11 +111,11 @@
"@babel/plugin-transform-react-jsx": "^7.17.12",
"@babel/traverse": "^7.18.2",
"@babel/types": "^7.18.4",
"@proload/core": "^0.3.3",
"@proload/plugin-tsm": "^0.2.1",
"@types/babel__core": "^7.1.19",
"@types/html-escaper": "^3.0.0",
"@types/yargs-parser": "^21.0.0",
"@proload/core": "^0.3.3",
"@proload/plugin-tsm": "^0.2.1",
"boxen": "^6.2.1",
"ci-info": "^3.3.1",
"common-ancestor-path": "^1.0.1",
Expand Down
17 changes: 16 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { SerializedSSRManifest } from '../core/app/types';
import type { PageBuildData } from '../core/build/types';
import type { AstroConfigSchema } from '../core/config';
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';
export type {
MarkdownHeading,
Expand Down Expand Up @@ -1398,10 +1398,25 @@ export interface SSRMetadata {
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';
matthewp marked this conversation as resolved.
Show resolved Hide resolved

export interface SSRResult {
styles: Set<SSRElement>;
scripts: Set<SSRElement>;
links: Set<SSRElement>;
propagation: Map<string, PropagationHint>;
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
extraHead: Array<any>;
cookies: AstroCookies | undefined;
createAstro(
Astro: AstroGlobalPartial,
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/compile/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface CompileProps {
astroConfig: AstroConfig;
viteConfig: ResolvedConfig;
filename: string;
id: string | undefined;
source: string;
}

Expand All @@ -24,6 +25,7 @@ export async function compile({
astroConfig,
viteConfig,
filename,
id: moduleId,
source,
}: CompileProps): Promise<CompileResult> {
const cssDeps = new Set<string>();
Expand All @@ -35,6 +37,7 @@ export async function compile({
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.
transformResult = await transform(source, {
moduleId,
pathname: filename,
projectRoot: astroConfig.root.toString(),
site: astroConfig.site?.toString(),
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 astroHeadPropagationPlugin from '../vite-plugin-head-propagation/index.js';
import { createCustomViteLogger } from './errors/dev/index.js';
import { resolveDependency } from './util.js';

Expand Down Expand Up @@ -112,6 +113,7 @@ export async function createVite(
astroPostprocessVitePlugin({ settings }),
astroIntegrationsContainerPlugin({ settings, logging }),
astroScriptsPageSSRPlugin({ settings }),
astroHeadPropagationPlugin({ settings }),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/render/context.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -11,6 +11,7 @@ export interface RenderContext {
scripts?: Set<SSRElement>;
links?: Set<SSRElement>;
styles?: Set<SSRElement>;
propagation?: SSRResult['propagation'];
route?: RouteData;
status?: number;
}
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
params,
props: pageProps,
pathname: ctx.pathname,
propagation: ctx.propagation,
resolve: env.resolve,
renderers: env.renderers,
request: ctx.request,
Expand Down
34 changes: 34 additions & 0 deletions packages/astro/src/core/render/dev/head.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
34 changes: 6 additions & 28 deletions packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,23 @@ import type {
AstroSettings,
ComponentInstance,
RouteData,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
} from '../../../@types/astro';
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { enhanceViteSSRError } from '../../errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
import { LogOptions } from '../../logger/core.js';
import type { ModuleLoader } from '../../module-loader/index';
import { isPage, resolveIdToUrl } from '../../util.js';
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
import { RouteCache } from '../route-cache.js';
import { getStylesForURL } from './css.js';
import type { DevelopmentEnvironment } from './environment';
import { getScriptsForURL } from './scripts.js';
import { getPropagationMap } from './head.js';
export { createDevelopmentEnvironment } from './environment.js';
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 {
/** The environment instance */
env: DevelopmentEnvironment;
Expand Down Expand Up @@ -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> {
Expand All @@ -173,7 +150,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
// The new instances are passed through.
options.env.renderers = renderers;

const { scripts, links, styles } = await getScriptsAndStyles({
const { scripts, links, styles, propagationMap } = await getScriptsAndStyles({
env: options.env,
filePath: options.filePath,
});
Expand All @@ -185,6 +162,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
scripts,
links,
styles,
propagation: propagationMap,
route: options.route,
});

Expand Down
4 changes: 4 additions & 0 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface CreateResultArgs {
links?: Set<SSRElement>;
scripts?: Set<SSRElement>;
styles?: Set<SSRElement>;
propagation?: SSRResult['propagation'];
request: Request;
status: number;
}
Expand Down Expand Up @@ -161,6 +162,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
styles: args.styles ?? new Set<SSRElement>(),
scripts: args.scripts ?? new Set<SSRElement>(),
links: args.links ?? new Set<SSRElement>(),
propagation: args.propagation ?? new Map(),
propagators: new Map(),
extraHead: [],
cookies,
/** This function returns the `Astro` faux-global */
createAstro(
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/jsx/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj {
clientOnlyComponents: [],
hydratedComponents: [],
scripts: [],
propagation: 'none',
};
}
path.node.body.splice(
Expand Down
29 changes: 29 additions & 0 deletions packages/astro/src/runtime/server/astro-component.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/astro/src/runtime/server/astro-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function createAstro(
fetchContent: createDeprecatedFetchContentFn(),
glob: createAstroGlobFn(),
// INVESTIGATE is there a use-case for multi args?
// TODO remove in 2.0
resolve(...segments: string[]) {
let resolved = segments.reduce((u, segment) => new URL(segment, u), referenceURL).pathname;
// When inside of project root, remove the leading path so you are
Expand Down
18 changes: 6 additions & 12 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,32 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from
export { renderJSX } from './jsx.js';
export {
addAttribute,
createHeadAndContent,
defineScriptVars,
Fragment,
maybeRenderHead,
renderAstroComponent,
renderAstroTemplateResult as renderAstroComponent,
renderComponent,
renderComponentToIterable,
Renderer as Renderer,
renderHead,
renderHTMLElement,
renderPage,
renderSlot,
renderTemplate as render,
renderTemplate,
renderUniqueStylesheet,
renderToString,
stringifyChunk,
voidElementNames,
} from './render/index.js';
export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
import type { AstroComponentFactory } from './render/index.js';
export { createComponent } from './astro-component.js';
export type { AstroComponentFactory, AstroComponentInstance, RenderInstruction } from './render/index.js';

import { markHTMLString } from './escape.js';
import { Renderer } 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[]) {
const slots: Record<string, () => any> = {};
for (const slot of slotted) {
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/runtime/server/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
escapeHTML,
HTMLString,
markHTMLString,
renderComponent,
renderComponentToIterable,
renderToString,
spreadAttributes,
voidElementNames,
Expand Down Expand Up @@ -177,15 +177,15 @@ Did you forget to import the component or is it possible there is a typo?`);
props[Skip.symbol] = skip;
let output: ComponentIterable;
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
output = await renderComponent(
output = await renderComponentToIterable(
result,
vnode.props['client:display-name'] ?? '',
null,
props,
slots
);
} else {
output = await renderComponent(
output = await renderComponentToIterable(
result,
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
vnode.type,
Expand Down