From 6782a8611a64b52dbf7d2f541c663185ceb2038e Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 2 Oct 2023 13:09:18 -0500 Subject: [PATCH] wip: content collection refactor --- packages/astro/content-module.template.mjs | 12 +- packages/astro/src/content/runtime.ts | 2 +- .../src/content/vite-plugin-content-assets.ts | 8 +- .../vite-plugin-content-virtual-mod.ts | 137 +++++++++++------- packages/astro/src/core/build/plugin.ts | 53 +++---- .../astro/src/core/build/plugins/index.ts | 4 + .../build/plugins/plugin-alias-resolve.ts | 2 +- .../src/core/build/plugins/plugin-analyzer.ts | 2 +- .../build/plugins/plugin-component-entry.ts | 2 +- .../src/core/build/plugins/plugin-content.ts | 70 +++++++++ .../src/core/build/plugins/plugin-css.ts | 10 +- .../src/core/build/plugins/plugin-external.ts | 42 ++++++ .../build/plugins/plugin-hoisted-scripts.ts | 2 +- .../core/build/plugins/plugin-internals.ts | 2 +- .../src/core/build/plugins/plugin-manifest.ts | 4 +- .../core/build/plugins/plugin-middleware.ts | 2 +- .../src/core/build/plugins/plugin-pages.ts | 2 +- .../core/build/plugins/plugin-prerender.ts | 2 +- .../core/build/plugins/plugin-renderers.ts | 3 +- .../src/core/build/plugins/plugin-ssr.ts | 4 +- packages/astro/src/core/build/static-build.ts | 120 +++++++++++++-- packages/astro/src/vite-plugin-head/index.ts | 2 +- scripts/cmd/copy.js | 2 +- 23 files changed, 359 insertions(+), 130 deletions(-) create mode 100644 packages/astro/src/core/build/plugins/plugin-content.ts create mode 100644 packages/astro/src/core/build/plugins/plugin-external.ts diff --git a/packages/astro/content-module.template.mjs b/packages/astro/content-module.template.mjs index 9ce06960f..b8328588b 100644 --- a/packages/astro/content-module.template.mjs +++ b/packages/astro/content-module.template.mjs @@ -13,17 +13,13 @@ export { z } from 'astro/zod'; const contentDir = '@@CONTENT_DIR@@'; -const contentEntryGlob = import.meta.glob('@@CONTENT_ENTRY_GLOB_PATH@@', { - query: { astroContentCollectionEntry: true }, -}); +const contentEntryGlob = '@@CONTENT_ENTRY_GLOB_PATH@@'; const contentCollectionToEntryMap = createCollectionToGlobResultMap({ globResult: contentEntryGlob, contentDir, }); -const dataEntryGlob = import.meta.glob('@@DATA_ENTRY_GLOB_PATH@@', { - query: { astroDataCollectionEntry: true }, -}); +const dataEntryGlob = '@@DATA_ENTRY_GLOB_PATH@@'; const dataCollectionToEntryMap = createCollectionToGlobResultMap({ globResult: dataEntryGlob, contentDir, @@ -45,9 +41,7 @@ function createGlobLookup(glob) { }; } -const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', { - query: { astroRenderContent: true }, -}); +const renderEntryGlob = '@@RENDER_ENTRY_GLOB_PATH@@' const collectionToRenderEntryMap = createCollectionToGlobResultMap({ globResult: renderEntryGlob, contentDir, diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index e507149fe..4a1e46bf3 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -69,7 +69,7 @@ export function createGetCollection({ let entries: any[] = []; // Cache `getCollection()` calls in production only // prevents stale cache in development - if (import.meta.env.PROD && cacheEntriesByCollection.has(collection)) { + if (!import.meta.env?.DEV && cacheEntriesByCollection.has(collection)) { // Always return a new instance so consumers can safely mutate it entries = [...cacheEntriesByCollection.get(collection)!]; } else { diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 5d82a684f..bf5129d9e 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -112,14 +112,14 @@ export function astroConfigBuildPlugin( ): AstroBuildPlugin { let ssrPluginContext: any = undefined; return { - build: 'ssr', + targets: ['server', 'content'], hooks: { - 'build:before': ({ build }) => { + 'build:before': ({ target }) => { return { vitePlugin: { name: 'astro:content-build-plugin', generateBundle() { - if (build === 'ssr') { + if (target === 'server') { ssrPluginContext = this; } }, @@ -206,7 +206,7 @@ export function astroConfigBuildPlugin( ) ); } - mutate(chunk, 'server', newCode); + mutate(chunk, ['server'], newCode); } } }, diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index d7924973c..095f88f9f 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -4,9 +4,9 @@ import { extname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import pLimit from 'p-limit'; import type { Plugin } from 'vite'; -import type { AstroSettings, ContentEntryType } from '../@types/astro.js'; +import type { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; -import { appendForwardSlash } from '../core/path.js'; +import { appendForwardSlash, joinPaths, removeFileExtension, removeLeadingForwardSlash, slash } from '../core/path.js'; import { rootRelativePath } from '../core/util.js'; import { VIRTUAL_MODULE_ID } from './consts.js'; import { @@ -20,7 +20,6 @@ import { getEntryType, getExtGlob, type ContentLookupMap, - type ContentPaths, } from './utils.js'; interface AstroContentVirtualModPluginParams { @@ -30,38 +29,6 @@ interface AstroContentVirtualModPluginParams { export function astroContentVirtualModPlugin({ settings, }: AstroContentVirtualModPluginParams): Plugin { - const contentPaths = getContentPaths(settings.config); - const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir); - - const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); - const contentEntryExts = [...contentEntryConfigByExt.keys()]; - const dataEntryExts = getDataEntryExts(settings); - - const virtualModContents = fsMod - .readFileSync(contentPaths.virtualModTemplate, 'utf-8') - .replace( - '@@COLLECTION_NAME_BY_REFERENCE_KEY@@', - new URL('reference-map.json', contentPaths.cacheDir).pathname - ) - .replace('@@CONTENT_DIR@@', relContentDir) - .replace( - "'@@CONTENT_ENTRY_GLOB_PATH@@'", - JSON.stringify(globWithUnderscoresIgnored(relContentDir, contentEntryExts)) - ) - .replace( - "'@@DATA_ENTRY_GLOB_PATH@@'", - JSON.stringify(globWithUnderscoresIgnored(relContentDir, dataEntryExts)) - ) - .replace( - "'@@RENDER_ENTRY_GLOB_PATH@@'", - JSON.stringify( - globWithUnderscoresIgnored( - relContentDir, - /** Note: data collections excluded */ contentEntryExts - ) - ) - ); - const astroContentVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; return { @@ -73,45 +40,80 @@ export function astroContentVirtualModPlugin({ }, async load(id) { if (id === astroContentVirtualModuleId) { - const stringifiedLookupMap = await getStringifiedLookupMap({ + const lookupMap = await generateLookupMap({ + settings, fs: fsMod, - contentPaths, - contentEntryConfigByExt, - dataEntryExts, - root: settings.config.root, }); + const code = await generateContentEntryFile({ settings, fs: fsMod, lookupMap }); return { - code: virtualModContents.replace( - '/* @@LOOKUP_MAP_ASSIGNMENT@@ */', - `lookupMap = ${stringifiedLookupMap};` - ), + code }; } }, }; } +export async function generateContentEntryFile({ + settings, + fs, + lookupMap, +}: { + settings: AstroSettings; + fs: typeof fsMod; + lookupMap: ContentLookupMap +}) { + const contentPaths = getContentPaths(settings.config); + const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir); + + const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); + const contentEntryExts = [...contentEntryConfigByExt.keys()]; + const dataEntryExts = getDataEntryExts(settings); + + const [contentEntryGlobResult, dataEntryGlobResult, renderEntryGlobResult] = await Promise.all([contentEntryExts, dataEntryExts, contentEntryExts].map((exts, i) => getStringifiedGlobResult(settings, exts, i === 2 ? '.render.mjs' : undefined))); + + const virtualModContents = fs + .readFileSync(contentPaths.virtualModTemplate, 'utf-8') + .replace('@@CONTENT_DIR@@', relContentDir) + .replace( + "'@@CONTENT_ENTRY_GLOB_PATH@@'", + contentEntryGlobResult + ) + .replace( + "'@@DATA_ENTRY_GLOB_PATH@@'", + dataEntryGlobResult + ) + .replace( + "'@@RENDER_ENTRY_GLOB_PATH@@'", + renderEntryGlobResult + ).replace( + '/* @@LOOKUP_MAP_ASSIGNMENT@@ */', + `lookupMap = ${JSON.stringify(lookupMap)};` + ); + + return virtualModContents; +} + /** * Generate a map from a collection + slug to the local file path. * This is used internally to resolve entry imports when using `getEntry()`. * @see `content-module.template.mjs` */ -export async function getStringifiedLookupMap({ - contentPaths, - contentEntryConfigByExt, - dataEntryExts, - root, +export async function generateLookupMap({ + settings, fs, }: { - contentEntryConfigByExt: Map; - dataEntryExts: string[]; - contentPaths: Pick; - root: URL; + settings: AstroSettings; fs: typeof fsMod; }) { + const { root } = settings.config; + const contentPaths = getContentPaths(settings.config); + const relContentDir = rootRelativePath(root, contentPaths.contentDir, false); + + const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); + const dataEntryExts = getDataEntryExts(settings); + const { contentDir } = contentPaths; - const relContentDir = rootRelativePath(root, contentDir, false); const contentEntryExts = [...contentEntryConfigByExt.keys()]; let lookupMap: ContentLookupMap = {}; @@ -200,7 +202,7 @@ export async function getStringifiedLookupMap({ await Promise.all(promises); - return JSON.stringify(lookupMap); + return lookupMap; } const UnexpectedLookupMapError = new AstroError({ @@ -208,6 +210,31 @@ const UnexpectedLookupMapError = new AstroError({ message: `Unexpected error while parsing content entry IDs and slugs.`, }); +async function getStringifiedGlobResult(settings: AstroSettings, exts: string[], importExtension = '.mjs'): Promise { + const pattern = globWithUnderscoresIgnored('./', exts); + const contentPaths = getContentPaths(settings.config); + + const files = await glob(pattern, { + cwd: fileURLToPath(contentPaths.contentDir), + fs: { + readdir: fsMod.readdir.bind(fsMod), + readdirSync: fsMod.readdirSync.bind(fsMod), + }, + onlyFiles: true, + objectMode: true, + }) + + let str = '{'; + for (const file of files) { + const importSpecifier = `./${removeFileExtension(removeLeadingForwardSlash(slash(file.path)))}${importExtension}`; + const srcRelativePath = new URL(`./${slash(file.path)}`, contentPaths.contentDir).toString().replace(settings.config.root.toString(), '/') + str += `\n "${srcRelativePath}": () => import("${importSpecifier}"),` + } + str += '\n}' + + return str; +} + function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[] { const extGlob = getExtGlob(exts); const contentDir = appendForwardSlash(relContentDir); diff --git a/packages/astro/src/core/build/plugin.ts b/packages/astro/src/core/build/plugin.ts index c5da47457..54901da1e 100644 --- a/packages/astro/src/core/build/plugin.ts +++ b/packages/astro/src/core/build/plugin.ts @@ -5,16 +5,19 @@ import type { StaticBuildOptions, ViteBuildReturn } from './types.js'; type RollupOutputArray = Extract>; type OutputChunkorAsset = RollupOutputArray[number]['output'][number]; type OutputChunk = Extract; +export type BuildTarget = 'server' | 'client' | 'content'; -type MutateChunk = (chunk: OutputChunk, build: 'server' | 'client', newCode: string) => void; +type MutateChunk = (chunk: OutputChunk, targets: BuildTarget[], newCode: string) => void; + +export interface BuildBeforeHookResult { + enforce?: 'after-user-plugins'; + vitePlugin: VitePlugin | VitePlugin[] | undefined; +} export type AstroBuildPlugin = { - build: 'ssr' | 'client' | 'both'; + targets: BuildTarget[]; hooks?: { - 'build:before'?: (opts: { build: 'ssr' | 'client'; input: Set }) => { - enforce?: 'after-user-plugins'; - vitePlugin: VitePlugin | VitePlugin[] | undefined; - }; + 'build:before'?: (opts: { target: BuildTarget; input: Set }) => BuildBeforeHookResult | Promise; 'build:post'?: (opts: { ssrOutputs: RollupOutputArray; clientOutputs: RollupOutputArray; @@ -24,40 +27,32 @@ export type AstroBuildPlugin = { }; export function createPluginContainer(options: StaticBuildOptions, internals: BuildInternals) { - const clientPlugins: AstroBuildPlugin[] = []; - const ssrPlugins: AstroBuildPlugin[] = []; + const plugins = new Map(); const allPlugins = new Set(); + for (const target of ['client', 'server', 'content'] satisfies BuildTarget[]) { + plugins.set(target, []); + } 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; - } + for (const target of plugin.targets) { + const targetPlugins = plugins.get(target) ?? []; + targetPlugins.push(plugin); + plugins.set(target, targetPlugins); } }, // Hooks - runBeforeHook(build: 'ssr' | 'client', input: Set) { - let plugins = build === 'ssr' ? ssrPlugins : clientPlugins; + async runBeforeHook(target: BuildTarget, input: Set) { + let targetPlugins = plugins.get(target) ?? []; let vitePlugins: Array = []; let lastVitePlugins: Array = []; - for (const plugin of plugins) { + for (const plugin of targetPlugins) { if (plugin.hooks?.['build:before']) { - let result = plugin.hooks['build:before']({ build, input }); + let result = await plugin.hooks['build:before']({ target, input }); if (result.vitePlugin) { vitePlugins.push(result.vitePlugin); } @@ -74,7 +69,7 @@ export function createPluginContainer(options: StaticBuildOptions, internals: Bu const mutations = new Map< string, { - build: 'server' | 'client'; + targets: BuildTarget[]; code: string; } >(); @@ -93,10 +88,10 @@ export function createPluginContainer(options: StaticBuildOptions, internals: Bu clientOutputs.push(clientReturn); } - const mutate: MutateChunk = (chunk, build, newCode) => { + const mutate: MutateChunk = (chunk, targets, newCode) => { chunk.code = newCode; mutations.set(chunk.fileName, { - build, + targets, code: newCode, }); }; diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index 90620cb28..b8b5f8982 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -4,7 +4,9 @@ import type { AstroBuildPluginContainer } from '../plugin.js'; import { pluginAliasResolve } from './plugin-alias-resolve.js'; import { pluginAnalyzer } from './plugin-analyzer.js'; import { pluginComponentEntry } from './plugin-component-entry.js'; +import { pluginContent } from './plugin-content.js'; import { pluginCSS } from './plugin-css.js'; +import { pluginExternalize } from './plugin-external.js'; import { pluginHoistedScripts } from './plugin-hoisted-scripts.js'; import { pluginInternals } from './plugin-internals.js'; import { pluginManifest } from './plugin-manifest.js'; @@ -23,6 +25,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginRenderers(options)); register(pluginMiddleware(options, internals)); register(pluginPages(options, internals)); + register(pluginContent(options, internals)); register(pluginCSS(options, internals)); register(astroHeadBuildPlugin(internals)); register(pluginPrerender(options, internals)); @@ -30,4 +33,5 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginHoistedScripts(options, internals)); register(pluginSSR(options, internals)); register(pluginSSRSplit(options, internals)); + register(pluginExternalize()); } diff --git a/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts b/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts index 052ea45b7..6fb09acf8 100644 --- a/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts +++ b/packages/astro/src/core/build/plugins/plugin-alias-resolve.ts @@ -52,7 +52,7 @@ function matches(pattern: string | RegExp, importee: string) { export function pluginAliasResolve(internals: BuildInternals): AstroBuildPlugin { return { - build: 'client', + targets: ['client'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index b81932dce..b99624a86 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -330,7 +330,7 @@ export function pluginAnalyzer( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-component-entry.ts b/packages/astro/src/core/build/plugins/plugin-component-entry.ts index 01e480e2f..bfa2ce58c 100644 --- a/packages/astro/src/core/build/plugins/plugin-component-entry.ts +++ b/packages/astro/src/core/build/plugins/plugin-component-entry.ts @@ -77,7 +77,7 @@ export function normalizeEntryId(id: string): string { export function pluginComponentEntry(internals: BuildInternals): AstroBuildPlugin { return { - build: 'client', + targets: ['client'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts new file mode 100644 index 000000000..cfcac7f1c --- /dev/null +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -0,0 +1,70 @@ +import type { Plugin as VitePlugin } from 'vite'; +import fsMod from 'node:fs'; +import { addRollupInput } from '../add-rollup-input.js'; +import { type BuildInternals } from '../internal.js'; +import type { AstroBuildPlugin } from '../plugin.js'; +import type { StaticBuildOptions } from '../types.js'; +import { generateContentEntryFile, generateLookupMap } from '../../../content/vite-plugin-content-virtual-mod.js'; +import { joinPaths } from '../../path.js'; +import { fileURLToPath } from 'node:url'; +import type { ContentLookupMap } from '../../../content/utils.js'; +import { CONTENT_RENDER_FLAG } from '../../../content/consts.js'; + +function vitePluginContent(opts: StaticBuildOptions, lookupMap: ContentLookupMap): VitePlugin { + return { + name: '@astro/plugin-build-content', + + options(options) { + let newOptions = Object.assign({}, options); + if (opts.settings.config.output === 'static') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_collectionName, { type, entries }] of Object.entries(lookupMap)) { + let newInputs = Object.values(entries).flatMap(entry => { + const input = fileURLToPath(joinPaths(opts.settings.config.root.toString(), entry)); + const inputs = [`${input}?${collectionTypeToFlag(type)}`]; + if (type === 'content') { + inputs.push(`${input}?${CONTENT_RENDER_FLAG}`) + } + return inputs; + }) + newOptions = addRollupInput(newOptions, newInputs) + } + } + return newOptions; + }, + + resolveId(id) { + console.log(id); + }, + + async generateBundle() { + const content = await generateContentEntryFile({ settings: opts.settings, fs: fsMod, lookupMap }); + this.emitFile({ + type: 'prebuilt-chunk', + code: content, + fileName: 'content/index.mjs' + }) + } + }; +} + +function collectionTypeToFlag(type: 'content' | 'data') { + const name = type[0].toUpperCase() + type.slice(1); + return `astro${name}CollectionEntry` +} + +export function pluginContent(opts: StaticBuildOptions, _internals: BuildInternals): AstroBuildPlugin { + return { + targets: ['content'], + hooks: { + async 'build:before'() { + // TODO: filter lookupMap based on file hashes + const lookupMap = await generateLookupMap({ settings: opts.settings, fs: fsMod }); + + return { + vitePlugin: vitePluginContent(opts, lookupMap), + }; + }, + }, + }; +} diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index d85dc8e56..4e1c4ccc1 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -4,7 +4,7 @@ import type { GetModuleInfo } from 'rollup'; import { type ResolvedConfig, type Plugin as VitePlugin } from 'vite'; import { isBuildableCSSRequest } from '../../../vite-plugin-astro-server/util.js'; import type { BuildInternals } from '../internal.js'; -import type { AstroBuildPlugin } from '../plugin.js'; +import type { AstroBuildPlugin, BuildTarget } from '../plugin.js'; import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js'; import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js'; @@ -22,7 +22,7 @@ import { extendManualChunks } from './util.js'; interface PluginOptions { internals: BuildInternals; buildOptions: StaticBuildOptions; - target: 'client' | 'server'; + target: BuildTarget; } /***** ASTRO PLUGIN *****/ @@ -32,13 +32,13 @@ export function pluginCSS( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'both', + targets: ['client', 'server'], hooks: { - 'build:before': ({ build }) => { + 'build:before': ({ target }) => { let plugins = rollupPluginAstroBuildCSS({ buildOptions: options, internals, - target: build === 'ssr' ? 'server' : 'client', + target, }); return { diff --git a/packages/astro/src/core/build/plugins/plugin-external.ts b/packages/astro/src/core/build/plugins/plugin-external.ts new file mode 100644 index 000000000..185f6561b --- /dev/null +++ b/packages/astro/src/core/build/plugins/plugin-external.ts @@ -0,0 +1,42 @@ +import type { Plugin as VitePlugin } from 'vite'; +import type { AstroBuildPlugin } from '../plugin.js'; +import { slash, removeLeadingForwardSlash } from '../../path.js'; + +export function vitePluginExternalize(): VitePlugin { + const MODULE_ID = `astro:content`; + const VIRTUAL_MODULE_ID = `\0${MODULE_ID}`; + + return { + name: '@astro/plugin-externalize', + enforce: 'pre', + resolveId(id) { + // Ensure that `astro:content` is treated as external and rewritten to the final entrypoint + if (id === MODULE_ID) { + return { id: VIRTUAL_MODULE_ID, external: true } + } + return null; + }, + renderChunk(code, chunk) { + if (chunk.imports.find(name => name === VIRTUAL_MODULE_ID)) { + // We want to generate a relative path and avoid a hardcoded absolute path in the output! + const steps = removeLeadingForwardSlash(slash(chunk.fileName)).split('/').length - 1; + const prefix = '../'.repeat(steps) || './'; + // dist/content/index.mjs is generated by the "content" build + return code.replace(VIRTUAL_MODULE_ID, `${prefix}content/index.mjs`); + } + } + }; +} + +export function pluginExternalize(): AstroBuildPlugin { + return { + targets: ['server'], + hooks: { + 'build:before': () => { + return { + vitePlugin: vitePluginExternalize(), + }; + }, + }, + }; +} diff --git a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts index 5c6b40992..1c02f7adc 100644 --- a/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts +++ b/packages/astro/src/core/build/plugins/plugin-hoisted-scripts.ts @@ -108,7 +108,7 @@ export function pluginHoistedScripts( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'client', + targets: ['client'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-internals.ts b/packages/astro/src/core/build/plugins/plugin-internals.ts index 03e6dfb37..c00b1a4ae 100644 --- a/packages/astro/src/core/build/plugins/plugin-internals.ts +++ b/packages/astro/src/core/build/plugins/plugin-internals.ts @@ -62,7 +62,7 @@ export function vitePluginInternals(input: Set, internals: BuildInternal export function pluginInternals(internals: BuildInternals): AstroBuildPlugin { return { - build: 'both', + targets: ['client', 'server'], hooks: { 'build:before': ({ input }) => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 41ceb282c..469b0377a 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -79,7 +79,7 @@ export function pluginManifest( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { return { @@ -107,7 +107,7 @@ export function pluginManifest( : undefined, }); const code = injectManifest(manifest, internals.manifestEntryChunk); - mutate(internals.manifestEntryChunk, 'server', code); + mutate(internals.manifestEntryChunk, ['server'], code); }, }, }; diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts index 22d3f795b..d01faf365 100644 --- a/packages/astro/src/core/build/plugins/plugin-middleware.ts +++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts @@ -71,7 +71,7 @@ export function pluginMiddleware( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 00401285f..4b47c681c 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -104,7 +104,7 @@ export function shouldBundleMiddleware(settings: AstroSettings) { export function pluginPages(opts: StaticBuildOptions, internals: BuildInternals): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-prerender.ts b/packages/astro/src/core/build/plugins/plugin-prerender.ts index 35e4813fc..6a3b06e8f 100644 --- a/packages/astro/src/core/build/plugins/plugin-prerender.ts +++ b/packages/astro/src/core/build/plugins/plugin-prerender.ts @@ -40,7 +40,7 @@ export function pluginPrerender( internals: BuildInternals ): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-renderers.ts b/packages/astro/src/core/build/plugins/plugin-renderers.ts index 6cb45dc59..006cd71bd 100644 --- a/packages/astro/src/core/build/plugins/plugin-renderers.ts +++ b/packages/astro/src/core/build/plugins/plugin-renderers.ts @@ -48,7 +48,8 @@ export function vitePluginRenderers(opts: StaticBuildOptions): VitePlugin { export function pluginRenderers(opts: StaticBuildOptions): AstroBuildPlugin { return { - build: 'ssr', + // TODO: handle for content? + targets: ['server'], hooks: { 'build:before': () => { return { diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 1887351b1..81caaa7eb 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -97,7 +97,7 @@ export function pluginSSR( const ssr = isServerLikeOutput(options.settings.config); const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter); return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { let vitePlugin = @@ -218,7 +218,7 @@ export function pluginSSRSplit( const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter); return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before': () => { let vitePlugin = diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 149b6760d..31099fd26 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -4,7 +4,7 @@ import glob from 'fast-glob'; import { bgGreen, bgMagenta, black, dim } from 'kleur/colors'; import fs from 'node:fs'; import path, { extname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import * as vite from 'vite'; import type { RouteData } from '../../@types/astro.js'; import { @@ -13,7 +13,7 @@ import { type BuildInternals, } from '../../core/build/internal.js'; import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js'; -import { appendForwardSlash, prependForwardSlash } from '../../core/path.js'; +import { appendForwardSlash, prependForwardSlash, removeFileExtension } from '../../core/path.js'; import { isModeServerWithNoAdapter } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/index.js'; import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js'; @@ -32,6 +32,8 @@ import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plug import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { PageBuildData, StaticBuildOptions } from './types.js'; import { getTimeStat } from './util.js'; +import { hasContentFlag } from '../../content/utils.js'; +import { CONTENT_FLAGS, CONTENT_RENDER_FLAG, PROPAGATED_ASSET_FLAG } from '../../content/consts.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -77,13 +79,26 @@ export async function viteBuild(opts: StaticBuildOptions) { const container = createPluginContainer(opts, internals); registerAllPlugins(container); - // Build your project (SSR application code, assets, client JS, etc.) - const ssrTime = performance.now(); - opts.logger.info('build', `Building ${settings.config.output} entrypoints...`); - const ssrOutput = await ssrBuild(opts, internals, pageInput, container); - opts.logger.info('build', dim(`Completed in ${getTimeStat(ssrTime, performance.now())}.`)); + let buildContent = async () => { + const contentTime = performance.now(); + opts.logger.info('content', `Building collections...`); + await contentBuild(opts, internals, new Set(), container); + opts.logger.info('content', dim(`Completed in ${getTimeStat(contentTime, performance.now())}.`)); + } + + let ssrOutput: any; + let buildServer = async () => { + // Build your project (SSR application code, assets, client JS, etc.) + const ssrTime = performance.now(); + opts.logger.info('build', `Building ${settings.config.output} entrypoints...`); + ssrOutput = await ssrBuild(opts, internals, pageInput, container); + opts.logger.info('build', dim(`Completed in ${getTimeStat(ssrTime, performance.now())}.`)); + + settings.timer.end('SSR build'); + } + + await Promise.all([buildContent(), buildServer()]); - settings.timer.end('SSR build'); settings.timer.start('Client build'); const rendererClientEntrypoints = settings.renderers @@ -149,7 +164,7 @@ async function ssrBuild( const ssr = isServerLikeOutput(settings.config); const out = getOutputDirectory(settings.config); const routes = Object.values(allPages).map((pd) => pd.route); - const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input); + const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('server', input); const viteBuildConfig: vite.InlineConfig = { ...viteConfig, @@ -234,6 +249,87 @@ async function ssrBuild( return await vite.build(updatedViteBuildConfig); } +async function contentBuild( + opts: StaticBuildOptions, + internals: BuildInternals, + input: Set, + container: AstroBuildPluginContainer +) { + const { settings, viteConfig } = opts; + const ssr = isServerLikeOutput(settings.config); + const out = getOutputDirectory(settings.config); + const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('content', input); + + const viteBuildConfig: vite.InlineConfig = { + ...viteConfig, + mode: viteConfig.mode || 'production', + // Check using `settings...` as `viteConfig` always defaults to `warn` by Astro + logLevel: settings.config.vite.logLevel ?? 'error', + build: { + target: 'esnext', + // Vite defaults cssMinify to false in SSR by default, but we want to minify it + // as the CSS generated are used and served to the client. + cssMinify: viteConfig.build?.minify == null ? true : !!viteConfig.build?.minify, + ...viteConfig.build, + emptyOutDir: false, + manifest: false, + outDir: fileURLToPath(out), + copyPublicDir: !ssr, + rollupOptions: { + ...viteConfig.build?.rollupOptions, + input: [], + output: { + format: 'esm', + chunkFileNames(info) { + if (info.moduleIds.length === 1) { + const url = pathToFileURL(info.moduleIds[0]); + const distRelative = url.toString().replace(settings.config.srcDir.toString(), '') + let entryFileName = removeFileExtension(distRelative); + return `${entryFileName}.render.mjs`; + } + return '[name]_[hash].mjs'; + }, + ...viteConfig.build?.rollupOptions?.output, + entryFileNames(info) { + const params = new URLSearchParams(info.moduleIds[0].split('?').pop() ?? ''); + const flags = Array.from(params.keys()); + const url = pathToFileURL(info.moduleIds[0]); + const distRelative = url.toString().replace(settings.config.srcDir.toString(), '') + + let entryFileName = removeFileExtension(distRelative); + if (flags[0] === PROPAGATED_ASSET_FLAG) { + entryFileName += `.assets` + } else if (flags[0] === CONTENT_RENDER_FLAG) { + entryFileName += '.render' + } + return `${entryFileName}.mjs`; + }, + assetFileNames: `${settings.config.build.assets}/[name].[extname]`, + }, + }, + ssr: true, + ssrEmitAssets: true, + // improve build performance + minify: false, + modulePreload: { polyfill: false }, + reportCompressedSize: false, + }, + plugins: [...vitePlugins, ...(viteConfig.plugins || []), ...lastVitePlugins], + envPrefix: viteConfig.envPrefix ?? 'PUBLIC_', + base: settings.config.base, + }; + + const updatedViteBuildConfig = await runHookBuildSetup({ + config: settings.config, + pages: internals.pagesByComponent, + vite: viteBuildConfig, + target: 'server', + logger: opts.logger, + }); + + return await vite.build(updatedViteBuildConfig); +} + async function clientBuild( opts: StaticBuildOptions, internals: BuildInternals, @@ -255,7 +351,7 @@ async function clientBuild( return null; } - const { lastVitePlugins, vitePlugins } = container.runBeforeHook('client', input); + const { lastVitePlugins, vitePlugins } = await container.runBeforeHook('client', input); opts.logger.info(null, `\n${bgGreen(black(' building client '))}`); const viteBuildConfig: vite.InlineConfig = { @@ -309,7 +405,7 @@ async function runPostBuildHooks( const build = container.options.settings.config.build; for (const [fileName, mutation] of mutations) { const root = isServerLikeOutput(config) - ? mutation.build === 'server' + ? mutation.targets.includes('server') ? build.server : build.client : config.outDir; @@ -412,7 +508,7 @@ async function copyFiles(fromFolder: URL, toFolder: URL, includeDotfiles = false const lastFolder = new URL('./', to); return fs.promises .mkdir(lastFolder, { recursive: true }) - .then(() => fs.promises.copyFile(from, to)); + .then(() => fs.promises.copyFile(from, to, fs.constants.COPYFILE_FICLONE)); }) ); } diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index 58722fdb2..228e4e437 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -103,7 +103,7 @@ export default function configHeadVitePlugin(): vite.Plugin { export function astroHeadBuildPlugin(internals: BuildInternals): AstroBuildPlugin { return { - build: 'ssr', + targets: ['server'], hooks: { 'build:before'() { return { diff --git a/scripts/cmd/copy.js b/scripts/cmd/copy.js index 1e64a793d..377a60a39 100644 --- a/scripts/cmd/copy.js +++ b/scripts/cmd/copy.js @@ -65,7 +65,7 @@ export default async function copy() { const dest = resolve(file.replace(/^[^/]+/, 'dist')); return fs .mkdir(dirname(dest), { recursive: true }) - .then(() => fs.copyFile(resolve(file), dest)); + .then(() => fs.copyFile(resolve(file), dest, fs.constants.COPYFILE_FICLONE)); }) ); }