@ilha/astro 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@ilha/astro`
2
2
 
3
- An [Astro](https://astro.build) integration that renders and hydrates [Ilha](https://github.com/ilhajs/ilha) islands as Astro components.
3
+ An [Astro](https://astro.build) integration that renders and hydrates [Ilha](https://github.com/ilhajs/ilha) components as Astro islands.
4
4
 
5
5
  ---
6
6
 
@@ -34,9 +34,7 @@ export default defineConfig({
34
34
 
35
35
  You can also pass `exclude` globs. Files outside the filter remain available to other Astro JSX renderers.
36
36
 
37
- Other JSX renderers (e.g. `@astrojs/solid-js`) can appear before or after `@ilha/astro` — Ilha islands are tagged with Astro's `astro:renderer` symbol, so Astro routes them to Ilha's renderer regardless of integration order.
38
-
39
- Then use any Ilha island as an Astro component, with a [client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) to control hydration:
37
+ Then use any Ilha component as an Astro component, with a [client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) to control hydration:
40
38
 
41
39
  ```astro
42
40
  ---
@@ -48,12 +46,12 @@ import { Counter } from "../islands/counter";
48
46
 
49
47
  ## How it works
50
48
 
51
- - **Server-side:** the component is rendered with [`.hydratable()`](https://github.com/ilhajs/ilha), which wraps the island's SSR output in a `[data-ilha]` element carrying serialized props and a state snapshot.
52
- - **Client-side:** on hydration, the renderer finds that `[data-ilha]` element inside Astro's island wrapper and calls the island's own `.mount()` — no re-render, no flicker, no post-mount setup needed since the snapshot already matches the DOM.
49
+ - **Server-side:** the renderer calls `renderToString()`, which wraps output in a `[data-ilha]` element with a state snapshot.
50
+ - **Client-side:** the renderer finds that host inside Astro's island wrapper and calls `mount(..., { hydrate: true })`.
53
51
 
54
- Because hydration is driven entirely by Ilha's own `data-ilha-*` attributes, every [Astro client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) (`client:load`, `client:idle`, `client:visible`, `client:media`, `client:only`) works as expected.
52
+ Every [Astro client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives) (`client:load`, `client:idle`, `client:visible`, `client:media`, `client:only`) works as expected.
55
53
 
56
54
  ## Notes
57
55
 
58
- - Ilha islands don't support Astro's `<slot />` / children forwarding — render everything through the island component and its props (via `Island.toString()` / `.hydratable()` on the server).
59
- - `client:only` skips SSR entirely and mounts fresh in the browser, matching Astro's usual behavior for other framework renderers.
56
+ - Astro slots arrive as string children. Plain text renders; HTML in slot content is escaped — don't pass markup through slots.
57
+ - `client:only` skips SSR and mounts fresh in the browser.
package/dist/client.js CHANGED
@@ -1,41 +1,13 @@
1
- import { i as wrapPlainAsIsland } from "./plain-Dy-BNLLq.js";
1
+ import { mount } from "ilha";
2
2
 
3
3
  //#region src/client.ts
4
- function isMountable(Component) {
5
- return !!Component && typeof Component.mount === "function";
6
- }
7
4
  var client_default = (element) => async (Component, props, slotted, _metadata) => {
8
- if (!element.hasAttribute("ssr")) {
9
- if (isMountable(Component)) {
10
- const unmount = Component.mount(element, props);
11
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
12
- }
13
- return;
14
- }
15
- if (isMountable(Component)) {
16
- const host = element.querySelector("[data-ilha]") ?? element;
17
- const unmount = Component.mount(host);
18
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
19
- return;
20
- }
21
5
  if (typeof Component !== "function") return;
22
6
  const merged = { ...props };
23
7
  for (const [name, value] of Object.entries(slotted ?? {})) merged[name === "default" ? "children" : name] = value;
24
- const nestedHost = element.querySelector("[data-ilha]");
25
- if (nestedHost?.querySelector("[data-ilha-slot]")) {
26
- try {
27
- const unmount = wrapPlainAsIsland(Component, merged).mount(nestedHost);
28
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
29
- } catch {
30
- throw new Error(`[@ilha/astro] Failed to mount nested islands inside plain component — expected a function that returns ilha RawHtml/string.`);
31
- }
32
- return;
33
- }
34
- try {
35
- Component(merged);
36
- } catch {
37
- throw new Error(`[@ilha/astro] Failed to bind client component — expected a function that can re-run against SSR markup.`);
38
- }
8
+ const nested = element.querySelector("[data-ilha]");
9
+ const unmount = mount(nested ?? element, () => Component(merged), { hydrate: !!nested });
10
+ element.addEventListener("astro:unmount", () => unmount(), { once: true });
39
11
  };
40
12
 
41
13
  //#endregion
package/dist/index.js CHANGED
@@ -1,10 +1,4 @@
1
1
  //#region src/index.ts
2
- const ASTRO_RENDERER_GLOBAL = Symbol.for("ilha.astroRenderer");
3
- const rendererName = "@ilha/astro";
4
- function setRendererMarker() {
5
- globalThis[ASTRO_RENDERER_GLOBAL] = rendererName;
6
- }
7
- setRendererMarker();
8
2
  const OPTIONS_MODULE = "virtual:@ilha/astro/options";
9
3
  function globToRegExp(pattern) {
10
4
  let source = "^";
@@ -63,7 +57,6 @@ function ilhaIntegration(options = {}) {
63
57
  return {
64
58
  name: "@ilha/astro",
65
59
  hooks: { "astro:config:setup": ({ addRenderer, updateConfig }) => {
66
- setRendererMarker();
67
60
  addRenderer(getRenderer());
68
61
  updateConfig({ vite: getViteConfig(options) });
69
62
  } }
package/dist/server.js CHANGED
@@ -1,20 +1,12 @@
1
- import { i as wrapPlainAsIsland, n as isRawHtml, r as resultToHtml, t as isIsland } from "./plain-Dy-BNLLq.js";
2
- import { raw } from "ilha";
1
+ import { renderToString } from "ilha";
3
2
 
4
3
  //#region src/server.ts
5
4
  let componentFilter;
6
5
  function getComponentFilter() {
7
6
  return componentFilter ??= import("virtual:@ilha/astro/options").then(({ default: filter }) => filter).catch(() => () => true);
8
7
  }
9
- /** True when plain SSR emitted nested island slot hosts that need a parent mount ctx. */
10
- function hasNestedIslandSlots(html) {
11
- return html.includes("data-ilha-slot");
12
- }
13
- function toRawHtml(value) {
14
- return raw(String(value));
15
- }
16
8
  function isAstroRenderable(value) {
17
- return !!value && typeof value === "object" && !isRawHtml(value) && typeof value.render === "function";
9
+ return !!value && typeof value === "object" && typeof value.render === "function";
18
10
  }
19
11
  async function resolveAstroRenderable(value) {
20
12
  let out = "";
@@ -24,49 +16,24 @@ async function resolveAstroRenderable(value) {
24
16
  return out;
25
17
  }
26
18
  async function resolvePropValue(value) {
27
- if (isAstroRenderable(value)) return toRawHtml(await resolveAstroRenderable(value));
19
+ if (isAstroRenderable(value)) return await resolveAstroRenderable(value);
28
20
  if (Array.isArray(value)) return Promise.all(value.map(resolvePropValue));
29
21
  return value;
30
22
  }
31
- async function resolveProps(props) {
23
+ async function mergeProps(props, slots) {
32
24
  const resolved = {};
33
25
  for (const [key, value] of Object.entries(props)) resolved[key] = await resolvePropValue(value);
26
+ for (const [name, value] of Object.entries(slots)) resolved[name === "default" ? "children" : name] = value;
34
27
  return resolved;
35
28
  }
36
- async function mergePlainProps(props, slots) {
37
- const merged = await resolveProps(props);
38
- for (const [name, value] of Object.entries(slots)) merged[name === "default" ? "children" : name] = toRawHtml(value);
39
- return merged;
40
- }
41
- async function renderPlainComponent(Component, props, slots, metadata) {
42
- const merged = await mergePlainProps(props, slots);
43
- const result = Component(merged);
44
- const html = resultToHtml(result);
45
- if (html === void 0) return void 0;
46
- if (!hasNestedIslandSlots(html)) return html;
47
- return wrapPlainAsIsland(Component, merged).hydratable({}, {
48
- name: metadata?.displayName ?? "IlhaPlain",
49
- snapshot: true
50
- });
51
- }
52
- async function check(Component, props, slots, metadata) {
29
+ async function check(Component, _props, _slots, metadata) {
53
30
  if (metadata?.componentUrl && !(await getComponentFilter())(metadata.componentUrl)) return false;
54
- if (isIsland(Component)) return true;
55
- if (typeof Component !== "function") return false;
56
- try {
57
- return await renderPlainComponent(Component, props, slots) !== void 0;
58
- } catch {
59
- return false;
60
- }
31
+ return typeof Component === "function";
61
32
  }
62
33
  async function renderToStaticMarkup(Component, props, slots, metadata) {
63
- if (isIsland(Component)) return { html: await Component.hydratable(await resolveProps(props), {
64
- name: metadata?.displayName ?? "IlhaIsland",
65
- snapshot: true
66
- }) };
67
- const html = await renderPlainComponent(Component, props, slots, metadata);
68
- if (html === void 0) throw new Error(`[@ilha/astro] Unable to render "${metadata?.displayName ?? "component"}" — expected an ilha island or a function returning ilha RawHtml/string.`);
69
- return { html };
34
+ if (typeof Component !== "function") throw new Error(`[@ilha/astro] Unable to render "${metadata?.displayName ?? "component"}" — expected a function.`);
35
+ const merged = await mergeProps(props, slots);
36
+ return { html: await renderToString(() => Component(merged)) };
70
37
  }
71
38
  const renderer = {
72
39
  name: "@ilha/astro",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/astro",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Astro integration for Ilha islands",
5
5
  "keywords": [
6
6
  "astro-component",
@@ -27,6 +27,7 @@
27
27
  ],
28
28
  "type": "module",
29
29
  "sideEffects": false,
30
+ "main": "./dist/index.js",
30
31
  "module": "./dist/index.js",
31
32
  "types": "./dist/index.d.ts",
32
33
  "exports": {
@@ -49,14 +50,15 @@
49
50
  "scripts": {
50
51
  "build": "tsdown && tsc -p tsconfig.build.json",
51
52
  "cleanup": "rimraf dist node_modules",
52
- "test": "bun test"
53
+ "test": "bun test src/index.test.ts",
54
+ "test:integration": "bun test src/integration.test.ts"
53
55
  },
54
56
  "devDependencies": {
55
57
  "astro": "^7.2.9",
56
- "ilha": "0.13.0"
58
+ "ilha": "0.14.0"
57
59
  },
58
60
  "peerDependencies": {
59
61
  "astro": "^7.2.9",
60
- "ilha": "^0.13.0"
62
+ "ilha": "^0.14.0"
61
63
  }
62
64
  }
@@ -1,24 +0,0 @@
1
- import { ilha, raw } from "ilha";
2
-
3
- //#region src/plain.ts
4
- const ISLAND = Symbol.for("ilha.island");
5
- const RAW = Symbol.for("ilha.raw");
6
- function isIsland(Component) {
7
- return typeof Component === "function" && ISLAND in Component;
8
- }
9
- function isRawHtml(value) {
10
- return !!(value && typeof value === "object" && RAW in value);
11
- }
12
- function resultToHtml(result) {
13
- if (typeof result === "string") return result;
14
- if (isRawHtml(result)) return result.value;
15
- }
16
- function wrapPlainAsIsland(Component, merged) {
17
- return ilha(() => {
18
- const html = resultToHtml(Component(merged));
19
- return html === void 0 ? raw("") : raw(html);
20
- });
21
- }
22
-
23
- //#endregion
24
- export { wrapPlainAsIsland as i, isRawHtml as n, resultToHtml as r, isIsland as t };
package/dist/plain.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { type Island } from "ilha";
2
- export type PlainComponent = (props: Record<string, unknown>) => unknown;
3
- export declare function isIsland(Component: unknown): Component is Island;
4
- export declare function isRawHtml(value: unknown): value is {
5
- value: string;
6
- };
7
- export declare function resultToHtml(result: unknown): string | undefined;
8
- export declare function wrapPlainAsIsland(Component: PlainComponent, merged: Record<string, unknown>): Island<unknown>;