@absolutejs/absolute 0.19.0-beta.169 → 0.19.0-beta.170
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/.absolutejs/prettier.cache.json +3 -3
- package/ROADMAP.md +582 -0
- package/dist/build.js +14 -2
- package/dist/build.js.map +3 -3
- package/dist/cli/index.js +713 -307
- package/dist/index.js +14 -2
- package/dist/index.js.map +3 -3
- package/dist/src/cli/scripts/compile.d.ts +1 -0
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"src/cli/scripts/info.ts": "1064snpc2kiy1",
|
|
26
26
|
"src/cli/scripts/telemetry.ts": "2x0orrt82x06b",
|
|
27
27
|
"src/cli/scripts/start.ts": "e8njhtkgh3tj",
|
|
28
|
-
"src/cli/scripts/dev.ts": "
|
|
28
|
+
"src/cli/scripts/dev.ts": "2cl20x8678dqh",
|
|
29
29
|
"src/cli/scripts/eslint.ts": "25o7waeno205q",
|
|
30
30
|
"src/cli/scripts/prettier.ts": "fxlaao2ialmq",
|
|
31
31
|
"src/svelte/pageHandler.ts": "3lttqsq105iy",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"types/build.ts": "8kjght2fsmik",
|
|
158
158
|
"types/typeGuards.ts": "1xjkc71v0qfvp",
|
|
159
159
|
"types/svelte.ts": "2y2l15nmf0gna",
|
|
160
|
-
"example/server.ts": "
|
|
160
|
+
"example/server.ts": "2l2vf9qrrfpu8",
|
|
161
161
|
"example/absolute.config.ts": "27kjp1g3njuw",
|
|
162
162
|
"example/vueImporter.ts": "3pz876k5csl5o",
|
|
163
163
|
"example/html/scripts/typescript-example.ts": "16hry8jgdyowr",
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"example/react/components/App.tsx": "sio6k26ozh68",
|
|
175
175
|
"eslint.config.mjs": "rb90j2qpxvx8",
|
|
176
176
|
"tsconfig.json": "3pf54bnsv1pxc",
|
|
177
|
-
"package.json": "
|
|
177
|
+
"package.json": "1y8a9q4rxz3bn",
|
|
178
178
|
"tsconfig.build.json": "976e92rva922",
|
|
179
179
|
"native/packages/windows-arm64/package.json": "193jwo8usmsb3",
|
|
180
180
|
"native/packages/darwin-x64/package.json": "1ygrye02o9pav",
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# AbsoluteJS Roadmap — Next.js Feature Parity
|
|
2
|
+
|
|
3
|
+
Features missing from AbsoluteJS that Next.js provides, ordered by priority. Each entry includes what Next.js does, what AbsoluteJS currently has, and what needs to be built.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## P0 — Static Site Generation (SSG)
|
|
8
|
+
|
|
9
|
+
**What Next.js does:**
|
|
10
|
+
Pages can be pre-rendered at build time into static HTML. `generateStaticParams()` tells the framework which dynamic routes to pre-render. The output is plain HTML + JS that can be served from a CDN with zero server runtime. Next.js also supports a full `output: 'export'` mode that produces a completely static site.
|
|
11
|
+
|
|
12
|
+
**What AbsoluteJS has today:**
|
|
13
|
+
Nothing. Every page is rendered at request time via streaming SSR. The `handleHTMLPageRequest` serves pre-written HTML files, but there's no mechanism to run a React/Svelte/Vue component through SSR at build time and save the output.
|
|
14
|
+
|
|
15
|
+
**What needs to be built:**
|
|
16
|
+
- A `static` option per-route or per-page that tells the build to render the component and write the HTML to disk
|
|
17
|
+
- For dynamic routes, a way to declare the set of params to pre-render (equivalent to `generateStaticParams`)
|
|
18
|
+
- A static export mode that produces a directory of HTML/CSS/JS with no server dependency
|
|
19
|
+
- The build pipeline already has streaming SSR for all frameworks — the core work is calling those renderers during `build()` instead of at request time, and writing the output to files
|
|
20
|
+
- Static pages should still hydrate on the client (same as today, just the initial HTML comes from disk instead of runtime SSR)
|
|
21
|
+
|
|
22
|
+
**Files likely involved:**
|
|
23
|
+
- `src/core/build.ts` — add a static rendering pass after bundling
|
|
24
|
+
- Each framework's `pageHandler.ts` — extract the rendering logic so it can be called at build time without an HTTP request
|
|
25
|
+
- `src/build/generateManifest.ts` — static pages need manifest entries pointing to `.html` files
|
|
26
|
+
- New: a config option or per-page export to opt into static rendering
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## P0 — Metadata API / SEO
|
|
31
|
+
|
|
32
|
+
**What Next.js does:**
|
|
33
|
+
Pages export a `metadata` object or `generateMetadata()` async function that defines title, description, Open Graph tags, Twitter cards, canonical URLs, etc. Next.js also supports file-based conventions: `sitemap.ts`, `robots.ts`, `opengraph-image.tsx` that auto-generate SEO assets. The metadata merges and deduplicates across layouts.
|
|
34
|
+
|
|
35
|
+
**What AbsoluteJS has today:**
|
|
36
|
+
`generateHeadElement()` utility that produces a `<head>` string with title, description, favicon, Google Fonts, and CSS paths. Vue and Angular page handlers accept a `headTag` parameter. React pages build their own `<Head>` component manually.
|
|
37
|
+
|
|
38
|
+
**What needs to be built:**
|
|
39
|
+
- A richer metadata type that covers Open Graph (`og:title`, `og:image`, `og:description`, `og:url`), Twitter cards (`twitter:card`, `twitter:title`, `twitter:image`), canonical URL, robots directives, and arbitrary meta tags
|
|
40
|
+
- Update `generateHeadElement()` to accept and render all of these
|
|
41
|
+
- A `sitemap.ts` convention or helper that generates `/sitemap.xml` from the route list
|
|
42
|
+
- A `robots.ts` convention or helper that generates `/robots.txt`
|
|
43
|
+
- JSON-LD / structured data helper for rich search results
|
|
44
|
+
|
|
45
|
+
**Files likely involved:**
|
|
46
|
+
- `src/utils/generateHeadElement.ts` — expand the metadata type and rendering
|
|
47
|
+
- `types/build.ts` or new `types/metadata.ts` — define the metadata type
|
|
48
|
+
- New: `src/utils/generateSitemap.ts`, `src/utils/generateRobots.ts`
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## P1 — Image Optimization
|
|
53
|
+
|
|
54
|
+
**What Next.js does:**
|
|
55
|
+
`<Image>` component that automatically converts images to WebP/AVIF, generates responsive `srcset` attributes, lazy loads with blur placeholders, and serves optimized images through an on-demand image optimization API route. Prevents layout shift with required width/height.
|
|
56
|
+
|
|
57
|
+
**What AbsoluteJS has today:**
|
|
58
|
+
Nothing. Images are served as-is from the public/assets directory.
|
|
59
|
+
|
|
60
|
+
**What needs to be built:**
|
|
61
|
+
- An `<Image>` component (React version at minimum, ideally per-framework) that renders responsive `<img>` tags with `srcset`, `sizes`, `loading="lazy"`, and `width`/`height` for CLS prevention
|
|
62
|
+
- An image optimization endpoint or build-time processor that converts to WebP/AVIF and generates multiple sizes
|
|
63
|
+
- Sharp or libvips integration for the actual image processing (Sharp works with Bun)
|
|
64
|
+
- Caching layer for optimized images so they're only processed once
|
|
65
|
+
- Optional blur placeholder generation (tiny base64 inline preview)
|
|
66
|
+
|
|
67
|
+
**Files likely involved:**
|
|
68
|
+
- New: `src/react/components/Image.tsx`, and equivalents for other frameworks
|
|
69
|
+
- New: `src/plugins/imageOptimizer.ts` — Elysia plugin that handles `/image?url=...&w=...&q=...` requests
|
|
70
|
+
- New: `src/build/optimizeImages.ts` — optional build-time optimization pass
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## P1 — Loading / Error / Not-Found States
|
|
75
|
+
|
|
76
|
+
**What Next.js does:**
|
|
77
|
+
Per-route-segment `loading.tsx` (shows during async data fetch), `error.tsx` (catches runtime errors with React error boundary), and `not-found.tsx` (404 page). These are automatic — drop the file in and it works.
|
|
78
|
+
|
|
79
|
+
**What AbsoluteJS has today:**
|
|
80
|
+
- Dev error overlay (`src/dev/client/errorOverlay.ts`) for compilation/runtime errors in development
|
|
81
|
+
- SSR error page (`src/utils/ssrErrorPage.ts`) that returns a styled error page when server rendering fails
|
|
82
|
+
- No production error boundaries, no loading states, no 404 handling
|
|
83
|
+
|
|
84
|
+
**What needs to be built:**
|
|
85
|
+
- A documented pattern for error boundaries per framework (React has `ErrorBoundary`, Vue has `onErrorCaptured`, Svelte has `<svelte:boundary>`, Angular has `ErrorHandler`)
|
|
86
|
+
- A helper or wrapper that each page handler can use to catch SSR errors and render a user-defined error page instead of the generic one
|
|
87
|
+
- A loading state pattern — for React this means Suspense boundaries with fallback UI; for streaming SSR, sending the shell immediately and streaming content as it resolves
|
|
88
|
+
- A 404 handler — an Elysia catch-all route that renders a user-defined not-found page
|
|
89
|
+
- Framework-specific examples showing how to wire each of these up
|
|
90
|
+
|
|
91
|
+
**Files likely involved:**
|
|
92
|
+
- `src/utils/ssrErrorPage.ts` — make it accept a user-defined error component
|
|
93
|
+
- Each framework's `pageHandler.ts` — add error/loading handling to the streaming pipeline
|
|
94
|
+
- New: `src/utils/notFoundPage.ts` or a convention for 404 pages
|
|
95
|
+
- Example directory — add error/loading/not-found examples
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## P1 — Client-Side Navigation / SPA Mode with `<Link>`
|
|
100
|
+
|
|
101
|
+
**What Next.js does:**
|
|
102
|
+
`<Link>` component that intercepts clicks and does client-side navigation — fetches only the new page's data/RSC payload, swaps the content, and preserves layout state (scroll position, open menus, form inputs). Prefetches linked pages on hover or when they enter the viewport. This is what makes Next.js apps feel like SPAs even though they're server-rendered.
|
|
103
|
+
|
|
104
|
+
**What AbsoluteJS has today:**
|
|
105
|
+
Plain `<a>` tags. Every navigation is a full page load — the browser tears down the entire DOM, re-requests the HTML, and re-hydrates from scratch. Shared UI like navbars and sidebars re-mount every time.
|
|
106
|
+
|
|
107
|
+
**Why this matters:**
|
|
108
|
+
Without client-side navigation, layouts are just a component pattern (wrap your page in a shared component — users can do this today). WITH client-side navigation, layouts become persistent — the navbar stays mounted, sidebar scroll position is preserved, and only the page content swaps. This is the single feature that turns a server-rendered app into an SPA experience.
|
|
109
|
+
|
|
110
|
+
**Architecture: One shared navigation module, thin framework wrappers**
|
|
111
|
+
|
|
112
|
+
React Router and similar client-side routers can't help here — they swap client-side components, but AbsoluteJS pages are server-rendered. The navigation module needs to fetch server-rendered HTML and swap it into the DOM. This is fundamentally framework-agnostic — the DOM swap logic is identical whether the page is React, Svelte, Vue, or Angular. So the right design is:
|
|
113
|
+
|
|
114
|
+
1. **One shared navigation module** (`navigate.ts`) that handles all the core logic: intercept clicks, fetch partial HTML, swap DOM content, manage history, prefetch. This runs as plain JS on every page regardless of framework.
|
|
115
|
+
2. **Thin per-framework `<Link>` wrappers** that just render an `<a>` tag with the right attributes. The shared module picks up all `<a>` tags with a `data-link` attribute (or all internal links by default) — the framework components are just ergonomic sugar so users don't have to remember the attribute.
|
|
116
|
+
|
|
117
|
+
**What needs to be built:**
|
|
118
|
+
|
|
119
|
+
*Shared navigation module (`navigate.ts`):*
|
|
120
|
+
- Intercept clicks on internal `<a>` tags (skip external links, `target="_blank"`, modifier keys)
|
|
121
|
+
- `fetch()` the URL with an `X-AbsoluteJS-Nav: partial` header
|
|
122
|
+
- Receive partial HTML (just the `<main>` content, not the full document)
|
|
123
|
+
- Swap the `<main>` innerHTML with the new content
|
|
124
|
+
- Load any new CSS before swapping to avoid FOUC
|
|
125
|
+
- `history.pushState()` on navigate, handle `popstate` for back/forward
|
|
126
|
+
- Hydrate the new content after swap (call the framework's hydration entry point)
|
|
127
|
+
- View Transitions API for smooth animated swaps (already used in Angular HMR)
|
|
128
|
+
|
|
129
|
+
*Prefetching:*
|
|
130
|
+
- On `mouseenter` (default) — prefetch the page so it's ready on click
|
|
131
|
+
- On viewport intersection (opt-in via `data-prefetch="viewport"`) — for visible links
|
|
132
|
+
- `data-prefetch="none"` to disable
|
|
133
|
+
- Cache prefetched responses in a Map to avoid duplicate fetches
|
|
134
|
+
- Smart limits — don't prefetch more than N pages at once
|
|
135
|
+
|
|
136
|
+
*Server-side partial rendering:*
|
|
137
|
+
- An Elysia `onBeforeHandle` hook that checks for the `X-AbsoluteJS-Nav: partial` header
|
|
138
|
+
- When present, each framework's page handler skips the outer `<html>/<head>/<body>` shell and returns only the inner page content (the part inside `<main>`)
|
|
139
|
+
- Also returns metadata in a response header or JSON wrapper: page title, CSS paths, framework type, hydration entry point
|
|
140
|
+
|
|
141
|
+
*Per-framework `<Link>` wrappers:*
|
|
142
|
+
- React: `<Link href="/about">About</Link>` → renders `<a href="/about">About</a>` with the right attributes. Thin component, no routing logic.
|
|
143
|
+
- Svelte: `<Link href="/about">About</Link>` → same thing
|
|
144
|
+
- Vue: `<Link href="/about">About</Link>` → same thing
|
|
145
|
+
- Angular: `<a absLink href="/about">About</a>` directive → same thing
|
|
146
|
+
- All wrappers accept `prefetch` prop (`"hover"` | `"viewport"` | `"none"`)
|
|
147
|
+
|
|
148
|
+
*Cross-framework navigation:*
|
|
149
|
+
- Same-framework navigations: swap `<main>`, hydrate with the same runtime. Fast.
|
|
150
|
+
- Cross-framework navigations (React page → Svelte page): need to tear down the old framework's hydration and bootstrap the new one. The partial response includes the framework type so the navigation module knows which hydration to call. May need to swap `<body>` instead of just `<main>` if the framework runtimes conflict.
|
|
151
|
+
- Fallback: if cross-framework swap is too complex initially, just do a full page load for cross-framework links. Still use View Transitions for visual continuity.
|
|
152
|
+
|
|
153
|
+
**Design considerations:**
|
|
154
|
+
- Progressive enhancement — the `<Link>` renders a real `<a>` tag with a real `href`. If JS fails, it's a normal link. If the partial fetch errors, fall back to full navigation. No JS-only routes.
|
|
155
|
+
- The shared module should be small (<5KB) and loaded on every page as part of the AbsoluteJS client runtime, alongside the HMR client in dev.
|
|
156
|
+
- CSS handling: the partial response should include which stylesheets the new page needs. The navigation module loads them before swapping to prevent FOUC. Stylesheets shared between pages stay loaded.
|
|
157
|
+
- Scroll behavior: scroll to top on navigation by default, restore scroll position on back/forward via `scrollRestoration: 'manual'`.
|
|
158
|
+
|
|
159
|
+
**Files likely involved:**
|
|
160
|
+
- New: `src/client/navigate.ts` — shared framework-agnostic navigation module (click interception, fetch, DOM swap, history, prefetch, View Transitions)
|
|
161
|
+
- New: `src/react/components/Link.tsx` — thin React wrapper
|
|
162
|
+
- New: `src/svelte/Link.svelte` — thin Svelte wrapper
|
|
163
|
+
- New: `src/vue/Link.vue` — thin Vue wrapper
|
|
164
|
+
- New: `src/angular/link.directive.ts` — thin Angular directive
|
|
165
|
+
- New: `src/plugins/navigation.ts` — Elysia plugin that detects `X-AbsoluteJS-Nav: partial` header and adjusts response
|
|
166
|
+
- Each framework's `pageHandler.ts` — add partial rendering mode that returns only inner content + metadata
|
|
167
|
+
- `src/dev/client/hmrClient.ts` — ensure HMR reconnects correctly after client-side navigation
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## P1 — Islands Architecture (Multi-Framework Pages)
|
|
172
|
+
|
|
173
|
+
**What Astro does:**
|
|
174
|
+
Pages are rendered as static HTML with interactive "islands" — individual components that hydrate independently. Each island can be a different framework (React, Svelte, Vue, etc.) on the same page. Hydration is controlled with directives: `client:load` (immediate), `client:idle` (requestIdleCallback), `client:visible` (IntersectionObserver). Non-interactive content ships zero JS.
|
|
175
|
+
|
|
176
|
+
**What AbsoluteJS has today:**
|
|
177
|
+
Each page is owned by one framework. A React page is fully React, a Svelte page is fully Svelte. All the framework SSR renderers, vendor bundles, and hydration entry points already exist — but they can't be mixed on a single page.
|
|
178
|
+
|
|
179
|
+
**Why AbsoluteJS is uniquely positioned:**
|
|
180
|
+
No other meta-framework has SSR renderers, build pipelines, and vendor bundles for React, Svelte, Vue, and Angular already running in the same process. Astro supports islands but the host page uses Astro's own template language — you can't use a React component as the page shell with Svelte islands inside it. AbsoluteJS can because the framework renderers are already first-class.
|
|
181
|
+
|
|
182
|
+
**What needs to be built:**
|
|
183
|
+
|
|
184
|
+
*Island rendering (SSR side):*
|
|
185
|
+
- An `island()` function that takes a component, its framework type, and props, and returns the server-rendered HTML wrapped in a marker element:
|
|
186
|
+
```html
|
|
187
|
+
<div data-island="react" data-component="Chart" data-props="..." data-hydrate="load">
|
|
188
|
+
<!-- SSR'd React HTML -->
|
|
189
|
+
</div>
|
|
190
|
+
```
|
|
191
|
+
- The page handler renders the host page first, then renders each island with its framework's SSR renderer and injects the HTML into the marker elements
|
|
192
|
+
- For streaming: the page shell streams immediately, islands can resolve asynchronously and stream in as they complete (similar to React Suspense boundaries)
|
|
193
|
+
|
|
194
|
+
*Island hydration (client side):*
|
|
195
|
+
- A small island runtime (~2-3KB) that runs on page load and finds all `data-island` elements
|
|
196
|
+
- For each island, loads the framework vendor bundle and component module, then hydrates that specific DOM subtree
|
|
197
|
+
- Hydration directives control timing:
|
|
198
|
+
- `client:load` — hydrate immediately on page load
|
|
199
|
+
- `client:idle` — hydrate during `requestIdleCallback` (after main thread is free)
|
|
200
|
+
- `client:visible` — hydrate when the island scrolls into the viewport (`IntersectionObserver`)
|
|
201
|
+
- `client:none` — never hydrate (static HTML only, zero JS for that island)
|
|
202
|
+
- Each island hydrates independently — React hydrates its div, Svelte mounts into its div, Vue creates its app on its div. They don't interfere with each other.
|
|
203
|
+
|
|
204
|
+
*Shared state between islands (zustand/vanilla):*
|
|
205
|
+
- Use `zustand/vanilla` as an internal dependency (~1KB, no framework coupling) for cross-island state
|
|
206
|
+
- AbsoluteJS exposes a simple API per framework — users never see zustand:
|
|
207
|
+
- React: `useIslandState(key)` hook — wraps zustand subscribe with `useSyncExternalStore`
|
|
208
|
+
- Svelte: `getIslandState(key)` — returns a Svelte-compatible readable store backed by zustand subscribe
|
|
209
|
+
- Vue: `useIslandState(key)` composable — returns a `ref` that syncs via zustand subscribe
|
|
210
|
+
- Angular: `IslandState` injectable service — wraps zustand subscribe in an Observable
|
|
211
|
+
- State is initialized on the server and serialized to `window.__ISLAND_STATE__` alongside `window.__INITIAL_PROPS__`
|
|
212
|
+
- Islands that share state subscribe to the same zustand store keys and stay in sync automatically
|
|
213
|
+
|
|
214
|
+
*Build pipeline changes:*
|
|
215
|
+
- Island components need to be compiled and bundled individually (not as part of a full-page bundle)
|
|
216
|
+
- Each island becomes its own entry point so the browser only loads the framework code for islands actually on the page
|
|
217
|
+
- The manifest needs to track island components: `{ "ReactChart": "/islands/react-chart.hash.js", ... }`
|
|
218
|
+
- Framework vendor bundles are loaded on-demand — if a page has only React and Svelte islands, Vue and Angular vendors are never loaded
|
|
219
|
+
|
|
220
|
+
*Type-safe island registry — instant IDE autocomplete, no codegen:*
|
|
221
|
+
|
|
222
|
+
Type safety comes from a user-maintained registry file. The user defines which island components exist per framework and what their props are. AbsoluteJS provides `defineIslandRegistry()` for validation and an `IslandRegistry` type that the `<Island>` components read from. This gives full IDE autocomplete immediately — no build step needed.
|
|
223
|
+
|
|
224
|
+
**The registry file (user creates once, updates as islands are added):**
|
|
225
|
+
```ts
|
|
226
|
+
// islands/registry.ts
|
|
227
|
+
import { defineIslandRegistry } from 'absolutejs'
|
|
228
|
+
import SvelteForm from './SvelteForm.svelte'
|
|
229
|
+
import ContactForm from './ContactForm.svelte'
|
|
230
|
+
import Notifications from './Notifications.vue'
|
|
231
|
+
import { Chart } from './Chart'
|
|
232
|
+
|
|
233
|
+
export const islandRegistry = defineIslandRegistry({
|
|
234
|
+
svelte: {
|
|
235
|
+
SvelteForm,
|
|
236
|
+
ContactForm,
|
|
237
|
+
},
|
|
238
|
+
vue: {
|
|
239
|
+
Notifications,
|
|
240
|
+
},
|
|
241
|
+
react: {
|
|
242
|
+
Chart,
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
No type casts. You pass the actual components and `defineIslandRegistry()` infers the props types from them — the same way `handleReactPageRequest` infers `Props` from `ReactComponent<Props>` and `handleSveltePageRequest` infers `P` from `SvelteComponent<P>`. The generic signature extracts props from each framework's component type (`ComponentType<P>` for React, `Component<P>` for Svelte, etc.) so the registry knows exactly what props each island accepts.
|
|
248
|
+
|
|
249
|
+
`defineIslandRegistry()` also serves as the runtime registry — the island renderer uses it to look up the actual component at SSR time. One object, two purposes: type inference for the IDE and component resolution for the server.
|
|
250
|
+
|
|
251
|
+
**How the types flow through `<Island>`:**
|
|
252
|
+
|
|
253
|
+
Same pattern as Eden Treaty — you pass the registry to a factory function that returns a typed `<Island>` component. No module augmentation, no global declarations. The type safety lives at the call site.
|
|
254
|
+
|
|
255
|
+
Each framework exports a `createIsland` function that takes the registry and returns a typed `<Island>` component:
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
// islands/index.ts — one line per framework you use as a host
|
|
259
|
+
import { createIsland } from 'absolutejs/react'
|
|
260
|
+
import { islandRegistry } from './registry'
|
|
261
|
+
|
|
262
|
+
export const Island = createIsland(islandRegistry)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Now `Island` knows the full registry type. `framework` narrows `component`, `component` narrows `props` — all inferred from what you passed in.
|
|
266
|
+
|
|
267
|
+
**React host page:**
|
|
268
|
+
```tsx
|
|
269
|
+
import { Island } from '../islands'
|
|
270
|
+
|
|
271
|
+
export const Dashboard = ({ data }: DashboardProps) => (
|
|
272
|
+
<div>
|
|
273
|
+
<ReactChart data={data} />
|
|
274
|
+
<Island
|
|
275
|
+
framework="svelte" // autocomplete: "svelte" | "vue" | "react"
|
|
276
|
+
component="SvelteForm" // autocomplete: "SvelteForm" | "ContactForm"
|
|
277
|
+
props={{ fields: data.form }} // typed as SvelteFormProps — error if wrong
|
|
278
|
+
hydrate="load"
|
|
279
|
+
/>
|
|
280
|
+
<Island
|
|
281
|
+
framework="vue"
|
|
282
|
+
component="Notifications" // autocomplete: "Notifications"
|
|
283
|
+
props={{ count: 3 }} // typed as NotificationsProps
|
|
284
|
+
hydrate="visible"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Svelte host page:**
|
|
291
|
+
```svelte
|
|
292
|
+
<script lang="ts">
|
|
293
|
+
// Svelte equivalent: createIsland from 'absolutejs/svelte'
|
|
294
|
+
import { Island } from '../islands/svelte'
|
|
295
|
+
</script>
|
|
296
|
+
|
|
297
|
+
<h1>Analytics</h1>
|
|
298
|
+
<Island
|
|
299
|
+
framework="react"
|
|
300
|
+
component="Chart"
|
|
301
|
+
props={{ data: [1, 2, 3] }}
|
|
302
|
+
hydrate="load"
|
|
303
|
+
/>
|
|
304
|
+
<Island
|
|
305
|
+
framework="vue"
|
|
306
|
+
component="Notifications"
|
|
307
|
+
props={{ count: 5 }}
|
|
308
|
+
hydrate="idle"
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Vue host page:**
|
|
313
|
+
```vue
|
|
314
|
+
<script setup lang="ts">
|
|
315
|
+
// Vue equivalent: createIsland from 'absolutejs/vue'
|
|
316
|
+
import { Island } from '../islands/vue'
|
|
317
|
+
const chartData = [1, 2, 3]
|
|
318
|
+
</script>
|
|
319
|
+
|
|
320
|
+
<template>
|
|
321
|
+
<h1>Dashboard</h1>
|
|
322
|
+
<Island
|
|
323
|
+
framework="react"
|
|
324
|
+
component="Chart"
|
|
325
|
+
:props="{ data: chartData }"
|
|
326
|
+
hydrate="visible"
|
|
327
|
+
/>
|
|
328
|
+
<Island
|
|
329
|
+
framework="svelte"
|
|
330
|
+
component="SvelteForm"
|
|
331
|
+
:props="{ fields: ['name', 'email'] }"
|
|
332
|
+
hydrate="load"
|
|
333
|
+
/>
|
|
334
|
+
</template>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Angular host page:**
|
|
338
|
+
```typescript
|
|
339
|
+
import { Component } from '@angular/core'
|
|
340
|
+
// Angular: createIslandDirective from 'absolutejs/angular'
|
|
341
|
+
import { IslandComponent } from '../islands/angular'
|
|
342
|
+
|
|
343
|
+
@Component({
|
|
344
|
+
selector: 'app-dashboard',
|
|
345
|
+
imports: [IslandComponent],
|
|
346
|
+
template: `
|
|
347
|
+
<h1>Dashboard</h1>
|
|
348
|
+
<abs-island
|
|
349
|
+
framework="react"
|
|
350
|
+
component="Chart"
|
|
351
|
+
[props]="{ data: chartData }"
|
|
352
|
+
hydrate="load"
|
|
353
|
+
/>
|
|
354
|
+
<abs-island
|
|
355
|
+
framework="svelte"
|
|
356
|
+
component="ContactForm"
|
|
357
|
+
[props]="{ fields: formFields }"
|
|
358
|
+
hydrate="idle"
|
|
359
|
+
/>
|
|
360
|
+
`
|
|
361
|
+
})
|
|
362
|
+
export class DashboardComponent {
|
|
363
|
+
chartData = [1, 2, 3]
|
|
364
|
+
formFields = ['name', 'email']
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**HTML/HTMX host page (attribute-based, no type safety):**
|
|
369
|
+
```html
|
|
370
|
+
<h1>Landing Page</h1>
|
|
371
|
+
<div data-island="react" data-component="Chart" data-hydrate="visible"
|
|
372
|
+
data-props='{"data":[1,2,3]}'>
|
|
373
|
+
<!-- SSR'd at request time -->
|
|
374
|
+
</div>
|
|
375
|
+
<div data-island="svelte" data-component="ContactForm" data-hydrate="load">
|
|
376
|
+
<!-- SSR'd at request time -->
|
|
377
|
+
</div>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
*What AbsoluteJS provides (in `types/island.ts`):*
|
|
381
|
+
```ts
|
|
382
|
+
import type { ComponentType as ReactComponent } from 'react'
|
|
383
|
+
import type { Component as SvelteComponent } from 'svelte'
|
|
384
|
+
|
|
385
|
+
// Extract props from any framework's component type
|
|
386
|
+
type ExtractReactProps<C> = C extends ReactComponent<infer P> ? P : never
|
|
387
|
+
type ExtractSvelteProps<C> = C extends SvelteComponent<infer P> ? P : never
|
|
388
|
+
// Vue and Angular equivalents follow the same pattern
|
|
389
|
+
|
|
390
|
+
// Maps a record of components to a record of their extracted props
|
|
391
|
+
type ExtractFrameworkProps<F extends string, Components extends Record<string, unknown>> = {
|
|
392
|
+
[K in keyof Components]: F extends 'react' ? ExtractReactProps<Components[K]>
|
|
393
|
+
: F extends 'svelte' ? ExtractSvelteProps<Components[K]>
|
|
394
|
+
: F extends 'vue' ? ExtractVueProps<Components[K]>
|
|
395
|
+
: F extends 'angular' ? ExtractAngularProps<Components[K]>
|
|
396
|
+
: never
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Inferred registry type — maps framework -> component name -> props
|
|
400
|
+
type InferredRegistry<T> = {
|
|
401
|
+
[F in keyof T]: ExtractFrameworkProps<F & string, T[F] & Record<string, unknown>>
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Defaults to 'load' — most islands are interactive, so the common case needs no prop
|
|
405
|
+
type IslandHydrate = 'load' | 'idle' | 'visible' | 'none'
|
|
406
|
+
|
|
407
|
+
// defineIslandRegistry — accepts actual components, infers all props types
|
|
408
|
+
// Returns the registry for both runtime SSR lookup and type-level inference
|
|
409
|
+
const defineIslandRegistry = <
|
|
410
|
+
T extends Partial<Record<'react' | 'svelte' | 'vue' | 'angular', Record<string, unknown>>>
|
|
411
|
+
>(registry: T) => registry as unknown as InferredRegistry<T>
|
|
412
|
+
|
|
413
|
+
// createIsland — factory that takes a registry and returns a typed component
|
|
414
|
+
// Same pattern as Eden's treaty() — pass the type source, get type safety out
|
|
415
|
+
// Each framework exports its own version:
|
|
416
|
+
// absolutejs/react → createIsland(registry) returns a React <Island> component
|
|
417
|
+
// absolutejs/svelte → createIsland(registry) returns a Svelte <Island> component
|
|
418
|
+
// absolutejs/vue → createIsland(registry) returns a Vue <Island> component
|
|
419
|
+
// absolutejs/angular → createIslandDirective(registry) returns an Angular directive
|
|
420
|
+
//
|
|
421
|
+
// The returned component's props are constrained by the registry:
|
|
422
|
+
// <Island framework={F} component={C} props={InferredRegistry[F][C]} hydrate={...} />
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The key insight: `defineIslandRegistry` accepts actual imported components at runtime (for SSR lookup) and infers their props types through generics (for IDE autocomplete). `createIsland` takes that registry and returns a framework-specific `<Island>` component whose `framework` → `component` → `props` chain is fully typed. Same pattern as Eden Treaty — pass the type source in, get type safety out. No module augmentation, no codegen, no type casts.
|
|
426
|
+
|
|
427
|
+
*How `<Island>` works under the hood:*
|
|
428
|
+
- **On the server:** The `<Island>` component calls the target framework's SSR renderer with the given props, gets back HTML, and renders it inside a `<div>` marker with `data-island`, `data-component`, `data-hydrate`, and serialized `data-props` attributes. In React this uses `dangerouslySetInnerHTML`, in Svelte it uses `{@html}`, in Vue it uses `v-html`, in Angular it uses `[innerHTML]`.
|
|
429
|
+
- **On the client:** The island runtime script finds all `data-island` elements, loads the right framework vendor + component module from the manifest, and hydrates each one independently based on its `hydrate` directive.
|
|
430
|
+
- **The `<Island>` component itself ships zero JS to the client** — it's SSR-only. The client-side hydration is handled entirely by the island runtime.
|
|
431
|
+
|
|
432
|
+
**Design considerations:**
|
|
433
|
+
- Islands must be self-contained — they hydrate independently with their own props. Cross-island communication goes through the shared zustand store, not prop drilling.
|
|
434
|
+
- The island runtime should detect which frameworks are used on the page and only load those vendor bundles. If a page has 5 React islands and 1 Svelte island, React vendor loads once (shared), Svelte vendor loads once.
|
|
435
|
+
- CSS for each island should be scoped or co-located. CSS Modules work naturally here — each island's styles are hashed and don't collide.
|
|
436
|
+
- Islands inside islands (nested cross-framework) should be explicitly unsupported in v1 to keep complexity down.
|
|
437
|
+
- HMR needs to work per-island — editing a Svelte island component should hot-reload just that island, not the entire page.
|
|
438
|
+
|
|
439
|
+
**Files likely involved:**
|
|
440
|
+
- New: `src/react/components/Island.tsx` — React `<Island>` component (SSR-only, renders other frameworks inline)
|
|
441
|
+
- New: `src/svelte/Island.svelte` — Svelte `<Island>` component
|
|
442
|
+
- New: `src/vue/Island.vue` — Vue `<Island>` component
|
|
443
|
+
- New: `src/angular/island.component.ts` — Angular `<abs-island>` component
|
|
444
|
+
- New: `src/core/islandRenderer.ts` — shared server-side logic that calls the right framework's SSR renderer for an island
|
|
445
|
+
- New: `types/island.ts` — `IslandRegistryMap`, `IslandFramework`, `IslandComponent`, `IslandProps`, `IslandHydrate` types + `defineIslandRegistry()`
|
|
446
|
+
- New: `src/build/scanIslands.ts` — scans the registry at build time to discover island entry points for bundling
|
|
447
|
+
- New: `src/client/islandRuntime.ts` — client-side island discovery, framework loading, and hydration orchestration
|
|
448
|
+
- New: `src/client/islandState.ts` — zustand/vanilla-backed shared state with per-framework wrappers
|
|
449
|
+
- New: `src/react/hooks/useIslandState.ts` — React hook for shared island state
|
|
450
|
+
- New: `src/svelte/islandState.ts` — Svelte store wrapper for shared island state
|
|
451
|
+
- New: `src/vue/useIslandState.ts` — Vue composable for shared island state
|
|
452
|
+
- New: `src/angular/island-state.service.ts` — Angular service for shared island state
|
|
453
|
+
- `src/core/build.ts` — add island entry point discovery and per-island bundling
|
|
454
|
+
- `src/build/generateManifest.ts` — track island components in the manifest
|
|
455
|
+
- Each framework's `pageHandler.ts` — support rendering island components inline during page SSR
|
|
456
|
+
- `src/plugins/hmr.ts` — per-island HMR updates
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## P2 — Incremental Static Regeneration (ISR)
|
|
461
|
+
|
|
462
|
+
**What Next.js does:**
|
|
463
|
+
Static pages can declare a `revalidate` interval (e.g., 60 seconds). After the interval, the next request triggers a background re-render. The stale page is served immediately while the new one generates. `revalidatePath()` and `revalidateTag()` allow on-demand revalidation from API routes or server actions.
|
|
464
|
+
|
|
465
|
+
**What AbsoluteJS has today:**
|
|
466
|
+
Nothing. No static generation means no revalidation.
|
|
467
|
+
|
|
468
|
+
**What needs to be built (requires SSG first):**
|
|
469
|
+
- A `revalidate` option per static page that sets a TTL
|
|
470
|
+
- Background re-rendering: when a request comes in for a stale page, serve the cached version and trigger a rebuild in the background
|
|
471
|
+
- On-demand revalidation: an API to invalidate specific pages programmatically
|
|
472
|
+
- A cache store for rendered pages (filesystem or in-memory)
|
|
473
|
+
|
|
474
|
+
**Files likely involved:**
|
|
475
|
+
- Builds on top of SSG implementation
|
|
476
|
+
- New: `src/core/staticCache.ts` — manages cached HTML with TTLs
|
|
477
|
+
- New: `src/plugins/revalidation.ts` — Elysia plugin for on-demand revalidation endpoints
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## P2 — Sass/SCSS/Less Preprocessing
|
|
482
|
+
|
|
483
|
+
**What Next.js does:**
|
|
484
|
+
Built-in Sass support. Import `.scss` or `.sass` files directly. Also supports `.module.scss` for scoped Sass modules.
|
|
485
|
+
|
|
486
|
+
**What AbsoluteJS has today:**
|
|
487
|
+
Only `.css` files. Tailwind handles utility classes. No preprocessor support.
|
|
488
|
+
|
|
489
|
+
**What needs to be built:**
|
|
490
|
+
- A Bun build plugin that compiles `.scss`/`.sass`/`.less` files to CSS before bundling
|
|
491
|
+
- Bun has a plugin API for custom loaders — register `.scss` extension with a loader that calls the sass compiler
|
|
492
|
+
- Support `.module.scss` for scoped Sass modules (Bun's CSS Module handling + Sass compilation)
|
|
493
|
+
- Add `sass` as an optional peer dependency
|
|
494
|
+
|
|
495
|
+
**Files likely involved:**
|
|
496
|
+
- New: `src/build/sassPlugin.ts` — Bun plugin that compiles Sass
|
|
497
|
+
- `src/core/build.ts` — register the plugin in the Bun.build() calls
|
|
498
|
+
- `src/build/scanCssEntryPoints.ts` — extend glob to include `**/*.scss`, `**/*.sass`, `**/*.less`
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## P2 — Middleware
|
|
503
|
+
|
|
504
|
+
**What Next.js does:**
|
|
505
|
+
A single `middleware.ts` at the project root that runs before every request. Can rewrite URLs, redirect, set headers, check auth, do A/B testing, geolocation-based routing. Runs on the edge (lightweight V8 isolate).
|
|
506
|
+
|
|
507
|
+
**What AbsoluteJS has today:**
|
|
508
|
+
Elysia's full middleware system — `onBeforeHandle`, `onAfterHandle`, `.use()` plugin chain, `derive`, `guard`. This is actually more powerful than Next.js middleware but less conventionalized.
|
|
509
|
+
|
|
510
|
+
**What needs to be built:**
|
|
511
|
+
- Honestly, this might just need documentation. Elysia's `onBeforeHandle` IS middleware. A `guard()` block with auth checks IS the auth middleware pattern.
|
|
512
|
+
- Consider a thin `middleware()` helper that wraps the common pattern: check auth, redirect if not authenticated, rewrite URLs, set CORS headers
|
|
513
|
+
- Examples showing: auth guard, redirect, URL rewrite, rate limiting, CORS
|
|
514
|
+
- The key gap isn't functionality — it's discoverability. New users don't know that Elysia's `onBeforeHandle` is the middleware they're looking for.
|
|
515
|
+
|
|
516
|
+
**Files likely involved:**
|
|
517
|
+
- Mostly documentation/examples
|
|
518
|
+
- Optional: `src/utils/middleware.ts` — convenience wrappers for common patterns
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## P3 — Internationalization (i18n)
|
|
523
|
+
|
|
524
|
+
**What Next.js does:**
|
|
525
|
+
Built-in locale routing (`/en/about`, `/fr/about`), locale detection from Accept-Language header, and domain-based routing. Integrates with i18n libraries like next-intl.
|
|
526
|
+
|
|
527
|
+
**What AbsoluteJS has today:**
|
|
528
|
+
Nothing.
|
|
529
|
+
|
|
530
|
+
**What needs to be built:**
|
|
531
|
+
- Locale detection middleware (Accept-Language header parsing, cookie-based locale persistence)
|
|
532
|
+
- URL prefix routing pattern (`/:locale/page`)
|
|
533
|
+
- A helper to load translation files and inject them as props
|
|
534
|
+
- Per-framework translation access patterns (React context, Vue provide/inject, Svelte stores)
|
|
535
|
+
- This is mostly a pattern/plugin, not core framework work
|
|
536
|
+
|
|
537
|
+
**Files likely involved:**
|
|
538
|
+
- New: `src/plugins/i18n.ts` — Elysia plugin for locale detection and routing
|
|
539
|
+
- Documentation showing integration with popular i18n libraries
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## P3 — Font Optimization
|
|
544
|
+
|
|
545
|
+
**What Next.js does:**
|
|
546
|
+
`next/font` automatically downloads Google Fonts at build time (no external requests), subsets them, adds `font-display: swap`, and inlines the CSS. Self-hosted fonts get the same optimizations. Zero layout shift from font loading.
|
|
547
|
+
|
|
548
|
+
**What AbsoluteJS has today:**
|
|
549
|
+
`generateHeadElement()` adds a Google Fonts `<link>` with `display=swap`. Fonts are loaded at runtime from Google's CDN.
|
|
550
|
+
|
|
551
|
+
**What needs to be built:**
|
|
552
|
+
- A build-time step that downloads declared Google Fonts, subsets them (woff2), and writes them to the assets directory
|
|
553
|
+
- Inline the `@font-face` CSS directly in the `<head>` instead of linking to Google
|
|
554
|
+
- This eliminates the external request to Google, improves privacy, and prevents FOUT
|
|
555
|
+
- Consider a `defineFont()` helper that takes a font config and returns the CSS + paths
|
|
556
|
+
|
|
557
|
+
**Files likely involved:**
|
|
558
|
+
- New: `src/build/downloadFonts.ts` — fetches and subsets Google Fonts at build time
|
|
559
|
+
- `src/utils/generateHeadElement.ts` — inline font CSS instead of external link
|
|
560
|
+
- `src/core/build.ts` — add font download step to build pipeline
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## P3 — Edge Runtime / Serverless Deployment
|
|
565
|
+
|
|
566
|
+
**What Next.js does:**
|
|
567
|
+
Routes can opt into the Edge Runtime (lightweight V8) for lower latency at the edge. Serverless function deployment on Vercel, AWS Lambda, Cloudflare Workers. Middleware always runs on edge.
|
|
568
|
+
|
|
569
|
+
**What AbsoluteJS has today:**
|
|
570
|
+
Bun-only. Requires a long-running Bun server process. No serverless or edge adapter.
|
|
571
|
+
|
|
572
|
+
**What needs to be built:**
|
|
573
|
+
- Deployment adapters for common platforms (Fly.io, Railway, Render are easy since they support Bun directly)
|
|
574
|
+
- Docker template with a minimal Bun image
|
|
575
|
+
- For serverless: an adapter that wraps the Elysia server as a Lambda/Cloud Function handler
|
|
576
|
+
- Edge runtime is unlikely to be worth pursuing — Bun doesn't run on Cloudflare Workers, and the Bun server is already fast enough that edge latency gains are marginal
|
|
577
|
+
- Focus on making traditional deployment dead simple rather than chasing edge
|
|
578
|
+
|
|
579
|
+
**Files likely involved:**
|
|
580
|
+
- New: `src/adapters/docker/Dockerfile`
|
|
581
|
+
- New: `src/adapters/lambda.ts` — AWS Lambda adapter
|
|
582
|
+
- Documentation for common deployment targets
|
package/dist/build.js
CHANGED
|
@@ -256,7 +256,19 @@ var getManifestKey = (folder, pascalName, isClientComponent, isReact, isVue, isS
|
|
|
256
256
|
const pascalName = toPascal(baseName);
|
|
257
257
|
const ext = extname(fileWithHash);
|
|
258
258
|
if (ext === ".css") {
|
|
259
|
-
const
|
|
259
|
+
const isFromReact = segments.some((seg) => seg === "react");
|
|
260
|
+
const isFromVue = segments.some((seg) => seg === "vue");
|
|
261
|
+
const isFromSvelte = segments.some((seg) => seg === "svelte");
|
|
262
|
+
const isFromAngular = segments.some((seg) => seg === "angular");
|
|
263
|
+
const isFromFramework = isFromReact || isFromVue || isFromSvelte || isFromAngular;
|
|
264
|
+
let cssKey;
|
|
265
|
+
if (isFromVue && segments.includes("css")) {
|
|
266
|
+
cssKey = `${pascalName}CompiledCSS`;
|
|
267
|
+
} else if (isFromFramework) {
|
|
268
|
+
cssKey = `${pascalName}BundledCSS`;
|
|
269
|
+
} else {
|
|
270
|
+
cssKey = `${pascalName}CSS`;
|
|
271
|
+
}
|
|
260
272
|
if (manifest[cssKey] && manifest[cssKey] !== `/${relative}`)
|
|
261
273
|
logWarn(`Duplicate manifest key "${cssKey}" \u2014 "${manifest[cssKey]}" will be overwritten by "/${relative}". Use unique page names across frameworks.`);
|
|
262
274
|
manifest[cssKey] = `/${relative}`;
|
|
@@ -205516,5 +205528,5 @@ export {
|
|
|
205516
205528
|
build
|
|
205517
205529
|
};
|
|
205518
205530
|
|
|
205519
|
-
//# debugId=
|
|
205531
|
+
//# debugId=BBCCF5F1F6563B7664756E2164756E21
|
|
205520
205532
|
//# sourceMappingURL=build.js.map
|