diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index fd73caf47..a5b205151 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -2,6 +2,7 @@ import { pathToFileURL } from 'url'; import type { Plugin } from 'vite'; import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js'; import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js'; +import { AstroBuildPlugin } from '../core/build/plugin.js'; import type { ModuleLoader } from '../core/module-loader/loader.js'; import { createViteLoader } from '../core/module-loader/vite.js'; import { getStylesForURL } from '../core/render/dev/css.js'; @@ -98,3 +99,16 @@ export function astroContentProdBundlePlugin({ internals }: { internals: BuildIn }, }; } + +export function astroConfigBuildPlugin(internals: BuildInternals): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before': () => { + return { + vitePlugin: astroContentProdBundlePlugin({ internals }) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/plugin.ts b/packages/astro/src/core/build/plugin.ts new file mode 100644 index 000000000..47c87e334 --- /dev/null +++ b/packages/astro/src/core/build/plugin.ts @@ -0,0 +1,116 @@ +import type { Plugin as VitePlugin } from 'vite'; +import type { BuildInternals } from './internal'; +import type { StaticBuildOptions, ViteBuildReturn } from './types'; + +type RollupOutputArray = Extract>; +type OutputChunkorAsset = RollupOutputArray[number]['output'][number] +type OutputChunk = Extract; + +type MutateChunk = (chunk: OutputChunk, build: 'server' | 'client', newCode: string) => void; + +export type AstroBuildPlugin = { + build: 'ssr' | 'client' | 'both'; + hooks?: { + 'build:before'?: (opts: { + build: 'ssr' | 'client'; + input: Set; + }) => { + enforce?: 'after-user-plugins'; + vitePlugin: VitePlugin | VitePlugin[] | undefined + }; + 'build:post'?: (opts: {ssrOutputs: RollupOutputArray; clientOutputs: RollupOutputArray; mutate: MutateChunk}) => void | Promise; + } +}; + +export function createPluginContainer(options: StaticBuildOptions, internals: BuildInternals) { + const clientPlugins: AstroBuildPlugin[] = []; + const ssrPlugins: AstroBuildPlugin[] = []; + const allPlugins = new Set(); + + return { + options, + internals, + register(plugin: AstroBuildPlugin) { + allPlugins.add(plugin); + switch(plugin.build) { + case 'client': { + clientPlugins.push(plugin); + break; + } + case 'ssr': { + ssrPlugins.push(plugin); + break; + } + case 'both': { + clientPlugins.push(plugin); + ssrPlugins.push(plugin); + break; + } + } + }, + + // Hooks + runBeforeHook(build: 'ssr' | 'client', input: Set) { + let plugins = build === 'ssr' ? ssrPlugins : clientPlugins; + let vitePlugins: Array = []; + let lastVitePlugins: Array = []; + for(const plugin of plugins) { + if(plugin.hooks?.['build:before']) { + let result = plugin.hooks['build:before']({ build, input }); + if(result.vitePlugin) { + vitePlugins.push(result.vitePlugin); + } + } + } + + return { + vitePlugins, + lastVitePlugins + }; + }, + + async runPostHook(ssrReturn: ViteBuildReturn, clientReturn: ViteBuildReturn | null) { + const mutations = new Map(); + const ssrOutputs: RollupOutputArray = []; + const clientOutputs: RollupOutputArray = []; + + if(Array.isArray(ssrReturn)) { + ssrOutputs.push(...ssrReturn); + } else if('output' in ssrReturn) { + ssrOutputs.push(ssrReturn); + } + + if(Array.isArray(clientReturn)) { + clientOutputs.push(...clientReturn); + } else if(clientReturn && 'output' in clientReturn) { + clientOutputs.push(clientReturn); + } + + const mutate: MutateChunk = (chunk, build, newCode) => { + chunk.code = newCode; + mutations.set(chunk.fileName, { + build, + code: newCode, + }); + }; + + for(const plugin of allPlugins) { + const postHook = plugin.hooks?.['build:post']; + if(postHook) { + await postHook({ + ssrOutputs, + clientOutputs, + mutate + }); + } + } + + return mutations; + } + }; +} + +export type AstroBuildPluginContainer = ReturnType; diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts new file mode 100644 index 000000000..f253fe289 --- /dev/null +++ b/packages/astro/src/core/build/plugins/index.ts @@ -0,0 +1,23 @@ +import type { AstroBuildPluginContainer, AstroBuildPlugin } from '../plugin'; +import type { PageBuildData, StaticBuildOptions } from '../types'; +import { pluginAnalyzer } from './plugin-analyzer.js'; +import { pluginInternals } from './plugin-internals.js'; +import { pluginPages } from './plugin-pages.js'; +import { pluginCSS } from './plugin-css.js'; +import { pluginPrerender } from './plugin-prerender.js'; +import { astroConfigBuildPlugin } from '../../../content/vite-plugin-content-assets.js'; +import { pluginSSR } from './plugin-ssr.js'; +import { pluginAliasResolve } from './plugin-alias-resolve.js'; +import { pluginHoistedScripts } from './plugin-hoisted-scripts.js'; + +export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { + register(pluginAliasResolve(internals)); + register(pluginAnalyzer(internals)); + register(pluginInternals(internals)); + register(pluginPages(options, internals)); + register(pluginCSS(options, internals)); + register(pluginPrerender(options, internals)); + register(astroConfigBuildPlugin(internals)); + register(pluginHoistedScripts(options, internals)); + register(pluginSSR(options, internals)); +} diff --git a/packages/astro/src/core/build/vite-plugin-alias-resolve.ts b/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts similarity index 81% rename from packages/astro/src/core/build/vite-plugin-alias-resolve.ts rename to packages/astro/src/core/build/plugins/plugin-alias-resolve.ts index ac37e66cd..1487ca2b9 100644 --- a/packages/astro/src/core/build/vite-plugin-alias-resolve.ts +++ b/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts @@ -1,5 +1,6 @@ import type { Alias, Plugin as VitePlugin } from 'vite'; -import type { BuildInternals } from '../../core/build/internal.js'; +import type { BuildInternals } from '../internal.js'; +import { AstroBuildPlugin } from '../plugin.js'; /** * `@rollup/plugin-alias` doesn't resolve aliases in Rollup input by default. This plugin fixes it @@ -48,3 +49,16 @@ function matches(pattern: string | RegExp, importee: string) { } return importee.startsWith(pattern + '/'); } + +export function pluginAliasResolve(internals: BuildInternals): AstroBuildPlugin { + return { + build: 'client', + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginAliasResolve(internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/vite-plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts similarity index 89% rename from packages/astro/src/core/build/vite-plugin-analyzer.ts rename to packages/astro/src/core/build/plugins/plugin-analyzer.ts index 0e6a991bd..4214cde8e 100644 --- a/packages/astro/src/core/build/vite-plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -1,11 +1,12 @@ import type { PluginContext } from 'rollup'; import type { Plugin as VitePlugin } from 'vite'; -import type { BuildInternals } from '../../core/build/internal.js'; -import type { PluginMetadata as AstroPluginMetadata } from '../../vite-plugin-astro/types'; +import type { BuildInternals } from '../internal.js'; +import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types'; +import type { AstroBuildPlugin } from '../plugin.js'; -import { prependForwardSlash } from '../../core/path.js'; -import { getTopLevelPages } from './graph.js'; -import { getPageDataByViteID, trackClientOnlyPageDatas } from './internal.js'; +import { prependForwardSlash } from '../../path.js'; +import { getTopLevelPages } from '../graph.js'; +import { getPageDataByViteID, trackClientOnlyPageDatas } from '../internal.js'; export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { function hoistedScriptScanner() { @@ -122,3 +123,16 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin { }, }; } + +export function pluginAnalyzer(internals: BuildInternals): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginAnalyzer(internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/vite-plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts similarity index 84% rename from packages/astro/src/core/build/vite-plugin-css.ts rename to packages/astro/src/core/build/plugins/plugin-css.ts index 2a9b5b739..b52b5c0fc 100644 --- a/packages/astro/src/core/build/vite-plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -2,20 +2,22 @@ import * as crypto from 'node:crypto'; import * as npath from 'node:path'; import type { GetModuleInfo } from 'rollup'; import { Plugin as VitePlugin, ResolvedConfig, transformWithEsbuild } from 'vite'; -import { isCSSRequest } from '../render/util.js'; -import type { BuildInternals } from './internal'; -import type { PageBuildData, StaticBuildOptions } from './types'; +import { isCSSRequest } from '../../render/util.js'; +import type { BuildInternals } from '../internal'; +import type { PageBuildData, StaticBuildOptions } from '../types'; +import type { AstroBuildPlugin } from '../plugin'; -import { PROPAGATED_ASSET_FLAG } from '../../content/consts.js'; -import * as assetName from './css-asset-name.js'; -import { moduleIsTopLevelPage, walkParentInfos } from './graph.js'; +import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; +import * as assetName from '../css-asset-name.js'; +import { moduleIsTopLevelPage, walkParentInfos } from '../graph.js'; import { eachPageData, getPageDataByViteID, getPageDatasByClientOnlyID, getPageDatasByHoistedScriptId, isHoistedScript, -} from './internal.js'; +} from '../internal.js'; +import { extendManualChunks } from './util.js'; interface PluginOptions { internals: BuildInternals; @@ -54,40 +56,30 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] name: 'astro:rollup-plugin-build-css', outputOptions(outputOptions) { - const manualChunks = outputOptions.manualChunks || Function.prototype; const assetFileNames = outputOptions.assetFileNames; const namingIncludesHash = assetFileNames?.toString().includes('[hash]'); const createNameForParentPages = namingIncludesHash ? assetName.shortHashedName : assetName.createSlugger(settings); - outputOptions.manualChunks = function (id, ...args) { - // Defer to user-provided `manualChunks`, if it was provided. - if (typeof manualChunks == 'object') { - if (id in manualChunks) { - return manualChunks[id]; - } - } else if (typeof manualChunks === 'function') { - const outid = manualChunks.call(this, id, ...args); - if (outid) { - return outid; - } - } - // For CSS, create a hash of all of the pages that use it. - // This causes CSS to be built into shared chunks when used by multiple pages. - if (isCSSRequest(id)) { - for (const [pageInfo] of walkParentInfos(id, { - getModuleInfo: args[0].getModuleInfo, - })) { - if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) { - // Split delayed assets to separate modules - // so they can be injected where needed - return createNameHash(id, [id]); + extendManualChunks(outputOptions, { + after(id, meta) { + // For CSS, create a hash of all of the pages that use it. + // This causes CSS to be built into shared chunks when used by multiple pages. + if (isCSSRequest(id)) { + for (const [pageInfo] of walkParentInfos(id, { + getModuleInfo: meta.getModuleInfo, + })) { + if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) { + // Split delayed assets to separate modules + // so they can be injected where needed + return createNameHash(id, [id]); + } } + return createNameForParentPages(id, meta); } - return createNameForParentPages(id, args[0]); } - }; + }); }, async generateBundle(_outputOptions, bundle) { @@ -272,3 +264,22 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] }, ]; } + +export function pluginCSS(options: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { + return { + build: 'both', + hooks: { + 'build:before': ({ build }) => { + let plugins = rollupPluginAstroBuildCSS({ + buildOptions: options, + internals, + target: build === 'ssr' ? 'server' : 'client' + }); + + return { + vitePlugin: plugins + }; + } + } + } +} diff --git a/packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts similarity index 80% rename from packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts rename to packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 65d38ec87..9e21177bd 100644 --- a/packages/astro/src/core/build/vite-plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -1,8 +1,10 @@ import type { Plugin as VitePlugin } from 'vite'; -import type { AstroSettings } from '../../@types/astro'; -import type { BuildInternals } from '../../core/build/internal.js'; -import { viteID } from '../util.js'; -import { getPageDataByViteID } from './internal.js'; +import type { AstroSettings } from '../../../@types/astro'; +import type { BuildInternals } from '../internal.js'; +import { viteID } from '../../util.js'; +import { getPageDataByViteID } from '../internal.js'; +import { StaticBuildOptions } from '../types'; +import { AstroBuildPlugin } from '../plugin'; function virtualHoistedEntry(id: string) { return id.startsWith('/astro/hoisted.js?q='); @@ -91,3 +93,16 @@ export function vitePluginHoistedScripts( }, }; } + +export function pluginHoistedScripts(options: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { + return { + build: 'client', + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginHoistedScripts(options.settings, internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/vite-plugin-internals.ts b/packages/astro/src/core/build/plugins/plugin-internals.ts similarity index 83% rename from packages/astro/src/core/build/vite-plugin-internals.ts rename to packages/astro/src/core/build/plugins/plugin-internals.ts index dfd346e77..7177d8bef 100644 --- a/packages/astro/src/core/build/vite-plugin-internals.ts +++ b/packages/astro/src/core/build/plugins/plugin-internals.ts @@ -1,5 +1,6 @@ import type { Plugin as VitePlugin, UserConfig } from 'vite'; -import type { BuildInternals } from './internal.js'; +import type { BuildInternals } from '../internal.js'; +import type { AstroBuildPlugin } from '../plugin'; export function vitePluginInternals(input: Set, internals: BuildInternals): VitePlugin { return { @@ -60,3 +61,16 @@ export function vitePluginInternals(input: Set, internals: BuildInternal }, }; } + +export function pluginInternals(internals: BuildInternals): AstroBuildPlugin { + return { + build: 'both', + hooks: { + 'build:before': ({ input }) => { + return { + vitePlugin: vitePluginInternals(input, internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/vite-plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts similarity index 78% rename from packages/astro/src/core/build/vite-plugin-pages.ts rename to packages/astro/src/core/build/plugins/plugin-pages.ts index 1d661865c..ac436a364 100644 --- a/packages/astro/src/core/build/vite-plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,8 +1,10 @@ import type { Plugin as VitePlugin } from 'vite'; -import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../app/index.js'; -import { addRollupInput } from './add-rollup-input.js'; -import { BuildInternals, eachPageData, hasPrerenderedPages } from './internal.js'; -import type { StaticBuildOptions } from './types'; +import type { StaticBuildOptions } from '../types'; +import type { AstroBuildPlugin } from '../plugin'; + +import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js'; +import { addRollupInput } from '../add-rollup-input.js'; +import { BuildInternals, eachPageData, hasPrerenderedPages } from '../internal.js'; export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { @@ -53,3 +55,16 @@ export const renderers = [${rendererItems}];`; }, }; } + +export function pluginPages(opts: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginPages(opts, internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/plugins/plugin-prerender.ts b/packages/astro/src/core/build/plugins/plugin-prerender.ts new file mode 100644 index 000000000..340e84ea6 --- /dev/null +++ b/packages/astro/src/core/build/plugins/plugin-prerender.ts @@ -0,0 +1,48 @@ +import type { Plugin as VitePlugin } from 'vite'; +import type { BuildInternals } from '../internal.js'; +import type { AstroBuildPlugin } from '../plugin.js'; +import type { StaticBuildOptions } from '../types'; +import { extendManualChunks } from './util.js'; + +export function vitePluginPrerender( + opts: StaticBuildOptions, + internals: BuildInternals +): VitePlugin { + return { + name: 'astro:rollup-plugin-prerender', + + outputOptions(outputOptions) { + extendManualChunks(outputOptions, { + after(id, meta) { + // Split the Astro runtime into a separate chunk for readability + if (id.includes('astro/dist')) { + return 'astro'; + } + const pageInfo = internals.pagesByViteID.get(id); + if (pageInfo) { + // prerendered pages should be split into their own chunk + // Important: this can't be in the `pages/` directory! + if (meta.getModuleInfo(id)?.meta.astro?.pageOptions?.prerender) { + return 'prerender'; + } + // dynamic pages should all go in their own chunk in the pages/* directory + return `pages/all`; + } + } + }); + }, + }; +} + +export function pluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginPrerender(opts, internals) + }; + } + } + }; +} diff --git a/packages/astro/src/core/build/vite-plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts similarity index 79% rename from packages/astro/src/core/build/vite-plugin-ssr.ts rename to packages/astro/src/core/build/plugins/plugin-ssr.ts index 817881c36..289703858 100644 --- a/packages/astro/src/core/build/vite-plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -1,21 +1,22 @@ import type { Plugin as VitePlugin } from 'vite'; -import type { AstroAdapter } from '../../@types/astro'; -import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types'; -import type { BuildInternals } from './internal.js'; -import type { StaticBuildOptions } from './types'; +import type { AstroAdapter } from '../../../@types/astro'; +import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types'; +import type { BuildInternals } from '../internal.js'; +import type { StaticBuildOptions } from '../types'; import glob from 'fast-glob'; import * as fs from 'fs'; import { fileURLToPath } from 'url'; -import { getContentPaths } from '../../content/index.js'; -import { runHookBuildSsr } from '../../integrations/index.js'; -import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; -import { pagesVirtualModuleId } from '../app/index.js'; -import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../path.js'; -import { serializeRouteData } from '../routing/index.js'; -import { addRollupInput } from './add-rollup-input.js'; -import { getOutFile, getOutFolder } from './common.js'; -import { eachPrerenderedPageData, eachServerPageData, sortedCSS } from './internal.js'; +import { getContentPaths } from '../../../content/index.js'; +import { runHookBuildSsr } from '../../../integrations/index.js'; +import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; +import { pagesVirtualModuleId } from '../../app/index.js'; +import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../../path.js'; +import { serializeRouteData } from '../../routing/index.js'; +import { addRollupInput } from '../add-rollup-input.js'; +import { getOutFile, getOutFolder } from '../common.js'; +import { eachPrerenderedPageData, eachServerPageData, sortedCSS } from '../internal.js'; +import { AstroBuildPlugin } from '../plugin'; export const virtualModuleId = '@astrojs-ssr-virtual-entry'; const resolvedVirtualModuleId = '\0' + virtualModuleId; @@ -29,7 +30,7 @@ export function vitePluginSSR(internals: BuildInternals, adapter: AstroAdapter): options(opts) { return addRollupInput(opts, [virtualModuleId]); }, - resolveId(id) { + resolveId(id, parent) { if (id === virtualModuleId) { return resolvedVirtualModuleId; } @@ -114,12 +115,10 @@ export async function injectManifest(buildOpts: StaticBuildOptions, internals: B const chunk = internals.ssrEntryChunk; const code = chunk.code; - chunk.code = code.replace(replaceExp, () => { + + return code.replace(replaceExp, () => { return JSON.stringify(manifest); }); - const serverEntryURL = new URL(buildOpts.buildConfig.serverEntry, buildOpts.buildConfig.server); - await fs.promises.mkdir(new URL('./', serverEntryURL), { recursive: true }); - await fs.promises.writeFile(serverEntryURL, chunk.code, 'utf-8'); } function buildManifest( @@ -220,3 +219,34 @@ function buildManifest( return ssrManifest; } + +export function pluginSSR(options: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { + const ssr = options.settings.config.output === 'server'; + return { + build: 'ssr', + hooks: { + 'build:before': () => { + let vitePlugin = ssr ? vitePluginSSR(internals, options.settings.adapter!) : undefined; + + return { + enforce: 'after-user-plugins', + vitePlugin + } + }, + 'build:post': async ({ mutate }) => { + if(!ssr) { + return; + } + + if(!internals.ssrEntryChunk) { + throw new Error(`Did not generate an entry chunk for SSR`); + } + // Mutate the filename + internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; + + const code = await injectManifest(options, internals); + mutate(internals.ssrEntryChunk, 'server', code); + } + } + }; +} diff --git a/packages/astro/src/core/build/plugins/util.ts b/packages/astro/src/core/build/plugins/util.ts new file mode 100644 index 000000000..6129aa1e1 --- /dev/null +++ b/packages/astro/src/core/build/plugins/util.ts @@ -0,0 +1,40 @@ +import type { Plugin as VitePlugin } from 'vite'; + +// eslint-disable-next-line @typescript-eslint/ban-types +type OutputOptionsHook = Extract; +type OutputOptions = Parameters[0]; + +type ExtendManualChunksHooks = { + before?: (id: string, meta: any) => string | undefined; + after?: (id: string, meta: any) => string | undefined; +} + +export function extendManualChunks(outputOptions: OutputOptions, hooks: ExtendManualChunksHooks) { + const manualChunks = outputOptions.manualChunks; + outputOptions.manualChunks = function(id, meta) { + if(hooks.before) { + let value = hooks.before(id, meta); + if(value) { + return value; + } + } + + // Defer to user-provided `manualChunks`, if it was provided. + if (typeof manualChunks == 'object') { + if (id in manualChunks) { + let value = manualChunks[id]; + return value[0]; + } + } else if (typeof manualChunks === 'function') { + const outid = manualChunks.call(this, id, meta); + if (outid) { + return outid; + } + } + + if(hooks.after) { + return hooks.after(id, meta) || null; + } + return null; + }; +} diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index b2fab0a14..726c1db88 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -4,7 +4,6 @@ import fs from 'fs'; import { bgGreen, bgMagenta, black, dim } from 'kleur/colors'; import { fileURLToPath } from 'url'; import * as vite from 'vite'; -import { astroContentProdBundlePlugin } from '../../content/index.js'; import { BuildInternals, createBuildInternals, @@ -23,14 +22,8 @@ import { generatePages } from './generate.js'; import { trackPageData } from './internal.js'; import type { PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; -import { vitePluginAliasResolve } from './vite-plugin-alias-resolve.js'; -import { vitePluginAnalyzer } from './vite-plugin-analyzer.js'; -import { rollupPluginAstroBuildCSS } from './vite-plugin-css.js'; -import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js'; -import { vitePluginInternals } from './vite-plugin-internals.js'; -import { vitePluginPages } from './vite-plugin-pages.js'; -import { vitePluginPrerender } from './vite-plugin-prerender.js'; -import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js'; +import { AstroBuildPluginContainer, createPluginContainer } from './plugin.js'; +import { registerAllPlugins } from './plugins/index.js'; export async function staticBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -70,10 +63,15 @@ export async function staticBuild(opts: StaticBuildOptions) { // condition, so we are doing it ourselves emptyDir(settings.config.outDir, new Set('.git')); + // Register plugins + const container = createPluginContainer(opts, internals); + registerAllPlugins(container); + + // Build your project (SSR application code, assets, client JS, etc.) timer.ssr = performance.now(); info(opts.logging, 'build', `Building ${settings.config.output} entrypoints...`); - await ssrBuild(opts, internals, pageInput); + const ssrOutput = await ssrBuild(opts, internals, pageInput, container); info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}.`)); const rendererClientEntrypoints = settings.renderers @@ -93,9 +91,11 @@ export async function staticBuild(opts: StaticBuildOptions) { // Run client build first, so the assets can be fed into the SSR rendered version. timer.clientBuild = performance.now(); - await clientBuild(opts, internals, clientInput); + const clientOutput = await clientBuild(opts, internals, clientInput, container); timer.generate = performance.now(); + await runPostBuildHooks(container, ssrOutput, clientOutput); + switch (settings.config.output) { case 'static': { await generatePages(opts, internals); @@ -103,7 +103,6 @@ export async function staticBuild(opts: StaticBuildOptions) { return; } case 'server': { - await injectManifest(opts, internals); await generatePages(opts, internals); await cleanStaticOutput(opts, internals); info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); @@ -113,11 +112,13 @@ export async function staticBuild(opts: StaticBuildOptions) { } } -async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { +async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set, container: AstroBuildPluginContainer) { const { settings, viteConfig } = opts; const ssr = settings.config.output === 'server'; const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(settings.config.outDir); + const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input); + const viteBuildConfig: vite.InlineConfig = { ...viteConfig, mode: viteConfig.mode || 'production', @@ -155,19 +156,9 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp reportCompressedSize: false, }, plugins: [ - vitePluginAnalyzer(internals), - vitePluginInternals(input, internals), - vitePluginPages(opts, internals), - rollupPluginAstroBuildCSS({ - buildOptions: opts, - internals, - target: 'server', - }), - vitePluginPrerender(opts, internals), + ...vitePlugins, ...(viteConfig.plugins || []), - astroContentProdBundlePlugin({ internals }), - // SSR needs to be last - ssr && vitePluginSSR(internals, settings.adapter!), + ...lastVitePlugins ], envPrefix: viteConfig.envPrefix ?? 'PUBLIC_', base: settings.config.base, @@ -187,7 +178,8 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp async function clientBuild( opts: StaticBuildOptions, internals: BuildInternals, - input: Set + input: Set, + container: AstroBuildPluginContainer ) { const { settings, viteConfig } = opts; const timer = performance.now(); @@ -204,8 +196,10 @@ async function clientBuild( return null; } + const { lastVitePlugins, vitePlugins } = container.runBeforeHook('client', input); info(opts.logging, null, `\n${bgGreen(black(' building client '))}`); + const viteBuildConfig: vite.InlineConfig = { ...viteConfig, mode: viteConfig.mode || 'production', @@ -229,15 +223,9 @@ async function clientBuild( }, }, plugins: [ - vitePluginAliasResolve(internals), - vitePluginInternals(input, internals), - vitePluginHoistedScripts(settings, internals), - rollupPluginAstroBuildCSS({ - buildOptions: opts, - internals, - target: 'client', - }), + ...vitePlugins, ...(viteConfig.plugins || []), + ...lastVitePlugins, ], envPrefix: viteConfig.envPrefix ?? 'PUBLIC_', base: settings.config.base, @@ -256,6 +244,21 @@ async function clientBuild( return buildResult; } +async function runPostBuildHooks( + container: AstroBuildPluginContainer, + ssrReturn: Awaited>, + clientReturn: Awaited> +) { + const mutations = await container.runPostHook(ssrReturn, clientReturn); + const buildConfig = container.options.settings.config.build; + for(const [fileName, mutation] of mutations) { + const root = mutation.build === 'server' ? buildConfig.server : buildConfig.client; + const fileURL = new URL(fileName, root); + await fs.promises.mkdir(new URL('./', fileURL), { recursive: true }); + await fs.promises.writeFile(fileURL, mutation.code, 'utf-8'); + } +} + /** * For each statically prerendered page, replace their SSR file with a noop. * This allows us to run the SSR build only once, but still remove dependencies for statically rendered routes. diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 0c4d9a069..a9ca08a0e 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -11,6 +11,7 @@ import type { } from '../../@types/astro'; import type { LogOptions } from '../logger/core'; import type { RouteCache } from '../render/route-cache'; +import type { default as vite } from 'vite'; export type ComponentPath = string; export type ViteID = string; @@ -44,3 +45,7 @@ export interface SingleFileBuiltModule { pageMap: Map; renderers: SSRLoadedRenderer[]; } + +export type ViteBuildReturn = Awaited>; +export type RollupOutput = Extract>>, { output: any }>; +export type OutputChunk = Extract; diff --git a/packages/astro/src/core/build/vite-plugin-prerender.ts b/packages/astro/src/core/build/vite-plugin-prerender.ts deleted file mode 100644 index fdc505378..000000000 --- a/packages/astro/src/core/build/vite-plugin-prerender.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Plugin as VitePlugin } from 'vite'; -import type { BuildInternals } from './internal.js'; -import type { StaticBuildOptions } from './types'; - -export function vitePluginPrerender( - opts: StaticBuildOptions, - internals: BuildInternals -): VitePlugin { - return { - name: 'astro:rollup-plugin-prerender', - - outputOptions(outputOptions) { - const manualChunks = outputOptions.manualChunks || Function.prototype; - outputOptions.manualChunks = function (id, api, ...args) { - // Defer to user-provided `manualChunks`, if it was provided. - if (typeof manualChunks == 'object') { - if (id in manualChunks) { - return manualChunks[id]; - } - } else if (typeof manualChunks === 'function') { - const outid = manualChunks.call(this, id, api, ...args); - if (outid) { - return outid; - } - } - // Split the Astro runtime into a separate chunk for readability - if (id.includes('astro/dist')) { - return 'astro'; - } - const pageInfo = internals.pagesByViteID.get(id); - if (pageInfo) { - // prerendered pages should be split into their own chunk - // Important: this can't be in the `pages/` directory! - if (api.getModuleInfo(id)?.meta.astro?.pageOptions?.prerender) { - return `prerender`; - } - // dynamic pages should all go in their own chunk in the pages/* directory - return `pages/all`; - } - }; - }, - }; -}