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
|
||||
with:
|
||||
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: |
|
||||
cache-node_modules-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
cache-node_modules-${{ hashFiles('**/yarn.lock') }}-
|
||||
cache-node_modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
cache-node_modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: yarn install --prefer-offline --frozen-lockfile --ignore-engines --registry https://registry.npmjs.org --network-timeout 300000
|
||||
|
|
|
@ -58,7 +58,7 @@ export const SIDEBAR = {
|
|||
|
||||
{ text: 'Anleitungen', header: true },
|
||||
{ text: 'Styling & CSS', link: 'de/guides/styling' },
|
||||
|
||||
|
||||
{ text: 'Referenz', header: true },
|
||||
],
|
||||
nl: [
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
```html
|
||||
<!-- src/components/MeineKomponente.astro -->
|
||||
<style>
|
||||
|
@ -25,7 +24,9 @@ Standardmäßig werden in Astro-Komponenten alle Styles nur auf Elemente im Rahm
|
|||
</style>
|
||||
|
||||
<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.
|
||||
|
@ -63,7 +64,7 @@ _Beachte: `Astro.resolve()` ist ein nützliches Hilfsmittel, um Verweise auf Dat
|
|||
|
||||
#### Styling untergeordneter Elemente
|
||||
|
||||
Falls du Styles, die im Scope der Komponente gesetzt werden, auch auf untergeordnete Komponenten anwenden willst, kannst du auf die `:global()`-Funktion aus den [CSS-Modules][css-modules] zurückgreifen:
|
||||
Falls du Styles, die im Scope der Komponente gesetzt werden, auch auf untergeordnete Komponenten anwenden willst, kannst du auf die `:global()`-Funktion aus den [CSS-Modules][css-modules] zurückgreifen:
|
||||
|
||||
```astro
|
||||
<!-- src/components/MeineKomponente.astro -->
|
||||
|
@ -88,7 +89,7 @@ import PostContent from './Post.astro';
|
|||
</article>
|
||||
```
|
||||
|
||||
Dies ist eine sehr gute Methode, um Dinge zu stylen wie Blogposts oder Dokumente, die mit Inhalten aus einem CMS außerhalb von Astro gefüttert werden. Aber Vorsicht, wenn untergeordnete Elemente frei von Abhängigkeiten gestaltet werden, bricht dies auch die Verkapselung der Komponente auf. Das Arbeiten mit Komponenten die unterschiedlich aussehen, abhängig davon ob sie ein bestimmtes übergeordnetes Element haben oder nicht, kann sehr schnell unübersichtlich werden.
|
||||
Dies ist eine sehr gute Methode, um Dinge zu stylen wie Blogposts oder Dokumente, die mit Inhalten aus einem CMS außerhalb von Astro gefüttert werden. Aber Vorsicht, wenn untergeordnete Elemente frei von Abhängigkeiten gestaltet werden, bricht dies auch die Verkapselung der Komponente auf. Das Arbeiten mit Komponenten die unterschiedlich aussehen, abhängig davon ob sie ein bestimmtes übergeordnetes Element haben oder nicht, kann sehr schnell unübersichtlich werden.
|
||||
|
||||
#### Globale Styles innerhalb eines `<style>`-Tags
|
||||
|
||||
|
@ -127,7 +128,7 @@ Es wird empfohlen diese Methoden nur dort einzusetzen, wo ein `<link>`-Tag nicht
|
|||
|
||||
## Autoprefixer
|
||||
|
||||
[Autoprefixer][autoprefixer] kümmert sich für dich um Browser-übergreifende CSS-Kompatibilität. Installiere autoprefixer (`npm install --save-dev autoprefixer`) und füge eine Datei mit dem Namen `postcss.config.cjs` deinem Hauptverzeichnis hinzu:
|
||||
[Autoprefixer][autoprefixer] kümmert sich für dich um Browser-übergreifende CSS-Kompatibilität. Installiere autoprefixer (`npm install --save-dev autoprefixer`) und füge eine Datei mit dem Namen `postcss.config.cjs` deinem Hauptverzeichnis hinzu:
|
||||
|
||||
```js
|
||||
// postcss.config.cjs
|
||||
|
@ -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:
|
||||
|
||||
| Framework | Globales CSS | Scoped CSS | CSS-Modules |
|
||||
| :--------------- | :----------: | :--------: | :----------: |
|
||||
| `.astro` | ✅ | ✅ | N/A¹ |
|
||||
| `.jsx` \| `.tsx` | ✅ | ❌ | ✅ |
|
||||
| `.vue` | ✅ | ✅ | ✅ |
|
||||
| `.svelte` | ✅ | ✅ | ❌ |
|
||||
|
||||
| Framework | Globales CSS | Scoped CSS | CSS-Modules |
|
||||
| :--------------- | :----------: | :--------: | :---------: |
|
||||
| `.astro` | ✅ | ✅ | N/A¹ |
|
||||
| `.jsx` \| `.tsx` | ✅ | ❌ | ✅ |
|
||||
| `.vue` | ✅ | ✅ | ✅ |
|
||||
| `.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)_
|
||||
|
||||
|
@ -228,7 +228,7 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
Damit bist du ausgerüstet, um Tailwind einzusetzen! Der von uns empfohlene Ansatz ist, eine Datei `src/styles/global.css` (oder wie du dein globales Stylesheet bevorzugterweise nennst) mit den [Tailwind-Utilities][tailwind-utilities] darin zu erzeugen - ungefähr in dieser Form:
|
||||
Damit bist du ausgerüstet, um Tailwind einzusetzen! Der von uns empfohlene Ansatz ist, eine Datei `src/styles/global.css` (oder wie du dein globales Stylesheet bevorzugterweise nennst) mit den [Tailwind-Utilities][tailwind-utilities] darin zu erzeugen - ungefähr in dieser Form:
|
||||
|
||||
```css
|
||||
/* src/styles/global.css */
|
||||
|
@ -260,7 +260,7 @@ Mit der [Version 0.20.0](https://github.com/withastro/astro/releases/tag/astro%4
|
|||
|
||||
### 🎭 PostCSS
|
||||
|
||||
PostCSS zu verwenden ist so einfach wie eine [`postcss.config.cjs`](https://github.com/postcss/postcss#usage)-Datei in deinem Hauptverzeichnis zu erzeugen.
|
||||
PostCSS zu verwenden ist so einfach wie eine [`postcss.config.cjs`](https://github.com/postcss/postcss#usage)-Datei in deinem Hauptverzeichnis zu erzeugen.
|
||||
|
||||
Beachte, dass dieses Plugin sämtliches CSS in deinem Projekt verarbeitet, einschließlich jeglicher Dateien die nach CSS kompiliert wurden (wie zum Beispiel `.scss`-Sass-Dateien).
|
||||
|
||||
|
@ -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].
|
||||
|
||||
_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
|
||||
|
||||
|
@ -320,9 +320,9 @@ Du benötigst keine Erläuterung zu Komponenten-basiertem Design. Dir ist bereit
|
|||
</button>
|
||||
```
|
||||
|
||||
_Beachte: Wir verwenden hier in sämtlichen Beispielen `lang="scss"`, welches das Verschachteln und Teilen von [Farben und Variablen][sass-use] stark vereinfacht. Dies ist jedoch gänzlich optional, und du kannst ebenso gut normales CSS verwenden._
|
||||
_Beachte: Wir verwenden hier in sämtlichen Beispielen `lang="scss"`, welches das Verschachteln und Teilen von [Farben und Variablen][sass-use] stark vereinfacht. Dies ist jedoch gänzlich optional, und du kannst ebenso gut normales CSS verwenden._
|
||||
|
||||
Die `.btn`-Klasse ist auf die Komponente begrenzt und wird nicht über das Dokument hinaus wirksam. Dies bedeutet, du kannst dich **auf das Styling und musst dich nicht auf die Benennung konzentrieren**. Dieser Ansatz, der das Lokale an den Anfang stellt, fügt sich sehr gut in das ESM-getriebene Design von Astro, das Einkapselung und Wiederverwendbarkeit über eine globale Wirksamkeit stellt. Auch wenn es sich um ein einfaches Beispiel handelt, sollte festgehalten werden, dass dies **extrem gut skaliert**. Und für den Fall, dass du gemeinsame Werte zwischen Komponenten teilen willst, empfehlen wir das [Sass-Modulsystem][sass-use], das sehr einfach zu verwenden ist und sich perfekt in ein Design einfügt, in dem die Komponente an den Anfang gestellt wird.
|
||||
Die `.btn`-Klasse ist auf die Komponente begrenzt und wird nicht über das Dokument hinaus wirksam. Dies bedeutet, du kannst dich **auf das Styling und musst dich nicht auf die Benennung konzentrieren**. Dieser Ansatz, der das Lokale an den Anfang stellt, fügt sich sehr gut in das ESM-getriebene Design von Astro, das Einkapselung und Wiederverwendbarkeit über eine globale Wirksamkeit stellt. Auch wenn es sich um ein einfaches Beispiel handelt, sollte festgehalten werden, dass dies **extrem gut skaliert**. Und für den Fall, dass du gemeinsame Werte zwischen Komponenten teilen willst, empfehlen wir das [Sass-Modulsystem][sass-use], das sehr einfach zu verwenden ist und sich perfekt in ein Design einfügt, in dem die Komponente an den Anfang gestellt wird.
|
||||
|
||||
Im Kontrast zu diesem Ansatz erlaubt Astro auch globale Styles mittels der `:global()` und `<style global>`-Notlösungen. Es sollte jedoch soweit möglich vermieden werden sie einzusetzen. Ein einfaches Beispiel: Nehmen wir an, du hast deinen Button in einer `<Nav />`-Komponente verwendet und willst ihn dort anders gestalten. Du könntest versucht sein, das folgendermaßen zu probieren:
|
||||
|
||||
|
@ -374,7 +374,7 @@ An anderer Stelle kannst du nun `<Button theme="nav">` verwenden, um zu bestimme
|
|||
|
||||
Vor Kurzem gab es eine Debatte über die ausschließliche Verwendung von Komponenten-Styles im Scope vs. die ausschließliche Verwendung von Utility-CSS. Aber wir stimmen Leuten wie Sarah Dayan zu, die fragen, [warum können wir nicht beides haben][utility-css]? Tatsache ist doch, dass während es großartig ist, Styles im Scope der Komponente zu haben, es immer noch hunderte von Male vorkommt, dass wenn die Website vollständig zusammengesetzt ist, zwei Elemente nicht _gut_ zusammenspielen und eines von beiden einen kleinen Stupser braucht. Oder es wird vielleicht eine abweichende Behandlung eines Textes in einer Komponenten-Instanz benötigt.
|
||||
|
||||
Zwar ist die Vorstellung von perfekten, makellosen Komponenten schön, aber sie ist auch unrealistisch. Kein Design-System ist absolut perfekt, und jedes Design-System zeigt auch Ungereimtheiten. Und es passiert bei dem Versuch diese Ungereimtheiten aufzulösen, dass Komponenten ohne Utility-CSS durcheinander geraten. Utility-CSS ist großartig darin kleinere Optimierungen hinzuzufügen, um die Website fertigzustellen und ausliefern zu können. Aber es ist an sich auch unvollständig auf ganz eigene Art - wenn du z. B. jemals versuchst responsive Styles oder Fokussierungen mit Utility-CSS zu verwalten, kann das schnell zu einem großen Durcheinander werden!
|
||||
Zwar ist die Vorstellung von perfekten, makellosen Komponenten schön, aber sie ist auch unrealistisch. Kein Design-System ist absolut perfekt, und jedes Design-System zeigt auch Ungereimtheiten. Und es passiert bei dem Versuch diese Ungereimtheiten aufzulösen, dass Komponenten ohne Utility-CSS durcheinander geraten. Utility-CSS ist großartig darin kleinere Optimierungen hinzuzufügen, um die Website fertigzustellen und ausliefern zu können. Aber es ist an sich auch unvollständig auf ganz eigene Art - wenn du z. B. jemals versuchst responsive Styles oder Fokussierungen mit Utility-CSS zu verwalten, kann das schnell zu einem großen Durcheinander werden!
|
||||
**Utility-CSS funktioniert am besten in Partnerschaft mit Styles im Scope der Komponente**. Und um so leicht wie möglich anwendbar zu sein, sollte Utility-CSS global sein (und sollte möglicherweise auch das einzige globale CSS sein - vielleicht neben reset.css), so dass du nicht mit Importen arbeiten musst, die allesamt willkürlich sind.
|
||||
|
||||
Einige größere Probleme, die am besten mit Utility-CSS gelöst werden, sind:
|
||||
|
@ -389,10 +389,7 @@ In Astro empfehlen wir folgendes Setup hierfür:
|
|||
|
||||
```html
|
||||
<head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href={Astro.resolve("../styles/global.css")}
|
||||
>
|
||||
<link rel="stylesheet" href={Astro.resolve("../styles/global.css")} >
|
||||
</head>
|
||||
```
|
||||
|
||||
|
@ -425,7 +422,7 @@ Zusammengefasst kannst du dir Styles im Scope der Komponente als das Rückgrat d
|
|||
|
||||
#### Vorschlag #1: Unterteile deine App in Layout-Komponenten und Basis-Komponenten
|
||||
|
||||
Sicher wird diese Anleitung niemals lang genug sein, um die Frage zu beantworten _"Wie sollte eine Seite aufgebaut sein?"_ (das ist ein [Design-Problem!][cassie-evans-css]).
|
||||
Sicher wird diese Anleitung niemals lang genug sein, um die Frage zu beantworten _"Wie sollte eine Seite aufgebaut sein?"_ (das ist ein [Design-Problem!][cassie-evans-css]).
|
||||
Und doch liegt darin versteckt auch eine etwas spezifischere Frage, die wir beantworten _können_: _"Ein bestimmtes Layout angenommen - wie sollten Komponenten und Styles darin organisiert sein?"_ Die Antwort ist, **brenne niemals das Layout in Komponenten ein**. Erzeuge Layout-Komponenten, die das Layout bestimmen, und Basis-Komponenten (Buttons, Karten etc.) die nicht das Layout bestimmen. _Was bedeutet das?_ Gehen wir das an einem Beispiel durch, damit es klarer wird. Angenommen wir haben eine Seite, die folgendermaßen aussieht (die Zahlen stehen für unterschiedliche Komponenten):
|
||||
|
||||
```
|
||||
|
@ -559,7 +556,7 @@ Viele Front-End-Entwicklerinnen und -Entwickler kennen den folgenden Gedankengan
|
|||
|
||||
Während die Logik in sich stimmig ist, ist es in Wirklichkeit doch so, dass #2 nur selten auf ein Projekt zutrifft. Möglicherweise wurden viele Teile der Website nicht entwickelt, um in diese netten, gepflegten 12-Säulen-Raster einer Bibliothek zu passen. Sogar relativ bescheidene Websites können _hunderte_ Layouts enthalten, wenn du sämtliche Breakpoints mit einrechnest. Frage dich einmal selbst: _Wenn die Website, die ich baue, wirklich soviele unterschiedliche Layouts beinhaltet, warum verwende ich dann eine schwergewichtige Grid-Bibliothek, die mir nur generische Layouts ermöglicht?_
|
||||
|
||||
Ein paar gut geschriebene Zeilen CSS-Grid hier und da werden sich perfekt an jede Situation anpassen; das Ganze ist höchstwahrscheinlich leichtgewichtiger und einfacher zu verwalten, als die schwergewichtige Bibliothek, mit der du doch so lange gekämpft hast. Anders herum betrachtet: Wenn du schon einige Stunden benötigst, um eine proprietäre Styling-Bibliothek zu lernen, dich mit ihr auseinanderzusetzen, Probleme zu melden etc., wäre es nicht besser diese Zeit darauf zu verwenden den Umgang mit Flexbox und Grid zu erlernen? Viele Leute brauchen nur eine Stunde, um die Grundlagen ausreichend zu verstehen - und damit kommt man schon ziemlich weit! Es gibt großartige kostenlose Lernmöglichkeiten, in die du deine Zeit investieren kannnst:
|
||||
Ein paar gut geschriebene Zeilen CSS-Grid hier und da werden sich perfekt an jede Situation anpassen; das Ganze ist höchstwahrscheinlich leichtgewichtiger und einfacher zu verwalten, als die schwergewichtige Bibliothek, mit der du doch so lange gekämpft hast. Anders herum betrachtet: Wenn du schon einige Stunden benötigst, um eine proprietäre Styling-Bibliothek zu lernen, dich mit ihr auseinanderzusetzen, Probleme zu melden etc., wäre es nicht besser diese Zeit darauf zu verwenden den Umgang mit Flexbox und Grid zu erlernen? Viele Leute brauchen nur eine Stunde, um die Grundlagen ausreichend zu verstehen - und damit kommt man schon ziemlich weit! Es gibt großartige kostenlose Lernmöglichkeiten, in die du deine Zeit investieren kannnst:
|
||||
|
||||
- [Flexbox Froggy](https://flexboxfroggy.com/)
|
||||
- [CSS Grid Garden](https://cssgridgarden.com/)
|
||||
|
|
|
@ -26,12 +26,8 @@ You can add import aliases from either `tsconfig.json` or `jsconfig.json`.
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"asset:*": [
|
||||
"src/assets/*?url"
|
||||
],
|
||||
"component:*": [
|
||||
"src/components/*.astro"
|
||||
]
|
||||
"asset:*": ["src/assets/*?url"],
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^0.5.4",
|
||||
"@astrojs/compiler": "^0.6.0",
|
||||
"@astrojs/language-server": "^0.8.2",
|
||||
"@astrojs/markdown-remark": "^0.5.0",
|
||||
"@astrojs/prism": "0.3.0",
|
||||
|
|
|
@ -363,11 +363,14 @@ export interface SSRElement {
|
|||
export interface SSRMetadata {
|
||||
renderers: Renderer[];
|
||||
pathname: string;
|
||||
experimentalStaticBuild: boolean;
|
||||
}
|
||||
|
||||
export interface SSRResult {
|
||||
styles: Set<SSRElement>;
|
||||
scripts: Set<SSRElement>;
|
||||
links: Set<SSRElement>;
|
||||
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
_metadata: SSRMetadata;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ interface CLIState {
|
|||
hostname?: string;
|
||||
port?: number;
|
||||
config?: string;
|
||||
experimentalStaticBuild?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,6 +38,7 @@ function resolveArgs(flags: Arguments): CLIState {
|
|||
port: typeof flags.port === 'number' ? flags.port : undefined,
|
||||
config: typeof flags.config === 'string' ? flags.config : undefined,
|
||||
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
|
||||
experimentalStaticBuild: typeof flags.experimentalStaticBuild === 'boolean' ? flags.experimentalStaticBuild : false,
|
||||
};
|
||||
|
||||
if (flags.version) {
|
||||
|
@ -73,6 +75,7 @@ function printHelp() {
|
|||
--config <path> Specify the path to the Astro config file.
|
||||
--project-root <path> Specify the path to the project root folder.
|
||||
--no-sitemap Disable sitemap generation (build only).
|
||||
--experimental-static-build A more performant build that expects assets to be define statically.
|
||||
--verbose Enable verbose logging
|
||||
--silent Disable logging
|
||||
--version Show the version number and exit.
|
||||
|
@ -92,6 +95,7 @@ function mergeCLIFlags(astroConfig: AstroConfig, flags: CLIState['options']) {
|
|||
if (typeof flags.site === 'string') astroConfig.buildOptions.site = flags.site;
|
||||
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
|
||||
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
|
||||
if (typeof flags.experimentalStaticBuild === 'boolean') astroConfig.buildOptions.experimentalStaticBuild = flags.experimentalStaticBuild;
|
||||
}
|
||||
|
||||
/** The primary CLI action */
|
||||
|
|
|
@ -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 { AllPagesData } from './types';
|
||||
import type { RenderedChunk } from 'rollup';
|
||||
|
||||
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
|
||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||
import fs from 'fs';
|
||||
import * as colors from 'kleur/colors';
|
||||
import { performance } from 'perf_hooks';
|
||||
import vite, { ViteDevServer } from '../vite.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
|
||||
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
|
||||
import { preload as ssrPreload } from '../ssr/index.js';
|
||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
||||
import { generateRssFunction } from '../ssr/rss.js';
|
||||
import { createRouteManifest } from '../ssr/routing.js';
|
||||
import { generateSitemap } from '../ssr/sitemap.js';
|
||||
import { collectPagesData } from './page-data.js';
|
||||
import { build as scanBasedBuild } from './scan-based-build.js';
|
||||
import { staticBuild } from './static-build.js';
|
||||
|
||||
export interface BuildOptions {
|
||||
mode?: string;
|
||||
|
@ -76,137 +71,45 @@ class AstroBuilder {
|
|||
debug(logging, 'build', timerMessage('Vite started', timer.viteStart));
|
||||
|
||||
timer.loadStart = performance.now();
|
||||
const assets: Record<string, string> = {};
|
||||
const allPages: AllPagesData = {};
|
||||
// Collect all routes ahead-of-time, before we start the build.
|
||||
// NOTE: This enforces that `getStaticPaths()` is only called once per route,
|
||||
// and is then cached across all future SSR builds. In the past, we've had trouble
|
||||
// with parallelized builds without guaranteeing that this is called first.
|
||||
await Promise.all(
|
||||
this.manifest.routes.map(async (route) => {
|
||||
// static route:
|
||||
if (route.pathname) {
|
||||
allPages[route.component] = {
|
||||
route,
|
||||
paths: [route.pathname],
|
||||
preload: await ssrPreload({
|
||||
astroConfig: this.config,
|
||||
filePath: new URL(`./${route.component}`, this.config.projectRoot),
|
||||
logging,
|
||||
mode: 'production',
|
||||
origin,
|
||||
pathname: route.pathname,
|
||||
route,
|
||||
routeCache: this.routeCache,
|
||||
viteServer,
|
||||
})
|
||||
.then((routes) => {
|
||||
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`);
|
||||
return routes;
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`);
|
||||
throw err;
|
||||
}),
|
||||
};
|
||||
return;
|
||||
}
|
||||
// dynamic route:
|
||||
const result = await this.getStaticPathsForRoute(route)
|
||||
.then((routes) => {
|
||||
const label = routes.paths.length === 1 ? 'page' : 'pages';
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${routes.paths.length} ${label}]`)}`);
|
||||
return routes;
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
|
||||
throw err;
|
||||
});
|
||||
if (result.rss?.xml) {
|
||||
const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), this.config.dist);
|
||||
if (assets[fileURLToPath(rssFile)]) {
|
||||
throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
||||
}
|
||||
assets[fileURLToPath(rssFile)] = result.rss.xml;
|
||||
}
|
||||
allPages[route.component] = {
|
||||
route,
|
||||
paths: result.paths,
|
||||
preload: await ssrPreload({
|
||||
astroConfig: this.config,
|
||||
filePath: new URL(`./${route.component}`, this.config.projectRoot),
|
||||
logging,
|
||||
mode: 'production',
|
||||
origin,
|
||||
pathname: result.paths[0],
|
||||
route,
|
||||
routeCache: this.routeCache,
|
||||
viteServer,
|
||||
}),
|
||||
};
|
||||
})
|
||||
);
|
||||
const { assets, allPages } = await collectPagesData({
|
||||
astroConfig: this.config,
|
||||
logging: this.logging,
|
||||
manifest: this.manifest,
|
||||
origin,
|
||||
routeCache: this.routeCache,
|
||||
viteServer: this.viteServer,
|
||||
});
|
||||
debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart));
|
||||
|
||||
// Pure CSS chunks are chunks that only contain CSS.
|
||||
// This is all of them, and chunkToReferenceIdMap maps them to a hash id used to find the final file.
|
||||
const pureCSSChunks = new Set<RenderedChunk>();
|
||||
const chunkToReferenceIdMap = new Map<string, string>();
|
||||
|
||||
// This is a mapping of pathname to the string source of all collected
|
||||
// inline <style> for a page.
|
||||
const astroStyleMap = new Map<string, string>();
|
||||
// This is a virtual JS module that imports all dependent styles for a page.
|
||||
const astroPageStyleMap = new Map<string, string>();
|
||||
|
||||
// The names of each pages
|
||||
const pageNames: string[] = [];
|
||||
|
||||
// Bundle the assets in your final build: This currently takes the HTML output
|
||||
// of every page (stored in memory) and bundles the assets pointed to on those pages.
|
||||
timer.buildStart = performance.now();
|
||||
await vite.build({
|
||||
logLevel: 'error',
|
||||
mode: 'production',
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles
|
||||
outDir: fileURLToPath(this.config.dist),
|
||||
rollupOptions: {
|
||||
// The `input` will be populated in the build rollup plugin.
|
||||
input: [],
|
||||
output: { format: 'esm' },
|
||||
},
|
||||
target: 'es2020', // must match an esbuild target
|
||||
},
|
||||
plugins: [
|
||||
rollupPluginAstroBuildHTML({
|
||||
astroConfig: this.config,
|
||||
astroPageStyleMap,
|
||||
astroStyleMap,
|
||||
chunkToReferenceIdMap,
|
||||
pureCSSChunks,
|
||||
logging,
|
||||
origin,
|
||||
allPages,
|
||||
pageNames,
|
||||
routeCache: this.routeCache,
|
||||
viteServer,
|
||||
}),
|
||||
rollupPluginAstroBuildCSS({
|
||||
astroPageStyleMap,
|
||||
astroStyleMap,
|
||||
chunkToReferenceIdMap,
|
||||
pureCSSChunks,
|
||||
}),
|
||||
...(viteConfig.plugins || []),
|
||||
],
|
||||
publicDir: viteConfig.publicDir,
|
||||
root: viteConfig.root,
|
||||
envPrefix: 'PUBLIC_',
|
||||
server: viteConfig.server,
|
||||
base: this.config.buildOptions.site ? new URL(this.config.buildOptions.site).pathname : '/',
|
||||
});
|
||||
|
||||
// Use the new faster static based build.
|
||||
if (this.config.buildOptions.experimentalStaticBuild) {
|
||||
await staticBuild({
|
||||
allPages,
|
||||
astroConfig: this.config,
|
||||
logging: this.logging,
|
||||
origin: this.origin,
|
||||
routeCache: this.routeCache,
|
||||
viteConfig: this.viteConfig,
|
||||
});
|
||||
} else {
|
||||
await scanBasedBuild({
|
||||
allPages,
|
||||
astroConfig: this.config,
|
||||
logging: this.logging,
|
||||
origin: this.origin,
|
||||
pageNames,
|
||||
routeCache: this.routeCache,
|
||||
viteConfig: this.viteConfig,
|
||||
viteServer: this.viteServer,
|
||||
});
|
||||
}
|
||||
debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart));
|
||||
|
||||
// Write any additionally generated assets to disk.
|
||||
|
@ -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 */
|
||||
private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) {
|
||||
/* 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')])
|
||||
.optional()
|
||||
.default('directory'),
|
||||
experimentalStaticBuild: z.boolean().optional().default(false),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
|
|
@ -2,8 +2,6 @@ import type { BuildResult } from 'esbuild';
|
|||
import type vite from '../vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroGlobal,
|
||||
AstroGlobalPartial,
|
||||
ComponentInstance,
|
||||
GetStaticPathsResult,
|
||||
Params,
|
||||
|
@ -14,20 +12,21 @@ import type {
|
|||
RuntimeMode,
|
||||
SSRElement,
|
||||
SSRError,
|
||||
SSRResult,
|
||||
} from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { AstroComponentFactory } from '../../runtime/server/index';
|
||||
|
||||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { renderPage, renderSlot } from '../../runtime/server/index.js';
|
||||
import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency } from '../util.js';
|
||||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { codeFrame, resolveDependency } from '../util.js';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { injectTags } from './html.js';
|
||||
import { generatePaginateFunction } from './paginate.js';
|
||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||
import { createResult } from './result.js';
|
||||
|
||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||
|
||||
|
@ -138,6 +137,78 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions)
|
|||
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 */
|
||||
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
|
||||
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.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
|
||||
// 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>(),
|
||||
/** 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,
|
||||
},
|
||||
const result = createResult({ astroConfig, origin, params, pathname, renderers });
|
||||
result.resolve = async (s: string) => {
|
||||
const [, path] = await viteServer.moduleGraph.resolveUrl(s);
|
||||
return path;
|
||||
};
|
||||
|
||||
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
|
||||
if (mode === 'development') {
|
||||
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.?)
|
||||
|
|
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 { SSRElement } from '../../@types/astro';
|
||||
import type { SSRElement, SSRResult } from '../../@types/astro';
|
||||
import { valueToEstree } from 'estree-util-value-to-estree';
|
||||
import * as astring from 'astring';
|
||||
import { serializeListValue } from './util.js';
|
||||
import { hydrationSpecifier, serializeListValue } from './util.js';
|
||||
|
||||
const { generate, GENERATOR } = astring;
|
||||
|
||||
|
@ -69,6 +69,9 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
|
|||
extracted.hydration.componentExport.value = value;
|
||||
break;
|
||||
}
|
||||
case 'client:component-hydration': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
extracted.hydration.directive = key.split(':')[1];
|
||||
extracted.hydration.value = value;
|
||||
|
@ -98,13 +101,14 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
|
|||
|
||||
interface HydrateScriptOptions {
|
||||
renderer: any;
|
||||
result: SSRResult;
|
||||
astroId: string;
|
||||
props: Record<string | number, any>;
|
||||
}
|
||||
|
||||
/** For hydrated components, generate a <script type="module"> to load the component */
|
||||
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;
|
||||
|
||||
if (!componentExport) {
|
||||
|
@ -117,16 +121,17 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
|
|||
}
|
||||
|
||||
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);
|
||||
`
|
||||
: `await import("${componentUrl}");
|
||||
return () => {};
|
||||
`;
|
||||
|
||||
|
||||
const hydrationScript = {
|
||||
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 () => {
|
||||
${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.
|
||||
// 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>`;
|
||||
}
|
||||
|
@ -372,14 +372,16 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
|
|||
// styles and scripts into the head.
|
||||
export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) {
|
||||
const template = await renderToString(result, Component, props, children);
|
||||
const styles = Array.from(result.styles)
|
||||
.filter(uniqueElements)
|
||||
.map((style) =>
|
||||
renderElement('style', {
|
||||
...style,
|
||||
props: { ...style.props, 'astro-style': true },
|
||||
})
|
||||
);
|
||||
const styles = result._metadata.experimentalStaticBuild
|
||||
? []
|
||||
: Array.from(result.styles)
|
||||
.filter(uniqueElements)
|
||||
.map((style) =>
|
||||
renderElement('style', {
|
||||
...style,
|
||||
props: { ...style.props, 'astro-style': true },
|
||||
})
|
||||
);
|
||||
let needsHydrationStyles = false;
|
||||
const scripts = Array.from(result.scripts)
|
||||
.filter(uniqueElements)
|
||||
|
@ -396,12 +398,16 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac
|
|||
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' }));
|
||||
}
|
||||
|
||||
const links = Array.from(result.links)
|
||||
.filter(uniqueElements)
|
||||
.map((link) => renderElement('link', link));
|
||||
|
||||
// inject styles & scripts at end of <head>
|
||||
let headPos = template.indexOf('</head>');
|
||||
if (headPos === -1) {
|
||||
return styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts
|
||||
return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts
|
||||
}
|
||||
return template.substring(0, headPos) + styles.join('\n') + scripts.join('\n') + template.substring(headPos);
|
||||
return template.substring(0, headPos) + links.join('\n') + styles.join('\n') + scripts.join('\n') + template.substring(headPos);
|
||||
}
|
||||
|
||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { hydrationSpecifier } from './util.js';
|
||||
|
||||
interface ModuleInfo {
|
||||
module: Record<string, any>;
|
||||
specifier: string;
|
||||
|
@ -8,10 +10,27 @@ interface ComponentMetadata {
|
|||
componentUrl: string;
|
||||
}
|
||||
|
||||
interface CreateMetadataOptions {
|
||||
modules: ModuleInfo[];
|
||||
hydratedComponents: any[];
|
||||
hydrationDirectives: Set<string>;
|
||||
hoisted: any[];
|
||||
}
|
||||
|
||||
export class Metadata {
|
||||
public fileURL: URL;
|
||||
public modules: ModuleInfo[];
|
||||
public hoisted: any[];
|
||||
public hydratedComponents: any[];
|
||||
public hydrationDirectives: Set<string>;
|
||||
|
||||
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.metadataCache = new Map<any, ComponentMetadata | null>();
|
||||
}
|
||||
|
@ -30,24 +49,50 @@ export class Metadata {
|
|||
return metadata?.componentExport || null;
|
||||
}
|
||||
|
||||
// Recursively collect all of the hydrated components' paths.
|
||||
getAllHydratedComponentPaths(): Set<string> {
|
||||
const paths = new Set<string>();
|
||||
for (const component of this.hydratedComponents) {
|
||||
const path = this.getPath(component);
|
||||
if (path) {
|
||||
paths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { module: mod } of this.modules) {
|
||||
if (typeof mod.$$metadata !== 'undefined') {
|
||||
for (const path of mod.$$metadata.getAllHydratedComponentPaths()) {
|
||||
paths.add(path);
|
||||
/**
|
||||
* Gets the paths of all hydrated components within this component
|
||||
* and children components.
|
||||
*/
|
||||
*hydratedComponentPaths() {
|
||||
const found = new Set<string>();
|
||||
for(const metadata of this.deepMetadata()) {
|
||||
for (const component of metadata.hydratedComponents) {
|
||||
const path = this.getPath(component);
|
||||
if(path && !found.has(path)) {
|
||||
found.add(path);
|
||||
yield path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all of the hydration specifiers used within this component.
|
||||
*/
|
||||
*hydrationDirectiveSpecifiers() {
|
||||
for(const directive of this.hydrationDirectives) {
|
||||
yield hydrationSpecifier(directive);
|
||||
}
|
||||
}
|
||||
|
||||
private *deepMetadata(): Generator<Metadata, void, unknown> {
|
||||
// Yield self
|
||||
yield this;
|
||||
// Keep a Set of metadata objects so we only yield them out once.
|
||||
const seen = new Set<Metadata>();
|
||||
for (const { module: mod } of this.modules) {
|
||||
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 {
|
||||
|
@ -83,12 +128,6 @@ export class Metadata {
|
|||
}
|
||||
}
|
||||
|
||||
interface CreateMetadataOptions {
|
||||
modules: ModuleInfo[];
|
||||
hydratedComponents: any[];
|
||||
hoisted: any[];
|
||||
}
|
||||
|
||||
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) {
|
||||
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 { AstroConfig } from '../@types/astro';
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { transform } from '@astrojs/compiler';
|
||||
import { AstroDevServer } from '../core/dev/index.js';
|
||||
import { getViteTransform, TransformHook, transformWithVite } from './styles.js';
|
||||
import { getViteTransform, TransformHook } from './styles.js';
|
||||
import { parseAstroRequest } from './query.js';
|
||||
import { cachedCompilation, invalidateCompilation } from './compile.js';
|
||||
|
||||
const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
|
||||
interface AstroPluginOptions {
|
||||
|
@ -16,22 +14,8 @@ interface AstroPluginOptions {
|
|||
devServer?: AstroDevServer;
|
||||
}
|
||||
|
||||
// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726
|
||||
function isSSR(options: undefined | boolean | { ssr: boolean }): boolean {
|
||||
if (options === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof options === 'boolean') {
|
||||
return options;
|
||||
}
|
||||
if (typeof options == 'object') {
|
||||
return !!options.ssr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Transform .astro files for Vite */
|
||||
export default function astro({ config, devServer }: AstroPluginOptions): vite.Plugin {
|
||||
export default function astro({ config }: AstroPluginOptions): vite.Plugin {
|
||||
let viteTransform: TransformHook;
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro',
|
||||
|
@ -40,57 +24,51 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
|
|||
viteTransform = getViteTransform(resolvedConfig);
|
||||
},
|
||||
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
|
||||
async load(id, opts) {
|
||||
if (!id.endsWith('.astro')) {
|
||||
return null;
|
||||
async resolveId(id) {
|
||||
// serve sub-part requests (*?astro) as virtual modules
|
||||
if (parseAstroRequest(id).query.astro) {
|
||||
return id;
|
||||
}
|
||||
},
|
||||
async load(id, opts) {
|
||||
let { filename, query } = parseAstroRequest(id);
|
||||
if (query.astro) {
|
||||
if (query.type === 'style') {
|
||||
if (filename.startsWith('/') && !filename.startsWith(config.projectRoot.pathname)) {
|
||||
filename = new URL('.' + filename, config.projectRoot).pathname;
|
||||
}
|
||||
const transformResult = await cachedCompilation(config, filename, null, viteTransform, opts);
|
||||
|
||||
if (typeof query.index === 'undefined') {
|
||||
throw new Error(`Requests for Astro CSS must include an index.`);
|
||||
}
|
||||
|
||||
const csses = transformResult.css;
|
||||
const code = csses[query.index];
|
||||
|
||||
return {
|
||||
code,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async transform(source, id, opts) {
|
||||
if (!id.endsWith('.astro')) {
|
||||
return;
|
||||
}
|
||||
// pages and layouts should be transformed as full documents (implicit <head> <body> etc)
|
||||
// everything else is treated as a fragment
|
||||
const normalizedID = fileURLToPath(new URL(`file://${id}`));
|
||||
const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts));
|
||||
let source = await fs.promises.readFile(id, 'utf8');
|
||||
let tsResult: TransformResult | undefined;
|
||||
let cssTransformError: Error | undefined;
|
||||
|
||||
try {
|
||||
// Transform from `.astro` to valid `.ts`
|
||||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||
// result passed to esbuild, but also available in the catch handler.
|
||||
tsResult = await transform(source, {
|
||||
as: isPage ? 'document' : 'fragment',
|
||||
projectRoot: config.projectRoot.toString(),
|
||||
site: config.buildOptions.site,
|
||||
sourcefile: id,
|
||||
sourcemap: 'both',
|
||||
internalURL: 'astro/internal',
|
||||
preprocessStyle: async (value: string, attrs: Record<string, string>) => {
|
||||
const lang = `.${attrs?.lang || 'css'}`.toLowerCase();
|
||||
try {
|
||||
const result = await transformWithVite({ value, lang, id, transformHook: viteTransform, ssr: isSSR(opts) });
|
||||
let map: SourceMapInput | undefined;
|
||||
if (!result) return null as any; // TODO: add type in compiler to fix "any"
|
||||
if (result.map) {
|
||||
if (typeof result.map === 'string') {
|
||||
map = result.map;
|
||||
} else if (result.map.mappings) {
|
||||
map = result.map.toString();
|
||||
}
|
||||
}
|
||||
return { code: result.code, map };
|
||||
} catch (err) {
|
||||
// save error to throw in plugin context
|
||||
cssTransformError = err as any;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// throw CSS transform errors here if encountered
|
||||
if (cssTransformError) throw cssTransformError;
|
||||
const transformResult = await cachedCompilation(config, id, source, viteTransform, opts);
|
||||
|
||||
// Compile all TypeScript to JavaScript.
|
||||
// Also, catches invalid JS/TS in the compiled output before returning.
|
||||
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'external', sourcefile: id });
|
||||
const { code, map } = await esbuild.transform(transformResult.code, {
|
||||
loader: 'ts',
|
||||
sourcemap: 'external',
|
||||
sourcefile: id,
|
||||
});
|
||||
|
||||
return {
|
||||
code,
|
||||
|
@ -125,21 +103,21 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
|
|||
labels: 'compiler',
|
||||
title: '🐛 BUG: `@astrojs/compiler` panic',
|
||||
body: `### Describe the Bug
|
||||
|
||||
\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file.
|
||||
|
||||
**${id.replace(fileURLToPath(config.projectRoot), '')}**
|
||||
\`\`\`astro
|
||||
${source}
|
||||
\`\`\`
|
||||
`,
|
||||
|
||||
\`@astrojs/compiler\` encountered an unrecoverable error when compiling the following file.
|
||||
|
||||
**${id.replace(fileURLToPath(config.projectRoot), '')}**
|
||||
\`\`\`astro
|
||||
${source}
|
||||
\`\`\`
|
||||
`,
|
||||
});
|
||||
err.url = `https://github.com/withastro/astro/issues/new?${search.toString()}`;
|
||||
err.message = `Error: Uh oh, the Astro compiler encountered an unrecoverable error!
|
||||
|
||||
Please open
|
||||
a GitHub issue using the link below:
|
||||
${err.url}`;
|
||||
|
||||
Please open
|
||||
a GitHub issue using the link below:
|
||||
${err.url}`;
|
||||
// TODO: remove stack replacement when compiler throws better errors
|
||||
err.stack = ` at ${id}`;
|
||||
}
|
||||
|
@ -147,10 +125,9 @@ ${err.url}`;
|
|||
throw err;
|
||||
}
|
||||
},
|
||||
// async handleHotUpdate(context) {
|
||||
// if (devServer) {
|
||||
// return devServer.handleHotUpdate(context);
|
||||
// }
|
||||
// },
|
||||
async handleHotUpdate(context) {
|
||||
// Invalidate the compilation cache so it recompiles
|
||||
invalidateCompilation(config, context.file);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
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 { Plugin as VitePlugin } from '../core/vite';
|
||||
import type { BuildInternals } from '../core/build/internal';
|
||||
|
||||
import { STYLE_EXTENSIONS } from '../core/ssr/css.js';
|
||||
import * as path from 'path';
|
||||
import esbuild from 'esbuild';
|
||||
import { Plugin as VitePlugin } from '../core/vite';
|
||||
import { STYLE_EXTENSIONS } from '../core/ssr/css.js';
|
||||
|
||||
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
|
||||
|
||||
|
@ -45,14 +46,11 @@ function isPageStyleVirtualModule(id: string) {
|
|||
}
|
||||
|
||||
interface PluginOptions {
|
||||
astroStyleMap: Map<string, string>;
|
||||
astroPageStyleMap: Map<string, string>;
|
||||
chunkToReferenceIdMap: Map<string, string>;
|
||||
pureCSSChunks: Set<RenderedChunk>;
|
||||
internals: BuildInternals;
|
||||
}
|
||||
|
||||
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
||||
const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks } = options;
|
||||
const { internals } = options;
|
||||
const styleSourceMap = new Map<string, string>();
|
||||
|
||||
return {
|
||||
|
@ -94,10 +92,10 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
|
||||
async load(id) {
|
||||
if (isPageStyleVirtualModule(id)) {
|
||||
return astroPageStyleMap.get(id) || null;
|
||||
return internals.astroPageStyleMap.get(id) || null;
|
||||
}
|
||||
if (isStyleVirtualModule(id)) {
|
||||
return astroStyleMap.get(id) || null;
|
||||
return internals.astroStyleMap.get(id) || null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
@ -127,17 +125,26 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
// if (!chunkCSS) return null; // don’t output empty .css files
|
||||
|
||||
if (isPureCSS) {
|
||||
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
|
||||
loader: 'css',
|
||||
minify: true,
|
||||
});
|
||||
const referenceId = this.emitFile({
|
||||
name: chunk.name + '.css',
|
||||
type: 'asset',
|
||||
source: minifiedCSS,
|
||||
});
|
||||
pureCSSChunks.add(chunk);
|
||||
chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
||||
internals.pureCSSChunks.add(chunk);
|
||||
}
|
||||
|
||||
const { code: minifiedCSS } = await esbuild.transform(chunkCSS, {
|
||||
loader: 'css',
|
||||
minify: true,
|
||||
});
|
||||
const referenceId = this.emitFile({
|
||||
name: chunk.name + '.css',
|
||||
type: 'asset',
|
||||
source: minifiedCSS,
|
||||
});
|
||||
|
||||
internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId);
|
||||
if (chunk.type === 'chunk') {
|
||||
const facadeId = chunk.facadeModuleId!;
|
||||
if (!internals.facadeIdToAssetsMap.has(facadeId)) {
|
||||
internals.facadeIdToAssetsMap.set(facadeId, []);
|
||||
}
|
||||
internals.facadeIdToAssetsMap.get(facadeId)!.push(this.getFileName(referenceId));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -145,8 +152,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
|
||||
// Delete CSS chunks so JS is not produced for them.
|
||||
generateBundle(opts, bundle) {
|
||||
if (pureCSSChunks.size) {
|
||||
const pureChunkFilenames = new Set([...pureCSSChunks].map((chunk) => chunk.fileName));
|
||||
if (internals.pureCSSChunks.size) {
|
||||
const pureChunkFilenames = new Set([...internals.pureCSSChunks].map((chunk) => chunk.fileName));
|
||||
const emptyChunkFiles = [...pureChunkFilenames]
|
||||
.map((file) => path.basename(file))
|
||||
.join('|')
|
||||
|
@ -155,7 +162,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
|
|||
|
||||
for (const [chunkId, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type === 'chunk') {
|
||||
if (pureCSSChunks.has(chunk)) {
|
||||
if (internals.pureCSSChunks.has(chunk)) {
|
||||
// Delete pure CSS chunks, these are JavaScript chunks that only import
|
||||
// other CSS files, so are empty at the end of bundling.
|
||||
delete bundle[chunkId];
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import type { AstroConfig, RouteCache } from '../@types/astro';
|
||||
import type { LogOptions } from '../core/logger';
|
||||
import type { ViteDevServer, Plugin as VitePlugin } from '../core/vite';
|
||||
import type { OutputChunk, PreRenderedChunk, RenderedChunk } from 'rollup';
|
||||
import type { OutputChunk, PreRenderedChunk } from 'rollup';
|
||||
import type { AllPagesData } from '../core/build/types';
|
||||
import type { BuildInternals } from '../core/build/internal';
|
||||
import parse5 from 'parse5';
|
||||
import srcsetParse from 'srcset-parse';
|
||||
import * as npath from 'path';
|
||||
|
@ -26,20 +27,17 @@ const STATUS_CODE_RE = /^404$/;
|
|||
|
||||
interface PluginOptions {
|
||||
astroConfig: AstroConfig;
|
||||
astroStyleMap: Map<string, string>;
|
||||
astroPageStyleMap: Map<string, string>;
|
||||
chunkToReferenceIdMap: Map<string, string>;
|
||||
internals: BuildInternals;
|
||||
logging: LogOptions;
|
||||
allPages: AllPagesData;
|
||||
pageNames: string[];
|
||||
pureCSSChunks: Set<RenderedChunk>;
|
||||
origin: string;
|
||||
routeCache: RouteCache;
|
||||
viteServer: ViteDevServer;
|
||||
}
|
||||
|
||||
export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
||||
const { astroConfig, astroStyleMap, astroPageStyleMap, chunkToReferenceIdMap, pureCSSChunks, logging, origin, allPages, routeCache, viteServer, pageNames } = options;
|
||||
const { astroConfig, internals, logging, origin, allPages, routeCache, viteServer, pageNames } = options;
|
||||
|
||||
// The filepath root of the src folder
|
||||
const srcRoot = astroConfig.src.pathname;
|
||||
|
@ -161,7 +159,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
|||
|
||||
if (styles) {
|
||||
const styleId = getAstroStyleId(pathname);
|
||||
astroStyleMap.set(styleId, styles);
|
||||
internals.astroStyleMap.set(styleId, styles);
|
||||
// Put this at the front of imports
|
||||
assetImports.unshift(styleId);
|
||||
}
|
||||
|
@ -175,7 +173,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
|||
if (assetImports.length) {
|
||||
const pageStyleId = getAstroPageStyleId(pathname);
|
||||
const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n');
|
||||
astroPageStyleMap.set(pageStyleId, jsSource);
|
||||
internals.astroPageStyleMap.set(pageStyleId, jsSource);
|
||||
assetInput.add(pageStyleId);
|
||||
|
||||
// preserve asset order in the order we encounter them
|
||||
|
@ -268,7 +266,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
|||
|
||||
// Sort CSS in order of appearance in HTML (pageStyleImportOrder)
|
||||
// This is the “global ordering” used below
|
||||
const sortedCSSChunks = [...pureCSSChunks];
|
||||
const sortedCSSChunks = [...internals.pureCSSChunks];
|
||||
sortedCSSChunks.sort((a, b) => {
|
||||
let aIndex = Math.min(
|
||||
...Object.keys(a.modules).map((id) => {
|
||||
|
@ -298,7 +296,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
|
|||
|
||||
const referenceIDs: string[] = [];
|
||||
for (const chunkID of chunkModules) {
|
||||
const referenceID = chunkToReferenceIdMap.get(chunkID);
|
||||
const referenceID = internals.chunkToReferenceIdMap.get(chunkID);
|
||||
if (referenceID) referenceIDs.push(referenceID);
|
||||
}
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
|
|
Loading…
Reference in a new issue