mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
Add React component SSR (#28)
* Add React component SSR * Add React component SSR
This commit is contained in:
parent
3db5959377
commit
04a443a888
8 changed files with 123 additions and 96 deletions
67
examples/snowpack/astro/components/Card.css
Normal file
67
examples/snowpack/astro/components/Card.css
Normal file
|
@ -0,0 +1,67 @@
|
|||
.card {
|
||||
display: flex;
|
||||
grid-column: span 1;
|
||||
overflow: hidden;
|
||||
font-family: Open Sans, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
Roboto, sans-serif;
|
||||
color: #1a202c;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #2a85ca;
|
||||
box-shadow: 0 2px 2px 0 rgba(46, 94, 130, 0.4);
|
||||
}
|
||||
|
||||
.card:hover .card-image {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 0) .card-image {
|
||||
background: #f2709c;
|
||||
background: linear-gradient(30deg, #ff9472, #f2709c);
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 1) .card-image {
|
||||
background: #fbd3e9;
|
||||
background: linear-gradient(30deg, #bb377d, #fbd3e9);
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 2) .card-image {
|
||||
background: #b993d6;
|
||||
background: linear-gradient(30deg, #8ca6db, #b993d6);
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 3) .card-image {
|
||||
background: #00d2ff;
|
||||
background: linear-gradient(30deg, #3a7bd5, #00d2ff);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card-image__sm {
|
||||
flex-grow: 1;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.card-image__lg {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
font-family: 'Overpass';
|
||||
line-height: 1.1;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import {h} from 'preact';
|
||||
import {format as formatDate, parseISO} from 'date-fns';
|
||||
import { h } from 'preact';
|
||||
import { format as formatDate, parseISO } from 'date-fns';
|
||||
import './Card.css';
|
||||
|
||||
export default function Card({ item }) {
|
||||
return (
|
||||
|
@ -10,18 +11,24 @@ export default function Card({ item }) {
|
|||
>
|
||||
{item.img ? (
|
||||
<img
|
||||
class="card-image card-image-small"
|
||||
class="card-image card-image__sm"
|
||||
src={item.img}
|
||||
alt=""
|
||||
style={{ background: item.imgBackground || undefined }}
|
||||
/>
|
||||
) : (
|
||||
<div class="card-image card-image-small"></div>
|
||||
<div class="card-image card-image__sm"></div>
|
||||
)}
|
||||
<div class="card-text">
|
||||
<h3 class="card-title">{item.title}</h3>
|
||||
{item.date && <time class="snow-toc-link">{ formatDate(parseISO(item.date), 'MMMM d, yyyy') }</time>}
|
||||
{item.description && <p style="margin: 0.5rem 0 0.25rem;">{ item.description }</p>}
|
||||
{item.date && (
|
||||
<time class="snow-toc-link">
|
||||
{formatDate(parseISO(item.date), 'MMMM d, yyyy')}
|
||||
</time>
|
||||
)}
|
||||
{item.description && (
|
||||
<p style="margin: 0.5rem 0 0.25rem;">{item.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
|
|
|
@ -76,8 +76,15 @@ let communityGuides;
|
|||
return
|
||||
<Card item={post} />;
|
||||
})}
|
||||
<Card item={{
|
||||
url: 'https://www.snowpack.dev/posts/2021-01-13-snowpack-3-0',
|
||||
img: 'https://www.snowpack.dev/img/social-snowpackv3.jpg',
|
||||
date: '2021-01-12 00:00:00Z',
|
||||
title: 'Snowpack v3.0',
|
||||
description: 'Snowpack v3.0 is here!',
|
||||
}} />
|
||||
</div>
|
||||
</MainLayout>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
.card-grid-3 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.card-grid-4 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.discord-banner {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid #2e2077;
|
||||
|
@ -38,68 +40,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
grid-column: span 1;
|
||||
overflow: hidden;
|
||||
font-family: Open Sans, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
Roboto, sans-serif;
|
||||
color: #1a202c;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: #2a85ca;
|
||||
box-shadow: 0 2px 2px 0 rgba(46, 94, 130, 0.4);
|
||||
}
|
||||
.card:hover .card-image {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
.card-image-small {
|
||||
flex-grow: 1;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.card-image-large {
|
||||
height: 200px;
|
||||
}
|
||||
.card-text {
|
||||
padding: 1rem;
|
||||
}
|
||||
.card-title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
font-family: 'Overpass';
|
||||
line-height: 1.1;
|
||||
}
|
||||
.content-title {
|
||||
font-family: 'Overpass';
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 0) .card-image {
|
||||
background: #f2709c;
|
||||
background: linear-gradient(30deg, #ff9472, #f2709c);
|
||||
}
|
||||
.card:nth-child(4n + 1) .card-image {
|
||||
background: #fbd3e9;
|
||||
background: linear-gradient(30deg, #bb377d, #fbd3e9);
|
||||
}
|
||||
.card:nth-child(4n + 2) .card-image {
|
||||
background: #b993d6;
|
||||
background: linear-gradient(30deg, #8ca6db, #b993d6);
|
||||
}
|
||||
|
||||
.card:nth-child(4n + 3) .card-image {
|
||||
background: #00d2ff;
|
||||
background: linear-gradient(30deg, #3a7bd5, #00d2ff);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = function (snowpackConfig, { resolve, extensions } = {}) {
|
|||
const contents = await readFile(filePath, 'utf-8');
|
||||
const compileOptions = {
|
||||
resolve,
|
||||
extensions
|
||||
extensions,
|
||||
};
|
||||
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
|
||||
return result.contents;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { walk } from 'estree-walker';
|
|||
import babelParser from '@babel/parser';
|
||||
import _babelGenerator from '@babel/generator';
|
||||
import traverse from '@babel/traverse';
|
||||
import { ImportDeclaration,ExportNamedDeclaration, VariableDeclarator, Identifier, VariableDeclaration } from '@babel/types';
|
||||
import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, VariableDeclaration } from '@babel/types';
|
||||
import { type } from 'node:os';
|
||||
|
||||
const babelGenerator: typeof _babelGenerator =
|
||||
|
@ -111,10 +111,7 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
|
|||
'.svelte': 'svelte',
|
||||
};
|
||||
|
||||
type DynamicImportMap = Map<
|
||||
'vue' | 'react' | 'react-dom' | 'preact',
|
||||
string
|
||||
>;
|
||||
type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>;
|
||||
|
||||
function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, dynamicImports: DynamicImportMap) {
|
||||
const [name, kind] = _name.split(':');
|
||||
|
@ -136,7 +133,9 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
|
|||
case 'preact': {
|
||||
if (kind === 'dynamic') {
|
||||
return {
|
||||
wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get('preact')!}')`,
|
||||
wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get(
|
||||
'preact'
|
||||
)!}')`,
|
||||
wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`,
|
||||
};
|
||||
} else {
|
||||
|
@ -177,7 +176,9 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
|
|||
case 'vue': {
|
||||
if (kind === 'dynamic') {
|
||||
return {
|
||||
wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.vue.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get('vue')!}')`,
|
||||
wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.vue.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get(
|
||||
'vue'
|
||||
)!}')`,
|
||||
wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`,
|
||||
};
|
||||
} else {
|
||||
|
@ -207,8 +208,8 @@ function compileExpressionSafe(raw: string): string {
|
|||
|
||||
async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> {
|
||||
const importMap: DynamicImportMap = new Map();
|
||||
for(let plugin of plugins) {
|
||||
switch(plugin) {
|
||||
for (let plugin of plugins) {
|
||||
switch (plugin) {
|
||||
case 'vue': {
|
||||
importMap.set('vue', await resolve('vue'));
|
||||
break;
|
||||
|
@ -236,9 +237,9 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
const componentExports: ExportNamedDeclaration[] = [];
|
||||
|
||||
let script = '';
|
||||
let propsStatement: string = '';
|
||||
let propsStatement = '';
|
||||
const importExportStatements: Set<string> = new Set();
|
||||
const components: Record<string, { type: string; url: string, plugin: string | undefined }> = {};
|
||||
const components: Record<string, { type: string; url: string; plugin: string | undefined }> = {};
|
||||
const componentPlugins = new Set<ValidExtensionPlugins>();
|
||||
|
||||
if (ast.module) {
|
||||
|
@ -277,9 +278,9 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
components[componentName] = {
|
||||
type: componentType,
|
||||
plugin,
|
||||
url: importUrl
|
||||
url: importUrl,
|
||||
};
|
||||
if(plugin) {
|
||||
if (plugin) {
|
||||
componentPlugins.add(plugin);
|
||||
}
|
||||
importExportStatements.add(ast.module.content.slice(componentImport.start!, componentImport.end!));
|
||||
|
@ -293,7 +294,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
for (const componentExport of componentProps) {
|
||||
propsStatement += `${(componentExport.id as Identifier).name}`;
|
||||
if (componentExport.init) {
|
||||
propsStatement += `= ${babelGenerator(componentExport.init!).code }`;
|
||||
propsStatement += `= ${babelGenerator(componentExport.init!).code}`;
|
||||
}
|
||||
propsStatement += `,`;
|
||||
}
|
||||
|
|
|
@ -105,12 +105,11 @@ async function transformFromSource(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export async function compileComponent(
|
||||
source: string,
|
||||
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||
): Promise<CompileResult> {
|
||||
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||
const isPage = path.extname(filename) === '.md' || sourceJsx.items.some((item) => item.name === 'html');
|
||||
// sort <style> tags first
|
||||
sourceJsx.items.sort((a, b) => (a.name === 'style' && b.name !== 'style' ? -1 : 0));
|
||||
|
@ -124,9 +123,9 @@ ${sourceJsx.imports.join('\n')}
|
|||
|
||||
// \`__render()\`: Render the contents of the Astro module.
|
||||
import { h, Fragment } from '${internalImport('h.js')}';
|
||||
async function __render(props, ...children) {
|
||||
async function __render(props, ...children) {
|
||||
${sourceJsx.script}
|
||||
return h(Fragment, null, ${sourceJsx.items.map(({ jsx }) => jsx).join(',')});
|
||||
return h(Fragment, null, ${sourceJsx.items.map(({ jsx }) => jsx).join(',')});
|
||||
}
|
||||
export default __render;
|
||||
`;
|
||||
|
@ -134,9 +133,8 @@ export default __render;
|
|||
if (isPage) {
|
||||
modJsx += `
|
||||
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
|
||||
// triggered by loading a component directly by URL.
|
||||
// triggered by loading a component directly by URL.
|
||||
export async function __renderPage({request, children, props}) {
|
||||
|
||||
const currentChild = {
|
||||
setup: typeof setup === 'undefined' ? (passthrough) => passthrough : setup,
|
||||
layout: typeof __layout === 'undefined' ? undefined : __layout,
|
||||
|
@ -155,7 +153,7 @@ export async function __renderPage({request, children, props}) {
|
|||
props: {content: currentChild.content},
|
||||
children: [childBodyResult],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return childBodyResult;
|
||||
};\n`;
|
||||
|
|
|
@ -63,7 +63,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
|
||||
try {
|
||||
const mod = await snowpackRuntime.importModule(selectedPageUrl);
|
||||
const html = (await mod.exports.__renderPage({
|
||||
let html = (await mod.exports.__renderPage({
|
||||
request: {
|
||||
host: fullurl.hostname,
|
||||
path: fullurl.pathname,
|
||||
|
@ -73,6 +73,15 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
props: {},
|
||||
})) as string;
|
||||
|
||||
// inject styles
|
||||
// TODO: handle this in compiler
|
||||
const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, url) => `${markup}\n<link rel="stylesheet" type="text/css" href="${url}" />`, '') : ``;
|
||||
if (html.indexOf('</head>') !== -1) {
|
||||
html = html.replace('</head>', `${styleTags}</head>`);
|
||||
} else {
|
||||
html = styleTags + html;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
contents: html,
|
||||
|
@ -99,7 +108,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
|
||||
interface RuntimeOptions {
|
||||
logging: LogOptions;
|
||||
env: 'dev' | 'build'
|
||||
env: 'dev' | 'build';
|
||||
}
|
||||
|
||||
export async function createRuntime(astroConfig: AstroConfig, { env, logging }: RuntimeOptions) {
|
||||
|
@ -113,9 +122,7 @@ export async function createRuntime(astroConfig: AstroConfig, { env, logging }:
|
|||
extensions?: Record<string, string>;
|
||||
} = {
|
||||
extensions,
|
||||
resolve: env === 'dev' ?
|
||||
async (pkgName: string) => snowpack.getUrlForPackage(pkgName) :
|
||||
async (pkgName: string) => `/_snowpack/pkg/${pkgName}.js`
|
||||
resolve: env === 'dev' ? async (pkgName: string) => snowpack.getUrlForPackage(pkgName) : async (pkgName: string) => `/_snowpack/pkg/${pkgName}.js`,
|
||||
};
|
||||
/*if (existsSync(new URL('./package-lock.json', projectRoot))) {
|
||||
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
|
||||
|
|
Loading…
Reference in a new issue