WIP: support shared app state

This commit is contained in:
Nate Moore 2022-08-30 17:17:44 +02:00
parent 9adb7cca33
commit 9d5ec1c8c7
12 changed files with 76 additions and 19 deletions

View file

@ -4,5 +4,7 @@ import preact from '@astrojs/preact';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
// Enable Preact to support Preact JSX components. // Enable Preact to support Preact JSX components.
integrations: [preact()], integrations: [preact({
appEntrypoint: '/src/pages/_app.tsx'
})],
}); });

View file

@ -0,0 +1,4 @@
import { createContext } from 'preact';
const noop = () => {};
export const Context = createContext({ count: 0, increment: noop, decrement: noop });

View file

@ -1,18 +1,16 @@
import { h, Fragment } from 'preact'; import { useContext } from 'preact/hooks';
import { useState } from 'preact/hooks'; import { Context } from './Context';
import './Counter.css'; import './Counter.css';
export default function Counter({ children }) { export default function Counter({ children }) {
const [count, setCount] = useState(0); const { count, increment, decrement } = useContext(Context);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
return ( return (
<> <>
<div class="counter"> <div class="counter">
<button onClick={subtract}>-</button> <button onClick={decrement}>-</button>
<pre>{count}</pre> <pre>{count}</pre>
<button onClick={add}>+</button> <button onClick={increment}>+</button>
</div> </div>
<div class="counter-message">{children}</div> <div class="counter-message">{children}</div>
</> </>

View file

@ -0,0 +1,10 @@
import { Context } from "../components/Context";
import { useState } from "preact/hooks";
export default function ({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(v => v + 1)
const decrement = () => setCount(v => v - 1);
return <Context.Provider value={{ count, increment, decrement }}>{children}</Context.Provider>
}

View file

@ -1,7 +1,6 @@
--- ---
// Component Imports // Component Imports
import Counter from '../components/Counter'; import Counter from '../components/Counter';
// Full Astro Component Syntax: // Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/ // https://docs.astro.build/core-concepts/astro-components/
--- ---
@ -28,6 +27,9 @@ import Counter from '../components/Counter';
<Counter client:visible> <Counter client:visible>
<h1>Hello, Preact!</h1> <h1>Hello, Preact!</h1>
</Counter> </Counter>
<Counter client:visible>
<h1>Hello, Preact!</h1>
</Counter>
</main> </main>
</body> </body>
</html> </html>

View file

@ -1099,6 +1099,8 @@ export interface AstroRenderer {
clientEntrypoint?: string; clientEntrypoint?: string;
/** Import entrypoint for the server/build/ssr renderer. */ /** Import entrypoint for the server/build/ssr renderer. */
serverEntrypoint: string; serverEntrypoint: string;
/** User-provided entrypoint for the browser app instance */
appEntrypoint?: string;
/** JSX identifier (e.g. 'react' or 'solid-js') */ /** JSX identifier (e.g. 'react' or 'solid-js') */
jsxImportSource?: string; jsxImportSource?: string;
/** Babel transform options */ /** Babel transform options */

View file

@ -16,5 +16,16 @@ export default function astroIntegrationsContainerPlugin({
configureServer(server) { configureServer(server) {
runHookServerSetup({ config, server, logging }); runHookServerSetup({ config, server, logging });
}, },
async resolveId(id, importer, options) {
if (id.startsWith('virtual:@astrojs/') && id.endsWith('/app')) {
const rendererName = id.slice('virtual:'.length, '/app'.length * -1);
const match = config._ctx.renderers.find(({ name }) => name === rendererName);
if (match && match.appEntrypoint) {
const app = await this.resolve(match.appEntrypoint, importer, { ...options, skipSelf: true });
return app;
}
return id.slice('virtual:'.length)
}
}
}; };
} }

View file

@ -0,0 +1 @@
export { Fragment as default } from 'preact';

View file

@ -1,14 +1,17 @@
import { h, render } from 'preact'; import { h } from 'preact';
import { createPortal } from 'preact/compat';
import StaticHtml from './static-html.js'; import StaticHtml from './static-html.js';
export default (element) => export default (element) =>
(Component, props, { default: children, ...slotted }) => { (Component, props, { default: children, ...slotted }) => {
if (!element.hasAttribute('ssr')) return; if (!element.hasAttribute('ssr')) return;
const { addChild } = globalThis['@astrojs/preact'];
while (!!element.firstElementChild) {
element.firstElementChild.remove();
}
for (const [key, value] of Object.entries(slotted)) { for (const [key, value] of Object.entries(slotted)) {
props[key] = h(StaticHtml, { value, name: key }); props[key] = h(StaticHtml, { value, name: key });
} }
render( const Portal = createPortal(h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), element)
h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), addChild(Portal);
element
);
}; };

View file

@ -21,6 +21,7 @@
"homepage": "https://docs.astro.build/en/guides/integrations-guide/preact/", "homepage": "https://docs.astro.build/en/guides/integrations-guide/preact/",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./app": "./app.js",
"./client.js": "./client.js", "./client.js": "./client.js",
"./server.js": "./server.js", "./server.js": "./server.js",
"./package.json": "./package.json" "./package.json": "./package.json"

View file

@ -1,6 +1,7 @@
import { h, Component as BaseComponent } from 'preact'; import { h, Component as BaseComponent } from 'preact';
import render from 'preact-render-to-string'; import render from 'preact-render-to-string';
import StaticHtml from './static-html.js'; import StaticHtml from './static-html.js';
import Provider from 'virtual:@astrojs/preact/app';
const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
@ -44,7 +45,9 @@ function renderToStaticMarkup(Component, props, { default: children, ...slotted
// Note: create newProps to avoid mutating `props` before they are serialized // Note: create newProps to avoid mutating `props` before they are serialized
const newProps = { ...props, ...slots }; const newProps = { ...props, ...slots };
const html = render( const html = render(
h(Component, newProps, children != null ? h(StaticHtml, { value: children }) : children) h(Provider, {},
h(Component, newProps, children != null ? h(StaticHtml, { value: children }) : children)
)
); );
return { html }; return { html };
} }

View file

@ -1,10 +1,11 @@
import { AstroIntegration, AstroRenderer, ViteUserConfig } from 'astro'; import { AstroIntegration, AstroRenderer, ViteUserConfig } from 'astro';
function getRenderer(): AstroRenderer { function getRenderer(appEntrypoint?: string): AstroRenderer {
return { return {
name: '@astrojs/preact', name: '@astrojs/preact',
clientEntrypoint: '@astrojs/preact/client.js', clientEntrypoint: '@astrojs/preact/client.js',
serverEntrypoint: '@astrojs/preact/server.js', serverEntrypoint: '@astrojs/preact/server.js',
appEntrypoint,
jsxImportSource: 'preact', jsxImportSource: 'preact',
jsxTransformOptions: async () => { jsxTransformOptions: async () => {
const { const {
@ -92,13 +93,32 @@ function getViteConfiguration(compat?: boolean): ViteUserConfig {
return viteConfig; return viteConfig;
} }
export default function ({ compat }: { compat?: boolean } = {}): AstroIntegration { export default function ({ compat, appEntrypoint }: { compat?: boolean, appEntrypoint?: string } = {}): AstroIntegration {
return { return {
name: '@astrojs/preact', name: '@astrojs/preact',
hooks: { hooks: {
'astro:config:setup': ({ addRenderer, updateConfig }) => { 'astro:config:setup': ({ addRenderer, updateConfig, injectScript }) => {
if (compat) addRenderer(getCompatRenderer()); if (compat) addRenderer(getCompatRenderer());
addRenderer(getRenderer()); injectScript('before-hydration', `import { h, Fragment, render } from "preact";
import { useState } from "preact/hooks";
import Provider from "virtual:@astrojs/preact/app";
let addChild = () => {};
const App = ({ children: c }) => {
const [children, setChildren] = useState([c]);
addChild = (child) => setChildren(v => ([...v, child]));
return h(Fragment, {}, children)
}
const el = document.createElement('astro-app');
el.setAttribute('renderer', '@astrojs/preact');
document.body.appendChild(el);
render(h(Provider, {}, h(App, {})), el)
globalThis['@astrojs/preact'] = {
addChild
}`)
addRenderer(getRenderer(appEntrypoint));
updateConfig({ updateConfig({
vite: getViteConfiguration(compat), vite: getViteConfiguration(compat),
}); });