diff --git a/Cargo.lock b/Cargo.lock index 1c9c2a046b69f..0c02866e4bb65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3813,7 +3813,6 @@ dependencies = [ "mime", "next-core", "next-dev", - "once_cell", "owo-colors", "parking_lot", "rand", @@ -3833,6 +3832,7 @@ dependencies = [ "turbopack-core", "turbopack-dev-server", "turbopack-node", + "turbopack-test-utils", "url", "webbrowser 0.7.1", ] @@ -7991,6 +7991,20 @@ dependencies = [ "turbopack-core", ] +[[package]] +name = "turbopack-test-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "once_cell", + "similar", + "turbo-tasks", + "turbo-tasks-build", + "turbo-tasks-fs", + "turbo-tasks-hash", + "turbopack-core", +] + [[package]] name = "turbopack-tests" version = "0.1.0" @@ -8000,18 +8014,17 @@ dependencies = [ "once_cell", "serde", "serde_json", - "similar", "test-generator", "tokio", "turbo-tasks", "turbo-tasks-build", "turbo-tasks-env", "turbo-tasks-fs", - "turbo-tasks-hash", "turbo-tasks-memory", "turbopack", "turbopack-core", "turbopack-env", + "turbopack-test-utils", ] [[package]] diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 2e161aab70cc4..2b6ace3720314 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -8,7 +8,7 @@ use turbo_tasks::{ CompletionVc, Value, }; use turbo_tasks_env::EnvMapVc; -use turbo_tasks_fs::json::parse_json_rope_with_source_context; +use turbo_tasks_fs::{json::parse_json_rope_with_source_context, FileSystemPathVc}; use turbopack::evaluate_context::node_evaluate_asset_context; use turbopack_core::{ asset::Asset, @@ -603,3 +603,11 @@ pub async fn load_next_config(execution_context: ExecutionContextVc) -> Result Result { + Ok(BoolVc::cell(!matches!( + *find_context_file(context, next_configs()).await?, + FindContextFileResult::NotFound(_) + ))) +} diff --git a/crates/next-core/src/router_source.rs b/crates/next-core/src/router_source.rs index 66ae50c512fd6..de22d55e03e71 100644 --- a/crates/next-core/src/router_source.rs +++ b/crates/next-core/src/router_source.rs @@ -12,7 +12,7 @@ use turbopack_dev_server::source::{ use turbopack_node::execution_context::ExecutionContextVc; use crate::{ - next_config::NextConfigVc, + next_config::{has_next_config, NextConfigVc}, router::{route, RouterRequest, RouterResult}, }; @@ -74,6 +74,16 @@ impl ContentSource for NextRouterContentSource { ) -> Result { let this = self_vc.await?; + // The next-dev server can currently run against projects as simple as + // `index.js`. If this isn't a Next.js project, don't try to use the Next.js + // router. + let project_root = this.execution_context.await?.project_root; + if !(*has_next_config(project_root).await?) { + return Ok(this + .inner + .get(path, Value::new(ContentSourceData::default()))); + } + let ContentSourceData { method: Some(method), raw_headers: Some(raw_headers), diff --git a/crates/next-dev-tests/Cargo.toml b/crates/next-dev-tests/Cargo.toml index bf1420a445c5f..b1dea860a299d 100644 --- a/crates/next-dev-tests/Cargo.toml +++ b/crates/next-dev-tests/Cargo.toml @@ -30,7 +30,6 @@ lazy_static = "1.4.0" mime = "0.3.16" next-core = { path = "../next-core" } next-dev = { path = "../next-dev" } -once_cell = "1.13.0" owo-colors = "3" parking_lot = "0.12.1" rand = "0.8.5" @@ -53,6 +52,7 @@ turbopack-cli-utils = { path = "../turbopack-cli-utils" } turbopack-core = { path = "../turbopack-core" } turbopack-dev-server = { path = "../turbopack-dev-server" } turbopack-node = { path = "../turbopack-node" } +turbopack-test-utils = { path = "../turbopack-test-utils" } url = "2.2.2" webbrowser = "0.7.1" diff --git a/crates/next-dev-tests/build.rs b/crates/next-dev-tests/build.rs index 09dd315050872..1cfa867e95d55 100644 --- a/crates/next-dev-tests/build.rs +++ b/crates/next-dev-tests/build.rs @@ -1,6 +1,7 @@ -use turbo_tasks_build::rerun_if_glob; +use turbo_tasks_build::{generate_register, rerun_if_glob}; fn main() { + generate_register(); // The test/integration crate need to be rebuilt if any test input is changed. // Unfortunately, we can't have the build.rs file operate differently on // each file, so the entire next-dev crate needs to be rebuilt. diff --git a/crates/next-dev-tests/tests/integration.rs b/crates/next-dev-tests/tests/integration.rs index c75afd8beaf7d..9eed0bcd845bd 100644 --- a/crates/next-dev-tests/tests/integration.rs +++ b/crates/next-dev-tests/tests/integration.rs @@ -1,3 +1,4 @@ +#![feature(min_specialization)] #![cfg(test)] extern crate test_generator; @@ -25,16 +26,31 @@ use chromiumoxide::{ }; use futures::StreamExt; use lazy_static::lazy_static; -use next_dev::{register, EntryRequest, NextDevServerBuilder}; +use next_dev::{EntryRequest, NextDevServerBuilder}; use owo_colors::OwoColorize; use serde::Deserialize; use test_generator::test_resources; -use tokio::{net::TcpSocket, task::JoinSet}; +use tokio::{ + net::TcpSocket, + sync::mpsc::{channel, Sender}, + task::JoinSet, +}; use tungstenite::{error::ProtocolError::ResetWithoutClosingHandshake, Error::Protocol}; -use turbo_tasks::TurboTasks; -use turbo_tasks_fs::util::sys_to_unix; +use turbo_tasks::{ + debug::{ValueDebug, ValueDebugStringReadRef}, + primitives::BoolVc, + NothingVc, RawVc, ReadRef, State, TransientInstance, TransientValue, TurboTasks, +}; +use turbo_tasks_fs::{DiskFileSystemVc, FileSystem}; use turbo_tasks_memory::MemoryBackend; use turbo_tasks_testing::retry::retry_async; +use turbopack_core::issue::{CapturedIssues, IssueReporter, IssueReporterVc, PlainIssueReadRef}; +use turbopack_test_utils::snapshot::snapshot_issues; + +fn register() { + next_dev::register(); + include!(concat!(env!("OUT_DIR"), "/register_test_integration.rs")); +} #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -155,18 +171,27 @@ async fn run_test(resource: &str) -> JestRunResult { ); let package_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let workspace_root = package_root.parent().unwrap().parent().unwrap(); - let project_dir = workspace_root.join(resource).join("input"); + let workspace_root = package_root + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); + let test_dir = workspace_root.join(resource); + let project_dir = test_dir.join("input"); let requested_addr = get_free_local_addr().unwrap(); let mock_dir = path.join("__httpmock__"); let mock_server_future = get_mock_server_future(&mock_dir); - let turbo_tasks = TurboTasks::new(MemoryBackend::default()); + let (issue_tx, mut issue_rx) = channel(u16::MAX as usize); + let issue_tx = TransientInstance::new(issue_tx); + + let tt = TurboTasks::new(MemoryBackend::default()); let server = NextDevServerBuilder::new( - turbo_tasks, - sys_to_unix(&project_dir.to_string_lossy()).to_string(), - sys_to_unix(&workspace_root.to_string_lossy()).to_string(), + tt.clone(), + project_dir.to_string_lossy().to_string(), + workspace_root.to_string_lossy().to_string(), ) .entry_request(EntryRequest::Module( "@turbo/pack-test-harness".to_string(), @@ -178,6 +203,9 @@ async fn run_test(resource: &str) -> JestRunResult { .port(requested_addr.port()) .log_level(turbopack_core::issue::IssueSeverity::Warning) .log_detail(true) + .issue_reporter(Box::new(move || { + TestIssueReporterVc::new(issue_tx.clone()).into() + })) .show_all(true) .build() .await @@ -198,6 +226,29 @@ async fn run_test(resource: &str) -> JestRunResult { env::remove_var("TURBOPACK_TEST_ONLY_MOCK_SERVER"); + let task = tt.spawn_once_task(async move { + let issues_fs = DiskFileSystemVc::new( + "issues".to_string(), + test_dir.join("issues").to_string_lossy().to_string(), + ) + .as_file_system(); + + let mut issues = vec![]; + while let Ok(issue) = issue_rx.try_recv() { + issues.push(issue); + } + + snapshot_issues( + issues.iter().cloned(), + issues_fs.root(), + &workspace_root.to_string_lossy(), + ) + .await?; + + Ok(NothingVc::new().into()) + }); + tt.wait_task_completion(task, true).await.unwrap(); + result } @@ -414,3 +465,40 @@ async fn get_mock_server_future(mock_dir: &Path) -> Result<(), String> { std::future::pending::>().await } } + +#[turbo_tasks::value(shared)] +struct TestIssueReporter { + #[turbo_tasks(trace_ignore, debug_ignore)] + pub issue_tx: State>, +} + +#[turbo_tasks::value_impl] +impl TestIssueReporterVc { + #[turbo_tasks::function] + fn new( + issue_tx: TransientInstance>, + ) -> Self { + TestIssueReporter { + issue_tx: State::new((*issue_tx).clone()), + } + .cell() + } +} + +#[turbo_tasks::value_impl] +impl IssueReporter for TestIssueReporter { + #[turbo_tasks::function] + async fn report_issues( + &self, + captured_issues: TransientInstance>, + _source: TransientValue, + ) -> Result { + let issue_tx = self.issue_tx.get_untracked().clone(); + for issue in captured_issues.iter() { + let plain = issue.into_plain(); + issue_tx.send((plain.await?, plain.dbg().await?)).await?; + } + + Ok(BoolVc::cell(false)) + } +} diff --git a/crates/next-dev-tests/tests/integration/next/resolve-alias/basic/input/pages/.gitkeep b/crates/next-dev-tests/tests/integration/next/resolve-alias/basic/input/pages/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.existsSync(__q____q____q____star__0__star__) is very dynamic-c19e79.txt b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.existsSync(__q____q____q____star__0__star__) is very dynamic-c19e79.txt new file mode 100644 index 0000000000000..0acc69df7766a --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.existsSync(__q____q____q____star__0__star__) is very dynamic-c19e79.txt @@ -0,0 +1,25 @@ +PlainIssue { + severity: Warning, + context: "[project-with-next]/node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/previous-map.js", + category: "parse", + title: "lint TP1004 fs.existsSync(???*0*) is very dynamic", + description: "- *0* arguments[0]\n ⚠\u{fe0f} function calls are not analysed yet", + detail: "", + documentation_link: "", + source: Some( + PlainIssueSource { + asset: PlainAsset { + path: "node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/previous-map.js", + }, + start: SourcePos { + line: 87, + column: 8, + }, + end: SourcePos { + line: 87, + column: 8, + }, + }, + ), + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.readFileSync(__q____q____q____star__0__star__, __quo__utf-8__quo__) is very dynamic-58d7af.txt b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.readFileSync(__q____q____q____star__0__star__, __quo__utf-8__quo__) is very dynamic-58d7af.txt new file mode 100644 index 0000000000000..866db710e5a72 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1004 fs.readFileSync(__q____q____q____star__0__star__, __quo__utf-8__quo__) is very dynamic-58d7af.txt @@ -0,0 +1,25 @@ +PlainIssue { + severity: Warning, + context: "[project-with-next]/node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/previous-map.js", + category: "parse", + title: "lint TP1004 fs.readFileSync(???*0*, \"utf-8\") is very dynamic", + description: "- *0* arguments[0]\n ⚠\u{fe0f} function calls are not analysed yet", + detail: "", + documentation_link: "", + source: Some( + PlainIssueSource { + asset: PlainAsset { + path: "node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/previous-map.js", + }, + start: SourcePos { + line: 89, + column: 13, + }, + end: SourcePos { + line: 89, + column: 13, + }, + }, + ), + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1006 path.resolve(__q____q____q____star__0__star__) is very dynamic-667243.txt b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1006 path.resolve(__q____q____q____star__0__star__) is very dynamic-667243.txt new file mode 100644 index 0000000000000..848737ac7abdc --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/tailwind/basic/issues/lint TP1006 path.resolve(__q____q____q____star__0__star__) is very dynamic-667243.txt @@ -0,0 +1,25 @@ +PlainIssue { + severity: Warning, + context: "[project-with-next]/node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/input.js", + category: "parse", + title: "lint TP1006 path.resolve(???*0*) is very dynamic", + description: "- *0* ???*1*[\"from\"]\n ⚠\u{fe0f} unknown object\n- *1* opts\n ⚠\u{fe0f} pattern without value", + detail: "", + documentation_link: "", + source: Some( + PlainIssueSource { + asset: PlainAsset { + path: "node_modules/.pnpm/postcss@8.4.20/node_modules/postcss/lib/input.js", + }, + start: SourcePos { + line: 43, + column: 20, + }, + end: SourcePos { + line: 43, + column: 20, + }, + }, + ), + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/index.js b/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/index.js deleted file mode 100644 index 02dbf512a5a01..0000000000000 --- a/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import source from "./hello.replace"; - -it("runs a loader with basic options", () => { - expect(source).toBe(3); -}); diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/hello.replace b/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/pages/hello.replace similarity index 100% rename from crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/hello.replace rename to crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/pages/hello.replace diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/pages/index.js b/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/pages/index.js new file mode 100644 index 0000000000000..e150264eb5543 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/webpack-loaders/basic-options/input/pages/index.js @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import source from "./hello.replace"; + +export default function Home() { + useEffect(() => { + // Only run on client + import("@turbo/pack-test-harness").then(runTests); + }); + + return null; +} + +function runTests() { + it("runs a loader with basic options", () => { + expect(source).toBe(3); + }); +} diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/index.js b/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/index.js deleted file mode 100644 index 400c55352f579..0000000000000 --- a/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import source from "./hello.raw"; - -it("runs a simple loader", () => { - expect(source).toBe("Hello World"); -}); diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/hello.raw b/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw similarity index 100% rename from crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/hello.raw rename to crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/hello.raw diff --git a/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js b/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js new file mode 100644 index 0000000000000..4df1f68aab843 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/next/webpack-loaders/no-options/input/pages/index.js @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import source from "./hello.raw"; + +export default function Home() { + useEffect(() => { + // Only run on client + import("@turbo/pack-test-harness").then(runTests); + }); + + return null; +} + +function runTests() { + it("runs a simple loader", () => { + expect(source).toBe("Hello World"); + }); +} diff --git a/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error evaluating Node.js code-b780fa.txt b/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error evaluating Node.js code-b780fa.txt new file mode 100644 index 0000000000000..726fe7cc5ae68 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error evaluating Node.js code-b780fa.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project]/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input", + category: "build", + title: "Error evaluating Node.js code", + description: "Error: > Couldn't find a `pages` directory. Please create one under the project root\n at Object.findPagesDir (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/lib/find-pages-dir.js:86:19)\n at DevServer.getRoutes (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/server/dev/next-dev-server.js:130:59)\n at new Server (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/server/base-server.js:108:47)\n at new NextNodeServer (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/server/next-server.js:69:9)\n at new DevServer (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/server/dev/next-dev-server.js:96:9)\n at Object.makeResolver (node_modules/.pnpm/next@13.1.7-canary.28_pjwopsidmaokadturxaafygjp4/node_modules/next/dist/server/lib/route-resolver.js:39:23)\n at getResolveRoute (crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input/.next/build/router/chunks/router.js:170:97)\n at async Module.route (crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input/.next/build/router/chunks/router.js:173:36)\n at async Module.run (crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input/.next/build/router/chunks/[turbopack-node]_ipc_evaluate.ts._.js:142:39)\n", + detail: "", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error resolving commonjs request-6b96ad.txt b/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error resolving commonjs request-6b96ad.txt new file mode 100644 index 0000000000000..05cbde4138bc6 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/issues/Error resolving commonjs request-6b96ad.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input/index.js", + category: "resolve", + title: "Error resolving commonjs request", + description: "unable to resolve relative \"./not-existing-file\"", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: relative \"./not-existing-file\"\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/turbopack/basic/comptime/input/index.js\nType of request: commonjs request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-1a1150.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-1a1150.txt new file mode 100644 index 0000000000000..cd5c8c3160753 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-1a1150.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js", + category: "resolve", + title: "Error resolving EcmaScript Modules request", + description: "unable to resolve relative \"./empty?import2-in-chunk1\"", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: relative \"./empty?import2-in-chunk1\"\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js\nType of request: EcmaScript Modules request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-431056.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-431056.txt new file mode 100644 index 0000000000000..12b6a9c3d414e --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-431056.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js", + category: "resolve", + title: "Error resolving EcmaScript Modules request", + description: "unable to resolve relative \"./empty?import1-in-chunk1\"", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: relative \"./empty?import1-in-chunk1\"\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js\nType of request: EcmaScript Modules request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-a3050d.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-a3050d.txt new file mode 100644 index 0000000000000..7bf04d0b03b36 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/issues/Error resolving EcmaScript Modules request-a3050d.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js", + category: "resolve", + title: "Error resolving EcmaScript Modules request", + description: "unable to resolve relative \"./empty?import3-in-chunk2\"", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: relative \"./empty?import3-in-chunk2\"\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/named-chunks/input/index.js\nType of request: EcmaScript Modules request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/parsing/issues/Error resolving commonjs request-92a826.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/parsing/issues/Error resolving commonjs request-92a826.txt new file mode 100644 index 0000000000000..a1e0386bd6a3f --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/parsing/issues/Error resolving commonjs request-92a826.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/parsing/input/index.js", + category: "resolve", + title: "Error resolving commonjs request", + description: "unable to resolve relative \"./empty?require.ensure:test\"", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: relative \"./empty?require.ensure:test\"\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/parsing/input/index.js\nType of request: commonjs request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/Error resolving commonjs request-354825.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/Error resolving commonjs request-354825.txt new file mode 100644 index 0000000000000..c7ffb33ad07b7 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/Error resolving commonjs request-354825.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/input/index.js", + category: "resolve", + title: "Error resolving commonjs request", + description: "unable to resolve dynamic", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: dynamic\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/input/index.js\nType of request: commonjs request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/lint TP1002 require([__quo__.__b__quo__]) is very dynamic-8bc115.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/lint TP1002 require([__quo__.__b__quo__]) is very dynamic-8bc115.txt new file mode 100644 index 0000000000000..9f61ddd73dd8d --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/issues/lint TP1002 require([__quo__.__b__quo__]) is very dynamic-8bc115.txt @@ -0,0 +1,25 @@ +PlainIssue { + severity: Warning, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/input/index.js", + category: "parse", + title: "lint TP1002 require([\"./b\"]) is very dynamic", + description: "", + detail: "", + documentation_link: "", + source: Some( + PlainIssueSource { + asset: PlainAsset { + path: "crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies-context/input/index.js", + }, + start: SourcePos { + line: 13, + column: 2, + }, + end: SourcePos { + line: 13, + column: 2, + }, + }, + ), + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/Error resolving commonjs request-86b73f.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/Error resolving commonjs request-86b73f.txt new file mode 100644 index 0000000000000..ecf350a66844a --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/Error resolving commonjs request-86b73f.txt @@ -0,0 +1,11 @@ +PlainIssue { + severity: Error, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/input/index.js", + category: "resolve", + title: "Error resolving commonjs request", + description: "unable to resolve dynamic", + detail: "It was not possible to find the requested file.\nParsed request as written in source code: dynamic\nPath where resolving has started: [project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/input/index.js\nType of request: commonjs request\nImport map: No import map entry\n", + documentation_link: "", + source: None, + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/lint TP1002 require([__quo__.__c__quo__]) is very dynamic-80ea5a.txt b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/lint TP1002 require([__quo__.__c__quo__]) is very dynamic-80ea5a.txt new file mode 100644 index 0000000000000..2340dffe7ab21 --- /dev/null +++ b/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/issues/lint TP1002 require([__quo__.__c__quo__]) is very dynamic-80ea5a.txt @@ -0,0 +1,25 @@ +PlainIssue { + severity: Warning, + context: "[project-with-next]/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/input/index.js", + category: "parse", + title: "lint TP1002 require([\"./c\"]) is very dynamic", + description: "", + detail: "", + documentation_link: "", + source: Some( + PlainIssueSource { + asset: PlainAsset { + path: "crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/weak-dependencies/input/index.js", + }, + start: SourcePos { + line: 5, + column: 2, + }, + end: SourcePos { + line: 5, + column: 2, + }, + }, + ), + sub_issues: [], +} \ No newline at end of file diff --git a/crates/next-dev/src/lib.rs b/crates/next-dev/src/lib.rs index 4116b462ee126..e960c670a2c43 100644 --- a/crates/next-dev/src/lib.rs +++ b/crates/next-dev/src/lib.rs @@ -247,12 +247,12 @@ async fn handle_issues + CollectiblesSource + Copy>( .strongly_consistent() .await?; - issue_reporter.report_issues( + let has_fatal = issue_reporter.report_issues( TransientInstance::new(issues.clone()), TransientValue::new(source.into()), ); - if issues.has_fatal().await? { + if *has_fatal.await? { Err(anyhow!("Fatal issue(s) occurred")) } else { Ok(()) diff --git a/crates/turbo-tasks/src/debug/mod.rs b/crates/turbo-tasks/src/debug/mod.rs index 51cd2e0c577b7..7844a2e3d7224 100644 --- a/crates/turbo-tasks/src/debug/mod.rs +++ b/crates/turbo-tasks/src/debug/mod.rs @@ -14,6 +14,7 @@ use internal::PassthroughDebug; /// /// We don't use `StringVc` directly because we don't want the `Debug`/`Display` /// representations to be escaped. +#[derive(Clone)] #[turbo_tasks::value] pub struct ValueDebugString(String); diff --git a/crates/turbo-tasks/src/state.rs b/crates/turbo-tasks/src/state.rs index 5f49d51511f69..1adc34fce1d6a 100644 --- a/crates/turbo-tasks/src/state.rs +++ b/crates/turbo-tasks/src/state.rs @@ -1,4 +1,8 @@ -use std::{fmt::Debug, mem::take}; +use std::{ + fmt::Debug, + mem::take, + ops::{Deref, DerefMut}, +}; use auto_hash_map::AutoSet; use parking_lot::{Mutex, MutexGuard}; @@ -17,6 +21,7 @@ struct StateInner { pub struct StateRef<'a, T> { inner: MutexGuard<'a, StateInner>, + mutated: bool, } impl Debug for State { @@ -47,7 +52,7 @@ impl PartialEq for State { } impl Eq for State {} -impl Serialize for State { +impl Serialize for State { fn serialize(&self, _serializer: S) -> Result { // For this to work at all we need to do more. Changing the the state need to // invalidate the serialization of the task that contains the state. So we @@ -57,7 +62,7 @@ impl Serialize for State { } } -impl<'de, T: Deserialize<'de>> Deserialize<'de> for State { +impl<'de, T> Deserialize<'de> for State { fn deserialize>(_deserializer: D) -> Result { panic!("State serialization is not implemented yet"); } @@ -90,7 +95,19 @@ impl State { let invalidator = get_invalidator(); let mut inner = self.inner.lock(); inner.invalidators.insert(invalidator); - StateRef { inner } + StateRef { + inner, + mutated: false, + } + } + + /// Gets the current value of the state. Untracked. + pub fn get_untracked(&self) -> StateRef<'_, T> { + let inner = self.inner.lock(); + StateRef { + inner, + mutated: false, + } } /// Sets the current state without comparing it with the old value. This @@ -133,10 +150,27 @@ impl State { } } -impl<'a, T> std::ops::Deref for StateRef<'a, T> { +impl<'a, T> Deref for StateRef<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.inner.value } } + +impl<'a, T> DerefMut for StateRef<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.mutated = true; + &mut self.inner.value + } +} + +impl<'a, T> Drop for StateRef<'a, T> { + fn drop(&mut self) { + if self.mutated { + for invalidator in take(&mut self.inner.invalidators) { + invalidator.invalidate(); + } + } + } +} diff --git a/crates/turbopack-cli-utils/src/issue.rs b/crates/turbopack-cli-utils/src/issue.rs index 52f8a6bd4f310..9fed3d3a04369 100644 --- a/crates/turbopack-cli-utils/src/issue.rs +++ b/crates/turbopack-cli-utils/src/issue.rs @@ -11,7 +11,8 @@ use anyhow::{anyhow, Result}; use crossterm::style::{StyledContent, Stylize}; use owo_colors::{OwoColorize as _, Style}; use turbo_tasks::{ - RawVc, ReadRef, TransientInstance, TransientValue, TryJoinIterExt, ValueToString, + primitives::BoolVc, RawVc, ReadRef, TransientInstance, TransientValue, TryJoinIterExt, + ValueToString, }; use turbo_tasks_fs::{ attach::AttachedFileSystemVc, @@ -433,7 +434,7 @@ impl IssueReporter for ConsoleUi { &self, issues: TransientInstance>, source: TransientValue, - ) -> Result<()> { + ) -> Result { let issues = &*issues; let LogOptions { ref current_dir, @@ -465,12 +466,17 @@ impl IssueReporter for ConsoleUi { .unwrap() .new_ids(source.into_value(), issue_ids); + let mut has_fatal = false; for (plain_issue, path, context, id) in issues { if !new_ids.remove(&id) { continue; } let severity = plain_issue.severity; + if severity == IssueSeverity::Fatal { + has_fatal = true; + } + let context_path = make_relative_to_cwd(context, current_dir).await?; let category = &plain_issue.category; let title = &plain_issue.title; @@ -606,7 +612,7 @@ impl IssueReporter for ConsoleUi { } } - Ok(()) + Ok(BoolVc::cell(has_fatal)) } } diff --git a/crates/turbopack-core/src/issue/mod.rs b/crates/turbopack-core/src/issue/mod.rs index 17e425e1510c5..a681f4882f8b3 100644 --- a/crates/turbopack-core/src/issue/mod.rs +++ b/crates/turbopack-core/src/issue/mod.rs @@ -21,8 +21,7 @@ use turbo_tasks::{ ValueToString, ValueToStringVc, }; use turbo_tasks_fs::{ - FileContent, FileContentReadRef, FileLine, FileLinesContent, FileSystemPathReadRef, - FileSystemPathVc, + FileContent, FileContentReadRef, FileLine, FileLinesContent, FileSystemPathVc, }; use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher}; @@ -341,21 +340,6 @@ pub struct CapturedIssues { processing_path: ItemIssueProcessingPathVc, } -impl CapturedIssues { - pub async fn has_fatal(&self) -> Result { - let mut has_fatal = false; - - for issue in self.issues.iter() { - let severity = *issue.severity().await?; - if severity == IssueSeverity::Fatal { - has_fatal = true; - break; - } - } - Ok(has_fatal) - } -} - #[turbo_tasks::value_impl] impl CapturedIssuesVc { #[turbo_tasks::function] @@ -471,24 +455,24 @@ pub struct PlainIssue { pub sub_issues: Vec, } -#[turbo_tasks::value_impl] -impl PlainIssueVc { +impl PlainIssue { /// We need deduplicate issues that can come from unique paths, but /// represent the same underlying problem. Eg, a parse error for a file /// that is compiled in both client and server contexts. - #[turbo_tasks::function] - pub async fn internal_hash(self) -> Result { - let this = self.await?; + pub fn internal_hash(&self) -> u64 { let mut hasher = Xxh3Hash64Hasher::new(); - hasher.write_ref(&this.severity); - hasher.write_ref(&this.context); - hasher.write_ref(&this.category); - hasher.write_ref(&this.title); - hasher.write_ref(&this.description); - hasher.write_ref(&this.detail); - hasher.write_ref(&this.documentation_link); - - if let Some(source) = &this.source { + hasher.write_ref(&self.severity); + hasher.write_ref(&self.context); + hasher.write_ref(&self.category); + hasher.write_ref(&self.title); + hasher.write_ref( + // Normalize syspaths from Windows. These appear in stack traces. + &self.description.replace("\\", "/"), + ); + hasher.write_ref(&self.detail); + hasher.write_ref(&self.documentation_link); + + if let Some(source) = &self.source { hasher.write_value(1_u8); // I'm assuming we don't need to hash the contents. Not 100% correct, but // probably 99%. @@ -498,7 +482,15 @@ impl PlainIssueVc { hasher.write_value(0_u8); } - Ok(U64Vc::cell(hasher.finish())) + hasher.finish() + } +} + +#[turbo_tasks::value_impl] +impl PlainIssueVc { + #[turbo_tasks::function] + pub async fn internal_hash(self) -> Result { + Ok(U64Vc::cell(self.await?.internal_hash())) } } @@ -562,7 +554,7 @@ impl IssueSourceVc { #[turbo_tasks::value(serialization = "none")] #[derive(Clone, Debug)] pub struct PlainAsset { - pub path: FileSystemPathReadRef, + pub path: String, #[turbo_tasks(debug_ignore)] pub content: FileContentReadRef, } @@ -578,7 +570,7 @@ impl PlainAssetVc { }; Ok(PlainAsset { - path: asset.path().await?, + path: asset.path().await?.to_string(), content, } .cell()) @@ -591,5 +583,5 @@ pub trait IssueReporter { &self, issues: TransientInstance>, source: TransientValue, - ); + ) -> BoolVc; } diff --git a/crates/turbopack-dev-server/src/lib.rs b/crates/turbopack-dev-server/src/lib.rs index 9851b65ff5682..17df99beda059 100644 --- a/crates/turbopack-dev-server/src/lib.rs +++ b/crates/turbopack-dev-server/src/lib.rs @@ -78,12 +78,12 @@ async fn handle_issues + CollectiblesSource + Copy>( .strongly_consistent() .await?; - issue_reporter.report_issues( + let has_fatal = issue_reporter.report_issues( TransientInstance::new(issues.clone()), TransientValue::new(source.into()), ); - if issues.has_fatal().await? { + if *has_fatal.await? { Err(anyhow!("Fatal issue(s) occurred in {path} ({operation})")) } else { Ok(()) diff --git a/crates/turbopack-dev-server/src/update/protocol.rs b/crates/turbopack-dev-server/src/update/protocol.rs index 23145b137131e..c99d1d258c830 100644 --- a/crates/turbopack-dev-server/src/update/protocol.rs +++ b/crates/turbopack-dev-server/src/update/protocol.rs @@ -129,7 +129,7 @@ impl<'a> From<&'a PlainIssue> for Issue<'a> { fn from(plain: &'a PlainIssue) -> Self { let source = plain.source.as_deref().map(|source| IssueSource { asset: Asset { - path: &source.asset.path.path, + path: &source.asset.path, }, start: source.start, end: source.end, diff --git a/crates/turbopack-node/src/evaluate.rs b/crates/turbopack-node/src/evaluate.rs index 376f8cd390bda..fc1d91e0d14ff 100644 --- a/crates/turbopack-node/src/evaluate.rs +++ b/crates/turbopack-node/src/evaluate.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::HashMap, thread::available_parallelism, time::Duration}; -use anyhow::Result; +use anyhow::{Context, Result}; use futures_retry::{FutureRetry, RetryPolicy}; use turbo_tasks::{ primitives::{JsonValueVc, StringVc}, @@ -226,6 +226,7 @@ pub async fn evaluate( EvaluationIssue { error, context_path: context_path_for_issue, + cwd, } .cell() .as_issue() @@ -279,6 +280,7 @@ pub async fn evaluate( #[turbo_tasks::value(shared)] pub struct EvaluationIssue { pub context_path: FileSystemPathVc, + pub cwd: FileSystemPathVc, pub error: StructuredError, } @@ -301,8 +303,14 @@ impl Issue for EvaluationIssue { #[turbo_tasks::function] async fn description(&self) -> Result { + let cwd = to_sys_path(self.cwd.root()) + .await? + .context("Must have path on disk")?; + Ok(StringVc::cell( - self.error.print(Default::default(), None).await?, + self.error + .print(Default::default(), &cwd.to_string_lossy()) + .await?, )) } } diff --git a/crates/turbopack-node/src/lib.rs b/crates/turbopack-node/src/lib.rs index 3d7c3b75978e5..2e6e6b7cd64fa 100644 --- a/crates/turbopack-node/src/lib.rs +++ b/crates/turbopack-node/src/lib.rs @@ -264,18 +264,17 @@ pub struct StructuredError { } impl StructuredError { - async fn print( - &self, - assets: HashMap, - root: Option, - ) -> Result { + async fn print(&self, assets: HashMap, root: &str) -> Result { let mut message = String::new(); writeln!(message, "{}: {}", self.name, self.message)?; for frame in &self.stack { if let Some((line, column)) = frame.get_pos() { - if let Some(path) = root.as_ref().and_then(|r| frame.file.strip_prefix(r)) { + if let Some(path) = frame.file.strip_prefix( + // Add a trailing slash so paths don't lead with `/`. + &format!("{}{}", root, std::path::MAIN_SEPARATOR), + ) { if let Some(map) = assets.get(path) { let trace = SourceMapTraceVc::new(*map, line, column, frame.name.clone()) .trace() @@ -333,7 +332,7 @@ pub async fn trace_stack( .flatten() .collect::>(); - error.print(assets, Some(root)).await + error.print(assets, &root).await } pub fn register() { diff --git a/crates/turbopack-test-utils/Cargo.toml b/crates/turbopack-test-utils/Cargo.toml new file mode 100644 index 0000000000000..9a3c35ae1aed3 --- /dev/null +++ b/crates/turbopack-test-utils/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "turbopack-test-utils" +version = "0.1.0" +description = "TBD" +license = "MPL-2.0" +edition = "2021" +autobenches = false + +[lib] +bench = false + +[dependencies] +anyhow = "1.0.47" +once_cell = "1.13.0" +similar = "2.2.0" +turbo-tasks = { path = "../turbo-tasks" } +turbo-tasks-fs = { path = "../turbo-tasks-fs" } +turbo-tasks-hash = { path = "../turbo-tasks-hash" } +turbopack-core = { path = "../turbopack-core" } + +[build-dependencies] +turbo-tasks-build = { path = "../turbo-tasks-build" } diff --git a/crates/turbopack-test-utils/build.rs b/crates/turbopack-test-utils/build.rs new file mode 100644 index 0000000000000..1673efed59cce --- /dev/null +++ b/crates/turbopack-test-utils/build.rs @@ -0,0 +1,5 @@ +use turbo_tasks_build::generate_register; + +fn main() { + generate_register(); +} diff --git a/crates/turbopack-test-utils/src/lib.rs b/crates/turbopack-test-utils/src/lib.rs new file mode 100644 index 0000000000000..bd283f6a618fb --- /dev/null +++ b/crates/turbopack-test-utils/src/lib.rs @@ -0,0 +1,4 @@ +#![feature(min_specialization)] +#![feature(str_split_as_str)] + +pub mod snapshot; diff --git a/crates/turbopack-test-utils/src/snapshot.rs b/crates/turbopack-test-utils/src/snapshot.rs new file mode 100644 index 0000000000000..c69ad42a6a493 --- /dev/null +++ b/crates/turbopack-test-utils/src/snapshot.rs @@ -0,0 +1,189 @@ +use std::{ + collections::{HashMap, HashSet}, + env, fs, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use once_cell::sync::Lazy; +use similar::TextDiff; +use turbo_tasks::{debug::ValueDebugStringReadRef, TryJoinIterExt, ValueToString}; +use turbo_tasks_fs::{ + DirectoryContent, DirectoryEntry, DiskFileSystemVc, File, FileContent, FileSystemEntryType, + FileSystemPathVc, +}; +use turbo_tasks_hash::encode_hex; +use turbopack_core::{ + asset::{AssetContent, AssetContentVc}, + issue::PlainIssueReadRef, +}; + +// Updates the existing snapshot outputs with the actual outputs of this run. +// e.g. `UPDATE=1 cargo test -p turbopack-tests -- test_my_pattern` +static UPDATE: Lazy = Lazy::new(|| env::var("UPDATE").unwrap_or_default() == "1"); + +pub async fn snapshot_issues< + I: IntoIterator, +>( + captured_issues: I, + issues_path: FileSystemPathVc, + workspace_root: &str, +) -> Result<()> { + let expected_issues = expected(issues_path).await?; + let mut seen = HashSet::new(); + for (plain_issue, debug_string) in captured_issues.into_iter() { + let hash = encode_hex(plain_issue.internal_hash()); + + let path = issues_path.join(&format!( + "{}-{}.txt", + plain_issue + .title + .replace('/', "__") + // We replace "*", "?", and '"' because they're not allowed in filenames on Windows. + .replace('*', "__star__") + .replace('"', "__quo__") + .replace('?', "__q__"), + &hash[0..6] + )); + seen.insert(path); + + // Annoyingly, the PlainIssue.source -> PlainIssueSource.asset -> + // PlainAsset.path -> FileSystemPath.fs -> DiskFileSystem.root changes + // for everyone. + let content = debug_string + .as_str() + .replace(workspace_root, "WORKSPACE_ROOT") + // Normalize syspaths from Windows. These appear in stack traces. + .replace("\\\\", "/"); + let asset = File::from(content).into(); + + diff(path, asset).await?; + } + + matches_expected(expected_issues, seen).await +} + +pub async fn expected(dir: FileSystemPathVc) -> Result> { + let mut expected = HashSet::new(); + let entries = dir.read_dir().await?; + if let DirectoryContent::Entries(entries) = &*entries { + for (file, entry) in entries { + match entry { + DirectoryEntry::File(file) => { + expected.insert(*file); + } + _ => bail!( + "expected file at {}, found {:?}", + file, + FileSystemEntryType::from(entry) + ), + } + } + } + Ok(expected) +} + +pub async fn matches_expected( + expected: HashSet, + seen: HashSet, +) -> Result<()> { + for path in diff_paths(&expected, &seen).await? { + let p = &path.await?.path; + if *UPDATE { + remove_file(path).await?; + println!("removed file {}", p); + } else { + bail!("expected file {}, but it was not emitted", p); + } + } + Ok(()) +} + +pub async fn diff(path: FileSystemPathVc, actual: AssetContentVc) -> Result<()> { + let path_str = &path.await?.path; + let expected = path.read().into(); + + let actual = match get_contents(actual, path).await? { + Some(s) => s, + None => bail!("could not generate {} contents", path_str), + }; + let expected = get_contents(expected, path).await?; + + if Some(&actual) != expected.as_ref() { + if *UPDATE { + let content = File::from(actual).into(); + path.write(content).await?; + println!("updated contents of {}", path_str); + } else { + if expected.is_none() { + eprintln!("new file {path_str} detected:"); + } else { + eprintln!("contents of {path_str} did not match:"); + } + let expected = expected.unwrap_or_default(); + let diff = TextDiff::from_lines(&expected, &actual); + eprintln!( + "{}", + diff.unified_diff() + .context_radius(3) + .header("expected", "actual") + ); + bail!("contents of {path_str} did not match"); + } + } + + Ok(()) +} + +async fn get_contents(file: AssetContentVc, path: FileSystemPathVc) -> Result> { + Ok( + match &*file.await.context(format!( + "Unable to read AssetContent of {}", + path.to_string().await? + ))? { + AssetContent::File(file) => match &*file.await.context(format!( + "Unable to read FileContent of {}", + path.to_string().await? + ))? { + FileContent::NotFound => None, + FileContent::Content(expected) => { + Some(expected.content().to_str()?.trim().to_string()) + } + }, + AssetContent::Redirect { target, link_type } => Some(format!( + "Redirect {{ target: {target}, link_type: {:?} }}", + link_type + )), + }, + ) +} + +async fn remove_file(path: FileSystemPathVc) -> Result<()> { + let fs = DiskFileSystemVc::resolve_from(path.fs()) + .await? + .context(anyhow!("unexpected fs type"))? + .await?; + let sys_path = fs.to_sys_path(path).await?; + fs::remove_file(&sys_path).context(format!("remove file {} error", sys_path.display()))?; + Ok(()) +} + +/// Values in left that are not in right. +/// FileSystemPathVc hashes as a Vc, not as the file path, so we need to get the +/// path to properly diff. +async fn diff_paths( + left: &HashSet, + right: &HashSet, +) -> Result> { + let mut map = left + .iter() + .map(|p| async move { Ok((p.await?.path.clone(), *p)) }) + .try_join() + .await? + .iter() + .cloned() + .collect::>(); + for p in right { + map.remove(&p.await?.path); + } + Ok(map.values().copied().collect()) +} diff --git a/crates/turbopack-tests/Cargo.toml b/crates/turbopack-tests/Cargo.toml index fc13864e5afb4..6a00d047b921f 100644 --- a/crates/turbopack-tests/Cargo.toml +++ b/crates/turbopack-tests/Cargo.toml @@ -18,16 +18,15 @@ next-core = { path = "../next-core", features = ['native-tls'] } once_cell = "1.13.0" serde = "1.0.136" serde_json = "1.0.85" -similar = "2.2.0" test-generator = "0.3.0" tokio = "1.21.2" turbo-tasks = { path = "../turbo-tasks" } turbo-tasks-env = { path = "../turbo-tasks-env" } turbo-tasks-fs = { path = "../turbo-tasks-fs" } -turbo-tasks-hash = { path = "../turbo-tasks-hash" } turbo-tasks-memory = { path = "../turbo-tasks-memory" } turbopack-core = { path = "../turbopack-core" } turbopack-env = { path = "../turbopack-env" } +turbopack-test-utils = { path = "../turbopack-test-utils" } [build-dependencies] turbo-tasks-build = { path = "../turbo-tasks-build" } diff --git a/crates/turbopack-tests/tests/snapshot.rs b/crates/turbopack-tests/tests/snapshot.rs index 1e833f075563a..0a9b7dec3064d 100644 --- a/crates/turbopack-tests/tests/snapshot.rs +++ b/crates/turbopack-tests/tests/snapshot.rs @@ -6,19 +6,16 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use once_cell::sync::Lazy; use serde::Deserialize; -use similar::TextDiff; use test_generator::test_resources; use turbo_tasks::{debug::ValueDebug, NothingVc, TryJoinIterExt, TurboTasks, Value, ValueToString}; use turbo_tasks_env::DotenvProcessEnvVc; use turbo_tasks_fs::{ - json::parse_json_with_source_context, util::sys_to_unix, DirectoryContent, DirectoryEntry, - DiskFileSystemVc, File, FileContent, FileSystem, FileSystemEntryType, FileSystemPathVc, - FileSystemVc, + json::parse_json_with_source_context, util::sys_to_unix, DiskFileSystemVc, FileSystem, + FileSystemPathVc, FileSystemVc, }; -use turbo_tasks_hash::encode_hex; use turbo_tasks_memory::MemoryBackend; use turbopack::{ condition::ContextCondition, @@ -29,7 +26,7 @@ use turbopack::{ ModuleAssetContextVc, }; use turbopack_core::{ - asset::{Asset, AssetContent, AssetContentVc, AssetVc}, + asset::{Asset, AssetVc}, chunk::{dev::DevChunkingContextVc, ChunkableAsset, ChunkableAssetVc}, compile_time_defines, compile_time_info::CompileTimeInfo, @@ -41,16 +38,13 @@ use turbopack_core::{ source_asset::SourceAssetVc, }; use turbopack_env::ProcessEnvAssetVc; +use turbopack_test_utils::snapshot::{diff, expected, matches_expected, snapshot_issues}; fn register() { turbopack::register(); include!(concat!(env!("OUT_DIR"), "/register_test_snapshot.rs")); } -// Updates the existing snapshot outputs with the actual outputs of this run. -// `UPDATE=1 cargo test -p turbopack -- test_my_pattern` -static UPDATE: Lazy = Lazy::new(|| env::var("UPDATE").unwrap_or_default() == "1"); - static WORKSPACE_ROOT: Lazy = Lazy::new(|| { let package_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); package_root @@ -104,16 +98,35 @@ async fn run(resource: &'static str) -> Result<()> { let tt = TurboTasks::new(MemoryBackend::default()); let task = tt.spawn_once_task(async move { let out = run_test(resource.to_string()); - handle_issues(out) - .await - .context("Unable to handle issues")?; + let captured_issues = IssueVc::peek_issues_with_path(out) + .await? + .strongly_consistent() + .await?; + + let plain_issues = captured_issues + .iter() + .map(|issue_vc| async move { + Ok(( + issue_vc.into_plain().await?, + issue_vc.into_plain().dbg().await?, + )) + }) + .try_join() + .await?; + + snapshot_issues( + plain_issues.into_iter(), + out.join("issues"), + &WORKSPACE_ROOT, + ) + .await + .context("Unable to handle issues")?; Ok(NothingVc::new().into()) }); tt.wait_task_completion(task, true).await?; Ok(()) } - #[turbo_tasks::function] async fn run_test(resource: String) -> Result { let test_path = Path::new(&resource) @@ -269,16 +282,6 @@ async fn run_test(resource: String) -> Result { Ok(path) } -async fn remove_file(path: FileSystemPathVc) -> Result<()> { - let fs = DiskFileSystemVc::resolve_from(path.fs()) - .await? - .context(anyhow!("unexpected fs type"))? - .await?; - let sys_path = fs.to_sys_path(path).await?; - fs::remove_file(&sys_path).context(format!("remove file {} error", sys_path.display()))?; - Ok(()) -} - async fn walk_asset( asset: AssetVc, seen: &mut HashSet, @@ -296,65 +299,6 @@ async fn walk_asset( Ok(()) } -async fn get_contents(file: AssetContentVc, path: FileSystemPathVc) -> Result> { - Ok( - match &*file.await.context(format!( - "Unable to read AssetContent of {}", - path.to_string().await? - ))? { - AssetContent::File(file) => match &*file.await.context(format!( - "Unable to read FileContent of {}", - path.to_string().await? - ))? { - FileContent::NotFound => None, - FileContent::Content(expected) => { - Some(expected.content().to_str()?.trim().to_string()) - } - }, - AssetContent::Redirect { target, link_type } => Some(format!( - "Redirect {{ target: {target}, link_type: {:?} }}", - link_type - )), - }, - ) -} - -async fn diff(path: FileSystemPathVc, actual: AssetContentVc) -> Result<()> { - let path_str = &path.await?.path; - let expected = path.read().into(); - - let actual = match get_contents(actual, path).await? { - Some(s) => s, - None => bail!("could not generate {} contents", path_str), - }; - let expected = get_contents(expected, path).await?; - - if Some(&actual) != expected.as_ref() { - if *UPDATE { - let content = File::from(actual).into(); - path.write(content).await?; - println!("updated contents of {}", path_str); - } else { - if expected.is_none() { - eprintln!("new file {path_str} detected:"); - } else { - eprintln!("contents of {path_str} did not match:"); - } - let expected = expected.unwrap_or_default(); - let diff = TextDiff::from_lines(&expected, &actual); - eprintln!( - "{}", - diff.unified_diff() - .context_radius(3) - .header("expected", "actual") - ); - bail!("contents of {path_str} did not match"); - } - } - - Ok(()) -} - async fn maybe_load_env( project_fs: FileSystemVc, path: &Path, @@ -373,95 +317,3 @@ async fn maybe_load_env( asset.as_ecmascript_chunk_placeable() ]))) } - -async fn expected(dir: FileSystemPathVc) -> Result> { - let mut expected = HashSet::new(); - let entries = dir.read_dir().await?; - if let DirectoryContent::Entries(entries) = &*entries { - for (file, entry) in entries { - match entry { - DirectoryEntry::File(file) => { - expected.insert(*file); - } - _ => bail!( - "expected file at {}, found {:?}", - file, - FileSystemEntryType::from(entry) - ), - } - } - } - Ok(expected) -} - -/// Values in left that are not in right. -/// FileSystemPathVc hashes as a Vc, not as the file path, so we need to get the -/// path to properly diff. -async fn diff_paths( - left: &HashSet, - right: &HashSet, -) -> Result> { - let mut map = left - .iter() - .map(|p| async move { Ok((p.await?.path.clone(), *p)) }) - .try_join() - .await? - .iter() - .cloned() - .collect::>(); - for p in right { - map.remove(&p.await?.path); - } - Ok(map.values().copied().collect()) -} - -async fn matches_expected( - expected: HashSet, - seen: HashSet, -) -> Result<()> { - for path in diff_paths(&expected, &seen).await? { - let p = &path.await?.path; - if *UPDATE { - remove_file(path).await?; - println!("removed file {}", p); - } else { - bail!("expected file {}, but it was not emitted", p); - } - } - Ok(()) -} - -async fn handle_issues(source: FileSystemPathVc) -> Result<()> { - let issues_path = source.join("issues"); - let expected_issues = expected(issues_path).await?; - - let mut seen = HashSet::new(); - let issues = IssueVc::peek_issues_with_path(source) - .await? - .strongly_consistent() - .await?; - - for issue in issues.iter() { - let plain_issue = issue.into_plain(); - let hash = encode_hex(*plain_issue.internal_hash().await?); - - // We replace "*" because it's not allowed for filename on Windows. - let path = issues_path.join(&format!( - "{}-{}.txt", - plain_issue.await?.title.replace('*', "__star__"), - &hash[0..6] - )); - seen.insert(path); - - // Annoyingly, the PlainIssue.source -> PlainIssueSource.asset -> - // PlainAsset.path -> FileSystemPath.fs -> DiskFileSystem.root changes - // for everyone. - let content = - format!("{}", plain_issue.dbg().await?).replace(&*WORKSPACE_ROOT, "WORKSPACE_ROOT"); - let asset = File::from(content).into(); - - diff(path, asset).await?; - } - - matches_expected(expected_issues, seen).await -}