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:
parent
3fa6396a7b
commit
d9084ff4ad
14 changed files with 135 additions and 28 deletions
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
}
|
||||||
|
|
|
@ -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
12
package-lock.json
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
19
test/astro-fallback.test.js
Normal file
19
test/astro-fallback.test.js
Normal 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();
|
8
test/fixtures/astro-fallback/astro.config.mjs
vendored
Normal file
8
test/fixtures/astro-fallback/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default {
|
||||||
|
projectRoot: '.',
|
||||||
|
astroRoot: './astro',
|
||||||
|
dist: './_site',
|
||||||
|
extensions: {
|
||||||
|
'.jsx': 'preact'
|
||||||
|
}
|
||||||
|
};
|
7
test/fixtures/astro-fallback/astro/components/Client.jsx
vendored
Normal file
7
test/fixtures/astro-fallback/astro/components/Client.jsx
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function(props) {
|
||||||
|
return (
|
||||||
|
<div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div>
|
||||||
|
);
|
||||||
|
};
|
16
test/fixtures/astro-fallback/astro/pages/index.astro
vendored
Normal file
16
test/fixtures/astro-fallback/astro/pages/index.astro
vendored
Normal 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
33
test/helpers.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue