Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10660-cjs-multiline-var-hoist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed CommonJS class hoisting when a comma-separated `var` declaration continues across lines. Classes such as Redis's `RedisClient` now remain inside their module factory when they capture private-field helper variables declared on later lines, preserving the initialized helper bindings at runtime. (#10660)
13 changes: 10 additions & 3 deletions crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,9 +808,10 @@ fn collect_top_level_let_const_var_names(source: &str) -> Vec<String> {
continue;
};
let mut q = p + kw_len;
// Walk through one or more comma-separated declarators on the same
// logical line. Stop at `=` (initializer), `;`, or the end of line
// (semicolon optional in JS).
// Walk through one or more comma-separated declarators. A comma may
// continue the declaration onto another physical line (as emitted by
// TypeScript for long lists of private-field helpers). Otherwise a
// top-level newline ends an ASI-terminated declaration.
loop {
while q < bytes.len() && (bytes[q] == b' ' || bytes[q] == b'\t') {
q += 1;
Expand Down Expand Up @@ -874,6 +875,12 @@ fn collect_top_level_let_const_var_names(source: &str) -> Vec<String> {
}
b',' if inner == 0 => {
q += 1;
// A top-level comma proves that another declarator
// follows, so newlines here are continuation
// whitespace rather than ASI boundaries.
while q < bytes.len() && bytes[q].is_ascii_whitespace() {
q += 1;
}
break;
}
b';' | b'\n' if inner == 0 => {
Expand Down
28 changes: 28 additions & 0 deletions crates/perry/src/commands/compile/cjs_wrap/tests/hoist_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,31 @@ module.exports = { ChecksumStream };
"the class declaration must remain at its source position inside the factory"
);
}

#[test]
fn multiline_var_declarator_keeps_dependent_class_in_cjs_iife() {
// @redis/client declares its transpiled private-field helpers as one long
// comma-separated `var` statement. The later helpers start on new lines;
// missing them here hoists RedisClient out of the CommonJS factory and
// severs the class methods' captures of those helper functions.
let src = r#"var _RedisClient_instances, _a,
_RedisClient_options,
_RedisClient_isolationPool,
_RedisClient_init;
class RedisClient {
connect() { return _RedisClient_isolationPool(this); }
}
_RedisClient_isolationPool = function _RedisClient_isolationPool() {};
module.exports = { RedisClient };
"#;

let (blocks, hoisted_names, rest) = extract_top_level_class_decls(src);
assert!(
!hoisted_names.iter().any(|name| name == "RedisClient"),
"a class depending on a continued CJS-local declaration must not hoist; hoisted block:\n{blocks}"
);
assert!(
rest.contains("class RedisClient"),
"the dependent class must remain inside the CommonJS factory"
);
}
Loading