Skip to content

Commit

Permalink
Head propagation (#5511)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
matthewp and bluwy committed Dec 6, 2022
1 parent b137657 commit 05915fe
Show file tree
Hide file tree
Showing 36 changed files with 804 additions and 279 deletions.
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';

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 @@ -154,6 +155,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
Loading

0 comments on commit 05915fe

Please sign in to comment.