Avoid implicit head injection when there is a head element in the tree (#6638)
* Avoid implicit head injection when there is a head element in the tree * more * only do it once * Update the tests * Update more tests * update compiler version * See if scope stuff can be removed now * Move up where head injection occurs * Remove result scoping
This commit is contained in:
parent
8bd0ca08cd
commit
7daef9a299
37 changed files with 260 additions and 300 deletions
5
.changeset/tender-hounds-juggle.md
Normal file
5
.changeset/tender-hounds-juggle.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Avoid implicit head injection when a head is in the tree
|
|
@ -106,7 +106,7 @@
|
||||||
"test:e2e:match": "playwright test -g"
|
"test:e2e:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^1.2.0",
|
"@astrojs/compiler": "^1.3.0",
|
||||||
"@astrojs/language-server": "^0.28.3",
|
"@astrojs/language-server": "^0.28.3",
|
||||||
"@astrojs/markdown-remark": "^2.1.2",
|
"@astrojs/markdown-remark": "^2.1.2",
|
||||||
"@astrojs/telemetry": "^2.1.0",
|
"@astrojs/telemetry": "^2.1.0",
|
||||||
|
|
|
@ -1578,6 +1578,7 @@ export interface SSRMetadata {
|
||||||
hasHydrationScript: boolean;
|
hasHydrationScript: boolean;
|
||||||
hasDirectives: Set<string>;
|
hasDirectives: Set<string>;
|
||||||
hasRenderedHead: boolean;
|
hasRenderedHead: boolean;
|
||||||
|
headInTree: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1592,11 +1593,16 @@ export interface SSRMetadata {
|
||||||
*/
|
*/
|
||||||
export type PropagationHint = 'none' | 'self' | 'in-tree';
|
export type PropagationHint = 'none' | 'self' | 'in-tree';
|
||||||
|
|
||||||
|
export type SSRComponentMetadata = {
|
||||||
|
propagation: PropagationHint,
|
||||||
|
containsHead: boolean
|
||||||
|
};
|
||||||
|
|
||||||
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>;
|
componentMetadata: Map<string, SSRComponentMetadata>;
|
||||||
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
|
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
|
||||||
extraHead: Array<string>;
|
extraHead: Array<string>;
|
||||||
cookies: AstroCookies | undefined;
|
cookies: AstroCookies | undefined;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { prependForwardSlash } from '../core/path.js';
|
||||||
import {
|
import {
|
||||||
createComponent,
|
createComponent,
|
||||||
createHeadAndContent,
|
createHeadAndContent,
|
||||||
createScopedResult,
|
|
||||||
renderComponent,
|
renderComponent,
|
||||||
renderScriptElement,
|
renderScriptElement,
|
||||||
renderStyleElement,
|
renderStyleElement,
|
||||||
|
@ -180,7 +179,7 @@ async function render({
|
||||||
return createHeadAndContent(
|
return createHeadAndContent(
|
||||||
unescapeHTML(styles + links + scripts) as any,
|
unescapeHTML(styles + links + scripts) as any,
|
||||||
renderTemplate`${renderComponent(
|
renderTemplate`${renderComponent(
|
||||||
createScopedResult(result),
|
result,
|
||||||
'Content',
|
'Content',
|
||||||
mod.Content,
|
mod.Content,
|
||||||
props,
|
props,
|
||||||
|
|
|
@ -14,12 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets = new Set<string>(serializedManifest.assets);
|
const assets = new Set<string>(serializedManifest.assets);
|
||||||
const propagation = new Map(serializedManifest.propagation);
|
const componentMetadata = new Map(serializedManifest.componentMetadata);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...serializedManifest,
|
...serializedManifest,
|
||||||
assets,
|
assets,
|
||||||
propagation,
|
componentMetadata,
|
||||||
routes,
|
routes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,7 +193,7 @@ export class App {
|
||||||
request,
|
request,
|
||||||
origin: url.origin,
|
origin: url.origin,
|
||||||
pathname,
|
pathname,
|
||||||
propagation: this.#manifest.propagation,
|
componentMetadata: this.#manifest.componentMetadata,
|
||||||
scripts,
|
scripts,
|
||||||
links,
|
links,
|
||||||
route: routeData,
|
route: routeData,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||||
import type {
|
import type {
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
PropagationHint,
|
SSRComponentMetadata,
|
||||||
RouteData,
|
RouteData,
|
||||||
SerializedRouteData,
|
SerializedRouteData,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
|
@ -36,13 +36,13 @@ export interface SSRManifest {
|
||||||
renderers: SSRLoadedRenderer[];
|
renderers: SSRLoadedRenderer[];
|
||||||
entryModules: Record<string, string>;
|
entryModules: Record<string, string>;
|
||||||
assets: Set<string>;
|
assets: Set<string>;
|
||||||
propagation: SSRResult['propagation'];
|
componentMetadata: SSRResult['componentMetadata'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'propagation'> & {
|
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
|
||||||
routes: SerializedRouteInfo[];
|
routes: SerializedRouteInfo[];
|
||||||
assets: string[];
|
assets: string[];
|
||||||
propagation: readonly [string, PropagationHint][];
|
componentMetadata: [string, SSRComponentMetadata][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdapterCreateExports<T = any> = (
|
export type AdapterCreateExports<T = any> = (
|
||||||
|
|
|
@ -405,7 +405,7 @@ async function generatePath(
|
||||||
origin,
|
origin,
|
||||||
pathname,
|
pathname,
|
||||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||||
propagation: internals.propagation,
|
componentMetadata: internals.componentMetadata,
|
||||||
scripts,
|
scripts,
|
||||||
links,
|
links,
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { GetModuleInfo, ModuleInfo } from 'rollup';
|
import type { GetModuleInfo, ModuleInfo } from 'rollup';
|
||||||
|
import type { ViteDevServer } from 'vite';
|
||||||
|
|
||||||
import { resolvedPagesVirtualModuleId } from '../app/index.js';
|
import { resolvedPagesVirtualModuleId } from '../app/index.js';
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ export interface BuildInternals {
|
||||||
staticFiles: Set<string>;
|
staticFiles: Set<string>;
|
||||||
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
|
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
|
||||||
ssrEntryChunk?: OutputChunk;
|
ssrEntryChunk?: OutputChunk;
|
||||||
propagation: SSRResult['propagation'];
|
componentMetadata: SSRResult['componentMetadata'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,7 +107,7 @@ export function createBuildInternals(): BuildInternals {
|
||||||
discoveredClientOnlyComponents: new Map(),
|
discoveredClientOnlyComponents: new Map(),
|
||||||
discoveredScripts: new Set(),
|
discoveredScripts: new Set(),
|
||||||
staticFiles: new Set(),
|
staticFiles: new Set(),
|
||||||
propagation: new Map(),
|
componentMetadata: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { astroConfigBuildPlugin } from '../../../content/vite-plugin-content-assets.js';
|
import { astroConfigBuildPlugin } from '../../../content/vite-plugin-content-assets.js';
|
||||||
import { astroHeadPropagationBuildPlugin } from '../../../vite-plugin-head-propagation/index.js';
|
import { astroHeadBuildPlugin } from '../../../vite-plugin-head/index.js';
|
||||||
import type { AstroBuildPluginContainer } from '../plugin';
|
import type { AstroBuildPluginContainer } from '../plugin';
|
||||||
import { pluginAliasResolve } from './plugin-alias-resolve.js';
|
import { pluginAliasResolve } from './plugin-alias-resolve.js';
|
||||||
import { pluginAnalyzer } from './plugin-analyzer.js';
|
import { pluginAnalyzer } from './plugin-analyzer.js';
|
||||||
|
@ -18,7 +18,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
|
||||||
register(pluginInternals(internals));
|
register(pluginInternals(internals));
|
||||||
register(pluginPages(options, internals));
|
register(pluginPages(options, internals));
|
||||||
register(pluginCSS(options, internals));
|
register(pluginCSS(options, internals));
|
||||||
register(astroHeadPropagationBuildPlugin(options, internals));
|
register(astroHeadBuildPlugin(options, internals));
|
||||||
register(pluginPrerender(options, internals));
|
register(pluginPrerender(options, internals));
|
||||||
register(astroConfigBuildPlugin(options, internals));
|
register(astroConfigBuildPlugin(options, internals));
|
||||||
register(pluginHoistedScripts(options, internals));
|
register(pluginHoistedScripts(options, internals));
|
||||||
|
|
|
@ -209,7 +209,7 @@ function buildManifest(
|
||||||
base: settings.config.base,
|
base: settings.config.base,
|
||||||
markdown: settings.config.markdown,
|
markdown: settings.config.markdown,
|
||||||
pageMap: null as any,
|
pageMap: null as any,
|
||||||
propagation: Array.from(internals.propagation),
|
componentMetadata: Array.from(internals.componentMetadata),
|
||||||
renderers: [],
|
renderers: [],
|
||||||
entryModules,
|
entryModules,
|
||||||
assets: staticFiles.map((s) => settings.config.base + s),
|
assets: staticFiles.map((s) => settings.config.base + s),
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js';
|
||||||
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
||||||
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
|
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
|
||||||
import envVitePlugin from '../vite-plugin-env/index.js';
|
import envVitePlugin from '../vite-plugin-env/index.js';
|
||||||
import astroHeadPropagationPlugin from '../vite-plugin-head-propagation/index.js';
|
import astroHeadPlugin from '../vite-plugin-head/index.js';
|
||||||
import htmlVitePlugin from '../vite-plugin-html/index.js';
|
import htmlVitePlugin from '../vite-plugin-html/index.js';
|
||||||
import { astroInjectEnvTsPlugin } from '../vite-plugin-inject-env-ts/index.js';
|
import { astroInjectEnvTsPlugin } from '../vite-plugin-inject-env-ts/index.js';
|
||||||
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
|
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
|
||||||
|
@ -121,7 +121,7 @@ export async function createVite(
|
||||||
astroPostprocessVitePlugin({ settings }),
|
astroPostprocessVitePlugin({ settings }),
|
||||||
astroIntegrationsContainerPlugin({ settings, logging }),
|
astroIntegrationsContainerPlugin({ settings, logging }),
|
||||||
astroScriptsPageSSRPlugin({ settings }),
|
astroScriptsPageSSRPlugin({ settings }),
|
||||||
astroHeadPropagationPlugin({ settings }),
|
astroHeadPlugin({ settings }),
|
||||||
astroScannerPlugin({ settings }),
|
astroScannerPlugin({ settings }),
|
||||||
astroInjectEnvTsPlugin({ settings, logging, fs }),
|
astroInjectEnvTsPlugin({ settings, logging, fs }),
|
||||||
astroContentVirtualModPlugin({ settings }),
|
astroContentVirtualModPlugin({ settings }),
|
||||||
|
|
|
@ -11,7 +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'];
|
componentMetadata?: SSRResult['componentMetadata'];
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +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,
|
componentMetadata: ctx.componentMetadata,
|
||||||
resolve: env.resolve,
|
resolve: env.resolve,
|
||||||
renderers: env.renderers,
|
renderers: env.renderers,
|
||||||
request: ctx.request,
|
request: ctx.request,
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
|
||||||
|
|
||||||
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
|
|
||||||
|
|
||||||
import { getAstroMetadata } from '../../../vite-plugin-astro/index.js';
|
|
||||||
import { viteID } from '../../util.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,7 @@ import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||||
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
|
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
|
||||||
import { getStylesForURL } from './css.js';
|
import { getStylesForURL } from './css.js';
|
||||||
import type { DevelopmentEnvironment } from './environment';
|
import type { DevelopmentEnvironment } from './environment';
|
||||||
import { getPropagationMap } from './head.js';
|
import { getComponentMetadata } from './metadata.js';
|
||||||
import { getScriptsForURL } from './scripts.js';
|
import { getScriptsForURL } from './scripts.js';
|
||||||
export { createDevelopmentEnvironment } from './environment.js';
|
export { createDevelopmentEnvironment } from './environment.js';
|
||||||
export type { DevelopmentEnvironment };
|
export type { DevelopmentEnvironment };
|
||||||
|
@ -142,9 +142,9 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const propagationMap = await getPropagationMap(filePath, env.loader);
|
const metadata = await getComponentMetadata(filePath, env.loader);
|
||||||
|
|
||||||
return { scripts, styles, links, propagationMap };
|
return { scripts, styles, links, metadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderPage(options: SSROptions): Promise<Response> {
|
export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
|
@ -154,7 +154,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, propagationMap } = await getScriptsAndStyles({
|
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||||
env: options.env,
|
env: options.env,
|
||||||
filePath: options.filePath,
|
filePath: options.filePath,
|
||||||
});
|
});
|
||||||
|
@ -166,7 +166,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
scripts,
|
scripts,
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
propagation: propagationMap,
|
componentMetadata: metadata,
|
||||||
route: options.route,
|
route: options.route,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
47
packages/astro/src/core/render/dev/metadata.ts
Normal file
47
packages/astro/src/core/render/dev/metadata.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import type { SSRResult, SSRComponentMetadata } from '../../../@types/astro';
|
||||||
|
|
||||||
|
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
|
||||||
|
|
||||||
|
import { getAstroMetadata } from '../../../vite-plugin-astro/index.js';
|
||||||
|
import { viteID } from '../../util.js';
|
||||||
|
import { crawlGraph } from './vite.js';
|
||||||
|
|
||||||
|
export async function getComponentMetadata(
|
||||||
|
filePath: URL,
|
||||||
|
loader: ModuleLoader
|
||||||
|
): Promise<SSRResult['componentMetadata']> {
|
||||||
|
const map: SSRResult['componentMetadata'] = new Map();
|
||||||
|
|
||||||
|
const rootID = viteID(filePath);
|
||||||
|
addMetadata(map, loader.getModuleInfo(rootID));
|
||||||
|
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
|
||||||
|
const id = moduleNode.id;
|
||||||
|
if (id) {
|
||||||
|
addMetadata(map, loader.getModuleInfo(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMetadata(
|
||||||
|
map: SSRResult['componentMetadata'],
|
||||||
|
modInfo: ModuleInfo | null
|
||||||
|
) {
|
||||||
|
if (modInfo) {
|
||||||
|
const astro = getAstroMetadata(modInfo);
|
||||||
|
if(astro) {
|
||||||
|
let metadata: SSRComponentMetadata = {
|
||||||
|
containsHead: false,
|
||||||
|
propagation: 'none'
|
||||||
|
};
|
||||||
|
if(astro.propagation) {
|
||||||
|
metadata.propagation = astro.propagation;
|
||||||
|
}
|
||||||
|
if(astro.containsHead) {
|
||||||
|
metadata.containsHead = astro.containsHead;
|
||||||
|
}
|
||||||
|
map.set(modInfo.id, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,7 @@ import type {
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import {
|
import {
|
||||||
createScopedResult,
|
|
||||||
renderSlot,
|
renderSlot,
|
||||||
ScopeFlags,
|
|
||||||
stringifyChunk,
|
stringifyChunk,
|
||||||
type ComponentSlots,
|
type ComponentSlots,
|
||||||
} from '../../runtime/server/index.js';
|
} from '../../runtime/server/index.js';
|
||||||
|
@ -48,7 +46,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'];
|
componentMetadata?: SSRResult['componentMetadata'];
|
||||||
request: Request;
|
request: Request;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
@ -95,7 +93,7 @@ class Slots {
|
||||||
public async render(name: string, args: any[] = []) {
|
public async render(name: string, args: any[] = []) {
|
||||||
if (!this.#slots || !this.has(name)) return;
|
if (!this.#slots || !this.has(name)) return;
|
||||||
|
|
||||||
const scoped = createScopedResult(this.#result, ScopeFlags.RenderSlot);
|
const result = this.#result;
|
||||||
if (!Array.isArray(args)) {
|
if (!Array.isArray(args)) {
|
||||||
warn(
|
warn(
|
||||||
this.#loggingOpts,
|
this.#loggingOpts,
|
||||||
|
@ -104,24 +102,24 @@ class Slots {
|
||||||
);
|
);
|
||||||
} else if (args.length > 0) {
|
} else if (args.length > 0) {
|
||||||
const slotValue = this.#slots[name];
|
const slotValue = this.#slots[name];
|
||||||
const component = typeof slotValue === 'function' ? await slotValue(scoped) : await slotValue;
|
const component = typeof slotValue === 'function' ? await slotValue(result) : await slotValue;
|
||||||
|
|
||||||
// Astro
|
// Astro
|
||||||
const expression = getFunctionExpression(component);
|
const expression = getFunctionExpression(component);
|
||||||
if (expression) {
|
if (expression) {
|
||||||
const slot = () => expression(...args);
|
const slot = () => expression(...args);
|
||||||
return await renderSlot(scoped, slot).then((res) => (res != null ? String(res) : res));
|
return await renderSlot(result, slot).then((res) => (res != null ? String(res) : res));
|
||||||
}
|
}
|
||||||
// JSX
|
// JSX
|
||||||
if (typeof component === 'function') {
|
if (typeof component === 'function') {
|
||||||
return await renderJSX(scoped, (component as any)(...args)).then((res) =>
|
return await renderJSX(result, (component as any)(...args)).then((res) =>
|
||||||
res != null ? String(res) : res
|
res != null ? String(res) : res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await renderSlot(scoped, this.#slots[name]);
|
const content = await renderSlot(result, this.#slots[name]);
|
||||||
const outHTML = stringifyChunk(scoped, content);
|
const outHTML = stringifyChunk(result, content);
|
||||||
|
|
||||||
return outHTML;
|
return outHTML;
|
||||||
}
|
}
|
||||||
|
@ -150,6 +148,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
|
|
||||||
// Astro.cookies is defined lazily to avoid the cost on pages that do not use it.
|
// Astro.cookies is defined lazily to avoid the cost on pages that do not use it.
|
||||||
let cookies: AstroCookies | undefined = undefined;
|
let cookies: AstroCookies | undefined = undefined;
|
||||||
|
let componentMetadata = args.componentMetadata ?? new Map();
|
||||||
|
|
||||||
// Create the result object that will be passed into the render function.
|
// Create the result object that will be passed into the render function.
|
||||||
// This object starts here as an empty shell (not yet the result) but then
|
// This object starts here as an empty shell (not yet the result) but then
|
||||||
|
@ -158,7 +157,7 @@ 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(),
|
componentMetadata,
|
||||||
propagators: new Map(),
|
propagators: new Map(),
|
||||||
extraHead: [],
|
extraHead: [],
|
||||||
scope: 0,
|
scope: 0,
|
||||||
|
@ -248,6 +247,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
hasHydrationScript: false,
|
hasHydrationScript: false,
|
||||||
hasRenderedHead: false,
|
hasRenderedHead: false,
|
||||||
hasDirectives: new Set(),
|
hasDirectives: new Set(),
|
||||||
|
headInTree: false,
|
||||||
},
|
},
|
||||||
response,
|
response,
|
||||||
};
|
};
|
||||||
|
|
|
@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj {
|
||||||
clientOnlyComponents: [],
|
clientOnlyComponents: [],
|
||||||
hydratedComponents: [],
|
hydratedComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
|
containsHead: false,
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
pageOptions: {},
|
pageOptions: {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,13 +5,10 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from
|
||||||
export { renderJSX } from './jsx.js';
|
export { renderJSX } from './jsx.js';
|
||||||
export {
|
export {
|
||||||
addAttribute,
|
addAttribute,
|
||||||
addScopeFlag,
|
|
||||||
createHeadAndContent,
|
createHeadAndContent,
|
||||||
createScopedResult,
|
|
||||||
defineScriptVars,
|
defineScriptVars,
|
||||||
Fragment,
|
Fragment,
|
||||||
maybeRenderHead,
|
maybeRenderHead,
|
||||||
removeScopeFlag,
|
|
||||||
renderAstroTemplateResult as renderAstroComponent,
|
renderAstroTemplateResult as renderAstroComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
renderComponentToIterable,
|
renderComponentToIterable,
|
||||||
|
@ -26,7 +23,6 @@ export {
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
renderToString,
|
renderToString,
|
||||||
renderUniqueStylesheet,
|
renderUniqueStylesheet,
|
||||||
ScopeFlags,
|
|
||||||
stringifyChunk,
|
stringifyChunk,
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
} from './render/index.js';
|
} from './render/index.js';
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import { HTMLParts } from './render/common.js';
|
import { HTMLParts } from './render/common.js';
|
||||||
import type { ComponentIterable } from './render/component';
|
import type { ComponentIterable } from './render/component';
|
||||||
import { createScopedResult, ScopeFlags } from './render/scope.js';
|
|
||||||
|
|
||||||
const ClientOnlyPlaceholder = 'astro-client-only';
|
const ClientOnlyPlaceholder = 'astro-client-only';
|
||||||
|
|
||||||
|
@ -95,8 +94,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
props[key] = value;
|
props[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const scoped = createScopedResult(result, ScopeFlags.JSX);
|
const html = markHTMLString(await renderToString(result, vnode.type as any, props, slots));
|
||||||
const html = markHTMLString(await renderToString(scoped, vnode.type as any, props, slots));
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
case !vnode.type && (vnode.type as any) !== 0:
|
case !vnode.type && (vnode.type as any) !== 0:
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { HeadAndContent } from './head-and-content';
|
||||||
import type { RenderTemplateResult } from './render-template';
|
import type { RenderTemplateResult } from './render-template';
|
||||||
|
|
||||||
import { HTMLParts } from '../common.js';
|
import { HTMLParts } from '../common.js';
|
||||||
import { createScopedResult, ScopeFlags } from '../scope.js';
|
|
||||||
import { isHeadAndContent } from './head-and-content.js';
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
import { renderAstroTemplateResult } from './render-template.js';
|
import { renderAstroTemplateResult } from './render-template.js';
|
||||||
|
|
||||||
|
@ -28,8 +27,7 @@ export async function renderToString(
|
||||||
props: any,
|
props: any,
|
||||||
children: any
|
children: any
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const scoped = createScopedResult(result, ScopeFlags.Astro);
|
const factoryResult = await componentFactory(result, props, children);
|
||||||
const factoryResult = await componentFactory(scoped, props, children);
|
|
||||||
|
|
||||||
if (factoryResult instanceof Response) {
|
if (factoryResult instanceof Response) {
|
||||||
const response = factoryResult;
|
const response = factoryResult;
|
||||||
|
@ -50,8 +48,8 @@ export function isAPropagatingComponent(
|
||||||
factory: AstroComponentFactory
|
factory: AstroComponentFactory
|
||||||
): boolean {
|
): boolean {
|
||||||
let hint: PropagationHint = factory.propagation || 'none';
|
let hint: PropagationHint = factory.propagation || 'none';
|
||||||
if (factory.moduleId && result.propagation.has(factory.moduleId) && hint === 'none') {
|
if(factory.moduleId && result.componentMetadata.has(factory.moduleId) && hint === 'none') {
|
||||||
hint = result.propagation.get(factory.moduleId)!;
|
hint = result.componentMetadata.get(factory.moduleId)!.propagation;
|
||||||
}
|
}
|
||||||
return hint === 'in-tree' || hint === 'self';
|
return hint === 'in-tree' || hint === 'self';
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.j
|
||||||
import { HydrationDirectiveProps } from '../../hydration.js';
|
import { HydrationDirectiveProps } from '../../hydration.js';
|
||||||
import { isPromise } from '../../util.js';
|
import { isPromise } from '../../util.js';
|
||||||
import { renderChild } from '../any.js';
|
import { renderChild } from '../any.js';
|
||||||
import { createScopedResult, ScopeFlags } from '../scope.js';
|
|
||||||
import { isAPropagatingComponent } from './factory.js';
|
import { isAPropagatingComponent } from './factory.js';
|
||||||
import { isHeadAndContent } from './head-and-content.js';
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
|
|
||||||
|
@ -31,9 +30,8 @@ export class AstroComponentInstance {
|
||||||
this.props = props;
|
this.props = props;
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
this.slotValues = {};
|
this.slotValues = {};
|
||||||
const scoped = createScopedResult(result, ScopeFlags.Slot);
|
|
||||||
for (const name in slots) {
|
for (const name in slots) {
|
||||||
const value = slots[name](scoped);
|
const value = slots[name](result);
|
||||||
this.slotValues[name] = () => value;
|
this.slotValues[name] = () => value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
type PrescriptType,
|
type PrescriptType,
|
||||||
} from '../scripts.js';
|
} from '../scripts.js';
|
||||||
import { renderAllHeadContent } from './head.js';
|
import { renderAllHeadContent } from './head.js';
|
||||||
import { hasScopeFlag, ScopeFlags } from './scope.js';
|
|
||||||
import { isSlotString, type SlotString } from './slot.js';
|
import { isSlotString, type SlotString } from './slot.js';
|
||||||
|
|
||||||
export const Fragment = Symbol.for('astro:fragment');
|
export const Fragment = Symbol.for('astro:fragment');
|
||||||
|
@ -50,52 +49,9 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
|
||||||
return renderAllHeadContent(result);
|
return renderAllHeadContent(result);
|
||||||
}
|
}
|
||||||
case 'maybe-head': {
|
case 'maybe-head': {
|
||||||
if (result._metadata.hasRenderedHead) {
|
if (result._metadata.hasRenderedHead || result._metadata.headInTree) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = instruction.scope;
|
|
||||||
switch (scope) {
|
|
||||||
// JSX with an Astro slot
|
|
||||||
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro:
|
|
||||||
case ScopeFlags.JSX | ScopeFlags.Astro | ScopeFlags.HeadBuffer:
|
|
||||||
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro | ScopeFlags.HeadBuffer: {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Astro rendered within JSX, head will be injected by the page itself.
|
|
||||||
case ScopeFlags.JSX | ScopeFlags.Astro: {
|
|
||||||
if (hasScopeFlag(result, ScopeFlags.JSX)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current scope is with Astro.slots.render()
|
|
||||||
case ScopeFlags.Slot:
|
|
||||||
case ScopeFlags.Slot | ScopeFlags.HeadBuffer: {
|
|
||||||
if (hasScopeFlag(result, ScopeFlags.RenderSlot)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nested element inside of JSX during head buffering phase
|
|
||||||
case ScopeFlags.HeadBuffer: {
|
|
||||||
if (hasScopeFlag(result, ScopeFlags.JSX | ScopeFlags.HeadBuffer)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Astro.slots.render() should never render head content.
|
|
||||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro:
|
|
||||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX:
|
|
||||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderAllHeadContent(result);
|
return renderAllHeadContent(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ 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 { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js';
|
|
||||||
export { renderSlot, type ComponentSlots } from './slot.js';
|
export { renderSlot, type ComponentSlots } from './slot.js';
|
||||||
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
|
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
|
||||||
export type { RenderInstruction } from './types';
|
export type { RenderInstruction } from './types';
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
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';
|
||||||
import { createScopedResult, ScopeFlags } from './scope.js';
|
|
||||||
|
|
||||||
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
||||||
|
|
||||||
|
@ -56,13 +55,12 @@ async function iterableToHTMLBytes(
|
||||||
// to be propagated up.
|
// to be propagated up.
|
||||||
async function bufferHeadContent(result: SSRResult) {
|
async function bufferHeadContent(result: SSRResult) {
|
||||||
const iterator = result.propagators.values();
|
const iterator = result.propagators.values();
|
||||||
const scoped = createScopedResult(result, ScopeFlags.HeadBuffer);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = iterator.next();
|
const { value, done } = iterator.next();
|
||||||
if (done) {
|
if (done) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const returnValue = await value.init(scoped);
|
const returnValue = await value.init(result);
|
||||||
if (isHeadAndContent(returnValue)) {
|
if (isHeadAndContent(returnValue)) {
|
||||||
result.extraHead.push(returnValue.head);
|
result.extraHead.push(returnValue.head);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +79,16 @@ 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;
|
||||||
|
let head = '';
|
||||||
try {
|
try {
|
||||||
|
if (nonAstroPageNeedsHeadInjection(componentFactory)) {
|
||||||
|
const parts = new HTMLParts();
|
||||||
|
for await(const chunk of maybeRenderHead(result)) {
|
||||||
|
parts.append(chunk, result);
|
||||||
|
}
|
||||||
|
head = parts.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const renderResult = await renderComponent(
|
const renderResult = await renderComponent(
|
||||||
result,
|
result,
|
||||||
componentFactory.name,
|
componentFactory.name,
|
||||||
|
@ -106,11 +113,7 @@ export async function renderPage(
|
||||||
|
|
||||||
// Accumulate the HTML string and append the head if necessary.
|
// Accumulate the HTML string and append the head if necessary.
|
||||||
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
|
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
|
||||||
if (nonAstroPageNeedsHeadInjection(componentFactory)) {
|
parts.append(head, result);
|
||||||
for await (let chunk of maybeRenderHead(result)) {
|
|
||||||
parts.append(chunk, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response(bytes, {
|
return new Response(bytes, {
|
||||||
|
@ -120,6 +123,9 @@ export async function renderPage(
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Mark if this page component contains a <head> within its tree. If it does
|
||||||
|
// We avoid implicit head injection entirely.
|
||||||
|
result._metadata.headInTree = result.componentMetadata.get(componentFactory.moduleId!)?.containsHead ?? false;
|
||||||
const factoryReturnValue = await componentFactory(result, props, children);
|
const factoryReturnValue = await componentFactory(result, props, children);
|
||||||
const factoryIsHeadAndContent = isHeadAndContent(factoryReturnValue);
|
const factoryIsHeadAndContent = isHeadAndContent(factoryReturnValue);
|
||||||
if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) {
|
if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) {
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
|
||||||
|
|
||||||
export const ScopeFlags = {
|
|
||||||
Astro: 1 << 0, // 1
|
|
||||||
JSX: 1 << 1, // 2
|
|
||||||
Slot: 1 << 2, // 4
|
|
||||||
HeadBuffer: 1 << 3, // 8
|
|
||||||
RenderSlot: 1 << 4, // 16
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type ScopeFlagValues = (typeof ScopeFlags)[keyof typeof ScopeFlags];
|
|
||||||
|
|
||||||
export function addScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
|
|
||||||
result.scope |= flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
|
|
||||||
result.scope &= ~flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
|
|
||||||
return (result.scope & flag) === flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult {
|
|
||||||
const scopedResult = Object.create(result, {
|
|
||||||
scope: {
|
|
||||||
writable: true,
|
|
||||||
value: result.scope,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (flag != null) {
|
|
||||||
addScopeFlag(scopedResult, flag);
|
|
||||||
}
|
|
||||||
return scopedResult;
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
import { HTMLString, markHTMLString } from '../escape.js';
|
import { HTMLString, markHTMLString } from '../escape.js';
|
||||||
import { renderChild } from './any.js';
|
import { renderChild } from './any.js';
|
||||||
import { createScopedResult, ScopeFlags } from './scope.js';
|
|
||||||
|
|
||||||
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
||||||
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
||||||
|
@ -32,8 +31,7 @@ export async function renderSlot(
|
||||||
fallback?: ComponentSlotValue | RenderTemplateResult
|
fallback?: ComponentSlotValue | RenderTemplateResult
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (slotted) {
|
if (slotted) {
|
||||||
const scoped = createScopedResult(result, ScopeFlags.Slot);
|
let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
|
||||||
let iterator = renderChild(typeof slotted === 'function' ? slotted(scoped) : slotted);
|
|
||||||
let content = '';
|
let content = '';
|
||||||
let instructions: null | RenderInstruction[] = null;
|
let instructions: null | RenderInstruction[] = null;
|
||||||
for await (const chunk of iterator) {
|
for await (const chunk of iterator) {
|
||||||
|
|
|
@ -153,6 +153,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,
|
||||||
|
containsHead: transformResult.containsHead,
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
pageOptions: {},
|
pageOptions: {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface PluginMetadata {
|
||||||
hydratedComponents: TransformResult['hydratedComponents'];
|
hydratedComponents: TransformResult['hydratedComponents'];
|
||||||
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
||||||
scripts: TransformResult['scripts'];
|
scripts: TransformResult['scripts'];
|
||||||
|
containsHead: TransformResult['containsHead'];
|
||||||
propagation: PropagationHint;
|
propagation: PropagationHint;
|
||||||
pageOptions: PageOptions;
|
pageOptions: PageOptions;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import type { ModuleInfo } from 'rollup';
|
|
||||||
import type { AstroSettings, SSRResult } from '../@types/astro';
|
|
||||||
import type { BuildInternals } from '../core/build/internal.js';
|
|
||||||
import type { AstroBuildPlugin } from '../core/build/plugin.js';
|
|
||||||
import type { StaticBuildOptions } from '../core/build/types';
|
|
||||||
|
|
||||||
import type * as vite from 'vite';
|
|
||||||
import { walkParentInfos } from '../core/build/graph.js';
|
|
||||||
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 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;
|
|
||||||
} else {
|
|
||||||
seen.add(parent.id);
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function astroHeadPropagationBuildPlugin(
|
|
||||||
options: StaticBuildOptions,
|
|
||||||
internals: BuildInternals
|
|
||||||
): AstroBuildPlugin {
|
|
||||||
return {
|
|
||||||
build: 'ssr',
|
|
||||||
hooks: {
|
|
||||||
'build:before'() {
|
|
||||||
const map: SSRResult['propagation'] = new Map();
|
|
||||||
return {
|
|
||||||
vitePlugin: {
|
|
||||||
name: 'vite-plugin-head-propagation-build',
|
|
||||||
generateBundle(_opts, bundle) {
|
|
||||||
const appendPropagation = (info: ModuleInfo) => {
|
|
||||||
const astroMetadata = getAstroMetadata(info);
|
|
||||||
if (astroMetadata) {
|
|
||||||
astroMetadata.propagation = 'in-tree';
|
|
||||||
map.set(info.id, 'in-tree');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [bundleId, output] of Object.entries(bundle)) {
|
|
||||||
if (output.type !== 'chunk') continue;
|
|
||||||
for (const [id, mod] of Object.entries(output.modules)) {
|
|
||||||
if (mod.code && injectExp.test(mod.code)) {
|
|
||||||
for (const [info] of walkParentInfos(id, this)) {
|
|
||||||
appendPropagation(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = this.getModuleInfo(id);
|
|
||||||
if (info) {
|
|
||||||
appendPropagation(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the map to internals so it can be passed into SSR and generation
|
|
||||||
internals.propagation = map;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
116
packages/astro/src/vite-plugin-head/index.ts
Normal file
116
packages/astro/src/vite-plugin-head/index.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import type * as vite from 'vite';
|
||||||
|
import type { ModuleInfo } from 'rollup';
|
||||||
|
import type { AstroSettings, SSRResult, SSRComponentMetadata } from '../@types/astro';
|
||||||
|
import type { AstroBuildPlugin } from '../core/build/plugin.js';
|
||||||
|
import type { StaticBuildOptions } from '../core/build/types';
|
||||||
|
import type { PluginMetadata } from '../vite-plugin-astro/types';
|
||||||
|
|
||||||
|
import { getTopLevelPages, walkParentInfos } from '../core/build/graph.js';
|
||||||
|
import type { BuildInternals } from '../core/build/internal.js';
|
||||||
|
import { getAstroMetadata } from '../vite-plugin-astro/index.js';
|
||||||
|
|
||||||
|
const injectExp = /^\/\/\s*astro-head-inject/;
|
||||||
|
|
||||||
|
export default function configHeadVitePlugin({
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
settings: AstroSettings;
|
||||||
|
}): vite.Plugin {
|
||||||
|
let server: vite.ViteDevServer;
|
||||||
|
|
||||||
|
function propagateMetadata<
|
||||||
|
P extends keyof PluginMetadata['astro'],
|
||||||
|
V extends PluginMetadata['astro'][P]
|
||||||
|
>(this: { getModuleInfo(id: string): ModuleInfo | null }, id: string, prop: P, value: V, seen = new Set<string>()) {
|
||||||
|
if(seen.has(id)) return;
|
||||||
|
seen.add(id);
|
||||||
|
const mod = server.moduleGraph.getModuleById(id);
|
||||||
|
const info = this.getModuleInfo(id);
|
||||||
|
if (info?.meta.astro) {
|
||||||
|
const astroMetadata = getAstroMetadata(info)
|
||||||
|
if(astroMetadata) {
|
||||||
|
Reflect.set(astroMetadata, prop, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parent of mod?.importers || []) {
|
||||||
|
if(parent.id) {
|
||||||
|
propagateMetadata.call(this, parent.id, prop, value, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'astro:head-metadata',
|
||||||
|
configureServer(_server) {
|
||||||
|
server = _server;
|
||||||
|
},
|
||||||
|
transform(source, id) {
|
||||||
|
if (!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = this.getModuleInfo(id);
|
||||||
|
if(info && getAstroMetadata(info)?.containsHead) {
|
||||||
|
propagateMetadata.call(this, id, 'containsHead', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (injectExp.test(source)) {
|
||||||
|
propagateMetadata.call(this, id, 'propagation', 'in-tree');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function astroHeadBuildPlugin(
|
||||||
|
options: StaticBuildOptions,
|
||||||
|
internals: BuildInternals
|
||||||
|
): AstroBuildPlugin {
|
||||||
|
return {
|
||||||
|
build: 'ssr',
|
||||||
|
hooks: {
|
||||||
|
'build:before'() {
|
||||||
|
return {
|
||||||
|
vitePlugin: {
|
||||||
|
name: 'astro:head-metadata-build',
|
||||||
|
generateBundle(_opts, bundle) {
|
||||||
|
const map: SSRResult['componentMetadata'] = internals.componentMetadata;
|
||||||
|
function getOrCreateMetadata(id: string): SSRComponentMetadata {
|
||||||
|
if(map.has(id)) return map.get(id)!;
|
||||||
|
const metadata: SSRComponentMetadata = {
|
||||||
|
propagation: 'none',
|
||||||
|
containsHead: false
|
||||||
|
};
|
||||||
|
map.set(id, metadata);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [,output] of Object.entries(bundle)) {
|
||||||
|
if (output.type !== 'chunk') continue;
|
||||||
|
for (const [id, mod] of Object.entries(output.modules)) {
|
||||||
|
const modinfo = this.getModuleInfo(id);
|
||||||
|
|
||||||
|
// <head> tag in the tree
|
||||||
|
if(modinfo && getAstroMetadata(modinfo)?.containsHead) {
|
||||||
|
for(const [pageInfo] of getTopLevelPages(id, this)) {
|
||||||
|
let metadata = getOrCreateMetadata(pageInfo.id);
|
||||||
|
metadata.containsHead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head propagation (aka bubbling)
|
||||||
|
if (mod.code && injectExp.test(mod.code)) {
|
||||||
|
for (const [info] of walkParentInfos(id, this)) {
|
||||||
|
getOrCreateMetadata(info.id).propagation = 'in-tree';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -178,6 +178,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
||||||
clientOnlyComponents: [],
|
clientOnlyComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
|
containsHead: false,
|
||||||
pageOptions: {},
|
pageOptions: {},
|
||||||
} as PluginMetadata['astro'],
|
} as PluginMetadata['astro'],
|
||||||
vite: {
|
vite: {
|
||||||
|
|
|
@ -41,8 +41,13 @@ describe('Content Collections - render()', () => {
|
||||||
const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
|
const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
|
||||||
const { Content } = await launchWeekEntry.render();
|
const { Content } = await launchWeekEntry.render();
|
||||||
---
|
---
|
||||||
<h1>testing</h1>
|
<html>
|
||||||
<Content />
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>testing</h1>
|
||||||
|
<Content />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
root
|
root
|
||||||
|
@ -250,8 +255,13 @@ description: Astro is launching this week!
|
||||||
---
|
---
|
||||||
import { Content } from '../launch-week.ts';
|
import { Content } from '../launch-week.ts';
|
||||||
---
|
---
|
||||||
<h1>Testing</h1>
|
<html>
|
||||||
<Content />
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
<Content />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
root
|
root
|
||||||
|
|
|
@ -49,8 +49,13 @@ describe('head injection', () => {
|
||||||
import { renderEntry } from '../common/head.js';
|
import { renderEntry } from '../common/head.js';
|
||||||
const Head = renderEntry();
|
const Head = renderEntry();
|
||||||
---
|
---
|
||||||
<h1>testing</h1>
|
<html>
|
||||||
<Head />
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>testing</h1>
|
||||||
|
<Head />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
root
|
root
|
||||||
|
|
|
@ -424,7 +424,7 @@ importers:
|
||||||
|
|
||||||
packages/astro:
|
packages/astro:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/compiler': ^1.2.0
|
'@astrojs/compiler': ^1.3.0
|
||||||
'@astrojs/language-server': ^0.28.3
|
'@astrojs/language-server': ^0.28.3
|
||||||
'@astrojs/markdown-remark': ^2.1.2
|
'@astrojs/markdown-remark': ^2.1.2
|
||||||
'@astrojs/telemetry': ^2.1.0
|
'@astrojs/telemetry': ^2.1.0
|
||||||
|
@ -517,7 +517,7 @@ importers:
|
||||||
yargs-parser: ^21.0.1
|
yargs-parser: ^21.0.1
|
||||||
zod: ^3.17.3
|
zod: ^3.17.3
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 1.2.0
|
'@astrojs/compiler': 1.3.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
|
||||||
|
@ -4185,8 +4185,8 @@ packages:
|
||||||
/@astrojs/compiler/0.31.4:
|
/@astrojs/compiler/0.31.4:
|
||||||
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
|
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
|
||||||
|
|
||||||
/@astrojs/compiler/1.2.0:
|
/@astrojs/compiler/1.3.0:
|
||||||
resolution: {integrity: sha512-O8yPCyuq+PU9Fjht2tIW6WzSWiq8qDF1e8uAX2x+SOGFzKqOznp52UlDG2mSf+ekf0Z3R34sb64O7SgX+asTxg==}
|
resolution: {integrity: sha512-VxSj3gh/UTB/27rkRCT7SvyGjWtuxUO7Jf7QqDduch7j/gr/uA5P/Q5I/4zIIrZjy2yQAKyKLoox2QI2mM/BSA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@astrojs/language-server/0.28.3:
|
/@astrojs/language-server/0.28.3:
|
||||||
|
|
Loading…
Reference in a new issue