Files
Sam PoderandGitHub e90189b940 [turbopack] Rename turbopackTreeShaking to turbopackModuleFragments (#95978)
This has been causing a lot of confusion when contributors have been
debugging tree shaking, eg.
https://github.com/vercel/next.js/issues/95698 and the linked issues.
Turbopack's standard tree-shaking features are controlled through the
`turbopackRemoveUnusedExports` and `turbopackRemoveUnusedImports`
options which default to true.
2026-07-20 18:14:11 -07:00

812 lines
32 KiB
Rust

#![allow(clippy::items_after_test_module)]
#![feature(arbitrary_self_types)]
#![feature(arbitrary_self_types_pointers)]
mod helpers;
use std::{
collections::HashSet,
env::temp_dir,
fmt::Display,
fs::{
remove_dir_all, {self},
},
io::{ErrorKind, Write as _},
path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex},
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use difference::Changeset;
use helpers::print_changeset;
use regex::Regex;
use rstest::*;
use rstest_reuse::{
*, {self},
};
use serde::Serialize;
use tokio::{process::Command, time::timeout};
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{
Effects, ResolvedVc, TurboTasks, ValueToString, Vc, backend::Backend,
read_strongly_consistent_and_apply_effects, take_effects, trace::TraceRawVcs,
};
use turbo_tasks_backend::TurboTasksBackend;
use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath};
use turbopack::{
ModuleAssetContext, emit_assets_into_dir_operation,
module_options::{
CssOptionsContext, EcmascriptOptionsContext, ModuleOptionsContext,
TypescriptTransformOptions,
},
};
use turbopack_core::{
chunk::SourceMapsType,
compile_time_info::CompileTimeInfo,
context::AssetContext,
environment::{Environment, ExecutionEnvironment, NodeJsEnvironment},
file_source::FileSource,
ident::Layer,
output::{OutputAsset, OutputAssetsReference},
rebase::RebasedAsset,
reference::all_assets_from_entry,
reference_type::ReferenceType,
resolve::options::ConditionValue,
};
use turbopack_ecmascript::AnalyzeMode;
use turbopack_resolve::resolve_options_context::ResolveOptionsContext;
#[global_allocator]
static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc;
#[template]
#[rstest]
#[case::analytics_node("integration/analytics-node.js")]
#[case::array_map_require("integration/array-map-require/index.js")]
#[case::apollo("integration/apollo.js")]
#[case::argon2("integration/argon2.js")]
#[case::auth0("integration/auth0.js")]
#[case::aws_sdk_old("integration/aws-sdk-old.js")]
#[case::aws_sdk("integration/aws-sdk.js")]
#[case::axios("integration/axios.js")]
#[case::azure_cosmos("integration/azure-cosmos.js")]
#[case::azure_storage("integration/azure-storage.js")]
#[case::bcrypt("integration/bcrypt.js")]
#[case::better_sqlite3("integration/better-sqlite3.js")]
#[cfg_attr(
not(feature = "bench_against_node_nft"),
should_panic(expected = "Error: Could not locate the bindings file."),
case::bindings_failure("integration/bindings-failure.js")
)]
#[case::browserify_middleware("integration/browserify-middleware.js")]
#[case::bugsnag_js("integration/bugsnag-js.js")]
#[case::bull("integration/bull.js")]
#[case::bull_mq("integration/bullmq.js")]
#[case::camaro("integration/camaro.js")]
#[case::canvas("integration/canvas.js")]
#[case::chromeless("integration/chromeless.js")]
#[case::core_js("integration/core-js.js")]
#[case::cosmosdb_query("integration/cosmosdb-query.js")]
#[case::cowsay("integration/cowsay.js")]
#[cfg_attr(
not(feature = "bench_against_node_nft"),
should_panic(expected = "Error: Cannot find module '../../out/node-file-trace'"),
case::dogfood("integration/dogfood.js")
)]
#[case::datadog_pprof("integration/datadog-pprof.js")]
#[case::dynamic_in_package("integration/dynamic-in-package.js")]
#[case::empty("integration/empty.js")]
#[case::env_var("integration/env-var.js")]
#[case::es_get_iterator("integration/es-get-iterator.js")]
// This is flakey on Windows. Disable for now.
#[cfg_attr(not(target_os = "windows"), case::esbuild("integration/esbuild.js"))]
#[case::esm("integration/esm.js")]
#[case::express_consolidate("integration/express-consolidate.js")]
#[case::express_template_engine("integration/express-template-engine.js")]
#[case::express_template_pug("integration/express-template.js")]
#[case::express("integration/express.js")]
#[case::fast_glob("integration/fast-glob.js")]
#[case::fetch_h2("integration/fetch-h2.js")]
// Could not find ffmpeg executable
#[cfg_attr(target_arch = "x86_64", case::ffmpeg_js("integration/ffmpeg.js"))]
#[case::ffmpeg_static("integration/ffmpeg-static.js")]
#[case::firebase_admin("integration/firebase-admin.js")]
#[case::firebase("integration/firebase.js")]
#[case::firestore("integration/firestore.js")]
#[case::fluent_ffmpeg("integration/fluent-ffmpeg.js")]
#[case::geo_tz("integration/geo-tz.js")]
#[case::geoip_lite("integration/geoip-lite.js")]
#[case::google_bigquery("integration/google-bigquery.js")]
#[case::got("integration/got.js")]
#[case::highlights("integration/highlights.js")]
#[case::hot_shots("integration/hot-shots.js")]
#[case::ioredis("integration/ioredis.js")]
#[case::isomorphic_unfetch("integration/isomorphic-unfetch.js")]
#[case::jimp("integration/jimp.js")]
#[case::jugglingdb("integration/jugglingdb.js")]
#[case::koa("integration/koa.js")]
#[case::leveldown("integration/leveldown.js")]
#[case::lighthouse("integration/lighthouse.js")]
#[case::loopback("integration/loopback.js")]
#[case::mailgun("integration/mailgun.js")]
#[case::mariadb("integration/mariadb.js")]
#[case::memcached("integration/memcached.js")]
#[cfg_attr(
not(feature = "bench_against_node_nft"),
should_panic(expected = "Error [ERR_MODULE_NOT_FOUND]: Cannot find module"),
case::mdx("integration/mdx/index.cjs")
)]
#[case::mongoose("integration/mongoose.js")]
#[case::mysql("integration/mysql.js")]
#[case::npm("integration/npm.js")]
// unable to resolve esm request module 'spdx-license-ids' in
// node-file-trace/node_modules/npm/node_modules/spdx-correct oracledb doesn't support non x86
// architectures
#[cfg_attr(target_arch = "x86_64", case::oracledb("integration/oracledb.js"))]
#[case::otel_api("integration/otel-api.js")]
#[case::paraphrase("integration/paraphrase.js")]
#[case::passport_trakt("integration/passport-trakt.js")]
#[case::passport("integration/passport.js")]
#[case::path_platform("integration/path-platform.js")]
#[case::pixelmatch("integration/pixelmatch.js")]
#[case::pdf2json("integration/pdf2json.js")]
#[case::pdfkit("integration/pdfkit.js")]
#[case::pg("integration/pg.js")]
#[case::pino("integration/pino.js")]
#[case::playwright_core("integration/playwright-core.js")]
#[case::pnpm_like("integration/pnpm/pnpm-like.js")]
#[case::polyfill_library("integration/polyfill-library.js")]
#[case::pug("integration/pug.js")]
#[case::react("integration/react.js")]
#[case::redis("integration/redis.js")]
#[case::remark_prism("integration/remark-prism.mjs")]
#[case::request("integration/request.js")]
#[case::rxjs("integration/rxjs.js")]
#[case::saslprep("integration/saslprep.js")]
#[case::semver("integration/semver.js")]
#[case::sentry("integration/sentry.js")]
#[case::sequelize("integration/sequelize.js")]
#[case::serialport("integration/serialport.js")]
#[cfg_attr(
target_os = "windows",
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
case::sharp030("integration/sharp030.js")
)]
#[cfg_attr(not(target_os = "windows"), case::sharp030("integration/sharp030.js"))]
#[cfg_attr(
target_os = "windows",
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
case::sharp033("integration/sharp033.js")
)]
#[cfg_attr(not(target_os = "windows"), case::sharp033("integration/sharp033.js"))]
#[cfg_attr(
target_os = "windows",
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
case::sharp034("integration/sharp034.js")
)]
#[cfg_attr(not(target_os = "windows"), case::sharp034("integration/sharp034.js"))]
#[cfg_attr(
target_os = "windows",
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
case::sharp("integration/sharp.js")
)]
#[cfg_attr(not(target_os = "windows"), case::sharp("integration/sharp.js"))]
#[cfg_attr(
target_os = "windows",
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
case::sharp_pnpm("integration/sharp-pnpm.js")
)]
#[cfg_attr(
not(target_os = "windows"),
case::sharp_pnpm("integration/sharp-pnpm.js")
)]
#[case::shiki("integration/shiki.js")]
#[case::simple("integration/simple.js")]
#[case::socket_io("integration/socket.io.js")]
#[case::source_map("integration/source-map/index.js")]
#[case::sparql_builder("integration/sparql-builder.js")]
#[case::sqlite("integration/sqlite.js")]
#[case::stripe("integration/stripe.js")]
#[case::strong_error_handler("integration/strong-error-handler.js")]
#[case::symlink_to_file("integration/symlink-to-file/index.js")]
#[case::tiny_json_http("integration/tiny-json-http.js")]
#[case::twilio("integration/twilio.js")]
#[case::ts_morph("integration/ts-morph.js")]
#[case::typescript("integration/typescript.js")]
#[case::uglify("integration/uglify.js")]
#[case::underscore("integration/underscore.js")]
#[case::vm2("integration/vm2.js")]
#[case::vue("integration/vue.js")]
#[case::webpack_target_node("integration/webpack-target-node/index.js")]
#[case::whatwg_url("integration/whatwg-url.js")]
#[case::when("integration/when.js")]
// TODO PACK-4987
// #[case::zeromq("integration/zeromq.js")]
#[case::package_exports_alt_folders_base(
CaseInput::new("integration/package-exports/pass/alt-folders.js").expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
)]
#[case::package_exports_folder(
CaseInput::new("integration/package-exports/pass/folder.js").expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
)]
#[case::package_exports_alt_base("integration/package-exports/pass/alt.js")]
#[case::package_exports_catch_all("integration/package-exports/pass/catch-all.js")]
#[case::package_exports_direct("integration/package-exports/pass/direct.js")]
#[case::package_exports_double("integration/package-exports/pass/double.js")]
#[case::package_exports_nested("integration/package-exports/pass/nested.js")]
#[case::package_exports_package_root("integration/package-exports/pass/root.js")]
#[case::package_exports_package_single_export_root(
"integration/package-exports/pass/single-export-root.js"
)]
#[case::package_exports_package_sub_infix_sep("integration/package-exports/pass/sub-infix-sep.js")]
#[case::package_exports_package_sub_infix_base("integration/package-exports/pass/sub-infix.js")]
#[case::package_exports_package_sub_prefix_sep(
"integration/package-exports/pass/sub-prefix-sep.js"
)]
#[case::package_exports_package_sub_prefix("integration/package-exports/pass/sub-prefix.js")]
#[case::package_exports_package_sub_suffix_sep(
"integration/package-exports/pass/sub-suffix-sep.js"
)]
#[case::package_exports_package_sub_suffix_base("integration/package-exports/pass/sub-suffix.js")]
#[case::package_exports_alt_folders_multiple(
CaseInput::new("integration/package-exports/fail/alt-folders-multiple.js")
.expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
)]
#[case::package_exports_alt_multiple(
CaseInput::new("integration/package-exports/fail/alt-multiple.js")
.expected_stderr("Error [ERR_MODULE_NOT_FOUND]: Cannot find module")
)]
#[case::read_file("integration/read-file.mjs")]
#[cfg_attr(
not(feature = "bench_against_node_nft"),
//[TODO]: WEB-1188 reenable once fixed.
//case::ts_package_base("integration/ts-package/index.ts"),
//case::ts_package_extends("integration/ts-package-extends/index.ts"),
//case::ts_package_from_js("integration/ts-package-from-js/index.js"),
case::ts_paths_alt_base("integration/ts-paths/pass/alt.ts"),
case::ts_paths_catch_all("integration/ts-paths/pass/catch-all.ts"),
case::ts_paths_direct("integration/ts-paths/pass/direct.ts"),
case::ts_paths_nested("integration/ts-paths/pass/nested.ts"),
case::ts_paths_package_sub_prefix("integration/ts-paths/pass/sub-prefix.ts"),
case::ts_paths_package_sub_suffix_sep_base("integration/ts-paths/pass/sub-suffix-sep.ts"),
case::ts_paths_package_sub_suffix_base("integration/ts-paths/pass/sub-suffix.ts"),
case::ts_paths_alt_folders(
CaseInput::new("integration/ts-paths/fail/alt-folders.ts")
.expected_stderr("Cannot find module 'fixtures/alt-folders/alt1.js'")
),
case::ts_paths_double(
CaseInput::new("integration/ts-paths/fail/double.ts")
.expected_stderr("Cannot find module 'double/sub' or its corresponding type declarations.")
),
case::ts_paths_folder(
CaseInput::new("integration/ts-paths/fail/folder.ts")
.expected_stderr("Cannot find module 'folder/alt1' or its corresponding type declarations.")
),
// TODO(alexkirsz) I expect the two following infix tests are only failing when using `tsconfig-paths`, since
// VSCode's TS language server can resolve them properly. We should pre-compile the TS files to JS and run Node.js
// on them directly instead.
case::ts_paths_package_sub_infix_sep(
CaseInput::new("integration/ts-paths/fail/sub-infix-sep.ts")
.expected_stderr("Cannot find module '@/sub/@'")
),
case::ts_paths_package_sub_infix_base(
CaseInput::new("integration/ts-paths/fail/sub-infix.ts")
.expected_stderr("Cannot find module '@sub@'")
),
case::ts_paths_sub_prefix_sep(
CaseInput::new("integration/ts-paths/fail/sub-prefix-sep.ts")
.expected_stderr("Cannot find module 'sub/@' or its corresponding type declarations")
),
)]
#[cfg_attr(
//[TODO]: WEB-1188 reenable windows once fixed.
not(any(feature = "bench_against_node_nft", target_os = "windows")),
case::ts_package_base("integration/ts-package/index.ts"),
case::ts_package_extends("integration/ts-package-extends/index.ts"),
case::ts_package_from_js("integration/ts-package-from-js/index.js"),
)]
fn test_cases() {}
#[apply(test_cases)]
fn node_file_trace_noop_backing_storage(#[case] input: CaseInput) {
node_file_trace(input, "noop_backing_storage", 1, 120, |_| {
TurboTasks::new(TurboTasksBackend::new(
turbo_tasks_backend::BackendOptions {
storage_mode: None,
dependency_tracking: false,
..Default::default()
},
turbo_tasks_backend::noop_backing_storage(),
))
});
}
#[apply(test_cases)]
fn node_file_trace_persistent(#[case] input: CaseInput) {
node_file_trace(input, "persistent_cache", 2, 240, |directory_path| {
TurboTasks::new(TurboTasksBackend::new(
turbo_tasks_backend::BackendOptions::default(),
turbo_tasks_backend::turbo_backing_storage(
&directory_path.join(".cache"),
&turbo_tasks_backend::GitVersionInfo {
describe: "test-unversioned",
dirty: false,
},
false,
true,
false,
)
.unwrap()
.0,
))
});
}
#[cfg(feature = "bench_against_node_nft")]
#[apply(test_cases)]
fn bench_against_node_nft(#[case] input: CaseInput) {
bench_against_node_nft_inner(input);
}
#[cfg(feature = "bench_against_node_nft")]
fn bench_against_node_nft_inner(input: CaseInput) {
node_file_trace(input, "noop_backing_storage", 1, 120, |_| {
TurboTasks::new(TurboTasksBackend::new(
turbo_tasks_backend::BackendOptions::default(),
turbo_tasks_backend::noop_backing_storage(),
))
});
}
#[turbo_tasks::value(serialization = "skip", evict = "never")]
struct NodeFileTraceResult {
rebased: ResolvedVc<RebasedAsset>,
effects: Effects,
}
#[turbo_tasks::function(operation, root)]
async fn node_file_trace_operation(
package_root: RcStr,
input: RcStr,
directory: RcStr,
) -> Result<Vc<NodeFileTraceResult>> {
let workspace_fs: Vc<Box<dyn FileSystem>> = Vc::upcast(DiskFileSystem::new(
rcstr!("workspace"),
Vc::cell(package_root.clone()),
));
let input_dir = workspace_fs.root().owned().await?;
let input = input_dir.join(&format!("tests/{input}"))?;
let output_fs = DiskFileSystem::new(rcstr!("output"), Vc::cell(directory.clone()));
let output_dir = output_fs.root().owned().await?;
let source = FileSource::new(input);
let environment = Environment::new(ExecutionEnvironment::NodeJsLambda(
NodeJsEnvironment {
cwd: ResolvedVc::cell(Some(input_dir.join("tests/node-file-trace/integration")?)),
..Default::default()
}
.resolved_cell(),
));
let module_asset_context = ModuleAssetContext::new_without_replace_externals(
Default::default(),
// TODO These test cases should move into the `node-file-trace` crate and use the same
// config.
// This config should be kept in sync with
// turbopack/crates/turbopack-tracing/tests/node-file-trace.rs and
// turbopack/crates/turbopack-tracing/tests/unit.rs and
// turbopack/crates/turbopack/src/lib.rs and
// turbopack/crates/turbopack-nft/src/nft.rs
CompileTimeInfo::new(environment),
ModuleOptionsContext {
ecmascript: EcmascriptOptionsContext {
enable_typescript_transform: Some(
TypescriptTransformOptions::default().resolved_cell(),
),
// enable_types is required here to ensure .d.ts files are collected.
enable_types: true,
..Default::default()
},
css: CssOptionsContext {
source_maps: SourceMapsType::None,
enable_raw_css: true,
..Default::default()
},
// Environment is not passed in order to avoid downleveling JS / CSS for
// node-file-trace.
environment: None,
analyze_mode: AnalyzeMode::Tracing,
// Disable tree shaking. Even side-effect-free imports need to be traced, as they will
// execute at runtime.
follow_reexports: false,
module_fragments_enabled: false,
..Default::default()
}
.cell(),
ResolveOptionsContext {
enable_node_native_modules: true,
enable_node_modules: Some(input_dir.clone()),
custom_conditions: vec![rcstr!("node")],
module_sync: ConditionValue::Unknown,
collect_affecting_sources: true,
..Default::default()
}
.cell(),
Layer::new(rcstr!("test")),
);
let module = module_asset_context
.process(Vc::upcast(source), ReferenceType::Undefined)
.module();
let rebased = RebasedAsset::new(module, input_dir.clone(), output_dir.clone())
.to_resolved()
.await?;
let assets = all_assets_from_entry(Vc::upcast(*rebased))
.to_resolved()
.await?;
let emit_op = emit_assets_into_dir_operation(assets, output_dir.clone());
emit_op.read_strongly_consistent().await?;
let effects = take_effects(emit_op).await?;
Ok(NodeFileTraceResult { rebased, effects }.cell())
}
fn node_file_trace<B: Backend + 'static>(
CaseInput {
path: input_path,
#[allow(unused)]
expected_stderr,
}: CaseInput,
mode: &str,
run_count: i32,
timeout_len: u64,
create_turbo_tasks: impl Fn(&Path) -> Arc<TurboTasks<B>>,
) {
static BENCH_SUITES: LazyLock<Arc<Mutex<Vec<BenchSuite>>>> =
LazyLock::new(|| Arc::new(Mutex::new(Vec::new())));
let r = &mut {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
builder.max_blocking_threads(20);
builder.build().unwrap()
};
r.block_on(async move {
let bench_suites = BENCH_SUITES.clone();
let package_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut tests_output_root = temp_dir();
tests_output_root.push("tests_output");
let package_root: RcStr = package_root.to_string_lossy().into();
let input: RcStr = format!("node-file-trace/{input_path}").into();
let directory_path = tests_output_root.join(format!("{mode}_{input}"));
let directory: RcStr = directory_path.to_string_lossy().into();
remove_dir_all(&directory)
.or_else(|err| {
if err.kind() == ErrorKind::NotFound {
Ok(())
} else {
Err(err)
}
})
.with_context(|| format!("Failed to remove directory: {directory}"))
.unwrap();
for _ in 0..run_count {
let bench_suites = bench_suites.clone();
let expected_stderr = expected_stderr.clone();
let package_root = package_root.clone();
let input = input.clone();
let directory = directory.clone();
let task = async move {
let before_start = Instant::now();
let trace_op = node_file_trace_operation(
package_root.clone(),
input.clone(),
directory.clone(),
);
let trace_result =
read_strongly_consistent_and_apply_effects(trace_op, |v| &v.effects).await?;
let rebased = trace_result.rebased;
let duration = before_start.elapsed();
print_graph_operation(ResolvedVc::upcast(rebased))
.read_strongly_consistent()
.await?;
if cfg!(feature = "bench_against_node_nft") {
let node_start = Instant::now();
exec_node(&package_root, &input).await?;
let node_duration = node_start.elapsed();
let is_faster = node_duration > duration;
{
let mut bench_suites_lock = bench_suites.lock().unwrap();
let rust_speedup =
node_duration.as_millis() as f32 / duration.as_millis() as f32 - 1.0;
let rust_duration = format!("{duration:?}");
let node_duration = format!("{node_duration:?}");
let rust_speedup = if rust_speedup > 1.0 {
format!("+{rust_speedup:.2}x")
} else if rust_speedup > 0.0 {
format!("+{:.0}%", rust_speedup * 100.0)
} else {
format!("-{:.0}%", -rust_speedup * 100.0)
};
bench_suites_lock.push(BenchSuite {
suite: input
.trim_start_matches("node-file-trace/integration/")
.to_string(),
is_faster,
rust_duration,
node_duration,
rust_speedup,
});
}
Ok(CommandOutput {
stdout: String::new(),
stderr: String::new(),
})
} else {
let output_path = &asset_path_operation(ResolvedVc::upcast(rebased))
.read_strongly_consistent()
.await?
.path;
let original_output =
exec_node(&package_root, &format!("{package_root}/tests/{input}")).await?;
let output = exec_node(&directory, output_path).await?;
assert_output(original_output, output, expected_stderr.map(From::from))
}
};
fn handle_result(result: Result<CommandOutput>) {
match result {
Ok(output) => {
if !cfg!(feature = "bench_against_node_nft") {
assert!(
output.is_empty(),
"emitted files behave differently when executed via \
node.js\n{output}"
);
}
}
Err(err) => {
panic!("Execution failed: {err:?}");
}
}
}
let tt = create_turbo_tasks(directory_path.as_path());
let output = timeout(Duration::from_secs(timeout_len), tt.run_once(task)).await;
let _ = timeout(Duration::from_secs(2), tt.wait_background_done()).await;
let stop = timeout(Duration::from_secs(60), tt.stop_and_wait()).await;
match (output, stop) {
(Ok(result), Ok(_)) => handle_result(result),
(Err(err), _) => {
panic!("Execution is hanging (for > {timeout_len}s): {err}");
}
(_, Err(err)) => {
panic!("Stopping is hanging (for > 60s): {err}");
}
}
}
let bench_suites_lock = BENCH_SUITES.lock().unwrap();
if !bench_suites_lock.is_empty() {
static BENCH_FILE_NAME: &str = "bench.json";
let mut bench_result = fs::File::options()
.append(true)
.open(BENCH_FILE_NAME)
.unwrap_or_else(|_| fs::File::create(BENCH_FILE_NAME).unwrap());
bench_result
.write_all(
{ serde_json::to_string(bench_suites_lock.as_slice()).unwrap() + "\n" }
.as_bytes(),
)
.unwrap();
drop(bench_result);
}
})
}
#[derive(TraceRawVcs)]
struct CommandOutput {
stdout: String,
stderr: String,
}
impl CommandOutput {
fn is_empty(&self) -> bool {
self.stderr.is_empty() && self.stdout.is_empty()
}
}
impl Display for CommandOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"---------- Stdout ----------\n{}\n---------- Stderr ----------\n{}",
self.stdout, self.stderr,
)
}
}
async fn exec_node(directory: &str, path: &str) -> Result<CommandOutput> {
let mut cmd = Command::new("node");
let f = Path::new(&directory).join(path);
let dir = f.parent().unwrap();
println!("[CWD]: {}", dir.display());
// See https://github.com/nodejs/node/issues/51555
// The compile cache is causing flaky crashes when run on github actions.
cmd.env("DISABLE_V8_COMPILE_CACHE", "1");
if path.contains("mdx") {
cmd.arg("--experimental-loader=@mdx-js/node-loader")
.arg("--no-warnings");
}
if cfg!(feature = "bench_against_node_nft") {
let current_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
cmd.current_dir(&current_dir);
let mut node_nft = current_dir;
node_nft.push("tests");
node_nft.push("node-file-trace");
node_nft.push("node_modules");
node_nft.push("@vercel");
node_nft.push("nft");
node_nft.push("out");
node_nft.push("cli.js");
cmd.arg(&node_nft).arg("build");
cmd.arg(path);
} else {
cmd.current_dir(dir);
if path.ends_with(".ts") {
let mut ts_node = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
ts_node.push("tests");
ts_node.push("node-file-trace");
ts_node.push("node_modules");
ts_node.push("ts-node");
ts_node.push("dist");
ts_node.push("bin.js");
cmd.arg(&ts_node);
}
cmd.arg(&f);
}
println!("[CMD]: {cmd:#?}");
let output = timeout(Duration::from_secs(100), cmd.output())
.await
.with_context(|| format!("node execution of {path} is hanging"))?
.with_context(|| format!("failed to spawn node process of {path}"))?;
let output = CommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: clean_stderr(String::from_utf8_lossy(&output.stderr).as_ref()),
};
println!("File: {}\n{}", f.display(), output,);
Ok(output)
}
fn clean_stderr(str: &str) -> String {
static EXPERIMENTAL_WARNING: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\(node:\d+\) ExperimentalWarning:").unwrap());
static DEPRECATION_WARNING: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\(node:\d+\) \[DEP\d+] DeprecationWarning:").unwrap());
let str = EXPERIMENTAL_WARNING.replace_all(str, "(node:XXXX) ExperimentalWarning:");
let str = DEPRECATION_WARNING.replace_all(&str, "(node:XXXX) [DEPXXXX] DeprecationWarning:");
str.to_string()
}
fn diff(expected: &str, actual: &str) -> String {
static JAVASCRIPT_TIMESTAMP: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z").unwrap());
static JAVASCRIPT_DATE_TIME: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{6}").unwrap());
// Remove timestamps from the output.
if JAVASCRIPT_DATE_TIME.replace_all(JAVASCRIPT_TIMESTAMP.replace_all(actual, "").as_ref(), "")
== JAVASCRIPT_DATE_TIME
.replace_all(JAVASCRIPT_TIMESTAMP.replace_all(expected, "").as_ref(), "")
{
return String::new();
}
print_changeset(&Changeset::new(expected.trim(), actual.trim(), "\n"))
}
fn assert_output(
expected: CommandOutput,
actual: CommandOutput,
expected_stderr: Option<RcStr>,
) -> Result<CommandOutput> {
Ok(CommandOutput {
stdout: diff(&expected.stdout, &actual.stdout),
stderr: if let Some(expected_stderr) = expected_stderr {
if actual.stderr.contains(&*expected_stderr)
&& expected.stderr.contains(&*expected_stderr)
{
String::new()
} else {
let stderr_diff = diff(&expected.stderr, &actual.stderr);
format!("could not find `{expected_stderr}` in stderr\n{stderr_diff}")
}
} else {
diff(&expected.stderr, &actual.stderr)
},
})
}
#[derive(Debug, Serialize)]
struct BenchSuite {
suite: String,
node_duration: String,
rust_duration: String,
rust_speedup: String,
is_faster: bool,
}
/// rstest's #[case] attribute does not allow for specifying default values.
/// However, it can automatically convert between types, so we can use a
/// custom input struct with the Builder pattern instead.
struct CaseInput {
/// The path to the JS or TS test file.
path: String,
/// The test will pass if the provided error message is included in both
/// stderr outputs.
expected_stderr: Option<String>,
}
impl CaseInput {
fn new(path: &str) -> Self {
Self {
path: path.to_owned(),
expected_stderr: None,
}
}
fn expected_stderr(mut self, msg: &str) -> Self {
self.expected_stderr = Some(msg.to_owned());
self
}
}
impl std::str::FromStr for CaseInput {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s))
}
}
#[turbo_tasks::function(operation, root)]
async fn asset_path_operation(asset: ResolvedVc<Box<dyn OutputAsset>>) -> Vc<FileSystemPath> {
asset.path()
}
#[turbo_tasks::function(operation, root)]
async fn print_graph_operation(asset: ResolvedVc<Box<dyn OutputAsset>>) -> Result<()> {
let mut visited = HashSet::new();
let mut queue = Vec::new();
queue.push((0, asset));
while let Some((depth, asset)) = queue.pop() {
let references = asset.references().all_assets().await?;
let mut indent = String::new();
for _ in 0..depth {
indent.push_str(" ");
}
if visited.insert(asset) {
for &asset in references.iter().rev() {
queue.push((depth + 1, asset));
}
println!("{}{}", indent, asset.path().to_string().await?);
} else if references.is_empty() {
println!("{}{} *", indent, asset.path().to_string().await?);
} else {
println!("{}{} *...", indent, asset.path().to_string().await?);
}
}
Ok(())
}