0
Fork 0
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:
Drew Powers 2021-03-25 16:59:38 -06:00 committed by GitHub
parent 3db5959377
commit 04a443a888
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 96 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 += `,`;
}

View file

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

View file

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