Compare commits
17 commits
main
...
no-more-vi
Author | SHA1 | Date | |
---|---|---|---|
|
992accf2b8 | ||
|
c1a15aa335 | ||
|
b3ad03bbb0 | ||
|
4d479a964c | ||
|
ce784126e1 | ||
|
3745916f2d | ||
|
858958490f | ||
|
3be5ddd05a | ||
|
98a868e8cb | ||
|
b7ff454453 | ||
|
19e2b5c56c | ||
|
f6e24d1a97 | ||
|
03447124a3 | ||
|
abdde962a9 | ||
|
6b3d4ed075 | ||
|
8f67687eef | ||
|
530b77ed0a |
33 changed files with 1733 additions and 418 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -148,13 +149,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)_
|
||||||
|
|
||||||
|
@ -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>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
examples/fast-build/astro.config.mjs
Normal file
11
examples/fast-build/astro.config.mjs
Normal 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()]
|
||||||
|
}
|
||||||
|
});
|
16
examples/fast-build/package.json
Normal file
16
examples/fast-build/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
24
examples/fast-build/src/components/Counter.vue
Normal file
24
examples/fast-build/src/components/Counter.vue
Normal 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>
|
20
examples/fast-build/src/components/Greeting.vue
Normal file
20
examples/fast-build/src/components/Greeting.vue
Normal 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>
|
BIN
examples/fast-build/src/images/penguin.jpg
Normal file
BIN
examples/fast-build/src/images/penguin.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
examples/fast-build/src/images/random.jpg
Normal file
BIN
examples/fast-build/src/images/random.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
38
examples/fast-build/src/pages/index.astro
Normal file
38
examples/fast-build/src/pages/index.astro
Normal 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>
|
3
examples/fast-build/src/styles/global.css
Normal file
3
examples/fast-build/src/styles/global.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
body {
|
||||||
|
background: lightcoral;
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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 = {};
|
astroConfig: this.config,
|
||||||
// Collect all routes ahead-of-time, before we start the build.
|
logging: this.logging,
|
||||||
// NOTE: This enforces that `getStaticPaths()` is only called once per route,
|
manifest: this.manifest,
|
||||||
// and is then cached across all future SSR builds. In the past, we've had trouble
|
origin,
|
||||||
// with parallelized builds without guaranteeing that this is called first.
|
routeCache: this.routeCache,
|
||||||
await Promise.all(
|
viteServer: this.viteServer,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
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,
|
allPages,
|
||||||
minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles
|
astroConfig: this.config,
|
||||||
outDir: fileURLToPath(this.config.dist),
|
logging: this.logging,
|
||||||
rollupOptions: {
|
origin: this.origin,
|
||||||
// The `input` will be populated in the build rollup plugin.
|
routeCache: this.routeCache,
|
||||||
input: [],
|
viteConfig: this.viteConfig,
|
||||||
output: { format: 'esm' },
|
});
|
||||||
},
|
} else {
|
||||||
target: 'es2020', // must match an esbuild target
|
await scanBasedBuild({
|
||||||
},
|
allPages,
|
||||||
plugins: [
|
astroConfig: this.config,
|
||||||
rollupPluginAstroBuildHTML({
|
logging: this.logging,
|
||||||
astroConfig: this.config,
|
origin: this.origin,
|
||||||
astroPageStyleMap,
|
pageNames,
|
||||||
astroStyleMap,
|
routeCache: this.routeCache,
|
||||||
chunkToReferenceIdMap,
|
viteConfig: this.viteConfig,
|
||||||
pureCSSChunks,
|
viteServer: this.viteServer,
|
||||||
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 : '/',
|
|
||||||
});
|
|
||||||
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 */
|
||||||
|
|
48
packages/astro/src/core/build/internal.ts
Normal file
48
packages/astro/src/core/build/internal.ts
Normal 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>(),
|
||||||
|
};
|
||||||
|
}
|
122
packages/astro/src/core/build/page-data.ts
Normal file
122
packages/astro/src/core/build/page-data.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
68
packages/astro/src/core/build/scan-based-build.ts
Normal file
68
packages/astro/src/core/build/scan-based-build.ts
Normal 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 : '/',
|
||||||
|
});
|
||||||
|
}
|
238
packages/astro/src/core/build/static-build.ts
Normal file
238
packages/astro/src/core/build/static-build.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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({}),
|
||||||
|
|
|
@ -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.?)
|
||||||
|
|
83
packages/astro/src/core/ssr/result.ts
Normal file
83
packages/astro/src/core/ssr/result.ts
Normal 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;
|
||||||
|
}
|
|
@ -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}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,14 +372,16 @@ 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
|
||||||
.filter(uniqueElements)
|
? []
|
||||||
.map((style) =>
|
: Array.from(result.styles)
|
||||||
renderElement('style', {
|
.filter(uniqueElements)
|
||||||
...style,
|
.map((style) =>
|
||||||
props: { ...style.props, 'astro-style': true },
|
renderElement('style', {
|
||||||
})
|
...style,
|
||||||
);
|
props: { ...style.props, 'astro-style': true },
|
||||||
|
})
|
||||||
|
);
|
||||||
let needsHydrationStyles = false;
|
let needsHydrationStyles = false;
|
||||||
const scripts = Array.from(result.scripts)
|
const scripts = Array.from(result.scripts)
|
||||||
.filter(uniqueElements)
|
.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; }' }));
|
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>) {
|
||||||
|
|
|
@ -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) {
|
*/
|
||||||
const path = this.getPath(component);
|
*hydratedComponentPaths() {
|
||||||
if (path) {
|
const found = new Set<string>();
|
||||||
paths.add(path);
|
for(const metadata of this.deepMetadata()) {
|
||||||
}
|
for (const component of metadata.hydratedComponents) {
|
||||||
}
|
const path = this.getPath(component);
|
||||||
|
if(path && !found.has(path)) {
|
||||||
for (const { module: mod } of this.modules) {
|
found.add(path);
|
||||||
if (typeof mod.$$metadata !== 'undefined') {
|
yield path;
|
||||||
for (const path of mod.$$metadata.getAllHydratedComponentPaths()) {
|
}
|
||||||
paths.add(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) {
|
||||||
|
if (typeof mod.$$metadata !== 'undefined') {
|
||||||
|
const md = mod.$$metadata as Metadata;
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`;
|
||||||
|
}
|
108
packages/astro/src/vite-plugin-astro/compile.ts
Normal file
108
packages/astro/src/vite-plugin-astro/compile.ts
Normal 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;
|
||||||
|
}
|
|
@ -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: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
|
// note: don’t 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
// 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 {
|
try {
|
||||||
// Transform from `.astro` to valid `.ts`
|
const transformResult = await cachedCompilation(config, id, source, viteTransform, opts);
|
||||||
// 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;
|
|
||||||
|
|
||||||
// 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);
|
||||||
// }
|
},
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
35
packages/astro/src/vite-plugin-astro/query.ts
Normal file
35
packages/astro/src/vite-plugin-astro/query.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,17 +125,26 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
||||||
// if (!chunkCSS) return null; // don’t output empty .css files
|
// if (!chunkCSS) return null; // don’t output empty .css files
|
||||||
|
|
||||||
if (isPureCSS) {
|
if (isPureCSS) {
|
||||||
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
|
internals.pureCSSChunks.add(chunk);
|
||||||
loader: 'css',
|
}
|
||||||
minify: true,
|
|
||||||
});
|
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
|
||||||
const referenceId = this.emitFile({
|
loader: 'css',
|
||||||
name: chunk.name + '.css',
|
minify: true,
|
||||||
type: 'asset',
|
});
|
||||||
source: minifiedCSS,
|
const referenceId = this.emitFile({
|
||||||
});
|
name: chunk.name + '.css',
|
||||||
pureCSSChunks.add(chunk);
|
type: 'asset',
|
||||||
chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
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;
|
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];
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
Loading…
Reference in a new issue