diff --git a/Cargo.lock b/Cargo.lock index b097f0830b3d..d125d53dbd5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,9 +902,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -1833,9 +1833,9 @@ checksum = "718efe674f3df645462677e22a3128e890d88ba55821bb091083d257707be76c" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -3045,6 +3045,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -4425,9 +4431,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memfd" @@ -4811,7 +4817,7 @@ dependencies = [ "next-api", "next-core", "num_cpus", - "rand 0.10.0", + "rand 0.10.1", "serde_json", "swc_core", "tokio", @@ -4966,7 +4972,7 @@ dependencies = [ "next-taskless", "once_cell", "owo-colors", - "rand 0.10.0", + "rand 0.10.1", "regex", "rustc-hash 2.1.1", "serde", @@ -5649,9 +5655,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -6128,9 +6134,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -6186,9 +6192,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.1", @@ -6380,9 +6386,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -6392,9 +6398,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -6535,12 +6541,12 @@ dependencies = [ [[package]] name = "ringmap" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c6baa3e518a2e7a539d79b9e5e5a24ab42697289734b0700b2c1dd42a04654" +checksum = "219d417c84282f92a4af7c9cf3586debefa76b82b618664772ff11732b8cd03c" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -6606,9 +6612,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.10.10" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652edd001c53df0b3f96a36a8dc93fce6866988efc16808235653c6bcac8bf2" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -9817,7 +9823,7 @@ dependencies = [ "postcard", "qfilter", "quick_cache", - "rand 0.10.0", + "rand 0.10.1", "rayon", "rustc-hash 2.1.1", "smallvec", @@ -9940,7 +9946,7 @@ dependencies = [ "indoc", "lzzzz", "parking_lot", - "rand 0.10.0", + "rand 0.10.1", "regex", "ringmap", "rstest", @@ -10043,7 +10049,7 @@ dependencies = [ "mime", "notify", "parking_lot", - "rand 0.10.0", + "rand 0.10.1", "regex", "rstest", "rustc-hash 2.1.1", @@ -10072,7 +10078,7 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", - "rand 0.10.0", + "rand 0.10.1", "rustc-hash 2.1.1", "tokio", "turbo-rcstr", @@ -10216,7 +10222,7 @@ dependencies = [ "owo-colors", "parking_lot", "portpicker", - "rand 0.10.0", + "rand 0.10.1", "regex", "rustc-hash 2.1.1", "serde_json", @@ -10490,6 +10496,7 @@ dependencies = [ "turbopack-swc-utils", "turbopack-test-utils", "url", + "urlencoding", ] [[package]] @@ -10890,9 +10897,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.16.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" diff --git a/Cargo.toml b/Cargo.toml index 9b745d86e4a1..c9879b8909ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -249,14 +249,14 @@ async-trait = "0.1.64" bincode = { version = "2.0.1", features = ["serde"] } bitfield = "0.19.4" byteorder = "1.5.0" -bytes = "1.1.0" +bytes = "1.11.1" bytes-str = "0.2.7" chrono = "0.4.23" clap = { version = "4.5.2", features = ["derive"] } concurrent-queue = "2.5.0" console-subscriber = "0.4.1" const_format = "0.2.30" -crc32fast = "1.4.2" +crc32fast = "1.5.0" criterion = { package = "codspeed-criterion-compat", version = "4.3.0" } ctor = "0.8" crossbeam-channel = "0.5.8" @@ -288,7 +288,7 @@ lightningcss-napi = { version = "0.4.6", default-features = false, features = [ ] } lzzzz = "2.0.0" markdown = "1.0.0-alpha.18" -memchr = "2.7.4" +memchr = "2.8.0" mime = "0.3.16" napi = { version = "2", default-features = false, features = [ "napi3", @@ -311,19 +311,19 @@ parking_lot = "0.12.1" pathdiff = "0.2.1" phf = { version = "0.11", features = ["macros"] } petgraph = "0.8.3" -pin-project-lite = "0.2.9" +pin-project-lite = "0.2.17" postcard = "1.0.4" proc-macro2 = "1.0.79" qstring = "0.7.2" quick_cache = { version = "0.6.14" } -quote = "1.0.23" -rand = "0.10.0" +quote = "1.0.45" +rand = "0.10.1" rayon = "1.10.0" -regex = "1.10.6" +regex = "1.12.3" regress = "0.10.4" reqwest = { version = "0.13.2", default-features = false } -ringmap = "0.2.3" -roaring = "0.10.10" +ringmap = "0.2.5" +roaring = "0.11.4" rstest = "0.16.0" rustc-hash = "2.1.1" semver = "1.0.16" diff --git a/crates/next-error-code-swc-plugin/src/lib.rs b/crates/next-error-code-swc-plugin/src/lib.rs index 1791e203148b..71f349acb349 100644 --- a/crates/next-error-code-swc-plugin/src/lib.rs +++ b/crates/next-error-code-swc-plugin/src/lib.rs @@ -2,6 +2,7 @@ use std::fs; use rustc_hash::FxHashMap; use swc_core::{ + common::{Span, SyntaxContext}, ecma::{ast::*, transforms::testing::test_inline, visit::*}, plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}, }; @@ -114,6 +115,98 @@ fn stringify_new_error_arg(expr: &Expr, bindings: &FxHashMap) -> Str } } +impl TransformVisitor { + // Look up `error_message` in `errors.json`. On miss, spill to + // `cwd/.errors/.json` so the check-error-codes consolidation step can + // pick it up. + fn lookup_or_emit(&self, error_message: String) -> Option { + // Normalize line endings by converting Windows CRLF (\r\n) to Unix LF (\n) + // This ensures the comparison works consistently across different operating systems. + // We assume `errors.json` uses Unix LF (\n) as line endings. + let error_message = error_message.replace("\r\n", "\n"); + + if let Some(code) = self + .errors + .iter() + .find_map(|(key, value)| (*value == error_message).then_some(key)) + { + return Some(format!("E{}", code)); + } + + let new_error = serde_json::to_string(&NewError { error_message }).unwrap(); + let hash_hex = format!("{:x}", md5::compute(new_error.as_bytes())); + let file_path = format!("cwd/.errors/{}.json", &hash_hex[0..8]); + + let _ = fs::create_dir_all("cwd/.errors"); + let _ = fs::write(&file_path, new_error); + + None + } + + // Build `Object.defineProperty(, "__NEXT_ERROR_CODE", { value: + // "", enumerable: false, configurable: true })`. + fn build_define_property_call( + &self, + span: Span, + ctxt: SyntaxContext, + code: String, + target: Box, + ) -> CallExpr { + CallExpr { + span, + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span, + obj: Box::new(Expr::Ident(Ident::new( + "Object".into(), + span, + Default::default(), + ))), + prop: MemberProp::Ident("defineProperty".into()), + }))), + args: vec![ + ExprOrSpread { + spread: None, + expr: target, + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span, + value: "__NEXT_ERROR_CODE".into(), + raw: None, + }))), + }, + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident("value".into()), + value: Box::new(Expr::Lit(Lit::Str(Str { + span, + value: code.into(), + raw: None, + }))), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident("enumerable".into()), + value: Box::new(Expr::Lit(Lit::Bool(Bool { span, value: false }))), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident("configurable".into()), + value: Box::new(Expr::Lit(Lit::Bool(Bool { span, value: true }))), + }))), + ], + })), + }, + ], + type_args: None, + ctxt, + } + } +} + impl VisitMut for TransformVisitor { fn visit_mut_program(&mut self, program: &mut Program) { let mut collector = BindingCollector { @@ -200,92 +293,118 @@ impl VisitMut for TransformVisitor { } let new_error_expr: NewExpr = new_error_expr.unwrap(); + let error_message = error_message.unwrap(); + + if let Some(code) = self.lookup_or_emit(error_message) { + let span = new_error_expr.span; + let ctxt = new_error_expr.ctxt; + let call = self.build_define_property_call( + span, + ctxt, + code, + Box::new(Expr::New(new_error_expr)), + ); + *expr = Expr::Call(call); + } + } - // Normalize line endings by converting Windows CRLF (\r\n) to Unix LF (\n) - // This ensures the comparison works consistently across different operating systems - let error_message = error_message.unwrap().replace("\r\n", "\n"); - - let code = self.errors.iter().find_map(|(key, value)| { - // We assume `errors.json` uses Unix LF (\n) as line endings - if *value == error_message { - Some(key) - } else { - None + fn visit_mut_class(&mut self, class: &mut Class) { + // Visit children first so any `new Error(...)` inside methods is still + // rewritten by `visit_mut_expr`. + class.visit_mut_children_with(self); + + // Only classes that extend a recognized Error class. + let super_class_name = match class.super_class.as_deref() { + Some(Expr::Ident(ident)) if is_error_class_name(ident.sym.as_str()) => { + ident.sym.as_str() } - }); + _ => return, + }; - if code.is_none() { - let new_error = serde_json::to_string(&NewError { error_message }).unwrap(); + // `AggregateError(errors, message)` takes the message as the second + // argument. All other recognized error classes take it as the first. + let message_arg_index = if super_class_name == "AggregateError" { + 1 + } else { + 0 + }; - let hash_hex = format!("{:x}", md5::compute(new_error.as_bytes())); - let file_path = format!("cwd/.errors/{}.json", &hash_hex[0..8]); + // Skip the injection if the class already declares `__NEXT_ERROR_CODE` + // itself. This respects manual overrides in classes whose code can't + // be derived statically from the `super(...)` message. + let declares_error_code = class.body.iter().any(|member| match member { + ClassMember::ClassProp(prop) => matches!( + &prop.key, + PropName::Ident(ident) if ident.sym.as_str() == "__NEXT_ERROR_CODE" + ), + _ => false, + }); + if declares_error_code { + return; + } - let _ = fs::create_dir_all("cwd/.errors"); - let _ = fs::write(&file_path, new_error); - } else { - let code = format!("E{}", code.unwrap()); - - // Mutate to Object.defineProperty(new Error(...), "__NEXT_ERROR_CODE", { value: - // "$code", enumerable: false }) - *expr = Expr::Call(CallExpr { - span: new_error_expr.span, - callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { - span: new_error_expr.span, - obj: Box::new(Expr::Ident(Ident::new( - "Object".into(), - new_error_expr.span, - Default::default(), - ))), - prop: MemberProp::Ident("defineProperty".into()), - }))), // Object.defineProperty( - args: vec![ - ExprOrSpread { - spread: None, - expr: Box::new(Expr::New(new_error_expr.clone())), // new Error(...) - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Lit(Lit::Str(Str { - span: new_error_expr.span, - value: "__NEXT_ERROR_CODE".into(), - raw: None, - }))), // "__NEXT_ERROR_CODE" - }, - ExprOrSpread { - spread: None, - expr: Box::new(Expr::Object(ObjectLit { - span: new_error_expr.span, - props: vec![ - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident("value".into()), - value: Box::new(Expr::Lit(Lit::Str(Str { - span: new_error_expr.span, - value: code.into(), - raw: None, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident("enumerable".into()), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: new_error_expr.span, - value: false, - }))), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident("configurable".into()), - value: Box::new(Expr::Lit(Lit::Bool(Bool { - span: new_error_expr.span, - value: true, - }))), - }))), - ], - })), // { value: "$code", enumerable: false } - }, - ], - type_args: None, - ctxt: new_error_expr.ctxt, - }); + // Find the first constructor with a body. + let ctor = class.body.iter_mut().find_map(|member| match member { + ClassMember::Constructor(Constructor { body: Some(_), .. }) => { + if let ClassMember::Constructor(ctor) = member { + Some(ctor) + } else { + None + } + } + _ => None, + }); + let Some(ctor) = ctor else { + return; + }; + let Some(body) = ctor.body.as_mut() else { + return; + }; + + // Locate the first top-level `super(arg)` statement. + let mut super_index: Option = None; + let mut super_info: Option<(Span, SyntaxContext, String)> = None; + for (i, stmt) in body.stmts.iter().enumerate() { + if let Stmt::Expr(ExprStmt { expr, .. }) = stmt + && let Expr::Call(CallExpr { + callee: Callee::Super(_), + args, + span, + ctxt, + .. + }) = &**expr + && let Some(message_arg) = args.get(message_arg_index) + && message_arg.spread.is_none() + { + let message = stringify_new_error_arg(&message_arg.expr, &self.resolved_bindings); + super_index = Some(i); + super_info = Some((*span, *ctxt, message)); + break; + } } + + let Some(stmt_index) = super_index else { + return; + }; + let (span, ctxt, message) = super_info.unwrap(); + + let Some(code) = self.lookup_or_emit(message) else { + return; + }; + + // Insert `Object.defineProperty(this, "__NEXT_ERROR_CODE", { ... })` + // immediately after the super call. + let call = self.build_define_property_call( + span, + ctxt, + code, + Box::new(Expr::This(ThisExpr { span })), + ); + let new_stmt = Stmt::Expr(ExprStmt { + span, + expr: Box::new(Expr::Call(call)), + }); + body.stmts.insert(stmt_index + 1, new_stmt); } } @@ -420,3 +539,155 @@ function test5() { } "# ); + +test_inline!( + Default::default(), + |_| visit_mut_pass(TransformVisitor { + errors: FxHashMap::from_iter([ + ("7".to_string(), "Timeout reached".to_string()), + ("8".to_string(), "Prefix: %s".to_string()), + ]), + resolved_bindings: FxHashMap::default(), + }), + subclass_super_messages, + // Input codes + r#" +class LiteralSuper extends Error { + constructor() { + super("Timeout reached"); + } +} + +class TemplateSuper extends Error { + constructor(x) { + super(`Prefix: ${x}`); + } +} + +class ExtendsKnownSubclass extends ApiError { + constructor() { + super("Timeout reached"); + this.extra = 1; + } +} + +class NoCtor extends Error {} + +class SpreadSuper extends Error { + constructor(...args) { + super(...args); + } +} + +class ExtendsUnknown extends Foo { + constructor() { + super("Timeout reached"); + } +} + +class SuperInIf extends Error { + constructor(cond) { + if (cond) { + super("Timeout reached"); + } else { + super("Timeout reached"); + } + } +} + +class UnknownMessage extends Error { + constructor() { + super("Not in errors.json"); + } +} + +class AggregateSubclass extends AggregateError { + constructor(errors) { + super(errors, "Timeout reached"); + } +} + +class ManualErrorCode extends Error { + __NEXT_ERROR_CODE = 'Manual'; + constructor(message) { + super(message); + } +} +"#, + // Output codes after transformed with plugin + r#" +class LiteralSuper extends Error { + constructor(){ + super("Timeout reached"); + Object.defineProperty(this, "__NEXT_ERROR_CODE", { + value: "E7", + enumerable: false, + configurable: true + }); + } +} +class TemplateSuper extends Error { + constructor(x){ + super(`Prefix: ${x}`); + Object.defineProperty(this, "__NEXT_ERROR_CODE", { + value: "E8", + enumerable: false, + configurable: true + }); + } +} +class ExtendsKnownSubclass extends ApiError { + constructor(){ + super("Timeout reached"); + Object.defineProperty(this, "__NEXT_ERROR_CODE", { + value: "E7", + enumerable: false, + configurable: true + }); + this.extra = 1; + } +} +class NoCtor extends Error { +} +class SpreadSuper extends Error { + constructor(...args){ + super(...args); + } +} +class ExtendsUnknown extends Foo { + constructor(){ + super("Timeout reached"); + } +} +class SuperInIf extends Error { + constructor(cond){ + if (cond) { + super("Timeout reached"); + } else { + super("Timeout reached"); + } + } +} +class UnknownMessage extends Error { + constructor(){ + super("Not in errors.json"); + } +} +class AggregateSubclass extends AggregateError { + constructor(errors){ + super(errors, "Timeout reached"); + Object.defineProperty(this, "__NEXT_ERROR_CODE", { + value: "E7", + enumerable: false, + configurable: true + }); + } +} +class ManualErrorCode extends Error { + __NEXT_ERROR_CODE = 'Manual'; + constructor(message){ + super(message); + } +} +"# +); diff --git a/docs/01-app/02-guides/migrating/from-vite.mdx b/docs/01-app/02-guides/migrating/from-vite.mdx index 9a34bc79cde9..f84262e5955b 100644 --- a/docs/01-app/02-guides/migrating/from-vite.mdx +++ b/docs/01-app/02-guides/migrating/from-vite.mdx @@ -533,6 +533,18 @@ aren’t supported by Next.js. You need to update their usage as follows: - `import.meta.env.DEV` ⇒ `process.env.NODE_ENV !== 'production'` - `import.meta.env.SSR` ⇒ `typeof window !== 'undefined'` +**`import.meta.glob`** is supported by Turbopack (the default Next.js bundler) with no changes needed. If you were using the `as` option (deprecated in Vite 5), replace it with `query`: + +```js +// Before (Vite) +const modules = import.meta.glob('./dir/*.txt', { as: 'raw' }) + +// After (Next.js / Turbopack) +const modules = import.meta.glob('./dir/*.txt', { query: '?raw' }) +``` + +See the [import.meta.glob docs](/docs/app/api-reference/turbopack#importmetaglob) for the full API. + Next.js also doesn't provide a built-in `BASE_URL` environment variable. However, you can still configure one, if you need it: diff --git a/docs/01-app/03-api-reference/08-turbopack.mdx b/docs/01-app/03-api-reference/08-turbopack.mdx index c1a8aa2f28cf..854407db3bee 100644 --- a/docs/01-app/03-api-reference/08-turbopack.mdx +++ b/docs/01-app/03-api-reference/08-turbopack.mdx @@ -137,6 +137,112 @@ Turbopack supports webpack-compatible magic comments for controlling import beha See [Lazy Loading](/docs/app/guides/lazy-loading#magic-comments) for usage examples. +## import.meta.glob + +Turbopack supports `import.meta.glob()`, a Vite-compatible API for importing multiple modules at once using glob patterns. The result is an object keyed by the file path relative to the calling file. + +```js +const modules = import.meta.glob('./dir/*.js') +// { +// './dir/foo.js': () => import('./dir/foo.js'), +// './dir/bar.js': () => import('./dir/bar.js'), +// } +``` + +> **Good to know:** `import.meta.glob` requires Turbopack. It is not available when using webpack. + +### Lazy loading (default) + +By default, each value in the result object is a thunk — a function that returns a `Promise` for the module. This enables lazy loading: + +```js +const modules = import.meta.glob('./dir/*.js') + +for (const path in modules) { + const module = await modules[path]() + console.log(path, module) +} +``` + +### Eager loading + +Pass `{ eager: true }` to import all matching modules up front. Each value is the module object directly instead of a thunk: + +```js +const modules = import.meta.glob('./dir/*.js', { eager: true }) + +for (const path in modules) { + console.log(path, modules[path].default) +} +``` + +### Named imports + +Use the `import` option to select a specific named export from each matched module. This works in both lazy and eager modes: + +```js +// Lazy: each value is () => Promise +const defaults = import.meta.glob('./dir/*.js', { import: 'default' }) + +// Eager: each value is the export value directly +const setups = import.meta.glob('./dir/*.js', { import: 'setup', eager: true }) +``` + +### Query strings + +Use the `query` option to append a query string to every import request. This is useful for loading files as raw strings or URLs: + +```js +const rawFiles = import.meta.glob('./dir/*.txt', { query: '?raw' }) +const urls = import.meta.glob('./dir/*.png', { query: '?url' }) +``` + +The `query` option also accepts an object. Keys and values are URL-encoded and joined into a query string: + +```js +const modules = import.meta.glob('./*.ts', { + query: { bar: 'foo', raw: true }, +}) +// equivalent to: { query: '?bar=foo&raw=true' } +``` + +### Multiple patterns and negation + +Pass an array of glob patterns as the first argument. Prefix a pattern with `!` to exclude matching files: + +```js +// Combine multiple directories +const modules = import.meta.glob(['./dir/*.js', './other/*.js']) + +// Exclude specific files +const withoutTests = import.meta.glob(['./src/**/*.js', '!**/*.test.js']) +``` + +### TypeScript + +TypeScript types for `import.meta.glob` are included in Next.js. They are available automatically when `"moduleResolution": "bundler"` (or `"node16"` / `"nodenext"`) is set in your `tsconfig.json`, which is the default for new Next.js projects. + +The return type differs based on the `eager` option: + +```ts +// Lazy (default) — Record Promise> +const lazy = import.meta.glob('./dir/*.ts') + +// Eager — Record +const eager = import.meta.glob('./dir/*.ts', { eager: true }) +``` + +### Options reference + +| Option | Type | Default | Description | +| -------- | --------------------------------------------- | ----------- | ---------------------------------------------------------------------- | +| `eager` | `boolean` | `false` | Import modules synchronously instead of returning thunks. | +| `import` | `string` | `undefined` | Named export to select from each matched module (e.g. `'default'`). | +| `query` | `string \| Record` | `undefined` | Query string (or object) to append to each import. | +| `base` | `string` | `undefined` | Override the base path used for resolving patterns and keying results. | + +> **Note:** The `as` option (deprecated in Vite 5) is not supported. Use `query: '?raw'` or `query: '?url'` instead. The legacy `import.meta.globEager()` API is also not supported — use `import.meta.glob('...', { eager: true })` instead. + ## Known gaps with webpack There are a number of non-trivial behavior differences between webpack and Turbopack that are important to be aware of when migrating an application. Generally, these are less of a concern for new applications. diff --git a/docs/02-pages/03-building-your-application/06-configuring/12-error-handling.mdx b/docs/02-pages/03-building-your-application/06-configuring/12-error-handling.mdx index eccf5e98b67e..6c32aed00c13 100644 --- a/docs/02-pages/03-building-your-application/06-configuring/12-error-handling.mdx +++ b/docs/02-pages/03-building-your-application/06-configuring/12-error-handling.mdx @@ -9,11 +9,6 @@ This documentation explains how you can handle development, server-side, and cli When there is a runtime error during the development phase of your Next.js application, you will encounter an **overlay**. It is a modal that covers the webpage. It is **only** visible when the development server runs using `next dev` via `pnpm dev`, `npm run dev`, `yarn dev`, or `bun dev` and will not be shown in production. Fixing the error will automatically dismiss the overlay. -Here is an example of an overlay: - -{/* TODO UPDATE SCREENSHOT */} -![Example of an overlay when in development mode](https://assets.vercel.com/image/upload/v1645118290/docs-assets/static/docs/error-handling/overlay.png) - ## Handling Server Errors Next.js provides a static 500 page by default to handle server-side errors that occur in your application. You can also [customize this page](/docs/pages/building-your-application/routing/custom-error#customizing-the-500-page) by creating a `pages/500.js` file. diff --git a/packages/next/errors.json b/packages/next/errors.json index bdde7f2ae0bf..42d7c3806490 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -1169,5 +1169,14 @@ "1168": "Route \"%s\": Next.js encountered runtime data such as \\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` inside \\`generateMetadata\\`, or you have file-based metadata such as icons that depend on dynamic params segments. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata", "1169": "Route \"%s\": Next.js encountered uncached or runtime data during the initial render.\\n\\n\\`fetch(...)\\`, \\`cookies()\\`, \\`headers()\\`, \\`params\\`, \\`searchParams\\`, or \\`connection()\\` accessed outside of \\`\\` blocks navigation, leading to a slower user experience.\\n\\nWays to fix this:\\n - Cache the data access with \\`\"use cache\"\\`\\n - Move the data access into a child component within a boundary\\n - Use \\`generateStaticParams\\` to make route params static\\n - Set \\`export const instant = false\\` to allow a blocking route\\n\\nLearn more: https://nextjs.org/docs/messages/blocking-route", "1170": "Route \"%s\": Next.js encountered uncached data such as \\`fetch(...)\\` or \\`connection()\\` inside \\`generateMetadata\\`. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata", - "1171": "Response body exceeded maximum size of %s bytes" + "1171": "Response body exceeded maximum size of %s bytes", + "1172": "The request.ua has been removed in favour of \\`userAgent\\` function.\n Read more: https://nextjs.org/docs/messages/middleware-parse-user-agent\n ", + "1173": "Nested Middleware is not allowed, found:\\n%s\\nPlease move your code to a single file at %s instead.\\nRead More - https://nextjs.org/docs/messages/nested-middleware", + "1174": "Method unavailable on `ReadonlyURLSearchParams`. Read more: https://nextjs.org/docs/app/api-reference/functions/use-search-params#updating-searchparams", + "1175": "%s\\n\\nLearn more: %s", + "1176": "Headers cannot be modified. Read more: https://nextjs.org/docs/app/api-reference/functions/headers", + "1177": "The middleware \"%s\" accepts an async API directly with the form:\n \n export function middleware(request, event) {\n return NextResponse.redirect('/new-location')\n }\n \n Read more: https://nextjs.org/docs/messages/middleware-new-signature\n ", + "1178": "The request.page has been deprecated in favour of \\`URLPattern\\`.\n Read more: https://nextjs.org/docs/messages/middleware-request-page\n ", + "1179": "Invariant: %s This is a bug in Next.js.", + "1180": "Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#options" } diff --git a/packages/next/next_error_code_swc_plugin.wasm b/packages/next/next_error_code_swc_plugin.wasm index ea38c0de9360..671321be5f9d 100755 Binary files a/packages/next/next_error_code_swc_plugin.wasm and b/packages/next/next_error_code_swc_plugin.wasm differ diff --git a/packages/next/types/global.d.ts b/packages/next/types/global.d.ts index e3d3ebc8c602..a9ee2a68eb72 100644 --- a/packages/next/types/global.d.ts +++ b/packages/next/types/global.d.ts @@ -81,6 +81,23 @@ interface TurbopackHotApi { readonly data: Record } +interface ImportMetaGlobOptions { + /** Import modules eagerly (synchronously). Default: `false`. */ + eager?: boolean + /** + * Named export to select from each matched module. + * Use `'default'` for the default export, or `'*'` for the full namespace. + */ + import?: string + /** + * Query string to append to each import request (e.g. `'?raw'`, `'?url'`). + * Can also be an object whose key-value pairs are serialized to a query string. + */ + query?: string | Record + /** Override the base path used for resolving patterns and keying results. */ + base?: string +} + interface ImportMeta { /** * The HMR API for ESM modules when using Turbopack. @@ -88,6 +105,29 @@ interface ImportMeta { * Only available in development mode. */ turbopackHot?: TurbopackHotApi + + /** + * Import multiple modules at once using glob patterns (Turbopack only). + * + * @example + * // Lazy (default) — values are thunks: () => Promise + * const modules = import.meta.glob('./dir/*.js') + * + * // Eager — values are module objects + * const modules = import.meta.glob('./dir/*.js', { eager: true }) + */ + glob( + pattern: string | string[], + options: ImportMetaGlobOptions & { eager: true } + ): Record + glob( + pattern: string | string[], + options?: ImportMetaGlobOptions & { eager?: false | undefined } + ): Record Promise> + glob( + pattern: string | string[], + options?: ImportMetaGlobOptions + ): Record | Record Promise> } interface Window { diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index 95dddb6fbb29..b309c5492d4a 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -58,6 +58,7 @@ turbopack-core = { workspace = true } turbopack-resolve = { workspace = true } turbopack-swc-utils = { workspace = true } url = { workspace = true } +urlencoding = { workspace = true } swc_core = { workspace = true, features = [ "ecma_ast", diff --git a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs index 12454c4c89fb..981a5e4ed0b6 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs @@ -172,7 +172,11 @@ pub fn parse_import_meta_glob( } Some("import") => { if let Some(s) = val.as_str() { - import = Some(s.into()); + // `import: '*'` means namespace import (whole module), + // which is the default behavior — no need to store it. + if s != "*" { + import = Some(s.into()); + } } else { handler.span_warn_with_code( span, @@ -191,6 +195,51 @@ pub fn parse_import_meta_glob( format!("?{s}").into() }; query = Some(q); + } else if let JsValue::Object { parts, .. } = val { + // Support object form: { query: { bar: 'foo', raw: true } } + // Serializes to "?bar=foo&raw=true" with URL-encoding. + use crate::analyzer::ObjectPart; + let mut pairs: Vec = Vec::new(); + for part in parts { + if let ObjectPart::KeyValue(k, v) = part { + if let Some(k_str) = k.as_str() { + let enc_key = urlencoding::encode(k_str); + if let Some(v_str) = v.as_str() { + let enc_val = urlencoding::encode(v_str); + pairs.push(format!("{enc_key}={enc_val}")); + } else if let Some(v_bool) = v.as_bool() { + pairs.push(format!("{enc_key}={v_bool}")); + } else { + handler.span_warn_with_code( + span, + &format!( + "import.meta.glob() 'query' object \ + value for key '{k_str}' must be a \ + constant string or boolean, ignoring" + ), + diagnostic_id.clone(), + ); + } + } else { + handler.span_warn_with_code( + span, + "import.meta.glob() 'query' object keys must \ + be constant strings", + diagnostic_id.clone(), + ); + } + } else { + handler.span_warn_with_code( + span, + "import.meta.glob() 'query' object must only \ + contain constant key-value pairs", + diagnostic_id.clone(), + ); + } + } + if !pairs.is_empty() { + query = Some(format!("?{}", pairs.join("&")).into()); + } } else { handler.span_warn_with_code( span, @@ -244,11 +293,12 @@ pub fn parse_import_meta_glob( } } _ => { - handler.span_warn_with_code( + handler.span_err_with_code( span, "import.meta.glob() second argument must be an object literal", diagnostic_id.clone(), ); + return None; } } } @@ -305,7 +355,7 @@ async fn flatten_read_glob(result: &ReadGlobResult) -> Result)> = Vec::new(); collect_files(result, "", &mut files); - // Resolve child directories + // Resolve child directories (skip dot-directories like .git, .next, etc.) for (segment, inner_vc) in &result.inner { let child_prefix = segment.to_string(); let inner = inner_vc.await?; diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/dir/foo.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/dir/foo.js new file mode 100644 index 000000000000..7e942cf45c8a --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/dir/foo.js @@ -0,0 +1 @@ +export default 'foo' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js new file mode 100644 index 000000000000..63e9337987d4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js @@ -0,0 +1,23 @@ +// These calls trigger compile-time errors (verified via issue snapshots). +// They are wrapped in functions to avoid runtime evaluation errors, since +// invalid glob calls are not transformed and would throw at runtime. + +function getTooMany() { + return import.meta.glob('./dir/*.js', {}, {}) +} + +function getNumPattern() { + return import.meta.glob(123) +} + +function getNonObjOptions() { + return import.meta.glob('./dir/*.js', 'eager') +} + +it('should emit errors for invalid glob calls', () => { + // The compile-time errors are the main verification (issue snapshots). + // At runtime, these untransformed calls would throw. + expect(() => getTooMany()).toThrow() + expect(() => getNumPattern()).toThrow() + expect(() => getNonObjOptions()).toThrow() +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() first argument m-501902.txt b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() first argument m-501902.txt new file mode 100644 index 000000000000..f233127bd85f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() first argument m-501902.txt @@ -0,0 +1,13 @@ +warning - [analysis] /turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js:10:9 TP1008 import.meta.glob() first argument must be a string literal or array of string literals + + 6 | return import.meta.glob('./dir/*.js', {}, {}) + 7 | } + 8 | + 9 | function getNumPattern() { + + v-------------------v + 10 + return import.meta.glob(123) + + ^-------------------^ + 11 | } + 12 | + 13 | function getNonObjOptions() { + 14 | return import.meta.glob('./dir/*.js', 'eager') \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() requires 1 or 2 -247c72.txt b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() requires 1 or 2 -247c72.txt new file mode 100644 index 000000000000..ee10070bcb2c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() requires 1 or 2 -247c72.txt @@ -0,0 +1,13 @@ +warning - [analysis] /turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js:6:9 TP1008 import.meta.glob() requires 1 or 2 arguments + + 2 | // They are wrapped in functions to avoid runtime evaluation errors, since + 3 | // invalid glob calls are not transformed and would throw at runtime. + 4 | + 5 | function getTooMany() { + + v------------------------------------v + 6 + return import.meta.glob('./dir/*.js', {}, {}) + + ^------------------------------------^ + 7 | } + 8 | + 9 | function getNumPattern() { + 10 | return import.meta.glob(123) \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() second argument -c74f94.txt b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() second argument -c74f94.txt new file mode 100644 index 000000000000..a997abf53967 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/issues/__l___TP1008__ import.meta.glob() second argument -c74f94.txt @@ -0,0 +1,13 @@ +error - [analysis] /turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/input/index.js:14:9 TP1008 import.meta.glob() second argument must be an object literal + + 10 | return import.meta.glob(123) + 11 | } + 12 | + 13 | function getNonObjOptions() { + + v-------------------------------------v + 14 + return import.meta.glob('./dir/*.js', 'eager') + + ^-------------------------------------^ + 15 | } + 16 | + 17 | it('should emit errors for invalid glob calls', () => { + 18 | // The compile-time errors are the main verification (issue snapshots). \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-warnings/input/dir3/baz.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-warnings/input/dir3/baz.js new file mode 100644 index 000000000000..5d969253b096 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-warnings/input/dir3/baz.js @@ -0,0 +1 @@ +export default 'baz' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/.foo/hidden.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/.foo/hidden.js new file mode 100644 index 000000000000..5aa4e80ced0f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/.foo/hidden.js @@ -0,0 +1 @@ +export default 'hidden' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js index 40241f4dc1f0..15b4edd09ad3 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js @@ -62,3 +62,66 @@ it('should support multiple patterns across directories', () => { expect(keys).toEqual(['./dir/bar.js', './dir/foo.js', './other/baz.js']) expect(multiModules['./other/baz.js'].default).toBe('baz') }) + +// import: '*' (namespace import) — should return the whole module namespace +// Uses ./other/*.js to avoid colliding with the eager test above (same pattern + eager + no import) +const namespaceModules = import.meta.glob('./other/*.js', { + import: '*', + eager: true, +}) + +it('should return the whole module namespace with import: "*"', () => { + const keys = Object.keys(namespaceModules).sort() + expect(keys).toEqual(['./other/baz.js']) + // Each value is the full module namespace object + expect(namespaceModules['./other/baz.js'].default).toBe('baz') + expect(namespaceModules['./other/baz.js'].value).toBe(7) +}) + +// Negative pattern combined with query +const queryWithNeg = import.meta.glob(['./dir/*.js', '!**/bar.js'], { + query: '?raw', + import: '*', +}) + +it('should support query option with negative patterns', () => { + const keys = Object.keys(queryWithNeg) + expect(keys).toEqual(['./dir/foo.js']) + // Values are thunks (lazy mode) + expect(typeof queryWithNeg['./dir/foo.js']).toBe('function') +}) + +// query as object literal — serialized to query string +const queryObjModules = import.meta.glob('./dir/*.js', { + query: { bar: 'foo', raw: true }, +}) + +it('should support query as object literal', () => { + const keys = Object.keys(queryObjModules).sort() + expect(keys).toEqual(['./dir/bar.js', './dir/foo.js']) + // Values are thunks (lazy) + expect(typeof queryObjModules['./dir/foo.js']).toBe('function') +}) + +// Dotfile directories are matched by wildcards (not excluded) +const dotfileGlob = import.meta.glob(['./**/*.js', '!./index.js'], { + eager: true, +}) + +it('should include dotfile directories with wildcard patterns', () => { + const keys = Object.keys(dotfileGlob).sort() + expect(keys).toEqual([ + './.foo/hidden.js', + './dir/bar.js', + './dir/foo.js', + './other/baz.js', + ]) +}) + +// Dotfile directories targeted explicitly should be included +const dotfileExplicit = import.meta.glob('./.foo/*.js', { eager: true }) + +it('should include dotfile directories when explicitly targeted', () => { + const keys = Object.keys(dotfileExplicit) + expect(keys).toEqual(['./.foo/hidden.js']) +})