Documentation
¶
Overview ¶
Package nextcompile transforms a Next.js standalone build into a runtime-native Worker/Lambda bundle by analyzing the compiled output and emitting a dispatch table + manifest that a minimal JS runtime consumes at request time.
This is the build-time half of the nextcore adapter story. The JS runtime half lives in runtime_src/ and is embedded as pre-built bundles under runtime_assets/.
Pipeline (see compiler.go):
NextCorePayload (from shared/nextcore) + .next/standalone
│
▼
DetectVersions ── pick runtime variant (v13 / v14 / v15)
│
▼
ScanCompiledServer ── walk .next/server/**, build ModuleRef graph
│
▼
DetectServerActions ── parse server-reference-manifest.json
│
▼
DeriveBindings ── static analysis → binding hints
│
▼
ElideDeadRoutes ── drop orphans
│
▼
Emit{Manifest,DispatchTable,ActionManifest} → <OutDir>/_nextdeploy/
│
▼
ExtractRuntimeForVersion + AssembleBundle → CompiledBundle
Index ¶
- Variables
- func DetectVersions(standaloneDir string) (NextVersion, ReactVersion, error)
- func EmitActionManifest(m *ActionManifest, outDir string) (string, error)
- func EmitDispatchTable(refs []ModuleRef, actions *ActionManifest, outDir, standaloneDir string) (string, error)
- func EmitManifest(m Manifest, outDir string) (string, error)
- func EmitWorkerEntry(outDir string) (string, error)
- func ExtractRuntime(outDir string) ([]string, error)
- func RuntimeSourceFiles() ([]string, error)
- type Action
- type ActionManifest
- type ActionRuntime
- type BindingHint
- type CompileOpts
- type CompileStats
- type CompiledBundle
- type I18nConfig
- type ISRRoute
- type ImageConfig
- type ImageRemotePattern
- type Logger
- type Manifest
- type ManifestFeatures
- type ManifestI18n
- type ManifestISR
- type ManifestImagePat
- type ManifestImages
- type ManifestMiddle
- type ManifestMiddleMat
- type ManifestRoutes
- type MiddlewareConfig
- type MiddlewareMatcher
- type ModuleRef
- type NextVersion
- type Payload
- type ReactVersion
- type RouteInfo
- type RouteKind
- type Target
- type VendoredPackage
Constants ¶
This section is empty.
Variables ¶
var ErrNoActionManifest = errors.New("server-reference-manifest.json not present in standalone tree")
ErrNoActionManifest is returned by DetectServerActions when the upstream manifest is absent. Expected for apps that don't use Server Actions — caller logs at debug and moves on.
var ErrRSCPackageNotFound = errors.New("react-server-dom-webpack not found in node_modules")
ErrRSCPackageNotFound signals that react-server-dom-webpack is not present in the standalone tree's node_modules. Callers match on this via errors.Is to branch:
- manifest.Features.RSC == true → surface to user with vendoring steps
- manifest.Features.RSC == false → silently skip, rsc.mjs will 501 anyway if ever reached
var ErrVersionNotFound = fmt.Errorf("nextcompile: could not locate next.js version in standalone tree")
ErrVersionNotFound is returned when no package.json yields a Next version.
Functions ¶
func DetectVersions ¶
func DetectVersions(standaloneDir string) (NextVersion, ReactVersion, error)
DetectVersions reads Next.js and React versions from the standalone build's dependency graph. Lookup order:
- <standaloneDir>/package.json (standalone builds vendor their own)
- <standaloneDir>/node_modules/next/package.json
- <standaloneDir>/../package.json (repo root, last resort)
Returns ErrVersionNotFound when none resolve. Callers should treat that as fatal — the runtime bundle selection depends on knowing which Next major is in play.
func EmitActionManifest ¶
func EmitActionManifest(m *ActionManifest, outDir string) (string, error)
EmitActionManifest writes the flattened manifest to <outDir>/_nextdeploy/action_manifest.json. Returns the final path. When the manifest has zero actions, the file is still written (the runtime dispatcher handles empty gracefully and its absence would be ambiguous — "no actions" vs "file missing" vs "malformed build").
func EmitDispatchTable ¶
func EmitDispatchTable(refs []ModuleRef, actions *ActionManifest, outDir, standaloneDir string) (string, error)
EmitDispatchTable writes the generated ESM at <outDir>/_nextdeploy/dispatch.mjs. The file exports five symbols the runtime imports:
staticTable — Record<routePath, { kind, usesRSC, hasActions, load }>
dynamicTable — Array<{ route, kind, pattern, paramNames, usesRSC, hasActions, load }>
middlewareRef — { compiled, load } | null (Edge-runtime middleware.ts)
proxyRef — { compiled, load } | null (Node-runtime proxy.ts, Next 15+)
actionLoaders — Record<moduleID, () => Promise<any>> for Server Actions dispatch
Each `load` is an arrow returning `import("<relative path to compiled module>")`. Relative paths are rooted at standaloneDir (the tree the ModuleRef.CompiledPath values are relative to). esbuild resolves them at bundle time.
actionLoaders must be emitted here (not in entry.mjs via dynamic string concatenation) because esbuild's tree-shaking only tracks literal import() calls. Hoisting action-module imports into this generated file keeps them visible to the bundler.
usesRSC and hasActions propagate from the scanner so the runtime dispatcher can route a page through rsc.mjs vs the Pages-Router legacy path without re-scanning the compiled source at request time.
standaloneDir is the path the ModuleRef.CompiledPath values are relative to. When outDir lives inside standaloneDir at any depth, dispatch.mjs's imports need to walk back out to standaloneDir before descending into `.next/...`. Pass empty string to get the legacy single-`../` behaviour (assumes dispatch.mjs ends up directly under standaloneDir).
func EmitManifest ¶
EmitManifest writes the manifest JSON to <outDir>/_nextdeploy/manifest.json. Returns the final path. Uses 2-space indent + trailing newline for human- friendly diffs; encoding/json's sorted map key emission is what gives us byte-identical output across runs.
func EmitWorkerEntry ¶
EmitWorkerEntry writes the Worker entrypoint that ties everything together. The output path is what the adapter feeds to esbuild.
func ExtractRuntime ¶
ExtractRuntime copies the embedded JS runtime into <outDir>/_nextdeploy/runtime/. The target directory is created if it doesn't exist; existing files are overwritten (every build regenerates).
Returns the list of extracted paths in deterministic order so callers can include them in a build summary and hash for reproducibility.
func RuntimeSourceFiles ¶
RuntimeSourceFiles returns the list of .mjs files bundled in the embedded runtime, sorted. Used for test assertions and the content-hash calculation without requiring extraction.
Types ¶
type Action ¶
type Action struct {
ID string `json:"-"`
Module string `json:"module"`
Export string `json:"export"`
Runtime ActionRuntime `json:"runtime"`
}
Action is one entry in our flattened manifest.
type ActionManifest ¶
type ActionManifest struct {
SchemaVersion string `json:"schemaVersion"`
Actions map[string]Action `json:"actions"`
}
ActionManifest is the on-disk shape emitted to action_manifest.json. Keys sort deterministically via encoding/json's map ordering.
func DetectServerActions ¶
func DetectServerActions(standaloneDir, distDir string) (*ActionManifest, error)
DetectServerActions reads Next's server-reference-manifest.json from the standalone tree and returns a flattened ActionManifest.
Lookup order:
- <standaloneDir>/<distDir>/server/server-reference-manifest.json
- <standaloneDir>/server/server-reference-manifest.json (standalone builds sometimes flatten the tree)
type ActionRuntime ¶
type ActionRuntime string
ActionRuntime is where the action was tagged to run in the upstream manifest. We carry it forward so the runtime dispatcher could branch if we ever care (today Workers runs everything identically).
const ( ActionRuntimeNode ActionRuntime = "node" ActionRuntimeEdge ActionRuntime = "edge" )
type BindingHint ¶
type BindingHint struct {
Kind string // "secret" | "kv" | "r2" | "d1" | "service" | "queue"
Name string // env var name or logical binding name
Reason string // human-readable justification
Sources []string // ModuleRef.CompiledPath list where the hint was derived
}
BindingHint is a compiler-derived suggestion for the deployment config. Emitted as warnings rather than errors — the user has final say.
type CompileOpts ¶
type CompileOpts struct {
// StandaloneDir is the path to .next/standalone (or its extracted tarball).
StandaloneDir string
// Payload is the nextcore extraction. Routes, middleware, image config,
// and ISR tag data all flow from here into the emitted manifest.
Payload Payload
// OutDir is where _nextdeploy/{manifest.json,dispatch.mjs,...} land.
// Defaults to <StandaloneDir>/.nextdeploy-build when empty.
OutDir string
// Target picks the emit strategy. Defaults to TargetCloudflareWorker.
Target Target
// Verbose toggles per-step timing logs.
Verbose bool
// Log sinks diagnostics. Compile tolerates a nil logger (no output).
Log Logger
}
CompileOpts is the input bag for Compile. StandaloneDir and Payload are required; everything else has a defensible zero value.
type CompileStats ¶
type CompileStats struct {
RouteCount int
ActionCount int
DeadRoutesElided int
BundleBytes int64
Duration time.Duration
ContentHash string
}
CompileStats is the post-run summary. Logged in full at info, content-hashed for reproducible-build verification.
type CompiledBundle ¶
type CompiledBundle struct {
BundleDir string
EntryPath string
ManifestPath string
DispatchPath string
ActionManifest string
DetectedVersion NextVersion
DetectedReact ReactVersion
SuggestedBindings []BindingHint
// VendoredRSC is populated when the target requires vendoring and the
// react-server-dom-webpack package was located in node_modules. Nil
// when the app does not use RSC, or when vendoring was not applicable
// for the target. Checked by adapter logs and bundle reports.
VendoredRSC *VendoredPackage
Stats CompileStats
}
CompiledBundle is the output of Compile. Everything in BundleDir is ready for the downstream esbuild step; the adapter doesn't need to know the subtree layout beyond EntryPath.
func Compile ¶
func Compile(ctx context.Context, opts CompileOpts) (*CompiledBundle, error)
Compile runs the full build-side pipeline against a Next.js standalone tree and produces a CompiledBundle ready for the adapter's esbuild step.
This is the single public entry point; callers should not invoke the phase functions directly. Phase ordering and error policy are documented inline — deviations will break reproducibility guarantees.
Contract:
- On error, no guarantee that OutDir has been cleaned. The adapter's build-bundle step should treat OutDir as disposable (blow it away and re-run on retry) rather than expect partial success.
- On success, every path in the returned CompiledBundle exists.
- CompileStats.ContentHash is deterministic for identical input.
type I18nConfig ¶
I18nConfig mirrors nextcore.I18nConfig for locale-aware dispatch.
type ImageConfig ¶
type ImageConfig struct {
RemotePatterns []ImageRemotePattern
Domains []string
Formats []string
Unoptimized bool
}
ImageConfig mirrors the minimal shape the /_next/image runtime handler needs: the remote-pattern whitelist. Everything else (device sizes, format preference, etc.) is included raw in the emitted manifest.
type ImageRemotePattern ¶
type Logger ¶
type Logger interface {
Info(format string, args ...any)
Warn(format string, args ...any)
Debug(format string, args ...any)
}
Logger is the minimal sink the compiler writes to. Matches the subset of shared.Logger the package actually uses; kept as an interface so tests can pass a no-op sink without pulling in the full shared package.
type Manifest ¶
type Manifest struct {
SchemaVersion string `json:"schemaVersion"`
GeneratedAt string `json:"generatedAt"`
AppName string `json:"appName"`
BasePath string `json:"basePath,omitempty"`
NextVersion string `json:"nextVersion"`
ReactVersion string `json:"reactVersion,omitempty"`
BuildID string `json:"buildId,omitempty"`
GitCommit string `json:"gitCommit,omitempty"`
Routes ManifestRoutes `json:"routes"`
ISR ManifestISR `json:"isr"`
Middleware *ManifestMiddle `json:"middleware,omitempty"`
Images *ManifestImages `json:"images,omitempty"`
I18n *ManifestI18n `json:"i18n,omitempty"`
HasAppRouter bool `json:"hasAppRouter"`
OutputMode string `json:"outputMode,omitempty"`
// Features is the app's detected capability surface. Runtime consults
// this to decide which handlers to wire; operators can eyeball it to
// confirm the deployed bundle actually supports what they expect.
Features ManifestFeatures `json:"features"`
}
Manifest is the wire format emitted into _nextdeploy/manifest.json. Every field is shaped for direct consumption by the JS runtime with no client-side transformation — what you see here is what the dispatcher reads at request time.
Field order is stable (json:"-" via explicit struct layout) so identical input produces identical bytes, which is the load-bearing property for content-addressable bundle hashing.
func BuildManifest ¶
func BuildManifest(p Payload, next NextVersion, react ReactVersion, refs []ModuleRef, generatedAt time.Time) Manifest
BuildManifest constructs the Manifest from Payload + version info + the scanned refs (for feature detection). Pure — no I/O — so tests can call it without a filesystem.
type ManifestFeatures ¶
type ManifestFeatures struct {
RSC bool `json:"rsc"` // any page or layout uses Server Components
ServerActions bool `json:"serverActions"` // any module carries action markers
Middleware bool `json:"middleware"` // Edge-runtime middleware.ts present
Proxy bool `json:"proxy"` // Node-runtime proxy.ts present (Next 15+)
ISR bool `json:"isr"` // any route has ISR revalidation
ImageOptimize bool `json:"imageOptimization"` // /_next/image is expected to work
I18n bool `json:"i18n"` // locales declared
PPR bool `json:"ppr"` // any page opts into Partial Prerendering
After bool `json:"after"` // any module uses the after() API
}
ManifestFeatures is the detected capability summary. True = the app uses the feature. The runtime bundle serves what it can; features whose runtime is not yet wired return a clear 501 rather than fail silently.
type ManifestI18n ¶
type ManifestISR ¶
type ManifestImagePat ¶
type ManifestImages ¶
type ManifestImages struct {
RemotePatterns []ManifestImagePat `json:"remotePatterns"`
Domains []string `json:"domains,omitempty"`
Formats []string `json:"formats,omitempty"`
Unoptimized bool `json:"unoptimized,omitempty"`
}
type ManifestMiddle ¶
type ManifestMiddle struct {
Path string `json:"path"`
Matchers []ManifestMiddleMat `json:"matchers"`
Runtime string `json:"runtime,omitempty"`
}
type ManifestMiddleMat ¶
type ManifestRoutes ¶
type ManifestRoutes struct {
Static []string `json:"static"`
SSG map[string]string `json:"ssg"` // route -> HTML path
SSR []string `json:"ssr"`
ISR map[string]string `json:"isr"`
API []string `json:"api"`
Dynamic []string `json:"dynamic"`
Fallback map[string]string `json:"fallback"`
Middleware []string `json:"middleware"`
}
ManifestRoutes is the dispatch classification the runtime consults in priority order: middleware → static → ssg → isr → dynamic (ssr/page) → api.
type MiddlewareConfig ¶
type MiddlewareConfig struct {
Path string
Matchers []MiddlewareMatcher
Runtime string
}
MiddlewareConfig mirrors the subset of nextcore.MiddlewareConfig the compiler forwards into the runtime manifest. The full matcher shape is opaque here — it gets emitted as JSON into manifest.json verbatim.
type MiddlewareMatcher ¶
type ModuleRef ¶
type ModuleRef struct {
// RoutePath is the user-facing URL (e.g. "/api/users" or "/dashboard/[id]").
RoutePath string
// Kind is what the dispatcher should do with this module.
Kind RouteKind
// CompiledPath is relative to StandaloneDir (e.g. "server/app/api/users/route.js").
CompiledPath string
// HasActions is true when Next's server-reference-manifest lists an
// action ID rooted in this module.
HasActions bool
// UsesRSC is true when the compiled source contains Server Component
// markers ("use client" boundaries or Flight-payload imports).
UsesRSC bool
// ClientManifestPath points at the Next-emitted
// page_client-reference-manifest.json sibling, when present.
// Relative to StandaloneDir. Used by rsc.mjs as Flight bundlerConfig.
ClientManifestPath string
// LayoutChain is the ordered list of compiled layout.js paths that
// wrap this page, from root to nearest. Empty for non-page kinds.
LayoutChain []string
// EnvRefs are the unique process.env.X identifiers the compiler
// found via lexical scan. Used by DeriveBindings to suggest secrets.
EnvRefs []string
// FetchTargets are literal fetch() URL prefixes extracted from the
// module. Used to suggest KV/R2/D1/service bindings.
FetchTargets []string
// ByteSize is the raw size of the compiled source on disk.
ByteSize int64
// PPREnabled is true when Next compiled this page with Partial
// Prerendering. The runtime dispatcher returns a clear 501 for now
// since our renderer doesn't implement the static-shell / dynamic-
// holes protocol yet.
PPREnabled bool
}
ModuleRef is one compiled server module — a page, layout, route handler, or middleware — plus the static-analysis facts the compiler derived.
func ScanCompiledServer ¶
func ScanCompiledServer(ctx context.Context, standaloneDir string, payload Payload) ([]ModuleRef, error)
ScanCompiledServer walks the server subtree of a Next standalone build in parallel and returns a ModuleRef per compiled route/handler/middleware. Non-server assets (client chunks, static files) are skipped — those flow to the CDN via packaging.S3Assets, not into the Worker bundle.
The caller's Payload supplies route classification; the scanner's job is to attach compiled file paths to each classified route and extract the static-analysis facts (env refs, fetch targets, RSC markers) that later phases consume.
type NextVersion ¶
NextVersion is the semver-ish breakdown of the detected Next.js version. Raw is preserved verbatim (e.g. "15.0.0-canary.42") for diagnostics.
func (NextVersion) RuntimeVariant ¶
func (v NextVersion) RuntimeVariant() string
RuntimeVariant picks which embedded runtime bundle matches the detected Next version. Only majors are consulted — minors share a runtime within a major because our JS runtime targets the stable public surface, not the NextServer internals that churn per-minor.
type Payload ¶
type Payload struct {
AppName string
DistDir string
OutputMode string
BasePath string
HasAppRouter bool
Routes RouteInfo
Middleware *MiddlewareConfig
ImageConfig *ImageConfig
I18n *I18nConfig
BuildID string
GitCommit string
}
Payload is the subset of nextcore.NextCorePayload that the compiler actually reads. Declared as a minimal interface-ish struct so the compiler package can stay free of the nextcore import cycle risk while staying strongly typed at the adapter boundary.
Callers populate this by translating from nextcore.NextCorePayload in the adapter (cli/internal/serverless/cloudflare_adapter.go).
type ReactVersion ¶
ReactVersion mirrors NextVersion. Tracked separately because the RSC runtime bundle is keyed on React version, not Next version — two Next minors can ship against the same React minor.
type RouteInfo ¶
type RouteInfo struct {
StaticRoutes []string
DynamicRoutes []string
SSGRoutes map[string]string // route -> HTML path
SSRRoutes []string
ISRRoutes map[string]string
ISRDetail []ISRRoute
APIRoutes []string
FallbackRoutes map[string]string
MiddlewareRoutes []string
}
RouteInfo mirrors nextcore.RouteInfo. Duplicated here so the compiler never imports nextcore directly (see Payload doc).
type RouteKind ¶
type RouteKind string
RouteKind categorizes a compiled module. Dispatch order in the runtime follows this roughly: Middleware → Static/SSG → ISR → SSR/Page → API/Action.
const ( RouteKindPage RouteKind = "page" RouteKindLayout RouteKind = "layout" RouteKindAPI RouteKind = "api" RouteKindAction RouteKind = "action" RouteKindMiddleware RouteKind = "middleware" // RouteKindProxy is Next 15's proxy.ts — a Node-runtime middleware // complement. Same dispatch position as middleware but uses the Node // execution model (can call crypto, Node streams, etc.). If both // proxy and middleware exist, proxy wins (Next's documented order). RouteKindProxy RouteKind = "proxy" RouteKindStatic RouteKind = "static" RouteKindUnknown RouteKind = "unknown" )
type Target ¶
type Target string
Target selects which deploy surface the bundle is compiled for. The same scan phase feeds every target; emit phases diverge.
type VendoredPackage ¶
type VendoredPackage struct {
Name string
Version string
SourcePath string
TargetPath string
Bytes int64
BuildKind string // "production" | "development" | "legacy"
}
VendoredPackage records what VendorRSC copied into the bundle. The adapter logs this and includes it in CompileStats.
func VendorRSC ¶
func VendorRSC(standaloneDir, bundleDir string) (*VendoredPackage, error)
VendorRSC resolves react-server-dom-webpack from the standalone tree's node_modules and copies its server.edge ESM bundle into <bundleDir>/_nextdeploy/runtime/vendor/react-server-dom-webpack/server.edge.mjs.
Lookup order (first hit wins):
- <standaloneDir>/node_modules/react-server-dom-webpack
- <standaloneDir>/../node_modules/react-server-dom-webpack (app root)
Returns ErrRSCPackageNotFound when neither location resolves. The CF Workers runtime has no npm at request time, so vendoring is the only way Server Components render without OpenNext.