@tschk/moonshine-next
v0.4.49
Published
Next.js API surface reimplemented on moonshine — drop Next, keep the imports
Readme
@tschk/moonshine-next
Moonshine reimplements the Next.js API surface. It does not wrap, re-export,
or depend on Next — next is not a dependency, a peer dependency, or a
devDependency of this package. You keep writing next/link, next/navigation,
next/image; moonshine provides them, on its own signal runtime and router.
import { createSignal, useSignal } from "@tschk/moonshine-next";
import { useRouter, usePathname } from "@tschk/moonshine-next/navigation";
import Link from "@tschk/moonshine-next/link";
import Image from "@tschk/moonshine-next/image";
import { headers, cookies } from "@tschk/moonshine-next/headers";
import { moonshineRoute, moonshineJson } from "@tschk/moonshine-next/server";Peers: react >= 18, react-dom >= 18. That is the whole runtime requirement.
Drop-in: keep your existing imports
You do not have to rewrite a single import. Map the next/* specifiers onto
this package at build time.
Bun
import { moonshineNextPlugin } from "@tschk/moonshine-next/aliases";
await Bun.build({
entrypoints: ["./app/page.tsx"],
plugins: [moonshineNextPlugin()],
});For bun run / bun test, register it in a preload file:
// bun-preload.ts
import { plugin } from "bun";
import { moonshineNextPlugin } from "@tschk/moonshine-next/aliases";
plugin(moonshineNextPlugin());# bunfig.toml
preload = ["./bun-preload.ts"]TypeScript
So the type checker follows the same mapping the bundler applies:
{
"compilerOptions": {
"paths": {
"next/link": ["@tschk/moonshine-next/link"],
"next/navigation": ["@tschk/moonshine-next/navigation"],
"next/image": ["@tschk/moonshine-next/image"],
"next/script": ["@tschk/moonshine-next/script"],
"next/headers": ["@tschk/moonshine-next/headers"],
"next/dynamic": ["@tschk/moonshine-next/dynamic"],
"next/font/google": ["@tschk/moonshine-next/font/google"],
},
},
}tsconfigPaths() from @tschk/moonshine-next/aliases returns exactly that
object, and nextAliases is the raw specifier map if you need to feed another
bundler (esbuild alias, Vite resolve.alias, webpack resolve.alias).
Supported
| Subpath | Provides |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| . / ./client | moonshine signals + resources ("use client") |
| ./navigation | useRouter usePathname useSearchParams useParams redirect permanentRedirect notFound useSelectedLayoutSegment(s) + usePathnameSignal |
| ./link | default Link — client navigation, hover/viewport prefetch |
| ./image | default Image — src alt width height fill priority loading sizes |
| ./script | default Script — strategy mapped to async/defer |
| ./headers | headers() cookies() draftMode() (server only) |
| ./dynamic | default dynamic() with loading, ssr: false, .preload() |
| ./font/google | named family loaders returning { className, style, variable, href } |
| ./aliases | next/* → moonshine specifier mapping + Bun plugin |
| ./shaders | moonshine WebGL fragment helpers |
| ./server | moonshineRoute / moonshineJson / moonshineHtml + page helpers |
Behavioural notes where moonshine deliberately differs from Next:
Linkprefetch is a browser hint. It appends<link rel="prefetch">on hover and on viewport entry. There is no RSC payload to warm, so this is not Next's prefetch cache — it only primes the HTTP cache.Linkscroll follows the destination. Next always jumps to the top; moonshine only scrolls when the pathname or hash actually changes, so query-only navigation (filters, pagination, tabs) keeps your scroll position. Passscrollexplicitly to force either behaviour.router.refresh()reloads the browser document. This requests fresh server output and resets local component state. State-preserving RSC refresh is unsupported. It is safe to call during SSR, where it does nothing.router.prefetch()is a no-op. UseLinkfor browser prefetch hints.- Browser navigation stays with the browser. Links preserve external URLs,
non-self targets, fragments and downloads without route prefetch. External
router.pushuses document assignment; externalreplacereplaces the history entry. Same-origin absolute URLs retain SPA navigation. The existing safe-href policy prevents executable schemes from reaching document navigation. redirect()/notFound()throwRedirectError/NotFoundError, exported from./navigationso your renderer can catch them.useSearchParams()reads the live query string. The moonshine router stores only the pathname, so the query is recovered fromwindow.location.
Not supported
Be aware of these before migrating — none of them are stubbed to silently "work":
- No image optimizer.
next/imagerenders a plain<img>.srcis served exactly as authored;qualityandunoptimizedare accepted and ignored. There is no resizing, no format negotiation, nosrcSetgeneration, noloader/remotePatternsconfig, no blur placeholder. - No React Server Components. No
"use server", no Server Actions, no server-only component tree, no streaming RSC payload. - No ISR / on-demand revalidation. No
revalidate, norevalidatePath, norevalidateTag, nounstable_cache, no fetch cache semantics. - No middleware.
middleware.tsis not read; there is no edge interception layer. Use the moonshine server directly. - No
next/fontself-hosting.next/font/googledoes not download or subset fonts at build time. It returns Next's{ className, style, variable }shape plus anhref, and you emit the Google Fonts<link>tags yourself viafontLinks(). That means a third-party request at runtime, which is exactly what Next's self-hosting avoids.next/font/localis not implemented. - No file-system routing conventions.
layout.tsx,loading.tsx,error.tsx,template.tsx, route groups, parallel and intercepting routes are not interpreted. Use the moonshine router. - Bounded cookie mutation.
cookies().set()and.delete()use the opt-in request response-header scope on Bun/Node.headers()has no response mutation API, anddraftMode()always reports disabled. Streaming Server Actions and Next's render-phase restrictions are not implemented. - Other
next/*modules are absent, includingnext/server(NextRequest/NextResponse),next/og,next/cache, andnext/navigation'sunstable_*APIs. Route Handlers take and return plainRequest/Response; see./server.
Does not export MoonshineRouter from the client entry — import it from
@tschk/moonshine/router if you want the moonshine router directly.
See Moonshine README.
Request helpers require requestContext: true in Moonshine config (enabled by
Next adoption). Bun/Node deployment and preview inject the runner from the same
compiled bundle as the route modules; a separately imported context cannot scope
an already-bundled route. Edge request context is currently unsupported.
cookies() retains synchronous reads (await cookies() also works). With
requestContext: true, loaders, actions and buffered rendering can call
set(name, value, options) or set({name, value, ...options}). Values are
percent-encoded; invalid names and attributes reject before changing headers or
same-request reads. The default path is /. Each write appends a separate
Set-Cookie; later writes to the same name update the shared request-local jar.
Ordinary incoming cookies still read without a response scope; mutation requires
one. This does not install a Next runtime.
delete(name) expires the root-path cookie. Use delete({name, path, domain})
for another scope; the incoming Cookie header cannot reveal a cookie's original
path or domain. Deletion leaves an empty tombstone in same-request reads; the
browser applies expiration on receipt. Signed/encrypted sessions, domain ownership
checks, cookie prefix policies, streaming and Next cache invalidation are outside
this API. Standard expiry, age, security, SameSite, priority and partitioned
attributes are serialized; browser acceptance still follows browser policy.
