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 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>
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
@ -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
12
package-lock.json
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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[]>;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
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