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 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({
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>

View file

@ -43,7 +43,7 @@ function Card({ result }) {
);
}
export default function PluginSearchPage() {
function PluginSearchPageLive() {
const searchParams = new URLSearchParams(window.location.search);
const [results, setResults] = useState(null);
const [searchQuery, setSearchQuery] = useState(searchParams.get('q'));
@ -65,9 +65,6 @@ export default function PluginSearchPage() {
setResults(await searchPlugins(formula));
return false;
}
// if (document.getElementById('loading-message')) {
// document.getElementById('loading-message').style.display = 'none';
// }
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>
</p>
<div style="margin-top:100vh;"></div>
<div style="margin-top:4rem;"></div>
<PluginSearchPage:load />
</MainLayout>

12
package-lock.json generated
View file

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

View file

@ -81,8 +81,8 @@
"eslint-plugin-prettier": "^3.3.1",
"estree-walker": "^3.0.0",
"nodemon": "^2.0.7",
"preact": "^10.5.12",
"preact-render-to-string": "^5.1.14",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.18",
"prettier": "^2.2.1",
"typescript": "^4.2.3",
"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 { runtimeConfig } = runtime;
const { snowpack } = runtimeConfig;
const { backendSnowpack: snowpack } = runtimeConfig;
const resolve = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
const imports = new Set<string>();

View file

@ -5,7 +5,7 @@ interface DynamicRenderContext {
}
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;
imports?: Record<string, string[]>;
}

View file

@ -10,9 +10,12 @@ import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpa
interface RuntimeConfig {
astroConfig: AstroConfig;
logging: LogOptions;
snowpack: SnowpackDevServer;
snowpackRuntime: SnowpackServerRuntime;
snowpackConfig: SnowpackConfig;
backendSnowpack: SnowpackDevServer;
backendSnowpackRuntime: SnowpackServerRuntime;
backendSnowpackConfig: SnowpackConfig;
frontendSnowpack: SnowpackDevServer;
frontendSnowpackRuntime: SnowpackServerRuntime;
frontendSnowpackConfig: SnowpackConfig;
}
type LoadResultSuccess = {
@ -29,7 +32,7 @@ export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultErro
snowpackLogger.level = 'silent';
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 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)
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
try {
const result = await snowpack.loadUrl(reqPath);
console.log('loading', reqPath);
const result = await frontendSnowpack.loadUrl(reqPath);
// success
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`]) {
try {
const mod = await snowpackRuntime.importModule(url);
const mod = await backendSnowpackRuntime.importModule(url);
debug(logging, 'resolve', `${reqPath} -> ${url}`);
let html = (await mod.exports.__renderPage({
request: {
@ -128,7 +132,7 @@ interface RuntimeOptions {
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 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'],
},
});
const envConfig = snowpackConfig.env || (snowpackConfig.env = {});
Object.assign(envConfig, env);
snowpack = await startSnowpackServer({
config: snowpackConfig,
lockfile: null,
});
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 = {
astroConfig,
logging,
snowpack,
snowpackRuntime,
snowpackConfig,
backendSnowpack,
backendSnowpackRuntime,
backendSnowpackConfig,
frontendSnowpack,
frontendSnowpackRuntime,
frontendSnowpackConfig,
};
return {
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);
});
}