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