summaryrefslogtreecommitdiff
path: root/packages/astro
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro')
-rw-r--r--packages/astro/package.json2
-rw-r--r--packages/astro/src/@types/astro.ts2
-rw-r--r--packages/astro/src/cli/index.ts4
-rw-r--r--packages/astro/src/core/build/index.ts187
-rw-r--r--packages/astro/src/core/build/internal.ts45
-rw-r--r--packages/astro/src/core/build/page-data.ts122
-rw-r--r--packages/astro/src/core/build/scan-based-build.ts68
-rw-r--r--packages/astro/src/core/build/static-build.ts195
-rw-r--r--packages/astro/src/core/config.ts1
-rw-r--r--packages/astro/src/core/ssr/index.ts117
-rw-r--r--packages/astro/src/runtime/server/index.ts26
-rw-r--r--packages/astro/src/vite-plugin-astro/compile.ts110
-rw-r--r--packages/astro/src/vite-plugin-astro/index.ts137
-rw-r--r--packages/astro/src/vite-plugin-astro/query.ts35
-rw-r--r--packages/astro/src/vite-plugin-build-css/index.ts53
-rw-r--r--packages/astro/src/vite-plugin-build-html/index.ts18
-rw-r--r--packages/astro/test/preact-component.test.js3
-rw-r--r--packages/astro/test/react-component.test.js3
-rw-r--r--packages/astro/test/solid-component.test.js3
-rw-r--r--packages/astro/test/svelte-component.test.js3
-rw-r--r--packages/astro/test/test-utils.js3
-rw-r--r--packages/astro/test/vue-component.test.js3
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'],
});