Implement fallback capability (#44)

* Implement fallback capability

This makes it possible for a dynamic component to render fallback content on the server.

The mechanism is a special `static` prop passed to the component. If `static` is true then the component knows it can render static content.

Putting aside the word `static`, is this the right approach? I think giving components the flexibility to make the decision themselves *is* the right approach.

However in this case we have a special property that is passed in non-explicitly. I think we have to do it this way because if the caller passes in a prop it will get serialized and appear on the client. By making this something we *add* during rendering, it only happens on the server (and only when using `:load`).

Assuming this is the right approach, is `static` the right name for this prop? Other candidates:

* `server`

That's all I have!

* Use `import.meta.env.astro` to tell if running in SSR mode.

* Run formatter
This commit is contained in:
Matthew Phillips 2021-03-31 16:10:27 -04:00 committed by GitHub
parent 3fa6396a7b
commit d9084ff4ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 135 additions and 28 deletions

View file

@ -345,8 +345,8 @@ export let version: string = '3.1.2';
}; };
</script> </script>
<script type="module" defer> <script type="module" defer>
import docsearch from 'docsearch.js/dist/cdn/docsearch.min.js'; import docsearch from 'https://cdn.skypack.dev/docsearch.js/dist/cdn/docsearch.min.js';
docsearch({ docsearch({
apiKey: '562139304880b94536fc53f5d65c5c19', indexName: 'snowpack', inputSelector: '.search-form-input', debug: true // Set debug to true if you want to inspect the dropdown apiKey: '562139304880b94536fc53f5d65c5c19', indexName: 'snowpack', inputSelector: '#search-form-input', debug: true // Set debug to true if you want to inspect the dropdown
}); });
</script> </script>

View file

@ -43,7 +43,7 @@ function Card({ result }) {
); );
} }
export default function PluginSearchPage() { function PluginSearchPageLive() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const [results, setResults] = useState(null); const [results, setResults] = useState(null);
const [searchQuery, setSearchQuery] = useState(searchParams.get('q')); const [searchQuery, setSearchQuery] = useState(searchParams.get('q'));
@ -65,9 +65,6 @@ export default function PluginSearchPage() {
setResults(await searchPlugins(formula)); setResults(await searchPlugins(formula));
return false; return false;
} }
// if (document.getElementById('loading-message')) {
// document.getElementById('loading-message').style.display = 'none';
// }
return ( return (
<> <>
@ -118,3 +115,7 @@ export default function PluginSearchPage() {
</> </>
); );
} }
export default function PluginSearchPage(props) {
return import.meta.env.astro ? <div>Loading...</div> : <PluginSearchPageLive {...props} />
}

View file

@ -66,7 +66,7 @@ let description = 'Snowpack plugins allow for configuration-minimal tooling inte
<a href="/reference/plugins">Creating your own plugin is easy!</a> <a href="/reference/plugins">Creating your own plugin is easy!</a>
</p> </p>
<div style="margin-top:100vh;"></div> <div style="margin-top:4rem;"></div>
<PluginSearchPage:load /> <PluginSearchPage:load />
</MainLayout> </MainLayout>

12
package-lock.json generated
View file

@ -2956,15 +2956,15 @@
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
}, },
"preact": { "preact": {
"version": "10.5.12", "version": "10.5.13",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.12.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.13.tgz",
"integrity": "sha512-r6siDkuD36oszwlCkcqDJCAKBQxGoeEGytw2DGMD5A/GGdu5Tymw+N2OBXwvOLxg6d1FeY8MgMV3cc5aVQo4Cg==", "integrity": "sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==",
"dev": true "dev": true
}, },
"preact-render-to-string": { "preact-render-to-string": {
"version": "5.1.16", "version": "5.1.18",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.1.16.tgz", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.1.18.tgz",
"integrity": "sha512-HvO3W29Sziz9r5FZGwl2e34XJKzyRLvjhouv3cpkCGszNPdnvkO8p4B6CBpe0MT/tzR+QVbmsAKLrMK222UXew==", "integrity": "sha512-jTL6iTZeheYOhb54r7KuyrNCf33lc+Z52Wos5P1z2wGZ/dREfhBVwrK1qGOrl4fboBN1KxC1lxhBchDHNZr8Uw==",
"dev": true, "dev": true,
"requires": { "requires": {
"pretty-format": "^3.8.0" "pretty-format": "^3.8.0"

View file

@ -81,8 +81,8 @@
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"estree-walker": "^3.0.0", "estree-walker": "^3.0.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"preact": "^10.5.12", "preact": "^10.5.13",
"preact-render-to-string": "^5.1.14", "preact-render-to-string": "^5.1.18",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"typescript": "^4.2.3", "typescript": "^4.2.3",
"uvu": "^0.5.1" "uvu": "^0.5.1"

View file

@ -61,7 +61,7 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const runtime = await createRuntime(astroConfig, { logging: runtimeLogging }); const runtime = await createRuntime(astroConfig, { logging: runtimeLogging });
const { runtimeConfig } = runtime; const { runtimeConfig } = runtime;
const { snowpack } = runtimeConfig; const { backendSnowpack: snowpack } = runtimeConfig;
const resolve = (pkgName: string) => snowpack.getUrlForPackage(pkgName); const resolve = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
const imports = new Set<string>(); const imports = new Set<string>();

View file

@ -5,7 +5,7 @@ interface DynamicRenderContext {
} }
export interface Renderer { export interface Renderer {
renderStatic(Component: any): (props: Record<string, string>, ...children: any[]) => string; renderStatic(Component: any): (props: Record<string, any>, ...children: any[]) => string;
render(context: { root: string; Component: string; props: string; [key: string]: string }): string; render(context: { root: string; Component: string; props: string; [key: string]: string }): string;
imports?: Record<string, string[]>; imports?: Record<string, string[]>;
} }

View file

@ -10,9 +10,12 @@ import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpa
interface RuntimeConfig { interface RuntimeConfig {
astroConfig: AstroConfig; astroConfig: AstroConfig;
logging: LogOptions; logging: LogOptions;
snowpack: SnowpackDevServer; backendSnowpack: SnowpackDevServer;
snowpackRuntime: SnowpackServerRuntime; backendSnowpackRuntime: SnowpackServerRuntime;
snowpackConfig: SnowpackConfig; backendSnowpackConfig: SnowpackConfig;
frontendSnowpack: SnowpackDevServer;
frontendSnowpackRuntime: SnowpackServerRuntime;
frontendSnowpackConfig: SnowpackConfig;
} }
type LoadResultSuccess = { type LoadResultSuccess = {
@ -29,7 +32,7 @@ export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultErro
snowpackLogger.level = 'silent'; snowpackLogger.level = 'silent';
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> { async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
const { logging, snowpack, snowpackRuntime } = config; const { logging, backendSnowpackRuntime, frontendSnowpack } = config;
const { astroRoot } = config.astroConfig; const { astroRoot } = config.astroConfig;
const fullurl = new URL(rawPathname || '/', 'https://example.org/'); const fullurl = new URL(rawPathname || '/', 'https://example.org/');
@ -43,7 +46,8 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
// Non-Astro pages (file resources) // Non-Astro pages (file resources)
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) { if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
try { try {
const result = await snowpack.loadUrl(reqPath); console.log('loading', reqPath);
const result = await frontendSnowpack.loadUrl(reqPath);
// success // success
return { return {
@ -63,7 +67,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
for (const url of [`/_astro/pages/${selectedPage}.astro.js`, `/_astro/pages/${selectedPage}.md.js`]) { for (const url of [`/_astro/pages/${selectedPage}.astro.js`, `/_astro/pages/${selectedPage}.md.js`]) {
try { try {
const mod = await snowpackRuntime.importModule(url); const mod = await backendSnowpackRuntime.importModule(url);
debug(logging, 'resolve', `${reqPath} -> ${url}`); debug(logging, 'resolve', `${reqPath} -> ${url}`);
let html = (await mod.exports.__renderPage({ let html = (await mod.exports.__renderPage({
request: { request: {
@ -128,7 +132,7 @@ interface RuntimeOptions {
logging: LogOptions; logging: LogOptions;
} }
export async function createRuntime(astroConfig: AstroConfig, { logging }: RuntimeOptions): Promise<AstroRuntime> { async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>) {
const { projectRoot, astroRoot, extensions } = astroConfig; const { projectRoot, astroRoot, extensions } = astroConfig;
const internalPath = new URL('./frontend/', import.meta.url); const internalPath = new URL('./frontend/', import.meta.url);
@ -170,23 +174,42 @@ export async function createRuntime(astroConfig: AstroConfig, { logging }: Runti
external: ['@vue/server-renderer', 'node-fetch'], external: ['@vue/server-renderer', 'node-fetch'],
}, },
}); });
const envConfig = snowpackConfig.env || (snowpackConfig.env = {});
Object.assign(envConfig, env);
snowpack = await startSnowpackServer({ snowpack = await startSnowpackServer({
config: snowpackConfig, config: snowpackConfig,
lockfile: null, lockfile: null,
}); });
const snowpackRuntime = snowpack.getServerRuntime(); const snowpackRuntime = snowpack.getServerRuntime();
return { snowpack, snowpackRuntime, snowpackConfig };
}
export async function createRuntime(astroConfig: AstroConfig, { logging }: RuntimeOptions): Promise<AstroRuntime> {
const { snowpack: backendSnowpack, snowpackRuntime: backendSnowpackRuntime, snowpackConfig: backendSnowpackConfig } = await createSnowpack(astroConfig, {
astro: true,
});
const { snowpack: frontendSnowpack, snowpackRuntime: frontendSnowpackRuntime, snowpackConfig: frontendSnowpackConfig } = await createSnowpack(astroConfig, {
astro: false,
});
const runtimeConfig: RuntimeConfig = { const runtimeConfig: RuntimeConfig = {
astroConfig, astroConfig,
logging, logging,
snowpack, backendSnowpack,
snowpackRuntime, backendSnowpackRuntime,
snowpackConfig, backendSnowpackConfig,
frontendSnowpack,
frontendSnowpackRuntime,
frontendSnowpackConfig,
}; };
return { return {
runtimeConfig, runtimeConfig,
load: load.bind(null, runtimeConfig), load: load.bind(null, runtimeConfig),
shutdown: () => snowpack.shutdown(), shutdown: () => Promise.all([backendSnowpack.shutdown(), frontendSnowpack.shutdown()]).then(() => void 0),
}; };
} }

View file

View file

@ -0,0 +1,19 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const Fallback = suite('Dynamic component fallback');
setup(Fallback, './fixtures/astro-fallback');
Fallback('Shows static content', async (context) => {
const result = await context.runtime.load('/');
assert.equal(result.statusCode, 200);
const $ = doc(result.contents);
assert.equal($('#fallback').text(), 'static');
});
Fallback.run();

View file

@ -0,0 +1,8 @@
export default {
projectRoot: '.',
astroRoot: './astro',
dist: './_site',
extensions: {
'.jsx': 'preact'
}
};

View file

@ -0,0 +1,7 @@
import { h } from 'preact';
export default function(props) {
return (
<div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div>
);
};

View file

@ -0,0 +1,16 @@
---
import Client from '../components/Client.jsx';
let title = 'My Page'
---
<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<Client:load />
</body>
</html>

33
test/helpers.js Normal file
View file

@ -0,0 +1,33 @@
import { createRuntime } from '../lib/runtime.js';
import { loadConfig } from '../lib/config.js';
import * as assert from 'uvu/assert';
export function setup(Suite, fixturePath) {
let runtime, setupError;
Suite.before(async (context) => {
const astroConfig = await loadConfig(new URL(fixturePath, import.meta.url).pathname);
const logging = {
level: 'error',
dest: process.stderr,
};
try {
runtime = await createRuntime(astroConfig, { logging });
} catch (err) {
console.error(err);
setupError = err;
}
context.runtime = runtime;
});
Suite.after(async () => {
(await runtime) && runtime.shutdown();
});
Suite('No errors creating a runtime', () => {
assert.equal(setupError, undefined);
});
}