summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/astro/package.json8
-rw-r--r--packages/astro/snowpack-plugin-jsx.cjs189
-rw-r--r--packages/astro/src/config_manager.ts10
-rw-r--r--packages/astro/src/runtime.ts7
-rw-r--r--packages/astro/test/fixtures/preact-component/src/components/ArrowFunction.jsx6
-rw-r--r--packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx5
-rw-r--r--packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro10
-rw-r--r--packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx2
-rw-r--r--packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx4
-rw-r--r--packages/astro/test/preact-component.test.js30
-rw-r--r--packages/astro/test/react-component.test.js22
-rw-r--r--packages/create-astro/src/frameworks.ts32
-rw-r--r--packages/renderers/renderer-preact/index.js11
-rw-r--r--packages/renderers/renderer-preact/package.json3
-rw-r--r--packages/renderers/renderer-preact/server.js10
-rw-r--r--packages/renderers/renderer-react/index.js11
-rw-r--r--packages/renderers/renderer-react/package.json3
-rw-r--r--packages/renderers/renderer-solid/client.js17
-rw-r--r--packages/renderers/renderer-solid/index.js25
-rw-r--r--packages/renderers/renderer-solid/package.json18
-rw-r--r--packages/renderers/renderer-solid/server.js26
-rw-r--r--packages/renderers/renderer-solid/static-html.js12
22 files changed, 434 insertions, 27 deletions
diff --git a/packages/astro/package.json b/packages/astro/package.json
index d700adfe0..4aac0a8c1 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -14,6 +14,7 @@
".": "./astro.cjs",
"./package.json": "./package.json",
"./snowpack-plugin": "./snowpack-plugin.cjs",
+ "./snowpack-plugin-jsx": "./snowpack-plugin-jsx.cjs",
"./components": "./components/index.js",
"./components/*": "./components/*",
"./runtime/svelte": "./dist/frontend/runtime/svelte.js",
@@ -49,8 +50,10 @@
"@astrojs/renderer-svelte": "0.1.1",
"@astrojs/renderer-vue": "0.1.3",
"@babel/code-frame": "^7.12.13",
+ "@babel/core": "^7.14.6",
"@babel/generator": "^7.13.9",
"@babel/parser": "^7.13.15",
+ "@babel/plugin-transform-react-jsx": "^7.14.5",
"@babel/traverse": "^7.13.15",
"@snowpack/plugin-postcss": "^1.4.3",
"@snowpack/plugin-sass": "^1.4.0",
@@ -58,11 +61,12 @@
"astring": "^1.7.4",
"autoprefixer": "^10.2.5",
"camel-case": "^4.1.2",
+ "babel-plugin-module-resolver": "^4.1.0",
"cheerio": "^1.0.0-rc.6",
"ci-info": "^3.2.0",
"del": "^6.0.0",
"es-module-lexer": "^0.4.1",
- "esbuild": "^0.10.1",
+ "esbuild": "^0.12.12",
"estree-util-value-to-estree": "^1.2.0",
"estree-walker": "^3.0.0",
"fast-xml-parser": "^3.19.0",
@@ -89,7 +93,7 @@
"semver": "^7.3.5",
"shorthash": "^0.0.2",
"slash": "^4.0.0",
- "snowpack": "^3.8.1",
+ "snowpack": "^3.8.3",
"string-width": "^5.0.0",
"tiny-glob": "^0.2.8",
"unified": "^9.2.1",
diff --git a/packages/astro/snowpack-plugin-jsx.cjs b/packages/astro/snowpack-plugin-jsx.cjs
new file mode 100644
index 000000000..f7b31b76f
--- /dev/null
+++ b/packages/astro/snowpack-plugin-jsx.cjs
@@ -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,
+ });
diff --git a/packages/astro/src/config_manager.ts b/packages/astro/src/config_manager.ts
index 8087f58c8..e63b126f5 100644
--- a/packages/astro/src/config_manager.ts
+++ b/packages/astro/src/config_manager.ts
@@ -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 { posix as path } from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
@@ -17,6 +17,8 @@ interface RendererInstance {
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';
@@ -119,12 +121,18 @@ export class ConfigManager {
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);
diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts
index e1feeeeea..6bf099b4a 100644
--- a/packages/astro/src/runtime.ts
+++ b/packages/astro/src/runtime.ts
@@ -302,6 +302,7 @@ export interface RuntimeOptions {
}
interface CreateSnowpackOptions {
+ logging: LogOptions;
mode: RuntimeMode;
resolvePackageUrl: (pkgName: string) => Promise<string>;
}
@@ -309,7 +310,7 @@ interface CreateSnowpackOptions {
/** Create a new Snowpack instance to power Astro */
async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackOptions) {
const { projectRoot, src } = astroConfig;
- const { mode, resolvePackageUrl } = options;
+ const { mode, logging, resolvePackageUrl } = options;
const frontendPath = new URL('./frontend/', import.meta.url);
const resolveDependency = (dep: string) => resolve.sync(dep, { basedir: fileURLToPath(projectRoot) });
@@ -324,10 +325,12 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
astroConfig: AstroConfig;
hmrPort?: number;
mode: RuntimeMode;
+ logging: LogOptions,
configManager: ConfigManager;
} = {
astroConfig,
mode,
+ logging,
resolvePackageUrl,
configManager,
};
@@ -370,6 +373,7 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
mount: mountOptions,
mode,
plugins: [
+ [fileURLToPath(new URL('../snowpack-plugin-jsx.cjs', import.meta.url)), astroPluginOptions],
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPluginOptions],
...rendererSnowpackPlugins,
resolveDependency('@snowpack/plugin-sass'),
@@ -440,6 +444,7 @@ export async function createRuntime(astroConfig: AstroConfig, { mode, logging }:
snowpackConfig,
configManager,
} = await createSnowpack(astroConfig, {
+ logging,
mode,
resolvePackageUrl,
});
diff --git a/packages/astro/test/fixtures/preact-component/src/components/ArrowFunction.jsx b/packages/astro/test/fixtures/preact-component/src/components/ArrowFunction.jsx
index e9bd13b21..ba6854bad 100644
--- a/packages/astro/test/fixtures/preact-component/src/components/ArrowFunction.jsx
+++ b/packages/astro/test/fixtures/preact-component/src/components/ArrowFunction.jsx
@@ -1,5 +1,5 @@
-import { h, Component } from 'preact';
+import { h } from 'preact';
export default () => {
- return <div id="arrow-fn-component"></div>;
-} \ No newline at end of file
+ return <div id="arrow-fn-component"></div>
+}
diff --git a/packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx b/packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx
new file mode 100644
index 000000000..3340e3f8e
--- /dev/null
+++ b/packages/astro/test/fixtures/preact-component/src/components/PragmaComment.jsx
@@ -0,0 +1,5 @@
+/** @jsxImportSource preact */
+
+export default function() {
+ return <div id="pragma-comment">Hello world</div>;
+}
diff --git a/packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro b/packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro
new file mode 100644
index 000000000..91db69830
--- /dev/null
+++ b/packages/astro/test/fixtures/preact-component/src/pages/pragma-comment.astro
@@ -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>
diff --git a/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx b/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx
index 21b8ca173..16fac5bb6 100644
--- a/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx
+++ b/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx
@@ -2,4 +2,4 @@ import React from 'react';
export default () => {
return <div id="arrow-fn-component"></div>;
-} \ No newline at end of file
+}
diff --git a/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx b/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx
index d7dfc29f7..9ee27faca 100644
--- a/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx
+++ b/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx
@@ -1,5 +1,3 @@
-
-
export default function ({}) {
return <h2>oops</h2>;
-} \ No newline at end of file
+}
diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.js
index 3b3a37dfc..55debc8ad 100644
--- a/packages/astro/test/preact-component.test.js
+++ b/packages/astro/test/preact-component.test.js
@@ -1,11 +1,12 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
-import { setup } from './helpers.js';
+import { setup, setupBuild } from './helpers.js';
const PreactComponent = suite('Preact component test');
setup(PreactComponent, './fixtures/preact-component');
+setupBuild(PreactComponent, './fixtures/preact-component');
PreactComponent('Can load class component', async ({ runtime }) => {
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.");
});
+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();
diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js
index 48c985be1..90b864925 100644
--- a/packages/astro/test/react-component.test.js
+++ b/packages/astro/test/react-component.test.js
@@ -72,11 +72,23 @@ React('Can load Vue', async () => {
assert.equal($('#vue-h2').text(), 'Hasta la vista, baby');
});
-React('Get good error message when react import is forgotten', async () => {
- const result = await runtime.load('/forgot-import');
+React('uses the new JSX transform', async () => {
+ const result = await runtime.load('/');
+ assert.ok(!result.error, `build error: ${result.error}`);
- assert.ok(result.error instanceof ReferenceError);
- assert.equal(result.error.message, 'React is not defined');
-});
+ // Grab the imports
+ 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();
diff --git a/packages/create-astro/src/frameworks.ts b/packages/create-astro/src/frameworks.ts
index 440dc067b..5b1546d48 100644
--- a/packages/create-astro/src/frameworks.ts
+++ b/packages/create-astro/src/frameworks.ts
@@ -1,10 +1,9 @@
export const COUNTER_COMPONENTS = {
'@astrojs/renderer-preact': {
filename: `src/components/PreactCounter.jsx`,
- content: `import { h } from 'preact';
-import { useState } from 'preact/hooks';
+ content: `import { useState } from 'preact/hooks';
-export default function PreactCounter({ children }) {
+export default function PreactCounter() {
const [count, setCount] = useState(0);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@@ -21,9 +20,9 @@ export default function PreactCounter({ children }) {
},
'@astrojs/renderer-react': {
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 add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@@ -38,6 +37,25 @@ export default function ReactCounter({ children }) {
}
`,
},
+ '@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': {
filename: `src/components/SvelteCounter.svelte`,
content: `<script>
@@ -99,6 +117,10 @@ export const FRAMEWORKS = [
value: '@astrojs/renderer-react',
},
{
+ title: 'Solid',
+ value: '@astrojs/renderer-solid',
+ },
+ {
title: 'Svelte',
value: '@astrojs/renderer-svelte',
},
diff --git a/packages/renderers/renderer-preact/index.js b/packages/renderers/renderer-preact/index.js
index 84d2021d1..5819ab0f2 100644
--- a/packages/renderers/renderer-preact/index.js
+++ b/packages/renderers/renderer-preact/index.js
@@ -2,5 +2,14 @@ export default {
name: '@astrojs/renderer-preact',
client: './client',
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' })
+ ]
+ }
+ }
};
diff --git a/packages/renderers/renderer-preact/package.json b/packages/renderers/renderer-preact/package.json
index f22560afb..a81097011 100644
--- a/packages/renderers/renderer-preact/package.json
+++ b/packages/renderers/renderer-preact/package.json
@@ -10,7 +10,8 @@
},
"dependencies": {
"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": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
diff --git a/packages/renderers/renderer-preact/server.js b/packages/renderers/renderer-preact/server.js
index ac8661a49..441ce8ef0 100644
--- a/packages/renderers/renderer-preact/server.js
+++ b/packages/renderers/renderer-preact/server.js
@@ -10,7 +10,15 @@ function check(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) {
diff --git a/packages/renderers/renderer-react/index.js b/packages/renderers/renderer-react/index.js
index ca9f35ff7..51a2ecba1 100644
--- a/packages/renderers/renderer-react/index.js
+++ b/packages/renderers/renderer-react/index.js
@@ -2,5 +2,14 @@ export default {
name: '@astrojs/renderer-react',
client: './client',
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' })
+ ]
+ }
+ }
};
diff --git a/packages/renderers/renderer-react/package.json b/packages/renderers/renderer-react/package.json
index 20c7595fe..d57e6cd4f 100644
--- a/packages/renderers/renderer-react/package.json
+++ b/packages/renderers/renderer-react/package.json
@@ -10,7 +10,8 @@
},
"dependencies": {
"react": "^17.0.2",
- "react-dom": "^17.0.2"
+ "react-dom": "^17.0.2",
+ "@babel/plugin-transform-react-jsx": "^7.14.5"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
diff --git a/packages/renderers/renderer-solid/client.js b/packages/renderers/renderer-solid/client.js
new file mode 100644
index 000000000..047de819d
--- /dev/null
+++ b/packages/renderers/renderer-solid/client.js
@@ -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);
+}
diff --git a/packages/renderers/renderer-solid/index.js b/packages/renderers/renderer-solid/index.js
new file mode 100644
index 000000000..5848f78a4
--- /dev/null
+++ b/packages/renderers/renderer-solid/index.js
@@ -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;
+ }
+};
diff --git a/packages/renderers/renderer-solid/package.json b/packages/renderers/renderer-solid/package.json
new file mode 100644
index 000000000..ed92851e1
--- /dev/null
+++ b/packages/renderers/renderer-solid/package.json
@@ -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"
+ }
+}
diff --git a/packages/renderers/renderer-solid/server.js b/packages/renderers/renderer-solid/server.js
new file mode 100644
index 000000000..8af4d8aab
--- /dev/null
+++ b/packages/renderers/renderer-solid/server.js
@@ -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,
+};
diff --git a/packages/renderers/renderer-solid/static-html.js b/packages/renderers/renderer-solid/static-html.js
new file mode 100644
index 000000000..953416c49
--- /dev/null
+++ b/packages/renderers/renderer-solid/static-html.js
@@ -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;