diff --git a/Cargo.lock b/Cargo.lock index 1fbb5e01ea..94206ef31f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5914,14 +5914,6 @@ dependencies = [ "rust_decimal", ] -[[package]] -name = "perry-ext-dotenv" -version = "0.5.1605" -dependencies = [ - "perry-ffi", - "serde_json", -] - [[package]] name = "perry-ext-ethers" version = "0.5.1605" diff --git a/Cargo.toml b/Cargo.toml index f5a536bf9d..da91a3a897 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-dotenv", "crates/perry-ext-nanoid", "crates/perry-ext-bcrypt", "crates/perry-ext-argon2", @@ -472,7 +471,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-dotenv = { path = "crates/perry-ext-dotenv" } perry-ext-nanoid = { path = "crates/perry-ext-nanoid" } perry-ext-bcrypt = { path = "crates/perry-ext-bcrypt" } perry-ext-argon2 = { path = "crates/perry-ext-argon2" } diff --git a/changelog.d/10691-dotenv-native-binding-removal.md b/changelog.d/10691-dotenv-native-binding-removal.md new file mode 100644 index 0000000000..2cc68fa650 --- /dev/null +++ b/changelog.d/10691-dotenv-native-binding-removal.md @@ -0,0 +1,16 @@ +Removed the native `dotenv` binding: `dotenv.parse(Buffer)` (the idiomatic +`dotenv.parse(fs.readFileSync(...))`) returned 0 keys, and `config()` +reported no error but populated neither `result.parsed` nor `process.env` — +a silent total no-op. `import dotenv from "dotenv"` (no +`perry.compilePackages` entry) now compiles the real npm package from +source, matching Node exactly. + +Deleted both duplicate hand-written implementations +(`crates/perry-ext-dotenv` and `crates/perry-stdlib/src/dotenv.rs`, which +independently exported the same `js_dotenv_*` symbols — #10678) and removed +`"dotenv"` from `PERRY_NATIVE_EXTENSION_PACKAGES` so the real package's +source (including the `dotenv/config` auto-load subpath) reaches the module +walker instead of being skipped as "handled by native stdlib". Also fixed a +standalone-workspace release fixture +(`tests/release/packages/next-app-route/provider/stdlib/Cargo.toml`) that +referenced the now-deleted `bundled-dotenv` feature. diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 7ea8ac5320..53e9dbe339 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -43,8 +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 - "dotenv", // .env file loader - "dotenv/config", // dotenv's auto-load-on-import subpath "nanoid", // compact URL-safe ID generation "ethers", // Ethereum library (utils/wallet/ABI) "mongodb", // MongoDB driver diff --git a/crates/perry-api-manifest/src/entries/part_1.rs b/crates/perry-api-manifest/src/entries/part_1.rs index 2b801b3cf7..9dc26c00a7 100644 --- a/crates/perry-api-manifest/src/entries/part_1.rs +++ b/crates/perry-api-manifest/src/entries/part_1.rs @@ -1111,28 +1111,6 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[ ), method("nodemailer", "sendMail", true, None), method("nodemailer", "verify", true, None), - method_sig("dotenv", "config", false, None, &[], TypeSpec::Any), - // `dotenv.parse(src)` — the native impl (`js_dotenv_parse`) has shipped - // since the module was added, but the manifest never registered the - // symbol, so the #463 gate compiled every call site to a deferred - // throw-on-reach error. Callers that wrap config loading in - // `try { … } catch {}` swallowed that throw and silently got no config - // at all, which is why this is registered as a data-loss fix, not a - // missing-feature one. The extern returns a JSON string; the dispatch - // row's `NR_OBJ_FROM_JSON_STR` pipes it through `js_json_parse` so the - // user-visible value is a real object. - method_sig( - "dotenv", - "parse", - false, - None, - &[ParamSpec::Named { - name: "src", - ty: TypeSpec::String, - optional: false, - }], - TypeSpec::Any, - ), method_sig( "nanoid", "nanoid", diff --git a/crates/perry-api-manifest/src/lib.rs b/crates/perry-api-manifest/src/lib.rs index 7a29b3d943..c9f0443985 100644 --- a/crates/perry-api-manifest/src/lib.rs +++ b/crates/perry-api-manifest/src/lib.rs @@ -597,48 +597,6 @@ mod tests { assert!(matches!(arena.kind, ApiKind::Property)); } - /// `dotenv.parse` regression guard. - /// - /// `js_dotenv_parse` has always been implemented and declared to codegen, - /// but the manifest only ever registered `dotenv.config`. That gap made - /// the #463 unimplemented-API gate fire for every `dotenv.parse(...)` - /// call site, which under the default (defer) policy compiles to a - /// throw-on-reach runtime error rather than a build failure. Callers that - /// load config inside `try { … } catch {}` — the common shape — swallowed - /// the throw and silently ran with no configuration at all. - #[test] - fn dotenv_parse_is_registered() { - let entry = module_has_symbol("dotenv", "parse") - .expect("dotenv.parse must be in the manifest — see js_dotenv_parse"); - assert!( - matches!( - entry.kind, - ApiKind::Method { - has_receiver: false, - class_filter: None - } - ), - "dotenv.parse must be a static module method, got {:?}", - entry.kind - ); - assert_eq!( - entry.params.len(), - 1, - "dotenv.parse takes exactly the source text" - ); - assert!( - matches!( - entry.params[0], - ParamSpec::Named { - ty: TypeSpec::String, - .. - } - ), - "dotenv.parse's argument is the .env source string, got {:?}", - entry.params[0] - ); - } - #[test] fn buffer_inspect_max_bytes_is_manifest_property() { let entry = module_has_symbol("node:buffer", "INSPECT_MAX_BYTES") 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 96529d8cd8..4b41aa587c 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,30 +29,6 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[ args: &[], ret: NR_GCPTR, }, - // ========== dotenv ========== - NativeModSig { - module: "dotenv", - has_receiver: false, - method: "config", - class_filter: None, - runtime: "js_dotenv_config", - args: &[], - ret: NR_F64, - }, - // `dotenv.parse(src)` → the JSON string `js_dotenv_parse` builds, piped - // through `js_json_parse` by NR_OBJ_FROM_JSON_STR so TypeScript sees a - // real object (`{ FOO: "bar" }`), not the encoded string. Without this - // row the symbol fell through the #463 gate to a deferred runtime throw - // even though the native implementation was already linked in. - NativeModSig { - module: "dotenv", - has_receiver: false, - method: "parse", - class_filter: None, - runtime: "js_dotenv_parse", - args: &[NA_STR], - ret: NR_OBJ_FROM_JSON_STR, - }, // ========== 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. @@ -243,38 +219,3 @@ pub(super) const UTILS_CRYPTO_ROWS: &[NativeModSig] = &[ ret: NR_VOID, }, ]; - -#[cfg(test)] -mod tests { - use super::*; - - /// `dotenv.parse` must dispatch to the native implementation and return a - /// real object. - /// - /// `js_dotenv_parse` was declared to codegen and linked into every binary, - /// but had no dispatch row, so the #463 gate compiled each call site to a - /// deferred throw-on-reach error. `readConfigFile()`-shaped callers wrap - /// the call in `try { … } catch {}`, so the throw was swallowed and the - /// `.env` config silently never loaded. - /// - /// The return kind matters as much as the row: `js_dotenv_parse` hands back - /// a JSON *string*, so only `NR_OBJ_FROM_JSON_STR` (which pipes it through - /// `js_json_parse`) makes `dotenv.parse(src).FOO` read a property instead - /// of indexing a string. - #[test] - fn dotenv_parse_dispatches_to_native_impl_as_an_object() { - let row = UTILS_CRYPTO_ROWS - .iter() - .find(|r| r.module == "dotenv" && r.method == "parse") - .expect("dotenv.parse needs a dispatch row"); - assert_eq!(row.runtime, "js_dotenv_parse"); - assert!(!row.has_receiver); - assert_eq!(row.class_filter, None); - assert!(matches!(row.args, [NativeArgKind::StrPtr])); - assert!( - matches!(row.ret, NativeRetKind::ObjFromJsonStr), - "dotenv.parse must be JSON-decoded into an object, got {:?}", - row.ret - ); - } -} diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs index 417c9b491e..cf739da7f8 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/utilities.rs @@ -35,11 +35,6 @@ pub(crate) fn declare_utilities(module: &mut LlModule) { module.declare_function("js_commander_required_option", I64, &[I64, I64, I64, I64]); module.declare_function("js_commander_version", I64, &[I64, I64]); - // ========== Dotenv ========== - module.declare_function("js_dotenv_config", DOUBLE, &[]); - module.declare_function("js_dotenv_config_path", DOUBLE, &[I64]); - module.declare_function("js_dotenv_parse", I64, &[I64]); - // ========== Date libs (dayjs/datefns/moment) ========== module.declare_function("js_datefns_add_days", DOUBLE, &[DOUBLE, DOUBLE]); module.declare_function("js_datefns_add_months", DOUBLE, &[DOUBLE, DOUBLE]); diff --git a/crates/perry-codegen/tests/manifest_consistency.rs b/crates/perry-codegen/tests/manifest_consistency.rs index a0ee87a780..043a61cf82 100644 --- a/crates/perry-codegen/tests/manifest_consistency.rs +++ b/crates/perry-codegen/tests/manifest_consistency.rs @@ -199,7 +199,7 @@ fn every_native_module_has_at_least_one_manifest_entry() { /// allowed list documents the exception so a future module that /// genuinely lacks coverage doesn't sneak past CI by being added /// here. - const SIDE_EFFECT_ONLY: &[&str] = &["dotenv/config"]; + const SIDE_EFFECT_ONLY: &[&str] = &[]; let mut missing: Vec<&'static str> = Vec::new(); for &module in perry_api_manifest::NATIVE_MODULES { @@ -267,7 +267,7 @@ fn cjs_style_node_builtins_have_default_entries() { /// the sibling test above and excluded here too. #[test] fn every_well_known_binding_has_manifest_entry() { - const SIDE_EFFECT_ONLY: &[&str] = &["dotenv/config"]; + const SIDE_EFFECT_ONLY: &[&str] = &[]; // Inline parse of well_known_bindings.toml — small enough that // pulling in `toml` as a dev-dep just for this test would be diff --git a/crates/perry-ext-dotenv/Cargo.toml b/crates/perry-ext-dotenv/Cargo.toml deleted file mode 100644 index e0ed850eff..0000000000 --- a/crates/perry-ext-dotenv/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "perry-ext-dotenv" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for the npm `dotenv` package — wraps Rust's std env API behind the same `dotenv.config()` / `dotenv.parse()` surface that Node code uses. Acceptance test for the perry-ffi v0.5 surface (#466 Phase 1 / 5 step 1)." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -serde_json.workspace = true - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-dotenv/src/lib.rs b/crates/perry-ext-dotenv/src/lib.rs deleted file mode 100644 index 3abd8675aa..0000000000 --- a/crates/perry-ext-dotenv/src/lib.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Native bindings for the npm `dotenv` package. -//! -//! Functionally identical to the implementation that lives in -//! `crates/perry-stdlib/src/dotenv.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 is sufficient for -//! a real wrapper. -//! -//! # Status -//! -//! Additive port (#466 Phase 5 step 1). The original -//! `perry-stdlib::dotenv` stays in place and is what compiled -//! programs link against today. Once a release ships and no -//! regressions surface, the well-known bindings table (#466 Phase 4) -//! flips `import 'dotenv'` resolution to point at this crate, and -//! the old code is deleted. - -use perry_ffi::{alloc_string, read_string, JsString, StringHeader}; -use std::collections::HashMap; -use std::fs; -use std::sync::Mutex; - -static DOTENV_LOADED: Mutex = Mutex::new(false); - -/// Parse a `.env` file's contents into key/value pairs. -/// -/// Implementation detail — exposed so the test crate can compare -/// against Node's parsing behavior. Not part of the FFI surface. -fn parse_dotenv_content(content: &str) -> HashMap { - let mut vars = HashMap::new(); - - for line in content.lines() { - let line = line.trim(); - - if line.is_empty() || line.starts_with('#') { - continue; - } - - if let Some(eq_pos) = line.find('=') { - let key = line[..eq_pos].trim().to_string(); - let mut value = line[eq_pos + 1..].trim().to_string(); - - if (value.starts_with('"') && value.ends_with('"')) - || (value.starts_with('\'') && value.ends_with('\'')) - { - value = value[1..value.len() - 1].to_string(); - } - - if value.contains("\\n") { - value = value.replace("\\n", "\n"); - } - if value.contains("\\t") { - value = value.replace("\\t", "\t"); - } - - vars.insert(key, value); - } - } - - vars -} - -/// `dotenv.config()` — load `.env` from CWD and apply to `std::env`. -#[no_mangle] -pub extern "C" fn js_dotenv_config() -> f64 { - // SAFETY: passing a null handle is documented input — the helper - // below treats it as "use default path .env". - unsafe { js_dotenv_config_path(std::ptr::null()) } -} - -/// `dotenv.config({ path })` — load `.env` from the given path. -/// -/// # Safety -/// -/// `path_ptr` must be either null (then `.env` is used) or a pointer -/// to a Perry-runtime-allocated `StringHeader`. Caller responsibility -/// is the same contract as any other `extern "C"` function in the -/// stdlib. -#[no_mangle] -pub unsafe extern "C" fn js_dotenv_config_path(path_ptr: *const StringHeader) -> f64 { - let path = if path_ptr.is_null() { - ".env".to_string() - } else { - let handle = JsString::from_raw(path_ptr as *mut StringHeader); - read_string(handle) - .map(|s| s.to_string()) - .unwrap_or_else(|| ".env".to_string()) - }; - - let content = match fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => return 0.0, // missing file is not an error in dotenv - }; - - let vars = parse_dotenv_content(&content); - for (key, value) in vars { - // SAFETY: setting env vars before any thread reads them is - // the documented use of dotenv. Concurrent set_var from - // multiple threads is undefined behavior in std — but - // dotenv.config() runs once at module-init time. - unsafe { std::env::set_var(&key, &value) }; - } - - *DOTENV_LOADED.lock().unwrap() = true; - 1.0 -} - -/// `dotenv.parse(content)` — parse `.env`-formatted text into a JSON -/// string the runtime can pass back to TypeScript as an object. -/// -/// # Safety -/// -/// `content_ptr` must be null or a pointer to a Perry-runtime -/// `StringHeader`. -#[no_mangle] -pub unsafe extern "C" fn js_dotenv_parse(content_ptr: *const StringHeader) -> *mut StringHeader { - let handle = JsString::from_raw(content_ptr as *mut StringHeader); - let content = match read_string(handle) { - Some(c) => c, - None => return std::ptr::null_mut(), - }; - - let vars = parse_dotenv_content(content); - let json = serde_json::to_string(&vars).unwrap_or_else(|_| "{}".to_string()); - alloc_string(&json).as_raw() -} - -#[cfg(test)] -mod tests { - use super::parse_dotenv_content; - - #[test] - fn parses_basic_kv() { - let vars = parse_dotenv_content("FOO=bar\nBAZ=qux\n"); - assert_eq!(vars.get("FOO"), Some(&"bar".to_string())); - assert_eq!(vars.get("BAZ"), Some(&"qux".to_string())); - } - - #[test] - fn skips_comments_and_empty_lines() { - let vars = parse_dotenv_content("# comment\n\nFOO=bar\n# another\n"); - assert_eq!(vars.len(), 1); - assert_eq!(vars.get("FOO"), Some(&"bar".to_string())); - } - - #[test] - fn unwraps_quoted_values() { - let vars = parse_dotenv_content( - r#"DOUBLE="hello" -SINGLE='world' -ESCAPED="line1\nline2" -"#, - ); - assert_eq!(vars.get("DOUBLE"), Some(&"hello".to_string())); - assert_eq!(vars.get("SINGLE"), Some(&"world".to_string())); - assert_eq!(vars.get("ESCAPED"), Some(&"line1\nline2".to_string())); - } - - #[test] - fn round_trips_through_perry_ffi() { - // Allocate a fake .env content string via perry-ffi, run it - // through js_dotenv_parse, read the JSON back. Proves the - // wrapper's only contact with the runtime — string read + - // string alloc — survives end-to-end. - let content = perry_ffi::alloc_string("KEY=value\n# c\nOTHER=42\n"); - let json_handle = unsafe { super::js_dotenv_parse(content.as_raw() as *const _) }; - let json_handle_wrapped = unsafe { perry_ffi::JsString::from_raw(json_handle) }; - let json_str = - perry_ffi::read_string(json_handle_wrapped).expect("parse returned non-null"); - // serde_json hash-map order isn't guaranteed; check substrings. - assert!(json_str.contains("\"KEY\":\"value\""), "got: {}", json_str); - assert!(json_str.contains("\"OTHER\":\"42\""), "got: {}", json_str); - } -} diff --git a/crates/perry-hir/tests/unimplemented_api_check.rs b/crates/perry-hir/tests/unimplemented_api_check.rs index 62419f4129..733acd9aa9 100644 --- a/crates/perry-hir/tests/unimplemented_api_check.rs +++ b/crates/perry-hir/tests/unimplemented_api_check.rs @@ -367,10 +367,7 @@ fn perry_native_namespace_rejects_unknown_call_in_strict_mode() { /// no value binding to read properties off, so the gate doesn't apply. #[test] fn every_supported_module_rejects_bogus_member() { - const SKIP: &[&str] = &[ - // Side-effect-only — no value binding to access. - "dotenv/config", - ]; + const SKIP: &[&str] = &[]; let mut failures: Vec = Vec::new(); for &module in perry_api_manifest::NATIVE_MODULES { @@ -444,10 +441,7 @@ fn every_supported_module_rejects_bogus_member() { /// land at the rejection. #[test] fn every_supported_module_rejects_bogus_call() { - const SKIP: &[&str] = &[ - // Side-effect-only — no value binding to access. - "dotenv/config", - ]; + const SKIP: &[&str] = &[]; let mut failures: Vec = Vec::new(); for &module in perry_api_manifest::NATIVE_MODULES { diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 57d1141486..0357575ca8 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -23,19 +23,11 @@ default = ["full"] # must stay out of this list: release archives enable `full` without linking # their per-program provider archives, and adding an external HTTP pump here # made HTTP-free Linux UI links require libperry_ext_http.a (#5983, #8587). -full = ["http-server", "http-client", "database", "crypto", "compression", "email", "websocket", "image", "scheduler", "ids", "html-parser", "rate-limit", "net", "tls", "bundled-dotenv", "bundled-lru-cache", "bundled-exponential-backoff", "bundled-events", "bundled-decimal", "bundled-dayjs", "bundled-moment", "bundled-commander", "bundled-streams"] +full = ["http-server", "http-client", "database", "crypto", "compression", "email", "websocket", "image", "scheduler", "ids", "html-parser", "rate-limit", "net", "tls", "bundled-lru-cache", "bundled-exponential-backoff", "bundled-events", "bundled-decimal", "bundled-dayjs", "bundled-moment", "bundled-commander", "bundled-streams"] # Minimal core - just what's needed for basic programs core = [] -# In-tree implementation of the npm `dotenv` package (#466 Phase 4 -# step 2). Default-on; turned off by the compiler when the -# well-known bindings table (`well_known_bindings.toml`) routes -# `import 'dotenv'` to `perry-ext-dotenv` so the link line doesn't -# end up with two copies of `_js_dotenv_*` symbols. Programs that -# don't import dotenv pay nothing for this either way. -bundled-dotenv = [] - # In-tree implementation of `lru-cache`. Default-on through # `default = ["full"]`; flipped to perry-ext-lru-cache by the # well-known table (#466 Phase 4). Pulls the `lru` crate dep so diff --git a/crates/perry-stdlib/src/dotenv.rs b/crates/perry-stdlib/src/dotenv.rs deleted file mode 100644 index fb17c93c27..0000000000 --- a/crates/perry-stdlib/src/dotenv.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Dotenv module (dotenv compatible) -//! -//! Native implementation of the 'dotenv' npm package. -//! Loads environment variables from .env files. - -use perry_runtime::{js_string_from_bytes, StringHeader}; -use std::collections::HashMap; -use std::fs; -use std::sync::Mutex; - -use crate::common::string_from_header; - -lazy_static::lazy_static! { - static ref DOTENV_LOADED: Mutex = Mutex::new(false); -} - -/// Parse a .env file content into key-value pairs -fn parse_dotenv_content(content: &str) -> HashMap { - let mut vars = HashMap::new(); - - for line in content.lines() { - let line = line.trim(); - - // Skip empty lines and comments - if line.is_empty() || line.starts_with('#') { - continue; - } - - // Find the first '=' to split key and value - if let Some(eq_pos) = line.find('=') { - let key = line[..eq_pos].trim().to_string(); - let mut value = line[eq_pos + 1..].trim().to_string(); - - // Remove surrounding quotes if present - if (value.starts_with('"') && value.ends_with('"')) - || (value.starts_with('\'') && value.ends_with('\'')) - { - value = value[1..value.len() - 1].to_string(); - } - - // Handle escape sequences in double-quoted strings - if value.contains("\\n") { - value = value.replace("\\n", "\n"); - } - if value.contains("\\t") { - value = value.replace("\\t", "\t"); - } - - vars.insert(key, value); - } - } - - vars -} - -/// Load .env file and set environment variables -/// dotenv.config() -> void -#[no_mangle] -pub extern "C" fn js_dotenv_config() -> f64 { - // SAFETY: We're passing a null pointer which is handled safely by js_dotenv_config_path - unsafe { js_dotenv_config_path(std::ptr::null()) } -} - -/// Load .env file from a specific path -/// dotenv.config({ path: '.env.local' }) -> void -#[no_mangle] -pub unsafe extern "C" fn js_dotenv_config_path(path_ptr: *const StringHeader) -> f64 { - let path = if path_ptr.is_null() { - ".env".to_string() - } else { - string_from_header(path_ptr).unwrap_or_else(|| ".env".to_string()) - }; - - // Read the file - let content = match fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => return 0.0, // File not found is not an error in dotenv - }; - - // Parse and set environment variables - let vars = parse_dotenv_content(&content); - for (key, value) in vars { - std::env::set_var(&key, &value); - } - - *DOTENV_LOADED.lock().unwrap() = true; - 1.0 // Success -} - -/// Parse a string as dotenv format without setting env vars -/// dotenv.parse(content) -> object -#[no_mangle] -pub unsafe extern "C" fn js_dotenv_parse(content_ptr: *const StringHeader) -> *mut StringHeader { - let content = match string_from_header(content_ptr) { - Some(c) => c, - None => return std::ptr::null_mut(), - }; - - let vars = parse_dotenv_content(&content); - - // Return as JSON string (simple key-value object) - let json = serde_json::to_string(&vars).unwrap_or_else(|_| "{}".to_string()); - js_string_from_bytes(json.as_ptr(), json.len() as u32) -} diff --git a/crates/perry-stdlib/src/lib.rs b/crates/perry-stdlib/src/lib.rs index 8812fb2a15..7c4e9ed331 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -51,8 +51,6 @@ pub mod decimal; // without duplicate _js_dotenv_* symbols at link time. Default-on // preserves byte-identical behavior for programs that don't opt into // the well-known path. -#[cfg(feature = "bundled-dotenv")] -pub mod dotenv; // events feature-gated as of v0.5.546 so the well-known flip // can route to perry-ext-events. #[cfg(feature = "bundled-events")] @@ -101,8 +99,6 @@ pub use dayjs::*; #[cfg(feature = "bundled-decimal")] pub use decimal::*; pub use domain::*; -#[cfg(feature = "bundled-dotenv")] -pub use dotenv::*; #[cfg(feature = "bundled-events")] pub use events::*; #[cfg(feature = "bundled-exponential-backoff")] diff --git a/crates/perry-ui-android/src/stdlib_stubs.rs b/crates/perry-ui-android/src/stdlib_stubs.rs index 07eafc3afe..6da9579d68 100644 --- a/crates/perry-ui-android/src/stdlib_stubs.rs +++ b/crates/perry-ui-android/src/stdlib_stubs.rs @@ -508,18 +508,6 @@ pub extern "C" fn js_decimal_to_string() -> i64 { 0 } #[no_mangle] -pub extern "C" fn js_dotenv_config() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_dotenv_config_path() -> i64 { - 0 -} -#[no_mangle] -pub extern "C" fn js_dotenv_parse() -> i64 { - 0 -} -#[no_mangle] pub extern "C" fn js_ethers_format_ether() -> i64 { 0 } diff --git a/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs b/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs index bc05d3cba6..8fcda7242c 100644 --- a/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs +++ b/crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs @@ -108,10 +108,6 @@ mod tests { let (root, binding) = lookup_well_known_for_import("mysql2/promise"); assert_eq!(root, "mysql2"); assert_eq!(binding.expect("subpath binding").package, "mysql2/promise"); - - let (root, binding) = lookup_well_known_for_import("dotenv/config"); - assert_eq!(root, "dotenv"); - assert_eq!(binding.expect("root fallback").package, "dotenv"); } #[test] diff --git a/crates/perry/src/commands/compile/resolve.rs b/crates/perry/src/commands/compile/resolve.rs index 54d2d205c0..8c5280cca3 100644 --- a/crates/perry/src/commands/compile/resolve.rs +++ b/crates/perry/src/commands/compile/resolve.rs @@ -146,8 +146,7 @@ mod tests; // without the guard, a deep import reached through another package's // compiled JS would make the walker read undici's real sources (llhttp // wasm) instead of routing to perry-ext-undici. -const PERRY_NATIVE_EXTENSION_PACKAGES: &[&str] = - &["ioredis", "ethers", "mysql2", "ws", "dotenv", "undici"]; +const PERRY_NATIVE_EXTENSION_PACKAGES: &[&str] = &["ioredis", "ethers", "mysql2", "ws", "undici"]; /// Absolute virtual prefix used by files extracted from a Bun standalone /// executable. `--bunfs-root` maps the suffix below this prefix to a real diff --git a/crates/perry/src/commands/compile/well_known.rs b/crates/perry/src/commands/compile/well_known.rs index f23031ba8a..4307c829bc 100644 --- a/crates/perry/src/commands/compile/well_known.rs +++ b/crates/perry/src/commands/compile/well_known.rs @@ -369,13 +369,6 @@ mod tests { let _ = registry(); } - #[test] - fn dotenv_is_registered() { - let binding = lookup_well_known("dotenv").expect("dotenv must be a well-known binding"); - assert_eq!(binding.krate, "perry-ext-dotenv"); - assert_eq!(binding.lib, "perry_ext_dotenv"); - } - #[test] fn undici_is_registered() { let binding = lookup_well_known("undici").expect("undici must be a well-known binding"); @@ -385,8 +378,8 @@ mod tests { #[test] fn node_prefix_stripped_on_lookup() { - let bare = lookup_well_known("dotenv"); - let prefixed = lookup_well_known("node:dotenv"); + let bare = lookup_well_known("bcrypt"); + let prefixed = lookup_well_known("node:bcrypt"); assert!(bare.is_some()); assert!(prefixed.is_some()); } @@ -485,7 +478,7 @@ mod tests { #[test] fn shipped_unproven_bindings_are_partial() { - for name in ["dotenv", "nanoid"] { + for name in ["nanoid"] { let b = lookup_well_known(name).unwrap_or_else(|| panic!("{name} registered")); assert_eq!( b.compat, diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index ae8e640b0c..b1b39d6359 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -199,15 +199,6 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { // commander: feature-gated v0.5.555 — well-known flip routes // to perry-ext-commander. "commander" => &["bundled-commander"], - // dotenv was always-on through v0.5.532; gated behind - // `bundled-dotenv` from v0.5.533 onwards so the well-known - // bindings flip (#466 Phase 4 step 2) can swap perry-stdlib's - // copy out for `perry-ext-dotenv` without duplicate - // `_js_dotenv_*` symbols at link time. The well-known path - // strips this feature from the set; the default path leaves - // it on so byte-identical behavior is preserved. - "dotenv" | "dotenv/config" => &["bundled-dotenv"], - // readline (#347) — needs the async-runtime feature so the // event-loop pump tick drains its line / data / keypress // queues. Without async-runtime, `import readline` still diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index 0b537d3ae1..4721c63bc0 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -36,31 +36,6 @@ # requires every ext crate and package mapping to have an explicit decision # (#5716). -[bindings.dotenv] -crate = "perry-ext-dotenv" -# Library file name without the `lib` prefix or `.a` extension. -# Cargo derives this from the crate name by replacing `-` with `_`, -# but stating it explicitly here is documentation for humans -# inspecting the table. -lib = "perry_ext_dotenv" -# Tracking issue for the migration; surfaced in error messages -# when the bundled .a is missing at link time. -tracking = "#466" -# `compat` — how faithful this wrapper is to the npm package's public -# API (see `BindingCompat` in well_known.rs). `full` = audited complete -# drop-in, safe to auto-prefer over an on-disk node_modules copy. -# ABSENT ⇒ conservative `partial` default. dotenv remains partial: this -# wrapper has `config()` / `parse()`, but not the upstream decrypt/populate/ -# configDotenv surface and option/error semantics. -compat = "partial" - -[bindings.dotenv.upstream] -version = "17.4.2" -sha256 = "8648852be8209110b34dca75dcc3ed12ce7fae9fcc8edd1ef9e180e708af1398" -repo = "https://github.com/motdotla/dotenv" -ref = "a61f616a3160bb6e6f22ff55f08b7eba3a3fab68" -ported-at = "17.4.2" -date = "2026-07-30" [bindings.nanoid] crate = "perry-ext-nanoid" lib = "perry_ext_nanoid" diff --git a/docs/api/perry.d.ts b/docs/api/perry.d.ts index 6497a7386b..47a24f494b 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: 2067 entries across 132 modules +// Coverage: 2065 entries across 131 modules type PerryI8 = number & { readonly __perryI8?: never }; type PerryI16 = number & { readonly __perryI16?: never }; @@ -1525,13 +1525,6 @@ declare module "domain" { export function createDomain(...args: any[]): any; } -declare module "dotenv" { - /** stdlib */ - export function config(...args: any[]): any; - /** stdlib */ - export function parse(src: string): any; -} - declare module "ethers" { /** stdlib */ export function formatEther(p0: any): string; diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index 458ee6d20a..c0dd3c2f51 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: 3009 entries across 134 modules. +Total: 3007 entries across 133 modules. ## Modules @@ -47,7 +47,6 @@ Total: 3009 entries across 134 modules. - [`dns`](#dns) - [`dns/promises`](#dnspromises) - [`domain`](#domain) -- [`dotenv`](#dotenv) - [`ethers`](#ethers) - [`events`](#events) - [`exponential-backoff`](#exponential-backoff) @@ -1329,13 +1328,6 @@ Total: 3009 entries across 134 modules. - `active` - `members` -## `dotenv` - -### Methods - -- `config` — module -- `parse` — module - ## `ethers` ### Methods diff --git a/docs/src/native-libraries/governance.md b/docs/src/native-libraries/governance.md index bc06dda3f5..3939cc0e72 100644 --- a/docs/src/native-libraries/governance.md +++ b/docs/src/native-libraries/governance.md @@ -93,7 +93,6 @@ from `well_known_bindings.toml`. Regenerate this table with | `perry-ext-cron` | `cron`
`node-cron` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-dayjs` | `date-fns`
`dayjs` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-decimal` | `bignumber.js`
`decimal.js` | Source package | Compile the upstream package source | Bundled; migration pending | -| `perry-ext-dotenv` | `dotenv` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-ethers` | `ethers` | Source package | Compile the upstream package source | Bundled; migration pending | | `perry-ext-events` | `events` | Runtime API | Keep near core; consolidate when practical | Bundled; retained | | `perry-ext-exponential-backoff` | `exponential-backoff` | Source package | Compile the upstream package source | Bundled; migration pending | diff --git a/tests/release/packages/next-app-route/provider/stdlib/Cargo.toml b/tests/release/packages/next-app-route/provider/stdlib/Cargo.toml index 1523a800c4..89baefca64 100644 --- a/tests/release/packages/next-app-route/provider/stdlib/Cargo.toml +++ b/tests/release/packages/next-app-route/provider/stdlib/Cargo.toml @@ -18,7 +18,6 @@ perry-stdlib-core = { package = "perry-stdlib", path = "../../../../../../crates "ids", "html-parser", "rate-limit", - "bundled-dotenv", "bundled-lru-cache", "bundled-exponential-backoff", "bundled-decimal", diff --git a/workspace-architecture.json b/workspace-architecture.json index 240f5ba834..85139f6cfb 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -25,7 +25,7 @@ ] }, "baseline": { - "workspace_members": 78, + "workspace_members": 77, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -68,7 +68,7 @@ "perry-updater" ], "decision_counts": { - "externalize": 29, + "externalize": 28, "keep": 44, "merge": 1, "remove": 1, @@ -185,11 +185,6 @@ "decision": "externalize", "migration": "compile-source" }, - "perry-ext-dotenv": { - "category": "binding", - "decision": "externalize", - "migration": "compile-source" - }, "perry-ext-ethers": { "category": "binding", "decision": "externalize",