diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 5f3f9338b..c2993b108 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "astro": "^1.2.1", - "preact": "^10.7.3", - "@astrojs/preact": "^1.1.0" + "preact": "^10.10.6", + "@astrojs/preact": "workspace:*", + "@preact/signals": "1.0.3" } } diff --git a/examples/framework-preact/src/components/Counter.tsx b/examples/framework-preact/src/components/Counter.tsx index 61a9f9d5a..c2b065e3c 100644 --- a/examples/framework-preact/src/components/Counter.tsx +++ b/examples/framework-preact/src/components/Counter.tsx @@ -1,11 +1,9 @@ import { h, Fragment } from 'preact'; -import { useState } from 'preact/hooks'; import './Counter.css'; -export default function Counter({ children }) { - const [count, setCount] = useState(0); - const add = () => setCount((i) => i + 1); - const subtract = () => setCount((i) => i - 1); +export default function Counter({ children, count }) { + const add = () => count.value++ + const subtract = () => count.value--; return ( <> diff --git a/examples/framework-preact/src/pages/index.astro b/examples/framework-preact/src/pages/index.astro index a6565f6c1..b37295d7b 100644 --- a/examples/framework-preact/src/pages/index.astro +++ b/examples/framework-preact/src/pages/index.astro @@ -2,8 +2,12 @@ // Component Imports import Counter from '../components/Counter'; +import { signal } from '@preact/signals'; + // Full Astro Component Syntax: // https://docs.astro.build/core-concepts/astro-components/ + +const count = signal(0); --- @@ -25,8 +29,12 @@ import Counter from '../components/Counter';
- -

Hello, Preact!

+ +

Hello, Preact 1!

+
+ + +

Hello, Preact 2!

diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index 30688237a..44924787e 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -135,7 +135,7 @@ export async function generateHydrateScript( // Attach renderer-provided attributes if (attrs) { for (const [key, value] of Object.entries(attrs)) { - island.props[key] = value; + island.props[key] = escapeHTML(value); } } diff --git a/packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro b/packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro index ea62dfd54..4f6eb6b0c 100644 --- a/packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro +++ b/packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro @@ -1,6 +1,7 @@ --- Astro.response.headers.set('One-Two', 'three'); Astro.response.headers.set('Four-Five', 'six'); +Astro.response.headers.set("Cache-Control", `max-age=0, s-maxage=86400`); --- diff --git a/packages/astro/test/ssr-response.test.js b/packages/astro/test/ssr-response.test.js index 2044c513c..d0dbdbff3 100644 --- a/packages/astro/test/ssr-response.test.js +++ b/packages/astro/test/ssr-response.test.js @@ -36,5 +36,6 @@ describe('Using Astro.response in SSR', () => { const headers = response.headers; expect(headers.get('one-two')).to.equal('three'); expect(headers.get('four-five')).to.equal('six'); + expect(headers.get('Cache-Control')).to.equal(`max-age=0, s-maxage=86400`) }); }); diff --git a/packages/integrations/preact/client.js b/packages/integrations/preact/client.js deleted file mode 100644 index 78d8720f0..000000000 --- a/packages/integrations/preact/client.js +++ /dev/null @@ -1,14 +0,0 @@ -import { h, render } from 'preact'; -import StaticHtml from './static-html.js'; - -export default (element) => - (Component, props, { default: children, ...slotted }) => { - if (!element.hasAttribute('ssr')) return; - for (const [key, value] of Object.entries(slotted)) { - props[key] = h(StaticHtml, { value, name: key }); - } - render( - h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), - element - ); - }; diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json index 2aa24cac9..038609b2c 100644 --- a/packages/integrations/preact/package.json +++ b/packages/integrations/preact/package.json @@ -21,9 +21,9 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/preact/", "exports": { ".": "./dist/index.js", - "./client.js": "./client.js", - "./client-dev.js": "./client-dev.js", - "./server.js": "./server.js", + "./client.js": "./dist/client.js", + "./client-dev.js": "./dist/client-dev.js", + "./server.js": "./dist/server.js", "./package.json": "./package.json" }, "scripts": { @@ -35,7 +35,8 @@ "@babel/core": ">=7.0.0-0 <8.0.0", "@babel/plugin-transform-react-jsx": "^7.17.12", "babel-plugin-module-resolver": "^4.1.0", - "preact-render-to-string": "^5.2.0" + "preact-render-to-string": "^5.2.0", + "@preact/signals": "1.0.3" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/integrations/preact/client-dev.js b/packages/integrations/preact/src/client-dev.ts similarity index 85% rename from packages/integrations/preact/client-dev.js rename to packages/integrations/preact/src/client-dev.ts index d37e6e0af..9a9edcb3b 100644 --- a/packages/integrations/preact/client-dev.js +++ b/packages/integrations/preact/src/client-dev.ts @@ -1,3 +1,4 @@ +// @ts-ignore import 'preact/debug'; import clientFn from './client.js'; diff --git a/packages/integrations/preact/src/client.ts b/packages/integrations/preact/src/client.ts new file mode 100644 index 000000000..c7ec2ef3a --- /dev/null +++ b/packages/integrations/preact/src/client.ts @@ -0,0 +1,31 @@ +import type { SignalLike } from './types'; +import { h, render } from 'preact'; +import StaticHtml from './static-html.js'; +// TODO change this +import { signal } from '@preact/signals'; + +const sharedSignalMap: Map = new Map(); + +export default (element: HTMLElement) => + (Component: any, props: Record, { default: children, ...slotted }: Record) => { + console.log("HERE I AM 2") + if (!element.hasAttribute('ssr')) return; + for (const [key, value] of Object.entries(slotted)) { + props[key] = h(StaticHtml, { value, name: key }); + } + let signalsRaw = element.dataset.preactSignals; + if(signalsRaw) { + let signals: Record = JSON.parse(element.dataset.preactSignals as string); + for(const [propName, signalId] of Object.entries(signals)) { + if(!sharedSignalMap.has(signalId)) { + const signalValue = signal(props[propName]); + sharedSignalMap.set(signalId, signalValue); + } + props[propName] = sharedSignalMap.get(signalId); + } + } + render( + h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), + element + ); + }; diff --git a/packages/integrations/preact/src/context.ts b/packages/integrations/preact/src/context.ts new file mode 100644 index 000000000..eccb07c25 --- /dev/null +++ b/packages/integrations/preact/src/context.ts @@ -0,0 +1,30 @@ +import type { RendererContext, SignalLike } from './types'; + +type Context = { + id: string; + c: number; + signals: Map; +}; + +const contexts = new WeakMap(); + +export function getContext(result: RendererContext['result']): Context { + if (contexts.has(result)) { + return contexts.get(result)!; + } + let ctx = { + c: 0, + get id() { + return 'p' + this.c.toString(); + }, + signals: new Map() + }; + contexts.set(result, ctx); + return ctx; +} + +export function incrementId(ctx: Context): string { + let id = ctx.id; + ctx.c++; + return id; +} diff --git a/packages/integrations/preact/server.js b/packages/integrations/preact/src/server.ts similarity index 67% rename from packages/integrations/preact/server.js rename to packages/integrations/preact/src/server.ts index f5b1a34e5..90aa5244f 100644 --- a/packages/integrations/preact/server.js +++ b/packages/integrations/preact/src/server.ts @@ -1,13 +1,15 @@ +import type { RendererContext, SignalLike } from './types'; import { h, Component as BaseComponent } from 'preact'; import render from 'preact-render-to-string'; import StaticHtml from './static-html.js'; +import { getContext, incrementId } from './context.js'; -const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); -let originalConsoleError; +let originalConsoleError: typeof console.error; let consoleFilterRefs = 0; -function check(Component, props, children) { +function check(this: RendererContext, Component: any, props: Record, children: any) { if (typeof Component !== 'function') return false; if (Component.prototype != null && typeof Component.prototype.render === 'function') { @@ -18,7 +20,7 @@ function check(Component, props, children) { try { try { - const { html } = renderToStaticMarkup(Component, props, children); + const { html } = renderToStaticMarkup.call(this, Component, props, children); if (typeof html !== 'string') { return false; } @@ -35,8 +37,33 @@ function check(Component, props, children) { } } -function renderToStaticMarkup(Component, props, { default: children, ...slotted }) { - const slots = {}; +function isSignal(x: any): x is SignalLike { + return x != null && typeof x === 'object' && typeof x.peek === 'function' && 'value' in x; +} + +function renderToStaticMarkup(this: RendererContext, Component: any, props: Record, { default: children, ...slotted }: Record) { + const ctx = getContext(this.result); + const signals: Record = {}; + + // Check for signals + for(const [key, value] of Object.entries(props)) { + if(isSignal(value)) { + // Set the value to the current signal value + props[key] = value.peek(); + + let id: string; + if(ctx.signals.has(value)) { + id = ctx.signals.get(value)!; + } else { + id = incrementId(ctx); + ctx.signals.set(value, id); + } + signals[key] = id; + } + } + + + const slots: Record> = {}; for (const [key, value] of Object.entries(slotted)) { const name = slotName(key); slots[name] = h(StaticHtml, { value, name }); @@ -46,7 +73,12 @@ function renderToStaticMarkup(Component, props, { default: children, ...slotted const html = render( h(Component, newProps, children != null ? h(StaticHtml, { value: children }) : children) ); - return { html }; + return { + attrs: { + 'data-preact-signals': JSON.stringify(signals), + }, + html + }; } /** @@ -91,7 +123,7 @@ function finishUsingConsoleFilter() { * Ignores known non-problematic errors while any code is using the console filter. * Otherwise, simply forwards all arguments to the original function. */ -function filteredConsoleError(msg, ...rest) { +function filteredConsoleError(msg: string, ...rest: any[]) { if (consoleFilterRefs > 0 && typeof msg === 'string') { // In `check`, we attempt to render JSX components through Preact. // When attempting this on a React component, React may output diff --git a/packages/integrations/preact/static-html.js b/packages/integrations/preact/src/static-html.ts similarity index 90% rename from packages/integrations/preact/static-html.js rename to packages/integrations/preact/src/static-html.ts index 7e964ef06..e474caa5a 100644 --- a/packages/integrations/preact/static-html.js +++ b/packages/integrations/preact/src/static-html.ts @@ -7,7 +7,7 @@ import { h } from 'preact'; * As a bonus, we can signal to Preact that this subtree is * entirely static and will never change via `shouldComponentUpdate`. */ -const StaticHtml = ({ value, name }) => { +const StaticHtml = ({ value, name }: { value: string; name?: string; }) => { if (!value) return null; return h('astro-slot', { name, dangerouslySetInnerHTML: { __html: value } }); }; diff --git a/packages/integrations/preact/src/types.ts b/packages/integrations/preact/src/types.ts new file mode 100644 index 000000000..1c741fdbd --- /dev/null +++ b/packages/integrations/preact/src/types.ts @@ -0,0 +1,8 @@ +import type { SSRResult } from 'astro'; +export type RendererContext = { + result: SSRResult; +}; + +export type SignalLike = { + peek(): any; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 836f848b8..adf371665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,11 +160,13 @@ importers: examples/framework-preact: specifiers: - '@astrojs/preact': ^1.1.0 + '@astrojs/preact': workspace:* + '@preact/signals': 1.0.3 astro: ^1.2.1 - preact: ^10.7.3 + preact: ^10.10.6 dependencies: '@astrojs/preact': link:../../packages/integrations/preact + '@preact/signals': 1.0.3_preact@10.10.6 astro: link:../../packages/astro preact: 10.10.6 @@ -2514,6 +2516,7 @@ importers: specifiers: '@babel/core': '>=7.0.0-0 <8.0.0' '@babel/plugin-transform-react-jsx': ^7.17.12 + '@preact/signals': 1.0.3 astro: workspace:* astro-scripts: workspace:* babel-plugin-module-resolver: ^4.1.0 @@ -2522,6 +2525,7 @@ importers: dependencies: '@babel/core': 7.19.0 '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.0 + '@preact/signals': 1.0.3_preact@10.10.6 babel-plugin-module-resolver: 4.1.0 preact-render-to-string: 5.2.3_preact@10.10.6 devDependencies: @@ -5768,6 +5772,19 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: false + /@preact/signals-core/1.0.1: + resolution: {integrity: sha512-1yBu72jd80QWdp8WvNFBg7K+0REv+NqJg1CGIwAl5kJ+qE7I06Lm2+K3z2qVZmABiDjQgjU2vKA9yEjniWv5uA==} + dev: false + + /@preact/signals/1.0.3_preact@10.10.6: + resolution: {integrity: sha512-aBUYPBzdw+UD99t3n2v+OKecjaA2SnvImLDKH7jZtgwPE6E4Jr+B+H3P1tYty+bRsIKnHuFcvYUIZJAP1armFw==} + peerDependencies: + preact: 10.x + dependencies: + '@preact/signals-core': 1.0.1 + preact: 10.10.6 + dev: false + /@proload/core/0.3.3: resolution: {integrity: sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ==} dependencies: