diff --git a/Cargo.lock b/Cargo.lock index ff29fd4c24957..72c76a9660cb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,7 +718,7 @@ dependencies = [ "biome_text_size", "bitflags 2.5.0", "bpaf", - "oxc_resolver", + "oxc_resolver 1.11.0", "serde", "termcolor", "unicode-width", @@ -2626,16 +2626,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.34.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" dependencies = [ "console", "lazy_static", "linked-hash-map", "serde", "similar", - "yaml-rust", ] [[package]] @@ -3598,6 +3597,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "oxc_resolver" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf8bcda84674ae69228a823dcdb81eac9a398d99f1bbc1dbf00623009fc11c1" +dependencies = [ + "cfg-if", + "dashmap 6.1.0", + "dunce", + "indexmap 2.2.6", + "json-strip-comments", + "once_cell", + "rustc-hash 2.0.0", + "serde", + "serde_json", + "simdutf8", + "thiserror", + "tracing", +] + [[package]] name = "parking" version = "2.0.0" @@ -6027,9 +6046,13 @@ dependencies = [ "anyhow", "assert_cmd", "build-target", + "camino", + "insta", "itertools 0.10.5", "miette", "pretty_assertions", + "serde_json", + "tempfile", "turborepo-lib", "winapi", ] @@ -6041,7 +6064,7 @@ dependencies = [ "camino", "clap", "miette", - "oxc_resolver", + "oxc_resolver 2.0.0", "swc_common", "swc_ecma_ast", "swc_ecma_parser", @@ -7429,15 +7452,6 @@ dependencies = [ "rustix 0.38.31", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/crates/turbo-trace/Cargo.toml b/crates/turbo-trace/Cargo.toml index 08e15986dca9a..80526b52ca1b2 100644 --- a/crates/turbo-trace/Cargo.toml +++ b/crates/turbo-trace/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" camino.workspace = true clap = { version = "4.5.17", features = ["derive"] } miette = { workspace = true, features = ["fancy"] } -oxc_resolver = "1.11.0" +oxc_resolver = { version = "2.0.0" } swc_common = { workspace = true } swc_ecma_ast = { workspace = true } swc_ecma_parser = { workspace = true } diff --git a/crates/turbo-trace/src/tracer.rs b/crates/turbo-trace/src/tracer.rs index f94e085ebdb6d..e1b0e2cbaa39d 100644 --- a/crates/turbo-trace/src/tracer.rs +++ b/crates/turbo-trace/src/tracer.rs @@ -162,7 +162,7 @@ impl Tracer { errors.push(TraceError::PathEncoding(err)); } }, - Err(ResolveError::Builtin(_)) => {} + Err(ResolveError::Builtin { .. }) => {} Err(_) => { let (start, end) = self.source_map.span_to_char_offset(&source_file, *span); diff --git a/crates/turborepo-lib/src/process/child.rs b/crates/turborepo-lib/src/process/child.rs index a5a4d3c6281e3..59198adc7109e 100644 --- a/crates/turborepo-lib/src/process/child.rs +++ b/crates/turborepo-lib/src/process/child.rs @@ -843,7 +843,8 @@ mod test { while !root.join_component(".git").exists() { root = root.parent().unwrap().to_owned(); } - root.join_components(&["crates", "turborepo-lib", "test", "scripts"]) + println!("root: {root:?}"); + root.join_components(&["crates", "turborepo-lib", "test-data", "scripts"]) } #[test_case(false)] diff --git a/crates/turborepo-lib/src/process/mod.rs b/crates/turborepo-lib/src/process/mod.rs index 5e1751a2be007..c3c6edd4e11e7 100644 --- a/crates/turborepo-lib/src/process/mod.rs +++ b/crates/turborepo-lib/src/process/mod.rs @@ -216,7 +216,7 @@ mod test { fn get_script_command(script_name: &str) -> Command { let mut cmd = Command::new("node"); - cmd.args([format!("./test/scripts/{script_name}")]); + cmd.args([format!("./test-data/scripts/{script_name}")]); cmd } diff --git a/crates/turborepo-lib/src/query/file.rs b/crates/turborepo-lib/src/query/file.rs index 78a23965cc748..8c3e8ac14d2e6 100644 --- a/crates/turborepo-lib/src/query/file.rs +++ b/crates/turborepo-lib/src/query/file.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_graphql::{Object, SimpleObject}; +use camino::Utf8PathBuf; use itertools::Itertools; use swc_ecma_ast::EsVersion; use swc_ecma_parser::{EsSyntax, Syntax, TsSyntax}; @@ -68,10 +69,11 @@ impl File { } } -#[derive(SimpleObject, Debug)] +#[derive(SimpleObject, Debug, Default)] pub struct TraceError { message: String, path: Option, + import: Option, start: Option, end: Option, } @@ -83,27 +85,32 @@ impl From for TraceError { turbo_trace::TraceError::FileNotFound(file) => TraceError { message, path: Some(file.to_string()), - start: None, - end: None, + ..Default::default() }, turbo_trace::TraceError::PathEncoding(_) => TraceError { message, - path: None, - start: None, - end: None, + ..Default::default() }, turbo_trace::TraceError::RootFile(path) => TraceError { message, path: Some(path.to_string()), - start: None, - end: None, - }, - turbo_trace::TraceError::Resolve { span, text } => TraceError { - message, - path: Some(text.name().to_string()), - start: Some(span.offset()), - end: Some(span.offset() + span.len()), + ..Default::default() }, + turbo_trace::TraceError::Resolve { span, text } => { + let import = text + .inner() + .read_span(&span, 1, 1) + .ok() + .map(|s| String::from_utf8_lossy(s.data()).to_string()); + + TraceError { + message, + import, + path: Some(text.name().to_string()), + start: Some(span.offset()), + end: Some(span.offset() + span.len()), + } + } } } } @@ -147,11 +154,21 @@ impl File { Ok(self.path.to_string()) } - async fn dependencies(&self, depth: Option) -> TraceResult { + async fn dependencies(&self, depth: Option, ts_config: Option) -> TraceResult { + let ts_config = match ts_config { + Some(ts_config) => Some(Utf8PathBuf::from(ts_config)), + None => self + .path + .ancestors() + .skip(1) + .find(|p| p.join_component("tsconfig.json").exists()) + .map(|p| p.as_path().to_owned()), + }; + let tracer = Tracer::new( self.run.repo_root().to_owned(), vec![self.path.clone()], - None, + ts_config, ); let mut result = tracer.trace(depth); diff --git a/crates/turborepo-lib/test/scripts/hello_no_line.js b/crates/turborepo-lib/test-data/scripts/hello_no_line.js similarity index 100% rename from crates/turborepo-lib/test/scripts/hello_no_line.js rename to crates/turborepo-lib/test-data/scripts/hello_no_line.js diff --git a/crates/turborepo-lib/test/scripts/hello_non_utf8.js b/crates/turborepo-lib/test-data/scripts/hello_non_utf8.js similarity index 100% rename from crates/turborepo-lib/test/scripts/hello_non_utf8.js rename to crates/turborepo-lib/test-data/scripts/hello_non_utf8.js diff --git a/crates/turborepo-lib/test/scripts/hello_world.js b/crates/turborepo-lib/test-data/scripts/hello_world.js similarity index 100% rename from crates/turborepo-lib/test/scripts/hello_world.js rename to crates/turborepo-lib/test-data/scripts/hello_world.js diff --git a/crates/turborepo-lib/test/scripts/hello_world_hello_moon.js b/crates/turborepo-lib/test-data/scripts/hello_world_hello_moon.js similarity index 100% rename from crates/turborepo-lib/test/scripts/hello_world_hello_moon.js rename to crates/turborepo-lib/test-data/scripts/hello_world_hello_moon.js diff --git a/crates/turborepo-lib/test/scripts/sleep_5_ignore.js b/crates/turborepo-lib/test-data/scripts/sleep_5_ignore.js similarity index 100% rename from crates/turborepo-lib/test/scripts/sleep_5_ignore.js rename to crates/turborepo-lib/test-data/scripts/sleep_5_ignore.js diff --git a/crates/turborepo-lib/test/scripts/sleep_5_interruptable.js b/crates/turborepo-lib/test-data/scripts/sleep_5_interruptable.js similarity index 100% rename from crates/turborepo-lib/test/scripts/sleep_5_interruptable.js rename to crates/turborepo-lib/test-data/scripts/sleep_5_interruptable.js diff --git a/crates/turborepo-lib/test/scripts/stdin_stdout.js b/crates/turborepo-lib/test-data/scripts/stdin_stdout.js similarity index 100% rename from crates/turborepo-lib/test/scripts/stdin_stdout.js rename to crates/turborepo-lib/test-data/scripts/stdin_stdout.js diff --git a/crates/turborepo/Cargo.toml b/crates/turborepo/Cargo.toml index f1331b13fa0ea..3e993d58acbc9 100644 --- a/crates/turborepo/Cargo.toml +++ b/crates/turborepo/Cargo.toml @@ -20,8 +20,13 @@ build-target = "0.4.0" [dev-dependencies] assert_cmd = { workspace = true } +camino = { workspace = true } +insta = { version = "1.40.0", features = ["json"] } itertools = { workspace = true } pretty_assertions = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } + [lints] workspace = true diff --git a/crates/turborepo/tests/query.rs b/crates/turborepo/tests/query.rs new file mode 100644 index 0000000000000..99bea9d9573e0 --- /dev/null +++ b/crates/turborepo/tests/query.rs @@ -0,0 +1,60 @@ +use std::{path::Path, process::Command}; + +use camino::Utf8Path; + +fn setup_fixture( + fixture: &str, + package_manager: Option<&str>, + test_dir: &Path, +) -> Result<(), anyhow::Error> { + let script_path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../turborepo-tests/helpers/setup_integration_test.sh"); + + Command::new("bash") + .arg("-c") + .arg(format!( + "{} {} {}", + script_path, + fixture, + package_manager.unwrap_or("npm@10.5.0") + )) + .current_dir(test_dir) + .spawn()? + .wait()?; + + Ok(()) +} + +fn check_query(fixture: &str, query: &str) -> Result<(), anyhow::Error> { + let tempdir = tempfile::tempdir()?; + setup_fixture(fixture, None, tempdir.path())?; + let output = assert_cmd::Command::cargo_bin("turbo")? + .arg("query") + .arg(query) + .current_dir(tempdir.path()) + .output()?; + + let stdout = String::from_utf8(output.stdout)?; + let query_output: serde_json::Value = serde_json::from_str(&stdout)?; + insta::assert_json_snapshot!(query_output); + + Ok(()) +} + +#[cfg(not(windows))] +#[test] +fn test_double_symlink() -> Result<(), anyhow::Error> { + check_query( + "oxc_repro", + "query { + file(path: \"./index.js\") { + path + dependencies { + files { items { path } } + errors { items { message import } } + } + } + }", + )?; + Ok(()) +} diff --git a/crates/turborepo/tests/snapshots/query__check_query.snap b/crates/turborepo/tests/snapshots/query__check_query.snap new file mode 100644 index 0000000000000..4fbff3206ef3c --- /dev/null +++ b/crates/turborepo/tests/snapshots/query__check_query.snap @@ -0,0 +1,23 @@ +--- +source: crates/turborepo-lib/tests/query.rs +expression: query_output +--- +{ + "data": { + "file": { + "path": "index.js", + "dependencies": { + "files": { + "items": [ + { + "path": "nm/index.js" + } + ] + }, + "errors": { + "items": [] + } + } + } + } +} diff --git a/turborepo-tests/helpers/install_deps.sh b/turborepo-tests/helpers/install_deps.sh index 9b2db4939183b..d35841020633c 100755 --- a/turborepo-tests/helpers/install_deps.sh +++ b/turborepo-tests/helpers/install_deps.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -eo pipefail - # TODO: Should we default to pnpm here? PACKAGE_MANAGER="npm" # Check if "@" is present in the argument and remove it if so @@ -11,7 +10,6 @@ elif [[ $1 != "" ]]; then PACKAGE_MANAGER=$1 fi - if [ "$PACKAGE_MANAGER" == "npm" ]; then npm install > /dev/null 2>&1 if [[ "$OSTYPE" == "msys" ]]; then diff --git a/turborepo-tests/helpers/setup_package_manager.sh b/turborepo-tests/helpers/setup_package_manager.sh index 829e67478dc4b..fd0836045476a 100755 --- a/turborepo-tests/helpers/setup_package_manager.sh +++ b/turborepo-tests/helpers/setup_package_manager.sh @@ -22,21 +22,25 @@ if [ "$pkgManager" != "" ]; then fi fi -# If we're in a prysk test, set the corepack install directory to the prysk temp directory. +# get just the packageManager name, without the version +# We pass the name to corepack enable so that it will work for npm also. +# `corepack enable` with no specified packageManager does not work for npm. +pkgManagerName="${pkgManager%%@*}" + +# Set the corepack install directory to a temp directory (either prysk temp or provided dir). # This will help isolate from the rest of the system, especially when running tests on a dev machine. if [ "$PRYSK_TEMP" == "" ]; then - COREPACK_INSTALL_DIR_CMD= + COREPACK_INSTALL_DIR="$dir/corepack" + mkdir -p "${COREPACK_INSTALL_DIR}" + export PATH=${COREPACK_INSTALL_DIR}:$PATH else COREPACK_INSTALL_DIR="${PRYSK_TEMP}/corepack" mkdir -p "${COREPACK_INSTALL_DIR}" export PATH=${COREPACK_INSTALL_DIR}:$PATH - COREPACK_INSTALL_DIR_CMD="--install-directory=${COREPACK_INSTALL_DIR}" fi -# get just the packageManager name, without the version -# We pass the name to corepack enable so that it will work for npm also. -# `corepack enable` with no specified packageManager does not work for npm. -pkgManagerName="${pkgManager%%@*}" - # Enable corepack so that the packageManager setting in package.json is respected. -corepack enable $pkgManagerName "${COREPACK_INSTALL_DIR_CMD}" +corepack enable $pkgManagerName "--install-directory=${COREPACK_INSTALL_DIR}" + + + diff --git a/turborepo-tests/integration/fixtures/oxc_repro/.gitignore b/turborepo-tests/integration/fixtures/oxc_repro/.gitignore new file mode 100644 index 0000000000000..d18ba2ea50998 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/.gitignore @@ -0,0 +1,3 @@ +.idea +/node_modules +.turbo diff --git a/turborepo-tests/integration/fixtures/oxc_repro/README.md b/turborepo-tests/integration/fixtures/oxc_repro/README.md new file mode 100644 index 0000000000000..936acd27084d9 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/README.md @@ -0,0 +1,30 @@ +# oxc symlink bug reproduction + +The bug occurs when a symlink is nested inside a directory that is also symlinked. +This occurs with pnpm since it recreates a conventional node_modules structure using +a content addressed store and symlinks. + +Here's the setup: + +- `apps/web/nm/@repo/typescript-config` is a symlink pointing to `tooling/typescript-config` (imagine `typescript-config` is a workspace package and symlinked into `apps/web`'s node modules) +- `tooling/typescript-config/index.js` is a _relative_ symlink pointing to `../../nm/index.js` +- Therefore, `apps/web/nm/@repo/typescript-config/index.js` is resolved as: + +``` +apps/web/nm/@repo/typescript-config/index.js +-> tooling/typescript-config/index.js +-> tooling/typescript-config/../../nm/index.js +-> nm/index.js +``` + +However, when oxc resolves this, it does not do the first resolution, so we get: + +``` +apps/web/nm/@repo/typescript-config/index.js +-> apps/web/nm/@repo/typescript-config/../../nm/index.js +-> apps/web/nm/nm/index.js +``` + +You can validate this by running `node main.mjs`, which attempts to resolve +both `apps/web/nm/@repo/typescript-config/index.js` and `apps/web/nm/@repo/index.js`. +The first fails while the second succeeds. diff --git a/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/index.js b/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/index.js new file mode 100644 index 0000000000000..7d3ced0fbf6d8 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/index.js @@ -0,0 +1 @@ +export const foo = "10"; diff --git a/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/typescript-config b/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/typescript-config new file mode 120000 index 0000000000000..db36eabbc51ca --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/apps/web/nm/@repo/typescript-config @@ -0,0 +1 @@ +../../../../tooling/typescript-config \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/oxc_repro/index.js b/turborepo-tests/integration/fixtures/oxc_repro/index.js new file mode 100644 index 0000000000000..21ce0d9effd01 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/index.js @@ -0,0 +1 @@ +import foo from "./apps/web/nm/@repo/typescript-config/index.js"; diff --git a/turborepo-tests/integration/fixtures/oxc_repro/nm/index.js b/turborepo-tests/integration/fixtures/oxc_repro/nm/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/turborepo-tests/integration/fixtures/oxc_repro/package.json b/turborepo-tests/integration/fixtures/oxc_repro/package.json new file mode 100644 index 0000000000000..92813c28f5196 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/package.json @@ -0,0 +1,14 @@ +{ + "name": "oxc-repro", + "version": "1.0.0", + "description": "", + "workspaces": ["c"], + "packageManager": "npm@10.5.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": {} +} diff --git a/turborepo-tests/integration/fixtures/oxc_repro/tooling/typescript-config/index.js b/turborepo-tests/integration/fixtures/oxc_repro/tooling/typescript-config/index.js new file mode 120000 index 0000000000000..5f339dec36552 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/tooling/typescript-config/index.js @@ -0,0 +1 @@ +../../nm/index.js \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/oxc_repro/turbo.json b/turborepo-tests/integration/fixtures/oxc_repro/turbo.json new file mode 100644 index 0000000000000..9e26dfeeb6e64 --- /dev/null +++ b/turborepo-tests/integration/fixtures/oxc_repro/turbo.json @@ -0,0 +1 @@ +{} \ No newline at end of file