Compare commits

...

1 commit

Author SHA1 Message Date
Nate Moore
3a56d77415 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>
2021-07-20 15:56:47 -05:00
32 changed files with 2613 additions and 2392 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/renderer-solid': minor
---
Initial release

View file

@ -0,0 +1,5 @@
---
'@astrojs/renderer-preact': patch
---
Update `check` logic to exclude false-positives from SolidJS

View file

@ -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.

View file

@ -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',
]
}; };

View 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>
</>
);
}

View file

@ -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>

View file

@ -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.

View file

@ -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
View 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

View file

@ -0,0 +1,2 @@
## force pnpm to hoist
shamefully-hoist = true

View 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.

View 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 dont 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'
]
};

View 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": "../.."
}
}

View 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>
</>
);
}

View 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>

View file

@ -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.

View file

@ -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.

View file

@ -58,6 +58,7 @@
"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",

View file

@ -0,0 +1,155 @@
const esbuild = require('esbuild');
const colors = require('kleur/colors');
const { logger } = require('snowpack');
const path = require('path');
const { promises: fs } = require('fs');
const babel = require('@babel/core')
const eslexer = require('es-module-lexer');
/**
* @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
} = options;
let didInit = false;
return {
name: '@astrojs/snowpack-plugin-jsx',
resolve: {
input: ['.jsx', '.tsx'],
output: ['.js'],
},
async load({ filePath, fileExt, ...transformContext }) {
if (!didInit) {
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) {
logger.error(`${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 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',
});
const [imports] = eslexer.parse(codeToScan);
let importSource;
if (imports) {
for (let { n: name } of imports) {
if (name.indexOf('/') > -1) name = name.split('/')[0];
if (importSources.has(name)) {
importSource = name;
break;
}
}
}
if (!importSource) {
let match;
while ((match = /\/\*\*(?:[^*][^/]|\s)*@jsxImportSource\s+(.+)\s*\*\//gm.exec(contents)) !== null) {
importSource = match[1].trim();
break;
}
}
if (!importSource) {
console.log(`${filePath}
Unable to resolve JSX transformer! If you have more than one renderer enabled, you should use a pragma comment.
/* jsxImportSource: preact */
`);
return {
'.js': {
code: ''
},
}
}
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,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';

View file

@ -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' })
]
}
}
}; };

View file

@ -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"

View file

@ -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) {

View file

@ -2,5 +2,15 @@ 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'],
external: ['react-dom/server'],
jsxImportSource: 'react',
jsxTransformOptions: async () => {
const { default: { default: jsx }} = await import('@babel/plugin-transform-react-jsx');
return {
plugins: [
jsx({}, { runtime: 'automatic', importSource: 'react' })
]
}
}
}; };

View file

@ -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"

View 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);
}

View 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-plugin-module-resolver', '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;
}
};

View file

@ -0,0 +1,19 @@
{
"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-plugin-module-resolver": "^4.1.0",
"babel-preset-solid": "^1.0.0",
"solid-js": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
}

View 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,
};

View 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;

View file

@ -59,7 +59,6 @@ export default async function build(...args) {
}, },
entryPoints, entryPoints,
outdir, outdir,
external,
format, format,
plugins: [svelte({ isDev })], plugins: [svelte({ isDev })],
}); });

4360
yarn.lock

File diff suppressed because it is too large Load diff