turbopack_ecmascript/references/
service_worker.rs1use 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
37pub 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#[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 EcmascriptChunkItemContent::default().cell()
126 }
127}
128
129#[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 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 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}