Skip to content

Commit d5276f0

Browse files
Make TurbopackResult payloads disjoint (#98575)
### What? Refactors the JavaScript-facing `TurbopackResult<T>` into a stable wrapper whose payload lives under `value` and whose issues remain top-level. Native API consumers, entrypoint conversion, development-server paths, HMR handling, and direct API tests now follow the disjoint shape. Two event subscriptions that had inaccurate wrapper declarations now expose their existing plain runtime payloads explicitly: update-info events remain `UpdateMessage`, and compilation events remain `CompilationEvent`. Nullable native entrypoint payloads are also declared accurately and normalized at the JavaScript API boundary. ### Why? The previous intersection-based representation merged payload fields with result metadata. That allowed fields such as `issues` to overwrite one another and caused non-object payloads to be discarded, making the result shape depend on `T`. A dedicated payload property avoids those collisions and preserves every payload type, including `null`. Keeping plain event streams distinct from result wrappers also ensures `TurbopackResult<T>` consistently means the native API actually provides wrapper metadata. ### How? The N-API serializer now always creates a fresh wrapper rather than mutating an object payload. The shared TypeScript type models that wrapper directly and requires an explicit payload type. Conversion layers replace only the nested payload while explicitly preserving wrapper issues. Call sites continue to process wrapper issues while reading domain data through `value`. The update-info and compilation-event declarations were aligned with their native callback types instead of introducing new runtime wrappers for streams that do not collect issues. Rust `Option<NapiEntrypoints>` payloads are declared as nullable, then normalized to the existing empty-entrypoints representation after a null-safe route check. ### Verification - `pnpm build-all` - `pnpm --filter=next types` - `pnpm swc-build-native` - `cargo check -p next-napi-bindings` - `cargo fmt --all -- --check` - ESLint and Prettier on changed files - `pnpm test-dev-experimental-turbo test/development/app-aspath/app-aspath.test.ts` - `pnpm test-dev-turbo test/development/basic/next-rs-api.test.ts` — 26 passed, 1 skipped, 15 snapshots; Jest reported lingering open handles after the green summary - `pnpm test-dev-turbo test/development/app-dir/concurrent-install/concurrent-install.test.ts` - `pnpm test-dev-experimental-turbo test/development/app-dir/concurrent-install/concurrent-install.test.ts` <!-- NEXT_JS_LLM --> <!-- fleet 74688340-f99a-462e-8f75-88d12c88000a --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
1 parent d4c9abe commit d5276f0

12 files changed

Lines changed: 161 additions & 147 deletions

File tree

‎crates/next-napi-bindings/src/next_api/endpoint.rs‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,8 @@ pub fn endpoint_server_changed_subscribe(
191191
env: Env,
192192
#[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: &External<ExternalEndpoint>,
193193
issues: bool,
194-
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult) => void")] func: FunctionRef<
195-
TurbopackResult<()>,
196-
(),
197-
>,
194+
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult<undefined>) => void")]
195+
func: FunctionRef<TurbopackResult<()>, ()>,
198196
) -> napi::Result<External<SubscriptionTask>> {
199197
let turbopack_ctx = endpoint.turbopack_ctx().clone();
200198
let endpoint = ****endpoint;
@@ -278,10 +276,8 @@ async fn subscribe_issues_and_diags_operation(
278276
pub fn endpoint_client_changed_subscribe(
279277
env: Env,
280278
#[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: &External<ExternalEndpoint>,
281-
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult) => void")] func: FunctionRef<
282-
TurbopackResult<()>,
283-
(),
284-
>,
279+
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult<undefined>) => void")]
280+
func: FunctionRef<TurbopackResult<()>, ()>,
285281
) -> napi::Result<External<SubscriptionTask>> {
286282
let turbopack_ctx = endpoint.turbopack_ctx().clone();
287283
let endpoint_op = ****endpoint;

‎crates/next-napi-bindings/src/next_api/project.rs‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,7 @@ async fn app_route_filter_for_write_phase(
13421342
}
13431343

13441344
#[tracing::instrument(level = "info", name = "write all entrypoints to disk", skip_all)]
1345-
#[napi(ts_return_type = "Promise<TurbopackResult<Partial<NapiEntrypoints>>>")]
1345+
#[napi(ts_return_type = "Promise<TurbopackResult<Partial<NapiEntrypoints> | null>>")]
13461346
pub async fn project_write_all_entrypoints_to_disk(
13471347
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
13481348
app_dir_only: bool,
@@ -1729,7 +1729,7 @@ async fn output_assets_operation(
17291729
}
17301730

17311731
#[tracing::instrument(level = "info", name = "get entrypoints", skip_all)]
1732-
#[napi(ts_return_type = "Promise<TurbopackResult<Partial<NapiEntrypoints>>>")]
1732+
#[napi(ts_return_type = "Promise<TurbopackResult<Partial<NapiEntrypoints> | null>>")]
17331733
pub async fn project_entrypoints(
17341734
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
17351735
) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
@@ -1770,7 +1770,10 @@ pub async fn project_entrypoints(
17701770
pub fn project_entrypoints_subscribe(
17711771
env: Env,
17721772
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
1773-
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult<Partial<NapiEntrypoints>>) => void")]
1773+
#[napi(
1774+
ts_arg_type = "(err: Error, value: TurbopackResult<Partial<NapiEntrypoints> | null>) => \
1775+
void"
1776+
)]
17741777
func: FunctionRef<TurbopackResult<Option<NapiEntrypoints>>, ()>,
17751778
) -> napi::Result<External<SubscriptionTask>> {
17761779
let turbopack_ctx = project.turbopack_ctx.clone();
@@ -2245,8 +2248,10 @@ pub fn project_update_info_subscribe(
22452248
env: Env,
22462249
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
22472250
aggregation_ms: u32,
2248-
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult<UpdateMessage>) => void")]
2249-
func: FunctionRef<NapiUpdateMessage, ()>,
2251+
#[napi(ts_arg_type = "(err: Error, value: UpdateMessage) => void")] func: FunctionRef<
2252+
NapiUpdateMessage,
2253+
(),
2254+
>,
22502255
) -> napi::Result<()> {
22512256
let func: ThreadsafeFunction<UpdateMessage, (), NapiUpdateMessage, Status, true> = func
22522257
.borrow_back(&env)?
@@ -2322,8 +2327,10 @@ impl From<Arc<dyn CompilationEvent>> for NapiCompilationEvent {
23222327
pub fn project_compilation_events_subscribe(
23232328
env: Env,
23242329
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
2325-
#[napi(ts_arg_type = "(err: Error, value: TurbopackResult<CompilationEvent>) => void")]
2326-
func: FunctionRef<NapiCompilationEvent, ()>,
2330+
#[napi(ts_arg_type = "(err: Error, value: CompilationEvent) => void")] func: FunctionRef<
2331+
NapiCompilationEvent,
2332+
(),
2333+
>,
23272334
event_types: Option<Vec<String>>,
23282335
) -> napi::Result<()> {
23292336
let tsfn: ThreadsafeFunction<

‎crates/next-napi-bindings/src/next_api/utils.rs‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -419,15 +419,9 @@ impl<T: ToNapiValue> ToNapiValue for TurbopackResult<T> {
419419
) -> napi::Result<napi::sys::napi_value> {
420420
let result_raw = unsafe { T::to_napi_value(env, val.result)? };
421421
let result = unsafe { Unknown::from_raw_unchecked(env, result_raw) };
422+
let mut obj = Object::new(&Env::from_raw(env))?;
422423

423-
// When the result is an object, extend it in place with the `issues`
424-
// property. Otherwise, produce a fresh object holding only `issues`.
425-
let mut obj = if matches!(result.get_type()?, napi::ValueType::Object) {
426-
Object::from_raw(env, result_raw)
427-
} else {
428-
Object::new(&Env::from_raw(env))?
429-
};
430-
424+
obj.set_named_property("value", result)?;
431425
obj.set_named_property("issues", val.issues)?;
432426

433427
Ok(obj.raw())

‎packages/next/src/build/print-build-errors.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export function formatWarningsHeader(count: number): string {
1616
* @throws {Error} If a fatal issue is encountered, this function throws an error. In development mode, we only throw on
1717
* 'fatal' and 'bug' issues. In production mode, we also throw on 'error' issues.
1818
*/
19-
export function printBuildErrors(
20-
entrypoints: TurbopackResult,
19+
export function printBuildErrors<T>(
20+
entrypoints: TurbopackResult<T>,
2121
isDev: boolean,
2222
opts?: { deferWarnings?: boolean }
2323
): { warnings: string[] } {

‎packages/next/src/build/swc/generated-native.d.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ export declare function codeFrameColumns(
7979

8080
export declare function endpointClientChangedSubscribe(
8181
endpoint: { __napiType: 'Endpoint' },
82-
func: (err: Error, value: TurbopackResult) => void
82+
func: (err: Error, value: TurbopackResult<undefined>) => void
8383
): { __napiType: 'RootTask' }
8484

8585
export declare function endpointServerChangedSubscribe(
8686
endpoint: { __napiType: 'Endpoint' },
8787
issues: boolean,
88-
func: (err: Error, value: TurbopackResult) => void
88+
func: (err: Error, value: TurbopackResult<undefined>) => void
8989
): { __napiType: 'RootTask' }
9090

9191
export declare function endpointWriteToDisk(endpoint: {
@@ -586,17 +586,20 @@ export declare function projectClientHmrEvents(
586586
/** Subscribes to all compilation events that are not cached like timing and progress information. */
587587
export declare function projectCompilationEventsSubscribe(
588588
project: { __napiType: 'Project' },
589-
func: (err: Error, value: TurbopackResult<CompilationEvent>) => void,
589+
func: (err: Error, value: CompilationEvent) => void,
590590
eventTypes?: Array<string> | undefined | null
591591
): void
592592

593593
export declare function projectEntrypoints(project: {
594594
__napiType: 'Project'
595-
}): Promise<TurbopackResult<Partial<NapiEntrypoints>>>
595+
}): Promise<TurbopackResult<Partial<NapiEntrypoints> | null>>
596596

597597
export declare function projectEntrypointsSubscribe(
598598
project: { __napiType: 'Project' },
599-
func: (err: Error, value: TurbopackResult<Partial<NapiEntrypoints>>) => void
599+
func: (
600+
err: Error,
601+
value: TurbopackResult<Partial<NapiEntrypoints> | null>
602+
) => void
600603
): { __napiType: 'RootTask' }
601604

602605
/**
@@ -698,13 +701,13 @@ export declare function projectUpdate(
698701
export declare function projectUpdateInfoSubscribe(
699702
project: { __napiType: 'Project' },
700703
aggregationMs: number,
701-
func: (err: Error, value: TurbopackResult<UpdateMessage>) => void
704+
func: (err: Error, value: UpdateMessage) => void
702705
): void
703706

704707
export declare function projectWriteAllEntrypointsToDisk(
705708
project: { __napiType: 'Project' },
706709
appDirOnly: boolean
707-
): Promise<TurbopackResult<Partial<NapiEntrypoints>>>
710+
): Promise<TurbopackResult<Partial<NapiEntrypoints> | null>>
708711

709712
export declare function projectWriteAnalyzeData(
710713
project: { __napiType: 'Project' },

‎packages/next/src/build/swc/index.ts‎

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -723,14 +723,15 @@ function bindingToApi(
723723
const napiEndpoints = (await binding.projectWriteAllEntrypointsToDisk(
724724
this._nativeProject,
725725
appDirOnly
726-
)) as TurbopackResult<Partial<NapiEntrypoints>>
726+
)) as TurbopackResult<Partial<NapiEntrypoints> | null>
727727

728-
if ('routes' in napiEndpoints) {
728+
if (napiEndpoints.value && 'routes' in napiEndpoints.value) {
729729
return napiEntrypointsToRawEntrypoints(
730730
napiEndpoints as TurbopackResult<NapiEntrypoints>
731731
)
732732
} else {
733733
return {
734+
value: {},
734735
issues: napiEndpoints.issues,
735736
}
736737
}
@@ -743,19 +744,20 @@ function bindingToApi(
743744
}
744745

745746
entrypointsSubscribe() {
746-
const subscription = subscribe<TurbopackResult<NapiEntrypoints | {}>>(
747-
false,
748-
async (callback) =>
749-
binding.projectEntrypointsSubscribe(this._nativeProject, callback)
747+
const subscription = subscribe<
748+
TurbopackResult<NapiEntrypoints | {} | null>
749+
>(false, async (callback) =>
750+
binding.projectEntrypointsSubscribe(this._nativeProject, callback)
750751
)
751752
return (async function* () {
752753
for await (const entrypoints of subscription) {
753-
if ('routes' in (entrypoints as TurbopackResult<NapiEntrypoints>)) {
754+
if (entrypoints.value && 'routes' in entrypoints.value) {
754755
yield napiEntrypointsToRawEntrypoints(
755756
entrypoints as TurbopackResult<NapiEntrypoints>
756757
)
757758
} else {
758759
yield {
760+
value: {},
759761
issues: entrypoints.issues,
760762
} as TurbopackResult<{}>
761763
}
@@ -819,7 +821,7 @@ function bindingToApi(
819821
}
820822

821823
updateInfoSubscribe(aggregationMs: number) {
822-
return subscribe<TurbopackResult<UpdateMessage>>(true, async (callback) =>
824+
return subscribe<UpdateMessage>(true, async (callback) =>
823825
binding.projectUpdateInfoSubscribe(
824826
this._nativeProject,
825827
aggregationMs,
@@ -829,16 +831,13 @@ function bindingToApi(
829831
}
830832

831833
compilationEventsSubscribe(eventTypes?: string[]) {
832-
return subscribe<TurbopackResult<CompilationEvent>>(
833-
true,
834-
async (callback) => {
835-
binding.projectCompilationEventsSubscribe(
836-
this._nativeProject,
837-
callback,
838-
eventTypes
839-
)
840-
}
841-
)
834+
return subscribe<CompilationEvent>(true, async (callback) => {
835+
binding.projectCompilationEventsSubscribe(
836+
this._nativeProject,
837+
callback,
838+
eventTypes
839+
)
840+
})
842841
}
843842

844843
invalidateFileSystemCache(): Promise<void> {
@@ -867,8 +866,10 @@ function bindingToApi(
867866
)) as TurbopackResult<WrittenEndpoint>
868867
}
869868

870-
async clientChanged(): Promise<AsyncIterableIterator<TurbopackResult>> {
871-
const clientSubscription = subscribe<TurbopackResult>(
869+
async clientChanged(): Promise<
870+
AsyncIterableIterator<TurbopackResult<void>>
871+
> {
872+
const clientSubscription = subscribe<TurbopackResult<void>>(
872873
false,
873874
async (callback) =>
874875
binding.endpointClientChangedSubscribe(this._nativeEndpoint, callback)
@@ -879,8 +880,8 @@ function bindingToApi(
879880

880881
async serverChanged(
881882
includeIssues: boolean
882-
): Promise<AsyncIterableIterator<TurbopackResult>> {
883-
const serverSubscription = subscribe<TurbopackResult>(
883+
): Promise<AsyncIterableIterator<TurbopackResult<void>>> {
884+
const serverSubscription = subscribe<TurbopackResult<void>>(
884885
false,
885886
async (callback) =>
886887
binding.endpointServerChangedSubscribe(
@@ -1186,7 +1187,7 @@ function bindingToApi(
11861187
entrypoints: TurbopackResult<NapiEntrypoints>
11871188
): TurbopackResult<RawEntrypoints> {
11881189
const routes = new Map()
1189-
for (const { pathname, ...nativeRoute } of entrypoints.routes) {
1190+
for (const { pathname, ...nativeRoute } of entrypoints.value.routes) {
11901191
let route: Route
11911192
const routeType = nativeRoute.type
11921193
switch (routeType) {
@@ -1240,28 +1241,32 @@ function bindingToApi(
12401241
endpoint: new EndpointImpl(middleware.endpoint),
12411242
isProxy: middleware.isProxy,
12421243
})
1243-
const middleware = entrypoints.middleware
1244-
? napiMiddlewareToMiddleware(entrypoints.middleware)
1244+
const middleware = entrypoints.value.middleware
1245+
? napiMiddlewareToMiddleware(entrypoints.value.middleware)
12451246
: undefined
12461247
const napiInstrumentationToInstrumentation = (
12471248
instrumentation: NapiInstrumentation
12481249
) => ({
12491250
nodeJs: new EndpointImpl(instrumentation.nodeJs),
12501251
edge: new EndpointImpl(instrumentation.edge),
12511252
})
1252-
const instrumentation = entrypoints.instrumentation
1253-
? napiInstrumentationToInstrumentation(entrypoints.instrumentation)
1253+
const instrumentation = entrypoints.value.instrumentation
1254+
? napiInstrumentationToInstrumentation(entrypoints.value.instrumentation)
12541255
: undefined
12551256

12561257
return {
1257-
routes,
1258-
middleware,
1259-
instrumentation,
1260-
pagesDocumentEndpoint: new EndpointImpl(
1261-
entrypoints.pagesDocumentEndpoint
1262-
),
1263-
pagesAppEndpoint: new EndpointImpl(entrypoints.pagesAppEndpoint),
1264-
pagesErrorEndpoint: new EndpointImpl(entrypoints.pagesErrorEndpoint),
1258+
value: {
1259+
routes,
1260+
middleware,
1261+
instrumentation,
1262+
pagesDocumentEndpoint: new EndpointImpl(
1263+
entrypoints.value.pagesDocumentEndpoint
1264+
),
1265+
pagesAppEndpoint: new EndpointImpl(entrypoints.value.pagesAppEndpoint),
1266+
pagesErrorEndpoint: new EndpointImpl(
1267+
entrypoints.value.pagesErrorEndpoint
1268+
),
1269+
},
12651270
issues: entrypoints.issues,
12661271
}
12671272
}

‎packages/next/src/build/swc/types.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ export interface BuildFeatureUsage {
177177
invocationCount: number
178178
}
179179

180-
export type TurbopackResult<T = {}> = T & {
180+
export type TurbopackResult<T> = {
181+
value: T
181182
issues: Issue[]
182183
}
183184

@@ -367,11 +368,11 @@ export interface Project {
367368

368369
updateInfoSubscribe(
369370
aggregationMs: number
370-
): AsyncIterableIterator<TurbopackResult<UpdateMessage>>
371+
): AsyncIterableIterator<UpdateMessage>
371372

372373
compilationEventsSubscribe(
373374
eventTypes?: string[]
374-
): AsyncIterableIterator<TurbopackResult<CompilationEvent>>
375+
): AsyncIterableIterator<CompilationEvent>
375376

376377
invalidateFileSystemCache(): Promise<void>
377378

@@ -417,7 +418,7 @@ export interface Endpoint {
417418
* After clientChanged() has been awaited it will listen to changes.
418419
* The async iterator will yield for each change.
419420
*/
420-
clientChanged(): Promise<AsyncIterableIterator<TurbopackResult>>
421+
clientChanged(): Promise<AsyncIterableIterator<TurbopackResult<void>>>
421422

422423
/**
423424
* Listen to server-side changes to the endpoint.
@@ -426,7 +427,7 @@ export interface Endpoint {
426427
*/
427428
serverChanged(
428429
includeIssues: boolean
429-
): Promise<AsyncIterableIterator<TurbopackResult>>
430+
): Promise<AsyncIterableIterator<TurbopackResult<void>>>
430431
}
431432

432433
interface EndpointConfig {

0 commit comments

Comments
 (0)