From efbbd9f75ad0b2742aa69e4c913febafb6ba4df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 19 Sep 2026 22:55:15 +0200 Subject: [PATCH 1/5] fix(cjs): evaluate a conditional require whatever its target exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A top-level CommonJS `require()` in a conditional position never ran its target when that target carried no CommonJS export marker. The branch was taken, the shim was reached, and the module body never executed — silently, with no crash and no diagnostic. #10754 reports this as a call-site-shape problem (`if` works, `&&` / `?:` / `try` / `switch` / loop-body do not). That is a confound in the reproducer: its `if` case required a module with `module.exports = 1` while the other five required side-effect-only modules. Crossing the two axes against Node 26.5.1 on a release build of main @ 91a566c8af shows the discriminator is the TARGET's export shape — all six shapes fail with a side-effect-only target, all six pass with a value-returning one. #10674 defers a conditional require correctly: the target stays `ModuleInitKind::Deferred` and the shim returns the `_lazyreq_N` import binding, with codegen firing `__init()` at the binding read. That init call is gated on the binding being a known imported FUNCTION (`ctx.import_function_prefixes`), which a target with no default export never is — so nothing fires. - `cjs_wrap/deferred_requires.rs`: an AST visitor replaces the text-scanning deferral classifier, which missed `if (cond) x = require('S')` (still eagerly hoisted on main, a residual #10437 shape), concise arrows, the ternary ALTERNATE arm, both halves of a `do`/`while`, and `&&=`/`||=`/`??=`. `extract_requires::function_local_specs` stays as the parse-failure fallback. - `cjs_wrap/wrap.rs`: a deferred specifier resolves through the path registry rather than its import binding, so initialization no longer depends on the target's export shape; the registry record is memoized per call site once `loaded === true`, because re-entering the registry on every call measured 3.4x on a hot require. - `cjs_wrap/wrap.rs`: the registry only holds EXPORTS for a target that publishes them, which is every CJS-wrapped module and no other. A file with no CommonJS marker is not CJS-wrapped, so it registers an initializer and never any exports, and the registry returned `undefined` where Node returns `{}`. The arm falls back to the import binding on a genuine registry miss, discriminated by `__perry_has_path_module` — the same guard the generic runtime-`require(path)` arm in the same wrapper already uses. - `perry-codegen/src/expr/dyn_extern_i18n.rs`: fire the deferred `__init()` before the imported-class and namespace fast paths, which can themselves depend on module initialization. The implementation is PR #10285's, rebased onto current main; the registry-miss fallback and the gap fixture are new here. `test_gap_10754_cjs_conditional_require_shapes.cts` crosses all six shapes with three cells each — taken/side-effect-only, taken/value-returning and not-taken — because a fix that loads the module unconditionally is #10437 again, not a fix. On unfixed main it differs from the Node oracle on 12 lines (six side-effect-only targets never load; three value-returning targets load before the program's first statement instead of at their call site) and the harness reports parity_fail; with this change it matches Node byte-for-byte and the harness reports PASS. Closes #10754 --- .../perry-codegen/src/expr/dyn_extern_i18n.rs | 23 +- .../compile/cjs_wrap/deferred_requires.rs | 312 ++++++++++++++ .../src/commands/compile/cjs_wrap/mod.rs | 6 +- .../src/commands/compile/cjs_wrap/tests.rs | 5 +- .../src/commands/compile/cjs_wrap/wrap.rs | 95 ++++- .../perry/tests/conditional_require_init.rs | 381 ++++++++++++++++++ test-files/_helpers/gap10754_off_and.cjs | 1 + test-files/_helpers/gap10754_off_for.cjs | 1 + test-files/_helpers/gap10754_off_if.cjs | 1 + test-files/_helpers/gap10754_off_switch.cjs | 1 + test-files/_helpers/gap10754_off_tern.cjs | 1 + test-files/_helpers/gap10754_off_try.cjs | 1 + test-files/_helpers/gap10754_on_exp_and.cjs | 2 + test-files/_helpers/gap10754_on_exp_for.cjs | 2 + test-files/_helpers/gap10754_on_exp_if.cjs | 2 + .../_helpers/gap10754_on_exp_switch.cjs | 2 + test-files/_helpers/gap10754_on_exp_tern.cjs | 2 + test-files/_helpers/gap10754_on_exp_try.cjs | 2 + test-files/_helpers/gap10754_on_sfx_and.cjs | 1 + test-files/_helpers/gap10754_on_sfx_for.cjs | 1 + test-files/_helpers/gap10754_on_sfx_if.cjs | 1 + .../_helpers/gap10754_on_sfx_switch.cjs | 1 + test-files/_helpers/gap10754_on_sfx_tern.cjs | 1 + test-files/_helpers/gap10754_on_sfx_try.cjs | 1 + ...p_10754_cjs_conditional_require_shapes.cts | 135 +++++++ 25 files changed, 947 insertions(+), 34 deletions(-) create mode 100644 crates/perry/src/commands/compile/cjs_wrap/deferred_requires.rs create mode 100644 crates/perry/tests/conditional_require_init.rs create mode 100644 test-files/_helpers/gap10754_off_and.cjs create mode 100644 test-files/_helpers/gap10754_off_for.cjs create mode 100644 test-files/_helpers/gap10754_off_if.cjs create mode 100644 test-files/_helpers/gap10754_off_switch.cjs create mode 100644 test-files/_helpers/gap10754_off_tern.cjs create mode 100644 test-files/_helpers/gap10754_off_try.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_and.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_for.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_if.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_switch.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_tern.cjs create mode 100644 test-files/_helpers/gap10754_on_exp_try.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_and.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_for.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_if.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_switch.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_tern.cjs create mode 100644 test-files/_helpers/gap10754_on_sfx_try.cjs create mode 100644 test-files/test_gap_10754_cjs_conditional_require_shapes.cts diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index fbdef0d15c..64a278cc65 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -832,6 +832,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // checks work; calling those values via stored references would // need a separate runtime path that this commit doesn't add. Expr::ExternFuncRef { name, .. } => { + // A synthetic deferred require evaluates its dependency at the + // original call site. Do this before the class/namespace fast + // paths too: those values can depend on module initialization. + if name.starts_with("_lazyreq_") { + if let Some(source_prefix) = ctx.import_function_prefixes.get(name) { + let init_fn = format!("{}__init", source_prefix); + ctx.pending_declares + .push((init_fn.clone(), crate::types::VOID, vec![])); + ctx.block().call_void(&init_fn, &[]); + } + } // Imported class references (refs #420 / drizzle): when `name` // resolves to a class registered in `ctx.class_ids` (populated // from `opts.imported_classes` for imported classes too), emit @@ -884,18 +895,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } } if let Some(source_prefix) = ctx.import_function_prefixes.get(name).cloned() { - // Next.js lazy-require: a `_lazyreq_N` binding is the CJS require - // shim's handle to a FUNCTION-LOCAL `require('S')`. S is - // `Deferred` (never eager-initialized), so before reading its - // default-export getter, fire `__init()` — idempotent, so - // re-reads cost a guard check. This is the moment Node would run - // S's module body: when `require('S')` is actually called. - if name.starts_with("_lazyreq_") { - let init_fn = format!("{}__init", source_prefix); - ctx.pending_declares - .push((init_fn.clone(), crate::types::VOID, vec![])); - ctx.block().call_void(&init_fn, &[]); - } // Issue #678 followup: a V8-fallback import used as a value // (rather than called directly) has no native singleton // wrapper to point at — the `__perry_wrap_extern_*` for V8 diff --git a/crates/perry/src/commands/compile/cjs_wrap/deferred_requires.rs b/crates/perry/src/commands/compile/cjs_wrap/deferred_requires.rs new file mode 100644 index 0000000000..b349528ba0 --- /dev/null +++ b/crates/perry/src/commands/compile/cjs_wrap/deferred_requires.rs @@ -0,0 +1,312 @@ +//! Preserve the evaluation boundary of conditional and function-local requires. + +use std::collections::{HashMap, HashSet}; + +use swc_ecma_ast as ast; +use swc_ecma_visit::{Visit, VisitWith}; + +/// Synthetic imports collect the target, but must not evaluate it before a +/// conditional branch or a function actually calls `require`. A specifier with +/// any unconditional occurrence keeps the existing eager/alias-adoption path. +/// Use the AST: brace scanning misses concise arrows, unbraced branches, and +/// short-circuit expressions. On a parse failure retain the existing scanner's +/// function-local classification (some CJS sources need wrapping to parse). +pub(super) fn deferred_require_specs(source: &str) -> HashSet { + let Ok(module) = perry_parser::parse_typescript(source, "requires.cjs") else { + return super::extract_requires::function_local_specs(source); + }; + let mut visitor = Requires::default(); + module.visit_with(&mut visitor); + visitor + .sites + .into_iter() + .filter_map(|(specifier, deferred)| deferred.then_some(specifier)) + .collect() +} + +#[derive(Default)] +struct Requires { + deferred: bool, + sites: HashMap, +} + +impl Requires { + fn defer(&mut self, visit: impl FnOnce(&mut Self)) { + let previous = self.deferred; + self.deferred = true; + visit(self); + self.deferred = previous; + } +} + +impl Visit for Requires { + fn visit_call_expr(&mut self, call: &ast::CallExpr) { + if let ast::Callee::Expr(callee) = &call.callee { + if matches!(callee.as_ref(), ast::Expr::Ident(name) if name.sym == *"require") { + if let [arg] = call.args.as_slice() { + if arg.spread.is_none() { + if let ast::Expr::Lit(ast::Lit::Str(specifier)) = arg.expr.as_ref() { + self.sites + .entry(specifier.value.to_string_lossy().into_owned()) + .and_modify(|deferred| *deferred &= self.deferred) + .or_insert(self.deferred); + } + } + } + } + } + call.visit_children_with(self); + } + + fn visit_function(&mut self, function: &ast::Function) { + function.decorators.visit_with(self); + self.defer(|visitor| { + function.params.visit_with(visitor); + function.body.visit_with(visitor); + }); + } + + fn visit_arrow_expr(&mut self, arrow: &ast::ArrowExpr) { + self.defer(|visitor| arrow.visit_children_with(visitor)); + } + + fn visit_constructor(&mut self, constructor: &ast::Constructor) { + self.defer(|visitor| constructor.visit_children_with(visitor)); + } + + fn visit_getter_prop(&mut self, getter: &ast::GetterProp) { + getter.key.visit_with(self); + self.defer(|visitor| getter.body.visit_with(visitor)); + } + + fn visit_setter_prop(&mut self, setter: &ast::SetterProp) { + setter.key.visit_with(self); + self.defer(|visitor| setter.body.visit_with(visitor)); + } + + fn visit_if_stmt(&mut self, stmt: &ast::IfStmt) { + stmt.test.visit_with(self); + self.defer(|visitor| { + stmt.cons.visit_with(visitor); + stmt.alt.visit_with(visitor); + }); + } + + fn visit_cond_expr(&mut self, expr: &ast::CondExpr) { + expr.test.visit_with(self); + self.defer(|visitor| { + expr.cons.visit_with(visitor); + expr.alt.visit_with(visitor); + }); + } + + fn visit_bin_expr(&mut self, expr: &ast::BinExpr) { + expr.left.visit_with(self); + if matches!( + expr.op, + ast::BinaryOp::LogicalAnd | ast::BinaryOp::LogicalOr | ast::BinaryOp::NullishCoalescing + ) { + self.defer(|visitor| expr.right.visit_with(visitor)); + } else { + expr.right.visit_with(self); + } + } + + fn visit_try_stmt(&mut self, stmt: &ast::TryStmt) { + // In particular, a throwing require must stay inside its try/catch. + self.defer(|visitor| stmt.visit_children_with(visitor)); + } + + fn visit_assign_expr(&mut self, expr: &ast::AssignExpr) { + expr.left.visit_with(self); + if matches!( + expr.op, + ast::AssignOp::AndAssign | ast::AssignOp::OrAssign | ast::AssignOp::NullishAssign + ) { + self.defer(|visitor| expr.right.visit_with(visitor)); + } else { + expr.right.visit_with(self); + } + } + + fn visit_switch_stmt(&mut self, stmt: &ast::SwitchStmt) { + stmt.discriminant.visit_with(self); + self.defer(|visitor| stmt.cases.visit_with(visitor)); + } + + fn visit_while_stmt(&mut self, stmt: &ast::WhileStmt) { + stmt.test.visit_with(self); + self.defer(|visitor| stmt.body.visit_with(visitor)); + } + + fn visit_do_while_stmt(&mut self, stmt: &ast::DoWhileStmt) { + // Both halves are conditional: the body can `break` or `return` before + // the test runs, so `do { break } while (require("dep"))` never + // evaluates the require in Node. + self.defer(|visitor| { + stmt.body.visit_with(visitor); + stmt.test.visit_with(visitor); + }); + } + + fn visit_for_stmt(&mut self, stmt: &ast::ForStmt) { + stmt.init.visit_with(self); + stmt.test.visit_with(self); + self.defer(|visitor| { + stmt.update.visit_with(visitor); + stmt.body.visit_with(visitor); + }); + } + + fn visit_for_in_stmt(&mut self, stmt: &ast::ForInStmt) { + stmt.right.visit_with(self); + self.defer(|visitor| { + stmt.left.visit_with(visitor); + stmt.body.visit_with(visitor); + }); + } + + fn visit_for_of_stmt(&mut self, stmt: &ast::ForOfStmt) { + stmt.right.visit_with(self); + self.defer(|visitor| { + stmt.left.visit_with(visitor); + stmt.body.visit_with(visitor); + }); + } +} + +#[cfg(test)] +mod tests { + use super::deferred_require_specs; + + #[test] + fn preserves_conditional_and_function_evaluation_boundaries() { + for source in [ + "if (enabled) require('dep');", + "if (enabled) { const dep = require('dep'); }", + "enabled ? require('dep') : 0;", + "enabled && require('dep');", + "enabled || require('dep');", + "enabled ?? require('dep');", + "value &&= require('dep');", + "value ||= require('dep');", + "value ??= require('dep');", + "try { require('dep'); } catch (e) {}", + "switch (value) { case 1: require('dep'); }", + "while (enabled) require('dep');", + "do { break; } while (require('dep'));", + "do { require('dep'); } while (enabled);", + "for (; enabled;) require('dep');", + "for (const item of items) require('dep');", + "for (const key in object) require('dep');", + "module.exports = () => require('dep');", + "function load(dep = require('dep',)) { return dep; }", + "module.exports = {get value() { return require('dep'); }};", + ] { + assert!(deferred_require_specs(source).contains("dep"), "{source}"); + } + } + + #[test] + fn unconditional_occurrences_keep_existing_eager_classification() { + for source in [ + "const dep = require('dep');", + "if (require('dep')) {}", + "require('dep') && enabled;", + "const x = require('dep') + 1;", + "value = require('dep');", + "{ require('dep'); }", + "if (enabled) require('dep'); require('dep');", + "require('dep'); module.exports = () => require('dep');", + ] { + assert!(!deferred_require_specs(source).contains("dep"), "{source}"); + } + } + + #[test] + fn ignores_comments_strings_and_member_calls() { + assert!(deferred_require_specs( + "// require('dep')\nconst text = \"require('dep')\";\nif (enabled) other.require('dep');" + ).is_empty()); + } + + #[test] + fn wrapping_keeps_conditional_aliases_and_exports_inside_the_body() { + let source = "class Unrelated {}\nif (enabled) {\nconst dep = require('dep');\nexports.value = require('dep');\nconsole.log(dep);\n}\n"; + let wrapped = + super::super::wrap::wrap_commonjs(source, std::path::Path::new("/fixture/index.cjs")); + assert!( + wrapped.contains("import _lazyreq_0 from 'dep';"), + "{wrapped}" + ); + assert!(wrapped.contains("const dep = require('dep');"), "{wrapped}"); + assert!(!wrapped.contains("const dep = _lazyreq_0;"), "{wrapped}"); + assert!( + wrapped.contains("export const value = _cjs.value;"), + "{wrapped}" + ); + assert!( + !wrapped.contains("export { _lazyreq_0 as value };"), + "{wrapped}" + ); + perry_parser::parse_typescript(&wrapped, "wrapped.cjs").unwrap(); + } + + /// A deferred require must not re-enter the path registry on every call. + /// + /// Deferring a conditional require routes it through + /// `__perry_require_path_module`, which is a registry lookup plus a + /// `globalThis` write pair inside a `try`/`finally`. That is needed only until + /// the target is loaded; without the memo it ran on EVERY call and cost 3.4x on + /// a hot require (1.43 B -> 4.86 B instructions over 300k calls). + /// + /// Nothing else pins the memo — delete it and every other test still passes, + /// the only symptom being that hot requires get slow again. So assert the + /// emitted shape: the cache slot is declared, the case is fronted by the + /// short-circuit, and the record is only cached once `loaded === true`. + #[test] + fn a_deferred_require_case_is_fronted_by_its_memo() { + let src = r#" +function get(flag) { + if (flag) { return require("./dep.js").v; } + return 0; +} +module.exports = { get }; +"#; + // The target must RESOLVE: the memo lives in the runtime-record arm, which + // is only emitted for a specifier that resolves to a real file. + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("dep.js"), "module.exports = { v: 1 };\n").unwrap(); + let wrapped = super::super::wrap::wrap_commonjs(src, &dir.path().join("entry.js")); + + for (needle, why) in [ + ("__rec;", "the per-site memo slot declaration"), + ( + "__rec !== undefined) return ", + "the short-circuit that skips the registry", + ), + ( + ".loaded === true", + "the guard that refuses to cache a half-loaded target", + ), + ] { + assert!( + wrapped.contains(needle), + "the deferred-require memo no longer emits `{needle}` ({why}).\n\ + Without it every call to a deferred require re-enters \ + `__perry_require_path_module`, which measured 3.4x slower on a hot \ + require. If the memo moved, update this test; if it was removed on \ + purpose, delete this test and say why in the PR.\n{wrapped}" + ); + } + + // The record, not the exports: a module that replaces `module.exports` + // after evaluation must still read through, as it does in Node. + assert!( + wrapped.contains("__rec.exports"), + "the memo must return `record.exports`, not a cached exports value, or a \ + post-evaluation `module.exports = X` would be invisible to later \ + requires.\n{wrapped}" + ); + } +} diff --git a/crates/perry/src/commands/compile/cjs_wrap/mod.rs b/crates/perry/src/commands/compile/cjs_wrap/mod.rs index 83079a1345..115b8b5898 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/mod.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/mod.rs @@ -37,6 +37,7 @@ //! follows up to a small depth (2 levels) to handle one level of env //! switching; deeper indirection is rare and gets the no-op fallback. +mod deferred_requires; pub(crate) mod detect; mod extract_exports; mod extract_requires; @@ -53,6 +54,7 @@ mod parcel_watcher_tests; mod preamble_canary_tests; // Cross-sibling helpers — siblings reach for these via `use super::*;`. +use deferred_requires::deferred_require_specs; use detect::is_js_reserved_word; use extract_exports::{ extract_exports_from_source, extract_named_exports_from_require, @@ -62,8 +64,8 @@ use extract_exports::{ // #8547: the stdlib-link decision needs the literal `require()` specifiers. pub(crate) use extract_requires::extract_require_specifiers; use extract_requires::{ - extract_export_star_specs, extract_require_aliases_with_ranges, function_local_specs, - identifier_is_declared_binding, identifier_is_reassigned, + extract_export_star_specs, extract_require_aliases_with_ranges, identifier_is_declared_binding, + identifier_is_reassigned, }; use hoist_classes::{ extract_top_level_class_decls, rewrite_module_exports_class_expression, diff --git a/crates/perry/src/commands/compile/cjs_wrap/tests.rs b/crates/perry/src/commands/compile/cjs_wrap/tests.rs index 22ab9db45a..8cf8a8f98f 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/tests.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/tests.rs @@ -751,9 +751,8 @@ exports.spawn = function spawn() { return terminalCtor; }; false, ); assert!( - wrapped.contains("import _req_0 from './windowsTerminal';") - || wrapped.contains("import terminalCtor from './windowsTerminal';"), - "expected live Windows require to stay hoisted, got:\n{}", + wrapped.contains("import _lazyreq_0 from './windowsTerminal';"), + "expected live Windows require to remain collected and initialize in its branch, got:\n{}", wrapped ); assert!( diff --git a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs index da26e86a93..6b8060127a 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs @@ -309,15 +309,21 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( } true }; - // Next.js lazy-require: specifiers whose every `require('S')` call site is - // inside a function body (lazy in Node). Computed up front because it also - // suppresses alias ADOPTION below — a function-local `const dep = - // require('S')` is a function-scoped const, not a module binding, and - // adopting it would hoist `import dep from 'S'` to module scope (eager). We - // instead keep the synthetic binding and rename it `_lazyreq_N` so the - // target stays `Deferred` and inits only when the shim's - // `return _lazyreq_N` runs (i.e. when the function actually calls require). - let mut lazy_specs = function_local_specs(source); + // Specifiers whose every `require('S')` call site is conditional or inside + // a function body. Computed up front because it also suppresses alias + // ADOPTION below — a function-local `const dep = require('S')` is a + // function-scoped const, not a module binding, and adopting it would hoist + // `import dep from 'S'` to module scope (eager). We instead keep the + // synthetic binding and rename it `_lazyreq_N` so the target stays + // `Deferred`, and the shim initializes it through the path registry at the + // moment control flow reaches the call. + // + // #10754: the registry, not the binding, is what makes this work for EVERY + // target. Reading `_lazyreq_N` fires `__init()` only when codegen knows + // the binding as an imported function (`import_function_prefixes`), which a + // target with no default export never is — so a side-effect-only dependency + // was deferred and then never evaluated at all. + let mut lazy_specs = deferred_require_specs(source); let cyclic_specs = cyclic_require_specs(source, source_path); let parent_sensitive_specs = parent_sensitive_require_specs(source, source_path); lazy_specs.extend(cyclic_specs.iter().cloned()); @@ -464,8 +470,11 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( ) }) .unwrap_or_default(); - let needs_runtime_record = - cyclic_specs.contains(spec) || parent_sensitive_specs.contains(spec); + // Deferred targets must initialize even when they have no default + // export getter (for example, a side-effect-only module). The path + // registry owns initialization and cached exports independently of + // the target's export shape, and preserves thrown exceptions here. + let needs_runtime_record = lazy_specs.contains(spec); let runtime_require = if needs_runtime_record { resolved_target .as_ref() @@ -487,9 +496,24 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( } else { String::new() }; + // #10754: the registry initializes the target whatever + // its export shape, but it only holds EXPORTS for a + // target that publishes them — which is every + // CJS-wrapped module and no other. A target with no + // CommonJS marker at all (`console.log('x')` and + // nothing else, the commonest polyfill/registration + // shape) is not CJS-wrapped, so it registers an + // initializer and never any exports: the registry runs + // its body and hands back `undefined` where Node hands + // back `{}`. `__perry_has_path_module` is the + // miss-vs-`undefined`-export discriminator (a real + // module may export `undefined`), and on a genuine miss + // the import binding is the value this arm returned + // before the target was deferred at all. format!( - "const childBefore = require.cache[{path:?}]; globalThis.__perry_cjs_pending_parent = module; let required; try {{ required = __perry_require_path_module({path:?}); }} finally {{ globalThis.__perry_cjs_pending_parent = undefined; }} {warnings}{link_child}return required;", + "const childBefore = require.cache[{path:?}]; globalThis.__perry_cjs_pending_parent = module; let required; try {{ required = __perry_require_path_module({path:?}); }} finally {{ globalThis.__perry_cjs_pending_parent = undefined; }} if (required === undefined && !__perry_has_path_module({path:?})) required = {local}; {warnings}{link_child}const __perry_rec = require.cache[{path:?}]; if (__perry_rec !== undefined && __perry_rec.loaded === true) {local}__rec = __perry_rec; return required;", path = target.to_string_lossy(), + local = local, ) }) } else { @@ -517,15 +541,29 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // `typeof {local} === 'boolean'` sentinel guard does not apply // (builtins are never the pruned-build TRUE sentinel). format!(" if (specifier === '{spec}') {{ {required_value} }}") - } else if require_site_in_try(source, spec) { + } else if require_site_in_try(source, spec) && runtime_require.is_none() { format!( " if (specifier === '{spec}') {{ if (typeof {local} === 'boolean') \ throw __perry_cjs_require_error('error', 'MODULE_NOT_FOUND', \ \"Cannot find module '{spec}'\"); {required_value} }}" ) } else { - if needs_runtime_record { - format!(" if (specifier === '{spec}') {{ {required_value} }}") + if needs_runtime_record && runtime_require.is_some() { + // A repeat require must not re-enter the path registry. + // The registry call exists so a DEFERRED target initializes + // even with no default-export getter, but it is only needed + // until the target is loaded; after that it was costing a + // registry lookup, a `globalThis` write pair and a + // try/finally on EVERY call — 3.4x on a hot require. + // + // The RECORD is cached rather than the exports, and only + // once `loaded === true`, so a module that replaces + // `module.exports` after evaluation still reads through + // (matching Node), and a cyclic target mid-initialisation + // keeps going through the registry until it completes. + format!( + " if (specifier === '{spec}') {{ if ({local}__rec !== undefined) return {local}__rec.exports; {required_value} }}" + ) } else if link_child.is_empty() { format!(" if (specifier === '{spec}') return {local};") } else { @@ -537,6 +575,23 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( }) .collect::>() .join("\n"); + // One memo slot per deferred specifier, declared in the factory so each + // module INSTANCE gets its own (they are per-module state, not global). + // A plain local is deliberate: an object keyed by specifier would put a + // property read on the hot require path, which is what this is removing. + let lazy_cache_decls = require_specs + .iter() + .zip(import_local_names.iter()) + .filter(|(spec, _)| { + // Only the specs that get the runtime-record arm ever assign a + // slot; an unresolvable target keeps the plain binding return and + // would otherwise carry a check nothing can ever satisfy. + lazy_specs.contains(*spec) + && super::super::resolve::resolve_relative_import_path(spec, source_path).is_some() + }) + .map(|(_, local)| format!(" let {local}__rec;")) + .collect::>() + .join("\n"); // Heuristic: is any `require('')` call site lexically inside a // `try { … }` block? Reverse brace-depth scan from the call offset to // the nearest unmatched `{`, checking whether `try` precedes it. @@ -734,7 +789,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( .iter() .filter_map(|(name, spec)| { let n = require_specs.iter().position(|s| s == spec)?; - if builtin_requires.contains(spec) { + if builtin_requires.contains(spec) || lazy_specs.contains(spec) { // #8343 followup: built-in specs no longer hoist a static // `import _req_N` (the codegen doesn't initialize // native-module import bindings in CJS-wrapped modules), @@ -743,7 +798,10 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // `exports.name = require("")` resolves through // the synthetic require's `createRequire` arm and populates // `_cjs.name`, so back the re-export with that — the same - // surface `named_export_decls` uses below. + // surface `named_export_decls` uses below. Conditional + // requires also need the actual CJS property: forwarding + // their import binding would bypass the branch and expose + // a dependency that the module never required. Some(format!("export const {name} = _cjs.{name};")) } else { Some(format!( @@ -840,6 +898,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // from the blanking filter below) and resolves through the synthetic // require's `createRequire` arm at runtime. .filter(|(_, spec, _)| !builtin_requires.contains(spec)) + .filter(|(_, spec, _)| !lazy_specs.contains(spec)) .filter_map(|(alias, spec, _range)| { let idx = require_specs.iter().position(|s| s == spec)?; // When the alias is already the spec's import local name @@ -858,6 +917,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( let ranges = aliases .into_iter() .filter(|(_, spec, _)| require_specs.iter().any(|s| s == spec)) + .filter(|(_, spec, _)| !lazy_specs.contains(spec)) .filter(|(alias, _, _)| !identifier_is_reassigned(source, alias)) // #sdxgen: Don't blank alias declarations for Node.js built-in // modules — let them stay in the IIFE body and resolve through @@ -1100,6 +1160,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // `test/reporters` are builtins only in their `node:` form, and the switch // accepted the bare spelling too. The runtime predicate agrees with Node // 26 on all 58 names in both spellings. +{lazy_cache_decls} function require(specifier) {{ if (typeof specifier !== 'string') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_TYPE', 'The "id" argument must be of type string.'); if (specifier === '') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_VALUE', 'The argument "id" must be a non-empty string.'); diff --git a/crates/perry/tests/conditional_require_init.rs b/crates/perry/tests/conditional_require_init.rs new file mode 100644 index 0000000000..0c3ba8a576 --- /dev/null +++ b/crates/perry/tests/conditional_require_init.rs @@ -0,0 +1,381 @@ +//! Conditional CommonJS dependencies must run at the require call site. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn compile(root: &Path, entry: &str) -> PathBuf { + let compiler = PathBuf::from(env!("CARGO_BIN_EXE_perry")); + let runtime = std::env::var_os("PERRY_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| compiler.parent().unwrap().to_owned()); + let output = root.join("native"); + let result = Command::new(compiler) + .current_dir(root) + .args(["compile", entry, "--no-cache", "--no-auto-optimize"]) + .arg("-o") + .arg(&output) + .env("PERRY_RUNTIME_DIR", runtime) + .output() + .expect("compile fixture"); + assert!( + result.status.success(), + "{}\n{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + output +} + +fn run(binary: &Path, args: &[&str]) -> String { + let result = Command::new(binary) + .args(args) + .output() + .expect("run fixture"); + assert!( + result.status.success(), + "{}\n{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + String::from_utf8(result.stdout).unwrap() +} + +#[test] +fn conditional_require_defers_the_transitive_graph_and_initializes_once() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("leaf.cjs"), + "console.log('leaf'); module.exports = 42;", + ) + .unwrap(); + std::fs::write(root.join("dep.cjs"), + "const leaf = require('./leaf.cjs');\nconsole.log('dependency');\nmodule.exports = {value: leaf};").unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +console.log('entry'); +if (process.argv.includes('--load')) { + const first = require('./dep.cjs'); + const second = require('./dep.cjs'); + console.log(first.value, first === second); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!( + run(&binary, &["--load"]), + "entry\nleaf\ndependency\n42 true\ndone\n" + ); +} + +#[test] +fn concise_arrow_and_short_circuit_require_stay_lazy() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency'); module.exports = {value: 42};", + ) + .unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +const load = () => require('./dep.cjs'); +console.log('entry'); +process.argv.includes('--load') && console.log(load().value); +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!(run(&binary, &["--load"]), "entry\ndependency\n42\ndone\n"); +} + +#[test] +fn require_exception_is_caught_at_its_original_try_boundary() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency'); throw new Error('fixture');", + ) + .unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +console.log('entry'); +try { require('./dep.cjs'); } +catch (error) { console.log('caught', error.message); } +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!( + run(&binary, &[]), + "entry\ndependency\ncaught fixture\ndone\n" + ); +} + +#[test] +fn static_import_still_evaluates_before_the_entry_body() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.mjs"), + "console.log('static'); export const value = 42;", + ) + .unwrap(); + std::fs::write( + root.join("lazy.mjs"), + "console.log('unexpected dynamic init'); export const value = 0;", + ) + .unwrap(); + std::fs::write( + root.join("entry.mjs"), + r#" +import { value } from './dep.mjs'; +export function unused() { return import('./lazy.mjs'); } +console.log('entry', value); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.mjs"); + assert_eq!(run(&binary, &[]), "static\nentry 42\n"); +} + +#[test] +fn conditional_class_require_initializes_static_state() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency');\nclass Value { static answer = 42; }\nmodule.exports = Value;", + ) + .unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +class Unrelated {} +console.log('entry'); +if (process.argv.includes('--load')) { + const Value = require('./dep.cjs'); + console.log(Value.answer); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!(run(&binary, &["--load"]), "entry\ndependency\n42\ndone\n"); +} + +#[test] +fn conditional_named_export_does_not_forward_an_unloaded_dependency() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency'); module.exports = {value: 42};", + ) + .unwrap(); + std::fs::write( + root.join("wrapper.cjs"), + r#" +console.log('wrapper'); +if (process.argv.includes('--load')) exports.optional = require('./dep.cjs'); +"#, + ) + .unwrap(); + std::fs::write( + root.join("entry.mjs"), + r#" +import { optional } from './wrapper.cjs'; +console.log('entry', optional === undefined ? 'absent' : optional.value); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.mjs"); + assert_eq!(run(&binary, &[]), "wrapper\nentry absent\n"); + assert_eq!(run(&binary, &["--load"]), "wrapper\ndependency\nentry 42\n"); +} + +#[test] +fn conditional_side_effect_only_require_runs_once_when_reached() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write(root.join("dep.cjs"), "console.log('dependency');").unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +console.log('entry'); +if (process.argv.includes('--load')) { + require('./dep.cjs'); + require('./dep.cjs'); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!(run(&binary, &["--load"]), "entry\ndependency\ndone\n"); +} + +#[test] +fn esm_function_local_class_require_initializes_static_state() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency');\nclass Value { static answer = 42; }\nmodule.exports = Value;", + ) + .unwrap(); + std::fs::write( + root.join("entry.mjs"), + r#" +import { createRequire } from 'node:module'; +const require = createRequire(import.meta.url); +const load = () => require('./dep.cjs'); +console.log('entry'); +if (process.argv.includes('--load')) console.log(load().answer); +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.mjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!(run(&binary, &["--load"]), "entry\ndependency\n42\ndone\n"); +} + +/// A deferred target in a require cycle must still see its partner's exports +/// assigned at run time: the partner reads the target's partial exports object +/// during the cycle and the value only exists after the target's body ends. +#[test] +fn conditional_require_cycle_partner_sees_runtime_assigned_exports() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("a.cjs"), + r#" +console.log('a start'); +exports.value = undefined; +const b = require('./b.cjs'); +exports.value = () => 'a-runtime'; +exports.read = () => b.callA(); +console.log('a end'); +"#, + ) + .unwrap(); + std::fs::write( + root.join("b.cjs"), + r#" +console.log('b start', typeof require('./a.cjs').value); +const a = require('./a.cjs'); +exports.callA = () => a.value(); +"#, + ) + .unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +console.log('entry'); +if (process.argv.includes('--load')) { + const a = require('./a.cjs'); + console.log(a.read(), typeof a.value); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!( + run(&binary, &["--load"]), + "entry\na start\nb start undefined\na end\na-runtime function\ndone\n" + ); +} + +/// The same cycle shape reached from ESM through `createRequire`, next to an +/// ESM partner whose export is assigned (not declared) at module run time. +#[test] +fn esm_conditional_require_cycle_keeps_runtime_assigned_bindings() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("partner.mjs"), + "export let handler;\nhandler = () => 'partner-runtime';\nexport function readLater() { return typeof handler; }\n", + ) + .unwrap(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dep');\nconst cyc = require('./cycle.cjs');\nmodule.exports = { run: () => cyc.call() };\n", + ) + .unwrap(); + std::fs::write( + root.join("cycle.cjs"), + "const dep = require('./dep.cjs');\nlet fn;\nfn = () => 'cycle-runtime:' + typeof dep;\nexports.call = () => fn();\n", + ) + .unwrap(); + std::fs::write( + root.join("entry.mjs"), + r#" +import { createRequire } from 'node:module'; +import { readLater } from './partner.mjs'; +const require = createRequire(import.meta.url); +console.log('entry'); +if (process.argv.includes('--load')) { + const dep = require('./dep.cjs'); + console.log(dep.run(), readLater()); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.mjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!( + run(&binary, &["--load"]), + "entry\ndep\ncycle-runtime:object function\ndone\n" + ); +} + +/// A `do…while` test runs only if the body falls through, so a `require` in +/// either half is conditional. Node never evaluates the dependency below. +#[test] +fn do_while_require_stays_at_its_call_site() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write( + root.join("dep.cjs"), + "console.log('dependency');\nmodule.exports = 7;", + ) + .unwrap(); + std::fs::write( + root.join("entry.cjs"), + r#" +console.log('entry'); +if (!process.argv.includes('--load')) { + do { break; } while (require('./dep.cjs')); +} else { + let seen = 0; + do { seen += require('./dep.cjs'); } while (false); + console.log('sum', seen); +} +console.log('done'); +"#, + ) + .unwrap(); + let binary = compile(root, "entry.cjs"); + assert_eq!(run(&binary, &[]), "entry\ndone\n"); + assert_eq!( + run(&binary, &["--load"]), + "entry\ndependency\nsum 7\ndone\n" + ); +} diff --git a/test-files/_helpers/gap10754_off_and.cjs b/test-files/_helpers/gap10754_off_and.cjs new file mode 100644 index 0000000000..e8b45c6255 --- /dev/null +++ b/test-files/_helpers/gap10754_off_and.cjs @@ -0,0 +1 @@ +console.log('load off_and'); diff --git a/test-files/_helpers/gap10754_off_for.cjs b/test-files/_helpers/gap10754_off_for.cjs new file mode 100644 index 0000000000..4100799c60 --- /dev/null +++ b/test-files/_helpers/gap10754_off_for.cjs @@ -0,0 +1 @@ +console.log('load off_for'); diff --git a/test-files/_helpers/gap10754_off_if.cjs b/test-files/_helpers/gap10754_off_if.cjs new file mode 100644 index 0000000000..f89b2896b6 --- /dev/null +++ b/test-files/_helpers/gap10754_off_if.cjs @@ -0,0 +1 @@ +console.log('load off_if'); diff --git a/test-files/_helpers/gap10754_off_switch.cjs b/test-files/_helpers/gap10754_off_switch.cjs new file mode 100644 index 0000000000..39dc47e848 --- /dev/null +++ b/test-files/_helpers/gap10754_off_switch.cjs @@ -0,0 +1 @@ +console.log('load off_switch'); diff --git a/test-files/_helpers/gap10754_off_tern.cjs b/test-files/_helpers/gap10754_off_tern.cjs new file mode 100644 index 0000000000..92f91ea7ca --- /dev/null +++ b/test-files/_helpers/gap10754_off_tern.cjs @@ -0,0 +1 @@ +console.log('load off_tern'); diff --git a/test-files/_helpers/gap10754_off_try.cjs b/test-files/_helpers/gap10754_off_try.cjs new file mode 100644 index 0000000000..15ba939868 --- /dev/null +++ b/test-files/_helpers/gap10754_off_try.cjs @@ -0,0 +1 @@ +console.log('load off_try'); diff --git a/test-files/_helpers/gap10754_on_exp_and.cjs b/test-files/_helpers/gap10754_on_exp_and.cjs new file mode 100644 index 0000000000..c5a8dd2f72 --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_and.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_and'); +module.exports = 'v_and'; diff --git a/test-files/_helpers/gap10754_on_exp_for.cjs b/test-files/_helpers/gap10754_on_exp_for.cjs new file mode 100644 index 0000000000..616590103a --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_for.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_for'); +module.exports = 'v_for'; diff --git a/test-files/_helpers/gap10754_on_exp_if.cjs b/test-files/_helpers/gap10754_on_exp_if.cjs new file mode 100644 index 0000000000..dcec378d29 --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_if.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_if'); +module.exports = 'v_if'; diff --git a/test-files/_helpers/gap10754_on_exp_switch.cjs b/test-files/_helpers/gap10754_on_exp_switch.cjs new file mode 100644 index 0000000000..ed82792aaa --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_switch.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_switch'); +module.exports = 'v_switch'; diff --git a/test-files/_helpers/gap10754_on_exp_tern.cjs b/test-files/_helpers/gap10754_on_exp_tern.cjs new file mode 100644 index 0000000000..730cbf2cbb --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_tern.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_tern'); +module.exports = 'v_tern'; diff --git a/test-files/_helpers/gap10754_on_exp_try.cjs b/test-files/_helpers/gap10754_on_exp_try.cjs new file mode 100644 index 0000000000..7d474d22ef --- /dev/null +++ b/test-files/_helpers/gap10754_on_exp_try.cjs @@ -0,0 +1,2 @@ +console.log('load on_exp_try'); +module.exports = 'v_try'; diff --git a/test-files/_helpers/gap10754_on_sfx_and.cjs b/test-files/_helpers/gap10754_on_sfx_and.cjs new file mode 100644 index 0000000000..3620d3acb1 --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_and.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_and'); diff --git a/test-files/_helpers/gap10754_on_sfx_for.cjs b/test-files/_helpers/gap10754_on_sfx_for.cjs new file mode 100644 index 0000000000..5c99758712 --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_for.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_for'); diff --git a/test-files/_helpers/gap10754_on_sfx_if.cjs b/test-files/_helpers/gap10754_on_sfx_if.cjs new file mode 100644 index 0000000000..94566d43d8 --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_if.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_if'); diff --git a/test-files/_helpers/gap10754_on_sfx_switch.cjs b/test-files/_helpers/gap10754_on_sfx_switch.cjs new file mode 100644 index 0000000000..1bc263969a --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_switch.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_switch'); diff --git a/test-files/_helpers/gap10754_on_sfx_tern.cjs b/test-files/_helpers/gap10754_on_sfx_tern.cjs new file mode 100644 index 0000000000..99700bb86f --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_tern.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_tern'); diff --git a/test-files/_helpers/gap10754_on_sfx_try.cjs b/test-files/_helpers/gap10754_on_sfx_try.cjs new file mode 100644 index 0000000000..205828bc7d --- /dev/null +++ b/test-files/_helpers/gap10754_on_sfx_try.cjs @@ -0,0 +1 @@ +console.log('load on_sfx_try'); diff --git a/test-files/test_gap_10754_cjs_conditional_require_shapes.cts b/test-files/test_gap_10754_cjs_conditional_require_shapes.cts new file mode 100644 index 0000000000..7aa6bc5802 --- /dev/null +++ b/test-files/test_gap_10754_cjs_conditional_require_shapes.cts @@ -0,0 +1,135 @@ +// #10754: a top-level CommonJS `require()` whose call site sits in a +// short-circuit (`cond && require(...)`), a ternary consequent +// (`cond ? require(...) : x`), a `try` block, a `switch` case or a loop body +// was SILENTLY NEVER EVALUATED — the module did not load even when the +// branch was taken. Node loads it. No crash, no diagnostic: the dependency's +// side effects simply never happened. +// +// History. #10437 was the opposite bug: every such `require()` was hoisted +// into an eager synthetic import and ran even when its branch did not. +// #10674 fixed that by DEFERRING the target instead of hoisting it, and got +// the `if (cond) require(...)` shape right. In the five shapes above the +// deferral had no evaluation site left: the require shim returned the +// deferred import binding, and firing the target's `__init()` was gated on +// that binding being a known imported FUNCTION. A dependency with no +// default export — a side-effect-only module, the commonest shape for a +// polyfill or a registration hook — has no such binding to read, so nothing +// ever ran it. +// +// The fixture therefore asserts BOTH directions for every shape, because a +// fix that makes the module load unconditionally is #10437 again, not a fix: +// +// on_sfx_* taken branch, SIDE-EFFECT-ONLY target -> must load (the bug) +// on_exp_* taken branch, value-returning target -> must load AND the +// value must be the +// target's exports +// off_* branch not taken -> must NOT load +// +// The not-taken direction with VALUE-RETURNING targets is #10437's own +// fixture (test_gap_cjs_conditional_require_deferred.ts); the side-effect-only +// targets here are the half it does not reach. +// +// `.cts`, deliberately: this repo's package is `"type": "module"`, so a plain +// `.ts` runs as an ES module under Node and a bare `require` dies with +// `require is not defined`. The extension makes both engines agree on the +// CommonJS goal — the goal the issue is about, since the reproducer is a +// `.cjs` program entry. See test_gap_9412's header for the full rationale. +// Keep this file free of top-level `import`/`export`. + +// Runtime-unknown guards: neither engine can fold these at compile time, so +// the compiler cannot decide the branch statically and must emit whatever it +// would emit for a genuine runtime condition (`pg`'s `if (forceNative)`). +const off = Boolean(process.env.PERRY_GAP10754_UNSET); +const on = !off; + +console.log('start'); + +// ---------------------------------------------------------------- not taken +// Each of these must print NOTHING. A load line here is #10437 again. +if (off) require('./_helpers/gap10754_off_if.cjs'); +off && require('./_helpers/gap10754_off_and.cjs'); +const offTern = off ? require('./_helpers/gap10754_off_tern.cjs') : 'skipped'; +try { + if (off) require('./_helpers/gap10754_off_try.cjs'); +} catch (e) { + console.log('unexpected off_try throw'); +} +switch (off) { + case true: + require('./_helpers/gap10754_off_switch.cjs'); + break; + default: +} +for (let i = 0; off && i < 1; i++) require('./_helpers/gap10754_off_for.cjs'); +console.log('not-taken done, offTern=' + offTern); + +// ------------------------------------------- taken, side-effect-only target +// Each load line must appear between its own two markers: the module runs at +// the moment control flow reaches the call, not before the file's first +// statement and not after the branch. +console.log('sfx if:'); +if (on) require('./_helpers/gap10754_on_sfx_if.cjs'); +console.log('sfx and:'); +on && require('./_helpers/gap10754_on_sfx_and.cjs'); +console.log('sfx tern:'); +const sfxTern = on ? require('./_helpers/gap10754_on_sfx_tern.cjs') : 'skipped'; +console.log('sfx try:'); +try { + if (on) require('./_helpers/gap10754_on_sfx_try.cjs'); +} catch (e) { + console.log('unexpected on_sfx_try throw'); +} +console.log('sfx switch:'); +switch (on) { + case true: + require('./_helpers/gap10754_on_sfx_switch.cjs'); + break; + default: +} +console.log('sfx for:'); +for (let i = 0; on && i < 1; i++) require('./_helpers/gap10754_on_sfx_for.cjs'); +console.log('sfx done, typeof sfxTern=' + typeof sfxTern); + +// ---------------------------------------------- taken, value-returning target +// Same six shapes, but the target replaces `module.exports`. Pins that the +// shim hands back the TARGET's exports, not the synthetic import binding or +// an empty object. +console.log('exp if:'); +let expIf: unknown = 'unset'; +if (on) expIf = require('./_helpers/gap10754_on_exp_if.cjs'); +console.log('exp and:'); +let expAnd: unknown = 'unset'; +on && (expAnd = require('./_helpers/gap10754_on_exp_and.cjs')); +console.log('exp tern:'); +const expTern = on ? require('./_helpers/gap10754_on_exp_tern.cjs') : 'unset'; +console.log('exp try:'); +let expTry: unknown = 'unset'; +try { + if (on) expTry = require('./_helpers/gap10754_on_exp_try.cjs'); +} catch (e) { + console.log('unexpected on_exp_try throw'); +} +console.log('exp switch:'); +let expSwitch: unknown = 'unset'; +switch (on) { + case true: + expSwitch = require('./_helpers/gap10754_on_exp_switch.cjs'); + break; + default: +} +console.log('exp for:'); +let expFor: unknown = 'unset'; +for (let i = 0; on && i < 1; i++) expFor = require('./_helpers/gap10754_on_exp_for.cjs'); +console.log( + 'exp values: ' + + [expIf, expAnd, expTern, expTry, expSwitch, expFor].join(','), +); + +// Require caching: a second conditional require of an ALREADY-loaded target +// must not re-run its body and must hand back the same exports. +console.log('cache:'); +let again: unknown = 'unset'; +on && (again = require('./_helpers/gap10754_on_exp_and.cjs')); +console.log('cache same=' + (again === expAnd)); + +console.log('end'); From 49849d9fb3dbff56f37551027abf718f51f33754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 19 Sep 2026 22:55:15 +0200 Subject: [PATCH 2/5] changelog: #10756 --- ...conditional-require-target-export-shape.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 changelog.d/10756-conditional-require-target-export-shape.md diff --git a/changelog.d/10756-conditional-require-target-export-shape.md b/changelog.d/10756-conditional-require-target-export-shape.md new file mode 100644 index 0000000000..cc9111f9b8 --- /dev/null +++ b/changelog.d/10756-conditional-require-target-export-shape.md @@ -0,0 +1,62 @@ +### Fixed + +**A conditional CommonJS `require()` of a side-effect-only module never ran it (#10754).** + +A top-level `require()` in a conditional position loaded nothing when its target +carried no CommonJS export marker — the branch was taken, the shim was reached, +and the module body never ran. Silent: no crash, no diagnostic, just a +dependency whose side effects never happened. + +The issue reports the failure as syntactic (`if` works, `&&` / `?:` / `try` / +`switch` / loop-body do not). That is a confound in the reproducer: its `if` +case required a module with `module.exports = 1` while the other five required +side-effect-only modules. Crossing the two axes against Node 26.5.1 on a +release build of `main` @ 91a566c8af shows the discriminator is the TARGET's +export shape, not the call-site shape — all six shapes fail with a +side-effect-only target and all six pass with a value-returning one. + +Root cause. #10674 defers a conditional require correctly: the target stays +`ModuleInitKind::Deferred` and the CJS shim returns the `_lazyreq_N` import +binding, with codegen firing `__init()` at the binding read +(`perry-codegen/src/expr/dyn_extern_i18n.rs`). That init call is gated on the +binding being a known imported FUNCTION (`ctx.import_function_prefixes`). A +target with no default export has no such entry, so nothing fires. The `if` +cases that "worked" worked because their targets had a default export to read. + +The fix (PR #10285's implementation, rebased, plus a registry-miss fallback): + +- `cjs_wrap/deferred_requires.rs` — an AST visitor replaces the text-scanning + deferral classifier. The scanner missed `if (cond) x = require('S')` (still + eagerly hoisted on `main`, a residual #10437 shape), concise arrows, the + ternary ALTERNATE arm, both halves of a `do`/`while`, and `&&=`/`||=`/`??=`. + `extract_requires::function_local_specs` remains the parse-failure fallback. +- `cjs_wrap/wrap.rs` — a deferred specifier resolves through the path registry + (`__perry_require_path_module`) rather than its import binding, so + initialization no longer depends on the target's export shape. The registry + record is memoized per call site once `loaded === true`: re-entering the + registry on every call measured 3.4x on a hot require. +- `cjs_wrap/wrap.rs`, new on top of #10285 — the registry only holds EXPORTS for + a target that publishes them, which is every CJS-wrapped module and no other. + A file with no CommonJS marker at all is not CJS-wrapped, so it registers an + initializer and never any exports: the registry ran its body and returned + `undefined` where Node returns `{}`, so + `const v = cond ? require('./side-effect-only.cjs') : 0` came back + `undefined`. The arm now falls back to the import binding on a genuine + registry miss, discriminated by `__perry_has_path_module` (a real module may + export `undefined`) — the same guard the generic runtime-`require(path)` arm + in the same wrapper already uses. +- `perry-codegen/src/expr/dyn_extern_i18n.rs` — fire the deferred `__init()` + before the imported-class and namespace fast paths, which can themselves + depend on module initialization. + +Validation. `test-files/test_gap_10754_cjs_conditional_require_shapes.cts` +crosses all six shapes with three cells each — taken/side-effect-only, +taken/value-returning, and not-taken — because a fix that loads the module +unconditionally is #10437 again, not a fix. It fails on unfixed `main` — +12 differing lines against the Node oracle: six side-effect-only targets never +load at all, and three value-returning targets load before the program's first +statement instead of at their call site (`if (cond) x = require('S')`, +`cond && (x = require('S'))` and the same in a `for` body, the shapes the text +scanner could not see) — and matches Node byte-for-byte with the fix. The +harness agrees: `parity_fail` on unfixed `main`, `PASS` with the change. The existing #10437 +fixture covers the not-taken direction with value-returning targets only. From 954c5dba0d95712e85f85be4fdeca91e45877086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 20 Sep 2026 00:18:36 +0200 Subject: [PATCH 3/5] refactor(stdlib): remove nanoid native binding customAlphabet(alphabet, size) is documented to return a generator function; native customAlphabet instead returns the generated id string directly (js_nanoid_custom's own doc comment: "For simplicity, we combine this into one call"), so the only documented usage -- const gen = customAlphabet(...); gen(); -- crashes with TypeError: value is not a function. Per #10678 (duplicate extern "C" exports across perry-ext-*/perry-stdlib pairs), this binding existed twice: crates/perry-ext-nanoid/ (governance- tracked) and crates/perry-stdlib/src/nanoid.rs (a second, independent implementation behind the default-on bundled-nanoid feature, exporting the same js_nanoid/js_nanoid_sized/js_nanoid_custom symbols). customAlphabet is declared to codegen (data_stores.rs's js_nanoid_custom) but has no call-site wiring anywhere in perry-codegen -- no NativeModSig row, no lower_call special case. Only plain nanoid(size) had a dispatch row (native_table/utils_crypto.rs, routing to js_nanoid_sized), consistent with customAlphabet not being a first-class compiled call at all. Removed both crates, the 1-entry NativeModSig dispatch row, the 2 js_nanoid* FFI declarations, the well_known_bindings.toml entry, the "nanoid" NATIVE_MODULES entry + manifest row, the bundled-nanoid stdlib feature, and 2 Android stub exports. DELETED the shipped_unproven_bindings_are_partial test rather than emptying it. Its subject population was exactly the hand-written wrappers shipped without proven upstream parity: uuid (#10701), dotenv (#10691, landed in train 227) and nanoid (here). With the last one gone the array would read `for name in []` -- a test that compiles, runs, asserts nothing and reports green forever, which is failure mode #4 in CLAUDE.md's "four ways a gate can be unable to fail". Coverage is not lost: shipped_subset_bindings_are_partial is a separate test and still asserts the same property for undici, node-forge, lru-cache and qs. Verified the deletion orphans nothing -- the test module is `use super::*`, lookup_well_known has 9 other callers, BindingCompat::Partial has 5 other uses, and nothing in the tree keys on the test's name. The "ids" umbrella is now EMPTY. It was retargeted to ["bundled-nanoid"] when #10701 removed uuid (train 225); removing bundled-nanoid leaves it with no members, so it is kept as `ids = []` -- an intentionally harmless no-op that preserves `--features ids` for existing callers rather than breaking them. The stale comments that described the two-member split (perry-stdlib/Cargo.toml, perry-stdlib/src/lib.rs, stdlib_features.rs) are rewritten to say so. perry-stdlib's `uuid` crate dependency is untouched: #10701 already made it non-optional because crypto/random.rs calls it unconditionally. test-files/test_parity_nanoid.ts (the exact customAlphabet(...)(); reproduction) is already excluded from the parity gate -- known_failures.json classifies it "ci-env": Node's own oracle fails with ERR_MODULE_NOT_FOUND in CI because nanoid was never added to the repo's root package.json, so npm ci never installs it. Not touched -- provisioning a real npm dependency in the root package.json is out of scope for a binding-removal PR, and the test remains excluded before and after this change for the same underlying reason. Every absolute count re-derived from its own script against the resolved tree, never carried across the rebase and never hand-merged: workspace-architecture.json 77 -> 76 members, externalize 28 -> 27, keep 44 (scripts/workspace_architecture.py; git auto-merged this file with NO conflict and left the stale 77/28, which the script caught); docs/api/perry.d.ts 2065 entries/131 modules -> 2064/130 and docs/src/api/reference.md 3007/133 -> 3006/132, both regenerated by running the built binary's --print-api-manifest rather than editing the headers; Cargo.lock regenerated with `cargo metadata`. native_result_ledger (376 rows/326 providers), unrooted_local_shape (578) and string_payload_access (perry-stdlib inline-offset 37) confirmed unchanged by running them, not by assuming. Rebased onto v0.5.1606 (train 227). Conflicts were all of the form "main deleted dotenv, this branch deleted nanoid, in one hunk"; every one was resolved to main's current content minus nanoid's own entries, which is neither side. --- Cargo.lock | 19 ---- Cargo.toml | 2 - crates/perry-api-manifest/src/entries.rs | 1 - .../perry-api-manifest/src/entries/part_1.rs | 12 -- .../lower_call/native_table/utils_crypto.rs | 12 -- .../runtime_decls/stdlib_ffi/data_stores.rs | 4 - crates/perry-ext-nanoid/Cargo.toml | 20 ---- crates/perry-ext-nanoid/src/lib.rs | 106 ------------------ crates/perry-stdlib/Cargo.toml | 15 +-- crates/perry-stdlib/src/lib.rs | 15 +-- crates/perry-stdlib/src/nanoid.rs | 62 ---------- crates/perry-ui-android/src/stdlib_stubs.rs | 8 -- .../perry/src/commands/compile/well_known.rs | 12 -- crates/perry/src/commands/stdlib_features.rs | 13 +-- crates/perry/well_known_bindings.toml | 15 --- docs/api/perry.d.ts | 7 +- docs/src/api/reference.md | 9 +- docs/src/native-libraries/governance.md | 1 - workspace-architecture.json | 9 +- 19 files changed, 21 insertions(+), 321 deletions(-) delete mode 100644 crates/perry-ext-nanoid/Cargo.toml delete mode 100644 crates/perry-ext-nanoid/src/lib.rs delete mode 100644 crates/perry-stdlib/src/nanoid.rs diff --git a/Cargo.lock b/Cargo.lock index 37062ae16c..b6b0cfce43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4875,15 +4875,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" -[[package]] -name = "nanoid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873" -dependencies = [ - "rand 0.9.4", -] - [[package]] name = "ndk-context" version = "0.1.1" @@ -6050,15 +6041,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "perry-ext-nanoid" -version = "0.5.1606" -dependencies = [ - "nanoid", - "perry-ffi", - "rand 0.10.2", -] - [[package]] name = "perry-ext-net" version = "0.5.1606" @@ -6382,7 +6364,6 @@ dependencies = [ "md-5 0.11.0", "ml-kem", "mongodb", - "nanoid", "once_cell", "p256", "p384", diff --git a/Cargo.toml b/Cargo.toml index 766712ef22..336f2b35af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ members = [ "crates/perry-runtime", "crates/perry-ffi", "crates/perry-native-registration", - "crates/perry-ext-nanoid", "crates/perry-ext-bcrypt", "crates/perry-ext-argon2", "crates/perry-perex", @@ -471,7 +470,6 @@ perry-dispatch = { path = "crates/perry-dispatch" } perry-runtime = { path = "crates/perry-runtime", version = "0.5.1011", default-features = false } perry-ffi = { path = "crates/perry-ffi", version = "0.5.1011" } perry-native-registration = { path = "crates/perry-native-registration", version = "0.5.1534" } -perry-ext-nanoid = { path = "crates/perry-ext-nanoid" } perry-ext-bcrypt = { path = "crates/perry-ext-bcrypt" } perry-ext-argon2 = { path = "crates/perry-ext-argon2" } perry-perex = { path = "crates/perry-perex" } diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 53e9dbe339..1ac5de2cb5 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -43,7 +43,6 @@ pub const NATIVE_MODULES: &[&str] = &[ "ws", // WebSocket client/server "zlib", // (Node builtin) gzip/deflate/brotli/zstd compression "crypto", // (Node builtin) hashing, HMAC, cipher, sign/verify, WebCrypto - "nanoid", // compact URL-safe ID generation "ethers", // Ethereum library (utils/wallet/ABI) "mongodb", // MongoDB driver "better-sqlite3", // synchronous SQLite (replaces the N-API addon) diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs index 9dc26c00a7..05bc326238 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -1111,18 +1111,6 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ ), method("nodemailer", "sendMail", true, None), method("nodemailer", "verify", true, None), - method_sig( - "nanoid", - "nanoid", - false, - None, - &[ParamSpec::Named { - name: "size", - ty: TypeSpec::Number, - optional: false, - }], - TypeSpec::String, - ), // #4917 — real retry semantics: options (numOfAttempts/startingDelay/ // timeMultiple/maxDelay/delayFirstAttempt/jitter/retry) honored; // Promise-returning tasks retry on rejection via promise reactions. diff --git a/crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs b/crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs index 4b41aa587c..83189264ff 100644 --- a/crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs +++ b/crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs @@ -29,18 +29,6 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_GCPTR, }, - // ========== nanoid ========== - // js_nanoid_sized(NaN) → size=0 → falls back to js_nanoid() (21-char default), - // so nanoid() and nanoid(N) both route through the same entry safely. - NativeModSig { - module: "nanoid", - has_receiver: false, - method: "nanoid", - class_filter: None, - runtime: "js_nanoid_sized", - args: &[NA_F64], - ret: NR_STR, - }, // ========== exponential-backoff ========== NativeModSig { module: "exponential-backoff", diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs index d9da94b533..13a2d481e2 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs @@ -288,8 +288,4 @@ pub(crate) fn declare_data_stores(module: &mut LlModule) { module.declare_function("js_crypto_x25519_shared_secret", I64, &[I64, I64]); module.declare_function("js_keccak256_native", I64, &[I64]); module.declare_function("js_keccak256_native_bytes", I64, &[I64]); - - // ========== Nanoid ========== - module.declare_function("js_nanoid", I64, &[DOUBLE]); - module.declare_function("js_nanoid_custom", I64, &[I64, DOUBLE]); } diff --git a/crates/perry-ext-nanoid/Cargo.toml b/crates/perry-ext-nanoid/Cargo.toml deleted file mode 100644 index e264ea3e8b..0000000000 --- a/crates/perry-ext-nanoid/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "perry-ext-nanoid" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for the npm `nanoid` package — uses only `perry-ffi`. Second port under #466 Phase 5 (after dotenv)." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -nanoid = "0.5" -rand = "0.10" - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-nanoid/src/lib.rs b/crates/perry-ext-nanoid/src/lib.rs deleted file mode 100644 index 8b58aecc9a..0000000000 --- a/crates/perry-ext-nanoid/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Native bindings for the npm `nanoid` package. -//! -//! Functionally identical to `crates/perry-stdlib/src/nanoid.rs`. The -//! point of this crate is that it depends only on [`perry_ffi`], not -//! on `perry-runtime` internals — proving the perry-ffi v0.5 surface -//! still suffices for the second wrapper port (#466 Phase 5 step 2). - -use nanoid::nanoid; -use perry_ffi::{alloc_string, read_string, JsString, StringHeader}; - -/// `nanoid()` — 21-char URL-safe id with the default alphabet. -#[no_mangle] -pub extern "C" fn js_nanoid() -> *mut StringHeader { - let id = nanoid!(); - alloc_string(&id).as_raw() -} - -/// `nanoid(size)` — id with a custom length. -#[no_mangle] -pub extern "C" fn js_nanoid_sized(size: f64) -> *mut StringHeader { - let size = size as usize; - if size == 0 { - return js_nanoid(); - } - let id = nanoid!(size); - alloc_string(&id).as_raw() -} - -/// `customAlphabet(alphabet, size)()` — id with a user-supplied -/// alphabet. Perry collapses this into a single call rather than the -/// curried form Node uses, so the FFI surface stays flat. -/// -/// # Safety -/// -/// `alphabet_ptr` must be null or a pointer to a Perry-runtime -/// `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_nanoid_custom( - alphabet_ptr: *const StringHeader, - size: f64, -) -> *mut StringHeader { - let handle = JsString::from_raw(alphabet_ptr as *mut StringHeader); - let alphabet = match read_string(handle) { - Some(a) => a, - None => return js_nanoid(), - }; - - let size = if size <= 0.0 { 21 } else { size as usize }; - let alphabet_chars: Vec = alphabet.chars().collect(); - - if alphabet_chars.is_empty() { - return js_nanoid(); - } - - use rand::RngExt; - let mut rng = rand::rng(); - let id: String = (0..size) - .map(|_| { - let idx = rng.random_range(0..alphabet_chars.len()); - alphabet_chars[idx] - }) - .collect(); - - alloc_string(&id).as_raw() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_id_is_21_chars() { - let handle = unsafe { JsString::from_raw(js_nanoid()) }; - let s = read_string(handle).expect("non-null"); - assert_eq!(s.chars().count(), 21); - } - - #[test] - fn sized_id_honors_length() { - for n in [1, 5, 16, 100] { - let handle = unsafe { JsString::from_raw(js_nanoid_sized(n as f64)) }; - let s = read_string(handle).expect("non-null"); - assert_eq!(s.chars().count(), n, "size={}", n); - } - } - - #[test] - fn sized_zero_falls_back_to_default() { - let handle = unsafe { JsString::from_raw(js_nanoid_sized(0.0)) }; - let s = read_string(handle).expect("non-null"); - assert_eq!(s.chars().count(), 21); - } - - #[test] - fn custom_alphabet_round_trips_through_perry_ffi() { - // Allocate the alphabet through perry-ffi so the FFI is - // exercised end-to-end. - let alphabet = alloc_string("abc"); - let handle = unsafe { js_nanoid_custom(alphabet.as_raw() as *const _, 8.0) }; - let s = read_string(unsafe { JsString::from_raw(handle) }).expect("non-null"); - assert_eq!(s.chars().count(), 8); - for c in s.chars() { - assert!("abc".contains(c), "char `{}` not in alphabet", c); - } - } -} diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 0357575ca8..bbf85a49a5 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -282,14 +282,12 @@ rate-limit = ["bundled-ratelimit"] bundled-ratelimit = ["dep:governor", "async-runtime"] -# UUID/nanoid — `ids` stays as the umbrella for backwards compat; -# from v0.5.534 onwards the per-binding split (`bundled-uuid` / -# `bundled-nanoid`) is what the well-known bindings flip (#466 -# Phase 4 step 2) toggles. Each sub-feature pulls in its own -# optional dep + gates its own module so a wrapper port can strip -# exactly one binding from perry-stdlib without affecting the other. -ids = ["bundled-nanoid"] -bundled-nanoid = ["dep:nanoid"] +# `ids` is now an empty umbrella kept only for backwards compat: +# both members are gone — `bundled-uuid` with the uuid binding +# (#10701) and `bundled-nanoid` with the nanoid binding (#10693). +# Real `uuid` / `nanoid` now compile from npm source, so nothing +# needs to be toggled here; enabling `ids` is a harmless no-op. +ids = [] # Async runtime (tokio) - internal feature async-runtime = ["dep:tokio"] @@ -435,7 +433,6 @@ governor = { version = "0.10", optional = true } # never optional, regardless of the bundled-uuid npm-binding feature # (removed; see #10678/#466). uuid = { version = "1.23", features = ["v4", "v1", "v3", "v5", "v7"] } -nanoid = { version = "0.5", optional = true } # LRU Cache — optional from v0.5.539 so the well-known flip can # strip the perry-stdlib copy when `import 'lru-cache'` resolves diff --git a/crates/perry-stdlib/src/lib.rs b/crates/perry-stdlib/src/lib.rs index 7c4e9ed331..2c64ee8296 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -398,16 +398,11 @@ pub mod ratelimit; pub use ratelimit::*; // === IDs === -// `bundled-uuid` / `bundled-nanoid` (v0.5.534) replace the old -// `ids` umbrella so the well-known flip (#466 Phase 4) can toggle -// each binding independently. The umbrella stays as -// `ids = ["bundled-uuid", "bundled-nanoid"]` so existing -// `--features ids` callers keep working byte-identically. - -#[cfg(feature = "bundled-nanoid")] -pub mod nanoid; -#[cfg(feature = "bundled-nanoid")] -pub use nanoid::*; +// Nothing left to gate: `bundled-uuid` went with the uuid binding +// (#10701) and `bundled-nanoid` with the nanoid binding (#10693); +// real `uuid` / `nanoid` now compile from npm source. The `ids` +// umbrella stays (empty) in Cargo.toml so existing +// `--features ids` callers keep working. // === Container Module === #[cfg(feature = "container")] diff --git a/crates/perry-stdlib/src/nanoid.rs b/crates/perry-stdlib/src/nanoid.rs deleted file mode 100644 index b39c64441a..0000000000 --- a/crates/perry-stdlib/src/nanoid.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! NanoID module (nanoid compatible) -//! -//! Native implementation of the 'nanoid' npm package. -//! Generates short, URL-friendly unique IDs. - -use nanoid::nanoid; -use perry_runtime::{js_string_from_bytes, StringHeader}; - -use crate::common::string_from_header; - -/// Generate a nanoid with default settings (21 chars, URL-safe alphabet) -/// nanoid() -> string -#[no_mangle] -pub extern "C" fn js_nanoid() -> *mut StringHeader { - let id = nanoid!(); - js_string_from_bytes(id.as_ptr(), id.len() as u32) -} - -/// Generate a nanoid with custom length -/// nanoid(size) -> string -#[no_mangle] -pub extern "C" fn js_nanoid_sized(size: f64) -> *mut StringHeader { - let size = size as usize; - if size == 0 { - return js_nanoid(); - } - let id = nanoid!(size); - js_string_from_bytes(id.as_ptr(), id.len() as u32) -} - -/// Generate a nanoid with custom alphabet and size -/// customAlphabet(alphabet, size)() -> string -/// For simplicity, we combine this into one call: nanoid.custom(alphabet, size) -#[no_mangle] -pub unsafe extern "C" fn js_nanoid_custom( - alphabet_ptr: *const StringHeader, - size: f64, -) -> *mut StringHeader { - let alphabet = match string_from_header(alphabet_ptr) { - Some(a) => a, - None => return js_nanoid(), - }; - - let size = if size <= 0.0 { 21 } else { size as usize }; - let alphabet_chars: Vec = alphabet.chars().collect(); - - if alphabet_chars.is_empty() { - return js_nanoid(); - } - - // Generate ID using custom alphabet - use rand::RngExt; - let mut rng = rand::rng(); - let id: String = (0..size) - .map(|_| { - let idx = rng.random_range(0..alphabet_chars.len()); - alphabet_chars[idx] - }) - .collect(); - - js_string_from_bytes(id.as_ptr(), id.len() as u32) -} diff --git a/crates/perry-ui-android/src/stdlib_stubs.rs b/crates/perry-ui-android/src/stdlib_stubs.rs index 6da9579d68..0fc7b58452 100644 --- a/crates/perry-ui-android/src/stdlib_stubs.rs +++ b/crates/perry-ui-android/src/stdlib_stubs.rs @@ -1267,14 +1267,6 @@ pub extern "C" fn js_mysql2_pool_query() -> i64 { 0 } #[no_mangle] -pub extern "C" fn js_nanoid() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_nanoid_custom() -> i64 { - 0 -} -#[no_mangle] pub extern "C" fn js_new_instance() -> i64 { 0 } diff --git a/crates/perry/src/commands/compile/well_known.rs b/crates/perry/src/commands/compile/well_known.rs index 4307c829bc..dab94870d9 100644 --- a/crates/perry/src/commands/compile/well_known.rs +++ b/crates/perry/src/commands/compile/well_known.rs @@ -476,18 +476,6 @@ mod tests { } } - #[test] - fn shipped_unproven_bindings_are_partial() { - for name in ["nanoid"] { - let b = lookup_well_known(name).unwrap_or_else(|| panic!("{name} registered")); - assert_eq!( - b.compat, - BindingCompat::Partial, - "{name} omits upstream API/behavior and must stay partial" - ); - } - } - #[test] fn aliases_inherit_target_compat_and_cycles_fail_closed() { let raw = r#" diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index b1b39d6359..6ca239fa23 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -160,13 +160,12 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { "argon2" => &["bundled-argon2"], // ── IDs (uuid / nanoid) ─────────────────────────────────────── - // Per-binding split as of v0.5.534 (#466 Phase 4 step 2) - // so the well-known flip can swap each one out - // independently. The `ids` umbrella stays in - // perry-stdlib/Cargo.toml as `bundled-uuid + bundled-nanoid` - // for backwards compat, but feature-set computation goes - // straight to the per-binding feature. - "nanoid" => &["bundled-nanoid"], + // No entries: the uuid binding (#10701) and the nanoid + // binding (#10693) are gone, so `import "uuid"` / + // `import "nanoid"` compile the real npm packages from + // source and need no perry-stdlib feature. The `ids` + // umbrella survives (empty) in perry-stdlib/Cargo.toml + // for backwards compat only. // ── Container ───────────────────────────────────────────────── "perry/container" | "perry/container-compose" | "perry/compose" | "perry/workloads" => { diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index 4721c63bc0..1a913b4cff 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -36,21 +36,6 @@ # requires every ext crate and package mapping to have an explicit decision # (#5716). -[bindings.nanoid] -crate = "perry-ext-nanoid" -lib = "perry_ext_nanoid" -tracking = "#466" -# Partial: the wrapper covers nanoid + custom alphabets, but not the complete -# upstream export set and flattens the curried customAlphabet contract. -compat = "partial" - -[bindings.nanoid.upstream] -version = "6.0.0" -sha256 = "5cade80a39ccf4fd174c8e412eca13accfce36c9c2a0982b4ea23403d729a8d8" -repo = "https://github.com/ai/nanoid" -ref = "4dacb107b54ffd0e1abfe91b7ef452e0fd5a8e12" -ported-at = "6.0.0" -date = "2026-07-30" [bindings.qs] crate = "perry-ext-qs" lib = "perry_ext_qs" diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 47a24f494b..78c924c802 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: 2065 entries across 131 modules +// Coverage: 2064 entries across 130 modules type PerryI8 = number & { readonly __perryI8?: never }; type PerryI16 = number & { readonly __perryI16?: never }; @@ -2215,11 +2215,6 @@ declare module "mysql2/promise" { export function createPool(p0: any): any; } -declare module "nanoid" { - /** stdlib */ - export function nanoid(size: number): string; -} - declare module "net" { /** stdlib */ export class BlockList { [key: string]: any; } diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index c0dd3c2f51..5ea2875952 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: 3007 entries across 133 modules. +Total: 3006 entries across 132 modules. ## Modules @@ -69,7 +69,6 @@ Total: 3007 entries across 133 modules. - [`mongodb`](#mongodb) - [`mysql2`](#mysql2) - [`mysql2/promise`](#mysql2promise) -- [`nanoid`](#nanoid) - [`net`](#net) - [`node-cron`](#node-cron) - [`node-fetch`](#node-fetch) @@ -2174,12 +2173,6 @@ Total: 3007 entries across 133 modules. - `release` — instance - `rollback` — instance -## `nanoid` - -### Methods - -- `nanoid` — module - ## `net` ### Classes diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index 3939cc0e72..d893a096a4 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -104,7 +104,6 @@ from `well_known_bindings.toml`. Regenerate this table with | `perry-ext-moment` | `moment` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-mongodb` | `mongodb` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-mysql2` | `mysql2`
`mysql2/promise` | Source package | Compile the upstream package source | Bundled; migration pending | -| `perry-ext-nanoid` | `nanoid` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-net` | `net` | Runtime API | Keep near core; consolidate when practical | Bundled; retained | | `perry-ext-node-forge` | `node-forge` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-nodemailer` | `nodemailer` | Source package | Compile the upstream package source | Bundled; migration pending | diff --git a/workspace-architecture.json b/workspace-architecture.json index 85139f6cfb..7de6b0d0a6 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -25,7 +25,7 @@ ] }, "baseline": { - "workspace_members": 77, + "workspace_members": 76, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -68,7 +68,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 28, + "externalize": 27, "keep": 44, "merge": 1, "remove": 1, @@ -255,11 +255,6 @@ "decision": "externalize", "migration": "compile-source" }, - "perry-ext-nanoid": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-net": { "category": "binding", "decision": "keep", From 7c2952cbdeaa3a706f311b91e9b2e89512522946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 20 Sep 2026 00:18:36 +0200 Subject: [PATCH 4/5] changelog: add fragment for #10693 (nanoid native binding removal) --- changelog.d/10693-nanoid-native-binding-removal.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 changelog.d/10693-nanoid-native-binding-removal.md diff --git a/changelog.d/10693-nanoid-native-binding-removal.md b/changelog.d/10693-nanoid-native-binding-removal.md new file mode 100644 index 0000000000..6bdd6dfe35 --- /dev/null +++ b/changelog.d/10693-nanoid-native-binding-removal.md @@ -0,0 +1,14 @@ +Removed the native `nanoid` binding: `customAlphabet(alphabet, size)` is +documented to return a generator function, but the native implementation +returned the generated id string directly, so the only documented usage +(`const gen = customAlphabet(...); gen();`) crashed with `TypeError: value +is not a function`. `import { nanoid, customAlphabet } from "nanoid"` (no +`perry.compilePackages` entry) now compiles the real npm package from +source, matching Node exactly. + +Deleted both duplicate hand-written implementations +(`crates/perry-ext-nanoid` and `crates/perry-stdlib/src/nanoid.rs`, which +independently exported the same `js_nanoid_*` symbols — #10678). +`customAlphabet` turned out to have no call-site wiring anywhere in +`perry-codegen` at all — only plain `nanoid(size)` had a dispatch row, +consistent with the reported crash. From 0c7b97f5f21c4f0eb4d57912707d5597d6b52ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 20 Sep 2026 00:26:40 +0200 Subject: [PATCH 5/5] chore: release merge train 228 as v0.5.1607 --- CLAUDE.md | 2 +- Cargo.lock | 148 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 71325fc631..f90dc57284 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1606 +**Current Version:** 0.5.1607 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index b6b0cfce43..1ab484f76c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5614,7 +5614,7 @@ checksum = "1473d470930ed48574515a25df34900f3af89c6fa422d903e019121312a9f13e" [[package]] name = "perry" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "base64 0.22.1", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-dispatch", "serde", @@ -5686,7 +5686,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "cc", "libc", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "aho-corasick", "anyhow", @@ -5712,7 +5712,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-hir", @@ -5720,7 +5720,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-hir", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-dispatch", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-hir", @@ -5745,7 +5745,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "base64 0.22.1", @@ -5757,7 +5757,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-hir", @@ -5765,7 +5765,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "async-trait", "clap", @@ -5789,14 +5789,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "serde", "serde_json", @@ -5804,7 +5804,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1606" +version = "0.5.1607" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5815,7 +5815,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "clap", @@ -5830,7 +5830,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "block2", "objc2", @@ -5840,7 +5840,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "argon2", "perry-ffi", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "bcrypt", "perry-ffi", @@ -5857,7 +5857,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "rusqlite", @@ -5865,7 +5865,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "scraper", @@ -5873,7 +5873,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "perry-runtime", @@ -5881,7 +5881,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "chrono", "cron", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "chrono", "perry-ffi", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "rust_decimal", @@ -5907,7 +5907,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5915,7 +5915,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "perry-runtime", @@ -5923,14 +5923,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "bytes", "http-body-util", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "bytes", "lazy_static", @@ -5960,7 +5960,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "bytes", @@ -5992,7 +5992,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "lazy_static", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "lru", "perry-ffi", @@ -6011,7 +6011,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "chrono", "perry-ffi", @@ -6019,7 +6019,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "bson", "futures-util", @@ -6031,7 +6031,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "chrono", "perry-ffi", @@ -6043,7 +6043,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "bytes", "perry-ffi", @@ -6058,7 +6058,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6077,7 +6077,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "lettre", "perry-ffi", @@ -6087,7 +6087,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "notify", "perry-ffi", @@ -6099,7 +6099,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "printpdf", @@ -6107,7 +6107,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "sqlx", @@ -6116,7 +6116,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "perry-runtime", @@ -6125,7 +6125,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "governor", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "fast_image_resize", "image", @@ -6144,7 +6144,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "lazy_static", "perry-ffi", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-ffi", @@ -6173,7 +6173,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-ffi", "perry-runtime", @@ -6182,7 +6182,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "futures-util", "lazy_static", @@ -6195,7 +6195,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "brotli", "flate2", @@ -6205,7 +6205,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6215,7 +6215,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-api-manifest", @@ -6235,11 +6235,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1606" +version = "0.5.1607" [[package]] name = "perry-parser" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "perry-diagnostics", @@ -6252,7 +6252,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perex", "regex", @@ -6260,7 +6260,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "ahash", "base64 0.22.1", @@ -6318,14 +6318,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6411,21 +6411,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "dirs", "perry-ffi", @@ -6435,7 +6435,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "jni", @@ -6450,7 +6450,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "rand 0.10.2", "serde", @@ -6460,7 +6460,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6483,7 +6483,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "block2", @@ -6500,7 +6500,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "block2", @@ -6517,7 +6517,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1606" +version = "0.5.1607" [[package]] name = "perry-ui-test" @@ -6528,11 +6528,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1606" +version = "0.5.1607" [[package]] name = "perry-ui-tvos" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "block2", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "block2", "libc", @@ -6580,7 +6580,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "libc", @@ -6599,7 +6599,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "base64 0.22.1", "libc", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "anyhow", "base64 0.22.1", @@ -6627,7 +6627,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1606" +version = "0.5.1607" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 336f2b35af..cf3ad03e29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -331,7 +331,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1606" +version = "0.5.1607" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"