Skip to content
Draft
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
49 changes: 27 additions & 22 deletions crates/next-core/src/next_import_map.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::BTreeMap, sync::LazyLock};
use std::{borrow::Cow, collections::BTreeMap, sync::LazyLock};

use anyhow::{Context, Result};
use async_trait::async_trait;
Expand All @@ -12,7 +12,8 @@ use turbopack_core::{
issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString},
reference_type::{CommonJsReferenceSubType, ReferenceType},
resolve::{
AliasPattern, ExternalTraced, ExternalType, ResolveAliasMap, SubpathValue,
AliasKey, AliasPattern, AliasTemplate, ExternalTraced, ExternalType,
ReplacedSubpathValueResultType, ResolveAliasMap, SubpathValue,
node::node_cjs_resolve_options,
options::{ConditionValue, ImportMap, ImportMapping, ResolvedMap},
parse::Request,
Expand Down Expand Up @@ -1302,31 +1303,35 @@ fn export_value_to_import_mapping(
conditions: &BTreeMap<RcStr, ConditionValue>,
project_path: &FileSystemPath,
) -> Option<ResolvedVc<ImportMapping>> {
let mut result = Vec::new();
value.add_results(
let alias_key = AliasKey::Exact;
let mut results = Vec::new();
value.convert().add_results(
Cow::Borrowed(""),
&alias_key,
conditions,
&ConditionValue::Unset,
&mut FxHashMap::default(),
&mut result,
&mut results,
);
if result.is_empty() {
None
} else {
Some(if result.len() == 1 {
ImportMapping::PrimaryAlternative(result[0].0.into(), Some(project_path.clone()))
.resolved_cell()
} else {
ImportMapping::Alternatives(
result
.iter()
.map(|(m, _)| {
ImportMapping::PrimaryAlternative((*m).into(), Some(project_path.clone()))
.resolved_cell()
})
.collect(),
)
.resolved_cell()

let mappings: Vec<_> = results
.iter()
.filter_map(|r| match &r.ty {
ReplacedSubpathValueResultType::Path(path) => {
let m = path.as_constant_string()?;
Some(
ImportMapping::PrimaryAlternative(m.clone(), Some(project_path.clone()))
.resolved_cell(),
)
}
ReplacedSubpathValueResultType::Empty => Some(ImportMapping::Empty.resolved_cell()),
})
.collect();

match mappings.len() {
0 => None,
1 => mappings.into_iter().next(),
_ => Some(ImportMapping::Alternatives(mappings).resolved_cell()),
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,13 @@ const zTurbopackConfig: zod.ZodType<TurbopackOptions> = z.strictObject({
.record(
z.string(),
z.union([
z.literal(false),
z.string(),
z.array(z.string()),
z.record(z.string(), z.union([z.string(), z.array(z.string())])),
z.record(
z.string(),
z.union([z.literal(false), z.string(), z.array(z.string())])
),
])
)
.optional(),
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export interface TurbopackOptions {
*/
resolveAlias?: Record<
string,
string | string[] | Record<string, string | string[]>
false | string | string[] | Record<string, false | string | string[]>
>

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 'some-lib' is aliased to `false` in next.config.js.
// require() should resolve to `{}`.
const mod = require('some-lib')

export function GET() {
return Response.json({
required: mod,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 'some-lib' is aliased to `false` in next.config.js.
// Dynamic import should resolve to `Promise.resolve({})`.
export async function GET() {
const mod = await import('some-lib')
return Response.json({
dynamicImport: mod,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 'some-lib' is aliased to `false` in next.config.js.
// Namespace import should resolve to `{}`.
import * as namespaceImport from 'some-lib'
// Named import should resolve to `undefined`.
import { someExport as namedImport } from 'some-lib'
// Default import should resolve to `undefined`.
import defaultImport from 'some-lib'

export function GET() {
return Response.json({
namespaceImport,
namedImportIsUndefined: namedImport === undefined,
defaultImportIsUndefined: defaultImport === undefined,
})
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/turbopack-resolve-alias-false/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/turbopack-resolve-alias-false/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/turbopack-resolve-alias-false/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
turbopack: {
resolveAlias: {
// Alias a non-existent module to `false` to resolve it as an empty module.
// This tests that `resolveAlias: false` produces `{}` for namespace/CJS
// imports and `undefined` for named/default imports.
'some-lib': false,
},
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { nextTestSetup } from 'e2e-utils'

// `resolveAlias: false` is a Turbopack-only feature. The `turbopack.resolveAlias`
// config is not read by webpack.
;(process.env.IS_TURBOPACK_TEST ? describe : describe.skip)(
'turbopack-resolve-alias-false',
() => {
const { next } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})

describe('ESM static imports', () => {
it('resolves namespace import (import * as ns) to {}', async () => {
const response = JSON.parse(await next.render('/api/esm'))
expect(response.namespaceImport).toEqual({})
})

it('resolves named import (import { foo }) to undefined', async () => {
const response = JSON.parse(await next.render('/api/esm'))
expect(response.namedImportIsUndefined).toBe(true)
})

it('resolves default import (import def) to undefined', async () => {
const response = JSON.parse(await next.render('/api/esm'))
expect(response.defaultImportIsUndefined).toBe(true)
})
})

describe('dynamic import()', () => {
it('resolves dynamic import to {}', async () => {
const response = JSON.parse(await next.render('/api/dynamic'))
expect(response.dynamicImport).toEqual({})
})
})

describe('CommonJS require()', () => {
it('resolves require() to {}', async () => {
const response = JSON.parse(await next.render('/api/cjs'))
expect(response.required).toEqual({})
})
})
}
)
136 changes: 82 additions & 54 deletions turbopack/crates/turbopack-core/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ use crate::{
raw_module::RawModule,
reference_type::ReferenceType,
resolve::{
alias_map::AliasKey,
error::{handle_resolve_error, resolve_error_severity},
node::{node_cjs_resolve_options, node_esm_resolve_options},
options::{
Expand All @@ -48,7 +47,7 @@ use crate::{
parse::{Request, stringify_data_uri},
pattern::{Pattern, PatternMatch, read_matches},
plugin::{AfterResolvePlugin, AfterResolvePluginCondition, BeforeResolvePlugin},
remap::{ExportsField, ImportsField, ReplacedSubpathValueResult},
remap::{ExportsField, ImportsField},
},
source::{OptionSource, Source, Sources},
};
Expand All @@ -64,9 +63,14 @@ pub mod plugin;
pub(crate) mod remap;

pub use alias_map::{
AliasMap, AliasMapIntoIter, AliasMapLookupIterator, AliasMatch, AliasPattern, AliasTemplate,
AliasKey, AliasMap, AliasMapIntoIter, AliasMapLookupIterator, AliasMatch, AliasPattern,
AliasTemplate,
};
use remap::TerminalState;
pub use remap::{
ReplacedSubpathValue, ReplacedSubpathValueResult, ReplacedSubpathValueResultType,
ResolveAliasMap, SubpathValue,
};
pub use remap::{ResolveAliasMap, SubpathValue};

/// Controls how resolve errors are handled.
#[turbo_tasks::value(shared)]
Expand Down Expand Up @@ -3183,6 +3187,21 @@ async fn resolved(
))
}

/// Attaches `conditions` to a resolve result.
///
/// When `conditions` is empty the original `Vc` is returned as-is to avoid an
/// unnecessary await. Otherwise the result is awaited, annotated, and re-wrapped.
async fn apply_conditions(
resolve_result: Vc<ResolveResult>,
conditions: &[(RcStr, bool)],
) -> Result<Vc<ResolveResult>> {
if conditions.is_empty() {
Ok(resolve_result)
} else {
Ok(resolve_result.await?.with_conditions(conditions).cell())
}
}

async fn handle_exports_imports_field(
package_path: FileSystemPath,
package_json_path: FileSystemPath,
Expand Down Expand Up @@ -3211,70 +3230,79 @@ async fn handle_exports_imports_field(
unspecified_conditions,
&mut conditions_state,
&mut results,
) {
// Match found, stop (leveraging the lazy `lookup` iterator).
) != TerminalState::Unset
{
// A definitive match was found (results added or import blocked);
// stop iterating over further alias entries.
break;
}
}

let mut resolved_results = Vec::new();
for ReplacedSubpathValueResult {
result_path,
ty,
conditions,
map_prefix,
map_key,
} in results
{
if let Some(result_path) = result_path.with_normalized_path() {
let request = *Request::parse(Pattern::Concatenation(vec![
Pattern::Constant(rcstr!("./")),
result_path.clone(),
]))
.to_resolved()
.await?;
match ty {
ReplacedSubpathValueResultType::Path(result_path) => {
if let Some(result_path) = result_path.with_normalized_path() {
let request = *Request::parse(Pattern::Concatenation(vec![
Pattern::Constant(rcstr!("./")),
result_path.clone(),
]))
.to_resolved()
.await?;

let resolve_result = Box::pin(resolve_internal_inline(
package_path.clone(),
request,
options,
))
.await?;
let resolve_result = Box::pin(resolve_internal_inline(
package_path.clone(),
request,
options,
))
.await?;

let resolve_result = if let Some(req) = req.as_constant_string() {
resolve_result.with_request(req.clone())
} else {
match map_key {
AliasKey::Exact => resolve_result.with_request(map_prefix.clone().into()),
AliasKey::Wildcard { .. } => {
// - `req` is the user's request (key of the export map)
// - `result_path` is the final request (value of the export map), so
// effectively `'{foo}*{bar}'`

// Because of the assertion in AliasMapLookupIterator, `req` is of the
// form:
// - "prefix...<dynamic>" or
// - "prefix...<dynamic>...suffix"

let mut old_request_key = result_path;
// Remove the Pattern::Constant(rcstr!("./")), from above again
old_request_key.push_front(rcstr!("./").into());
let new_request_key = req.clone();

resolve_result.with_replaced_request_key_pattern(
Pattern::new(old_request_key),
Pattern::new(new_request_key),
)
}
}
};
let resolve_result = if let Some(req) = req.as_constant_string() {
resolve_result.with_request(req.clone())
} else {
match map_key {
AliasKey::Exact => {
resolve_result.with_request(map_prefix.clone().into())
}
AliasKey::Wildcard { .. } => {
// - `req` is the user's request (key of the export map)
// - `result_path` is the final request (value of the export map),
// so effectively `'{foo}*{bar}'`

// Because of the assertion in AliasMapLookupIterator, `req` is of
// the form:
// - "prefix...<dynamic>" or
// - "prefix...<dynamic>...suffix"

let mut old_request_key = result_path;
// Remove the Pattern::Constant(rcstr!("./")), from above again
old_request_key.push_front(rcstr!("./").into());
let new_request_key = req.clone();

resolve_result.with_replaced_request_key_pattern(
Pattern::new(old_request_key),
Pattern::new(new_request_key),
)
}
}
};

let resolve_result = if !conditions.is_empty() {
let resolve_result = resolve_result.await?.with_conditions(&conditions);
resolve_result.cell()
} else {
resolve_result
};
resolved_results.push(resolve_result);
let resolve_result = apply_conditions(resolve_result, &conditions).await?;
resolved_results.push(resolve_result);
}
}
ReplacedSubpathValueResultType::Empty => {
// `false` in the exports/imports field: resolve to an empty module.
let resolve_result = ResolveResult::primary(ResolveResultItem::Empty).cell();
let resolve_result = apply_conditions(resolve_result, &conditions).await?;
resolved_results.push(resolve_result);
}
}
}

Expand Down
Loading
Loading