diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index bcb4fc80df96c..c24de7b509694 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -7,7 +7,7 @@ use next_core::{ next_edge::entry::wrap_edge_entry, next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2, Regions}, next_server::{get_server_runtime_entries, ServerContextType}, - util::{parse_config_from_source, MiddlewareMatcherKind}, + util::{parse_config_from_source, MiddlewareMatcherKind, NextRuntime}, }; use tracing::Instrument; use turbo_rcstr::RcStr; @@ -16,12 +16,13 @@ use turbo_tasks_fs::{self, File, FileContent, FileSystemPath}; use turbopack_core::{ asset::AssetContent, chunk::{ - availability_info::AvailabilityInfo, ChunkGroupType, ChunkingContextExt, EvaluatableAsset, + availability_info::AvailabilityInfo, ChunkGroupType, ChunkingContext, ChunkingContextExt, + EntryChunkGroupResult, EvaluatableAsset, }, context::AssetContext, module::Module, module_graph::GraphEntries, - output::OutputAssets, + output::{OutputAsset, OutputAssets}, reference_type::{EntryReferenceSubType, ReferenceType}, source::Source, virtual_output::VirtualOutputAsset, @@ -29,6 +30,7 @@ use turbopack_core::{ use turbopack_ecmascript::chunk::EcmascriptChunkPlaceable; use crate::{ + nft_json::NftJsonAsset, paths::{ all_paths_in_root, all_server_paths, get_asset_paths_from_root, get_js_paths_from_root, get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, @@ -40,6 +42,7 @@ use crate::{ #[turbo_tasks::value] pub struct MiddlewareEndpoint { project: ResolvedVc, + build_id: RcStr, asset_context: ResolvedVc>, source: ResolvedVc>, app_dir: Option>, @@ -51,6 +54,7 @@ impl MiddlewareEndpoint { #[turbo_tasks::function] pub fn new( project: ResolvedVc, + build_id: RcStr, asset_context: ResolvedVc>, source: ResolvedVc>, app_dir: Option>, @@ -58,6 +62,7 @@ impl MiddlewareEndpoint { ) -> Vc { Self { project, + build_id, asset_context, source, app_dir, @@ -67,7 +72,7 @@ impl MiddlewareEndpoint { } #[turbo_tasks::function] - async fn entry_module(&self) -> Vc> { + async fn entry_module(&self) -> Result>> { let userland_module = self .asset_context .process( @@ -82,12 +87,17 @@ impl MiddlewareEndpoint { userland_module, ); - wrap_edge_entry( + let config = parse_config_from_source(userland_module, NextRuntime::Edge).await?; + + if matches!(config.runtime, NextRuntime::NodeJs) { + return Ok(module); + } + Ok(wrap_edge_entry( *self.asset_context, self.project.project_path(), module, "middleware".into(), - ) + )) } #[turbo_tasks::function] @@ -134,42 +144,48 @@ impl MiddlewareEndpoint { } #[turbo_tasks::function] - async fn output_assets(self: Vc) -> Result> { + async fn node_chunk(self: Vc) -> Result>> { let this = self.await?; - let userland_module = self.userland_module(); - - let config = parse_config_from_source(userland_module); + let chunking_context = this.project.server_chunking_context(false); - let edge_files = self.edge_files(); - let mut output_assets = edge_files.owned().await?; - - let node_root = this.project.node_root(); - let node_root_value = node_root.await?; - - let file_paths_from_root = get_js_paths_from_root(&node_root_value, &output_assets).await?; + let userland_module = self.entry_module().to_resolved().await?; + let module_graph = this + .project + .module_graph(*userland_module, ChunkGroupType::Entry); - let all_output_assets = all_assets_from_entries(edge_files).await?; + let Some(module) = ResolvedVc::try_downcast(userland_module) else { + bail!("Entry module must be evaluatable"); + }; - let wasm_paths_from_root = - get_wasm_paths_from_root(&node_root_value, &all_output_assets).await?; + let EntryChunkGroupResult { asset: chunk, .. } = *chunking_context + .entry_chunk_group( + this.project.node_root().join("server/middleware.js".into()), + *module, + get_server_runtime_entries( + Value::new(ServerContextType::Middleware { + app_dir: this.app_dir, + ecmascript_client_reference_transition_name: this + .ecmascript_client_reference_transition_name, + }), + this.project.next_mode(), + ) + .resolve_entries(*this.asset_context), + module_graph, + OutputAssets::empty(), + Value::new(AvailabilityInfo::Root), + ) + .await?; + Ok(*chunk) + } - let all_assets = get_asset_paths_from_root(&node_root_value, &all_output_assets).await?; + #[turbo_tasks::function] + async fn output_assets(self: Vc) -> Result> { + let this = self.await?; - // Awaited later for parallelism - let config = config.await?; + let userland_module = self.userland_module(); - let regions = if let Some(regions) = config.regions.as_ref() { - if regions.len() == 1 { - regions - .first() - .map(|region| Regions::Single(region.clone())) - } else { - Some(Regions::Multiple(regions.clone())) - } - } else { - None - }; + let config = parse_config_from_source(userland_module, NextRuntime::Edge).await?; let next_config = this.project.next_config().await?; let has_i18n = next_config.i18n.is_some(); @@ -220,8 +236,9 @@ impl MiddlewareEndpoint { source.insert_str(0, base_path); } - // TODO: The implementation of getMiddlewareMatchers outputs a regex here using - // path-to-regexp. Currently there is no equivalent of that so it post-processes + // TODO: The implementation of getMiddlewareMatchers outputs a regex here + // using path-to-regexp. Currently there is no + // equivalent of that so it post-processes // this value to the relevant regex in manifest-loader.ts matcher.regexp = Some(RcStr::from(source)); @@ -236,36 +253,97 @@ impl MiddlewareEndpoint { }] }; - let edge_function_definition = EdgeFunctionDefinition { - files: file_paths_from_root, - wasm: wasm_paths_to_bindings(wasm_paths_from_root), - assets: paths_to_bindings(all_assets), - name: "middleware".into(), - page: "/".into(), - regions, - matchers, - env: this.project.edge_env().owned().await?, - }; - let middleware_manifest_v2 = MiddlewaresManifestV2 { - middleware: [("/".into(), edge_function_definition)] - .into_iter() - .collect(), - ..Default::default() - }; - let middleware_manifest_v2 = VirtualOutputAsset::new( - node_root.join("server/middleware/middleware-manifest.json".into()), - AssetContent::file( - FileContent::Content(File::from(serde_json::to_string_pretty( - &middleware_manifest_v2, - )?)) - .cell(), - ), - ) - .to_resolved() - .await?; - output_assets.push(ResolvedVc::upcast(middleware_manifest_v2)); + if matches!(config.runtime, NextRuntime::NodeJs) { + let chunk = self.node_chunk().to_resolved().await?; + let mut output_assets = vec![chunk]; + if this.project.next_mode().await?.is_production() { + output_assets.push(ResolvedVc::upcast( + NftJsonAsset::new(*this.project, *chunk, vec![]) + .to_resolved() + .await?, + )); + } + let middleware_manifest_v2 = MiddlewaresManifestV2 { + middleware: [].into_iter().collect(), + ..Default::default() + }; + let middleware_manifest_v2 = VirtualOutputAsset::new( + this.project + .node_root() + .join("server/middleware/middleware-manifest.json".into()), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &middleware_manifest_v2, + )?)) + .cell(), + ), + ) + .to_resolved() + .await?; + output_assets.push(ResolvedVc::upcast(middleware_manifest_v2)); + + Ok(Vc::cell(output_assets)) + } else { + let edge_files = self.edge_files(); + let mut output_assets = edge_files.owned().await?; + + let node_root = this.project.node_root(); + let node_root_value = node_root.await?; + + let file_paths_from_root = + get_js_paths_from_root(&node_root_value, &output_assets).await?; + + let all_output_assets = all_assets_from_entries(edge_files).await?; + + let wasm_paths_from_root = + get_wasm_paths_from_root(&node_root_value, &all_output_assets).await?; - Ok(Vc::cell(output_assets)) + let all_assets = + get_asset_paths_from_root(&node_root_value, &all_output_assets).await?; + + let regions = if let Some(regions) = config.regions.as_ref() { + if regions.len() == 1 { + regions + .first() + .map(|region| Regions::Single(region.clone())) + } else { + Some(Regions::Multiple(regions.clone())) + } + } else { + None + }; + + let edge_function_definition = EdgeFunctionDefinition { + files: file_paths_from_root, + wasm: wasm_paths_to_bindings(wasm_paths_from_root), + assets: paths_to_bindings(all_assets), + name: "middleware".into(), + page: "/".into(), + regions, + matchers: matchers.clone(), + env: this.project.edge_env().owned().await?, + }; + let middleware_manifest_v2 = MiddlewaresManifestV2 { + middleware: [("/".into(), edge_function_definition)] + .into_iter() + .collect(), + ..Default::default() + }; + let middleware_manifest_v2 = VirtualOutputAsset::new( + node_root.join("server/middleware/middleware-manifest.json".into()), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &middleware_manifest_v2, + )?)) + .cell(), + ), + ) + .to_resolved() + .await?; + output_assets.push(ResolvedVc::upcast(middleware_manifest_v2)); + + Ok(Vc::cell(output_assets)) + } } #[turbo_tasks::function] diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index 1960209c98759..3a7060e97bab4 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -843,7 +843,7 @@ impl PageEndpoint { .process(self.source(), reference_type.clone()) .module(); - let config = parse_config_from_source(ssr_module).await?; + let config = parse_config_from_source(ssr_module, NextRuntime::default()).await?; let is_edge = matches!(config.runtime, NextRuntime::Edge); let ssr_module = if is_edge { diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 94d0e23bef200..12c82ca5bbf58 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -18,7 +18,7 @@ use next_core::{ get_server_resolve_options_context, ServerContextType, }, next_telemetry::NextFeatureTelemetry, - util::NextRuntime, + util::{parse_config_from_source, NextRuntime}, }; use serde::{Deserialize, Serialize}; use tracing::Instrument; @@ -55,6 +55,7 @@ use turbopack_core::{ module::Module, module_graph::{GraphEntries, ModuleGraph, SingleModuleGraph, VisitedModules}, output::{OutputAsset, OutputAssets}, + reference_type::{EntryReferenceSubType, ReferenceType}, resolve::{find_context_file, FindContextFileResult}, source_map::OptionStringifiedSourceMap, version::{ @@ -1243,9 +1244,9 @@ impl Project { )); } - Ok(Vc::upcast(ModuleAssetContext::new( + let edge_module_context = ModuleAssetContext::new( TransitionOptions { - named_transitions: transitions.into_iter().collect(), + named_transitions: transitions.clone().into_iter().collect(), ..Default::default() } .cell(), @@ -1273,7 +1274,60 @@ impl Project { self.execution_context(), ), Vc::cell("middleware".into()), - ))) + ); + + let middleware = self.find_middleware(); + let FindContextFileResult::Found(fs_path, _) = *middleware.await? else { + return Ok(Vc::upcast(edge_module_context)); + }; + let source = Vc::upcast(FileSource::new(*fs_path)); + + let module = edge_module_context + .process( + source, + Value::new(ReferenceType::Entry(EntryReferenceSubType::Middleware)), + ) + .module(); + + let config = parse_config_from_source(module, NextRuntime::Edge).await?; + + if matches!(config.runtime, NextRuntime::NodeJs) { + let server_module_context = ModuleAssetContext::new( + TransitionOptions { + named_transitions: transitions.clone().into_iter().collect(), + ..Default::default() + } + .cell(), + self.server_compile_time_info(), + get_server_module_options_context( + self.project_path(), + self.execution_context(), + Value::new(ServerContextType::Middleware { + app_dir, + ecmascript_client_reference_transition_name, + }), + self.next_mode(), + self.next_config(), + NextRuntime::NodeJs, + self.encryption_key(), + ), + get_server_resolve_options_context( + self.project_path(), + Value::new(ServerContextType::Middleware { + app_dir, + ecmascript_client_reference_transition_name, + }), + self.next_mode(), + self.next_config(), + self.execution_context(), + ), + Vc::cell("middleware".into()), + ); + + Ok(Vc::upcast(server_module_context)) + } else { + Ok(Vc::upcast(edge_module_context)) + } } #[turbo_tasks::function] @@ -1300,6 +1354,7 @@ impl Project { Ok(Vc::upcast(MiddlewareEndpoint::new( self, + self.await?.build_id.clone(), middleware_asset_context, source, app_dir.as_deref().copied(), diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index 0fd7d1a49deef..c7ad92d32b3f5 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -270,10 +270,7 @@ pub async fn get_server_resolve_options_context( ResolvedVc::upcast(next_external_plugin), ] } - ServerContextType::Middleware { .. } => { - vec![ResolvedVc::upcast(next_node_shared_runtime_plugin)] - } - ServerContextType::Instrumentation { .. } => { + ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => { vec![ ResolvedVc::upcast(next_node_shared_runtime_plugin), ResolvedVc::upcast(server_external_packages_plugin), diff --git a/crates/next-core/src/util.rs b/crates/next-core/src/util.rs index 5c6508ea62883..d8d29e88ae95b 100644 --- a/crates/next-core/src/util.rs +++ b/crates/next-core/src/util.rs @@ -427,6 +427,7 @@ async fn parse_route_matcher_from_js_value( #[turbo_tasks::function] pub async fn parse_config_from_source( module: ResolvedVc>, + default_runtime: NextRuntime, ) -> Result> { if let Some(ecmascript_asset) = ResolvedVc::try_sidecast::>(module) { @@ -456,9 +457,13 @@ pub async fn parse_config_from_source( return WrapFuture::new( async { let value = eval_context.eval(init); - Ok(parse_config_from_js_value(*module, &value) - .await? - .cell()) + Ok(parse_config_from_js_value( + *module, + &value, + default_runtime, + ) + .await? + .cell()) }, |f, ctx| GLOBALS.set(globals, || f.poll(ctx)), ) @@ -537,14 +542,23 @@ pub async fn parse_config_from_source( } } } - Ok(Default::default()) + let config = NextSourceConfig { + runtime: default_runtime, + ..Default::default() + }; + + Ok(config.cell()) } async fn parse_config_from_js_value( module: Vc>, value: &JsValue, + default_runtime: NextRuntime, ) -> Result { - let mut config = NextSourceConfig::default(); + let mut config = NextSourceConfig { + runtime: default_runtime, + ..Default::default() + }; if let JsValue::Object { parts, .. } = value { for part in parts { diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 4a4674caa0e62..2bcdeff19d9e9 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -80,6 +80,7 @@ import { UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, UNDERSCORE_NOT_FOUND_ROUTE, DYNAMIC_CSS_MANIFEST, + TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, } from '../shared/lib/constants' import { getSortedRoutes, @@ -2348,6 +2349,18 @@ export default async function build( }, ], } + + if (turboNextBuild) { + await writeManifest( + path.join( + distDir, + 'static', + buildId, + TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST + ), + functionsConfigManifest.functions['/_middleware'].matchers || [] + ) + } } } diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index c9ff170824d17..faa1acdb5ba04 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -692,12 +692,14 @@ export async function handleEntrypoints({ dev.hooks.handleWrittenEndpoint(key, writtenEndpoint) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') - if (dev) { + const middlewareConfig = + manifestLoader.getMiddlewareManifest(key)?.middleware['/'] + + if (dev && middlewareConfig) { dev.serverFields.middleware = { match: null as any, page: '/', - matchers: - manifestLoader.getMiddlewareManifest(key)?.middleware['/'].matchers, + matchers: middlewareConfig.matchers, } } } diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 9ad83a12dba38..5e1538d1cd480 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -49,6 +49,7 @@ import { DEV_CLIENT_PAGES_MANIFEST, DEV_CLIENT_MIDDLEWARE_MANIFEST, PHASE_DEVELOPMENT_SERVER, + TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, } from '../../../shared/lib/constants' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' @@ -902,6 +903,9 @@ async function startWatcher(opts: SetupOpts) { const devMiddlewareManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${DEV_CLIENT_MIDDLEWARE_MANIFEST}` opts.fsChecker.devVirtualFsItems.add(devMiddlewareManifestPath) + const devTurbopackMiddlewareManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST}` + opts.fsChecker.devVirtualFsItems.add(devTurbopackMiddlewareManifestPath) + async function requestHandler(req: IncomingMessage, res: ServerResponse) { const parsedUrl = url.parse(req.url || '/') @@ -918,7 +922,10 @@ async function startWatcher(opts: SetupOpts) { return { finished: true } } - if (parsedUrl.pathname?.includes(devMiddlewareManifestPath)) { + if ( + parsedUrl.pathname?.includes(devMiddlewareManifestPath) || + parsedUrl.pathname?.includes(devTurbopackMiddlewareManifestPath) + ) { res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') res.end(JSON.stringify(serverFields.middleware?.matchers || [])) diff --git a/test/e2e/middleware-custom-matchers/app/middleware-node.js b/test/e2e/middleware-custom-matchers/app/middleware-node.js new file mode 100644 index 0000000000000..dcf91034a44a5 --- /dev/null +++ b/test/e2e/middleware-custom-matchers/app/middleware-node.js @@ -0,0 +1,90 @@ +import { NextResponse } from 'next/server' + +export default function middleware(request) { + const res = NextResponse.rewrite(new URL('/', request.url)) + res.headers.set('X-From-Middleware', 'true') + return res +} + +export const config = { + runtime: 'nodejs', + matcher: [ + { source: '/source-match' }, + { + source: '/has-match-1', + has: [ + { + type: 'header', + key: 'x-my-header', + value: '(?.*)', + }, + ], + }, + { + source: '/has-match-2', + has: [ + { + type: 'query', + key: 'my-query', + }, + ], + }, + { + source: '/has-match-3', + has: [ + { + type: 'cookie', + key: 'loggedIn', + value: '(?true)', + }, + ], + }, + { + source: '/has-match-4', + has: [ + { + type: 'host', + value: 'example.com', + }, + ], + }, + { + source: '/has-match-5', + has: [ + { + type: 'header', + key: 'hasParam', + value: 'with-params', + }, + ], + }, + { + source: '/missing-match-1', + missing: [ + { + type: 'header', + key: 'hello', + value: '(.*)', + }, + ], + }, + { + source: '/missing-match-2', + missing: [ + { + type: 'query', + key: 'test', + value: 'value', + }, + ], + }, + { + source: + '/((?!api|monitoring|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|manifest|icon|source-match|has-match-1|has-match-2|has-match-3|has-match-4|has-match-5|missing-match-1|missing-match-2|routes).*)', + missing: [ + { type: 'header', key: 'next-router-prefetch' }, + { type: 'header', key: 'purpose', value: 'prefetch' }, + ], + }, + ], +} diff --git a/test/e2e/middleware-custom-matchers/app/next.config.js b/test/e2e/middleware-custom-matchers/app/next.config.js new file mode 100644 index 0000000000000..a1d86b5c6dd6e --- /dev/null +++ b/test/e2e/middleware-custom-matchers/app/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + nodeMiddleware: true, + }, +} diff --git a/test/e2e/middleware-custom-matchers/test/index.test.ts b/test/e2e/middleware-custom-matchers/test/index.test.ts index 001b187798b40..34b6deea3272f 100644 --- a/test/e2e/middleware-custom-matchers/test/index.test.ts +++ b/test/e2e/middleware-custom-matchers/test/index.test.ts @@ -13,9 +13,20 @@ const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy' describe('Middleware custom matchers', () => { let next: NextInstance + if ((global as any).isNextDeploy && process.env.TEST_NODE_MIDDLEWARE) { + return it('should skip deploy for now', () => {}) + } + beforeAll(async () => { next = await createNext({ files: new FileRef(join(__dirname, '../app')), + overrideFiles: process.env.TEST_NODE_MIDDLEWARE + ? { + 'middleware.js': new FileRef( + join(__dirname, '../app/middleware-node.js') + ), + } + : {}, }) }) afterAll(() => next.destroy()) diff --git a/test/e2e/middleware-custom-matchers/test/node-runtime.test.ts b/test/e2e/middleware-custom-matchers/test/node-runtime.test.ts new file mode 100644 index 0000000000000..28fdbfc88b2ec --- /dev/null +++ b/test/e2e/middleware-custom-matchers/test/node-runtime.test.ts @@ -0,0 +1,3 @@ +process.env.TEST_NODE_MIDDLEWARE = 'true' + +require('./index.test') diff --git a/test/e2e/middleware-general/test/node-runtime.test.ts b/test/e2e/middleware-general/test/node-runtime.test.ts index c383e639c40ec..28fdbfc88b2ec 100644 --- a/test/e2e/middleware-general/test/node-runtime.test.ts +++ b/test/e2e/middleware-general/test/node-runtime.test.ts @@ -1,7 +1,3 @@ process.env.TEST_NODE_MIDDLEWARE = 'true' -if (process.env.TURBOPACK) { - it('should skip', () => {}) -} else { - require('./index.test') -} +require('./index.test') diff --git a/test/integration/edge-runtime-module-errors/test/index.test.js b/test/integration/edge-runtime-module-errors/test/index.test.js index eab8dc9e93331..2e8fa2f572914 100644 --- a/test/integration/edge-runtime-module-errors/test/index.test.js +++ b/test/integration/edge-runtime-module-errors/test/index.test.js @@ -111,14 +111,18 @@ describe('Edge runtime code with imports', () => { stderr: true, }) expect(stderr).toContain(getUnsupportedModuleWarning(moduleName)) - context.app = await nextStart( - context.appDir, - context.appPort, - appOption - ) - const res = await fetchViaHTTP(context.appPort, url) - expect(res.status).toBe(500) - expectUnsupportedModuleProdError(moduleName) + + // TODO: should this be failing build or not in turbopack + if (!process.env.TURBOPACK) { + context.app = await nextStart( + context.appDir, + context.appPort, + appOption + ) + const res = await fetchViaHTTP(context.appPort, url) + expect(res.status).toBe(500) + expectUnsupportedModuleProdError(moduleName) + } }) } )