nextcompile

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2026 License: MIT Imports: 17 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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
View Source
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:

  1. <standaloneDir>/package.json (standalone builds vendor their own)
  2. <standaloneDir>/node_modules/next/package.json
  3. <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

func EmitManifest(m Manifest, outDir string) (string, error)

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

func EmitWorkerEntry(outDir string) (string, error)

EmitWorkerEntry writes the Worker entrypoint that ties everything together. The output path is what the adapter feeds to esbuild.

func ExtractRuntime

func ExtractRuntime(outDir string) ([]string, error)

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

func RuntimeSourceFiles() ([]string, error)

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:

  1. <standaloneDir>/<distDir>/server/server-reference-manifest.json
  2. <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

type I18nConfig struct {
	Locales         []string
	DefaultLocale   string
	LocaleDetection bool
}

I18nConfig mirrors nextcore.I18nConfig for locale-aware dispatch.

type ISRRoute

type ISRRoute struct {
	Path       string
	Tags       []string
	Revalidate int
}

ISRRoute mirrors nextcore.ISRRoute for the same duplication reason.

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 ImageRemotePattern struct {
	Protocol string
	Hostname string
	Port     string
	Pathname string
}

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 ManifestI18n struct {
	Locales         []string `json:"locales"`
	DefaultLocale   string   `json:"defaultLocale"`
	LocaleDetection bool     `json:"localeDetection"`
}

type ManifestISR

type ManifestISR struct {
	Tags      map[string][]string `json:"tags"`
	Intervals map[string]int      `json:"intervals"`
}

type ManifestImagePat

type ManifestImagePat struct {
	Protocol string `json:"protocol,omitempty"`
	Hostname string `json:"hostname,omitempty"`
	Port     string `json:"port,omitempty"`
	Pathname string `json:"pathname,omitempty"`
}

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 ManifestMiddleMat struct {
	Pathname string `json:"pathname,omitempty"`
	Pattern  string `json:"pattern,omitempty"`
}

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 MiddlewareMatcher struct {
	Pathname string
	Pattern  string
}

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

type NextVersion struct {
	Major int
	Minor int
	Patch int
	Raw   string
}

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

type ReactVersion struct {
	Major int
	Minor int
	Patch int
	Raw   string
}

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.

const (
	TargetCloudflareWorker Target = "cloudflare-worker"
	TargetAWSLambda        Target = "aws-lambda"
	TargetVPS              Target = "vps"
)

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):

  1. <standaloneDir>/node_modules/react-server-dom-webpack
  2. <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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL