diff --git a/changelog.d/10153-cjs-getter-reexport-binding.md b/changelog.d/10153-cjs-getter-reexport-binding.md new file mode 100644 index 0000000000..31c304656b --- /dev/null +++ b/changelog.d/10153-cjs-getter-reexport-binding.md @@ -0,0 +1,12 @@ +### Fixed + +- Recognize CommonJS exports installed with `Object.defineProperty`, including + Babel's getter re-exports, as value exports. ESM named and namespace imports + now read the current property through the existing getter-aware runtime + lookup and call the returned value, instead of referencing a function symbol + that the CommonJS barrel never defined. +- Avoid evaluating synthetic CommonJS property exports during initialization, + preserving accessor side effects and exports assigned or replaced later. + Regression coverage includes named imports, namespace calls, ESM barrels, + materialized and dynamic namespaces, writable descriptors, and late + assignments (#10153). diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index fcfd8ec434..60128290d3 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1955,5 +1955,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { ); progress.checkpoint("string pool and registration initializer"); + super::namespace_value_getters::emit(llmod, module_prefix, cross_module); + Ok(()) } diff --git a/crates/perry-codegen/src/codegen/cjs_exports.rs b/crates/perry-codegen/src/codegen/cjs_exports.rs new file mode 100644 index 0000000000..1451edf820 --- /dev/null +++ b/crates/perry-codegen/src/codegen/cjs_exports.rs @@ -0,0 +1,123 @@ +//! Turn the CJS wrapper's synthetic property exports into live value getters. + +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + +use perry_hir::{Expr, Module, Stmt}; + +use crate::module::LlModule; +use crate::types::{DOUBLE, I32, I64, PTR}; + +pub(super) type PropertyExports = HashMap; + +/// The wrapper emits `export const x = _cjs.x` to register value-export +/// metadata. Do not execute that read during module initialization: it can +/// invoke a getter too early and freezes exports assigned after initialization. +/// Keep the caller's HIR immutable (it is also the object-cache input). +pub(super) fn prepare(hir: &Module) -> (Cow<'_, Module>, PropertyExports) { + // Pair the wrapper's private binding with its compiler-owned preamble; + // an ordinary ESM variable named `_cjs` still has snapshot semantics. + let has_preamble = hir.imports.iter().any(|import| { + import + .source + .strip_prefix("node:") + .unwrap_or(&import.source) + == "module" + && import.specifiers.iter().any(|specifier| { + matches!(specifier, perry_hir::ImportSpecifier::Named { imported, local } + if imported == "createRequire" && local == "__perry_cjs_create_require") + }) + }); + if !has_preamble { + return (Cow::Borrowed(hir), HashMap::new()); + } + let entry = super::entry_outline::logical_entry_stmts(hir); + let Some(object_id) = entry.iter().find_map(|stmt| match stmt { + Stmt::Let { id, name, .. } if name == "_cjs" => Some(*id), + _ => None, + }) else { + return (Cow::Borrowed(hir), HashMap::new()); + }; + let exported: HashSet<&str> = hir + .exports + .iter() + .filter_map(|export| match export { + perry_hir::Export::Named { local, .. } => Some(local.as_str()), + _ => None, + }) + .collect(); + let mut properties = HashMap::new(); + let mut bindings = HashSet::new(); + for stmt in entry { + if let Stmt::Let { + id, + name, + init: Some(Expr::PropertyGet { + object, property, .. + }), + .. + } = stmt + { + if exported.contains(name.as_str()) + && matches!(object.as_ref(), Expr::LocalGet(id) if *id == object_id) + { + properties.insert(name.clone(), (object_id, property.clone())); + bindings.insert(*id); + } + } + } + if properties.is_empty() { + return (Cow::Borrowed(hir), properties); + } + let mut owned = hir.clone(); + let clear_snapshots = |stmts: &mut [Stmt]| { + for stmt in stmts { + if let Stmt::Let { id, init, .. } = stmt { + if bindings.contains(id) { + *init = None; + } + } + } + }; + clear_snapshots(&mut owned.init); + for function in &mut owned.functions { + if super::entry_outline::is_entry_chunk(function) { + clear_snapshots(&mut function.body); + } + } + (Cow::Owned(owned), properties) +} + +pub(super) fn emit_getter( + llmod: &mut LlModule, + getter_name: &str, + module_prefix: &str, + object_id: u32, + property: &str, +) { + let (key_global, key_len) = llmod.add_string_constant(property); + let getter = llmod.define_function(getter_name, DOUBLE, vec![]); + getter.create_block("entry"); + let blk = getter.block_mut(0).unwrap(); + // Allocate the key before loading the GC-rooted namespace. No raw object + // pointer survives that allocation; the boxed property helper handles + // accessors, prototypes and function-valued module.exports. + let key = blk.call( + I64, + "js_string_from_bytes", + &[ + (PTR, &format!("@{key_global}")), + (I32, &key_len.to_string()), + ], + ); + let object = blk.load( + DOUBLE, + &format!("@perry_global_{module_prefix}__{object_id}"), + ); + let value = blk.call( + DOUBLE, + "js_object_get_field_by_name_boxed", + &[(DOUBLE, &object), (I64, &key)], + ); + blk.ret(DOUBLE, &value); +} diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 2c93833b55..df28e079e1 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -182,6 +182,7 @@ mod artifact_display_names; mod artifact_source_text; mod artifacts; mod boxed_locals; +mod cjs_exports; #[cfg(test)] mod clone_suffix_tests; mod closure; @@ -213,6 +214,7 @@ mod method; mod method_registry; mod method_trampolines; mod module_globals_emit; +pub(crate) mod namespace_value_getters; mod native_namespace_exports; #[cfg(test)] mod number_exactness_tests; @@ -410,6 +412,8 @@ pub fn user_function_symbol(module_name: &str, function_name: &str) -> String { /// guarantee — do not change to `&mut` without also moving the cache /// hash to AFTER codegen. pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> { + let (live_cjs_hir, cjs_property_exports) = cjs_exports::prepare(hir); + let hir = live_cjs_hir.as_ref(); let progress = CompileProgress::new(&hir.name, module_callable_count(hir)); let triple = opts.target.clone().unwrap_or_else(default_target_triple); let fp_flags = crate::block::FpFlags::new(opts.fast_math, opts.fp_contract_mode); @@ -2731,6 +2735,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> &opts.imported_classes, &cross_module.compile_time_constants, &module_prefix, + &cjs_property_exports, ); cross_module.module_global_proven_types = module_global_proven_types; diff --git a/crates/perry-codegen/src/codegen/module_globals_emit.rs b/crates/perry-codegen/src/codegen/module_globals_emit.rs index 3e4c057b18..ee0669c9a4 100644 --- a/crates/perry-codegen/src/codegen/module_globals_emit.rs +++ b/crates/perry-codegen/src/codegen/module_globals_emit.rs @@ -160,6 +160,7 @@ pub(crate) fn emit_module_globals( imported_classes: &[ImportedClass], compile_time_constants: &HashMap, module_prefix: &str, + cjs_property_exports: &super::cjs_exports::PropertyExports, ) -> ModuleGlobals { // Module-level globals registry. Pre-walk: // 1. Collect every LocalId referenced from any function or method @@ -170,6 +171,9 @@ pub(crate) fn emit_module_globals( // as cheap stack alloca (preserves perf for the bench // benchmarks that don't share state with helper functions). let mut referenced_from_fn: std::collections::HashSet = std::collections::HashSet::new(); + // Live CJS value getters read the namespace even after its synthetic + // snapshot initializers have been removed. + referenced_from_fn.extend(cjs_property_exports.values().map(|(id, _)| *id)); // Helper that handles "params + lets define a scope, refs minus // defines flow out". Used for every function/method/closure body. let scan_body = |params: &[perry_hir::Param], @@ -505,11 +509,21 @@ pub(crate) fn emit_module_globals( let getter_name = format!("perry_fn_{}__{}", module_prefix, sanitize(public_name)); if !llmod.has_function(&getter_name) { - let getter = llmod.define_function(&getter_name, DOUBLE, vec![]); - let _ = getter.create_block("entry"); - let blk = getter.block_mut(0).unwrap(); - let val = blk.load(DOUBLE, &format!("@{}", global_name)); - blk.ret(DOUBLE, &val); + if let Some((object_id, property)) = cjs_property_exports.get(name) { + super::cjs_exports::emit_getter( + llmod, + &getter_name, + module_prefix, + *object_id, + property, + ); + } else { + let getter = llmod.define_function(&getter_name, DOUBLE, vec![]); + let _ = getter.create_block("entry"); + let blk = getter.block_mut(0).unwrap(); + let val = blk.load(DOUBLE, &format!("@{}", global_name)); + blk.ret(DOUBLE, &val); + } } // Import-origin metadata preserves the raw exported diff --git a/crates/perry-codegen/src/codegen/namespace_value_getters.rs b/crates/perry-codegen/src/codegen/namespace_value_getters.rs new file mode 100644 index 0000000000..4b5d14dd0d --- /dev/null +++ b/crates/perry-codegen/src/codegen/namespace_value_getters.rs @@ -0,0 +1,41 @@ +//! Closure-ABI adapters for live exports in materialized static namespaces. + +use super::helpers::sanitize_member; +use super::opts::CrossModuleCtx; +use crate::module::LlModule; +use crate::types::{DOUBLE, I64}; + +pub(crate) fn symbol(module_prefix: &str, namespace: &str, member: &str) -> String { + format!( + "__perry_namespace_value_{}__{}", + module_prefix, + sanitize_member(&format!("{namespace}:{member}")) + ) +} + +pub(super) fn emit(llmod: &mut LlModule, module_prefix: &str, imports: &CrossModuleCtx) { + let mut members: Vec<_> = imports.namespace_member_prefixes.iter().collect(); + members.sort_by_key(|(key, _)| *key); + for ((namespace, member), source_prefix) in members { + let wrapper_name = symbol(module_prefix, namespace, member); + // Lowering declares an adapter only when a namespace value actually + // escapes. Direct `ns.x` reads need no additional wrapper. + if !llmod.is_declared(&wrapper_name) || llmod.has_function(&wrapper_name) { + continue; + } + let origin = crate::expr::import_origin_suffix_ns( + &imports.import_function_origin_names, + &imports.namespace_member_origin_names, + namespace, + member, + ); + let getter_name = format!("perry_fn_{source_prefix}__{origin}"); + llmod.declare_function(&getter_name, DOUBLE, &[]); + let wrapper = + llmod.define_function(wrapper_name, DOUBLE, vec![(I64, "%closure".to_string())]); + wrapper.create_block("entry"); + let block = wrapper.block_mut(0).unwrap(); + let value = block.call(DOUBLE, &getter_name, &[]); + block.ret(DOUBLE, &value); + } +} diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index f6e9c4edf8..9eda91dd0d 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, bail, Result}; use perry_hir::types::Type as HirType; use perry_hir::Expr; -use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::nanbox::double_literal; use crate::rooting::{with_rooted_accumulator, with_rooted_group, Arg, Repr}; use crate::types::{DOUBLE, I32, I64, PTR}; @@ -436,9 +436,10 @@ fn materialize_compiled_namespace(ctx: &mut FnCtx<'_>, name: &str) -> Result, name: &str) -> Result bool { // — no `exports.X =`, no `require(`. Without this arm they fall // through to the ESM pipeline, where the bare `exports` identifier // throws a ReferenceError at module init. - || stripped.contains("defineProperty(exports,") + || perry_perex::tooling::Regex::new( + r"(?:^|[^A-Za-z0-9_$.])Object\s*\.\s*defineProperty\s*\(\s*(?:module\s*\.\s*)?exports\s*,", + ) + .unwrap() + .is_match(&stripped) { return true; } diff --git a/crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs b/crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs index bf8faf1689..ccb1dd31d1 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs @@ -385,7 +385,8 @@ pub fn extract_object_literal_exports_from_require(source: &str) -> Vec<(String, out } -/// Extract named-export patterns from CJS source. Three shapes are matched: +/// Extract named-export patterns from CJS source. These are value bindings, +/// including properties installed by accessors or by later function calls. /// /// 1. `exports.X = ...` and `module.exports.X = ...` — the canonical CJS /// named-export form. Skips `__esModule` (the interop marker injected @@ -463,6 +464,23 @@ pub fn extract_exports_from_source(source: &str) -> Vec { } } + // #10153: Babel emits accessor re-exports with defineProperty rather + // than an assignment. A descriptor's `get` or `value` may hold a function, + // but that is not a function declaration in this module. Surface the name + // through the same value-export path as ordinary exports assignments. + let descriptor_re = perry_perex::tooling::Regex::new( + r#"(?:^|[^A-Za-z0-9_$.])(Object\s*\.\s*defineProperty\s*\(\s*(?:module\s*\.\s*)?exports\s*,\s*['"]([A-Za-z_$][A-Za-z0-9_$]*)['"]\s*,)"#, + ) + .unwrap(); + let stripped = detect::strip_comments_and_strings(source); + for cap in descriptor_re.captures_iter(source) { + let call = cap.get(1).unwrap(); + // Ignore examples embedded in comments and strings. + if stripped[call.start()..].starts_with("Object") { + push_unique(&mut names, cap.get(2).unwrap().as_str()); + } + } + // Shape 2: `module.exports = { ... }` — extract every key from the // object literal body. Brace-balanced scan because the body may contain // nested braces (`module.exports = { fn: function() {} }`). Two key diff --git a/crates/perry/src/commands/compile/cjs_wrap/mod.rs b/crates/perry/src/commands/compile/cjs_wrap/mod.rs index e9ef420884..815617bb51 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/mod.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/mod.rs @@ -21,7 +21,8 @@ //! `_req_N` bindings, runs the original code, and returns //! `module.exports`. The IIFE result is bound to `_cjs`. //! 3. Emit `export default _cjs;` plus `export const X = _cjs.X;` for each -//! detected named export. +//! detected named export. Codegen turns these synthetic property exports +//! into live getters on `_cjs`, without evaluating a snapshot at init. //! //! Two named-export sources are unioned: //! diff --git a/crates/perry/src/commands/compile/cjs_wrap/tests/source_graph.rs b/crates/perry/src/commands/compile/cjs_wrap/tests/source_graph.rs index c11538364d..22c5e81eb4 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/tests/source_graph.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/tests/source_graph.rs @@ -1,5 +1,51 @@ use super::{extract_exports_from_source, wrap_commonjs, PathBuf}; +#[test] +fn detects_whitespace_separated_descriptor_exports() { + let source = "Object . defineProperty (\n exports , 'answer', { value: 42 });"; + assert!(super::is_commonjs(source)); + assert_eq!(extract_exports_from_source(source), ["answer"]); +} + +#[test] +fn descriptor_and_late_exports_are_values_not_declared_functions() { + let source = r#" +Object.defineProperty(exports, "__esModule", { value: true }); +Object.defineProperty(exports, "transform", { + enumerable: true, get: function () { return impl.transform; } +}); +Object.defineProperty(module.exports, 'value', { value: function () { return 42; } }); +exports.update = function () { exports.late = function () { return 43; }; }; +// Object.defineProperty(exports, "comment", { value: 0 }); +var text = 'Object.defineProperty(exports, "text", { value: 0 });'; +other.Object.defineProperty(exports, "other", { value: 0 }); +Object.defineProperty(other.exports, "inner", { value: 0 }); +"#; + let names = extract_exports_from_source(source); + assert_eq!(names, ["update", "late", "transform", "value"]); + let wrapped = wrap_commonjs(source, &PathBuf::from("/tmp/descriptor/index.cjs")); + for name in &names { + assert!(wrapped.contains(&format!("export const {name} = _cjs.{name};"))); + assert!(!wrapped.contains(&format!("export function {name}"))); + } + let ast = perry_parser::parse_typescript(&wrapped, "index.cjs").unwrap(); + let hir = perry_hir::lower_module(&ast, "index", "/tmp/descriptor/index.cjs").unwrap(); + for name in names { + assert!(hir + .exported_objects + .iter() + .any(|exported| exported == &name)); + assert!(!hir + .functions + .iter() + .any(|function| function.is_exported && function.name == name)); + assert!(!hir + .exported_functions + .iter() + .any(|(exported, _)| exported == &name)); + } +} + #[test] fn extracts_esbuild_export_helper_keys() { let src = r#" diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index ceade525bc..dc9b42d01d 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -6,6 +6,9 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Once; +#[path = "source_graph_export_regressions/issue_10153.rs"] +mod issue_10153; + const GC_ENV_OVERRIDES: &[&str] = &[ "PERRY_GEN_GC", "PERRY_GC_SCAVENGE", diff --git a/crates/perry/tests/source_graph_export_regressions/issue_10153.rs b/crates/perry/tests/source_graph_export_regressions/issue_10153.rs new file mode 100644 index 0000000000..6f4e4b5ae6 --- /dev/null +++ b/crates/perry/tests/source_graph_export_regressions/issue_10153.rs @@ -0,0 +1,126 @@ +//! Babel's CommonJS accessor re-exports must remain live value bindings. + +use super::{compile_and_run, write}; + +fn fixtures(dir: &std::path::Path) { + write( + dir, + "index.cjs", + include_str!("../../../../test-files/cjs_getter_reexport/index.cjs"), + ); + write( + dir, + "transform.cjs", + include_str!("../../../../test-files/cjs_getter_reexport/transform.cjs"), + ); +} + +#[test] +fn named_cjs_getter_reexport_is_a_live_value() { + let dir = tempfile::tempdir().unwrap(); + fixtures(dir.path()); + write( + dir.path(), + "main.mjs", + include_str!("../../../../test-files/test_cjs_getter_reexport.ts") + .replace("./cjs_getter_reexport/index.cjs", "./index.cjs") + .as_str(), + ); + assert_eq!( + compile_and_run(dir.path(), "main.mjs"), + "0\nfunction\n2\n3\nundefined\n12\n13\n14\n3\n" + ); +} + +#[test] +fn namespace_cjs_getter_reexport_is_a_live_value() { + let dir = tempfile::tempdir().unwrap(); + fixtures(dir.path()); + write( + dir.path(), + "main.mjs", + "import * as ns from './index.cjs';\n\ + console.log(ns.reads());\n\ + console.log(typeof ns.transform);\n\ + console.log(ns.transform(1));\n\ + console.log(ns.value(2));\n\ + console.log(typeof ns.late);\n\ + ns.update();\n\ + console.log(ns.transform(1));\n\ + console.log(ns.value(2));\n\ + console.log(ns.late(3));\n\ + console.log(ns.reads());\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.mjs"), + "0\nfunction\n2\n3\nundefined\n12\n13\n14\n3\n" + ); +} + +#[test] +fn cjs_getter_reexport_survives_barrels_and_materialized_namespaces() { + let dir = tempfile::tempdir().unwrap(); + fixtures(dir.path()); + write( + dir.path(), + "barrel.mjs", + "export { transform as renamed, update, reads } from './index.cjs';\n", + ); + write( + dir.path(), + "main.mjs", + "import { renamed as transform, update, reads } from './barrel.mjs';\n\ + import * as ns from './barrel.mjs';\n\ + const materialized = ns;\n\ + console.log(reads());\n\ + console.log(transform(1));\n\ + console.log(materialized.renamed(2));\n\ + update();\n\ + console.log(transform(1));\n\ + console.log(materialized.renamed(2));\n\ + console.log(reads());\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.mjs"), + "0\n2\n3\n12\n13\n4\n" + ); +} + +#[test] +fn dynamic_cjs_namespace_keeps_getter_reexports_live() { + let dir = tempfile::tempdir().unwrap(); + fixtures(dir.path()); + write( + dir.path(), + "main.mjs", + "const ns = await import('./index.cjs');\n\ + const key = process.argv[2] || 'transform';\n\ + console.log(ns.reads());\n\ + console.log(Reflect.get(ns, key)(1));\n\ + ns.update();\n\ + console.log(Reflect.get(ns, key)(1));\n\ + console.log(ns.reads());\n", + ); + assert_eq!(compile_and_run(dir.path(), "main.mjs"), "0\n2\n12\n2\n"); +} + +#[test] +fn ordinary_esm_property_export_keeps_its_snapshot() { + let dir = tempfile::tempdir().unwrap(); + write( + dir.path(), + "value.mjs", + "const _cjs = { value: 1 };\n\ + export const value = _cjs.value;\n\ + export function update() { _cjs.value = 2; }\n", + ); + write( + dir.path(), + "main.mjs", + "import { value, update } from './value.mjs';\n\ + console.log(value);\n\ + update();\n\ + console.log(value);\n", + ); + assert_eq!(compile_and_run(dir.path(), "main.mjs"), "1\n1\n"); +} diff --git a/test-files/cjs_getter_reexport/index.cjs b/test-files/cjs_getter_reexport/index.cjs new file mode 100644 index 0000000000..a20a1d5ee8 --- /dev/null +++ b/test-files/cjs_getter_reexport/index.cjs @@ -0,0 +1,19 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +var _transform = require("./transform.cjs"); +Object.defineProperty(exports, "transform", { + enumerable: true, + get: function () { + return _transform.transform; + } +}); +Object.defineProperty(module.exports, "value", { + enumerable: true, + writable: true, + value: function (value) { return value + 1; } +}); +exports.reads = function () { return _transform.reads(); }; +exports.update = function () { + _transform.update(); + exports.value = function (value) { return value + 11; }; + exports.late = function (value) { return value + 11; }; +}; diff --git a/test-files/cjs_getter_reexport/transform.cjs b/test-files/cjs_getter_reexport/transform.cjs new file mode 100644 index 0000000000..8a19078fc7 --- /dev/null +++ b/test-files/cjs_getter_reexport/transform.cjs @@ -0,0 +1,14 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +var transform = function (value) { return value + 1; }; +var readCount = 0; +Object.defineProperty(exports, "transform", { + enumerable: true, + get: function () { + readCount++; + return transform; + } +}); +exports.reads = function () { return readCount; }; +exports.update = function () { + transform = function (value) { return value + 11; }; +}; diff --git a/test-files/test_cjs_getter_reexport.ts b/test-files/test_cjs_getter_reexport.ts new file mode 100644 index 0000000000..351a6f4d66 --- /dev/null +++ b/test-files/test_cjs_getter_reexport.ts @@ -0,0 +1,14 @@ +// #10153 requires live CJS value reads. The parity runner uses the companion +// test-parity/expected file because Node snapshots CJS named imports. +import { transform, value, late, update, reads } from "./cjs_getter_reexport/index.cjs"; + +console.log(reads()); +console.log(typeof transform); +console.log(transform(1)); +console.log(value(2)); +console.log(typeof late); +update(); +console.log(transform(1)); +console.log(value(2)); +console.log(late(3)); +console.log(reads()); diff --git a/test-parity/expected/test_cjs_getter_reexport.txt b/test-parity/expected/test_cjs_getter_reexport.txt new file mode 100644 index 0000000000..24e5ae9123 --- /dev/null +++ b/test-parity/expected/test_cjs_getter_reexport.txt @@ -0,0 +1,9 @@ +0 +function +2 +3 +undefined +12 +13 +14 +3