Repository navigation
Re-exporting a Node builtin's named export through a local module is broken: export { createHash } from "crypto" fails to link, import { createHash } from "crypto"; export { createHash } yields a function that returns undefined #10432
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A named export of a Node builtin (crypto, fs, path, ...) that is re-exported from a user/package module is unusable in the importer: the export { x } from "<builtin>" form produces an undefined perry_fn_<module>__<x> symbol at link time, and the import { x } from "<builtin>"; export { x } form links but the imported value returns undefined when called.
Reproduction
Case 1 (link error):
mkdir case1 && cd case1
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
export { createHash } from "crypto";
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./outCase 2 (links, wrong value):
mkdir case2 && cd case2
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
import { createHash } from "crypto";
export { createHash };
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(typeof createHash);
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./outExpected (Node 26.5.1)
Case 1:
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
Case 2:
function
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
Actual (Perry)
Case 1 (object-file path shortened, strip-dedup lines omitted):
/usr/bin/ld: node_modules/.cache/perry/objects/host/<hash>.o: in function `main':
perry_module:(.text+0x4a): undefined reference to `perry_fn_dep_ts__createHash'
collect2: error: ld returned 1 exit status
Error: Linking failed
Case 2:
function
TypeError: Cannot read properties of undefined (reading 'update')
at <anonymous>
Same result with the default build and with PERRY_NO_AUTO_OPTIMIZE=1.
Impact
- ethers 6.17.0:
src.ts/crypto/crypto.ts:1-3isexport { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";-> link fails withundefined reference to perry_fn_..._crypto_crypto_ts__createHmac(andpbkdf2Sync,randomBytes,createHash). - jimp 1.6.1:
@jimp/file-ops/dist/esm/index.js:2isexport { existsSync } from "fs";(imported by@jimp/core/dist/esm/index.js:4) ->undefined reference to perry_fn_node_modules__jimp_file_ops_dist_esm_index_js__existsSync. - Any barrel/facade module that forwards builtin functions (a common pattern for isomorphic packages that swap
crypto/fsper platform).
Notes
Variant matrix (all with a local dep module and main.ts importing from it; Node prints the correct value for every row):
| dep module | Perry |
|---|---|
export { createHash } from "crypto" (.ts or .js) |
link error perry_fn_dep_ts__createHash |
export { existsSync } from "fs" (.js) |
link error perry_fn_dep_js__existsSync |
export { join } from "node:path" |
link error perry_fn_dep_ts__join |
export * from "node:crypto" |
link error __perry_wrap_perry_fn_dep_ts__createHash / perry_fn_dep_ts__createHash |
case-1 dep, but importer uses import * as dep from "./dep.ts" |
links; typeof dep.createHash is undefined |
import { existsSync } from "fs"; export { existsSync } |
links; typeof is function, call returns undefined |
import { join } from "path"; export { join } |
links; typeof is function, call returns undefined |
import * as c from "crypto"; export const createHash = c.createHash |
works |
import { createHash as h } from "crypto"; export function createHash(a) { return h(a); } |
works |
export { hello } from "./impl.ts" (user module, not a builtin) |
works |
builtin used as a value inside one module (const h = createHash; h("sha256")) |
works |
Suspected locations (inferred, not verified):
- Case 1: the driver's re-export propagation only records an origin when the re-export source resolves to a compiled module (
crates/perry/src/commands/compile/run_pipeline.rs:1913-1950,resolve_import_with_context(...)returns nothing for a builtin). The importer then falls back to "the name is a function defined in the immediate module" (run_pipeline.rs:4425-4469: when no origin is known,import_function_prefixesgets the immediate module's prefix), so codegen referencesperry_fn_dep_ts__createHash, which nothing defines. HIR records the specifier asExport::ReExport(crates/perry-hir/src/lower/module_decl.rs:1491) with no builtin-specific handling. - Case 2: the local builtin import is exported as a plain
Export::Named(crates/perry-hir/src/lower/module_decl.rs:1562); the exported slot does not hold the callable builtin value.
The same "unresolved named import becomes a perry_fn_ reference" fallback produces #10433.
Activity
proggeramlug commentedon Sep 22, 2026
Fixed on main by PR #10867, which landed with both
Closes #10802andCloses #10432in its body.Closing manually because merge trains close source PRs without firing close keywords, so neither reference took effect. Verified by content rather than PR state:
crates/perry-hir/tests/node_named_export_hygiene.rsand the named-re-export handling inmodule_decl.rsare both present on main.This issue documented the defect more completely than #10802 did — it covered both forms, the
export { x } from "<builtin>"link failure and theimport { x } from "<builtin>"; export { x }form that links but returnsundefinedwhen called. Worth noting for anyone auditing the fix that the second form is the easier one to regress silently, since it produces no link error.