diff --git a/test/e2e/app-dir/worker/app/shared-bundled/page.js b/test/e2e/app-dir/worker/app/shared-bundled/page.js new file mode 100644 index 00000000000000..c328f4c10bd413 --- /dev/null +++ b/test/e2e/app-dir/worker/app/shared-bundled/page.js @@ -0,0 +1,26 @@ +'use client' +import { useState } from 'react' + +export default function SharedWorkerBundledPage() { + const [state, setState] = useState('default') + return ( +
+ +

SharedWorker bundled state:

+

{state}

+
+ ) +} diff --git a/test/e2e/app-dir/worker/app/shared-unbundled/page.js b/test/e2e/app-dir/worker/app/shared-unbundled/page.js new file mode 100644 index 00000000000000..46037438949230 --- /dev/null +++ b/test/e2e/app-dir/worker/app/shared-unbundled/page.js @@ -0,0 +1,23 @@ +'use client' +import { useState } from 'react' + +export default function SharedWorkerPage() { + const [state, setState] = useState('default') + return ( +
+ +

SharedWorker state:

+

{state}

+
+ ) +} diff --git a/test/e2e/app-dir/worker/app/shared-worker.ts b/test/e2e/app-dir/worker/app/shared-worker.ts new file mode 100644 index 00000000000000..b77dfdac01cf35 --- /dev/null +++ b/test/e2e/app-dir/worker/app/shared-worker.ts @@ -0,0 +1,16 @@ +// SharedWorker script - handles multiple connections +self.addEventListener('connect', (event: MessageEvent) => { + const port = event.ports[0] + + // Import the dependency and send the message + import('./worker-dep') + .then((mod) => { + port.postMessage('shared-worker.ts:' + mod.default) + }) + .catch((error) => { + console.error('SharedWorker import error:', error) + port.postMessage('error: ' + error.message) + }) + + port.start() +}) diff --git a/test/e2e/app-dir/worker/public/unbundled-shared-worker.js b/test/e2e/app-dir/worker/public/unbundled-shared-worker.js new file mode 100644 index 00000000000000..606fb5d041d3f3 --- /dev/null +++ b/test/e2e/app-dir/worker/public/unbundled-shared-worker.js @@ -0,0 +1,6 @@ +// SharedWorker script for unbundled test +self.addEventListener('connect', (event) => { + const port = event.ports[0] + port.postMessage('unbundled-shared-worker') + port.start() +}) diff --git a/test/e2e/app-dir/worker/worker.test.ts b/test/e2e/app-dir/worker/worker.test.ts index aa4bd25edf7d13..0ee39c8e6eb0ff 100644 --- a/test/e2e/app-dir/worker/worker.test.ts +++ b/test/e2e/app-dir/worker/worker.test.ts @@ -49,4 +49,34 @@ describe('app dir - workers', () => { ) ) }) + + it('should support shared workers with string specifiers', async () => { + const browser = await next.browser('/shared-unbundled') + expect(await browser.elementByCss('#shared-worker-state').text()).toBe( + 'default' + ) + + await browser.elementByCss('button').click() + + await retry(async () => + expect(await browser.elementByCss('#shared-worker-state').text()).toBe( + 'unbundled-shared-worker' + ) + ) + }) + + it('should support shared workers with dynamic imports', async () => { + const browser = await next.browser('/shared-bundled') + expect( + await browser.elementByCss('#shared-worker-bundled-state').text() + ).toBe('default') + + await browser.elementByCss('button').click() + + await retry(async () => + expect( + await browser.elementByCss('#shared-worker-bundled-state').text() + ).toBe('shared-worker.ts:worker-dep') + ) + }) }) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs index 11dc6d364b2723..880e78496a54ad 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs @@ -770,7 +770,9 @@ impl Visit for Analyzer<'_> { // we could actually unwrap thanks to the optimisation above but it can't hurt to be safe... if let Some(comments) = self.comments { let callee_span = match &n.callee { - box Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span), + box Expr::Ident(Ident { sym, .. }) if sym == "Worker" || sym == "SharedWorker" => { + Some(n.span) + } _ => None, }; diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index 6b2334dabea335..fe806cc043be17 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -1958,6 +1958,10 @@ impl JsValue { "Worker".to_string(), "The standard Worker constructor: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker" ), + WellKnownFunctionKind::SharedWorkerConstructor => ( + "SharedWorker".to_string(), + "The standard SharedWorker constructor: https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker/SharedWorker" + ), WellKnownFunctionKind::URLConstructor => ( "URL".to_string(), "The standard URL constructor: https://developer.mozilla.org/en-US/docs/Web/API/URL/URL" @@ -3623,6 +3627,7 @@ pub enum WellKnownFunctionKind { NodeResolveFrom, NodeProtobufLoad, WorkerConstructor, + SharedWorkerConstructor, URLConstructor, } @@ -3795,6 +3800,12 @@ pub mod test_utils { true, "ignored Worker constructor", ), + "SharedWorker" => JsValue::unknown_if( + ignore, + JsValue::WellKnownFunction(WellKnownFunctionKind::SharedWorkerConstructor), + true, + "ignored SharedWorker constructor", + ), "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define), "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor), "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcess), diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 064887b42c128b..90daf597f8b950 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -75,7 +75,7 @@ use turbopack_core::{ issue::{IssueExt, IssueSeverity, IssueSource, StyledString, analyze::AnalyzeIssue}, module::Module, reference::{ModuleReference, ModuleReferences}, - reference_type::{CommonJsReferenceSubType, ReferenceType}, + reference_type::{CommonJsReferenceSubType, ReferenceType, WorkerReferenceSubType}, resolve::{ FindContextFileResult, ModulePart, find_context_file, origin::{PlainResolveOrigin, ResolveOrigin, ResolveOriginExt}, @@ -1719,6 +1719,43 @@ async fn handle_call) + Send + Sync>( WorkerAssetReference::new( origin, Request::parse(pat).to_resolved().await?, + WorkerReferenceSubType::WebWorker, + issue_source(source, span), + in_try, + ), + ast_path.to_vec().into(), + ); + } + + return Ok(()); + } + // Ignore (e.g. dynamic parameter or string literal), just as Webpack does + return Ok(()); + } + JsValue::WellKnownFunction(WellKnownFunctionKind::SharedWorkerConstructor) => { + let args = linked_args(args).await?; + if let Some(url @ JsValue::Url(_, JsValueUrlKind::Relative)) = args.first() { + let pat = js_value_to_pattern(url); + if !pat.has_constant_parts() { + let (args, hints) = explain_args(&args); + handler.span_warn_with_code( + span, + &format!("new SharedWorker({args}) is very dynamic{hints}",), + DiagnosticId::Lint( + errors::failed_to_analyze::ecmascript::NEW_WORKER.to_string(), + ), + ); + if ignore_dynamic_requests { + return Ok(()); + } + } + + if *compile_time_info.environment().rendering().await? == Rendering::Client { + analysis.add_reference_code_gen( + WorkerAssetReference::new( + origin, + Request::parse(pat).to_resolved().await?, + WorkerReferenceSubType::SharedWorker, issue_source(source, span), in_try, ), @@ -3149,6 +3186,12 @@ async fn value_visitor_inner( true, "ignored Worker constructor", ), + "SharedWorker" => JsValue::unknown_if( + ignore, + JsValue::WellKnownFunction(WellKnownFunctionKind::SharedWorkerConstructor), + true, + "ignored SharedWorker constructor", + ), "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define), "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor), "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcess), diff --git a/turbopack/crates/turbopack-ecmascript/src/references/worker.rs b/turbopack/crates/turbopack-ecmascript/src/references/worker.rs index 17e7b474fd6ed3..cc41a0905d381d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/worker.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/worker.rs @@ -32,6 +32,7 @@ use crate::{ pub struct WorkerAssetReference { pub origin: ResolvedVc>, pub request: ResolvedVc, + pub worker_type: WorkerReferenceSubType, pub issue_source: IssueSource, pub in_try: bool, } @@ -40,12 +41,14 @@ impl WorkerAssetReference { pub fn new( origin: ResolvedVc>, request: ResolvedVc, + worker_type: WorkerReferenceSubType, issue_source: IssueSource, in_try: bool, ) -> Self { WorkerAssetReference { origin, request, + worker_type, issue_source, in_try, } @@ -59,8 +62,7 @@ impl WorkerAssetReference { let module = url_resolve( *self.origin, *self.request, - // TODO support more worker types - ReferenceType::Worker(WorkerReferenceSubType::WebWorker), + ReferenceType::Worker(self.worker_type), Some(self.issue_source), self.in_try, ); @@ -81,7 +83,7 @@ impl WorkerAssetReference { return Ok(None); }; - Ok(Some(WorkerLoaderModule::new(*chunkable))) + Ok(Some(WorkerLoaderModule::new(*chunkable, self.worker_type))) } } @@ -103,8 +105,14 @@ impl ModuleReference for WorkerAssetReference { impl ValueToString for WorkerAssetReference { #[turbo_tasks::function] async fn to_string(&self) -> Result> { + let worker_name = match self.worker_type { + WorkerReferenceSubType::WebWorker => "Worker", + WorkerReferenceSubType::SharedWorker => "SharedWorker", + WorkerReferenceSubType::ServiceWorker => "ServiceWorker", + _ => "Worker", + }; Ok(Vc::cell( - format!("new Worker {}", self.request.to_string().await?,).into(), + format!("new {} {}", worker_name, self.request.to_string().await?,).into(), )) } } diff --git a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/chunk_item.rs b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/chunk_item.rs index 919e1e336e46d0..2f9933295f0968 100644 --- a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/chunk_item.rs +++ b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/chunk_item.rs @@ -58,6 +58,7 @@ impl WorkerLoaderChunkItem { impl EcmascriptChunkItem for WorkerLoaderChunkItem { #[turbo_tasks::function] async fn content(self: Vc) -> Result> { + let _this = self.await?; let chunks_data = self.chunks_data().await?; let chunks_data = chunks_data.iter().try_join().await?; let chunks_data: Vec<_> = chunks_data @@ -65,6 +66,8 @@ impl EcmascriptChunkItem for WorkerLoaderChunkItem { .map(|chunk_data| EcmascriptChunkData::new(chunk_data)) .collect(); + // All worker types use the same blob URL generation + // The difference is handled at the JavaScript level when creating the worker let code = formatdoc! { r#" {TURBOPACK_EXPORT_VALUE}({TURBOPACK_WORKER_BLOB_URL}({chunks:#})); diff --git a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs index 1e9072b9a0c2f4..3317301e1d9c18 100644 --- a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs @@ -11,6 +11,7 @@ use turbopack_core::{ module::Module, module_graph::ModuleGraph, reference::{ModuleReference, ModuleReferences}, + reference_type::WorkerReferenceSubType, resolve::ModuleResolveResult, }; @@ -21,13 +22,20 @@ use super::chunk_item::WorkerLoaderChunkItem; #[turbo_tasks::value] pub struct WorkerLoaderModule { pub inner: ResolvedVc>, + pub worker_type: WorkerReferenceSubType, } #[turbo_tasks::value_impl] impl WorkerLoaderModule { #[turbo_tasks::function] - pub fn new(module: ResolvedVc>) -> Vc { - Self::cell(WorkerLoaderModule { inner: module }) + pub fn new( + module: ResolvedVc>, + worker_type: WorkerReferenceSubType, + ) -> Vc { + Self::cell(WorkerLoaderModule { + inner: module, + worker_type, + }) } #[turbo_tasks::function]