diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 4040c402df..3ea786c900 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -519,6 +519,12 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("async_hooks", "enterWith", true, None), method("async_hooks", "exit", true, None), method("async_hooks", "disable", true, None), + // AsyncResource — Nest's `@nestjs/core` request-scoped DI uses + // this to bind a callback to a synthetic async resource. The + // stub in `node:async_hooks` JS module satisfies callers that + // only need the `runInAsyncScope` shape. + class("async_hooks", "AsyncResource"), + class("async_hooks", "AsyncLocalStorage"), method("decimal.js", "plus", true, None), method("decimal.js", "minus", true, None), method("decimal.js", "times", true, None), @@ -1654,6 +1660,13 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("util", "isDeepStrictEqual", false, None), class("util", "TextEncoder"), class("util", "TextDecoder"), + // util.types — Node's runtime type-introspection namespace. Required + // for `@nestjs/core` / rxjs internal dispatch (PR #754 fixture). The + // backing object lives in the `node:util` stub in + // perry-jsruntime/src/modules.rs and answers every is* probe with + // `false` (a safe default — no Perry value type matches Node's + // privileged BoxedPrimitive/Proxy/external introspection cases). + property("util", "types"), // --- stream (Web Streams API + Node stream classes — see // perry-stdlib/src/streams.rs and perry-ext-streams) --- class("stream", "Readable"), diff --git a/crates/perry-codegen-arkts/src/lib.rs b/crates/perry-codegen-arkts/src/lib.rs index add8e8684a..18de5fca7a 100644 --- a/crates/perry-codegen-arkts/src/lib.rs +++ b/crates/perry-codegen-arkts/src/lib.rs @@ -8494,6 +8494,7 @@ mod tests { name: "item".to_string(), ty: perry_types::Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }; let inner_text = nmc("Text", vec![Expr::LocalGet(42)]); @@ -9027,6 +9028,7 @@ mod tests { name: "item".to_string(), ty: perry_types::Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }; let inner_text = nmc("Text", vec![Expr::LocalGet(99)]); diff --git a/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs b/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs index 1712271b89..26bd5fe2f6 100644 --- a/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs +++ b/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs @@ -84,6 +84,7 @@ fn param(id: LocalId, name: &str) -> Param { name: name.to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: false, } } diff --git a/crates/perry-codegen/src/codegen.rs b/crates/perry-codegen/src/codegen.rs index 32bf524d40..da217b78f7 100644 --- a/crates/perry-codegen/src/codegen.rs +++ b/crates/perry-codegen/src/codegen.rs @@ -685,6 +685,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> init: None, is_private: false, is_readonly: false, + decorators: Vec::new(), }) .collect(), constructor: None, @@ -711,6 +712,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> setters: Vec::new(), static_fields: Vec::new(), static_methods: Vec::new(), + decorators: Vec::new(), is_exported: false, aliases: Vec::new(), }; @@ -1832,9 +1834,18 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> // Names are scoped by module prefix to avoid cross-module collisions. let mut func_names: HashMap = HashMap::new(); let mut func_signatures: HashMap = HashMap::new(); + let mut func_synthetic_arguments: std::collections::HashSet = + std::collections::HashSet::new(); for f in &hir.functions { func_names.insert(f.id, scoped_fn_name(&module_prefix, &f.name)); let has_rest = f.params.iter().any(|p| p.is_rest); + if f.params + .last() + .map(|p| p.is_rest && p.name == "arguments") + .unwrap_or(false) + { + func_synthetic_arguments.insert(f.id); + } let returns_number = matches!( f.return_type, perry_types::Type::Number | perry_types::Type::Int32 @@ -2101,6 +2112,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_boxed_vars, &closure_rest_params, &cross_module, @@ -2214,6 +2226,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_prefix, &module_boxed_vars, &module_local_types, @@ -2244,6 +2257,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_boxed_vars, &closure_rest_params, &cross_module, @@ -2271,6 +2285,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_boxed_vars, &closure_rest_params, &cross_module, @@ -2295,6 +2310,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_boxed_vars, &closure_rest_params, &cross_module, @@ -2351,6 +2367,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> name: format!("__forward_arg{}", i), ty: perry_types::Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }); } @@ -2368,6 +2385,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> name: format!("__forward_arg{}", i), ty: perry_types::Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }); } @@ -2414,6 +2432,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_boxed_vars, &closure_rest_params, &cross_module, @@ -2438,6 +2457,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_prefix, &module_boxed_vars, &closure_rest_params, @@ -2734,6 +2754,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &static_field_globals, &class_ids, &func_signatures, + &func_synthetic_arguments, &module_prefix, opts.is_entry_module, &opts.non_entry_module_prefixes, @@ -2897,6 +2918,7 @@ fn compile_function( static_field_globals: &HashMap<(String, String), String>, class_ids: &HashMap, func_signatures: &HashMap, + func_synthetic_arguments: &std::collections::HashSet, module_boxed_vars: &std::collections::HashSet, closure_rest_params: &HashMap, cross_module: &CrossModuleCtx, @@ -3048,6 +3070,7 @@ fn compile_function( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params, @@ -3216,6 +3239,7 @@ fn compile_closure( static_field_globals: &HashMap<(String, String), String>, class_ids: &HashMap, func_signatures: &HashMap, + func_synthetic_arguments: &std::collections::HashSet, module_prefix: &str, module_boxed_vars: &std::collections::HashSet, module_local_types: &HashMap, @@ -3429,6 +3453,7 @@ fn compile_closure( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars: closure_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params, @@ -3542,6 +3567,7 @@ fn compile_method( static_field_globals: &HashMap<(String, String), String>, class_ids: &HashMap, func_signatures: &HashMap, + func_synthetic_arguments: &std::collections::HashSet, module_boxed_vars: &std::collections::HashSet, closure_rest_params: &HashMap, cross_module: &CrossModuleCtx, @@ -3655,6 +3681,7 @@ fn compile_method( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars: method_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params, @@ -3920,6 +3947,7 @@ fn compile_module_entry( static_field_globals: &HashMap<(String, String), String>, class_ids: &HashMap, func_signatures: &HashMap, + func_synthetic_arguments: &std::collections::HashSet, module_prefix: &str, is_entry: bool, non_entry_module_prefixes: &[String], @@ -4095,6 +4123,7 @@ fn compile_module_entry( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars: main_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params: closure_rest_params, @@ -4375,6 +4404,7 @@ fn compile_module_entry( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars: init_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params: closure_rest_params, @@ -4718,18 +4748,20 @@ fn emit_string_pool( if *class_name != class.name { continue; } + // Imported class stubs carry id == 0 (they're typed-name + // placeholders for cross-module dispatch; the defining module's init + // registers their methods). Skip them here so we don't re-emit the + // registration. Previously this filter was `method.body.is_empty()`; + // the id check is equivalent for stubs and also catches getter/setter + // and property-decorator init that legitimately has an empty body. + if class.id == 0 { + continue; + } let cid = match class_ids.get(class_name) { Some(&c) if c != 0 => c, _ => continue, }; for method in &class.methods { - // Skip imported class stubs: their `body` is empty - // (they're just typed-name placeholders for cross-module - // dispatch). The defining module's init registers them. - // Local methods always have non-empty bodies. - if method.body.is_empty() { - continue; - } let llvm_name = format!( "perry_method_{}__{}__{}", module_prefix, @@ -4821,16 +4853,20 @@ fn emit_string_pool( if *class_name != class.name { continue; } + // Imported class stubs carry id == 0 (they're typed-name + // placeholders for cross-module dispatch; the defining module's init + // registers their methods). Skip them here so we don't re-emit the + // registration. Previously this filter was `method.body.is_empty()`; + // the id check is equivalent for stubs and also catches getter/setter + // and property-decorator init that legitimately has an empty body. + if class.id == 0 { + continue; + } let cid = match class_ids.get(class_name).copied() { Some(c) if c != 0 => c, _ => continue, }; for (prop, getter_fn) in &class.getters { - // Skip imported class stubs: their `body` is empty (the - // defining module's init registers them). - if getter_fn.body.is_empty() { - continue; - } // The local-emit path at codegen.rs:1858 prepends `__get_` // to the HIR-assigned getter name (`get_`), giving // the LLVM symbol `perry_method_____)>`. @@ -4882,16 +4918,20 @@ fn emit_string_pool( if *class_name != class.name { continue; } + // Imported class stubs carry id == 0 (they're typed-name + // placeholders for cross-module dispatch; the defining module's init + // registers their methods). Skip them here so we don't re-emit the + // registration. Previously this filter was `method.body.is_empty()`; + // the id check is equivalent for stubs and also catches getter/setter + // and property-decorator init that legitimately has an empty body. + if class.id == 0 { + continue; + } let cid = match class_ids.get(class_name).copied() { Some(c) if c != 0 => c, _ => continue, }; for (prop, setter_fn) in &class.setters { - // Skip imported class stubs (their body is empty — the defining - // module's init registers them). - if setter_fn.body.is_empty() { - continue; - } let inner = format!("__set_{}", setter_fn.name); let llvm_name = format!( "perry_method_{}__{}__{}", @@ -5004,6 +5044,7 @@ fn compile_static_method( static_field_globals: &HashMap<(String, String), String>, class_ids: &HashMap, func_signatures: &HashMap, + func_synthetic_arguments: &std::collections::HashSet, module_prefix: &str, module_boxed_vars: &std::collections::HashSet, closure_rest_params: &HashMap, @@ -5104,6 +5145,7 @@ fn compile_static_method( class_keys_globals: &cross_module.class_keys_globals, imported_class_ctors: &cross_module.imported_class_ctors, func_signatures, + func_synthetic_arguments, boxed_vars: static_boxed_vars, prealloc_boxes: std::collections::HashSet::new(), closure_rest_params, diff --git a/crates/perry-codegen/src/collectors.rs b/crates/perry-codegen/src/collectors.rs index cbf1e734a4..909b9a24dc 100644 --- a/crates/perry-codegen/src/collectors.rs +++ b/crates/perry-codegen/src/collectors.rs @@ -860,6 +860,63 @@ pub(crate) fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet { + walk(key, out); + walk(value, out); + walk(target, out); + if let Some(property_key) = property_key { + walk(property_key, out); + } + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } + | Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + walk(key, out); + walk(target, out); + if let Some(property_key) = property_key { + walk(property_key, out); + } + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } + | Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + walk(target, out); + if let Some(property_key) = property_key { + walk(property_key, out); + } + } Expr::Closure { body, captures, .. } => { // Closure literals don't introduce captures into the outer // scope, but their explicit captures + body references may diff --git a/crates/perry-codegen/src/expr.rs b/crates/perry-codegen/src/expr.rs index d37ace8431..3e9e6a4ed5 100644 --- a/crates/perry-codegen/src/expr.rs +++ b/crates/perry-codegen/src/expr.rs @@ -265,6 +265,11 @@ pub(crate) struct FnCtx<'a> { /// has_rest_param)`. Used by FuncRef call sites to know whether /// to bundle trailing arguments into a rest array. pub func_signatures: &'a std::collections::HashMap, + /// Function declarations where Perry appended a synthetic trailing + /// `arguments` binding. Unlike a real rest parameter, it must receive + /// every actual argument while fixed parameters still receive their + /// normal positional values. + pub func_synthetic_arguments: &'a std::collections::HashSet, /// LocalIds that must be stored in heap boxes (`js_box_alloc`) /// instead of stack allocas. A local gets boxed when at least /// one closure captures it AND it's written to (either by the @@ -10778,6 +10783,148 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // the compiler folds to a compile-time bool. Return target. lower_expr(ctx, target) } + Expr::ReflectDefineMetadata { + key, + value, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let v = lower_expr(ctx, value)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_define_metadata", + &[(DOUBLE, &k), (DOUBLE, &v), (DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_get_metadata", + &[(DOUBLE, &k), (DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_get_own_metadata", + &[(DOUBLE, &k), (DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectHasMetadata { + key, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_has_metadata", + &[(DOUBLE, &k), (DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_has_own_metadata", + &[(DOUBLE, &k), (DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } => { + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_get_metadata_keys", + &[(DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_get_own_metadata_keys", + &[(DOUBLE, &t), (DOUBLE, &p)], + )) + } + Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + let k = lower_expr(ctx, key)?; + let t = lower_expr(ctx, target)?; + let p = property_key + .as_ref() + .map(|p| lower_expr(ctx, p)) + .transpose()? + .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + Ok(ctx.block().call( + DOUBLE, + "js_reflect_delete_metadata", + &[(DOUBLE, &k), (DOUBLE, &t), (DOUBLE, &p)], + )) + } // Issue #100: compile-time-resolved dynamic `import()`. // @@ -11822,6 +11969,20 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { )) } + Expr::JsCallValue { callee, args } => { + let func_dbl = lower_expr(ctx, callee)?; + let mut lowered_args: Vec = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let (args_ptr, args_len_str) = lower_js_args_array(ctx, &lowered_args); + Ok(ctx.block().call( + DOUBLE, + "js_call_value", + &[(DOUBLE, &func_dbl), (PTR, &args_ptr), (I64, &args_len_str)], + )) + } + Expr::JsGetProperty { object, property_name, diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index 583b185423..907dc48590 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -695,7 +695,27 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R let sig = ctx.func_signatures.get(fid).copied(); let (declared_count, has_rest, _) = sig.unwrap_or((args.len(), false, false)); let mut lowered: Vec = Vec::with_capacity(declared_count); - if has_rest { + if has_rest && ctx.func_synthetic_arguments.contains(fid) { + let fixed_count = declared_count.saturating_sub(1); + let undef_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + for idx in 0..fixed_count { + if let Some(arg) = args.get(idx) { + lowered.push(lower_expr(ctx, arg)?); + } else { + lowered.push(undef_lit.clone()); + } + } + + let cap = (args.len() as u32).to_string(); + let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + for a in args { + let v = lower_expr(ctx, a)?; + let blk = ctx.block(); + current = blk.call(I64, "js_array_push_f64", &[(I64, ¤t), (DOUBLE, &v)]); + } + let arguments_box = nanbox_pointer_inline(ctx.block(), ¤t); + lowered.push(arguments_box); + } else if has_rest { // Rest is always the LAST declared param. Pass the // first (declared_count - 1) args as-is, then bundle // the rest into an array. diff --git a/crates/perry-codegen/src/runtime_decls.rs b/crates/perry-codegen/src/runtime_decls.rs index eee9c6872c..b4d37a7a1f 100644 --- a/crates/perry-codegen/src/runtime_decls.rs +++ b/crates/perry-codegen/src/runtime_decls.rs @@ -1531,6 +1531,34 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { DOUBLE, &[DOUBLE, DOUBLE, DOUBLE], ); + module.declare_function( + "js_reflect_define_metadata", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_reflect_get_metadata", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_reflect_get_own_metadata", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_reflect_has_metadata", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]); + module.declare_function( + "js_reflect_has_own_metadata", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); + module.declare_function("js_reflect_get_metadata_keys", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function( + "js_reflect_get_own_metadata_keys", + DOUBLE, + &[DOUBLE, DOUBLE], + ); + module.declare_function( + "js_reflect_delete_metadata", + DOUBLE, + &[DOUBLE, DOUBLE, DOUBLE], + ); declare_stdlib_ffi(module); } @@ -2343,6 +2371,7 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) { // ========== Closures / functions ========== module.declare_function("js_call_function", DOUBLE, &[I64, I64, I64, I64, I64]); module.declare_function("js_call_method", DOUBLE, &[DOUBLE, I64, I64, I64, I64]); + module.declare_function("js_call_value", DOUBLE, &[DOUBLE, I64, I64]); module.declare_function("js_closure_call_array", DOUBLE, &[I64, I64, I64]); module.declare_function( "js_closure_call_apply_with_spread", @@ -2426,6 +2455,7 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) { // Lets `typeof obj.method === "function"` and `let f = obj.method; f(args)` // dispatch through CLASS_VTABLE_REGISTRY instead of returning undefined. module.declare_function("js_class_method_bind", DOUBLE, &[DOUBLE, I64, I64]); + module.declare_function("js_class_prototype_method_value", DOUBLE, &[DOUBLE, DOUBLE]); // #519: read the implicit `this` thread-local set by // `js_native_call_method`'s field-scan dispatch when invoking a // closure-typed class field method-style. `Expr::This` codegen reads diff --git a/crates/perry-hir/examples/stable_hash_cross_process.rs b/crates/perry-hir/examples/stable_hash_cross_process.rs index d9aed823ad..c1447e6bb0 100644 --- a/crates/perry-hir/examples/stable_hash_cross_process.rs +++ b/crates/perry-hir/examples/stable_hash_cross_process.rs @@ -67,6 +67,7 @@ fn build_canonical() -> Module { name: "n".to_string(), ty: Type::Number, default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::Number, diff --git a/crates/perry-hir/src/ir.rs b/crates/perry-hir/src/ir.rs index bd7da61e5f..4cf7a6f2b0 100644 --- a/crates/perry-hir/src/ir.rs +++ b/crates/perry-hir/src/ir.rs @@ -667,6 +667,8 @@ pub struct Class { pub static_fields: Vec, /// Static methods pub static_methods: Vec, + /// Legacy TypeScript decorators applied to the class. + pub decorators: Vec, /// Whether this class is exported from the module pub is_exported: bool, /// Self-binding aliases for class-expression bindings: @@ -688,6 +690,8 @@ pub struct ClassField { pub init: Option, pub is_private: bool, pub is_readonly: bool, + /// Legacy TypeScript decorators applied to this property. + pub decorators: Vec, } /// A global variable @@ -707,6 +711,10 @@ pub struct Decorator { pub name: String, /// Arguments if this is a decorator factory call (e.g., @log("prefix") -> args = ["prefix"]) pub args: Vec, + /// True for decorator factories (`@dec(...)`), false for bare decorators (`@dec`). + pub is_factory: bool, + /// True for `@Reflect.metadata(key, value)`, which Perry lowers directly. + pub is_reflect_metadata: bool, } /// A function definition @@ -753,6 +761,8 @@ pub struct Param { pub name: String, pub ty: Type, pub default: Option, + /// Legacy TypeScript decorators applied to this parameter. + pub decorators: Vec, /// True if this is a rest parameter (...args) pub is_rest: bool, } @@ -2320,6 +2330,14 @@ pub enum Expr { args: Vec, }, + /// Call a V8 JavaScript function value + JsCallValue { + /// JS handle to the function value + callee: Box, + /// Arguments to pass to the function + args: Vec, + }, + /// Get a property from a V8 JavaScript object JsGetProperty { /// The object to get the property from @@ -2436,6 +2454,45 @@ pub enum Expr { descriptor: Box, }, ReflectGetPrototypeOf(Box), + ReflectDefineMetadata { + key: Box, + value: Box, + target: Box, + property_key: Option>, + }, + ReflectGetMetadata { + key: Box, + target: Box, + property_key: Option>, + }, + ReflectGetOwnMetadata { + key: Box, + target: Box, + property_key: Option>, + }, + ReflectHasMetadata { + key: Box, + target: Box, + property_key: Option>, + }, + ReflectHasOwnMetadata { + key: Box, + target: Box, + property_key: Option>, + }, + ReflectGetMetadataKeys { + target: Box, + property_key: Option>, + }, + ReflectGetOwnMetadataKeys { + target: Box, + property_key: Option>, + }, + ReflectDeleteMetadata { + key: Box, + target: Box, + property_key: Option>, + }, /// Issue #100: dynamic `import()` call whose path argument the /// const-folder resolved to a finite set of module sources. Lowered diff --git a/crates/perry-hir/src/js_transform.rs b/crates/perry-hir/src/js_transform.rs index 205067ad36..e38806ac3f 100644 --- a/crates/perry-hir/src/js_transform.rs +++ b/crates/perry-hir/src/js_transform.rs @@ -484,6 +484,7 @@ fn is_js_value_expr(expr: &Expr, tracker: &JsValueTracker) -> bool { Expr::JsGetExport { .. } => true, Expr::JsCallFunction { .. } => true, Expr::JsCallMethod { .. } => true, + Expr::JsCallValue { .. } => true, Expr::JsGetProperty { .. } => true, Expr::JsNew { .. } => true, Expr::JsNewFromHandle { .. } => true, @@ -518,6 +519,7 @@ fn is_js_object_expr( Expr::JsGetExport { .. } => true, Expr::JsCallFunction { .. } => true, Expr::JsCallMethod { .. } => true, + Expr::JsCallValue { .. } => true, Expr::JsGetProperty { .. } => true, Expr::JsNew { .. } => true, Expr::JsNewFromHandle { .. } => true, @@ -635,8 +637,45 @@ fn transform_expr( } } - // Not a JS import call, transform normally + // Call through a JavaScript function value, e.g. a decorator factory + // result from `@nestjs/common` (`const dec = Injectable(); dec(target)`). transform_expr(callee, js_imports, extern_func_to_js, local_name_to_js, tracker); + if is_js_object_expr(callee, tracker, extern_func_to_js) { + let transformed_args: Vec = args + .iter_mut() + .map(|arg| { + if let Expr::Closure { params, body, .. } = arg { + let param_count = params.len(); + let mut closure_tracker = tracker.clone(); + for param in params.iter() { + closure_tracker.mark_js_local(param.id); + } + transform_stmts( + body, + js_imports, + extern_func_to_js, + local_name_to_js, + &mut closure_tracker, + ); + Expr::JsCreateCallback { + closure: Box::new(std::mem::replace(arg, Expr::Undefined)), + param_count, + } + } else { + transform_expr(arg, js_imports, extern_func_to_js, local_name_to_js, tracker); + std::mem::replace(arg, Expr::Undefined) + } + }) + .collect(); + let callee_expr = std::mem::replace(callee.as_mut(), Expr::Undefined); + *expr = Expr::JsCallValue { + callee: Box::new(callee_expr), + args: transformed_args, + }; + return; + } + + // Not a JS import call, transform normally for arg in args.iter_mut() { transform_expr(arg, js_imports, extern_func_to_js, local_name_to_js, tracker); } @@ -1096,6 +1135,63 @@ fn transform_expr( Expr::JsCreateCallback { closure, .. } => { transform_expr(closure, js_imports, extern_func_to_js, local_name_to_js, tracker); } + Expr::ReflectDefineMetadata { + key, + value, + target, + property_key, + } => { + transform_expr(key, js_imports, extern_func_to_js, local_name_to_js, tracker); + transform_expr(value, js_imports, extern_func_to_js, local_name_to_js, tracker); + transform_expr(target, js_imports, extern_func_to_js, local_name_to_js, tracker); + if let Some(property_key) = property_key { + transform_expr(property_key, js_imports, extern_func_to_js, local_name_to_js, tracker); + } + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } + | Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + transform_expr(key, js_imports, extern_func_to_js, local_name_to_js, tracker); + transform_expr(target, js_imports, extern_func_to_js, local_name_to_js, tracker); + if let Some(property_key) = property_key { + transform_expr(property_key, js_imports, extern_func_to_js, local_name_to_js, tracker); + } + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } + | Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + transform_expr(target, js_imports, extern_func_to_js, local_name_to_js, tracker); + if let Some(property_key) = property_key { + transform_expr(property_key, js_imports, extern_func_to_js, local_name_to_js, tracker); + } + } // Expressions that don't need transformation Expr::Number(_) | Expr::Integer(_) | Expr::BigInt(_) | Expr::String(_) | Expr::Bool(_) | Expr::Null | Expr::Undefined | Expr::This | Expr::LocalGet(_) | Expr::GlobalGet(_) | diff --git a/crates/perry-hir/src/lower.rs b/crates/perry-hir/src/lower.rs index 03548b6e96..81d4c2a591 100644 --- a/crates/perry-hir/src/lower.rs +++ b/crates/perry-hir/src/lower.rs @@ -3,7 +3,7 @@ //! Converts SWC's TypeScript AST into our HIR representation. use anyhow::{anyhow, Result}; -use perry_types::{FuncId, GlobalId, LocalId, Type, TypeParam}; +use perry_types::{FuncId, FunctionType, GlobalId, LocalId, Type, TypeParam}; use std::collections::{HashMap, HashSet}; use swc_ecma_ast as ast; @@ -554,6 +554,7 @@ impl LoweringContext { name: "__x".to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::Any, @@ -973,6 +974,7 @@ impl LoweringContext { init: None, is_private: false, is_readonly: false, + decorators: Vec::new(), }) .collect(); @@ -991,6 +993,7 @@ impl LoweringContext { name: name.clone(), ty: ty.clone(), default: None, + decorators: Vec::new(), is_rest: false, }); ctor_body.push(Stmt::Expr(Expr::PropertySet { @@ -1035,6 +1038,7 @@ impl LoweringContext { setters: Vec::new(), static_fields: Vec::new(), static_methods: Vec::new(), + decorators: Vec::new(), is_exported: false, aliases: Vec::new(), }); @@ -3632,6 +3636,385 @@ fn collect_closure_assigned_in_body_expr( } } +fn append_legacy_decorator_init_for_class( + ctx: &mut LoweringContext, + init: &mut Vec, + class: &Class, +) { + let has_param_decorators = class + .constructor + .as_ref() + .map(|ctor| ctor.params.iter().any(|p| !p.decorators.is_empty())) + .unwrap_or(false); + let has_method_decorators = class.methods.iter().any(method_has_legacy_decorators) + || class + .static_methods + .iter() + .any(method_has_legacy_decorators); + let has_property_decorators = class.fields.iter().any(field_has_legacy_decorators) + || class.static_fields.iter().any(field_has_legacy_decorators); + if class.decorators.is_empty() + && !has_param_decorators + && !has_method_decorators + && !has_property_decorators + { + return; + } + + if let Some(ctor) = &class.constructor { + let param_types = ctor + .params + .iter() + .map(|p| type_metadata_expr(&p.ty)) + .collect(); + init.push(Stmt::Expr(Expr::ReflectDefineMetadata { + key: Box::new(Expr::String("design:paramtypes".to_string())), + value: Box::new(Expr::Array(param_types)), + target: Box::new(Expr::ClassRef(class.name.clone())), + property_key: None, + })); + } + + if let Some(ctor) = &class.constructor { + for (index, param) in ctor.params.iter().enumerate().rev() { + append_decorator_invocations( + ctx, + init, + ¶m.decorators, + vec![ + Expr::ClassRef(class.name.clone()), + Expr::Undefined, + Expr::Number(index as f64), + ], + ); + } + } + + for method in &class.methods { + append_method_decorator_init(ctx, init, class, method); + } + for method in &class.static_methods { + append_method_decorator_init(ctx, init, class, method); + } + for field in &class.fields { + append_property_decorator_init(ctx, init, class, field); + } + for field in &class.static_fields { + append_property_decorator_init(ctx, init, class, field); + } + + append_class_decorator_invocations( + ctx, + init, + &class.decorators, + &class.name, + vec![Expr::ClassRef(class.name.clone())], + ); +} + +fn method_has_legacy_decorators(method: &Function) -> bool { + !method.decorators.is_empty() || method.params.iter().any(|p| !p.decorators.is_empty()) +} + +fn field_has_legacy_decorators(field: &ClassField) -> bool { + !field.decorators.is_empty() +} + +fn append_property_decorator_init( + ctx: &mut LoweringContext, + out: &mut Vec, + class: &Class, + field: &ClassField, +) { + if field.decorators.is_empty() { + return; + } + + out.push(Stmt::Expr(Expr::ReflectDefineMetadata { + key: Box::new(Expr::String("design:type".to_string())), + value: Box::new(type_metadata_expr(&field.ty)), + target: Box::new(Expr::ClassRef(class.name.clone())), + property_key: Some(Box::new(Expr::String(field.name.clone()))), + })); + + append_decorator_invocations( + ctx, + out, + &field.decorators, + vec![ + Expr::ClassRef(class.name.clone()), + Expr::String(field.name.clone()), + ], + ); +} + +fn append_method_decorator_init( + ctx: &mut LoweringContext, + out: &mut Vec, + class: &Class, + method: &Function, +) { + if !method_has_legacy_decorators(method) { + return; + } + + if method.params.iter().any(|p| !p.decorators.is_empty()) || !method.decorators.is_empty() { + let param_types = method + .params + .iter() + .map(|p| type_metadata_expr(&p.ty)) + .collect(); + out.push(Stmt::Expr(Expr::ReflectDefineMetadata { + key: Box::new(Expr::String("design:paramtypes".to_string())), + value: Box::new(Expr::Array(param_types)), + target: Box::new(Expr::ClassRef(class.name.clone())), + property_key: Some(Box::new(Expr::String(method.name.clone()))), + })); + } + + for (index, param) in method.params.iter().enumerate().rev() { + append_decorator_invocations( + ctx, + out, + ¶m.decorators, + vec![ + Expr::ClassRef(class.name.clone()), + Expr::String(method.name.clone()), + Expr::Number(index as f64), + ], + ); + } + + let descriptor = Expr::Object(vec![( + "value".to_string(), + Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_class_prototype_method_value".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args: vec![ + Expr::ClassRef(class.name.clone()), + Expr::String(method.name.clone()), + ], + type_args: Vec::new(), + }, + )]); + append_decorator_invocations( + ctx, + out, + &method.decorators, + vec![ + Expr::ClassRef(class.name.clone()), + Expr::String(method.name.clone()), + descriptor, + ], + ); +} + +fn append_decorator_invocations( + ctx: &mut LoweringContext, + out: &mut Vec, + decorators: &[Decorator], + invocation_args: Vec, +) { + append_decorator_invocations_inner(ctx, out, decorators, invocation_args, None); +} + +/// Same as `append_decorator_invocations`, but each non-`Reflect.metadata` +/// invocation captures its return value and throws a `TypeError` if it is +/// anything other than `undefined`. Used for class decorators, where TS +/// allows the decorator to return a replacement class but Perry does not +/// install the replacement (the lowered class is fixed in the IR). Throwing +/// on a non-`undefined` return surfaces the silent-failure case the +/// maintainer flagged on PR #754 (`@Memoize`, `@Throttle`, GraphQL resolver +/// wrappers, etc.). +fn append_class_decorator_invocations( + ctx: &mut LoweringContext, + out: &mut Vec, + decorators: &[Decorator], + class_name: &str, + invocation_args: Vec, +) { + append_decorator_invocations_inner(ctx, out, decorators, invocation_args, Some(class_name)); +} + +fn append_decorator_invocations_inner( + ctx: &mut LoweringContext, + out: &mut Vec, + decorators: &[Decorator], + invocation_args: Vec, + class_name_for_replacement_check: Option<&str>, +) { + let mut callees: Vec<(Expr, String)> = Vec::with_capacity(decorators.len()); + for dec in decorators { + if dec.is_reflect_metadata { + append_reflect_metadata_decorator(out, dec, &invocation_args); + continue; + } + let base = decorator_callee_expr(ctx, &dec.name); + if dec.is_factory { + let temp_id = ctx.fresh_local(); + out.push(Stmt::Let { + id: temp_id, + name: format!("__perry_dec_{}", temp_id), + ty: Type::Function(FunctionType { + params: Vec::new(), + return_type: Box::new(Type::Any), + is_async: false, + is_generator: false, + }), + mutable: false, + init: Some(Expr::Call { + callee: Box::new(base), + args: dec.args.clone(), + type_args: Vec::new(), + }), + }); + callees.push((Expr::LocalGet(temp_id), dec.name.clone())); + } else { + callees.push((base, dec.name.clone())); + } + } + + for (callee, dec_name) in callees.into_iter().rev() { + let call = Expr::Call { + callee: Box::new(callee), + args: invocation_args.clone(), + type_args: Vec::new(), + }; + match class_name_for_replacement_check { + Some(class_name) => { + let ret_id = ctx.fresh_local(); + out.push(Stmt::Let { + id: ret_id, + name: format!("__perry_dec_ret_{}", ret_id), + ty: Type::Any, + mutable: false, + init: Some(call), + }); + let msg = format!( + "Class decorator `@{dec_name}` on `{class_name}` returned a replacement \ +class. Perry does not install class replacements from decorators (see \ +docs/src/language/decorators.md). Return `undefined` (or nothing) to keep \ +the decorator running for side effects only." + ); + // Check `typeof ret === "function"` rather than + // `ret !== undefined`: Perry's lowering for a function + // expression with no explicit `return` currently leaves a + // numeric sentinel in the return slot rather than the + // NaN-boxed undefined value, so `!== undefined` would + // false-positive on side-effect-only decorators. The + // semantic check the maintainer asked for is "did the + // decorator return a class?" — a class is `typeof + // "function"` in JS, so this catches the @Memoize / + // @Throttle / GraphQL-wrapper case while leaving the bare + // `@Injectable` (no return) shape alone. + out.push(Stmt::If { + condition: Expr::Compare { + op: CompareOp::Eq, + left: Box::new(Expr::TypeOf(Box::new(Expr::LocalGet(ret_id)))), + right: Box::new(Expr::String("function".to_string())), + }, + // Perry has dedicated HIR variants for built-in errors + // (`Expr::TypeErrorNew`, etc.); the generic + // `Expr::New { class_name: "TypeError" }` path falls + // through to an empty-object placeholder and prints + // `Uncaught exception: [object] (bits=…)`. + then_branch: vec![Stmt::Throw(Expr::TypeErrorNew(Box::new(Expr::String(msg))))], + else_branch: None, + }); + } + None => { + out.push(Stmt::Expr(call)); + } + } + } +} + +fn append_reflect_metadata_decorator( + out: &mut Vec, + dec: &Decorator, + invocation_args: &[Expr], +) { + let key = dec.args.first().cloned().unwrap_or(Expr::Undefined); + let value = dec.args.get(1).cloned().unwrap_or(Expr::Undefined); + let target = invocation_args.first().cloned().unwrap_or(Expr::Undefined); + let property_key = invocation_args.get(1).cloned().and_then(|arg| { + if matches!(arg, Expr::Undefined) { + None + } else { + Some(Box::new(arg)) + } + }); + out.push(Stmt::Expr(Expr::ReflectDefineMetadata { + key: Box::new(key), + value: Box::new(value), + target: Box::new(target), + property_key, + })); +} + +fn type_metadata_expr(ty: &Type) -> Expr { + match ty { + Type::Named(name) => Expr::ClassRef(name.clone()), + Type::Generic { base, .. } => Expr::ClassRef(base.clone()), + Type::Array(_) | Type::Tuple(_) => Expr::ClassRef("Array".to_string()), + Type::String => Expr::ClassRef("String".to_string()), + Type::Number | Type::Int32 | Type::BigInt => Expr::ClassRef("Number".to_string()), + Type::Boolean => Expr::ClassRef("Boolean".to_string()), + Type::Object(_) => Expr::ClassRef("Object".to_string()), + Type::Function(_) => Expr::ClassRef("Function".to_string()), + _ => Expr::Undefined, + } +} + +fn decorator_callee_expr(ctx: &LoweringContext, name: &str) -> Expr { + if let Some(id) = ctx.lookup_func(name) { + Expr::FuncRef(id) + } else if let Some(orig_name) = ctx.lookup_imported_func(name) { + let (param_types, return_type) = ctx + .lookup_extern_func_types(orig_name) + .map(|(p, r)| (p.clone(), r.clone())) + .unwrap_or_else(|| (Vec::new(), Type::Any)); + Expr::ExternFuncRef { + name: orig_name.to_string(), + param_types, + return_type, + } + } else { + Expr::ExternFuncRef { + name: name.to_string(), + param_types: Vec::new(), + return_type: Type::Any, + } + } +} + +/// Emit a one-shot note when the user imports `reflect-metadata`. Perry +/// ships a built-in subset that's enough for the Nest-style decorator +/// metadata canaries (see docs/src/language/decorators.md), but is NOT +/// the full npm package's surface — `class-validator` and `TypeORM` reach +/// into corners that aren't shimmed. Telling the user up-front avoids the +/// silent-failure surprise where a polyfill they `import`-ed turns out +/// not to be the polyfill they expected. +fn emit_reflect_metadata_shim_note() { + use std::sync::atomic::{AtomicBool, Ordering}; + static EMITTED: AtomicBool = AtomicBool::new(false); + if EMITTED.swap(true, Ordering::Relaxed) { + return; + } + eprintln!( + "[perry] note: `import \"reflect-metadata\"` is satisfied by Perry's built-in \ +metadata subset. Implemented surface: Reflect.defineMetadata, getMetadata, \ +getOwnMetadata, hasMetadata, hasOwnMetadata, getMetadataKeys, \ +getOwnMetadataKeys, deleteMetadata, and @Reflect.metadata(...). \ +Anything outside this surface (Symbol-keyed metadata, the full reflect-metadata \ +runtime used by class-validator/TypeORM) is not provided. \ +See docs/src/language/decorators.md." + ); +} + fn lower_module_decl( ctx: &mut LoweringContext, module: &mut Module, @@ -3647,6 +4030,11 @@ fn lower_module_decl( .unwrap_or(&raw_source) .to_string(); + if source == "reflect-metadata" { + emit_reflect_metadata_shim_note(); + return Ok(()); + } + // Check if this is a native module import let is_native = is_native_module(&source); @@ -4397,6 +4785,7 @@ fn lower_module_decl( } } } + append_legacy_decorator_init_for_class(ctx, &mut module.init, &class); push_class_dedup(module, class); module.exports.push(Export::Named { local: class_name.clone(), @@ -4814,6 +5203,7 @@ fn lower_namespace_as_class( setters: Vec::new(), static_fields: Vec::new(), static_methods: Vec::new(), + decorators: Vec::new(), is_exported, aliases: Vec::new(), }); @@ -4995,6 +5385,7 @@ fn lower_namespace_as_class( setters: Vec::new(), static_fields: Vec::new(), static_methods, + decorators: Vec::new(), is_exported, aliases: Vec::new(), }) @@ -5792,6 +6183,7 @@ fn lower_stmt(ctx: &mut LoweringContext, module: &mut Module, stmt: &ast::Stmt) } } } + append_legacy_decorator_init_for_class(ctx, &mut module.init, &class); push_class_dedup(module, class); } ast::Decl::TsEnum(enum_decl) => { @@ -8275,6 +8667,7 @@ pub(super) fn try_desugar_reactive_text( name: "__v".to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }; let fresh_concat = lower_tpl_to_concat(ctx, tpl)?; @@ -8525,6 +8918,7 @@ pub(super) fn try_desugar_reactive_animate( name: "__v".to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }; diff --git a/crates/perry-hir/src/lower/expr_call.rs b/crates/perry-hir/src/lower/expr_call.rs index 5fec29e892..ce7a57b816 100644 --- a/crates/perry-hir/src/lower/expr_call.rs +++ b/crates/perry-hir/src/lower/expr_call.rs @@ -327,6 +327,41 @@ pub(super) fn lower_call(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Res } } + // --- Object.hasOwnProperty.call(obj, key) → js_object_has_own(obj, key) --- + // + // Current NestJS `@Module()` uses this inherited Object.prototype helper + // through the `Object` constructor value instead of spelling + // `Object.prototype.hasOwnProperty.call(...)`. + if !has_spread && args.len() == 2 { + if let ast::Callee::Expr(callee_expr) = &call.callee { + if let ast::Expr::Member(outer) = callee_expr.as_ref() { + if let (ast::MemberProp::Ident(outer_prop), ast::Expr::Member(mid)) = + (&outer.prop, outer.obj.as_ref()) + { + if outer_prop.sym.as_ref() == "call" { + if let (ast::MemberProp::Ident(mid_prop), ast::Expr::Ident(mid_obj)) = + (&mid.prop, mid.obj.as_ref()) + { + if mid_obj.sym.as_ref() == "Object" + && mid_prop.sym.as_ref() == "hasOwnProperty" + { + return Ok(Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_object_has_own".to_string(), + param_types: Vec::new(), + return_type: Type::Any, + }), + args, + type_args: Vec::new(), + }); + } + } + } + } + } + } + } + // --- Object.prototype.toString.call(x) → js_object_to_string(x) --- // AST shape is a four-level member expression: // call.call(x) @@ -1155,6 +1190,70 @@ pub(super) fn lower_call(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Res let target = args.into_iter().next().unwrap_or(Expr::Undefined); return Ok(Expr::ReflectGetPrototypeOf(Box::new(target))); } + "defineMetadata" => { + let (key, value, target, property_key) = + take_reflect_kvtp_args(args); + return Ok(Expr::ReflectDefineMetadata { + key: Box::new(key), + value: Box::new(value), + target: Box::new(target), + property_key, + }); + } + "getMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Expr::ReflectGetMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + }); + } + "getOwnMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Expr::ReflectGetOwnMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + }); + } + "hasMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Expr::ReflectHasMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + }); + } + "hasOwnMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Expr::ReflectHasOwnMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + }); + } + "getMetadataKeys" => { + let (target, property_key) = take_reflect_tp_args(args); + return Ok(Expr::ReflectGetMetadataKeys { + target: Box::new(target), + property_key, + }); + } + "getOwnMetadataKeys" => { + let (target, property_key) = take_reflect_tp_args(args); + return Ok(Expr::ReflectGetOwnMetadataKeys { + target: Box::new(target), + property_key, + }); + } + "deleteMetadata" => { + let (key, target, property_key) = take_reflect_ktp_args(args); + return Ok(Expr::ReflectDeleteMetadata { + key: Box::new(key), + target: Box::new(target), + property_key, + }); + } "setPrototypeOf" => return Ok(Expr::Bool(true)), "isExtensible" => { let target = args.into_iter().next().unwrap_or(Expr::Undefined); @@ -5995,3 +6094,33 @@ fn register_super_stream_controller_params( } } } + +/// (key, value, target, propertyKey?) — `Reflect.defineMetadata`'s 3-or-4 arg +/// shape. Defaults missing leading args to `undefined`; `property_key` stays +/// `None` when omitted so the runtime can distinguish class-level metadata +/// from a property-level one. +fn take_reflect_kvtp_args(args: Vec) -> (Expr, Expr, Expr, Option>) { + let mut it = args.into_iter(); + let key = it.next().unwrap_or(Expr::Undefined); + let value = it.next().unwrap_or(Expr::Undefined); + let target = it.next().unwrap_or(Expr::Undefined); + let property_key = it.next().map(Box::new); + (key, value, target, property_key) +} + +/// (key, target, propertyKey?) — `Reflect.{get,getOwn,has,hasOwn,delete}Metadata`. +fn take_reflect_ktp_args(args: Vec) -> (Expr, Expr, Option>) { + let mut it = args.into_iter(); + let key = it.next().unwrap_or(Expr::Undefined); + let target = it.next().unwrap_or(Expr::Undefined); + let property_key = it.next().map(Box::new); + (key, target, property_key) +} + +/// (target, propertyKey?) — `Reflect.{get,getOwn}MetadataKeys`. +fn take_reflect_tp_args(args: Vec) -> (Expr, Option>) { + let mut it = args.into_iter(); + let target = it.next().unwrap_or(Expr::Undefined); + let property_key = it.next().map(Box::new); + (target, property_key) +} diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index ebcfa0caff..b523390790 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -54,6 +54,7 @@ pub(super) fn lower_arrow(ctx: &mut LoweringContext, arrow: &ast::ArrowExpr) -> name: param_name, ty: param_ty, default: param_default, + decorators: Vec::new(), is_rest, }); // Track destructuring patterns to generate extraction statements @@ -270,6 +271,7 @@ pub(super) fn lower_fn_expr(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> name: param_name, ty: Type::Any, default: param_default, + decorators: Vec::new(), is_rest, }); // Track destructuring patterns to generate extraction statements diff --git a/crates/perry-hir/src/lower/expr_misc.rs b/crates/perry-hir/src/lower/expr_misc.rs index 42863ffc49..2702a3c491 100644 --- a/crates/perry-hir/src/lower/expr_misc.rs +++ b/crates/perry-hir/src/lower/expr_misc.rs @@ -47,22 +47,39 @@ pub(super) fn lower_await(ctx: &mut LoweringContext, await_expr: &ast::AwaitExpr } pub(super) fn lower_super_prop( - _ctx: &mut LoweringContext, + ctx: &mut LoweringContext, super_prop: &ast::SuperPropExpr, ) -> Result { - // super.property access — used in super.method() calls. When used - // as a call target, the Call expression detects this and routes - // through SuperMethodCall. Direct super property access (without - // the trailing call) is a syntax form Perry hasn't implemented yet. + // `super.` as a value (NOT followed by a call). Call-form + // `super.method(...)` is detected in lower_call.rs and routed through + // SuperMethodCall before this function ever runs, so we only land + // here for value-form reads like `super._next` (rxjs's + // OperatorSubscriber), `super.value` (NestJS adapter chains), etc. + // + // Strict JS semantics would resolve through the parent class's + // prototype, bypassing any override on the child. Perry currently + // doesn't carry a runtime parent-vtable lookup separate from the + // instance's own vtable chain, so we approximate by lowering to + // `this.` — the instance method dispatch already walks the + // class chain and returns the inherited method when the child does + // not override. The substitution is correct when the child does + // not override the property (the dominant rxjs / NestJS pattern; + // see PR #754 maintainer review on the NestJS smoke test). When + // the child *does* override, this approximation will resolve to + // the override rather than the parent — a TODO for a future + // explicit super-vtable path in codegen. match &super_prop.prop { - ast::SuperProp::Ident(_ident) => crate::lower_bail!( - super_prop.span, - "Direct super property access not yet supported, use super.method()" - ), - ast::SuperProp::Computed(_) => crate::lower_bail!( - super_prop.span, - "Computed super property access not supported" - ), + ast::SuperProp::Ident(ident) => Ok(Expr::PropertyGet { + object: Box::new(Expr::This), + property: ident.sym.to_string(), + }), + ast::SuperProp::Computed(computed) => { + let index = Box::new(lower_expr(ctx, &computed.expr)?); + Ok(Expr::IndexGet { + object: Box::new(Expr::This), + index, + }) + } } } diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index 75a66d660e..c3d7aaa379 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -345,6 +345,7 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R name: param_name, ty: param_type, default: param_default, + decorators: Vec::new(), is_rest: is_rest_param(¶m.pat), }); } @@ -492,6 +493,7 @@ pub(super) fn lower_object(ctx: &mut LoweringContext, obj: &ast::ObjectLit) -> R name: "__perry_obj_iife".to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: false, }; let mut body: Vec = Vec::with_capacity(computed_post_init.len() + 1); diff --git a/crates/perry-hir/src/lower_decl.rs b/crates/perry-hir/src/lower_decl.rs index abc8bc04c7..33d1d8883b 100644 --- a/crates/perry-hir/src/lower_decl.rs +++ b/crates/perry-hir/src/lower_decl.rs @@ -256,6 +256,7 @@ pub(crate) fn append_synthetic_arguments_param(ctx: &mut LoweringContext, params name: "arguments".to_string(), ty: Type::Any, default: None, + decorators: Vec::new(), is_rest: true, }); } @@ -316,6 +317,7 @@ pub(crate) fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> name: param_name, ty: param_type, default: param_default, + decorators: lower_decorators(ctx, ¶m.decorators), is_rest, }); // Track destructuring patterns (or an Assign wrapping one) for extraction stmts @@ -523,38 +525,32 @@ pub(crate) fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> }) } -/// Refuse to lower classes that use `@decorator` syntax. Perry parses decorators -/// into the HIR but has no codegen path — before this check they were silently -/// dropped, producing executables where the decorator body never ran (issue #144). -/// Walks every decoration point: the class itself, methods/accessors/private-methods, -/// class properties, and constructor parameters (TS parameter decorators). -fn reject_decorators(class: &ast::Class, class_name: &str) -> Result<()> { - if let Some(dec) = class.decorators.first() { - let name = decorator_name_hint(dec); - bail!( - "TypeScript decorators are not supported (found `@{name}` on class `{class_name}`). \ - See docs/src/language/limitations.md#no-decorators. Rewrite as an explicit wrapper \ - function or remove the annotation.", - ); - } +/// Validate the legacy TypeScript decorator surface Perry implements. Perry +/// currently lowers class, method, property, and parameter decorators, which +/// is enough for Nest-style DI metadata canaries. Accessor (getter/setter) +/// decorators and private decoration points still fail loudly instead of +/// being dropped — the runtime path for descriptor replacement on accessors +/// is not implemented and silently ignoring them would mask real bugs in +/// user code. +fn validate_legacy_decorator_surface(class: &ast::Class, class_name: &str) -> Result<()> { for member in &class.body { match member { ast::ClassMember::Method(m) => { - if let Some(dec) = m.function.decorators.first() { - let name = decorator_name_hint(dec); - let key = method_key_hint(&m.key); - bail!( - "TypeScript decorators are not supported (found `@{name}` on method `{class_name}.{key}`). \ - See docs/src/language/limitations.md#no-decorators.", - ); - } - for param in &m.function.params { - if let Some(dec) = param.decorators.first() { + // SWC models getters/setters as Method with kind != Method. + // Their decorators would expect descriptor replacement, which + // Perry does not implement; reject rather than drop silently. + if matches!(m.kind, ast::MethodKind::Getter | ast::MethodKind::Setter) { + if let Some(dec) = m.function.decorators.first() { let name = decorator_name_hint(dec); let key = method_key_hint(&m.key); + let kind = match m.kind { + ast::MethodKind::Getter => "getter", + ast::MethodKind::Setter => "setter", + _ => "accessor", + }; bail!( - "TypeScript parameter decorators are not supported (found `@{name}` on a parameter of `{class_name}.{key}`). \ - See docs/src/language/limitations.md#no-decorators.", + "TypeScript {kind} decorators are not supported (found `@{name}` on `{class_name}.{key}`). \ + See docs/src/language/decorators.md — accessor descriptor replacement is not implemented.", ); } } @@ -563,50 +559,34 @@ fn reject_decorators(class: &ast::Class, class_name: &str) -> Result<()> { if let Some(dec) = m.function.decorators.first() { let name = decorator_name_hint(dec); bail!( - "TypeScript decorators are not supported (found `@{name}` on private method of `{class_name}`). \ - See docs/src/language/limitations.md#no-decorators.", - ); - } - } - ast::ClassMember::ClassProp(p) => { - if let Some(dec) = p.decorators.first() { - let name = decorator_name_hint(dec); - bail!( - "TypeScript decorators are not supported (found `@{name}` on a property of `{class_name}`). \ - See docs/src/language/limitations.md#no-decorators.", + "TypeScript private method decorators are not supported yet (found `@{name}` on private method of `{class_name}`).", ); } } + ast::ClassMember::ClassProp(_) => {} ast::ClassMember::PrivateProp(p) => { if let Some(dec) = p.decorators.first() { let name = decorator_name_hint(dec); bail!( - "TypeScript decorators are not supported (found `@{name}` on a private property of `{class_name}`). \ - See docs/src/language/limitations.md#no-decorators.", + "TypeScript private property decorators are not supported yet (found `@{name}` on a private property of `{class_name}`).", ); } } - ast::ClassMember::Constructor(c) => { - for param in &c.params { - let decs = match param { - ast::ParamOrTsParamProp::Param(p) => &p.decorators, - ast::ParamOrTsParamProp::TsParamProp(tp) => &tp.decorators, - }; - if let Some(dec) = decs.first() { - let name = decorator_name_hint(dec); - bail!( - "TypeScript parameter decorators are not supported (found `@{name}` on a constructor parameter of `{class_name}`). \ - See docs/src/language/limitations.md#no-decorators.", - ); - } - } - } _ => {} } } Ok(()) } +fn method_key_hint(key: &ast::PropName) -> String { + match key { + ast::PropName::Ident(i) => i.sym.to_string(), + ast::PropName::Str(s) => format!("{:?}", s.value), + ast::PropName::Num(n) => n.value.to_string(), + _ => "".to_string(), + } +} + fn decorator_name_hint(dec: &ast::Decorator) -> String { match dec.expr.as_ref() { ast::Expr::Ident(i) => i.sym.to_string(), @@ -622,22 +602,13 @@ fn decorator_name_hint(dec: &ast::Decorator) -> String { } } -fn method_key_hint(key: &ast::PropName) -> String { - match key { - ast::PropName::Ident(i) => i.sym.to_string(), - ast::PropName::Str(s) => format!("{:?}", s.value), - ast::PropName::Num(n) => n.value.to_string(), - _ => "".to_string(), - } -} - pub(crate) fn lower_class_decl( ctx: &mut LoweringContext, class_decl: &ast::ClassDecl, is_exported: bool, ) -> Result { let name = class_decl.ident.sym.to_string(); - reject_decorators(&class_decl.class, &name)?; + validate_legacy_decorator_surface(&class_decl.class, &name)?; let class_id = match ctx.lookup_class(&name) { Some(id) => id, None => { @@ -916,6 +887,7 @@ pub(crate) fn lower_class_decl( name: "this".to_string(), ty: Type::Named(name.clone()), default: None, + decorators: Vec::new(), is_rest: false, }); new_params.extend(getter.params.into_iter()); @@ -1006,6 +978,7 @@ pub(crate) fn lower_class_decl( name: "this".to_string(), ty: Type::Named(name.clone()), default: None, + decorators: Vec::new(), is_rest: false, }); new_params.append(&mut func.params); @@ -1165,6 +1138,7 @@ pub(crate) fn lower_class_decl( init: None, is_private: false, is_readonly: ts_prop.readonly, + decorators: lower_decorators(ctx, &ts_prop.decorators), }); } } @@ -1264,6 +1238,7 @@ pub(crate) fn lower_class_decl( init: None, is_private: false, is_readonly: false, + decorators: Vec::new(), }); } } @@ -1453,6 +1428,7 @@ pub(crate) fn lower_class_decl( init: None, is_private: false, is_readonly: false, + decorators: Vec::new(), }); } if let Some(existing) = ctx.lookup_class_field_names(&name) { @@ -1581,6 +1557,7 @@ pub(crate) fn lower_class_decl( name: format!("__perry_cap_{}", outer_id), ty, default: None, + decorators: Vec::new(), is_rest: false, }); assignment_stmts.push(Stmt::Expr(Expr::PropertySet { @@ -1647,6 +1624,7 @@ pub(crate) fn lower_class_decl( setters, static_fields, static_methods, + decorators: lower_decorators(ctx, &class_decl.class.decorators), is_exported, aliases: Vec::new(), }) @@ -1660,7 +1638,7 @@ pub(crate) fn lower_class_from_ast( name: &str, is_exported: bool, ) -> Result { - reject_decorators(class, name)?; + validate_legacy_decorator_surface(class, name)?; let class_id = match ctx.lookup_class(name) { Some(id) => id, None => { @@ -1952,6 +1930,7 @@ pub(crate) fn lower_class_from_ast( setters, static_fields, static_methods, + decorators: lower_decorators(ctx, &class.decorators), is_exported, aliases: Vec::new(), }) @@ -2274,6 +2253,7 @@ pub(crate) fn lower_constructor( name: param_name, ty: param_type, default: param_default, + decorators: lower_decorators(ctx, &p.decorators), is_rest, }); let inner_pat = if let ast::Pat::Assign(assign) = &p.pat { @@ -2311,6 +2291,7 @@ pub(crate) fn lower_constructor( name: param_name, ty: param_type, default: None, + decorators: lower_decorators(ctx, &ts_prop.decorators), is_rest: false, // TsParamProp cannot be a rest parameter }); } @@ -2654,6 +2635,7 @@ pub(crate) fn lower_class_method( name: param_name, ty: param_type, default: param_default, + decorators: lower_decorators(ctx, ¶m.decorators), is_rest, }); // Mirror the lower_fn_decl shape: an `Assign` pattern can wrap a @@ -2901,6 +2883,7 @@ pub(crate) fn lower_setter_method( name: param_name, ty: param_type, default: None, + decorators: Vec::new(), is_rest: false, }); let inner_pat = if let ast::Pat::Assign(assign) = ¶m.pat { @@ -3004,6 +2987,7 @@ pub(crate) fn lower_class_prop( init, is_private: false, // TODO: check accessibility is_readonly: prop.readonly, + decorators: lower_decorators(ctx, &prop.decorators), }) } @@ -3050,6 +3034,7 @@ pub(crate) fn lower_private_method( name: param_name, ty: param_type, default: param_default, + decorators: Vec::new(), is_rest, }); let inner_pat = if let ast::Pat::Assign(assign) = ¶m.pat { @@ -3195,6 +3180,7 @@ pub(crate) fn lower_private_setter( name: param_name, ty: param_type, default: None, + decorators: Vec::new(), is_rest: false, }); let inner_pat = if let ast::Pat::Assign(assign) = ¶m.pat { @@ -3279,6 +3265,7 @@ pub(crate) fn lower_private_prop( init, is_private: true, is_readonly: prop.readonly, + decorators: lower_decorators(ctx, &prop.decorators), }) } @@ -3934,6 +3921,7 @@ pub(crate) fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Re name: param_name, ty: Type::Any, default: param_default, + decorators: Vec::new(), is_rest, }); if is_destructuring_pattern(¶m.pat) { diff --git a/crates/perry-hir/src/lower_types.rs b/crates/perry-hir/src/lower_types.rs index 6353f3425a..3789694b51 100644 --- a/crates/perry-hir/src/lower_types.rs +++ b/crates/perry-hir/src/lower_types.rs @@ -7,7 +7,7 @@ use perry_types::{Type, TypeParam}; use swc_ecma_ast as ast; use crate::ir::*; -use crate::lower::LoweringContext; +use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::{get_pat_name, lower_lit}; pub(crate) fn infer_type_from_expr(expr: &ast::Expr, ctx: &LoweringContext) -> Type { @@ -1153,7 +1153,7 @@ pub(crate) fn extract_binding_type(binding: &ast::Pat) -> Type { /// Lower decorators from SWC AST to HIR Decorators pub(crate) fn lower_decorators( - _ctx: &mut LoweringContext, + ctx: &mut LoweringContext, decorators: &[ast::Decorator], ) -> Vec { decorators @@ -1166,30 +1166,56 @@ pub(crate) fn lower_decorators( ast::Expr::Ident(ident) => Some(Decorator { name: ident.sym.to_string(), args: Vec::new(), + is_factory: false, + is_reflect_metadata: false, }), ast::Expr::Call(call) => { // Get the callee name if let ast::Callee::Expr(callee_expr) = &call.callee { + if let ast::Expr::Member(member) = callee_expr.as_ref() { + if let ast::Expr::Ident(obj) = member.obj.as_ref() { + if obj.sym.as_ref() == "Reflect" { + if let ast::MemberProp::Ident(method) = &member.prop { + if method.sym.as_ref() == "metadata" { + let args: Vec = call + .args + .iter() + .filter_map(|arg| { + if arg.spread.is_some() { + None + } else { + lower_decorator_arg(ctx, arg.expr.as_ref()) + } + }) + .collect(); + return Some(Decorator { + name: "Reflect.metadata".to_string(), + args, + is_factory: true, + is_reflect_metadata: true, + }); + } + } + } + } + } if let ast::Expr::Ident(ident) = callee_expr.as_ref() { - // Lower the arguments - for now just handle simple literals let args: Vec = call .args .iter() .filter_map(|arg| { if arg.spread.is_some() { - None // Skip spread arguments for now + None } else { - // For decorator args, only handle simple literals for now - match arg.expr.as_ref() { - ast::Expr::Lit(lit) => lower_lit(lit).ok(), - _ => None, - } + lower_decorator_arg(ctx, arg.expr.as_ref()) } }) .collect(); return Some(Decorator { name: ident.sym.to_string(), args, + is_factory: true, + is_reflect_metadata: false, }); } } @@ -1200,3 +1226,63 @@ pub(crate) fn lower_decorators( }) .collect() } + +fn lower_decorator_arg(ctx: &mut LoweringContext, expr: &ast::Expr) -> Option { + match expr { + ast::Expr::Lit(lit) => lower_lit(lit).ok(), + ast::Expr::Ident(ident) => match lower_expr(ctx, expr).ok() { + Some(Expr::GlobalGet(0)) => Some(Expr::ClassRef(ident.sym.to_string())), + other => other, + }, + ast::Expr::Array(arr) => { + let items = arr + .elems + .iter() + .map(|elem| { + elem.as_ref() + .and_then(|elem| { + if elem.spread.is_some() { + None + } else { + lower_decorator_arg(ctx, elem.expr.as_ref()) + } + }) + .unwrap_or(Expr::Undefined) + }) + .collect(); + Some(Expr::Array(items)) + } + ast::Expr::Object(obj) => { + let mut fields = Vec::new(); + for prop in &obj.props { + let ast::PropOrSpread::Prop(prop) = prop else { + return None; + }; + match prop.as_ref() { + ast::Prop::KeyValue(kv) => { + let key = decorator_prop_name(&kv.key)?; + let value = lower_decorator_arg(ctx, kv.value.as_ref())?; + fields.push((key, value)); + } + ast::Prop::Shorthand(ident) => { + let name = ident.sym.to_string(); + let value = lower_decorator_arg(ctx, &ast::Expr::Ident(ident.clone()))?; + fields.push((name, value)); + } + _ => return None, + } + } + Some(Expr::Object(fields)) + } + _ => lower_expr(ctx, expr).ok(), + } +} + +fn decorator_prop_name(name: &ast::PropName) -> Option { + match name { + ast::PropName::Ident(ident) => Some(ident.sym.to_string()), + ast::PropName::Str(s) => Some(s.value.as_str().unwrap_or("").to_string()), + ast::PropName::Num(n) => Some(n.value.to_string()), + _ => None, + } +} diff --git a/crates/perry-hir/src/monomorph.rs b/crates/perry-hir/src/monomorph.rs index 2dd627944d..bfc7ec773a 100644 --- a/crates/perry-hir/src/monomorph.rs +++ b/crates/perry-hir/src/monomorph.rs @@ -1589,6 +1589,7 @@ fn substitute_expr(expr: &Expr, substitutions: &HashMap) -> Expr { .default .as_ref() .map(|d| substitute_expr(d, substitutions)), + decorators: p.decorators.clone(), is_rest: p.is_rest, }) .collect(), @@ -1847,6 +1848,7 @@ pub fn specialize_function(func: &Function, type_args: &[Type], new_id: FuncId) .default .as_ref() .map(|d| substitute_expr(d, &substitutions)), + decorators: p.decorators.clone(), is_rest: p.is_rest, }) .collect(), @@ -1897,6 +1899,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C init: f.init.as_ref().map(|e| substitute_expr(e, &substitutions)), is_private: f.is_private, is_readonly: f.is_readonly, + decorators: f.decorators.clone(), }) .collect(), constructor: class.constructor.as_ref().map(|ctor| Function { @@ -1914,6 +1917,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C .default .as_ref() .map(|d| substitute_expr(d, &substitutions)), + decorators: p.decorators.clone(), is_rest: p.is_rest, }) .collect(), @@ -1946,6 +1950,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C .default .as_ref() .map(|d| substitute_expr(d, &substitutions)), + decorators: p.decorators.clone(), is_rest: p.is_rest, }) .collect(), @@ -2006,6 +2011,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C .default .as_ref() .map(|d| substitute_expr(d, &substitutions)), + decorators: p.decorators.clone(), is_rest: p.is_rest, }) .collect(), @@ -2024,6 +2030,7 @@ pub fn specialize_class(class: &Class, type_args: &[Type], new_id: ClassId) -> C .collect(), static_fields: class.static_fields.clone(), static_methods: class.static_methods.clone(), + decorators: class.decorators.clone(), is_exported: class.is_exported, aliases: class.aliases.clone(), } @@ -3562,6 +3569,7 @@ mod tests { name: "x".to_string(), ty: Type::TypeVar("T".to_string()), default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::TypeVar("T".to_string()), @@ -3636,6 +3644,7 @@ mod tests { name: "x".to_string(), ty: Type::TypeVar("T".to_string()), default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::TypeVar("T".to_string()), @@ -3703,6 +3712,7 @@ mod tests { name: "x".to_string(), ty: Type::TypeVar("T".to_string()), default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::TypeVar("T".to_string()), @@ -3799,6 +3809,7 @@ mod tests { name: "x".to_string(), ty: Type::TypeVar("T".to_string()), default: None, + decorators: Vec::new(), is_rest: false, }], return_type: Type::TypeVar("T".to_string()), diff --git a/crates/perry-hir/src/stable_hash.rs b/crates/perry-hir/src/stable_hash.rs index 07e676695f..870c90b30d 100644 --- a/crates/perry-hir/src/stable_hash.rs +++ b/crates/perry-hir/src/stable_hash.rs @@ -480,6 +480,7 @@ impl SH for Class { setters, static_fields, static_methods, + decorators, is_exported, aliases, } = self; @@ -497,6 +498,7 @@ impl SH for Class { setters.hash(h); static_fields.hash(h); static_methods.hash(h); + decorators.hash(h); is_exported.hash(h); aliases.hash(h); } @@ -511,6 +513,7 @@ impl SH for ClassField { init, is_private, is_readonly, + decorators, } = self; name.hash(h); key_expr.hash(h); @@ -518,6 +521,7 @@ impl SH for ClassField { init.hash(h); is_private.hash(h); is_readonly.hash(h); + decorators.hash(h); } } @@ -646,9 +650,16 @@ impl SH for Global { impl SH for Decorator { fn hash(&self, h: &mut H) { - let Decorator { name, args } = self; + let Decorator { + name, + args, + is_factory, + is_reflect_metadata, + } = self; name.hash(h); args.hash(h); + is_factory.hash(h); + is_reflect_metadata.hash(h); } } @@ -692,12 +703,14 @@ impl SH for Param { name, ty, default, + decorators, is_rest, } = self; id.hash(h); name.hash(h); ty.hash(h); default.hash(h); + decorators.hash(h); is_rest.hash(h); } } @@ -3282,6 +3295,11 @@ impl SH for Expr { method_name.hash(h); args.hash(h); } + Expr::JsCallValue { callee, args } => { + tag(h, 452); + callee.as_ref().hash(h); + args.hash(h); + } Expr::JsGetProperty { object, property_name, @@ -3426,6 +3444,84 @@ impl SH for Expr { tag(h, 441); e.as_ref().hash(h); } + Expr::ReflectDefineMetadata { + key, + value, + target, + property_key, + } => { + tag(h, 453); + key.as_ref().hash(h); + value.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } => { + tag(h, 454); + key.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } => { + tag(h, 455); + key.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectHasMetadata { + key, + target, + property_key, + } => { + tag(h, 456); + key.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } => { + tag(h, 457); + key.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } => { + tag(h, 458); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + tag(h, 459); + target.as_ref().hash(h); + property_key.hash(h); + } + Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + tag(h, 460); + key.as_ref().hash(h); + target.as_ref().hash(h); + property_key.hash(h); + } Expr::AsyncStepDone { value, step_closure, @@ -3625,6 +3721,7 @@ mod tests { setters: vec![], static_fields: vec![], static_methods: vec![], + decorators: vec![], is_exported: false, aliases: vec![], }); @@ -3702,6 +3799,7 @@ mod tests { ty: Type::Number, default: None, is_rest: false, + decorators: vec![], }, Param { id: 1, @@ -3709,6 +3807,7 @@ mod tests { ty: Type::Number, default: None, is_rest: false, + decorators: vec![], }, ], return_type: Type::Number, diff --git a/crates/perry-hir/src/walker.rs b/crates/perry-hir/src/walker.rs index e2c9668e12..bae0d3db3c 100644 --- a/crates/perry-hir/src/walker.rs +++ b/crates/perry-hir/src/walker.rs @@ -632,6 +632,12 @@ where f(a); } } + Expr::JsCallValue { callee, args } => { + f(callee); + for a in args { + f(a); + } + } Expr::JsSetProperty { object, value, .. } => { f(object); f(value); @@ -1204,6 +1210,63 @@ where f(key); f(descriptor); } + Expr::ReflectDefineMetadata { + key, + value, + target, + property_key, + } => { + f(key); + f(value); + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } + | Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + f(key); + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } + | Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } // ─── FinalizationRegistry register/unregister ──────────────────── Expr::FinalizationRegistryRegister { @@ -1829,6 +1892,12 @@ where f(a); } } + Expr::JsCallValue { callee, args } => { + f(callee); + for a in args { + f(a); + } + } Expr::JsSetProperty { object, value, .. } => { f(object); f(value); @@ -2375,6 +2444,63 @@ where f(key); f(descriptor); } + Expr::ReflectDefineMetadata { + key, + value, + target, + property_key, + } => { + f(key); + f(value); + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } + Expr::ReflectGetMetadata { + key, + target, + property_key, + } + | Expr::ReflectGetOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasMetadata { + key, + target, + property_key, + } + | Expr::ReflectHasOwnMetadata { + key, + target, + property_key, + } + | Expr::ReflectDeleteMetadata { + key, + target, + property_key, + } => { + f(key); + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } + Expr::ReflectGetMetadataKeys { + target, + property_key, + } + | Expr::ReflectGetOwnMetadataKeys { + target, + property_key, + } => { + f(target); + if let Some(property_key) = property_key { + f(property_key); + } + } Expr::FinalizationRegistryRegister { registry, target, diff --git a/crates/perry-jsruntime/src/bridge.rs b/crates/perry-jsruntime/src/bridge.rs index d422646576..39d2cea4ec 100644 --- a/crates/perry-jsruntime/src/bridge.rs +++ b/crates/perry-jsruntime/src/bridge.rs @@ -21,6 +21,7 @@ const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; +const SHORT_STRING_TAG: u64 = 0x7FF9_0000_0000_0000; const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000; @@ -35,10 +36,112 @@ const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; thread_local! { /// Maps handle IDs to V8 Global handles static JS_OBJECT_HANDLES: RefCell>> = RefCell::new(HashMap::new()); + /// Stable V8 constructor-like wrappers for Perry class references. + static NATIVE_CLASS_HANDLES: RefCell>> = RefCell::new(HashMap::new()); /// Counter for generating unique handle IDs static NEXT_HANDLE_ID: Cell = const { Cell::new(1) }; } +fn native_class_constructor( + scope: &mut v8::PinScope<'_, '_>, + _args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + retval.set(v8::Object::new(scope).into()); +} + +fn native_class_to_v8<'s>( + scope: &mut v8::PinScope<'s, '_>, + class_id: u32, +) -> v8::Local<'s, v8::Value> { + if let Some(existing) = NATIVE_CLASS_HANDLES.with(|handles| { + handles + .borrow() + .get(&class_id) + .map(|global| v8::Local::new(scope, global)) + }) { + return existing; + } + + let function = v8::Function::builder(native_class_constructor) + .build(scope) + .unwrap_or_else(|| v8::Function::new(scope, native_class_constructor).unwrap()); + if let Some(key) = v8::String::new(scope, "__perry_native_class_id") { + let value = v8::Integer::new_from_unsigned(scope, class_id); + function.set(scope, key.into(), value.into()); + } + let value: v8::Local = function.into(); + NATIVE_CLASS_HANDLES.with(|handles| { + handles + .borrow_mut() + .insert(class_id, v8::Global::new(scope, value)); + }); + value +} + +fn native_class_id_from_v8( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local, +) -> Option { + if !(value.is_function() || value.is_object()) { + return None; + } + let obj = v8::Local::::try_from(value).ok()?; + let key = v8::String::new(scope, "__perry_native_class_id")?; + let id_value = obj.get(scope, key.into())?; + if id_value.is_undefined() || id_value.is_null() || !id_value.is_uint32() { + return None; + } + let id = id_value.uint32_value(scope)?; + if id == 0 { + return None; + } + Some(id) +} + +pub fn v8_to_native_metadata_target( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local, +) -> f64 { + if let Some(class_id) = native_class_id_from_v8(scope, value) { + return f64::from_bits(INT32_TAG | class_id as u64); + } + + if value.is_object() { + if let Ok(obj) = v8::Local::::try_from(value) { + if let Some(key) = v8::String::new(scope, "__native_ptr__") { + if let Some(ptr_value) = obj.get(scope, key.into()) { + if ptr_value.is_external() { + let external = v8::Local::::try_from(ptr_value).unwrap(); + return f64::from_bits( + POINTER_TAG | (external.value() as u64 & POINTER_MASK), + ); + } + } + } + } + } + + v8_to_native(scope, value) +} + +pub fn v8_to_native_metadata_value( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local, +) -> f64 { + if let Some(class_id) = native_class_id_from_v8(scope, value) { + return f64::from_bits(INT32_TAG | class_id as u64); + } + + if value.is_array() { + let array = v8::Local::::try_from(value).unwrap(); + let ptr = v8_array_to_native_metadata(scope, array); + return f64::from_bits(POINTER_TAG | (ptr as u64 & POINTER_MASK)); + } + + v8_to_native(scope, value) +} + /// Store a V8 value in the handle table and return a handle ID pub fn store_js_handle(scope: &mut v8::PinScope<'_, '_>, value: v8::Local) -> u64 { let handle_id = NEXT_HANDLE_ID.with(|id| { @@ -140,6 +243,19 @@ pub fn native_to_v8<'s>(scope: &mut v8::PinScope<'s, '_>, value: f64) -> v8::Loc // Check for int32 if tag == INT32_TAG { let int_val = (bits & 0xFFFF_FFFF) as i32; + // Perry encodes class references as INT32_TAG | class_id (see + // `Expr::ClassRef` codegen). When such a value crosses into V8 we + // surface it as a stable constructor-like function so JS code can use + // it as a metadata target. NOTE: this means raw integers that happen + // to equal a registered class id (low positive numbers, the common + // range) cannot round-trip through the bridge — they materialize as + // the class function on the JS side. Decorator metadata is the only + // existing caller, where the input is always a real class ref. If a + // future caller needs int round-trip, switch class refs to a + // dedicated NaN-box tag (see review on #754). + if int_val > 0 && perry_runtime::object::is_class_id_registered(int_val as u32) { + return native_class_to_v8(scope, int_val as u32); + } return v8::Integer::new(scope, int_val).into(); } @@ -155,6 +271,17 @@ pub fn native_to_v8<'s>(scope: &mut v8::PinScope<'s, '_>, value: f64) -> v8::Loc return v8::String::empty(scope).into(); } + if tag == SHORT_STRING_TAG { + let value = JSValue::from_bits(bits); + let mut buf = [0u8; perry_runtime::value::SHORT_STRING_MAX_LEN]; + let len = value.short_string_to_buf(&mut buf); + let rust_str = String::from_utf8_lossy(&buf[..len]); + if let Some(v8_str) = v8::String::new(scope, &rust_str) { + return v8_str.into(); + } + return v8::String::empty(scope).into(); + } + // Check for BigInt pointer if tag == BIGINT_TAG { let ptr = (bits & POINTER_MASK) as *const u8; @@ -355,18 +482,11 @@ fn native_object_to_v8<'s>( let count = std::cmp::min(field_count, keys_length); for i in 0..count { - // Get key string from keys_array (NaN-boxed with STRING_TAG) + // Get key string from keys_array. Keys may be heap strings or + // inline short strings, so route through the general V8 bridge. let key_f64 = unsafe { *keys_elements_ptr.add(i as usize) }; - let key_bits = key_f64.to_bits(); - let key_ptr = (key_bits & POINTER_MASK) as *const u8; - if key_ptr.is_null() || (key_ptr as usize) < 0x1000 { - continue; - } - let key_str = unsafe { native_string_to_rust(key_ptr) }; - if key_str.is_empty() { - continue; - } - let v8_key = match v8::String::new(scope, &key_str) { + let key_val = native_to_v8(scope, key_f64); + let v8_key = match key_val.to_string(scope) { Some(k) => k, None => continue, }; @@ -540,6 +660,9 @@ fn v8_array_to_native(scope: &mut v8::PinScope<'_, '_>, array: v8::Local, array: v8::Local, + array: v8::Local, +) -> *mut u8 { + use perry_runtime::js_array_alloc; + + let length = array.length(); + let native_array = js_array_alloc(length); + unsafe { + (*native_array).length = length; + } + + for i in 0..length { + if let Some(val) = array.get_index(scope, i) { + let native_val = v8_to_native_metadata_value(scope, val); + unsafe { + let data_ptr = (native_array as *mut u8).add(8) as *mut f64; + *data_ptr.add(i as usize) = native_val; + } + } + } + + native_array as *mut u8 +} + /// Convert a V8 BigInt to a native BigInt pointer fn v8_bigint_to_native( _scope: &mut v8::PinScope<'_, '_>, diff --git a/crates/perry-jsruntime/src/interop.rs b/crates/perry-jsruntime/src/interop.rs index 57bdd22e31..cac3376553 100644 --- a/crates/perry-jsruntime/src/interop.rs +++ b/crates/perry-jsruntime/src/interop.rs @@ -5,13 +5,16 @@ use crate::bridge::{ fixup_native_for_v8, get_handle_id, get_js_handle, is_js_handle, make_js_handle_value, - native_to_v8, store_js_handle, v8_to_native, + native_to_v8, store_js_handle, v8_to_native, v8_to_native_metadata_target, + v8_to_native_metadata_value, }; use crate::{ ensure_runtime_initialized, get_tokio_runtime, with_runtime, JsRuntimeState, JS_RUNTIME, }; use deno_core::v8; +use std::collections::hash_map::DefaultHasher; use std::ffi::{c_char, CStr}; +use std::hash::{Hash, Hasher}; use std::path::PathBuf; /// Convert a NaN-boxed f64 to a V8 value, returning None if the conversion fails @@ -49,6 +52,243 @@ pub extern "C" fn js_runtime_init() { perry_runtime::js_set_native_module_js_loader(native_module_js_property_loader); perry_runtime::js_set_new_from_handle_v8(js_new_from_handle_v8_impl); perry_runtime::js_set_handle_typeof(js_handle_typeof); + + with_runtime(install_reflect_metadata_bridge); +} + +fn install_reflect_metadata_bridge(state: &mut JsRuntimeState) { + deno_core::scope!(scope, &mut state.runtime); + let global = scope.get_current_context().global(scope); + + macro_rules! define_global_function { + ($name:literal, $callback:ident) => { + if let (Some(key), Some(function)) = ( + v8::String::new(scope, $name), + v8::Function::builder($callback).build(scope), + ) { + global.set(scope, key.into(), function.into()); + } + }; + } + + define_global_function!( + "__perryReflectDefineMetadata", + reflect_define_metadata_bridge + ); + define_global_function!("__perryReflectGetMetadata", reflect_get_metadata_bridge); + define_global_function!( + "__perryReflectGetOwnMetadata", + reflect_get_own_metadata_bridge + ); + define_global_function!("__perryReflectHasMetadata", reflect_has_metadata_bridge); + define_global_function!( + "__perryReflectHasOwnMetadata", + reflect_has_own_metadata_bridge + ); + define_global_function!( + "__perryReflectGetMetadataKeys", + reflect_get_metadata_keys_bridge + ); + define_global_function!( + "__perryReflectGetOwnMetadataKeys", + reflect_get_own_metadata_keys_bridge + ); + define_global_function!( + "__perryReflectDeleteMetadata", + reflect_delete_metadata_bridge + ); + + let Some(source) = v8::String::new( + scope, + r#" +(function () { + if (typeof Reflect !== "object" || Reflect === null) return; + if ( + Reflect.__perryMetadataBridgeInstalled === true && + Reflect.defineMetadata && + Reflect.defineMetadata.__perryMetadataBridgeWrapper === true + ) { + return; + } + const markBridgeWrapper = fn => { + try { + Object.defineProperty(fn, "__perryMetadataBridgeWrapper", { value: true }); + } catch (_) {} + return fn; + }; + const originalDefine = Reflect.defineMetadata; + const originalGet = Reflect.getMetadata; + const originalGetOwn = Reflect.getOwnMetadata; + const originalHas = Reflect.hasMetadata; + const originalHasOwn = Reflect.hasOwnMetadata; + if (typeof originalDefine !== "function" || typeof originalGet !== "function") { + Reflect.defineMetadata = markBridgeWrapper(function (key, value, target, propertyKey) { + return globalThis.__perryReflectDefineMetadata(key, value, target, propertyKey); + }); + Reflect.getMetadata = markBridgeWrapper(function (key, target, propertyKey) { + return globalThis.__perryReflectGetMetadata(key, target, propertyKey); + }); + Reflect.getOwnMetadata = markBridgeWrapper(function (key, target, propertyKey) { + return globalThis.__perryReflectGetOwnMetadata(key, target, propertyKey); + }); + Reflect.hasMetadata = markBridgeWrapper(function (key, target, propertyKey) { + return globalThis.__perryReflectHasMetadata(key, target, propertyKey); + }); + Reflect.hasOwnMetadata = markBridgeWrapper(function (key, target, propertyKey) { + return globalThis.__perryReflectHasOwnMetadata(key, target, propertyKey); + }); + Reflect.getMetadataKeys = markBridgeWrapper(function (target, propertyKey) { + return globalThis.__perryReflectGetMetadataKeys(target, propertyKey); + }); + Reflect.getOwnMetadataKeys = markBridgeWrapper(function (target, propertyKey) { + return globalThis.__perryReflectGetOwnMetadataKeys(target, propertyKey); + }); + Reflect.deleteMetadata = markBridgeWrapper(function (key, target, propertyKey) { + return globalThis.__perryReflectDeleteMetadata(key, target, propertyKey); + }); + Reflect.metadata = markBridgeWrapper(function (key, value) { + return function (target, propertyKey) { + Reflect.defineMetadata(key, value, target, propertyKey); + }; + }); + Reflect.__perryMetadataBridgeInstalled = true; + return; + } + + Reflect.defineMetadata = markBridgeWrapper(function (key, value, target, propertyKey) { + const result = originalDefine.apply(this, arguments); + globalThis.__perryReflectDefineMetadata(key, value, target, propertyKey); + return result; + }); + + Reflect.getMetadata = markBridgeWrapper(function (key, target, propertyKey) { + const original = originalGet.apply(this, arguments); + return original === undefined + ? globalThis.__perryReflectGetMetadata(key, target, propertyKey) + : original; + }); + + Reflect.getOwnMetadata = markBridgeWrapper(function (key, target, propertyKey) { + const original = typeof originalGetOwn === "function" + ? originalGetOwn.apply(this, arguments) + : undefined; + return original === undefined + ? globalThis.__perryReflectGetOwnMetadata(key, target, propertyKey) + : original; + }); + + Reflect.hasMetadata = markBridgeWrapper(function (key, target, propertyKey) { + if (typeof originalHas === "function" && originalHas.apply(this, arguments)) return true; + return globalThis.__perryReflectHasMetadata(key, target, propertyKey); + }); + + Reflect.hasOwnMetadata = markBridgeWrapper(function (key, target, propertyKey) { + if (typeof originalHasOwn === "function" && originalHasOwn.apply(this, arguments)) return true; + return globalThis.__perryReflectHasOwnMetadata(key, target, propertyKey); + }); + + Reflect.__perryMetadataBridgeInstalled = true; +})(); +"#, + ) else { + return; + }; + let _ = v8::Script::compile(scope, source, None).and_then(|script| script.run(scope)); +} + +fn reflect_define_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let value = v8_to_native_metadata_value(scope, args.get(1)); + let target = v8_to_native_metadata_target(scope, args.get(2)); + let property_key = v8_to_native(scope, args.get(3)); + let result = perry_runtime::proxy::js_reflect_define_metadata(key, value, target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_get_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let target = v8_to_native_metadata_target(scope, args.get(1)); + let property_key = v8_to_native(scope, args.get(2)); + let result = perry_runtime::proxy::js_reflect_get_metadata(key, target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_get_own_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let target = v8_to_native_metadata_target(scope, args.get(1)); + let property_key = v8_to_native(scope, args.get(2)); + let result = perry_runtime::proxy::js_reflect_get_own_metadata(key, target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_has_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let target = v8_to_native_metadata_target(scope, args.get(1)); + let property_key = v8_to_native(scope, args.get(2)); + let result = perry_runtime::proxy::js_reflect_has_metadata(key, target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_has_own_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let target = v8_to_native_metadata_target(scope, args.get(1)); + let property_key = v8_to_native(scope, args.get(2)); + let result = perry_runtime::proxy::js_reflect_has_own_metadata(key, target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_get_metadata_keys_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let target = v8_to_native_metadata_target(scope, args.get(0)); + let property_key = v8_to_native(scope, args.get(1)); + let result = perry_runtime::proxy::js_reflect_get_metadata_keys(target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_get_own_metadata_keys_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let target = v8_to_native_metadata_target(scope, args.get(0)); + let property_key = v8_to_native(scope, args.get(1)); + let result = perry_runtime::proxy::js_reflect_get_own_metadata_keys(target, property_key); + retval.set(native_to_v8(scope, result)); +} + +fn reflect_delete_metadata_bridge( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = v8_to_native(scope, args.get(0)); + let target = v8_to_native_metadata_target(scope, args.get(1)); + let property_key = v8_to_native(scope, args.get(2)); + let result = perry_runtime::proxy::js_reflect_delete_metadata(key, target, property_key); + retval.set(native_to_v8(scope, result)); } /// Probe a V8 handle's `typeof` discriminator. Returns 1 for callables (functions), @@ -212,7 +452,7 @@ pub unsafe extern "C" fn js_load_module(path_ptr: *const i8, path_len: usize) -> let canonical = resolved_path.clone(); - let specifier = match deno_core::ModuleSpecifier::from_file_path(&canonical) { + let target_specifier = match deno_core::ModuleSpecifier::from_file_path(&canonical) { Ok(s) => s, Err(_) => { log::error!( @@ -222,6 +462,37 @@ pub unsafe extern "C" fn js_load_module(path_ptr: *const i8, path_len: usize) -> return 0; } }; + let target_specifier_str = target_specifier.to_string(); + let mut hasher = DefaultHasher::new(); + canonical.hash(&mut hasher); + // Materialize the proxy in a per-process temp directory rather than the + // user's CWD. Deno's recursive loader still resolves the proxy specifier + // through our NodeModuleLoader, so the file must exist on disk even + // though the source is also supplied via load_side_es_module_from_code. + let proxy_dir = std::env::temp_dir().join(format!("perry-js-proxy-{}", std::process::id())); + let _ = std::fs::create_dir_all(&proxy_dir); + let proxy_path = proxy_dir.join(format!("__perry_js_proxy_{:016x}.mjs", hasher.finish())); + let specifier = match deno_core::ModuleSpecifier::from_file_path(&proxy_path) { + Ok(s) => s, + Err(_) => { + log::error!( + "Failed to create proxy module specifier for {:?}", + canonical + ); + return 0; + } + }; + let proxy_code = format!( + r#"import * as __perry_ns from {target:?}; +const __perry_default = Object.prototype.hasOwnProperty.call(__perry_ns, "default") ? __perry_ns.default : __perry_ns; +export {{ __perry_default as default }}; +export * from {target:?}; +"#, + target = target_specifier_str + ); + if let Ok(proxy_file_path) = specifier.to_file_path() { + let _ = std::fs::write(proxy_file_path, &proxy_code); + } let tokio_rt = get_tokio_runtime(); @@ -248,8 +519,15 @@ pub unsafe extern "C" fn js_load_module(path_ptr: *const i8, path_len: usize) -> .build() .expect("Failed to create local Tokio runtime for module loading"); local_rt.block_on(async { - // Load the module (use load_side_es_module since native code is the main module) - let module_id = match state.runtime.load_side_es_module(&specifier).await { + // Load a proxy module rather than the target directly. The target may + // already have been evaluated as a dependency of another JS module; a + // proxy imports and re-exports it without evaluating that target as a + // new side root. + let module_id = match state + .runtime + .load_side_es_module_from_code(&specifier, proxy_code) + .await + { Ok(id) => id, Err(e) => { eprintln!("[js_load_module] FAILED to load '{}': {}", path_str, e); @@ -274,6 +552,8 @@ pub unsafe extern "C" fn js_load_module(path_ptr: *const i8, path_len: usize) -> return Err(()); } + install_reflect_metadata_bridge(state); + // Cache the module state.loaded_modules.insert(canonical.clone(), module_id); @@ -571,9 +851,10 @@ pub unsafe extern "C" fn js_call_value( with_runtime(|state| { deno_core::scope!(scope, &mut state.runtime); + v8::tc_scope!(tc_scope, scope); // Extract the function from the NaN-boxed value - let func_local = match nanbox_to_v8(scope, func_value) { + let func_local = match nanbox_to_v8(tc_scope, func_value) { Some(v) => v, None => { log::error!("Failed to convert function value from NaN-boxed"); @@ -591,20 +872,42 @@ pub unsafe extern "C" fn js_call_value( // Convert arguments let v8_args: Vec> = args .iter() - .map(|&arg| native_to_v8(scope, fixup_native_for_v8(arg))) + .map(|&arg| native_to_v8(tc_scope, fixup_native_for_v8(arg))) .collect(); // Call with undefined as 'this' - let undefined = v8::undefined(scope); - let result = match func.call(scope, undefined.into(), &v8_args) { + let undefined = v8::undefined(tc_scope); + let result = match func.call(tc_scope, undefined.into(), &v8_args) { Some(r) => r, None => { - log::error!("Function call failed"); + if tc_scope.has_caught() { + if let Some(msg_obj) = tc_scope.message() { + let msg_str = msg_obj.get(tc_scope).to_rust_string_lossy(tc_scope); + let line = msg_obj.get_line_number(tc_scope).unwrap_or(0); + let script = msg_obj + .get_script_resource_name(tc_scope) + .map(|s| s.to_rust_string_lossy(tc_scope)) + .unwrap_or_default(); + log::error!( + "[JS-INTEROP] Function value threw: {} ({}:{})", + msg_str, + script, + line + ); + } else if let Some(exception) = tc_scope.exception() { + log::error!( + "[JS-INTEROP] Function value threw: {}", + exception.to_rust_string_lossy(tc_scope) + ); + } + } else { + log::error!("Function call failed"); + } return f64::from_bits(0x7FFC_0000_0000_0001); } }; - v8_to_native(scope, result) + v8_to_native(tc_scope, result) }) } diff --git a/crates/perry-jsruntime/src/modules.rs b/crates/perry-jsruntime/src/modules.rs index ba48a8ca67..296372dc51 100644 --- a/crates/perry-jsruntime/src/modules.rs +++ b/crates/perry-jsruntime/src/modules.rs @@ -9,8 +9,23 @@ use deno_core::{ ModuleSourceCode, ModuleSpecifier, ModuleType, ResolutionKind, }; use deno_error::JsErrorBox; +use once_cell::sync::Lazy; +use regex::Regex; use std::path::{Path, PathBuf}; +// CJS heuristics regex set. These are tight, hot path on every loaded JS +// module (called once per import); compiling them once amortizes the cost. +static EXPORTS_WORD_RE: Lazy = Lazy::new(|| Regex::new(r"\bexports\b").unwrap()); +static REQUIRE_CALL_RE: Lazy = + Lazy::new(|| Regex::new(r#"require\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap()); +static EXPORTS_ASSIGN_RE: Lazy = Lazy::new(|| Regex::new(r"exports\.(\w+)\s*=").unwrap()); +static EXPORT_STAR_RE: Lazy = Lazy::new(|| { + Regex::new(r#"__exportStar\s*\(\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)\s*,\s*exports\s*\)"#) + .unwrap() +}); +static BLOCK_COMMENT_RE: Lazy = Lazy::new(|| Regex::new(r"(?s)/\*.*?\*/").unwrap()); +static LINE_COMMENT_RE: Lazy = Lazy::new(|| Regex::new(r"(?m)//.*$").unwrap()); + /// Node.js-compatible module loader pub struct NodeModuleLoader { /// Base directory for module resolution @@ -68,7 +83,7 @@ impl NodeModuleLoader { if specifier.starts_with("file://") { let path_str = specifier.strip_prefix("file://").unwrap_or(specifier); let path = PathBuf::from(path_str); - if path.exists() { + if path.exists() && path.is_file() { return Ok(path); } return self.resolve_with_extensions(path); @@ -134,17 +149,20 @@ impl NodeModuleLoader { /// Check if a specifier is a Node.js built-in module fn is_node_builtin(specifier: &str) -> bool { + let specifier = specifier.trim_end_matches('/'); matches!( specifier, "net" | "tls" | "http" + | "http2" | "https" | "fs" | "path" | "os" | "crypto" | "stream" + | "stream/web" | "buffer" | "util" | "events" @@ -157,6 +175,8 @@ impl NodeModuleLoader { | "string_decoder" | "zlib" | "readline" + | "repl" + | "timers" | "tty" | "vm" | "worker_threads" @@ -169,12 +189,14 @@ impl NodeModuleLoader { | "node:net" | "node:tls" | "node:http" + | "node:http2" | "node:https" | "node:fs" | "node:path" | "node:os" | "node:crypto" | "node:stream" + | "node:stream/web" | "node:buffer" | "node:util" | "node:events" @@ -187,6 +209,8 @@ impl NodeModuleLoader { | "node:string_decoder" | "node:zlib" | "node:readline" + | "node:repl" + | "node:timers" | "node:tty" | "node:vm" | "node:worker_threads" @@ -272,9 +296,7 @@ impl NodeModuleLoader { if let Some(exports) = pkg.get("exports") { if let Some(entry) = resolve_exports(exports, ".") { let entry_path = package_dir.join(entry); - if entry_path.exists() { - return Ok(entry_path); - } + return self.resolve_with_extensions(entry_path); } } @@ -344,7 +366,10 @@ impl ModuleLoader for NodeModuleLoader { ) -> Result { // Handle Node.js built-in modules with a special URL scheme if Self::is_node_builtin(specifier) { - let builtin_name = specifier.strip_prefix("node:").unwrap_or(specifier); + let builtin_name = specifier + .strip_prefix("node:") + .unwrap_or(specifier) + .trim_end_matches('/'); // Use a special URL scheme for built-ins so we can intercept them in load() return ModuleSpecifier::parse(&format!("node:{}", builtin_name)) .map_err(|e| JsErrorBox::generic(e.to_string())); @@ -364,6 +389,12 @@ impl ModuleLoader for NodeModuleLoader { .map_err(|e| JsErrorBox::generic(e.to_string()))?; let canonical = std::fs::canonicalize(&resolved_path).unwrap_or(resolved_path); + let canonical = if canonical.is_dir() { + self.resolve_with_extensions(canonical) + .map_err(|e| JsErrorBox::generic(e.to_string()))? + } else { + canonical + }; ModuleSpecifier::from_file_path(&canonical).map_err(|_| { JsErrorBox::generic(format!( @@ -402,8 +433,8 @@ impl ModuleLoader for NodeModuleLoader { Ok(c) => c, Err(e) => { return ModuleLoadResponse::Sync(Err(JsErrorBox::generic(format!( - "Failed to read module: {}", - e + "Failed to read module {:?}: {}", + path, e )))) } }; @@ -411,7 +442,7 @@ impl ModuleLoader for NodeModuleLoader { let module_type = self.detect_module_type(&path); // Wrap CommonJS modules if needed - let code = if is_commonjs(&code) { + let code = if module_type != ModuleType::Json && is_commonjs(&code) { wrap_commonjs(&code) } else { code @@ -484,18 +515,35 @@ fn resolve_exports(exports: &serde_json::Value, subpath: &str) -> Option /// Check if code appears to be CommonJS fn is_commonjs(code: &str) -> bool { + if looks_like_esm(code) { + return false; + } + + let code = strip_js_comments(code); + // Quick heuristics for CommonJS detection code.contains("module.exports") || code.contains("exports.") + || EXPORTS_WORD_RE.is_match(&code) + || code.contains("Object.defineProperty(exports,") || (code.contains("require(") && !code.contains("import ")) } +fn looks_like_esm(code: &str) -> bool { + code.lines().any(|line| { + let trimmed = line.trim_start(); + trimmed.starts_with("import ") + || trimmed.starts_with("export ") + || trimmed.starts_with("export{") + }) +} + /// Wrap CommonJS code as ESM fn wrap_commonjs(code: &str) -> String { // Extract all require() specifiers so we can convert them to ESM imports - let require_re = regex::Regex::new(r#"require\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap(); + let code_without_comments = strip_js_comments(code); let mut require_specs: Vec = Vec::new(); - for cap in require_re.captures_iter(code) { + for cap in REQUIRE_CALL_RE.captures_iter(&code_without_comments) { if let Some(spec) = cap.get(1) { let spec_str = spec.as_str().to_string(); if !require_specs.contains(&spec_str) { @@ -504,11 +552,19 @@ fn wrap_commonjs(code: &str) -> String { } } - // Generate ESM import statements for each require() specifier + // Generate ESM namespace imports for each require() specifier. `require()` + // unwraps wrapped CJS default exports when safe, but falls back to the + // namespace if a circular module's default binding is still in TDZ. let imports = require_specs .iter() .enumerate() - .map(|(i, spec)| format!("import _req_{} from '{}';", i, spec)) + .map(|(i, spec)| { + if spec.ends_with(".json") { + format!("import _req_{} from '{}' with {{ type: 'json' }};", i, spec) + } else { + format!("import * as _req_{} from '{}';", i, spec) + } + }) .collect::>() .join("\n"); @@ -516,26 +572,46 @@ fn wrap_commonjs(code: &str) -> String { let require_cases = require_specs .iter() .enumerate() - .map(|(i, spec)| format!(" if (specifier === '{}') return _req_{};", spec, i)) + .map(|(i, spec)| { + if spec.ends_with(".json") { + format!(" if (specifier === '{}') return _req_{};", spec, i) + } else { + format!( + " if (specifier === '{}') return __perry_require_namespace(_req_{});", + spec, i + ) + } + }) .collect::>() .join("\n"); // Extract exported names from CommonJS code to properly re-export them let mut named_exports = Vec::new(); + let mut export_star_specs = Vec::new(); // Find exports.X = assignments - for cap in regex::Regex::new(r"exports\.(\w+)\s*=") - .unwrap() - .captures_iter(code) - { + for cap in EXPORTS_ASSIGN_RE.captures_iter(code) { if let Some(name) = cap.get(1) { let name = name.as_str(); - if name != "__esModule" && !named_exports.contains(&name.to_string()) { + if name != "__esModule" + && name != "default" + && !named_exports.contains(&name.to_string()) + { named_exports.push(name.to_string()); } } } + // Find tslib __exportStar(require("..."), exports) barrel re-exports. + for cap in EXPORT_STAR_RE.captures_iter(code) { + if let Some(spec) = cap.get(1) { + let spec = spec.as_str().to_string(); + if !export_star_specs.contains(&spec) { + export_star_specs.push(spec); + } + } + } + // Use a more sophisticated approach: wrap the code in an IIFE and then export // the results using dynamic re-exports let named_export_decls = if named_exports.is_empty() { @@ -544,7 +620,27 @@ fn wrap_commonjs(code: &str) -> String { // Create individual export statements that reference the _cjs object named_exports .iter() - .map(|n| format!("export const {} = _cjs.{};", n, n)) + .map(|n| { + if is_safe_js_binding_name(n) { + format!("export const {} = _cjs.{};", n, n) + } else { + let alias = format!("_cjs_export_{}", n); + format!( + "const {} = _cjs.{};\nexport {{ {} as {} }};", + alias, n, alias, n + ) + } + }) + .collect::>() + .join("\n") + }; + + let export_star_decls = if export_star_specs.is_empty() { + String::new() + } else { + export_star_specs + .iter() + .map(|spec| format!("export * from '{}';", spec)) .collect::>() .join("\n") }; @@ -552,8 +648,15 @@ fn wrap_commonjs(code: &str) -> String { format!( r#"{} const _cjs = (function() {{ - const module = {{ exports: {{}} }}; - const exports = module.exports; + var module = {{ exports: {{}} }}; + var exports = module.exports; + function __perry_require_namespace(ns) {{ + try {{ + if (ns.__perry_commonjs === true && ns.default !== undefined) return ns.default; + }} catch (_) {{ + }} + return ns; + }} function require(specifier) {{ {} throw new Error('require() is not supported: ' + specifier); @@ -565,9 +668,70 @@ const _cjs = (function() {{ }})(); export default _cjs; +export const __perry_commonjs = true; +{} {} "#, - imports, require_cases, code, named_export_decls + imports, require_cases, code, named_export_decls, export_star_decls + ) +} + +fn strip_js_comments(code: &str) -> String { + let without_blocks = BLOCK_COMMENT_RE.replace_all(code, ""); + LINE_COMMENT_RE + .replace_all(&without_blocks, "") + .into_owned() +} + +fn is_safe_js_binding_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + let mut chars = name.chars(); + let first = chars.next().unwrap(); + if !(first == '_' || first == '$' || first.is_ascii_alphabetic()) { + return false; + } + if !chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric()) { + return false; + } + !matches!( + name, + "await" + | "break" + | "case" + | "catch" + | "class" + | "const" + | "continue" + | "debugger" + | "default" + | "delete" + | "do" + | "else" + | "export" + | "extends" + | "finally" + | "for" + | "function" + | "if" + | "import" + | "in" + | "instanceof" + | "new" + | "return" + | "static" + | "super" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "yield" ) } @@ -614,15 +778,16 @@ export function connect() { return new TLSSocket(); } export function createSecureContext() { return {}; } export default { TLSSocket, connect, createSecureContext }; "#.to_string(), - "http" | "https" => r#" -// Stub implementation for Node.js http/https module + "http" | "https" | "http2" => r#" +// Stub implementation for Node.js http/https/http2 module export class IncomingMessage {} export class ServerResponse {} export class Agent {} export function request() { throw new Error('http.request not supported in this environment'); } export function get() { throw new Error('http.get not supported in this environment'); } export function createServer() { throw new Error('http.createServer not supported in this environment'); } -export default { IncomingMessage, ServerResponse, Agent, request, get, createServer }; +export function createSecureServer() { throw new Error('http2.createSecureServer not supported in this environment'); } +export default { IncomingMessage, ServerResponse, Agent, request, get, createServer, createSecureServer }; "#.to_string(), "crypto" => r#" // Stub implementation for Node.js 'crypto' module @@ -700,7 +865,7 @@ export function networkInterfaces() { return {}; } export const EOL = '\n'; export default { platform, arch, cpus, homedir, tmpdir, hostname, type, release, totalmem, freemem, uptime, loadavg, networkInterfaces, EOL }; "#.to_string(), - "stream" => r#" + "stream" | "stream/web" => r#" // Stub implementation for Node.js 'stream' module export class Readable { constructor() {} @@ -720,9 +885,33 @@ export class Duplex extends Readable { } export class Transform extends Duplex {} export class PassThrough extends Transform {} +export class ReadableStream {} +export class WritableStream {} +export class TransformStream {} export function pipeline() {} export function finished() {} -export default { Readable, Writable, Duplex, Transform, PassThrough, pipeline, finished }; +export default { Readable, Writable, Duplex, Transform, PassThrough, ReadableStream, WritableStream, TransformStream, pipeline, finished }; +"#.to_string(), + "repl" => r#" +// Stub implementation for Node.js 'repl' module +export function start() { + return { + context: {}, + on() { return this; }, + close() {} + }; +} +export default { start }; +"#.to_string(), + "timers" => r#" +// Stub implementation for Node.js 'timers' module +export const setTimeout = globalThis.setTimeout.bind(globalThis); +export const clearTimeout = globalThis.clearTimeout.bind(globalThis); +export const setInterval = globalThis.setInterval.bind(globalThis); +export const clearInterval = globalThis.clearInterval.bind(globalThis); +export const setImmediate = globalThis.setImmediate || ((fn, ...args) => setTimeout(fn, 0, ...args)); +export const clearImmediate = globalThis.clearImmediate || clearTimeout; +export default { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate }; "#.to_string(), "buffer" => r#" // Stub implementation for Node.js 'buffer' module @@ -752,7 +941,34 @@ export function deprecate(fn) { return fn; } export function inherits(ctor, superCtor) { Object.setPrototypeOf(ctor.prototype, superCtor.prototype); } export const TextEncoder = globalThis.TextEncoder; export const TextDecoder = globalThis.TextDecoder; -export default { promisify, callbackify, inspect, format, debuglog, deprecate, inherits, TextEncoder, TextDecoder }; +// util.types — Node's runtime introspection namespace. NestJS / rxjs +// reach into this for cheap Promise / TypedArray / Map / Set probes +// during DI dispatch. Most call sites just want a boolean; returning +// `false` for an unknown shape is the conservative answer (the caller +// then falls through to its own duck-typing path). +const _isPromiseLike = (v) => v != null && (typeof v === "object" || typeof v === "function") && typeof v.then === "function"; +export const types = { + isPromise: (v) => _isPromiseLike(v), + isAsyncFunction: (v) => typeof v === "function" && v.constructor && v.constructor.name === "AsyncFunction", + isGeneratorFunction: (v) => typeof v === "function" && v.constructor && v.constructor.name === "GeneratorFunction", + isMap: (v) => v instanceof Map, + isSet: (v) => v instanceof Set, + isWeakMap: (v) => v instanceof WeakMap, + isWeakSet: (v) => v instanceof WeakSet, + isRegExp: (v) => v instanceof RegExp, + isDate: (v) => v instanceof Date, + isArrayBuffer: (v) => v instanceof ArrayBuffer, + isSharedArrayBuffer: () => false, + isDataView: (v) => v instanceof DataView, + isUint8Array: (v) => v instanceof Uint8Array, + isTypedArray: (v) => ArrayBuffer.isView(v) && !(v instanceof DataView), + isProxy: () => false, + isNativeError: (v) => v instanceof Error, + isBoxedPrimitive: () => false, + isAnyArrayBuffer: (v) => v instanceof ArrayBuffer, + isModuleNamespaceObject: () => false, +}; +export default { promisify, callbackify, inspect, format, debuglog, deprecate, inherits, TextEncoder, TextDecoder, types }; "#.to_string(), "events" => r#" // Stub implementation for Node.js 'events' module @@ -769,6 +985,12 @@ export class EventEmitter { setMaxListeners() { return this; } getMaxListeners() { return 10; } } +export function once(emitter, event) { + return new Promise((resolve) => emitter.once(event, (...args) => resolve(args))); +} +EventEmitter.EventEmitter = EventEmitter; +EventEmitter.once = once; +export const __perry_commonjs = true; export default EventEmitter; "#.to_string(), "assert" => r#" @@ -798,6 +1020,13 @@ export function parse(str) { const params = new URLSearchParams(str); const obj export function escape(str) { return encodeURIComponent(str); } export function unescape(str) { return decodeURIComponent(str); } export default { stringify, parse, escape, unescape }; +"#.to_string(), + "tty" => r#" +// Stub implementation for Node.js 'tty' module +export function isatty() { return false; } +export class ReadStream {} +export class WriteStream {} +export default { isatty, ReadStream, WriteStream }; "#.to_string(), "string_decoder" => r#" // Stub implementation for Node.js 'string_decoder' module @@ -827,6 +1056,48 @@ export function createGunzip() { throw new Error('zlib.createGunzip not supporte export function createDeflate() { throw new Error('zlib.createDeflate not supported'); } export function createInflate() { throw new Error('zlib.createInflate not supported'); } export default { gzip, gunzip, gzipSync, gunzipSync, deflate, inflate, deflateSync, inflateSync, brotliCompress, brotliDecompress, brotliCompressSync, brotliDecompressSync, createGzip, createGunzip, createDeflate, createInflate }; +"#.to_string(), + "async_hooks" => r#" +// Stub implementation for Node.js 'async_hooks' module +// Used by @nestjs/core for request-scoped DI context propagation (PR #754). +// No real async-context tracking here: each AsyncResource is a thin +// wrapper that just runs the callback in the current context. +export class AsyncResource { + constructor(_type, _options) {} + runInAsyncScope(fn, thisArg, ...args) { return fn.apply(thisArg, args); } + emitDestroy() { return this; } + asyncId() { return 0; } + triggerAsyncId() { return 0; } + bind(fn) { + const ar = this; + return function (...args) { return ar.runInAsyncScope(fn, this, ...args); }; + } + static bind(fn, type, thisArg) { + const ar = new AsyncResource(type || "bound-anonymous-fn"); + return ar.bind(thisArg !== undefined ? fn.bind(thisArg) : fn); + } +} +export class AsyncLocalStorage { + constructor() { this._store = undefined; } + run(store, fn, ...args) { + const prev = this._store; + this._store = store; + try { return fn(...args); } finally { this._store = prev; } + } + exit(fn, ...args) { + const prev = this._store; + this._store = undefined; + try { return fn(...args); } finally { this._store = prev; } + } + getStore() { return this._store; } + enterWith(store) { this._store = store; } + disable() { this._store = undefined; } +} +export function executionAsyncId() { return 0; } +export function executionAsyncResource() { return {}; } +export function triggerAsyncId() { return 0; } +export function createHook() { return { enable() { return this; }, disable() { return this; } }; } +export default { AsyncResource, AsyncLocalStorage, executionAsyncId, executionAsyncResource, triggerAsyncId, createHook }; "#.to_string(), _ => format!(r#" // Empty stub for unsupported Node.js built-in: {} @@ -863,7 +1134,121 @@ mod tests { fn test_is_commonjs() { assert!(is_commonjs("module.exports = {};")); assert!(is_commonjs("exports.foo = 'bar';")); + assert!(is_commonjs("var base64 = exports;")); + assert!(is_commonjs( + "Object.defineProperty(exports, \"__esModule\", { value: true });" + )); assert!(!is_commonjs("export default {};")); assert!(!is_commonjs("import foo from 'bar';")); } + + #[test] + fn test_is_commonjs_does_not_wrap_esm_with_exports_text() { + let code = + "import fs from 'node:fs';\n/** docs mention exports.foo */\nexport const value = 1;"; + + assert!(!is_commonjs(code)); + } + + #[test] + fn test_wrap_commonjs_skips_default_named_export() { + let wrapped = wrap_commonjs("exports.default = 1;\nexports.iterate = 2;"); + + assert!(!wrapped.contains("export const default")); + assert!(wrapped.contains("export default _cjs;")); + assert!(wrapped.contains("export const iterate = _cjs.iterate;")); + } + + #[test] + fn test_wrap_commonjs_requires_namespace_imports() { + let wrapped = wrap_commonjs("const uid = require('uid');\nexports.value = uid.uid();"); + + assert!(wrapped.contains("import * as _req_0 from 'uid';")); + assert!( + wrapped.contains("if (specifier === 'uid') return __perry_require_namespace(_req_0);") + ); + assert!(wrapped.contains( + "if (ns.__perry_commonjs === true && ns.default !== undefined) return ns.default;" + )); + assert!(wrapped.contains("catch (_)")); + assert!(wrapped.contains("export const __perry_commonjs = true;")); + } + + #[test] + fn test_wrap_commonjs_ignores_require_in_comments() { + let wrapped = wrap_commonjs( + "module.exports = roots;\n/** Example only: require('./compiled.js'); */", + ); + + assert!(!wrapped.contains("import * as _req_0 from './compiled.js';")); + assert!(!wrapped.contains("specifier === './compiled.js'")); + } + + #[test] + fn test_wrap_commonjs_imports_json_with_attribute() { + let wrapped = wrap_commonjs("exports.version = require('../package.json').version;"); + + assert!(wrapped.contains("import _req_0 from '../package.json' with { type: 'json' };")); + assert!(wrapped.contains("if (specifier === '../package.json') return _req_0;")); + } + + #[test] + fn test_wrap_commonjs_emits_export_star_barrels() { + let wrapped = wrap_commonjs( + "const tslib_1 = require('tslib');\ntslib_1.__exportStar(require('./decorators'), exports);", + ); + + assert!(wrapped.contains("export * from './decorators';")); + } + + #[test] + fn test_wrap_commonjs_aliases_reserved_export_names() { + let wrapped = wrap_commonjs("exports.static = require('serve-static');"); + + assert!(wrapped.contains("const _cjs_export_static = _cjs.static;")); + assert!(wrapped.contains("export { _cjs_export_static as static };")); + assert!(!wrapped.contains("export const static")); + } + + #[test] + fn test_file_url_directory_resolves_to_index() { + let root = std::env::temp_dir().join(format!( + "perry-jsruntime-module-test-{}", + std::process::id() + )); + let module_dir = root.join("pkg"); + std::fs::create_dir_all(&module_dir).unwrap(); + let index = module_dir.join("index.js"); + std::fs::write(&index, "export const value = 1;").unwrap(); + + let loader = NodeModuleLoader::with_base_dir(root.clone()); + let specifier = format!("file://{}", module_dir.display()); + let resolved = loader + .resolve_module_path(&specifier, &root.join("entry.js")) + .unwrap(); + + assert_eq!(resolved, index); + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn test_package_main_resolves_to_file() { + let root = std::env::temp_dir().join(format!( + "perry-jsruntime-package-test-{}", + std::process::id() + )); + let package_dir = root.join("node_modules").join("pkg"); + std::fs::create_dir_all(&package_dir).unwrap(); + let index = package_dir.join("index.js"); + std::fs::write(&index, "module.exports = {};").unwrap(); + std::fs::write(package_dir.join("package.json"), r#"{"main":"index.js"}"#).unwrap(); + + let loader = NodeModuleLoader::with_base_dir(root.clone()); + let resolved = loader + .resolve_module_path("pkg", &root.join("entry.js")) + .unwrap(); + + assert_eq!(resolved, index); + let _ = std::fs::remove_dir_all(root); + } } diff --git a/crates/perry-runtime/src/object.rs b/crates/perry-runtime/src/object.rs index b178de8ee2..a2fba547f6 100644 --- a/crates/perry-runtime/src/object.rs +++ b/crates/perry-runtime/src/object.rs @@ -35,6 +35,8 @@ thread_local! { /// + arena_walk_objects in the GC path). static OVERFLOW_FIELDS: RefCell>> = RefCell::new(crate::fast_hash::new_ptr_hash_map()); + static CLASS_PROTOTYPE_METHOD_VALUES: RefCell> = + RefCell::new(HashMap::new()); /// Sidecar hash index for object key lookup. The on-object /// `keys_array` only supports O(N) linear scan; for objects that @@ -3097,6 +3099,13 @@ pub extern "C" fn js_object_get_field_by_name( if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { return JSValue::from_bits(bits); } + if name == "prototype" && class_id != 0 && is_class_id_registered(class_id) { + return JSValue::from_bits(bits); + } + if class_id != 0 && class_has_own_method(class_id, name) { + let value = class_prototype_method_value_for_name(class_id, name); + return JSValue::from_bits(value.to_bits()); + } if !name.is_empty() { let result = CLASS_DYNAMIC_PROPS.with(|m| { m.borrow() @@ -7487,6 +7496,75 @@ pub extern "C" fn js_class_method_bind( crate::value::js_nanbox_pointer(closure as i64) } +fn class_ref_id(value: f64) -> Option { + let bits = value.to_bits(); + if (bits >> 48) == 0x7FFE { + let class_id = (bits & 0xFFFF_FFFF) as u32; + if class_id != 0 && is_class_id_registered(class_id) { + return Some(class_id); + } + } + None +} + +unsafe fn metadata_key_to_string(value: f64) -> Option { + let key_str = crate::builtins::js_string_coerce(value); + if key_str.is_null() { + return None; + } + let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); + let name_len = (*key_str).byte_len as usize; + std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) + .ok() + .map(|s| s.to_string()) +} + +fn class_has_own_method(class_id: u32, method_name: &str) -> bool { + let registry = match CLASS_VTABLE_REGISTRY.read() { + Ok(g) => g, + Err(_) => return false, + }; + registry + .as_ref() + .and_then(|reg| reg.get(&class_id)) + .map(|vtable| vtable.methods.contains_key(method_name)) + .unwrap_or(false) +} + +fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) -> f64 { + CLASS_PROTOTYPE_METHOD_VALUES.with(|cache| { + let mut cache = cache.borrow_mut(); + if let Some(bits) = cache.get(&(class_id, method_name.to_string())).copied() { + return f64::from_bits(bits); + } + + // Bounded leak: `js_class_method_bind` keeps the byte pointer for the + // lifetime of the bound closure (it's stashed inside the closure's + // capture frame). We leak one allocation per unique + // `(class_id, method_name)` pair the program ever asks for, so the + // total leak is bounded by the static set of decorated method + // descriptors. The cache below short-circuits repeat queries. + let leaked: &'static [u8] = method_name.as_bytes().to_vec().leak(); + let class_bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF); + let class_ref = f64::from_bits(class_bits); + let value = js_class_method_bind(class_ref, leaked.as_ptr(), leaked.len()); + cache.insert((class_id, method_name.to_string()), value.to_bits()); + value + }) +} + +#[no_mangle] +pub extern "C" fn js_class_prototype_method_value(class_ref: f64, method_key: f64) -> f64 { + let Some(class_id) = class_ref_id(class_ref) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let method_name = unsafe { metadata_key_to_string(method_key) }; + let Some(method_name) = method_name else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + class_prototype_method_value_for_name(class_id, &method_name) +} + /// Extract the module name string from a native module namespace object. unsafe fn get_module_name_from_namespace(namespace_obj: f64) -> &'static str { let jsval = JSValue::from_bits(namespace_obj.to_bits()); @@ -8659,6 +8737,34 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; unsafe { + if let Some(class_id) = class_ref_id(obj_value) { + let method_name = metadata_key_to_string(key_value); + if let Some(method_name) = method_name { + if method_name == "constructor" || class_has_own_method(class_id, &method_name) { + let value = if method_name == "constructor" { + obj_value + } else { + class_prototype_method_value_for_name(class_id, &method_name) + }; + let packed = b"value\0writable\0enumerable\0configurable"; + let desc = js_object_alloc_with_shape( + 0x0D_E5_C2, + 4, + packed.as_ptr(), + packed.len() as u32, + ); + let header_size = std::mem::size_of::(); + let fields = (desc as *mut u8).add(header_size) as *mut f64; + *fields = value; + *fields.add(1) = f64::from_bits(TAG_TRUE); + *fields.add(2) = f64::from_bits(TAG_FALSE); + *fields.add(3) = f64::from_bits(TAG_TRUE); + return f64::from_bits((desc as u64) | 0x7FFD_0000_0000_0000); + } + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + let obj = extract_obj_ptr(obj_value); if obj.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); @@ -8891,6 +8997,25 @@ pub extern "C" fn js_get_global_this() -> f64 { #[no_mangle] pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { unsafe { + if let Some(class_id) = class_ref_id(obj_value) { + let mut names: Vec = vec!["constructor".to_string()]; + if let Ok(registry) = CLASS_VTABLE_REGISTRY.read() { + if let Some(reg) = registry.as_ref() { + if let Some(vtable) = reg.get(&class_id) { + let mut methods: Vec = vtable.methods.keys().cloned().collect(); + methods.sort(); + names.extend(methods); + } + } + } + let result = crate::array::js_array_alloc(names.len() as u32); + for name in names { + let str_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(str_ptr)); + } + return f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000); + } + let obj = extract_obj_ptr(obj_value); if obj.is_null() { let empty = crate::array::js_array_alloc(0); diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 317fa779ab..d0f9614213 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -16,6 +16,7 @@ //! HIR lowering time, which route through the entry points here. use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; use crate::closure::{js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3}; @@ -30,9 +31,29 @@ pub struct ProxyEntry { thread_local! { /// id -> entry. Index 0 is reserved so we never return a null handle. static PROXIES: RefCell>>> = RefCell::new(vec![None]); + /// Backing store for `Reflect.{define,get,has,delete}Metadata` and friends. + /// + /// IMPORTANT: keys are raw NaN-box bits of the target value. For the + /// canary scope (Nest-style DI) targets are always `ClassRef`s + /// (INT32_TAG | class_id) and method-descriptor `.value` closures, both of + /// which have stable bit patterns across the program lifetime. Regular + /// heap-pointer targets are NOT GC-tracked here, so under the generational + /// evacuating GC their entries become stale if the underlying object + /// moves. If/when general object metadata becomes load-bearing, register + /// a scanner that rewrites `target_bits` during GC fixup (similar to the + /// 9 existing scanners in gc.rs). + static REFLECT_METADATA: RefCell> = RefCell::new(HashMap::new()); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct MetadataKey { + target_bits: u64, + key: String, + property_key: Option, } const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; +const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; @@ -75,7 +96,7 @@ fn lookup(proxy_boxed: f64) -> Option { if (bits >> 48) != (POINTER_TAG >> 48) { return None; } - let lower48 = (bits & POINTER_MASK); + let lower48 = bits & POINTER_MASK; // Real heap pointers live >= 0x1_0000_0000 on macOS/iOS arenas. if lower48 >= 0x1_0000_0000 { return None; @@ -591,3 +612,240 @@ pub extern "C" fn js_reflect_define_property(obj: f64, key: f64, descriptor: f64 pub extern "C" fn js_reflect_get_prototype_of(obj: f64) -> f64 { obj } + +#[no_mangle] +pub extern "C" fn js_reflect_define_metadata( + key: f64, + value: f64, + target: f64, + property_key: f64, +) -> f64 { + if let Some(metadata_key) = make_metadata_key(key, target, property_key) { + REFLECT_METADATA.with(|store| { + store.borrow_mut().insert(metadata_key, value); + }); + } + f64::from_bits(TAG_UNDEFINED) +} + +#[no_mangle] +pub extern "C" fn js_reflect_get_metadata(key: f64, target: f64, property_key: f64) -> f64 { + let Some(key_part) = metadata_key_part(key) else { + return f64::from_bits(TAG_UNDEFINED); + }; + let Some(property_key_part) = metadata_property_key_part(property_key) else { + return f64::from_bits(TAG_UNDEFINED); + }; + get_metadata_in_prototype_chain(&key_part, target, property_key_part.as_ref()) +} + +fn get_own_metadata(key: f64, target: f64, property_key: f64) -> f64 { + let Some(metadata_key) = make_metadata_key(key, target, property_key) else { + return f64::from_bits(TAG_UNDEFINED); + }; + REFLECT_METADATA.with(|store| { + store + .borrow() + .get(&metadata_key) + .copied() + .unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)) + }) +} + +#[no_mangle] +pub extern "C" fn js_reflect_get_own_metadata(key: f64, target: f64, property_key: f64) -> f64 { + get_own_metadata(key, target, property_key) +} + +#[no_mangle] +pub extern "C" fn js_reflect_has_metadata(key: f64, target: f64, property_key: f64) -> f64 { + let Some(key_part) = metadata_key_part(key) else { + return f64::from_bits(TAG_FALSE); + }; + let Some(property_key_part) = metadata_property_key_part(property_key) else { + return f64::from_bits(TAG_FALSE); + }; + let found = get_metadata_in_prototype_chain(&key_part, target, property_key_part.as_ref()) + .to_bits() + != TAG_UNDEFINED; + f64::from_bits(if found { TAG_TRUE } else { TAG_FALSE }) +} + +#[no_mangle] +pub extern "C" fn js_reflect_has_own_metadata(key: f64, target: f64, property_key: f64) -> f64 { + let Some(metadata_key) = make_metadata_key(key, target, property_key) else { + return f64::from_bits(TAG_FALSE); + }; + let found = REFLECT_METADATA.with(|store| store.borrow().contains_key(&metadata_key)); + f64::from_bits(if found { TAG_TRUE } else { TAG_FALSE }) +} + +#[no_mangle] +pub extern "C" fn js_reflect_get_metadata_keys(target: f64, property_key: f64) -> f64 { + metadata_keys_for(target, property_key, true) +} + +#[no_mangle] +pub extern "C" fn js_reflect_get_own_metadata_keys(target: f64, property_key: f64) -> f64 { + metadata_keys_for(target, property_key, false) +} + +#[no_mangle] +pub extern "C" fn js_reflect_delete_metadata(key: f64, target: f64, property_key: f64) -> f64 { + let Some(metadata_key) = make_metadata_key(key, target, property_key) else { + return f64::from_bits(TAG_FALSE); + }; + let deleted = REFLECT_METADATA.with(|store| store.borrow_mut().remove(&metadata_key).is_some()); + f64::from_bits(if deleted { TAG_TRUE } else { TAG_FALSE }) +} + +fn make_metadata_key(key: f64, target: f64, property_key: f64) -> Option { + Some(MetadataKey { + target_bits: target.to_bits(), + key: metadata_key_part(key)?, + property_key: metadata_property_key_part(property_key)?, + }) +} + +/// Resolve the `propertyKey` argument of a `Reflect.*Metadata(…)` call. +/// +/// Returns: +/// - `Some(None)` when the argument is `undefined` — class-level metadata. +/// - `Some(Some(s))` for any value that coerces to a string. +/// - `None` for values we explicitly refuse to key on (e.g. Symbols). The +/// caller treats this as "skip the operation" so we never silently store +/// metadata under an unstable bit-pattern key (#754 review). +fn metadata_property_key_part(property_key: f64) -> Option> { + if property_key.to_bits() == TAG_UNDEFINED { + return Some(None); + } + metadata_key_part(property_key).map(Some) +} + +/// Coerce a metadata key to a stable owned String, or return None if the +/// value cannot be represented as a string key. Returning None makes the +/// caller treat the op as a no-op rather than fabricating a fake key. +/// +/// Symbol-keyed metadata is explicitly unsupported (see +/// docs/src/language/decorators.md) — Symbols flow through here and return +/// None rather than colliding on `toString()`'s `"Symbol()"` rendering. +fn metadata_key_part(value: f64) -> Option { + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if let Some((ptr, len)) = crate::string::str_bytes_from_jsvalue(value, &mut scratch) { + if ptr.is_null() { + return None; + } + if len == 0 { + return Some(String::new()); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }; + return Some(String::from_utf8_lossy(bytes).into_owned()); + } + if crate::value::is_js_handle(value) { + let str_ptr = crate::value::js_jsvalue_to_string(value); + if !str_ptr.is_null() { + let nb = + f64::from_bits(crate::value::STRING_TAG | (str_ptr as u64 & 0x0000_FFFF_FFFF_FFFF)); + if let Some((ptr, len)) = crate::string::str_bytes_from_jsvalue(nb, &mut scratch) { + if !ptr.is_null() { + if len == 0 { + return Some(String::new()); + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }; + return Some(String::from_utf8_lossy(bytes).into_owned()); + } + } + } + } + // Numbers, booleans, null — coerce through the standard JS path so + // e.g. `0`, `true`, etc. produce deterministic string keys. + let coerced = crate::builtins::js_string_coerce(value); + if !coerced.is_null() { + let name_ptr = + unsafe { (coerced as *const u8).add(std::mem::size_of::()) }; + let name_len = unsafe { (*coerced).byte_len as usize }; + if let Ok(s) = + std::str::from_utf8(unsafe { std::slice::from_raw_parts(name_ptr, name_len) }) + { + return Some(s.to_string()); + } + } + None +} + +fn get_metadata_in_prototype_chain(key: &str, target: f64, property_key: Option<&String>) -> f64 { + let mut current = target; + loop { + let current_bits = current.to_bits(); + let found = REFLECT_METADATA.with(|store| { + store + .borrow() + .get(&MetadataKey { + target_bits: current_bits, + key: key.to_string(), + property_key: property_key.cloned(), + }) + .copied() + }); + if let Some(value) = found { + return value; + } + + let next = crate::object::js_object_get_prototype_of(current); + let next_bits = next.to_bits(); + if next_bits == TAG_NULL || next_bits == TAG_UNDEFINED || next_bits == current_bits { + return f64::from_bits(TAG_UNDEFINED); + } + current = next; + } +} + +fn metadata_keys_for(target: f64, property_key: f64, include_prototypes: bool) -> f64 { + let Some(wanted_property_key) = metadata_property_key_part(property_key) else { + let empty = crate::array::js_array_alloc(0); + return f64::from_bits(POINTER_TAG | ((empty as u64) & POINTER_MASK)); + }; + + let keys = REFLECT_METADATA.with(|store| { + let mut seen = HashSet::new(); + let mut keys = Vec::new(); + let store = store.borrow(); + let mut current = target; + + loop { + let current_bits = current.to_bits(); + for metadata_key in store.keys() { + if metadata_key.target_bits == current_bits + && metadata_key.property_key == wanted_property_key + && seen.insert(metadata_key.key.clone()) + { + keys.push(metadata_key.key.clone()); + } + } + + if !include_prototypes { + break; + } + + let next = crate::object::js_object_get_prototype_of(current); + let next_bits = next.to_bits(); + if next_bits == TAG_NULL || next_bits == TAG_UNDEFINED || next_bits == current_bits { + break; + } + current = next; + } + + keys + }); + + let mut values = Vec::with_capacity(keys.len()); + for key in keys { + values.push(crate::string::js_string_new_sso( + key.as_ptr(), + key.len() as u32, + )); + } + + let arr = crate::array::js_array_from_f64(values.as_ptr(), values.len() as u32); + f64::from_bits(POINTER_TAG | ((arr as u64) & POINTER_MASK)) +} diff --git a/crates/perry-transform/src/deforest.rs b/crates/perry-transform/src/deforest.rs index 434045a6e4..86757af55f 100644 --- a/crates/perry-transform/src/deforest.rs +++ b/crates/perry-transform/src/deforest.rs @@ -1208,6 +1208,7 @@ fn rewrite_producer_body( name: "__deforest_out".to_string(), ty: Type::Array(Box::new(info.elem_ty.clone())), default: None, + decorators: Vec::new(), is_rest: false, }); diff --git a/crates/perry-transform/src/generator.rs b/crates/perry-transform/src/generator.rs index dcce8e1fd2..ed391f4f9a 100644 --- a/crates/perry-transform/src/generator.rs +++ b/crates/perry-transform/src/generator.rs @@ -1102,6 +1102,7 @@ fn transform_generator_function( ty: Type::Any, is_rest: false, default: None, + decorators: Vec::new(), }], return_type: Type::Any, body: return_body, @@ -1224,6 +1225,7 @@ fn transform_generator_function( ty: Type::Any, is_rest: false, default: None, + decorators: Vec::new(), }], return_type: Type::Any, body: throw_body, @@ -1296,6 +1298,7 @@ fn transform_generator_function( ty: Type::Any, is_rest: false, default: None, + decorators: Vec::new(), }], return_type: Type::Any, body: next_body, @@ -1545,6 +1548,7 @@ fn build_async_step_driver_direct( ty: any_ty.clone(), is_rest: false, default: None, + decorators: Vec::new(), }, perry_hir::Param { id: is_error_param_id, @@ -1552,6 +1556,7 @@ fn build_async_step_driver_direct( ty: bool_ty.clone(), is_rest: false, default: None, + decorators: Vec::new(), }, ], return_type: any_ty.clone(), diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 772c7c6368..bf9aeb001a 100644 --- a/docs/api/perry.d.ts +++ b/docs/api/perry.d.ts @@ -1,6 +1,6 @@ // Auto-generated from Perry's API manifest (#465). Do not edit by hand. // Source: perry-api-manifest::API_MANIFEST -// Coverage: 822 entries across 70 modules +// Coverage: 825 entries across 70 modules declare module "argon2" { /** stdlib */ @@ -10,6 +10,10 @@ declare module "argon2" { } declare module "async_hooks" { + /** stdlib */ + export class AsyncLocalStorage { [key: string]: any; } + /** stdlib */ + export class AsyncResource { [key: string]: any; } } declare module "axios" { @@ -1195,6 +1199,8 @@ declare module "util" { /** stdlib */ export class TextEncoder { [key: string]: any; } /** stdlib */ + export const types: any; + /** stdlib */ export function callbackify(...args: any[]): any; /** stdlib */ export function deprecate(...args: any[]): any; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index fe4c297f71..f0e97a4977 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 822 entries across 70 modules. +Total: 825 entries across 70 modules. ## Modules @@ -88,6 +88,11 @@ Total: 822 entries across 70 modules. ## `async_hooks` +### Classes + +- `AsyncLocalStorage` +- `AsyncResource` + ### Methods - `disable` — instance @@ -1259,6 +1264,10 @@ Total: 822 entries across 70 modules. - `isDeepStrictEqual` — module - `promisify` — module +### Properties + +- `types` + ## `uuid` ### Methods diff --git a/docs/src/language/decorators.md b/docs/src/language/decorators.md index b9ec2e9fd8..dc5ac465c2 100644 --- a/docs/src/language/decorators.md +++ b/docs/src/language/decorators.md @@ -15,34 +15,55 @@ already AOT-deletes most decorator metadata at build time, and TC39's new stage-3 decorator spec deliberately drops the runtime type reflection that NestJS and TypeORM rely on. -Perry follows the modern direction: types are erased at compile time -(see [Limitations](limitations.md)), there is no `Reflect.metadata`, -no `Symbol`-keyed metadata side-tables, and no runtime DI container. -Code that depends on those facilities does not run on Perry as-is and -must be migrated to one of the patterns below. +Perry still follows the modern direction: types are erased at compile +time (see [Limitations](limitations.md)) and there is no runtime DI +container. A small legacy compatibility path exists for libraries that +only need AOT-lowerable decorator side effects and metadata. +Code that depends on richer decorator behavior still needs one of the +patterns below. ## What works today -Perry parses decorator syntax (legacy / experimental form) and supports -**compile-time-only** transforms. The bundled `@log` transform is the -canonical example — it rewrites a decorated method into a wrapper that -prints entry/exit at compile time, with zero runtime decorator -machinery. See `crates/perry-hir/src/decorator_log.rs` for the -implementation. +Perry parses legacy / experimental TypeScript decorator syntax and +supports two paths: + +- **Legacy class decorators, method decorators, property decorators, + constructor parameter decorators, and method parameter decorators** for + Nest-style DI and route metadata canaries. Decorator functions run for + side effects, `Reflect.defineMetadata`, `Reflect.getMetadata`, + `Reflect.getOwnMetadata`, `Reflect.hasMetadata`, + `Reflect.hasOwnMetadata`, `Reflect.getMetadataKeys`, + `Reflect.getOwnMetadataKeys`, `Reflect.deleteMetadata`, and + `@Reflect.metadata(...)` are available. Perry emits + `design:paramtypes` for decorated classes/methods and `design:type` + for decorated properties. +- **Compile-time-only transforms.** The bundled `@log` transform is the + canonical example — it rewrites a decorated method into a wrapper that + prints entry/exit at compile time, with zero runtime decorator + machinery. See `crates/perry-hir/src/decorator_log.rs` for the + implementation. ## What does not work -- `Reflect.metadata(...)` and `Reflect.getMetadata(...)` +- Accessor decorators and descriptor replacement +- Decorator class replacement return values. If a class decorator + returns anything other than `undefined`, Perry throws a `TypeError` + at decorator application time. Real-world decorators like + `@Memoize`, `@Throttle`, and GraphQL resolver wrappers that return + wrapped classes need a Perry-aware port — the lowered class is fixed + in the IR and cannot be replaced at runtime. +- General `Reflect.metadata(...)` helper calls outside decorator syntax - `Symbol(...)` as a metadata key -- `emitDecoratorMetadata`-style runtime type capture (constructor - parameter types are erased; there is no `design:paramtypes`) +- `emitDecoratorMetadata` beyond class/method `design:paramtypes` and + property `design:type` - Runtime DI containers that resolve dependencies by type - (`tsyringe`, NestJS's injector, Angular's root injector) + beyond the reduced class-constructor canary (`tsyringe`, full NestJS + injector behavior, Angular's root injector) - `class-validator`, `type-graphql`, `TypeORM` runtime metadata flows -If your code depends on any of these, the port path is *not* "wait for -Perry to add Reflect" — it is to migrate to the explicit-wiring pattern -below. +If your code depends on any of these, the port path is still explicit +wiring or a dedicated AOT transform, not relying on the full legacy +TypeScript decorator runtime. ## Recommended pattern: explicit construction @@ -178,24 +199,24 @@ collapses into one `new RatingService(api)` line in `services.ts`. ## What about Angular components, NestJS controllers, TypeORM entities? -Perry does not support these decorator surfaces today, and the runtime -metadata they rely on is not on the roadmap. The Path-B option of +Perry's reduced legacy path is enough for small Nest-style +constructor-injection and route-metadata canaries, but it is not full +Angular, NestJS, or TypeORM compatibility. The Path-B option of recognizing `@Component` / `@Controller` / `@Entity` at the compiler level (analogous to Angular Ivy's AOT step) is reserved for if and when a concrete port needs it — see [issue #581][issue-581] for the tracking -discussion. For now, the recommendation is the same: drop the -decorator, write the equivalent explicit construction, register routes -or schema as plain function calls / module-level constants. +discussion. For now, the recommendation is the same: drop the decorator +where possible, write the equivalent explicit construction, register +routes or schema as plain function calls / module-level constants. [issue-581]: https://github.com/PerryTS/perry/issues/581 ## Future direction -If decorators come back into ecosystem fashion, it will be in the -[TC39 stage-3 form][tc39-decorators] — pure compile-time, no metadata -reflection — which aligns naturally with Perry's "types erased, -compile to native" architecture. Any future investment in decorator -support will target that spec, not the legacy / experimental form that -Angular and NestJS use today. +New feature work should prefer the [TC39 stage-3 form][tc39-decorators] +because it aligns better with Perry's "types erased, compile to native" +architecture. The legacy TypeScript path exists for compatibility and +will stay focused on narrow AOT-lowerable metadata cases rather than +becoming a full `tsc` decorator runtime. [tc39-decorators]: https://github.com/tc39/proposal-decorators diff --git a/docs/src/language/limitations.md b/docs/src/language/limitations.md index c45b4b0def..793b340d36 100644 --- a/docs/src/language/limitations.md +++ b/docs/src/language/limitations.md @@ -25,21 +25,35 @@ new Function("return 42"); ## Decorators -Perry parses decorator syntax and supports compile-time-only transforms -(see the bundled `@log` example), but does not implement the runtime -metadata facilities (`Reflect.metadata`, `Symbol`-keyed metadata, -`emitDecoratorMetadata` type capture) that Angular / NestJS / TypeORM -DI containers rely on. See [Decorators](decorators.md) for the full -stance and a worked migration recipe. +Perry parses decorator syntax, supports compile-time-only transforms +(see the bundled `@log` example), and has a reduced legacy TypeScript +compatibility path for class decorators, method decorators, constructor +parameter decorators, method parameter decorators, and property +decorators. That path emits `design:paramtypes` for decorated +classes/methods, `design:type` for decorated properties, and implements +`Reflect.defineMetadata`, `Reflect.getMetadata`, +`Reflect.getOwnMetadata`, `Reflect.hasMetadata`, +`Reflect.hasOwnMetadata`, `Reflect.getMetadataKeys`, +`Reflect.getOwnMetadataKeys`, `Reflect.deleteMetadata`, and +`@Reflect.metadata(...)`. + +Accessor decorators, descriptor replacement, general +`Reflect.metadata(...)` calls outside decorator syntax, `Symbol` +metadata keys, and full Angular / NestJS / TypeORM runtime metadata flows +are not supported. See [Decorators](decorators.md) for details and a +worked migration recipe. ## No Runtime Metadata Reflection -TypeScript-style runtime metadata is not supported: +Perry implements a small metadata subset for legacy decorators. General +runtime reflection is not supported: ```text -// Not supported Reflect.getMetadata("design:type", target, key); +Reflect.getMetadataKeys(target, key); +// Not supported as a general helper call outside decorator syntax +Reflect.metadata("design:type", String)(target, key); ``` ## No User-Space CommonJS require() diff --git a/test-files/decorator-metadata-module/service.ts b/test-files/decorator-metadata-module/service.ts new file mode 100644 index 0000000000..78bbe53353 --- /dev/null +++ b/test-files/decorator-metadata-module/service.ts @@ -0,0 +1,12 @@ +function Injectable() { + return function (target: any) { + Reflect.defineMetadata("module:injectable", true, target); + }; +} + +export class ModuleRepo {} + +@Injectable() +export class ModuleService { + constructor(repo: ModuleRepo) {} +} diff --git a/test-files/fixtures/nest_like_common.js b/test-files/fixtures/nest_like_common.js new file mode 100644 index 0000000000..21835141ca --- /dev/null +++ b/test-files/fixtures/nest_like_common.js @@ -0,0 +1,41 @@ +export const MODULE_METADATA = { + PROVIDERS: "providers", + CONTROLLERS: "controllers", +}; + +export const PATH_METADATA = "path"; +export const METHOD_METADATA = "method"; +export const PARAMTYPES_METADATA = "design:paramtypes"; +export const INJECTABLE_WATERMARK = "__injectable__"; +export const CONTROLLER_WATERMARK = "__controller__"; +export const RequestMethod = { GET: 0 }; + +export function Injectable() { + return target => { + Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); + }; +} + +export function Controller(prefix) { + return target => { + Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target); + Reflect.defineMetadata(PATH_METADATA, prefix, target); + }; +} + +export function Get(path) { + return (_target, _key, descriptor) => { + Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); + Reflect.defineMetadata(METHOD_METADATA, RequestMethod.GET, descriptor.value); + }; +} + +export function Module(metadata) { + return target => { + for (const property in metadata) { + if (Object.hasOwnProperty.call(metadata, property)) { + Reflect.defineMetadata(property, metadata[property], target); + } + } + }; +} diff --git a/test-files/test_decorators_legacy_metadata.ts b/test-files/test_decorators_legacy_metadata.ts new file mode 100644 index 0000000000..6dbc373b97 --- /dev/null +++ b/test-files/test_decorators_legacy_metadata.ts @@ -0,0 +1,24 @@ +import "reflect-metadata"; + +function Injectable() { + return function (target: any) { + Reflect.defineMetadata("injectable", true, target); + }; +} + +function Inject() { + return function (target: any, _propertyKey: any, parameterIndex: number) { + Reflect.defineMetadata("param:" + parameterIndex, true, target); + }; +} + +class Repo {} + +@Injectable() +class Service { + constructor(@Inject() repo: Repo) {} +} + +console.log("injectable", Reflect.getMetadata("injectable", Service)); +console.log("paramtypes", Reflect.getMetadata("design:paramtypes", Service)[0] === Repo); +console.log("param0", Reflect.hasMetadata("param:0", Service)); diff --git a/test-files/test_decorators_legacy_metadata_import.ts b/test-files/test_decorators_legacy_metadata_import.ts new file mode 100644 index 0000000000..8613b2d4f6 --- /dev/null +++ b/test-files/test_decorators_legacy_metadata_import.ts @@ -0,0 +1,8 @@ +import "reflect-metadata"; +import { ModuleRepo, ModuleService } from "./decorator-metadata-module/service"; + +console.log("module injectable", Reflect.getMetadata("module:injectable", ModuleService)); +console.log( + "module paramtypes", + Reflect.getMetadata("design:paramtypes", ModuleService)[0] === ModuleRepo, +); diff --git a/test-files/test_decorators_legacy_methods.ts b/test-files/test_decorators_legacy_methods.ts new file mode 100644 index 0000000000..028d823d48 --- /dev/null +++ b/test-files/test_decorators_legacy_methods.ts @@ -0,0 +1,42 @@ +import "reflect-metadata"; + +let routeHandler: any; + +function Controller(prefix: string) { + return function (target: any) { + Reflect.defineMetadata("controller:prefix", prefix, target); + }; +} + +function Get(path: string) { + return function (target: any, key: string, descriptor: any) { + routeHandler = descriptor.value; + Reflect.defineMetadata("route:path", path, descriptor.value); + Reflect.defineMetadata("route:key", key, target, key); + }; +} + +function RouteParam() { + return function (target: any, key: string, index: number) { + Reflect.defineMetadata("route:param:" + index, key, target, key); + }; +} + +class User {} + +@Controller("/users") +class UsersController { + @Reflect.metadata("custom:method", "ok") + @Get("/:id") + find(@RouteParam() user: User) {} +} + +console.log("controller", Reflect.getMetadata("controller:prefix", UsersController)); +console.log("route path", Reflect.getMetadata("route:path", routeHandler)); +console.log("route key", Reflect.getMetadata("route:key", UsersController, "find")); +console.log("method custom", Reflect.getMetadata("custom:method", UsersController, "find")); +console.log("method param", Reflect.getMetadata("route:param:0", UsersController, "find")); +console.log( + "method paramtypes", + Reflect.getMetadata("design:paramtypes", UsersController, "find")[0] === User, +); diff --git a/test-files/test_decorators_legacy_module_args.ts b/test-files/test_decorators_legacy_module_args.ts new file mode 100644 index 0000000000..4cd6f02c21 --- /dev/null +++ b/test-files/test_decorators_legacy_module_args.ts @@ -0,0 +1,29 @@ +import "reflect-metadata"; + +class Repo {} +class Service { + constructor(repo: Repo) {} +} +class UsersController {} + +function Module(metadata: any): ClassDecorator { + return target => { + for (const key of Object.keys(metadata)) { + Reflect.defineMetadata(key, metadata[key], target); + } + }; +} + +@Module({ + providers: [Service], + controllers: [UsersController], +}) +class AppModule {} + +console.log("provider", Reflect.getMetadata("providers", AppModule)[0] === Service); +console.log("controller", Reflect.getMetadata("controllers", AppModule)[0] === UsersController); +console.log( + "module lengths", + Reflect.getMetadata("providers", AppModule).length, + Reflect.getMetadata("controllers", AppModule).length, +); diff --git a/test-files/test_decorators_legacy_property_metadata.ts b/test-files/test_decorators_legacy_property_metadata.ts new file mode 100644 index 0000000000..f16a9effbe --- /dev/null +++ b/test-files/test_decorators_legacy_property_metadata.ts @@ -0,0 +1,40 @@ +import "reflect-metadata"; + +class Repo {} + +function Inject(token?: any): PropertyDecorator & ParameterDecorator { + return (target: object, key: string | symbol | undefined, index?: number) => { + let type = token || Reflect.getMetadata("design:type", target, key!); + if (index !== undefined) { + const deps = Reflect.getMetadata("self:paramtypes", target) || []; + Reflect.defineMetadata("self:paramtypes", [...deps, { index, param: type }], target); + return; + } + + const props = Reflect.getMetadata("self:properties_metadata", (target as any).constructor) || []; + Reflect.defineMetadata( + "self:properties_metadata", + [...props, { key, type }], + (target as any).constructor, + ); + }; +} + +class Service { + @Inject() + repo!: Repo; + + constructor(@Inject("manual") repo: Repo) {} +} + +const props = Reflect.getMetadata("self:properties_metadata", Service); +console.log("property dep", props[0].key, props[0].type === Repo); +console.log("design type", Reflect.getOwnMetadata("design:type", Service, "repo") === Repo); +console.log("has own", Reflect.hasOwnMetadata("design:type", Service, "repo")); +console.log("keys", Reflect.getMetadataKeys(Service, "repo").join("|")); + +const deps = Reflect.getMetadata("self:paramtypes", Service); +console.log("ctor inject", deps[0].index, deps[0].param); + +Reflect.defineMetadata("temp", true, Service); +console.log("delete", Reflect.deleteMetadata("temp", Service), Reflect.hasMetadata("temp", Service)); diff --git a/test-files/test_decorators_metadata_inheritance.ts b/test-files/test_decorators_metadata_inheritance.ts new file mode 100644 index 0000000000..37c6ac7e90 --- /dev/null +++ b/test-files/test_decorators_metadata_inheritance.ts @@ -0,0 +1,58 @@ +import "reflect-metadata"; + +function BaseController(): ClassDecorator { + return target => { + Reflect.defineMetadata("role", "base", target); + Reflect.defineMetadata("shared", "base", target); + }; +} + +function ChildController(): ClassDecorator { + return target => { + Reflect.defineMetadata("shared", "child", target); + }; +} + +function Route(path: string): MethodDecorator { + return (target: object, key: string | symbol) => { + Reflect.defineMetadata("route:path", path, target, key); + }; +} + +@BaseController() +class BaseUsersController { + @Route("/base") + find() {} +} + +@ChildController() +class UsersController extends BaseUsersController {} + +const metadataKeys = Reflect.getMetadataKeys(UsersController); +const ownKeys = Reflect.getOwnMetadataKeys(UsersController); +let sawInheritedRole = false; +let sawOwnShared = false; +let sawOwnRole = false; + +for (const key of metadataKeys) { + if (key === "role") { + sawInheritedRole = true; + } + if (key === "shared") { + sawOwnShared = true; + } +} + +for (const key of ownKeys) { + if (key === "role") { + sawOwnRole = true; + } +} + +console.log("class inherited", Reflect.getMetadata("role", UsersController)); +console.log("class own missing", Reflect.getOwnMetadata("role", UsersController)); +console.log("class override", Reflect.getMetadata("shared", UsersController)); +console.log("has inherited", Reflect.hasMetadata("role", UsersController), Reflect.hasOwnMetadata("role", UsersController)); +console.log("keys inherited", sawInheritedRole && sawOwnShared, sawOwnRole); +console.log("method inherited", Reflect.getMetadata("route:path", UsersController.prototype, "find")); +console.log("method own missing", Reflect.getOwnMetadata("route:path", UsersController.prototype, "find")); diff --git a/test-files/test_decorators_nest_common_canary.ts b/test-files/test_decorators_nest_common_canary.ts new file mode 100644 index 0000000000..1f385aa4b1 --- /dev/null +++ b/test-files/test_decorators_nest_common_canary.ts @@ -0,0 +1,113 @@ +import "reflect-metadata"; + +const MODULE_METADATA = { + IMPORTS: "imports", + PROVIDERS: "providers", + CONTROLLERS: "controllers", + EXPORTS: "exports", +}; +const PATH_METADATA = "path"; +const METHOD_METADATA = "method"; +const PARAMTYPES_METADATA = "design:paramtypes"; +const SELF_DECLARED_DEPS_METADATA = "self:paramtypes"; +const PROPERTY_DEPS_METADATA = "self:properties_metadata"; +const INJECTABLE_WATERMARK = "__injectable__"; +const CONTROLLER_WATERMARK = "__controller__"; +const RequestMethod = { GET: 0 }; + +function Injectable(): ClassDecorator { + return target => Reflect.defineMetadata("__injectable__", true, target); +} + +function Controller(prefix: string): ClassDecorator { + return target => { + Reflect.defineMetadata("__controller__", true, target); + Reflect.defineMetadata("path", prefix, target); + }; +} + +function Inject(token?: any): PropertyDecorator & ParameterDecorator { + const injectCallHasArguments = arguments.length > 0; + return (target: object, key: string | symbol | undefined, index?: number) => { + let type = token || Reflect.getMetadata("design:type", target, key!); + + if (!type && !injectCallHasArguments) { + type = Reflect.getMetadata("design:paramtypes", target, key!)?.[index!]; + } + + if (index !== undefined) { + let dependencies = Reflect.getMetadata("self:paramtypes", target) || []; + dependencies = [...dependencies, { index, param: type }]; + Reflect.defineMetadata("self:paramtypes", dependencies, target); + return; + } + + let properties = Reflect.getMetadata("self:properties_metadata", (target as any).constructor) || []; + properties = [...properties, { key, type }]; + Reflect.defineMetadata("self:properties_metadata", properties, (target as any).constructor); + }; +} + +function Module(metadata: any): ClassDecorator { + return target => { + for (const property in metadata) { + if (Object.hasOwnProperty.call(metadata, property)) { + Reflect.defineMetadata(property, metadata[property], target); + } + } + }; +} + +let routeHandler: any; + +function Get(path: string): MethodDecorator { + return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => { + routeHandler = descriptor.value; + Reflect.defineMetadata("path", path, descriptor.value); + Reflect.defineMetadata("method", 0, descriptor.value); + }; +} + +class Repo {} + +@Injectable() +class Service { + @Inject() + repo!: Repo; + + constructor(@Inject("CUSTOM_REPO") repo: Repo) {} +} + +@Controller("/users") +class UsersController { + @Get("/:id") + find() {} +} + +@Module({ + providers: [Repo, Service], + controllers: [UsersController], + exports: [Service], +}) +class AppModule {} + +const ctorDeps = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, Service); +const propDeps = Reflect.getMetadata(PROPERTY_DEPS_METADATA, Service); + +console.log("injectable", Reflect.getMetadata(INJECTABLE_WATERMARK, Service)); +console.log("design param", Reflect.getMetadata(PARAMTYPES_METADATA, Service)[0] === Repo); +console.log("ctor inject", ctorDeps[0].index, ctorDeps[0].param); +console.log("property inject", propDeps[0].key, propDeps[0].type === Repo); +console.log("controller", Reflect.getMetadata(CONTROLLER_WATERMARK, UsersController)); +console.log("controller path", Reflect.getMetadata(PATH_METADATA, UsersController)); +console.log("route path", Reflect.getMetadata(PATH_METADATA, routeHandler)); +console.log("route method", Reflect.getMetadata(METHOD_METADATA, routeHandler)); +console.log("module providers", Reflect.getMetadata(MODULE_METADATA.PROVIDERS, AppModule)[1] === Service); +console.log("module controllers", Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, AppModule)[0] === UsersController); +console.log("module exports", Reflect.getMetadata(MODULE_METADATA.EXPORTS, AppModule)[0] === Service); +console.log( + "module lengths", + Reflect.getMetadata(MODULE_METADATA.PROVIDERS, AppModule).length, + Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, AppModule).length, + Reflect.getMetadata(MODULE_METADATA.EXPORTS, AppModule).length, +); diff --git a/test-files/test_decorators_nest_integration_canary.ts b/test-files/test_decorators_nest_integration_canary.ts new file mode 100644 index 0000000000..29e189cbf8 --- /dev/null +++ b/test-files/test_decorators_nest_integration_canary.ts @@ -0,0 +1,131 @@ +import "reflect-metadata"; + +const MODULE_METADATA = { + PROVIDERS: "providers", + CONTROLLERS: "controllers", +}; +const PATH_METADATA = "path"; +const METHOD_METADATA = "method"; +const PARAMTYPES_METADATA = "design:paramtypes"; +const INJECTABLE_WATERMARK = "__injectable__"; +const CONTROLLER_WATERMARK = "__controller__"; +const RequestMethod = { GET: 0 }; + +function Injectable(): ClassDecorator { + return target => { + Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); + }; +} + +function Controller(prefix: string): ClassDecorator { + return target => { + Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target); + Reflect.defineMetadata(PATH_METADATA, prefix, target); + }; +} + +function Get(path: string): MethodDecorator { + return (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => { + Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); + Reflect.defineMetadata(METHOD_METADATA, RequestMethod.GET, descriptor.value); + }; +} + +function Module(metadata: any): ClassDecorator { + return target => { + for (const property in metadata) { + if (Object.hasOwnProperty.call(metadata, property)) { + Reflect.defineMetadata(property, metadata[property], target); + } + } + }; +} + +@Injectable() +class Repo { + getLabel() { + return "repo"; + } +} + +@Injectable() +class UsersService { + repo: Repo; + + constructor(repo: Repo) { + this.repo = repo; + } + + findOne() { + return "user:" + this.repo.getLabel(); + } +} + +@Controller("/users") +class UsersController { + service: UsersService; + + constructor(service: UsersService) { + this.service = service; + } + + @Get("/:id") + find() { + return this.service.findOne(); + } +} + +@Module({ + providers: [Repo, UsersService], + controllers: [UsersController], +}) +class AppModule {} + +function resolveProvider(token: any): any { + if (token === Repo) { + return new Repo(); + } + + if (token === UsersService) { + const deps = Reflect.getMetadata(PARAMTYPES_METADATA, UsersService) || []; + return new UsersService(resolveProvider(deps[0])); + } + + return undefined; +} + +const providers = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, AppModule); +const controllers = Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, AppModule); +const controllerType = controllers[0]; +const controllerDeps = Reflect.getMetadata(PARAMTYPES_METADATA, controllerType) || []; +const controller = new UsersController(resolveProvider(controllerDeps[0])); +const names = Object.getOwnPropertyNames(controllerType.prototype); + +let routePath = ""; +let routeMethod = -1; +let routeResult = ""; +for (const name of names) { + const descriptor = Object.getOwnPropertyDescriptor(controllerType.prototype, name); + if (!descriptor) { + continue; + } + + const path = Reflect.getMetadata(PATH_METADATA, descriptor.value); + if (path === "/:id") { + routePath = path; + routeMethod = Reflect.getMetadata(METHOD_METADATA, descriptor.value); + routeResult = controller.find(); + } +} + +console.log("module providers", providers[0] === Repo && providers[1] === UsersService); +console.log("module provider length", providers.length); +console.log("module controllers", controllerType === UsersController); +console.log("module controller length", controllers.length); +console.log("provider injectable", Reflect.getMetadata(INJECTABLE_WATERMARK, UsersService)); +console.log("controller metadata", Reflect.getMetadata(CONTROLLER_WATERMARK, UsersController), Reflect.getMetadata(PATH_METADATA, UsersController)); +console.log("service deps", Reflect.getMetadata(PARAMTYPES_METADATA, UsersService)[0] === Repo); +console.log("controller deps", controllerDeps[0] === UsersService); +console.log("controller dep length", controllerDeps.length); +console.log("route metadata", routePath, routeMethod); +console.log("injected handler", routeResult); diff --git a/test-files/test_decorators_nest_js_common_canary.ts b/test-files/test_decorators_nest_js_common_canary.ts new file mode 100644 index 0000000000..7b94ba18ee --- /dev/null +++ b/test-files/test_decorators_nest_js_common_canary.ts @@ -0,0 +1,49 @@ +import "reflect-metadata"; +import { + Controller, + CONTROLLER_WATERMARK, + Get, + Injectable, + INJECTABLE_WATERMARK, + METHOD_METADATA, + Module, + MODULE_METADATA, + PARAMTYPES_METADATA, + PATH_METADATA, +} from "./fixtures/nest_like_common.js"; + +@Injectable() +class Repo {} + +@Injectable() +class Service { + constructor(repo: Repo) {} +} + +@Controller("/users") +class UsersController { + constructor(service: Service) {} + + @Get("/:id") + find() { + return "ok"; + } +} + +@Module({ + providers: [Repo, Service], + controllers: [UsersController], +}) +class AppModule {} + +const providers = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, AppModule); +const controllers = Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, AppModule); +const routeHandler = UsersController.prototype.find; + +console.log("js decorator injectable", Reflect.getMetadata(INJECTABLE_WATERMARK, Service)); +console.log("js decorator controller", Reflect.getMetadata(CONTROLLER_WATERMARK, UsersController)); +console.log("js decorator provider array", providers.length, providers[0] === Repo, providers[1] === Service); +console.log("js decorator controller array", controllers.length, controllers[0] === UsersController); +console.log("js decorator service deps", Reflect.getMetadata(PARAMTYPES_METADATA, Service)[0] === Repo); +console.log("js decorator controller deps", Reflect.getMetadata(PARAMTYPES_METADATA, UsersController)[0] === Service); +console.log("js decorator route", Reflect.getMetadata(PATH_METADATA, routeHandler), Reflect.getMetadata(METHOD_METADATA, routeHandler)); diff --git a/test-files/test_decorators_nest_metadata_constants.ts b/test-files/test_decorators_nest_metadata_constants.ts new file mode 100644 index 0000000000..ab55bee810 --- /dev/null +++ b/test-files/test_decorators_nest_metadata_constants.ts @@ -0,0 +1,26 @@ +import "reflect-metadata"; + +const INJECTABLE_WATERMARK = "__injectable__"; +const SELF_DECLARED_DEPS_METADATA = "self:paramtypes"; + +function Injectable(): ClassDecorator { + return target => Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); +} + +function Inject(token: any): ParameterDecorator { + return (target: object, _key: string | symbol | undefined, index: number) => { + const deps = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || []; + Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, [...deps, { index, param: token }], target); + }; +} + +class Repo {} + +@Injectable() +class Service { + constructor(@Inject("CUSTOM_REPO") repo: Repo) {} +} + +const deps = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, Service); +console.log("constant injectable key", Reflect.getMetadata(INJECTABLE_WATERMARK, Service)); +console.log("constant param key", !!deps && deps[0].index === 0 && deps[0].param === "CUSTOM_REPO"); diff --git a/test-files/test_decorators_nest_metadata_scanner.ts b/test-files/test_decorators_nest_metadata_scanner.ts new file mode 100644 index 0000000000..eb256c9456 --- /dev/null +++ b/test-files/test_decorators_nest_metadata_scanner.ts @@ -0,0 +1,37 @@ +import "reflect-metadata"; + +const PATH_METADATA = "path"; +const METHOD_METADATA = "method"; + +function Get(path: string): MethodDecorator { + return (_target: object, _key: string | symbol, descriptor: PropertyDescriptor) => { + Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); + Reflect.defineMetadata(METHOD_METADATA, 0, descriptor.value); + }; +} + +class UsersController { + @Get("/:id") + find() {} + create() {} +} + +const names = Object.getOwnPropertyNames(UsersController.prototype); +const findDescriptor = Object.getOwnPropertyDescriptor(UsersController.prototype, "find"); +const findHandler = findDescriptor && findDescriptor.value; +let sawFind = false; +let sawCreate = false; +for (const name of names) { + if (name === "find") { + sawFind = true; + } + if (name === "create") { + sawCreate = true; + } +} + +console.log("prototype methods", sawFind && sawCreate); +console.log("find descriptor", !!findDescriptor); +console.log("scanner ready", !!findDescriptor && typeof (UsersController.prototype as any).find === "function"); +console.log("route path", Reflect.getMetadata(PATH_METADATA, findHandler)); +console.log("route method", Reflect.getMetadata(METHOD_METADATA, findHandler)); diff --git a/test-files/test_decorators_replacement_unsupported.ts b/test-files/test_decorators_replacement_unsupported.ts new file mode 100644 index 0000000000..1ea6a5529e --- /dev/null +++ b/test-files/test_decorators_replacement_unsupported.ts @@ -0,0 +1,25 @@ +// Canary for the maintainer's PR #754 ask: class decorators that return a +// replacement class are now rejected with a runtime TypeError at decorator +// application time. Silent failure (Perry running the decorator for side +// effects and discarding the return) is what this test guards against. +// +// Decorator-init lives in module.init in Perry's lowering, so the throw +// fires before any user code runs — that's why the console.log below is +// unreachable. The expected-output file captures the TypeError header +// Perry prints for an uncaught throw at module init. + +function ReplaceWithOther() { + return function (_target: any) { + return class Other { + static marker = "replacement"; + }; + }; +} + +@ReplaceWithOther() +class Original { + static marker = "original"; +} + +// Unreachable: the throw above stops module init. +console.log("unreachable", Original.marker); diff --git a/test-parity/expected/test_decorators_legacy_metadata.txt b/test-parity/expected/test_decorators_legacy_metadata.txt new file mode 100644 index 0000000000..14a3b93b05 --- /dev/null +++ b/test-parity/expected/test_decorators_legacy_metadata.txt @@ -0,0 +1,3 @@ +injectable true +paramtypes true +param0 true diff --git a/test-parity/expected/test_decorators_legacy_metadata_import.txt b/test-parity/expected/test_decorators_legacy_metadata_import.txt new file mode 100644 index 0000000000..75d8238556 --- /dev/null +++ b/test-parity/expected/test_decorators_legacy_metadata_import.txt @@ -0,0 +1,2 @@ +module injectable true +module paramtypes true diff --git a/test-parity/expected/test_decorators_legacy_methods.txt b/test-parity/expected/test_decorators_legacy_methods.txt new file mode 100644 index 0000000000..7e42a2c952 --- /dev/null +++ b/test-parity/expected/test_decorators_legacy_methods.txt @@ -0,0 +1,6 @@ +controller /users +route path /:id +route key find +method custom ok +method param find +method paramtypes true diff --git a/test-parity/expected/test_decorators_legacy_module_args.txt b/test-parity/expected/test_decorators_legacy_module_args.txt new file mode 100644 index 0000000000..c423965a34 --- /dev/null +++ b/test-parity/expected/test_decorators_legacy_module_args.txt @@ -0,0 +1,3 @@ +provider true +controller true +module lengths 1 1 diff --git a/test-parity/expected/test_decorators_legacy_property_metadata.txt b/test-parity/expected/test_decorators_legacy_property_metadata.txt new file mode 100644 index 0000000000..b93bb9dbde --- /dev/null +++ b/test-parity/expected/test_decorators_legacy_property_metadata.txt @@ -0,0 +1,6 @@ +property dep repo true +design type true +has own true +keys design:type +ctor inject 0 manual +delete true false diff --git a/test-parity/expected/test_decorators_metadata_inheritance.txt b/test-parity/expected/test_decorators_metadata_inheritance.txt new file mode 100644 index 0000000000..0104d40540 --- /dev/null +++ b/test-parity/expected/test_decorators_metadata_inheritance.txt @@ -0,0 +1,7 @@ +class inherited base +class own missing undefined +class override child +has inherited true false +keys inherited true false +method inherited /base +method own missing undefined diff --git a/test-parity/expected/test_decorators_nest_common_canary.txt b/test-parity/expected/test_decorators_nest_common_canary.txt new file mode 100644 index 0000000000..d87bb076c6 --- /dev/null +++ b/test-parity/expected/test_decorators_nest_common_canary.txt @@ -0,0 +1,12 @@ +injectable true +design param true +ctor inject 0 CUSTOM_REPO +property inject repo true +controller true +controller path /users +route path /:id +route method 0 +module providers true +module controllers true +module exports true +module lengths 2 1 1 diff --git a/test-parity/expected/test_decorators_nest_integration_canary.txt b/test-parity/expected/test_decorators_nest_integration_canary.txt new file mode 100644 index 0000000000..a1b4f0d430 --- /dev/null +++ b/test-parity/expected/test_decorators_nest_integration_canary.txt @@ -0,0 +1,11 @@ +module providers true +module provider length 2 +module controllers true +module controller length 1 +provider injectable true +controller metadata true /users +service deps true +controller deps true +controller dep length 1 +route metadata /:id 0 +injected handler user:repo diff --git a/test-parity/expected/test_decorators_nest_js_common_canary.txt b/test-parity/expected/test_decorators_nest_js_common_canary.txt new file mode 100644 index 0000000000..d28fa8fbfa --- /dev/null +++ b/test-parity/expected/test_decorators_nest_js_common_canary.txt @@ -0,0 +1,7 @@ +js decorator injectable true +js decorator controller true +js decorator provider array 2 true true +js decorator controller array 1 true +js decorator service deps true +js decorator controller deps true +js decorator route /:id 0 diff --git a/test-parity/expected/test_decorators_nest_metadata_constants.txt b/test-parity/expected/test_decorators_nest_metadata_constants.txt new file mode 100644 index 0000000000..ecb17161ab --- /dev/null +++ b/test-parity/expected/test_decorators_nest_metadata_constants.txt @@ -0,0 +1,2 @@ +constant injectable key true +constant param key true diff --git a/test-parity/expected/test_decorators_nest_metadata_scanner.txt b/test-parity/expected/test_decorators_nest_metadata_scanner.txt new file mode 100644 index 0000000000..389169f63f --- /dev/null +++ b/test-parity/expected/test_decorators_nest_metadata_scanner.txt @@ -0,0 +1,5 @@ +prototype methods true +find descriptor true +scanner ready true +route path /:id +route method 0 diff --git a/test-parity/expected/test_decorators_replacement_unsupported.txt b/test-parity/expected/test_decorators_replacement_unsupported.txt new file mode 100644 index 0000000000..a5e4fd92e3 --- /dev/null +++ b/test-parity/expected/test_decorators_replacement_unsupported.txt @@ -0,0 +1,2 @@ +TypeError: Class decorator `@ReplaceWithOther` on `Original` returned a replacement class. Perry does not install class replacements from decorators (see docs/src/language/decorators.md). Return `undefined` (or nothing) to keep the decorator running for side effects only. + at diff --git a/tests/release/packages/nestjs-hello/WALLS.md b/tests/release/packages/nestjs-hello/WALLS.md new file mode 100644 index 0000000000..9da27f1df4 --- /dev/null +++ b/tests/release/packages/nestjs-hello/WALLS.md @@ -0,0 +1,107 @@ +# nestjs-hello — current compilation walls + +This fixture is the maintainer's PR #754 ask for an end-to-end smoke test +that boots a real `@nestjs/common` + `@nestjs/core` app through Perry's +legacy decorator metadata path. It is **wired but not yet passing** — +`fixture.sh` reports SKIP and points back here so the release sweep records +the gap without going red. + +The fixture's TypeScript surface (`entry.ts`) is intentionally minimal: +one `@Injectable` service, one `@Controller` with a `@Get` route, one +`@Module`, and `NestFactory.create(AppModule)` + `app.listen(port)`. If +this fixture compiles cleanly and the curl assertions pass, the PR +delivers what was pitched. The walls below are what stand in the way. + +Run order to reproduce: + +```sh +cd tests/release/packages/nestjs-hello +npm install +../../../../target/release/perry entry.ts -o ./out +``` + +## Resolved by PR #754 (this PR) + +- **`util.types` missing from the API manifest.** Caused `\`util.types\` + is not implemented in Perry` early in the compile. Fixed by declaring + `property("util", "types")` in + `crates/perry-api-manifest/src/entries.rs` and shipping a real `types` + surface (isPromise / isAsyncFunction / isMap / isSet / isUint8Array / + isProxy → defaults to `false` for the unknown shapes) in the `node:util` + stub at `crates/perry-jsruntime/src/modules.rs`. +- **`super.` not implemented.** Caused `Direct super property + access not yet supported, use super.method()` for rxjs's + OperatorSubscriber pattern (`this._next = onNext ? wrapper : + super._next`). Lowered to `this.` (and `this[expr]` for computed + access) — correct when the subclass does not override the property, + which covers the rxjs / NestJS pattern. See the inline comment in + `crates/perry-hir/src/lower/expr_misc.rs`. Strict-super semantics with + parent-vtable lookup is still a TODO for a follow-up. +- **`async_hooks.AsyncResource` missing.** Caused + `\`async_hooks.AsyncResource\` is not implemented in Perry` from + `@nestjs/core`'s context-bind path. Fixed by adding + `AsyncResource` + `AsyncLocalStorage` classes (plus + `executionAsyncId` / `createHook` helpers) to a real `async_hooks` + stub in `crates/perry-jsruntime/src/modules.rs`. The bind/runInAsyncScope + shape is enough for the NestJS code path; no real async-context + tracking yet. + +## Open + +### Wall 4 — cross-module method symbol mangling for re-exported classes + +After the three resolved walls, the compile and codegen succeed but +linking fails: + +``` +Undefined symbols for architecture arm64: + "_perry_method_node_modules_rxjs_src_index_ts__Observable__subscribe", … + "_perry_method_node_modules_rxjs_src_index_ts__Subject__error", … + "_perry_method_node_modules_rxjs_src_index_ts__Subscriber__next", … + "_perry_method_node_modules_rxjs_src_index_ts__Subscription__unsubscribe", … +``` + +The class definitions live in +`node_modules/rxjs/src/internal/.ts`, +so the **defining** module's prefix is +`node_modules_rxjs_src_internal_Subscription_ts` (etc.) — that's the +emitted symbol. But callers that import via `rxjs`'s barrel +`src/index.ts` reference the symbol with the **importing** module's +prefix (`node_modules_rxjs_src_index_ts__Subscription__unsubscribe`), +which never gets emitted by anyone, so the linker fails. + +The same mangling site that emits the `extern` declaration at the call +site needs to follow the re-export chain back to the defining module's +prefix. The hono / fastify fixtures don't hit this because they import +flat classes from a single file, not barrel-re-exported class +hierarchies. + +This is a Perry codegen surgery (probably in +`crates/perry-codegen/src/codegen.rs` around the +`emit_string_pool` / class-vtable-registration loop or the cross-module +method-call lowering). Out of scope for PR #754; a focused follow-up. + +### Probable walls after #4 + +Even with the mangling fix in, the NestJS bootstrap likely surfaces a +few more — these are educated guesses based on what NestJS does at +container build time, ranked in order of likelihood: + +1. **Express adapter** — `@nestjs/platform-express` instantiates an + express app, attaches middleware, calls `.listen(port)`. The + compile already showed `_perry_method_node_modules__nestjs_platform_express_adapters_express_adapter_js__ExpressAdapter__initHttpServer` as missing, which is the same wall #4 hitting from a different angle. +2. **`process` / `events` surfaces NestJS Logger reaches into.** The + `EventEmitter` stub already covers the dominant calls, but `setImmediate` + / `process.nextTick` may need a closer look. +3. **`@nestjs/common` reflection helpers** — the container does a + prototype walk via `Object.getOwnPropertyNames` (already working + thanks to the PR's `js_object_get_own_property_names` extension on + class refs). + +## When this fixture flips to PASS + +Once the open walls are gone and `fixture.sh` succeeds end-to-end, +delete this `WALLS.md`. The fixture driver treats `WALLS.md`'s presence +as the marker that turns compile / startup failures into SKIP; removing +it converts those into hard FAILs so a regression past this baseline +becomes impossible. diff --git a/tests/release/packages/nestjs-hello/entry.ts b/tests/release/packages/nestjs-hello/entry.ts new file mode 100644 index 0000000000..e3c5ea0fe9 --- /dev/null +++ b/tests/release/packages/nestjs-hello/entry.ts @@ -0,0 +1,39 @@ +import "reflect-metadata"; +import { Controller, Get, Injectable, Module } from "@nestjs/common"; +import { NestFactory } from "@nestjs/core"; + +@Injectable() +class AppService { + getHello(): string { + return "Hello Perry"; + } +} + +@Controller() +class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} + +@Module({ + imports: [], + controllers: [AppController], + providers: [AppService], +}) +class AppModule {} + +async function bootstrap() { + const port = Number(process.env.PERRY_NEST_PORT ?? "13754"); + const app = await NestFactory.create(AppModule, { logger: false }); + await app.listen(port); + // Tell the fixture driver we're up. Use a fixed marker so the driver + // can wait on it rather than polling the port. Print to stdout (the + // fixture redirects stdout to a log + watches that log). + console.log(`nestjs-hello listening on ${port}`); +} + +bootstrap(); diff --git a/tests/release/packages/nestjs-hello/expected.txt b/tests/release/packages/nestjs-hello/expected.txt new file mode 100644 index 0000000000..d166b299af --- /dev/null +++ b/tests/release/packages/nestjs-hello/expected.txt @@ -0,0 +1,2 @@ +curl status: 200 +curl body: Hello Perry diff --git a/tests/release/packages/nestjs-hello/fixture.sh b/tests/release/packages/nestjs-hello/fixture.sh new file mode 100755 index 0000000000..f2c3b965d6 --- /dev/null +++ b/tests/release/packages/nestjs-hello/fixture.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Tier-3 fixture: nestjs-hello. +# +# Boots a minimal real NestJS app (NestFactory.create + app.listen) compiled +# natively by Perry. The PR #754 maintainer review asks specifically for an +# end-to-end test against the actual `@nestjs/common` + `@nestjs/core` npm +# packages, not a hand-rolled DI mock. +# +# Acceptance (all four must pass for PASS): +# - `npm install` resolves @nestjs/* into node_modules +# - `perry entry.ts -o ./out` exits 0 +# - `./out` reaches the "listening on " marker within startup budget +# - `curl http://localhost:/` returns status 200 with body "Hello Perry" +# +# Any wall before the curl check is documented in WALLS.md and the fixture +# reports SKIP with the wall as the reason (so the release harness records +# the gap without going red, and the next iteration knows where to attack). + +set -uo pipefail +cd "$(dirname "$0")" +. "$(dirname "$0")/../_fixture_lib.sh" + +NAME="nestjs-hello" +PORT="${PERRY_NEST_PORT:-13754}" +STARTUP_TIMEOUT_SECS="${PERRY_NEST_STARTUP_TIMEOUT:-15}" + +fixture_setup "$NAME" || exit 1 + +if ! command -v curl >/dev/null 2>&1; then + fixture_skip "$NAME" "curl not on PATH" +fi + +# Compile. +echo " [compile] perry entry.ts..." +if ! "$PERRY_BIN" entry.ts -o ./out > perry-compile.log 2>&1; then + echo "FAIL $NAME — perry compile errored" + echo " See WALLS.md for the current known compatibility gaps." + sed 's/^/ /' perry-compile.log | tail -60 + if [[ -f WALLS.md ]]; then + # Treat documented compile-time walls as SKIP rather than FAIL so + # the release harness records the gap without blocking. Remove + # WALLS.md once the wall is gone to flip this to a hard FAIL. + fixture_skip "$NAME" "compile-time wall — see WALLS.md" + fi + exit 1 +fi + +# Run in background. +echo " [run] starting compiled binary on port $PORT..." +PERRY_NEST_PORT="$PORT" ./out > perry-run.log 2>&1 & +SERVER_PID=$! + +cleanup() { + if kill -0 "$SERVER_PID" 2>/dev/null; then + kill "$SERVER_PID" 2>/dev/null || true + # SIGTERM first; if still alive after 2 s send SIGKILL. + for _ in 1 2 3 4 5; do + kill -0 "$SERVER_PID" 2>/dev/null || break + sleep 0.4 + done + kill -9 "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Wait for the "listening on" marker. Polling the log avoids racing the +# listener: even if the port is open, NestJS may still be wiring up routes. +echo " [wait] for listener (timeout ${STARTUP_TIMEOUT_SECS}s)..." +deadline=$(( SECONDS + STARTUP_TIMEOUT_SECS )) +while ! grep -q "nestjs-hello listening on" perry-run.log 2>/dev/null; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "FAIL $NAME — server exited before reaching the listening marker" + echo " --- perry-run.log (tail) ---" + tail -60 perry-run.log | sed 's/^/ /' + if [[ -f WALLS.md ]]; then + fixture_skip "$NAME" "runtime wall during bootstrap — see WALLS.md" + fi + exit 1 + fi + if (( SECONDS >= deadline )); then + echo "FAIL $NAME — listener did not come up within ${STARTUP_TIMEOUT_SECS}s" + tail -60 perry-run.log | sed 's/^/ /' + if [[ -f WALLS.md ]]; then + fixture_skip "$NAME" "startup wall — see WALLS.md" + fi + exit 1 + fi + sleep 0.3 +done + +# Hit the endpoint. +echo " [curl] GET http://localhost:$PORT/..." +curl_status="$(curl -s -o curl-body.txt -w "%{http_code}" "http://localhost:$PORT/" || true)" +curl_body="$(cat curl-body.txt 2>/dev/null || echo "")" + +# Build the actual output in the same format as expected.txt for a clean diff. +{ + echo "curl status: $curl_status" + echo "curl body: $curl_body" +} > actual.txt + +if ! diff -u expected.txt actual.txt > diff.log; then + echo "FAIL $NAME — output diverges from expected.txt" + sed 's/^/ /' diff.log + echo " --- perry-run.log (tail) ---" + tail -30 perry-run.log | sed 's/^/ /' + exit 1 +fi + +echo "PASS $NAME" diff --git a/tests/release/packages/nestjs-hello/package-lock.json b/tests/release/packages/nestjs-hello/package-lock.json new file mode 100644 index 0000000000..bdbc2e3cf1 --- /dev/null +++ b/tests/release/packages/nestjs-hello/package-lock.json @@ -0,0 +1,1417 @@ +{ + "name": "perry-release-fixture-nestjs-hello", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "perry-release-fixture-nestjs-hello", + "version": "0.0.0", + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/platform-express": "^10.4.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/tests/release/packages/nestjs-hello/package.json b/tests/release/packages/nestjs-hello/package.json new file mode 100644 index 0000000000..cd6c58eb0b --- /dev/null +++ b/tests/release/packages/nestjs-hello/package.json @@ -0,0 +1,23 @@ +{ + "name": "perry-release-fixture-nestjs-hello", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Tier-3 fixture: minimal NestJS app that wires @Module/@Controller/@Injectable/@Get + DI through Perry's legacy decorator metadata path (PR #754). Starts a real HTTP listener on $PERRY_NEST_PORT (default 13754) so the test exercises bootstrap end-to-end — not a bespoke mock container.", + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/platform-express": "^10.4.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "perry": { + "compilePackages": [ + "@nestjs/common", + "@nestjs/core", + "@nestjs/platform-express", + "reflect-metadata", + "rxjs" + ] + } +}