Skip to main content

turbopack_ecmascript/references/
service_worker.rs

1use anyhow::Result;
2use bincode::{Decode, Encode};
3use swc_core::{
4    ecma::ast::{Expr, ExprOrSpread, Lit},
5    quote_expr,
6};
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{
9    NonLocalValue, ResolvedVc, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs,
10    turbofmt,
11};
12use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
13use turbopack_core::{
14    chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, ChunkingType},
15    ident::AssetIdent,
16    issue::IssueSource,
17    module::{Module, ModuleSideEffects},
18    module_graph::ModuleGraph,
19    reference::{ModuleReference, ModuleReferences},
20    reference_type::{ReferenceType, WorkerReferenceSubType},
21    resolve::{
22        ModuleResolveResult, ModuleResolveResultItem, ResolveErrorMode, origin::ResolveOrigin,
23        parse::Request, url_resolve,
24    },
25    source::OptionSource,
26};
27
28use crate::{
29    chunk::{
30        EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
31        ecmascript_chunk_item,
32    },
33    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
34    create_visitor,
35    references::AstPath,
36};
37
38/// The root-served file name for a service worker registered at `scope`. One worker is supported
39/// **per scope**; the scope is encoded into the (flat, root-served) file name so distinct scopes
40/// get distinct files.
41///
42/// The human-readable slug is lossy (e.g. `/foo/bar` and `/foo-bar` both slugify to `foo-bar`), so
43/// a hash of the original scope is appended to guarantee distinct scopes get distinct file names.
44pub fn service_worker_chunk_filename(scope: &str) -> RcStr {
45    let trimmed = scope.trim_matches('/');
46    if trimmed.is_empty() {
47        return rcstr!("sw.js");
48    }
49    let slug: String = trimmed
50        .chars()
51        .map(|c| match c {
52            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c,
53            _ => '-',
54        })
55        .collect();
56    let hash = encode_hex(hash_xxh3_hash64(trimmed));
57    RcStr::from(format!("sw-{slug}-{hash}.js"))
58}
59
60/// A marker module that wraps a service-worker entry source plus its registration `scope`. It
61/// carries the inner source so `next-api` can discover it in the module graph and compile it
62/// standalone.
63#[turbo_tasks::value(shared)]
64pub struct ServiceWorkerEntryModule {
65    pub inner: ResolvedVc<Box<dyn Module>>,
66    pub scope: RcStr,
67}
68
69#[turbo_tasks::value_impl]
70impl Module for ServiceWorkerEntryModule {
71    #[turbo_tasks::function]
72    async fn ident(&self) -> Result<Vc<AssetIdent>> {
73        Ok(self
74            .inner
75            .ident()
76            .owned()
77            .await?
78            .with_modifier(format!("service worker entry [{}]", self.scope).into())
79            .into_vc())
80    }
81
82    #[turbo_tasks::function]
83    fn source(&self) -> Vc<OptionSource> {
84        Vc::cell(None)
85    }
86
87    #[turbo_tasks::function]
88    fn references(&self) -> Vc<ModuleReferences> {
89        Vc::cell(vec![])
90    }
91
92    #[turbo_tasks::function]
93    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
94        ModuleSideEffects::ModuleEvaluationIsSideEffectFree.cell()
95    }
96}
97
98#[turbo_tasks::value_impl]
99impl ChunkableModule for ServiceWorkerEntryModule {
100    #[turbo_tasks::function]
101    fn as_chunk_item(
102        self: ResolvedVc<Self>,
103        module_graph: ResolvedVc<ModuleGraph>,
104        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
105    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
106        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
107    }
108}
109
110#[turbo_tasks::value_impl]
111impl EcmascriptChunkPlaceable for ServiceWorkerEntryModule {
112    #[turbo_tasks::function]
113    fn get_exports(&self) -> Vc<EcmascriptExports> {
114        EcmascriptExports::None.cell()
115    }
116
117    #[turbo_tasks::function]
118    fn chunk_item_content(
119        &self,
120        _chunking_context: Vc<Box<dyn ChunkingContext>>,
121        _module_graph: Vc<ModuleGraph>,
122        _async_module_info: Option<Vc<AsyncModuleInfo>>,
123        _estimated: bool,
124    ) -> Vc<EcmascriptChunkItemContent> {
125        // Marker module: contributes no code to the page bundle.
126        EcmascriptChunkItemContent::default().cell()
127    }
128}
129
130/// Reference created for `navigator.serviceWorker.register(new URL(...), { scope })`. It resolves
131/// the URL to the service-worker source and wraps it in a [`ServiceWorkerEntryModule`] (carrying
132/// the `scope`) so the source is discoverable in the page's module graph (but not bundled into it).
133#[turbo_tasks::value]
134#[derive(Hash, Debug)]
135pub struct ServiceWorkerAssetReference {
136    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
137    request: ResolvedVc<Request>,
138    scope: RcStr,
139    issue_source: IssueSource,
140    error_mode: ResolveErrorMode,
141}
142
143impl ServiceWorkerAssetReference {
144    pub fn new(
145        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
146        request: ResolvedVc<Request>,
147        scope: RcStr,
148        issue_source: IssueSource,
149        error_mode: ResolveErrorMode,
150    ) -> Self {
151        ServiceWorkerAssetReference {
152            origin,
153            request,
154            scope,
155            issue_source,
156            error_mode,
157        }
158    }
159}
160
161#[turbo_tasks::value_impl]
162impl ModuleReference for ServiceWorkerAssetReference {
163    #[turbo_tasks::function]
164    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
165        let result = url_resolve(
166            *self.origin,
167            *self.request,
168            ReferenceType::Worker(WorkerReferenceSubType::ServiceWorker),
169            Some(self.issue_source),
170            self.error_mode,
171        );
172
173        let result_ref = result.await?;
174        let mut primary = Vec::with_capacity(result_ref.primary.len());
175        for (request_key, item) in result_ref.primary.iter() {
176            match item {
177                ModuleResolveResultItem::Module(module) => {
178                    let marker = ServiceWorkerEntryModule {
179                        inner: *module,
180                        scope: self.scope.clone(),
181                    }
182                    .resolved_cell();
183                    primary.push((
184                        request_key.clone(),
185                        ModuleResolveResultItem::Module(ResolvedVc::upcast(marker)),
186                    ));
187                }
188                _ => primary.push((request_key.clone(), item.clone())),
189            }
190        }
191
192        Ok(ModuleResolveResult {
193            primary: primary.into_boxed_slice(),
194            affecting_sources: result_ref.affecting_sources.clone(),
195        }
196        .cell())
197    }
198
199    fn chunking_type(&self) -> Option<ChunkingType> {
200        // Keep the marker in the page graph (so it is discoverable) without making it
201        // an async/isolated boundary. It emits no code, so it adds nothing to the bundle.
202        Some(ChunkingType::Parallel {
203            inherit_async: false,
204            hoisted: false,
205        })
206    }
207
208    fn source(&self) -> Option<IssueSource> {
209        Some(self.issue_source)
210    }
211}
212
213#[turbo_tasks::value_impl]
214impl ValueToString for ServiceWorkerAssetReference {
215    #[turbo_tasks::function]
216    async fn to_string(&self) -> Result<Vc<RcStr>> {
217        let request = self.request.to_string();
218        Ok(Vc::cell(turbofmt!("service worker {request}").await?))
219    }
220}
221
222impl IntoCodeGenReference for ServiceWorkerAssetReference {
223    fn into_code_gen_reference(
224        self,
225        path: AstPath,
226    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
227        let scope = self.scope.clone();
228        let reference = self.resolved_cell();
229        (
230            ResolvedVc::upcast(reference),
231            CodeGen::ServiceWorkerAssetReferenceCodeGen(ServiceWorkerAssetReferenceCodeGen {
232                scope,
233                path,
234            }),
235        )
236    }
237}
238
239#[derive(
240    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
241)]
242pub struct ServiceWorkerAssetReferenceCodeGen {
243    scope: RcStr,
244    path: AstPath,
245}
246
247impl ServiceWorkerAssetReferenceCodeGen {
248    pub async fn code_generation(
249        &self,
250        _chunking_context: Vc<Box<dyn ChunkingContext>>,
251    ) -> Result<CodeGeneration> {
252        // The worker is served at a fixed, root-scoped URL derived from its `scope`. Rewrite the
253        // `new URL(...)` script argument to that URL string.
254        let url = format!("/{}", service_worker_chunk_filename(&self.scope));
255
256        let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| {
257            let message = if let Expr::Call(call_expr) = expr {
258                match call_expr.args.first_mut() {
259                    Some(ExprOrSpread {
260                        spread: None,
261                        expr: url_expr,
262                    }) => {
263                        **url_expr = Expr::Lit(Lit::Str(url.as_str().into()));
264                        return;
265                    }
266                    Some(ExprOrSpread {
267                        spread: Some(_), ..
268                    }) => "spread operator is illegal in navigator.serviceWorker.register().",
269                    None => "navigator.serviceWorker.register() requires at least 1 argument",
270                }
271            } else {
272                "visitor must be executed on a CallExpr"
273            };
274            *expr = *quote_expr!(
275                "(() => { throw new Error($message); })()",
276                message: Expr = Expr::Lit(Lit::Str(message.into()))
277            );
278        });
279
280        Ok(CodeGeneration::visitors(vec![visitor]))
281    }
282}
283
284impl From<ServiceWorkerAssetReferenceCodeGen> for CodeGen {
285    fn from(val: ServiceWorkerAssetReferenceCodeGen) -> Self {
286        CodeGen::ServiceWorkerAssetReferenceCodeGen(val)
287    }
288}