diff --git a/.changeset/fresh-bobcats-report.md b/.changeset/fresh-bobcats-report.md new file mode 100644 index 000000000000..9c5a7c16886f --- /dev/null +++ b/.changeset/fresh-bobcats-report.md @@ -0,0 +1,20 @@ +--- +'@astrojs/solid-js': minor +--- + +Adds `createIslandSignal` for sharing reactive signals across multiple Solid islands + +A new `createIslandSignal` function, importable from `@astrojs/solid-js/signals`, lets you create a signal in `.astro` frontmatter and pass it to multiple `client:*` Solid components. All islands that receive the same signal share a single reactive instance on the client, so updating the signal in one island automatically updates the others. + +```astro +--- +import { createIslandSignal } from '@astrojs/solid-js/signals'; +import Counter from '../components/Counter'; +import Display from '../components/Display'; +const [count, setCount] = createIslandSignal(0); +--- + + +``` + +Signals can also be passed inside arrays and objects. Setters are supported as props alongside getters. diff --git a/packages/astro/e2e/fixtures/solid-signals/astro.config.mjs b/packages/astro/e2e/fixtures/solid-signals/astro.config.mjs new file mode 100644 index 000000000000..dcd5306e9485 --- /dev/null +++ b/packages/astro/e2e/fixtures/solid-signals/astro.config.mjs @@ -0,0 +1,6 @@ +import solid from '@astrojs/solid-js'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [solid()], +}); diff --git a/packages/astro/e2e/fixtures/solid-signals/package.json b/packages/astro/e2e/fixtures/solid-signals/package.json new file mode 100644 index 000000000000..7812222e6ec0 --- /dev/null +++ b/packages/astro/e2e/fixtures/solid-signals/package.json @@ -0,0 +1,10 @@ +{ + "name": "@e2e/solid-signals", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/solid-js": "workspace:*", + "astro": "workspace:*", + "solid-js": "^1.9.13" + } +} diff --git a/packages/astro/e2e/fixtures/solid-signals/src/components/Counter.jsx b/packages/astro/e2e/fixtures/solid-signals/src/components/Counter.jsx new file mode 100644 index 000000000000..91035562f7d5 --- /dev/null +++ b/packages/astro/e2e/fixtures/solid-signals/src/components/Counter.jsx @@ -0,0 +1,13 @@ +export default function Counter(props) { + return ( +
+ +
{props.count()}
+ +
+ ); +} diff --git a/packages/astro/e2e/fixtures/solid-signals/src/components/Display.jsx b/packages/astro/e2e/fixtures/solid-signals/src/components/Display.jsx new file mode 100644 index 000000000000..631b2e40cbab --- /dev/null +++ b/packages/astro/e2e/fixtures/solid-signals/src/components/Display.jsx @@ -0,0 +1,7 @@ +export default function Display(props) { + return ( +
+ {props.count()} +
+ ); +} diff --git a/packages/astro/e2e/fixtures/solid-signals/src/pages/index.astro b/packages/astro/e2e/fixtures/solid-signals/src/pages/index.astro new file mode 100644 index 000000000000..31120bb1deaa --- /dev/null +++ b/packages/astro/e2e/fixtures/solid-signals/src/pages/index.astro @@ -0,0 +1,15 @@ +--- +import { createIslandSignal } from '@astrojs/solid-js/signals'; +import Counter from '../components/Counter'; +import Display from '../components/Display'; +const [count, setCount] = createIslandSignal(10); +--- + + + Solid Signals E2E + + + + + + diff --git a/packages/astro/e2e/solid-signals.test.ts b/packages/astro/e2e/solid-signals.test.ts new file mode 100644 index 000000000000..1750f69981dc --- /dev/null +++ b/packages/astro/e2e/solid-signals.test.ts @@ -0,0 +1,62 @@ +import { expect } from '@playwright/test'; +import { type DevServer, testFactory, waitForHydrate } from './test-utils.ts'; + +const test = testFactory(import.meta.url, { root: './fixtures/solid-signals/' }); + +let devServer: DevServer; + +test.beforeAll(async ({ astro }) => { + devServer = await astro.startDevServer(); +}); + +test.afterAll(async () => { + await devServer.stop(); +}); + +test.describe('Solid island signals', () => { + test('signal hydrates with correct initial value', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); + + const counter = page.locator('#counter'); + await expect(counter, 'counter is visible').toBeVisible(); + + const display = page.locator('#display'); + await expect(display, 'display is visible').toBeVisible(); + + // SSR renders the initial value + await expect(counter.locator('pre'), 'counter initial value is 10').toHaveText('10'); + await expect(display.locator('.value'), 'display initial value is 10').toHaveText('10'); + + // Wait for both islands to hydrate + await waitForHydrate(page, counter); + await waitForHydrate(page, display); + + // Values should still be correct after hydration + await expect(counter.locator('pre'), 'counter value after hydration is 10').toHaveText('10'); + await expect(display.locator('.value'), 'display value after hydration is 10').toHaveText('10'); + }); + + test('shared signal updates across islands', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); + + const counter = page.locator('#counter'); + const display = page.locator('#display'); + + await waitForHydrate(page, counter); + await waitForHydrate(page, display); + + // Click increment in the counter island + await counter.locator('.increment').click(); + + // Both islands should reflect the updated value + await expect(counter.locator('pre'), 'counter incremented to 11').toHaveText('11'); + await expect(display.locator('.value'), 'display updated to 11').toHaveText('11'); + + // Click decrement twice + await counter.locator('.decrement').click(); + await counter.locator('.decrement').click(); + + await expect(counter.locator('pre'), 'counter decremented to 9').toHaveText('9'); + await expect(display.locator('.value'), 'display updated to 9').toHaveText('9'); + }); +}); diff --git a/packages/integrations/solid/package.json b/packages/integrations/solid/package.json index 8bd8ca25eb28..b334add0fa2b 100644 --- a/packages/integrations/solid/package.json +++ b/packages/integrations/solid/package.json @@ -21,6 +21,7 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/solid-js/", "exports": { ".": "./dist/index.js", + "./signals": "./dist/island-signal.js", "./container-renderer": "./dist/container-renderer.js", "./client.js": "./dist/client.js", "./server.js": "./dist/server.js", @@ -32,7 +33,8 @@ "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc -b", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "dev": "astro-scripts dev \"src/**/*.ts\"" + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test": "astro-scripts test \"test/**/*.test.ts\"" }, "dependencies": { "vite": "^8.0.13", @@ -41,6 +43,7 @@ "devDependencies": { "astro": "workspace:*", "astro-scripts": "workspace:*", + "cheerio": "1.2.0", "solid-js": "^1.9.13" }, "peerDependencies": { diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts index 94261e0ec5eb..8f501951b6e9 100644 --- a/packages/integrations/solid/src/client.ts +++ b/packages/integrations/solid/src/client.ts @@ -1,9 +1,58 @@ -import { Suspense } from 'solid-js'; +import { createSignal, Suspense } from 'solid-js'; import { createStore, reconcile } from 'solid-js/store'; import { createComponent, hydrate, render } from 'solid-js/web'; +type SignalTuple = [() => any, (v: any) => any]; +const sharedSignalMap = new Map(); const alreadyInitializedElements = new WeakMap(); +// Signal IDs ending with "!" indicate a setter prop, e.g. "sg0!" = setter, "sg0" = getter. +function decodeSignalRef(ref: string): { id: string; isSetter: boolean } { + if (ref.endsWith('!')) { + return { id: ref.slice(0, -1), isSetter: true }; + } + return { id: ref, isSetter: false }; +} + +function getSignalPart(tuple: SignalTuple, isSetter: boolean): any { + return isSetter ? tuple[1] : tuple[0]; +} + +function restoreSignals(element: HTMLElement, props: Record) { + const signalsRaw = element.dataset.solidSignals; + if (!signalsRaw) return; + + const signals: Record = JSON.parse(signalsRaw); + for (const [propName, signalId] of Object.entries(signals)) { + if (Array.isArray(signalId)) { + // Array or object case: signalId is [ref, indexOrKey][] + signalId.forEach(([ref, indexOrKeyInProps]) => { + const { id, isSetter } = decodeSignalRef(ref); + const mapValue = props[propName][indexOrKeyInProps]; + let valueOfSignal = mapValue; + + // For arrays, the serialized form is [value, originalIndex] + if (typeof indexOrKeyInProps !== 'string') { + valueOfSignal = mapValue[0]; + indexOrKeyInProps = mapValue[1]; + } + + if (!sharedSignalMap.has(id)) { + sharedSignalMap.set(id, createSignal(valueOfSignal)); + } + props[propName][indexOrKeyInProps] = getSignalPart(sharedSignalMap.get(id)!, isSetter); + }); + } else { + // Direct signal prop + const { id, isSetter } = decodeSignalRef(signalId); + if (!sharedSignalMap.has(id)) { + sharedSignalMap.set(id, createSignal(props[propName])); + } + props[propName] = getSignalPart(sharedSignalMap.get(id)!, isSetter); + } + } +} + export default (element: HTMLElement) => (Component: any, props: any, slotted: any, { client }: { client: string }) => { if (!element.hasAttribute('ssr')) return; @@ -33,6 +82,10 @@ export default (element: HTMLElement) => const { default: children, ...slots } = _slots; const renderId = element.dataset.solidRenderId; + + // Restore shared island signals onto props before creating the store + restoreSignals(element, props); + if (alreadyInitializedElements.has(element)) { // update the mounted component alreadyInitializedElements.get(element)!( diff --git a/packages/integrations/solid/src/context.ts b/packages/integrations/solid/src/context.ts index 6e201e3f5534..fe84d980ddb8 100644 --- a/packages/integrations/solid/src/context.ts +++ b/packages/integrations/solid/src/context.ts @@ -1,8 +1,13 @@ -import type { RendererContext } from './types.js'; +import type { IslandAccessor } from './island-signal.js'; +import type { PropNameToSignalMap, RendererContext } from './types.js'; -type Context = { +export type Context = { id: string; c: number; + signalId: string; + sc: number; + signals: Map, string>; + propsToSignals: Map, PropNameToSignalMap>; }; const contexts = new WeakMap(); @@ -16,6 +21,12 @@ export function getContext(result: RendererContext['result']): Context { get id() { return 's' + this.c.toString(); }, + sc: 0, + get signalId() { + return 'sg' + this.sc.toString(); + }, + signals: new Map(), + propsToSignals: new Map(), }; contexts.set(result, ctx); return ctx; @@ -26,3 +37,9 @@ export function incrementId(ctx: Context): string { ctx.c++; return id; } + +export function incrementSignalId(ctx: Context): string { + let id = ctx.signalId; + ctx.sc++; + return id; +} diff --git a/packages/integrations/solid/src/island-signal.ts b/packages/integrations/solid/src/island-signal.ts new file mode 100644 index 000000000000..d23f58bfb3d2 --- /dev/null +++ b/packages/integrations/solid/src/island-signal.ts @@ -0,0 +1,56 @@ +import { createSignal } from 'solid-js'; + +const ISLAND_SIGNAL = Symbol.for('astro.solid.island-signal'); +const ISLAND_SIGNAL_PEEK = Symbol.for('astro.solid.island-signal.peek'); +const ISLAND_SETTER_FOR = Symbol.for('astro.solid.island-signal.setter-for'); + +export type IslandAccessor = (() => T) & { + [ISLAND_SIGNAL]: true; + [ISLAND_SIGNAL_PEEK]: () => T; +}; + +export type IslandSetter = ((v: T | ((prev: T) => T)) => T) & { + [ISLAND_SETTER_FOR]: IslandAccessor; +}; + +/** + * Creates a signal that can be shared across multiple Solid islands. + * Use this in `.astro` frontmatter to pass reactive state to `client:*` components. + * + * ```astro + * --- + * import { createIslandSignal } from '@astrojs/solid-js/signals'; + * const [count, setCount] = createIslandSignal(0); + * --- + * + * + * ``` + */ +export function createIslandSignal(value: T): [IslandAccessor, IslandSetter] { + const [get, set] = createSignal(value); + + const accessor = get as IslandAccessor; + accessor[ISLAND_SIGNAL] = true; + accessor[ISLAND_SIGNAL_PEEK] = () => get(); + + const setter = set as unknown as IslandSetter; + setter[ISLAND_SETTER_FOR] = accessor; + + return [accessor, setter]; +} + +export function isIslandSignal(x: any): x is IslandAccessor { + return typeof x === 'function' && x[ISLAND_SIGNAL] === true; +} + +export function isIslandSetter(x: any): x is IslandSetter { + return typeof x === 'function' && x[ISLAND_SETTER_FOR] != null; +} + +export function peekIslandSignal(x: IslandAccessor): T { + return x[ISLAND_SIGNAL_PEEK](); +} + +export function getSetterAccessor(x: IslandSetter): IslandAccessor { + return x[ISLAND_SETTER_FOR]; +} diff --git a/packages/integrations/solid/src/server.ts b/packages/integrations/solid/src/server.ts index e6390fffe924..f72f7ee78b67 100644 --- a/packages/integrations/solid/src/server.ts +++ b/packages/integrations/solid/src/server.ts @@ -9,7 +9,8 @@ import { ssr, } from 'solid-js/web'; import { getContext, incrementId } from './context.js'; -import type { RendererContext } from './types.js'; +import { replaceSignalsWithValues, restoreSignalsOnProps, serializeSignals } from './signals.js'; +import type { AstroSolidAttrs, RendererContext } from './types.js'; const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); @@ -61,6 +62,12 @@ async function renderToStaticMarkup( const renderStrategy = (metadata?.renderStrategy ?? 'async') as RenderStrategy; + // Restore signals back onto props so that they will be passed as-is to components + let propsMap = restoreSignalsOnProps(ctx, props); + + const attrs: AstroSolidAttrs = {}; + serializeSignals(ctx, props, attrs, propsMap); + const renderFn = () => { const slots: Record = {}; for (const [key, value] of Object.entries(slotted)) { @@ -124,8 +131,13 @@ async function renderToStaticMarkup( }) : renderToString(renderFn, { renderId }); + // Replace signal getters/setters with scalar values so that Astro's serializeProps + // serializes plain values (functions would be dropped by JSON.stringify). + replaceSignalsWithValues(props); + return { attrs: { + ...attrs, 'data-solid-render-id': renderId, }, html: componentHtml, diff --git a/packages/integrations/solid/src/signals.ts b/packages/integrations/solid/src/signals.ts new file mode 100644 index 000000000000..ff99cce3fee2 --- /dev/null +++ b/packages/integrations/solid/src/signals.ts @@ -0,0 +1,144 @@ +import type { Context } from './context.js'; +import { incrementSignalId } from './context.js'; +import { + type IslandAccessor, + getSetterAccessor, + isIslandSetter, + isIslandSignal, + peekIslandSignal, +} from './island-signal.js'; +import type { + ArrayObjectMapping, + AstroSolidAttrs, + PropNameToSignalMap, + Signals, + SignalToKeyOrIndexMap, +} from './types.js'; + +export function restoreSignalsOnProps(ctx: Context, props: Record) { + let propMap: PropNameToSignalMap; + if (ctx.propsToSignals.has(props)) { + propMap = ctx.propsToSignals.get(props)!; + } else { + propMap = new Map(); + ctx.propsToSignals.set(props, propMap); + } + for (const [key, signal] of propMap) { + props[key] = signal; + } + return propMap; +} + +// Returns the accessor for the signal, whether the prop is a getter or setter. +function getAccessorForProp(value: any): IslandAccessor | null { + if (isIslandSignal(value)) return value; + if (isIslandSetter(value)) return getSetterAccessor(value); + return null; +} + +// Signal IDs use a "!" suffix to indicate a setter prop, e.g. "sg0" = getter, "sg0!" = setter. +function encodeSignalRef(id: string, isSetter: boolean): string { + return isSetter ? id + '!' : id; +} + +/** + * Detects island signals in props, builds the serialization mapping for `data-solid-signals`, + * and records what needs to be restored. Does NOT mutate props yet — Solid components need + * the signal getters to remain callable during SSR. Call `replaceSignalsWithValues` after + * rendering to prepare props for Astro's `serializeProps`. + */ +export function serializeSignals( + ctx: Context, + props: Record, + attrs: AstroSolidAttrs, + map: PropNameToSignalMap, +) { + const signals: Signals = {}; + for (const [key, value] of Object.entries(props)) { + const isPropArray = Array.isArray(value); + const isPropObject = + !isIslandSignal(value) && + !isIslandSetter(value) && + typeof value === 'object' && + value !== null && + !isPropArray; + + if (isPropObject || isPropArray) { + const values = isPropObject ? Object.keys(value) : value; + values.forEach((valueKey: number | string, valueIndex: number) => { + const item = isPropObject ? props[key][valueKey] : valueKey; + const accessor = getAccessorForProp(item); + if (accessor) { + const keyOrIndex = isPropObject ? valueKey.toString() : valueIndex; + const isSetter = isIslandSetter(item); + + const currentMap = (map.get(key) || []) as SignalToKeyOrIndexMap; + map.set(key, [...currentMap, [item, keyOrIndex]]); + + const currentSignals = (signals[key] || []) as ArrayObjectMapping; + signals[key] = [ + ...currentSignals, + [encodeSignalRef(getSignalId(ctx, accessor), isSetter), keyOrIndex], + ]; + } + }); + } else { + const accessor = getAccessorForProp(value); + if (accessor) { + const isSetter = isIslandSetter(value); + map.set(key, value); + signals[key] = encodeSignalRef(getSignalId(ctx, accessor), isSetter); + } + } + } + + if (Object.keys(signals).length) { + attrs['data-solid-signals'] = JSON.stringify(signals); + } +} + +/** + * Replaces island signal getters/setters on props with their peeked scalar values. + * Call this AFTER SSR rendering so that Astro's `serializeProps` gets plain values. + */ +export function replaceSignalsWithValues(props: Record) { + for (const [key, value] of Object.entries(props)) { + const isPropArray = Array.isArray(value); + const isPropObject = + !isIslandSignal(value) && + !isIslandSetter(value) && + typeof value === 'object' && + value !== null && + !isPropArray; + + if (isPropObject || isPropArray) { + const values = isPropObject ? Object.keys(value) : value; + values.forEach((valueKey: number | string, valueIndex: number) => { + const item = isPropObject ? props[key][valueKey] : valueKey; + const accessor = getAccessorForProp(item); + if (accessor) { + const keyOrIndex = isPropObject ? valueKey.toString() : valueIndex; + props[key] = isPropObject + ? Object.assign({}, props[key], { [keyOrIndex]: peekIslandSignal(accessor) }) + : props[key].map((v: any, i: number) => + i === valueIndex ? [peekIslandSignal(accessor), i] : v, + ); + } + }); + } else { + const accessor = getAccessorForProp(value); + if (accessor) { + props[key] = peekIslandSignal(accessor); + } + } + } +} + +function getSignalId(ctx: Context, accessor: IslandAccessor) { + let id = ctx.signals.get(accessor); + if (!id) { + id = incrementSignalId(ctx); + ctx.signals.set(accessor, id); + } + return id; +} diff --git a/packages/integrations/solid/src/types.ts b/packages/integrations/solid/src/types.ts index 5dff5b0b4e21..1c896713a687 100644 --- a/packages/integrations/solid/src/types.ts +++ b/packages/integrations/solid/src/types.ts @@ -2,3 +2,17 @@ import type { SSRResult } from 'astro'; export type RendererContext = { result: SSRResult; }; + +// A branded island signal accessor or setter +export type IslandSignalLike = (...args: any[]) => any; + +export type ArrayObjectMapping = [string, number | string][]; +export type Signals = Record; + +export type SignalToKeyOrIndexMap = [IslandSignalLike, number | string][]; +export type PropNameToSignalMap = Map; + +export type AstroSolidAttrs = { + ['data-solid-render-id']?: string; + ['data-solid-signals']?: string; +}; diff --git a/packages/integrations/solid/test/fixtures/signals/astro.config.mjs b/packages/integrations/solid/test/fixtures/signals/astro.config.mjs new file mode 100644 index 000000000000..dcd5306e9485 --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/astro.config.mjs @@ -0,0 +1,6 @@ +import solid from '@astrojs/solid-js'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [solid()], +}); diff --git a/packages/integrations/solid/test/fixtures/signals/package.json b/packages/integrations/solid/test/fixtures/signals/package.json new file mode 100644 index 000000000000..9d17622692f3 --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/solid-signals", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/solid-js": "workspace:*", + "astro": "workspace:*", + "solid-js": "^1.9.13" + } +} diff --git a/packages/integrations/solid/test/fixtures/signals/src/components/SignalCounter.jsx b/packages/integrations/solid/test/fixtures/signals/src/components/SignalCounter.jsx new file mode 100644 index 000000000000..98e10e750b76 --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/src/components/SignalCounter.jsx @@ -0,0 +1,3 @@ +export default function SignalCounter(props) { + return
{props.count()}
; +} diff --git a/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInArray.jsx b/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInArray.jsx new file mode 100644 index 000000000000..020a478bea42 --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInArray.jsx @@ -0,0 +1,12 @@ +export default function SignalsInArray(props) { + return ( +
+

+ {props.signalsArray[0]} {props.signalsArray[3]} +

+

+ {props.signalsArray[1]()}-{props.signalsArray[2]()}-{props.signalsArray[4]()} +

+
+ ); +} diff --git a/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInObject.jsx b/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInObject.jsx new file mode 100644 index 000000000000..0e29f4c60dee --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/src/components/SignalsInObject.jsx @@ -0,0 +1,8 @@ +export default function SignalsInObject(props) { + return ( +
+

{props.signalsObject.title}

+

{props.signalsObject.counter()}

+
+ ); +} diff --git a/packages/integrations/solid/test/fixtures/signals/src/pages/index.astro b/packages/integrations/solid/test/fixtures/signals/src/pages/index.astro new file mode 100644 index 000000000000..10701df5b0ce --- /dev/null +++ b/packages/integrations/solid/test/fixtures/signals/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { createIslandSignal } from '@astrojs/solid-js/signals'; +import SignalCounter from '../components/SignalCounter'; +import SignalsInArray from '../components/SignalsInArray'; +import SignalsInObject from '../components/SignalsInObject'; +const [count] = createIslandSignal(1); +const [secondCount] = createIslandSignal(2); +--- + + + Testing + + + + + + + + diff --git a/packages/integrations/solid/test/signals.test.ts b/packages/integrations/solid/test/signals.test.ts new file mode 100644 index 000000000000..4fa881e478e7 --- /dev/null +++ b/packages/integrations/solid/test/signals.test.ts @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture, type Fixture } from './test-utils.ts'; + +describe('Solid island signals', () => { + let fixture: Fixture; + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/signals/', import.meta.url), + }); + await fixture.build(); + }); + + it('Can use shared signals between islands', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerioLoad(html); + assert.equal($('.solid-signal').length, 2); + + const sigs1Raw = $($('astro-island')[0]).attr('data-solid-signals')!; + const sigs2Raw = $($('astro-island')[1]).attr('data-solid-signals')!; + + assert.notEqual(sigs1Raw, undefined); + assert.notEqual(sigs2Raw, undefined); + + const sigs1 = JSON.parse(sigs1Raw); + const sigs2 = JSON.parse(sigs2Raw); + + assert.notEqual(sigs1.count, undefined); + assert.equal(sigs1.count, sigs2.count); + }); + + it('Can use signals in array', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerioLoad(html); + const element = $('.solid-signal-array'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[2]).attr('data-solid-signals')!; + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsArray: [ + ['sg0', 1], + ['sg0', 2], + ['sg1', 4], + ], + }); + + assert.equal(element.find('h1').text(), "I'm not a signal 12345"); + assert.equal(element.find('p').text(), '1-1-2'); + }); + + it('Can use signals in object', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerioLoad(html); + const element = $('.solid-signal-object'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[3]).attr('data-solid-signals')!; + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsObject: [['sg0', 'counter']], + }); + + assert.equal(element.find('h1').text(), 'I am a title'); + assert.equal(element.find('p').text(), '1'); + }); +}); diff --git a/packages/integrations/solid/test/test-utils.ts b/packages/integrations/solid/test/test-utils.ts new file mode 100644 index 000000000000..f3612bea6b85 --- /dev/null +++ b/packages/integrations/solid/test/test-utils.ts @@ -0,0 +1 @@ +export { loadFixture, type Fixture } from 'astro/_internal/test/test-utils'; diff --git a/packages/integrations/solid/tsconfig.json b/packages/integrations/solid/tsconfig.json index 504a76f4f5ba..c0e0de315104 100644 --- a/packages/integrations/solid/tsconfig.json +++ b/packages/integrations/solid/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../../configs/tsconfig.base.json", "files": [], - "references": [{ "path": "./tsconfig.build.json" }] + "references": [{ "path": "./tsconfig.build.json" }, { "path": "./tsconfig.test.json" }] } diff --git a/packages/integrations/solid/tsconfig.test.json b/packages/integrations/solid/tsconfig.test.json new file mode 100644 index 000000000000..9af37150bd9d --- /dev/null +++ b/packages/integrations/solid/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../configs/tsconfig.test.json", + "references": [{ "path": "./tsconfig.build.json" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b142f5c49053..51d0ab19bcea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1689,6 +1689,18 @@ importers: specifier: ^1.9.11 version: 1.9.13 + packages/astro/e2e/fixtures/solid-signals: + dependencies: + '@astrojs/solid-js': + specifier: workspace:* + version: link:../../../../integrations/solid + astro: + specifier: workspace:* + version: link:../../.. + solid-js: + specifier: ^1.9.13 + version: 1.9.13 + packages/astro/e2e/fixtures/svelte-component: dependencies: '@astrojs/mdx': @@ -5809,6 +5821,21 @@ importers: astro-scripts: specifier: workspace:* version: link:../../../scripts + cheerio: + specifier: 1.2.0 + version: 1.2.0 + solid-js: + specifier: ^1.9.13 + version: 1.9.13 + + packages/integrations/solid/test/fixtures/signals: + dependencies: + '@astrojs/solid-js': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro solid-js: specifier: ^1.9.13 version: 1.9.13