diff --git a/changelog.d/8817-compiled-package-builtin-import.md b/changelog.d/8817-compiled-package-builtin-import.md new file mode 100644 index 0000000000..b62df17cf3 --- /dev/null +++ b/changelog.d/8817-compiled-package-builtin-import.md @@ -0,0 +1,6 @@ +Added regression coverage for Node builtin named imports used from natively +compiled dependencies. The exact `@hono/node-server` fallback from +`options.createServer` to its module-scope `http.createServer` import now has an +offline compiler fixture and a real-package listen/fetch/close release smoke, +covering both `http` and `node:http` spellings without relying on app-level +imports. diff --git a/crates/perry/tests/issue_8749_compiled_package_builtin_import.rs b/crates/perry/tests/issue_8749_compiled_package_builtin_import.rs new file mode 100644 index 0000000000..c3125b7627 --- /dev/null +++ b/crates/perry/tests/issue_8749_compiled_package_builtin_import.rs @@ -0,0 +1,114 @@ +//! Regression test for #8749: a Node builtin named import used from a +//! `compilePackages` dependency must retain its runtime binding. +//! +//! `@hono/node-server` imports `createServer` from `http`, selects it through a +//! module-scope fallback (`options.createServer || createServerHTTP`), and calls +//! the selected function later from `serve()`. App-level imports already +//! worked; the binding was lost specifically while compiling the dependency. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +#[test] +fn builtin_named_import_survives_module_scope_fallback_in_compiled_package() { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + + std::fs::write( + root.join("package.json"), + r#"{ + "name": "compiled-builtin-import-consumer", + "private": true, + "type": "module", + "perry": { + "compilePackages": ["fake-node-server"], + "allow": { "compilePackages": ["fake-node-server"] } + } +}"#, + ) + .expect("write consumer package.json"); + + let pkg = root.join("node_modules").join("fake-node-server"); + std::fs::create_dir_all(&pkg).expect("mkdir fake-node-server"); + std::fs::write( + pkg.join("package.json"), + r#"{ + "name": "fake-node-server", + "version": "1.0.0", + "type": "module", + "exports": "./index.mjs" +}"#, + ) + .expect("write dependency package.json"); + std::fs::write( + pkg.join("index.mjs"), + r#" +import { createServer as createServerHTTP } from "http"; +import { createServer as createServerNodeHTTP } from "node:http"; + +const options = {}; +const selectedHTTP = options.createServer || createServerHTTP; +const selectedNodeHTTP = options.createServer || createServerNodeHTTP; + +export function inspectBindings() { + const serverHTTP = selectedHTTP({}, () => {}); + const serverNodeHTTP = selectedNodeHTTP({}, () => {}); + return [ + typeof createServerHTTP, + typeof createServerNodeHTTP, + typeof selectedHTTP, + typeof selectedNodeHTTP, + typeof serverHTTP.listen, + typeof serverNodeHTTP.listen, + ].join(","); +} +"#, + ) + .expect("write compiled dependency"); + + let entry = root.join("main.ts"); + std::fs::write( + &entry, + r#" +import { inspectBindings } from "fake-node-server"; +console.log(inspectBindings()); +process.exit(0); +"#, + ) + .expect("write entry"); + + let output = root.join("main_bin"); + let compile = Command::new(perry_bin()) + .current_dir(root) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output).output().expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "function,function,function,function,function,function\n", + "both builtin spellings must stay bound through the dependency's module global" + ); +} diff --git a/tests/release/packages/hono-node-server/entry.ts b/tests/release/packages/hono-node-server/entry.ts new file mode 100644 index 0000000000..c11cf339e7 --- /dev/null +++ b/tests/release/packages/hono-node-server/entry.ts @@ -0,0 +1,16 @@ +// Issue #8749: @hono/node-server selects its imported node:http factory +// through `options.createServer || createServerHTTP` inside compiled package +// code. Exercise the real package through listen, fetch, response, and close. +import { serve } from "@hono/node-server"; + +const port = 38139; +const server = serve({ + fetch: () => new Response("ok"), + port, +}, async () => { + const response = await fetch(`http://127.0.0.1:${port}/`); + console.log(`status=${response.status}`); + console.log(`body=${await response.text()}`); + server.close(); + process.exit(0); +}); diff --git a/tests/release/packages/hono-node-server/expected.txt b/tests/release/packages/hono-node-server/expected.txt new file mode 100644 index 0000000000..e03d31153e --- /dev/null +++ b/tests/release/packages/hono-node-server/expected.txt @@ -0,0 +1,2 @@ +status=200 +body=ok diff --git a/tests/release/packages/hono-node-server/fixture.sh b/tests/release/packages/hono-node-server/fixture.sh new file mode 100755 index 0000000000..5e49ae2828 --- /dev/null +++ b/tests/release/packages/hono-node-server/fixture.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Issue #8749: real-package smoke for @hono/node-server's module-scope +# `options.createServer || createServerHTTP` binding under compilePackages. + +set -uo pipefail +cd "$(dirname "$0")" +. "$(dirname "$0")/../_fixture_lib.sh" + +fixture_setup "hono-node-server" || exit 1 +fixture_compile_run_diff "hono-node-server" diff --git a/tests/release/packages/hono-node-server/package-lock.json b/tests/release/packages/hono-node-server/package-lock.json new file mode 100644 index 0000000000..bd5dbd0074 --- /dev/null +++ b/tests/release/packages/hono-node-server/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "perry-release-fixture-hono-node-server", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "perry-release-fixture-hono-node-server", + "version": "0.0.0", + "dependencies": { + "@hono/node-server": "1.19.17", + "hono": "4.13.4" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/hono": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.4.tgz", + "integrity": "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + } + } +} diff --git a/tests/release/packages/hono-node-server/package.json b/tests/release/packages/hono-node-server/package.json new file mode 100644 index 0000000000..ed2aa1437d --- /dev/null +++ b/tests/release/packages/hono-node-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "perry-release-fixture-hono-node-server", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Tier-3 fixture for @hono/node-server's compiled-package Node builtin imports.", + "dependencies": { + "@hono/node-server": "1.19.17", + "hono": "4.13.4" + }, + "perry": { + "compilePackages": ["@hono/node-server", "hono"], + "allow": { + "compilePackages": ["@hono/node-server", "hono"] + } + } +}