diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 1e56404a86ad5..10889cd39d3b8 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -733,7 +733,8 @@ pub fn project_hmr_events( async move { let project = project.project().resolve().await?; let state = project.hmr_version_state(identifier.clone(), session); - let update = hmr_update(project, identifier, state) + + let update = hmr_update(project, identifier.clone(), state) .strongly_consistent() .await .inspect_err(|e| log_panic_and_inform(e))?; @@ -743,7 +744,7 @@ pub fn project_hmr_events( diagnostics, } = &*update; match &**update { - Update::None => {} + Update::Missing | Update::None => {} Update::Total(TotalUpdate { to }) => { state.set(to.clone()).await?; } @@ -751,7 +752,7 @@ pub fn project_hmr_events( state.set(to.clone()).await?; } } - Ok((update.clone(), issues.clone(), diagnostics.clone())) + Ok((Some(update.clone()), issues.clone(), diagnostics.clone())) } .instrument(tracing::info_span!( "HMR subscription", @@ -775,14 +776,16 @@ pub fn project_hmr_events( path: identifier.clone(), headers: None, }; - let update = match &*update { - Update::Total(_) => ClientUpdateInstruction::restart(&identifier, &update_issues), - Update::Partial(update) => ClientUpdateInstruction::partial( + let update = match update.as_deref() { + None | Some(Update::Missing) | Some(Update::Total(_)) => { + ClientUpdateInstruction::restart(&identifier, &update_issues) + } + Some(Update::Partial(update)) => ClientUpdateInstruction::partial( &identifier, &update.instruction, &update_issues, ), - Update::None => ClientUpdateInstruction::issues(&identifier, &update_issues), + Some(Update::None) => ClientUpdateInstruction::issues(&identifier, &update_issues), }; Ok(vec![TurbopackResult { @@ -1030,18 +1033,22 @@ pub async fn project_trace_source( .client_relative_path() .join(chunk_base.into()); - let mut map_result = project + let mut map = project .container .get_source_map(server_path, module.clone()) - .await; - if map_result.is_err() { + .await?; + + if map.is_none() { // If the chunk doesn't exist as a server chunk, try a client chunk. // TODO: Properly tag all server chunks and use the `isServer` query param. // Currently, this is inaccurate as it does not cover RSC server // chunks. - map_result = project.container.get_source_map(client_path, module).await; + map = project + .container + .get_source_map(client_path, module) + .await?; } - let map = map_result?.context("chunk/module is missing a sourcemap")?; + let map = map.context("chunk/module is missing a sourcemap")?; let Some(line) = frame.line else { return Ok(None); diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 06aec98beb5cb..65e07fe2026cf 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -49,7 +49,9 @@ use turbopack_core::{ output::{OutputAsset, OutputAssets}, resolve::{find_context_file, FindContextFileResult}, source_map::OptionSourceMap, - version::{Update, Version, VersionState, VersionedContent}, + version::{ + NotFoundVersion, OptionVersionedContent, Update, Version, VersionState, VersionedContent, + }, PROJECT_FILESYSTEM_NAME, }; use turbopack_node::execution_context::ExecutionContext; @@ -1155,12 +1157,10 @@ impl Project { } #[turbo_tasks::function] - async fn hmr_content( - self: Vc, - identifier: RcStr, - ) -> Result>> { + async fn hmr_content(self: Vc, identifier: RcStr) -> Result> { if let Some(map) = self.await?.versioned_content_map { - Ok(map.get(self.client_relative_path().join(identifier))) + let content = map.get(self.client_relative_path().join(identifier.clone())); + Ok(content) } else { bail!("must be in dev mode to hmr") } @@ -1168,9 +1168,12 @@ impl Project { #[turbo_tasks::function] async fn hmr_version(self: Vc, identifier: RcStr) -> Result>> { - let content = self.hmr_content(identifier); - - Ok(content.version()) + let content = self.hmr_content(identifier).await?; + if let Some(content) = &*content { + Ok(content.version()) + } else { + Ok(Vc::upcast(NotFoundVersion::new())) + } } /// Get the version state for a session. Initialized with the first seen @@ -1190,12 +1193,13 @@ impl Project { // INVALIDATION: This is intentionally untracked to avoid invalidating this // function completely. We want to initialize the VersionState with the // first seen version of the session. - VersionState::new( + let state = VersionState::new( version .into_trait_ref_strongly_consistent_untracked() .await?, ) - .await + .await?; + Ok(state) } /// Emits opaque HMR events whenever a change is detected in the chunk group @@ -1207,7 +1211,12 @@ impl Project { from: Vc, ) -> Result> { let from = from.get(); - Ok(self.hmr_content(identifier).update(from)) + let content = self.hmr_content(identifier).await?; + if let Some(content) = *content { + Ok(content.update(from)) + } else { + Ok(Update::Missing.cell()) + } } /// Gets a list of all HMR identifiers that can be subscribed to. This is diff --git a/crates/next-api/src/versioned_content_map.rs b/crates/next-api/src/versioned_content_map.rs index 07cc3831c1061..a16ed6fd13bb0 100644 --- a/crates/next-api/src/versioned_content_map.rs +++ b/crates/next-api/src/versioned_content_map.rs @@ -11,9 +11,9 @@ use turbo_tasks::{ use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ asset::Asset, - output::{OutputAsset, OutputAssets}, + output::{OptionOutputAsset, OutputAsset, OutputAssets}, source_map::{GenerateSourceMap, OptionSourceMap}, - version::VersionedContent, + version::OptionVersionedContent, }; /// An unresolved output assets operation. We need to pass an operation here as @@ -26,6 +26,7 @@ pub struct OutputAssetsOperation(Vc); struct MapEntry { assets_operation: Vc, side_effects: Vc, + /// Precomputed map for quick access to output asset by filepath path_to_asset: HashMap, Vc>>, } @@ -33,6 +34,7 @@ struct MapEntry { struct OptionMapEntry(Option); type PathToOutputOperation = HashMap, IndexSet>>; +// A precomputed map for quick access to output asset by filepath type OutputOperationToComputeEntry = HashMap, Vc>; #[turbo_tasks::value] @@ -67,6 +69,7 @@ impl VersionedContentMap { #[turbo_tasks::function] pub async fn insert_output_assets( self: Vc, + // Output assets to emit assets_operation: Vc, node_root: Vc, client_relative_path: Vc, @@ -88,6 +91,8 @@ impl VersionedContentMap { Ok(entry.side_effects) } + /// Creates a ComputEntry (a pre-computed map for optimized lookup) for an output assets + /// operation. When assets change, map_path_to_op is updated. #[turbo_tasks::function] async fn compute_entry( self: Vc, @@ -148,8 +153,13 @@ impl VersionedContentMap { } #[turbo_tasks::function] - pub fn get(self: Vc, path: Vc) -> Vc> { - self.get_asset(path).versioned_content() + pub async fn get( + self: Vc, + path: Vc, + ) -> Result> { + Ok(Vc::cell( + (*self.get_asset(path).await?).map(|a| a.versioned_content()), + )) } #[turbo_tasks::function] @@ -158,8 +168,12 @@ impl VersionedContentMap { path: Vc, section: Option, ) -> Result> { + let Some(asset) = &*self.get_asset(path).await? else { + return Ok(Vc::cell(None)); + }; + if let Some(generate_source_map) = - Vc::try_resolve_sidecast::>(self.get_asset(path)).await? + Vc::try_resolve_sidecast::>(*asset).await? { Ok(if let Some(section) = section { generate_source_map.by_section(section) @@ -176,7 +190,7 @@ impl VersionedContentMap { pub async fn get_asset( self: Vc, path: Vc, - ) -> Result>> { + ) -> Result> { let result = self.raw_get(path).await?; if let Some(MapEntry { assets_operation: _, @@ -187,17 +201,17 @@ impl VersionedContentMap { side_effects.await?; if let Some(asset) = path_to_asset.get(&path) { - return Ok(*asset); + return Ok(Vc::cell(Some(*asset))); } else { let path = path.to_string().await?; bail!( "could not find asset for path {} (asset has been removed)", - path + path, ); } } - let path = path.to_string().await?; - bail!("could not find asset for path {}", path); + + Ok(Vc::cell(None)) } #[turbo_tasks::function] diff --git a/crates/next-build-test/src/lib.rs b/crates/next-build-test/src/lib.rs index 686857487391f..95652e1291928 100644 --- a/crates/next-build-test/src/lib.rs +++ b/crates/next-build-test/src/lib.rs @@ -257,12 +257,8 @@ async fn hmr(tt: &TurboTasks, project: Vc) -> R let session = session.clone(); async move { let project = project.project(); - project - .hmr_update( - ident.clone(), - project.hmr_version_state(ident.clone(), session), - ) - .await?; + let state = project.hmr_version_state(ident.clone(), session); + project.hmr_update(ident.clone(), state).await?; Ok(Vc::<()>::cell(())) } }); diff --git a/crates/next-core/src/next_server/transforms.rs b/crates/next-core/src/next_server/transforms.rs index 48e903979d2e0..0e0ae9d671ec9 100644 --- a/crates/next-core/src/next_server/transforms.rs +++ b/crates/next-core/src/next_server/transforms.rs @@ -123,14 +123,17 @@ pub async fn get_next_server_transforms_rules( // optimize_use_state)) rules.push(get_next_image_rule()); + } - if let NextRuntime::Edge = next_runtime { - rules.push(get_middleware_dynamic_assert_rule(mdx_rs)); + if let NextRuntime::Edge = next_runtime { + rules.push(get_middleware_dynamic_assert_rule(mdx_rs)); + if !foreign_code { rules.push(next_edge_node_api_assert( mdx_rs, matches!(context_ty, ServerContextType::Middleware { .. }) && matches!(*mode.await?, NextMode::Build), + matches!(*mode.await?, NextMode::Build), )); } } diff --git a/crates/next-core/src/next_shared/transforms/next_edge_node_api_assert.rs b/crates/next-core/src/next_shared/transforms/next_edge_node_api_assert.rs index e6936d8b516a9..99d4040b451e5 100644 --- a/crates/next-core/src/next_shared/transforms/next_edge_node_api_assert.rs +++ b/crates/next-core/src/next_shared/transforms/next_edge_node_api_assert.rs @@ -14,9 +14,11 @@ use super::module_rule_match_js_no_url; pub fn next_edge_node_api_assert( enable_mdx_rs: bool, should_error_for_node_apis: bool, + is_production: bool, ) -> ModuleRule { let transformer = EcmascriptInputTransform::Plugin(Vc::cell(Box::new(NextEdgeNodeApiAssert { should_error_for_node_apis, + is_production, }) as _)); ModuleRule::new( module_rule_match_js_no_url(enable_mdx_rs), @@ -30,6 +32,7 @@ pub fn next_edge_node_api_assert( #[derive(Debug)] struct NextEdgeNodeApiAssert { should_error_for_node_apis: bool, + is_production: bool, } #[async_trait] @@ -43,6 +46,7 @@ impl CustomTransformer for NextEdgeNodeApiAssert { unresolved_ctxt: SyntaxContext::empty().apply_mark(ctx.unresolved_mark), }, self.should_error_for_node_apis, + self.is_production, ); program.visit_with(&mut visitor); Ok(()) diff --git a/crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs b/crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs index 0e26ad21f2a8c..558597045444d 100644 --- a/crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs +++ b/crates/next-custom-transforms/src/transforms/warn_for_edge_runtime.rs @@ -18,6 +18,7 @@ pub fn warn_for_edge_runtime( cm: Arc, ctx: ExprCtx, should_error_for_node_apis: bool, + is_production: bool, ) -> impl Visit { WarnForEdgeRuntime { cm, @@ -26,6 +27,7 @@ pub fn warn_for_edge_runtime( should_add_guards: false, guarded_symbols: Default::default(), guarded_process_props: Default::default(), + is_production, } } @@ -39,6 +41,7 @@ struct WarnForEdgeRuntime { /// `if(typeof clearImmediate !== "function") clearImmediate();` guarded_symbols: FxHashSet, guarded_process_props: FxHashSet, + is_production: bool, } const EDGE_UNSUPPORTED_NODE_APIS: &[&str] = &[ @@ -232,6 +235,18 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime", _ => (), } } + + fn emit_dynamic_not_allowed_error(&self, span: Span) { + if self.is_production { + let msg = "Dynamic Code Evaluation (e. g. 'eval', 'new Function', \ + 'WebAssembly.compile') not allowed in Edge Runtime" + .to_string(); + + HANDLER.with(|h| { + h.struct_span_err(span, &msg).emit(); + }); + } + } } impl Visit for WarnForEdgeRuntime { @@ -266,6 +281,11 @@ impl Visit for WarnForEdgeRuntime { fn visit_expr(&mut self, n: &Expr) { if let Expr::Ident(ident) = n { if ident.ctxt == self.ctx.unresolved_ctxt { + if ident.sym == "eval" { + self.emit_dynamic_not_allowed_error(ident.span); + return; + } + for api in EDGE_UNSUPPORTED_NODE_APIS { if self.is_in_middleware_layer() && ident.sym == *api { self.emit_unsupported_api_error(ident.span, api); diff --git a/crates/next-custom-transforms/tests/fixture.rs b/crates/next-custom-transforms/tests/fixture.rs index 029b542f45a1d..4f4e24580c5ca 100644 --- a/crates/next-custom-transforms/tests/fixture.rs +++ b/crates/next-custom-transforms/tests/fixture.rs @@ -694,6 +694,7 @@ fn test_edge_assert(input: PathBuf) { is_unresolved_ref_safe: false, unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark), }, + true, true )) ) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 2566ca8b079fd..3ef3e0a1b596b 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1542,7 +1542,9 @@ function runTests({ dev }) { '/d/[id]': 'pages/d/[id].html', '/dash/[hello-world]': 'pages/dash/[hello-world].html', '/': 'pages/index.html', - '/index/[...slug]': 'pages/index/[...slug].html', + '/index/[...slug]': process.env.TURBOPACK + ? 'pages/index/index/[...slug].html' + : 'pages/index/[...slug].html', '/on-mount/[post]': 'pages/on-mount/[post].html', '/p1/p2/all-ssg/[...rest]': 'pages/p1/p2/all-ssg/[...rest].js', '/p1/p2/all-ssr/[...rest]': 'pages/p1/p2/all-ssr/[...rest].js', diff --git a/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js index 9fbd1a1a163cf..6a50a2c41a716 100644 --- a/test/integration/edge-runtime-configurable-guards/test/index.test.js +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -111,13 +111,18 @@ describe('Edge runtime configurable guards', () => { stderr: true, env: { NEXT_TELEMETRY_DEBUG: 1 }, }) - expect(output.stderr).toContain(`Build failed`) - expect(output.stderr).toContain(`./pages/api/route.js`) + + expect(output.code).toBe(1) + if (!process.env.TURBOPACK) { + expect(output.stderr).toContain(`./pages/api/route.js`) + } expect(output.stderr).toContain( `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` ) - expect(output.stderr).toContain(`Used by default`) - expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + if (!process.env.TURBOPACK) { + expect(output.stderr).toContain(`Used by default`) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + } }) } ) diff --git a/test/integration/edge-runtime-dynamic-code/test/index.test.js b/test/integration/edge-runtime-dynamic-code/test/index.test.js index a93b4b59999da..a434ae1027862 100644 --- a/test/integration/edge-runtime-dynamic-code/test/index.test.js +++ b/test/integration/edge-runtime-dynamic-code/test/index.test.js @@ -174,13 +174,18 @@ describe.each([ }) it('should have middleware warning during build', () => { - expect(buildResult.stderr).toContain(`Failed to compile`) - expect(buildResult.stderr).toContain( - `Used by usingEval, usingEvalSync` - ) - expect(buildResult.stderr).toContain( - `Used by usingWebAssemblyCompile` - ) + if (process.env.TURBOPACK) { + expect(buildResult.stderr).toContain(`Ecmascript file had an error`) + } else { + expect(buildResult.stderr).toContain(`Failed to compile`) + expect(buildResult.stderr).toContain( + `Used by usingEval, usingEvalSync` + ) + expect(buildResult.stderr).toContain( + `Used by usingWebAssemblyCompile` + ) + } + expect(buildResult.stderr).toContain(DYNAMIC_CODE_ERROR) }) } diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index 3aeb5a7c66993..2c146c053fa41 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -8720,11 +8720,10 @@ "Dynamic Routing production mode should serve file with space from static folder", "Dynamic Routing production mode should support long URLs for dynamic routes", "Dynamic Routing production mode should update dynamic values on mount", - "Dynamic Routing production mode should update with a hash in the URL" - ], - "failed": [ + "Dynamic Routing production mode should update with a hash in the URL", "Dynamic Routing production mode should output a pages-manifest correctly" ], + "failed": [], "pending": [ "Dynamic Routing development mode [catch all] should not decode slashes (end)", "Dynamic Routing development mode [catch all] should not decode slashes (middle)", @@ -8879,11 +8878,10 @@ "Dynamic Routing production mode should serve file with space from static folder", "Dynamic Routing production mode should support long URLs for dynamic routes", "Dynamic Routing production mode should update dynamic values on mount", - "Dynamic Routing production mode should update with a hash in the URL" - ], - "failed": [ + "Dynamic Routing production mode should update with a hash in the URL", "Dynamic Routing production mode should output a pages-manifest correctly" ], + "failed": [], "pending": [ "Dynamic Routing development mode [catch all] should not decode slashes (end)", "Dynamic Routing development mode [catch all] should not decode slashes (middle)", @@ -8977,7 +8975,8 @@ "Edge runtime configurable guards Middleware with use of Function as a type does not warn in dev at runtime", "Edge runtime configurable guards Middleware with use of Function as a type production mode build and does not warn at runtime", "Edge runtime configurable guards Multiple functions with different configurations warns in dev for allowed code", - "Edge runtime configurable guards Multiple functions with different configurations warns in dev for unallowed code" + "Edge runtime configurable guards Multiple functions with different configurations warns in dev for unallowed code", + "Edge runtime configurable guards Multiple functions with different configurations production mode fails to build because of unallowed code" ], "failed": [ "Edge runtime configurable guards Edge API using lib with allowed, unused dynamic code production mode build and does not warn at runtime", @@ -8985,8 +8984,7 @@ "Edge runtime configurable guards Edge API with allowed, unused dynamic code production mode build and does not warn at runtime", "Edge runtime configurable guards Middleware using lib with allowed, unused dynamic code production mode build and does not warn at runtime", "Edge runtime configurable guards Middleware using lib with unallowed, used dynamic code production mode fails to build because of dynamic code evaluation", - "Edge runtime configurable guards Middleware with allowed, unused dynamic code production mode build and does not warn at runtime", - "Edge runtime configurable guards Multiple functions with different configurations production mode fails to build because of unallowed code" + "Edge runtime configurable guards Middleware with allowed, unused dynamic code production mode build and does not warn at runtime" ], "pending": [], "flakey": [], diff --git a/turbopack/crates/turbopack-browser/src/ecmascript/list/update.rs b/turbopack/crates/turbopack-browser/src/ecmascript/list/update.rs index 87284f950c755..1ba6f0332125b 100644 --- a/turbopack/crates/turbopack-browser/src/ecmascript/list/update.rs +++ b/turbopack/crates/turbopack-browser/src/ecmascript/list/update.rs @@ -122,7 +122,7 @@ pub(super) async fn update_chunk_list( }, ); } - Update::None => {} + Update::Missing | Update::None => {} } } else { chunks.insert(chunk_path.as_ref(), ChunkUpdate::Deleted); @@ -156,7 +156,7 @@ pub(super) async fn update_chunk_list( Update::Partial(partial) => { merged.push(partial.instruction.clone()); } - Update::None => {} + Update::Missing | Update::None => {} } } } diff --git a/turbopack/crates/turbopack-core/src/output.rs b/turbopack/crates/turbopack-core/src/output.rs index 3f706d5223430..020acad64e670 100644 --- a/turbopack/crates/turbopack-core/src/output.rs +++ b/turbopack/crates/turbopack-core/src/output.rs @@ -4,6 +4,9 @@ use turbo_tasks::Vc; use crate::{asset::Asset, ident::AssetIdent}; +#[turbo_tasks::value(transparent)] +pub struct OptionOutputAsset(Option>>); + /// An asset that should be outputted, e. g. written to disk or served from a /// server. #[turbo_tasks::value_trait] diff --git a/turbopack/crates/turbopack-core/src/version.rs b/turbopack/crates/turbopack-core/src/version.rs index 08fb9f6ec3572..fb624844bc2f9 100644 --- a/turbopack/crates/turbopack-core/src/version.rs +++ b/turbopack/crates/turbopack-core/src/version.rs @@ -10,6 +10,9 @@ use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64}; use crate::asset::AssetContent; +#[turbo_tasks::value(transparent)] +pub struct OptionVersionedContent(Option>>); + /// The content of an [Asset] alongside its version. #[turbo_tasks::value_trait] pub trait VersionedContent { @@ -176,6 +179,9 @@ pub enum Update { /// specific set of instructions. Partial(PartialUpdate), + // The asset is now missing, so it can't be updated. A full reload is required. + Missing, + /// No update required. None, } diff --git a/turbopack/crates/turbopack-dev-server/src/update/server.rs b/turbopack/crates/turbopack-dev-server/src/update/server.rs index 04e41444ef0e9..4211fa521ec26 100644 --- a/turbopack/crates/turbopack-dev-server/src/update/server.rs +++ b/turbopack/crates/turbopack-dev-server/src/update/server.rs @@ -143,7 +143,7 @@ impl UpdateServer

{ )) .await?; } - Update::Total(_total) => { + Update::Missing | Update::Total(_) => { client .send(ClientUpdateInstruction::restart(&resource, &issues)) .await?; diff --git a/turbopack/crates/turbopack-dev-server/src/update/stream.rs b/turbopack/crates/turbopack-dev-server/src/update/stream.rs index a287e7ed7c1f1..e62d2c88e5c0b 100644 --- a/turbopack/crates/turbopack-dev-server/src/update/stream.rs +++ b/turbopack/crates/turbopack-dev-server/src/update/stream.rs @@ -234,7 +234,7 @@ impl UpdateStream { Some(item) } // Do not propagate empty updates. - Update::None => { + Update::None | Update::Missing => { if has_issues || issues_changed { Some(item) } else {