@ilha/astro 0.2.1 → 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,54 +1,13 @@
1
- import { ilha, raw } from "ilha";
1
+ import { mount } from "ilha";
2
2
 
3
3
  //#region src/client.ts
4
- const RAW = Symbol.for("ilha.raw");
5
- function isMountable(Component) {
6
- return !!Component && typeof Component.mount === "function";
7
- }
8
- function isRawHtml(value) {
9
- return !!(value && typeof value === "object" && RAW in value);
10
- }
11
- function wrapPlainAsIsland(Component, merged) {
12
- return ilha(() => {
13
- const result = Component(merged);
14
- if (result == null) return raw("");
15
- if (typeof result === "string") return raw(result);
16
- if (isRawHtml(result)) return raw(result.value);
17
- return raw("");
18
- });
19
- }
20
4
  var client_default = (element) => async (Component, props, slotted, _metadata) => {
21
- if (!element.hasAttribute("ssr")) {
22
- if (isMountable(Component)) {
23
- const unmount = Component.mount(element, props);
24
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
25
- }
26
- return;
27
- }
28
- if (isMountable(Component)) {
29
- const host = element.querySelector("[data-ilha]") ?? element;
30
- const unmount = Component.mount(host);
31
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
32
- return;
33
- }
34
5
  if (typeof Component !== "function") return;
35
6
  const merged = { ...props };
36
7
  for (const [name, value] of Object.entries(slotted ?? {})) merged[name === "default" ? "children" : name] = value;
37
- const nestedHost = element.querySelector("[data-ilha]");
38
- if (nestedHost?.querySelector("[data-ilha-slot]")) {
39
- try {
40
- const unmount = wrapPlainAsIsland(Component, merged).mount(nestedHost);
41
- element.addEventListener("astro:unmount", () => unmount(), { once: true });
42
- } catch {
43
- throw new Error(`[@ilha/astro] Failed to mount nested islands inside plain component — expected a function that returns ilha RawHtml/string.`);
44
- }
45
- return;
46
- }
47
- try {
48
- Component(merged);
49
- } catch {
50
- throw new Error(`[@ilha/astro] Failed to bind client component — expected a function that can re-run against SSR markup.`);
51
- }
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 });
52
11
  };
53
12
 
54
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,45 +1,12 @@
1
- import { ilha, raw } from "ilha";
1
+ import { renderToString } from "ilha";
2
2
 
3
3
  //#region src/server.ts
4
- const ISLAND = Symbol.for("ilha.island");
5
- const RAW = Symbol.for("ilha.raw");
6
4
  let componentFilter;
7
5
  function getComponentFilter() {
8
6
  return componentFilter ??= import("virtual:@ilha/astro/options").then(({ default: filter }) => filter).catch(() => () => true);
9
7
  }
10
- function isIsland(Component) {
11
- return typeof Component === "function" && ISLAND in Component;
12
- }
13
- function isRawHtml(value) {
14
- return !!(value && typeof value === "object" && RAW in value);
15
- }
16
- function resultToHtml(result) {
17
- if (typeof result === "string") return result;
18
- if (isRawHtml(result)) return result.value;
19
- }
20
- /** True when plain SSR emitted nested island slot hosts that need a parent mount ctx. */
21
- function hasNestedIslandSlots(html) {
22
- return html.includes("data-ilha-slot");
23
- }
24
- /**
25
- * Wrap a plain function so its render runs under an island render ctx.
26
- * Nested islands then get unique `data-ilha-slot` ids and can be mounted
27
- * when the shell hydrates on the client.
28
- */
29
- function wrapPlainAsIsland(Component, merged) {
30
- return ilha(() => {
31
- const result = Component(merged);
32
- if (result == null) return raw("");
33
- if (typeof result === "string") return raw(result);
34
- if (isRawHtml(result)) return raw(result.value);
35
- return raw("");
36
- });
37
- }
38
- function toRawHtml(value) {
39
- return raw(String(value));
40
- }
41
8
  function isAstroRenderable(value) {
42
- return !!value && typeof value === "object" && !isRawHtml(value) && typeof value.render === "function";
9
+ return !!value && typeof value === "object" && typeof value.render === "function";
43
10
  }
44
11
  async function resolveAstroRenderable(value) {
45
12
  let out = "";
@@ -49,48 +16,24 @@ async function resolveAstroRenderable(value) {
49
16
  return out;
50
17
  }
51
18
  async function resolvePropValue(value) {
52
- if (isAstroRenderable(value)) return toRawHtml(await resolveAstroRenderable(value));
19
+ if (isAstroRenderable(value)) return await resolveAstroRenderable(value);
53
20
  if (Array.isArray(value)) return Promise.all(value.map(resolvePropValue));
54
21
  return value;
55
22
  }
56
- async function resolveProps(props) {
23
+ async function mergeProps(props, slots) {
57
24
  const resolved = {};
58
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;
59
27
  return resolved;
60
28
  }
61
- async function mergePlainProps(props, slots) {
62
- const merged = await resolveProps(props);
63
- for (const [name, value] of Object.entries(slots)) merged[name === "default" ? "children" : name] = toRawHtml(value);
64
- return merged;
65
- }
66
- async function renderPlainComponent(Component, props, slots, metadata) {
67
- const merged = await mergePlainProps(props, slots);
68
- const html = resultToHtml(Component(merged));
69
- if (html === void 0) return void 0;
70
- if (!hasNestedIslandSlots(html)) return html;
71
- return wrapPlainAsIsland(Component, merged).hydratable({}, {
72
- name: metadata?.displayName ?? "IlhaPlain",
73
- snapshot: true
74
- });
75
- }
76
- async function check(Component, props, slots, metadata) {
29
+ async function check(Component, _props, _slots, metadata) {
77
30
  if (metadata?.componentUrl && !(await getComponentFilter())(metadata.componentUrl)) return false;
78
- if (isIsland(Component)) return true;
79
- if (typeof Component !== "function") return false;
80
- try {
81
- return await renderPlainComponent(Component, props, slots) !== void 0;
82
- } catch {
83
- return false;
84
- }
31
+ return typeof Component === "function";
85
32
  }
86
33
  async function renderToStaticMarkup(Component, props, slots, metadata) {
87
- if (isIsland(Component)) return { html: await Component.hydratable(await resolveProps(props), {
88
- name: metadata?.displayName ?? "IlhaIsland",
89
- snapshot: true
90
- }) };
91
- const html = await renderPlainComponent(Component, props, slots, metadata);
92
- 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.`);
93
- 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)) };
94
37
  }
95
38
  const renderer = {
96
39
  name: "@ilha/astro",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/astro",
3
- "version": "0.2.1",
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
- "astro": "^7.2.4",
56
- "ilha": "^0.12.1"
57
+ "astro": "^7.2.9",
58
+ "ilha": "0.14.0"
57
59
  },
58
60
  "peerDependencies": {
59
- "astro": "^7.2.4",
60
- "ilha": ">=0.12.1"
61
+ "astro": "^7.2.9",
62
+ "ilha": "^0.14.0"
61
63
  }
62
64
  }