aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/solid/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/solid/src')
-rw-r--r--packages/integrations/solid/src/client.ts77
-rw-r--r--packages/integrations/solid/src/context.ts28
-rw-r--r--packages/integrations/solid/src/index.ts125
-rw-r--r--packages/integrations/solid/src/server.ts143
-rw-r--r--packages/integrations/solid/src/types.ts4
5 files changed, 377 insertions, 0 deletions
diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts
new file mode 100644
index 000000000..f2020bb56
--- /dev/null
+++ b/packages/integrations/solid/src/client.ts
@@ -0,0 +1,77 @@
+import { Suspense } from 'solid-js';
+import { createStore, reconcile } from 'solid-js/store';
+import { createComponent, hydrate, render } from 'solid-js/web';
+
+const alreadyInitializedElements = new WeakMap<Element, any>();
+
+export default (element: HTMLElement) =>
+ (Component: any, props: any, slotted: any, { client }: { client: string }) => {
+ if (!element.hasAttribute('ssr')) return;
+ const isHydrate = client !== 'only';
+ const bootstrap = isHydrate ? hydrate : render;
+
+ let slot: HTMLElement | null;
+ let _slots: Record<string, any> = {};
+ if (Object.keys(slotted).length > 0) {
+ // hydratable
+ if (client !== 'only') {
+ const iterator = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, (node) => {
+ if (node === element) return NodeFilter.FILTER_SKIP;
+ if (node.nodeName === 'ASTRO-SLOT') return NodeFilter.FILTER_ACCEPT;
+ if (node.nodeName === 'ASTRO-ISLAND') return NodeFilter.FILTER_REJECT;
+ return NodeFilter.FILTER_SKIP;
+ });
+ while ((slot = iterator.nextNode() as HTMLElement | null))
+ _slots[slot.getAttribute('name') || 'default'] = slot;
+ }
+ for (const [key, value] of Object.entries(slotted)) {
+ if (_slots[key]) continue;
+ _slots[key] = document.createElement('astro-slot');
+ if (key !== 'default') _slots[key].setAttribute('name', key);
+ _slots[key].innerHTML = value;
+ }
+ }
+
+ const { default: children, ...slots } = _slots;
+ const renderId = element.dataset.solidRenderId;
+ if (alreadyInitializedElements.has(element)) {
+ // update the mounted component
+ alreadyInitializedElements.get(element)!(
+ // reconcile will make sure to apply as little updates as possible, and also remove missing values w/o breaking reactivity
+ reconcile({
+ ...props,
+ ...slots,
+ children,
+ }),
+ );
+ } else {
+ const [store, setStore] = createStore({
+ ...props,
+ ...slots,
+ children,
+ });
+ // store the function to update the current mounted component
+ alreadyInitializedElements.set(element, setStore);
+
+ const dispose = bootstrap(
+ () => {
+ const inner = () => createComponent(Component, store);
+
+ if (isHydrate) {
+ return createComponent(Suspense, {
+ get children() {
+ return inner();
+ },
+ });
+ } else {
+ return inner();
+ }
+ },
+ element,
+ {
+ renderId,
+ },
+ );
+ element.addEventListener('astro:unmount', () => dispose(), { once: true });
+ }
+ };
diff --git a/packages/integrations/solid/src/context.ts b/packages/integrations/solid/src/context.ts
new file mode 100644
index 000000000..6e201e3f5
--- /dev/null
+++ b/packages/integrations/solid/src/context.ts
@@ -0,0 +1,28 @@
+import type { RendererContext } from './types.js';
+
+type Context = {
+ id: string;
+ c: number;
+};
+
+const contexts = new WeakMap<RendererContext['result'], Context>();
+
+export function getContext(result: RendererContext['result']): Context {
+ if (contexts.has(result)) {
+ return contexts.get(result)!;
+ }
+ let ctx: Context = {
+ c: 0,
+ get id() {
+ return 's' + this.c.toString();
+ },
+ };
+ 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/solid/src/index.ts b/packages/integrations/solid/src/index.ts
new file mode 100644
index 000000000..deab40fd0
--- /dev/null
+++ b/packages/integrations/solid/src/index.ts
@@ -0,0 +1,125 @@
+import type {
+ AstroIntegration,
+ AstroIntegrationLogger,
+ AstroRenderer,
+ ContainerRenderer,
+} from 'astro';
+import type { PluginOption, UserConfig } from 'vite';
+import solid, { type Options as ViteSolidPluginOptions } from 'vite-plugin-solid';
+
+// TODO: keep in sync with https://github.com/thetarnav/solid-devtools/blob/main/packages/main/src/vite/index.ts#L7
+type DevtoolsPluginOptions = {
+ /** Add automatic name when creating signals, memos, stores, or mutables */
+ autoname?: boolean;
+ locator?:
+ | boolean
+ | {
+ /** Choose in which IDE the component source code should be revealed. */
+ targetIDE?: string;
+ /**
+ * Holding which key should enable the locator overlay?
+ * @default 'Alt'
+ */
+ key?: string;
+ /** Inject location attributes to jsx templates */
+ jsxLocation?: boolean;
+ /** Inject location information to component declarations */
+ componentLocation?: boolean;
+ };
+};
+type DevtoolsPlugin = (_options?: DevtoolsPluginOptions) => PluginOption;
+
+async function getDevtoolsPlugin(logger: AstroIntegrationLogger, retrieve: boolean) {
+ if (!retrieve) {
+ return null;
+ }
+
+ try {
+ // @ts-ignore
+ return (await import('solid-devtools/vite')).default as DevtoolsPlugin;
+ } catch (_) {
+ logger.warn(
+ 'Solid Devtools requires `solid-devtools` as a peer dependency, add it to your project.',
+ );
+ return null;
+ }
+}
+
+function getViteConfiguration(
+ { include, exclude }: Options,
+ devtoolsPlugin: DevtoolsPlugin | null,
+) {
+ const config: UserConfig = {
+ optimizeDeps: {
+ include: ['@astrojs/solid-js/client.js'],
+ exclude: ['@astrojs/solid-js/server.js'],
+ },
+ plugins: [solid({ include, exclude, ssr: true })],
+ };
+
+ if (devtoolsPlugin) {
+ config.plugins?.push(devtoolsPlugin({ autoname: true }));
+ }
+
+ return config;
+}
+
+function getRenderer(): AstroRenderer {
+ return {
+ name: '@astrojs/solid-js',
+ clientEntrypoint: '@astrojs/solid-js/client.js',
+ serverEntrypoint: '@astrojs/solid-js/server.js',
+ };
+}
+
+export function getContainerRenderer(): ContainerRenderer {
+ return {
+ name: '@astrojs/solid',
+ serverEntrypoint: '@astrojs/solid-js/server.js',
+ };
+}
+
+export interface Options extends Pick<ViteSolidPluginOptions, 'include' | 'exclude'> {
+ devtools?: boolean;
+}
+
+export default function (options: Options = {}): AstroIntegration {
+ return {
+ name: '@astrojs/solid-js',
+ hooks: {
+ 'astro:config:setup': async ({
+ command,
+ addRenderer,
+ updateConfig,
+ injectScript,
+ logger,
+ }) => {
+ const devtoolsPlugin = await getDevtoolsPlugin(
+ logger,
+ !!options.devtools && command === 'dev',
+ );
+
+ addRenderer(getRenderer());
+ updateConfig({
+ vite: getViteConfiguration(options, devtoolsPlugin),
+ });
+
+ if (devtoolsPlugin) {
+ injectScript('page', 'import "solid-devtools";');
+ }
+ },
+ 'astro:config:done': ({ logger, config }) => {
+ const knownJsxRenderers = ['@astrojs/react', '@astrojs/preact', '@astrojs/solid-js'];
+ const enabledKnownJsxRenderers = config.integrations.filter((renderer) =>
+ knownJsxRenderers.includes(renderer.name),
+ );
+
+ if (enabledKnownJsxRenderers.length > 1 && !options.include && !options.exclude) {
+ logger.warn(
+ 'More than one JSX renderer is enabled. This will lead to unexpected behavior unless you set the `include` or `exclude` option. See https://docs.astro.build/en/guides/integrations-guide/solid-js/#combining-multiple-jsx-frameworks for more information.',
+ );
+ }
+ },
+ },
+ };
+}
diff --git a/packages/integrations/solid/src/server.ts b/packages/integrations/solid/src/server.ts
new file mode 100644
index 000000000..8d66fffe1
--- /dev/null
+++ b/packages/integrations/solid/src/server.ts
@@ -0,0 +1,143 @@
+import type { NamedSSRLoadedRendererValue } from 'astro';
+import {
+ NoHydration,
+ Suspense,
+ createComponent,
+ generateHydrationScript,
+ renderToString,
+ renderToStringAsync,
+ ssr,
+} from 'solid-js/web';
+import { getContext, incrementId } from './context.js';
+import type { RendererContext } from './types.js';
+
+const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
+
+type RenderStrategy = 'sync' | 'async';
+
+async function check(
+ this: RendererContext,
+ Component: any,
+ props: Record<string, any>,
+ children: any,
+) {
+ if (typeof Component !== 'function') return false;
+ if (Component.name === 'QwikComponent') return false;
+ // Svelte component renders fine by Solid as an empty string. The only way to detect
+ // if this isn't a Solid but Svelte component is to unfortunately copy the check
+ // implementation of the Svelte renderer.
+ if (Component.toString().includes('$$payload')) return false;
+
+ // There is nothing particularly special about Solid components. Basically they are just functions.
+ // In general, components from other frameworks (eg, MDX, React, etc.) tend to render as "undefined",
+ // so we take advantage of this trick to decide if this is a Solid component or not.
+
+ let html: string | undefined;
+ try {
+ const result = await renderToStaticMarkup.call(this, Component, props, children, {
+ // The purpose of check() is just to validate that this is a Solid component and not
+ // React, etc. We should render in sync mode which should skip Suspense boundaries
+ // or loading resources like external API calls.
+ renderStrategy: 'sync' as RenderStrategy,
+ });
+ html = result.html;
+ } catch {}
+
+ return typeof html === 'string';
+}
+
+// AsyncRendererComponentFn
+async function renderToStaticMarkup(
+ this: RendererContext,
+ Component: any,
+ props: Record<string, any>,
+ { default: children, ...slotted }: any,
+ metadata?: Record<string, any>,
+) {
+ const ctx = getContext(this.result);
+ const renderId = metadata?.hydrate ? incrementId(ctx) : '';
+ const needsHydrate = metadata?.astroStaticSlot ? !!metadata.hydrate : true;
+ const tagName = needsHydrate ? 'astro-slot' : 'astro-static-slot';
+
+ const renderStrategy = (metadata?.renderStrategy ?? 'async') as RenderStrategy;
+
+ const renderFn = () => {
+ const slots: Record<string, any> = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = ssr(`<${tagName} name="${name}">${value}</${tagName}>`);
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
+ ...props,
+ ...slots,
+ // In Solid SSR mode, `ssr` creates the expected structure for `children`.
+ children: children != null ? ssr(`<${tagName}>${children}</${tagName}>`) : children,
+ };
+
+ if (renderStrategy === 'sync') {
+ // Sync Render:
+ // <Component />
+ // This render mode is not exposed directly to the end user. It is only
+ // used in the check() function.
+ return createComponent(Component, newProps);
+ } else {
+ if (needsHydrate) {
+ // Hydrate + Async Render:
+ // <Suspense>
+ // <Component />
+ // </Suspense>
+ return createComponent(Suspense, {
+ get children() {
+ return createComponent(Component, newProps);
+ },
+ });
+ } else {
+ // Static + Async Render
+ // <NoHydration>
+ // <Suspense>
+ // <Component />
+ // </Suspense>
+ // </NoHydration>
+ return createComponent(NoHydration, {
+ get children() {
+ return createComponent(Suspense, {
+ get children() {
+ return createComponent(Component, newProps);
+ },
+ });
+ },
+ });
+ }
+ }
+ };
+
+ const componentHtml =
+ renderStrategy === 'async'
+ ? await renderToStringAsync(renderFn, {
+ renderId,
+ // New setting since Solid 1.8.4 that fixes an errant hydration event appearing in
+ // server only components. Not available in TypeScript types yet.
+ // https://github.com/solidjs/solid/issues/1931
+ // https://github.com/ryansolid/dom-expressions/commit/e09e255ac725fd59195aa0f3918065d4bd974e6b
+ ...({ noScripts: !needsHydrate } as any),
+ })
+ : renderToString(renderFn, { renderId });
+
+ return {
+ attrs: {
+ 'data-solid-render-id': renderId,
+ },
+ html: componentHtml,
+ };
+}
+
+const renderer: NamedSSRLoadedRendererValue = {
+ name: '@astrojs/solid',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+ renderHydrationScript: () => generateHydrationScript(),
+};
+
+export default renderer;
diff --git a/packages/integrations/solid/src/types.ts b/packages/integrations/solid/src/types.ts
new file mode 100644
index 000000000..5dff5b0b4
--- /dev/null
+++ b/packages/integrations/solid/src/types.ts
@@ -0,0 +1,4 @@
+import type { SSRResult } from 'astro';
+export type RendererContext = {
+ result: SSRResult;
+};