atollic 0.0.2
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 +432 -0
- package/dist/adapter.d.ts +39 -0
- package/dist/adapter.js +0 -0
- package/dist/adapters/solid.d.ts +10 -0
- package/dist/adapters/solid.js +37 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +113 -0
- package/dist/head.d.ts +14 -0
- package/dist/head.js +7 -0
- package/dist/html/index.d.ts +2 -0
- package/dist/html/index.js +2 -0
- package/dist/html/jsx-dev-runtime.d.ts +3 -0
- package/dist/html/jsx-dev-runtime.js +7 -0
- package/dist/html/jsx-runtime.d.ts +5 -0
- package/dist/html/jsx-runtime.js +61 -0
- package/dist/html/types.d.ts +559 -0
- package/dist/html/types.js +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/server-DhToIUB0.js +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/servers/elysia.d.ts +29 -0
- package/dist/servers/elysia.js +12 -0
- package/dist/servers/hono.d.ts +2 -0
- package/dist/servers/hono.js +15 -0
- package/dist/shared-CfRCLggd.js +23 -0
- package/dist/shared.d.ts +13 -0
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +313 -0
- package/package.json +127 -0
package/README.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# atollic
|
|
2
|
+
|
|
3
|
+
Island architecture for WinterCG-compatible runtimes. Bring your own server (Elysia, Hono, …) and your own UI framework, powered by Vite.
|
|
4
|
+
|
|
5
|
+
> **Status: experimental.** atollic is pre-1.0 (`v0.0.x`). The API may change between minor versions until 1.0.
|
|
6
|
+
|
|
7
|
+
- **Server-agnostic** — any WinterCG runtime that speaks `Request`/`Response` (Elysia, Hono, Bun, Node via adapter, Workers, …).
|
|
8
|
+
- **UI-framework-agnostic** — ships with a Solid.js adapter; Preact / React / others pluggable via `FrameworkAdapter`.
|
|
9
|
+
- **Zero-JS by default** — pages render to HTML strings on the server; only `"use client"` islands ship JavaScript.
|
|
10
|
+
- **HMR with state preserved** — server changes morph the DOM via idiomorph, keeping mounted islands alive.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Server (Elysia, Hono, ...) Client (Browser)
|
|
16
|
+
───────────────────────────── ─────────────────────────
|
|
17
|
+
1. Route handler returns JSX 4. Find [data-island] elements
|
|
18
|
+
2. Islands SSR to real HTML 5. Lazy-import component module
|
|
19
|
+
3. Full page sent to browser 6. Hydrate with matching props
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Pages are **server-rendered JSX** using atollic's built-in HTML runtime — no virtual DOM, just strings.
|
|
23
|
+
- Components marked with `"use client"` become **islands** — they SSR on the server and hydrate on the client.
|
|
24
|
+
- Everything else is zero-JS static HTML.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun add atollic elysia solid-js vite-plugin-solid
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Project structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-app/
|
|
36
|
+
src/
|
|
37
|
+
app.tsx # Server entry — routes and layouts
|
|
38
|
+
islands/
|
|
39
|
+
Counter.tsx # Interactive island component
|
|
40
|
+
vite.config.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `vite.config.ts`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { defineConfig } from "vite";
|
|
47
|
+
import { solid } from "atollic/solid";
|
|
48
|
+
import { atollic } from "atollic/vite";
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
plugins: [
|
|
52
|
+
atollic({
|
|
53
|
+
entry: "./src/app.tsx",
|
|
54
|
+
frameworks: [solid()],
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `src/app.tsx` — Server entry (Elysia)
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Elysia } from "elysia";
|
|
64
|
+
import { Head } from "atollic/head";
|
|
65
|
+
import { atollic } from "atollic/elysia";
|
|
66
|
+
import Counter from "./islands/Counter.js";
|
|
67
|
+
|
|
68
|
+
const app = new Elysia()
|
|
69
|
+
.use(atollic())
|
|
70
|
+
.get("/", () => (
|
|
71
|
+
<html lang="en">
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="UTF-8" />
|
|
74
|
+
<Head />
|
|
75
|
+
</head>
|
|
76
|
+
<body>
|
|
77
|
+
<h1>Hello from the server</h1>
|
|
78
|
+
<Counter initial={0} />
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
));
|
|
82
|
+
|
|
83
|
+
export default app.handle;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `src/islands/Counter.tsx` — Island component
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
/** @jsxImportSource solid-js */
|
|
90
|
+
"use client";
|
|
91
|
+
|
|
92
|
+
import { createSignal } from "solid-js";
|
|
93
|
+
|
|
94
|
+
export default function Counter(props: { initial: number }) {
|
|
95
|
+
const [count, setCount] = createSignal(props.initial);
|
|
96
|
+
return (
|
|
97
|
+
<button onClick={() => setCount((c) => c + 1)}>
|
|
98
|
+
Count: {count()}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Run
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
bunx --bun vite # Dev server with HMR
|
|
108
|
+
bunx --bun vite build # Production build
|
|
109
|
+
bun dist/server/app.js # Start production server
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Server adapters
|
|
113
|
+
|
|
114
|
+
Atollic is decoupled from any specific server. Your entry file exports a fetch function — atollic handles the rest.
|
|
115
|
+
|
|
116
|
+
### Elysia
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Elysia } from "elysia";
|
|
120
|
+
import { atollic } from "atollic/elysia";
|
|
121
|
+
|
|
122
|
+
const app = new Elysia()
|
|
123
|
+
.use(atollic())
|
|
124
|
+
.get("/", () => <h1>Hello</h1>);
|
|
125
|
+
|
|
126
|
+
export default app.handle;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The Elysia adapter intercepts responses via `mapResponse`, extracts HTML from Solid's SSR format, ensures DOCTYPE, and injects production assets.
|
|
130
|
+
|
|
131
|
+
### Hono
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Hono } from "hono";
|
|
135
|
+
import { atollic } from "atollic/hono";
|
|
136
|
+
import { html } from "atollic";
|
|
137
|
+
|
|
138
|
+
const app = new Hono();
|
|
139
|
+
app.use(atollic());
|
|
140
|
+
|
|
141
|
+
app.get("/", () => html(<h1>Hello</h1>));
|
|
142
|
+
|
|
143
|
+
export default app.fetch;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The Hono adapter is middleware that processes `text/html` responses. Use the `html()` helper to wrap JSX into a proper `Response`.
|
|
147
|
+
|
|
148
|
+
## Islands
|
|
149
|
+
|
|
150
|
+
Any `.tsx` or `.jsx` file with `"use client"` at the top becomes an island:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
/** @jsxImportSource solid-js */
|
|
154
|
+
"use client";
|
|
155
|
+
|
|
156
|
+
// This component SSRs on the server and hydrates on the client
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### How islands work
|
|
160
|
+
|
|
161
|
+
1. **Discovery** — The Vite plugin scans for files with `"use client"` and identifies PascalCase component exports
|
|
162
|
+
2. **SSR stub** — During SSR, island files are replaced with stubs that render the component to HTML inside a `<div data-island="Name">` wrapper with serialized props
|
|
163
|
+
3. **Client entry** — A virtual module registers all discovered islands with lazy imports
|
|
164
|
+
4. **Hydration** — The client runtime finds `[data-island]` elements and:
|
|
165
|
+
- If SSR content exists: hydrates with matching `renderId` (reuses existing DOM)
|
|
166
|
+
- If empty (dynamically added): falls back to full client-side render
|
|
167
|
+
|
|
168
|
+
### Framework directive
|
|
169
|
+
|
|
170
|
+
When using multiple UI frameworks, specify which one:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
"use client:solid" // Solid.js
|
|
174
|
+
"use client:preact" // Preact (when adapter exists)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Plain `"use client"` uses the first registered framework.
|
|
178
|
+
|
|
179
|
+
### Named exports
|
|
180
|
+
|
|
181
|
+
A single file can export multiple island components. Each PascalCase export becomes its own island boundary:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
"use client";
|
|
185
|
+
|
|
186
|
+
export function SearchBar(props: { placeholder: string }) { /* ... */ }
|
|
187
|
+
export function TagCloud(props: { tags: string[] }) { /* ... */ }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Both are independently hydratable islands.
|
|
191
|
+
|
|
192
|
+
### Client scripts
|
|
193
|
+
|
|
194
|
+
Non-JSX files (`.ts`, `.js`) with `"use client"` are bundled as client-side scripts — no framework, just plain JS:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
"use client";
|
|
198
|
+
|
|
199
|
+
document.addEventListener("click", (e) => {
|
|
200
|
+
// Runs only in the browser
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Import the script from your server entry — it's skipped during SSR and loaded in the browser.
|
|
205
|
+
|
|
206
|
+
### Cross-island state
|
|
207
|
+
|
|
208
|
+
Islands are independent roots, but you can share reactive state between them using module-level signals:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// shared.ts
|
|
212
|
+
import { createSignal } from "solid-js";
|
|
213
|
+
export const [count, setCount] = createSignal(0);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
// Increment.tsx
|
|
218
|
+
"use client";
|
|
219
|
+
import { setCount } from "./shared";
|
|
220
|
+
export default () => <button onClick={() => setCount((c) => c + 1)}>+</button>;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// Display.tsx
|
|
225
|
+
"use client";
|
|
226
|
+
import { count } from "./shared";
|
|
227
|
+
export default () => <p>Count: {count()}</p>;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Both islands share the same signal — no context provider needed.
|
|
231
|
+
|
|
232
|
+
## JSX runtime
|
|
233
|
+
|
|
234
|
+
Atollic includes a server-side JSX runtime (`atollic/jsx-runtime`) that compiles JSX to HTML strings:
|
|
235
|
+
|
|
236
|
+
- All standard HTML elements and attributes
|
|
237
|
+
- Async components (`Promise<string>` return values)
|
|
238
|
+
- Boolean attributes (`disabled`, `checked`, etc.)
|
|
239
|
+
- Automatic XSS escaping
|
|
240
|
+
- Void elements (`<br />`, `<img />`, etc.)
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"compilerOptions": {
|
|
245
|
+
"jsx": "react-jsx",
|
|
246
|
+
"jsxImportSource": "atollic"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## `<Head />`
|
|
252
|
+
|
|
253
|
+
Place `<Head />` in your document `<head>` to mark where atollic injects CSS and script tags:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { Head } from "atollic/head";
|
|
257
|
+
|
|
258
|
+
<head>
|
|
259
|
+
<meta charset="UTF-8" />
|
|
260
|
+
<Head />
|
|
261
|
+
</head>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
In dev, atollic injects the hydration bootstrap, collected CSS, and the client entry. In production, it injects the built asset tags. If `<Head />` is omitted, assets are injected before `</head>` as a fallback.
|
|
265
|
+
|
|
266
|
+
## HMR
|
|
267
|
+
|
|
268
|
+
Dual HMR strategy for instant feedback:
|
|
269
|
+
|
|
270
|
+
- **Server file changes** — atollic sends a custom `atollic:reload` event. The client refetches the page and uses [idiomorph](https://github.com/bigskysoftware/idiomorph) to morph the DOM, preserving mounted island state.
|
|
271
|
+
- **Island file changes** — handled by the framework's own HMR (e.g., `solid-refresh`).
|
|
272
|
+
|
|
273
|
+
### Events
|
|
274
|
+
|
|
275
|
+
Listen to lifecycle events on `document`:
|
|
276
|
+
|
|
277
|
+
| Event | Detail | Description |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `atollic:ready` | — | All initial islands mounted |
|
|
280
|
+
| `atollic:before-morph` | `{ newDoc, mountedIslands }` | Cancelable, before HMR page morph |
|
|
281
|
+
| `atollic:after-morph` | `{ defaultSwap }` | After HMR page morph |
|
|
282
|
+
|
|
283
|
+
## htmx integration
|
|
284
|
+
|
|
285
|
+
Islands hydrate automatically after htmx swaps. The client runtime listens for `htmx:afterSettle` and mounts any new `[data-island]` elements. A `MutationObserver` also catches dynamically added islands from any source.
|
|
286
|
+
|
|
287
|
+
## CSS handling
|
|
288
|
+
|
|
289
|
+
CSS files imported in your server entry (or its transitive imports) are automatically discovered:
|
|
290
|
+
|
|
291
|
+
- **Dev**: collected from Vite's module graph and injected as `<link>` tags via `<Head />`
|
|
292
|
+
- **Prod**: included in the client build, hashed, and referenced in the build manifest
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import "./styles.css"; // discovered automatically
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Production build
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
bunx --bun vite build
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Produces:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
dist/
|
|
308
|
+
client/ # Static assets (JS, CSS) with content-hashed filenames
|
|
309
|
+
assets/
|
|
310
|
+
server/
|
|
311
|
+
app.js # Self-contained server entry
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The generated server entry calls `setProductionAssets()` with the built CSS/JS tags, imports your app, and starts `Bun.serve()` with static file serving from `dist/client/`.
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
PORT=3000 bun dist/server/app.js
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Writing a framework adapter
|
|
321
|
+
|
|
322
|
+
Implement `FrameworkAdapter` to add support for any UI framework:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import type { FrameworkAdapter } from "atollic/adapter";
|
|
326
|
+
|
|
327
|
+
export function myFramework(): FrameworkAdapter {
|
|
328
|
+
return {
|
|
329
|
+
name: "my-framework",
|
|
330
|
+
|
|
331
|
+
// Vite plugins for JSX transform
|
|
332
|
+
plugins: () => [myFrameworkVitePlugin()],
|
|
333
|
+
|
|
334
|
+
// Generate SSR stub for "use client" components
|
|
335
|
+
ssrStub(rawImportPath, fileExports) {
|
|
336
|
+
return `/* SSR stub that renders to HTML string */`;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
// Client-side hydrate/render functions
|
|
340
|
+
clientRuntime: `
|
|
341
|
+
export function hydrateIsland(el, Component, props, id) {
|
|
342
|
+
// Hydrate existing SSR content — return dispose function
|
|
343
|
+
}
|
|
344
|
+
export function renderIsland(el, Component, props) {
|
|
345
|
+
// Render into empty container — return dispose function
|
|
346
|
+
}
|
|
347
|
+
`,
|
|
348
|
+
|
|
349
|
+
// Optional: script tag for hydration bootstrap (e.g., Solid's _$HY)
|
|
350
|
+
hydrationScript: `<script>/* bootstrap */</script>`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## API reference
|
|
356
|
+
|
|
357
|
+
### `atollic/vite`
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
atollic(options: AtollicOptions): Plugin[]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| Option | Type | Description |
|
|
364
|
+
|---|---|---|
|
|
365
|
+
| `entry` | `string` | Path to server entry that default-exports a fetch function |
|
|
366
|
+
| `frameworks` | `FrameworkAdapter[]` | UI framework adapters (e.g., `[solid()]`) |
|
|
367
|
+
|
|
368
|
+
### `atollic`
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
html(input: string): Response // Wrap HTML string in a Response with DOCTYPE
|
|
372
|
+
setProductionAssets(assets: string): void // Set production asset tags
|
|
373
|
+
getProductionAssets(): string | undefined // Get production asset tags
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `atollic/elysia`
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
atollic(): Elysia // Elysia plugin — intercepts HTML responses, injects assets
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### `atollic/hono`
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
atollic(): MiddlewareHandler // Hono middleware — processes HTML responses, injects assets
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### `atollic/head`
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
Head(): string // Returns marker for asset injection
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### `atollic/solid`
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
solid(): FrameworkAdapter // Solid.js framework adapter
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `atollic/html`
|
|
401
|
+
|
|
402
|
+
Types for server-side JSX:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
type Component<P = {}> = (props: P & { children?: Children }) => string
|
|
406
|
+
type Children = string | string[] | Promise<string>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Exports
|
|
410
|
+
|
|
411
|
+
| Export | Description |
|
|
412
|
+
|---|---|
|
|
413
|
+
| `atollic` | Core — `html()`, `setProductionAssets()`, `getProductionAssets()` |
|
|
414
|
+
| `atollic/vite` | Vite plugin |
|
|
415
|
+
| `atollic/client` | Client runtime (auto-imported) |
|
|
416
|
+
| `atollic/adapter` | `FrameworkAdapter` type |
|
|
417
|
+
| `atollic/head` | `<Head />` component |
|
|
418
|
+
| `atollic/solid` | Solid.js adapter |
|
|
419
|
+
| `atollic/elysia` | Elysia server adapter |
|
|
420
|
+
| `atollic/hono` | Hono server adapter |
|
|
421
|
+
| `atollic/jsx-runtime` | Server JSX runtime |
|
|
422
|
+
| `atollic/html` | HTML types (`Component`, `Children`, JSX namespace) |
|
|
423
|
+
|
|
424
|
+
## Requirements
|
|
425
|
+
|
|
426
|
+
- A WinterCG-compatible runtime — [Bun](https://bun.sh), [Node](https://nodejs.org) (via fetch adapter), [Deno](https://deno.com), or Cloudflare Workers
|
|
427
|
+
- [Vite](https://vite.dev) ^8.0.0
|
|
428
|
+
- Examples and the bundled production server template currently target Bun; other runtimes work but require providing your own server entry
|
|
429
|
+
|
|
430
|
+
## License
|
|
431
|
+
|
|
432
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
export interface IslandExport {
|
|
3
|
+
/** "default" for default exports, or the named export identifier */
|
|
4
|
+
exportName: string;
|
|
5
|
+
/** PascalCase name used in the `data-island` attribute */
|
|
6
|
+
islandName: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FrameworkAdapter {
|
|
9
|
+
/** Identifier matching the directive suffix, e.g. `"solid"` for `"use client:solid"` */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Vite plugins needed for this framework's JSX/TSX transform */
|
|
12
|
+
plugins?: () => Plugin[];
|
|
13
|
+
/**
|
|
14
|
+
* Generate SSR stub code for a `"use client"` component file.
|
|
15
|
+
*
|
|
16
|
+
* The returned source must:
|
|
17
|
+
* - Import the real component from `rawImportPath` (the `?raw-island` version)
|
|
18
|
+
* - For each export in `fileExports`, produce a wrapper that renders the
|
|
19
|
+
* component to an HTML string inside a `<div data-island>` wrapper with
|
|
20
|
+
* serialized props
|
|
21
|
+
* - The stub may use framework-specific JSX if the adapter provides Vite
|
|
22
|
+
* plugins that transform it (e.g. Solid's SSR stub uses Solid JSX)
|
|
23
|
+
*/
|
|
24
|
+
ssrStub(rawImportPath: string, fileExports: IslandExport[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Source code string for the client-side hydrate/render functions.
|
|
27
|
+
*
|
|
28
|
+
* Must export:
|
|
29
|
+
* - `hydrateIsland(el, Component, props, id)` → returns dispose function
|
|
30
|
+
* - `renderIsland(el, Component, props)` → returns dispose function
|
|
31
|
+
*/
|
|
32
|
+
clientRuntime: string;
|
|
33
|
+
/**
|
|
34
|
+
* HTML script tag to inject in `<head>` for hydration bootstrap.
|
|
35
|
+
* For example, Solid needs `_$HY`. Frameworks without a bootstrap script
|
|
36
|
+
* can omit this.
|
|
37
|
+
*/
|
|
38
|
+
hydrationScript?: string;
|
|
39
|
+
}
|
package/dist/adapter.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FrameworkAdapter } from "../adapter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Solid.js framework adapter for atollic.
|
|
4
|
+
*
|
|
5
|
+
* Requires peer dependencies: `solid-js`, `vite-plugin-solid`
|
|
6
|
+
*
|
|
7
|
+
* Files are automatically detected via the `@jsxImportSource solid-js`
|
|
8
|
+
* pragma — no include/exclude patterns needed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function solid(): FrameworkAdapter;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createRequire as e } from "node:module";
|
|
2
|
+
//#region src/adapters/solid.ts
|
|
3
|
+
var t = e(import.meta.url);
|
|
4
|
+
function n() {
|
|
5
|
+
return {
|
|
6
|
+
name: "solid",
|
|
7
|
+
plugins() {
|
|
8
|
+
let e = t("vite-plugin-solid"), n = (typeof e == "function" ? e : e.default)({ ssr: !0 });
|
|
9
|
+
return Array.isArray(n) ? n : [n];
|
|
10
|
+
},
|
|
11
|
+
ssrStub(e, t) {
|
|
12
|
+
let n = t.find((e) => e.exportName === "default"), r = t.filter((e) => e.exportName !== "default"), i = [];
|
|
13
|
+
if (n && i.push("__raw_default"), r.length) {
|
|
14
|
+
let e = r.map((e) => `${e.exportName} as __raw_${e.exportName}`).join(", ");
|
|
15
|
+
i.push(`{ ${e} }`);
|
|
16
|
+
}
|
|
17
|
+
let a = "import { renderToString } from \"solid-js/web\";\n";
|
|
18
|
+
a += `import ${i.join(", ")} from "${e}";\n`, a += "let __ix_idx = 0;\n", a += "function __unwrap(v) { return v && typeof v === \"object\" && \"t\" in v ? v.t : String(v); }\n";
|
|
19
|
+
for (let e of t) {
|
|
20
|
+
let t = e.exportName === "default", n = t ? "__raw_default" : `__raw_${e.exportName}`, r = t ? "export default function" : `export function ${e.exportName}`;
|
|
21
|
+
a += `${r}(props) {
|
|
22
|
+
const id = "ix-${e.islandName}-" + __ix_idx++;
|
|
23
|
+
const { children: __children, ...jsonProps } = props;
|
|
24
|
+
const propsJson = JSON.stringify(jsonProps);
|
|
25
|
+
const html = __unwrap(renderToString(() => ${n}(props), { renderId: id }));
|
|
26
|
+
return '<div data-island="${e.islandName}" data-framework="solid" id="' + id + '">'
|
|
27
|
+
+ html + '<script type="application/json">' + propsJson + '<\/script></div>';
|
|
28
|
+
}\n`;
|
|
29
|
+
}
|
|
30
|
+
return a;
|
|
31
|
+
},
|
|
32
|
+
clientRuntime: "\nimport { hydrate as _hydrate, render as _render } from \"solid-js/web\";\nimport { $DEVCOMP } from \"solid-js\";\n\nexport function hydrateIsland(el, Component, props, id) {\n if ($DEVCOMP && !($DEVCOMP in Component)) {\n Component[$DEVCOMP] = true;\n }\n return _hydrate(() => Component(props), el, { renderId: id });\n}\n\nexport function renderIsland(el, Component, props) {\n if ($DEVCOMP && !($DEVCOMP in Component)) {\n Component[$DEVCOMP] = true;\n }\n el.textContent = \"\";\n return _render(() => Component(props), el);\n}\n",
|
|
33
|
+
hydrationScript: "<script>(self._$HY||(self._$HY={events:[],completed:new WeakSet,r:{}}))<\/script>"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { n as solid };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side island runtime for atollic.
|
|
3
|
+
*
|
|
4
|
+
* Discovers [data-island] elements in the DOM, dynamically imports
|
|
5
|
+
* the corresponding component, and mounts it using the registered
|
|
6
|
+
* framework adapter. Handles cleanup on removal and HMR.
|
|
7
|
+
*
|
|
8
|
+
* Events (on `document`):
|
|
9
|
+
* - `atollic:ready` — all initial islands mounted
|
|
10
|
+
* - `atollic:before-morph` — cancelable, detail: { newDoc, mountedIslands }
|
|
11
|
+
* - `atollic:after-morph` — detail: { defaultSwap }, after HMR page replacement
|
|
12
|
+
*/
|
|
13
|
+
type HydrateFn = (el: HTMLElement, Component: any, props: Record<string, unknown>, id: string) => () => void;
|
|
14
|
+
type RenderFn = (el: HTMLElement, Component: any, props: Record<string, unknown>) => () => void;
|
|
15
|
+
export declare function registerFramework(name: string, hydrate: HydrateFn, render: RenderFn): void;
|
|
16
|
+
type IslandLoader = () => Promise<{
|
|
17
|
+
default: (props: any) => any;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function registerIsland(name: string, framework: string, loader: IslandLoader): void;
|
|
20
|
+
export declare function mountIslands(root?: Element | Document): Promise<void>;
|
|
21
|
+
export declare function disposeIslands(root?: Element | Document): void;
|
|
22
|
+
export declare function initRuntime(): void;
|
|
23
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//#region src/client.ts
|
|
2
|
+
var e = null;
|
|
3
|
+
function t() {
|
|
4
|
+
return e ||= import("idiomorph").then((e) => e.Idiomorph), e;
|
|
5
|
+
}
|
|
6
|
+
var n = "data-island", r = `[${n}]`, i = "atollic:reload", a = /* @__PURE__ */ new Map();
|
|
7
|
+
function o(e, t, n) {
|
|
8
|
+
a.set(e, {
|
|
9
|
+
hydrate: t,
|
|
10
|
+
render: n
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
var s = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(), l = !1;
|
|
14
|
+
function u(e, t, n) {
|
|
15
|
+
s.set(e, {
|
|
16
|
+
framework: t,
|
|
17
|
+
loader: n
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function d(e) {
|
|
21
|
+
let t = c.get(e);
|
|
22
|
+
t && (t.dispose(), c.delete(e));
|
|
23
|
+
}
|
|
24
|
+
async function f(e = document) {
|
|
25
|
+
let t = e.querySelectorAll(r), i = [];
|
|
26
|
+
for (let e of t) {
|
|
27
|
+
if (c.has(e)) continue;
|
|
28
|
+
let t = e.getAttribute(n);
|
|
29
|
+
if (!t) continue;
|
|
30
|
+
let r = s.get(t);
|
|
31
|
+
if (!r) {
|
|
32
|
+
console.warn(`[atollic] Unknown island: "${t}"`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let o = a.get(r.framework);
|
|
36
|
+
if (!o) {
|
|
37
|
+
console.warn(`[atollic] Unknown framework "${r.framework}" for island "${t}"`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let l = e.querySelector(":scope > script[type=\"application/json\"]"), u = l ? JSON.parse(l.textContent || "{}") : {};
|
|
41
|
+
l && l.remove();
|
|
42
|
+
let d = e.childNodes.length > 0;
|
|
43
|
+
i.push((async () => {
|
|
44
|
+
try {
|
|
45
|
+
let n = (await r.loader()).default, i;
|
|
46
|
+
i = d && e.id ? o.hydrate(e, n, u, e.id) : o.render(e, n, u), c.set(e, {
|
|
47
|
+
dispose: i,
|
|
48
|
+
name: t,
|
|
49
|
+
props: u
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(`[atollic] Failed to mount island "${t}":`, e);
|
|
53
|
+
}
|
|
54
|
+
})());
|
|
55
|
+
}
|
|
56
|
+
await Promise.all(i);
|
|
57
|
+
}
|
|
58
|
+
function p(e = document) {
|
|
59
|
+
for (let t of e.querySelectorAll(r)) d(t);
|
|
60
|
+
}
|
|
61
|
+
async function m(e) {
|
|
62
|
+
let n = new DOMParser().parseFromString(e, "text/html");
|
|
63
|
+
l = !0;
|
|
64
|
+
let r = new Set(c.keys()), i = document.dispatchEvent(new CustomEvent("atollic:before-morph", {
|
|
65
|
+
cancelable: !0,
|
|
66
|
+
detail: {
|
|
67
|
+
newDoc: n,
|
|
68
|
+
mountedIslands: r
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
if (i) {
|
|
72
|
+
let e = new Set(c.keys());
|
|
73
|
+
(await t()).morph(document.body, n.body, { morphStyle: "innerHTML" });
|
|
74
|
+
for (let t of e) t.isConnected || d(t);
|
|
75
|
+
}
|
|
76
|
+
let a = n.querySelector("title");
|
|
77
|
+
a && (document.title = a.textContent || ""), await f(), l = !1, document.dispatchEvent(new CustomEvent("atollic:after-morph", { detail: { defaultSwap: i } }));
|
|
78
|
+
}
|
|
79
|
+
function h() {
|
|
80
|
+
if (f().then(() => {
|
|
81
|
+
document.dispatchEvent(new CustomEvent("atollic:ready"));
|
|
82
|
+
}), s.size > 0) {
|
|
83
|
+
let e = !1;
|
|
84
|
+
new MutationObserver((t) => {
|
|
85
|
+
if (!l) for (let i of t) {
|
|
86
|
+
for (let e of i.removedNodes) if (e instanceof HTMLElement) {
|
|
87
|
+
e.hasAttribute(n) && d(e);
|
|
88
|
+
for (let t of e.querySelectorAll(r)) d(t);
|
|
89
|
+
}
|
|
90
|
+
if (!e) {
|
|
91
|
+
for (let t of i.addedNodes) if (t instanceof HTMLElement && (t.hasAttribute(n) || t.querySelector(r))) {
|
|
92
|
+
e = !0, queueMicrotask(() => {
|
|
93
|
+
e = !1, f();
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}).observe(document.body, {
|
|
100
|
+
childList: !0,
|
|
101
|
+
subtree: !0
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
import.meta.hot && import.meta.hot.on(i, async () => {
|
|
105
|
+
try {
|
|
106
|
+
await m(await (await fetch(window.location.href, { headers: { "X-Atollic-HMR": "1" } })).text()), console.log("[atollic] Server HMR applied");
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn("[atollic] Server HMR failed, full reload", e), window.location.reload();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { p as disposeIslands, h as initRuntime, f as mountIslands, o as registerFramework, u as registerIsland };
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the marker tag for atollic asset injection.
|
|
3
|
+
* Place this in your document `<head>` — the framework replaces it
|
|
4
|
+
* with the actual CSS and script tags.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <head>
|
|
9
|
+
* <meta charset="UTF-8" />
|
|
10
|
+
* <Head />
|
|
11
|
+
* </head>
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function Head(_props?: {}): JSX.Element;
|
package/dist/head.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "./jsx-runtime.js";
|
|
2
|
+
export { Fragment, jsx, jsxs };
|
|
3
|
+
export declare function jsxDEV(tag: string | ((props: Record<string, unknown>) => unknown), props: Record<string, unknown>, _key?: string, _isStaticChildren?: boolean, _source?: unknown, _self?: unknown): string | Promise<string>;
|