Astro Integration System (#2820)

* update examples

* add initial integrations

* update tests

* update astro

* update ci

* get final tests working

* update injectelement todo

* update ben code review

* respond to final code review feedback
This commit is contained in:
Fred K. Schott 2022-03-18 15:35:45 -07:00 committed by GitHub
parent 0f376a7c52
commit 6386c14d00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
266 changed files with 3619 additions and 1117 deletions

View file

@ -99,7 +99,8 @@ jobs:
with:
name: artifacts
path: |
packages/**/dist/**
packages/*/dist/**
packages/*/*/dist/**
packages/webapi/mod.js
packages/webapi/mod.js.map
if-no-files-found: error

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
// https://astro.build/config
export default defineConfig({
// Enable the Preact renderer to support Preact JSX components.
renderers: ['@astrojs/renderer-preact'],
// Enable the Preact integration to support Preact JSX components.
integrations: [preact()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/preact": "^0.0.1",
"astro": "^0.24.3",
"sass": "^1.49.9"
}

View file

@ -1,8 +1,9 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
// https://astro.build/config
export default defineConfig({
renderers: ['@astrojs/renderer-preact'],
integrations: [preact()],
buildOptions: {
site: 'https://example.com/',
},

View file

@ -10,6 +10,6 @@
},
"devDependencies": {
"astro": "^0.24.3",
"@astrojs/renderer-preact": "^0.5.0"
"@astrojs/preact": "^0.0.1"
}
}

View file

@ -1,7 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});
export default defineConfig({});

View file

@ -1,11 +1,13 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
renderers: [
// Enable the Preact renderer to support Preact JSX components.
'@astrojs/renderer-preact',
// Enable the React renderer, for the Algolia search component
'@astrojs/renderer-react',
integrations: [
// Enable Preact to support Preact JSX components.
preact(),
// Enable React for the Algolia search component.
react(),
],
});

View file

@ -17,8 +17,8 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/preact": "^0.0.1",
"@astrojs/react": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,6 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
renderers: [],
});
export default defineConfig({});

View file

@ -2,6 +2,6 @@ import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// No renderers are needed for AlpineJS support, just use Astro components!
renderers: [],
// No integrations are needed for AlpineJS support, just use Astro components!
integrations: [],
});

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import lit from '@astrojs/lit';
// https://astro.build/config
export default defineConfig({
// Enable the lit renderer to support LitHTML components and templates.
renderers: ['@astrojs/renderer-lit'],
// Enable Lit to support LitHTML components and templates.
integrations: [lit()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-lit": "^0.4.0",
"@astrojs/lit": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,12 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many renderers to support all different kinds of components.
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue', '@astrojs/renderer-solid'],
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
});

View file

@ -9,12 +9,12 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-lit": "^0.4.0",
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/renderer-solid": "^0.4.0",
"@astrojs/renderer-svelte": "^0.5.2",
"@astrojs/renderer-vue": "^0.4.0",
"@astrojs/lit": "^0.0.1",
"@astrojs/preact": "^0.0.1",
"@astrojs/react": "^0.0.1",
"@astrojs/solid-js": "^0.0.1",
"@astrojs/svelte": "^0.0.1",
"@astrojs/vue": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
// https://astro.build/config
export default defineConfig({
// Enable the Preact renderer to support Preact JSX components.
renderers: ['@astrojs/renderer-preact'],
// Enable Preact to support Preact JSX components.
integrations: [preact()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/preact": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
// Enable the React renderer to support React JSX components.
renderers: ['@astrojs/renderer-react'],
// Enable React to support React JSX components.
integrations: [react()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/react": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable the Solid renderer to support Solid JSX components.
renderers: ['@astrojs/renderer-solid'],
// Enable Solid to support Solid JSX components.
integrations: [solid()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-solid": "^0.4.0",
"@astrojs/solid-js": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
// Enable the Svelte renderer to support Svelte components.
renderers: ['@astrojs/renderer-svelte'],
// Enable Svelte to support Svelte components.
integrations: [svelte()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-svelte": "^0.5.2",
"@astrojs/svelte": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,8 @@
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
// https://astro.build/config
export default defineConfig({
// Enable the Vue renderer to support Vue components.
renderers: ['@astrojs/renderer-vue'],
// Enable Vue to support Vue components.
integrations: [vue()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-vue": "^0.4.0",
"@astrojs/vue": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -0,0 +1,17 @@
# build output
dist
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

View file

@ -0,0 +1,2 @@
# Expose Astro dependencies for `pnpm` users
shamefully-hoist=true

View file

@ -0,0 +1,6 @@
{
"startCommand": "npm start",
"env": {
"ENABLE_CJS_IMPORTS": true
}
}

View file

@ -0,0 +1,7 @@
# Integration Playground
```
npm init astro -- --template integration-playground
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/integration-playground)

View file

@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';
import lit from '@astrojs/lit';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import turbolinks from '@astrojs/turbolinks';
import sitemap from '@astrojs/sitemap';
import partytown from '@astrojs/partytown';
export default defineConfig({
integrations: [lit(), react(), tailwind(), turbolinks(), partytown(), sitemap()],
});

View file

@ -0,0 +1,20 @@
{
"name": "@example/integrations-playground",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/lit": "^0.0.1",
"@astrojs/react": "^0.0.1",
"@astrojs/partytown": "^0.0.1",
"@astrojs/sitemap": "^0.0.1",
"@astrojs/tailwind": "^0.0.1",
"@astrojs/turbolinks": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -0,0 +1,12 @@
<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
#flame { fill: #FF5D01; }
#a { fill: #000014; }
@media (prefers-color-scheme: dark) {
#a { fill: #fff; }
}
</style>
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/>
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -0,0 +1,11 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "14"
}
}

View file

@ -0,0 +1,34 @@
import { LitElement, html } from 'lit';
export const tagName = 'my-counter';
class Counter extends LitElement {
static get properties() {
return {
count: {
type: Number,
},
};
}
constructor() {
super();
this.count = 0;
}
increment() {
this.count++;
}
render() {
return html`
<div>
<p>Count: ${this.count}</p>
<button type="button" @click=${this.increment}>Increment</button>
</div>
`;
}
}
customElements.define(tagName, Counter);

View file

@ -0,0 +1,3 @@
export default function Link({ to, text }) {
return <a href={to}>{text}</a>;
}

View file

@ -0,0 +1,66 @@
---
//hey
---
<style>
p {
color: red;
}
</style>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi quam arcu, rhoncus et dui at, volutpat viverra augue. Suspendisse placerat libero tellus, ut consequat ligula
rutrum id. Vestibulum lectus libero, viverra in lacus eget, porttitor tincidunt leo. Integer sit amet turpis et felis fringilla lacinia in id nibh. Proin vitae dapibus odio.
Mauris ornare eget urna id volutpat. Duis tellus nisi, hendrerit id sodales in, rutrum a quam. Proin tempor velit turpis, et tempor lacus sagittis in. Sed congue mauris quis nibh
posuere, nec semper lacus auctor. Morbi sit amet enim sit amet arcu ullamcorper sollicitudin. Donec dignissim posuere tincidunt. Donec ultrices quam nec orci venenatis suscipit.
Maecenas sapien quam, pretium sit amet ullamcorper at, vulputate sit amet urna. Suspendisse potenti. Integer in sapien turpis. Nulla accumsan viverra diam, quis convallis magna
finibus eget. Integer sed eros bibendum, consequat velit sit amet, tincidunt orci. Mauris varius id metus in fringilla. Vestibulum dignissim massa eget erat luctus, ac congue
mauris pellentesque. In et tempor dolor. Cras blandit congue lorem at facilisis. Aenean vel lacinia quam. Pellentesque luctus metus ut scelerisque efficitur. Mauris laoreet
sodales libero eget luctus. Proin at congue dui, a cursus risus. Pellentesque lorem sem, rhoncus fermentum arcu ut, euismod fermentum ligula. Nullam eu orci posuere, laoreet leo
in, commodo dolor. Fusce at felis elementum, commodo justo at, placerat justo. Nam feugiat scelerisque arcu, ut fermentum tellus elementum in. Sed ut vulputate ante. Morbi cursus
arcu quis odio convallis egestas. Donec vulputate vestibulum dolor eget tristique. Nullam tempor semper augue, vitae lobortis neque tempor ac. Pellentesque massa leo, congue id
ligula auctor, sollicitudin pharetra lorem. Curabitur a lacus porttitor, venenatis est quis, mattis velit. Fusce hendrerit lobortis mi ac efficitur. Mauris ornare, lorem sed
varius faucibus, nisi dui pretium urna, sit amet lacinia nibh ligula in ipsum. Phasellus gravida, metus eget ornare ultrices, dolor ipsum consectetur erat, ac aliquet eros metus
sed lectus. Nullam eleifend posuere rhoncus. Curabitur semper ligula vel ante posuere, at blandit orci accumsan. Vivamus accumsan metus in lorem laoreet, a luctus arcu tempus.
Donec posuere sollicitudin nulla at vulputate. Nulla condimentum imperdiet purus, et lobortis ligula iaculis in. Donec suscipit viverra neque, ut elementum eros lacinia ut. Fusce
at odio enim. Donec rutrum lectus sit amet est auctor, ac rhoncus lorem imperdiet. Curabitur commodo ex est, non tempus massa pulvinar nec. Sed fermentum, lectus eget ultricies
luctus, enim sem sodales quam, sed laoreet tortor sem feugiat nisi. Morbi molestie vehicula viverra. Integer accumsan mi in orci ultrices posuere. Integer mi quam, faucibus et
aliquet imperdiet, ornare ac ex. Nunc mattis molestie nisi, eu venenatis nibh vehicula at. Aliquam ut elit consectetur, finibus lorem sed, condimentum sapien. Praesent fermentum
iaculis orci, vitae tincidunt est viverra nec. Morbi semper turpis sed lectus ornare tristique. Sed congue dui ex. Maecenas orci ligula, imperdiet sit amet accumsan et, finibus a
velit. Ut vitae blandit eros. Nam gravida nec ipsum non volutpat. Integer quam metus, porttitor id ante sed, rutrum porta quam. Aenean at mattis ante. Morbi id libero eget risus
sagittis gravida. Proin consequat sapien a dignissim posuere. Ut luctus sed metus ut elementum. Mauris tincidunt condimentum risus at bibendum. Aenean a sapien justo. Morbi vel
neque in eros venenatis scelerisque vitae nec justo. Vestibulum lacinia, dui eu sollicitudin ornare, est elit vestibulum arcu, nec ultrices augue turpis in massa. Duis commodo
lectus sed est posuere, et mollis nisi dapibus. Sed id ultrices arcu. Praesent tempor sodales aliquet. Donec suscipit ipsum eu odio cursus, quis sodales metus sodales. Nunc
vestibulum massa at felis ullamcorper cursus. Pellentesque facilisis ante ut lectus vulputate vestibulum. Nullam pharetra felis ac lacus sodales, vel suscipit metus faucibus.
Donec facilisis imperdiet risus, in volutpat odio tincidunt a. Aliquam vitae leo lorem. Proin scelerisque efficitur velit, vel cursus ipsum accumsan id. Morbi nibh nulla, pretium
quis venenatis et, pharetra et sapien. Cras lobortis, massa sit amet blandit pulvinar, mi magna condimentum ex, quis commodo ipsum est quis metus. Maecenas pulvinar, leo sit amet
congue pulvinar, neque magna ultrices mi, et rhoncus massa sapien quis libero. Etiam a nunc et ipsum faucibus pretium. Nulla facilisi. Nunc nec dolor velit. In semper semper mi
non condimentum. Pellentesque vehicula volutpat odio, a semper sem porta a. In sit amet lectus rutrum, sollicitudin augue auctor, maximus quam. Mauris congue, nisl non fermentum
iaculis, leo erat interdum lorem, quis bibendum arcu eros et elit. Fusce tortor ante, gravida a arcu in, lacinia finibus ante. Phasellus facilisis lectus vitae sapien feugiat
laoreet. Curabitur ultricies libero sit amet condimentum suscipit. Duis at vestibulum mi. Suspendisse at neque augue. Duis ornare a mauris id efficitur. Suspendisse in dui nec
dolor dignissim venenatis. Curabitur a magna turpis. Aliquam at commodo tellus. In id sem interdum, suscipit felis at, mattis velit. Proin accumsan sodales felis a lacinia.
Curabitur at magna a massa varius maximus. Vestibulum in auctor ante. Donec aliquam tortor sed nulla rutrum, et egestas mi efficitur. Sed viverra quam tellus, quis vulputate
felis ultrices sed. Mauris sagittis, neque quis laoreet gravida, nisi est ultrices mi, at tempus nunc justo non dui. Suspendisse porttitor tortor nulla, eget luctus quam finibus
id. Proin sodales eros mollis tellus euismod luctus a eu mi. Quisque consectetur iaculis nibh, at mollis tellus volutpat eu. Aenean a nulla vel lectus rhoncus aliquam. Donec
vitae lacinia neque. Donec non lectus eget sem finibus ultrices vel nec felis. Proin fringilla mi a leo rhoncus aliquam sit amet quis augue. Duis congue ligula at est suscipit
fringilla. Proin aliquam erat ut consequat dapibus. Suspendisse non nisi orci. Donec ac erat vel libero egestas laoreet. Nullam felis odio, tincidunt eget eleifend a, porttitor
eu nisi. Suspendisse tristique eros at dolor scelerisque hendrerit. Etiam id dignissim lectus. Fusce lacinia metus eu risus placerat, et eleifend nunc ultrices. Ut gravida a dui
sed volutpat. Sed semper quis erat sed ornare. Pellentesque sapien sem, fermentum vel nunc at, auctor posuere nisl. Maecenas aliquet lobortis leo. Vivamus tellus urna, dignissim
consectetur sapien vitae, hendrerit varius sem. Nunc dictum tristique fermentum. Duis eu suscipit odio. Curabitur quis egestas neque. Fusce eu fringilla orci, vitae euismod
sapien. Donec sit amet iaculis urna. Phasellus maximus nisl in libero bibendum volutpat. Nulla at vehicula lorem. Phasellus varius, elit ac suscipit pretium, turpis ipsum
porttitor lectus, vitae ullamcorper orci velit ut ligula. Proin mollis, orci vel commodo auctor, sapien ipsum vulputate enim, sit amet aliquam nulla sapien ut sapien. Proin
tincidunt ex non massa aliquet, quis aliquam nulla egestas. Maecenas mollis turpis dapibus, dignissim lectus tincidunt, egestas ligula. Suspendisse in lobortis purus. Sed tellus
tellus, mollis eget tempor sed, interdum ut lectus. Nulla sed ex efficitur, porta dui cursus, tristique elit. Maecenas tincidunt tortor vitae massa laoreet ultricies. Mauris ac
elit vitae orci eleifend ornare non eu ligula. Curabitur venenatis nulla ut neque tristique, non tincidunt justo pretium. Suspendisse mattis semper dui, eget vestibulum risus
elementum sed. In consequat nisi sit amet nulla euismod, at convallis tortor tincidunt. Aliquam hendrerit venenatis risus in interdum. Duis ullamcorper imperdiet elit sit amet
blandit. Mauris placerat lacinia velit id pharetra. Nam nec iaculis dui. Etiam odio mi, fringilla in rutrum in, viverra quis tellus. Aliquam egestas mauris id nisi facilisis, in
laoreet nibh malesuada. Ut eu dui laoreet, venenatis tellus ac, feugiat mauris. Nunc in velit laoreet, venenatis tellus quis, blandit dolor. Nulla ultrices et neque id placerat.
Nulla eu interdum nulla. Aliquam molestie enim quis rutrum finibus. Nulla bibendum orci vel scelerisque posuere. Praesent quis magna molestie, luctus tortor tincidunt, gravida
neque. Quisque et ligula eget magna viverra interdum at a sapien. Mauris ornare efficitur nunc sed vulputate. Praesent laoreet mollis tincidunt. Vestibulum id arcu vulputate,
eleifend enim vel, accumsan turpis. Morbi faucibus convallis tellus, semper laoreet justo lacinia nec. Sed sodales ligula consectetur dui rhoncus, et convallis metus accumsan.
Sed ullamcorper non ex sit amet ultricies. Donec finibus nulla nec blandit porttitor. Etiam aliquam quis leo a imperdiet. Cras at lobortis est. In convallis semper enim, ac porta
ligula fringilla at. Donec augue est, facilisis et odio sit amet, viverra ullamcorper nisl. Ut porta velit nec sem lacinia, sit amet mollis magna auctor. Nulla lobortis lacinia
mauris nec sagittis. Suspendisse rutrum ex vel nisi interdum hendrerit et ut purus. Sed consectetur sodales nibh eget tempus. Aenean egestas luctus viverra. Integer fermentum
tincidunt tellus, nec rhoncus velit hendrerit vitae. Proin quis neque porttitor, scelerisque risus gravida, volutpat sem. Fusce nec ex rhoncus, tempor libero nec, pellentesque
ex. Integer quis iaculis purus. Nullam vitae imperdiet orci. Sed sit amet eros condimentum, scelerisque turpis facilisis, dignissim ante. Proin quis tristique lacus, sed sagittis
nisl. Cras pharetra ultrices purus, sed ullamcorper nisi fringilla eu. Praesent risus turpis, auctor in fringilla a, fringilla eu dolor. Phasellus auctor tristique enim, eleifend
molestie diam venenatis ut. Mauris dapibus, enim eget pharetra semper, nulla dui porttitor mi, auctor hendrerit augue nulla quis urna. Aliquam in cursus justo.
</p>

View file

@ -0,0 +1,19 @@
import { LitElement, html } from 'lit';
export const tagName = 'calc-add';
class CalcAdd extends LitElement {
static get properties() {
return {
num: {
type: Number,
},
};
}
render() {
return html` <div>Number: ${this.num}</div> `;
}
}
customElements.define(tagName, CalcAdd);

View file

@ -0,0 +1,15 @@
---
// Page 2!
import Link from '../components/Link.jsx';
---
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Demo: Page 2</title>
</head>
<body>
<Link to="/" text="Go Home" />
</body>
</html>

View file

@ -0,0 +1,53 @@
---
import Lorem from '../components/Lorem.astro';
import Link from '../components/Link.jsx';
import '../components/Test.js';
import '../components/Counter.js';
---
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Demo</title>
</head>
<body>
<h1 class="px-4 py-4">Test app</h1>
<h2 class="partytown-status">
<strong>Party Mode!</strong>
Colors changing = partytown is enabled
</h2>
<my-counter client:load></my-counter>
<Link to="/foo" text="Go to Page 2" />
<Lorem />
<calc-add num={33}></calc-add>
<script type="text/partytown">
// Remove `type="text/partytown"` to see this block the page
// and cause the page to become unresponsive
console.log('start partytown blocking script')
const now = Date.now()
let count = 1;
while (Date.now() - now < 10000) {
if (Date.now() - now > count * 1000) {
console.log('blocking', count);
count += 1;
}
}
console.log('end partytown blocking script')
</script>
<script>
setInterval(() => {
const randomColor = Math.floor(Math.random()*16777215).toString(16);
document.querySelector('.partytown-status').style.color = "#" + randomColor;
}, 100);
</script>
<style>
h1, h2 {
color: blue;
}
</style>
</body>
</html>

View file

@ -0,0 +1,5 @@
{
"compilerOptions": {
"moduleResolution": "node"
}
}

View file

@ -1,7 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});
export default defineConfig({});

View file

@ -1,7 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});
export default defineConfig({});

View file

@ -1,7 +1,7 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/render-preact';
// https://astro.build/config
export default defineConfig({
// Enable the Preact renderer to support Preact JSX components.
renderers: ['@astrojs/renderer-preact'],
integrations: [preact()],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/preact": "^0.0.1",
"astro": "^0.24.3",
"sass": "^1.49.9"
}

View file

@ -1,7 +1,9 @@
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
renderers: ['@astrojs/renderer-svelte'],
integrations: [svelte()],
vite: {
server: {
cors: {

View file

@ -12,7 +12,7 @@
"server": "node server/server.mjs"
},
"devDependencies": {
"@astrojs/renderer-svelte": "^0.5.2",
"@astrojs/svelte": "^0.0.1",
"astro": "^0.24.3",
"concurrently": "^7.0.0",
"lightcookie": "^1.0.25",

View file

@ -1,7 +1,4 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
// Set "renderers" to "[]" to disable all default, builtin component support.
renderers: [],
});
export default defineConfig({});

View file

@ -1,10 +1,10 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
// Comment out "renderers: []" to enable Astro's default component support.
integrations: [react()],
buildOptions: {
site: 'http://example.com/blog',
},
renderers: ['@astrojs/renderer-react'],
});

View file

@ -9,7 +9,7 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/react": "^0.0.1",
"astro": "^0.24.3",
"sass": "^1.49.9"
}

View file

@ -5,7 +5,6 @@ import addClasses from './add-classes.mjs';
// https://astro.build/config
export default defineConfig({
// Enable Custom Markdown options, plugins, etc.
renderers: [],
markdownOptions: {
render: [
astroRemark,

View file

@ -4,7 +4,6 @@ import astroRemark from '@astrojs/markdown-remark';
// https://astro.build/config
export default defineConfig({
// Enable Custom Markdown options, plugins, etc.
renderers: [],
markdownOptions: {
render: [
astroRemark,

View file

@ -1,6 +1,11 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
// https://astro.build/config
export default defineConfig({
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue()],
});

View file

@ -10,10 +10,10 @@
},
"devDependencies": {
"@astrojs/markdown-remark": "^0.6.4",
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/renderer-svelte": "^0.5.2",
"@astrojs/renderer-vue": "^0.4.0",
"@astrojs/preact": "^0.0.1",
"@astrojs/react": "^0.0.1",
"@astrojs/svelte": "^0.0.1",
"@astrojs/vue": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,12 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
// Enable many renderers to support all different kinds of components.
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue', '@astrojs/renderer-solid'],
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
});

View file

@ -20,11 +20,11 @@
"vue": "^3.2.31"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "^0.5.0",
"@astrojs/renderer-solid": "^0.4.0",
"@astrojs/renderer-svelte": "^0.5.2",
"@astrojs/renderer-vue": "^0.4.0",
"@astrojs/preact": "^0.0.1",
"@astrojs/react": "^0.0.1",
"@astrojs/solid-js": "^0.0.1",
"@astrojs/svelte": "^0.0.1",
"@astrojs/vue": "^0.0.1",
"astro": "^0.24.3"
}
}

View file

@ -1,7 +1,7 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
// https://astro.build/config
export default defineConfig({
// Enable the Preact renderer to support Preact JSX components.
renderers: ['@astrojs/renderer-preact'],
integrations: [tailwind()],
});

View file

@ -9,9 +9,10 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/tailwind": "^0.0.1",
"astro": "^0.24.3",
"autoprefixer": "^10.4.4",
"canvas-confetti": "^1.5.1",
"postcss": "^8.4.12",
"tailwindcss": "^3.0.23"
}

View file

@ -1,10 +0,0 @@
const path = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: path.join(__dirname, 'tailwind.config.js'), // update this if your path differs!
},
autoprefixer: {},
},
};

View file

@ -1,10 +1,11 @@
---
let { type = 'button' } = Astro.props;
// Click button, get confetti!
// Styled by Tailwind :)
---
<button
class="py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75"
{type}
>
<button class="py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75">
<slot />
</button>
<script hoist>
import confetti from 'canvas-confetti';
document.body.querySelector('button').addEventListener("click", () => confetti());
</script>

View file

@ -1,7 +1,6 @@
---
// Component Imports
import Button from '../components/Button.astro';
import '../styles/global.css';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
@ -15,6 +14,8 @@ import '../styles/global.css';
</head>
<body>
<Button>Im a Tailwind Button!</Button>
<div class="grid place-items-center h-screen">
<Button>Click Me!</Button>
</div>
</body>
</html>

View file

@ -1,3 +0,0 @@
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,svelte,ts,tsx,vue}'],
};

View file

@ -3,7 +3,6 @@ import { VitePWA } from 'vite-plugin-pwa';
// https://astro.build/config
export default defineConfig({
renderers: [],
vite: {
plugins: [VitePWA()],
},

View file

@ -23,6 +23,7 @@
"compiled/*",
"packages/markdown/*",
"packages/renderers/*",
"packages/integrations/*",
"packages/*",
"examples/*",
"examples/component/demo",

View file

@ -57,7 +57,7 @@
"dev": "astro-scripts dev \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "mocha --parallel --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
"test:match": "mocha --timeout 20000 -g"
},
"dependencies": {
@ -65,10 +65,6 @@
"@astrojs/language-server": "^0.8.10",
"@astrojs/markdown-remark": "^0.6.4",
"@astrojs/prism": "0.4.0",
"@astrojs/renderer-preact": "^0.5.0",
"@astrojs/renderer-react": "0.5.0",
"@astrojs/renderer-svelte": "0.5.2",
"@astrojs/renderer-vue": "0.4.0",
"@astrojs/webapi": "^0.11.0",
"@babel/core": "^7.17.7",
"@babel/traverse": "^7.17.3",

View file

@ -1,9 +1,10 @@
import type { AddressInfo } from 'net';
import type * as babel from '@babel/core';
import type * as vite from 'vite';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request';
import type * as vite from 'vite';
export interface AstroBuiltinProps {
'client:load'?: boolean;
@ -127,21 +128,30 @@ export interface AstroUserConfig {
/**
* @docs
* @name renderers
* @type {string[]}
* @default `['@astrojs/renderer-svelte','@astrojs/renderer-vue','@astrojs/renderer-react','@astrojs/renderer-preact']`
* @name integrations
* @type {AstroIntegration[]}
* @default `[]`
* @description
* Set the UI framework renderers for your project. Framework renderers are what power Astro's ability to use other frameworks inside of your project, like React, Svelte, and Vue.
* Add Integrations to your project to extend Astro.
*
* Setting this configuration will disable Astro's default framework support, so you will need to provide a renderer for every framework that you want to use.
* Integrations are your one-stop shop to add new frameworks (like Solid.js), new features (like sitemaps), and new libraries (like Partytown and Turbolinks).
*
* Setting this configuration will disable Astro's default integration, so it is recommended to provide a renderer for every framework that you use:
*
* Note: Integrations are currently under active development, and only first-party integrations are supported. In the future, 3rd-party integrations will be allowed.
*
* ```js
* import react from '@astrojs/react';
* import vue from '@astrojs/vue';
* {
* // Use Astro + React, with no other frameworks.
* renderers: ['@astrojs/renderer-react']
* // Example: Use Astro with Vue + React, and no other frameworks.
* integrations: [react(), vue()]
* }
* ```
*/
integrations?: AstroIntegration[];
/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
renderers?: string[];
/**
@ -170,6 +180,7 @@ export interface AstroUserConfig {
* }
* ```
*/
/** Options for rendering markdown content */
markdownOptions?: {
render?: MarkdownRenderOptions;
};
@ -379,7 +390,7 @@ export interface AstroUserConfig {
/**
* @docs
* @name devOptions.vite
* @name vite
* @type {vite.UserConfig}
* @description
*
@ -421,11 +432,33 @@ export interface AstroUserConfig {
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// }
/**
* IDs for different stages of JS script injection:
* - "before-hydration": Imported client-side, before the hydration script runs. Processed & resolved by Vite.
* - "head-inline": Injected into a script tag in the `<head>` of every page. Not processed or resolved by Vite.
* - "page": Injected into the JavaScript bundle of every page. Processed & resolved by Vite.
* - "page-ssr": Injected into the frontmatter of every Astro page. Processed & resolved by Vite.
*/
type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';
/**
* Resolved Astro Config
* Config with user settings along with all defaults filled in.
*/
export type AstroConfig = z.output<typeof AstroConfigSchema>;
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// Public:
// This is a more detailed type than zod validation gives us.
// TypeScript still confirms zod validation matches this type.
integrations: AstroIntegration[];
// Private:
// We have a need to pass context based on configured state,
// that is different from the user-exposed configuration.
// TODO: Create an AstroConfig class to manage this, long-term.
_ctx: {
renderers: AstroRenderer[];
scripts: { stage: InjectedScriptStage; content: string }[];
};
}
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;
@ -560,38 +593,51 @@ export interface EndpointHandler {
[method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response;
}
/**
* Astro Renderer
* Docs: https://docs.astro.build/reference/renderer-reference/
*/
export interface Renderer {
/** Name of the renderer (required) */
export interface AstroRenderer {
/** Name of the renderer. */
name: string;
/** Import statement for renderer */
source?: string;
/** Import statement for the server renderer */
serverEntry: string;
/** Scripts to be injected before component */
polyfills?: string[];
/** Polyfills that need to run before hydration ever occurs */
hydrationPolyfills?: string[];
/** Import entrypoint for the client/browser renderer. */
clientEntrypoint?: string;
/** Import entrypoint for the server/build/ssr renderer. */
serverEntrypoint: string;
/** JSX identifier (e.g. 'react' or 'solid-js') */
jsxImportSource?: string;
/** Babel transform options */
jsxTransformOptions?: JSXTransformFn;
/** Utilies for server-side rendering */
}
export interface SSRLoadedRenderer extends AstroRenderer {
ssr: {
check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{
html: string;
}>;
};
/** Return configuration object for Vite ("options" should match https://vitejs.dev/guide/api-plugin.html#config) */
viteConfig?: (options: { mode: 'string'; command: 'build' | 'serve' }) => Promise<vite.InlineConfig>;
/** @deprecated Dont try and build these dependencies for client (deprecated in 0.21) */
external?: string[];
/** @deprecated Clientside requirements (deprecated in 0.21) */
knownEntrypoints?: string[];
}
export interface AstroIntegration {
/** The name of the integration. */
name: string;
/** The different hooks available to extend. */
hooks: {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
// This may require some refactoring of `scripts`, `styles`, and `links` into something
// more generalized. Consider the SSR use-case as well.
// injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void;
}) => void;
'astro:config:done'?: (options: { config: AstroConfig }) => void | Promise<void>;
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>;
'astro:build:start'?: () => void | Promise<void>;
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL }) => void | Promise<void>;
};
}
export type RouteType = 'page' | 'endpoint';
@ -665,7 +711,7 @@ export interface SSRElement {
}
export interface SSRMetadata {
renderers: Renderer[];
renderers: SSRLoadedRenderer[];
pathname: string;
legacyBuild: boolean;
}

View file

@ -1,4 +1,4 @@
import type { ComponentInstance, ManifestData, RouteData, Renderer } from '../../@types/astro';
import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } from '../../@types/astro';
import type { SSRManifest as Manifest, RouteInfo } from './types';
import { defaultLogOptions } from '../logger.js';
@ -6,7 +6,6 @@ import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { createRenderer } from '../render/renderer.js';
import { prependForwardSlash } from '../path.js';
export class App {
@ -15,7 +14,7 @@ export class App {
#rootFolder: URL;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#routeCache: RouteCache;
#renderersPromise: Promise<Renderer[]>;
#renderersPromise: Promise<SSRLoadedRenderer[]>;
constructor(manifest: Manifest, rootFolder: URL) {
this.#manifest = manifest;
@ -84,18 +83,11 @@ export class App {
status: 200,
});
}
async #loadRenderers(): Promise<Renderer[]> {
const rendererNames = this.#manifest.renderers;
async #loadRenderers(): Promise<SSRLoadedRenderer[]> {
return await Promise.all(
rendererNames.map(async (rendererName) => {
return createRenderer(rendererName, {
renderer(name) {
return import(name);
},
server(entry) {
return import(entry);
},
});
this.#manifest.renderers.map(async (renderer) => {
const mod = (await import(renderer.serverEntrypoint)) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default };
})
);
}

View file

@ -1,4 +1,4 @@
import type { RouteData, SerializedRouteData, MarkdownRenderOptions } from '../../@types/astro';
import type { RouteData, SerializedRouteData, MarkdownRenderOptions, AstroRenderer } from '../../@types/astro';
export interface RouteInfo {
routeData: RouteData;
@ -17,7 +17,7 @@ export interface SSRManifest {
markdown: {
render: MarkdownRenderOptions;
};
renderers: string[];
renderers: AstroRenderer[];
entryModules: Record<string, string>;
}

View file

@ -14,6 +14,7 @@ import { collectPagesData } from './page-data.js';
import { build as scanBasedBuild } from './scan-based-build.js';
import { staticBuild } from './static-build.js';
import { RouteCache } from '../render/route-cache.js';
import { runHookBuildDone, runHookBuildStart, runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js';
export interface BuildOptions {
mode?: string;
@ -57,23 +58,23 @@ class AstroBuilder {
const timer: Record<string, number> = {};
timer.init = performance.now();
timer.viteStart = performance.now();
this.config = await runHookConfigSetup({ config: this.config, command: 'build' });
const viteConfig = await createVite(
vite.mergeConfig(
{
mode: this.mode,
server: {
hmr: false,
middlewareMode: 'ssr',
},
{
mode: this.mode,
server: {
hmr: false,
middlewareMode: 'ssr',
},
this.config.vite || {}
),
},
{ astroConfig: this.config, logging, mode: 'build' }
);
await runHookConfigDone({ config: this.config });
this.viteConfig = viteConfig;
const viteServer = await vite.createServer(viteConfig);
this.viteServer = viteServer;
debug('build', timerMessage('Vite started', timer.viteStart));
await runHookBuildStart({ config: this.config });
timer.loadStart = performance.now();
const { assets, allPages } = await collectPagesData({
@ -160,6 +161,8 @@ class AstroBuilder {
// You're done! Time to clean up.
await viteServer.close();
await runHookBuildDone({ config: this.config, pages: pageNames });
if (logging.level && levels[logging.level] <= levels['info']) {
await this.printStats({ logging, timeStart: timer.init, pageCount: pageNames.length });
}

View file

@ -1,31 +1,28 @@
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from 'vite';
import type { AstroConfig, ComponentInstance, EndpointHandler, ManifestData, Renderer, RouteType } 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 { RenderOptions } from '../../core/render/core';
import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
import glob from 'fast-glob';
import fs from 'fs';
import npath from 'path';
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
import { fileURLToPath } from 'url';
import glob from 'fast-glob';
import type { Manifest as ViteManifest, Plugin as VitePlugin, UserConfig } from 'vite';
import * as vite from 'vite';
import { debug, error } from '../../core/logger.js';
import { prependForwardSlash, appendForwardSlash } from '../../core/path.js';
import { emptyDir, removeDir, resolveDependency } from '../../core/util.js';
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, ManifestData, RouteType, SSRLoadedRenderer } from '../../@types/astro';
import type { BuildInternals } from '../../core/build/internal.js';
import { createBuildInternals } from '../../core/build/internal.js';
import { debug, error } from '../../core/logger.js';
import { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
import type { RenderOptions } from '../../core/render/core';
import { emptyDir, removeDir, resolveDependency } from '../../core/util.js';
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
import { RouteCache } from '../render/route-cache.js';
import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
import type { ViteConfigWithSSR } from '../create-vite';
import { call as callEndpoint } from '../endpoint/index.js';
import { serializeRouteData } from '../routing/index.js';
import type { LogOptions } from '../logger';
import { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { createRequest } from '../render/request.js';
import { serializeRouteData } from '../routing/index.js';
import type { AllPagesData, PageBuildData } from './types';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
export interface StaticBuildOptions {
allPages: AllPagesData;
@ -116,17 +113,8 @@ export async function staticBuild(opts: StaticBuildOptions) {
// about that page, such as its paths.
const facadeIdToPageDataMap = new Map<string, PageBuildData>();
// Collects polyfills and passes them as top-level inputs
const polyfills = getRenderers(opts).flatMap((renderer) => {
return (renderer.polyfills || []).concat(renderer.hydrationPolyfills || []);
});
for (const polyfill of polyfills) {
jsInput.add(polyfill);
}
// Build internals needed by the CSS plugin
const internals = createBuildInternals();
for (const [component, pageData] of Object.entries(allPages)) {
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
const astroModuleId = prependForwardSlash(component);
@ -145,7 +133,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Any hydration directive like astro/client/idle.js
...metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer
...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!),
...renderers.filter((renderer) => !!renderer.clientEntrypoint).map((renderer) => renderer.clientEntrypoint!),
]);
// Add hoisted scripts
@ -172,6 +160,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Build your project (SSR application code, assets, client JS, etc.)
const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput;
await clientBuild(opts, internals, jsInput);
// SSG mode, generate pages.
@ -189,10 +178,11 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
const { astroConfig, viteConfig } = opts;
const ssr = astroConfig.buildOptions.experimentalSsr;
const out = ssr ? getServerRoot(astroConfig) : getOutRoot(astroConfig);
// TODO: use vite.mergeConfig() here?
return await vite.build({
logLevel: 'warn',
logLevel: 'error',
mode: 'production',
css: viteConfig.css,
build: {
...viteConfig.build,
emptyOutDir: false,
@ -200,6 +190,9 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
outDir: fileURLToPath(out),
ssr: true,
rollupOptions: {
// onwarn(warn) {
// console.log(warn);
// },
input: Array.from(input),
output: {
format: 'esm',
@ -240,9 +233,12 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
}
const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig);
// TODO: use vite.mergeConfig() here?
return await vite.build({
logLevel: 'warn',
logLevel: 'error',
mode: 'production',
css: viteConfig.css,
build: {
emptyOutDir: false,
minify: 'esbuild',
@ -275,38 +271,20 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
});
}
function getRenderers(opts: StaticBuildOptions) {
// All of the PageDatas have the same renderers, so just grab one.
const pageData = Object.values(opts.allPages)[0];
// These renderers have been loaded through Vite. To generate pages
// we need the ESM loaded version. This creates that.
const viteLoadedRenderers = pageData.preload[0];
return viteLoadedRenderers;
async function loadRenderer(renderer: AstroRenderer, config: AstroConfig): Promise<SSRLoadedRenderer> {
const mod = (await import(resolveDependency(renderer.serverEntrypoint, config))) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default };
}
async function collectRenderers(opts: StaticBuildOptions): Promise<Renderer[]> {
const viteLoadedRenderers = getRenderers(opts);
const renderers = await Promise.all(
viteLoadedRenderers.map(async (r) => {
const mod = await import(resolveDependency(r.serverEntry, opts.astroConfig));
return Object.create(r, {
ssr: {
value: mod.default,
},
}) as Renderer;
})
);
return renderers;
async function loadRenderers(config: AstroConfig): Promise<SSRLoadedRenderer[]> {
return Promise.all(config._ctx.renderers.map((r) => loadRenderer(r, config)));
}
async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
debug('build', 'Finish build. Begin generating.');
// Get renderers to be shared for each page generation.
const renderers = await collectRenderers(opts);
const renderers = await loadRenderers(opts.astroConfig);
for (let output of result.output) {
if (chunkIsPage(opts.astroConfig, output, internals)) {
@ -315,7 +293,13 @@ async function generatePages(result: RollupOutput, opts: StaticBuildOptions, int
}
}
async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>, renderers: Renderer[]) {
async function generatePage(
output: OutputChunk,
opts: StaticBuildOptions,
internals: BuildInternals,
facadeIdToPageDataMap: Map<string, PageBuildData>,
renderers: SSRLoadedRenderer[]
) {
const { astroConfig } = opts;
let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
@ -359,7 +343,7 @@ interface GeneratePathOptions {
linkIds: string[];
hoistedId: string | null;
mod: ComponentInstance;
renderers: Renderer[];
renderers: SSRLoadedRenderer[];
}
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
@ -377,6 +361,16 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
const links = createLinkStylesheetElementSet(linkIds.reverse(), site);
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
// Add all injected scripts to the page.
for (const script of astroConfig._ctx.scripts) {
if (script.stage === 'head-inline') {
scripts.add({
props: {},
children: script.content,
});
}
}
try {
const options: RenderOptions = {
legacyBuild: false,
@ -391,6 +385,14 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
async resolve(specifier: string) {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
if (typeof hashedFilePath !== 'string') {
// If no "astro:scripts/before-hydration.js" script exists in the build,
// then we can assume that no before-hydration scripts are needed.
// Return this as placeholder, which will be ignored by the browser.
// TODO: In the future, we hope to run this entire script through Vite,
// removing the need to maintain our own custom Vite-mimic resolve logic.
if (specifier === 'astro:scripts/before-hydration.js') {
return 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
}
throw new Error(`Cannot find the built path for ${specifier}`);
}
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
@ -480,7 +482,7 @@ async function generateManifest(result: RollupOutput, opts: StaticBuildOptions,
markdown: {
render: astroConfig.markdownOptions.render,
},
renderers: astroConfig.renderers,
renderers: astroConfig._ctx.renderers,
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
};
@ -628,8 +630,8 @@ export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals
}
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)!;
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
}
}

View file

@ -1,15 +1,48 @@
import type { AstroConfig, AstroUserConfig, CLIFlags } from '../@types/astro';
import type { Arguments as Flags } from 'yargs-parser';
import type * as Postcss from 'postcss';
import * as colors from 'kleur/colors';
import path from 'path';
import { pathToFileURL, fileURLToPath } from 'url';
import { mergeConfig as mergeViteConfig } from 'vite';
import { z } from 'zod';
import load from '@proload/core';
import loadTypeScript from '@proload/plugin-tsm';
import postcssrc from 'postcss-load-config';
import { arraify, isObject } from './util.js';
load.use([loadTypeScript]);
interface PostCSSConfigResult {
options: Postcss.ProcessOptions;
plugins: Postcss.Plugin[];
}
async function resolvePostcssConfig(inlineOptions: any, root: URL): Promise<PostCSSConfigResult> {
if (isObject(inlineOptions)) {
const options = { ...inlineOptions };
delete options.plugins;
return {
options,
plugins: inlineOptions.plugins || [],
};
}
const searchPath = typeof inlineOptions === 'string' ? inlineOptions : fileURLToPath(root);
try {
// @ts-ignore
return await postcssrc({}, searchPath);
} catch (err: any) {
if (!/No PostCSS Config found/.test(err.message)) {
throw err;
}
return {
options: {},
plugins: [],
};
}
}
export const AstroConfigSchema = z.object({
projectRoot: z
.string()
@ -36,7 +69,31 @@ export const AstroConfigSchema = z.object({
.optional()
.default('./dist')
.transform((val) => new URL(val)),
renderers: z.array(z.string()).optional().default(['@astrojs/renderer-svelte', '@astrojs/renderer-vue', '@astrojs/renderer-react', '@astrojs/renderer-preact']),
integrations: z.preprocess(
// preprocess
(val) => (Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val),
// validate
z
.array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }))
.default([])
// validate: first-party integrations only
// TODO: Add `To use 3rd-party integrations or to create your own, use the --experimental-integrations flag.`,
.refine((arr) => arr.every((integration) => integration.name.startsWith('@astrojs/')), {
message: `Astro integrations are still experimental, and only official integrations are currently supported`,
})
),
styleOptions: z
.object({
postcss: z
.object({
options: z.any(),
plugins: z.array(z.any()),
})
.optional()
.default({ options: {}, plugins: [] }),
})
.optional()
.default({}),
markdownOptions: z
.object({
render: z.any().optional().default(['@astrojs/markdown-remark', {}]),
@ -81,6 +138,37 @@ export const AstroConfigSchema = z.object({
/** Turn raw config values into normalized values */
export async function validateConfig(userConfig: any, root: string): Promise<AstroConfig> {
const fileProtocolRoot = pathToFileURL(root + path.sep);
// Manual deprecation checks
/* eslint-disable no-console */
if (userConfig.hasOwnProperty('renderers')) {
console.error('Astro "renderers" are now "integrations"!');
console.error('Update your configuration and install new dependencies:');
try {
const rendererKeywords = userConfig.renderers.map((r: string) => r.replace('@astrojs/renderer-', ''));
const rendererImports = rendererKeywords.map((r: string) => ` import ${r} from '@astrojs/${r}';`).join('\n');
const rendererIntegrations = rendererKeywords.map((r: string) => ` ${r}(),`).join('\n');
console.error('');
console.error(colors.dim(' // astro.config.js'));
if (rendererImports.length > 0) {
console.error(colors.green(rendererImports));
}
console.error('');
console.error(colors.dim(' // ...'));
if (rendererIntegrations.length > 0) {
console.error(colors.green(' integrations: ['));
console.error(colors.green(rendererIntegrations));
console.error(colors.green(' ],'));
} else {
console.error(colors.green(' integrations: [],'));
}
console.error('');
} catch (err) {
// We tried, better to just exit.
}
process.exit(1);
}
/* eslint-enable no-console */
// We need to extend the global schema to add transforms that are relative to root.
// This is type checked against the global schema to make sure we still match.
const AstroConfigRelativeSchema = AstroConfigSchema.extend({
@ -104,8 +192,26 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
.string()
.default('./dist')
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
styleOptions: z
.object({
postcss: z.preprocess(
(val) => resolvePostcssConfig(val, fileProtocolRoot),
z
.object({
options: z.any(),
plugins: z.array(z.any()),
})
.optional()
.default({ options: {}, plugins: [] })
),
})
.optional()
.default({}),
});
return AstroConfigRelativeSchema.parseAsync(userConfig);
return {
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
_ctx: { scripts: [], renderers: [] },
};
}
/** Adds '/' to end of string but doesnt double-up */
@ -175,7 +281,11 @@ export async function loadConfig(configOptions: LoadConfigOptions): Promise<Astr
if (config) {
userConfig = config.value;
}
// normalize, validate, and return
return resolveConfig(userConfig, root, flags);
}
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
export async function resolveConfig(userConfig: AstroUserConfig, root: string, flags: CLIFlags = {}): Promise<AstroConfig> {
const mergedConfig = mergeCLIFlags(userConfig, flags);
const validatedConfig = await validateConfig(mergedConfig, root);
return validatedConfig;
@ -185,3 +295,42 @@ export function formatConfigError(err: z.ZodError) {
const errorList = err.issues.map((issue) => ` ! ${colors.bold(issue.path.join('.'))} ${colors.red(issue.message + '.')}`);
return `${colors.red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join('\n')}`;
}
function mergeConfigRecursively(defaults: Record<string, any>, overrides: Record<string, any>, rootPath: string) {
const merged: Record<string, any> = { ...defaults };
for (const key in overrides) {
const value = overrides[key];
if (value == null) {
continue;
}
const existing = merged[key];
if (existing == null) {
merged[key] = value;
continue;
}
// fields that require special handling:
if (key === 'vite' && rootPath === '') {
merged[key] = mergeViteConfig(existing, value);
continue;
}
if (Array.isArray(existing) || Array.isArray(value)) {
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
continue;
}
if (isObject(existing) && isObject(value)) {
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
continue;
}
merged[key] = value;
}
return merged;
}
export function mergeConfig(defaults: Record<string, any>, overrides: Record<string, any>, isRoot = true): Record<string, any> {
return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.');
}

View file

@ -5,6 +5,7 @@ import { builtinModules } from 'module';
import { fileURLToPath } from 'url';
import fs from 'fs';
import * as vite from 'vite';
import { runHookServerSetup } from '../integrations/index.js';
import astroVitePlugin from '../vite-plugin-astro/index.js';
import astroViteServerPlugin from '../vite-plugin-astro-server/index.js';
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
@ -12,7 +13,8 @@ import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import envVitePlugin from '../vite-plugin-env/index.js';
import { resolveDependency } from './util.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
// Some packages are just external, and thats the way it goes.
const ALWAYS_EXTERNAL = new Set([
@ -41,12 +43,11 @@ interface CreateViteOptions {
}
/** Return a common starting point for all Vite actions */
export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig, logging, mode }: CreateViteOptions): Promise<ViteConfigWithSSR> {
export async function createVite(commandConfig: ViteConfigWithSSR, { astroConfig, logging, mode }: CreateViteOptions): Promise<ViteConfigWithSSR> {
// Scan for any third-party Astro packages. Vite needs these to be passed to `ssr.noExternal`.
const astroPackages = await getAstroPackages(astroConfig);
// Start with the Vite configuration that Astro core needs
let viteConfig: ViteConfigWithSSR = {
const commonConfig: ViteConfigWithSSR = {
cacheDir: fileURLToPath(new URL('./node_modules/.vite/', astroConfig.projectRoot)), // using local caches allows Astro to be used in monorepos, etc.
clearScreen: false, // we want to control the output, not Vite
logLevel: 'warn', // log warnings and errors only
@ -56,6 +57,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
plugins: [
configAliasVitePlugin({ config: astroConfig }),
astroVitePlugin({ config: astroConfig, logging }),
astroScriptsPlugin({ config: astroConfig }),
// The server plugin is for dev only and having it run during the build causes
// the build to run very slow as the filewatcher is triggered often.
mode === 'dev' && astroViteServerPlugin({ config: astroConfig, logging }),
@ -63,6 +65,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
markdownVitePlugin({ config: astroConfig }),
jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig }),
astroIntegrationsContainerPlugin({ config: astroConfig }),
],
publicDir: fileURLToPath(astroConfig.public),
root: fileURLToPath(astroConfig.projectRoot),
@ -75,6 +78,9 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
// add proxies here
},
},
css: {
postcss: astroConfig.styleOptions.postcss || {},
},
// Note: SSR API is in beta (https://vitejs.dev/guide/ssr.html)
ssr: {
external: [...ALWAYS_EXTERNAL],
@ -82,26 +88,16 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
},
};
// Add in Astro renderers, which will extend the base config
for (const name of astroConfig.renderers) {
try {
const { default: renderer } = await import(resolveDependency(name, astroConfig));
if (!renderer) continue;
// if a renderer provides viteConfig(), call it and pass in results
if (renderer.viteConfig) {
if (typeof renderer.viteConfig !== 'function') {
throw new Error(`${name}: viteConfig(options) must be a function! Got ${typeof renderer.viteConfig}.`);
}
const rendererConfig = await renderer.viteConfig({ mode: inlineConfig.mode, command: inlineConfig.mode === 'production' ? 'build' : 'serve' }); // is this command true?
viteConfig = vite.mergeConfig(viteConfig, rendererConfig) as ViteConfigWithSSR;
}
} catch (err) {
throw new Error(`${name}: ${err}`);
}
}
viteConfig = vite.mergeConfig(viteConfig, inlineConfig); // merge in inline Vite config
return viteConfig;
// Merge configs: we merge vite configuration objects together in the following order,
// where future values will override previous values.
// 1. common vite config
// 2. user-provided vite config, via AstroConfig
// 3. integration-provided vite config, via the `config:setup` hook
// 4. command vite config, passed as the argument to this function
let result = commonConfig;
result = vite.mergeConfig(result, astroConfig.vite || {});
result = vite.mergeConfig(result, commandConfig);
return result;
}
// Scans `projectRoot` for third-party Astro packages that could export an `.astro` file

View file

@ -1,12 +1,12 @@
import type { AstroConfig } from '../../@types/astro';
import type { AddressInfo } from 'net';
import { performance } from 'perf_hooks';
import { apply as applyPolyfill } from '../polyfill.js';
import { createVite } from '../create-vite.js';
import { defaultLogOptions, info, warn, LogOptions } from '../logger.js';
import * as vite from 'vite';
import type { AstroConfig } from '../../@types/astro';
import { runHookConfigDone, runHookConfigSetup, runHookServerDone, runHookServerSetup, runHookServerStart } from '../../integrations/index.js';
import { createVite } from '../create-vite.js';
import { defaultLogOptions, info, LogOptions, warn } from '../logger.js';
import * as msg from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { getResolvedHostForVite } from './util.js';
export interface DevOptions {
@ -22,31 +22,36 @@ export interface DevServer {
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
const devStart = performance.now();
applyPolyfill();
// TODO: remove call once --hostname is baselined
const host = getResolvedHostForVite(config);
const viteUserConfig = vite.mergeConfig(
config = await runHookConfigSetup({ config, command: 'dev' });
const viteConfig = await createVite(
{
mode: 'development',
server: { host },
// TODO: remove call once --hostname is baselined
server: { host: getResolvedHostForVite(config) },
},
config.vite || {}
{ astroConfig: config, logging: options.logging, mode: 'dev' }
);
const viteConfig = await createVite(viteUserConfig, { astroConfig: config, logging: options.logging, mode: 'dev' });
await runHookConfigDone({ config });
const viteServer = await vite.createServer(viteConfig);
runHookServerSetup({ config, server: viteServer });
await viteServer.listen(config.devOptions.port);
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
info(options.logging, null, msg.devStart({ startupTime: performance.now() - devStart, config, devServerAddressInfo, site, https: !!viteUserConfig.server?.https }));
info(options.logging, null, msg.devStart({ startupTime: performance.now() - devStart, config, devServerAddressInfo, site, https: !!viteConfig.server?.https }));
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
if (currentVersion.includes('-')) {
warn(options.logging, null, msg.prerelease({ currentVersion }));
}
await runHookServerStart({ config, address: devServerAddressInfo });
return {
address: devServerAddressInfo,
stop: () => viteServer.close(),
stop: async () => {
await viteServer.close();
await runHookServerDone({ config });
},
};
}

View file

@ -1,4 +1,4 @@
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, SSRLoadedRenderer, RouteData, SSRElement } from '../../@types/astro';
import type { LogOptions } from '../logger.js';
import type { AstroRequest } from './request';
@ -66,7 +66,7 @@ export interface RenderOptions {
pathname: string;
scripts: Set<SSRElement>;
resolve: (s: string) => Promise<string>;
renderers: Renderer[];
renderers: SSRLoadedRenderer[];
route?: RouteData;
routeCache: RouteCache;
site?: string;

View file

@ -1,7 +1,7 @@
import type * as vite from 'vite';
import path from 'path';
import { viteID } from '../../util.js';
import { unwrapId, viteID } from '../../util.js';
// https://vitejs.dev/guide/features.html#css-pre-processors
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']);
@ -13,41 +13,50 @@ const cssRe = new RegExp(
);
export const isCSSRequest = (request: string): boolean => cssRe.test(request);
/**
* getStylesForURL
* Given a filePath URL, crawl Vites module graph to find style files
*/
/** Given a filePath URL, crawl Vites module graph to find all style imports. */
export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> {
const css = new Set<string>();
const importedCssUrls = new Set<string>();
// recursively crawl module graph to get all style files imported by parent id
function crawlCSS(id: string, scanned = new Set<string>()) {
// note: use .getModulesByFile() to get all related nodes of the same URL
// using .getModuleById() could cause missing style imports on initial server load
const relatedMods = viteServer.moduleGraph.getModulesByFile(id) ?? new Set();
/** recursively crawl the module graph to get all style files imported by parent id */
function crawlCSS(_id: string, isFile: boolean, scanned = new Set<string>()) {
const id = unwrapId(_id);
const importedModules = new Set<vite.ModuleNode>();
const moduleEntriesForId = isFile
? // If isFile = true, then you are at the root of your module import tree.
// The `id` arg is a filepath, so use `getModulesByFile()` to collect all
// nodes for that file. This is needed for advanced imports like Tailwind.
viteServer.moduleGraph.getModulesByFile(id) ?? new Set()
: // Otherwise, you are following an import in the module import tree.
// You are safe to use getModuleById() here because Vite has already
// resolved the correct `id` for you, by creating the import you followed here.
new Set([viteServer.moduleGraph.getModuleById(id)!]);
for (const relatedMod of relatedMods) {
if (id === relatedMod.id) {
// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
if (id === entry.id) {
scanned.add(id);
for (const importedMod of relatedMod.importedModules) {
importedModules.add(importedMod);
for (const importedModule of entry.importedModules) {
importedModules.add(importedModule);
}
}
}
// scan importedModules
// scan imported modules for CSS imports & add them to our collection.
// Then, crawl that file to follow and scan all deep imports as well.
for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) continue;
const ext = path.extname(importedModule.url.toLowerCase());
if (STYLE_EXTENSIONS.has(ext)) {
css.add(importedModule.url); // note: return `url`s for HTML (not .id, which will break Windows)
if (!importedModule.id || scanned.has(importedModule.id)) {
continue;
}
crawlCSS(importedModule.id, scanned);
const ext = path.extname(importedModule.url).toLowerCase();
if (STYLE_EXTENSIONS.has(ext)) {
// NOTE: We use the `url` property here. `id` would break Windows.
importedCssUrls.add(importedModule.url);
}
crawlCSS(importedModule.id, false, scanned);
}
}
crawlCSS(viteID(filePath));
return css;
// Crawl your import graph for CSS files, populating `importedCssUrls` as a result.
crawlCSS(viteID(filePath), true);
return importedCssUrls;
}

View file

@ -1,19 +1,15 @@
import type * as vite from 'vite';
import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode, SSRElement } from '../../../@types/astro';
import type { AstroRequest } from '../request';
import { LogOptions } from '../../logger.js';
import { fileURLToPath } from 'url';
import { getStylesForURL } from './css.js';
import { injectTags } from './html.js';
import type * as vite from 'vite';
import type { AstroConfig, AstroRenderer, ComponentInstance, RouteData, RuntimeMode, SSRElement, SSRLoadedRenderer } from '../../../@types/astro';
import { LogOptions } from '../../logger.js';
import { render as coreRender } from '../core.js';
import { prependForwardSlash } from '../../../core/path.js';
import { RouteCache } from '../route-cache.js';
import { resolveRenderers } from './renderers.js';
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
import { getStylesForURL } from './css.js';
import { errorHandler } from './error.js';
import { getHmrScript } from './hmr.js';
import { prependForwardSlash } from '../../path.js';
import { render as coreRender } from '../core.js';
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
import { injectTags } from './html.js';
export interface SSROptions {
/** an instance of the AstroConfig */
astroConfig: AstroConfig;
@ -39,15 +35,25 @@ export interface SSROptions {
headers: Headers;
}
export type ComponentPreload = [Renderer[], ComponentInstance];
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
export type RenderResponse = { type: 'html'; html: string } | { type: 'response'; response: Response };
const svelteStylesRE = /svelte\?svelte&type=style/;
async function loadRenderer(viteServer: vite.ViteDevServer, renderer: AstroRenderer): Promise<SSRLoadedRenderer> {
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(renderer.serverEntrypoint);
const mod = (await viteServer.ssrLoadModule(url)) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default };
}
export async function loadRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<SSRLoadedRenderer[]> {
return Promise.all(astroConfig._ctx.renderers.map((r) => loadRenderer(viteServer, r)));
}
export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROptions, 'astroConfig' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
// Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await resolveRenderers(viteServer, astroConfig);
const renderers = await loadRenderers(viteServer, astroConfig);
// Load the module from the Vite SSR Runtime.
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
@ -55,7 +61,7 @@ export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROpt
}
/** use Vite to SSR */
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<RenderResponse> {
export async function render(renderers: SSRLoadedRenderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<RenderResponse> {
const { astroConfig, filePath, logging, mode, origin, pathname, method, headers, route, routeCache, viteServer } = ssrOpts;
const legacy = astroConfig.buildOptions.legacyBuild;
@ -69,10 +75,19 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
children: '',
});
scripts.add({
props: { type: 'module', src: new URL('../../../runtime/client/hmr.js', import.meta.url).pathname },
props: { type: 'module', src: '/@id/astro/client/hmr.js' },
children: '',
});
}
// TODO: We should allow adding generic HTML elements to the head, not just scripts
for (const script of astroConfig._ctx.scripts) {
if (script.stage === 'head-inline') {
scripts.add({
props: {},
children: script.content,
});
}
}
// Pass framework CSS in as link tags to be appended to the page.
let links = new Set<SSRElement>();
@ -105,13 +120,22 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
origin,
pathname,
scripts,
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/preact/client.js"
// TODO: Can we pass the hydration code more directly through Vite, so that we
// don't need to copy-paste and maintain Vite's import resolution here?
async resolve(s: string) {
// The legacy build needs these to remain unresolved so that vite HTML
// Can do the resolution. Without this condition the build output will be
// broken in the legacy build. This can be removed once the legacy build is removed.
if (!astroConfig.buildOptions.legacyBuild) {
const [, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
const [resolvedUrl, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
if (resolvedPath.includes('node_modules/.vite')) {
return resolvedPath.replace(/.*?node_modules\/\.vite/, '/node_modules/.vite');
}
// NOTE: This matches the same logic that Vite uses to add the `/@id/` prefix.
if (!resolvedUrl.startsWith('.') && !resolvedUrl.startsWith('/')) {
return '/@id' + prependForwardSlash(resolvedUrl);
}
return '/@fs' + prependForwardSlash(resolvedPath);
} else {
return s;

View file

@ -1,36 +0,0 @@
import type * as vite from 'vite';
import type { AstroConfig, Renderer } from '../../../@types/astro';
import { resolveDependency } from '../../util.js';
import { createRenderer } from '../renderer.js';
const cache = new Map<string, Promise<Renderer>>();
async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string, astroConfig: AstroConfig): Promise<Renderer> {
const resolvedRenderer: Renderer = await createRenderer(renderer, {
renderer(name) {
return import(resolveDependency(name, astroConfig));
},
async server(entry) {
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(entry);
const mod = await viteServer.ssrLoadModule(url);
return mod;
},
});
return resolvedRenderer;
}
export async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<Renderer[]> {
const ids: string[] = astroConfig.renderers;
const renderers = await Promise.all(
ids.map((renderer) => {
if (cache.has(renderer)) return cache.get(renderer)!;
let promise = resolveRenderer(viteServer, renderer, astroConfig);
cache.set(renderer, promise);
return promise;
})
);
return renderers;
}

View file

@ -1,30 +0,0 @@
import type { Renderer } from '../../@types/astro';
import npath from 'path';
interface RendererResolverImplementation {
renderer: (name: string) => Promise<any>;
server: (entry: string) => Promise<any>;
}
export async function createRenderer(renderer: string, impl: RendererResolverImplementation) {
const resolvedRenderer: any = {};
// We can dynamically import the renderer by itself because it shouldn't have
// any non-standard imports, the index is just meta info.
// The other entrypoints need to be loaded through Vite.
const {
default: { name, client, polyfills, hydrationPolyfills, server },
} = await impl.renderer(renderer); //await import(resolveDependency(renderer, astroConfig));
resolvedRenderer.name = name;
if (client) resolvedRenderer.source = npath.posix.join(renderer, client);
resolvedRenderer.serverEntry = npath.posix.join(renderer, server);
if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => npath.posix.join(renderer, src));
if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => npath.posix.join(renderer, src));
const { default: rendererSSR } = await impl.server(resolvedRenderer.serverEntry);
resolvedRenderer.ssr = rendererSSR;
const completedRenderer: Renderer = resolvedRenderer;
return completedRenderer;
}

View file

@ -1,12 +1,10 @@
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
import type { AstroRequest } from './request';
import { bold } from 'kleur/colors';
import { createRequest } from './request.js';
import { isCSSRequest } from './dev/css.js';
import { isScriptRequest } from './script.js';
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro';
import { renderSlot } from '../../runtime/server/index.js';
import { warn, LogOptions } from '../logger.js';
import { LogOptions, warn } from '../logger.js';
import { isCSSRequest } from './dev/css.js';
import { createRequest } from './request.js';
import { isScriptRequest } from './script.js';
function onlyAvailableInSSR(name: string) {
return function () {
@ -23,7 +21,7 @@ export interface CreateResultArgs {
markdownRender: MarkdownRenderOptions;
params: Params;
pathname: string;
renderers: Renderer[];
renderers: SSRLoadedRenderer[];
resolve: (s: string) => Promise<string>;
site: string | undefined;
links?: Set<SSRElement>;

View file

@ -25,6 +25,16 @@ export function isValidURL(url: string): boolean {
return false;
}
/** Returns true if argument is an object of any prototype/class (but not null). */
export function isObject(value: unknown): value is Record<string, any> {
return typeof value === 'object' && value != null;
}
/** Wraps an object in an array. If an array is passed, ignore it. */
export function arraify<T>(target: T | T[]): T[] {
return Array.isArray(target) ? target : [target];
}
/** is a specifier an npm package? */
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
// not an npm package
@ -98,6 +108,14 @@ export function viteID(filePath: URL): string {
return slash(fileURLToPath(filePath));
}
export const VALID_ID_PREFIX = `/@id/`;
// Strip valid id prefix. This is prepended to resolved Ids that are
// not valid browser import specifiers by the importAnalysis plugin.
export function unwrapId(id: string): string {
return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length) : id;
}
/** An fs utility, similar to `rimraf` or `rm -rf` */
export function removeDir(_dir: URL): void {
const dir = fileURLToPath(_dir);

View file

@ -0,0 +1,76 @@
import type { AddressInfo } from 'net';
import type { ViteDevServer } from 'vite';
import { AstroConfig, AstroRenderer } from '../@types/astro.js';
import { mergeConfig } from '../core/config.js';
export async function runHookConfigSetup({ config: _config, command }: { config: AstroConfig; command: 'dev' | 'build' }): Promise<AstroConfig> {
let updatedConfig: AstroConfig = { ..._config };
for (const integration of _config.integrations) {
if (integration.hooks['astro:config:setup']) {
await integration.hooks['astro:config:setup']({
config: updatedConfig,
command,
addRenderer(renderer: AstroRenderer) {
updatedConfig._ctx.renderers.push(renderer);
},
injectScript: (stage, content) => {
updatedConfig._ctx.scripts.push({ stage, content });
},
updateConfig: (newConfig) => {
updatedConfig = mergeConfig(updatedConfig, newConfig) as AstroConfig;
},
});
}
}
return updatedConfig;
}
export async function runHookConfigDone({ config }: { config: AstroConfig }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:config:done']) {
await integration.hooks['astro:config:done']({
config,
});
}
}
}
export async function runHookServerSetup({ config, server }: { config: AstroConfig; server: ViteDevServer }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:setup']) {
await integration.hooks['astro:server:setup']({ server });
}
}
}
export async function runHookServerStart({ config, address }: { config: AstroConfig; address: AddressInfo }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:start']) {
await integration.hooks['astro:server:start']({ address });
}
}
}
export async function runHookServerDone({ config }: { config: AstroConfig }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:done']) {
await integration.hooks['astro:server:done']();
}
}
}
export async function runHookBuildStart({ config }: { config: AstroConfig }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:start']) {
await integration.hooks['astro:build:start']();
}
}
}
export async function runHookBuildDone({ config, pages }: { config: AstroConfig; pages: string[] }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:done']) {
await integration.hooks['astro:build:done']({ pages: pages.map((p) => ({ pathname: p })), dir: config.dist });
}
}
}

View file

@ -1,4 +1,4 @@
import type { AstroComponentMetadata } from '../../@types/astro';
import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro';
import type { SSRElement, SSRResult } from '../../@types/astro';
import { hydrationSpecifier, serializeListValue } from './util.js';
import serializeJavaScript from 'serialize-javascript';
@ -81,7 +81,7 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
}
interface HydrateScriptOptions {
renderer: any;
renderer: SSRLoadedRenderer;
result: SSRResult;
astroId: string;
props: Record<string | number, any>;
@ -96,16 +96,11 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`);
}
let hydrationSource = '';
if (renderer.hydrationPolyfills) {
hydrationSource += `await Promise.all([${(await Promise.all(renderer.hydrationPolyfills.map(async (src: string) => `\n import("${await result.resolve(src)}")`))).join(
', '
)}]);\n`;
}
let hydrationSource = ``;
hydrationSource += renderer.source
hydrationSource += renderer.clientEntrypoint
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(componentUrl)}"), import("${await result.resolve(
renderer.source
renderer.clientEntrypoint
)}")]);
return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
`
@ -116,6 +111,7 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
const hydrationScript = {
props: { type: 'module', 'data-astro-component-hydration': true },
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
${`import '${await result.resolve('astro:scripts/before-hydration.js')}';`}
setup("${astroId}", {name:"${metadata.displayName}",${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
${hydrationSource}
});

View file

@ -1,19 +1,14 @@
import type { AstroComponentMetadata, EndpointHandler, Renderer, Params } from '../../@types/astro';
import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/astro';
import type { AstroRequest } from '../../core/render/request';
import shorthash from 'shorthash';
import type { AstroComponentMetadata, AstroGlobalPartial, EndpointHandler, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro';
import type { AstroRequest } from '../../core/render/request';
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
import { serializeListValue } from './util.js';
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
export type { Metadata } from './metadata';
export { createMetadata } from './metadata.js';
export { markHTMLString } from './escape.js';
// TODO(deprecated): This name has been updated in Astro runtime but not yet in the Astro compiler.
export { markHTMLString as unescapeHTML } from './escape.js';
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes =
/^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
@ -116,14 +111,14 @@ function guessRenderers(componentUrl?: string): string[] {
const extname = componentUrl?.split('.').pop();
switch (extname) {
case 'svelte':
return ['@astrojs/renderer-svelte'];
return ['@astrojs/svelte'];
case 'vue':
return ['@astrojs/renderer-vue'];
return ['@astrojs/vue'];
case 'jsx':
case 'tsx':
return ['@astrojs/renderer-react', '@astrojs/renderer-preact'];
return ['@astrojs/react', '@astrojs/preact'];
default:
return ['@astrojs/renderer-react', '@astrojs/renderer-preact', '@astrojs/renderer-vue', '@astrojs/renderer-svelte'];
return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue', '@astrojs/svelte'];
}
}
@ -171,13 +166,13 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !componentIsHTMLElement(Component)) {
const message = `Unable to render ${metadata.displayName}!
There are no \`renderers\` set in your \`astro.config.mjs\` file.
Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
There are no \`integrations\` set in your \`astro.config.mjs\` file.
Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
throw new Error(message);
}
// Call the renderers `check` hook to see if any claim this component.
let renderer: Renderer | undefined;
let renderer: SSRLoadedRenderer | undefined;
if (metadata.hydrate !== 'only') {
for (const r of renderers) {
if (await r.ssr.check(Component, props, children)) {
@ -195,7 +190,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
// Attempt: use explicitly passed renderer name
if (metadata.hydrateArgs) {
const rendererName = metadata.hydrateArgs;
renderer = renderers.filter(({ name }) => name === `@astrojs/renderer-${rendererName}` || name === rendererName)[0];
renderer = renderers.filter(({ name }) => name === `@astrojs/${rendererName}` || name === rendererName)[0];
}
// Attempt: user only has a single renderer, default to that
if (!renderer && renderers.length === 1) {
@ -204,7 +199,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
// Attempt: can we guess the renderer from the export extension?
if (!renderer) {
const extname = metadata.componentUrl?.split('.').pop();
renderer = renderers.filter(({ name }) => name === `@astrojs/renderer-${extname}` || name === extname)[0];
renderer = renderers.filter(({ name }) => name === `@astrojs/${extname}` || name === extname)[0];
}
}
@ -215,7 +210,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
throw new Error(`Unable to render ${metadata.displayName}!
Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.
Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames.map((r) => r.replace('@astrojs/renderer-', '')).join('|')}" />
Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames.map((r) => r.replace('@astrojs/', '')).join('|')}" />
`);
} else if (typeof Component !== 'string') {
const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name));
@ -264,16 +259,6 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
);
}
// This is used to add polyfill scripts to the page, if the renderer needs them.
if (renderer?.polyfills?.length) {
for (const src of renderer.polyfills) {
result.scripts.add({
props: { type: 'module' },
children: `import "${await result.resolve(src)}";`,
});
}
}
if (!hydration) {
return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, ''));
}
@ -283,7 +268,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, result, astroId, props }, metadata as Required<AstroComponentMetadata>));
result.scripts.add(await generateHydrateScript({ renderer: renderer!, result, astroId, props }, metadata as Required<AstroComponentMetadata>));
// Render a template if no fragment is provided.
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);

View file

@ -122,13 +122,7 @@ export function invalidateCompilation(config: AstroConfig, filename: string) {
}
}
export async function cachedCompilation(
config: AstroConfig,
filename: string,
source: string | null,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
export async function cachedCompilation(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> {
let cache: CompilationCache;
if (!configCache.has(config)) {
cache = new Map();
@ -139,11 +133,6 @@ export async function cachedCompilation(
if (cache.has(filename)) {
return cache.get(filename)!;
}
if (source === null) {
const fileUrl = new URL(`file://${filename}`);
source = await fs.promises.readFile(fileUrl, 'utf-8');
}
const compileResult = await compile(config, filename, source, viteTransform, opts);
cache.set(filename, compileResult);
return compileResult;

View file

@ -35,7 +35,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return slash(fileURLToPath(url)) + url.search;
}
let isProduction: boolean;
let resolvedConfig: vite.ResolvedConfig;
let viteTransform: TransformHook;
let viteDevServer: vite.ViteDevServer | null = null;
@ -46,9 +46,9 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return {
name: 'astro:build',
enforce: 'pre', // run transforms before other plugins can
configResolved(resolvedConfig) {
configResolved(_resolvedConfig) {
resolvedConfig = _resolvedConfig;
viteTransform = getViteTransform(resolvedConfig);
isProduction = resolvedConfig.isProduction;
},
configureServer(server) {
viteDevServer = server;
@ -83,20 +83,26 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
}
},
async load(id, opts) {
let { filename, query } = parseAstroRequest(id);
const parsedId = parseAstroRequest(id);
const query = parsedId.query;
if (!id.endsWith('.astro') && !query.astro) {
return null;
}
const filename = normalizeFilename(parsedId.filename);
const fileUrl = new URL(`file://${filename}`);
let source = await fs.promises.readFile(fileUrl, 'utf-8');
const isPage = filename.startsWith(config.pages.pathname);
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
source += `\n<script hoist src="astro:scripts/page.js" />`;
}
if (query.astro) {
if (query.type === 'style') {
if (filename.startsWith('/@fs')) {
filename = filename.slice('/@fs'.length);
} else if (filename.startsWith('/') && !ancestor(filename, config.projectRoot.pathname)) {
filename = new URL('.' + filename, config.projectRoot).pathname;
}
if (typeof query.index === 'undefined') {
throw new Error(`Requests for Astro CSS must include an index.`);
}
const transformResult = await cachedCompilation(config, normalizeFilename(filename), null, viteTransform, { ssr: Boolean(opts?.ssr) });
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) });
// Track any CSS dependencies so that HMR is triggered when they change.
await trackCSSDependencies.call(this, { viteDevServer, id, filename, deps: transformResult.rawCSSDeps });
@ -111,7 +117,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
throw new Error(`Requests for hoisted scripts must include an index`);
}
const transformResult = await cachedCompilation(config, normalizeFilename(filename), null, viteTransform, { ssr: Boolean(opts?.ssr) });
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) });
const scripts = transformResult.scripts;
const hoistedScript = scripts[query.index];
@ -125,13 +131,8 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
}
}
if (!id.endsWith('.astro')) {
return null;
}
const source = await fs.promises.readFile(id, { encoding: 'utf-8' });
try {
const transformResult = await cachedCompilation(config, id, source, viteTransform, { ssr: Boolean(opts?.ssr) });
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) });
// Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning.
@ -143,9 +144,15 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
define: config.vite.define,
});
// Signal to Vite that we accept HMR updates
const SUFFIX = isProduction ? '' : `\nif (import.meta.hot) import.meta.hot.accept((mod) => mod);`;
let SUFFIX = '';
// Add HMR handling in dev mode.
if (!resolvedConfig.isProduction) {
SUFFIX += `\nif (import.meta.hot) import.meta.hot.accept((mod) => mod);`;
}
// Add handling to inject scripts into each page JS bundle, if needed.
if (isPage) {
SUFFIX += `\nimport "astro:scripts/page-ssr.js";`;
}
return {
code: `${code}${SUFFIX}`,
map,

View file

@ -0,0 +1,13 @@
import { Plugin as VitePlugin, ResolvedConfig } from 'vite';
import { AstroConfig } from '../@types/astro.js';
import { runHookServerSetup } from '../integrations/index.js';
/** Connect Astro integrations into Vite, as needed. */
export default function astroIntegrationsContainerPlugin({ config }: { config: AstroConfig }): VitePlugin {
return {
name: 'astro:integration-container',
configureServer(server) {
runHookServerSetup({ config, server });
},
};
}

View file

@ -1,6 +1,6 @@
import type { TransformResult } from 'rollup';
import type { Plugin, ResolvedConfig } from 'vite';
import type { AstroConfig, Renderer } from '../@types/astro';
import type { AstroConfig, AstroRenderer } from '../@types/astro';
import type { LogOptions } from '../core/logger.js';
import babel from '@babel/core';
@ -9,9 +9,9 @@ import * as colors from 'kleur/colors';
import * as eslexer from 'es-module-lexer';
import path from 'path';
import { error } from '../core/logger.js';
import { parseNpmName, resolveDependency } from '../core/util.js';
import { parseNpmName } from '../core/util.js';
const JSX_RENDERER_CACHE = new WeakMap<AstroConfig, Map<string, Renderer>>();
const JSX_RENDERER_CACHE = new WeakMap<AstroConfig, Map<string, AstroRenderer>>();
const JSX_EXTENSIONS = new Set(['.jsx', '.tsx']);
const IMPORT_STATEMENTS: Record<string, string> = {
react: "import React from 'react'",
@ -28,24 +28,16 @@ function getEsbuildLoader(fileExt: string): string {
return fileExt.substr(1);
}
async function importJSXRenderers(config: AstroConfig): Promise<Map<string, Renderer>> {
const renderers = new Map<string, Renderer>();
await Promise.all(
config.renderers.map((name) => {
return import(resolveDependency(name, config)).then(({ default: renderer }) => {
if (!renderer.jsxImportSource) return;
renderers.set(renderer.jsxImportSource, renderer);
});
})
);
return renderers;
function collectJSXRenderers(renderers: AstroRenderer[]): Map<string, AstroRenderer> {
const renderersWithJSXSupport = renderers.filter((r) => r.jsxImportSource);
return new Map(renderersWithJSXSupport.map((r) => [r.jsxImportSource, r] as [string, AstroRenderer]));
}
interface TransformJSXOptions {
code: string;
id: string;
mode: string;
renderer: Renderer;
renderer: AstroRenderer;
ssr: boolean;
}
@ -100,13 +92,13 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
// load renderers (on first run only)
if (!jsxRenderers) {
jsxRenderers = new Map();
const possibleRenderers = await importJSXRenderers(config);
const possibleRenderers = await collectJSXRenderers(config._ctx.renderers);
if (possibleRenderers.size === 0) {
// note: we have filtered out all non-JSX files, so this error should only show if a JSX file is loaded with no matching renderers
throw new Error(
`${colors.yellow(
id
)}\nUnable to resolve a renderer that handles JSX transforms! Please include a \`renderer\` plugin which supports JSX in your \`astro.config.mjs\` file.`
)}\nUnable to resolve a JSX renderer! Did you forget to include one? Add a JSX integration like \`@astrojs/react\` to your \`astro.config.mjs\` file.`
);
}
for (const [importSource, renderer] of possibleRenderers) {
@ -173,7 +165,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
const jsxRenderer = jsxRenderers.get(importSource);
// if renderer not installed for this JSX source, throw error
if (!jsxRenderer) {
error(logging, 'renderer', `${colors.yellow(id)} No renderer installed for ${importSource}. Try adding \`@astrojs/renderer-${importSource}\` to your dependencies.`);
error(logging, 'renderer', `${colors.yellow(id)} No renderer installed for ${importSource}. Try adding \`@astrojs/${importSource}\` to your project.`);
return null;
}
// downlevel any non-standard syntax, but preserve JSX
@ -183,7 +175,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
sourcefile: id,
sourcemap: 'inline',
});
return await transformJSX({ code: jsxCode, id, renderer: jsxRenderers.get(importSource) as Renderer, mode, ssr });
return await transformJSX({ code: jsxCode, id, renderer: jsxRenderers.get(importSource) as AstroRenderer, mode, ssr });
}
// if we still cant tell, throw error

View file

@ -0,0 +1,59 @@
import { Plugin as VitePlugin } from 'vite';
import { AstroConfig } from '../@types/astro.js';
// NOTE: We can't use the virtual "\0" ID convention because we need to
// inject these as ESM imports into actual code, where they would not
// resolve correctly.
const SCRIPT_ID_PREFIX = `astro:scripts/`;
const BEFORE_HYDRATION_SCRIPT_ID = `${SCRIPT_ID_PREFIX}before-hydration.js`;
const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}page.js`;
const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}page-ssr.js`;
export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin {
return {
name: 'astro:scripts',
async resolveId(id) {
if (id.startsWith(SCRIPT_ID_PREFIX)) {
return id;
}
return undefined;
},
async load(id) {
if (id === BEFORE_HYDRATION_SCRIPT_ID) {
return config._ctx.scripts
.filter((s) => s.stage === 'before-hydration')
.map((s) => s.content)
.join('\n');
}
if (id === PAGE_SCRIPT_ID) {
return config._ctx.scripts
.filter((s) => s.stage === 'page')
.map((s) => s.content)
.join('\n');
}
if (id === PAGE_SSR_SCRIPT_ID) {
return config._ctx.scripts
.filter((s) => s.stage === 'page-ssr')
.map((s) => s.content)
.join('\n');
}
return null;
},
buildStart(options) {
// We only want to inject this script if we are building
// for the frontend AND some hydrated components exist in
// the final build. We can detect this by looking for a
// `astro/client/*` input, which signifies both conditions are met.
const hasHydratedComponents = Array.isArray(options.input) && options.input.some((input) => input.startsWith('astro/client'));
const hasHydrationScripts = config._ctx.scripts.some((s) => s.stage === 'before-hydration');
if (hasHydratedComponents && hasHydrationScripts) {
this.emitFile({
type: 'chunk',
id: BEFORE_HYDRATION_SCRIPT_ID,
name: BEFORE_HYDRATION_SCRIPT_ID,
});
}
},
};
}

View file

@ -12,15 +12,7 @@ let fixture;
describe('CSS', function () {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/0-css/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
fixture = await loadFixture({ projectRoot: './fixtures/0-css/' });
});
// test HTML and CSS contents for accuracy

View file

@ -13,11 +13,6 @@ describe('Assets', () => {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-assets/',
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
await fixture.build();
});

View file

@ -6,10 +6,7 @@ describe('Component children', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-children/',
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-vue', '@astrojs/renderer-svelte'],
});
fixture = await loadFixture({ projectRoot: './fixtures/astro-children/' });
await fixture.build();
});

View file

@ -32,24 +32,11 @@ describe('Dynamic components', () => {
const html = await fixture.readFile('/client-only/index.html');
const $ = cheerio.load(html);
// test 1: <astro-root> is empty
// test 1: <astro-root> is empty.
expect($('<astro-root>').html()).to.equal('');
const script = $('script').text();
// Grab the svelte import
// const exp = /import\("(.+?)"\)/g;
// let match, svelteRenderer;
// while ((match = exp.exec(result.contents))) {
// if (match[1].includes('renderers/renderer-svelte/client.js')) {
// svelteRenderer = match[1];
// }
// }
// test 2: Svelte renderer is on the page
// expect(svelteRenderer).to.be.ok;
// test 3: Can load svelte renderer
// const result = await fixture.fetch(svelteRenderer);
// expect(result.status).to.equal(200);
// test 2: correct script is being loaded.
// because of bundling, we don't have access to the source import,
// only the bundled import.
expect($('script').html()).to.include(`import setup from '../only`);
});
});

View file

@ -8,7 +8,6 @@ describe('Expressions', () => {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-expr/',
renderers: ['@astrojs/renderer-preact'],
});
await fixture.build();
});

View file

@ -8,7 +8,6 @@ describe('Dynamic component fallback', () => {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-fallback',
renderers: ['@astrojs/renderer-preact'],
});
await fixture.build();
});

View file

@ -1,43 +0,0 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
describe('JSX', () => {
let cwd = './fixtures/astro-jsx/';
let orders = [
['preact', 'react', 'solid'],
['preact', 'solid', 'react'],
['react', 'preact', 'solid'],
['react', 'solid', 'preact'],
['solid', 'react', 'preact'],
['solid', 'preact', 'react'],
];
let fixtures = {};
before(async () => {
await Promise.all(
orders.map((renderers, n) =>
loadFixture({
projectRoot: cwd,
renderers: renderers.map((name) => `@astrojs/renderer-${name}`),
dist: new URL(`${cwd}dist-${n}/`, import.meta.url),
}).then((fixture) => {
fixtures[renderers.toString()] = fixture;
return fixture.build();
})
)
);
});
it('Renderer order', () => {
it('JSX renderers can be defined in any order', async () => {
if (!Object.values(fixtures).length) {
throw new Error(`JSX renderers didnt build properly`);
}
for (const [name, fixture] of Object.entries(fixtures)) {
const html = await fixture.readFile('/index.html');
expect(html, name).to.be.ok;
}
});
});
});

View file

@ -10,7 +10,6 @@ describe('Astro Markdown plugins', () => {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-markdown-plugins/',
renderers: ['@astrojs/renderer-preact'],
markdownOptions: {
render: [
markdownRemark,

View file

@ -8,10 +8,6 @@ describe('Astro Markdown', () => {
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-markdown/',
renderers: ['@astrojs/renderer-preact'],
buildOptions: {
sitemap: false,
},
});
await fixture.build();
});

View file

@ -2,7 +2,7 @@ import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Partial HTML ', async () => {
describe('Partial HTML', async () => {
let fixture;
let devServer;

View file

@ -31,7 +31,7 @@ describe('Config Validation', () => {
it('Multiple validation errors can be formatted correctly', async () => {
const veryBadConfig = {
renderers: [42],
integrations: [42],
buildOptions: { pageUrlFormat: 'invalid' },
pages: {},
};
@ -41,8 +41,34 @@ describe('Config Validation', () => {
expect(formattedError).to.equal(
`[config] Astro found issue(s) with your configuration:
! pages Expected string, received object.
! renderers.0 Expected string, received number.
! integrations.0 Expected object, received number.
! buildOptions.pageUrlFormat Invalid input.`
);
});
it('ignores falsey "integration" values', async () => {
const result = await validateConfig({ integrations: [0, false, null, undefined] }, process.cwd());
expect(result.integrations).to.deep.equal([]);
});
it('normalizes "integration" values', async () => {
const result = await validateConfig({ integrations: [{ name: '@astrojs/a' }] }, process.cwd());
expect(result.integrations).to.deep.equal([{ name: '@astrojs/a', hooks: {} }]);
});
it('flattens array "integration" values', async () => {
const result = await validateConfig({ integrations: [{ name: '@astrojs/a' }, [{ name: '@astrojs/b' }, { name: '@astrojs/c' }]] }, process.cwd());
expect(result.integrations).to.deep.equal([
{ name: '@astrojs/a', hooks: {} },
{ name: '@astrojs/b', hooks: {} },
{ name: '@astrojs/c', hooks: {} },
]);
});
it('blocks third-party "integration" values', async () => {
const configError = await validateConfig({ integrations: [{ name: '@my-plugin/a' }] }, process.cwd()).catch((err) => err);
expect(configError instanceof z.ZodError).to.equal(true);
const formattedError = stripAnsi(formatConfigError(configError));
expect(formattedError).to.equal(
`[config] Astro found issue(s) with your configuration:
! integrations Astro integrations are still experimental, and only official integrations are currently supported.`
);
});
});

View file

@ -10,9 +10,24 @@ describe('config', () => {
before(async () => {
[hostnameFixture, hostFixture, portFixture] = await Promise.all([
loadFixture({ projectRoot: './fixtures/config-hostname/' }),
loadFixture({ projectRoot: './fixtures/config-host/' }),
loadFixture({ projectRoot: './fixtures/config-port/' }),
loadFixture({
projectRoot: './fixtures/config-host/',
devOptions: {
hostname: '0.0.0.0',
},
}),
loadFixture({
projectRoot: './fixtures/config-host/',
devOptions: {
host: true,
},
}),
loadFixture({
projectRoot: './fixtures/config-host/',
devOptions: {
port: 5006,
},
}),
]);
});

Some files were not shown because too many files have changed in this diff Show more