Compare commits

...

17 commits

Author SHA1 Message Date
Matthew Phillips
992accf2b8 Work on removing vite-postprocess 2021-12-20 16:03:57 -05:00
Matthew Phillips
c1a15aa335 Only build chunks that are astro pages 2021-12-15 10:55:31 -05:00
Matthew Phillips
b3ad03bbb0 Fix types 2021-12-15 10:26:53 -05:00
github-actions[bot]
4d479a964c chore(lint): Prettier fix 2021-12-15 14:54:42 +00:00
Matthew Phillips
ce784126e1 Update yarn lock 2021-12-15 09:53:21 -05:00
github-actions[bot]
3745916f2d chore(lint): Prettier fix 2021-12-15 09:51:43 -05:00
Matthew Phillips
858958490f Use the logger 2021-12-15 09:51:43 -05:00
Matthew Phillips
3be5ddd05a Do not use ssr mode 2021-12-15 09:51:43 -05:00
Matthew Phillips
98a868e8cb Pass through the origin 2021-12-15 09:51:43 -05:00
github-actions[bot]
b7ff454453 chore(lint): Prettier fix 2021-12-15 09:51:43 -05:00
Matthew Phillips
19e2b5c56c Remove debugging stuff 2021-12-15 09:51:43 -05:00
Matthew Phillips
f6e24d1a97 Almost done 2021-12-15 09:51:41 -05:00
Matthew Phillips
03447124a3 Add support for hot reload 2021-12-15 09:51:07 -05:00
Matthew Phillips
abdde962a9 More progress here 2021-12-15 09:51:04 -05:00
Matthew Phillips
6b3d4ed075 Some more progress on the new build 2021-12-15 09:47:34 -05:00
Matthew Phillips
8f67687eef Go back to ESM 2021-12-15 09:47:34 -05:00
Matthew Phillips
530b77ed0a Progress on build demo 2021-12-15 09:47:34 -05:00
33 changed files with 1733 additions and 418 deletions

View file

@ -88,10 +88,10 @@ jobs:
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: "**/node_modules" path: "**/node_modules"
key: cache-node_modules-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }} key: cache-node_modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: | restore-keys: |
cache-node_modules-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }} cache-node_modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
cache-node_modules-${{ hashFiles('**/yarn.lock') }}- cache-node_modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-
- name: Install NPM Dependencies - name: Install NPM Dependencies
run: yarn install --prefer-offline --frozen-lockfile --ignore-engines --registry https://registry.npmjs.org --network-timeout 300000 run: yarn install --prefer-offline --frozen-lockfile --ignore-engines --registry https://registry.npmjs.org --network-timeout 300000

View file

@ -10,7 +10,6 @@ Astro verfügt über eine besondere Möglichkeit, um das Schreiben von CSS so ei
Standardmäßig werden in Astro-Komponenten alle Styles nur auf Elemente im Rahmen der Komponente (genannt **Scope**) angewandt, der sie hinzugefügt wurden. Dies kann die Arbeit mit Styles erheblich erleichtern, da du dich zu jeder Zeit nur um die Gestaltung der Komponente kümmern musst, an der du arbeitest. Standardmäßig werden in Astro-Komponenten alle Styles nur auf Elemente im Rahmen der Komponente (genannt **Scope**) angewandt, der sie hinzugefügt wurden. Dies kann die Arbeit mit Styles erheblich erleichtern, da du dich zu jeder Zeit nur um die Gestaltung der Komponente kümmern musst, an der du arbeitest.
```html ```html
<!-- src/components/MeineKomponente.astro --> <!-- src/components/MeineKomponente.astro -->
<style> <style>
@ -25,7 +24,9 @@ Standardmäßig werden in Astro-Komponenten alle Styles nur auf Elemente im Rahm
</style> </style>
<h1>Ich bin ein Style im Scope der Komponente, und ich bin rot!</h1> <h1>Ich bin ein Style im Scope der Komponente, und ich bin rot!</h1>
<p class="text">Ich bin ein Style im Scope der Komponente, und ich bin kursiv!!</p> <p class="text">
Ich bin ein Style im Scope der Komponente, und ich bin kursiv!!
</p>
``` ```
Beachte dass der der `h1`-Selektor hier nicht über die Komponente hinaus wirksam wird! Die Styles werden nicht auf andere `h1`-Tags außerhalb dieses Dokuments angewandt - auch nicht in untergeordneten Komponenten. Beachte dass der der `h1`-Selektor hier nicht über die Komponente hinaus wirksam wird! Die Styles werden nicht auf andere `h1`-Tags außerhalb dieses Dokuments angewandt - auch nicht in untergeordneten Komponenten.
@ -149,13 +150,12 @@ Du kannst jedes beliebige PostCSS-Plugin verwenden, indem du eine `postcss.confi
Styling in Astro sollte so flexibel sein, wie du es haben willst! Die folgenden Optionen werden unterstützt: Styling in Astro sollte so flexibel sein, wie du es haben willst! Die folgenden Optionen werden unterstützt:
| Framework | Globales CSS | Scoped CSS | CSS-Modules | | Framework | Globales CSS | Scoped CSS | CSS-Modules |
| :--------------- | :----------: | :--------: | :----------: | | :--------------- | :----------: | :--------: | :---------: |
| `.astro` | ✅ | ✅ | N/A¹ | | `.astro` | ✅ | ✅ | N/A¹ |
| `.jsx` \| `.tsx` | ✅ | ❌ | ✅ | | `.jsx` \| `.tsx` | ✅ | ❌ | ✅ |
| `.vue` | ✅ | ✅ | ✅ | | `.vue` | ✅ | ✅ | ✅ |
| `.svelte` | ✅ | ✅ | ❌ | | `.svelte` | ✅ | ✅ | ❌ |
¹ _`.astro`-Dateien haben keine Laufzeit, daher nimmt Scoped-CSS hier den Platz von CSS-Modules ein (Styles sind im Scope der Komponenten, benötigen aber keine dynamischen Werte)_ ¹ _`.astro`-Dateien haben keine Laufzeit, daher nimmt Scoped-CSS hier den Platz von CSS-Modules ein (Styles sind im Scope der Komponenten, benötigen aber keine dynamischen Werte)_
Alle Styles in Astro werden automatisch minifiziert und gepackt, du kannst so einfach nur dein CSS schreiben - und wir machen den Rest ✨. Alle Styles in Astro werden automatisch minifiziert und gepackt, du kannst so einfach nur dein CSS schreiben - und wir machen den Rest ✨.
@ -276,7 +276,7 @@ Sämtliches CSS wird minifiziert und automatisch gebündelt, wenn du `astro buil
Wir werden unsere Styling-Optimierungen im Laufe der Zeit stetig weiterentwickeln und würden gerne euer Feedback dazu hören! Falls `astro build` unerwartete Styles generiert, oder wenn du Vorschläge zur Verbesserung hast, [eröffne bitte ein Issue][issues]. Wir werden unsere Styling-Optimierungen im Laufe der Zeit stetig weiterentwickeln und würden gerne euer Feedback dazu hören! Falls `astro build` unerwartete Styles generiert, oder wenn du Vorschläge zur Verbesserung hast, [eröffne bitte ein Issue][issues].
_Beachte: Wenn einige Seiten-Styles gemeinsam gebündelt werden und andere Seiten-Styles auf die Seite bezogen bleiben, entwickeln sich hieraus meistens keine Probleme. Aber wenn Teile deiner Styles gebündelt werden, könnten sie _technisch_ auch in einer anderen Reihenfolge laden, als von dir in deiner Kaskade intendiert. Auch wenn dieses Problem nicht nur Astro zu eigen ist - es besteht potentiell bei so ziemlich jedem Bündelungsprozess - so kann es dich doch unerwartet treffen, wenn du diese Möglichkeit nicht von vorne herein in Betracht ziehst. Stelle sicher, dass du deinen abschließenden Build eingehend diesbezüglich inspizierst - und [melde bitte auftretende Probleme][issues], auf die du stößt._ _Beachte: Wenn einige Seiten-Styles gemeinsam gebündelt werden und andere Seiten-Styles auf die Seite bezogen bleiben, entwickeln sich hieraus meistens keine Probleme. Aber wenn Teile deiner Styles gebündelt werden, könnten sie \_technisch_ auch in einer anderen Reihenfolge laden, als von dir in deiner Kaskade intendiert. Auch wenn dieses Problem nicht nur Astro zu eigen ist - es besteht potentiell bei so ziemlich jedem Bündelungsprozess - so kann es dich doch unerwartet treffen, wenn du diese Möglichkeit nicht von vorne herein in Betracht ziehst. Stelle sicher, dass du deinen abschließenden Build eingehend diesbezüglich inspizierst - und [melde bitte auftretende Probleme][issues], auf die du stößt.\_
## Fortgeschrittene Styling-Architektur ## Fortgeschrittene Styling-Architektur
@ -389,10 +389,7 @@ In Astro empfehlen wir folgendes Setup hierfür:
```html ```html
<head> <head>
<link <link rel="stylesheet" href={Astro.resolve("../styles/global.css")} >
rel="stylesheet"
href={Astro.resolve("../styles/global.css")}
>
</head> </head>
``` ```

View file

@ -26,12 +26,8 @@ You can add import aliases from either `tsconfig.json` or `jsconfig.json`.
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"asset:*": [ "asset:*": ["src/assets/*?url"],
"src/assets/*?url" "component:*": ["src/components/*.astro"]
],
"component:*": [
"src/components/*.astro"
]
} }
} }
} }

View file

@ -0,0 +1,11 @@
import { imagetools } from 'vite-imagetools';
// @ts-check
export default /** @type {import('astro').AstroUserConfig} */ ({
renderers: [
"@astrojs/renderer-vue"
],
vite: {
plugins: [imagetools()]
}
});

View file

@ -0,0 +1,16 @@
{
"name": "@example/fast-build",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev --experimental-static-build",
"start": "astro dev",
"build": "astro build --experimental-static-build",
"preview": "astro preview"
},
"devDependencies": {
"astro": "^0.21.6",
"unocss": "^0.15.5",
"vite-imagetools": "^4.0.1"
}
}

View file

@ -0,0 +1,24 @@
<template>
<div id="vue" class="counter">
<button @click="subtract()">-</button>
<pre>{{ count }}</pre>
<button @click="add()">+</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0)
const add = () => count.value = count.value + 1;
const subtract = () => count.value = count.value - 1;
return {
count,
add,
subtract
}
}
}
</script>

View file

@ -0,0 +1,20 @@
<script>
export default {
data() {
return {
greeting: 'Hello World!',
};
},
};
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,38 @@
---
import imgUrl from '../images/penguin.jpg';
import grayscaleUrl from '../images/random.jpg?grayscale=true';
import Greeting from '../components/Greeting.vue';
import Counter from '../components/Counter.vue';
---
<html>
<head>
<title>Demo app</title>
<style>
h1 { color: salmon; }
</style>
</head>
<body>
<section>
<h1>Images</h1>
<h2>Imported in JS</h2>
<img src={imgUrl} />
</section>
<section>
<h1>Component CSS</h1>
<Greeting />
</section>
<section>
<h1>ImageTools</h1>
<img src={grayscaleUrl} />
</section>
<section>
<h1>Hydrated component</h1>
<Counter client:idle />
</section>
</body>
</html>

View file

@ -0,0 +1,3 @@
body {
background: lightcoral;
}

View file

@ -56,7 +56,7 @@
"test": "mocha --parallel --timeout 15000" "test": "mocha --parallel --timeout 15000"
}, },
"dependencies": { "dependencies": {
"@astrojs/compiler": "^0.5.4", "@astrojs/compiler": "^0.6.0",
"@astrojs/language-server": "^0.8.2", "@astrojs/language-server": "^0.8.2",
"@astrojs/markdown-remark": "^0.5.0", "@astrojs/markdown-remark": "^0.5.0",
"@astrojs/prism": "0.3.0", "@astrojs/prism": "0.3.0",

View file

@ -363,11 +363,14 @@ export interface SSRElement {
export interface SSRMetadata { export interface SSRMetadata {
renderers: Renderer[]; renderers: Renderer[];
pathname: string; pathname: string;
experimentalStaticBuild: boolean;
} }
export interface SSRResult { export interface SSRResult {
styles: Set<SSRElement>; styles: Set<SSRElement>;
scripts: Set<SSRElement>; scripts: Set<SSRElement>;
links: Set<SSRElement>;
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal; createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
resolve: (s: string) => Promise<string>;
_metadata: SSRMetadata; _metadata: SSRMetadata;
} }

View file

@ -25,6 +25,7 @@ interface CLIState {
hostname?: string; hostname?: string;
port?: number; port?: number;
config?: string; config?: string;
experimentalStaticBuild?: boolean;
}; };
} }
@ -37,6 +38,7 @@ function resolveArgs(flags: Arguments): CLIState {
port: typeof flags.port === 'number' ? flags.port : undefined, port: typeof flags.port === 'number' ? flags.port : undefined,
config: typeof flags.config === 'string' ? flags.config : undefined, config: typeof flags.config === 'string' ? flags.config : undefined,
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined, hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
experimentalStaticBuild: typeof flags.experimentalStaticBuild === 'boolean' ? flags.experimentalStaticBuild : false,
}; };
if (flags.version) { if (flags.version) {
@ -73,6 +75,7 @@ function printHelp() {
--config <path> Specify the path to the Astro config file. --config <path> Specify the path to the Astro config file.
--project-root <path> Specify the path to the project root folder. --project-root <path> Specify the path to the project root folder.
--no-sitemap Disable sitemap generation (build only). --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 --verbose Enable verbose logging
--silent Disable logging --silent Disable logging
--version Show the version number and exit. --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.site === 'string') astroConfig.buildOptions.site = flags.site;
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port; if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname; if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
if (typeof flags.experimentalStaticBuild === 'boolean') astroConfig.buildOptions.experimentalStaticBuild = flags.experimentalStaticBuild;
} }
/** The primary CLI action */ /** The primary CLI action */

View file

@ -1,22 +1,17 @@
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 { 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 fs from 'fs';
import * as colors from 'kleur/colors'; import * as colors from 'kleur/colors';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import vite, { ViteDevServer } from '../vite.js'; import vite, { ViteDevServer } from '../vite.js';
import { fileURLToPath } from 'url';
import { createVite, ViteConfigWithSSR } from '../create-vite.js'; import { createVite, ViteConfigWithSSR } from '../create-vite.js';
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js'; import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
import { preload as ssrPreload } from '../ssr/index.js'; import { createRouteManifest } from '../ssr/routing.js';
import { generatePaginateFunction } from '../ssr/paginate.js';
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
import { generateRssFunction } from '../ssr/rss.js';
import { generateSitemap } from '../ssr/sitemap.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 { export interface BuildOptions {
mode?: string; mode?: string;
@ -76,137 +71,45 @@ class AstroBuilder {
debug(logging, 'build', timerMessage('Vite started', timer.viteStart)); debug(logging, 'build', timerMessage('Vite started', timer.viteStart));
timer.loadStart = performance.now(); timer.loadStart = performance.now();
const assets: Record<string, string> = {}; const { assets, allPages } = await collectPagesData({
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, astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot), logging: this.logging,
logging, manifest: this.manifest,
mode: 'production',
origin, origin,
pathname: route.pathname,
route,
routeCache: this.routeCache, routeCache: this.routeCache,
viteServer, viteServer: this.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,
}),
};
})
);
debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart)); debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart));
// Pure CSS chunks are chunks that only contain CSS. // The names of each pages
// 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>();
const pageNames: string[] = []; const pageNames: string[] = [];
// Bundle the assets in your final build: This currently takes the HTML output // 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. // of every page (stored in memory) and bundles the assets pointed to on those pages.
timer.buildStart = performance.now(); timer.buildStart = performance.now();
await vite.build({
logLevel: 'error', // Use the new faster static based build.
mode: 'production', if (this.config.buildOptions.experimentalStaticBuild) {
build: { await staticBuild({
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, 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, pageNames,
routeCache: this.routeCache, routeCache: this.routeCache,
viteServer, viteConfig: this.viteConfig,
}), viteServer: this.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 : '/',
}); });
}
debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart)); debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart));
// Write any additionally generated assets to disk. // Write any additionally generated assets to disk.
@ -237,22 +140,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 */ /** Stats */
private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) { private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) {
/* eslint-disable no-console */ /* eslint-disable no-console */

View file

@ -0,0 +1,48 @@
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[]>;
entrySpecifierToBundleMap: 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,
entrySpecifierToBundleMap: new Map<string, string>(),
};
}

View file

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

View file

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

View file

@ -0,0 +1,238 @@
import type { OutputChunk, PreRenderedChunk, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin } from '../vite';
import type { AstroConfig, RouteCache, SSRElement } 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 { getParamsAndProps } from '../ssr/index.js';
import { createResult } from '../ssr/result.js';
import { renderPage } from '../../runtime/server/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;
const topLevelImports = new Set([
// Any component that gets hydrated
...mod.$$metadata.hydratedComponentPaths(),
// Any hydration directive like astro/client/idle.js
...mod.$$metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer
...renderers.filter(renderer => !!renderer.source).map(renderer => renderer.source!),
]);
for(const specifier of topLevelImports) {
jsInput.add(specifier);
}
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(input, internals),
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)!;
let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || [];
let compiledModule = await import(url.toString());
let Component = compiledModule.default;
const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
Component,
};
const renderPromises = pageData.paths.map((path) => {
return generatePath(path, opts, generationOptions);
});
return await Promise.all(renderPromises);
}
interface GeneratePathOptions {
pageData: PageBuildData;
internals: BuildInternals;
linkIds: string[];
Component: AstroComponentFactory;
}
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
const { astroConfig, logging, origin, routeCache } = opts;
const { Component, internals, linkIds, pageData } = gopts;
const [renderers, mod] = pageData.preload;
try {
const [params, pageProps] = await getParamsAndProps({
route: pageData.route,
routeCache,
logging,
pathname,
mod,
});
info(logging, 'generate', `Generating: ${pathname}`);
const result = createResult({ astroConfig, origin, params, pathname, renderers });
result.links = new Set<SSRElement>(
linkIds.map((href) => ({
props: {
rel: 'stylesheet',
href,
},
children: '',
}))
);
result.resolve = async (specifier: string) => {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
if(typeof hashedFilePath !== 'string') {
throw new Error(`Cannot find the built path for ${specifier}`);
}
console.log("WE GOT", hashedFilePath)
return hashedFilePath;
};
let html = await renderPage(result, Component, pageProps, null);
const outFolder = new URL('.' + pathname + '/', 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(input: Set<string>, internals: BuildInternals): 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;
},
async generateBundle(_options, bundle) {
const promises = [];
const mapping = new Map<string, string>();
for(const specifier of input) {
promises.push(this.resolve(specifier).then(result => {
if(result) {
mapping.set(result.id, specifier);
}
}));
}
await Promise.all(promises);
for(const [, chunk] of Object.entries(bundle)) {
if(chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) {
const specifier = mapping.get(chunk.facadeModuleId)!;
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
}
}
console.log(internals.entrySpecifierToBundleMap);
}
};
}

View file

@ -58,6 +58,7 @@ export const AstroConfigSchema = z.object({
.union([z.literal('file'), z.literal('directory')]) .union([z.literal('file'), z.literal('directory')])
.optional() .optional()
.default('directory'), .default('directory'),
experimentalStaticBuild: z.boolean().optional().default(false),
}) })
.optional() .optional()
.default({}), .default({}),

View file

@ -2,8 +2,6 @@ import type { BuildResult } from 'esbuild';
import type vite from '../vite'; import type vite from '../vite';
import type { import type {
AstroConfig, AstroConfig,
AstroGlobal,
AstroGlobalPartial,
ComponentInstance, ComponentInstance,
GetStaticPathsResult, GetStaticPathsResult,
Params, Params,
@ -14,20 +12,21 @@ import type {
RuntimeMode, RuntimeMode,
SSRElement, SSRElement,
SSRError, SSRError,
SSRResult,
} from '../../@types/astro'; } from '../../@types/astro';
import type { LogOptions } from '../logger'; import type { LogOptions } from '../logger';
import type { AstroComponentFactory } from '../../runtime/server/index';
import eol from 'eol'; import eol from 'eol';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { renderPage, renderSlot } from '../../runtime/server/index.js'; import { renderPage } from '../../runtime/server/index.js';
import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency } from '../util.js'; import { codeFrame, resolveDependency } from '../util.js';
import { getStylesForURL } from './css.js'; import { getStylesForURL } from './css.js';
import { injectTags } from './html.js'; import { injectTags } from './html.js';
import { generatePaginateFunction } from './paginate.js'; import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js'; import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
import { createResult } from './result.js';
const svelteStylesRE = /svelte\?svelte&type=style/; const svelteStylesRE = /svelte\?svelte&type=style/;
@ -138,6 +137,78 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions)
return [renderers, mod]; return [renderers, mod];
} }
// TODO REMOVE
export async function renderComponent(
renderers: Renderer[],
Component: AstroComponentFactory,
astroConfig: AstroConfig,
pathname: string,
origin: string,
params: Params,
pageProps: Props,
links: string[] = []
): Promise<string> {
const result = createResult({ astroConfig, origin, params, pathname, renderers });
result.links = new Set<SSRElement>(
links.map((href) => ({
props: {
rel: 'stylesheet',
href,
},
children: '',
}))
);
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 */ /** use Vite to SSR */
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> { export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
@ -177,55 +248,10 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`); if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
// Create the result object that will be passed into the render function. const result = createResult({ astroConfig, origin, params, pathname, renderers });
// This object starts here as an empty shell (not yet the result) but then result.resolve = async (s: string) => {
// calling the render() function will populate the object with scripts, styles, etc. const [, path] = await viteServer.moduleGraph.resolveUrl(s);
const result: SSRResult = { return path;
styles: new Set<SSRElement>(),
scripts: 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);
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];
}
// ['rehype-toc', opts]
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
// [import('rehype-toc'), opts]
else if (mdRender instanceof Promise) {
({ default: mdRender } = await mdRender);
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
_metadata: {
renderers,
pathname,
},
}; };
let html = await renderPage(result, Component, pageProps, null); let html = await renderPage(result, Component, pageProps, null);
@ -272,7 +298,8 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
// run transformIndexHtml() in dev to run Vite dev transformations // run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development') { if (mode === 'development') {
const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
html = await viteServer.transformIndexHtml(relativeURL, html, pathname); console.log("TRANFORM", relativeURL, html);
//html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
} }
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?) // inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)

View file

@ -0,0 +1,83 @@
import type {
AstroConfig,
AstroGlobal,
AstroGlobalPartial,
Params,
Renderer,
SSRElement,
SSRResult,
} from '../../@types/astro';
import { canonicalURL as getCanonicalURL } from '../util.js';
import { renderSlot } from '../../runtime/server/index.js';
export interface CreateResultArgs {
astroConfig: AstroConfig;
origin: string;
params: Params;
pathname: string;
renderers: Renderer[];
}
export function createResult(args: CreateResultArgs): SSRResult {
const { astroConfig, origin, params, pathname, renderers } = args;
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
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);
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];
}
// ['rehype-toc', opts]
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
// [import('rehype-toc'), opts]
else if (mdRender instanceof Promise) {
({ default: mdRender } = await mdRender);
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
// This is a stub and will be implemented by dev and build.
async resolve(s: string): Promise<string> {
return '';
},
_metadata: {
renderers,
pathname,
experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
},
};
return result;
}

View file

@ -1,8 +1,8 @@
import type { AstroComponentMetadata } from '../../@types/astro'; import type { AstroComponentMetadata } from '../../@types/astro';
import type { SSRElement } from '../../@types/astro'; import type { SSRElement, SSRResult } from '../../@types/astro';
import { valueToEstree } from 'estree-util-value-to-estree'; import { valueToEstree } from 'estree-util-value-to-estree';
import * as astring from 'astring'; import * as astring from 'astring';
import { serializeListValue } from './util.js'; import { hydrationSpecifier, serializeListValue } from './util.js';
const { generate, GENERATOR } = astring; const { generate, GENERATOR } = astring;
@ -69,6 +69,9 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
extracted.hydration.componentExport.value = value; extracted.hydration.componentExport.value = value;
break; break;
} }
case 'client:component-hydration': {
break;
}
default: { default: {
extracted.hydration.directive = key.split(':')[1]; extracted.hydration.directive = key.split(':')[1];
extracted.hydration.value = value; extracted.hydration.value = value;
@ -98,13 +101,14 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
interface HydrateScriptOptions { interface HydrateScriptOptions {
renderer: any; renderer: any;
result: SSRResult;
astroId: string; astroId: string;
props: Record<string | number, any>; props: Record<string | number, any>;
} }
/** For hydrated components, generate a <script type="module"> to load the component */ /** For hydrated components, generate a <script type="module"> to load the component */
export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>): Promise<SSRElement> { export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>): Promise<SSRElement> {
const { renderer, astroId, props } = scriptOptions; const { renderer, result, astroId, props } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata; const { hydrate, componentUrl, componentExport } = metadata;
if (!componentExport) { if (!componentExport) {
@ -117,16 +121,17 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
} }
hydrationSource += renderer.source hydrationSource += renderer.source
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]); ? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${await result.resolve(renderer.source)}")]);
return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children); return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
` `
: `await import("${componentUrl}"); : `await import("${componentUrl}");
return () => {}; return () => {};
`; `;
const hydrationScript = { const hydrationScript = {
props: { type: 'module', 'data-astro-component-hydration': true }, props: { type: 'module', 'data-astro-component-hydration': true },
children: `import setup from 'astro/client/${hydrate}.js'; children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => { setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
${hydrationSource} ${hydrationSource}
}); });

View file

@ -249,7 +249,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
// Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head. // Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point. // INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
result.scripts.add(await generateHydrateScript({ renderer, astroId, props }, metadata as Required<AstroComponentMetadata>)); result.scripts.add(await generateHydrateScript({ renderer, result, astroId, props }, metadata as Required<AstroComponentMetadata>));
return `<astro-root uid="${astroId}">${html ?? ''}</astro-root>`; return `<astro-root uid="${astroId}">${html ?? ''}</astro-root>`;
} }
@ -372,7 +372,9 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
// styles and scripts into the head. // styles and scripts into the head.
export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) { export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) {
const template = await renderToString(result, Component, props, children); const template = await renderToString(result, Component, props, children);
const styles = Array.from(result.styles) const styles = result._metadata.experimentalStaticBuild
? []
: Array.from(result.styles)
.filter(uniqueElements) .filter(uniqueElements)
.map((style) => .map((style) =>
renderElement('style', { renderElement('style', {
@ -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; }' })); 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> // inject styles & scripts at end of <head>
let headPos = template.indexOf('</head>'); let headPos = template.indexOf('</head>');
if (headPos === -1) { 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>) { export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {

View file

@ -1,3 +1,5 @@
import { hydrationSpecifier } from './util.js';
interface ModuleInfo { interface ModuleInfo {
module: Record<string, any>; module: Record<string, any>;
specifier: string; specifier: string;
@ -8,10 +10,27 @@ interface ComponentMetadata {
componentUrl: string; componentUrl: string;
} }
interface CreateMetadataOptions {
modules: ModuleInfo[];
hydratedComponents: any[];
hydrationDirectives: Set<string>;
hoisted: any[];
}
export class Metadata { export class Metadata {
public fileURL: URL; public fileURL: URL;
public modules: ModuleInfo[];
public hoisted: any[];
public hydratedComponents: any[];
public hydrationDirectives: Set<string>;
private metadataCache: Map<any, ComponentMetadata | null>; private metadataCache: Map<any, ComponentMetadata | null>;
constructor(fileURL: string, public modules: ModuleInfo[], public hydratedComponents: any[], public hoisted: any[]) {
constructor(fileURL: string, opts: CreateMetadataOptions) {
this.modules = opts.modules;
this.hoisted = opts.hoisted;
this.hydratedComponents = opts.hydratedComponents;
this.hydrationDirectives = opts.hydrationDirectives;
this.fileURL = new URL(fileURL); this.fileURL = new URL(fileURL);
this.metadataCache = new Map<any, ComponentMetadata | null>(); this.metadataCache = new Map<any, ComponentMetadata | null>();
} }
@ -30,24 +49,50 @@ export class Metadata {
return metadata?.componentExport || null; return metadata?.componentExport || null;
} }
// Recursively collect all of the hydrated components' paths. /**
getAllHydratedComponentPaths(): Set<string> { * Gets the paths of all hydrated components within this component
const paths = new Set<string>(); * and children components.
for (const component of this.hydratedComponents) { */
*hydratedComponentPaths() {
const found = new Set<string>();
for(const metadata of this.deepMetadata()) {
for (const component of metadata.hydratedComponents) {
const path = this.getPath(component); const path = this.getPath(component);
if (path) { if(path && !found.has(path)) {
paths.add(path); found.add(path);
yield path;
}
}
} }
} }
/**
* Gets all of the hydration specifiers used within this component.
*/
*hydrationDirectiveSpecifiers() {
for(const directive of this.hydrationDirectives) {
yield hydrationSpecifier(directive);
}
}
private *deepMetadata(): Generator<Metadata, void, unknown> {
// Yield self
yield this;
// Keep a Set of metadata objects so we only yield them out once.
const seen = new Set<Metadata>();
for (const { module: mod } of this.modules) { for (const { module: mod } of this.modules) {
if (typeof mod.$$metadata !== 'undefined') { if (typeof mod.$$metadata !== 'undefined') {
for (const path of mod.$$metadata.getAllHydratedComponentPaths()) { const md = mod.$$metadata as Metadata;
paths.add(path); // Call children deepMetadata() which will yield the child metadata
// and any of its children metadatas
for(const childMetdata of md.deepMetadata()) {
if(!seen.has(childMetdata)) {
seen.add(childMetdata);
yield childMetdata;
}
} }
} }
} }
return paths;
} }
private getComponentMetadata(Component: any): ComponentMetadata | null { private getComponentMetadata(Component: any): ComponentMetadata | null {
@ -83,12 +128,6 @@ export class Metadata {
} }
} }
interface CreateMetadataOptions {
modules: ModuleInfo[];
hydratedComponents: any[];
hoisted: any[];
}
export function createMetadata(fileURL: string, options: CreateMetadataOptions) { export function createMetadata(fileURL: string, options: CreateMetadataOptions) {
return new Metadata(fileURL, options.modules, options.hydratedComponents, options.hoisted); return new Metadata(fileURL, options);
} }

View file

@ -1,3 +1,10 @@
function formatList(values: string[]): string {
if (values.length === 1) {
return values[0];
}
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
}
export function serializeListValue(value: any) { export function serializeListValue(value: any) {
const hash: Record<string, any> = {}; const hash: Record<string, any> = {};
@ -27,3 +34,12 @@ export function serializeListValue(value: any) {
} }
} }
} }
/**
* Get the import specifier for a given hydration directive.
* @param hydrate The hydration directive such as `idle` or `visible`
* @returns
*/
export function hydrationSpecifier(hydrate: string) {
return `astro/client/${hydrate}.js`;
}

View file

@ -0,0 +1,108 @@
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 source = await fs.promises.readFile(id, 'utf8');
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) {
throw new Error(`Oh no, this should have been cached.`);
}
const transformResult = await compile(config, filename, source, viteTransform, opts);
cache.set(filename, transformResult);
return transformResult;
}

View file

@ -1,14 +1,12 @@
import type { TransformResult } from '@astrojs/compiler';
import type { SourceMapInput } from 'rollup';
import type vite from '../core/vite'; import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import fs from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../core/dev/index.js'; 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; const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
interface AstroPluginOptions { interface AstroPluginOptions {
@ -16,22 +14,8 @@ interface AstroPluginOptions {
devServer?: AstroDevServer; 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 */ /** 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; let viteTransform: TransformHook;
return { return {
name: '@astrojs/vite-plugin-astro', name: '@astrojs/vite-plugin-astro',
@ -40,57 +24,51 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
viteTransform = getViteTransform(resolvedConfig); viteTransform = getViteTransform(resolvedConfig);
}, },
// note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.) // note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id, opts) { async resolveId(id) {
if (!id.endsWith('.astro')) { // serve sub-part requests (*?astro) as virtual modules
return null; if (parseAstroRequest(id).query.astro) {
} return id;
// 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;
} }
}, },
}); 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);
// throw CSS transform errors here if encountered if (typeof query.index === 'undefined') {
if (cssTransformError) throw cssTransformError; 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;
}
try {
const transformResult = await cachedCompilation(config, id, source, viteTransform, opts);
// Compile all TypeScript to JavaScript. // Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning. // 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 { return {
code, code,
@ -126,20 +104,20 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
title: '🐛 BUG: `@astrojs/compiler` panic', title: '🐛 BUG: `@astrojs/compiler` panic',
body: `### Describe the Bug body: `### Describe the Bug
\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file. \`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file.
**${id.replace(fileURLToPath(config.projectRoot), '')}** **${id.replace(fileURLToPath(config.projectRoot), '')}**
\`\`\`astro \`\`\`astro
${source} ${source}
\`\`\` \`\`\`
`, `,
}); });
err.url = `https://github.com/withastro/astro/issues/new?${search.toString()}`; err.url = `https://github.com/withastro/astro/issues/new?${search.toString()}`;
err.message = `Error: Uh oh, the Astro compiler encountered an unrecoverable error! err.message = `Error: Uh oh, the Astro compiler encountered an unrecoverable error!
Please open Please open
a GitHub issue using the link below: a GitHub issue using the link below:
${err.url}`; ${err.url}`;
// TODO: remove stack replacement when compiler throws better errors // TODO: remove stack replacement when compiler throws better errors
err.stack = ` at ${id}`; err.stack = ` at ${id}`;
} }
@ -147,10 +125,9 @@ ${err.url}`;
throw err; throw err;
} }
}, },
// async handleHotUpdate(context) { async handleHotUpdate(context) {
// if (devServer) { // Invalidate the compilation cache so it recompiles
// return devServer.handleHotUpdate(context); invalidateCompilation(config, context.file);
// } },
// },
}; };
} }

View file

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

View file

@ -1,9 +1,10 @@
import type { RenderedChunk } from 'rollup'; 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 * as path from 'path';
import esbuild from 'esbuild'; 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'; const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
@ -45,14 +46,11 @@ function isPageStyleVirtualModule(id: string) {
} }
interface PluginOptions { interface PluginOptions {
astroStyleMap: Map<string, string>; internals: BuildInternals;
astroPageStyleMap: Map<string, string>;
chunkToReferenceIdMap: Map<string, string>;
pureCSSChunks: Set<RenderedChunk>;
} }
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks } = options; const { internals } = options;
const styleSourceMap = new Map<string, string>(); const styleSourceMap = new Map<string, string>();
return { return {
@ -94,10 +92,10 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
async load(id) { async load(id) {
if (isPageStyleVirtualModule(id)) { if (isPageStyleVirtualModule(id)) {
return astroPageStyleMap.get(id) || null; return internals.astroPageStyleMap.get(id) || null;
} }
if (isStyleVirtualModule(id)) { if (isStyleVirtualModule(id)) {
return astroStyleMap.get(id) || null; return internals.astroStyleMap.get(id) || null;
} }
return null; return null;
}, },
@ -127,6 +125,9 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
// if (!chunkCSS) return null; // dont output empty .css files // if (!chunkCSS) return null; // dont output empty .css files
if (isPureCSS) { if (isPureCSS) {
internals.pureCSSChunks.add(chunk);
}
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, { const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
loader: 'css', loader: 'css',
minify: true, minify: true,
@ -136,8 +137,14 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
type: 'asset', type: 'asset',
source: minifiedCSS, source: minifiedCSS,
}); });
pureCSSChunks.add(chunk);
chunkToReferenceIdMap.set(chunk.fileName, referenceId); 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; return null;
@ -145,8 +152,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
// Delete CSS chunks so JS is not produced for them. // Delete CSS chunks so JS is not produced for them.
generateBundle(opts, bundle) { generateBundle(opts, bundle) {
if (pureCSSChunks.size) { if (internals.pureCSSChunks.size) {
const pureChunkFilenames = new Set([...pureCSSChunks].map((chunk) => chunk.fileName)); const pureChunkFilenames = new Set([...internals.pureCSSChunks].map((chunk) => chunk.fileName));
const emptyChunkFiles = [...pureChunkFilenames] const emptyChunkFiles = [...pureChunkFilenames]
.map((file) => path.basename(file)) .map((file) => path.basename(file))
.join('|') .join('|')
@ -155,7 +162,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
for (const [chunkId, chunk] of Object.entries(bundle)) { for (const [chunkId, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk') { if (chunk.type === 'chunk') {
if (pureCSSChunks.has(chunk)) { if (internals.pureCSSChunks.has(chunk)) {
// Delete pure CSS chunks, these are JavaScript chunks that only import // Delete pure CSS chunks, these are JavaScript chunks that only import
// other CSS files, so are empty at the end of bundling. // other CSS files, so are empty at the end of bundling.
delete bundle[chunkId]; delete bundle[chunkId];

View file

@ -1,8 +1,9 @@
import type { AstroConfig, RouteCache } from '../@types/astro'; import type { AstroConfig, RouteCache } from '../@types/astro';
import type { LogOptions } from '../core/logger'; import type { LogOptions } from '../core/logger';
import type { ViteDevServer, Plugin as VitePlugin } from '../core/vite'; 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 { AllPagesData } from '../core/build/types';
import type { BuildInternals } from '../core/build/internal';
import parse5 from 'parse5'; import parse5 from 'parse5';
import srcsetParse from 'srcset-parse'; import srcsetParse from 'srcset-parse';
import * as npath from 'path'; import * as npath from 'path';
@ -26,20 +27,17 @@ const STATUS_CODE_RE = /^404$/;
interface PluginOptions { interface PluginOptions {
astroConfig: AstroConfig; astroConfig: AstroConfig;
astroStyleMap: Map<string, string>; internals: BuildInternals;
astroPageStyleMap: Map<string, string>;
chunkToReferenceIdMap: Map<string, string>;
logging: LogOptions; logging: LogOptions;
allPages: AllPagesData; allPages: AllPagesData;
pageNames: string[]; pageNames: string[];
pureCSSChunks: Set<RenderedChunk>;
origin: string; origin: string;
routeCache: RouteCache; routeCache: RouteCache;
viteServer: ViteDevServer; viteServer: ViteDevServer;
} }
export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { 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 // The filepath root of the src folder
const srcRoot = astroConfig.src.pathname; const srcRoot = astroConfig.src.pathname;
@ -161,7 +159,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
if (styles) { if (styles) {
const styleId = getAstroStyleId(pathname); const styleId = getAstroStyleId(pathname);
astroStyleMap.set(styleId, styles); internals.astroStyleMap.set(styleId, styles);
// Put this at the front of imports // Put this at the front of imports
assetImports.unshift(styleId); assetImports.unshift(styleId);
} }
@ -175,7 +173,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
if (assetImports.length) { if (assetImports.length) {
const pageStyleId = getAstroPageStyleId(pathname); const pageStyleId = getAstroPageStyleId(pathname);
const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n'); const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n');
astroPageStyleMap.set(pageStyleId, jsSource); internals.astroPageStyleMap.set(pageStyleId, jsSource);
assetInput.add(pageStyleId); assetInput.add(pageStyleId);
// preserve asset order in the order we encounter them // 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) // Sort CSS in order of appearance in HTML (pageStyleImportOrder)
// This is the “global ordering” used below // This is the “global ordering” used below
const sortedCSSChunks = [...pureCSSChunks]; const sortedCSSChunks = [...internals.pureCSSChunks];
sortedCSSChunks.sort((a, b) => { sortedCSSChunks.sort((a, b) => {
let aIndex = Math.min( let aIndex = Math.min(
...Object.keys(a.modules).map((id) => { ...Object.keys(a.modules).map((id) => {
@ -298,7 +296,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
const referenceIDs: string[] = []; const referenceIDs: string[] = [];
for (const chunkID of chunkModules) { for (const chunkID of chunkModules) {
const referenceID = chunkToReferenceIdMap.get(chunkID); const referenceID = internals.chunkToReferenceIdMap.get(chunkID);
if (referenceID) referenceIDs.push(referenceID); if (referenceID) referenceIDs.push(referenceID);
} }
for (const id of Object.keys(chunk.modules)) { for (const id of Object.keys(chunk.modules)) {

586
yarn.lock

File diff suppressed because it is too large Load diff