Add generic plugin for page-ssr
injection (#4049)
* feat: add generic page-ssr plugin * refactor: remove page-specific logic from astro/markdown/mdx plugins * refactor: revert changes to vite-plugin-scripts * fix: handle injected `page` scripts in build * fix: prepend injected `page` scripts with `/@id/` in dev Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
9cc3a11c44
commit
b60cc0538b
10 changed files with 93 additions and 30 deletions
6
.changeset/plenty-hats-grow.md
Normal file
6
.changeset/plenty-hats-grow.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
'@astrojs/mdx': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve `injectScript` handling for non-Astro pages
|
|
@ -18,7 +18,7 @@ import {
|
||||||
removeTrailingForwardSlash,
|
removeTrailingForwardSlash,
|
||||||
} from '../../core/path.js';
|
} from '../../core/path.js';
|
||||||
import type { RenderOptions } from '../../core/render/core';
|
import type { RenderOptions } from '../../core/render/core';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { debug, info } from '../logger/core.js';
|
import { debug, info } from '../logger/core.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
|
@ -272,6 +272,18 @@ async function generatePath(
|
||||||
const links = createLinkStylesheetElementSet(linkIds.reverse(), site);
|
const links = createLinkStylesheetElementSet(linkIds.reverse(), site);
|
||||||
const scripts = createModuleScriptsSet(hoistedScripts ? [hoistedScripts] : [], site);
|
const scripts = createModuleScriptsSet(hoistedScripts ? [hoistedScripts] : [], site);
|
||||||
|
|
||||||
|
if (astroConfig._ctx.scripts.some((script) => script.stage === 'page')) {
|
||||||
|
const hashedFilePath = internals.entrySpecifierToBundleMap.get(PAGE_SCRIPT_ID);
|
||||||
|
if (typeof hashedFilePath !== 'string') {
|
||||||
|
throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
|
||||||
|
}
|
||||||
|
const src = prependForwardSlash(npath.posix.join(astroConfig.base, hashedFilePath));
|
||||||
|
scripts.add({
|
||||||
|
props: { type: 'module', src },
|
||||||
|
children: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Add all injected scripts to the page.
|
// Add all injected scripts to the page.
|
||||||
for (const script of astroConfig._ctx.scripts) {
|
for (const script of astroConfig._ctx.scripts) {
|
||||||
if (script.stage === 'head-inline') {
|
if (script.stage === 'head-inline') {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { prependForwardSlash } from '../../core/path.js';
|
||||||
import { emptyDir, isModeServerWithNoAdapter, removeDir } from '../../core/util.js';
|
import { emptyDir, isModeServerWithNoAdapter, removeDir } from '../../core/util.js';
|
||||||
import { runHookBuildSetup } from '../../integrations/index.js';
|
import { runHookBuildSetup } from '../../integrations/index.js';
|
||||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||||
|
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import type { ViteConfigWithSSR } from '../create-vite';
|
import type { ViteConfigWithSSR } from '../create-vite';
|
||||||
import { info } from '../logger/core.js';
|
import { info } from '../logger/core.js';
|
||||||
import { generatePages } from './generate.js';
|
import { generatePages } from './generate.js';
|
||||||
|
@ -85,6 +86,10 @@ Example:
|
||||||
...internals.discoveredScripts,
|
...internals.discoveredScripts,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (astroConfig._ctx.scripts.some((script) => script.stage === 'page')) {
|
||||||
|
clientInput.add(PAGE_SCRIPT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
// Run client build first, so the assets can be fed into the SSR rendered version.
|
// Run client build first, so the assets can be fed into the SSR rendered version.
|
||||||
timer.clientBuild = performance.now();
|
timer.clientBuild = performance.now();
|
||||||
await clientBuild(opts, internals, clientInput);
|
await clientBuild(opts, internals, clientInput);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import glob from 'fast-glob';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { runHookBuildSsr } from '../../integrations/index.js';
|
import { runHookBuildSsr } from '../../integrations/index.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { pagesVirtualModuleId } from '../app/index.js';
|
import { pagesVirtualModuleId } from '../app/index.js';
|
||||||
import { serializeRouteData } from '../routing/index.js';
|
import { serializeRouteData } from '../routing/index.js';
|
||||||
import { addRollupInput } from './add-rollup-input.js';
|
import { addRollupInput } from './add-rollup-input.js';
|
||||||
|
@ -123,12 +123,19 @@ function buildManifest(
|
||||||
const { astroConfig } = opts;
|
const { astroConfig } = opts;
|
||||||
|
|
||||||
const routes: SerializedRouteInfo[] = [];
|
const routes: SerializedRouteInfo[] = [];
|
||||||
|
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
||||||
|
if (astroConfig._ctx.scripts.some((script) => script.stage === 'page')) {
|
||||||
|
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
|
||||||
|
}
|
||||||
|
|
||||||
for (const pageData of eachPageData(internals)) {
|
for (const pageData of eachPageData(internals)) {
|
||||||
const scripts: SerializedRouteInfo['scripts'] = [];
|
const scripts: SerializedRouteInfo['scripts'] = [];
|
||||||
if (pageData.hoistedScript) {
|
if (pageData.hoistedScript) {
|
||||||
scripts.unshift(pageData.hoistedScript);
|
scripts.unshift(pageData.hoistedScript);
|
||||||
}
|
}
|
||||||
|
if (astroConfig._ctx.scripts.some((script) => script.stage === 'page')) {
|
||||||
|
scripts.push({ type: 'external', value: entryModules[PAGE_SCRIPT_ID] });
|
||||||
|
}
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
file: '',
|
file: '',
|
||||||
|
@ -144,7 +151,6 @@ function buildManifest(
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK! Patch this special one.
|
// HACK! Patch this special one.
|
||||||
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
|
||||||
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
|
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
|
||||||
entryModules[BEFORE_HYDRATION_SCRIPT_ID] =
|
entryModules[BEFORE_HYDRATION_SCRIPT_ID] =
|
||||||
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||||
|
|
|
@ -14,6 +14,7 @@ import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-contai
|
||||||
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
|
import jsxVitePlugin from '../vite-plugin-jsx/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 { createCustomViteLogger } from './errors.js';
|
import { createCustomViteLogger } from './errors.js';
|
||||||
import { resolveDependency } from './util.js';
|
import { resolveDependency } from './util.js';
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ export async function createVite(
|
||||||
jsxVitePlugin({ config: astroConfig, logging }),
|
jsxVitePlugin({ config: astroConfig, logging }),
|
||||||
astroPostprocessVitePlugin({ config: astroConfig }),
|
astroPostprocessVitePlugin({ config: astroConfig }),
|
||||||
astroIntegrationsContainerPlugin({ config: astroConfig }),
|
astroIntegrationsContainerPlugin({ config: astroConfig }),
|
||||||
|
astroScriptsPageSSRPlugin({ config: astroConfig }),
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(astroConfig.publicDir),
|
publicDir: fileURLToPath(astroConfig.publicDir),
|
||||||
root: fileURLToPath(astroConfig.root),
|
root: fileURLToPath(astroConfig.root),
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
import { prependForwardSlash } from '../../../core/path.js';
|
import { prependForwardSlash } from '../../../core/path.js';
|
||||||
import { LogOptions } from '../../logger/core.js';
|
import { LogOptions } from '../../logger/core.js';
|
||||||
import { isPage } from '../../util.js';
|
import { isPage } from '../../util.js';
|
||||||
|
@ -124,6 +125,7 @@ export async function render(
|
||||||
children: '',
|
children: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We should allow adding generic HTML elements to the head, not just scripts
|
// TODO: We should allow adding generic HTML elements to the head, not just scripts
|
||||||
for (const script of astroConfig._ctx.scripts) {
|
for (const script of astroConfig._ctx.scripts) {
|
||||||
if (script.stage === 'head-inline') {
|
if (script.stage === 'head-inline') {
|
||||||
|
@ -131,6 +133,11 @@ export async function render(
|
||||||
props: {},
|
props: {},
|
||||||
children: script.content,
|
children: script.content,
|
||||||
});
|
});
|
||||||
|
} else if (script.stage === 'page' && isPage(filePath, astroConfig)) {
|
||||||
|
scripts.add({
|
||||||
|
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,6 @@ import esbuild from 'esbuild';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
|
import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
|
||||||
import { PAGE_SCRIPT_ID, PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
|
||||||
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
import { cachedCompilation, CompileProps, getCachedSource } from './compile.js';
|
import { cachedCompilation, CompileProps, getCachedSource } from './compile.js';
|
||||||
import { handleHotUpdate, trackCSSDependencies } from './hmr.js';
|
import { handleHotUpdate, trackCSSDependencies } from './hmr.js';
|
||||||
|
@ -215,14 +213,6 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = normalizeFilename(parsedId.filename);
|
const filename = normalizeFilename(parsedId.filename);
|
||||||
let isPage = false;
|
|
||||||
try {
|
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
|
||||||
isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname);
|
|
||||||
} catch {}
|
|
||||||
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
|
||||||
source += `\n<script src="${PAGE_SCRIPT_ID}" />`;
|
|
||||||
}
|
|
||||||
const compileProps: CompileProps = {
|
const compileProps: CompileProps = {
|
||||||
config,
|
config,
|
||||||
filename,
|
filename,
|
||||||
|
@ -269,10 +259,6 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add handling to inject scripts into each page JS bundle, if needed.
|
|
||||||
if (isPage) {
|
|
||||||
SUFFIX += `\nimport "${PAGE_SSR_SCRIPT_ID}";`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer live reload to HMR in `.astro` files
|
// Prefer live reload to HMR in `.astro` files
|
||||||
if (!resolvedConfig.isProduction) {
|
if (!resolvedConfig.isProduction) {
|
||||||
|
|
|
@ -10,11 +10,9 @@ import { pagesVirtualModuleId } from '../core/app/index.js';
|
||||||
import { collectErrorMetadata } from '../core/errors.js';
|
import { collectErrorMetadata } from '../core/errors.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
|
||||||
import { cachedCompilation, CompileProps } from '../vite-plugin-astro/compile.js';
|
import { cachedCompilation, CompileProps } from '../vite-plugin-astro/compile.js';
|
||||||
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
|
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
||||||
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
|
||||||
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
|
@ -147,8 +145,6 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
|
const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
|
||||||
|
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
const fileUrl = new URL(`file://${filename}`);
|
||||||
const isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname);
|
|
||||||
const hasInjectedScript = isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr');
|
|
||||||
|
|
||||||
// Extract special frontmatter keys
|
// Extract special frontmatter keys
|
||||||
let { data: frontmatter, content: markdownContent } = safeMatter(source, filename);
|
let { data: frontmatter, content: markdownContent } = safeMatter(source, filename);
|
||||||
|
@ -187,7 +183,6 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
import Slugger from 'github-slugger';
|
import Slugger from 'github-slugger';
|
||||||
${layout ? `import Layout from '${layout}';` : ''}
|
${layout ? `import Layout from '${layout}';` : ''}
|
||||||
${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
|
${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
|
||||||
${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''}
|
|
||||||
${isAstroFlavoredMd ? setup : ''}
|
${isAstroFlavoredMd ? setup : ''}
|
||||||
|
|
||||||
const slugger = new Slugger();
|
const slugger = new Slugger();
|
||||||
|
@ -224,7 +219,8 @@ ${isAstroFlavoredMd ? setup : ''}`.trim();
|
||||||
if (/\bLayout\b/.test(imports)) {
|
if (/\bLayout\b/.test(imports)) {
|
||||||
astroResult = `${prelude}\n<Layout content={$$content}>\n\n${astroResult}\n\n</Layout>`;
|
astroResult = `${prelude}\n<Layout content={$$content}>\n\n${astroResult}\n\n</Layout>`;
|
||||||
} else {
|
} else {
|
||||||
astroResult = `${prelude}\n${astroResult}`;
|
// Note: without a Layout, we need to inject `head` manually so `maybeRenderHead` runs
|
||||||
|
astroResult = `${prelude}\n<head></head>${astroResult}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform from `.astro` to valid `.ts`
|
// Transform from `.astro` to valid `.ts`
|
||||||
|
|
50
packages/astro/src/vite-plugin-scripts/page-ssr.ts
Normal file
50
packages/astro/src/vite-plugin-scripts/page-ssr.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Plugin as VitePlugin } from 'vite';
|
||||||
|
import { AstroConfig } from '../@types/astro.js';
|
||||||
|
import { PAGE_SSR_SCRIPT_ID } from './index.js';
|
||||||
|
|
||||||
|
import { isPage } from '../core/util.js';
|
||||||
|
import ancestor from 'common-ancestor-path';
|
||||||
|
import MagicString from 'magic-string';
|
||||||
|
|
||||||
|
export default function astroScriptsPostPlugin({ config }: { config: AstroConfig }): VitePlugin {
|
||||||
|
function normalizeFilename(filename: string) {
|
||||||
|
if (filename.startsWith('/@fs')) {
|
||||||
|
filename = filename.slice('/@fs'.length);
|
||||||
|
} else if (filename.startsWith('/') && !ancestor(filename, config.root.pathname)) {
|
||||||
|
filename = new URL('.' + filename, config.root).pathname;
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'astro:scripts:page-ssr',
|
||||||
|
enforce: 'post',
|
||||||
|
|
||||||
|
transform(this, code, id, options) {
|
||||||
|
if (!options?.ssr) return;
|
||||||
|
|
||||||
|
const hasInjectedScript = config._ctx.scripts.some((s) => s.stage === 'page-ssr');
|
||||||
|
if (!hasInjectedScript) return;
|
||||||
|
|
||||||
|
const filename = normalizeFilename(id);
|
||||||
|
let fileURL: URL;
|
||||||
|
try {
|
||||||
|
fileURL = new URL(`file://${filename}`);
|
||||||
|
} catch (e) {
|
||||||
|
// If we can't construct a valid URL, exit early
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileIsPage = isPage(fileURL, config);
|
||||||
|
if (!fileIsPage) return;
|
||||||
|
|
||||||
|
const s = new MagicString(code, { filename });
|
||||||
|
s.prepend(`import '${PAGE_SSR_SCRIPT_ID}';\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: s.generateMap(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -125,13 +125,6 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
||||||
if (!id.endsWith('.mdx')) return;
|
if (!id.endsWith('.mdx')) return;
|
||||||
const [, moduleExports] = parseESM(code);
|
const [, moduleExports] = parseESM(code);
|
||||||
|
|
||||||
// This adds support for injected "page-ssr" scripts in MDX files.
|
|
||||||
// TODO: This should only be happening on page entrypoints, not all imported MDX.
|
|
||||||
// TODO: This code is copy-pasted across all Astro/Vite plugins that deal with page
|
|
||||||
// entrypoints (.astro, .md, .mdx). This should be handled in some centralized place,
|
|
||||||
// or otherwise refactored to not require copy-paste handling logic.
|
|
||||||
code += `\nimport "${'astro:scripts/page-ssr.js'}";`;
|
|
||||||
|
|
||||||
const { fileUrl, fileId } = getFileInfo(id, config);
|
const { fileUrl, fileId } = getFileInfo(id, config);
|
||||||
if (!moduleExports.includes('url')) {
|
if (!moduleExports.includes('url')) {
|
||||||
code += `\nexport const url = ${JSON.stringify(fileUrl)};`;
|
code += `\nexport const url = ${JSON.stringify(fileUrl)};`;
|
||||||
|
|
Loading…
Reference in a new issue