0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-20 22:12:38 -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 { h } from 'preact';
import { format as formatDate, parseISO } from 'date-fns'; import { format as formatDate, parseISO } from 'date-fns';
import './Card.css';
export default function Card({ item }) { export default function Card({ item }) {
return ( return (
@ -10,18 +11,24 @@ export default function Card({ item }) {
> >
{item.img ? ( {item.img ? (
<img <img
class="card-image card-image-small" class="card-image card-image__sm"
src={item.img} src={item.img}
alt="" alt=""
style={{ background: item.imgBackground || undefined }} 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"> <div class="card-text">
<h3 class="card-title">{item.title}</h3> <h3 class="card-title">{item.title}</h3>
{item.date && <time class="snow-toc-link">{ formatDate(parseISO(item.date), 'MMMM d, yyyy') }</time>} {item.date && (
{item.description && <p style="margin: 0.5rem 0 0.25rem;">{ item.description }</p>} <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> </div>
</a> </a>
</article> </article>

View file

@ -76,6 +76,13 @@ let communityGuides;
return return
<Card item={post} />; <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> </div>
</MainLayout> </MainLayout>
</body> </body>

View file

@ -8,9 +8,11 @@
.card-grid-3 { .card-grid-3 {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
} }
.card-grid-4 { .card-grid-4 {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
} }
.discord-banner { .discord-banner {
grid-column: 1 / -1; grid-column: 1 / -1;
border: 1px solid #2e2077; 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 { .content-title {
font-family: 'Overpass'; 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 contents = await readFile(filePath, 'utf-8');
const compileOptions = { const compileOptions = {
resolve, resolve,
extensions extensions,
}; };
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot }); const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
return result.contents; return result.contents;

View file

@ -111,10 +111,7 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
'.svelte': 'svelte', '.svelte': 'svelte',
}; };
type DynamicImportMap = Map< type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact', string>;
'vue' | 'react' | 'react-dom' | 'preact',
string
>;
function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, dynamicImports: DynamicImportMap) { function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, dynamicImports: DynamicImportMap) {
const [name, kind] = _name.split(':'); const [name, kind] = _name.split(':');
@ -136,7 +133,9 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
case 'preact': { case 'preact': {
if (kind === 'dynamic') { if (kind === 'dynamic') {
return { 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')}';`, wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`,
}; };
} else { } else {
@ -177,7 +176,9 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
case 'vue': { case 'vue': {
if (kind === 'dynamic') { if (kind === 'dynamic') {
return { 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')}';`, wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`,
}; };
} else { } else {
@ -236,9 +237,9 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
const componentExports: ExportNamedDeclaration[] = []; const componentExports: ExportNamedDeclaration[] = [];
let script = ''; let script = '';
let propsStatement: string = ''; let propsStatement = '';
const importExportStatements: Set<string> = new Set(); 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>(); const componentPlugins = new Set<ValidExtensionPlugins>();
if (ast.module) { if (ast.module) {
@ -277,7 +278,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
components[componentName] = { components[componentName] = {
type: componentType, type: componentType,
plugin, plugin,
url: importUrl url: importUrl,
}; };
if (plugin) { if (plugin) {
componentPlugins.add(plugin); componentPlugins.add(plugin);

View file

@ -105,7 +105,6 @@ async function transformFromSource(
} }
} }
export async function compileComponent( export async function compileComponent(
source: string, source: string,
{ compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } { compileOptions = defaultCompileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
@ -136,7 +135,6 @@ export default __render;
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow, // \`__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}) { export async function __renderPage({request, children, props}) {
const currentChild = { const currentChild = {
setup: typeof setup === 'undefined' ? (passthrough) => passthrough : setup, setup: typeof setup === 'undefined' ? (passthrough) => passthrough : setup,
layout: typeof __layout === 'undefined' ? undefined : __layout, layout: typeof __layout === 'undefined' ? undefined : __layout,

View file

@ -63,7 +63,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
try { try {
const mod = await snowpackRuntime.importModule(selectedPageUrl); const mod = await snowpackRuntime.importModule(selectedPageUrl);
const html = (await mod.exports.__renderPage({ let html = (await mod.exports.__renderPage({
request: { request: {
host: fullurl.hostname, host: fullurl.hostname,
path: fullurl.pathname, path: fullurl.pathname,
@ -73,6 +73,15 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
props: {}, props: {},
})) as string; })) 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 { return {
statusCode: 200, statusCode: 200,
contents: html, contents: html,
@ -99,7 +108,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
interface RuntimeOptions { interface RuntimeOptions {
logging: LogOptions; logging: LogOptions;
env: 'dev' | 'build' env: 'dev' | 'build';
} }
export async function createRuntime(astroConfig: AstroConfig, { env, logging }: RuntimeOptions) { 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?: Record<string, string>;
} = { } = {
extensions, extensions,
resolve: env === 'dev' ? resolve: env === 'dev' ? async (pkgName: string) => snowpack.getUrlForPackage(pkgName) : async (pkgName: string) => `/_snowpack/pkg/${pkgName}.js`,
async (pkgName: string) => snowpack.getUrlForPackage(pkgName) :
async (pkgName: string) => `/_snowpack/pkg/${pkgName}.js`
}; };
/*if (existsSync(new URL('./package-lock.json', projectRoot))) { /*if (existsSync(new URL('./package-lock.json', projectRoot))) {
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8'); const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');