0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-31 23:31:30 -05:00

Make directRenderScript the default (#11791)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Bjorn Lu 2024-08-28 22:50:50 +08:00 committed by GitHub
parent 4e5cc5aadd
commit 93932432e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 159 additions and 739 deletions

View file

@ -0,0 +1,11 @@
---
'astro': patch
---
Updates Astro's default `<script>` rendering strategy and removes the `experimental.directRenderScript` option as this is now the default behavior: scripts are always rendered directly. This new strategy prevents scripts from being executed in pages where they are not used.
Scripts will directly render as declared in Astro files (including existing features like TypeScript, importing `node_modules`, and deduplicating scripts). You can also now conditionally render scripts in your Astro file.
However, this means scripts are no longer hoisted to the `<head>`, multiple scripts on a page are no longer bundled together, and the `<script>` tag may interfere with the CSS styling.
As this is a potentially breaking change to your script behavior, please review your `<script>` tags and ensure that they behave as expected.

View file

@ -35,7 +35,7 @@ test.describe('Astro component HMR', () => {
);
});
test('hoisted scripts', async ({ page, astro }) => {
test('Scripts', async ({ page, astro }) => {
const initialLog = page.waitForEvent(
'console',
(message) => message.text() === 'Hello, Astro!',
@ -52,7 +52,7 @@ test.describe('Astro component HMR', () => {
(message) => message.text() === 'Hello, updated Astro!',
);
// Edit the hoisted script on the page
// Edit the script on the page
await astro.editFile('./src/pages/index.astro', (content) =>
content.replace('Hello, Astro!', 'Hello, updated Astro!'),
);

View file

@ -22,7 +22,6 @@ export const ASSET_IMPORTS_VIRTUAL_ID = 'astro:asset-imports';
export const ASSET_IMPORTS_RESOLVED_STUB_ID = '\0' + ASSET_IMPORTS_VIRTUAL_ID;
export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@';
export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@';
export const SCRIPTS_PLACEHOLDER = '@@ASTRO-SCRIPTS@@';
export const IMAGE_IMPORT_PREFIX = '__ASTRO_IMAGE_';
export const CONTENT_FLAGS = [

View file

@ -9,15 +9,12 @@ import type { ModuleLoader } from '../core/module-loader/loader.js';
import { createViteLoader } from '../core/module-loader/vite.js';
import { joinPaths, prependForwardSlash } from '../core/path.js';
import type { AstroSettings } from '../types/astro.js';
import type { SSRElement } from '../types/public/internal.js';
import { getStylesForURL } from '../vite-plugin-astro-server/css.js';
import { getScriptsForURL } from '../vite-plugin-astro-server/scripts.js';
import {
CONTENT_IMAGE_FLAG,
CONTENT_RENDER_FLAG,
LINKS_PLACEHOLDER,
PROPAGATED_ASSET_FLAG,
SCRIPTS_PLACEHOLDER,
STYLES_PLACEHOLDER,
} from './consts.js';
import { hasContentFlag } from './utils.js';
@ -69,7 +66,7 @@ export function astroContentAssetPropagationPlugin({
async transform(_, id, options) {
if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) {
const basePath = id.split('?')[0];
let stringifiedLinks: string, stringifiedStyles: string, stringifiedScripts: string;
let stringifiedLinks: string, stringifiedStyles: string;
// We can access the server in dev,
// so resolve collected styles and scripts here.
@ -83,16 +80,6 @@ export function astroContentAssetPropagationPlugin({
crawledFiles: styleCrawledFiles,
} = await getStylesForURL(pathToFileURL(basePath), devModuleLoader);
// Add hoisted script tags, skip if direct rendering with `directRenderScript`
const { scripts: hoistedScripts, crawledFiles: scriptCrawledFiles } = settings.config
.experimental.directRenderScript
? { scripts: new Set<SSRElement>(), crawledFiles: new Set<string>() }
: await getScriptsForURL(
pathToFileURL(basePath),
settings.config.root,
devModuleLoader,
);
// Register files we crawled to be able to retrieve the rendered styles and scripts,
// as when they get updated, we need to re-transform ourselves.
// We also only watch files within the user source code, as changes in node_modules
@ -102,22 +89,15 @@ export function astroContentAssetPropagationPlugin({
this.addWatchFile(file);
}
}
for (const file of scriptCrawledFiles) {
if (!file.includes('node_modules')) {
this.addWatchFile(file);
}
}
stringifiedLinks = JSON.stringify([...urls]);
stringifiedStyles = JSON.stringify(styles.map((s) => s.content));
stringifiedScripts = JSON.stringify([...hoistedScripts]);
} else {
// Otherwise, use placeholders to inject styles and scripts
// during the production bundle step.
// @see the `astro:content-build-plugin` below.
stringifiedLinks = JSON.stringify(LINKS_PLACEHOLDER);
stringifiedStyles = JSON.stringify(STYLES_PLACEHOLDER);
stringifiedScripts = JSON.stringify(SCRIPTS_PLACEHOLDER);
}
const code = `
@ -126,8 +106,7 @@ export function astroContentAssetPropagationPlugin({
}
const collectedLinks = ${stringifiedLinks};
const collectedStyles = ${stringifiedStyles};
const collectedScripts = ${stringifiedScripts};
const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts };
const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts: [] };
export default defaultMod;
`;
// ^ Use a default export for tools like Markdoc
@ -145,7 +124,7 @@ export function astroConfigBuildPlugin(
return {
targets: ['server'],
hooks: {
'build:post': ({ ssrOutputs, clientOutputs, mutate }) => {
'build:post': ({ ssrOutputs, mutate }) => {
const outputs = ssrOutputs.flatMap((o) => o.output);
const prependBase = (src: string) => {
const { assetsPrefix } = options.settings.config.build;
@ -158,17 +137,12 @@ export function astroConfigBuildPlugin(
}
};
for (const chunk of outputs) {
if (
chunk.type === 'chunk' &&
(chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER))
) {
if (chunk.type === 'chunk' && chunk.code.includes(LINKS_PLACEHOLDER)) {
const entryStyles = new Set<string>();
const entryLinks = new Set<string>();
const entryScripts = new Set<string>();
for (const id of chunk.moduleIds) {
const _entryCss = internals.propagatedStylesMap.get(id);
const _entryScripts = internals.propagatedScriptsMap.get(id);
if (_entryCss) {
// TODO: Separating styles and links this way is not ideal. The `entryCss` list is order-sensitive
// and splitting them into two sets causes the order to be lost, because styles are rendered after
@ -178,11 +152,6 @@ export function astroConfigBuildPlugin(
if (value.type === 'external') entryLinks.add(value.src);
}
}
if (_entryScripts) {
for (const value of _entryScripts) {
entryScripts.add(value);
}
}
}
let newCode = chunk.code;
@ -202,33 +171,6 @@ export function astroConfigBuildPlugin(
} else {
newCode = newCode.replace(JSON.stringify(LINKS_PLACEHOLDER), '[]');
}
if (entryScripts.size) {
const entryFileNames = new Set<string>();
for (const output of clientOutputs) {
for (const clientChunk of output.output) {
if (clientChunk.type !== 'chunk') continue;
for (const [id] of Object.entries(clientChunk.modules)) {
if (entryScripts.has(id)) {
entryFileNames.add(clientChunk.fileName);
}
}
}
}
newCode = newCode.replace(
JSON.stringify(SCRIPTS_PLACEHOLDER),
JSON.stringify(
[...entryFileNames].map((src) => ({
props: {
src: prependBase(src),
type: 'module',
},
children: '',
})),
),
);
} else {
newCode = newCode.replace(JSON.stringify(SCRIPTS_PLACEHOLDER), '[]');
}
mutate(chunk, ['server'], newCode);
}
}

View file

@ -160,7 +160,6 @@ async function generatePage(
.reduce(mergeInlineCss, []);
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const linkIds: [] = [];
const scripts = pageData.hoistedScript ?? null;
if (!pageModulePromise) {
throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`,
@ -170,7 +169,7 @@ async function generatePage(
const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
linkIds,
scripts,
scripts: null,
styles,
mod: pageModule,
};

View file

@ -15,15 +15,10 @@ export interface BuildInternals {
*/
cssModuleToChunkIdMap: Map<string, string>;
// A mapping of hoisted script ids back to the exact hoisted scripts it references
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
// A mapping of hoisted script ids back to the pages which reference it
hoistedScriptIdToPagesMap: Map<string, Set<string>>;
/**
* Used by the `directRenderScript` option. If script is inlined, its id and
* inlined code is mapped here. The resolved id is an URL like "/_astro/something.js"
* but will no longer exist as the content is now inlined in this map.
* If script is inlined, its id and inlined code is mapped here. The resolved id is
* an URL like "/_astro/something.js" but will no longer exist as the content is now
* inlined in this map.
*/
inlinedScripts: Map<string, string>;
@ -72,7 +67,7 @@ export interface BuildInternals {
*/
discoveredClientOnlyComponents: Map<string, string[]>;
/**
* A list of hoisted scripts that are discovered during the SSR build
* A list of scripts that are discovered during the SSR build.
* These will be used as the top-level entrypoints for the client build.
*/
discoveredScripts: Set<string>;
@ -85,11 +80,6 @@ export interface BuildInternals {
* to a set of stylesheets that it uses.
*/
propagatedStylesMap: Map<string, Set<StylesheetAsset>>;
/**
* Map of propagated module ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`)
* to a set of hoisted scripts that it uses.
*/
propagatedScriptsMap: Map<string, Set<string>>;
// A list of all static files created during the build. Used for SSR.
staticFiles: Set<string>;
@ -113,17 +103,9 @@ export interface BuildInternals {
* @returns {BuildInternals}
*/
export function createBuildInternals(): BuildInternals {
// These are for tracking hoisted script bundling
const hoistedScriptIdToHoistedMap = new Map<string, Set<string>>();
// This tracks hoistedScriptId => page components
const hoistedScriptIdToPagesMap = new Map<string, Set<string>>();
return {
cachedClientEntries: [],
cssModuleToChunkIdMap: new Map(),
hoistedScriptIdToHoistedMap,
hoistedScriptIdToPagesMap,
inlinedScripts: new Map(),
entrySpecifierToBundleMap: new Map<string, string>(),
pagesByKeys: new Map(),
@ -132,7 +114,6 @@ export function createBuildInternals(): BuildInternals {
pagesByScriptId: new Map(),
propagatedStylesMap: new Map(),
propagatedScriptsMap: new Map(),
discoveredHydratedComponents: new Map(),
discoveredClientOnlyComponents: new Map(),
@ -179,7 +160,7 @@ export function trackClientOnlyPageDatas(
}
/**
* Tracks scripts to the pages they are associated with. (experimental.directRenderScript)
* Tracks scripts to the pages they are associated with.
*/
export function trackScriptPageDatas(
internals: BuildInternals,
@ -247,19 +228,6 @@ export function getPageData(
return undefined;
}
/**
* Get all pages datas from the build internals, using a specific component.
* @param internals Build Internals with all the pages
* @param component path to the component, used to identify related pages
*/
function getPagesDatasByComponent(internals: BuildInternals, component: string): PageBuildData[] {
const pageDatas: PageBuildData[] = [];
internals.pagesByKeys.forEach((pageData) => {
if (component === pageData.component) pageDatas.push(pageData);
});
return pageDatas;
}
// TODO: Should be removed in the future. (Astro 5?)
/**
* Map internals.pagesByKeys to a new map with the public key instead of the internal key.
@ -371,24 +339,3 @@ export function mergeInlineCss(
acc.push(current);
return acc;
}
/**
* Get all pages data from the build internals, using a specific hoisted script id.
* @param internals Build Internals with all the pages
* @param id Hoisted script id, used to identify the pages using it
*/
export function getPageDatasByHoistedScriptId(
internals: BuildInternals,
id: string,
): PageBuildData[] {
const set = internals.hoistedScriptIdToPagesMap.get(id);
const pageDatas: PageBuildData[] = [];
if (set) {
for (const pageId of set) {
getPagesDatasByComponent(internals, pageId.slice(1)).forEach((pageData) => {
pageDatas.push(pageData);
});
}
}
return pageDatas;
}

View file

@ -39,7 +39,6 @@ export function collectPagesData(opts: CollectPagesDataOptions): CollectPagesDat
route,
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
};
if (settings.config.output === 'static') {
@ -60,7 +59,6 @@ export function collectPagesData(opts: CollectPagesDataOptions): CollectPagesDat
route,
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
};
}

View file

@ -1,17 +1,18 @@
import { getOutputDirectory } from '../../prerender/utils.js';
import type { ComponentInstance } from '../../types/astro.js';
import type { RewritePayload } from '../../types/public/common.js';
import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../types/public/internal.js';
import type {
RouteData,
SSRElement,
SSRLoadedRenderer,
SSRResult,
} from '../../types/public/internal.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import type { SSRManifest } from '../app/types.js';
import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { Pipeline } from '../render/index.js';
import {
createAssetLink,
createModuleScriptsSet,
createStylesheetElementSet,
} from '../render/ssr-element.js';
import { createAssetLink, createStylesheetElementSet } from '../render/ssr-element.js';
import { createDefaultRoutes } from '../routing/default.js';
import { findRouteToRewrite } from '../routing/rewrite.js';
import { isServerLikeOutput } from '../util.js';
@ -153,11 +154,7 @@ export class BuildPipeline extends Pipeline {
} = this;
const links = new Set<never>();
const pageBuildData = getPageData(internals, routeData.route, routeData.component);
const scripts = createModuleScriptsSet(
pageBuildData?.hoistedScript ? [pageBuildData.hoistedScript] : [],
base,
assetsPrefix,
);
const scripts = new Set<SSRElement>();
const sortedCssAssets = pageBuildData?.styles
.sort(cssOrder)
.map(({ sheet }) => sheet)

View file

@ -6,7 +6,6 @@ import { pluginChunks } from './plugin-chunks.js';
import { pluginComponentEntry } from './plugin-component-entry.js';
import { pluginContent } from './plugin-content.js';
import { pluginCSS } from './plugin-css.js';
import { pluginHoistedScripts } from './plugin-hoisted-scripts.js';
import { pluginInternals } from './plugin-internals.js';
import { pluginManifest } from './plugin-manifest.js';
import { pluginMiddleware } from './plugin-middleware.js';
@ -18,7 +17,7 @@ import { pluginSSR } from './plugin-ssr.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
register(pluginAnalyzer(options, internals));
register(pluginAnalyzer(internals));
register(pluginInternals(internals));
register(pluginManifest(options, internals));
register(pluginRenderers(options));
@ -29,11 +28,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
register(astroHeadBuildPlugin(internals));
register(pluginPrerender(options, internals));
register(astroConfigBuildPlugin(options, internals));
if (options.settings.config.experimental.directRenderScript) {
register(pluginScripts(internals));
} else {
register(pluginHoistedScripts(options, internals));
}
register(pluginScripts(internals));
register(pluginSSR(options, internals));
register(pluginChunks());
}

View file

@ -1,130 +1,18 @@
import type { PluginContext } from 'rollup';
import type { Plugin as VitePlugin } from 'vite';
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types.js';
import { getTopLevelPageModuleInfos } from '../graph.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import { prependForwardSlash } from '../../../core/path.js';
import {
getParentModuleInfos,
getTopLevelPageModuleInfos,
moduleIsTopLevelPage,
} from '../graph.js';
import {
getPageDataByViteID,
trackClientOnlyPageDatas,
trackScriptPageDatas,
} from '../internal.js';
import type { StaticBuildOptions } from '../types.js';
function isPropagatedAsset(id: string) {
try {
return new URL('file://' + id).searchParams.has(PROPAGATED_ASSET_FLAG);
} catch {
return false;
}
}
export function vitePluginAnalyzer(
options: StaticBuildOptions,
internals: BuildInternals,
): VitePlugin {
function hoistedScriptScanner() {
const uniqueHoistedIds = new Map<string, string>();
const pageScriptsMap = new Map<
string,
{
hoistedSet: Set<string>;
}
>();
return {
async scan(
this: PluginContext,
scripts: AstroPluginMetadata['astro']['scripts'],
from: string,
) {
const hoistedScripts = new Set<string>();
for (let i = 0; i < scripts.length; i++) {
const hid = `${from.replace('/@fs', '')}?astro&type=script&index=${i}&lang.ts`;
hoistedScripts.add(hid);
}
if (hoistedScripts.size) {
for (const parentInfo of getParentModuleInfos(from, this, isPropagatedAsset)) {
if (isPropagatedAsset(parentInfo.id)) {
if (!internals.propagatedScriptsMap.has(parentInfo.id)) {
internals.propagatedScriptsMap.set(parentInfo.id, new Set());
}
const propagatedScripts = internals.propagatedScriptsMap.get(parentInfo.id)!;
for (const hid of hoistedScripts) {
propagatedScripts.add(hid);
}
} else if (moduleIsTopLevelPage(parentInfo)) {
if (!pageScriptsMap.has(parentInfo.id)) {
pageScriptsMap.set(parentInfo.id, {
hoistedSet: new Set(),
});
}
const pageScripts = pageScriptsMap.get(parentInfo.id)!;
for (const hid of hoistedScripts) {
pageScripts.hoistedSet.add(hid);
}
}
}
}
},
finalize() {
// Add propagated scripts to client build,
// but DON'T add to pages -> hoisted script map.
for (const propagatedScripts of internals.propagatedScriptsMap.values()) {
for (const propagatedScript of propagatedScripts) {
internals.discoveredScripts.add(propagatedScript);
}
}
for (const [pageId, { hoistedSet }] of pageScriptsMap) {
const pageData = getPageDataByViteID(internals, pageId);
if (!pageData) continue;
const { component } = pageData;
const astroModuleId = prependForwardSlash(component);
const uniqueHoistedId = JSON.stringify(Array.from(hoistedSet).sort());
let moduleId: string;
// If we're already tracking this set of hoisted scripts, get the unique id
if (uniqueHoistedIds.has(uniqueHoistedId)) {
moduleId = uniqueHoistedIds.get(uniqueHoistedId)!;
} else {
// Otherwise, create a unique id for this set of hoisted scripts
moduleId = `/astro/hoisted.js?q=${uniqueHoistedIds.size}`;
uniqueHoistedIds.set(uniqueHoistedId, moduleId);
}
internals.discoveredScripts.add(moduleId);
// Make sure to track that this page uses this set of hoisted scripts
if (internals.hoistedScriptIdToPagesMap.has(moduleId)) {
const pages = internals.hoistedScriptIdToPagesMap.get(moduleId);
pages!.add(astroModuleId);
} else {
internals.hoistedScriptIdToPagesMap.set(moduleId, new Set([astroModuleId]));
internals.hoistedScriptIdToHoistedMap.set(moduleId, hoistedSet);
}
}
},
};
}
import type { AstroBuildPlugin } from '../plugin.js';
export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
return {
name: '@astro/rollup-plugin-astro-analyzer',
async generateBundle() {
const hoistScanner = options.settings.config.experimental.directRenderScript
? { scan: async () => {}, finalize: () => {} }
: hoistedScriptScanner();
const ids = this.getModuleIds();
for (const id of ids) {
@ -143,9 +31,6 @@ export function vitePluginAnalyzer(
}
}
// Scan hoisted scripts
await hoistScanner.scan.call(this, astro.scripts, id);
if (astro.clientOnlyComponents.length) {
const clientOnlys: string[] = [];
@ -176,7 +61,7 @@ export function vitePluginAnalyzer(
// When directly rendering scripts, we don't need to group them together when bundling,
// each script module is its own entrypoint, so we directly assign each script modules to
// `discoveredScripts` here, which will eventually be passed as inputs of the client build.
if (options.settings.config.experimental.directRenderScript && astro.scripts.length) {
if (astro.scripts.length) {
const scriptIds = astro.scripts.map(
(_, i) => `${id.replace('/@fs', '')}?astro&type=script&index=${i}&lang.ts`,
);
@ -195,23 +80,17 @@ export function vitePluginAnalyzer(
}
}
}
// Finalize hoisting
hoistScanner.finalize();
},
};
}
export function pluginAnalyzer(
options: StaticBuildOptions,
internals: BuildInternals,
): AstroBuildPlugin {
export function pluginAnalyzer(internals: BuildInternals): AstroBuildPlugin {
return {
targets: ['server'],
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginAnalyzer(options, internals),
vitePlugin: vitePluginAnalyzer(internals),
};
},
},

View file

@ -13,11 +13,7 @@ import {
getParentModuleInfos,
moduleIsTopLevelPage,
} from '../graph.js';
import {
getPageDataByViteID,
getPageDatasByClientOnlyID,
getPageDatasByHoistedScriptId,
} from '../internal.js';
import { getPageDataByViteID, getPageDatasByClientOnlyID } from '../internal.js';
import { extendManualChunks, shouldInlineAsset } from './util.js';
interface PluginOptions {
@ -146,19 +142,11 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
appendCSSToPage(pageData, meta, pagesToCss, depth, order);
}
} else if (options.target === 'client') {
// For scripts or hoisted scripts, walk parents until you find a page, and add the CSS to that page.
if (buildOptions.settings.config.experimental.directRenderScript) {
const pageDatas = internals.pagesByScriptId.get(pageInfo.id)!;
if (pageDatas) {
for (const pageData of pageDatas) {
appendCSSToPage(pageData, meta, pagesToCss, -1, order);
}
}
} else {
if (internals.hoistedScriptIdToPagesMap.has(pageInfo.id)) {
for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
appendCSSToPage(pageData, meta, pagesToCss, -1, order);
}
// For scripts, walk parents until you find a page, and add the CSS to that page.
const pageDatas = internals.pagesByScriptId.get(pageInfo.id)!;
if (pageDatas) {
for (const pageData of pageDatas) {
appendCSSToPage(pageData, meta, pagesToCss, -1, order);
}
}
}

View file

@ -1,119 +0,0 @@
import type { BuildOptions, Rollup, Plugin as VitePlugin } from 'vite';
import type { AstroSettings } from '../../../types/astro.js';
import { viteID } from '../../util.js';
import type { BuildInternals } from '../internal.js';
import { getPageDataByViteID } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { shouldInlineAsset } from './util.js';
function virtualHoistedEntry(id: string) {
return id.startsWith('/astro/hoisted.js?q=');
}
export function vitePluginHoistedScripts(
settings: AstroSettings,
internals: BuildInternals,
): VitePlugin {
let assetsInlineLimit: NonNullable<BuildOptions['assetsInlineLimit']>;
return {
name: '@astro/rollup-plugin-astro-hoisted-scripts',
configResolved(config) {
assetsInlineLimit = config.build.assetsInlineLimit;
},
resolveId(id) {
if (virtualHoistedEntry(id)) {
return id;
}
},
load(id) {
if (virtualHoistedEntry(id)) {
let code = '';
for (let path of internals.hoistedScriptIdToHoistedMap.get(id)!) {
let importPath = path;
// `/@fs` is added during the compiler's transform() step
if (importPath.startsWith('/@fs')) {
importPath = importPath.slice('/@fs'.length);
}
code += `import "${importPath}";`;
}
return {
code,
};
}
return void 0;
},
async generateBundle(_options, bundle) {
const considerInlining = new Map<string, Rollup.OutputChunk>();
const importedByOtherScripts = new Set<string>();
// Find all page entry points and create a map of the entry point to the hashed hoisted script.
// This is used when we render so that we can add the script to the head.
Object.entries(bundle).forEach(([id, output]) => {
if (
output.type === 'chunk' &&
output.facadeModuleId &&
virtualHoistedEntry(output.facadeModuleId)
) {
considerInlining.set(id, output);
output.imports.forEach((imported) => importedByOtherScripts.add(imported));
}
});
for (const [id, output] of considerInlining.entries()) {
const canBeInlined =
importedByOtherScripts.has(output.fileName) === false &&
output.imports.length === 0 &&
output.dynamicImports.length === 0 &&
shouldInlineAsset(output.code, output.fileName, assetsInlineLimit);
let removeFromBundle = false;
const facadeId = output.facadeModuleId!;
const pages = internals.hoistedScriptIdToPagesMap.get(facadeId)!;
for (const pathname of pages) {
const vid = viteID(new URL('.' + pathname, settings.config.root));
const pageInfo = getPageDataByViteID(internals, vid);
if (pageInfo) {
if (canBeInlined) {
pageInfo.hoistedScript = {
type: 'inline',
value: output.code,
};
removeFromBundle = true;
} else {
pageInfo.hoistedScript = {
type: 'external',
value: id,
};
}
}
}
// Remove the bundle if it was inlined
if (removeFromBundle) {
delete bundle[id];
}
}
},
};
}
export function pluginHoistedScripts(
options: StaticBuildOptions,
internals: BuildInternals,
): AstroBuildPlugin {
return {
targets: ['client'],
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginHoistedScripts(options.settings, internals),
};
},
},
};
}

View file

@ -193,16 +193,6 @@ function buildManifest(
const pageData = internals.pagesByKeys.get(makePageDataKey(route.route, route.component));
if (route.prerender || !pageData) continue;
const scripts: SerializedRouteInfo['scripts'] = [];
if (pageData.hoistedScript) {
const shouldPrefixAssetPath = pageData.hoistedScript.type === 'external';
const hoistedValue = pageData.hoistedScript.value;
const value = shouldPrefixAssetPath ? prefixAssetPath(hoistedValue) : hoistedValue;
scripts.unshift(
Object.assign({}, pageData.hoistedScript, {
value,
}),
);
}
if (settings.scripts.some((script) => script.stage === 'page')) {
const src = entryModules[PAGE_SCRIPT_ID];

View file

@ -4,7 +4,7 @@ import type { AstroBuildPlugin } from '../plugin.js';
import { shouldInlineAsset } from './util.js';
/**
* Used by the `experimental.directRenderScript` option to inline scripts directly into the HTML.
* Inline scripts from Astro files directly into the HTML.
*/
export function vitePluginScripts(internals: BuildInternals): VitePlugin {
let assetInlineLimit: NonNullable<BuildOptions['assetsInlineLimit']>;

View file

@ -13,15 +13,12 @@ export type StylesheetAsset =
| { type: 'inline'; content: string }
| { type: 'external'; src: string };
export type HoistedScriptAsset = { type: 'inline' | 'external'; value: string };
/** Public type exposed through the `astro:build:setup` integration hook */
export interface PageBuildData {
key: string;
component: ComponentPath;
route: RouteData;
moduleSpecifier: string;
hoistedScript: HoistedScriptAsset | undefined;
styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
}

View file

@ -60,7 +60,7 @@ export async function compile({
astroConfig.devToolbar &&
astroConfig.devToolbar.enabled &&
(await preferences.get('devToolbar.enabled')),
renderScript: astroConfig.experimental.directRenderScript,
renderScript: true,
preprocessStyle: createStylePreprocessor({
filename,
viteConfig,

View file

@ -90,7 +90,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
},
experimental: {
actions: false,
directRenderScript: false,
contentCollectionCache: false,
clientPrerender: false,
serverIslands: false,
@ -516,10 +515,6 @@ export const AstroConfigSchema = z.object({
experimental: z
.object({
actions: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.actions),
directRenderScript: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.directRenderScript),
contentCollectionCache: z
.boolean()
.optional()

View file

@ -73,13 +73,3 @@ export function createModuleScriptElementWithSrc(
children: '',
};
}
export function createModuleScriptsSet(
scripts: { type: 'inline' | 'external'; value: string }[],
base?: string,
assetsPrefix?: AssetsPrefix,
): Set<SSRElement> {
return new Set<SSRElement>(
scripts.map((script) => createModuleScriptElement(script, base, assetsPrefix)),
);
}

View file

@ -1464,33 +1464,6 @@ export interface AstroUserConfig {
* These flags are not guaranteed to be stable.
*/
experimental?: {
/**
* @docs
* @name experimental.directRenderScript
* @type {boolean}
* @default `false`
* @version 4.5.0
* @description
* Enables a more reliable strategy to prevent scripts from being executed in pages where they are not used.
*
* Scripts will directly render as declared in Astro files (including existing features like TypeScript, importing `node_modules`,
* and deduplicating scripts). You can also now conditionally render scripts in your Astro file.
* However, this means scripts are no longer hoisted to the `<head>` and multiple scripts on a page are no longer bundled together.
* If you enable this option, you should check that all your `<script>` tags behave as expected.
*
* This option will be enabled by default in Astro 5.0.
*
* ```js
* {
* experimental: {
* directRenderScript: true,
* },
* }
* ```
*/
directRenderScript?: boolean;
/**
* @docs
* @name experimental.actions

View file

@ -24,7 +24,6 @@ import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
import { getStylesForURL } from './css.js';
import { getComponentMetadata } from './metadata.js';
import { createResolve } from './resolve.js';
import { getScriptsForURL } from './scripts.js';
export class DevPipeline extends Pipeline {
// renderers are loaded on every request,
@ -77,10 +76,7 @@ export class DevPipeline extends Pipeline {
settings,
} = this;
const filePath = new URL(`${routeData.component}`, root);
// Add hoisted script tags, skip if direct rendering with `directRenderScript`
const { scripts } = settings.config.experimental.directRenderScript
? { scripts: new Set<SSRElement>() }
: await getScriptsForURL(filePath, settings.config.root, loader);
const scripts = new Set<SSRElement>();
// Inject HMR scripts
if (isPage(filePath, settings) && mode === 'development') {

View file

@ -1,46 +0,0 @@
import type { ModuleInfo, ModuleLoader } from '../core/module-loader/index.js';
import { createModuleScriptElementWithSrc } from '../core/render/ssr-element.js';
import { viteID } from '../core/util.js';
import { rootRelativePath } from '../core/viteUtils.js';
import type { SSRElement } from '../types/public/internal.js';
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types.js';
import { crawlGraph } from './vite.js';
export async function getScriptsForURL(
filePath: URL,
root: URL,
loader: ModuleLoader,
): Promise<{ scripts: Set<SSRElement>; crawledFiles: Set<string> }> {
const elements = new Set<SSRElement>();
const crawledFiles = new Set<string>();
const rootID = viteID(filePath);
const modInfo = loader.getModuleInfo(rootID);
addHoistedScripts(elements, modInfo, root);
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
if (moduleNode.file) {
crawledFiles.add(moduleNode.file);
}
const id = moduleNode.id;
if (id) {
const info = loader.getModuleInfo(id);
addHoistedScripts(elements, info, root);
}
}
return { scripts: elements, crawledFiles };
}
function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null, root: URL) {
if (!info?.meta?.astro) {
return;
}
let id = info.id;
const astro = info?.meta?.astro as AstroPluginMetadata['astro'];
for (let i = 0; i < astro.scripts.length; i++) {
let scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
scriptId = rootRelativePath(root, scriptId);
const element = createModuleScriptElementWithSrc(scriptId);
set.add(element);
}
}

View file

@ -148,22 +148,22 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
}
case 'script': {
if (typeof query.index === 'undefined') {
throw new Error(`Requests for hoisted scripts must include an index`);
throw new Error(`Requests for scripts must include an index`);
}
// HMR hoisted script only exists to make them appear in the module graph.
// SSR script only exists to make them appear in the module graph.
if (opts?.ssr) {
return {
code: `/* client hoisted script, empty in SSR: ${id} */`,
code: `/* client script, empty in SSR: ${id} */`,
};
}
const hoistedScript = compileMetadata.scripts[query.index];
if (!hoistedScript) {
throw new Error(`No hoisted script at index ${query.index}`);
const script = compileMetadata.scripts[query.index];
if (!script) {
throw new Error(`No script at index ${query.index}`);
}
if (hoistedScript.type === 'external') {
const src = hoistedScript.src;
if (script.type === 'external') {
const src = script.src;
if (src.startsWith('/') && !isBrowserPath(src)) {
const publicDir = config.publicDir.pathname.replace(/\/$/, '').split('/').pop() + '/';
throw new Error(
@ -181,14 +181,14 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
},
};
switch (hoistedScript.type) {
switch (script.type) {
case 'inline': {
const { code, map } = hoistedScript;
const { code, map } = script;
result.code = appendSourceMap(code, map);
break;
}
case 'external': {
const { src } = hoistedScript;
const { src } = script;
result.code = `import "${src}"`;
break;
}

View file

@ -44,6 +44,6 @@ export interface CompileMetadata {
originalCode: string;
/** For Astro CSS virtual module */
css: CompileCssResult[];
/** For Astro hoisted scripts virtual module */
/** For Astro scripts virtual module */
scripts: HoistedScript[];
}

View file

@ -3,7 +3,7 @@ import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Scripts (hoisted and not)', () => {
describe('Scripts', () => {
describe('Build', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
@ -19,58 +19,26 @@ describe('Scripts (hoisted and not)', () => {
await fixture.build();
});
it('Moves external scripts up', async () => {
it('Renders scripts in place', async () => {
const html = await fixture.readFile('/external/index.html');
const $ = cheerio.load(html);
assert.equal($('head script[type="module"]:not([src="/regular_script.js"])').length, 1);
assert.equal($('body script').length, 0);
assert.equal($('head script').length, 1);
assert.equal($('body script').length, 2);
});
it('Moves inline scripts up', async () => {
const html = await fixture.readFile('/inline/index.html');
const $ = cheerio.load(html);
assert.equal($('head script[type="module"]').length, 1);
assert.equal($('body script').length, 0);
});
it('Inline page builds the scripts to a single bundle', async () => {
// Inline page
it('Inline page builds the scripts each as its own entry', async () => {
let inline = await fixture.readFile('/inline/index.html');
let $ = cheerio.load(inline);
let $el = $('script');
// test 1: Just one entry module
assert.equal($el.length, 1);
const src = $el.attr('src');
const inlineEntryJS = await fixture.readFile(src);
// test 3: the JS exists
assert.ok(inlineEntryJS);
// test 4: Inline imported JS is included
assert.equal(
inlineEntryJS.includes('I AM IMPORTED INLINE'),
true,
'The inline imported JS is included in the bundle',
);
});
it("Inline scripts that are shared by multiple pages create chunks, and aren't inlined into the HTML", async () => {
let html = await fixture.readFile('/inline-shared-one/index.html');
let $ = cheerio.load(html);
assert.equal($('script').length, 1);
assert.notEqual($('script').attr('src'), undefined);
assert.equal($el.length, 2);
});
it('External page using non-hoist scripts that are modules are built standalone', async () => {
let external = await fixture.readFile('/external-no-hoist/index.html');
let $ = cheerio.load(external);
// test 1: there is 1 scripts
assert.equal($('script').length, 1);
// test 2: inside assets
@ -97,7 +65,7 @@ describe('Scripts (hoisted and not)', () => {
assert.equal($('script[type="module"]').length > 0, true);
});
it('Styles imported by hoisted scripts are included on the page', async () => {
it('Styles imported by scripts are included on the page', async () => {
let html = await fixture.readFile('/with-styles/index.html');
let $ = cheerio.load(html);
@ -116,18 +84,16 @@ describe('Scripts (hoisted and not)', () => {
await fixture.build();
});
it('External page builds the hoisted scripts to a single bundle', async () => {
it('External page builds the scripts to a single bundle', async () => {
let external = await fixture.readFile('/external/index.html');
let $ = cheerio.load(external);
// test 1: there are two scripts
assert.equal($('script').length, 2);
assert.equal($('script').length, 3);
let el = $('script').get(1);
assert.equal($(el).attr('src'), undefined, 'This should have been inlined');
let externalEntryJS = $(el).text();
// test 2: the JS exists
assert.ok(externalEntryJS);
});
});

View file

@ -60,18 +60,8 @@ describe('Content Collections - render()', () => {
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
const $ = cheerio.load(html);
const allScripts = $('head > script[type="module"]');
assert.ok(allScripts.length);
// Includes hoisted script
const scriptWithSrc = [...allScripts].find((script) =>
$(script).attr('src')?.includes('WithScripts'),
);
assert.notEqual(
scriptWithSrc,
undefined,
'`WithScripts.astro` hoisted script missing from head.',
);
// Includes script
assert.equal($('script[type="module"]').length, 1);
// Includes inline script
assert.equal($('script[data-is-inline]').length, 1);
@ -81,17 +71,7 @@ describe('Content Collections - render()', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const allScripts = $('head > script[type="module"]');
// Excludes hoisted script
const scriptWithText = [...allScripts].find((script) =>
$(script).text().includes('document.querySelector("#update-me")'),
);
assert.equal(
scriptWithText,
undefined,
'`WithScripts.astro` hoisted script included unexpectedly.',
);
assert.equal($('script').length, 0);
});
it('Applies MDX components export', async () => {
@ -233,17 +213,8 @@ describe('Content Collections - render()', () => {
const html = await response.text();
const $ = cheerio.load(html);
const allScripts = $('head > script[src]');
assert.ok(allScripts.length);
// Includes hoisted script
const scriptWithSrc = [...allScripts].find((script) =>
script.attribs.src.includes('WithScripts.astro'),
);
assert.notEqual(
scriptWithSrc,
undefined,
'`WithScripts.astro` hoisted script missing from head.',
);
// Includes script
assert.equal($('script[type="module"][src*="WithScripts.astro"]').length, 1);
// Includes inline script
assert.equal($('script[data-is-inline]').length, 1);

View file

@ -168,20 +168,16 @@ describe('Content Collections', () => {
});
});
describe('Hoisted scripts', () => {
describe('Scripts', () => {
it('Contains all the scripts imported by components', async () => {
const html = await fixture.readFile('/with-scripts/one/index.html');
const $ = cheerio.load(html);
// NOTE: Hoisted scripts have two tags currently but could be optimized as one. However, we're moving towards
// `experimental.directRenderScript` so this optimization isn't a priority at the moment.
assert.equal($('script').length, 2);
// Read the scripts' content
const scripts = $('script')
.map((_, el) => $(el).attr('src'))
.toArray();
const scriptsCode = (
await Promise.all(scripts.map(async (src) => await fixture.readFile(src)))
).join('\n');
const scriptsCode = $('script')
.map((_, el) => $(el).text())
.toArray()
.join('\n');
assert.match(scriptsCode, /ScriptCompA/);
assert.match(scriptsCode, /ScriptCompB/);
});
@ -386,7 +382,7 @@ describe('Content Collections', () => {
assert.equal($('link').attr('href').startsWith('/docs'), true);
});
it('Includes base in hoisted scripts', async () => {
it('Includes base in scripts', async () => {
const html = await fixture.readFile('/docs/index.html');
const $ = cheerio.load(html);
assert.equal($('script').attr('src').startsWith('/docs'), true);

View file

@ -67,15 +67,8 @@ if (!isWindows) {
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
const $ = cheerio.load(html);
const allScripts = $('head > script[type="module"]');
assert.ok(allScripts.length > 0);
// Includes hoisted script
assert.notEqual(
[...allScripts].find((script) => $(script).attr('src')?.includes('/_astro/WithScripts')),
undefined,
'hoisted script missing from head.',
);
// Includes script
assert.equal($('script[type="module"]').length, 1);
// Includes inline script
assert.equal($('script[data-is-inline]').length, 1);
@ -87,12 +80,12 @@ if (!isWindows) {
const allScripts = $('head > script[type="module"]');
// Excludes hoisted script
// Excludes script
assert.notEqual(
[...allScripts].find((script) =>
$(script).text().includes('document.querySelector("#update-me")'),
),
'`WithScripts.astro` hoisted script included unexpectedly.',
'`WithScripts.astro` script included unexpectedly.',
undefined,
);
});
@ -137,20 +130,20 @@ if (!isWindows) {
assert.equal(files.includes('chunks'), false, 'chunks folder removed');
});
it('hoisted script is built', async () => {
it('Script is built', async () => {
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
const $ = cheerio.load(html);
const allScripts = $('head > script[type="module"]');
const allScripts = $('script[type="module"]');
assert.ok(allScripts.length > 0);
// Includes hoisted script
// Includes script
assert.notEqual(
[...allScripts].find((script) =>
$(script).attr('src')?.includes('/_astro/WithScripts'),
),
undefined,
'hoisted script missing from head.',
'Script missing.',
);
});
});

View file

@ -383,7 +383,7 @@ describe('Experimental Content Collections cache', () => {
assert.equal($('link').attr('href').startsWith('/docs'), true);
});
it('Includes base in hoisted scripts', async () => {
it('Includes base in scripts', async () => {
const html = await fixture.readFile('/docs/index.html');
const $ = cheerio.load(html);
assert.equal($('script').attr('src').startsWith('/docs'), true);

View file

@ -1,3 +0,0 @@
<script>
console.log('im a inlined script');
</script>

View file

@ -1,14 +0,0 @@
---
import InlineShared from '../components/InlineShared.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<InlineShared />
<script>
console.log("page one");
</script>
</body>
</html>

View file

@ -1,14 +0,0 @@
---
import InlineShared from '../components/InlineShared.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<InlineShared />
<script>
console.log("page two");
</script>
</body>
</html>

View file

@ -1 +1 @@
console.log('some hoisted script');
console.log('some script');

View file

@ -1,9 +1,9 @@
<p id="update-me">Hoisted script didn't update me :(</p>
<p id="update-me">Script didn't update me :(</p>
<p id="update-me-inline">Inline script didn't update me :(</p>
<script>
document.querySelector('#update-me').innerText = 'Updated client-side with hoisted script!';
document.querySelector('#update-me').innerText = 'Updated client-side with script!';
</script>
<script is:inline data-is-inline>

View file

@ -1,9 +1,9 @@
<p id="update-me">Hoisted script didn't update me :(</p>
<p id="update-me">Script didn't update me :(</p>
<p id="update-me-inline">Inline script didn't update me :(</p>
<script>
document.querySelector('#update-me').innerText = 'Updated client-side with hoisted script!';
document.querySelector('#update-me').innerText = 'Updated client-side with script!';
</script>
<script is:inline data-is-inline>

View file

@ -1,9 +1,9 @@
<p id="update-me">Hoisted script didn't update me :(</p>
<p id="update-me">Script didn't update me :(</p>
<p id="update-me-inline">Inline script didn't update me :(</p>
<script>
document.querySelector('#update-me').innerText = 'Updated client-side with hoisted script!';
document.querySelector('#update-me').innerText = 'Updated client-side with script!';
</script>
<script is:inline data-is-inline>

View file

@ -1,9 +1,6 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
directRenderScript: true,
},
vite: {
build: {
assetsInlineLimit: 100,

View file

@ -1,2 +0,0 @@
<div id="external-hoist"></div>
<script type="module" hoist src="/src/scripts/external-hoist"></script>

View file

@ -0,0 +1,2 @@
<div id="external-script"></div>
<script type="module" src="/src/scripts/external-hoist"></script>

View file

@ -1,14 +0,0 @@
<script type="module" hoist>
import { h, render } from 'preact';
import '../scripts/inline-hoist.js';
const mount = document.querySelector('#inline-hoist');
function App() {
return h('strong', null, 'Hello again');
}
render(h(App), mount);
</script>
<div id="inline-hoist"></div>
<div id="inline-hoist-two"></div>

View file

@ -0,0 +1,14 @@
<script type="module">
import { h, render } from 'preact';
import '../scripts/inline-script.js';
const mount = document.querySelector('#inline-script');
function App() {
return h('strong', null, 'Hello again');
}
render(h(App), mount);
</script>
<div id="inline-script"></div>
<div id="inline-script-two"></div>

View file

@ -1,17 +0,0 @@
---
import ExternalHoisted from '../components/ExternalHoisted.astro';
import InlineHoisted from '../components/InlineHoisted.astro';
---
<html>
<head>
<title>Demo app</title>
</head>
<body>
<section>
<h1>Hoisted scripts</h1>
<InlineHoisted />
<ExternalHoisted />
</section>
</body>
</html>

View file

@ -0,0 +1,17 @@
---
import ExternalHoisted from '../components/ExternalScripts.astro';
import InlineHoisted from '../components/InlineScripts.astro';
---
<html>
<head>
<title>Demo app</title>
</head>
<body>
<section>
<h1>Scripts</h1>
<InlineHoisted />
<ExternalHoisted />
</section>
</body>
</html>

View file

@ -1,2 +0,0 @@
const element: HTMLElement = document.querySelector('#external-hoist');
element.textContent = `This was loaded externally`;

View file

@ -0,0 +1,2 @@
const element: HTMLElement = document.querySelector('#external-script');
element.textContent = `This was loaded externally`;

View file

@ -1,2 +0,0 @@
const el = document.querySelector('#inline-hoist-two');
el.textContent = 'works';

View file

@ -0,0 +1,2 @@
const el = document.querySelector('#inline-script-two');
el.textContent = 'works';

View file

@ -24,7 +24,7 @@ describe('Projects with a space in the folder name', () => {
await devServer.stop();
});
it('Work with hoisted scripts', async () => {
it('Work with scripts', async () => {
const html = await fixture.fetch('/').then((r) => r.text());
const $ = cheerio.load(html);

View file

@ -12,15 +12,15 @@ async function fetchHTML(fixture, path) {
return html;
}
/** @type {import('./test-utils').AstroInlineConfig} */
/** @type {import('./test-utils.js').AstroInlineConfig} */
const defaultFixtureOptions = {
root: './fixtures/ssr-hoisted-script/',
root: './fixtures/ssr-script/',
output: 'server',
adapter: testAdapter(),
};
describe('Hoisted inline scripts in SSR', () => {
/** @type {import('./test-utils').Fixture} */
describe('Inline scripts in SSR', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
describe('without base path', () => {
@ -62,13 +62,13 @@ describe('Hoisted inline scripts in SSR', () => {
it('Inlined scripts get included without base path in the script', async () => {
const html = await fetchHTML(fixture, '/hello/');
const $ = cheerioLoad(html);
assert.equal($('script').html(), 'console.log("hello world");\n');
assert.equal($('script').html(), 'console.log("hello world");');
});
});
});
describe('Hoisted external scripts in SSR', () => {
/** @type {import('./test-utils').Fixture} */
describe('External scripts in SSR', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
describe('without base path', () => {
@ -92,7 +92,7 @@ describe('Hoisted external scripts in SSR', () => {
it('script has correct path', async () => {
const html = await fetchHTML(fixture, '/');
const $ = cheerioLoad(html);
assert.match($('script').attr('src'), /^\/_astro\/hoisted\..{8}\.js$/);
assert.match($('script').attr('src'), /^\/_astro\/.*\.js$/);
});
});
@ -118,7 +118,7 @@ describe('Hoisted external scripts in SSR', () => {
it('script has correct path', async () => {
const html = await fetchHTML(fixture, '/hello/');
const $ = cheerioLoad(html);
assert.match($('script').attr('src'), /^\/hello\/_astro\/hoisted\..{8}\.js$/);
assert.match($('script').attr('src'), /^\/hello\/_astro\/.*\.js$/);
});
});
@ -144,10 +144,7 @@ describe('Hoisted external scripts in SSR', () => {
it('script has correct path', async () => {
const html = await fetchHTML(fixture, '/');
const $ = cheerioLoad(html);
assert.match(
$('script').attr('src'),
/^https:\/\/cdn\.example\.com\/_astro\/hoisted\..{8}\.js$/,
);
assert.match($('script').attr('src'), /^https:\/\/cdn\.example\.com\/_astro\/.*\.js$/);
});
});

View file

@ -160,16 +160,16 @@ describe('Static build', () => {
});
});
describe('Hoisted scripts', () => {
it('Get bundled together on the page', async () => {
const html = await fixture.readFile('/hoisted/index.html');
describe('Scripts', () => {
it('Get included on the page', async () => {
const html = await fixture.readFile('/scripts/index.html');
const $ = cheerioLoad(html);
assert.equal($('script[type="module"]').length, 1, 'hoisted script added');
assert.equal($('script[type="module"]').length, 2, 'Script added');
});
it('Do not get added to the wrong page', async () => {
const hoistedHTML = await fixture.readFile('/hoisted/index.html');
const $ = cheerioLoad(hoistedHTML);
const scriptsHTML = await fixture.readFile('/scripts/index.html');
const $ = cheerioLoad(scriptsHTML);
const href = $('script[type="module"]').attr('src');
const indexHTML = await fixture.readFile('/index.html');
const $$ = cheerioLoad(indexHTML);

View file

@ -23,14 +23,14 @@ describe('Head injection w/ MDX', () => {
await fixture.build();
});
it('only injects contents into head', async () => {
it('injects content styles into head', async () => {
const html = await fixture.readFile('/indexThree/index.html');
const { document } = parseHTML(html);
const links = document.querySelectorAll('head link[rel=stylesheet]');
assert.equal(links.length, 1);
const scripts = document.querySelectorAll('head script[type=module]');
const scripts = document.querySelectorAll('script[type=module]');
assert.equal(scripts.length, 1);
});
@ -49,7 +49,7 @@ describe('Head injection w/ MDX', () => {
const links = document.querySelectorAll('head link[rel=stylesheet]');
assert.equal(links.length, 1);
const scripts = document.querySelectorAll('head script[type=module]');
const scripts = document.querySelectorAll('script[type=module]');
assert.equal(scripts.length, 1);
});

12
pnpm-lock.yaml generated
View file

@ -3847,12 +3847,6 @@ importers:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/ssr-hoisted-script:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/ssr-locals:
dependencies:
astro:
@ -3942,6 +3936,12 @@ importers:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/ssr-script:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/ssr-scripts:
dependencies:
'@astrojs/preact':