wip: content collection refactor

This commit is contained in:
Nate Moore 2023-10-02 13:09:18 -05:00
parent e8495c853b
commit 6782a8611a
23 changed files with 359 additions and 130 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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);
}
}
},

View file

@ -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<string, ContentEntryType>;
dataEntryExts: string[];
contentPaths: Pick<ContentPaths, 'contentDir' | 'config'>;
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<string> {
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);

View file

@ -5,16 +5,19 @@ import type { StaticBuildOptions, ViteBuildReturn } from './types.js';
type RollupOutputArray = Extract<ViteBuildReturn, Array<any>>;
type OutputChunkorAsset = RollupOutputArray[number]['output'][number];
type OutputChunk = Extract<OutputChunkorAsset, { type: 'chunk' }>;
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<string> }) => {
enforce?: 'after-user-plugins';
vitePlugin: VitePlugin | VitePlugin[] | undefined;
};
'build:before'?: (opts: { target: BuildTarget; input: Set<string> }) => BuildBeforeHookResult | Promise<BuildBeforeHookResult>;
'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<BuildTarget, AstroBuildPlugin[]>();
const allPlugins = new Set<AstroBuildPlugin>();
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<string>) {
let plugins = build === 'ssr' ? ssrPlugins : clientPlugins;
async runBeforeHook(target: BuildTarget, input: Set<string>) {
let targetPlugins = plugins.get(target) ?? [];
let vitePlugins: Array<VitePlugin | VitePlugin[]> = [];
let lastVitePlugins: Array<VitePlugin | VitePlugin[]> = [];
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,
});
};

View file

@ -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());
}

View file

@ -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 {

View file

@ -330,7 +330,7 @@ export function pluginAnalyzer(
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'ssr',
targets: ['server'],
hooks: {
'build:before': () => {
return {

View file

@ -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 {

View file

@ -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),
};
},
},
};
}

View file

@ -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 {

View file

@ -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(),
};
},
},
};
}

View file

@ -108,7 +108,7 @@ export function pluginHoistedScripts(
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'client',
targets: ['client'],
hooks: {
'build:before': () => {
return {

View file

@ -62,7 +62,7 @@ export function vitePluginInternals(input: Set<string>, internals: BuildInternal
export function pluginInternals(internals: BuildInternals): AstroBuildPlugin {
return {
build: 'both',
targets: ['client', 'server'],
hooks: {
'build:before': ({ input }) => {
return {

View file

@ -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);
},
},
};

View file

@ -71,7 +71,7 @@ export function pluginMiddleware(
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'ssr',
targets: ['server'],
hooks: {
'build:before': () => {
return {

View file

@ -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 {

View file

@ -40,7 +40,7 @@ export function pluginPrerender(
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'ssr',
targets: ['server'],
hooks: {
'build:before': () => {
return {

View file

@ -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 {

View file

@ -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 =

View file

@ -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<string>,
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));
})
);
}

View file

@ -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 {

View file

@ -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));
})
);
}