Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 58 additions & 36 deletions crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ pub struct NapiProjectOptions {
/// Unix path. E.g. `apps/my-app`
pub project_path: RcStr,

/// A path where to emit the build outputs, relative to [`Project::project_path`], always Unix
/// path. Corresponds to next.config.js's `distDir`.
/// A path where tracing output will be written to and/or cache is read/written.
Copy link
Contributor

@mischnic mischnic Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but where does the config specify the output directory to write the chunks to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's read from nextConfig

/// Usually equal to the `distDir` in next.config.js.
/// E.g. `.next`
pub dist_dir: RcStr,

Expand Down Expand Up @@ -192,11 +192,6 @@ pub struct NapiPartialProjectOptions {
/// E.g. `apps/my-app`
pub project_path: Option<RcStr>,

/// A path where to emit the build outputs, relative to [`Project::project_path`], always a
/// Unix path. Corresponds to next.config.js's `distDir`.
/// E.g. `.next`
pub dist_dir: Option<Option<RcStr>>,

/// Filesystem watcher options.
pub watch: Option<NapiWatchOptions>,

Expand Down Expand Up @@ -267,43 +262,70 @@ impl From<NapiWatchOptions> for WatchOptions {

impl From<NapiProjectOptions> for ProjectOptions {
fn from(val: NapiProjectOptions) -> Self {
let NapiProjectOptions {
root_path,
project_path,
// Only used for initializing cache and tracing
dist_dir: _,
watch,
next_config,
env,
define_env,
dev,
encryption_key,
build_id,
preview_props,
browserslist_query,
no_mangling,
current_node_js_version,
} = val;
ProjectOptions {
root_path: val.root_path,
project_path: val.project_path,
watch: val.watch.into(),
next_config: val.next_config,
env: val
.env
.into_iter()
.map(|var| (var.name, var.value))
.collect(),
define_env: val.define_env.into(),
dev: val.dev,
encryption_key: val.encryption_key,
build_id: val.build_id,
preview_props: val.preview_props.into(),
browserslist_query: val.browserslist_query,
no_mangling: val.no_mangling,
current_node_js_version: val.current_node_js_version,
root_path,
project_path,
watch: watch.into(),
next_config,
env: env.into_iter().map(|var| (var.name, var.value)).collect(),
define_env: define_env.into(),
dev,
encryption_key,
build_id,
preview_props: preview_props.into(),
browserslist_query,
no_mangling,
current_node_js_version,
}
}
}

impl From<NapiPartialProjectOptions> for PartialProjectOptions {
fn from(val: NapiPartialProjectOptions) -> Self {
let NapiPartialProjectOptions {
root_path,
project_path,
watch,
next_config,
env,
define_env,
dev,
encryption_key,
build_id,
preview_props,
browserslist_query,
no_mangling,
} = val;
PartialProjectOptions {
root_path: val.root_path,
project_path: val.project_path,
watch: val.watch.map(From::from),
next_config: val.next_config,
env: val
.env
.map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()),
define_env: val.define_env.map(|env| env.into()),
dev: val.dev,
encryption_key: val.encryption_key,
build_id: val.build_id,
preview_props: val.preview_props.map(|props| props.into()),
root_path,
project_path,
watch: watch.map(From::from),
next_config,
env: env.map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()),
define_env: define_env.map(|env| env.into()),
dev,
encryption_key,
build_id,
preview_props: preview_props.map(|props| props.into()),
browserslist_query,
no_mangling,
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions crates/next-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ pub struct PartialProjectOptions {

/// Options for draft mode.
pub preview_props: Option<DraftModeOptions>,

/// The browserslist query to use for targeting browsers.
pub browserslist_query: Option<RcStr>,

/// When the code is minified, this opts out of the default mangling of
/// local names for variables, functions etc., which can be useful for
/// debugging/profiling purposes.
pub no_mangling: Option<bool>,
}

#[derive(
Expand Down Expand Up @@ -347,6 +355,8 @@ impl ProjectContainer {
encryption_key,
build_id,
preview_props,
browserslist_query,
no_mangling,
} = options;

let resolved_self = self.to_resolved().await?;
Expand Down Expand Up @@ -388,6 +398,12 @@ impl ProjectContainer {
if let Some(preview_props) = preview_props {
new_options.preview_props = preview_props;
}
if let Some(browserslist_query) = browserslist_query {
new_options.browserslist_query = browserslist_query;
}
if let Some(no_mangling) = no_mangling {
new_options.no_mangling = no_mangling;
}

// TODO: Handle mode switch, should prevent mode being switched.
let watch = new_options.watch;
Expand Down
10 changes: 2 additions & 8 deletions packages/next/src/build/swc/generated-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export interface NapiProjectOptions {
*/
projectPath: RcStr
/**
* A path where to emit the build outputs, relative to [`Project::project_path`], always Unix
* path. Corresponds to next.config.js's `distDir`.
* A path where tracing output will be written to and/or cache is read/written.
* Usually equal to the `distDir` in next.config.js.
* E.g. `.next`
*/
distDir: RcStr
Expand Down Expand Up @@ -172,12 +172,6 @@ export interface NapiPartialProjectOptions {
* E.g. `apps/my-app`
*/
projectPath?: RcStr
/**
* A path where to emit the build outputs, relative to [`Project::project_path`], always a
* Unix path. Corresponds to next.config.js's `distDir`.
* E.g. `.next`
*/
distDir?: RcStr | undefined | null
/** Filesystem watcher options. */
watch?: NapiWatchOptions
/** The contents of next.config.js, serialized to JSON. */
Expand Down
7 changes: 4 additions & 3 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
Endpoint,
HmrIdentifiers,
Lockfile,
PartialProjectOptions,
Project,
ProjectOptions,
RawEntrypoints,
Expand Down Expand Up @@ -651,15 +652,15 @@ function bindingToApi(
}

async function rustifyPartialProjectOptions(
options: Partial<ProjectOptions>
options: PartialProjectOptions
): Promise<NapiPartialProjectOptions> {
return {
...options,
nextConfig:
options.nextConfig &&
(await serializeNextConfig(
options.nextConfig,
path.join(options.rootPath!, options.projectPath!)
path.join(options.rootPath, options.projectPath)
)),
env: options.env && rustifyEnv(options.env),
}
Expand All @@ -672,7 +673,7 @@ function bindingToApi(
this._nativeProject = nativeProject
}

async update(options: Partial<ProjectOptions>) {
async update(options: PartialProjectOptions) {
await binding.projectUpdate(
this._nativeProject,
await rustifyPartialProjectOptions(options)
Expand Down
75 changes: 13 additions & 62 deletions packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
RefCell,
NapiTurboEngineOptions,
NapiSourceDiagnostic,
NapiProjectOptions,
NapiPartialProjectOptions,
} from './generated-native'

export type { NapiTurboEngineOptions as TurboEngineOptions }
Expand Down Expand Up @@ -367,27 +369,8 @@ export type WrittenEndpoint =
config: EndpointConfig
}

export interface ProjectOptions {
/**
* An absolute root path (Unix or Windows path) from which all files must be nested under. Trying
* to access a file outside this root will fail, so think of this as a chroot.
* E.g. `/home/user/projects/my-repo`.
*/
rootPath: string

/**
* A path which contains the app/pages directories, relative to `root_path`, always a Unix path.
* E.g. `apps/my-app`
*/
projectPath: string

/**
* A path where to emit the build outputs, relative to [`Project::project_path`], always a Unix
* path. Corresponds to next.config.js's `distDir`.
* E.g. `.next`
*/
distDir: string

export interface ProjectOptions
extends Omit<NapiProjectOptions, 'nextConfig' | 'env'> {
/**
* The next.config.js contents.
*/
Expand All @@ -397,53 +380,21 @@ export interface ProjectOptions {
* A map of environment variables to use when compiling code.
*/
env: Record<string, string>
}

defineEnv: DefineEnv

/**
* Whether to watch the filesystem for file changes.
*/
watch: {
enable: boolean
pollIntervalMs?: number
}

/**
* The mode in which Next.js is running.
*/
dev: boolean

/**
* The server actions encryption key.
*/
encryptionKey: string

/**
* The build id.
*/
buildId: string

/**
* Options for draft mode.
*/
previewProps: __ApiPreviewProps

/**
* The browserslist query to use for targeting browsers.
*/
browserslistQuery: string

export interface PartialProjectOptions
extends Omit<NapiPartialProjectOptions, 'nextConfig' | 'env'> {
rootPath: NapiProjectOptions['rootPath']
projectPath: NapiProjectOptions['projectPath']
Comment on lines +387 to +388
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PartialProjectOptions interface incorrectly marks rootPath and projectPath as required fields, but these should be optional since they correspond to optional fields in NapiPartialProjectOptions and should only be required when updating specific properties like nextConfig.

View Details
📝 Patch Details
diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts
index c0ff835a8f..73340b1d0c 100644
--- a/packages/next/src/build/swc/index.ts
+++ b/packages/next/src/build/swc/index.ts
@@ -658,6 +658,8 @@ function bindingToApi(
       ...options,
       nextConfig:
         options.nextConfig &&
+        options.rootPath &&
+        options.projectPath &&
         (await serializeNextConfig(
           options.nextConfig,
           path.join(options.rootPath, options.projectPath)
diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts
index b9e0fee184..adb84156fa 100644
--- a/packages/next/src/build/swc/types.ts
+++ b/packages/next/src/build/swc/types.ts
@@ -384,8 +384,6 @@ export interface ProjectOptions
 
 export interface PartialProjectOptions
   extends Omit<NapiPartialProjectOptions, 'nextConfig' | 'env'> {
-  rootPath: NapiProjectOptions['rootPath']
-  projectPath: NapiProjectOptions['projectPath']
   /**
    * The next.config.js contents.
    */

Analysis

PartialProjectOptions incorrectly marks rootPath and projectPath as required

What fails: The PartialProjectOptions TypeScript interface in packages/next/src/build/swc/types.ts marks rootPath and projectPath as required fields (lines 387-388), but these should be optional to match the Rust type definitions and semantic intent of a "partial" update interface.

How to reproduce:

// This should work but currently fails TypeScript type checking
const options: PartialProjectOptions = {
  nextConfig: nextConfig,
};
// Error: Type '{ nextConfig: NextConfigComplete; }' is missing the following properties from type 'PartialProjectOptions': rootPath, projectPath

Root cause: The PartialProjectOptions interface extends Omit<NapiPartialProjectOptions, 'nextConfig' | 'env'> which correctly inherits optional rootPath? and projectPath? fields from the generated Rust bindings. However, lines 387-388 explicitly redefine these fields as required, overriding the optional versions inherited from the Omit type.

The underlying Rust types confirm this design: NapiPartialProjectOptions has root_path: Option<RcStr> and project_path: Option<RcStr>, and the conversion code in crates/next-api/src/project.rs lines 371-376 validates these fields with if let Some(...) patterns, indicating they are meant to be optional for partial updates.

Fix applied:

  1. Removed the explicit rootPath and projectPath field redefinitions from PartialProjectOptions interface (lines 387-388), allowing them to be inherited as optional from the Omit type
  2. Updated rustifyPartialProjectOptions in packages/next/src/build/swc/index.ts to check that rootPath and projectPath exist before using them to serialize nextConfig (defensive guard to prevent runtime errors if someone updates nextConfig without providing paths)

This allows proper partial updates where callers can update individual fields like nextConfig, dev, encryption_key, etc. without being forced to provide paths that are only needed for specific operations.

/**
* When the code is minified, this opts out of the default mangling of local
* names for variables, functions etc., which can be useful for
* debugging/profiling purposes.
* The next.config.js contents.
*/
noMangling: boolean
nextConfig?: NextConfigComplete

/**
* The version of Node.js that is available/currently running.
* A map of environment variables to use when compiling code.
*/
currentNodeJsVersion: string
env?: Record<string, string>
}

export interface DefineEnv {
Expand Down
Loading