Expose JSX compilation to renderers (#588)
* feat: add support for `jsxImportSource`, new JSX transform * Renderer: add Solid renderer (#667) * feat: add support for `jsxImportSource`, new JSX transform * WIP: solid renderer * [Renderer] Solid (#656) * feat: add support for `jsxImportSource`, new JSX transform * WIP: solid renderer * Solid renderer: fix SSR of children, hydration (top level) Caveat: cannot hydrate children/descendants of hydrated parents * Fix hydration of fragments * fix: SyntaxError in React/Preact renderers * fix: errors in React/Preact renderers * feat: update react external * chore: update examples * chore: delete old changelog * chore: update astro config Co-authored-by: Nate Moore <nate@skypack.dev> * Changing the preact to Solid (#669) * chore: use new client:visible syntax * fix: dev script issue * chore: cleanup SolidJS example * docs: update framework example docs * chore: cleanup framework-multiple example * fix: remove SolidJS false-positives from Preact renderer * chore: add changeset Co-authored-by: eyelidlessness <eyelidlessness@users.noreply.github.com> Co-authored-by: Abdullah Mzaien <s201540830@kfupm.edu.sa> * feat(create-astro): add Solid support * docs: add JSX options to renderer reference * chore: add changeset for P/React renderers * fix: move react/server.js to external * chore: remove brewfile * Revert "feat: add support for `jsxImportSource`, new JSX transform" This reverts commit 077c4bfc135c58a85d4ebfca6012e90403694d8d. * fix: remove `react-dom/server` from `external` * chore: remove unused dependency * feat: improve JSX error messages * Revert "Revert "feat: add support for `jsxImportSource`, new JSX transform"" This reverts commit f6c2896b9ec6430611fc0abae7d586c42aca87e5. * docs: update jsxImportSource * feat: improve error message * feat: improve error logging for JSX renderers * tests: add jsx-runtime tests * chore: update snowpack Co-authored-by: eyelidlessness <eyelidlessness@users.noreply.github.com> Co-authored-by: Abdullah Mzaien <s201540830@kfupm.edu.sa>
This commit is contained in:
parent
ba6b47eda7
commit
bd18e14a2c
46 changed files with 2947 additions and 2419 deletions
5
.changeset/dull-queens-melt.md
Normal file
5
.changeset/dull-queens-melt.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/renderer-solid': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Initial release
|
10
.changeset/fair-cats-count.md
Normal file
10
.changeset/fair-cats-count.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
'@astrojs/renderer-preact': minor
|
||||||
|
'@astrojs/renderer-react': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Switches to [the new JSX Transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) originally introduced for React v17. This also leverages the new `jsxTransformOptions` options for renderers.
|
||||||
|
|
||||||
|
This change also removes the need for importing your Framework's `jsxFactory` directly, which means you can wave goodbye to `import React from "react";` and `import { h } from "preact";`.
|
||||||
|
|
||||||
|
> **If you are using mutliple frameworks** and a file doesn't reference `react` or `preact`, Astro might not be able to locate the correct renderer! You can add a pragma comment like `/** @jsxImportSource preact */` to the top of your file. Alternatively, just import the JSX pragma as you traditionally would have.
|
5
.changeset/five-bobcats-joke.md
Normal file
5
.changeset/five-bobcats-joke.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'create-astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for [Solid](https://www.solidjs.com/)
|
5
.changeset/lemon-yaks-dream.md
Normal file
5
.changeset/lemon-yaks-dream.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/renderer-preact': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update `check` logic to exclude false-positives from SolidJS
|
|
@ -62,9 +62,54 @@ export default {
|
||||||
external: ['dep'] // optional, dependencies that should not be built by snowpack
|
external: ['dep'] // optional, dependencies that should not be built by snowpack
|
||||||
polyfills: ['./shadow-dom-polyfill.js'] // optional, module scripts that should be loaded before client hydration.
|
polyfills: ['./shadow-dom-polyfill.js'] // optional, module scripts that should be loaded before client hydration.
|
||||||
hydrationPolyfills: ['./hydrate-framework.js'] // optional, polyfills that need to run before hydration ever occurs.
|
hydrationPolyfills: ['./hydrate-framework.js'] // optional, polyfills that need to run before hydration ever occurs.
|
||||||
|
jsxImportSource: 'preact', // optional, the name of the library from which JSX is imported
|
||||||
|
jsxTransformOptions: async () => { // optional, a function to transform JSX files
|
||||||
|
const { default: { default: jsx }} = await import('@babel/plugin-transform-react-jsx');
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
jsx({}, { runtime: 'automatic', importSource: 'preact' })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSX Support
|
||||||
|
|
||||||
|
Astro is unique in that it allows you to mix multiple types of JSX/TSX files in a single project. It does this by reading the `jsxImportSource` and `jsxTransformOptions` from renderers and transforming a file with [Babel](https://babeljs.io/).
|
||||||
|
|
||||||
|
#### `jsxImportSource`
|
||||||
|
This is the name of your library (for example `preact` or `react` or `solid-js`) which, if encountered in a file, will signal to Astro that this renderer should be used.
|
||||||
|
|
||||||
|
Users may also manually define `/** @jsxImportSource preact */` in to ensure that the file is processed by this renderer (if, for example, the file has no imports).
|
||||||
|
|
||||||
|
#### `jsxTransformOptions`
|
||||||
|
This is an `async` function that returns information about how to transform matching JSX files with [Babel](https://babeljs.io/). It supports [`plugins`](https://babeljs.io/docs/en/plugins) or [`presets`](https://babeljs.io/docs/en/presets) to be passed directly to Babel.
|
||||||
|
|
||||||
|
> Keep in mind that this transform doesn't need to handle TSX separately from JSX, Astro handles that for you!
|
||||||
|
|
||||||
|
The arguments passed to `jsxTransformOptions` follow Snowpack's `load()` plugin hook. These allow you to pass separate Babel configurations for various conditions, like if your files should be compiled differently in SSR mode.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface JSXTransformOptions {
|
||||||
|
(context: {
|
||||||
|
/** True if builder is in dev mode (`astro dev`) */
|
||||||
|
isDev: boolean;
|
||||||
|
/** True if HMR is enabled (add any HMR code to the output here). */
|
||||||
|
isHmrEnabled: boolean;
|
||||||
|
/** True if builder is in SSR mode */
|
||||||
|
isSSR: boolean;
|
||||||
|
/** True if file being transformed is inside of a package. */
|
||||||
|
isPackage: boolean;
|
||||||
|
}) => {
|
||||||
|
plugins?: any[];
|
||||||
|
presets?: any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
### Server Entrypoint (`server.js`)
|
### Server Entrypoint (`server.js`)
|
||||||
|
|
||||||
The server entrypoint of a renderer is responsible for checking if a component should use this renderer, and if so, how that component should be rendered to a string of static HTML.
|
The server entrypoint of a renderer is responsible for checking if a component should use this renderer, and if so, how that component should be rendered to a string of static HTML.
|
||||||
|
|
|
@ -7,5 +7,3 @@ npm init astro -- --template framework-multiple
|
||||||
This example showcases Astro's built-in support for multiple frameworks ([React](https://reactjs.org), [Preact](https://preactjs.com), [Svelte](https://svelte.dev), and [Vue (`v3.x`)](https://v3.vuejs.org/)).
|
This example showcases Astro's built-in support for multiple frameworks ([React](https://reactjs.org), [Preact](https://preactjs.com), [Svelte](https://svelte.dev), and [Vue (`v3.x`)](https://v3.vuejs.org/)).
|
||||||
|
|
||||||
No configuration is needed to enable these frameworks—just start writing components in `src/components`.
|
No configuration is needed to enable these frameworks—just start writing components in `src/components`.
|
||||||
|
|
||||||
> **Note**: If used, components _must_ include a JSX factory (ex. `import React from "react"`, `import { h } from "preact"`). Astro is unable to determine which framework is used without having the [JSX factory](https://mariusschulz.com/blog/per-file-jsx-factories-in-typescript#what-is-a-jsx-factory) in scope.
|
|
||||||
|
|
|
@ -11,5 +11,11 @@ export default {
|
||||||
// port: 3000, // The port to run the dev server on.
|
// port: 3000, // The port to run the dev server on.
|
||||||
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
||||||
},
|
},
|
||||||
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-react', '@astrojs/renderer-svelte', '@astrojs/renderer-vue'],
|
renderers: [
|
||||||
|
'@astrojs/renderer-preact',
|
||||||
|
'@astrojs/renderer-react',
|
||||||
|
'@astrojs/renderer-svelte',
|
||||||
|
'@astrojs/renderer-vue',
|
||||||
|
'@astrojs/renderer-solid',
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { h, Fragment } from 'preact';
|
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
/** a counter written in Preact */
|
/** a counter written in Preact */
|
||||||
|
|
12
examples/framework-multiple/src/components/PreactSFC.tsx
Normal file
12
examples/framework-multiple/src/components/PreactSFC.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/** @jsxImportSource preact */
|
||||||
|
|
||||||
|
/** a counter written in Preact */
|
||||||
|
export default function PreactSFC({ children }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="counter">
|
||||||
|
Hello from Preact!
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
/** a counter written in React */
|
/** a counter written in React */
|
||||||
export function Counter({ children }) {
|
export function Counter({ children }) {
|
||||||
|
|
21
examples/framework-multiple/src/components/SolidCounter.tsx
Normal file
21
examples/framework-multiple/src/components/SolidCounter.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
/** a counter written with Solid */
|
||||||
|
export default function SolidCounter({ children }) {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const add = () => setCount(count() + 1);
|
||||||
|
const subtract = () => setCount(count() - 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="solid" class="counter">
|
||||||
|
<button onClick={subtract}>-</button>
|
||||||
|
<pre>{count()}</pre>
|
||||||
|
<button onClick={add}>+</button>
|
||||||
|
</div>
|
||||||
|
<div class="children">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,10 +3,11 @@
|
||||||
import { A, B as Renamed } from '../components';
|
import { A, B as Renamed } from '../components';
|
||||||
import * as react from '../components/ReactCounter.jsx';
|
import * as react from '../components/ReactCounter.jsx';
|
||||||
import { PreactCounter } from '../components/PreactCounter.tsx';
|
import { PreactCounter } from '../components/PreactCounter.tsx';
|
||||||
|
import PreactSFC from '../components/PreactSFC.tsx';
|
||||||
|
import SolidCounter from '../components/SolidCounter.tsx';
|
||||||
import VueCounter from '../components/VueCounter.vue';
|
import VueCounter from '../components/VueCounter.vue';
|
||||||
import SvelteCounter from '../components/SvelteCounter.svelte';
|
import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Full Astro Component Syntax:
|
// Full Astro Component Syntax:
|
||||||
// https://docs.astro.build/core-concepts/astro-components/
|
// https://docs.astro.build/core-concepts/astro-components/
|
||||||
---
|
---
|
||||||
|
@ -45,6 +46,10 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||||
<h1>Hello Preact!</h1>
|
<h1>Hello Preact!</h1>
|
||||||
</PreactCounter>
|
</PreactCounter>
|
||||||
|
|
||||||
|
<SolidCounter client:visible>
|
||||||
|
<h1>Hello Solid!</h1>
|
||||||
|
</SolidCounter>
|
||||||
|
|
||||||
<VueCounter client:visible>
|
<VueCounter client:visible>
|
||||||
<h1>Hello Vue!</h1>
|
<h1>Hello Vue!</h1>
|
||||||
</VueCounter>
|
</VueCounter>
|
||||||
|
|
|
@ -1,11 +1,38 @@
|
||||||
# Using Preact with Astro
|
# Using Preact with Astro
|
||||||
|
|
||||||
```
|
This example showcases Astro's built-in support for [Preact](https://www.preactjs.com/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Automatic
|
||||||
|
|
||||||
|
Bootstrap your Astro project with this template!
|
||||||
|
|
||||||
|
```shell
|
||||||
npm init astro -- --template framework-preact
|
npm init astro -- --template framework-preact
|
||||||
```
|
```
|
||||||
|
|
||||||
This example showcases Astro's built-in support for [Preact](https://preactjs.com/).
|
### Manual
|
||||||
|
|
||||||
No configuration is needed to enable Preact support—just start writing Preact components in `src/components`.
|
To use Preact components in your Astro project:
|
||||||
|
|
||||||
> **Note**: If used, components _must_ include the JSX factory (ex. `import { h } from "preact"`). Astro is unable to determine which framework is used without having the [JSX factory](https://mariusschulz.com/blog/per-file-jsx-factories-in-typescript#what-is-a-jsx-factory) in scope.
|
1. Install `@astrojs/renderer-preact`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i @astrojs/renderer-preact
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `"@astrojs/renderer-preact"` to your `renderers` in `astro.config.mjs`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-preact",
|
||||||
|
// optionally, others...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write your Preact components as `.jsx` or `.tsx` files in your project.
|
||||||
|
|
|
@ -1,11 +1,38 @@
|
||||||
# Using React with Astro
|
# Using React with Astro
|
||||||
|
|
||||||
```
|
|
||||||
npm init astro -- --template framework-react
|
|
||||||
```
|
|
||||||
|
|
||||||
This example showcases Astro's built-in support for [React](https://reactjs.org/).
|
This example showcases Astro's built-in support for [React](https://reactjs.org/).
|
||||||
|
|
||||||
No configuration is needed to enable React support—just start writing React components in `src/components`.
|
## Installation
|
||||||
|
|
||||||
> **Note**: If used, components _must_ include the JSX factory (ex. `import React from "react"`). Astro is unable to determine which framework is used without having the [JSX factory](https://mariusschulz.com/blog/per-file-jsx-factories-in-typescript#what-is-a-jsx-factory) in scope.
|
### Automatic
|
||||||
|
|
||||||
|
Bootstrap your Astro project with this template!
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm init astro -- --template framework-react
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
To use React components in your Astro project:
|
||||||
|
|
||||||
|
1. Install `@astrojs/renderer-react`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i @astrojs/renderer-react
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `"@astrojs/renderer-react"` to your `renderers` in `astro.config.mjs`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-react",
|
||||||
|
// optionally, others...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write your React components as `.jsx` or `.tsx` files in your project.
|
||||||
|
|
18
examples/framework-solid/.gitignore
vendored
Normal file
18
examples/framework-solid/.gitignore
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# build output
|
||||||
|
dist
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
.snowpack/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
2
examples/framework-solid/.npmrc
Normal file
2
examples/framework-solid/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## force pnpm to hoist
|
||||||
|
shamefully-hoist = true
|
38
examples/framework-solid/README.md
Normal file
38
examples/framework-solid/README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Using Solid with Astro
|
||||||
|
|
||||||
|
This example showcases Astro's built-in support for [Solid](https://www.solidjs.com/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Automatic
|
||||||
|
|
||||||
|
Bootstrap your Astro project with this template!
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm init astro --template framework-solid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
To use Solid components in your Astro project:
|
||||||
|
|
||||||
|
1. Install `@astrojs/renderer-solid`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i @astrojs/renderer-solid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `"@astrojs/renderer-solid"` to your `renderers` in `astro.config.mjs`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-solid",
|
||||||
|
// optionally, others...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write your Solid components as `.jsx` or `.tsx` files in your project.
|
17
examples/framework-solid/astro.config.mjs
Normal file
17
examples/framework-solid/astro.config.mjs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export default {
|
||||||
|
// projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
|
||||||
|
// pages: './src/pages', // Path to Astro components, pages, and data
|
||||||
|
// dist: './dist', // When running `astro build`, path to final static output
|
||||||
|
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
|
||||||
|
buildOptions: {
|
||||||
|
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
|
||||||
|
// sitemap: true, // Generate sitemap (set to "false" to disable)
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
// port: 3000, // The port to run the dev server on.
|
||||||
|
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
||||||
|
},
|
||||||
|
renderers: [
|
||||||
|
'@astrojs/renderer-solid'
|
||||||
|
]
|
||||||
|
};
|
16
examples/framework-solid/package.json
Normal file
16
examples/framework-solid/package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "@example/framework-solid",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"astro": "^0.18.0-next.1",
|
||||||
|
"@astrojs/renderer-solid": "0.0.1"
|
||||||
|
},
|
||||||
|
"snowpack": {
|
||||||
|
"workspaceRoot": "../.."
|
||||||
|
}
|
||||||
|
}
|
21
examples/framework-solid/src/components/Counter.tsx
Normal file
21
examples/framework-solid/src/components/Counter.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
/** */
|
||||||
|
export default function SolidCounter({ children }) {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const add = () => setCount(count() + 1);
|
||||||
|
const subtract = () => setCount(count() - 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="counter">
|
||||||
|
<button onClick={subtract}>-</button>
|
||||||
|
<pre>{count()}</pre>
|
||||||
|
<button onClick={add}>+</button>
|
||||||
|
</div>
|
||||||
|
<div class="children">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
38
examples/framework-solid/src/pages/index.astro
Normal file
38
examples/framework-solid/src/pages/index.astro
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import Counter from '../components/Counter.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:global(:root) {
|
||||||
|
font-family: system-ui;
|
||||||
|
padding: 2em 0;
|
||||||
|
}
|
||||||
|
:global(.counter) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
place-items: center;
|
||||||
|
font-size: 2em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
:global(.children) {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<Counter client:visible>
|
||||||
|
<h1>Hello Solid!</h1>
|
||||||
|
</Counter>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,9 +1,38 @@
|
||||||
# Using Svelte with Astro
|
# Using Svelte with Astro
|
||||||
|
|
||||||
```
|
|
||||||
npm init astro -- --template framework-svelte
|
|
||||||
```
|
|
||||||
|
|
||||||
This example showcases Astro's built-in support for [Svelte](https://svelte.dev/).
|
This example showcases Astro's built-in support for [Svelte](https://svelte.dev/).
|
||||||
|
|
||||||
No configuration is needed to enable Svelte support—just start writing Svelte components in `src/components`.
|
## Installation
|
||||||
|
|
||||||
|
### Automatic
|
||||||
|
|
||||||
|
Bootstrap your Astro project with this template!
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm init astro -- --template framework-svelte
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
To use Svelte components in your Astro project:
|
||||||
|
|
||||||
|
1. Install `@astrojs/renderer-svelte`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i @astrojs/renderer-svelte
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `"@astrojs/renderer-svelte"` to your `renderers` in `astro.config.mjs`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-svelte",
|
||||||
|
// optionally, others...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write your Svelte components as `.svelte` files in your project.
|
||||||
|
|
|
@ -1,9 +1,38 @@
|
||||||
# Using Vue with Astro
|
# Using Vue with Astro
|
||||||
|
|
||||||
```
|
This example showcases Astro's built-in support for [Vue](https://v3.vuejs.org/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Automatic
|
||||||
|
|
||||||
|
Bootstrap your Astro project with this template!
|
||||||
|
|
||||||
|
```shell
|
||||||
npm init astro -- --template framework-vue
|
npm init astro -- --template framework-vue
|
||||||
```
|
```
|
||||||
|
|
||||||
This example showcases Astro's built-in support for [Vue (`v3.x`)](https://v3.vuejs.org/).
|
### Manual
|
||||||
|
|
||||||
No configuration is needed to enable Vue support—just start writing Vue components in `src/components`.
|
To use Vue components in your Astro project:
|
||||||
|
|
||||||
|
1. Install `@astrojs/renderer-vue`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm i @astrojs/renderer-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `"@astrojs/renderer-vue"` to your `renderers` in `astro.config.mjs`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
renderers: [
|
||||||
|
"@astrojs/renderer-vue",
|
||||||
|
// optionally, others...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Write your Vue components as `.vue` files in your project.
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
".": "./astro.cjs",
|
".": "./astro.cjs",
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
"./snowpack-plugin": "./snowpack-plugin.cjs",
|
"./snowpack-plugin": "./snowpack-plugin.cjs",
|
||||||
|
"./snowpack-plugin-jsx": "./snowpack-plugin-jsx.cjs",
|
||||||
"./components": "./components/index.js",
|
"./components": "./components/index.js",
|
||||||
"./components/*": "./components/*",
|
"./components/*": "./components/*",
|
||||||
"./runtime/svelte": "./dist/frontend/runtime/svelte.js",
|
"./runtime/svelte": "./dist/frontend/runtime/svelte.js",
|
||||||
|
@ -49,8 +50,10 @@
|
||||||
"@astrojs/renderer-svelte": "0.1.1",
|
"@astrojs/renderer-svelte": "0.1.1",
|
||||||
"@astrojs/renderer-vue": "0.1.3",
|
"@astrojs/renderer-vue": "0.1.3",
|
||||||
"@babel/code-frame": "^7.12.13",
|
"@babel/code-frame": "^7.12.13",
|
||||||
|
"@babel/core": "^7.14.6",
|
||||||
"@babel/generator": "^7.13.9",
|
"@babel/generator": "^7.13.9",
|
||||||
"@babel/parser": "^7.13.15",
|
"@babel/parser": "^7.13.15",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.14.5",
|
||||||
"@babel/traverse": "^7.13.15",
|
"@babel/traverse": "^7.13.15",
|
||||||
"@snowpack/plugin-postcss": "^1.4.3",
|
"@snowpack/plugin-postcss": "^1.4.3",
|
||||||
"@snowpack/plugin-sass": "^1.4.0",
|
"@snowpack/plugin-sass": "^1.4.0",
|
||||||
|
@ -58,11 +61,12 @@
|
||||||
"astring": "^1.7.4",
|
"astring": "^1.7.4",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
"camel-case": "^4.1.2",
|
"camel-case": "^4.1.2",
|
||||||
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
"cheerio": "^1.0.0-rc.6",
|
"cheerio": "^1.0.0-rc.6",
|
||||||
"ci-info": "^3.2.0",
|
"ci-info": "^3.2.0",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"es-module-lexer": "^0.4.1",
|
"es-module-lexer": "^0.4.1",
|
||||||
"esbuild": "^0.10.1",
|
"esbuild": "^0.12.12",
|
||||||
"estree-util-value-to-estree": "^1.2.0",
|
"estree-util-value-to-estree": "^1.2.0",
|
||||||
"estree-walker": "^3.0.0",
|
"estree-walker": "^3.0.0",
|
||||||
"fast-xml-parser": "^3.19.0",
|
"fast-xml-parser": "^3.19.0",
|
||||||
|
@ -89,7 +93,7 @@
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
"shorthash": "^0.0.2",
|
"shorthash": "^0.0.2",
|
||||||
"slash": "^4.0.0",
|
"slash": "^4.0.0",
|
||||||
"snowpack": "^3.8.1",
|
"snowpack": "^3.8.3",
|
||||||
"string-width": "^5.0.0",
|
"string-width": "^5.0.0",
|
||||||
"tiny-glob": "^0.2.8",
|
"tiny-glob": "^0.2.8",
|
||||||
"unified": "^9.2.1",
|
"unified": "^9.2.1",
|
||||||
|
|
189
packages/astro/snowpack-plugin-jsx.cjs
Normal file
189
packages/astro/snowpack-plugin-jsx.cjs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
error(logging, 'renderer', `${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.`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'.js': {
|
||||||
|
code: `(() => {
|
||||||
|
throw new Error("Hello world!");
|
||||||
|
})()`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 to scan for imports
|
||||||
|
// so let's just use `h` and `Fragment` as placeholders
|
||||||
|
const { code: codeToScan } = await esbuild.transform(code, {
|
||||||
|
loader: 'jsx',
|
||||||
|
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,
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ServerRuntime as SnowpackServerRuntime } from 'snowpack';
|
import type { ServerRuntime as SnowpackServerRuntime, PluginLoadOptions } from 'snowpack';
|
||||||
import type { AstroConfig } from './@types/astro';
|
import type { AstroConfig } from './@types/astro';
|
||||||
import { posix as path } from 'path';
|
import { posix as path } from 'path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'url';
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
|
@ -17,6 +17,8 @@ interface RendererInstance {
|
||||||
external: string[] | undefined;
|
external: string[] | undefined;
|
||||||
polyfills: string[];
|
polyfills: string[];
|
||||||
hydrationPolyfills: 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_BASE_NAME = '__astro_config.js';
|
||||||
|
@ -119,12 +121,18 @@ export class ConfigManager {
|
||||||
external: raw.external,
|
external: raw.external,
|
||||||
polyfills: polyfillsNormalized,
|
polyfills: polyfillsNormalized,
|
||||||
hydrationPolyfills: hydrationPolyfillsNormalized,
|
hydrationPolyfills: hydrationPolyfillsNormalized,
|
||||||
|
jsxImportSource: raw.jsxImportSource
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return rendererInstances;
|
return rendererInstances;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRenderers(): Promise<RendererInstance[]> {
|
||||||
|
const renderers = await this.buildRendererInstances();
|
||||||
|
return renderers;
|
||||||
|
}
|
||||||
|
|
||||||
async buildSource(contents: string): Promise<string> {
|
async buildSource(contents: string): Promise<string> {
|
||||||
const renderers = await this.buildRendererInstances();
|
const renderers = await this.buildRendererInstances();
|
||||||
const rendererServerPackages = renderers.map(({ server }) => server);
|
const rendererServerPackages = renderers.map(({ server }) => server);
|
||||||
|
|
|
@ -302,6 +302,7 @@ export interface RuntimeOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateSnowpackOptions {
|
interface CreateSnowpackOptions {
|
||||||
|
logging: LogOptions;
|
||||||
mode: RuntimeMode;
|
mode: RuntimeMode;
|
||||||
resolvePackageUrl: (pkgName: string) => Promise<string>;
|
resolvePackageUrl: (pkgName: string) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
@ -309,7 +310,7 @@ interface CreateSnowpackOptions {
|
||||||
/** Create a new Snowpack instance to power Astro */
|
/** Create a new Snowpack instance to power Astro */
|
||||||
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
|
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
|
||||||
const { projectRoot, src } = astroConfig;
|
const { projectRoot, src } = astroConfig;
|
||||||
const { mode, resolvePackageUrl } = options;
|
const { mode, logging, resolvePackageUrl } = options;
|
||||||
|
|
||||||
const frontendPath = new URL('./frontend/', import.meta.url);
|
const frontendPath = new URL('./frontend/', import.meta.url);
|
||||||
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
|
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
|
||||||
|
@ -324,10 +325,12 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
||||||
astroConfig: AstroConfig;
|
astroConfig: AstroConfig;
|
||||||
hmrPort?: number;
|
hmrPort?: number;
|
||||||
mode: RuntimeMode;
|
mode: RuntimeMode;
|
||||||
|
logging: LogOptions,
|
||||||
configManager: ConfigManager;
|
configManager: ConfigManager;
|
||||||
} = {
|
} = {
|
||||||
astroConfig,
|
astroConfig,
|
||||||
mode,
|
mode,
|
||||||
|
logging,
|
||||||
resolvePackageUrl,
|
resolvePackageUrl,
|
||||||
configManager,
|
configManager,
|
||||||
};
|
};
|
||||||
|
@ -370,6 +373,7 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
||||||
mount: mountOptions,
|
mount: mountOptions,
|
||||||
mode,
|
mode,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
[fileURLToPath(new URL('../snowpack-plugin-jsx.cjs', import.meta.url)), astroPluginOptions],
|
||||||
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPluginOptions],
|
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPluginOptions],
|
||||||
...rendererSnowpackPlugins,
|
...rendererSnowpackPlugins,
|
||||||
resolveDependency('@snowpack/plugin-sass'),
|
resolveDependency('@snowpack/plugin-sass'),
|
||||||
|
@ -440,6 +444,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
|
||||||
snowpackConfig,
|
snowpackConfig,
|
||||||
configManager,
|
configManager,
|
||||||
} = await createSnowpack(astroConfig, {
|
} = await createSnowpack(astroConfig, {
|
||||||
|
logging,
|
||||||
mode,
|
mode,
|
||||||
resolvePackageUrl,
|
resolvePackageUrl,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { h, Component } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return <div id="arrow-fn-component"></div>;
|
return <div id="arrow-fn-component"></div>
|
||||||
}
|
}
|
||||||
|
|
5
packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx
vendored
Normal file
5
packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/** @jsxImportSource preact */
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
return <div id="pragma-comment">Hello world</div>;
|
||||||
|
}
|
10
packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro
vendored
Normal file
10
packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
import PragmaComponent from '../components/PragmaComment.jsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Preact component works with Pragma comment</title>
|
||||||
|
</head>
|
||||||
|
<body><PragmaComponent client:load/></body>
|
||||||
|
</html>
|
|
@ -2,4 +2,4 @@ import React from 'react';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return <div id="arrow-fn-component"></div>;
|
return <div id="arrow-fn-component"></div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
export default function ({}) {
|
export default function ({}) {
|
||||||
return <h2>oops</h2>;
|
return <h2>oops</h2>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { suite } from 'uvu';
|
import { suite } from 'uvu';
|
||||||
import * as assert from 'uvu/assert';
|
import * as assert from 'uvu/assert';
|
||||||
import { doc } from './test-utils.js';
|
import { doc } from './test-utils.js';
|
||||||
import { setup } from './helpers.js';
|
import { setup, setupBuild } from './helpers.js';
|
||||||
|
|
||||||
const PreactComponent = suite('Preact component test');
|
const PreactComponent = suite('Preact component test');
|
||||||
|
|
||||||
setup(PreactComponent, './fixtures/preact-component');
|
setup(PreactComponent, './fixtures/preact-component');
|
||||||
|
setupBuild(PreactComponent, './fixtures/preact-component');
|
||||||
|
|
||||||
PreactComponent('Can load class component', async ({ runtime }) => {
|
PreactComponent('Can load class component', async ({ runtime }) => {
|
||||||
const result = await runtime.load('/class');
|
const result = await runtime.load('/class');
|
||||||
|
@ -40,4 +41,31 @@ PreactComponent('Can export a Fragment', async ({ runtime }) => {
|
||||||
assert.equal($('body').children().length, 0, "nothing rendered but it didn't throw.");
|
assert.equal($('body').children().length, 0, "nothing rendered but it didn't throw.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PreactComponent('Can use a pragma comment', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/pragma-comment');
|
||||||
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
|
const $ = doc(result.contents);
|
||||||
|
assert.equal($('#pragma-comment').length, 1, "rendered the PragmaComment component.");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
PreactComponent('Uses the new JSX transform', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/pragma-comment');
|
||||||
|
|
||||||
|
// Grab the imports
|
||||||
|
const exp = /import\("(.+?)"\)/g;
|
||||||
|
let match, componentUrl;
|
||||||
|
while ((match = exp.exec(result.contents))) {
|
||||||
|
if (match[1].includes('PragmaComment.js')) {
|
||||||
|
componentUrl = match[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = await runtime.load(componentUrl);
|
||||||
|
const jsxRuntime = component.imports.filter(i => i.specifier.includes('jsx-runtime'));
|
||||||
|
|
||||||
|
assert.ok(jsxRuntime, 'preact/jsx-runtime is used for the component');
|
||||||
|
});
|
||||||
|
|
||||||
PreactComponent.run();
|
PreactComponent.run();
|
||||||
|
|
|
@ -72,11 +72,23 @@ React('Can load Vue', async () => {
|
||||||
assert.equal($('#vue-h2').text(), 'Hasta la vista, baby');
|
assert.equal($('#vue-h2').text(), 'Hasta la vista, baby');
|
||||||
});
|
});
|
||||||
|
|
||||||
React('Get good error message when react import is forgotten', async () => {
|
React('uses the new JSX transform', async () => {
|
||||||
const result = await runtime.load('/forgot-import');
|
const result = await runtime.load('/');
|
||||||
|
assert.ok(!result.error, `build error: ${result.error}`);
|
||||||
|
|
||||||
assert.ok(result.error instanceof ReferenceError);
|
// Grab the imports
|
||||||
assert.equal(result.error.message, 'React is not defined');
|
const exp = /import\("(.+?)"\)/g;
|
||||||
});
|
let match, componentUrl;
|
||||||
|
while ((match = exp.exec(result.contents))) {
|
||||||
|
if (match[1].includes('Research.js')) {
|
||||||
|
componentUrl = match[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const component = await runtime.load(componentUrl);
|
||||||
|
const jsxRuntime = component.imports.filter(i => i.specifier.includes('jsx-runtime'));
|
||||||
|
|
||||||
|
assert.ok(jsxRuntime, 'react/jsx-runtime is used for the component');
|
||||||
|
})
|
||||||
|
|
||||||
React.run();
|
React.run();
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
export const COUNTER_COMPONENTS = {
|
export const COUNTER_COMPONENTS = {
|
||||||
'@astrojs/renderer-preact': {
|
'@astrojs/renderer-preact': {
|
||||||
filename: `src/components/PreactCounter.jsx`,
|
filename: `src/components/PreactCounter.jsx`,
|
||||||
content: `import { h } from 'preact';
|
content: `import { useState } from 'preact/hooks';
|
||||||
import { useState } from 'preact/hooks';
|
|
||||||
|
|
||||||
export default function PreactCounter({ children }) {
|
export default function PreactCounter() {
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const add = () => setCount((i) => i + 1);
|
const add = () => setCount((i) => i + 1);
|
||||||
const subtract = () => setCount((i) => i - 1);
|
const subtract = () => setCount((i) => i - 1);
|
||||||
|
@ -21,9 +20,9 @@ export default function PreactCounter({ children }) {
|
||||||
},
|
},
|
||||||
'@astrojs/renderer-react': {
|
'@astrojs/renderer-react': {
|
||||||
filename: `src/components/ReactCounter.jsx`,
|
filename: `src/components/ReactCounter.jsx`,
|
||||||
content: `import React, { useState } from 'react';
|
content: `import { useState } from 'react';
|
||||||
|
|
||||||
export default function ReactCounter({ children }) {
|
export default function ReactCounter() {
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const add = () => setCount((i) => i + 1);
|
const add = () => setCount((i) => i + 1);
|
||||||
const subtract = () => setCount((i) => i - 1);
|
const subtract = () => setCount((i) => i - 1);
|
||||||
|
@ -36,6 +35,25 @@ export default function ReactCounter({ children }) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
'@astrojs/renderer-solid': {
|
||||||
|
filename: `src/components/SolidCounter.jsx`,
|
||||||
|
content: `import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
export default function SolidCounter() {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const add = () => setCount(count() + 1);
|
||||||
|
const subtract = () => setCount(count() - 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="solid" class="counter">
|
||||||
|
<button onClick={subtract}>-</button>
|
||||||
|
<pre>{count()}</pre>
|
||||||
|
<button onClick={add}>+</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
'@astrojs/renderer-svelte': {
|
'@astrojs/renderer-svelte': {
|
||||||
|
@ -98,6 +116,10 @@ export const FRAMEWORKS = [
|
||||||
title: 'React',
|
title: 'React',
|
||||||
value: '@astrojs/renderer-react',
|
value: '@astrojs/renderer-react',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Solid',
|
||||||
|
value: '@astrojs/renderer-solid',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Svelte',
|
title: 'Svelte',
|
||||||
value: '@astrojs/renderer-svelte',
|
value: '@astrojs/renderer-svelte',
|
||||||
|
|
|
@ -2,5 +2,14 @@ export default {
|
||||||
name: '@astrojs/renderer-preact',
|
name: '@astrojs/renderer-preact',
|
||||||
client: './client',
|
client: './client',
|
||||||
server: './server',
|
server: './server',
|
||||||
knownEntrypoints: ['preact', 'preact-render-to-string'],
|
knownEntrypoints: ['preact', 'preact/jsx-runtime', 'preact-render-to-string'],
|
||||||
|
jsxImportSource: 'preact',
|
||||||
|
jsxTransformOptions: async () => {
|
||||||
|
const { default: { default: jsx }} = await import('@babel/plugin-transform-react-jsx');
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
jsx({}, { runtime: 'automatic', importSource: 'preact' })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"preact": "^10.5.13",
|
"preact": "^10.5.13",
|
||||||
"preact-render-to-string": "^5.1.18"
|
"preact-render-to-string": "^5.1.18",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.14.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
|
|
@ -10,7 +10,15 @@ function check(Component, props, children) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { html } = renderToStaticMarkup(Component, props, children);
|
const { html } = renderToStaticMarkup(Component, props, children);
|
||||||
return typeof html === 'string';
|
|
||||||
|
if (typeof html !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are edge cases (SolidJS) where Preact *might* render a string,
|
||||||
|
// but components would be <undefined></undefined>
|
||||||
|
|
||||||
|
return !/\<undefined\>/.test(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderToStaticMarkup(Component, props, children) {
|
function renderToStaticMarkup(Component, props, children) {
|
||||||
|
|
|
@ -2,5 +2,14 @@ export default {
|
||||||
name: '@astrojs/renderer-react',
|
name: '@astrojs/renderer-react',
|
||||||
client: './client',
|
client: './client',
|
||||||
server: './server',
|
server: './server',
|
||||||
knownEntrypoints: ['react', 'react-dom', 'react-dom/server'],
|
knownEntrypoints: ['react', 'react/jsx-runtime', 'react-dom', 'react-dom/server.js'],
|
||||||
|
jsxImportSource: 'react',
|
||||||
|
jsxTransformOptions: async () => {
|
||||||
|
const { default: { default: jsx }} = await import('@babel/plugin-transform-react-jsx');
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
jsx({}, { runtime: 'automatic', importSource: 'react' })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.14.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
|
17
packages/renderers/renderer-solid/client.js
Normal file
17
packages/renderers/renderer-solid/client.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { createComponent } from 'solid-js/web';
|
||||||
|
|
||||||
|
export default (element) => (Component, props) => {
|
||||||
|
// Solid `createComponent` just returns a DOM node with all reactivity
|
||||||
|
// already attached. There's no VDOM, so there's no real need to "mount".
|
||||||
|
// Likewise, `children` can just reuse the nearest `astro-fragment` node.
|
||||||
|
const component = createComponent(Component, {
|
||||||
|
...props,
|
||||||
|
children: element.querySelector('astro-fragment'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const children = Array.isArray(component)
|
||||||
|
? component
|
||||||
|
: [ component ];
|
||||||
|
|
||||||
|
element.replaceChildren(...children);
|
||||||
|
}
|
25
packages/renderers/renderer-solid/index.js
Normal file
25
packages/renderers/renderer-solid/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export default {
|
||||||
|
name: '@astrojs/renderer-solid',
|
||||||
|
client: './client',
|
||||||
|
server: './server',
|
||||||
|
knownEntrypoints: ['solid-js', 'solid-js/web'],
|
||||||
|
external: ['solid-js/web/dist/server.js', 'solid-js/dist/server.js', 'babel-preset-solid'],
|
||||||
|
jsxImportSource: 'solid-js',
|
||||||
|
jsxTransformOptions: async ({ isSSR }) => {
|
||||||
|
const [{ default: solid }] = await Promise.all([import('babel-preset-solid')]);
|
||||||
|
const options = {
|
||||||
|
presets: [
|
||||||
|
solid({}, { generate: isSSR ? 'ssr' : 'dom' }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSSR) {
|
||||||
|
options.alias = {
|
||||||
|
'solid-js/web': 'solid-js/web/dist/server.js',
|
||||||
|
'solid-js': 'solid-js/dist/server.js',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
};
|
18
packages/renderers/renderer-solid/package.json
Normal file
18
packages/renderers/renderer-solid/package.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "@astrojs/renderer-solid",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./client": "./client.js",
|
||||||
|
"./server": "./server.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"babel-preset-solid": "^1.0.0",
|
||||||
|
"solid-js": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
}
|
26
packages/renderers/renderer-solid/server.js
Normal file
26
packages/renderers/renderer-solid/server.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { createComponent } from 'solid-js';
|
||||||
|
import { renderToStringAsync, ssr } from 'solid-js/web/dist/server.js';
|
||||||
|
|
||||||
|
async function check(Component, props, children) {
|
||||||
|
if (typeof Component !== 'function') return false;
|
||||||
|
|
||||||
|
const { html } = await renderToStaticMarkup(Component, props, children);
|
||||||
|
return typeof html === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderToStaticMarkup(Component, props, children) {
|
||||||
|
const html = await renderToStringAsync(() => (
|
||||||
|
() => createComponent(Component, {
|
||||||
|
...props,
|
||||||
|
// In Solid SSR mode, `ssr` creates the expected structure for `children`.
|
||||||
|
// In Solid client mode, `ssr` is just a stub.
|
||||||
|
children: ssr([`<astro-fragment>${children}</astro-fragment>`]),
|
||||||
|
})
|
||||||
|
));
|
||||||
|
return { html };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
check,
|
||||||
|
renderToStaticMarkup,
|
||||||
|
};
|
12
packages/renderers/renderer-solid/static-html.js
Normal file
12
packages/renderers/renderer-solid/static-html.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { createComponent } from 'solid-js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Astro passes `children` as a string of HTML, so we need
|
||||||
|
* a wrapper `astro-fragment` to render that content as VNodes.
|
||||||
|
*/
|
||||||
|
const StaticHtml = ({ innerHTML }) => {
|
||||||
|
if (!innerHTML) return null;
|
||||||
|
return () => createComponent('astro-fragment', { innerHTML });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaticHtml;
|
Loading…
Reference in a new issue