Compare commits

...

40 commits

Author SHA1 Message Date
Nate Moore
4eafad8017 refactor: cleanup render package 2021-10-16 17:31:47 -05:00
Nate Moore
2a6f2601d6 feat: add @astrojs/render package 2021-10-16 10:41:22 -05:00
Nate Moore
75801aafd9 chore: update node 2021-10-16 10:40:57 -05:00
Drew Powers
8145dc042c
Restructure (#1569) 2021-10-15 18:28:04 -06:00
Nate Moore
e878fc3026 [Next] fetch support (#1563)
* fix: polyfill fetch in every ssr scenario

* test(fetch): update fetch tests

* docs: update data fetching guide to remove caveats about `fetch` and isomorphic usage

* refactor: update regex for clarity
2021-10-15 14:11:40 -06:00
Drew Powers
986f028714 Chore: Enable more tests with new compiler changes (#1558) 2021-10-15 14:11:39 -06:00
Nate Moore
19d45496e7 [next] Update renderers (#1509)
* chore: update vite

* fix(renderers): point renderers to resolved server/client entrypoints
2021-10-15 14:10:26 -06:00
Matthew Phillips
5fbd05be15 Implements top-level Astro + Astro.resolve (#1556)
* Implements top-level Astro + Astro.resolve

* Fix linting
2021-10-15 14:08:43 -06:00
Matthew Phillips
f199c69eab Brings back astro-dynamic tests (#1548) 2021-10-15 14:08:07 -06:00
Matthew Phillips
b418ae31b8 Move hydration to the compiler (#1547)
* Move hydration to the compiler

* Move extracting url, export to util fn
2021-10-15 14:07:31 -06:00
Fred K. Schott
215f46aa01 [next] Fix Astro.fetchContent (#1480)
* fix Astro.fetchContent

* fix(fetchContent): cast type

Co-authored-by: Nate Moore <nate@skypack.dev>
2021-10-15 14:06:30 -06:00
Matthew Phillips
190fba394d [next] Support for custom elements (#1528)
* [next] Support for custom elements

* Fix eslint errors

* eslint again
2021-10-15 14:05:36 -06:00
Matthew Phillips
dc7260bea2 [next] support Astro.slots API (#1516) 2021-10-15 14:05:34 -06:00
Matthew Phillips
8c30740442 Add hydration to Solid renderer (#1479) (#1495)
* feat: add hydration to Solid renderer

* fix: intersection observer, move script to the end

Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
2021-10-15 14:05:01 -06:00
Matthew Phillips
d7152acb85 Enables most slot tests (#1494)
* Enables most slot tests

* Use spreadAttributes
2021-10-15 14:00:38 -06:00
Matthew Phillips
09038cd457 Add back in support for children (#1486)
* Add back in support for children

* Be more careful
2021-10-15 13:59:45 -06:00
Matthew Phillips
21294f7e0b Apply changes from #1387 2021-10-15 13:59:44 -06:00
Pranav Karawale
3ddc3646d9 Fix logger locale parsing (#1439)
* fix(logger): locale parsing
* Fixed issue of compiler crash when "c" locale was encountered
* Return default locale if parsed locale is less than 2 chars long

* chore: add changeset
2021-10-15 13:59:32 -06:00
Drew Powers
d818c9bb92 Fix CI release on merge to next (#1427) 2021-10-15 13:59:30 -06:00
Drew Powers
b99eacfa54 Try mocha/chai test runners (#1418)
* Try mocha/chai test runners

* Disable failing smoke test for now

Will revert when next can build docs

* Enable mocha in parallel mode

* Remove warning

* Update docs

* Fix Windows bug

* Fix internal imports

* Fix styles
2021-10-15 13:58:29 -06:00
Nate Moore
73eb8edfe3 feat: update compiler (#1421) 2021-10-15 13:57:24 -06:00
Drew Powers
7e42b006f9 Use new renderers (#1412) 2021-10-15 13:57:04 -06:00
Drew Powers
ccf3b22069 Clean up astro deps (#1411) 2021-10-15 13:56:24 -06:00
Drew Powers
f454b25c91 Create .npmrc 2021-10-15 13:53:04 -06:00
Drew Powers
47ac7875ac Add NPM_TOKEN to publish script (#1409) 2021-10-15 13:53:04 -06:00
Drew Powers
d7e3faf49a Automate publish on merge (#1408) 2021-10-15 13:53:04 -06:00
Drew Powers
68749c7df2 Fix npm install (#1407) 2021-10-15 13:53:04 -06:00
Drew Powers
17cebebbee Allow users to pass config to Vite 2021-10-15 13:53:04 -06:00
Drew Powers
5f6eaf24fe Improve testing suite 2021-10-15 13:53:04 -06:00
Nate Moore
480b7dbafc Throw Error for WIP Features 2021-10-15 13:53:04 -06:00
Fred K. Schott
34e016aeb3 Fred fixes part 2 2021-10-15 13:53:03 -06:00
Nate Moore
75fc9f2c7a Tycho fixes 2021-10-15 13:50:08 -06:00
Nate Moore
2cf885ee73 Add Markdown support 2021-10-15 13:50:06 -06:00
Drew Powers
561fecd03b Fix runtime, improve code frame 2021-10-15 13:43:49 -06:00
Drew Powers
bf38f9ea93 Update compiler, improve tests 2021-10-15 13:43:49 -06:00
Fred K. Schott
00389e3fc0 fred fixes 2021-10-15 13:43:48 -06:00
Drew Powers
7ae9a09186 More test improvements 2021-10-15 13:43:48 -06:00
Nate Moore
c6a5b518ab Update tests 2021-10-15 13:43:47 -06:00
Drew Powers
7eaf14ce15 Move Astro to Vite 2021-10-15 13:34:53 -06:00
Fred K. Schott
a195e3155a make astro-root uids unique 2021-10-15 13:26:49 -06:00
330 changed files with 8488 additions and 10966 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix logger locale parsing

View file

@ -0,0 +1,10 @@
---
'@astrojs/renderer-lit': patch
'@astrojs/renderer-preact': patch
'@astrojs/renderer-react': patch
'@astrojs/renderer-solid': patch
'@astrojs/renderer-svelte': patch
'@astrojs/renderer-vue': patch
---
Point renderer entrypoints to resolved JS files

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Use new compiler, move Astro to Vite

View file

@ -0,0 +1,9 @@
---
'@astrojs/renderer-preact': minor
'@astrojs/renderer-react': minor
'@astrojs/renderer-solid': minor
'@astrojs/renderer-svelte': minor
'@astrojs/renderer-vue': minor
---
Update renderers for Vite

View file

@ -8,6 +8,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'off',

View file

@ -18,11 +18,13 @@ jobs:
matrix:
os: [ubuntu-latest]
node_version: [12, 14, 16]
include:
- os: windows-latest
node_version: 14
# TODO: uncomment this (Vite has trouble resolving imports on Windows)
# include:
# - os: windows-latest
# node_version: 14
fail-fast: false
env:
LANG: en-us
name: 'Test: node-${{ matrix.node_version }}, ${{ matrix.os }}'
steps:
- name: Checkout
@ -72,33 +74,34 @@ jobs:
- name: Lint
run: yarn lint
smoke:
runs-on: ubuntu-latest
name: 'Smoke: node-14, ubuntu-latest'
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
# NOTE: temporarily disabled until `next` branch can build docs again
# smoke:
# runs-on: ubuntu-latest
# name: 'Smoke: node-14, ubuntu-latest'
# steps:
# - uses: actions/checkout@v2
# with:
# fetch-depth: 0
- name: Set node version to 14
uses: actions/setup-node@v2
with:
node-version: 14
cache: 'yarn'
# - name: Set node version to 14
# uses: actions/setup-node@v2
# with:
# node-version: 14
# cache: 'yarn'
- name: Debug
run: yarn versions
# - name: Debug
# run: yarn versions
- name: Install dependencies
run: yarn install --frozen-lockfile --ignore-engines
# - name: Install dependencies
# run: yarn install --frozen-lockfile --ignore-engines
- name: Build
run: yarn build:all
# - name: Build
# run: yarn build:all
- name: "Smoke Test: Build 'docs'"
run: yarn build
working-directory: ./docs
# - name: "Smoke Test: Build 'docs'"
# run: yarn build
# working-directory: ./docs
- name: "Smoke Test: Build 'www'"
run: yarn build
working-directory: ./www
# - name: "Smoke Test: Build 'www'"
# run: yarn build
# working-directory: ./www

30
.github/workflows/release-next.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: release astro@next--compiler
on:
push:
branches:
- next
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
- run: yarn
- # 1. create new snapshot version
run: yarn changeset version --snapshot compiler
- # 2. discard examples/docs/www changes (just in case)
run: git checkout -- examples/ docs/ www/
- # 3: use compiler--next renderers (but dont commit)
run: |
cd packages/astro
yarn add @astrojs/renderer-preact@next--compiler @astrojs/renderer-react@next--compiler @astrojs/renderer-svelte@next--compiler @astrojs/renderer-vue@next--compiler
cd ../..
- # 4: auth
run: echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}' > ${{ github.workspace }}/.npmrc
- # 5: publish!
run: yarn release --tag next--compiler

View file

@ -5,7 +5,9 @@ We welcome contributions of any size and skill level. As an open source project,
> **Tip for new contributors:**
> Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing
## Prerequisite
## Quick Guide
### Prerequisite
```shell
node: "^12.20.0 || ^14.13.1 || >=16.0.0"
@ -13,7 +15,7 @@ yarn: "^1.22.10"
# otherwise, your build will fail
```
## Setting up your local repo
### Setting up your local repo
Astro uses yarn workspaces, so you should **always run `yarn install` from the top-level project directory.** running `yarn install` in the top-level project root will install dependencies for `astro`, `www`, `docs`, and every package in the repo.
@ -23,7 +25,7 @@ yarn install
yarn build:all
```
## Development
### Development
```shell
# starts a file-watching, live-reloading dev script for active development
@ -32,17 +34,17 @@ yarn dev
yarn build
```
## Running tests
### Running tests
```shell
# run this in the top-level project root to run all tests
yarn test
# run only a few tests, great for working on a single feature
# (example - `yarn test rss` runs `astro-rss.test.js` tests)
yarn test $STRING_MATCH
# (example - `yarn test -g "RSS"` runs `astro-rss.test.js`)
yarn test -g "$STRING_MATCH"
```
## Other useful commands
### Other useful commands
```shell
# auto-format the entire project
@ -56,7 +58,7 @@ yarn format
yarn lint
```
## Making a Pull Request
### Making a Pull Request
When making a pull request, be sure to add a changeset when something has changed with Astro. Non-packages (`examples/*`, `docs/*`, and `www/*`) do not need changesets.
@ -64,7 +66,7 @@ When making a pull request, be sure to add a changeset when something has change
yarn changeset
```
## Running benchmarks
### Running benchmarks
We have benchmarks to keep performance under control. You can run these by running (from the project root):
@ -83,7 +85,31 @@ node test/benchmark/dev.bench.js --save
Which will update the build and dev benchmarks.
# Releasing Astro
## Code Structure
Server-side rendering (SSR) can be complicated. The Astro package (`packages/astro`) is structured in a way to help think about the different systems.
- `components/`: Built-in components to use in your project (e.g. `import Code from 'astro/components/Code.astro'`)
- `src/`: Astro source
- `@types/`: TypeScript types. These are centralized to cut down on circular dependencies
- `cli/`: Code that powers the `astro` CLI command
- `core/`: Code that executes **in the top-level scope** (in Node). Within, youll find code that powers the `astro build` and `astro dev` commands, as well as top-level SSR code.
- `runtime/`: Code that executes **in different scopes** (i.e. not in a pure Node context). Youll have to think about code differently here.
- `client/`: Code that executes **in the browser.** Astros partial hydration code lives here, and only browser-compatible code can be used.
- `server/`: Code that executes **inside Vites SSR.** Though this is a Node environment inside, this will be executed independently from `core/` and may have to be structured differently.
- `vite-plugin-*/`: Any Vite plugins that Astro needs to run. For the most part, these also execute within Vite similar to `src/runtime/server/`, but its also helpful to think about them as independent modules. _Note: at the moment these are internal while theyre in development_
### Thinking about SSR
There are 3 contexts in which code executes:
- **Node.js**: this code lives in `src/core/`.
- **Inside Vite**: this code lives in `src/runtime/server/`.
- **In the browser**: this code lives in `src/runtime/client/`.
Understanding in which environment code runs, and at which stage in the process, can help clarify thinking about what Astro is doing. It also helps with debugging, for instance, if youre working within `src/core/`, you know that your code isnt executing within Vite, so you dont have to debug Vites setup. But you will have to debug vite inside `runtime/server/`.
## Releasing Astro
_Note: Only priviledged contributors (L3+) can release new versions of Astro._
@ -91,7 +117,7 @@ The repo is set up with automatic releases, using the changeset GitHub action &
To release a new version of Astro, find the `Version Packages` PR, read it over, and merge it.
## Releasing PR preview snapshots
### Releasing PR preview snapshots
Our release tool `changeset` has a feature for releasing "snapshot" releases from a PR or custom branch. These are npm package publishes that live temporarily, so that you can give users a way to test a PR before merging. This can be a great way to get early user feedback while still in the PR review process.
@ -112,7 +138,7 @@ git reset --hard
Full documentation: https://github.com/atlassian/changesets/blob/main/docs/snapshot-releases.md
## Releasing `astro@next` (aka "prerelease mode")
### Releasing `astro@next` (aka "prerelease mode")
Sometimes, the repo will enter into "prerelease mode". In prerelease mode, our normal release process will publish npm versions under the `next` dist-tag, instead of the default `latest` tag. We do this from time-to-time to test large features before sharing them with the larger Astro audience.
@ -154,7 +180,7 @@ When in prerelease mode, the automatic PR release process will no longer release
1. Go to https://github.com/snowpackjs/astro/releases/new and create a new release. Copy the new changelog entry from https://github.com/snowpackjs/astro/blob/latest/packages/astro/CHANGELOG.md.
1. Post in Discord #announcements channel, if needed!
# Translations
## Translations
Help us translate [docs.astro.build](https://docs.astro.build/) into as many languages as possible! This can be a great way to get involved with open source development without having to code.

View file

@ -21,7 +21,7 @@
"broken-link-checker": "^0.7.8",
"npm-run-all": "^4.1.5",
"pa11y-ci": "^2.4.2",
"prettier": "^2.3.2",
"prettier": "^2.4.1",
"start-server-and-test": "^1.12.6"
},
"dependencies": {

View file

@ -32,11 +32,10 @@ console.log(data);
## Using `fetch()` outside of Astro Components
If you want to use `fetch()` in a non-astro component, use the [`node-fetch`](https://github.com/node-fetch/node-fetch) library:
If you want to use `fetch()` in a non-astro component, it is also globally available:
```tsx
// Movies.tsx
import fetch from 'node-fetch';
import type { FunctionalComponent } from 'preact';
import { h } from 'preact';
@ -55,11 +54,3 @@ const Movies: FunctionalComponent = () => {
export default Movies;
```
If you load a component using `node-fetch` [interactively](/core-concepts/component-hydration), with `client:load`, `client:visible`, etc., you'll need to either not use `node-fetch` or switch to an [isomorphic](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) library that will run both at build time and on the client, as the [`node-fetch` README.md](https://github.com/node-fetch/node-fetch#motivation) recommends:
> Instead of implementing XMLHttpRequest in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native http to fetch API directly? Hence, node-fetch, minimal code for a window.fetch compatible API on Node.js runtime.
>
> See Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) or Leonardo Quixada's [cross-fetch](https://github.com/lquixada/cross-fetch) for isomorphic usage (exports node-fetch for server-side, whatwg-fetch for client-side).
> Quoted from https://github.com/node-fetch/node-fetch#motivation

View file

@ -22,7 +22,3 @@ export default /** @type {import('astro').AstroUserConfig} */ (
}
);
```
## Snowpack Config
Astro is powered internally by Snowpack. You can configure Snowpack directly by creating a `snowpack.config.mjs` file. See [snowpack.dev](https://www.snowpack.dev/reference/configuration) for full documentation on this file.

View file

@ -38,8 +38,8 @@ A renderer should include any framework dependencies as package dependencies. Fo
// package.json
"name": "@astrojs/renderer-react",
"dependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0"
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
```
@ -72,6 +72,7 @@ export default {
plugins: [jsx({}, { runtime: 'automatic', importSource: 'preact' })],
};
},
vitePlugins: [], // optional, inject Vite plugins here (https://vitejs.dev/plugins/#plugins)
};
```

View file

@ -18,7 +18,7 @@ const { title, description, image, type, next, prev, canonicalURL } = Astro.prop
<meta name="description" content={description}>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/src/global.scss">
<!-- Sitemap -->
<link rel="sitemap" href="/sitemap.xml">
<!-- RSS -->

View file

@ -0,0 +1,9 @@
<h1>
<slot/>
</h1>
<style>
h1 {
color: red;
}
</style>

View file

@ -1,12 +1,12 @@
---
import { Markdown } from 'astro/components';
import BaseHead from '../components/BaseHead.astro';
import BlogHeader from '../components/BlogHeader.astro';
import BlogPost from '../components/BlogPost.astro';
// import BlogHeader from '../components/BlogHeader.astro';
// import BlogPost from '../components/BlogPost.astro';
const {content} = Astro.props;
const {title, description, publishDate, author, heroImage, permalink, alt} = content;
---
<html lang={ content.lang || 'en' }>
<head>
<BaseHead title={title} description={description} permalink={permalink} />
@ -14,10 +14,13 @@ const {title, description, publishDate, author, heroImage, permalink, alt} = con
</head>
<body>
<BlogHeader />
<BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}>
<h1>Hello world!</h1>
<div class="container">
<slot />
</BlogPost>
</div>
<!-- <BlogHeader /> -->
<!-- <BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}> -->
<!-- </BlogPost> -->
</body>
</html>

View file

@ -18,7 +18,7 @@ let permalink = 'https://example.com/';
// Data Fetching: List all Markdown posts in the repo.
let allPosts = Astro.fetchContent<MarkdownFrontmatter>('./posts/*.md');
let allPosts = await Astro.fetchContent('./posts/*.md');
allPosts = allPosts.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
// Full Astro Component Syntax:

View file

@ -0,0 +1,15 @@
---
setup: |
import Layout from '../../layouts/BlogPost.astro'
import Cool from '../../components/Author.astro'
name: Nate Moore
value: 128
---
# Hello world!
<Cool name={frontmatter.name} href="https://twitter.com/n_moore" client:load />
This is so cool!
Do variables work {frontmatter.value * 2}?

View file

@ -1,86 +0,0 @@
---
title: 'Introducing Astro: Ship Less JavaScript'
description: "We're excited to announce Astro as a new way to build static websites and deliver lightning-fast performance without sacrificing a modern developer experience."
publishDate: 'Tuesday, June 8 2021'
author: 'fred'
heroImage: '/social.jpg'
alt: 'Astro'
layout: '../../layouts/BlogPost.astro'
---
There's a simple secret to building a faster website — _just ship less_.
Unfortunately, modern web development has been trending in the opposite direction—towards _more._ More JavaScript, more features, more moving parts, and ultimately more complexity needed to keep it all running smoothly.
Today I'm excited to publicly share Astro: a new kind of static site builder that delivers lightning-fast performance with a modern developer experience. To design Astro, we borrowed the best parts of our favorite tools and then added a few innovations of our own, including:
- **Bring Your Own Framework (BYOF):** Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.
- **100% Static HTML, No JS:** Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
- **On-Demand Components:** Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
- **Fully-Featured:** Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
- **SEO Enabled:** Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication.
This post marks the first public beta release of Astro. **Missing features and bugs are still to be expected at this early stage.** There are still some months to go before an official 1.0 release, but there are already several fast sites built with Astro in production today. We would love your early feedback as we move towards a v1.0 release later this year.
> To learn more about Astro and start building your first site, check out [the project README.](https://github.com/snowpackjs/astro#-guides).
## Getting Started
Starting a new project in Astro is easy:
```shell
# create your project
mkdir new-project-directory
cd new-project-directory
npm init astro
# install your dependencies
npm install
# start the dev server and open your browser
npm run dev
```
> To learn more about Astro and start building your first site, check out [the project README.](https://github.com/snowpackjs/astro#-guides).
## How Astro Works
Astro works a lot like a static site generator. If you have ever used Eleventy, Hugo, or Jekyll (or even a server-side web framework like Rails, Laravel, or Django) then you should feel right at home with Astro.
In Astro, you compose your website using UI components from your favorite JavaScript web framework (React, Svelte, Vue, etc). Astro renders your entire site to static HTML during the build. The result is a fully static website with all JavaScript removed from the final page. No monolithic JavaScript application required, just static HTML that loads as fast as possible in the browser regardless of how many UI components you used to generate it.
Of course, sometimes client-side JavaScript is inevitable. Image carousels, shopping carts, and auto-complete search bars are just a few examples of things that require some JavaScript to run in the browser. This is where Astro really shines: When a component needs some JavaScript, Astro only loads that one component (and any dependencies). The rest of your site continues to exist as static, lightweight HTML.
In other full-stack web frameworks this level of per-component optimization would be impossible without loading the entire page in JavaScript, delaying interactivity. In Astro, this kind of [partial hydration](https://addyosmani.com/blog/rehydration/) is built into the tool itself.
You can even [automatically defer components](https://codepen.io/jonneal/full/ZELvMvw) to only load once they become visible on the page with the `client:visible` directive.
This new approach to web architecture is called [islands architecture](https://jasonformat.com/islands-architecture/). We didn't coin the term, but Astro may have perfected the technique. We are confident that an HTML-first, JavaScript-only-as-needed approach is the best solution for the majority of content-based websites.
> To learn more about Astro and start building your first site, check out [the project README.](https://github.com/snowpackjs/astro#-guides)
## Embracing the Pit of Success
> A well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things<div class="source"><p> Jeff Atwood</p>[Falling Into The Pit of Success](https://blog.codinghorror.com/falling-into-the-pit-of-success/)</div>
Poor performance is often framed as a failure of the developer, but we respectfully disagree. In many cases, poor performance is a failure of tooling. It should be difficult to build a slow website.
Astro's main design principle is to lead developers into what [Rico Mariani](https://twitter.com/ricomariani) dubbed "the pit of success". It is our goal to build every site "fast by default" while also delivering a familiar, modern developer experience.
By building your site to static HTML by default, Astro makes it difficult (but never impossible 😉) to build a slow site.
## Long-Term Sustainability
Astro is built by the team of open source developers behind [Snowpack](https://snowpack.dev) and [Skypack](https://skypack.dev), with additional contributions from the community.
**Astro is and always will be free.** It is an open source project released under the [MIT license](https://github.com/snowpackjs/astro/blob/main/LICENSE).
We care deeply about building a more sustainable future for open source software. At the same time, we need to support Astro's development long-term. This requires money (donations alone aren't enough.)
We're inspired by the early success of projects like [Tailwind](https://tailwindcss.com/), [Rome](https://rome.tools/), [Remix](https://remix.run/), [Ionic](https://ionicframework.com/), and others who are experimenting with long-term financial sustainability on top of Open Source. Over the next year we'll be exploring how we can create a sustainable business to support a 100% free, open source Astro for years to come.
If your company is as excited about Astro as we are, [we'd love to hear from you.](https://astro.build/chat)
Finally, I'd like to give a **HUGE** thanks to the 300+ developers who joined our earliest private beta. Your feedback has been essential in shaping Astro into the tool it is today. If you're interested in getting involved (or just following along with development) please [join us on Discord.](https://astro.build/chat)
> To learn more about Astro and start building your first site, check out [the project README.](https://github.com/snowpackjs/astro#-guides)

View file

@ -1,6 +1,6 @@
---
// Component Imports
import Counter from '../components/Counter.jsx'
import Counter from '../components/Counter.tsx'
// Full Astro Component Syntax:

View file

@ -1,5 +1,6 @@
---
---
<html lang="en">
<head>
@ -7,6 +8,7 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<title>Welcome to Astro</title>
</head>
<body>

View file

@ -38,7 +38,7 @@ import { Markdown } from 'astro/components';
<h2>👀 Want to learn more?</h2>
<p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p>
</section>
</article>
<style>

View file

@ -1,5 +1,10 @@
{
"ignoreChanges": ["**/test/**", "**/*.md"],
"useWorkspaces": true,
"version": "4.0.0"
"version": "4.0.0",
"command": {
"run": {
"npmClient": "yarn"
}
}
}

View file

@ -12,17 +12,19 @@
"build": "yarn build:core",
"build:one": "lerna run build --scope",
"build:all": "lerna run build --scope \"{astro,@astrojs/*}\"",
"build:core": "lerna run build --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\"",
"build:core": "lerna run build --scope \"{astro,@astrojs/parser,@astrojs/markdown-remark}\"",
"dev": "yarn dev:core --parallel --stream",
"dev:one": "lerna run dev --scope --parallel --stream",
"dev:all": "lerna run dev --scope \"{astro,@astrojs/*}\" --parallel --stream",
"dev:core": "lerna run dev --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\" --parallel --stream",
"dev:core": "lerna run dev --scope \"{astro,@astrojs/parser,@astrojs/markdown-remark}\" --parallel --stream",
"format": "prettier -w .",
"lint": "eslint \"packages/**/*.ts\"",
"test": "yarn workspace astro run test",
"test:templates": "lerna run test --scope create-astro --stream"
},
"workspaces": [
"compiled/*",
"packages/markdown/*",
"packages/renderers/*",
"packages/*",
"examples/*",
@ -34,36 +36,26 @@
"packages/astro/test/fixtures/custom-elements/my-component-lib"
],
"volta": {
"node": "14.16.1",
"node": "14.18.1",
"npm": "7.11.2",
"yarn": "1.22.10"
},
"dependencies": {
"camel-case": "^4.1.2"
},
"dependencies": {},
"devDependencies": {
"@changesets/cli": "^2.16.0",
"@octokit/action": "^3.15.4",
"@snowpack/plugin-postcss": "^1.4.3",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.18.0",
"autoprefixer": "^10.2.6",
"cheerio": "^1.0.0-rc.6",
"cheerio-select-tmp": "^0.1.1",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"del": "^6.0.0",
"esbuild": "^0.11.17",
"eslint": "^7.25.0",
"esbuild": "^0.13.7",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-prettier": "^4.0.0",
"execa": "^5.0.0",
"lerna": "^4.0.0",
"postcss": "^8.2.15",
"postcss-icss-keyframes": "^0.2.1",
"prettier": "^2.2.1",
"svelte": "^3.38.0",
"prettier": "^2.4.1",
"tiny-glob": "^0.2.8",
"typescript": "^4.2.4",
"uvu": "^0.5.1"
"typescript": "^4.4.3"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"

View file

@ -13,6 +13,7 @@ const CI_INSTRUCTIONS = {
VERCEL: 'https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-version',
};
/** `astro *` */
async function main() {
// Check for ESM support.
// Load the "supports-esm" package in an way that works in both ESM & CJS.
@ -29,7 +30,7 @@ async function main() {
// Preflight check complete. Enjoy! ✨
if (supportsESM) {
return import('./dist/cli.js')
return import('./dist/cli/index.js')
.then(({ cli }) => cli(process.argv))
.catch((error) => {
console.error(error);

View file

@ -4,14 +4,14 @@ import shiki from 'shiki';
export interface Props {
/** The code to highlight. Required. */
code: string;
/**
* The language of your code. Defaults to "plaintext".
* Supports all languages listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md
/**
* The language of your code. Defaults to "plaintext".
* Supports all languages listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md
*/
lang?: string;
/**
/**
* The styling theme. Defaults to "github-dark".
* Supports all themes listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md
* Supports all themes listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md
* Instructions for loading a custom theme: https://github.com/shikijs/shiki/blob/main/docs/themes.md#loading-theme
*/
theme?: string;

View file

@ -1,5 +1,5 @@
---
import { renderMarkdown } from '@astrojs/markdown-support';
import { renderMarkdown } from '@astrojs/markdown-remark';
export interface Props {
content?: string;

View file

@ -1,4 +1,17 @@
export { default as Code } from './Code.astro';
export { default as Debug } from './Debug.astro';
export { default as Markdown } from './Markdown.astro';
export { default as Prism } from './Prism.astro';
// export { default as Code } from './Code.astro';
// export { default as Debug } from './Debug.astro';
// export { default as Markdown } from './Markdown.astro';
// export { default as Prism } from './Prism.astro';
export const Code = () => {
throw new Error(`Cannot render <Code />. "astro/components" are still WIP!`)
}
export const Debug = () => {
throw new Error(`Cannot render <Debug />. "astro/components" are still WIP!`)
}
export const Markdown = () => {
throw new Error(`Cannot render <Markdown />. "astro/components" are still WIP!`)
}
export const Prism = () => {
throw new Error(`Cannot render <Prism />. "astro/components" are still WIP!`)
}

View file

@ -1,27 +0,0 @@
declare module '#astro/compiler' {
export * from 'astro/dist/types/compiler';
}
declare module '#astro/ast' {
export * from 'astro/dist/types/ast';
}
declare module '#astro/build' {
export * from 'astro/dist/types/build';
}
declare module '#astro/cli' {
export * from 'astro/dist/types/cli';
}
declare module '#astro/config' {
export * from 'astro/dist/types/config';
}
declare module '#astro/dev' {
export * from 'astro/dist/types/dev';
}
declare module '#astro/logger' {
export * from 'astro/dist/types/logger';
}
declare module '#astro/runtime' {
export * from 'astro/dist/types/runtime';
}
declare module '#astro/search' {
export * from 'astro/dist/types/search';
}

View file

@ -4,7 +4,7 @@
"author": "Skypack",
"license": "MIT",
"type": "module",
"types": "./dist/types/@types/public.d.ts",
"types": "./dist/types/@types/astro-public.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/snowpackjs/astro.git",
@ -12,18 +12,27 @@
},
"exports": {
".": "./astro.js",
"./package.json": "./package.json",
"./snowpack-plugin": "./snowpack-plugin.cjs",
"./snowpack-plugin-jsx": "./snowpack-plugin-jsx.cjs",
"./client/*": "./dist/runtime/client/*",
"./components": "./components/index.js",
"./debug": "./components/Debug.astro",
"./components/*": "./components/*",
"./runtime/svelte": "./dist/frontend/runtime/svelte.js",
"./internal/*": "./dist/internal/*",
"./dist/internal/*": "./dist/internal/*"
"./debug": "./components/Debug.astro",
"./internal": "./dist/runtime/server/index.js",
"./internal/*": "./dist/runtime/server/*",
"./package.json": "./package.json",
"./runtime/*": "./dist/runtime/*",
"./server/*": "./dist/runtime/server/*",
"./vite-plugin-astro": "./dist/vite-plugin-astro/index.js",
"./vite-plugin-astro/*": "./dist/vite-plugin-astro/*",
"./vite-plugin-astro-postprocess": "./dist/vite-plugin-astro-postprocess/index.js",
"./vite-plugin-astro-postprocess/*": "./dist/vite-plugin-astro-postprocess/*",
"./vite-plugin-fetch": "./dist/vite-plugin-fetch/index.js",
"./vite-plugin-fetch/*": "./dist/vite-plugin-fetch/*",
"./vite-plugin-jsx/*": "./dist/vite-plugin-jsx/*",
"./vite-plugin-jsx": "./dist/vite-plugin-jsx/index.js",
"./vite-plugin-markdown": "./dist/vite-plugin-markdown/index.js",
"./vite-plugin-markdown/*": "./dist/vite-plugin-markdown/*"
},
"imports": {
"#astro/compiler": "./dist/compiler/index.js",
"#astro/*": "./dist/*.js"
},
"bin": {
@ -32,93 +41,73 @@
"files": [
"components",
"dist",
"snowpack-plugin-jsx.cjs",
"snowpack-plugin.cjs",
"astro.js"
"astro.js",
"README.md"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "uvu test -i fixtures -i benchmark -i test-utils.js"
"test": "mocha --parallel --timeout 15000"
},
"dependencies": {
"@astrojs/compiler": "^0.1.15",
"@astrojs/language-server": "^0.7.16",
"@astrojs/markdown-remark": "^0.3.1",
"@astrojs/markdown-support": "0.3.1",
"@astrojs/parser": "0.20.2",
"@astrojs/prism": "0.2.2",
"@astrojs/renderer-preact": "0.2.2",
"@astrojs/renderer-preact": "^0.2.2",
"@astrojs/renderer-react": "0.2.2",
"@astrojs/renderer-svelte": "0.1.2",
"@astrojs/renderer-vue": "0.1.9",
"@babel/code-frame": "^7.12.13",
"@babel/core": "^7.14.6",
"@babel/generator": "^7.13.9",
"@babel/parser": "^7.13.15",
"@babel/traverse": "^7.13.15",
"@snowpack/plugin-postcss": "^1.4.3",
"@snowpack/plugin-sass": "^1.4.0",
"@types/send": "^0.17.1",
"acorn": "^7.4.0",
"astring": "^1.7.4",
"autoprefixer": "^10.2.5",
"babel-plugin-module-resolver": "^4.1.0",
"camel-case": "^4.1.2",
"cheerio": "^1.0.0-rc.6",
"@babel/core": "^7.15.8",
"@babel/traverse": "^7.15.4",
"@web/rollup-plugin-html": "^1.10.1",
"astring": "^1.7.5",
"cheerio": "^1.0.0-rc.10",
"ci-info": "^3.2.0",
"del": "^6.0.0",
"es-module-lexer": "^0.4.1",
"esbuild": "^0.12.12",
"connect": "^3.7.0",
"es-module-lexer": "^0.7.1",
"esbuild": "^0.13.6",
"estree-util-value-to-estree": "^1.2.0",
"estree-walker": "^3.0.0",
"fast-xml-parser": "^3.19.0",
"fdir": "^5.0.0",
"find-up": "^5.0.0",
"get-port": "^5.1.1",
"gzip-size": "^6.0.0",
"html-entities": "^2.3.2",
"kleur": "^4.1.4",
"magic-string": "^0.25.3",
"mime": "^2.5.2",
"moize": "^6.0.1",
"node-fetch": "^2.6.1",
"morphdom": "^2.6.1",
"node-fetch": "^2.6.5",
"path-to-regexp": "^6.2.0",
"picomatch": "^2.2.3",
"postcss": "^8.2.15",
"postcss-icss-keyframes": "^0.2.1",
"pretty-bytes": "^5.6.0",
"prismjs": "^1.23.0",
"resolve": "^1.20.0",
"rollup": "^2.43.1",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.32.13",
"sass": "^1.43.2",
"semver": "^7.3.5",
"send": "^0.17.1",
"shiki": "^0.9.10",
"shorthash": "^0.0.2",
"slash": "^4.0.0",
"snowpack": "^3.8.6",
"srcset-parse": "^1.1.0",
"sourcemap-codec": "^1.4.8",
"string-width": "^5.0.0",
"strip-ansi": "^7.0.1",
"supports-esm": "^1.0.0",
"tiny-glob": "^0.2.8",
"yargs-parser": "^20.2.7",
"vite": "^2.6.7",
"yargs-parser": "^20.2.9",
"zod": "^3.8.1"
},
"devDependencies": {
"@babel/types": "^7.14.0",
"@types/babel__code-frame": "^7.0.2",
"@types/babel__generator": "^7.6.2",
"@types/babel__parser": "^7.1.1",
"@types/babel__traverse": "^7.11.1",
"@types/estree": "0.0.46",
"@astrojs/parser": "^0.20.2",
"@babel/types": "^7.15.6",
"@types/babel__core": "^7.1.15",
"@types/chai": "^4.2.22",
"@types/connect": "^3.4.35",
"@types/mime": "^2.0.3",
"@types/node": "^14.14.31",
"@types/sass": "^1.16.0",
"@types/yargs-parser": "^20.2.0",
"astro-scripts": "0.0.1",
"is-windows": "^1.0.2",
"strip-ansi": "^7.0.0"
"@types/mocha": "^9.0.0",
"@types/node-fetch": "^2.5.12",
"@types/rimraf": "^3.0.2",
"@types/send": "^0.17.1",
"@types/yargs-parser": "^20.2.1",
"chai": "^4.3.4",
"cheerio": "^1.0.0-rc.10",
"mocha": "^9.1.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0",

View file

@ -1,190 +0,0 @@
const esbuild = require('esbuild');
const colors = require('kleur/colors');
const loggerPromise = import('./dist/logger.js');
const { promises: fs } = require('fs');
const babel = require('@babel/core');
const eslexer = require('es-module-lexer');
let error = (...args) => {};
/**
* @typedef {Object} PluginOptions - creates a new type named 'SpecialType'
* @prop {import('./src/config_manager').ConfigManager} configManager
* @prop {'development' | 'production'} mode
*/
/**
* Returns esbuild loader for a given file
* @param filePath {string}
* @returns {import('esbuild').Loader}
*/
function getLoader(fileExt) {
/** @type {any} */
return fileExt.substr(1);
}
// The `tsx` loader in esbuild will remove unused imports, so we need to
// be careful about esbuild not treating h, React, Fragment, etc. as unused.
const PREVENT_UNUSED_IMPORTS = ';;(React,Fragment,h);';
/**
* @type {import('snowpack').SnowpackPluginFactory<PluginOptions>}
*/
module.exports = function jsxPlugin(config, options = {}) {
const { configManager, logging } = options;
let didInit = false;
return {
name: '@astrojs/snowpack-plugin-jsx',
resolve: {
input: ['.jsx', '.tsx'],
output: ['.js'],
},
async load({ filePath, fileExt, ...transformContext }) {
if (!didInit) {
const logger = await loggerPromise;
error = logger.error;
await eslexer.init;
didInit = true;
}
const contents = await fs.readFile(filePath, 'utf8');
const loader = getLoader(fileExt);
const { code, warnings } = await esbuild.transform(contents, {
loader,
jsx: 'preserve',
sourcefile: filePath,
sourcemap: config.buildOptions.sourcemap ? 'inline' : undefined,
charset: 'utf8',
sourcesContent: config.mode !== 'production',
});
for (const warning of warnings) {
error(
logging,
'renderer',
`${colors.bold('!')} ${filePath}
${warning.text}`
);
}
let renderers = await configManager.getRenderers();
const importSources = new Set(renderers.map(({ jsxImportSource }) => jsxImportSource).filter((i) => i));
const getRenderer = (importSource) => renderers.find(({ jsxImportSource }) => jsxImportSource === importSource);
const getTransformOptions = async (importSource) => {
const { name } = getRenderer(importSource);
const { default: renderer } = await import(name);
return renderer.jsxTransformOptions(transformContext);
};
if (importSources.size === 0) {
throw new Error(`${colors.yellow(filePath)}
Unable to resolve a renderer that handles JSX transforms! Please include a \`renderer\` plugin which supports JSX in your \`astro.config.mjs\` file.`);
}
// If we only have a single renderer, we can skip a bunch of work!
if (importSources.size === 1) {
const result = transform(code, filePath, await getTransformOptions(Array.from(importSources)[0]));
return {
'.js': {
code: result.code || '',
},
};
}
// we need valid JS here, so we can use `h` and `Fragment` as placeholders
// NOTE(fks, matthewp): Make sure that you're transforming the original contents here.
const { code: codeToScan } = await esbuild.transform(contents + PREVENT_UNUSED_IMPORTS, {
loader,
jsx: 'transform',
jsxFactory: 'h',
jsxFragment: 'Fragment',
});
let imports = [];
if (/import/.test(codeToScan)) {
let [i] = eslexer.parse(codeToScan);
// @ts-ignore
imports = i;
}
let importSource;
if (imports.length > 0) {
for (let { n: name } of imports) {
if (name.indexOf('/') > -1) name = name.split('/')[0];
if (importSources.has(name)) {
importSource = name;
break;
}
}
}
if (!importSource) {
const multiline = contents.match(/\/\*\*[\S\s]*\*\//gm) || [];
for (const comment of multiline) {
const [_, lib] = comment.match(/@jsxImportSource\s*(\S+)/) || [];
if (lib) {
importSource = lib;
break;
}
}
}
if (!importSource) {
const importStatements = {
react: "import React from 'react'",
preact: "import { h } from 'preact'",
'solid-js': "import 'solid-js/web'",
};
if (importSources.size > 1) {
const defaultRenderer = Array.from(importSources)[0];
error(
logging,
'renderer',
`${colors.yellow(filePath)}
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
Add ${colors.cyan(importStatements[defaultRenderer] || `import '${defaultRenderer}';`)} or ${colors.cyan(`/* jsxImportSource: ${defaultRenderer} */`)} to this file.
`
);
}
return {
'.js': {
code: contents,
},
};
}
const result = transform(code, filePath, await getTransformOptions(importSource));
return {
'.js': {
code: result.code || '',
},
};
},
cleanup() {},
};
};
/**
*
* @param code {string}
* @param id {string}
* @param opts {{ plugins?: import('@babel/core').PluginItem[], presets?: import('@babel/core').PluginItem[] }|undefined}
*/
const transform = (code, id, { alias, plugins = [], presets = [] } = {}) =>
babel.transformSync(code, {
presets,
plugins: [...plugins, alias ? ['babel-plugin-module-resolver', { root: process.cwd(), alias }] : undefined].filter((v) => v),
cwd: process.cwd(),
filename: id,
ast: false,
compact: false,
sourceMaps: false,
configFile: false,
babelrc: false,
});

View file

@ -1,69 +0,0 @@
const { readFile } = require('fs').promises;
const getPort = require('get-port');
// Snowpack plugins must be CommonJS :(
const transformPromise = import('./dist/compiler/index.js');
const DEFAULT_HMR_PORT = 12321;
/**
* @typedef {Object} PluginOptions - creates a new type named 'SpecialType'
* @prop {import('./src/config_manager').ConfigManager} configManager
* @prop {'development' | 'production'} mode
*/
/**
* @type {import('snowpack').SnowpackPluginFactory<PluginOptions>}
*/
module.exports = (snowpackConfig, options = {}) => {
const { resolvePackageUrl, astroConfig, configManager, mode } = options;
let hmrPort = DEFAULT_HMR_PORT;
return {
name: 'snowpack-astro',
knownEntrypoints: ['astro/dist/internal/h.js', 'astro/components/Prism.astro', 'shorthash', 'estree-util-value-to-estree', 'astring'],
resolve: {
input: ['.astro', '.md'],
output: ['.js', '.css'],
},
async transform({ contents, id, fileExt }) {
if (configManager.isConfigModule(fileExt, id)) {
configManager.configModuleId = id;
const source = await configManager.buildSource(contents);
return source;
}
},
onChange({ filePath }) {
// If the astro.config.mjs file changes, mark the generated config module as changed.
if (configManager.isAstroConfig(filePath) && configManager.configModuleId) {
this.markChanged(configManager.configModuleId);
configManager.markDirty();
}
},
async config(snowpackConfig) {
if (!isNaN(snowpackConfig.devOptions.hmrPort)) {
hmrPort = snowpackConfig.devOptions.hmrPort;
} else {
hmrPort = await getPort({ port: DEFAULT_HMR_PORT, host: snowpackConfig.devOptions.hostname });
snowpackConfig.devOptions.hmrPort = hmrPort;
}
},
async load({ filePath }) {
const { compileComponent } = await transformPromise;
const projectRoot = snowpackConfig.root;
const contents = await readFile(filePath, 'utf-8');
/** @type {import('./src/@types/compiler').CompileOptions} */
const compileOptions = {
astroConfig,
hmrPort,
mode,
resolvePackageUrl,
};
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
const output = {
'.js': { code: result.contents },
};
if (result.css) output['.css'] = result.css;
return output;
},
};
};

View file

@ -0,0 +1,5 @@
# `@types/`
TypeScript definitions and types for untyped modules.
[See CONTRIBUTING.md](../../../../CONTRIBUTING.md) for a code overview.

View file

@ -0,0 +1,321 @@
import type babel from '@babel/core';
import type vite from 'vite';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory } from '../runtime/server';
export interface AstroComponentMetadata {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
hydrateArgs?: any;
componentUrl?: string;
componentExport?: { value: string; namespace?: boolean };
}
/**
* The Astro User Config Format:
* This is the type interface for your astro.config.mjs default export.
*/
export interface AstroUserConfig {
/**
* Where to resolve all URLs relative to. Useful if you have a monorepo project.
* Default: '.' (current working directory)
*/
projectRoot?: string;
/**
* Path to the `astro build` output.
* Default: './dist'
*/
dist?: string;
/**
* Path to all of your Astro components, pages, and data.
* Default: './src'
*/
src?: string;
/**
* Path to your Astro/Markdown pages. Each file in this directory
* becomes a page in your final build.
* Default: './src/pages'
*/
pages?: string;
/**
* Path to your public files. These are copied over into your build directory, untouched.
* Useful for favicons, images, and other files that don't need processing.
* Default: './public'
*/
public?: string;
/**
* Framework component renderers enable UI framework rendering (static and dynamic).
* When you define this in your configuration, all other defaults are disabled.
* Default: [
* '@astrojs/renderer-svelte',
* '@astrojs/renderer-vue',
* '@astrojs/renderer-react',
* '@astrojs/renderer-preact',
* ],
*/
renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: {
render?: [string, Record<string, any>];
};
/** Options specific to `astro build` */
buildOptions?: {
/** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */
site?: string;
/** Generate an automatically-generated sitemap for your build.
* Default: true
*/
sitemap?: boolean;
/**
* Control the output file URL format of each page.
* If 'file', Astro will generate a matching HTML file (ex: "/foo.html") instead of a directory.
* If 'directory', Astro will generate a directory with a nested index.html (ex: "/foo/index.html") for each page.
* Default: 'directory'
*/
pageUrlFormat?: 'file' | 'directory';
};
/** Options for the development server run with `astro dev`. */
devOptions?: {
hostname?: string;
/** The port to run the dev server on. */
port?: number;
/** Path to tailwind.config.js, if used */
tailwindConfig?: string;
/**
* Configure The trailing slash behavior of URL route matching:
* 'always' - Only match URLs that include a trailing slash (ex: "/foo/")
* 'never' - Never match URLs that include a trailing slash (ex: "/foo")
* 'ignore' - Match URLs regardless of whether a trailing "/" exists
* Default: 'always'
*/
trailingSlash?: 'always' | 'never' | 'ignore';
};
/** Pass configuration options to Vite */
vite?: vite.InlineConfig;
}
// NOTE(fks): We choose to keep our hand-generated AstroUserConfig interface so that
// we can add JSDoc-style documentation and link to the definition file in our repo.
// However, Zod comes with the ability to auto-generate AstroConfig from the schema
// above. If we ever get to the point where we no longer need the dedicated
// @types/config.ts file, consider replacing it with the following lines:
//
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// }
export type AstroConfig = z.output<typeof AstroConfigSchema>;
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;
export interface CollectionRSS {
/** (required) Title of the RSS Feed */
title: string;
/** (required) Description of the RSS Feed */
description: string;
/** Specify arbitrary metadata on opening <xml> tag */
xmlns?: Record<string, string>;
/** Specify custom data in opening of file */
customData?: string;
/**
* Specify where the RSS xml file should be written.
* Relative to final build directory. Example: '/foo/bar.xml'
* Defaults to '/rss.xml'.
*/
dest?: string;
/** Return data about each item */
items: {
/** (required) Title of item */
title: string;
/** (required) Link to item */
link: string;
/** Publication date of item */
pubDate?: Date;
/** Item description */
description?: string;
/** Append some other XML-valid data to this item */
customData?: string;
}[];
}
/** Generic interface for a component (Astro, Svelte, React, etc.) */
export interface ComponentInstance {
default: AstroComponentFactory;
css?: string[];
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}
export type GetStaticPathsArgs = { paginate: PaginateFunction; rss: RSSFunction };
export interface GetStaticPathsOptions {
paginate?: PaginateFunction;
rss?: (...args: any[]) => any;
}
export type GetStaticPathsResult = { params: Params; props?: Props }[] | { params: Params; props?: Props }[];
export interface JSXTransformConfig {
/** Babel presets */
presets?: babel.PluginItem[];
/** Babel plugins */
plugins?: babel.PluginItem[];
}
export type JSXTransformFn = (options: { isSSR: boolean }) => Promise<JSXTransformConfig>;
export interface ManifestData {
routes: RouteData[];
}
export interface PaginatedCollectionProp<T = any> {
/** result */
data: T[];
/** metadata */
/** the count of the first item on the page, starting from 0 */
start: number;
/** the count of the last item on the page, starting from 0 */
end: number;
/** total number of results */
total: number;
/** the current page number, starting from 1 */
currentPage: number;
/** number of items per page (default: 25) */
size: number;
/** number of last page */
lastPage: number;
url: {
/** url of the current page */
current: string;
/** url of the previous page (if there is one) */
prev: string | undefined;
/** url of the next page (if there is one) */
next: string | undefined;
};
}
export interface PaginatedCollectionResult<T = any> {
/** result */
data: T[];
/** metadata */
/** the count of the first item on the page, starting from 0 */
start: number;
/** the count of the last item on the page, starting from 0 */
end: number;
/** total number of results */
total: number;
/** the current page number, starting from 1 */
currentPage: number;
/** number of items per page (default: 25) */
size: number;
/** number of last page */
lastPage: number;
url: {
/** url of the current page */
current: string;
/** url of the previous page (if there is one) */
prev: string | undefined;
/** url of the next page (if there is one) */
next: string | undefined;
};
}
export type PaginateFunction = (data: [], args?: { pageSize?: number; params?: Params; props?: Props }) => GetStaticPathsResult;
export type Params = Record<string, string | undefined>;
export type Props = Record<string, unknown>;
export interface RenderPageOptions {
request: {
params?: Params;
url: URL;
canonicalURL: URL;
};
children: any[];
props: Props;
css?: string[];
}
export interface Renderer {
/** Name of the renderer (required) */
name: string;
hydrationPolyfills?: string[];
/** Dont try and build these dependencies for client */
external?: string[];
/** Clientside requirements */
knownEntrypoints?: string[];
polyfills?: string[];
/** Import statement for renderer */
source?: string;
/** JSX identifier (e.g. 'react' or 'solid-js') */
jsxImportSource?: string;
/** Babel transform options */
jsxTransformOptions?: JSXTransformFn;
/** Utilies for server-side rendering */
ssr: {
check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{
html: string;
}>;
};
/** Add plugins to Vite, if any */
vitePlugins?: vite.Plugin[];
}
export interface RouteData {
component: string;
generate: (data?: any) => string;
params: string[];
pathname?: string;
pattern: RegExp;
type: 'page';
}
export type RouteCache = Record<string, GetStaticPathsResult>;
export type RuntimeMode = 'development' | 'production';
export type RSSFunction = (args: RSSFunctionArgs) => void;
export interface RSSFunctionArgs {
/** (required) Title of the RSS Feed */
title: string;
/** (required) Description of the RSS Feed */
description: string;
/** Specify arbitrary metadata on opening <xml> tag */
xmlns?: Record<string, string>;
/** Specify custom data in opening of file */
customData?: string;
/**
* Specify where the RSS xml file should be written.
* Relative to final build directory. Example: '/foo/bar.xml'
* Defaults to '/rss.xml'.
*/
dest?: string;
/** Return data about each item */
items: {
/** (required) Title of item */
title: string;
/** (required) Link to item */
link: string;
/** Publication date of item */
pubDate?: Date;
/** Item description */
description?: string;
/** Append some other XML-valid data to this item */
customData?: string;
}[];
}
export type RSSResult = { url: string; xml?: string };
export type ScriptInfo = ScriptInfoInline | ScriptInfoExternal;
export type SSRError = Error & vite.ErrorPayload['err'];
export interface ScriptInfoInline {
content: string;
}
export interface ScriptInfoExternal {
src: string;
}

View file

@ -0,0 +1 @@
export { AstroConfig, AstroUserConfig } from './astro-core';

View file

@ -0,0 +1,59 @@
import type { Renderer } from './astro-core';
export interface AstroBuiltinProps {
'client:load'?: boolean;
'client:idle'?: boolean;
'client:media'?: string;
'client:visible'?: boolean;
}
export interface AstroGlobal extends TopLevelAstro {
props: Record<string, number | string | any>;
request: AstroPageRequest;
slots: Record<string, true | undefined>;
}
interface AstroPageRequest {
url: URL;
canonicalURL: URL;
params: Params;
}
type AstroRenderedHTML = string;
export type FetchContentResultBase = {
astro: {
headers: string[];
source: string;
html: AstroRenderedHTML;
};
url: URL;
};
export type FetchContentResult<T> = FetchContentResultBase & T;
export interface HydrateOptions {
value?: string;
}
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>;
export type Params = Record<string, string | undefined>;
export interface TopLevelAstro {
isPage: boolean;
fetchContent<T = any>(globStr: string): Promise<FetchContentResult<T>[]>;
resolve: (path: string) => string;
site: URL;
}
export interface SSRMetadata {
renderers: Renderer[];
}
export interface SSRResult {
styles: Set<string>;
scripts: Set<string>;
createAstro(Astro: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
_metadata: SSRMetadata;
}

View file

@ -1,174 +0,0 @@
import type { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from '@babel/types';
import type { AstroUserConfig, AstroConfig } from './config';
export { AstroUserConfig, AstroConfig };
export interface RouteData {
type: 'page';
pattern: RegExp;
params: string[];
path: string | null;
component: string;
generate: (data?: any) => string;
}
export interface ManifestData {
routes: RouteData[];
}
export interface JsxItem {
name: string;
jsx: string;
}
export interface InlineScriptInfo {
content: string;
}
export interface ExternalScriptInfo {
src: string;
}
export type ScriptInfo = InlineScriptInfo | ExternalScriptInfo;
export interface TransformResult {
script: string;
imports: string[];
exports: string[];
components: string[];
html: string;
css?: string;
hoistedScripts: ScriptInfo[];
getStaticPaths?: string;
hasCustomElements: boolean;
customElementCandidates: Map<string, string>;
}
export interface CompileResult {
result: TransformResult;
contents: string;
css?: string;
}
export type RuntimeMode = 'development' | 'production';
export type Params = Record<string, string | undefined>;
export type Props = Record<string, any>;
/** Entire output of `astro build`, stored in memory */
export interface BuildOutput {
[dist: string]: BuildFile;
}
export interface BuildFile {
/** The original location. Needed for code frame errors. */
srcPath: URL;
/** File contents */
contents: string | Buffer;
/** File content type (to determine encoding, etc) */
contentType: string;
/** Encoding */
encoding?: 'utf8';
/** Extracted scripts */
hoistedScripts?: ScriptInfo[];
}
/** Mapping of every URL and its required assets. All URLs are absolute relative to the project. */
export type BundleMap = {
[pageUrl: string]: PageDependencies;
};
export interface PageDependencies {
/** JavaScript files needed for page. No distinction between blocking/non-blocking or sync/async. */
js: Set<string>;
/** CSS needed for page, whether imported via <link>, JS, or Astro component. */
css: Set<string>;
/** Images needed for page. Can be loaded via CSS, <link>, or otherwise. */
images: Set<string>;
/** Async hoisted Javascript */
hoistedJS: Map<string, ScriptInfo>;
}
export interface RSSFunctionArgs {
/** (required) Title of the RSS Feed */
title: string;
/** (required) Description of the RSS Feed */
description: string;
/** Specify arbitrary metadata on opening <xml> tag */
xmlns?: Record<string, string>;
/** Specify custom data in opening of file */
customData?: string;
/**
* Specify where the RSS xml file should be written.
* Relative to final build directory. Example: '/foo/bar.xml'
* Defaults to '/rss.xml'.
*/
dest?: string;
/** Return data about each item */
items: {
/** (required) Title of item */
title: string;
/** (required) Link to item */
link: string;
/** Publication date of item */
pubDate?: Date;
/** Item description */
description?: string;
/** Append some other XML-valid data to this item */
customData?: string;
}[];
}
export interface PaginatedCollectionProp<T = any> {
/** result */
data: T[];
/** metadata */
/** the count of the first item on the page, starting from 0 */
start: number;
/** the count of the last item on the page, starting from 0 */
end: number;
/** total number of results */
total: number;
/** the current page number, starting from 1 */
currentPage: number;
/** number of items per page (default: 25) */
size: number;
/** number of last page */
lastPage: number;
url: {
/** url of the current page */
current: string;
/** url of the previous page (if there is one) */
prev: string | undefined;
/** url of the next page (if there is one) */
next: string | undefined;
};
}
export type RSSFunction = (args: RSSFunctionArgs) => void;
export type PaginateFunction = (data: [], args?: { pageSize?: number; params?: Params; props?: Props }) => GetStaticPathsResult;
export type GetStaticPathsArgs = { paginate: PaginateFunction; rss: RSSFunction };
export type GetStaticPathsResult = { params: Params; props?: Props }[] | { params: Params; props?: Props }[];
export interface ComponentInfo {
url: string;
importSpecifier: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier;
}
export type Components = Map<string, ComponentInfo>;
export interface AstroComponentMetadata {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
componentUrl?: string;
componentExport?: { value: string; namespace?: boolean };
value?: undefined | string;
}
type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;
export interface Renderer {
check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{
html: string;
}>;
}

View file

@ -1,10 +0,0 @@
import type { LogOptions } from '../logger';
import type { AstroConfig, RuntimeMode } from './astro';
export interface CompileOptions {
logging: LogOptions;
resolvePackageUrl: (p: string) => Promise<string>;
astroConfig: AstroConfig;
hmrPort?: number;
mode: RuntimeMode;
}

View file

@ -1,95 +0,0 @@
import type { AstroMarkdownOptions } from '@astrojs/markdown-support';
import type { AstroConfigSchema } from '../config';
import type { z } from 'zod';
/**
* The Astro User Config Format:
* This is the type interface for your astro.config.mjs default export.
*/
export interface AstroUserConfig {
/**
* Where to resolve all URLs relative to. Useful if you have a monorepo project.
* Default: '.' (current working directory)
*/
projectRoot?: string;
/**
* Path to the `astro build` output.
* Default: './dist'
*/
dist?: string;
/**
* Path to all of your Astro components, pages, and data.
* Default: './src'
*/
src?: string;
/**
* Path to your Astro/Markdown pages. Each file in this directory
* becomes a page in your final build.
* Default: './src/pages'
*/
pages?: string;
/**
* Path to your public files. These are copied over into your build directory, untouched.
* Useful for favicons, images, and other files that don't need processing.
* Default: './public'
*/
public?: string;
/**
* Framework component renderers enable UI framework rendering (static and dynamic).
* When you define this in your configuration, all other defaults are disabled.
* Default: [
* '@astrojs/renderer-svelte',
* '@astrojs/renderer-vue',
* '@astrojs/renderer-react',
* '@astrojs/renderer-preact',
* ],
*/
renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: Partial<AstroMarkdownOptions>;
/** Options specific to `astro build` */
buildOptions?: {
/** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */
site?: string;
/** Generate an automatically-generated sitemap for your build.
* Default: true
*/
sitemap?: boolean;
/**
* Control the output file URL format of each page.
* If 'file', Astro will generate a matching HTML file (ex: "/foo.html") instead of a directory.
* If 'directory', Astro will generate a directory with a nested index.html (ex: "/foo/index.html") for each page.
* Default: 'directory'
*/
pageUrlFormat?: 'file' | 'directory';
};
/** Options for the development server run with `astro dev`. */
devOptions?: {
hostname?: string;
/** The port to run the dev server on. */
port?: number;
/** Path to tailwind.config.js, if used */
tailwindConfig?: string;
/**
* Configure The trailing slash behavior of URL route matching:
* 'always' - Only match URLs that include a trailing slash (ex: "/foo/")
* 'never' - Never match URLs that include a trailing slash (ex: "/foo")
* 'ignore' - Match URLs regardless of whether a trailing "/" exists
* Default: 'always'
*/
trailingSlash?: 'always' | 'never' | 'ignore';
};
}
// NOTE(fks): We choose to keep our hand-generated AstroUserConfig interface so that
// we can add JSDoc-style documentation and link to the definition file in our repo.
// However, Zod comes with the ability to auto-generate AstroConfig from the schema
// above. If we ever get to the point where we no longer need the dedicated
// @types/config.ts file, consider replacing it with the following lines:
//
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// markdownOptions?: Partial<AstroMarkdownOptions>;
// }
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
markdownOptions: Partial<AstroMarkdownOptions>;
}

View file

@ -1,25 +0,0 @@
import { BaseNode } from 'estree-walker';
declare module 'estree-walker' {
export function walk<T = BaseNode>(
ast: T,
{
enter,
leave,
}: {
enter?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
}
): T;
export function asyncWalk<T = BaseNode>(
ast: T,
{
enter,
leave,
}: {
enter?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
leave?: (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, key: string, index: number) => void;
}
): T;
}

View file

@ -1,5 +0,0 @@
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>;
export interface HydrateOptions {
value?: string;
}

View file

@ -1,3 +0,0 @@
// TODO: add types (if helpful)
declare module 'micromark-extension-gfm';
declare module 'micromark-extension-gfm/html.js';

View file

@ -1,14 +0,0 @@
export interface MicromarkExtensionContext {
sliceSerialize(node: any): string;
raw(value: string): void;
tag(value: string): void;
data(value: string): void;
resume(): any;
}
export type MicromarkExtensionCallback = (this: MicromarkExtensionContext, node: any) => void;
export interface MicromarkExtension {
enter?: Record<string, MicromarkExtensionCallback>;
exit?: Record<string, MicromarkExtensionCallback>;
}

View file

@ -1,5 +0,0 @@
declare module 'postcss-icss-keyframes' {
import type { Plugin } from 'postcss';
export default function (options: { generateScopedName(keyframesName: string, filepath: string, css: string): string }): Plugin;
}

View file

@ -1 +0,0 @@
export { AstroConfig, AstroUserConfig } from './config';

View file

@ -1 +0,0 @@
declare module 'resolve';

View file

@ -1,2 +0,0 @@
// we shouldnt have this as a dependency for Astro, but we may dynamically import it if a user requests it, so let TS know about it
declare module 'tailwindcss';

View file

@ -1,23 +0,0 @@
import type { TemplateNode } from '@astrojs/parser';
import type { CompileOptions } from './compiler';
export type VisitorFn<T = TemplateNode> = (this: { skip: () => void; remove: () => void; replace: (node: T) => void }, node: T, parent: T, type: string, index: number) => void;
export interface NodeVisitor {
enter?: VisitorFn;
leave?: VisitorFn;
}
export interface Transformer {
visitors?: {
html?: Record<string, NodeVisitor>;
css?: Record<string, NodeVisitor>;
};
finalize: () => Promise<void>;
}
export interface TransformOptions {
compileOptions: CompileOptions;
filename: string;
fileID: string;
}

View file

@ -1,27 +0,0 @@
import type { Attribute } from '@astrojs/parser';
// AST utility functions
/** Get TemplateNode attribute from name */
export function getAttr(attributes: Attribute[], name: string): Attribute | undefined {
const attr = attributes.find((a) => a.name === name);
return attr;
}
/** Get TemplateNode attribute by value */
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
if (attributes.length === 0) return '';
const attr = getAttr(attributes, name);
if (attr) {
return attr.value[0]?.data;
}
}
/** Set TemplateNode attribute value */
export function setAttrValue(attributes: Attribute[], name: string, value: string): void {
const attr = attributes.find((a) => a.name === name);
if (attr && attr.value[0]) {
attr.value[0].data = value;
attr.value[0].raw = value;
}
}

View file

@ -1,365 +0,0 @@
import cheerio from 'cheerio';
import del from 'del';
import eslexer from 'es-module-lexer';
import fs from 'fs';
import { bold, green, red, underline, yellow } from 'kleur/colors';
import mime from 'mime';
import path from 'path';
import { performance } from 'perf_hooks';
import glob from 'tiny-glob';
import hash from 'shorthash';
import srcsetParse from 'srcset-parse';
import { fileURLToPath } from 'url';
import type { AstroConfig, BuildOutput, BundleMap, PageDependencies, RouteData, RuntimeMode, ScriptInfo } from './@types/astro';
import { bundleCSS } from './build/bundle/css.js';
import { bundleJS, bundleHoistedJS, collectJSImports } from './build/bundle/js.js';
import { buildStaticPage, getStaticPathsForPage } from './build/page.js';
import { generateSitemap } from './build/sitemap.js';
import { collectBundleStats, logURLStats, mapBundleStatsToURLStats } from './build/stats.js';
import { getDistPath, stopTimer } from './build/util.js';
import type { LogOptions } from './logger';
import { debug, defaultLogDestination, defaultLogLevel, error, info, warn } from './logger.js';
import { createRuntime, LoadResult } from './runtime.js';
// This package isn't real ESM, so have to coerce it
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;
const defaultLogging: LogOptions = {
level: defaultLogLevel,
dest: defaultLogDestination,
};
/** Is this URL remote or embedded? */
function isRemoteOrEmbedded(url: string) {
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//') || url.startsWith('data:');
}
/** The primary build action */
export async function build(astroConfig: AstroConfig, logging: LogOptions = defaultLogging): Promise<0 | 1> {
const { projectRoot } = astroConfig;
const buildState: BuildOutput = {};
const depTree: BundleMap = {};
const timer: Record<string, number> = {};
const runtimeLogging: LogOptions = {
level: 'error',
dest: defaultLogDestination,
};
// warn users if missing config item in build that may result in broken SEO (cant disable, as they should provide this)
if (!astroConfig.buildOptions.site) {
warn(logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
}
const mode: RuntimeMode = 'production';
const astroRuntime = await createRuntime(astroConfig, { mode, logging: runtimeLogging });
const { runtimeConfig } = astroRuntime;
const { snowpackRuntime } = runtimeConfig;
try {
// 0. erase build directory
await del(fileURLToPath(astroConfig.dist));
/**
* 1. Build Pages
* Source files are built in parallel and stored in memory. Most assets are also gathered here, too.
*/
timer.build = performance.now();
info(logging, 'build', yellow('! building pages...'));
const allRoutesAndPaths = await Promise.all(
runtimeConfig.manifest.routes.map(async (route): Promise<[RouteData, string[]]> => {
if (route.path) {
return [route, [route.path]];
} else {
const result = await getStaticPathsForPage({
astroConfig,
astroRuntime,
route,
snowpackRuntime,
logging,
});
if (result.rss.xml) {
if (buildState[result.rss.url]) {
throw new Error(`[getStaticPaths] RSS feed ${result.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
}
buildState[result.rss.url] = {
srcPath: new URL(result.rss.url, projectRoot),
contents: result.rss.xml,
contentType: 'text/xml',
encoding: 'utf8',
};
}
return [route, result.paths];
}
})
);
try {
await Promise.all(
allRoutesAndPaths.map(async ([route, paths]: [RouteData, string[]]) => {
for (const p of paths) {
await buildStaticPage({
astroConfig,
buildState,
route,
path: p,
astroRuntime,
});
}
})
);
} catch (e) {
if (e.filename) {
let stack = e.stack
.replace(/Object\.__render \(/gm, '')
.replace(/\/_astro\/(.+)\.astro\.js\:\d+\:\d+\)/gm, (_: string, $1: string) => 'file://' + fileURLToPath(projectRoot) + $1 + '.astro')
.split('\n');
stack.splice(1, 0, ` at file://${e.filename}`);
stack = stack.join('\n');
error(
logging,
'build',
`${red(`Unable to render ${underline(e.filename.replace(fileURLToPath(projectRoot), ''))}`)}
${stack}
`
);
} else {
error(logging, 'build', e.message);
}
error(logging, 'build', red('✕ building pages failed!'));
await astroRuntime.shutdown();
return 1;
}
info(logging, 'build', green('✔'), 'pages built.');
debug(logging, 'build', `built pages [${stopTimer(timer.build)}]`);
// after pages are built, build depTree
timer.deps = performance.now();
const scanPromises: Promise<void>[] = [];
await eslexer.init;
for (const id of Object.keys(buildState)) {
if (buildState[id].contentType !== 'text/html') continue; // only scan HTML files
const pageDeps = findDeps(buildState[id].contents as string, {
astroConfig,
srcPath: buildState[id].srcPath,
id,
});
depTree[id] = pageDeps;
// while scanning we will find some unbuilt files; make sure those are all built while scanning
for (const url of [...pageDeps.js, ...pageDeps.css, ...pageDeps.images]) {
if (!buildState[url])
scanPromises.push(
astroRuntime.load(url).then((result: LoadResult) => {
if (result.statusCode === 404) {
if (url.startsWith('/_astro/')) {
throw new Error(`${buildState[id].srcPath.href}: could not find file "${url}".`);
}
warn(logging, 'build', `${buildState[id].srcPath.href}: could not find file "${url}". Marked as external.`);
return;
}
if (result.statusCode !== 200) {
// there shouldnt be a build error here
throw (result as any).error || new Error(`unexpected ${result.statusCode} response from "${url}".`);
}
buildState[url] = {
srcPath: new URL(url, projectRoot),
contents: result.contents,
contentType: result.contentType || mime.getType(url) || '',
};
})
);
}
}
await Promise.all(scanPromises);
debug(logging, 'build', `scanned deps [${stopTimer(timer.deps)}]`);
/**
* 2. Bundling 1st Pass: In-memory
* Bundle CSS, and anything else that can happen in memory (for now, JS bundling happens after writing to disk)
*/
info(logging, 'build', yellow('! optimizing css...'));
timer.prebundleCSS = performance.now();
await Promise.all([
bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => {
debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundleCSS)}]`);
}),
bundleHoistedJS({ buildState, astroConfig, logging, depTree, runtime: astroRuntime, dist: astroConfig.dist }),
// TODO: optimize images?
]);
// TODO: minify HTML?
info(logging, 'build', green('✔'), 'css optimized.');
/**
* 3. Write to disk
* Also clear in-memory bundle
*/
// collect stats output
const urlStats = await collectBundleStats(buildState, depTree);
// collect JS imports for bundling
const jsImports = await collectJSImports(buildState);
// write sitemap
if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) {
timer.sitemap = performance.now();
info(logging, 'build', yellow('! creating sitemap...'));
const sitemap = generateSitemap(buildState, astroConfig.buildOptions.site);
const sitemapPath = new URL('sitemap.xml', astroConfig.dist);
await fs.promises.mkdir(path.dirname(fileURLToPath(sitemapPath)), { recursive: true });
await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
info(logging, 'build', green('✔'), 'sitemap built.');
debug(logging, 'build', `built sitemap [${stopTimer(timer.sitemap)}]`);
}
// write to disk and free up memory
timer.write = performance.now();
for (const id of Object.keys(buildState)) {
const outPath = new URL(`.${id}`, astroConfig.dist);
const parentDir = path.dirname(fileURLToPath(outPath));
await fs.promises.mkdir(parentDir, { recursive: true });
const handle = await fs.promises.open(outPath, 'w');
await fs.promises.writeFile(handle, buildState[id].contents, buildState[id].encoding);
// Ensure the file handle is not left hanging which will
// result in the garbage collector loggin errors in the console
// when it eventually has to close them.
await handle.close();
delete buildState[id];
delete depTree[id];
}
debug(logging, 'build', `wrote files to disk [${stopTimer(timer.write)}]`);
/**
* 4. Copy Public Assets
*/
if (fs.existsSync(astroConfig.public)) {
info(logging, 'build', yellow(`! copying public folder...`));
timer.public = performance.now();
const cwd = fileURLToPath(astroConfig.public);
const publicFiles = await glob('**/*', { cwd, filesOnly: true });
await Promise.all(
publicFiles.map(async (filepath) => {
const srcPath = new URL(filepath, astroConfig.public);
const distPath = new URL(filepath, astroConfig.dist);
await fs.promises.mkdir(path.dirname(fileURLToPath(distPath)), { recursive: true });
await fs.promises.copyFile(srcPath, distPath);
})
);
debug(logging, 'build', `copied public folder [${stopTimer(timer.public)}]`);
info(logging, 'build', green('✔'), 'public folder copied.');
} else {
if (path.basename(astroConfig.public.toString()) !== 'public') {
info(logging, 'tip', yellow(`! no public folder ${astroConfig.public} found...`));
}
}
/**
* 5. Bundling 2nd Pass: On disk
* Bundle JS, which requires hard files to optimize
*/
info(logging, 'build', yellow(`! bundling...`));
if (jsImports.size > 0) {
timer.bundleJS = performance.now();
const jsStats = await bundleJS(jsImports, { dist: astroConfig.dist, astroRuntime });
mapBundleStatsToURLStats({ urlStats, depTree, bundleStats: jsStats });
debug(logging, 'build', `bundled JS [${stopTimer(timer.bundleJS)}]`);
info(logging, 'build', green(``), 'bundling complete.');
}
/**
* 6. Print stats
*/
logURLStats(logging, urlStats);
await astroRuntime.shutdown();
info(logging, 'build', bold(green('▶ Build Complete!')));
return 0;
} catch (err) {
error(logging, 'build', err.message);
await astroRuntime.shutdown();
return 1;
}
}
/** Given an HTML string, collect <link> and <img> tags */
export function findDeps(html: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL; id: string }): PageDependencies {
const pageDeps: PageDependencies = {
js: new Set<string>(),
css: new Set<string>(),
images: new Set<string>(),
hoistedJS: new Map<string, ScriptInfo>(),
};
const $ = cheerio.load(html);
$('script').each((_i, el) => {
const src = $(el).attr('src');
const hoist = $(el).attr('data-astro') === 'hoist';
if (hoist) {
if (src) {
pageDeps.hoistedJS.set(src, {
src,
});
} else {
let content = $(el).html() || '';
pageDeps.hoistedJS.set(`astro-virtual:${hash.unique(content)}`, {
content,
});
}
} else if (src) {
if (isRemoteOrEmbedded(src)) return;
pageDeps.js.add(getDistPath(src, { astroConfig, srcPath }));
} else {
const text = $(el).html();
if (!text) return;
const [imports] = eslexer.parse(text);
for (const spec of imports) {
const importSrc = spec.n;
if (importSrc && !isRemoteOrEmbedded(importSrc)) {
pageDeps.js.add(getDistPath(importSrc, { astroConfig, srcPath }));
}
}
}
});
$('link[href]').each((_i, el) => {
const href = $(el).attr('href');
if (href && !isRemoteOrEmbedded(href) && ($(el).attr('rel') === 'stylesheet' || $(el).attr('type') === 'text/css' || href.endsWith('.css'))) {
const dist = getDistPath(href, { astroConfig, srcPath });
pageDeps.css.add(dist);
}
});
$('img[src]').each((_i, el) => {
const src = $(el).attr('src');
if (src && !isRemoteOrEmbedded(src)) {
pageDeps.images.add(getDistPath(src, { astroConfig, srcPath }));
}
});
$('img[srcset]').each((_i, el) => {
const srcset = $(el).attr('srcset') || '';
for (const src of matchSrcset(srcset)) {
if (!isRemoteOrEmbedded(src.url)) {
pageDeps.images.add(getDistPath(src.url, { astroConfig, srcPath }));
}
}
});
// Add in srcset check for <source>
$('source[srcset]').each((_i, el) => {
const srcset = $(el).attr('srcset') || '';
for (const src of matchSrcset(srcset)) {
if (!isRemoteOrEmbedded(src.url)) {
pageDeps.images.add(getDistPath(src.url, { astroConfig, srcPath }));
}
}
});
// important: preserve the scan order of deps! order matters on pages
return pageDeps;
}

View file

@ -1,156 +0,0 @@
import type { AstroConfig, BuildOutput, BundleMap } from '../../@types/astro';
import type { LogOptions } from '../../logger.js';
import { performance } from 'perf_hooks';
import shorthash from 'shorthash';
import cheerio from 'cheerio';
import esbuild from 'esbuild';
import { getDistPath, getSrcPath, IS_ASTRO_FILE_URL, stopTimer } from '../util.js';
import { debug } from '../../logger.js';
// config
const COMMON_URL = `/_astro/common-[HASH].css`; // [HASH] will be replaced
/**
* Bundle CSS
* For files within dep tree, find ways to combine them.
* Current logic:
* - If CSS appears across multiple pages, combine into `/_astro/common.css` bundle
* - Otherwise, combine page CSS into one request as `/_astro/[page].css` bundle
*
* This operation _should_ be relatively-safe to do in parallel with other bundling,
* assuming other bundling steps dont touch CSS. While this step does modify HTML,
* it doesnt keep anything in local memory so other processes may modify HTML too.
*
* This operation mutates the original references of the buildOutput not only for
* safety (prevents possible conflicts), but for efficiency.
*/
export async function bundleCSS({
astroConfig,
buildState,
logging,
depTree,
}: {
astroConfig: AstroConfig;
buildState: BuildOutput;
logging: LogOptions;
depTree: BundleMap;
}): Promise<void> {
const timer: Record<string, number> = {};
const cssMap = new Map<string, string>();
// 1. organize CSS into common or page-specific CSS
timer.bundle = performance.now();
const sortedPages = Object.keys(depTree); // these were scanned in parallel; sort to create somewhat deterministic order
sortedPages.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
for (const pageUrl of sortedPages) {
const { css } = depTree[pageUrl];
for (const cssUrl of css.keys()) {
if (!IS_ASTRO_FILE_URL.test(cssUrl)) {
// do not add to cssMap, leave as-is.
} else if (cssMap.has(cssUrl)) {
// scenario 1: if multiple URLs require this CSS, upgrade to common chunk
cssMap.set(cssUrl, COMMON_URL);
} else {
// scenario 2: otherwise, assume this CSS is page-specific
cssMap.set(cssUrl, '/_astro' + pageUrl.replace(/.html$/, '').replace(/^\./, '') + '-[HASH].css');
}
}
}
// 2. bundle (note: assume cssMap keys are in specific, correct order; assume buildState[] keys are in different order each time)
timer.bundle = performance.now();
// note: dont parallelize here otherwise CSS may end up in random order
for (const id of cssMap.keys()) {
const newUrl = cssMap.get(id) as string;
// if new bundle, create
if (!buildState[newUrl]) {
buildState[newUrl] = {
srcPath: getSrcPath(id, { astroConfig }), // this isnt accurate, but we can at least reference a file in the bundle
contents: '',
contentType: 'text/css',
encoding: 'utf8',
};
}
// append to bundle, delete old file
(buildState[newUrl] as any).contents += Buffer.isBuffer(buildState[id].contents) ? buildState[id].contents.toString('utf8') : buildState[id].contents;
delete buildState[id];
}
debug(logging, 'css', `bundled [${stopTimer(timer.bundle)}]`);
// 3. minify
timer.minify = performance.now();
await Promise.all(
Object.keys(buildState).map(async (id) => {
if (buildState[id].contentType !== 'text/css') return;
const { code } = await esbuild.transform(buildState[id].contents.toString(), {
loader: 'css',
minify: true,
});
buildState[id].contents = code;
})
);
debug(logging, 'css', `minified [${stopTimer(timer.minify)}]`);
// 4. determine hashes based on CSS content (deterministic), and update HTML <link> tags with final hashed URLs
timer.hashes = performance.now();
const cssHashes = new Map<string, string>();
for (const id of Object.keys(buildState)) {
if (!id.includes('[HASH].css')) continue; // iterate through buildState, looking to replace [HASH]
const hash = shorthash.unique(buildState[id].contents as string);
const newID = id.replace(/\[HASH\]/, hash);
cssHashes.set(id, newID);
buildState[newID] = buildState[id]; // copy ref without cloning to save memory
delete buildState[id]; // delete old ref
}
debug(logging, 'css', `built hashes [${stopTimer(timer.hashes)}]`);
// 5. update HTML <link> tags with final hashed URLs
timer.html = performance.now();
await Promise.all(
Object.keys(buildState).map(async (id) => {
if (buildState[id].contentType !== 'text/html') return;
const $ = cheerio.load(buildState[id].contents);
const stylesheets = new Set<string>(); // keep track of page-specific CSS so we remove dupes
const preloads = new Set<string>(); // list of stylesheets preloads, to remove dupes
$('link[href]').each((i, el) => {
const srcPath = getSrcPath(id, { astroConfig });
const oldHref = getDistPath($(el).attr('href') || '', { astroConfig, srcPath }); // note: this may be a relative URL; transform to absolute to find a buildOutput match
const newHref = cssMap.get(oldHref);
if (!newHref) {
return;
}
if (el.attribs?.rel === 'preload') {
if (preloads.has(newHref)) {
$(el).remove();
} else {
$(el).attr('href', cssHashes.get(newHref) || '');
preloads.add(newHref);
}
return;
}
if (stylesheets.has(newHref)) {
$(el).remove(); // this is a dupe; remove
} else {
$(el).attr('href', cssHashes.get(newHref) || ''); // new CSS; update href (important! use cssHashes, not cssMap)
// bonus: add [rel] and [type]. not necessary, but why not?
$(el).attr('rel', 'stylesheet');
$(el).attr('type', 'text/css');
stylesheets.add(newHref);
}
});
(buildState[id] as any).contents = $.html(); // save updated HTML in global buildState
})
);
debug(logging, 'css', `parsed html [${stopTimer(timer.html)}]`);
}

View file

@ -1,255 +0,0 @@
import type { InputOptions, OutputOptions, OutputChunk } from 'rollup';
import type { AstroConfig, BundleMap, BuildOutput, ScriptInfo, InlineScriptInfo } from '../../@types/astro';
import type { AstroRuntime } from '../../runtime';
import type { LogOptions } from '../../logger.js';
import { fileURLToPath } from 'url';
import { rollup } from 'rollup';
import { terser } from 'rollup-plugin-terser';
import { createBundleStats, addBundleStats, BundleStatsMap } from '../stats.js';
import { IS_ASTRO_FILE_URL } from '../util.js';
import cheerio from 'cheerio';
import path from 'path';
interface BundleOptions {
dist: URL;
astroRuntime: AstroRuntime;
}
/** Collect JS imports from build output */
export function collectJSImports(buildState: BuildOutput): Set<string> {
const imports = new Set<string>();
for (const id of Object.keys(buildState)) {
if (buildState[id].contentType === 'application/javascript') imports.add(id);
}
return imports;
}
function pageUrlToVirtualJSEntry(pageUrl: string) {
return 'astro-virtual:' + pageUrl.replace(/.html$/, '').replace(/^\./, '') + '.js';
}
export async function bundleHoistedJS({
buildState,
astroConfig,
logging,
depTree,
dist,
runtime,
}: {
astroConfig: AstroConfig;
buildState: BuildOutput;
logging: LogOptions;
depTree: BundleMap;
dist: URL;
runtime: AstroRuntime;
}) {
const sortedPages = Object.keys(depTree); // these were scanned in parallel; sort to create somewhat deterministic order
sortedPages.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
/**
* 1. Go over sorted pages and create a virtual module for all of its dependencies
*/
const entryImports: string[] = [];
const virtualScripts = new Map<string, ScriptInfo>();
const pageToEntryMap = new Map<string, string>();
for (let pageUrl of sortedPages) {
const hoistedJS = depTree[pageUrl].hoistedJS;
if (hoistedJS.size) {
for (let [url, scriptInfo] of hoistedJS) {
if (virtualScripts.has(url) || !url.startsWith('astro-virtual:')) continue;
virtualScripts.set(url, scriptInfo);
}
const entryURL = pageUrlToVirtualJSEntry(pageUrl);
const entryJS = Array.from(hoistedJS.keys())
.map((url) => `import '${url}';`)
.join('\n');
virtualScripts.set(entryURL, {
content: entryJS,
});
entryImports.push(entryURL);
pageToEntryMap.set(pageUrl, entryURL);
}
}
if (!entryImports.length) {
// There are no hoisted scripts, bail
return;
}
/**
* 2. Run the bundle to bundle each pages JS into a single bundle (with shared content)
*/
const inputOptions: InputOptions = {
input: entryImports,
plugins: [
{
name: 'astro:build',
resolveId(source: string, imported?: string) {
if (virtualScripts.has(source)) {
return source;
}
if (source.startsWith('/')) {
return source;
}
if (imported) {
const outUrl = new URL(source, 'http://example.com' + imported);
return outUrl.pathname;
}
return null;
},
async load(id: string) {
if (virtualScripts.has(id)) {
let info = virtualScripts.get(id) as InlineScriptInfo;
return info.content;
}
const result = await runtime.load(id);
if (result.statusCode !== 200) {
return null;
}
return result.contents.toString('utf-8');
},
},
],
};
const build = await rollup(inputOptions);
const outputOptions: OutputOptions = {
dir: fileURLToPath(dist),
format: 'esm',
exports: 'named',
entryFileNames(chunk) {
const { facadeModuleId } = chunk;
if (!facadeModuleId) throw new Error(`facadeModuleId missing: ${chunk.name}`);
return facadeModuleId.substr('astro-virtual:/'.length, facadeModuleId.length - 'astro-virtual:/'.length - 3 /* .js */) + '-[hash].js';
},
plugins: [
// We are using terser for the demo, but might switch to something else long term
// Look into that rather than adding options here.
terser(),
],
};
const { output } = await build.write(outputOptions);
/**
* 3. Get a mapping of the virtual filename to the chunk file name
*/
const entryToChunkFileName = new Map<string, string>();
output.forEach((chunk) => {
const { fileName, facadeModuleId, isEntry } = chunk as OutputChunk;
if (!facadeModuleId || !isEntry) return;
entryToChunkFileName.set(facadeModuleId, fileName);
});
/**
* 4. Update the original HTML with the new chunk scripts
*/
Object.keys(buildState).forEach((id) => {
if (buildState[id].contentType !== 'text/html') return;
const entryVirtualURL = pageUrlToVirtualJSEntry(id);
let hasHoisted = false;
const $ = cheerio.load(buildState[id].contents);
$('script[data-astro="hoist"]').each((i, el) => {
hasHoisted = true;
if (i === 0) {
let chunkName = entryToChunkFileName.get(entryVirtualURL);
if (!chunkName) return;
let chunkPathname = '/' + chunkName;
let relLink = path.relative(path.dirname(id), chunkPathname);
$(el).attr('src', relLink.startsWith('.') ? relLink : './' + relLink);
$(el).removeAttr('data-astro');
$(el).html('');
} else {
$(el).remove();
}
});
if (hasHoisted) {
(buildState[id] as any).contents = $.html(); // save updated HTML in global buildState
}
});
}
/** Bundle JS action */
export async function bundleJS(imports: Set<string>, { astroRuntime, dist }: BundleOptions): Promise<BundleStatsMap> {
const ROOT = 'astro:root';
const validImports = [...imports].filter((url) => IS_ASTRO_FILE_URL.test(url));
const root = `
${validImports.map((url) => `import '${url}';`).join('\n')}
`;
const inputOptions: InputOptions = {
input: validImports,
plugins: [
{
name: 'astro:build',
resolveId(source: string, imported?: string) {
if (source === ROOT) {
return source;
}
if (source.startsWith('/')) {
return source;
}
if (imported) {
const outUrl = new URL(source, 'http://example.com' + imported);
return outUrl.pathname;
}
return null;
},
async load(id: string) {
if (id === ROOT) {
return root;
}
const result = await astroRuntime.load(id);
if (result.statusCode !== 200) {
return null;
}
return result.contents.toString('utf-8');
},
},
],
};
const build = await rollup(inputOptions);
const outputOptions: OutputOptions = {
dir: fileURLToPath(dist),
format: 'esm',
exports: 'named',
entryFileNames(chunk) {
const { facadeModuleId } = chunk;
if (!facadeModuleId) throw new Error(`facadeModuleId missing: ${chunk.name}`);
return facadeModuleId.substr(1);
},
plugins: [
// We are using terser for the demo, but might switch to something else long term
// Look into that rather than adding options here.
terser(),
],
};
const stats = createBundleStats();
const { output } = await build.write(outputOptions);
await Promise.all(
output.map(async (chunk) => {
const code = (chunk as OutputChunk).code || '';
await addBundleStats(stats, code, chunk.fileName);
})
);
return stats;
}

View file

@ -1,77 +0,0 @@
import _path from 'path';
import type { ServerRuntime as SnowpackServerRuntime } from 'snowpack';
import { fileURLToPath } from 'url';
import type { AstroConfig, BuildOutput, RouteData } from '../@types/astro';
import { LogOptions } from '../logger';
import type { AstroRuntime } from '../runtime.js';
import { convertMatchToLocation, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../util.js';
import { generatePaginateFunction } from './paginate.js';
import { generateRssFunction } from './rss.js';
interface PageBuildOptions {
astroConfig: AstroConfig;
buildState: BuildOutput;
path: string;
route: RouteData;
astroRuntime: AstroRuntime;
}
/** Build dynamic page */
export async function getStaticPathsForPage({
astroConfig,
astroRuntime,
snowpackRuntime,
route,
logging,
}: {
astroConfig: AstroConfig;
astroRuntime: AstroRuntime;
route: RouteData;
snowpackRuntime: SnowpackServerRuntime;
logging: LogOptions;
}): Promise<{ paths: string[]; rss: any }> {
const location = convertMatchToLocation(route, astroConfig);
const mod = await snowpackRuntime.importModule(location.snowpackURL);
validateGetStaticPathsModule(mod);
const [rssFunction, rssResult] = generateRssFunction(astroConfig.buildOptions.site, route);
const staticPaths = await astroRuntime.getStaticPaths(route.component, mod, {
paginate: generatePaginateFunction(route),
rss: rssFunction,
});
validateGetStaticPathsResult(staticPaths, logging);
return {
paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean),
rss: rssResult,
};
}
function formatOutFile(path: string, pageUrlFormat: AstroConfig['buildOptions']['pageUrlFormat']) {
if (path === '/404') {
return '/404.html';
}
if (path === '/') {
return '/index.html';
}
if (pageUrlFormat === 'directory') {
return _path.posix.join(path, '/index.html');
}
return `${path}.html`;
}
/** Build static page */
export async function buildStaticPage({ astroConfig, buildState, path, route, astroRuntime }: PageBuildOptions): Promise<void> {
const location = convertMatchToLocation(route, astroConfig);
const normalizedPath = astroConfig.devOptions.trailingSlash === 'never' ? path : path.endsWith('/') ? path : `${path}/`;
const result = await astroRuntime.load(normalizedPath);
if (result.statusCode !== 200) {
let err = (result as any).error;
if (!(err instanceof Error)) err = new Error(err);
err.filename = fileURLToPath(location.fileURL);
throw err;
}
buildState[formatOutFile(path, astroConfig.buildOptions.pageUrlFormat)] = {
srcPath: location.fileURL,
contents: result.contents,
contentType: 'text/html',
encoding: 'utf8',
};
}

View file

@ -1,27 +0,0 @@
import type { BuildOutput } from '../@types/astro';
import { canonicalURL } from './util.js';
/** Construct sitemap.xml given a set of URLs */
export function generateSitemap(buildState: BuildOutput, site: string): string {
const uniqueURLs = new Set<string>();
// TODO: find way to respect <link rel="canonical"> URLs here
// TODO: find way to exclude pages from sitemap (currently only skips 404 pages)
// look through built pages, only add HTML
for (const id of Object.keys(buildState)) {
if (buildState[id].contentType !== 'text/html') continue;
if (id === '/404.html') continue;
uniqueURLs.add(canonicalURL(id, site).href);
}
const pages = [...uniqueURLs];
pages.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
let sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
for (const page of pages) {
sitemap += `<url><loc>${page}</loc></url>`;
}
sitemap += `</urlset>\n`;
return sitemap;
}

View file

@ -1,92 +0,0 @@
import type { BuildOutput, BundleMap } from '../@types/astro';
import type { LogOptions } from '../logger';
import { info, table } from '../logger.js';
import { underline, bold } from 'kleur/colors';
import gzipSize from 'gzip-size';
import prettyBytes from 'pretty-bytes';
interface BundleStats {
size: number;
gzipSize: number;
}
interface URLStats {
dynamicImports: Set<string>;
stats: BundleStats[];
}
export type BundleStatsMap = Map<string, BundleStats>;
export type URLStatsMap = Map<string, URLStats>;
export function createURLStats(): URLStatsMap {
return new Map<string, URLStats>();
}
export function createBundleStats(): BundleStatsMap {
return new Map<string, BundleStats>();
}
export async function addBundleStats(bundleStatsMap: BundleStatsMap, code: string, filename: string) {
const gzsize = await gzipSize(code);
bundleStatsMap.set(filename, {
size: Buffer.byteLength(code),
gzipSize: gzsize,
});
}
export function mapBundleStatsToURLStats({ urlStats, depTree, bundleStats }: { urlStats: URLStatsMap; depTree: BundleMap; bundleStats: BundleStatsMap }) {
for (let [srcPath, stats] of bundleStats) {
for (let url of urlStats.keys()) {
if (depTree[url] && depTree[url].js.has('/' + srcPath)) {
urlStats.get(url)?.stats.push(stats);
}
}
}
}
export async function collectBundleStats(buildState: BuildOutput, depTree: BundleMap): Promise<URLStatsMap> {
const urlStats = createURLStats();
await Promise.all(
Object.keys(buildState).map(async (id) => {
if (!depTree[id]) return;
const stats = await Promise.all(
[...depTree[id].js, ...depTree[id].css, ...depTree[id].images].map(async (url) => {
if (!buildState[url]) return undefined;
const stat = {
size: Buffer.byteLength(buildState[url].contents),
gzipSize: await gzipSize(buildState[url].contents),
};
return stat;
})
);
urlStats.set(id, {
dynamicImports: new Set<string>(),
stats: stats.filter((s) => !!s) as any,
});
})
);
return urlStats;
}
export function logURLStats(logging: LogOptions, urlStats: URLStatsMap) {
const builtURLs = [...urlStats.keys()].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
info(logging, null, '');
const log = table(logging, [60, 20]);
log(info, ' ' + bold(underline('Pages')), bold(underline('Page Weight (GZip)')));
const lastIndex = builtURLs.length - 1;
builtURLs.forEach((url, index) => {
const sep = index === 0 ? '┌' : index === lastIndex ? '└' : '├';
const urlPart = ' ' + sep + ' ' + url;
const bytes =
urlStats
.get(url)
?.stats.map((s) => s.gzipSize)
.reduce((a, b) => a + b, 0) || 0;
const sizePart = prettyBytes(bytes);
log(info, urlPart, sizePart);
});
}

View file

@ -1,81 +0,0 @@
import type { AstroConfig } from '../@types/astro';
import { performance } from 'perf_hooks';
import fs from 'fs';
import path from 'path';
import { URL } from 'url';
/**
* Only Astro-handled imports need bundling. Any other imports are considered
* a part of `public/`, and should not be touched.
*/
export const IS_ASTRO_FILE_URL = /^\/(_astro|_astro_frontend|_snowpack)\//;
/** Normalize URL to its canonical form */
export function canonicalURL(url: string, base?: string): URL {
let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical
pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections)
if (!path.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if theres no extension
pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() wont)
if (base) {
return new URL('.' + pathname, base);
} else {
return new URL(pathname, base);
}
}
/** Resolve final output URL */
export function getDistPath(specifier: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL }): string {
if (specifier[0] === '/') return specifier; // assume absolute URLs are correct
const { pages: pagesRoot, projectRoot } = astroConfig;
const fileLoc = new URL(specifier, srcPath);
const projectLoc = fileLoc.pathname.replace(projectRoot.pathname, '');
const ext = path.extname(fileLoc.pathname);
const isPage = fileLoc.pathname.includes(pagesRoot.pathname) && (ext === '.astro' || ext === '.md');
// if this lives in src/pages, return that URL
if (isPage) {
const [, publicURL] = projectLoc.split(pagesRoot.pathname);
return publicURL || '/index.html'; // if this is missing, this is the root
}
// if this is in public/, use that as final URL
const isPublicAsset = fileLoc.pathname.includes(astroConfig.public.pathname);
if (isPublicAsset) {
return fileLoc.pathname.replace(astroConfig.public.pathname, '/');
}
// otherwise, return /_astro/* url
return '/_astro/' + projectLoc;
}
/** Given a final output URL, guess at src path (may be inaccurate; only for non-pages) */
export function getSrcPath(distURL: string, { astroConfig }: { astroConfig: AstroConfig }): URL {
if (distURL.startsWith('/_astro/')) {
return new URL('.' + distURL.replace(/^\/_astro\//, ''), astroConfig.projectRoot);
} else if (distURL === '/index.html') {
return new URL('./index.astro', astroConfig.pages);
}
const possibleURLs = [
new URL('.' + distURL, astroConfig.public), // public asset
new URL('.' + distURL.replace(/([^\/])+\/d+\/index.html/, '$$1.astro'), astroConfig.pages), // collection page
new URL('.' + distURL.replace(/\/index\.html$/, '.astro'), astroConfig.pages), // page
// TODO: Astro pages (this isnt currently used for that lookup)
];
// if this is in public/ or pages/, return that
for (const possibleURL of possibleURLs) {
if (fs.existsSync(possibleURL)) return possibleURL;
}
// otherwise resolve relative to project
return new URL('.' + distURL, astroConfig.projectRoot);
}
/** Stop timer & format time for profiling */
export function stopTimer(start: number): string {
const diff = performance.now() - start;
return diff < 750 ? `${Math.round(diff)}ms` : `${(diff / 1000).toFixed(1)}s`;
}

View file

@ -0,0 +1,5 @@
# `cli/`
Code that controls Astros binfile and is responsible for `astro *` CLI commands.
[See CONTRIBUTING.md](../../../../CONTRIBUTING.md) for a code overview.

View file

@ -1,7 +1,8 @@
/* eslint-disable no-console */
import { AstroCheck, DiagnosticSeverity } from '@astrojs/language-server';
import type { AstroConfig } from './@types/astro';
import { bold, blue, black, bgWhite, red, cyan, yellow } from 'kleur/colors';
import type { AstroConfig } from '../@types/astro-core';
import { bold, black, bgWhite, red, cyan, yellow } from 'kleur/colors';
import glob from 'fast-glob';
import * as path from 'path';
import { pathToFileURL } from 'url';

View file

@ -1,29 +1,18 @@
/* eslint-disable no-console */
import { promises as fsPromises } from 'fs';
import type { AstroConfig } from '../@types/astro-core';
import { LogOptions } from '../core/logger.js';
import * as colors from 'kleur/colors';
import fs from 'fs';
import yargs from 'yargs-parser';
import { z } from 'zod';
import type { AstroConfig } from './@types/astro';
import { build } from './build.js';
import { formatConfigError, loadConfig } from './config.js';
import devServer from './dev.js';
import { preview } from './preview.js';
import { reload } from './reload.js';
import { defaultLogDestination } from '../core/logger.js';
import build from '../core/build/index.js';
import devServer from '../core/dev/index.js';
import preview from '../core/preview/index.js';
import { check } from './check.js';
const { readFile } = fsPromises;
const buildAndExit = async (astroConfig: AstroConfig) => {
const ret = await build(astroConfig);
process.exit(ret);
};
const reloadAndExit = async () => {
const ret = await reload();
process.exit(ret);
};
const checkAndExit = async (astroConfig: AstroConfig) => {
const ret = await check(astroConfig);
process.exit(ret);
};
import { formatConfigError, loadConfig } from '../core/config.js';
type Arguments = yargs.Arguments;
type cliCommand = 'help' | 'version' | 'dev' | 'build' | 'preview' | 'reload' | 'check';
@ -36,7 +25,6 @@ interface CLIState {
hostname?: string;
port?: number;
config?: string;
reload?: boolean;
};
}
@ -67,10 +55,6 @@ function resolveArgs(flags: Arguments): CLIState {
case 'check':
return { cmd: 'check', options };
default:
if (flags.reload) {
return { cmd: 'reload', options };
}
return { cmd: 'help', options };
}
}
@ -78,7 +62,6 @@ function resolveArgs(flags: Arguments): CLIState {
/** Display --help flag */
function printHelp() {
console.error(` ${colors.bold('astro')} - Futuristic web development tool.
${colors.bold('Commands:')}
astro dev Run Astro in development mode.
astro build Build a pre-compiled production version of your site.
@ -89,7 +72,6 @@ function printHelp() {
--config <path> Specify the path to the Astro config file.
--project-root <path> Specify the path to the project root folder.
--no-sitemap Disable sitemap generation (build only).
--reload Clean the cache, reinstalling dependencies.
--verbose Enable verbose logging
--silent Disable logging
--version Show the version number and exit.
@ -99,7 +81,7 @@ function printHelp() {
/** Display --version flag */
async function printVersion() {
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8'));
const pkg = JSON.parse(await fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf8'));
console.error(pkg.version);
}
@ -111,63 +93,83 @@ function mergeCLIFlags(astroConfig: AstroConfig, flags: CLIState['options']) {
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
}
/** Handle `astro run` command */
async function runCommand(rawRoot: string, cmd: (a: AstroConfig, opts: any) => Promise<void>, options: CLIState['options']) {
try {
const projectRoot = options.projectRoot || rawRoot;
const astroConfig = await loadConfig(projectRoot, options.config);
mergeCLIFlags(astroConfig, options);
return cmd(astroConfig, options);
} catch (err: any) {
if (err instanceof z.ZodError) {
console.log(formatConfigError(err));
} else {
console.error(colors.red(err.toString()));
}
process.exit(1);
}
}
const cmdMap = new Map<string, (a: AstroConfig, opts?: any) => Promise<any>>([
['build', buildAndExit],
['dev', devServer],
['preview', preview],
['reload', reloadAndExit],
['check', checkAndExit],
]);
/** The primary CLI action */
export async function cli(args: string[]) {
const flags = yargs(args);
const state = resolveArgs(flags);
const options = { ...state.options };
const projectRoot = options.projectRoot || flags._[3];
// logLevel
let logging: LogOptions = {
dest: defaultLogDestination,
level: 'info',
};
if (flags.verbose) logging.level = 'debug';
if (flags.silent) logging.level = 'silent';
let config: AstroConfig;
try {
config = await loadConfig({ cwd: projectRoot, filename: options.config });
mergeCLIFlags(config, options);
} catch (err) {
if (err instanceof z.ZodError) {
console.log(formatConfigError(err));
} else {
console.error(colors.red((err as any).toString() || err));
}
process.exit(1);
}
switch (state.cmd) {
case 'help': {
printHelp();
process.exit(1);
break;
return;
}
case 'version': {
await printVersion();
process.exit(0);
break;
return;
}
case 'reload': {
await reloadAndExit();
break;
}
case 'build':
case 'preview':
case 'dev':
case 'check': {
if (flags.reload) {
await reload();
case 'dev': {
try {
const server = await devServer(config, { logging });
await new Promise(() => {}); // dont close dev server
} catch (err) {
throwAndExit(err);
}
const cmd = cmdMap.get(state.cmd);
if (!cmd) throw new Error(`Error running ${state.cmd}`);
runCommand(flags._[3], cmd, state.options);
break;
return;
}
case 'build': {
try {
await build(config, { logging });
process.exit(0);
} catch (err) {
throwAndExit(err);
}
return;
}
case 'check': {
const ret = await check(config);
process.exit(ret);
return;
}
case 'preview': {
try {
await preview(config, { logging }); // this will keep running
} catch (err) {
throwAndExit(err);
}
return;
}
default: {
throw new Error(`Error running ${state.cmd}`);
}
}
}
/** Display error and exit */
function throwAndExit(err: any) {
console.error(colors.red(err.toString() || err));
process.exit(1);
}

View file

@ -1,934 +0,0 @@
import type { Ast, Script, Style, TemplateNode, Expression } from '@astrojs/parser';
import type { CompileOptions } from '../../@types/compiler';
import type { AstroConfig, TransformResult, ComponentInfo, Components, ScriptInfo } from '../../@types/astro';
import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, ImportDefaultSpecifier } from '@babel/types';
import type { Attribute } from './interfaces';
import eslexer from 'es-module-lexer';
import esbuild from 'esbuild';
import path from 'path';
import astroParser from '@astrojs/parser';
import { walk, asyncWalk } from 'estree-walker';
import _babelGenerator from '@babel/generator';
import babelParser from '@babel/parser';
import { codeFrameColumns } from '@babel/code-frame';
import * as babelTraverse from '@babel/traverse';
import { error, warn, parseError } from '../../logger.js';
import { yellow } from 'kleur/colors';
import { isComponentTag, isCustomElementTag, positionAt } from '../utils.js';
import { warnIfRelativeStringLiteral } from './utils.js';
import { renderMarkdown } from '@astrojs/markdown-support';
import { camelCase } from 'camel-case';
import { transform } from '../transform/index.js';
import { PRISM_IMPORT } from '../transform/prism.js';
import { nodeBuiltinsSet } from '../../node_builtins.js';
import { readFileSync } from 'fs';
import { fileURLToPath, pathToFileURL } from 'url';
const { parse, FEATURE_CUSTOM_ELEMENT } = astroParser;
const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
// @ts-ignore
const babelGenerator: typeof _babelGenerator = _babelGenerator.default;
const { transformSync } = esbuild;
const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible', 'client:media', 'client:only']);
interface CodeGenOptions {
compileOptions: CompileOptions;
filename: string;
fileID: string;
}
interface HydrationAttributes {
method?: 'load' | 'idle' | 'visible' | 'media' | 'only';
value?: undefined | string;
}
/** Searches through attributes to extract hydration-rlated attributes */
function findHydrationAttributes(attrs: Record<string, string>): HydrationAttributes {
let method: HydrationAttributes['method'];
let value: undefined | string;
for (const [key, val] of Object.entries(attrs)) {
if (hydrationDirectives.has(key)) {
method = key.slice(7) as HydrationAttributes['method'];
value = val === 'true' ? undefined : val;
}
}
return { method, value };
}
/** Retrieve attributes from TemplateNode */
async function getAttributes(nodeName: string, attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> {
const isPage = state.filename.startsWith(fileURLToPath(compileOptions.astroConfig.pages));
let result: Record<string, string> = {};
for (const attr of attrs) {
if (attr.type === 'Spread') {
const code = await compileExpression(attr.expression as Expression, state, compileOptions);
if (code) {
result[`...(${code})`] = '';
}
continue;
}
if (attr.value === true) {
result[attr.name] = JSON.stringify(attr.value);
continue;
}
if (attr.value === false || attr.value === undefined) {
// note: attr.value shouldnt be `undefined`, but a bad transform would cause a compile error here, so prevent that
continue;
}
if (attr.value.length === 0) {
result[attr.name] = '""';
continue;
}
if (attr.value.length > 1) {
result[attr.name] =
'(' +
attr.value
.map((v: TemplateNode) => {
if (v.content) {
return v.content;
} else {
return JSON.stringify(getTextFromAttribute(v));
}
})
.join('+') +
')';
continue;
}
const val = attr.value[0];
if (!val) {
result[attr.name] = '(' + val + ')';
continue;
}
switch (val.type) {
case 'MustacheTag': {
const code = await compileExpression(val.expression, state, compileOptions);
if (code) {
result[attr.name] = '(' + code + ')';
}
continue;
}
case 'Text': {
let text = getTextFromAttribute(val);
if (!isPage) {
warnIfRelativeStringLiteral(compileOptions.logging, nodeName, attr, text);
}
result[attr.name] = JSON.stringify(text);
continue;
}
case 'AttributeShorthand':
result[attr.name] = '(' + attr.name + ')';
continue;
default:
throw new Error(`UNKNOWN: ${val.type}`);
}
}
return result;
}
/** Get value from a TemplateNode Attribute (text attributes only!) */
function getTextFromAttribute(attr: any): string {
switch (attr.type) {
case 'Text': {
if (attr.raw !== undefined) {
return attr.raw;
}
if (attr.data !== undefined) {
return attr.data;
}
break;
}
case 'MustacheTag': {
// FIXME: this won't work when JSX element can appear in attributes (rare but possible).
return attr.expression.codeChunks[0];
}
}
throw new Error(`Unknown attribute type ${attr.type}`);
}
/** Convert TemplateNode attributes to string */
function generateAttributes(attrs: Record<string, string>): string {
let result = '{';
for (const [key, val] of Object.entries(attrs)) {
if (hydrationDirectives.has(key)) {
continue;
} else if (key.startsWith('...')) {
result += key + ',';
} else {
result += JSON.stringify(key) + ':' + val + ',';
}
}
result += `[__astroContext]:$$props[__astroContext]`;
return result + '}';
}
function getComponentUrl(astroConfig: AstroConfig, url: string, parentUrl: string | URL) {
const componentExt = path.extname(url);
const ext = PlainExtensions.has(componentExt) ? '.js' : `${componentExt}.js`;
const outUrl = new URL(url, parentUrl);
return '/_astro/' + outUrl.href.replace(astroConfig.projectRoot.href, '').replace(/\.[^.]+$/, ext);
}
interface GetComponentWrapperOptions {
filename: string;
astroConfig: AstroConfig;
compileOptions: CompileOptions;
}
const PlainExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
/** Generate Astro-friendly component import */
function getComponentWrapper(_name: string, hydration: HydrationAttributes, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) {
const { astroConfig, filename, compileOptions } = opts;
let name = _name;
let method = hydration.method;
/** Legacy support for original hydration syntax */
if (name.indexOf(':') > 0) {
const [legacyName, legacyMethod] = _name.split(':');
name = legacyName;
method = legacyMethod as HydrationAttributes['method'];
const shortname = path.posix.relative(compileOptions.astroConfig.projectRoot.pathname, filename);
warn(compileOptions.logging, shortname, yellow(`Deprecation warning: Partial hydration now uses a directive syntax. Please update to "<${name} client:${method} />"`));
}
// Special flow for custom elements
if (isCustomElementTag(_name)) {
return {
wrapper: `__astro_component(...__astro_element_registry.astroComponentArgs("${name}", ${JSON.stringify({ hydrate: method, displayName: _name })}))`,
wrapperImports: [
`import {AstroElementRegistry} from 'astro/dist/internal/element-registry.js';`,
`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`,
],
};
} else {
const getComponentExport = () => {
switch (importSpecifier.type) {
case 'ImportDefaultSpecifier':
return { value: 'default' };
case 'ImportSpecifier': {
if (importSpecifier.imported.type === 'Identifier') {
return { value: importSpecifier.imported.name };
}
return { value: importSpecifier.imported.value };
}
case 'ImportNamespaceSpecifier': {
const [_, value] = _name.split('.');
return { value };
}
}
};
let metadata = '';
if (method) {
const componentUrl = getComponentUrl(astroConfig, url, pathToFileURL(filename));
const componentExport = getComponentExport();
metadata = `{ hydrate: "${method}", displayName: "${name}", componentUrl: "${componentUrl}", componentExport: ${JSON.stringify(componentExport)}, value: ${
hydration.value || 'null'
} }`;
// for client:only components, only render a Fragment on the server
if (method === 'only') {
name = 'Fragment';
}
} else {
metadata = `{ hydrate: undefined, displayName: "${name}", value: ${hydration.value || 'null'} }`;
}
return {
wrapper: `__astro_component(${name}, ${metadata})`,
wrapperImports: [`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`],
};
}
}
/**
* Convert an Expression Node to a string
*
* @param expression Expression Node to compile
* @param state CodegenState
* @param compileOptions CompileOptions
*/
async function compileExpression(node: Expression, state: CodegenState, compileOptions: CompileOptions) {
const children: string[] = await Promise.all((node.children ?? []).map((child) => compileHtml(child, state, compileOptions)));
let raw = '';
let nextChildIndex = 0;
for (const chunk of node.codeChunks) {
raw += chunk;
if (nextChildIndex < children.length) {
raw += children[nextChildIndex++];
}
}
const location = { start: node.start, end: node.end };
let code = transpileExpressionSafe('(' + raw + ')', { state, compileOptions, location });
if (code === null) throw new Error(`Unable to compile expression`);
code = code.trim().replace(/\;$/, '');
return code;
}
/** Evaluate expression (safely) */
function transpileExpressionSafe(
raw: string,
{ state, compileOptions, location }: { state: CodegenState; compileOptions: CompileOptions; location: { start: number; end: number } }
): string | null {
try {
let { code } = transformSync(raw, {
loader: 'tsx',
jsxFactory: 'h',
jsxFragment: 'Fragment',
charset: 'utf8',
});
return code;
} catch ({ errors }) {
const err = new Error() as any;
const e = errors[0];
err.filename = state.filename;
const text = readFileSync(state.filename).toString();
const start = positionAt(location.start, text);
start.line += e.location.line;
start.character += e.location.column + 1;
err.start = { line: start.line, column: start.character };
const end = { ...start };
end.character += e.location.length;
const frame = codeFrameColumns(text, {
start: { line: start.line, column: start.character },
end: { line: end.line, column: end.character },
});
err.frame = frame;
err.message = e.text;
parseError(compileOptions.logging, err);
return null;
}
}
interface CompileResult {
script: string;
getStaticPaths?: string;
}
interface CodegenState {
components: Components;
css: string[];
hoistedScripts: ScriptInfo[];
filename: string;
fileID: string;
markers: {
insideMarkdown: boolean | Record<string, any>;
};
declarations: Set<string>;
exportStatements: Set<string>;
importStatements: Set<string>;
componentImports: Map<string, string[]>;
customElementCandidates: Map<string, string>;
}
/** Compile/prepare Astro frontmatter scripts */
function compileModule(ast: Ast, module: Script, state: CodegenState, compileOptions: CompileOptions): CompileResult {
const { astroConfig } = compileOptions;
const { filename } = state;
const componentImports: ImportDeclaration[] = [];
const componentProps: VariableDeclarator[] = [];
const componentExports: ExportNamedDeclaration[] = [];
let script = '';
let propsStatement = '';
let getStaticPaths = ''; // function for executing collection
if (module) {
const parseOptions: babelParser.ParserOptions = {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'topLevelAwait', 'throwExpressions'],
};
let parseResult;
try {
parseResult = babelParser.parse(module.content, parseOptions);
} catch (err) {
const location = { start: err.loc };
const frame = codeFrameColumns(module.content, location);
err.frame = frame;
err.filename = state.filename;
err.start = err.loc;
throw err;
}
// Convert Astro.fetchContent() to use import.meta.glob
if (/Astro\s*\.\s*fetchContent/.test(module.content)) {
state.importStatements.add(`import {fetchContent} from 'astro/dist/internal/fetch-content.js';\n`);
traverse(parseResult, {
enter({ node }) {
if (
node.type !== 'CallExpression' ||
node.callee.type !== 'MemberExpression' ||
(node.callee.object as any).name !== 'Astro' ||
(node.callee.property as any).name !== 'fetchContent'
) {
return;
}
if (node.arguments[0].type !== 'StringLiteral') {
throw new Error(`[Astro.fetchContent] Only string literals allowed, ex: \`Astro.fetchContent('./post/*.md')\`\n ${state.filename}`);
}
// Replace `Astro.fetchContent(str)` with `Astro.fetchContent(import.meta.globEager(str))`
node.arguments = [
{
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'MetaProperty', meta: { type: 'Identifier', name: 'import' }, property: { type: 'Identifier', name: 'meta' } },
property: { type: 'Identifier', name: 'globEager' },
computed: false,
},
arguments: node.arguments,
},
] as any;
},
});
}
const program = parseResult.program;
const { body } = program;
let i = body.length;
while (--i >= 0) {
const node = body[i];
switch (node.type) {
// case 'ExportAllDeclaration':
// case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration': {
if (!node.declaration) break;
if (node.declaration.type === 'VariableDeclaration') {
// case 1: prop (export let title)
const declaration = node.declaration.declarations[0];
if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') {
componentExports.push(node);
} else {
componentProps.push(declaration);
}
} else if (node.declaration.type === 'FunctionDeclaration') {
// case 2: getStaticPaths (export async function)
if (!node.declaration.id || node.declaration.id.name !== 'getStaticPaths') break;
getStaticPaths = babelGenerator(node).code;
}
body.splice(i, 1);
break;
}
case 'FunctionDeclaration': {
if (node.id?.name) {
state.declarations.add(node.id?.name);
}
break;
}
case 'ImportDeclaration': {
componentImports.push(node);
body.splice(i, 1); // remove node
break;
}
case 'VariableDeclaration': {
// Support frontmatter-defined components
for (const declaration of node.declarations) {
if (declaration.id.type === 'Identifier') {
state.declarations.add(declaration.id.name);
}
}
break;
}
}
}
for (const componentImport of componentImports) {
const importUrl = componentImport.source.value;
if (nodeBuiltinsSet.has(importUrl)) {
throw new Error(`Node builtins must be prefixed with 'node:'. Use node:${importUrl} instead.`);
}
for (const specifier of componentImport.specifiers) {
const componentName = specifier.local.name;
state.components.set(componentName, {
importSpecifier: specifier,
url: importUrl,
});
if (!state.componentImports.has(componentName)) {
state.componentImports.set(componentName, []);
}
// Track component imports to be used for server-rendered components
const { start, end } = componentImport;
state.componentImports.get(componentName)?.push(module.content.slice(start || undefined, end || undefined));
}
const { start, end } = componentImport;
if (ast.meta.features & FEATURE_CUSTOM_ELEMENT && componentImport.specifiers.length === 0) {
// Add possible custom element, but only if the AST says there are custom elements.
const moduleImportName = camelCase(importUrl + 'Module');
state.importStatements.add(`import * as ${moduleImportName} from '${importUrl}';\n`);
state.customElementCandidates.set(moduleImportName, getComponentUrl(astroConfig, importUrl, pathToFileURL(filename)));
} else {
state.importStatements.add(module.content.slice(start || undefined, end || undefined));
}
}
// TODO: actually expose componentExports other than __layout and __content
for (const componentImport of componentExports) {
const { start, end } = componentImport;
state.exportStatements.add(module.content.slice(start || undefined, end || undefined));
}
if (componentProps.length > 0) {
const shortname = path.posix.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
const props = componentProps.map((prop) => (prop.id as Identifier)?.name).filter((v) => v);
warn(
compileOptions.logging,
shortname,
yellow(`\nDefining props with "export" has been removed! Please see https://github.com/snowpackjs/astro/blob/main/packages/astro/CHANGELOG.md#0150
Please update your code to use:
const { ${props.join(', ')} } = Astro.props;\n`)
);
}
script = propsStatement + babelGenerator(program).code;
const location = { start: module.start, end: module.end };
let transpiledScript = transpileExpressionSafe(script, { state, compileOptions, location });
if (transpiledScript === null) throw new Error(`Unable to compile script`);
script = transpiledScript;
}
return {
script,
getStaticPaths: getStaticPaths || undefined,
};
}
/** Compile styles */
function compileCss(style: Style, state: CodegenState) {
walk(style, {
enter(node: TemplateNode) {
if (node.type === 'Style') {
state.css.push(node.content.styles); // if multiple <style> tags, combine together
this.skip();
}
},
leave(node: TemplateNode) {
if (node.type === 'Style') {
this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined
}
},
});
}
/** dedent markdown */
function dedent(str: string) {
let arr = str.match(/^[ \t]*(?=\S)/gm);
let first = !!arr && arr.find((x) => x.length > 0)?.length;
return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), '');
}
const FALSY_EXPRESSIONS = new Set(['false', 'null', 'undefined', 'void 0']);
function isFrontmatterDefinedComponent(componentName: string, componentInfo: ComponentInfo | undefined, state: CodegenState) {
let hasVariableDeclaration = state.declarations.has(componentName);
let isNotImported = !componentInfo;
return hasVariableDeclaration && isNotImported;
}
function isFragmentComponent(componentName: string) {
return componentName === 'Fragment';
}
/** Compile page markup */
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
return new Promise((resolve) => {
const { components, css, importStatements, exportStatements, filename, fileID } = state;
const { astroConfig } = compileOptions;
let paren = -1;
let buffers = {
out: '',
markdown: '',
};
let curr: keyof typeof buffers = 'out';
/** renders markdown stored in `buffers.markdown` to JSX and pushes that to `buffers.out` */
async function pushMarkdownToBuffer() {
const md = buffers.markdown;
const { markdownOptions = {} } = astroConfig;
if (!md.trim()) {
buffers.out += ',' + md;
buffers.markdown = '';
curr = 'out';
return;
}
const { $scope: scopedClassName } = state.markers.insideMarkdown as Record<'$scope', any>;
let { content: rendered } = await renderMarkdown(dedent(md), {
...markdownOptions,
$: { scopedClassName: scopedClassName && scopedClassName.slice(1, -1) },
});
// 1. Parse
const ast = parse(rendered);
// 2. Transform the AST
await transform(ast, {
compileOptions,
filename,
fileID,
});
// 3. Codegen
const result = await compileHtml(ast.html, { ...state, markers: { insideMarkdown: false } }, compileOptions);
buffers.out += ',' + result;
buffers.markdown = '';
curr = 'out';
}
asyncWalk(enterNode, {
async enter(node: TemplateNode, parent: TemplateNode) {
switch (node.type) {
case 'Expression': {
let code = await compileExpression(node as Expression, state, compileOptions);
if (FALSY_EXPRESSIONS.has(code)) {
this.skip();
break;
}
if (code !== '') {
if (state.markers.insideMarkdown) {
buffers[curr] += `{${code}}`;
} else {
buffers[curr] += `,(${code})`;
}
}
this.skip();
break;
}
case 'MustacheTag':
if (state.markers.insideMarkdown) {
if (curr === 'out') curr = 'markdown';
}
return;
case 'Comment':
return;
case 'Fragment': {
buffers[curr] += `h(Fragment, null,`;
break;
}
case 'SlotTemplate': {
buffers[curr] += `h(Fragment, null, children`;
paren++;
return;
}
case 'Slot':
case 'Head':
case 'InlineComponent': {
switch (node.name) {
case 'Prism': {
if (!importStatements.has(PRISM_IMPORT)) {
importStatements.add(PRISM_IMPORT);
}
if (!components.has('Prism')) {
components.set('Prism', {
importSpecifier: {
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: 'Prism' } as Identifier,
} as ImportDefaultSpecifier,
url: 'astro/components/Prism.astro',
});
}
break;
}
}
// Do not break.
}
case 'Title':
case 'Element': {
const name: string = node.name;
if (!name) {
throw new Error('AHHHH');
}
try {
const attributes = await getAttributes(name, node.attributes, state, compileOptions);
const hydrationAttributes = findHydrationAttributes(attributes);
buffers.out += buffers.out === '' ? '' : ',';
if (node.type === 'Slot') {
state.importStatements.add(`import { __astro_slot } from 'astro/dist/internal/__astro_slot.js';`);
buffers[curr] += `h(__astro_slot, ${generateAttributes(attributes)}, children`;
paren++;
return;
}
if (!isComponentTag(name)) {
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
if (attributes.slot) {
state.importStatements.add(`import { __astro_slot_content } from 'astro/dist/internal/__astro_slot.js';`);
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
paren++;
}
if (attributes.hoist) {
if (attributes.src) {
state.hoistedScripts.push({
src: attributes.src.substr(1, attributes.src.length - 2),
});
} else if (node.children && node.children.length === 1 && node.children[0].type === 'Text') {
state.hoistedScripts.push({
content: node.children[0].data,
});
}
this.skip();
return;
}
buffers[curr] += `h("${name}", ${generateAttributes(attributes)},`;
paren++;
return;
}
const [componentName, _componentKind] = name.split(':');
let componentInfo = components.get(componentName);
if (/\./.test(componentName)) {
const [componentNamespace] = componentName.split('.');
componentInfo = components.get(componentNamespace);
}
if ((isFrontmatterDefinedComponent(componentName, componentInfo, state) && !isCustomElementTag(componentName)) || isFragmentComponent(componentName)) {
if (hydrationAttributes.method) {
throw new Error(
`Unable to hydrate "${componentName}" because it is statically defined in the frontmatter script. Hydration directives may only be used on imported components.`
);
}
// Previously we would throw here, but this is valid!
// If the frontmatter script defines `const Element = 'h1'`,
// you should be able to statically render `<Element>`
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
if (attributes.slot) {
state.importStatements.add(`import { __astro_slot_content } from 'astro/dist/internal/__astro_slot.js';`);
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
paren++;
}
buffers[curr] += `h(${componentName}, ${generateAttributes(attributes)},`;
paren++;
return;
} else if (!componentInfo && !isCustomElementTag(componentName)) {
throw new Error(`Unable to render "${componentName}" because it is undefined\n ${state.filename}`);
}
if (componentName === 'Markdown') {
const { $scope } = attributes ?? {};
state.markers.insideMarkdown = typeof state.markers.insideMarkdown === 'object' ? { $scope, count: state.markers.insideMarkdown.count + 1 } : { $scope, count: 1 };
const keys = Object.keys(attributes).filter((attr) => attr !== '$scope');
if (keys.length > 0) {
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
buffers[curr] += `,${componentName}.__render(${generateAttributes(attributes)}),`;
}
curr = 'markdown';
return;
}
const { wrapper, wrapperImports } = getComponentWrapper(name, hydrationAttributes, componentInfo ?? ({} as any), { astroConfig, filename, compileOptions });
if (wrapperImports) {
for (let wrapperImport of wrapperImports) {
importStatements.add(wrapperImport);
}
}
if (hydrationAttributes.method === 'only') {
// Remove component imports for client-only components
const componentImports = state.componentImports.get(componentName) || [];
componentImports.map((componentImport) => state.importStatements.delete(componentImport));
}
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
if (attributes.slot) {
state.importStatements.add(`import { __astro_slot_content } from 'astro/dist/internal/__astro_slot.js';`);
buffers[curr] += `h(__astro_slot_content, { name: ${attributes.slot} },`;
paren++;
}
paren++;
buffers[curr] += `h(${wrapper}, ${generateAttributes(attributes)}`;
} catch (err) {
paren--;
// handle errors in scope with filename
const rel = filename.replace(astroConfig.projectRoot.pathname, '');
// TODO: return actual codeframe here
error(compileOptions.logging, rel, err.toString());
}
return;
}
case 'Attribute':
case 'Spread': {
this.skip();
return;
}
case 'Style': {
css.push(node.content.styles); // if multiple <style> tags, combine together
this.skip();
return;
}
case 'CodeSpan':
case 'CodeFence': {
if (state.markers.insideMarkdown) {
if (curr === 'out') curr = 'markdown';
buffers[curr] += node.raw;
return;
}
buffers[curr] += ',' + JSON.stringify(node.data);
return;
}
case 'Text': {
let text = getTextFromAttribute(node);
if (state.markers.insideMarkdown) {
if (curr === 'out') curr = 'markdown';
buffers[curr] += text;
return;
}
if (parent.name !== 'Markdown' && !text.trim()) {
return;
}
if (parent.name === 'code') {
// Special case, escaped { characters from markdown content
text = node.raw.replace(/ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0/g, '{');
}
buffers[curr] += ',' + JSON.stringify(text);
return;
}
default:
throw new Error('Unexpected (enter) node type: ' + node.type);
}
},
async leave(node, parent, prop, index) {
switch (node.type) {
case 'Fragment': {
if (curr === 'markdown') curr = 'out';
buffers[curr] += `)`;
break;
}
case 'Text':
case 'Attribute':
case 'Spread':
case 'Comment':
case 'Expression':
case 'MustacheTag':
case 'CodeSpan':
case 'CodeFence':
return;
case 'SlotTemplate':
case 'Slot':
case 'Head':
case 'Body':
case 'Title':
case 'Element': {
if (curr === 'markdown') {
await pushMarkdownToBuffer();
}
if (node.attributes.find((attr: any) => attr.name === 'slot')) {
buffers.out += ')';
paren--;
}
if (paren !== -1) {
buffers.out += ')';
paren--;
}
return;
}
case 'InlineComponent': {
if (node.name === 'Markdown') {
(state.markers.insideMarkdown as Record<string, any>).count--;
if ((state.markers.insideMarkdown as Record<string, any>).count <= 0) {
state.markers.insideMarkdown = false;
}
const hasAttrs = node.attributes.filter(({ name }: Attribute) => name !== '$scope').length > 0;
if (hasAttrs) {
return;
}
}
if (curr === 'markdown' && buffers.markdown !== '') {
await pushMarkdownToBuffer();
if (!state.markers.insideMarkdown) {
return;
}
}
if (node.attributes.find((attr: any) => attr.name === 'slot')) {
buffers.out += ')';
paren--;
}
if (paren !== -1) {
buffers.out += ')';
paren--;
}
return;
}
case 'Style': {
this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined
return;
}
default:
throw new Error('Unexpected (leave) node type: ' + node.type);
}
},
}).then(() => {
const content = buffers.out.replace(/^\,/, '').replace(/\,\)/g, ')').replace(/\,+/g, ',').replace(/\)h/g, '),h');
buffers.out = '';
buffers.markdown = '';
return resolve(content);
});
});
}
/**
* Codegen
* Step 3/3 in Astro SSR.
* This is the final pass over a document AST before its converted to an h() function
* and handed off to Snowpack to build.
* @param {Ast} AST The parsed AST to crawl
* @param {object} CodeGenOptions
*/
export async function codegen(ast: Ast, { compileOptions, filename, fileID }: CodeGenOptions): Promise<TransformResult> {
await eslexer.init;
const state: CodegenState = {
filename,
fileID,
components: new Map(),
css: [],
hoistedScripts: [],
markers: {
insideMarkdown: false,
},
declarations: new Set(),
importStatements: new Set(),
exportStatements: new Set(),
componentImports: new Map(),
customElementCandidates: new Map(),
};
const { script, getStaticPaths } = compileModule(ast, ast.module, state, compileOptions);
(ast.css || []).map((css) => compileCss(css, state));
const html = await compileHtml(ast.html, state, compileOptions);
return {
script: script,
imports: Array.from(state.importStatements),
exports: Array.from(state.exportStatements),
html,
css: state.css.length ? state.css.join('\n\n') : undefined,
hoistedScripts: state.hoistedScripts,
components: Array.from(state.components.keys()),
getStaticPaths,
hasCustomElements: Boolean(ast.meta.features & FEATURE_CUSTOM_ELEMENT),
customElementCandidates: state.customElementCandidates,
};
}

View file

@ -1,10 +0,0 @@
import type { Expression, TemplateNode } from '@astrojs/parser';
export interface Attribute {
start: number;
end: number;
type: 'Attribute' | 'Spread';
name: string;
value: TemplateNode[] | boolean;
expression?: Expression;
}

View file

@ -1,35 +0,0 @@
/**
* Codegen utils
*/
import type { VariableDeclarator, CallExpression } from '@babel/types';
import type { Attribute } from './interfaces';
import type { LogOptions } from '../../logger';
import { warn } from '../../logger.js';
/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */
export function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean {
let { init } = declaration;
if (!init) return false; // definitely not import.meta
// this could be `await import.meta`; if so, evaluate that:
if (init.type === 'AwaitExpression') {
init = init.argument;
}
// continue evaluating
if (init.type !== 'CallExpression' || init.callee.type !== 'MemberExpression' || init.callee.object.type !== 'MetaProperty') return false;
// optional: if metaName specified, match that
if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false;
return true;
}
const warnableRelativeValues = new Set(['img+src', 'a+href', 'script+src', 'link+href', 'source+srcset']);
const matchesRelative = /^(?![A-Za-z][+-.0-9A-Za-z]*:|\/|#)/;
export function warnIfRelativeStringLiteral(logging: LogOptions, nodeName: string, attr: Attribute, value: string) {
let key = nodeName + '+' + attr.name;
if (warnableRelativeValues.has(key) && matchesRelative.test(value)) {
let message = `This value will be resolved relative to the page: <${nodeName} ${attr.name}="${value}">`;
warn(logging, 'relative-link', message);
}
}

View file

@ -1,258 +0,0 @@
import type { CompileResult, TransformResult } from '../@types/astro';
import type { CompileOptions } from '../@types/compiler.js';
import path from 'path';
import { MarkdownRenderingOptions, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support';
import { parse } from '@astrojs/parser';
import { transform } from './transform/index.js';
import { codegen } from './codegen/index.js';
export { scopeRule } from './transform/postcss-scoped-styles/index.js';
interface ConvertAstroOptions {
compileOptions: CompileOptions;
filename: string;
fileID: string;
}
/**
* .astro -> .jsx
* Core function processing .astro files. Initiates all 3 phases of compilation:
* 1. Parse
* 2. Transform
* 3. Codegen
*/
export async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
const { filename } = opts;
// 1. Parse
const ast = parse(template, {
filename,
});
// 2. Transform the AST
await transform(ast, opts);
// 3. Turn AST into JSX
return await codegen(ast, opts);
}
/**
* .md -> .astro source
*/
export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, opts?: MarkdownRenderingOptions): Promise<string> {
let {
content,
frontmatter: { layout, ...frontmatter },
...data
} = await renderMarkdownWithFrontmatter(contents, opts);
if (frontmatter['astro'] !== undefined) {
throw new Error(`"astro" is a reserved word but was used as a frontmatter value!\n\tat ${filename}`);
}
const contentData: any = {
...frontmatter,
...data,
};
// </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails.
// Break it up here so that the HTML parser won't detect it.
const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`);
return `---
${layout ? `import {__renderPage as __layout} from '${layout}';` : 'const __layout = undefined;'}
export const __content = ${stringifiedSetupContext};
---
${content}`;
}
/**
* .md -> .jsx
* Core function processing Markdown, but along the way also calls convertAstroToJsx().
*/
async function convertMdToJsx(
contents: string,
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
): Promise<TransformResult> {
const raw = await convertMdToAstroSource(contents, { filename }, compileOptions.astroConfig.markdownOptions);
const convertOptions = { compileOptions, filename, fileID };
return await convertAstroToJsx(raw, convertOptions);
}
/** Given a file, process it either as .astro, .md */
async function transformFromSource(
contents: string,
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
): Promise<TransformResult> {
const fileID = path.relative(projectRoot, filename);
switch (true) {
case filename.slice(-6) === '.astro':
return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
case filename.slice(-3) === '.md':
return await convertMdToJsx(contents, { compileOptions, filename, fileID });
default:
throw new Error('Not Supported!');
}
}
/** Return internal code that gets processed in Snowpack */
interface CompileComponentOptions {
compileOptions: CompileOptions;
filename: string;
projectRoot: string;
isPage?: boolean;
}
/** Compiles an Astro component */
export async function compileComponent(source: string, { compileOptions, filename, projectRoot }: CompileComponentOptions): Promise<CompileResult> {
const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
const { mode } = compileOptions;
const { hostname, port } = compileOptions.astroConfig.devOptions;
const devSite = `http://${hostname}:${port}`;
const site = compileOptions.astroConfig.buildOptions.site || devSite;
const fileID = path.join('/_astro', path.relative(projectRoot, filename));
const fileURL = new URL('.' + fileID, mode === 'production' ? site : devSite);
// return template
let moduleJavaScript = `
import fetch from 'node-fetch';
${result.imports.join('\n')}
if(!('fetch' in globalThis)) {
globalThis.fetch = fetch;
}
${/* Global Astro Namespace (shadowed & extended by the scoped namespace inside of __render()) */ ''}
const __TopLevelAstro = {
site: new URL(${JSON.stringify(site)}),
fetchContent: (globResult) => fetchContent(globResult, import.meta.url),
resolve(...segments) {
return segments.reduce(
(url, segment) => new URL(segment, url),
new URL(${JSON.stringify(fileURL)})
).pathname
},
};
const Astro = __TopLevelAstro;
${
result.hasCustomElements
? `
const __astro_element_registry = new AstroElementRegistry({
candidates: new Map([${Array.from(result.customElementCandidates)
.map(([identifier, url]) => `[${identifier}, '${url}']`)
.join(', ')}])
});
`.trim()
: ''
}
${result.getStaticPaths || ''}
// \`__render()\`: Render the contents of the Astro module.
import { h, Fragment } from 'astro/dist/internal/h.js';
import { __astro_hoisted_scripts } from 'astro/dist/internal/__astro_hoisted_scripts.js';
const __astroScripts = __astro_hoisted_scripts([${result.components.map((n) => `typeof ${n} !== 'undefined' && ${n}`)}], ${JSON.stringify(result.hoistedScripts)});
const __astroInternal = Symbol('astro.internal');
const __astroContext = Symbol.for('astro.context');
const __astroSlotted = Symbol.for('astro.slotted');
async function __render($$props, ...children) {
const Astro = Object.create(__TopLevelAstro, {
props: {
value: $$props,
enumerable: true
},
slots: {
value: children.reduce(
(slots, child) => {
for (let name in child.$slots) {
slots[name] = Boolean(child.$slots[name])
}
return slots
},
{}
),
enumerable: true
},
pageCSS: {
value: ($$props[__astroContext] && $$props[__astroContext].pageCSS) || [],
enumerable: true
},
pageScripts: {
value: ($$props[__astroContext] && $$props[__astroContext].pageScripts) || [],
enumerable: true
},
isPage: {
value: ($$props[__astroInternal] && $$props[__astroInternal].isPage) || false,
enumerable: true
},
request: {
value: ($$props[__astroContext] && $$props[__astroContext].request) || {},
enumerable: true
},
});
${result.script}
return h(Fragment, null, ${result.html});
}
export default { isAstroComponent: true, __render, [Symbol.for('astro.hoistedScripts')]: __astroScripts };
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL.
export async function __renderPage({request, children, props: $$props, css, scripts}) {
const currentChild = {
isAstroComponent: true,
layout: typeof __layout === 'undefined' ? undefined : __layout,
content: typeof __content === 'undefined' ? undefined : __content,
__render,
};
const isLayout = (__astroContext in $$props);
if(!isLayout) {
let astroRootUIDCounter = 0;
Object.defineProperty($$props, __astroContext, {
value: {
pageCSS: css,
request,
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
pageScripts: scripts,
},
writable: false,
enumerable: false
});
}
Object.defineProperty($$props, __astroInternal, {
value: {
isPage: !isLayout
},
writable: false,
enumerable: false
});
const childBodyResult = await currentChild.__render($$props, children);
// find layout, if one was given.
if (currentChild.layout) {
return currentChild.layout({
request,
props: {content: currentChild.content, [__astroContext]: $$props[__astroContext]},
children: [childBodyResult],
});
}
return childBodyResult;
};
${result.exports.join('\n')}
`;
return {
result,
contents: moduleJavaScript,
css: result.css,
};
}

View file

@ -1,35 +0,0 @@
import { Transformer } from '../../@types/transformer';
/** Transform <!doctype> tg */
export default function (_opts: { filename: string; fileID: string }): Transformer {
let hasDoctype = false;
return {
visitors: {
html: {
Element: {
enter(node, parent, _key, index) {
if (node.name.toLowerCase() === '!doctype') {
hasDoctype = true;
}
if (node.name === 'html' && !hasDoctype) {
const dtNode = {
start: 0,
end: 0,
attributes: [{ type: 'Attribute', name: 'html', value: true, start: 0, end: 0 }],
children: [],
name: '!doctype',
type: 'Element',
};
(parent.children || []).splice(index, 0, dtNode);
hasDoctype = true;
}
},
},
},
},
async finalize() {
// Nothing happening here.
},
};
}

View file

@ -1,285 +0,0 @@
import type { Transformer, TransformOptions } from '../../@types/transformer';
import type { TemplateNode } from '@astrojs/parser';
import { EndOfHead } from './util/end-of-head.js';
/** If there are hydrated components, inject styles for [data-astro-root] and [data-astro-children] */
export default function (opts: TransformOptions): Transformer {
let hasComponents = false;
let isHmrEnabled = typeof opts.compileOptions.hmrPort !== 'undefined' && opts.compileOptions.mode === 'development';
const eoh = new EndOfHead();
return {
visitors: {
html: {
Fragment: {
enter(node) {
eoh.enter(node);
},
leave(node) {
eoh.leave(node);
},
},
InlineComponent: {
enter(node) {
if (hasComponents) {
return;
}
// Initialize eoh if there are no elements
eoh.enter(node);
if (node.attributes && node.attributes.some(({ name }: any) => name?.startsWith('client:'))) {
hasComponents = true;
return;
}
/** Check for legacy hydration */
const [_name, kind] = node.name.split(':');
if (kind) {
hasComponents = true;
}
},
leave(node) {
eoh.leave(node);
},
},
Element: {
enter(node) {
eoh.enter(node);
},
leave(node) {
eoh.leave(node);
},
},
},
},
async finalize() {
const children = [];
/**
* Injects an expression that adds link tags for provided css.
* Turns into:
* ```
* { Astro.css.map(css => (
* <link rel="stylesheet" href={css}>
* ))}
* ```
*/
children.push({
start: 0,
end: 0,
type: 'Fragment',
children: [
{
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['Astro.pageCSS.map(css => (', '))'],
children: [
{
type: 'Element',
name: 'link',
attributes: [
{
name: 'rel',
type: 'Attribute',
value: [
{
type: 'Text',
raw: 'stylesheet',
data: 'stylesheet',
},
],
},
{
name: 'href',
type: 'Attribute',
value: [
{
start: 0,
end: 0,
type: 'MustacheTag',
expression: {
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['css'],
children: [],
},
},
],
},
],
start: 0,
end: 0,
children: [],
},
],
},
{
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['Astro.pageScripts.map(script => (', '))'],
children: [
{
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['script.src ? (', ') : (', ')'],
children: [
{
type: 'Element',
name: 'script',
attributes: [
{
type: 'Attribute',
name: 'type',
value: [
{
type: 'Text',
raw: 'module',
data: 'module',
},
],
},
{
type: 'Attribute',
name: 'src',
value: [
{
start: 0,
end: 0,
type: 'MustacheTag',
expression: {
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['script.src'],
children: [],
},
},
],
},
{
type: 'Attribute',
name: 'data-astro',
value: [
{
type: 'Text',
raw: 'hoist',
data: 'hoist',
},
],
},
],
start: 0,
end: 0,
children: [],
},
{
type: 'Element',
name: 'script',
attributes: [
{
type: 'Attribute',
name: 'type',
value: [
{
type: 'Text',
raw: 'module',
data: 'module',
},
],
},
{
type: 'Attribute',
name: 'data-astro',
value: [
{
type: 'Text',
raw: 'hoist',
data: 'hoist',
},
],
},
],
start: 0,
end: 0,
children: [
{
start: 0,
end: 0,
type: 'MustacheTag',
expression: {
start: 0,
end: 0,
type: 'Expression',
codeChunks: ['script.content'],
children: [],
},
},
],
},
],
},
],
},
],
});
if (hasComponents) {
children.push({
type: 'Element',
name: 'style',
attributes: [{ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }],
start: 0,
end: 0,
children: [
{
start: 0,
end: 0,
type: 'Text',
data: 'astro-root, astro-fragment { display: contents; }',
raw: 'astro-root, astro-fragment { display: contents; }',
},
],
});
}
if (isHmrEnabled) {
const { hmrPort } = opts.compileOptions;
children.push(
{
type: 'Element',
name: 'script',
attributes: [],
children: [{ type: 'Text', data: `window.HMR_WEBSOCKET_PORT = ${hmrPort};`, start: 0, end: 0 }],
start: 0,
end: 0,
},
{
type: 'Element',
name: 'script',
attributes: [
{ type: 'Attribute', name: 'type', value: [{ type: 'Text', data: 'module', start: 0, end: 0 }], start: 0, end: 0 },
{ type: 'Attribute', name: 'src', value: [{ type: 'Text', data: '/_snowpack/hmr-client.js', start: 0, end: 0 }], start: 0, end: 0 },
],
children: [],
start: 0,
end: 0,
}
);
}
if (eoh.foundHeadOrHtmlElement || eoh.foundHeadAndBodyContent) {
const topLevelFragment = {
start: 0,
end: 0,
type: 'Fragment',
children,
};
eoh.append(topLevelFragment);
}
},
};
}

View file

@ -1,99 +0,0 @@
import type { Ast, TemplateNode } from '@astrojs/parser';
import type { NodeVisitor, TransformOptions, Transformer, VisitorFn } from '../../@types/transformer';
import { walk } from 'estree-walker';
// Transformers
import transformStyles from './styles.js';
import transformDoctype from './doctype.js';
import transformModuleScripts from './module-scripts.js';
import transformCodeBlocks from './prism.js';
import transformHead from './head.js';
interface VisitorCollection {
enter: Map<string, VisitorFn[]>;
leave: Map<string, VisitorFn[]>;
}
/** Add visitors to given collection */
function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') {
if (typeof visitor[event] !== 'function') return;
if (!collection[event]) collection[event] = new Map<string, VisitorFn[]>();
const visitors = collection[event].get(nodeName) || [];
visitors.push(visitor[event] as any);
collection[event].set(nodeName, visitors);
}
/** Compile visitor actions from transformer */
function collectVisitors(transformer: Transformer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
if (transformer.visitors) {
if (transformer.visitors.html) {
for (const [nodeName, visitor] of Object.entries(transformer.visitors.html)) {
addVisitor(visitor, htmlVisitors, nodeName, 'enter');
addVisitor(visitor, htmlVisitors, nodeName, 'leave');
}
}
if (transformer.visitors.css) {
for (const [nodeName, visitor] of Object.entries(transformer.visitors.css)) {
addVisitor(visitor, cssVisitors, nodeName, 'enter');
addVisitor(visitor, cssVisitors, nodeName, 'leave');
}
}
}
finalizers.push(transformer.finalize);
}
/** Utility for formatting visitors */
function createVisitorCollection() {
return {
enter: new Map<string, VisitorFn[]>(),
leave: new Map<string, VisitorFn[]>(),
};
}
/** Walk AST with collected visitors */
function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) {
walk(tmpl, {
enter(node, parent, key, index) {
if (collection.enter.has(node.type)) {
const fns = collection.enter.get(node.type) || [];
for (let fn of fns) {
fn.call(this, node, parent, key, index);
}
}
},
leave(node, parent, key, index) {
if (collection.leave.has(node.type)) {
const fns = collection.leave.get(node.type) || [];
for (let fn of fns) {
fn.call(this, node, parent, key, index);
}
}
},
});
}
/**
* Transform
* Step 2/3 in Astro SSR.
* Transform is the point at which we mutate the AST before sending off to
* Codegen, and then to Snowpack. In some ways, its a preprocessor.
*/
export async function transform(ast: Ast, opts: TransformOptions) {
const htmlVisitors = createVisitorCollection();
const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = [];
const optimizers = [transformHead(opts), transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
for (const optimizer of optimizers) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
}
(ast.css || []).map((css) => walkAstWithVisitors(css, cssVisitors));
walkAstWithVisitors(ast.html, htmlVisitors);
// Run all of the finalizer functions in parallel because why not.
await Promise.all(finalizers.map((fn) => fn()));
}

View file

@ -1,55 +0,0 @@
import type { Transformer } from '../../@types/transformer';
import type { CompileOptions } from '../../@types/compiler';
import { getAttrValue, setAttrValue } from '../../ast.js';
/** Transform <script type="module"> */
export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Transformer {
const { astroConfig } = compileOptions;
const fileUrl = new URL(`file://${filename}`);
return {
visitors: {
html: {
Element: {
enter(node) {
let name = node.name;
if (name !== 'script') {
return;
}
let type = getAttrValue(node.attributes, 'type');
if (type !== 'module') {
return;
}
let src = getAttrValue(node.attributes, 'src');
// scenario 1: if missing "src", ignore
if (!src) {
return;
}
// scenario 2: if absolute path, ignore
if (src.startsWith('/')) {
return;
}
// scenario 3: if remote URL, ignore
try {
new URL(src); // if this succeeds, this is a complete, valid URL
return;
} catch (err) {
// do nothing
}
const srcUrl = new URL(src, fileUrl);
const absoluteUrl = `/_astro/${srcUrl.href.replace(astroConfig.projectRoot.href, '')}`;
setAttrValue(node.attributes, 'src', absoluteUrl);
},
},
},
},
async finalize() {},
};
}

View file

@ -1,140 +0,0 @@
import { Plugin } from 'postcss';
interface AstroScopedOptions {
className: string;
}
interface Selector {
start: number;
end: number;
value: string;
}
const GLOBAL = ':global('; // custom function to prevent scoping
const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']);
const KEYFRAME_PERCENT = /\d+\.?\d*%/;
/** minify selector CSS */
function minifySelector(selector: string): string {
return selector.replace(/(\r?\n|\s)+/g, ' ').replace(/\s*(,|\+|>|~|\(|\))\s*/g, '$1');
}
/** find matching paren */
function matchParen(search: string, start: number): number {
if (search[start] !== '(') return -1;
let parenCount = 0;
for (let n = start + 1; n < search.length; n++) {
if (search[n] === ')' && parenCount === 0) return n;
if (search[n] === '(') parenCount += 1;
if (search[n] === ')') parenCount -= 1;
}
return -1;
}
/** HTML tags that should never get scoped classes */
export const NEVER_SCOPED_TAGS = new Set<string>(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']);
/**
* Scope Rules
* Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`)
* @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax).
* @param {string} className The CSS class to apply.
*/
export function scopeRule(selector: string, className: string) {
// if this is a keyframe keyword, return original selector
if (selector === 'from' || selector === 'to' || KEYFRAME_PERCENT.test(selector)) {
return selector;
}
// sanitize & normalize
const input = minifySelector(selector);
// For everything else, parse & scope
const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.'
const selectors: Selector[] = [];
let ss = input; // sanitize
// Pass 1: parse selector string; extract top-level selectors
{
let start = 0;
let lastValue = '';
let parenCount = 0;
for (let n = 0; n < ss.length; n++) {
const isEnd = n === input.length - 1;
if (input[n] === '(') parenCount += 1;
if (input[n] === ')') parenCount -= 1;
if (isEnd || (parenCount === 0 && CSS_SEPARATORS.has(input[n]))) {
lastValue = input.substring(start, isEnd ? undefined : n);
if (!lastValue) continue;
selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue });
start = n + 1;
}
}
}
// Pass 2: starting from end, transform selectors w/ scoped class
for (let i = selectors.length - 1; i >= 0; i--) {
const { start, end } = selectors[i];
let value = selectors[i].value;
const head = ss.substring(0, start);
const tail = ss.substring(end);
// leave :global() alone!
if (value.includes(GLOBAL)) {
let withoutGlobal = value;
// :global() may appear multiple times; if so, extract contents of each and combine
while (withoutGlobal.includes(GLOBAL)) {
const globalStart = withoutGlobal.indexOf(GLOBAL);
const globalParenOpen = globalStart + GLOBAL.length - 1;
const globalEnd = matchParen(withoutGlobal, globalParenOpen);
const globalContents = withoutGlobal.substring(globalParenOpen + 1, globalEnd);
withoutGlobal = withoutGlobal.substring(0, globalStart) + globalContents + withoutGlobal.substring(globalEnd + 1);
}
ss = head + withoutGlobal + tail;
continue;
}
// replace '*' with scoped class
if (value.includes('*')) {
ss = head + value.replace(/\*/g, c) + tail;
continue;
}
// dont scope body, title, etc.
if (CSS_SEPARATORS.has(value) || NEVER_SCOPED_TAGS.has(value)) {
ss = head + value + tail;
continue;
}
// scope everything else (place class just before any pseudoclasses)
let pseudoclassStart = -1;
for (let n = 0; n < value.length; n++) {
// note: CSS may allow backslash-escaped colons, which does not count as a pseudoclass
if (value[n] === ':' && value[n - 1] !== '\\') {
pseudoclassStart = n;
break;
}
}
if (pseudoclassStart !== -1) {
ss = head + value.substring(0, pseudoclassStart) + c + value.substring(pseudoclassStart) + tail;
} else {
ss = head + value + c + tail;
}
}
return ss;
}
/** PostCSS Scope plugin */
export default function astroScopedStyles(options: AstroScopedOptions): Plugin {
const rulesScopedCache = new WeakSet();
return {
postcssPlugin: '@astrojs/postcss-scoped-styles',
Rule(rule) {
if (!rulesScopedCache.has(rule)) {
rule.selector = scopeRule(rule.selector, options.className);
rulesScopedCache.add(rule);
}
},
};
}

View file

@ -1,123 +0,0 @@
import type { Transformer } from '../../@types/transformer';
import type { Script, TemplateNode } from '@astrojs/parser';
import { getAttrValue } from '../../ast.js';
export const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';`;
const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]/;
/** escaping code samples that contain template string replacement parts, ${foo} or example. */
function escape(code: string) {
return code
.replace(/[`$]/g, (match) => {
return '\\' + match;
})
.replace(/ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0/g, '{');
}
/** Unescape { characters transformed by Markdown generation */
function unescapeCode(code: TemplateNode) {
code.children = code.children?.map((child) => {
if (child.type === 'Text') {
return { ...child, raw: child.raw.replace(/ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0/g, '{') };
}
return child;
});
}
/** default export - Transform prism */
export default function (module: Script): Transformer {
let usesPrism = false;
return {
visitors: {
html: {
Element: {
enter(node) {
if (node.name === 'code') {
unescapeCode(node);
return;
}
if (node.name !== 'pre') return;
const codeEl = node.children && node.children[0];
if (!codeEl || codeEl.name !== 'code') return;
const className = getAttrValue(codeEl.attributes, 'class') || '';
const classes = className.split(' ');
let lang: string | undefined;
for (let cn of classes) {
const matches = /language-(.+)/.exec(cn);
if (matches) {
lang = matches[1];
break;
}
}
if (!lang) return;
let classesWithoutLang = classes.filter((cn) => cn !== `language-${lang}`);
let codeData = codeEl.children && codeEl.children[0];
if (!codeData) return;
let code = codeData.data as string;
const repl = {
start: 0,
end: 0,
type: 'InlineComponent',
name: 'Prism',
attributes: [
{
type: 'Attribute',
name: 'lang',
value: [
{
type: 'Text',
raw: lang,
data: lang,
},
],
},
{
type: 'Attribute',
name: 'class',
value: [
{
type: 'Text',
raw: classesWithoutLang.join(' '),
data: classesWithoutLang.join(' '),
},
],
},
{
type: 'Attribute',
name: 'code',
value: [
{
type: 'MustacheTag',
expression: {
type: 'Expression',
codeChunks: ['`' + escape(code) + '`'],
children: [],
},
},
],
},
],
children: [],
};
this.replace(repl);
usesPrism = true;
},
},
},
},
async finalize() {
// Add the Prism import if needed.
if (usesPrism && module && !prismImportExp.test(module.content)) {
module.content = PRISM_IMPORT + '\n' + module.content;
}
},
};
}

View file

@ -1,309 +0,0 @@
import type { TransformOptions, Transformer } from '../../@types/transformer';
import type { TemplateNode } from '@astrojs/parser';
import crypto from 'crypto';
import { createRequire } from 'module';
import path from 'path';
import autoprefixer from 'autoprefixer';
import postcss, { Plugin } from 'postcss';
import postcssKeyframes from 'postcss-icss-keyframes';
import findUp from 'find-up';
import sass from 'sass';
import { error, LogOptions } from '../../logger.js';
import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js';
import slash from 'slash';
type StyleType = 'css' | 'scss' | 'sass' | 'postcss';
declare global {
interface ImportMeta {
/** https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent */
resolve(specifier: string, parent?: string): Promise<any>;
}
}
const getStyleType: Map<string, StyleType> = new Map([
['.css', 'css'],
['.pcss', 'postcss'],
['.sass', 'sass'],
['.scss', 'scss'],
['css', 'css'],
['sass', 'sass'],
['scss', 'scss'],
['text/css', 'css'],
['text/sass', 'sass'],
['text/scss', 'scss'],
]);
/** Should be deterministic, given a unique filename */
function hashFromFilename(filename: string): string {
const hash = crypto.createHash('sha256');
return hash
.update(slash(filename))
.digest('base64')
.toString()
.replace(/[^A-Za-z0-9-]/g, '')
.substr(0, 8);
}
export interface StyleTransformResult {
css: string;
type: StyleType;
}
interface StylesMiniCache {
nodeModules: Map<string, string>; // filename: node_modules location
}
/** Simple cache that only exists in memory per-run. Prevents the same lookups from happening over and over again within the same build or dev server session. */
const miniCache: StylesMiniCache = {
nodeModules: new Map<string, string>(),
};
export interface TransformStyleOptions {
logging: LogOptions;
type?: string;
filename: string;
scopedClass: string;
tailwindConfig?: string;
global?: boolean;
}
/** given a class="" string, does it contain a given class? */
function hasClass(classList: string, className: string): boolean {
if (!className) return false;
for (const c of classList.split(' ')) {
if (className === c.trim()) return true;
}
return false;
}
/** Convert styles to scoped CSS */
async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig, global }: TransformStyleOptions): Promise<StyleTransformResult> {
let styleType: StyleType = 'css'; // important: assume CSS as default
if (type) {
styleType = getStyleType.get(type) || styleType;
}
// add file path to includePaths
let includePaths: string[] = [path.dirname(filename)];
// include node_modules to includePaths (allows @use-ing node modules, if it can be located)
const cachedNodeModulesDir = miniCache.nodeModules.get(filename);
if (cachedNodeModulesDir) {
includePaths.push(cachedNodeModulesDir);
} else {
const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) });
if (nodeModulesDir) {
miniCache.nodeModules.set(filename, nodeModulesDir);
includePaths.push(nodeModulesDir);
}
}
// 1. Preprocess (currently only Sass supported)
let css = '';
switch (styleType) {
case 'css': {
css = code;
break;
}
case 'sass':
case 'scss': {
css = sass.renderSync({ data: code, includePaths, indentedSyntax: styleType === 'sass' }).css.toString('utf8');
break;
}
default: {
throw new Error(`Unsupported: <style lang="${styleType}">`);
}
}
// 2. Post-process (PostCSS)
const postcssPlugins: Plugin[] = [];
// 2a. Tailwind (only if project uses Tailwind)
if (tailwindConfig) {
try {
const require = createRequire(import.meta.url);
const tw = require.resolve('tailwindcss', { paths: [import.meta.url, process.cwd()] });
postcssPlugins.push(require(tw)(tailwindConfig) as any);
} catch (err) {
error(logging, 'transform', err);
throw new Error(`tailwindcss not installed. Try running \`npm install tailwindcss\` and trying again.`);
}
}
if (!global) {
// 2b. Astro scoped styles (skip for global style blocks)
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
// 2c. Scoped @keyframes
postcssPlugins.push(
postcssKeyframes({
generateScopedName(keyframesName) {
return `${keyframesName}-${scopedClass}`;
},
})
);
}
// 2d. Autoprefixer (always on)
postcssPlugins.push(autoprefixer());
// 2e. Run PostCSS
css = await postcss(postcssPlugins)
.process(css, { from: filename, to: undefined })
.then((result) => result.css);
return { css, type: styleType };
}
/** For a given node, inject or append a `scopedClass` to its `class` attribute */
function injectScopedClassAttribute(node: TemplateNode, scopedClass: string, attribute = 'class') {
if (!node.attributes) node.attributes = [];
const classIndex = node.attributes.findIndex(({ name }: any) => name === attribute);
if (classIndex === -1) {
// 3a. element has no class="" attribute; add one and append scopedClass
node.attributes.push({ start: -1, end: -1, type: 'Attribute', name: attribute, value: [{ type: 'Text', raw: scopedClass, data: scopedClass }] });
} else {
// 3b. element has class=""; append scopedClass
const attr = node.attributes[classIndex];
for (let k = 0; k < attr.value.length; k++) {
if (attr.value[k].type === 'Text') {
// dont add same scopedClass twice
if (!hasClass(attr.value[k].data, scopedClass)) {
// string literal
attr.value[k].raw += ' ' + scopedClass;
attr.value[k].data += ' ' + scopedClass;
}
} else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) {
// dont add same scopedClass twice (this check is a little more basic, but should suffice)
if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) {
// MustacheTag
// FIXME: this won't work when JSX element can appear in attributes (rare but possible).
attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`;
}
}
}
}
}
/** Transform <style> tags */
export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer {
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time
const nodesToScope = new Set<TemplateNode>();
return {
visitors: {
html: {
InlineComponent: {
enter(node) {
if (node.name === 'Markdown') {
injectScopedClassAttribute(node, scopedClass, '$scope');
}
for (let attr of node.attributes) {
if (attr.name === 'class') {
injectScopedClassAttribute(node, scopedClass, 'class');
break;
}
}
},
},
Element: {
enter(node) {
// 1. if <style> tag, transform it and continue to next node
if (node.name === 'style') {
// Same as ast.css (below)
const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : '';
if (!code) return;
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
styleNodes.push(node);
styleTransformPromises.push(
transformStyle(code, {
logging: compileOptions.logging,
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
filename,
scopedClass,
tailwindConfig: compileOptions.astroConfig.devOptions.tailwindConfig,
global: globalAttr && globalAttr.value,
})
);
return;
}
// 2. add scoped HTML classes
if (NEVER_SCOPED_TAGS.has(node.name) || node.name.toLowerCase() === '!doctype') {
return; // only continue if this is NOT a <script> tag, etc.
}
// Note: currently we _do_ scope web components/custom elements. This seems correct?
nodesToScope.add(node);
},
},
},
// CSS: compile styles, apply CSS Modules scoping
css: {
Style: {
enter(node) {
// Same as ast.html (above)
// Note: this is duplicated from html because of the compiler were using; in a future version we should combine these
if (!node.content || !node.content.styles) return;
const code = node.content.styles;
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
styleNodes.push(node);
styleTransformPromises.push(
transformStyle(code, {
logging: compileOptions.logging,
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
filename,
scopedClass,
global: globalAttr && globalAttr.value,
})
);
},
},
},
},
async finalize() {
const styleTransforms = await Promise.all(styleTransformPromises);
// If we DO have styles, let's inject the scoped `class` attribute
// Otherwise, our final optimization is easier if we skip this
if (styleTransforms.length > 0) {
for (const node of nodesToScope.values()) {
injectScopedClassAttribute(node, scopedClass);
}
}
styleTransforms.forEach((result, n) => {
if (styleNodes[n].attributes) {
// 1. Replace with final CSS
const isHeadStyle = !styleNodes[n].content;
if (isHeadStyle) {
// Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why
(styleNodes[n].children as any) = [{ ...(styleNodes[n].children as any)[0], data: result.css }];
} else {
styleNodes[n].content.styles = result.css;
}
// 2. Update <style> attributes
const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type');
// add type="text/css"
if (styleTypeIndex !== -1) {
styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css';
styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css';
} else {
styleNodes[n].attributes.push({ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] });
}
// remove lang="*"
const styleLangIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'lang');
if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1);
// TODO: add data-astro for later
// styleNodes[n].attributes.push({ name: 'data-astro', type: 'Attribute', value: true });
}
});
},
};
}

View file

@ -1,96 +0,0 @@
import type { TemplateNode } from '@astrojs/parser';
const beforeHeadElements = new Set(['!doctype', 'html']);
const validHeadElements = new Set(['title', 'meta', 'link', 'style', 'script', 'noscript', 'base']);
export class EndOfHead {
private html: TemplateNode | null = null;
private head: TemplateNode | null = null;
private firstNonHead: TemplateNode | null = null;
private parent: TemplateNode | null = null;
private stack: TemplateNode[] = [];
public foundHeadElements = false;
public foundBodyElements = false;
public append: (...node: TemplateNode[]) => void = () => void 0;
get found(): boolean {
return !!(this.head || this.firstNonHead);
}
get foundHeadContent(): boolean {
return !!this.head || this.foundHeadElements;
}
get foundHeadAndBodyContent(): boolean {
return this.foundHeadContent && this.foundBodyElements;
}
get foundHeadOrHtmlElement(): boolean {
return !!(this.html || this.head);
}
enter(node: TemplateNode) {
const name = node.name ? node.name.toLowerCase() : null;
if (this.found) {
if (!validHeadElements.has(name)) {
if (node.type === 'Element') {
this.foundBodyElements = true;
}
}
return;
}
this.stack.push(node);
// Fragment has no name
if (!node.name) {
return;
}
if (name === 'head') {
this.head = node;
this.parent = this.stack[this.stack.length - 2];
this.append = this.appendToHead;
return;
}
// Skip !doctype and html elements
if (beforeHeadElements.has(name)) {
if (name === 'html') {
this.html = node;
}
return;
}
if (!validHeadElements.has(name)) {
if (node.type === 'Element') {
this.foundBodyElements = true;
}
this.firstNonHead = node;
this.parent = this.stack[this.stack.length - 2];
this.append = this.prependToFirstNonHead;
return;
} else {
this.foundHeadElements = true;
}
}
leave(_node: TemplateNode) {
this.stack.pop();
}
private appendToHead(...nodes: TemplateNode[]) {
if (this.head) {
const head = this.head;
head.children = head.children ?? [];
head.children.push(...nodes);
}
}
private prependToFirstNonHead(...nodes: TemplateNode[]) {
let idx: number = (this.firstNonHead && this.parent?.children?.indexOf(this.firstNonHead)) || 0;
this.parent?.children?.splice(idx, 0, ...nodes);
}
}

View file

@ -1,93 +0,0 @@
/** Is the given string a custom-element tag? */
export function isCustomElementTag(tag: string) {
return /[-]/.test(tag);
}
/** Is the given string a valid component tag */
export function isComponentTag(tag: string) {
return /^[A-Z]/.test(tag) || /^[a-z]+\./.test(tag) || isCustomElementTag(tag);
}
export interface Position {
line: number;
character: number;
}
/** Clamps a number between min and max */
export function clamp(num: number, min: number, max: number): number {
return Math.max(min, Math.min(max, num));
}
/**
* Get the line and character based on the offset
* @param offset The index of the position
* @param text The text for which the position should be retrived
*/
export function positionAt(offset: number, text: string): Position {
offset = clamp(offset, 0, text.length);
const lineOffsets = getLineOffsets(text);
let low = 0;
let high = lineOffsets.length;
if (high === 0) {
return { line: 0, character: offset };
}
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (lineOffsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
}
}
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return { line, character: offset - lineOffsets[line] };
}
/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
*/
export function offsetAt(position: Position, text: string): number {
const lineOffsets = getLineOffsets(text);
if (position.line >= lineOffsets.length) {
return text.length;
} else if (position.line < 0) {
return 0;
}
const lineOffset = lineOffsets[position.line];
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
}
/** Get the offset of all lines */
function getLineOffsets(text: string) {
const lineOffsets = [];
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}
if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
}

View file

@ -1,180 +0,0 @@
import type { ServerRuntime as SnowpackServerRuntime, PluginLoadOptions } from 'snowpack';
import type { AstroConfig } from './@types/astro';
import { posix as path } from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import resolve from 'resolve';
import { loadConfig } from './config.js';
type RendererSnowpackPlugin = string | [string, any] | undefined;
interface RendererInstance {
name: string;
options: any;
snowpackPlugin: RendererSnowpackPlugin;
client: string | null;
server: string;
knownEntrypoints: string[] | undefined;
external: string[] | undefined;
polyfills: string[];
hydrationPolyfills: string[];
jsxImportSource?: string;
jsxTransformOptions?: (
transformContext: Omit<PluginLoadOptions, 'filePath' | 'fileExt'>
) => undefined | { plugins?: any[]; presets?: any[] } | Promise<{ plugins?: any[]; presets?: any[] }>;
}
const CONFIG_MODULE_BASE_NAME = '__astro_config.js';
const CONFIG_MODULE_URL = `/_astro_frontend/${CONFIG_MODULE_BASE_NAME}`;
const DEFAULT_RENDERERS = ['@astrojs/renderer-vue', '@astrojs/renderer-svelte', '@astrojs/renderer-react', '@astrojs/renderer-preact'];
export class ConfigManager {
private state: 'initial' | 'dirty' | 'clean' = 'initial';
public snowpackRuntime: SnowpackServerRuntime | null = null;
public configModuleId: string | null = null;
private rendererNames!: string[];
private version = 1;
constructor(private astroConfig: AstroConfig, private resolvePackageUrl: (pkgName: string) => Promise<string>) {
this.setRendererNames(this.astroConfig);
}
markDirty() {
this.state = 'dirty';
}
async update() {
if (this.needsUpdate() && this.snowpackRuntime) {
// astro.config.mjs has changed, reload it.
if (this.state === 'dirty') {
const version = this.version++;
const astroConfig = await loadConfig(this.astroConfig.projectRoot.pathname, `astro.config.mjs?version=${version}`);
this.setRendererNames(astroConfig);
}
await this.importModule(this.snowpackRuntime);
this.state = 'clean';
}
}
isConfigModule(fileExt: string, filename: string) {
return fileExt === '.js' && filename.endsWith(CONFIG_MODULE_BASE_NAME);
}
isAstroConfig(filename: string) {
const { projectRoot } = this.astroConfig;
return new URL('./astro.config.mjs', projectRoot).pathname === filename;
}
async buildRendererInstances(): Promise<RendererInstance[]> {
const { projectRoot } = this.astroConfig;
const rendererNames = this.rendererNames;
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
const rendererInstances = (
await Promise.all(
rendererNames.map(async (rendererName) => {
let _options: any = null;
if (Array.isArray(rendererName)) {
_options = rendererName[1];
rendererName = rendererName[0];
}
const entrypoint = pathToFileURL(resolveDependency(rendererName)).toString();
const r = await import(entrypoint);
return {
raw: r.default,
options: _options,
};
})
)
).map(({ raw, options }, i) => {
const { name = rendererNames[i], client, server, snowpackPlugin: snowpackPluginName, snowpackPluginOptions } = raw;
if (typeof client !== 'string' && client != null) {
throw new Error(`Expected "client" from ${name} to be a relative path to the client-side renderer!`);
}
if (typeof server !== 'string') {
throw new Error(`Expected "server" from ${name} to be a relative path to the server-side renderer!`);
}
let snowpackPlugin: RendererSnowpackPlugin;
if (typeof snowpackPluginName === 'string') {
if (snowpackPluginOptions) {
snowpackPlugin = [resolveDependency(snowpackPluginName), snowpackPluginOptions];
} else {
snowpackPlugin = resolveDependency(snowpackPluginName);
}
} else if (snowpackPluginName) {
throw new Error(`Expected the snowpackPlugin from ${name} to be a "string" but encountered "${typeof snowpackPluginName}"!`);
}
const polyfillsNormalized = (raw.polyfills || []).map((p: string) => (p.startsWith('.') ? path.join(name, p) : p));
const hydrationPolyfillsNormalized = (raw.hydrationPolyfills || []).map((p: string) => (p.startsWith('.') ? path.join(name, p) : p));
return {
name,
options,
snowpackPlugin,
client: raw.client ? path.join(name, raw.client) : null,
server: path.join(name, raw.server),
knownEntrypoints: raw.knownEntrypoints,
external: raw.external,
polyfills: polyfillsNormalized,
hydrationPolyfills: hydrationPolyfillsNormalized,
jsxImportSource: raw.jsxImportSource,
};
});
return rendererInstances;
}
async getRenderers(): Promise<RendererInstance[]> {
const renderers = await this.buildRendererInstances();
return renderers;
}
async buildSource(contents: string): Promise<string> {
const renderers = await this.buildRendererInstances();
const rendererServerPackages = renderers.map(({ server }) => server);
const rendererClientPackages = await Promise.all(
renderers.filter((instance): instance is RendererInstance & { client: string } => !!instance.client).map(({ client }) => this.resolvePackageUrl(client))
);
const rendererPolyfills = await Promise.all(renderers.map(({ polyfills }) => Promise.all(polyfills.map((src) => this.resolvePackageUrl(src)))));
const rendererHydrationPolyfills = await Promise.all(renderers.map(({ hydrationPolyfills }) => Promise.all(hydrationPolyfills.map((src) => this.resolvePackageUrl(src)))));
const result = /* js */ `${rendererServerPackages.map((pkg, i) => `import __renderer_${i} from "${pkg}";`).join('\n')}
import { setRenderers } from 'astro/dist/internal/__astro_component.js';
let rendererInstances = [${renderers
.map(
(r, i) => `{
name: "${r.name}",
source: ${rendererClientPackages[i] ? `"${rendererClientPackages[i]}"` : 'null'},
renderer: typeof __renderer_${i} === 'function' ? __renderer_${i}(${r.options ? JSON.stringify(r.options) : 'null'}) : __renderer_${i},
polyfills: ${JSON.stringify(rendererPolyfills[i])},
hydrationPolyfills: ${JSON.stringify(rendererHydrationPolyfills[i])}
}`
)
.join(', ')}];
${contents}
`;
return result;
}
needsUpdate(): boolean {
return this.state === 'initial' || this.state === 'dirty';
}
private setRendererNames(astroConfig: AstroConfig) {
this.rendererNames = astroConfig.renderers || DEFAULT_RENDERERS;
}
private async importModule(snowpackRuntime: SnowpackServerRuntime): Promise<void> {
await snowpackRuntime.importModule(CONFIG_MODULE_URL);
}
}

View file

@ -0,0 +1,5 @@
# `core/`
Code that executes within the top-level Node context. Contains the main Astro logic for the `build` and `dev` commands, and also manages the Vite server and SSR.
[See CONTRIBUTING.md](../../../../CONTRIBUTING.md) for a code overview.

View file

@ -0,0 +1,207 @@
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, RouteCache, RouteData, RSSResult } from '../../@types/astro-core';
import type { LogOptions } from '../logger';
import { rollupPluginHTML } from '@web/rollup-plugin-html';
import fs from 'fs';
import { bold, cyan, green, dim } from 'kleur/colors';
import { performance } from 'perf_hooks';
import vite, { ViteDevServer } from 'vite';
import { fileURLToPath } from 'url';
import { createVite } from '../create-vite.js';
import { pad } from '../dev/util.js';
import { defaultLogOptions, levels, warn } from '../logger.js';
import { ssr } from '../ssr/index.js';
import { generatePaginateFunction } from '../ssr/paginate.js';
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
import { generateRssFunction } from '../ssr/rss.js';
import { generateSitemap } from '../ssr/sitemap.js';
import { kb, profileHTML, profileJS } from './stats.js';
export interface BuildOptions {
mode?: string;
logging: LogOptions;
}
/** `astro build` */
export default async function build(config: AstroConfig, options: BuildOptions = { logging: defaultLogOptions }): Promise<void> {
const builder = new AstroBuilder(config, options);
await builder.build();
}
class AstroBuilder {
private config: AstroConfig;
private logging: LogOptions;
private mode = 'production';
private origin: string;
private routeCache: RouteCache = {};
private manifest: ManifestData;
constructor(config: AstroConfig, options: BuildOptions) {
if (!config.buildOptions.site && config.buildOptions.sitemap !== false) {
warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
}
if (options.mode) this.mode = options.mode;
this.config = config;
const port = config.devOptions.port; // no need to save this (dont rely on port in builder)
this.logging = options.logging;
this.origin = config.buildOptions.site ? new URL(config.buildOptions.site).origin : `http://localhost:${port}`;
this.manifest = createRouteManifest({ config });
}
/** Build all pages */
async build() {
const start = performance.now();
// 1. initialize fresh Vite instance
const { logging, origin } = this;
const viteConfig = await createVite(
{
mode: this.mode,
server: {
hmr: { overlay: false },
middlewareMode: 'ssr',
},
...(this.config.vite || {}),
},
{ astroConfig: this.config, logging }
);
const viteServer = await vite.createServer(viteConfig);
// 2. get all routes
const allPages: Promise<{ html: string; name: string }>[] = [];
const assets: Record<string, string> = {}; // additional assets to be written
await Promise.all(
this.manifest.routes.map(async (route) => {
const { pathname } = route;
const filePath = new URL(`./${route.component}`, this.config.projectRoot);
// static pages
if (pathname) {
allPages.push(
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname, viteServer }).then((html) => ({
html,
name: pathname.replace(/\/?$/, '/index.html').replace(/^\//, ''),
}))
);
}
// dynamic pages
else {
const staticPaths = await this.getStaticPathsForRoute(route, viteServer);
// handle RSS (TODO: improve this?)
if (staticPaths.rss && staticPaths.rss.xml) {
const rssFile = new URL(staticPaths.rss.url.replace(/^\/?/, './'), this.config.dist);
if (assets[fileURLToPath(rssFile)]) {
throw new Error(
`[getStaticPaths] RSS feed ${staticPaths.rss.url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`
);
}
assets[fileURLToPath(rssFile)] = staticPaths.rss.xml;
}
// TODO: throw error if conflict
staticPaths.paths.forEach((staticPath) => {
allPages.push(
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname: staticPath, viteServer }).then(
(html) => ({
html,
name: staticPath.replace(/\/?$/, '/index.html').replace(/^\//, ''),
})
)
);
});
}
})
);
const input = await Promise.all(allPages);
// 3. build with Vite
await vite.build({
logLevel: 'error',
mode: 'production',
build: {
emptyOutDir: true,
minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles
outDir: fileURLToPath(this.config.dist),
rollupOptions: {
input: [],
output: { format: 'esm' },
},
target: 'es2020', // must match an esbuild target
},
plugins: [
rollupPluginHTML({ input, extractAssets: false }) as any, // "any" needed for CI; also we dont need typedefs for this anyway
...(viteConfig.plugins || []),
],
publicDir: viteConfig.publicDir,
root: viteConfig.root,
server: viteConfig.server,
});
// 4. write assets to disk
Object.keys(assets).map((k) => {
if (!assets[k]) return;
const filePath = new URL(`file://${k}`);
fs.mkdirSync(new URL('./', filePath), { recursive: true });
fs.writeFileSync(filePath, assets[k], 'utf8');
delete assets[k]; // free up memory
});
// 5. build sitemap
let sitemapTime = 0;
if (this.config.buildOptions.sitemap && this.config.buildOptions.site) {
const sitemapStart = performance.now();
const sitemap = generateSitemap(input.map(({ name }) => new URL(`/${name}`, this.config.buildOptions.site).href));
const sitemapPath = new URL('./sitemap.xml', this.config.dist);
await fs.promises.mkdir(new URL('./', sitemapPath), { recursive: true });
await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
sitemapTime = performance.now() - sitemapStart;
}
// 6. clean up
await viteServer.close();
// 7. log output
if (logging.level && levels[logging.level] <= levels['info']) {
await this.printStats({
cwd: this.config.dist,
pageCount: input.length,
pageTime: Math.round(performance.now() - start),
sitemapTime,
});
}
}
/** Extract all static paths from a dynamic route */
private async getStaticPathsForRoute(route: RouteData, viteServer: ViteDevServer): Promise<{ paths: string[]; rss?: RSSResult }> {
const filePath = new URL(`./${route.component}`, this.config.projectRoot);
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
validateGetStaticPathsModule(mod);
const rss = generateRssFunction(this.config.buildOptions.site, route);
const staticPaths: GetStaticPathsResult = (await mod.getStaticPaths!({ paginate: generatePaginateFunction(route), rss: rss.generator })).flat();
validateGetStaticPathsResult(staticPaths, this.logging);
return {
paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean),
rss: rss.rss,
};
}
/** Stats */
private async printStats({ cwd, pageTime, pageCount, sitemapTime }: { cwd: URL; pageTime: number; pageCount: number; sitemapTime: number }) {
const end = Math.round(performance.now() - pageTime);
const [js, html] = await Promise.all([profileJS({ cwd, entryHTML: new URL('./index.html', cwd) }), profileHTML({ cwd })]);
/* eslint-disable no-console */
console.log(`${pad(bold(cyan('Done')), 70)}${dim(` ${pad(`${end}ms`, 8, 'left')}`)}
Pages (${pageCount} total)
${green(`✔ All pages under ${kb(html.maxSize)}`)}
JS
${pad('initial load', 50)}${pad(kb(js.entryHTML || 0), 8, 'left')}
${pad('total size', 50)}${pad(kb(js.total), 8, 'left')}
CSS
${pad('initial load', 50)}${pad('0 kB', 8, 'left')}
${pad('total size', 50)}${pad('0 kB', 8, 'left')}
Images
${green(`✔ All images under 50 kB`)}
`);
if (sitemapTime > 0) console.log(`Sitemap\n ${green(`✔ Built in ${sitemapTime}`)}`);
}
}

View file

@ -0,0 +1,140 @@
import cheerio from 'cheerio';
import * as eslexer from 'es-module-lexer';
import fetch from 'node-fetch';
import fs from 'fs';
import slash from 'slash';
import glob from 'tiny-glob';
import { fileURLToPath } from 'url';
type FileSizes = { [file: string]: number };
// Feel free to modify output to whatevers needed in display. If its not needed, kill it and improve stat speeds!
/** JS: prioritize entry HTML, but also show total */
interface JSOutput {
/** breakdown of JS per-file */
js: FileSizes;
/** weight of index.html */
entryHTML?: number;
/** total bytes of [js], added for convenience */
total: number;
}
/** HTML: total isnt important, because those are broken up requests. However, surface any anomalies / bloated HTML */
interface HTMLOutput {
/** breakdown of HTML per-file */
html: FileSizes;
/** biggest HTML file */
maxSize: number;
}
/** Scan any directory */
async function scan(cwd: URL, pattern: string): Promise<URL[]> {
const results = await glob(pattern, { cwd: fileURLToPath(cwd) });
return results.map((filepath) => new URL(slash(filepath), cwd));
}
/** get total HTML size */
export async function profileHTML({ cwd }: { cwd: URL }): Promise<HTMLOutput> {
const sizes: FileSizes = {};
const html = await scan(cwd, '**/*.html');
let maxSize = 0;
await Promise.all(
html.map(async (file) => {
const relPath = file.pathname.replace(cwd.pathname, '');
const size = (await fs.promises.stat(file)).size;
sizes[relPath] = size;
if (size > maxSize) maxSize = size;
})
);
return {
html: sizes,
maxSize,
};
}
/** get total JS size (note: .wasm counts as JS!) */
export async function profileJS({ cwd, entryHTML }: { cwd: URL; entryHTML?: URL }): Promise<JSOutput> {
const sizes: FileSizes = {};
let htmlSize = 0;
// profile HTML entry (do this first, before all JS in a project is scanned)
if (entryHTML) {
let $ = cheerio.load(await fs.promises.readFile(entryHTML));
let entryScripts: URL[] = [];
let visitedEntry = false; // note: a quirk of Vite is that the entry file is async-loaded. Count that, but dont count subsequent async loads
// scan <script> files, keep adding to total until done
$('script').each((n, el) => {
const src = $(el).attr('src');
const innerHTML = $(el).html();
// if inline script, add to overall JS weight
if (innerHTML) {
htmlSize += Buffer.byteLength(innerHTML);
}
// otherwise if external script, load & scan it
if (src) {
entryScripts.push(new URL(src, entryHTML));
}
});
let scanPromises: Promise<void>[] = [];
await Promise.all(entryScripts.map(parseJS));
/** parse JS for imports, and add to total size */
async function parseJS(url: URL): Promise<void> {
const relPath = url.pathname.replace(cwd.pathname, '');
if (sizes[relPath]) return;
try {
let code = url.protocol === 'file:' ? await fs.promises.readFile(url, 'utf8') : await fetch(url.href).then((body) => body.text());
sizes[relPath] = Buffer.byteLength(code);
const staticImports = eslexer.parse(code)[0].filter(({ d }) => {
if (!visitedEntry) return true; // if were on the entry file, count async imports, too
return d === -1; // subsequent runs: dont count deferred code toward total
});
for (const { n } of staticImports) {
if (!n) continue;
let nextURL: URL | undefined;
// external import
if (n.startsWith('http://') || n.startsWith('https://') || n.startsWith('//')) nextURL = new URL(n);
// relative import
else if (n[0] === '.') nextURL = new URL(n, url);
// absolute import (note: make sure "//" is already handled!)
else if (n[0] === '/') nextURL = new URL(`.${n}`, cwd);
if (!nextURL) continue; // unknown format: skip
if (sizes[nextURL.pathname.replace(cwd.pathname, '')]) continue; // already scanned: skip
scanPromises.push(parseJS(nextURL));
}
} catch (err) {
console.warn(`Could not access ${url.href} to include in bundle size`); // eslint-disable-line no-console
}
visitedEntry = true; // after first run, stop counting async imports toward total
}
await Promise.all(scanPromises);
htmlSize = Object.values(sizes).reduce((sum, next) => sum + next, 0);
}
// collect size of all JS in project (note: some may have already been scanned; skip when possible)
const js = await scan(cwd, '**/*.(js|mjs|wasm)');
await Promise.all(
js.map(async (file) => {
const relPath = file.pathname.replace(cwd.pathname, '');
if (!sizes[relPath]) sizes[relPath] = (await fs.promises.stat(file)).size; // only scan if new
})
);
return {
js: sizes,
entryHTML: htmlSize || undefined,
total: Object.values(sizes).reduce((sum, acc) => sum + acc, 0),
};
}
/** b -> kB */
export function kb(bytes: number): string {
if (bytes === 0) return `0 kB`;
return (Math.round(bytes / 1000) || 1) + ' kB'; // if this is between 0.10.4, round up to 1
}

View file

@ -1,11 +1,10 @@
import type { AstroConfig, AstroUserConfig } from '../@types/astro-core';
import { existsSync } from 'fs';
import getPort from 'get-port';
import * as colors from 'kleur/colors';
import path from 'path';
import { pathToFileURL } from 'url';
import { z } from 'zod';
import { AstroConfig, AstroUserConfig } from './@types/astro';
import { addTrailingSlash } from './util.js';
export const AstroConfigSchema = z.object({
projectRoot: z
@ -40,6 +39,7 @@ export const AstroConfigSchema = z.object({
gfm: z.boolean().optional(),
remarkPlugins: z.array(z.any()).optional(),
rehypePlugins: z.array(z.any()).optional(),
render: z.any().optional().default(['@astrojs/markdown-remark', {}]),
})
.optional()
.default({}),
@ -57,10 +57,7 @@ export const AstroConfigSchema = z.object({
devOptions: z
.object({
hostname: z.string().optional().default('localhost'),
port: z
.number()
.optional()
.transform((val) => val || getPort({ port: getPort.makeRange(3000, 3050) })),
port: z.number().optional().default(3000),
tailwindConfig: z.string().optional(),
trailingSlash: z
.union([z.literal('always'), z.literal('never'), z.literal('ignore')])
@ -69,6 +66,7 @@ export const AstroConfigSchema = z.object({
})
.optional()
.default({}),
vite: z.any().optional().default({}), // TODO: we dont need validation, but can we get better type inference?
});
/** Turn raw config values into normalized values */
@ -101,10 +99,25 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
return AstroConfigRelativeSchema.parseAsync(userConfig);
}
interface LoadConfigOptions {
cwd?: string;
filename?: string;
}
/** Adds '/' to end of string but doesnt double-up */
function addTrailingSlash(str: string): string {
return str.replace(/\/*$/, '/');
}
interface LoadConfigOptions {
cwd?: string;
filename?: string;
}
/** Attempt to load an `astro.config.mjs` file */
export async function loadConfig(rawRoot: string | undefined, configFileName = 'astro.config.mjs'): Promise<AstroConfig> {
const root = rawRoot ? path.resolve(rawRoot) : process.cwd();
const astroConfigPath = new URL(`./${configFileName}`, `file://${root}/`);
export async function loadConfig(options: LoadConfigOptions): Promise<AstroConfig> {
const root = options.cwd ? path.resolve(options.cwd) : process.cwd();
const astroConfigPath = new URL(`./${options.filename || 'astro.config.mjs'}`, `file://${root}/`);
let userConfig: AstroUserConfig = {};
// Load a user-config, if one exists and is provided
if (existsSync(astroConfigPath)) {

View file

@ -0,0 +1,201 @@
import type { AstroConfig } from '../@types/astro-core';
import type { AstroDevServer } from './dev';
import type { LogOptions } from './logger';
import fs from 'fs';
import slash from 'slash';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import vite from 'vite';
import astroVitePlugin from '../vite-plugin-astro/index.js';
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import fetchVitePlugin from '../vite-plugin-fetch/index.js';
import { getPackageJSON, parseNpmName } from './util.js';
const require = createRequire(import.meta.url);
// note: ssr is still an experimental API hence the type omission
type ViteConfigWithSSR = vite.InlineConfig & { ssr?: { external?: string[]; noExternal?: string[] } };
/** Return a common starting point for all Vite actions */
export async function createVite(
viteConfig: ViteConfigWithSSR,
{ astroConfig, logging, devServer }: { astroConfig: AstroConfig; logging: LogOptions; devServer?: AstroDevServer }
): Promise<ViteConfigWithSSR> {
const optimizedDeps = new Set<string>(); // dependencies that must be bundled for the client (Vite may not detect all of these)
const dedupe = new Set<string>(); // dependencies that cant be duplicated (e.g. React & SolidJS)
const plugins: Plugin[] = []; // Vite plugins
// load project deps
const packageJSON = (await getPackageJSON(astroConfig.projectRoot)) || {};
const userDeps = Object.keys(packageJSON?.dependencies || {});
userDeps.forEach((dep) => {
optimizedDeps.add(dep); // prepare all user deps for client ahead of time
});
const userDevDeps = Object.keys(packageJSON?.devDependencies || {});
const { external, noExternal } = await viteSSRDeps([...userDeps, ...userDevDeps]);
// console.log(external.has('tiny-glob'), noExternal.has('tiny-glob'));
// load Astro renderers
await Promise.all(
astroConfig.renderers.map(async (name) => {
const { default: renderer } = await import(name);
// 1. prepare client-side hydration code for browser
if (renderer.client) {
optimizedDeps.add(name + renderer.client.substr(1));
}
// 2. knownEntrypoints and polyfills need to be added to the client
for (let dep of [...(renderer.knownEntrypoints || []), ...(renderer.polyfills || [])]) {
if (dep[0] === '.') dep = name + dep.substr(1); // if local polyfill, use full path
optimizedDeps.add(dep);
dedupe.add(dep); // we can try and dedupe renderers by default
}
// 3. let renderer inject Vite plugins
if (renderer.vitePlugins) {
plugins.push(...renderer.vitePlugins);
}
// 4. mark external packages as external to Vite
if (renderer.external) {
for (const dep of renderer.external) {
external.add(dep);
noExternal.delete(dep);
}
}
})
);
// load client-side hydrations
fs.readdirSync(new URL('../runtime/client', import.meta.url)).forEach((hydrator) => {
optimizedDeps.add(`astro/client/${hydrator}`); // always prepare these for client
});
return vite.mergeConfig(
{
cacheDir: fileURLToPath(new URL('./node_modules/.vite/', astroConfig.projectRoot)), // using local caches allows Astro to be used in monorepos, etc.
clearScreen: false,
logLevel: 'error',
optimizeDeps: {
/** Try and scan a users project (wont catch everything) */
entries: ['src/**/*'],
/** Always include these dependencies for optimization */
include: [...optimizedDeps],
},
plugins: [
astroVitePlugin({ config: astroConfig, devServer }),
markdownVitePlugin({ config: astroConfig, devServer }),
jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig, devServer }),
fetchVitePlugin(),
...plugins,
],
publicDir: fileURLToPath(astroConfig.public),
resolve: {
dedupe: [...dedupe],
},
root: fileURLToPath(astroConfig.projectRoot),
server: {
/** prevent serving outside of project root (will become new default soon) */
fs: { strict: true },
/** disable HMR for test */
hmr: process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production' ? false : undefined,
/** handle Vite URLs */
proxy: {
// add proxies here
},
},
/** Note: SSR API is in beta (https://vitejs.dev/guide/ssr.html) */
ssr: {
external: [...external],
noExternal: [...noExternal],
},
},
vite.mergeConfig(viteConfig, astroConfig.vite || {}) // merge in user vite settings
);
}
/** Try and automatically figure out Vite external & noExternal */
async function viteSSRDeps(deps: string[]): Promise<{ external: Set<string>; noExternal: Set<string> }> {
const skip = new Set<string>();
const external = new Set<string>();
const noExternal = new Set<string>();
/** categorize package as ESM or CJS */
async function sortPkg(spec: string): Promise<void> {
// already sorted; skip
if (external.has(spec) || noExternal.has(spec) || skip.has(spec)) return;
// not an npm package: ignore
const pkg = parseNpmName(spec);
if (!pkg) {
skip.add(spec);
return;
}
try {
const moduleLoc = require.resolve(spec);
// node cant find this: skip
if (!moduleLoc) {
skip.add(spec);
return;
}
// load modules package.json
let cwd = new URL('../', `file://${slash(moduleLoc)}/`);
let packageJSON = await getPackageJSON(cwd);
while (!packageJSON) {
const next = new URL('../', cwd);
if (next.href === cwd.href) return; // were at root; skip
cwd = next;
packageJSON = await getPackageJSON(cwd);
}
// couldnt locate: skip
if (!packageJSON) {
skip.add(spec);
return;
}
// sort this package
let isExternal = true; // external by default
// ESM gets noExternal
if (packageJSON.type === 'module') isExternal = false;
// TODO: manual bugfixes for Vite
if (pkg.name === '@sveltejs/vite-plugin-svelte') isExternal = true;
if (pkg.name === 'micromark-util-events-to-acorn') isExternal = true;
if (pkg.name === 'unified') isExternal = true;
// TODO: add more checks here if needed
// add to list
if (isExternal === true) {
external.add(spec);
} else {
noExternal.add(spec);
}
// recursively load dependencies for package (but not devDeps)
await Promise.all(Object.keys(packageJSON.dependencies || {}).map(sortPkg));
} catch (err) {
// cant load package: skip
skip.add(spec);
return;
}
}
// for top-level, load deps and devDeps (just in case)
await Promise.all(deps.map(sortPkg));
// sort (when debugging, makes packages easier to find)
const externalSorted = [...external];
externalSorted.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
const noExternalSorted = [...noExternal];
noExternalSorted.sort((a, b) => a.localeCompare(b, 'en-us', { numeric: true }));
return {
external: new Set([...externalSorted]),
noExternal: new Set([...noExternalSorted]),
};
}

View file

@ -0,0 +1,307 @@
import type { NextFunction } from 'connect';
import type http from 'http';
import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../../@types/astro-core';
import type { LogOptions } from '../logger';
import type { HmrContext, ModuleNode } from 'vite';
import { fileURLToPath } from 'url';
import connect from 'connect';
import mime from 'mime';
import { performance } from 'perf_hooks';
import stripAnsi from 'strip-ansi';
import vite from 'vite';
import { defaultLogOptions, error, info } from '../logger.js';
import { ssr } from '../ssr/index.js';
import { createRouteManifest, matchRoute } from '../ssr/routing.js';
import { createVite } from '../create-vite.js';
import * as msg from './messages.js';
import { errorTemplate } from './template/error.js';
export interface DevOptions {
logging: LogOptions;
}
interface DevServer {
hostname: string;
port: number;
server: connect.Server;
stop(): Promise<void>;
}
/** `astro dev` */
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
// start dev server
const server = new AstroDevServer(config, options);
await server.start();
// attempt shutdown
process.on('SIGTERM', () => server.stop());
return {
hostname: server.hostname,
port: server.port,
server: server.app,
stop: () => server.stop(),
};
}
/** Dev server */
export class AstroDevServer {
app = connect();
httpServer: http.Server | undefined;
hostname: string;
port: number;
private config: AstroConfig;
private logging: LogOptions;
private manifest: ManifestData;
private mostRecentRoute?: RouteData;
private origin: string;
private routeCache: RouteCache = {};
private viteServer: vite.ViteDevServer | undefined;
constructor(config: AstroConfig, options: DevOptions) {
this.config = config;
this.hostname = config.devOptions.hostname || 'localhost';
this.logging = options.logging;
this.port = config.devOptions.port;
this.origin = `http://localhost:${this.port}`;
this.manifest = createRouteManifest({ config });
}
/** Start dev server */
async start() {
// 1. profile startup time
const devStart = performance.now();
// 2. create Vite instance
this.viteServer = await this.createViteServer();
// 3. add middlewares
this.app.use((req, res, next) => this.handleRequest(req, res, next));
this.app.use(this.viteServer.middlewares);
this.app.use((req, res, next) => this.renderError(req, res, next));
// 4. listen on port (and retry if taken)
await new Promise<void>((resolve, reject) => {
const onError = (err: NodeJS.ErrnoException) => {
if (err.code && err.code === 'EADDRINUSE') {
info(this.logging, 'astro', msg.portInUse({ port: this.port }));
this.port++;
} else {
error(this.logging, 'astro', err.stack);
this.httpServer?.removeListener('error', onError);
reject(err);
}
};
this.httpServer = this.app.listen(this.port, this.hostname, () => {
info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}` }));
resolve();
});
this.httpServer.on('error', onError);
});
}
/** Stop dev server */
async stop() {
this.httpServer?.close(); // close HTTP server
if (this.viteServer) await this.viteServer.close(); // close Vite server
}
/** Handle HMR */
public async handleHotUpdate({ file, modules }: HmrContext): Promise<void | ModuleNode[]> {
if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
for (const module of modules) {
this.viteServer.moduleGraph.invalidateModule(module);
}
const route = this.mostRecentRoute;
const pathname = route?.pathname ?? '/';
if (!route) {
this.viteServer.ws.send({
type: 'full-reload',
});
return [];
}
try {
// try to update the most recent route
const html = await ssr({
astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot),
logging: this.logging,
mode: 'development',
origin: this.origin,
pathname,
route,
routeCache: this.routeCache,
viteServer: this.viteServer,
});
// TODO: log update
this.viteServer.ws.send({
type: 'custom',
event: 'astro:reload',
data: { html },
});
return [];
} catch (e) {
const err = e as Error;
this.viteServer.ssrFixStacktrace(err);
console.log(err.stack);
this.viteServer.ws.send({
type: 'full-reload',
});
return [];
}
}
/** Set up Vite server */
private async createViteServer() {
const viteConfig = await createVite(
{
mode: 'development',
server: {
middlewareMode: 'ssr',
host: this.hostname,
},
...(this.config.vite || {}),
},
{ astroConfig: this.config, logging: this.logging, devServer: this }
);
const viteServer = await vite.createServer(viteConfig);
const pagesDirectory = fileURLToPath(this.config.pages);
viteServer.watcher.on('add', (file) => {
// Only rebuild routes if new file is a page.
if (!file.startsWith(pagesDirectory)) {
return;
}
this.routeCache = {};
this.manifest = createRouteManifest({ config: this.config });
});
viteServer.watcher.on('unlink', (file) => {
// Only rebuild routes if deleted file is a page.
if (!file.startsWith(pagesDirectory)) {
return;
}
this.routeCache = {};
this.manifest = createRouteManifest({ config: this.config });
});
viteServer.watcher.on('change', () => {
// No need to rebuild routes on file content changes.
// However, we DO want to clear the cache in case
// the change caused a getStaticPaths() return to change.
this.routeCache = {};
});
return viteServer;
}
/** The primary router (runs before Vite, in case we need to modify or intercept anything) */
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
let pathname = req.url || '/'; // original request
const reqStart = performance.now();
if (pathname.startsWith('/@astro')) {
const spec = pathname.slice(2);
const url = await this.viteServer.moduleGraph.resolveUrl(spec);
req.url = url[1];
return this.viteServer.middlewares.handle(req, res, next);
}
let filePath: URL | undefined;
try {
const route = matchRoute(pathname, this.manifest);
// 404: continue to Vite
if (!route) {
next();
return;
}
this.mostRecentRoute = route;
// handle .astro and .md pages
filePath = new URL(`./${route.component}`, this.config.projectRoot);
const html = await ssr({
astroConfig: this.config,
filePath,
logging: this.logging,
mode: 'development',
origin: this.origin,
pathname,
route,
routeCache: this.routeCache,
viteServer: this.viteServer,
});
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 200, reqTime: performance.now() - reqStart }));
res.writeHead(200, {
'Content-Type': mime.getType('.html') as string,
'Content-Length': Buffer.byteLength(html, 'utf8'),
});
res.write(html);
res.end();
} catch (err: any) {
this.viteServer.ssrFixStacktrace(err);
this.viteServer.ws.send({ type: 'error', err });
const statusCode = 500;
const html = errorTemplate({
statusCode,
title: 'Internal Error',
tabTitle: '500: Error',
message: stripAnsi(err.message),
});
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
res.writeHead(statusCode, {
'Content-Type': mime.getType('.html') as string,
'Content-Length': Buffer.byteLength(html, 'utf8'),
});
res.write(html);
res.end();
}
}
/** Render error page */
private async renderError(req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
const pathname = req.url || '/';
const reqStart = performance.now();
let html = '';
const statusCode = 404;
// attempt to load user-given page
const relPages = this.config.pages.href.replace(this.config.projectRoot.href, '');
const userDefined404 = this.manifest.routes.find((route) => route.component === relPages + '404.astro');
if (userDefined404) {
html = await ssr({
astroConfig: this.config,
filePath: new URL(`./${userDefined404.component}`, this.config.projectRoot),
logging: this.logging,
mode: 'development',
pathname: `/${userDefined404.component}`,
origin: this.origin,
routeCache: this.routeCache,
viteServer: this.viteServer,
});
}
// if not found, fall back to default template
else {
html = errorTemplate({ statusCode, title: 'Not found', tabTitle: '404: Not Found', message: pathname });
}
info(this.logging, 'astro', msg.req({ url: pathname, statusCode, reqTime: performance.now() - reqStart }));
res.writeHead(statusCode, {
'Content-Type': mime.getType('.html') as string,
'Content-Length': Buffer.byteLength(html, 'utf8'),
});
res.write(html);
res.end();
}
}

View file

@ -0,0 +1,37 @@
/**
* Dev server messages (organized here to prevent clutter)
*/
import { bold, dim, green, magenta, yellow } from 'kleur/colors';
import { pad } from './util.js';
/** Display */
export function req({ url, statusCode, reqTime }: { url: string; statusCode: number; reqTime: number }): string {
let color = dim;
if (statusCode >= 500) color = magenta;
else if (statusCode >= 400) color = yellow;
else if (statusCode >= 300) color = dim;
else if (statusCode >= 200) color = green;
return `${color(statusCode)} ${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
}
/** Display */
export function reload({ url, reqTime }: { url: string; reqTime: number }): string {
let color = yellow;
return `${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
}
/** Display dev server host and startup time */
export function devStart({ startupTime }: { startupTime: number }): string {
return `${pad(`Server started`, 44)} ${dim(`${Math.round(startupTime)}ms`)}`;
}
/** Display dev server host */
export function devHost({ host }: { host: string }): string {
return `Local: ${bold(magenta(host))}`;
}
/** Display port in use */
export function portInUse({ port }: { port: number }): string {
return `Port ${port} in use. Trying a new one…`;
}

View file

@ -0,0 +1,53 @@
import { encode } from 'html-entities';
interface ErrorTemplateOptions {
statusCode?: number;
tabTitle: string;
title: string;
message: string;
}
/** Display internal 404 page (if user didnt provide one) */
export function errorTemplate({ title, message, statusCode, tabTitle }: ErrorTemplateOptions): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${tabTitle}</title>
<style>
body {
background-color: #101010;
color: #d0d0d0;
font-family: monospace;
line-height: 1.6;
margin: 0;
}
.wrapper {
padding-left: 2rem;
padding-right: 2rem;
}
h1 {
font-weight: 800;
margin-top: 1rem;
margin-bottom: 0;
}
pre {
color: #999;
font-size: 1.4em;
margin-top: 0;
max-width: 60em;
}
.status {
opacity: 0.7;
}
</style>
</head>
<body>
<main class="wrapper">
<h1>${statusCode ? `<span class="statusCode">${statusCode}</span> ` : ''}${title}</h1>
<pre><code>${encode(message)}</code></pre>
</main>
</body>
</html>
`;
}

View file

@ -0,0 +1,8 @@
/** Pad string () */
export function pad(input: string, minLength: number, dir?: 'left' | 'right'): string {
let output = input;
while (output.length < minLength) {
output = dir === 'left' ? ' ' + output : output + ' ';
}
return output;
}

View file

@ -1,13 +1,30 @@
import type { CompileError } from '@astrojs/parser';
import { bold, blue, red, grey, underline, yellow } from 'kleur/colors';
import { bold, blue, dim, red, grey, underline, yellow } from 'kleur/colors';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';
import stringWidth from 'string-width';
import { format as utilFormat } from 'util';
type ConsoleStream = Writable & {
fd: 1 | 2;
};
function getLoggerLocale(): string {
const defaultLocale = 'en-US';
if (process.env.LANG) {
const extractedLocale = process.env.LANG.split('.')[0].replace(/_/g, '-');
// Check if language code is atleast two characters long (ie. en, es).
// NOTE: if "c" locale is encountered, the default locale will be returned.
if (extractedLocale.length < 2) return defaultLocale;
else return extractedLocale;
} else return defaultLocale;
}
const dt = new Intl.DateTimeFormat(getLoggerLocale(), {
hour: '2-digit',
minute: '2-digit',
});
export const defaultLogDestination = new Writable({
objectMode: true,
write(event: LogMessage, _, callback) {
@ -15,6 +32,9 @@ export const defaultLogDestination = new Writable({
if (levels[event.level] < levels['error']) {
dest = process.stdout;
}
dest.write(dim(dt.format(new Date()) + ' '));
let type = event.type;
if (type !== null) {
if (event.level === 'info') {
@ -59,7 +79,7 @@ export interface LogMessage {
args: Array<any>;
}
const levels: Record<LoggerLevel, number> = {
export const levels: Record<LoggerLevel, number> = {
debug: 20,
info: 30,
warn: 40,
@ -133,11 +153,8 @@ export function parseError(opts: LogOptions, err: CompileError) {
opts,
'parse-error',
`
${underline(bold(grey(`${err.filename || ''}:${err.start.line}:${err.start.column}`)))}
${bold(red(`𝘅 ${err.message}`))}
${frame}
`
);

View file

@ -0,0 +1,67 @@
import type { AstroConfig } from '../../@types/astro-core';
import type { LogOptions } from '../logger';
import http from 'http';
import { performance } from 'perf_hooks';
import send from 'send';
import { fileURLToPath } from 'url';
import * as msg from '../dev/messages.js';
import { error, info } from '../logger.js';
interface PreviewOptions {
logging: LogOptions;
}
interface PreviewServer {
hostname: string;
port: number;
server: http.Server;
stop(): Promise<void>;
}
/** The primary dev action */
export default async function preview(config: AstroConfig, { logging }: PreviewOptions): Promise<PreviewServer> {
const startServerTime = performance.now();
// Create the preview server, send static files out of the `dist/` directory.
const server = http.createServer((req, res) => {
send(req, req.url!, {
root: fileURLToPath(config.dist),
}).pipe(res);
});
// Start listening on `hostname:port`.
let port = config.devOptions.port;
const { hostname } = config.devOptions;
await new Promise<http.Server>((resolve, reject) => {
const onError = (err: NodeJS.ErrnoException) => {
if (err.code && err.code === 'EADDRINUSE') {
info(logging, 'astro', msg.portInUse({ port }));
port++;
} else {
error(logging, 'preview', err.stack);
server.removeListener('error', onError);
reject(err);
}
};
server
.listen(port, hostname, () => {
info(logging, 'preview', msg.devStart({ startupTime: performance.now() - startServerTime }));
info(logging, 'preview', msg.devHost({ host: `http://${hostname}:${port}/` }));
resolve(server);
})
.on('error', (err: NodeJS.ErrnoException) => {
process.exit(1);
});
});
return {
hostname,
port,
server,
stop: async () => {
server.close();
},
};
}

View file

@ -0,0 +1,240 @@
import type { BuildResult } from 'esbuild';
import type { ViteDevServer } from 'vite';
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRError } from '../../@types/astro-core';
import type { AstroGlobal, TopLevelAstro, SSRResult } from '../../@types/astro-runtime';
import type { LogOptions } from '../logger';
import cheerio from 'cheerio';
import * as eslexer from 'es-module-lexer';
import { fileURLToPath } from 'url';
import fs from 'fs';
import path from 'path';
import { renderPage } from '../../runtime/server/index.js';
import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from '../util.js';
import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
interface SSROptions {
/** an instance of the AstroConfig */
astroConfig: AstroConfig;
/** location of file on disk */
filePath: URL;
/** logging options */
logging: LogOptions;
/** "development" or "production" */
mode: RuntimeMode;
/** production website, needed for some RSS & Sitemap functions */
origin: string;
/** the web request (needed for dynamic routes) */
pathname: string;
/** optional, in case we need to render something outside of a dev server */
route?: RouteData;
/** pass in route cache because SSR cant manage cache-busting */
routeCache: RouteCache;
/** Vite instance */
viteServer: ViteDevServer;
}
// note: not every request has a Vite browserHash. if we ever receive one, hang onto it
// this prevents client-side errors such as the "double React bug" (https://reactjs.org/warnings/invalid-hook-call-warning.html#mismatching-versions-of-react-and-react-dom)
let browserHash: string | undefined;
const cache = new Map<string, Promise<Renderer>>();
// TODO: improve validation and error handling here.
async function resolveRenderer(viteServer: ViteDevServer, renderer: string) {
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 import(renderer);
resolvedRenderer.name = name;
if (client) resolvedRenderer.source = path.posix.join(renderer, client);
if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => path.posix.join(renderer, src));
if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => path.posix.join(renderer, src));
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(path.posix.join(renderer, server));
const { default: rendererSSR } = await viteServer.ssrLoadModule(url);
resolvedRenderer.ssr = rendererSSR;
const completedRenderer: Renderer = resolvedRenderer;
return completedRenderer;
}
async function resolveRenderers(viteServer: ViteDevServer, ids: string[]): Promise<Renderer[]> {
const renderers = await Promise.all(
ids.map((renderer) => {
if (cache.has(renderer)) return cache.get(renderer)!;
let promise = resolveRenderer(viteServer, renderer);
cache.set(renderer, promise);
return promise;
})
);
return renderers;
}
/** use Vite to SSR */
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
try {
// 1. resolve renderers
// Important this happens before load module in case a renderer provides polyfills.
const renderers = await resolveRenderers(viteServer, astroConfig.renderers);
// 1.5. load module
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
// 2. handle dynamic routes
let params: Params = {};
let pageProps: Props = {};
if (route && !route.pathname) {
if (route.params.length) {
const paramsMatch = route.pattern.exec(pathname);
if (paramsMatch) {
params = getParams(route.params)(paramsMatch);
}
}
validateGetStaticPathsModule(mod);
routeCache[route.component] =
routeCache[route.component] ||
(
await mod.getStaticPaths!({
paginate: generatePaginateFunction(route),
rss: () => {
/* noop */
},
})
).flat();
validateGetStaticPathsResult(routeCache[route.component], logging);
const routePathParams: GetStaticPathsResult = routeCache[route.component];
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
if (!matchedStaticPath) {
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
}
pageProps = { ...matchedStaticPath.props } || {};
}
// 3. render page
if (!browserHash && (viteServer as any)._optimizeDepsMetadata?.browserHash) browserHash = (viteServer as any)._optimizeDepsMetadata.browserHash; // note: this is "private" and may change over time
const fullURL = new URL(pathname, origin);
const Component = await mod.default;
const ext = path.posix.extname(filePath.pathname);
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
const result: SSRResult = {
styles: new Set(),
scripts: new Set(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params: {},
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
} as unknown as AstroGlobal;
},
_metadata: { renderers },
};
let html = await renderPage(result, Component, pageProps, null);
// 4. modify response
if (mode === 'development') {
// inject Astro HMR code
html = injectAstroHMR(html);
// inject Vite HMR code
html = injectViteClient(html);
// replace client hydration scripts
html = resolveNpmImports(html);
}
// 5. finish
return html;
} catch (e: any) {
viteServer.ssrFixStacktrace(e);
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
if (e.errors) {
const { location, pluginName, text } = (e as BuildResult).errors[0];
const err = new Error(text) as SSRError;
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
const frame = codeFrame(await fs.promises.readFile(filePath, 'utf8'), err.loc);
err.frame = frame;
err.id = location?.file;
err.message = `${location?.file}: ${text}
${frame}
`;
err.stack = e.stack;
if (pluginName) err.plugin = pluginName;
throw err;
}
// Vite error (already formatted)
throw e;
}
}
/** Injects Vite client code */
function injectViteClient(html: string): string {
return html.replace('<head>', `<head><script type="module" src="/@vite/client"></script>`);
}
/** Injects Astro HMR client code */
function injectAstroHMR(html: string): string {
return html.replace('<head>', `<head><script type="module" src="/@astro/runtime/client/hmr"></script>`);
}
/** Convert npm specifier into Vite URL */
function resolveViteNpmPackage(spec: string): string {
const pkg = parseNpmName(spec);
if (!pkg) return spec;
let viteURL = '/node_modules/.vite/'; // start with /node_modules/.vite
viteURL += `${pkg.name}${pkg.subpath ? pkg.subpath.substr(1) : ''}`.replace(/[\/\.]/g, '_'); // flatten package name by replacing slashes (and dots) with underscores
viteURL += '.js'; // add .js
if (browserHash) viteURL += `?v=${browserHash}`; // add browserHash (if provided)
return viteURL;
}
/** Replaces npm imports with Vite-friendly paths */
function resolveNpmImports(html: string): string {
const $ = cheerio.load(html);
// find all <script type="module">
const moduleScripts = $('script[type="module"]');
if (!moduleScripts.length) return html; // if none, return
// for each <script>, update all npm imports with Vite-friendly imports
moduleScripts.each((_, el) => {
let code = $(el).html() || '';
if (!code || $(el).attr('src')) return;
try {
const scan = () => eslexer.parse(code)[0].filter(({ n }) => n && parseNpmName(n));
let specs = scan();
while (specs.length) {
const next = specs[0];
let pkgName = resolveViteNpmPackage(next.n as string);
if (next.d !== -1) pkgName = JSON.stringify(pkgName); // if dynamic import, stringify
code = code.substring(0, next.s) + pkgName + code.substring(next.e);
specs = scan();
}
$(el).html(code);
} catch (err) {
// if invalid JS, ignore (error will be thrown elsewhere)
}
});
return $.html();
}

View file

@ -1,4 +1,4 @@
import { GetStaticPathsResult, PaginatedCollectionProp, PaginateFunction, Params, Props, RouteData } from '../@types/astro';
import { GetStaticPathsResult, PaginatedCollectionProp, PaginateFunction, Params, Props, RouteData } from '../../@types/astro-core';
// return filters.map((filter) => {
// const filteredRecipes = allRecipes.filter((recipe) =>

View file

@ -1,9 +1,70 @@
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, Params, RouteData } from '../../@types/astro-core';
import type { LogOptions } from '../logger';
import fs from 'fs';
import path from 'path';
import { compile } from 'path-to-regexp';
import slash from 'slash';
import { fileURLToPath } from 'url';
import { AstroConfig, ManifestData, RouteData } from '../@types/astro';
import { warn } from '../logger.js';
/**
* given an array of params like `['x', 'y', 'z']` for
* src/routes/[x]/[y]/[z]/svelte, create a function
* that turns a RegExpExecArray into ({ x, y, z })
*/
export function getParams(array: string[]) {
const fn = (match: RegExpExecArray) => {
const params: Params = {};
array.forEach((key, i) => {
if (key.startsWith('...')) {
params[key.slice(3)] = match[i + 1] ? decodeURIComponent(match[i + 1]) : undefined;
} else {
params[key] = decodeURIComponent(match[i + 1]);
}
});
return params;
};
return fn;
}
/** Find matching route from pathname */
export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
return manifest.routes.find((route) => route.pattern.test(pathname));
}
/** Throw error for deprecated/malformed APIs */
export function validateGetStaticPathsModule(mod: ComponentInstance) {
if ((mod as any).createCollection) {
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
}
if (!mod.getStaticPaths) {
throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
}
}
/** Throw error for malformed getStaticPaths() response */
export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
if (!Array.isArray(result)) {
throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`);
}
result.forEach((pathObject) => {
if (!pathObject.params) {
warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`);
return;
}
for (const [key, val] of Object.entries(pathObject.params)) {
if (!(typeof val === 'undefined' || typeof val === 'string')) {
warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`);
}
if (val === '') {
warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`);
}
}
});
}
interface Part {
content: string;
dynamic: boolean;
@ -21,10 +82,8 @@ interface Item {
routeSuffix: string;
}
// Needed?
// const specials = new Set([]);
export function createManifest({ config, cwd }: { config: AstroConfig; cwd?: string }): ManifestData {
/** Create manifest of all static routes */
export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?: string }): ManifestData {
const components: string[] = [];
const routes: RouteData[] = [];
@ -129,7 +188,7 @@ export function createManifest({ config, cwd }: { config: AstroConfig; cwd?: str
params,
component,
generate,
path: pathname,
pathname: pathname || undefined,
});
}
});

View file

@ -1,6 +1,7 @@
import type { RSSFunctionArgs, RouteData } from '../@types/astro';
import type { RSSFunction, RSSFunctionArgs, RSSResult, RouteData } from '../../@types/astro-core';
import parser from 'fast-xml-parser';
import { canonicalURL } from './util.js';
import { canonicalURL } from '../util.js';
/** Validates getStaticPaths.rss */
export function validateRSS(args: GenerateRSSArgs): void {
@ -70,16 +71,19 @@ export function generateRSS(args: GenerateRSSArgs): string {
return xml;
}
export function generateRssFunction(site: string | undefined, routeMatch: RouteData): [(args: any) => void, { url?: string; xml?: string }] {
let result: { url?: string; xml?: string } = {};
function rssUtility(args: any) {
if (!site) {
throw new Error(`[${routeMatch.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
}
const { dest, ...rssData } = args;
const feedURL = dest || '/rss.xml';
result.url = feedURL;
result.xml = generateRSS({ rssData, site, srcFile: routeMatch.component, feedURL });
}
return [rssUtility, result];
/** Generated function to be run */
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult } {
let result: RSSResult = {} as any;
return {
generator: function rssUtility(args: any) {
if (!site) {
throw new Error(`[${route.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
}
const { dest, ...rssData } = args;
const feedURL = dest || '/feed.xml';
result.url = feedURL;
result.xml = generateRSS({ rssData, site, srcFile: route.component, feedURL });
},
rss: result,
};
}

View file

@ -0,0 +1,14 @@
/** Construct sitemap.xml given a set of URLs */
export function generateSitemap(pages: string[]): string {
// TODO: find way to respect <link rel="canonical"> URLs here
// TODO: find way to exclude pages from sitemap
const urls = [...pages]; // copy just in case original copy is needed
urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
let sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
for (const url of urls) {
sitemap += `<url><loc>${url}</loc></url>`;
}
sitemap += `</urlset>\n`;
return sitemap;
}

View file

@ -0,0 +1,77 @@
import type { ErrorPayload } from 'vite';
import fs from 'fs';
import path from 'path';
/** Normalize URL to its canonical form */
export function canonicalURL(url: string, base?: string): URL {
let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical
pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections)
if (!path.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if theres no extension
pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() wont)
return new URL(pathname, base);
}
/** get user dependency list for Vite */
export async function getPackageJSON(projectRoot: URL): Promise<Record<string, any> | undefined> {
const possibleLocs = new Set(['./package.json']);
for (const possibleLoc of possibleLocs) {
const packageJSONLoc = new URL(possibleLoc, projectRoot);
if (fs.existsSync(packageJSONLoc)) {
return JSON.parse(await fs.promises.readFile(packageJSONLoc, 'utf8'));
}
}
}
/** is a specifier an npm package? */
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined {
// not an npm package
if (!spec || spec[0] === '.' || spec[0] === '/') return undefined;
let scope: string | undefined;
let name = '';
let parts = spec.split('/');
if (parts[0][0] === '@') {
scope = parts[0];
name = parts.shift() + '/';
}
name += parts.shift();
let subpath = parts.length ? `./${parts.join('/')}` : undefined;
return {
scope,
name,
subpath,
};
}
/** generate code frame from esbuild error */
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
if (!loc) return '';
const lines = src.replace(/\r\n/g, '\n').split('\n');
// 1. grab 2 lines before, and 3 lines after focused line
const visibleLines = [];
for (let n = -2; n <= 2; n++) {
if (lines[loc.line + n]) visibleLines.push(loc.line + n);
}
// 2. figure out gutter width
let gutterWidth = 0;
for (const lineNo of visibleLines) {
let w = `> ${lineNo}`;
if (w.length > gutterWidth) gutterWidth = w.length;
}
// 3. print lines
let output = '';
for (const lineNo of visibleLines) {
const isFocusedLine = lineNo === loc.line - 1;
output += isFocusedLine ? '> ' : ' ';
output += `${lineNo + 1} | ${lines[lineNo]}\n`;
if (isFocusedLine) output += `${[...new Array(gutterWidth)].join(' ')} | ${[...new Array(loc.column)].join(' ')}^\n`;
}
return output;
}

View file

@ -1,109 +0,0 @@
import type { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger.js';
import { green } from 'kleur/colors';
import http from 'http';
import path from 'path';
import { performance } from 'perf_hooks';
import { defaultLogDestination, defaultLogLevel, debug, error, info, parseError } from './logger.js';
import { createRuntime } from './runtime.js';
import { stopTimer } from './build/util.js';
const logging: LogOptions = {
level: defaultLogLevel,
dest: defaultLogDestination,
};
/** The primary dev action */
export default async function dev(astroConfig: AstroConfig) {
const startServerTime = performance.now();
const { projectRoot } = astroConfig;
const timer: Record<string, number> = {};
timer.runtime = performance.now();
const runtime = await createRuntime(astroConfig, { mode: 'development', logging });
debug(logging, 'dev', `runtime created [${stopTimer(timer.runtime)}]`);
const server = http.createServer(async (req, res) => {
timer.load = performance.now();
const result = await runtime.load(req.url);
debug(logging, 'dev', `loaded ${req.url} [${stopTimer(timer.load)}]`);
switch (result.statusCode) {
case 200: {
if (result.contentType) {
res.setHeader('Content-Type', result.contentType);
}
res.statusCode = 200;
res.write(result.contents);
res.end();
break;
}
case 404: {
const { hostname, port } = astroConfig.devOptions;
const fullurl = new URL(req.url || '/', astroConfig.buildOptions.site || `http://${hostname}:${port}`);
const reqPath = decodeURI(fullurl.pathname);
error(logging, 'access', 'Not Found:', reqPath);
res.statusCode = 404;
const fourOhFourResult = await runtime.load('/404');
if (fourOhFourResult.statusCode === 200) {
if (fourOhFourResult.contentType) {
res.setHeader('Content-Type', fourOhFourResult.contentType);
}
res.write(fourOhFourResult.contents);
} else {
res.setHeader('Content-Type', 'text/plain');
res.write('Not Found');
}
res.end();
break;
}
case 500: {
res.setHeader('Content-Type', 'text/html;charset=utf-8');
switch (result.type) {
case 'parse-error': {
const err = result.error;
if (err.filename) err.filename = path.posix.relative(projectRoot.pathname, err.filename);
parseError(logging, err);
break;
}
default: {
error(logging, 'executing astro', result.error);
break;
}
}
res.statusCode = 500;
let errorResult = await runtime.load(`/500?error=${encodeURIComponent(result.error.stack || result.error.toString())}`);
if (errorResult.statusCode === 200) {
if (errorResult.contentType) {
res.setHeader('Content-Type', errorResult.contentType);
}
res.write(errorResult.contents);
} else {
res.write(result.error.toString());
}
res.end();
break;
}
}
});
const { hostname, port } = astroConfig.devOptions;
server
.listen(port, hostname, () => {
const endServerTime = performance.now();
info(logging, 'dev server', green(`Server started in ${Math.floor(endServerTime - startServerTime)}ms.`));
info(logging, 'dev server', `${green('Local:')} http://${hostname}:${port}/`);
})
.on('error', (err: NodeJS.ErrnoException) => {
if (err.code && err.code === 'EADDRINUSE') {
error(logging, 'dev server', `Address ${hostname}:${port} already in use. Try changing devOptions.port in your config file`);
} else {
error(logging, 'dev server', err.stack);
}
process.exit(1);
});
}

View file

@ -1,35 +0,0 @@
import { createRequire } from 'module';
import { nodeBuiltinsMap } from './node_builtins.js';
const require = createRequire(import.meta.url);
const pkg = require('../package.json');
/**
* This file allows us to automatically exclude
* particular packages from Snowpack's `esinstall`
* step.
*/
// These packages SHOULD be built by `esinstall`
const allowList = new Set(['astring', '@astrojs/prism', 'estree-util-value-to-estree', 'prismjs', 'shorthash']);
const isAstroRenderer = (name: string) => {
return name.startsWith(`@astrojs/renderer-`);
};
// These packages should NOT be built by `esinstall`
// But might not be explicit dependencies of `astro`
const denyList = ['prismjs/components/index.js', '@astrojs/markdown-support', 'node:fs/promises', ...nodeBuiltinsMap.values()];
export default Object.keys(pkg.dependencies)
// Filter out packages that should be loaded through Snowpack
.filter((name) => {
// Explicitly allowed packages should NOT be external
if (allowList.has(name)) return false;
// Astro renderers should NOT be external
if (isAstroRenderer(name)) return false;
// Everything else SHOULD be external
return true;
})
// Add extras
.concat(denyList)
.sort();

View file

@ -1,128 +0,0 @@
---
import { Prism } from 'astro/components';
let title = 'Uh oh...';
const error = Astro.request.url.searchParams.get('error');
---
<!doctype html>
<html lang="en">
<head>
<title>Error 500</title>
<link rel="preconnect"href="https://fonts.gstatic.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans:wght@400;700&display=swap">
<link rel="stylesheet" href="http://cdn.skypack.dev/prism-themes/themes/prism-material-dark.css">
<style>
* {
box-sizing: border-box;
margin: 0;
}
:global(:root) {
--font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
--font-mono: "IBM Plex Mono", Consolas, "Andale Mono WT", "Andale Mono",
"Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono",
"Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco,
"Courier New", Courier, monospace;
--color-gray-800: #1F2937;
--color-gray-500: #6B7280;
--color-gray-400: #9CA3AF;
--color-gray-100: #F3F4F6;
--color-red: #FF1639;
}
html, body {
width: 100vw;
height: 100%;
min-height: 100vh;
font-family: var(--font-sans);
font-weight: 400;
background: var(--color-gray-100);
text-align: center;
}
body {
display: grid;
place-content: center;
}
header {
display: flex;
flex-direction: column;
align-items: center;
font-family: var(--font-sans);
font-size: 2.5rem;
font-size: clamp(24px, calc(2vw + 1rem), 2.5rem);
}
header h1 {
margin: 0.25em;
margin-right: 0;
font-weight: 400;
letter-spacing: -2px;
line-height: 1;
}
header h1 .title {
color: var(--color-gray-400);
white-space: nowrap;
}
header svg {
margin-bottom: -0.125em;
color: var(--color-red);
}
p {
font-size: 1.75rem;
font-size: clamp(14px, calc(2vw + 0.5rem), 1.75rem);
flex: 1;
}
.error-message {
display: grid;
justify-content: center;
margin-top: 4rem;
}
.error-message :global(code[class*="language-"]) {
background: var(--color-gray-800);
}
.error-message :global(pre) {
margin: 0;
font-family: var(--font-mono);
font-size: 0.85rem;
background: var(--color-gray-800);
border-radius: 8px;
}
.error-message :global(.token.punctuation) {
color: var(--color-gray-400);
}
.error-message :global(.token.operator) {
color: var(--color-gray-400);
}
</style>
</head>
<body>
<main>
<header>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="1.75em" height="1.75em">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h1><span class="error">500 Error </span><span class="title">{title}</span></h1>
</header>
<article>
<p>Astro had some trouble loading this page.</p>
<div class="error-message">
<Prism lang="shell" code={error} />
</div>
</article>
</main>
</body>
</html>

View file

@ -1,6 +0,0 @@
import type { RendererInstance } from '../internal/__astro_component';
declare function setRenderers(instances: RendererInstance[]): void;
declare let rendererInstances: RendererInstance[];
setRenderers(rendererInstances);

View file

@ -1,255 +0,0 @@
import type { Renderer, AstroComponentMetadata } from '../@types/astro';
import hash from 'shorthash';
import { valueToEstree, Value } from 'estree-util-value-to-estree';
import { generate, GENERATOR, Generator } from 'astring';
import * as astroHtml from './renderer-html';
// A more robust version alternative to `JSON.stringify` that can handle most values
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
const customGenerator: Generator = {
...GENERATOR,
Literal(node, state) {
if (node.raw != null) {
// escape closing script tags in strings so browsers wouldn't interpret them as
// closing the actual end tag in HTML
state.write(node.raw.replace('</script>', '<\\/script>'));
} else {
GENERATOR.Literal(node, state);
}
},
};
const serialize = (value: Value) =>
generate(valueToEstree(value), {
generator: customGenerator,
});
export interface RendererInstance {
name: string | null;
source: string | null;
renderer: Renderer;
polyfills: string[];
hydrationPolyfills: string[];
}
const astroHtmlRendererInstance: RendererInstance = {
name: null,
source: '',
renderer: astroHtml as Renderer,
polyfills: [],
hydrationPolyfills: [],
};
let rendererInstances: RendererInstance[] = [];
export function setRenderers(_rendererInstances: RendererInstance[]) {
rendererInstances = ([] as RendererInstance[]).concat(_rendererInstances);
}
function isCustomElementTag(name: unknown) {
return typeof name === 'string' && /-/.test(name);
}
const rendererCache = new Map<any, RendererInstance>();
/** For client:only components, attempt to infer the required renderer. */
function inferClientRenderer(metadata: Partial<AstroComponentMetadata>) {
// If there's only one renderer, assume it's the required renderer
if (rendererInstances.length === 1) {
return rendererInstances[0];
} else if (metadata.value) {
// Attempt to find the renderer by matching the hydration value
const hint = metadata.value;
let match = rendererInstances.find((instance) => instance.name === hint);
if (!match) {
// Didn't find an exact match, try shorthand hints for the internal renderers
const fullHintName = `@astrojs/renderer-${hint}`;
match = rendererInstances.find((instance) => instance.name === fullHintName);
}
if (!match) {
throw new Error(
`Couldn't find a renderer for <${metadata.displayName} client:only="${metadata.value}" />. Is there a renderer that matches the "${metadata.value}" hint in your Astro config?`
);
}
return match;
} else {
// Multiple renderers included but no hint was provided
throw new Error(
`Can't determine the renderer for ${metadata.displayName}. Include a hint similar to <${metadata.displayName} client:only="react" /> when multiple renderers are included in your Astro config.`
);
}
}
/** For a given component, resolve the renderer. Results are cached if this instance is encountered again */
async function resolveRenderer(Component: any, props: any = {}, children?: string, metadata: Partial<AstroComponentMetadata> = {}): Promise<RendererInstance | undefined> {
// For client:only components, the component can't be imported
// during SSR. We need to infer the required renderer.
if (metadata.hydrate === 'only') {
return inferClientRenderer(metadata);
}
if (rendererCache.has(Component)) {
return rendererCache.get(Component);
}
const errors: Error[] = [];
for (const instance of rendererInstances) {
const { renderer } = instance;
// Yes, we do want to `await` inside of this loop!
// __renderer.check can't be run in parallel, it
// returns the first match and skips any subsequent checks
try {
const shouldUse: boolean = await renderer.check(Component, props, children);
if (shouldUse) {
rendererCache.set(Component, instance);
return instance;
}
} catch (err) {
errors.push(err);
}
}
if (errors.length) {
// For now just throw the first error we encounter.
throw errors[0];
}
}
interface HydrateScriptOptions {
instance: RendererInstance;
astroId: string;
props: any;
}
/** For hydrated components, generate a <script type="module"> to load the component */
async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>) {
const { instance, astroId, props } = scriptOptions;
const { source } = instance;
const { hydrate, componentUrl, componentExport } = metadata;
let hydrationSource = '';
if (instance.hydrationPolyfills.length) {
hydrationSource += `await Promise.all([${instance.hydrationPolyfills.map((src) => `import("${src}")`).join(', ')}]);\n`;
}
hydrationSource += source
? `
const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${source}")]);
return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
`
: `
await import("${componentUrl}");
return () => {};
`;
const hydrationScript = `<script type="module">
import setup from '/_astro_frontend/hydrate/${hydrate}.js';
setup("${astroId}", {${metadata.value ? `value: "${metadata.value}"` : ''}}, async () => {
${hydrationSource}
});
</script>`;
return hydrationScript;
}
const getComponentName = (Component: any, componentProps: any) => {
if (componentProps.displayName) return componentProps.displayName;
switch (typeof Component) {
case 'function':
return Component.displayName ?? Component.name;
case 'string':
return Component;
default: {
return Component;
}
}
};
const prepareSlottedChildren = (children: string | Record<any, any>[]) => {
const $slots: Record<string, string> = {
default: '',
};
for (const child of children) {
if (typeof child === 'string') {
$slots.default += child;
} else if (typeof child === 'object' && child['$slot']) {
if (!$slots[child['$slot']]) $slots[child['$slot']] = '';
$slots[child['$slot']] += child.children.join('').replace(new RegExp(`slot="${child['$slot']}"\s*`, ''));
}
}
return { $slots };
};
const removeSlottedChildren = (_children: string | Record<any, any>[]) => {
let children = '';
for (const child of _children) {
if (typeof child === 'string') {
children += child;
} else if (typeof child === 'object' && child['$slot']) {
children += child.children.join('');
}
}
return children;
};
/** The main wrapper for any components in Astro files */
export function __astro_component(Component: any, metadata: AstroComponentMetadata = {} as any) {
if (Component == null) {
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
} else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
}
return async function __astro_component_internal(props: any, ..._children: any[]) {
if (Component.isAstroComponent) {
return Component.__render(props, prepareSlottedChildren(_children));
}
const children = removeSlottedChildren(_children);
let instance = await resolveRenderer(Component, props, children, metadata);
if (!instance) {
if (isCustomElementTag(Component)) {
instance = astroHtmlRendererInstance;
} else {
// If the user only specifies a single renderer, but the check failed
// for some reason... just default to their preferred renderer.
instance = rendererInstances.length === 2 ? rendererInstances[1] : undefined;
}
if (!instance) {
const name = getComponentName(Component, metadata);
throw new Error(`No renderer found for ${name}! Did you forget to add a renderer to your Astro config?`);
}
}
let html = '';
// Skip SSR for components using client:only hydration
if (metadata.hydrate !== 'only') {
const rendered = await instance.renderer.renderToStaticMarkup(Component, props, children, metadata);
html = rendered.html;
}
if (instance.polyfills.length) {
let polyfillScripts = instance.polyfills.map((src) => `<script type="module" src="${src}"></script>`).join('');
html = html + polyfillScripts;
}
// If we're NOT hydrating this component, just return the HTML
if (!metadata.hydrate) {
// It's safe to remove <astro-fragment>, static content doesn't need the wrapper
return html.replace(/\<\/?astro-fragment\>/g, '');
}
// If we ARE hydrating this component, let's generate the hydration script
const uniqueId = props[Symbol.for('astro.context')].createAstroRootUID(html);
const uniqueIdHashed = hash.unique(uniqueId);
const script = await generateHydrateScript({ instance, astroId: uniqueIdHashed, props }, metadata as Required<AstroComponentMetadata>);
const astroRoot = `<astro-root uid="${uniqueIdHashed}">${html}</astro-root>`;
return [astroRoot, script].join('\n');
};
}

View file

@ -1,35 +0,0 @@
import type { ScriptInfo } from '../@types/astro';
const sym = Symbol.for('astro.hoistedScripts');
interface ComponentThatMaybeHasHoistedScripts {
[sym]: ScriptInfo[];
}
/**
* Takes all of the components this component uses and combines them with its
* own scripts and flattens it to a deduped list.
* The page component will have an array of all scripts used by all child components and itself.
*/
function hoistedScripts(Components: ComponentThatMaybeHasHoistedScripts[], scripts: ScriptInfo[]) {
const flatScripts = [];
const allScripts: ScriptInfo[] = Components.map((c) => c && c[sym])
.filter((a) => a)
.concat(scripts)
.flatMap((a) => a);
const visitedSource = new Set();
for (let script of allScripts) {
if (!('src' in script)) {
flatScripts.push(script);
} else if (!visitedSource.has(script.src)) {
flatScripts.push(script);
visitedSource.add(script.src);
}
}
return flatScripts;
}
export { hoistedScripts as __astro_hoisted_scripts };

View file

@ -1,15 +0,0 @@
/** */
export function __astro_slot_content({ name }: { name: string }, ...children: any[]) {
return { $slot: name, children };
}
export const __astro_slot = ({ name = 'default' }: { name: string }, _children: any, ...fallback: string[]) => {
if (name === 'default' && typeof _children === 'string') {
return _children ? _children : fallback;
}
if (!_children || !_children.$slots) {
throw new Error(`__astro_slot encountered an unexpected child:\n${JSON.stringify(_children)}`);
}
const children = _children.$slots[name];
return children ? children : fallback;
};

View file

@ -1,48 +0,0 @@
import type { AstroComponentMetadata } from '../@types/astro';
type ModuleCandidates = Map<any, string>;
interface RegistryOptions {
candidates: ModuleCandidates;
}
class AstroElementRegistry {
private candidates: ModuleCandidates;
private cache: Map<string, string> = new Map();
constructor(options: RegistryOptions) {
this.candidates = options.candidates;
}
find(tagName: string) {
for (let [module, importSpecifier] of this.candidates) {
if (module && typeof module.tagName === 'string') {
if (module.tagName === tagName) {
// Found!
return importSpecifier;
}
}
}
}
findCached(tagName: string) {
if (this.cache.has(tagName)) {
return this.cache.get(tagName);
}
let specifier = this.find(tagName);
if (specifier) {
this.cache.set(tagName, specifier);
}
return specifier;
}
astroComponentArgs(tagName: string, metadata: AstroComponentMetadata) {
const specifier = this.findCached(tagName);
const outMeta: AstroComponentMetadata = {
...metadata,
componentUrl: specifier || metadata.componentUrl,
};
return [tagName, outMeta];
}
}
export { AstroElementRegistry };

View file

@ -1,21 +0,0 @@
/**
* Convert the result of an `import.meta.globEager()` call to an array of processed
* Markdown content objects. Filter out any non-Markdown files matched in the glob
* result, by default.
*/
export function fetchContent(importMetaGlobResult: Record<string, any>, url: string) {
return [...Object.entries(importMetaGlobResult)]
.map(([spec, mod]) => {
// Only return Markdown files, which export the __content object.
if (!mod.__content) {
return;
}
const urlSpec = new URL(spec, url).pathname.replace(/[\\/\\\\]/, '/');
return {
...mod.__content,
url: urlSpec.includes('/pages/') && urlSpec.replace(/^.*\/pages\//, '/').replace(/\.md$/, ''),
file: new URL(spec, url),
};
})
.filter(Boolean);
}

View file

@ -1,71 +0,0 @@
export type HProps = Record<string, string | boolean> | null | undefined;
export type HChild = string | undefined | (() => string);
export type AstroComponent = (props: HProps, ...children: Array<HChild>) => string;
export type HTag = string | AstroComponent;
const voidTags = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
function* _children(children: Array<HChild>) {
for (let child of children) {
// Special: If a child is a function, call it automatically.
// This lets you do {() => ...} without the extra boilerplate
// of wrapping it in a function and calling it.
if (typeof child === 'function') {
yield child();
} else if (typeof child === 'string') {
yield child;
} else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values.
} else {
yield child;
}
}
}
/** Generator for primary h() function */
function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
if (tag.toLowerCase() === '!doctype') {
yield `<${tag} `;
if (attrs) {
yield Object.keys(attrs).join(' ');
}
yield '>';
return;
}
yield `<${tag}`;
if (attrs) {
for (let [key, value] of Object.entries(attrs)) {
if (value === '') yield ` ${key}=""`;
else if (value == null || value === false) yield '';
else if (value === true) yield ` ${key}`;
else yield ` ${key}="${value}"`;
}
}
yield '>';
// Void tags have no children.
if (voidTags.has(tag)) {
return;
}
yield* _children(children);
yield `</${tag}>`;
}
/** Astros primary h() function. Allows it to use JSX-like syntax. */
export async function h(tag: HTag, attrs: HProps, ...pChildren: Array<Promise<HChild>>) {
const children = await Promise.all(pChildren.flat(Infinity));
if (typeof tag === 'function') {
// We assume it's an astro component
return tag(attrs, ...children);
}
return Array.from(_h(tag, attrs, children)).join('');
}
/** Fragment helper, similar to React.Fragment */
export function Fragment(_: HProps, ...children: Array<HChild>) {
return Array.from(_children(children)).join('');
}

View file

@ -1,10 +0,0 @@
import { h } from './h';
async function renderToStaticMarkup(tag: string, props: Record<string, any>, children: string) {
const html = await h(tag, props, Promise.resolve(children));
return {
html,
};
}
export { renderToStaticMarkup };

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