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: with:
name: artifacts name: artifacts
path: | path: |
packages/**/dist/** packages/*/dist/**
packages/*/*/dist/**
packages/webapi/mod.js packages/webapi/mod.js
packages/webapi/mod.js.map packages/webapi/mod.js.map
if-no-files-found: error if-no-files-found: error

View file

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

View file

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

View file

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

View file

@ -10,6 +10,6 @@
}, },
"devDependencies": { "devDependencies": {
"astro": "^0.24.3", "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'; import { defineConfig } from 'astro/config';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({});
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
"preview": "astro preview" "preview": "astro preview"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/renderer-vue": "^0.4.0", "@astrojs/vue": "^0.0.1",
"astro": "^0.24.3" "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'; import { defineConfig } from 'astro/config';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({});
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,11 @@
import { defineConfig } from 'astro/config'; 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 // https://astro.build/config
export default defineConfig({ 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": { "devDependencies": {
"@astrojs/markdown-remark": "^0.6.4", "@astrojs/markdown-remark": "^0.6.4",
"@astrojs/renderer-preact": "^0.5.0", "@astrojs/preact": "^0.0.1",
"@astrojs/renderer-react": "^0.5.0", "@astrojs/react": "^0.0.1",
"@astrojs/renderer-svelte": "^0.5.2", "@astrojs/svelte": "^0.0.1",
"@astrojs/renderer-vue": "^0.4.0", "@astrojs/vue": "^0.0.1",
"astro": "^0.24.3" "astro": "^0.24.3"
} }
} }

View file

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

View file

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

View file

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

View file

@ -9,9 +9,10 @@
"preview": "astro preview" "preview": "astro preview"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/renderer-preact": "^0.5.0", "@astrojs/tailwind": "^0.0.1",
"astro": "^0.24.3", "astro": "^0.24.3",
"autoprefixer": "^10.4.4", "autoprefixer": "^10.4.4",
"canvas-confetti": "^1.5.1",
"postcss": "^8.4.12", "postcss": "^8.4.12",
"tailwindcss": "^3.0.23" "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">
<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}
>
<slot /> <slot />
</button> </button>
<script hoist>
import confetti from 'canvas-confetti';
document.body.querySelector('button').addEventListener("click", () => confetti());
</script>

View file

@ -1,7 +1,6 @@
--- ---
// Component Imports // Component Imports
import Button from '../components/Button.astro'; import Button from '../components/Button.astro';
import '../styles/global.css';
// Full Astro Component Syntax: // Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/ // https://docs.astro.build/core-concepts/astro-components/
@ -15,6 +14,8 @@ import '../styles/global.css';
</head> </head>
<body> <body>
<Button>Im a Tailwind Button!</Button> <div class="grid place-items-center h-screen">
<Button>Click Me!</Button>
</div>
</body> </body>
</html> </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 // https://astro.build/config
export default defineConfig({ export default defineConfig({
renderers: [],
vite: { vite: {
plugins: [VitePWA()], plugins: [VitePWA()],
}, },

View file

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

View file

@ -57,7 +57,7 @@
"dev": "astro-scripts dev \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"", "postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js", "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" "test:match": "mocha --timeout 20000 -g"
}, },
"dependencies": { "dependencies": {
@ -65,10 +65,6 @@
"@astrojs/language-server": "^0.8.10", "@astrojs/language-server": "^0.8.10",
"@astrojs/markdown-remark": "^0.6.4", "@astrojs/markdown-remark": "^0.6.4",
"@astrojs/prism": "0.4.0", "@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", "@astrojs/webapi": "^0.11.0",
"@babel/core": "^7.17.7", "@babel/core": "^7.17.7",
"@babel/traverse": "^7.17.3", "@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 babel from '@babel/core';
import type * as vite from 'vite';
import type { z } from 'zod'; import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config'; import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server'; import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request'; import type { AstroRequest } from '../core/render/request';
import type * as vite from 'vite';
export interface AstroBuiltinProps { export interface AstroBuiltinProps {
'client:load'?: boolean; 'client:load'?: boolean;
@ -127,21 +128,30 @@ export interface AstroUserConfig {
/** /**
* @docs * @docs
* @name renderers * @name integrations
* @type {string[]} * @type {AstroIntegration[]}
* @default `['@astrojs/renderer-svelte','@astrojs/renderer-vue','@astrojs/renderer-react','@astrojs/renderer-preact']` * @default `[]`
* @description * @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 * ```js
* import react from '@astrojs/react';
* import vue from '@astrojs/vue';
* { * {
* // Use Astro + React, with no other frameworks. * // Example: Use Astro with Vue + React, and no other frameworks.
* renderers: ['@astrojs/renderer-react'] * integrations: [react(), vue()]
* } * }
* ``` * ```
*/ */
integrations?: AstroIntegration[];
/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
renderers?: string[]; renderers?: string[];
/** /**
@ -170,6 +180,7 @@ export interface AstroUserConfig {
* } * }
* ``` * ```
*/ */
/** Options for rendering markdown content */
markdownOptions?: { markdownOptions?: {
render?: MarkdownRenderOptions; render?: MarkdownRenderOptions;
}; };
@ -379,7 +390,7 @@ export interface AstroUserConfig {
/** /**
* @docs * @docs
* @name devOptions.vite * @name vite
* @type {vite.UserConfig} * @type {vite.UserConfig}
* @description * @description
* *
@ -421,11 +432,33 @@ export interface AstroUserConfig {
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> { // 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 * Resolved Astro Config
* Config with user settings along with all defaults filled in. * 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>; 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; [method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response;
} }
/** export interface AstroRenderer {
* Astro Renderer /** Name of the renderer. */
* Docs: https://docs.astro.build/reference/renderer-reference/
*/
export interface Renderer {
/** Name of the renderer (required) */
name: string; name: string;
/** Import statement for renderer */ /** Import entrypoint for the client/browser renderer. */
source?: string; clientEntrypoint?: string;
/** Import statement for the server renderer */ /** Import entrypoint for the server/build/ssr renderer. */
serverEntry: string; serverEntrypoint: string;
/** Scripts to be injected before component */
polyfills?: string[];
/** Polyfills that need to run before hydration ever occurs */
hydrationPolyfills?: string[];
/** JSX identifier (e.g. 'react' or 'solid-js') */ /** JSX identifier (e.g. 'react' or 'solid-js') */
jsxImportSource?: string; jsxImportSource?: string;
/** Babel transform options */ /** Babel transform options */
jsxTransformOptions?: JSXTransformFn; jsxTransformOptions?: JSXTransformFn;
/** Utilies for server-side rendering */ }
export interface SSRLoadedRenderer extends AstroRenderer {
ssr: { ssr: {
check: AsyncRendererComponentFn<boolean>; check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{ renderToStaticMarkup: AsyncRendererComponentFn<{
html: string; 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) */ export interface AstroIntegration {
external?: string[]; /** The name of the integration. */
/** @deprecated Clientside requirements (deprecated in 0.21) */ name: string;
knownEntrypoints?: 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'; export type RouteType = 'page' | 'endpoint';
@ -665,7 +711,7 @@ export interface SSRElement {
} }
export interface SSRMetadata { export interface SSRMetadata {
renderers: Renderer[]; renderers: SSRLoadedRenderer[];
pathname: string; pathname: string;
legacyBuild: boolean; 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 type { SSRManifest as Manifest, RouteInfo } from './types';
import { defaultLogOptions } from '../logger.js'; import { defaultLogOptions } from '../logger.js';
@ -6,7 +6,6 @@ import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js'; import { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js'; import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js'; import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { createRenderer } from '../render/renderer.js';
import { prependForwardSlash } from '../path.js'; import { prependForwardSlash } from '../path.js';
export class App { export class App {
@ -15,7 +14,7 @@ export class App {
#rootFolder: URL; #rootFolder: URL;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>; #routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#routeCache: RouteCache; #routeCache: RouteCache;
#renderersPromise: Promise<Renderer[]>; #renderersPromise: Promise<SSRLoadedRenderer[]>;
constructor(manifest: Manifest, rootFolder: URL) { constructor(manifest: Manifest, rootFolder: URL) {
this.#manifest = manifest; this.#manifest = manifest;
@ -84,18 +83,11 @@ export class App {
status: 200, status: 200,
}); });
} }
async #loadRenderers(): Promise<Renderer[]> { async #loadRenderers(): Promise<SSRLoadedRenderer[]> {
const rendererNames = this.#manifest.renderers;
return await Promise.all( return await Promise.all(
rendererNames.map(async (rendererName) => { this.#manifest.renderers.map(async (renderer) => {
return createRenderer(rendererName, { const mod = (await import(renderer.serverEntrypoint)) as { default: SSRLoadedRenderer['ssr'] };
renderer(name) { return { ...renderer, ssr: mod.default };
return import(name);
},
server(entry) {
return import(entry);
},
});
}) })
); );
} }

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

View file

@ -1,31 +1,28 @@
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup'; import glob from 'fast-glob';
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 fs from 'fs'; import fs from 'fs';
import npath from 'path'; import npath from 'path';
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
import { fileURLToPath } from 'url'; 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 * as vite from 'vite';
import { debug, error } from '../../core/logger.js'; import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, ManifestData, RouteType, SSRLoadedRenderer } from '../../@types/astro';
import { prependForwardSlash, appendForwardSlash } from '../../core/path.js'; import type { BuildInternals } from '../../core/build/internal.js';
import { emptyDir, removeDir, resolveDependency } from '../../core/util.js';
import { createBuildInternals } 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 { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js'; import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
import { RouteCache } from '../render/route-cache.js'; import type { ViteConfigWithSSR } from '../create-vite';
import { call as callEndpoint } from '../endpoint/index.js'; 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 { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.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 { export interface StaticBuildOptions {
allPages: AllPagesData; allPages: AllPagesData;
@ -116,17 +113,8 @@ export async function staticBuild(opts: StaticBuildOptions) {
// about that page, such as its paths. // about that page, such as its paths.
const facadeIdToPageDataMap = new Map<string, PageBuildData>(); 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 // Build internals needed by the CSS plugin
const internals = createBuildInternals(); const internals = createBuildInternals();
for (const [component, pageData] of Object.entries(allPages)) { for (const [component, pageData] of Object.entries(allPages)) {
const astroModuleURL = new URL('./' + component, astroConfig.projectRoot); const astroModuleURL = new URL('./' + component, astroConfig.projectRoot);
const astroModuleId = prependForwardSlash(component); const astroModuleId = prependForwardSlash(component);
@ -145,7 +133,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Any hydration directive like astro/client/idle.js // Any hydration directive like astro/client/idle.js
...metadata.hydrationDirectiveSpecifiers(), ...metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer // 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 // Add hoisted scripts
@ -172,6 +160,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Build your project (SSR application code, assets, client JS, etc.) // Build your project (SSR application code, assets, client JS, etc.)
const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput; const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput;
await clientBuild(opts, internals, jsInput); await clientBuild(opts, internals, jsInput);
// SSG mode, generate pages. // SSG mode, generate pages.
@ -189,10 +178,11 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
const { astroConfig, viteConfig } = opts; const { astroConfig, viteConfig } = opts;
const ssr = astroConfig.buildOptions.experimentalSsr; const ssr = astroConfig.buildOptions.experimentalSsr;
const out = ssr ? getServerRoot(astroConfig) : getOutRoot(astroConfig); const out = ssr ? getServerRoot(astroConfig) : getOutRoot(astroConfig);
// TODO: use vite.mergeConfig() here?
return await vite.build({ return await vite.build({
logLevel: 'warn', logLevel: 'error',
mode: 'production', mode: 'production',
css: viteConfig.css,
build: { build: {
...viteConfig.build, ...viteConfig.build,
emptyOutDir: false, emptyOutDir: false,
@ -200,6 +190,9 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
outDir: fileURLToPath(out), outDir: fileURLToPath(out),
ssr: true, ssr: true,
rollupOptions: { rollupOptions: {
// onwarn(warn) {
// console.log(warn);
// },
input: Array.from(input), input: Array.from(input),
output: { output: {
format: 'esm', format: 'esm',
@ -240,9 +233,12 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
} }
const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig); const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig);
// TODO: use vite.mergeConfig() here?
return await vite.build({ return await vite.build({
logLevel: 'warn', logLevel: 'error',
mode: 'production', mode: 'production',
css: viteConfig.css,
build: { build: {
emptyOutDir: false, emptyOutDir: false,
minify: 'esbuild', minify: 'esbuild',
@ -275,38 +271,20 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
}); });
} }
function getRenderers(opts: StaticBuildOptions) { async function loadRenderer(renderer: AstroRenderer, config: AstroConfig): Promise<SSRLoadedRenderer> {
// All of the PageDatas have the same renderers, so just grab one. const mod = (await import(resolveDependency(renderer.serverEntrypoint, config))) as { default: SSRLoadedRenderer['ssr'] };
const pageData = Object.values(opts.allPages)[0]; return { ...renderer, ssr: mod.default };
// 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 collectRenderers(opts: StaticBuildOptions): Promise<Renderer[]> { async function loadRenderers(config: AstroConfig): Promise<SSRLoadedRenderer[]> {
const viteLoadedRenderers = getRenderers(opts); return Promise.all(config._ctx.renderers.map((r) => loadRenderer(r, config)));
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 generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
debug('build', 'Finish build. Begin generating.'); debug('build', 'Finish build. Begin generating.');
// Get renderers to be shared for each page generation. // 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) { for (let output of result.output) {
if (chunkIsPage(opts.astroConfig, output, internals)) { 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; const { astroConfig } = opts;
let url = new URL('./' + output.fileName, getOutRoot(astroConfig)); let url = new URL('./' + output.fileName, getOutRoot(astroConfig));
@ -359,7 +343,7 @@ interface GeneratePathOptions {
linkIds: string[]; linkIds: string[];
hoistedId: string | null; hoistedId: string | null;
mod: ComponentInstance; mod: ComponentInstance;
renderers: Renderer[]; renderers: SSRLoadedRenderer[];
} }
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { 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 links = createLinkStylesheetElementSet(linkIds.reverse(), site);
const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], 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 { try {
const options: RenderOptions = { const options: RenderOptions = {
legacyBuild: false, legacyBuild: false,
@ -391,6 +385,14 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
async resolve(specifier: string) { async resolve(specifier: string) {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
if (typeof hashedFilePath !== 'string') { 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}`); throw new Error(`Cannot find the built path for ${specifier}`);
} }
const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); const relPath = npath.posix.relative(pathname, '/' + hashedFilePath);
@ -480,7 +482,7 @@ async function generateManifest(result: RollupOutput, opts: StaticBuildOptions,
markdown: { markdown: {
render: astroConfig.markdownOptions.render, render: astroConfig.markdownOptions.render,
}, },
renderers: astroConfig.renderers, renderers: astroConfig._ctx.renderers,
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()), entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
}; };
@ -628,8 +630,8 @@ export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals
} }
await Promise.all(promises); await Promise.all(promises);
for (const [, chunk] of Object.entries(bundle)) { for (const [, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) { if (chunk.type === 'chunk' && chunk.facadeModuleId) {
const specifier = mapping.get(chunk.facadeModuleId)!; const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName); internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
} }
} }

View file

@ -1,15 +1,48 @@
import type { AstroConfig, AstroUserConfig, CLIFlags } from '../@types/astro'; import type { AstroConfig, AstroUserConfig, CLIFlags } from '../@types/astro';
import type { Arguments as Flags } from 'yargs-parser'; import type { Arguments as Flags } from 'yargs-parser';
import type * as Postcss from 'postcss';
import * as colors from 'kleur/colors'; import * as colors from 'kleur/colors';
import path from 'path'; import path from 'path';
import { pathToFileURL, fileURLToPath } from 'url'; import { pathToFileURL, fileURLToPath } from 'url';
import { mergeConfig as mergeViteConfig } from 'vite';
import { z } from 'zod'; import { z } from 'zod';
import load from '@proload/core'; import load from '@proload/core';
import loadTypeScript from '@proload/plugin-tsm'; import loadTypeScript from '@proload/plugin-tsm';
import postcssrc from 'postcss-load-config';
import { arraify, isObject } from './util.js';
load.use([loadTypeScript]); 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({ export const AstroConfigSchema = z.object({
projectRoot: z projectRoot: z
.string() .string()
@ -36,7 +69,31 @@ export const AstroConfigSchema = z.object({
.optional() .optional()
.default('./dist') .default('./dist')
.transform((val) => new URL(val)), .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 markdownOptions: z
.object({ .object({
render: z.any().optional().default(['@astrojs/markdown-remark', {}]), render: z.any().optional().default(['@astrojs/markdown-remark', {}]),
@ -81,6 +138,37 @@ export const AstroConfigSchema = z.object({
/** Turn raw config values into normalized values */ /** Turn raw config values into normalized values */
export async function validateConfig(userConfig: any, root: string): Promise<AstroConfig> { export async function validateConfig(userConfig: any, root: string): Promise<AstroConfig> {
const fileProtocolRoot = pathToFileURL(root + path.sep); 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. // 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. // This is type checked against the global schema to make sure we still match.
const AstroConfigRelativeSchema = AstroConfigSchema.extend({ const AstroConfigRelativeSchema = AstroConfigSchema.extend({
@ -104,8 +192,26 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
.string() .string()
.default('./dist') .default('./dist')
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)), .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 */ /** Adds '/' to end of string but doesnt double-up */
@ -175,7 +281,11 @@ export async function loadConfig(configOptions: LoadConfigOptions): Promise<Astr
if (config) { if (config) {
userConfig = config.value; 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 mergedConfig = mergeCLIFlags(userConfig, flags);
const validatedConfig = await validateConfig(mergedConfig, root); const validatedConfig = await validateConfig(mergedConfig, root);
return validatedConfig; 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 + '.')}`); 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')}`; 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 { fileURLToPath } from 'url';
import fs from 'fs'; import fs from 'fs';
import * as vite from 'vite'; import * as vite from 'vite';
import { runHookServerSetup } from '../integrations/index.js';
import astroVitePlugin from '../vite-plugin-astro/index.js'; import astroVitePlugin from '../vite-plugin-astro/index.js';
import astroViteServerPlugin from '../vite-plugin-astro-server/index.js'; import astroViteServerPlugin from '../vite-plugin-astro-server/index.js';
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/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 markdownVitePlugin from '../vite-plugin-markdown/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js'; import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import envVitePlugin from '../vite-plugin-env/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. // Some packages are just external, and thats the way it goes.
const ALWAYS_EXTERNAL = new Set([ const ALWAYS_EXTERNAL = new Set([
@ -41,12 +43,11 @@ interface CreateViteOptions {
} }
/** Return a common starting point for all Vite actions */ /** 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`. // Scan for any third-party Astro packages. Vite needs these to be passed to `ssr.noExternal`.
const astroPackages = await getAstroPackages(astroConfig); const astroPackages = await getAstroPackages(astroConfig);
// Start with the Vite configuration that Astro core needs // 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. 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 clearScreen: false, // we want to control the output, not Vite
logLevel: 'warn', // log warnings and errors only logLevel: 'warn', // log warnings and errors only
@ -56,6 +57,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
plugins: [ plugins: [
configAliasVitePlugin({ config: astroConfig }), configAliasVitePlugin({ config: astroConfig }),
astroVitePlugin({ config: astroConfig, logging }), astroVitePlugin({ config: astroConfig, logging }),
astroScriptsPlugin({ config: astroConfig }),
// The server plugin is for dev only and having it run during the build causes // 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. // the build to run very slow as the filewatcher is triggered often.
mode === 'dev' && astroViteServerPlugin({ config: astroConfig, logging }), mode === 'dev' && astroViteServerPlugin({ config: astroConfig, logging }),
@ -63,6 +65,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
markdownVitePlugin({ config: astroConfig }), markdownVitePlugin({ config: astroConfig }),
jsxVitePlugin({ config: astroConfig, logging }), jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig }), astroPostprocessVitePlugin({ config: astroConfig }),
astroIntegrationsContainerPlugin({ config: astroConfig }),
], ],
publicDir: fileURLToPath(astroConfig.public), publicDir: fileURLToPath(astroConfig.public),
root: fileURLToPath(astroConfig.projectRoot), root: fileURLToPath(astroConfig.projectRoot),
@ -75,6 +78,9 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
// add proxies here // add proxies here
}, },
}, },
css: {
postcss: astroConfig.styleOptions.postcss || {},
},
// Note: SSR API is in beta (https://vitejs.dev/guide/ssr.html) // Note: SSR API is in beta (https://vitejs.dev/guide/ssr.html)
ssr: { ssr: {
external: [...ALWAYS_EXTERNAL], external: [...ALWAYS_EXTERNAL],
@ -82,26 +88,16 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
}, },
}; };
// Add in Astro renderers, which will extend the base config // Merge configs: we merge vite configuration objects together in the following order,
for (const name of astroConfig.renderers) { // where future values will override previous values.
try { // 1. common vite config
const { default: renderer } = await import(resolveDependency(name, astroConfig)); // 2. user-provided vite config, via AstroConfig
if (!renderer) continue; // 3. integration-provided vite config, via the `config:setup` hook
// if a renderer provides viteConfig(), call it and pass in results // 4. command vite config, passed as the argument to this function
if (renderer.viteConfig) { let result = commonConfig;
if (typeof renderer.viteConfig !== 'function') { result = vite.mergeConfig(result, astroConfig.vite || {});
throw new Error(`${name}: viteConfig(options) must be a function! Got ${typeof renderer.viteConfig}.`); result = vite.mergeConfig(result, commandConfig);
} return result;
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;
} }
// Scans `projectRoot` for third-party Astro packages that could export an `.astro` file // 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 type { AddressInfo } from 'net';
import { performance } from 'perf_hooks'; 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 * 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 * as msg from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { getResolvedHostForVite } from './util.js'; import { getResolvedHostForVite } from './util.js';
export interface DevOptions { export interface DevOptions {
@ -22,31 +22,36 @@ export interface DevServer {
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> { export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
const devStart = performance.now(); const devStart = performance.now();
applyPolyfill(); applyPolyfill();
config = await runHookConfigSetup({ config, command: 'dev' });
// TODO: remove call once --hostname is baselined const viteConfig = await createVite(
const host = getResolvedHostForVite(config);
const viteUserConfig = vite.mergeConfig(
{ {
mode: 'development', 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); const viteServer = await vite.createServer(viteConfig);
runHookServerSetup({ config, server: viteServer });
await viteServer.listen(config.devOptions.port); await viteServer.listen(config.devOptions.port);
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo; const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined; 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'; const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
if (currentVersion.includes('-')) { if (currentVersion.includes('-')) {
warn(options.logging, null, msg.prerelease({ currentVersion })); warn(options.logging, null, msg.prerelease({ currentVersion }));
} }
await runHookServerStart({ config, address: devServerAddressInfo });
return { return {
address: devServerAddressInfo, 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 { LogOptions } from '../logger.js';
import type { AstroRequest } from './request'; import type { AstroRequest } from './request';
@ -66,7 +66,7 @@ export interface RenderOptions {
pathname: string; pathname: string;
scripts: Set<SSRElement>; scripts: Set<SSRElement>;
resolve: (s: string) => Promise<string>; resolve: (s: string) => Promise<string>;
renderers: Renderer[]; renderers: SSRLoadedRenderer[];
route?: RouteData; route?: RouteData;
routeCache: RouteCache; routeCache: RouteCache;
site?: string; site?: string;

View file

@ -1,7 +1,7 @@
import type * as vite from 'vite'; import type * as vite from 'vite';
import path from 'path'; import path from 'path';
import { viteID } from '../../util.js'; import { unwrapId, viteID } from '../../util.js';
// https://vitejs.dev/guide/features.html#css-pre-processors // https://vitejs.dev/guide/features.html#css-pre-processors
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']); 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); export const isCSSRequest = (request: string): boolean => cssRe.test(request);
/** /** Given a filePath URL, crawl Vites module graph to find all style imports. */
* getStylesForURL
* Given a filePath URL, crawl Vites module graph to find style files
*/
export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> { 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 /** recursively crawl the module graph to get all style files imported by parent id */
function crawlCSS(id: string, scanned = new Set<string>()) { function crawlCSS(_id: string, isFile: boolean, scanned = new Set<string>()) {
// note: use .getModulesByFile() to get all related nodes of the same URL const id = unwrapId(_id);
// using .getModuleById() could cause missing style imports on initial server load
const relatedMods = viteServer.moduleGraph.getModulesByFile(id) ?? new Set();
const importedModules = new Set<vite.ModuleNode>(); 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) { // Collect all imported modules for the module(s).
if (id === relatedMod.id) { for (const entry of moduleEntriesForId) {
if (id === entry.id) {
scanned.add(id); scanned.add(id);
for (const importedMod of relatedMod.importedModules) { for (const importedModule of entry.importedModules) {
importedModules.add(importedMod); 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) { for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) continue; if (!importedModule.id || scanned.has(importedModule.id)) {
const ext = path.extname(importedModule.url.toLowerCase()); continue;
if (STYLE_EXTENSIONS.has(ext)) {
css.add(importedModule.url); // note: return `url`s for HTML (not .id, which will break Windows)
} }
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)); // Crawl your import graph for CSS files, populating `importedCssUrls` as a result.
crawlCSS(viteID(filePath), true);
return css; 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 { fileURLToPath } from 'url';
import { getStylesForURL } from './css.js'; import type * as vite from 'vite';
import { injectTags } from './html.js'; 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 { 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 { errorHandler } from './error.js';
import { getHmrScript } from './hmr.js'; import { getHmrScript } from './hmr.js';
import { prependForwardSlash } from '../../path.js'; import { injectTags } from './html.js';
import { render as coreRender } from '../core.js';
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
export interface SSROptions { export interface SSROptions {
/** an instance of the AstroConfig */ /** an instance of the AstroConfig */
astroConfig: AstroConfig; astroConfig: AstroConfig;
@ -39,15 +35,25 @@ export interface SSROptions {
headers: Headers; headers: Headers;
} }
export type ComponentPreload = [Renderer[], ComponentInstance]; export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
export type RenderResponse = { type: 'html'; html: string } | { type: 'response'; response: Response }; export type RenderResponse = { type: 'html'; html: string } | { type: 'response'; response: Response };
const svelteStylesRE = /svelte\?svelte&type=style/; 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> { 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. // 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. // Load the module from the Vite SSR Runtime.
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance; 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 */ /** 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 { astroConfig, filePath, logging, mode, origin, pathname, method, headers, route, routeCache, viteServer } = ssrOpts;
const legacy = astroConfig.buildOptions.legacyBuild; const legacy = astroConfig.buildOptions.legacyBuild;
@ -69,10 +75,19 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
children: '', children: '',
}); });
scripts.add({ 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: '', 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. // Pass framework CSS in as link tags to be appended to the page.
let links = new Set<SSRElement>(); let links = new Set<SSRElement>();
@ -105,13 +120,22 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
origin, origin,
pathname, pathname,
scripts, 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) { async resolve(s: string) {
// The legacy build needs these to remain unresolved so that vite HTML // The legacy build needs these to remain unresolved so that vite HTML
// Can do the resolution. Without this condition the build output will be // 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. // broken in the legacy build. This can be removed once the legacy build is removed.
if (!astroConfig.buildOptions.legacyBuild) { 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); return '/@fs' + prependForwardSlash(resolvedPath);
} else { } else {
return s; 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 { bold } from 'kleur/colors';
import { createRequest } from './request.js'; import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro';
import { isCSSRequest } from './dev/css.js';
import { isScriptRequest } from './script.js';
import { renderSlot } from '../../runtime/server/index.js'; 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) { function onlyAvailableInSSR(name: string) {
return function () { return function () {
@ -23,7 +21,7 @@ export interface CreateResultArgs {
markdownRender: MarkdownRenderOptions; markdownRender: MarkdownRenderOptions;
params: Params; params: Params;
pathname: string; pathname: string;
renderers: Renderer[]; renderers: SSRLoadedRenderer[];
resolve: (s: string) => Promise<string>; resolve: (s: string) => Promise<string>;
site: string | undefined; site: string | undefined;
links?: Set<SSRElement>; links?: Set<SSRElement>;

View file

@ -25,6 +25,16 @@ export function isValidURL(url: string): boolean {
return false; 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? */ /** is a specifier an npm package? */
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined { export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
// not an npm package // not an npm package
@ -98,6 +108,14 @@ export function viteID(filePath: URL): string {
return slash(fileURLToPath(filePath)); 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` */ /** An fs utility, similar to `rimraf` or `rm -rf` */
export function removeDir(_dir: URL): void { export function removeDir(_dir: URL): void {
const dir = fileURLToPath(_dir); 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 type { SSRElement, SSRResult } from '../../@types/astro';
import { hydrationSpecifier, serializeListValue } from './util.js'; import { hydrationSpecifier, serializeListValue } from './util.js';
import serializeJavaScript from 'serialize-javascript'; import serializeJavaScript from 'serialize-javascript';
@ -81,7 +81,7 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
} }
interface HydrateScriptOptions { interface HydrateScriptOptions {
renderer: any; renderer: SSRLoadedRenderer;
result: SSRResult; result: SSRResult;
astroId: string; astroId: string;
props: Record<string | number, any>; 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.`); throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`);
} }
let hydrationSource = ''; 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`;
}
hydrationSource += renderer.source hydrationSource += renderer.clientEntrypoint
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(componentUrl)}"), import("${await result.resolve( ? `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); return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
` `
@ -116,6 +111,7 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
const hydrationScript = { const hydrationScript = {
props: { type: 'module', 'data-astro-component-hydration': true }, props: { type: 'module', 'data-astro-component-hydration': true },
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}'; 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 () => { setup("${astroId}", {name:"${metadata.displayName}",${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
${hydrationSource} ${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 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 { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
import { serializeListValue } from './util.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 type { Metadata } from './metadata';
export { createMetadata } from './metadata.js'; 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 voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes = 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; /^(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(); const extname = componentUrl?.split('.').pop();
switch (extname) { switch (extname) {
case 'svelte': case 'svelte':
return ['@astrojs/renderer-svelte']; return ['@astrojs/svelte'];
case 'vue': case 'vue':
return ['@astrojs/renderer-vue']; return ['@astrojs/vue'];
case 'jsx': case 'jsx':
case 'tsx': case 'tsx':
return ['@astrojs/renderer-react', '@astrojs/renderer-preact']; return ['@astrojs/react', '@astrojs/preact'];
default: 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)) { if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !componentIsHTMLElement(Component)) {
const message = `Unable to render ${metadata.displayName}! const message = `Unable to render ${metadata.displayName}!
There are no \`renderers\` set in your \`astro.config.mjs\` file. There are no \`integrations\` set in your \`astro.config.mjs\` file.
Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`; Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
throw new Error(message); throw new Error(message);
} }
// Call the renderers `check` hook to see if any claim this component. // Call the renderers `check` hook to see if any claim this component.
let renderer: Renderer | undefined; let renderer: SSRLoadedRenderer | undefined;
if (metadata.hydrate !== 'only') { if (metadata.hydrate !== 'only') {
for (const r of renderers) { for (const r of renderers) {
if (await r.ssr.check(Component, props, children)) { 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 // Attempt: use explicitly passed renderer name
if (metadata.hydrateArgs) { if (metadata.hydrateArgs) {
const rendererName = 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 // Attempt: user only has a single renderer, default to that
if (!renderer && renderers.length === 1) { 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? // Attempt: can we guess the renderer from the export extension?
if (!renderer) { if (!renderer) {
const extname = metadata.componentUrl?.split('.').pop(); 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}! throw new Error(`Unable to render ${metadata.displayName}!
Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer. 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') { } else if (typeof Component !== 'string') {
const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name)); 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) { if (!hydration) {
return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, '')); 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. // Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point. // INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
result.scripts.add(await generateHydrateScript({ renderer, 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. // Render a template if no fragment is provided.
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html); const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);

View file

@ -122,13 +122,7 @@ export function invalidateCompilation(config: AstroConfig, filename: string) {
} }
} }
export async function cachedCompilation( export async function cachedCompilation(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> {
config: AstroConfig,
filename: string,
source: string | null,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
let cache: CompilationCache; let cache: CompilationCache;
if (!configCache.has(config)) { if (!configCache.has(config)) {
cache = new Map(); cache = new Map();
@ -139,11 +133,6 @@ export async function cachedCompilation(
if (cache.has(filename)) { if (cache.has(filename)) {
return cache.get(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); const compileResult = await compile(config, filename, source, viteTransform, opts);
cache.set(filename, compileResult); cache.set(filename, compileResult);
return compileResult; return compileResult;

View file

@ -35,7 +35,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return slash(fileURLToPath(url)) + url.search; return slash(fileURLToPath(url)) + url.search;
} }
let isProduction: boolean; let resolvedConfig: vite.ResolvedConfig;
let viteTransform: TransformHook; let viteTransform: TransformHook;
let viteDevServer: vite.ViteDevServer | null = null; let viteDevServer: vite.ViteDevServer | null = null;
@ -46,9 +46,9 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
return { return {
name: 'astro:build', name: 'astro:build',
enforce: 'pre', // run transforms before other plugins can enforce: 'pre', // run transforms before other plugins can
configResolved(resolvedConfig) { configResolved(_resolvedConfig) {
resolvedConfig = _resolvedConfig;
viteTransform = getViteTransform(resolvedConfig); viteTransform = getViteTransform(resolvedConfig);
isProduction = resolvedConfig.isProduction;
}, },
configureServer(server) { configureServer(server) {
viteDevServer = server; viteDevServer = server;
@ -83,20 +83,26 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
} }
}, },
async load(id, opts) { 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.astro) {
if (query.type === 'style') { 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') { if (typeof query.index === 'undefined') {
throw new Error(`Requests for Astro CSS must include an index.`); 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. // Track any CSS dependencies so that HMR is triggered when they change.
await trackCSSDependencies.call(this, { viteDevServer, id, filename, deps: transformResult.rawCSSDeps }); 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`); 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 scripts = transformResult.scripts;
const hoistedScript = scripts[query.index]; 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 { 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. // Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning. // Also, catches invalid JS/TS in the compiled output before returning.
@ -143,9 +144,15 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
define: config.vite.define, define: config.vite.define,
}); });
// Signal to Vite that we accept HMR updates let SUFFIX = '';
const SUFFIX = isProduction ? '' : `\nif (import.meta.hot) import.meta.hot.accept((mod) => mod);`; // 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 { return {
code: `${code}${SUFFIX}`, code: `${code}${SUFFIX}`,
map, 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 { TransformResult } from 'rollup';
import type { Plugin, ResolvedConfig } from 'vite'; 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 type { LogOptions } from '../core/logger.js';
import babel from '@babel/core'; import babel from '@babel/core';
@ -9,9 +9,9 @@ import * as colors from 'kleur/colors';
import * as eslexer from 'es-module-lexer'; import * as eslexer from 'es-module-lexer';
import path from 'path'; import path from 'path';
import { error } from '../core/logger.js'; 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 JSX_EXTENSIONS = new Set(['.jsx', '.tsx']);
const IMPORT_STATEMENTS: Record<string, string> = { const IMPORT_STATEMENTS: Record<string, string> = {
react: "import React from 'react'", react: "import React from 'react'",
@ -28,24 +28,16 @@ function getEsbuildLoader(fileExt: string): string {
return fileExt.substr(1); return fileExt.substr(1);
} }
async function importJSXRenderers(config: AstroConfig): Promise<Map<string, Renderer>> { function collectJSXRenderers(renderers: AstroRenderer[]): Map<string, AstroRenderer> {
const renderers = new Map<string, Renderer>(); const renderersWithJSXSupport = renderers.filter((r) => r.jsxImportSource);
await Promise.all( return new Map(renderersWithJSXSupport.map((r) => [r.jsxImportSource, r] as [string, AstroRenderer]));
config.renderers.map((name) => {
return import(resolveDependency(name, config)).then(({ default: renderer }) => {
if (!renderer.jsxImportSource) return;
renderers.set(renderer.jsxImportSource, renderer);
});
})
);
return renderers;
} }
interface TransformJSXOptions { interface TransformJSXOptions {
code: string; code: string;
id: string; id: string;
mode: string; mode: string;
renderer: Renderer; renderer: AstroRenderer;
ssr: boolean; ssr: boolean;
} }
@ -100,13 +92,13 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
// load renderers (on first run only) // load renderers (on first run only)
if (!jsxRenderers) { if (!jsxRenderers) {
jsxRenderers = new Map(); jsxRenderers = new Map();
const possibleRenderers = await importJSXRenderers(config); const possibleRenderers = await collectJSXRenderers(config._ctx.renderers);
if (possibleRenderers.size === 0) { 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 // 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( throw new Error(
`${colors.yellow( `${colors.yellow(
id 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) { for (const [importSource, renderer] of possibleRenderers) {
@ -173,7 +165,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
const jsxRenderer = jsxRenderers.get(importSource); const jsxRenderer = jsxRenderers.get(importSource);
// if renderer not installed for this JSX source, throw error // if renderer not installed for this JSX source, throw error
if (!jsxRenderer) { 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; return null;
} }
// downlevel any non-standard syntax, but preserve JSX // downlevel any non-standard syntax, but preserve JSX
@ -183,7 +175,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
sourcefile: id, sourcefile: id,
sourcemap: 'inline', 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 // 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 () { describe('CSS', function () {
before(async () => { before(async () => {
fixture = await loadFixture({ fixture = await loadFixture({ projectRoot: './fixtures/0-css/' });
projectRoot: './fixtures/0-css/',
renderers: ['@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
}); });
// test HTML and CSS contents for accuracy // test HTML and CSS contents for accuracy

View file

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

View file

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

View file

@ -32,24 +32,11 @@ describe('Dynamic components', () => {
const html = await fixture.readFile('/client-only/index.html'); const html = await fixture.readFile('/client-only/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
// test 1: <astro-root> is empty // test 1: <astro-root> is empty.
expect($('<astro-root>').html()).to.equal(''); expect($('<astro-root>').html()).to.equal('');
const script = $('script').text(); // test 2: correct script is being loaded.
// because of bundling, we don't have access to the source import,
// Grab the svelte import // only the bundled import.
// const exp = /import\("(.+?)"\)/g; expect($('script').html()).to.include(`import setup from '../only`);
// 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);
}); });
}); });

View file

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

View file

@ -8,7 +8,6 @@ describe('Dynamic component fallback', () => {
before(async () => { before(async () => {
fixture = await loadFixture({ fixture = await loadFixture({
projectRoot: './fixtures/astro-fallback', projectRoot: './fixtures/astro-fallback',
renderers: ['@astrojs/renderer-preact'],
}); });
await fixture.build(); 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 () => { before(async () => {
fixture = await loadFixture({ fixture = await loadFixture({
projectRoot: './fixtures/astro-markdown-plugins/', projectRoot: './fixtures/astro-markdown-plugins/',
renderers: ['@astrojs/renderer-preact'],
markdownOptions: { markdownOptions: {
render: [ render: [
markdownRemark, markdownRemark,

View file

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

View file

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

View file

@ -31,7 +31,7 @@ describe('Config Validation', () => {
it('Multiple validation errors can be formatted correctly', async () => { it('Multiple validation errors can be formatted correctly', async () => {
const veryBadConfig = { const veryBadConfig = {
renderers: [42], integrations: [42],
buildOptions: { pageUrlFormat: 'invalid' }, buildOptions: { pageUrlFormat: 'invalid' },
pages: {}, pages: {},
}; };
@ -41,8 +41,34 @@ describe('Config Validation', () => {
expect(formattedError).to.equal( expect(formattedError).to.equal(
`[config] Astro found issue(s) with your configuration: `[config] Astro found issue(s) with your configuration:
! pages Expected string, received object. ! pages Expected string, received object.
! renderers.0 Expected string, received number. ! integrations.0 Expected object, received number.
! buildOptions.pageUrlFormat Invalid input.` ! 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 () => { before(async () => {
[hostnameFixture, hostFixture, portFixture] = await Promise.all([ [hostnameFixture, hostFixture, portFixture] = await Promise.all([
loadFixture({ projectRoot: './fixtures/config-hostname/' }), loadFixture({
loadFixture({ projectRoot: './fixtures/config-host/' }), projectRoot: './fixtures/config-host/',
loadFixture({ projectRoot: './fixtures/config-port/' }), 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