diff options
Diffstat (limited to 'packages/astro')
22 files changed, 865 insertions, 275 deletions
diff --git a/packages/astro/package.json b/packages/astro/package.json index 8435c3c20..9a0db9f66 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -56,7 +56,7 @@ "test": "mocha --parallel --timeout 15000" }, "dependencies": { - "@astrojs/compiler": "^0.5.4", + "@astrojs/compiler": "^0.6.0", "@astrojs/language-server": "^0.8.2", "@astrojs/markdown-remark": "^0.5.0", "@astrojs/prism": "0.3.0", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index bce09f050..cdb2771d5 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -363,11 +363,13 @@ export interface SSRElement { export interface SSRMetadata { renderers: Renderer[]; pathname: string; + experimentalStaticBuild: boolean; } export interface SSRResult { styles: Set<SSRElement>; scripts: Set<SSRElement>; + links: Set<SSRElement>; createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal; _metadata: SSRMetadata; } diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 621678e50..03c84a711 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -25,6 +25,7 @@ interface CLIState { hostname?: string; port?: number; config?: string; + experimentalStaticBuild?: boolean; }; } @@ -37,6 +38,7 @@ function resolveArgs(flags: Arguments): CLIState { port: typeof flags.port === 'number' ? flags.port : undefined, config: typeof flags.config === 'string' ? flags.config : undefined, hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined, + experimentalStaticBuild: typeof flags.experimentalStaticBuild === 'boolean' ? flags.experimentalStaticBuild : false, }; if (flags.version) { @@ -73,6 +75,7 @@ function printHelp() { --config <path> Specify the path to the Astro config file. --project-root <path> Specify the path to the project root folder. --no-sitemap Disable sitemap generation (build only). + --experimental-static-build A more performant build that expects assets to be define statically. --verbose Enable verbose logging --silent Disable logging --version Show the version number and exit. @@ -92,6 +95,7 @@ function mergeCLIFlags(astroConfig: AstroConfig, flags: CLIState['options']) { if (typeof flags.site === 'string') astroConfig.buildOptions.site = flags.site; if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port; if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname; + if (typeof flags.experimentalStaticBuild === 'boolean') astroConfig.buildOptions.experimentalStaticBuild = flags.experimentalStaticBuild; } /** The primary CLI action */ diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index be1496b61..3789f9789 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -1,23 +1,18 @@ -import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, RouteCache, RouteData, RSSResult } from '../../@types/astro'; +import type { AstroConfig, ManifestData, RouteCache } from '../../@types/astro'; import type { LogOptions } from '../logger'; -import type { AllPagesData } from './types'; -import type { RenderedChunk } from 'rollup'; -import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js'; -import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; import fs from 'fs'; import * as colors from 'kleur/colors'; import { polyfill } from '@astropub/webapi'; import { performance } from 'perf_hooks'; import vite, { ViteDevServer } from '../vite.js'; -import { fileURLToPath } from 'url'; import { createVite, ViteConfigWithSSR } from '../create-vite.js'; import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js'; -import { preload as ssrPreload } from '../ssr/index.js'; -import { generatePaginateFunction } from '../ssr/paginate.js'; -import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js'; -import { generateRssFunction } from '../ssr/rss.js'; +import { createRouteManifest } from '../ssr/routing.js'; import { generateSitemap } from '../ssr/sitemap.js'; +import { collectPagesData } from './page-data.js'; +import { build as scanBasedBuild } from './scan-based-build.js'; +import { staticBuild } from './static-build.js'; export interface BuildOptions { mode?: string; @@ -82,137 +77,45 @@ class AstroBuilder { debug(logging, 'build', timerMessage('Vite started', timer.viteStart)); timer.loadStart = performance.now(); - const assets: Record<string, string> = {}; - const allPages: AllPagesData = {}; - // Collect all routes ahead-of-time, before we start the build. - // NOTE: This enforces that `getStaticPaths()` is only called once per route, - // and is then cached across all future SSR builds. In the past, we've had trouble - // with parallelized builds without guaranteeing that this is called first. - await Promise.all( - this.manifest.routes.map(async (route) => { - // static route: - if (route.pathname) { - allPages[route.component] = { - route, - paths: [route.pathname], - preload: await ssrPreload({ - astroConfig: this.config, - filePath: new URL(`./${route.component}`, this.config.projectRoot), - logging, - mode: 'production', - origin, - pathname: route.pathname, - route, - routeCache: this.routeCache, - viteServer, - }) - .then((routes) => { - const html = `${route.pathname}`.replace(/\/?$/, '/index.html'); - debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`); - return routes; - }) - .catch((err) => { - debug(logging, 'build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`); - throw err; - }), - }; - return; - } - // dynamic route: - const result = await this.getStaticPathsForRoute(route) - .then((routes) => { - const label = routes.paths.length === 1 ? 'page' : 'pages'; - debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${routes.paths.length} ${label}]`)}`); - return routes; - }) - .catch((err) => { - debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`); - throw err; - }); - if (result.rss?.xml) { - const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), this.config.dist); - if (assets[fileURLToPath(rssFile)]) { - throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`); - } - assets[fileURLToPath(rssFile)] = result.rss.xml; - } - allPages[route.component] = { - route, - paths: result.paths, - preload: await ssrPreload({ - astroConfig: this.config, - filePath: new URL(`./${route.component}`, this.config.projectRoot), - logging, - mode: 'production', - origin, - pathname: result.paths[0], - route, - routeCache: this.routeCache, - viteServer, - }), - }; - }) - ); + const { assets, allPages } = await collectPagesData({ + astroConfig: this.config, + logging: this.logging, + manifest: this.manifest, + origin, + routeCache: this.routeCache, + viteServer: this.viteServer, + }); debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart)); - // Pure CSS chunks are chunks that only contain CSS. - // This is all of them, and chunkToReferenceIdMap maps them to a hash id used to find the final file. - const pureCSSChunks = new Set<RenderedChunk>(); - const chunkToReferenceIdMap = new Map<string, string>(); - - // This is a mapping of pathname to the string source of all collected - // inline <style> for a page. - const astroStyleMap = new Map<string, string>(); - // This is a virtual JS module that imports all dependent styles for a page. - const astroPageStyleMap = new Map<string, string>(); - + // The names of each pages const pageNames: string[] = []; // Bundle the assets in your final build: This currently takes the HTML output // of every page (stored in memory) and bundles the assets pointed to on those pages. timer.buildStart = performance.now(); - await vite.build({ - logLevel: 'error', - mode: 'production', - build: { - emptyOutDir: true, - minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles - outDir: fileURLToPath(this.config.dist), - rollupOptions: { - // The `input` will be populated in the build rollup plugin. - input: [], - output: { format: 'esm' }, - }, - target: 'es2020', // must match an esbuild target - }, - plugins: [ - rollupPluginAstroBuildHTML({ - astroConfig: this.config, - astroPageStyleMap, - astroStyleMap, - chunkToReferenceIdMap, - pureCSSChunks, - logging, - origin, - allPages, - pageNames, - routeCache: this.routeCache, - viteServer, - }), - rollupPluginAstroBuildCSS({ - astroPageStyleMap, - astroStyleMap, - chunkToReferenceIdMap, - pureCSSChunks, - }), - ...(viteConfig.plugins || []), - ], - publicDir: viteConfig.publicDir, - root: viteConfig.root, - envPrefix: 'PUBLIC_', - server: viteConfig.server, - base: this.config.buildOptions.site ? new URL(this.config.buildOptions.site).pathname : '/', - }); + + // Use the new faster static based build. + if (this.config.buildOptions.experimentalStaticBuild) { + await staticBuild({ + allPages, + astroConfig: this.config, + logging: this.logging, + origin: this.origin, + routeCache: this.routeCache, + viteConfig: this.viteConfig, + }); + } else { + await scanBasedBuild({ + allPages, + astroConfig: this.config, + logging: this.logging, + origin: this.origin, + pageNames, + routeCache: this.routeCache, + viteConfig: this.viteConfig, + viteServer: this.viteServer, + }); + } debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart)); // Write any additionally generated assets to disk. @@ -243,22 +146,6 @@ class AstroBuilder { } } - /** Extract all static paths from a dynamic route */ - private async getStaticPathsForRoute(route: RouteData): Promise<{ paths: string[]; rss?: RSSResult }> { - if (!this.viteServer) throw new Error(`vite.createServer() not called!`); - const filePath = new URL(`./${route.component}`, this.config.projectRoot); - const mod = (await this.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance; - validateGetStaticPathsModule(mod); - const rss = generateRssFunction(this.config.buildOptions.site, route); - const staticPaths: GetStaticPathsResult = (await mod.getStaticPaths!({ paginate: generatePaginateFunction(route), rss: rss.generator })).flat(); - this.routeCache[route.component] = staticPaths; - validateGetStaticPathsResult(staticPaths, this.logging); - return { - paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean), - rss: rss.rss, - }; - } - /** Stats */ private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) { /* eslint-disable no-console */ diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts new file mode 100644 index 000000000..1028f3e4e --- /dev/null +++ b/packages/astro/src/core/build/internal.ts @@ -0,0 +1,45 @@ +import type { RenderedChunk } from 'rollup'; + +export interface BuildInternals { + // Pure CSS chunks are chunks that only contain CSS. + pureCSSChunks: Set<RenderedChunk>; + // chunkToReferenceIdMap maps them to a hash id used to find the final file. + chunkToReferenceIdMap: Map<string, string>; + + // This is a mapping of pathname to the string source of all collected + // inline <style> for a page. + astroStyleMap: Map<string, string>; + // This is a virtual JS module that imports all dependent styles for a page. + astroPageStyleMap: Map<string, string>; + + // A mapping to entrypoints (facadeId) to assets (styles) that are added. + facadeIdToAssetsMap: Map<string, string[]>; +} + +/** + * Creates internal maps used to coordinate the CSS and HTML plugins. + * @returns {BuildInternals} + */ +export function createBuildInternals(): BuildInternals { + // Pure CSS chunks are chunks that only contain CSS. + // This is all of them, and chunkToReferenceIdMap maps them to a hash id used to find the final file. + const pureCSSChunks = new Set<RenderedChunk>(); + const chunkToReferenceIdMap = new Map<string, string>(); + + // This is a mapping of pathname to the string source of all collected + // inline <style> for a page. + const astroStyleMap = new Map<string, string>(); + // This is a virtual JS module that imports all dependent styles for a page. + const astroPageStyleMap = new Map<string, string>(); + + // A mapping to entrypoints (facadeId) to assets (styles) that are added. + const facadeIdToAssetsMap = new Map<string, string[]>(); + + return { + pureCSSChunks, + chunkToReferenceIdMap, + astroStyleMap, + astroPageStyleMap, + facadeIdToAssetsMap, + }; +} diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts new file mode 100644 index 000000000..aa6247537 --- /dev/null +++ b/packages/astro/src/core/build/page-data.ts @@ -0,0 +1,122 @@ +import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, RouteCache, RouteData, RSSResult } from '../../@types/astro'; +import type { AllPagesData } from './types'; +import type { LogOptions } from '../logger'; +import type { ViteDevServer } from '../vite.js'; + +import { fileURLToPath } from 'url'; +import * as colors from 'kleur/colors'; +import { debug } from '../logger.js'; +import { preload as ssrPreload } from '../ssr/index.js'; +import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js'; +import { generatePaginateFunction } from '../ssr/paginate.js'; +import { generateRssFunction } from '../ssr/rss.js'; + +export interface CollectPagesDataOptions { + astroConfig: AstroConfig; + logging: LogOptions; + manifest: ManifestData; + origin: string; + routeCache: RouteCache; + viteServer: ViteDevServer; +} + +export interface CollectPagesDataResult { + assets: Record<string, string>; + allPages: AllPagesData; +} + +// Examines the routes and returns a collection of information about each page. +export async function collectPagesData(opts: CollectPagesDataOptions): Promise<CollectPagesDataResult> { + const { astroConfig, logging, manifest, origin, routeCache, viteServer } = opts; + + const assets: Record<string, string> = {}; + const allPages: AllPagesData = {}; + + // Collect all routes ahead-of-time, before we start the build. + // NOTE: This enforces that `getStaticPaths()` is only called once per route, + // and is then cached across all future SSR builds. In the past, we've had trouble + // with parallelized builds without guaranteeing that this is called first. + await Promise.all( + manifest.routes.map(async (route) => { + // static route: + if (route.pathname) { + allPages[route.component] = { + route, + paths: [route.pathname], + preload: await ssrPreload({ + astroConfig, + filePath: new URL(`./${route.component}`, astroConfig.projectRoot), + logging, + mode: 'production', + origin, + pathname: route.pathname, + route, + routeCache, + viteServer, + }) + .then((routes) => { + const html = `${route.pathname}`.replace(/\/?$/, '/index.html'); + debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`); + return routes; + }) + .catch((err) => { + debug(logging, 'build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`); + throw err; + }), + }; + return; + } + // dynamic route: + const result = await getStaticPathsForRoute(opts, route) + .then((routes) => { + const label = routes.paths.length === 1 ? 'page' : 'pages'; + debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${routes.paths.length} ${label}]`)}`); + return routes; + }) + .catch((err) => { + debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`); + throw err; + }); + if (result.rss?.xml) { + const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), astroConfig.dist); + if (assets[fileURLToPath(rssFile)]) { + throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`); + } + assets[fileURLToPath(rssFile)] = result.rss.xml; + } + allPages[route.component] = { + route, + paths: result.paths, + preload: await ssrPreload({ + astroConfig, + filePath: new URL(`./${route.component}`, astroConfig.projectRoot), + logging, + mode: 'production', + origin, + pathname: result.paths[0], + route, + routeCache, + viteServer, + }), + }; + }) + ); + + return { assets, allPages }; +} + +async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<{ paths: string[]; rss?: RSSResult }> { + const { astroConfig, logging, routeCache, viteServer } = opts; + if (!viteServer) throw new Error(`vite.createServer() not called!`); + const filePath = new URL(`./${route.component}`, astroConfig.projectRoot); + const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance; + validateGetStaticPathsModule(mod); + const rss = generateRssFunction(astroConfig.buildOptions.site, route); + const staticPaths: GetStaticPathsResult = (await mod.getStaticPaths!({ paginate: generatePaginateFunction(route), rss: rss.generator })).flat(); + routeCache[route.component] = staticPaths; + validateGetStaticPathsResult(staticPaths, logging); + return { + paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean), + rss: rss.rss, + }; +} diff --git a/packages/astro/src/core/build/scan-based-build.ts b/packages/astro/src/core/build/scan-based-build.ts new file mode 100644 index 000000000..7bb787758 --- /dev/null +++ b/packages/astro/src/core/build/scan-based-build.ts @@ -0,0 +1,68 @@ +import type { ViteDevServer } from '../vite.js'; +import type { AstroConfig, RouteCache } from '../../@types/astro'; +import type { AllPagesData } from './types'; +import type { LogOptions } from '../logger'; +import type { ViteConfigWithSSR } from '../create-vite.js'; + +import { fileURLToPath } from 'url'; +import vite from '../vite.js'; +import { createBuildInternals } from '../../core/build/internal.js'; +import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js'; +import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; + +export interface ScanBasedBuildOptions { + allPages: AllPagesData; + astroConfig: AstroConfig; + logging: LogOptions; + origin: string; + pageNames: string[]; + routeCache: RouteCache; + viteConfig: ViteConfigWithSSR; + viteServer: ViteDevServer; +} + +export async function build(opts: ScanBasedBuildOptions) { + const { allPages, astroConfig, logging, origin, pageNames, routeCache, viteConfig, viteServer } = opts; + + // Internal maps used to coordinate the HTML and CSS plugins. + const internals = createBuildInternals(); + + return await vite.build({ + logLevel: 'error', + mode: 'production', + build: { + emptyOutDir: true, + minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles + outDir: fileURLToPath(astroConfig.dist), + rollupOptions: { + // The `input` will be populated in the build rollup plugin. + input: [], + output: { + format: 'esm', + }, + }, + target: 'es2020', // must match an esbuild target + }, + plugins: [ + rollupPluginAstroBuildHTML({ + astroConfig, + internals, + logging, + origin, + allPages, + pageNames, + routeCache, + viteServer, + }), + rollupPluginAstroBuildCSS({ + internals, + }), + ...(viteConfig.plugins || []), + ], + publicDir: viteConfig.publicDir, + root: viteConfig.root, + envPrefix: 'PUBLIC_', + server: viteConfig.server, + base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', + }); +} diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts new file mode 100644 index 000000000..a6ed89e3c --- /dev/null +++ b/packages/astro/src/core/build/static-build.ts @@ -0,0 +1,195 @@ +import type { OutputChunk, PreRenderedChunk, RollupOutput } from 'rollup'; +import type { Plugin as VitePlugin } from '../vite'; +import type { AstroConfig, RouteCache } from '../../@types/astro'; +import type { AllPagesData } from './types'; +import type { LogOptions } from '../logger'; +import type { ViteConfigWithSSR } from '../create-vite'; +import type { PageBuildData } from './types'; +import type { BuildInternals } from '../../core/build/internal.js'; +import type { AstroComponentFactory } from '../../runtime/server'; + +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import vite from '../vite.js'; +import { debug, info, error } from '../../core/logger.js'; +import { createBuildInternals } from '../../core/build/internal.js'; +import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; +import { renderComponent, getParamsAndProps } from '../ssr/index.js'; + +export interface StaticBuildOptions { + allPages: AllPagesData; + astroConfig: AstroConfig; + logging: LogOptions; + origin: string; + routeCache: RouteCache; + viteConfig: ViteConfigWithSSR; +} + +export async function staticBuild(opts: StaticBuildOptions) { + const { allPages, astroConfig } = opts; + + // The JavaScript entrypoints. + const jsInput: Set<string> = new Set(); + + // A map of each page .astro file, to the PageBuildData which contains information + // about that page, such as its paths. + const facadeIdToPageDataMap = new Map<string, PageBuildData>(); + + for (const [component, pageData] of Object.entries(allPages)) { + const [renderers, mod] = pageData.preload; + + // Hydrated components are statically identified. + for (const path of mod.$$metadata.getAllHydratedComponentPaths()) { + // Note that this part is not yet implemented in the static build. + //jsInput.add(path); + } + + let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname; + jsInput.add(astroModuleId); + facadeIdToPageDataMap.set(astroModuleId, pageData); + } + + // Build internals needed by the CSS plugin + const internals = createBuildInternals(); + + // Perform the SSR build + const result = (await ssrBuild(opts, internals, jsInput)) as RollupOutput; + + // Generate each of the pages. + await generatePages(result, opts, internals, facadeIdToPageDataMap); +} + +async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) { + const { astroConfig, viteConfig } = opts; + + return await vite.build({ + logLevel: 'error', + mode: 'production', + build: { + emptyOutDir: true, + minify: false, // 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles + outDir: fileURLToPath(astroConfig.dist), + ssr: true, + rollupOptions: { + input: Array.from(input), + output: { + format: 'esm', + }, + }, + target: 'es2020', // must match an esbuild target + }, + plugins: [ + vitePluginNewBuild(), + rollupPluginAstroBuildCSS({ + internals, + }), + ...(viteConfig.plugins || []), + ], + publicDir: viteConfig.publicDir, + root: viteConfig.root, + envPrefix: 'PUBLIC_', + server: viteConfig.server, + base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', + }); +} + +async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { + debug(opts.logging, 'generate', 'End build step, now generating'); + const generationPromises = []; + for (let output of result.output) { + if (output.type === 'chunk' && output.facadeModuleId && output.facadeModuleId.endsWith('.astro')) { + generationPromises.push(generatePage(output, opts, internals, facadeIdToPageDataMap)); + } + } + await Promise.all(generationPromises); +} + +async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { + const { astroConfig } = opts; + + let url = new URL('./' + output.fileName, astroConfig.dist); + const facadeId: string = output.facadeModuleId as string; + let pageData = + facadeIdToPageDataMap.get(facadeId) || + // Check with a leading `/` because on Windows it doesn't have one. + facadeIdToPageDataMap.get('/' + facadeId); + + if (!pageData) { + throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`); + } + + let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || []; + let compiledModule = await import(url.toString()); + let Component = compiledModule.default; + + const generationOptions: Readonly<GeneratePathOptions> = { + pageData, + linkIds, + Component, + }; + + const renderPromises = pageData.paths.map((path) => { + return generatePath(path, opts, generationOptions); + }); + return await Promise.all(renderPromises); +} + +interface GeneratePathOptions { + pageData: PageBuildData; + linkIds: string[]; + Component: AstroComponentFactory; +} + +async function generatePath(path: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { + const { astroConfig, logging, origin, routeCache } = opts; + const { Component, linkIds, pageData } = gopts; + + const [renderers, mod] = pageData.preload; + + try { + const [params, pageProps] = await getParamsAndProps({ + route: pageData.route, + routeCache, + logging, + pathname: path, + mod, + }); + + info(logging, 'generate', `Generating: ${path}`); + + const html = await renderComponent(renderers, Component, astroConfig, path, origin, params, pageProps, linkIds); + const outFolder = new URL('.' + path + '/', astroConfig.dist); + const outFile = new URL('./index.html', outFolder); + await fs.promises.mkdir(outFolder, { recursive: true }); + await fs.promises.writeFile(outFile, html, 'utf-8'); + } catch (err) { + error(opts.logging, 'build', `Error rendering:`, err); + } +} + +export function vitePluginNewBuild(): VitePlugin { + return { + name: '@astro/rollup-plugin-new-build', + + configResolved(resolvedConfig) { + // Delete this hook because it causes assets not to be built + const plugins = resolvedConfig.plugins as VitePlugin[]; + const viteAsset = plugins.find((p) => p.name === 'vite:asset'); + if (viteAsset) { + delete viteAsset.generateBundle; + } + }, + + outputOptions(outputOptions) { + Object.assign(outputOptions, { + entryFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].mjs'; + }, + chunkFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].mjs'; + }, + }); + return outputOptions; + }, + }; +} diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts index f2e999f80..38a4e8ea2 100644 --- a/packages/astro/src/core/config.ts +++ b/packages/astro/src/core/config.ts @@ -58,6 +58,7 @@ export const AstroConfigSchema = z.object({ .union([z.literal('file'), z.literal('directory')]) .optional() .default('directory'), + experimentalStaticBuild: z.boolean().optional().default(false), }) .optional() .default({}), diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts index 4b979a182..18d2e8c67 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -17,6 +17,7 @@ import type { SSRResult, } from '../../@types/astro'; import type { LogOptions } from '../logger'; +import type { AstroComponentFactory } from '../../runtime/server/index'; import eol from 'eol'; import fs from 'fs'; @@ -138,6 +139,120 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions) return [renderers, mod]; } +export async function renderComponent( + renderers: Renderer[], + Component: AstroComponentFactory, + astroConfig: AstroConfig, + pathname: string, + origin: string, + params: Params, + pageProps: Props, + links: string[] = [] +): Promise<string> { + const _links = new Set<SSRElement>( + links.map((href) => ({ + props: { + rel: 'stylesheet', + href, + }, + children: '', + })) + ); + const result: SSRResult = { + styles: new Set<SSRElement>(), + scripts: new Set<SSRElement>(), + links: _links, + /** This function returns the `Astro` faux-global */ + createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) { + const site = new URL(origin); + const url = new URL('.' + pathname, site); + const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); + return { + __proto__: astroGlobal, + props, + request: { + canonicalURL, + params, + url, + }, + slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), + // This is used for <Markdown> but shouldn't be used publicly + privateRenderSlotDoNotUse(slotName: string) { + return renderSlot(result, slots ? slots[slotName] : null); + }, + // <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages + async privateRenderMarkdownDoNotUse(content: string, opts: any) { + let mdRender = astroConfig.markdownOptions.render; + let renderOpts = {}; + if (Array.isArray(mdRender)) { + renderOpts = mdRender[1]; + mdRender = mdRender[0]; + } + if (typeof mdRender === 'string') { + ({ default: mdRender } = await import(mdRender)); + } + const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); + return code; + }, + } as unknown as AstroGlobal; + }, + _metadata: { + renderers, + pathname, + experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, + }, + }; + + let html = await renderPage(result, Component, pageProps, null); + + return html; +} + +export async function getParamsAndProps({ + route, + routeCache, + logging, + pathname, + mod, +}: { + route: RouteData | undefined; + routeCache: RouteCache; + pathname: string; + mod: ComponentInstance; + logging: LogOptions; +}): Promise<[Params, Props]> { + // Handle dynamic routes + let params: Params = {}; + let pageProps: Props = {}; + if (route && !route.pathname) { + if (route.params.length) { + const paramsMatch = route.pattern.exec(pathname); + if (paramsMatch) { + params = getParams(route.params)(paramsMatch); + } + } + validateGetStaticPathsModule(mod); + if (!routeCache[route.component]) { + routeCache[route.component] = await ( + await mod.getStaticPaths!({ + paginate: generatePaginateFunction(route), + rss: () => { + /* noop */ + }, + }) + ).flat(); + } + validateGetStaticPathsResult(routeCache[route.component], logging); + const routePathParams: GetStaticPathsResult = routeCache[route.component]; + const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params)); + if (!matchedStaticPath) { + throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`); + } + pageProps = { ...matchedStaticPath.props } || {}; + } + return [params, pageProps]; +} + /** use Vite to SSR */ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> { const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; @@ -183,6 +298,7 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO const result: SSRResult = { styles: new Set<SSRElement>(), scripts: new Set<SSRElement>(), + links: new Set<SSRElement>(), /** This function returns the `Astro` faux-global */ createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) { const site = new URL(origin); @@ -225,6 +341,7 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO _metadata: { renderers, pathname, + experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, }, }; diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 9e0d75f48..601dec4bc 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -372,14 +372,16 @@ const uniqueElements = (item: any, index: number, all: any[]) => { // styles and scripts into the head. export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) { const template = await renderToString(result, Component, props, children); - const styles = Array.from(result.styles) - .filter(uniqueElements) - .map((style) => - renderElement('style', { - ...style, - props: { ...style.props, 'astro-style': true }, - }) - ); + const styles = result._metadata.experimentalStaticBuild + ? [] + : Array.from(result.styles) + .filter(uniqueElements) + .map((style) => + renderElement('style', { + ...style, + props: { ...style.props, 'astro-style': true }, + }) + ); let needsHydrationStyles = false; const scripts = Array.from(result.scripts) .filter(uniqueElements) @@ -396,12 +398,16 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' })); } + const links = Array.from(result.links) + .filter(uniqueElements) + .map((link) => renderElement('link', link)); + // inject styles & scripts at end of <head> let headPos = template.indexOf('</head>'); if (headPos === -1) { - return styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts + return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts } - return template.substring(0, headPos) + styles.join('\n') + scripts.join('\n') + template.substring(headPos); + return template.substring(0, headPos) + links.join('\n') + styles.join('\n') + scripts.join('\n') + template.substring(headPos); } export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) { diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts new file mode 100644 index 000000000..71a08a94a --- /dev/null +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -0,0 +1,110 @@ +import type { AstroConfig } from '../@types/astro'; +import type { TransformResult } from '@astrojs/compiler'; +import type { SourceMapInput } from 'rollup'; +import type { TransformHook } from './styles'; + +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import { transform } from '@astrojs/compiler'; +import { transformWithVite } from './styles.js'; + +type CompilationCache = Map<string, TransformResult>; + +const configCache = new WeakMap<AstroConfig, CompilationCache>(); + +// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726 +function isSSR(options: undefined | boolean | { ssr: boolean }): boolean { + if (options === undefined) { + return false; + } + if (typeof options === 'boolean') { + return options; + } + if (typeof options == 'object') { + return !!options.ssr; + } + return false; +} + +async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: boolean | undefined) { + // pages and layouts should be transformed as full documents (implicit <head> <body> etc) + // everything else is treated as a fragment + const normalizedID = fileURLToPath(new URL(`file://${filename}`)); + const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts)); + + let cssTransformError: Error | undefined; + + // Transform from `.astro` to valid `.ts` + // use `sourcemap: "both"` so that sourcemap is included in the code + // result passed to esbuild, but also available in the catch handler. + const transformResult = await transform(source, { + as: isPage ? 'document' : 'fragment', + projectRoot: config.projectRoot.toString(), + site: config.buildOptions.site, + sourcefile: filename, + sourcemap: 'both', + internalURL: 'astro/internal', + experimentalStaticExtraction: config.buildOptions.experimentalStaticBuild, + // TODO add experimental flag here + preprocessStyle: async (value: string, attrs: Record<string, string>) => { + const lang = `.${attrs?.lang || 'css'}`.toLowerCase(); + try { + const result = await transformWithVite({ + value, + lang, + id: filename, + transformHook: viteTransform, + ssr: isSSR(opts), + }); + + let map: SourceMapInput | undefined; + if (!result) return null as any; // TODO: add type in compiler to fix "any" + if (result.map) { + if (typeof result.map === 'string') { + map = result.map; + } else if (result.map.mappings) { + map = result.map.toString(); + } + } + return { code: result.code, map }; + } catch (err) { + // save error to throw in plugin context + cssTransformError = err as any; + return null; + } + }, + }); + + // throw CSS transform errors here if encountered + if (cssTransformError) throw cssTransformError; + + return transformResult; +} + +export function invalidateCompilation(config: AstroConfig, filename: string) { + if (configCache.has(config)) { + const cache = configCache.get(config)!; + cache.delete(filename); + } +} + +export async function cachedCompilation(config: AstroConfig, filename: string, source: string | null, viteTransform: TransformHook, opts: boolean | undefined) { + let cache: CompilationCache; + if (!configCache.has(config)) { + cache = new Map(); + configCache.set(config, cache); + } else { + cache = configCache.get(config)!; + } + if (cache.has(filename)) { + return cache.get(filename)!; + } + + if (source === null) { + const fileUrl = new URL(`file://${filename}`); + source = await fs.promises.readFile(fileUrl, 'utf-8'); + } + const transformResult = await compile(config, filename, source, viteTransform, opts); + cache.set(filename, transformResult); + return transformResult; +} diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index e77389ff6..918d597a5 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -1,14 +1,12 @@ -import type { TransformResult } from '@astrojs/compiler'; -import type { SourceMapInput } from 'rollup'; import type vite from '../core/vite'; import type { AstroConfig } from '../@types/astro'; import esbuild from 'esbuild'; -import fs from 'fs'; import { fileURLToPath } from 'url'; -import { transform } from '@astrojs/compiler'; import { AstroDevServer } from '../core/dev/index.js'; -import { getViteTransform, TransformHook, transformWithVite } from './styles.js'; +import { getViteTransform, TransformHook } from './styles.js'; +import { parseAstroRequest } from './query.js'; +import { cachedCompilation, invalidateCompilation } from './compile.js'; const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms; interface AstroPluginOptions { @@ -16,22 +14,8 @@ interface AstroPluginOptions { devServer?: AstroDevServer; } -// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726 -function isSSR(options: undefined | boolean | { ssr: boolean }): boolean { - if (options === undefined) { - return false; - } - if (typeof options === 'boolean') { - return options; - } - if (typeof options == 'object') { - return !!options.ssr; - } - return false; -} - /** Transform .astro files for Vite */ -export default function astro({ config, devServer }: AstroPluginOptions): vite.Plugin { +export default function astro({ config }: AstroPluginOptions): vite.Plugin { let viteTransform: TransformHook; return { name: '@astrojs/vite-plugin-astro', @@ -40,57 +24,51 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P viteTransform = getViteTransform(resolvedConfig); }, // note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.) + async resolveId(id) { + // serve sub-part requests (*?astro) as virtual modules + if (parseAstroRequest(id).query.astro) { + return id; + } + }, async load(id, opts) { + let { filename, query } = parseAstroRequest(id); + if (query.astro) { + if (query.type === 'style') { + if (filename.startsWith('/') && !filename.startsWith(config.projectRoot.pathname)) { + filename = new URL('.' + filename, config.projectRoot).pathname; + } + const transformResult = await cachedCompilation(config, filename, null, viteTransform, opts); + + if (typeof query.index === 'undefined') { + throw new Error(`Requests for Astro CSS must include an index.`); + } + + const csses = transformResult.css; + const code = csses[query.index]; + + return { + code, + }; + } + } + + return null; + }, + async transform(source, id, opts) { if (!id.endsWith('.astro')) { - return null; + return; } - // pages and layouts should be transformed as full documents (implicit <head> <body> etc) - // everything else is treated as a fragment - const normalizedID = fileURLToPath(new URL(`file://${id}`)); - const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts)); - let source = await fs.promises.readFile(id, 'utf8'); - let tsResult: TransformResult | undefined; - let cssTransformError: Error | undefined; try { - // Transform from `.astro` to valid `.ts` - // use `sourcemap: "both"` so that sourcemap is included in the code - // result passed to esbuild, but also available in the catch handler. - tsResult = await transform(source, { - as: isPage ? 'document' : 'fragment', - projectRoot: config.projectRoot.toString(), - site: config.buildOptions.site, - sourcefile: id, - sourcemap: 'both', - internalURL: 'astro/internal', - preprocessStyle: async (value: string, attrs: Record<string, string>) => { - const lang = `.${attrs?.lang || 'css'}`.toLowerCase(); - try { - const result = await transformWithVite({ value, lang, id, transformHook: viteTransform, ssr: isSSR(opts) }); - let map: SourceMapInput | undefined; - if (!result) return null as any; // TODO: add type in compiler to fix "any" - if (result.map) { - if (typeof result.map === 'string') { - map = result.map; - } else if (result.map.mappings) { - map = result.map.toString(); - } - } - return { code: result.code, map }; - } catch (err) { - // save error to throw in plugin context - cssTransformError = err as any; - return null; - } - }, - }); - - // throw CSS transform errors here if encountered - if (cssTransformError) throw cssTransformError; + const transformResult = await cachedCompilation(config, id, source, viteTransform, opts); // Compile all TypeScript to JavaScript. // Also, catches invalid JS/TS in the compiled output before returning. - const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id }); + const { code, map } = await esbuild.transform(transformResult.code, { + loader: 'ts', + sourcemap: 'external', + sourcefile: id, + }); return { code, @@ -125,21 +103,21 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P labels: 'compiler', title: '🐛 BUG: `@astrojs/compiler` panic', body: `### Describe the Bug - -\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file. - -**${id.replace(fileURLToPath(config.projectRoot), '')}** -\`\`\`astro -${source} -\`\`\` -`, + + \`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file. + + **${id.replace(fileURLToPath(config.projectRoot), '')}** + \`\`\`astro + ${source} + \`\`\` + `, }); err.url = `https://github.com/withastro/astro/issues/new?${search.toString()}`; err.message = `Error: Uh oh, the Astro compiler encountered an unrecoverable error! - -Please open -a GitHub issue using the link below: -${err.url}`; + + Please open + a GitHub issue using the link below: + ${err.url}`; // TODO: remove stack replacement when compiler throws better errors err.stack = ` at ${id}`; } @@ -147,10 +125,9 @@ ${err.url}`; throw err; } }, - // async handleHotUpdate(context) { - // if (devServer) { - // return devServer.handleHotUpdate(context); - // } - // }, + async handleHotUpdate(context) { + // Invalidate the compilation cache so it recompiles + invalidateCompilation(config, context.file); + }, }; } diff --git a/packages/astro/src/vite-plugin-astro/query.ts b/packages/astro/src/vite-plugin-astro/query.ts new file mode 100644 index 000000000..f6ea8414a --- /dev/null +++ b/packages/astro/src/vite-plugin-astro/query.ts @@ -0,0 +1,35 @@ +export interface AstroQuery { + astro?: boolean; + src?: boolean; + type?: 'script' | 'template' | 'style' | 'custom'; + index?: number; + lang?: string; + raw?: boolean; +} + +// Parses an id to check if its an Astro request. +// CSS is imported like `import '/src/pages/index.astro?astro&type=style&index=0&lang.css'; +// This parses those ids and returns an object representing what it found. +export function parseAstroRequest(id: string): { + filename: string; + query: AstroQuery; +} { + const [filename, rawQuery] = id.split(`?`, 2); + const query = Object.fromEntries(new URLSearchParams(rawQuery).entries()) as AstroQuery; + if (query.astro != null) { + query.astro = true; + } + if (query.src != null) { + query.src = true; + } + if (query.index != null) { + query.index = Number(query.index); + } + if (query.raw != null) { + query.raw = true; + } + return { + filename, + query, + }; +} diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts index f26a36dce..cba865351 100644 --- a/packages/astro/src/vite-plugin-build-css/index.ts +++ b/packages/astro/src/vite-plugin-build-css/index.ts @@ -1,9 +1,10 @@ import type { RenderedChunk } from 'rollup'; -import { Plugin as VitePlugin } from '../core/vite'; +import type { BuildInternals } from '../core/build/internal'; -import { STYLE_EXTENSIONS } from '../core/ssr/css.js'; import * as path from 'path'; import esbuild from 'esbuild'; +import { Plugin as VitePlugin } from '../core/vite'; +import { STYLE_EXTENSIONS } from '../core/ssr/css.js'; const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css'; @@ -45,14 +46,11 @@ function isPageStyleVirtualModule(id: string) { } interface PluginOptions { - astroStyleMap: Map<string, string>; - astroPageStyleMap: Map<string, string>; - chunkToReferenceIdMap: Map<string, string>; - pureCSSChunks: Set<RenderedChunk>; + internals: BuildInternals; } export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { - const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks } = options; + const { internals } = options; const styleSourceMap = new Map<string, string>(); return { @@ -94,10 +92,10 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { async load(id) { if (isPageStyleVirtualModule(id)) { - return astroPageStyleMap.get(id) || null; + return internals.astroPageStyleMap.get(id) || null; } if (isStyleVirtualModule(id)) { - return astroStyleMap.get(id) || null; + return internals.astroStyleMap.get(id) || null; } return null; }, @@ -127,17 +125,26 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { // if (!chunkCSS) return null; // don’t output empty .css files if (isPureCSS) { - const { code: minifiedCSS } = await esbuild.transform(chunkCSS, { - loader: 'css', - minify: true, - }); - const referenceId = this.emitFile({ - name: chunk.name + '.css', - type: 'asset', - source: minifiedCSS, - }); - pureCSSChunks.add(chunk); - chunkToReferenceIdMap.set(chunk.fileName, referenceId); + internals.pureCSSChunks.add(chunk); + } + + const { code: minifiedCSS } = await esbuild.transform(chunkCSS, { + loader: 'css', + minify: true, + }); + const referenceId = this.emitFile({ + name: chunk.name + '.css', + type: 'asset', + source: minifiedCSS, + }); + + internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId); + if (chunk.type === 'chunk') { + const facadeId = chunk.facadeModuleId!; + if (!internals.facadeIdToAssetsMap.has(facadeId)) { + internals.facadeIdToAssetsMap.set(facadeId, []); + } + internals.facadeIdToAssetsMap.get(facadeId)!.push(this.getFileName(referenceId)); } return null; @@ -145,8 +152,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { // Delete CSS chunks so JS is not produced for them. generateBundle(opts, bundle) { - if (pureCSSChunks.size) { - const pureChunkFilenames = new Set([...pureCSSChunks].map((chunk) => chunk.fileName)); + if (internals.pureCSSChunks.size) { + const pureChunkFilenames = new Set([...internals.pureCSSChunks].map((chunk) => chunk.fileName)); const emptyChunkFiles = [...pureChunkFilenames] .map((file) => path.basename(file)) .join('|') @@ -155,7 +162,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { for (const [chunkId, chunk] of Object.entries(bundle)) { if (chunk.type === 'chunk') { - if (pureCSSChunks.has(chunk)) { + if (internals.pureCSSChunks.has(chunk)) { // Delete pure CSS chunks, these are JavaScript chunks that only import // other CSS files, so are empty at the end of bundling. delete bundle[chunkId]; diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index cdc5c1877..90765c35d 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -1,8 +1,9 @@ import type { AstroConfig, RouteCache } from '../@types/astro'; import type { LogOptions } from '../core/logger'; import type { ViteDevServer, Plugin as VitePlugin } from '../core/vite'; -import type { OutputChunk, PreRenderedChunk, RenderedChunk } from 'rollup'; +import type { OutputChunk, PreRenderedChunk } from 'rollup'; import type { AllPagesData } from '../core/build/types'; +import type { BuildInternals } from '../core/build/internal'; import parse5 from 'parse5'; import srcsetParse from 'srcset-parse'; import * as npath from 'path'; @@ -26,20 +27,17 @@ const STATUS_CODE_RE = /^404$/; interface PluginOptions { astroConfig: AstroConfig; - astroStyleMap: Map<string, string>; - astroPageStyleMap: Map<string, string>; - chunkToReferenceIdMap: Map<string, string>; + internals: BuildInternals; logging: LogOptions; allPages: AllPagesData; pageNames: string[]; - pureCSSChunks: Set<RenderedChunk>; origin: string; routeCache: RouteCache; viteServer: ViteDevServer; } export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { - const { astroConfig, astroStyleMap, astroPageStyleMap, chunkToReferenceIdMap, pureCSSChunks, logging, origin, allPages, routeCache, viteServer, pageNames } = options; + const { astroConfig, internals, logging, origin, allPages, routeCache, viteServer, pageNames } = options; // The filepath root of the src folder const srcRoot = astroConfig.src.pathname; @@ -161,7 +159,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { if (styles) { const styleId = getAstroStyleId(pathname); - astroStyleMap.set(styleId, styles); + internals.astroStyleMap.set(styleId, styles); // Put this at the front of imports assetImports.unshift(styleId); } @@ -175,7 +173,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { if (assetImports.length) { const pageStyleId = getAstroPageStyleId(pathname); const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n'); - astroPageStyleMap.set(pageStyleId, jsSource); + internals.astroPageStyleMap.set(pageStyleId, jsSource); assetInput.add(pageStyleId); // preserve asset order in the order we encounter them @@ -268,7 +266,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { // Sort CSS in order of appearance in HTML (pageStyleImportOrder) // This is the “global ordering” used below - const sortedCSSChunks = [...pureCSSChunks]; + const sortedCSSChunks = [...internals.pureCSSChunks]; sortedCSSChunks.sort((a, b) => { let aIndex = Math.min( ...Object.keys(a.modules).map((id) => { @@ -298,7 +296,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { const referenceIDs: string[] = []; for (const chunkID of chunkModules) { - const referenceID = chunkToReferenceIdMap.get(chunkID); + const referenceID = internals.chunkToReferenceIdMap.get(chunkID); if (referenceID) referenceIDs.push(referenceID); } for (const id of Object.keys(chunk.modules)) { diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.js index d9fb14d1b..247232aef 100644 --- a/packages/astro/test/preact-component.test.js +++ b/packages/astro/test/preact-component.test.js @@ -6,6 +6,9 @@ let fixture; before(async () => { fixture = await loadFixture({ + devOptions: { + port: 3009, + }, projectRoot: './fixtures/preact-component/', renderers: ['@astrojs/renderer-preact'], }); diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index c6485460f..78af0d97c 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -7,6 +7,9 @@ let fixture; describe('React Components', () => { before(async () => { fixture = await loadFixture({ + devOptions: { + port: 3008, + }, projectRoot: './fixtures/react-component/', renderers: ['@astrojs/renderer-react', '@astrojs/renderer-vue'], }); diff --git a/packages/astro/test/solid-component.test.js b/packages/astro/test/solid-component.test.js index 426f687bc..1166b1515 100644 --- a/packages/astro/test/solid-component.test.js +++ b/packages/astro/test/solid-component.test.js @@ -7,6 +7,9 @@ describe('Solid component', () => { before(async () => { fixture = await loadFixture({ + devOptions: { + port: 3006, + }, projectRoot: './fixtures/solid-component/', renderers: ['@astrojs/renderer-solid'], }); diff --git a/packages/astro/test/svelte-component.test.js b/packages/astro/test/svelte-component.test.js index f50f24e0c..4e4ef5e56 100644 --- a/packages/astro/test/svelte-component.test.js +++ b/packages/astro/test/svelte-component.test.js @@ -7,6 +7,9 @@ describe('Svelte component', () => { before(async () => { fixture = await loadFixture({ + devOptions: { + port: 3007, + }, projectRoot: './fixtures/svelte-component/', renderers: ['@astrojs/renderer-svelte'], }); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index f017b0cb9..eeffb8676 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -9,6 +9,7 @@ import preview from '../dist/core/preview/index.js'; /** * @typedef {import('node-fetch').Response} Response * @typedef {import('../src/core/dev/index').DevServer} DevServer + * @typedef {import('../src/@types/astro').AstroConfig AstroConfig} * * * @typedef {Object} Fixture @@ -21,7 +22,7 @@ import preview from '../dist/core/preview/index.js'; /** * Load Astro fixture - * @param {Object} inlineConfig Astro config partial (note: must specify projectRoot) + * @param {AstroConfig} inlineConfig Astro config partial (note: must specify projectRoot) * @returns {Fixture} The fixture. Has the following properties: * .config - Returns the final config. Will be automatically passed to the methods below: * diff --git a/packages/astro/test/vue-component.test.js b/packages/astro/test/vue-component.test.js index f375174bb..d4928cf1f 100644 --- a/packages/astro/test/vue-component.test.js +++ b/packages/astro/test/vue-component.test.js @@ -7,6 +7,9 @@ describe('Vue component', () => { before(async () => { fixture = await loadFixture({ + devOptions: { + port: 3005, + }, projectRoot: './fixtures/vue-component/', renderers: ['@astrojs/renderer-vue'], }); |
