diff --git a/packages/next-swc/crates/core/tests/errors/next-font-loaders/export-let/output.js b/packages/next-swc/crates/core/tests/errors/next-font-loaders/export-let/output.js index 796ecd5517111..9ddf24e397409 100644 --- a/packages/next-swc/crates/core/tests/errors/next-font-loaders/export-let/output.js +++ b/packages/next-swc/crates/core/tests/errors/next-font-loaders/export-let/output.js @@ -1,5 +1,5 @@ -import firaCode from '@next/font/google/target.css?{"arguments":[],"import":"Abel","path":"pages/test.tsx"}'; -import inter from '@next/font/google/target.css?{"arguments":[],"import":"Inter","path":"pages/test.tsx"}'; +import firaCode from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Abel","arguments":[]}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[]}'; import React from 'react'; export { firaCode }; export { inter }; diff --git a/packages/next-swc/crates/core/tests/errors/next-font-loaders/not-const/output.js b/packages/next-swc/crates/core/tests/errors/next-font-loaders/not-const/output.js index 2fcefbe750dec..f54f3cf2f8213 100644 --- a/packages/next-swc/crates/core/tests/errors/next-font-loaders/not-const/output.js +++ b/packages/next-swc/crates/core/tests/errors/next-font-loaders/not-const/output.js @@ -1,4 +1,4 @@ -import inter1 from '@next/font/google/target.css?{"arguments":[{"variant":"400"}],"import":"Inter","path":"pages/test.tsx"}'; -import inter2 from '@next/font/google/target.css?{"arguments":[{"variant":"400"}],"import":"Inter","path":"pages/test.tsx"}'; +import inter1 from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"400"}]}'; +import inter2 from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"400"}]}'; var i = 10; var i2 = 20; diff --git a/packages/next-swc/crates/core/tests/errors/next-font-loaders/options-object/output.js b/packages/next-swc/crates/core/tests/errors/next-font-loaders/options-object/output.js index 39bfd190fa534..a8c2866a10e41 100644 --- a/packages/next-swc/crates/core/tests/errors/next-font-loaders/options-object/output.js +++ b/packages/next-swc/crates/core/tests/errors/next-font-loaders/options-object/output.js @@ -1,7 +1,7 @@ -import a from '@next/font/google/target.css?{"arguments":[{}],"import":"ABeeZee","path":"pages/test.tsx"}'; -import a from '@next/font/google/target.css?{"arguments":[{}],"import":"ABeeZee","path":"pages/test.tsx"}'; -import a from '@next/font/google/target.css?{"arguments":[{}],"import":"ABeeZee","path":"pages/test.tsx"}'; -import a from '@next/font/google/target.css?{"arguments":[{}],"import":"ABeeZee","path":"pages/test.tsx"}'; +import a from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"ABeeZee","arguments":[{}]}'; +import a from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"ABeeZee","arguments":[{}]}'; +import a from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"ABeeZee","arguments":[{}]}'; +import a from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"ABeeZee","arguments":[{}]}'; const a = fn({ 10: 'hello' }); diff --git a/packages/next-swc/crates/core/tests/errors/next-font-loaders/spread-arg/output.js b/packages/next-swc/crates/core/tests/errors/next-font-loaders/spread-arg/output.js index e2c1faef27c6b..07ff833332e63 100644 --- a/packages/next-swc/crates/core/tests/errors/next-font-loaders/spread-arg/output.js +++ b/packages/next-swc/crates/core/tests/errors/next-font-loaders/spread-arg/output.js @@ -1,2 +1,2 @@ -import inter from '@next/font/google/target.css?{"arguments":[{},[]],"import":"Inter","path":"pages/test.tsx"}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{},[]]}'; const a = fn(...{}, ...[]); diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/default-import/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/default-import/output.js index ca8b3362b844d..d834e14dc28e6 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/default-import/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/default-import/output.js @@ -1 +1 @@ -import font from 'cool-fonts/target.css?{"arguments":[{"prop":true}],"import":"","path":"pages/test.tsx"}'; +import font from 'cool-fonts/target.css?{"path":"pages/test.tsx","import":"","arguments":[{"prop":true}]}'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/export-const/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/export-const/output.js index 796ecd5517111..9ddf24e397409 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/export-const/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/export-const/output.js @@ -1,5 +1,5 @@ -import firaCode from '@next/font/google/target.css?{"arguments":[],"import":"Abel","path":"pages/test.tsx"}'; -import inter from '@next/font/google/target.css?{"arguments":[],"import":"Inter","path":"pages/test.tsx"}'; +import firaCode from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Abel","arguments":[]}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[]}'; import React from 'react'; export { firaCode }; export { inter }; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/exports/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/exports/output.js index 251d06fab78b6..e011c842d8f2d 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/exports/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/exports/output.js @@ -1,5 +1,5 @@ -import firaCode from '@next/font/google/target.css?{"arguments":[],"import":"Abel","path":"pages/test.tsx"}'; -import inter from '@next/font/google/target.css?{"arguments":[],"import":"Inter","path":"pages/test.tsx"}'; +import firaCode from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Abel","arguments":[]}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[]}'; import React from 'react'; export { firaCode }; export default inter; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/font-options/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/font-options/output.js index 5a5a35b0a02f1..a1a92848f288b 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/font-options/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/font-options/output.js @@ -1,3 +1,3 @@ -import firaCode from '@next/font/google/target.css?{"arguments":[{"fallback":["system-ui",{"key":false},[]],"key":{"key2":{}},"preload":true,"variant":"400"}],"import":"Fira_Code","path":"pages/test.tsx"}'; +import firaCode from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Fira_Code","arguments":[{"variant":"400","fallback":["system-ui",{"key":false},[]],"preload":true,"key":{"key2":{}}}]}'; import React from 'react'; console.log(firaCode); diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/import-as/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/import-as/output.js index bc508f27fc169..b7dc24e494f83 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/import-as/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/import-as/output.js @@ -1,2 +1,2 @@ -import acme1 from 'cool-fonts/target.css?{"arguments":[{"variant":"400"}],"import":"Acme","path":"pages/test.tsx"}'; +import acme1 from 'cool-fonts/target.css?{"path":"pages/test.tsx","import":"Acme","arguments":[{"variant":"400"}]}'; import React from 'react'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/many-args/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/many-args/output.js index efdafef4fe6f7..b79734b7ba114 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/many-args/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/many-args/output.js @@ -1 +1 @@ -import geo from '@next/font/google/target.css?{"arguments":["test",[1.0],{"a":2.0},3.0],"import":"Geo","path":"pages/test.tsx"}'; +import geo from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Geo","arguments":["test",[1.0],{"a":2.0},3.0]}'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-calls/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-calls/output.js index 974f49f2c14b7..5541ae8803f11 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-calls/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-calls/output.js @@ -1,3 +1,3 @@ -import inter from '@next/font/google/target.css?{"arguments":[{"display":"swap","variant":"900"}],"import":"Inter","path":"pages/test.tsx"}'; -import inter from '@next/font/google/target.css?{"arguments":[{"display":"swap","variant":"900"}],"import":"Inter","path":"pages/test.tsx"}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"900","display":"swap"}]}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"900","display":"swap"}]}'; import React from 'react'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-font-downloaders/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-font-downloaders/output.js index 7b6b047ebaca5..3c6e3c26b2367 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-font-downloaders/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-font-downloaders/output.js @@ -1,3 +1,3 @@ -import inter from '@next/font/google/target.css?{"arguments":[{"variant":"900"}],"import":"Inter","path":"pages/test.tsx"}'; -import fira from 'cool-fonts/target.css?{"arguments":[{"display":"swap","variant":"400"}],"import":"Fira_Code","path":"pages/test.tsx"}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"900"}]}'; +import fira from 'cool-fonts/target.css?{"path":"pages/test.tsx","import":"Fira_Code","arguments":[{"variant":"400","display":"swap"}]}'; import React from 'react'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-fonts/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-fonts/output.js index f561b311d5565..086c584518d4d 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-fonts/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-fonts/output.js @@ -1,3 +1,3 @@ -import firaCode from '@next/font/google/target.css?{"arguments":[{"fallback":["system-ui"],"variant":"400"}],"import":"Fira_Code","path":"pages/test.tsx"}'; -import inter from '@next/font/google/target.css?{"arguments":[{"display":"swap","variant":"900"}],"import":"Inter","path":"pages/test.tsx"}'; +import firaCode from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Fira_Code","arguments":[{"variant":"400","fallback":["system-ui"]}]}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"900","display":"swap"}]}'; import React from 'react'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-imports/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-imports/output.js index 6243e848ff5c8..040502437700b 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-imports/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/multiple-imports/output.js @@ -1,3 +1,3 @@ -import inter from '@next/font/google/target.css?{"arguments":[{"variant":"900"}],"import":"Inter","path":"pages/test.tsx"}'; -import fira from '@next/font/google/target.css?{"arguments":[{"display":"swap","variant":"400"}],"import":"Fira_Code","path":"pages/test.tsx"}'; +import inter from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Inter","arguments":[{"variant":"900"}]}'; +import fira from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Fira_Code","arguments":[{"variant":"400","display":"swap"}]}'; import React from 'react'; diff --git a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/no-args/output.js b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/no-args/output.js index 6c690950480ca..389ee70328dd5 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-font-loaders/no-args/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-font-loaders/no-args/output.js @@ -1 +1 @@ -import fira from '@next/font/google/target.css?{"arguments":[],"import":"Fira_Code","path":"pages/test.tsx"}'; +import fira from '@next/font/google/target.css?{"path":"pages/test.tsx","import":"Fira_Code","arguments":[]}'; diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index daa5da4893068..97f307d1eaa73 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -24,7 +24,7 @@ sentry_rustls = ["_sentry_rustls"] anyhow = "1.0" backtrace = "0.3" fxhash = "0.2.1" -napi = {version = "2", default-features = false, features = ["napi3", "serde-json"]} +napi = { version = "2", default-features = false, features = ["napi3", "serde-json", "tokio_rt", "error_anyhow"] } napi-derive = "2" next-swc = {version = "0.0.0", path = "../core"} once_cell = "1.13.0" @@ -55,6 +55,13 @@ tracing = { version = "0.1.32", features = ["release_max_level_info"] } tracing-futures = "0.2.5" tracing-subscriber = "0.3.9" tracing-chrome = "0.5.0" +owo-colors = "3" +turbo-tasks = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb" } +turbo-tasks-memory = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb" } +turbopack-core = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb" } +turbopack-dev-server = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb" } +next-dev = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb" } +node-file-trace = { git = "https://github.com/vercel/turbo.git", rev = "56039940cdfac07b68e030da1396214962e99ceb", default-features = false, features = ["node-api"] } # There are few build targets we can't use native-tls which default features rely on, # allow to specify alternative (rustls) instead via features. # Note to opt in rustls default-features should be disabled @@ -67,6 +74,7 @@ _sentry_rustls = { package = "sentry", version = "0.27.0", default-features = fa "rustls", "reqwest" ], optional = true } +indexmap = "=1.6.2" [build-dependencies] napi-build = "2" diff --git a/packages/next-swc/crates/napi/src/lib.rs b/packages/next-swc/crates/napi/src/lib.rs index 1960ecdaf9a75..71014f892c3dd 100644 --- a/packages/next-swc/crates/napi/src/lib.rs +++ b/packages/next-swc/crates/napi/src/lib.rs @@ -26,6 +26,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#![feature(future_join)] #![recursion_limit = "2048"] //#![deny(clippy::all)] @@ -47,6 +48,8 @@ use swc_core::{ pub mod minify; pub mod parse; pub mod transform; +pub mod turbo_tracing; +pub mod turbopack; pub mod util; static COMPILER: Lazy> = Lazy::new(|| { diff --git a/packages/next-swc/crates/napi/src/turbo_tracing.rs b/packages/next-swc/crates/napi/src/turbo_tracing.rs new file mode 100644 index 0000000000000..05b046ab5f7fb --- /dev/null +++ b/packages/next-swc/crates/napi/src/turbo_tracing.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use napi::bindgen_prelude::*; +use node_file_trace::{start, Args}; + +#[napi] +pub async fn run_turbo_tracing(options: Buffer) -> napi::Result> { + let args: Args = serde_json::from_slice(options.as_ref())?; + let files = start(Arc::new(args)).await?; + Ok(files) +} diff --git a/packages/next-swc/crates/napi/src/turbopack.rs b/packages/next-swc/crates/napi/src/turbopack.rs new file mode 100644 index 0000000000000..6b4c0e3d6d23a --- /dev/null +++ b/packages/next-swc/crates/napi/src/turbopack.rs @@ -0,0 +1,157 @@ +use std::{ + future::join, + net::{IpAddr, Ipv4Addr}, + path::PathBuf, + time::{Duration, Instant}, +}; + +use crate::util::MapErr; +use napi::bindgen_prelude::*; +use next_dev::{register, NextDevServerBuilder}; +use owo_colors::OwoColorize; +use serde::Deserialize; +use turbo_tasks::{util::FormatDuration, TurboTasks}; +use turbo_tasks_memory::MemoryBackend; +use turbopack_core::issue::IssueSeverity; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(unused)] +struct TurboDevServerOptions { + #[serde(default = "default_port")] + port: u16, + + #[serde(default = "default_host")] + hostname: IpAddr, + + #[serde(default)] + eager_compile: bool, + + #[serde(default)] + log_level: Option, + + #[serde(default)] + show_all: bool, + + #[serde(default)] + log_detail: bool, + + #[serde(default = "default_dir")] + dir: PathBuf, + + #[serde(default = "default_dir")] + root_dir: PathBuf, + + #[serde(default)] + allow_retry: bool, + + #[serde(default)] + dev: bool, + + #[serde(default)] + is_next_dev_command: bool, + + #[serde(default)] + server_components_external_packages: Vec, +} + +fn default_port() -> u16 { + 3000 +} + +fn default_host() -> IpAddr { + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) +} + +fn default_dir() -> PathBuf { + std::env::current_dir().expect("Current dir should be accessible") +} + +async fn start_server(options: TurboDevServerOptions) -> napi::Result<()> { + let start = Instant::now(); + + register(); + + let tt = TurboTasks::new(MemoryBackend::new()); + let tt_clone = tt.clone(); + + let dir = options + .dir + .canonicalize() + .expect("Cannot canonicalize project directory") + .to_str() + .expect("project directory contains invalid characters") + .to_string(); + + let root_dir = options + .root_dir + .canonicalize() + .expect("Cannot canonicalize project directory") + .to_str() + .expect("project directory contains invalid characters") + .to_string(); + + //server_component_external + + let mut server = NextDevServerBuilder::new(tt, dir, root_dir) + .entry_request("src/index".into()) + .eager_compile(options.eager_compile) + .hostname(options.hostname) + .port(options.port) + .log_detail(options.log_detail) + .show_all(options.show_all) + .log_level( + options + .log_level + .map_or_else(|| IssueSeverity::Warning, |l| l), + ); + + for package in options.server_components_external_packages { + server = server.server_component_external(package); + } + + let server = server.build().await.convert_err()?; + + let index_uri = if server.addr.ip().is_loopback() || server.addr.ip().is_unspecified() { + format!("http://localhost:{}", server.addr.port()) + } else { + format!("http://{}", server.addr) + }; + println!( + "{} - started server on {}:{}, url: {}", + "ready".green(), + server.addr.ip(), + server.addr.port(), + index_uri + ); + + let stats_future = async move { + println!( + "{event_type} - initial compilation {start}", + event_type = "event".purple(), + start = FormatDuration(start.elapsed()), + ); + + loop { + let (elapsed, _count) = tt_clone + .get_or_wait_update_info(Duration::from_millis(100)) + .await; + println!( + "{event_type} - updated in {elapsed}", + event_type = "event".purple(), + elapsed = FormatDuration(elapsed), + ); + } + }; + + join!(stats_future, async { server.future.await.unwrap() }).await; + + Ok(()) +} + +#[napi] +pub async fn start_turbo_dev(options: Buffer) -> napi::Result<()> { + let options: TurboDevServerOptions = serde_json::from_slice(&options)?; + + start_server(options).await +} diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 4c26a58b8fa12..13d3a7ad8a0f2 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -112,6 +112,7 @@ import { lockfilePatchPromise, teardownTraceSubscriber, teardownCrashReporter, + loadBindings, } from './swc' import { injectedClientEntries } from './webpack/plugins/flight-client-entry-plugin' import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex' @@ -1606,8 +1607,18 @@ export default async function build( } if (config.outputFileTracing) { - const { nodeFileTrace } = - require('next/dist/compiled/@vercel/nft') as typeof import('next/dist/compiled/@vercel/nft') + let nodeFileTrace: any + if (config.experimental.turbotrace) { + let binding = (await loadBindings()) as any + if (!binding?.isWasm) { + nodeFileTrace = binding.turbo?.startTrace + } + } + + if (!nodeFileTrace) { + nodeFileTrace = + require('next/dist/compiled/@vercel/nft').nodeFileTrace + } const includeExcludeSpan = nextBuildSpan.traceChild( 'apply-include-excludes' @@ -1751,53 +1762,64 @@ export default async function build( ) } - const serverResult = await nodeFileTrace(toTrace, { - base: root, - processCwd: dir, - ignore: [ - '**/next/dist/pages/**/*', - '**/next/dist/compiled/webpack/(bundle4|bundle5).js', - '**/node_modules/webpack5/**/*', - '**/next/dist/server/lib/squoosh/**/*.wasm', - ...(ciEnvironment.hasNextSupport - ? [ - // only ignore image-optimizer code when - // this is being handled outside of next-server - '**/next/dist/server/image-optimizer.js', - '**/node_modules/sharp/**/*', - ] - : []), - ...(!hasSsrAmpPages - ? ['**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*'] - : []), - ], - }) + let serverResult: import('next/dist/compiled/@vercel/nft').NodeFileTraceResult + if (config.experimental.turbotrace) { + // handle the cache in the turbo-tracing side in the future + await nodeFileTrace({ + action: 'annotate', + input: toTrace, + contextDirectory: root, + }) + } else { + serverResult = await nodeFileTrace(toTrace, { + base: root, + processCwd: dir, + ignore: [ + '**/next/dist/pages/**/*', + '**/next/dist/compiled/webpack/(bundle4|bundle5).js', + '**/node_modules/webpack5/**/*', + '**/next/dist/server/lib/squoosh/**/*.wasm', + ...(ciEnvironment.hasNextSupport + ? [ + // only ignore image-optimizer code when + // this is being handled outside of next-server + '**/next/dist/server/image-optimizer.js', + '**/node_modules/sharp/**/*', + ] + : []), + ...(!hasSsrAmpPages + ? [ + '**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*', + ] + : []), + ], + }) + const tracedFiles = new Set() - const tracedFiles = new Set() + serverResult.fileList.forEach((file) => { + tracedFiles.add( + path + .relative(distDir, path.join(root, file)) + .replace(/\\/g, '/') + ) + }) - serverResult.fileList.forEach((file) => { - tracedFiles.add( - path - .relative(distDir, path.join(root, file)) - .replace(/\\/g, '/') + await promises.writeFile( + nextServerTraceOutput, + JSON.stringify({ + version: 1, + cacheKey, + files: [...tracedFiles], + } as { + version: number + files: string[] + }) ) - }) - - await promises.writeFile( - nextServerTraceOutput, - JSON.stringify({ - version: 1, - cacheKey, - files: [...tracedFiles], - } as { - version: number - files: string[] - }) - ) - await promises.unlink(cachedTracePath).catch(() => {}) - await promises - .copyFile(nextServerTraceOutput, cachedTracePath) - .catch(() => {}) + await promises.unlink(cachedTracePath).catch(() => {}) + await promises + .copyFile(nextServerTraceOutput, cachedTracePath) + .catch(() => {}) + } }) } diff --git a/packages/next/build/swc/index.js b/packages/next/build/swc/index.js index 16692eb7956d5..56be55263b25c 100644 --- a/packages/next/build/swc/index.js +++ b/packages/next/build/swc/index.js @@ -207,9 +207,12 @@ async function loadWasm(importPath = '') { getTargetTriple() { return undefined }, - diagnostics: { - startDiagnostics: () => { - Log.error('Wasm binding does not support --diagnostics yet') + turbo: { + startDev: () => { + Log.error('Wasm binding does not support --turbo yet') + }, + startTrace: () => { + Log.error('Wasm binding does not support trace yet') }, }, } @@ -338,9 +341,10 @@ function loadNative() { initCustomTraceSubscriber: bindings.initCustomTraceSubscriber, teardownTraceSubscriber: bindings.teardownTraceSubscriber, teardownCrashReporter: bindings.teardownCrashReporter, - diagnostics: { - startDiagnostics: (options) => - bindings.startDiagnostics(toBuffer(options)), + turbo: { + startDev: (options) => bindings.startTurboDev(toBuffer(options)), + startTrace: (options = {}) => + bindings.runTurboTracing(toBuffer({ exact: true, ...options })), }, } return nativeBindings diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 6647d7dfa0222..a51abb5acbac9 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -93,6 +93,20 @@ const BABEL_CONFIG_FILES = [ 'babel.config.cjs', ] +export const getBabelConfigFile = async (dir: string) => { + const babelConfigFile = await BABEL_CONFIG_FILES.reduce( + async (memo: Promise, filename) => { + const configFilePath = path.join(dir, filename) + return ( + (await memo) || + ((await fileExists(configFilePath)) ? configFilePath : undefined) + ) + }, + Promise.resolve(undefined) + ) + return babelConfigFile +} + // Support for NODE_PATH const nodePathList = (process.env.NODE_PATH || '') .split(process.platform === 'win32' ? ';' : ':') @@ -611,17 +625,7 @@ export default async function getBaseWebpackConfig( } } - const babelConfigFile = await BABEL_CONFIG_FILES.reduce( - async (memo: Promise, filename) => { - const configFilePath = path.join(dir, filename) - return ( - (await memo) || - ((await fileExists(configFilePath)) ? configFilePath : undefined) - ) - }, - Promise.resolve(undefined) - ) - + const babelConfigFile = await getBabelConfigFile(dir) const distDir = path.join(dir, config.distDir) let useSWCLoader = !babelConfigFile || config.experimental.forceSwcTransforms @@ -1994,6 +1998,7 @@ export default async function getBaseWebpackConfig( esmExternals: config.experimental.esmExternals, outputFileTracingRoot: config.experimental.outputFileTracingRoot, appDirEnabled: hasAppDir, + turbotrace: config.experimental.turbotrace, } ), // Moment.js is an extremely popular library that bundles large locale files diff --git a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts index 1654e68e9329d..9f09f5883afb5 100644 --- a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -1,4 +1,5 @@ import nodePath from 'path' +import nodeFs from 'fs' import { Span } from '../../../trace' import { spans } from './profiling-plugin' import isError from '../../../lib/is-error' @@ -14,6 +15,7 @@ import { resolveExternal, } from '../../webpack-config' import { NextConfigComplete } from '../../../server/config-shared' +import { loadBindings } from '../../swc' const PLUGIN_NAME = 'TraceEntryPointsPlugin' const TRACE_IGNORES = [ @@ -89,6 +91,10 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { private entryTraces: Map> private excludeFiles: string[] private esmExternals?: NextConfigComplete['experimental']['esmExternals'] + private turbotrace?: NextConfigComplete['experimental']['turbotrace'] + private chunksToTrace: string[] = [] + private turbotraceOutputPath?: string + private turbotraceFiles?: string[] constructor({ appDir, @@ -96,12 +102,14 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { excludeFiles, esmExternals, outputFileTracingRoot, + turbotrace, }: { appDir: string appDirEnabled?: boolean excludeFiles?: string[] outputFileTracingRoot?: string esmExternals?: NextConfigComplete['experimental']['esmExternals'] + turbotrace?: NextConfigComplete['experimental']['turbotrace'] }) { this.appDir = appDir this.entryTraces = new Map() @@ -109,6 +117,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { this.appDirEnabled = appDirEnabled this.excludeFiles = excludeFiles || [] this.tracingRoot = outputFileTracingRoot || appDir + this.turbotrace = turbotrace } // Here we output all traced assets and webpack chunks to a @@ -151,6 +160,18 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { entryFilesMap.set(entrypoint, entryFiles) } + // startTrace existed and callable + if (this.turbotrace) { + let binding = (await loadBindings()) as any + if ( + !binding?.isWasm && + typeof binding.turbo.startTrace === 'function' + ) { + this.chunksToTrace = [...chunksToTrace] + return + } + } + const result = await nodeFileTrace([...chunksToTrace], { base: this.tracingRoot, processCwd: this.appDir, @@ -355,6 +376,62 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { entriesToTrace.push(...curExtraEntries.keys()) } }) + // startTrace existed and callable + if (this.turbotrace) { + let binding = (await loadBindings()) as any + if ( + !binding?.isWasm && + typeof binding.turbo.startTrace === 'function' + ) { + await finishModulesSpan + .traceChild('turbo-trace', { + traceEntryCount: entriesToTrace.length + '', + }) + .traceAsyncFn(async () => { + const contextDirectory = + this.turbotrace?.contextDirectory ?? this.tracingRoot + const filesTracedInEntries: string[] = + await binding.turbo.startTrace({ + action: 'print', + input: entriesToTrace, + contextDirectory, + processCwd: this.turbotrace?.processCwd ?? this.appDir, + }) + // only trace the assets under the appDir + // exclude files from node_modules, entries and processed by webpack + const filesTracedFromEntries = filesTracedInEntries + .map((f) => nodePath.join(contextDirectory, f)) + .filter( + (f) => + !f.includes('/node_modules/') && + f.startsWith(this.appDir) && + !entriesToTrace.includes(f) && + !depModMap.has(f) + ) + if (!filesTracedFromEntries.length) { + return + } + + // The turbo trace doesn't provide the traced file type and reason at present + // let's write the traced files into the first [entry].nft.json + const [[, entryName]] = Array.from( + entryNameMap.entries() + ).filter(([k]) => k.startsWith(this.appDir)) + const outputPath = compilation.outputOptions.path! + const traceOutputPath = nodePath.join( + outputPath, + `../${entryName}.js.nft.json` + ) + const traceOutputDir = nodePath.dirname(traceOutputPath) + + this.turbotraceOutputPath = traceOutputPath + this.turbotraceFiles = filesTracedFromEntries.map((file) => + nodePath.relative(traceOutputDir, file) + ) + }) + return + } + } let fileList: Set let reasons: NodeFileTraceReasons await finishModulesSpan @@ -670,5 +747,49 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { ) }) }) + + if (this.turbotrace) { + compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => { + const compilationSpan = spans.get(compilation) || spans.get(compiler)! + const traceEntrypointsPluginSpan = compilationSpan.traceChild( + 'next-trace-entrypoint-plugin' + ) + const turbotraceAfterEmitSpan = traceEntrypointsPluginSpan.traceChild( + 'after-emit-turbo-trace' + ) + await turbotraceAfterEmitSpan.traceAsyncFn(async () => { + let binding = (await loadBindings()) as any + if ( + !binding?.isWasm && + typeof binding.turbo.startTrace === 'function' + ) { + await binding.turbo.startTrace({ + action: 'annotate', + input: this.chunksToTrace, + contextDirectory: + this.turbotrace?.contextDirectory ?? this.tracingRoot, + processCwd: this.turbotrace?.processCwd ?? this.appDir, + }) + if (this.turbotraceOutputPath && this.turbotraceFiles) { + const existedNftFile = await nodeFs.promises + .readFile(this.turbotraceOutputPath, 'utf8') + .then((content) => JSON.parse(content)) + .catch(() => ({ + version: TRACE_OUTPUT_VERSION, + files: [], + })) + console.log(this.turbotraceOutputPath, this.turbotraceFiles) + existedNftFile.files.push(...this.turbotraceFiles) + const filesSet = new Set(existedNftFile.files) + existedNftFile.files = [...filesSet] + nodeFs.promises.writeFile( + this.turbotraceOutputPath, + JSON.stringify(existedNftFile) + ) + } + } + }) + }) + } } } diff --git a/packages/next/cli/next-dev.ts b/packages/next/cli/next-dev.ts index 5cfe4320c88a7..7a5f882148eb6 100755 --- a/packages/next/cli/next-dev.ts +++ b/packages/next/cli/next-dev.ts @@ -10,7 +10,7 @@ import isError from '../lib/is-error' import { getProjectDir } from '../lib/get-project-dir' import { CONFIG_FILES } from '../shared/lib/constants' import path from 'path' -import { loadBindings } from '../build/swc' +import type { NextConfig } from '../types' const nextDev: cliCommand = (argv) => { const validArgs: arg.Spec = { @@ -18,7 +18,12 @@ const nextDev: cliCommand = (argv) => { '--help': Boolean, '--port': Number, '--hostname': String, - '--diagnostics': Boolean, + '--turbo': Boolean, + + // To align current messages with native binary. + // Will need to adjust subcommand later. + '--show-all': Boolean, + '--root': String, // Aliases '-h': '--help', @@ -96,20 +101,192 @@ const nextDev: cliCommand = (argv) => { port, } - if (args['--diagnostics']) { - Log.warn('running diagnostics...') + if (args['--turbo']) { + // check for postcss, babelrc, swc plugins + return new Promise(async (resolve) => { + const { findConfigPath } = + require('../lib/find-config') as typeof import('../lib/find-config') + const { loadBindings } = + require('../build/swc') as typeof import('../build/swc') + const { getPkgManager } = + require('../lib/helpers/get-pkg-manager') as typeof import('../lib/helpers/get-pkg-manager') + const { getBabelConfigFile } = + require('../build/webpack-config') as typeof import('../build/webpack-config') + const { defaultConfig } = + require('../server/config-shared') as typeof import('../server/config-shared') + const { default: loadConfig } = + require('../server/config') as typeof import('../server/config') + const { PHASE_DEVELOPMENT_SERVER } = + require('../shared/lib/constants') as typeof import('../shared/lib/constants') + const chalk = + require('next/dist/compiled/chalk') as typeof import('next/dist/compiled/chalk') + + // To regenerate the TURBOPACK gradient require('gradient-string')('blue', 'red')('>>> TURBOPACK') + const isTTY = process.stdout.isTTY + + const turbopackGradient = `${chalk.bold( + isTTY + ? '\x1B[38;2;0;0;255m>\x1B[39m\x1B[38;2;23;0;232m>\x1B[39m\x1B[38;2;46;0;209m>\x1B[39m \x1B[38;2;70;0;185mT\x1B[39m\x1B[38;2;93;0;162mU\x1B[39m\x1B[38;2;116;0;139mR\x1B[39m\x1B[38;2;139;0;116mB\x1B[39m\x1B[38;2;162;0;93mO\x1B[39m\x1B[38;2;185;0;70mP\x1B[39m\x1B[38;2;209;0;46mA\x1B[39m\x1B[38;2;232;0;23mC\x1B[39m\x1B[38;2;255;0;0mK\x1B[39m' + : '>>> TURBOPACK' + )} ${chalk.dim('(alpha)')}\n\n` + + let thankYouMsg = `Thank you for trying Next.js v13 with Turbopack! As a reminder,\nTurbopack is currently in alpha and not yet ready for production.\nWe appreciate your ongoing support as we work to make it ready\nfor everyone.\n` + + let unsupportedParts = '' + // TODO: warning for postcss mentioning sidecar + let babelrc = await getBabelConfigFile(dir) + if (babelrc) babelrc = path.basename(babelrc) - loadBindings().then((bindings: any) => { - const packagePath = require('next/dist/compiled/find-up').sync( - 'package.json' + const rawNextConfig = (await loadConfig( + PHASE_DEVELOPMENT_SERVER, + dir, + undefined, + true + )) as NextConfig + + const hasNonDefaultConfig = Object.keys(rawNextConfig).some( + (configKey) => { + if (!(configKey in defaultConfig)) return false + if (typeof defaultConfig[configKey] !== 'object') { + return defaultConfig[configKey] !== rawNextConfig[configKey] + } + return ( + JSON.stringify(rawNextConfig[configKey]) !== + JSON.stringify(defaultConfig[configKey]) + ) + } ) - let r = bindings.diagnostics.startDiagnostics({ - ...devServerOptions, - rootDir: path.dirname(packagePath), - }) - // Start preflight after server is listening and ignore errors: - preflight().catch(() => {}) - return r + const findUp = + require('next/dist/compiled/find-up') as typeof import('next/dist/compiled/find-up') + const packagePath = findUp.sync('package.json', { cwd: dir }) + let hasSideCar = false + + if (packagePath) { + const pkgData = require(packagePath) + hasSideCar = Object.values( + (pkgData.scripts || {}) as Record + ).some( + (script) => script.includes('tailwind') || script.includes('postcss') + ) + } + let postcssFile = !hasSideCar && (await findConfigPath(dir, 'postcss')) + let tailwindFile = !hasSideCar && (await findConfigPath(dir, 'tailwind')) + + if (postcssFile) postcssFile = path.basename(postcssFile) + if (tailwindFile) tailwindFile = path.basename(tailwindFile) + + const hasWarningOrError = + tailwindFile || postcssFile || babelrc || hasNonDefaultConfig + if (!hasWarningOrError) { + thankYouMsg = chalk.dim(thankYouMsg) + } + console.log(turbopackGradient + thankYouMsg) + + let feedbackMessage = `Learn more about Next.js v13 and Turbopack: ${chalk.underline( + 'https://nextjs.link/with-turbopack' + )}\nPlease direct feedback to: ${chalk.underline( + 'https://nextjs.link/turbopack-feedback' + )}\n` + + if (!hasWarningOrError) { + feedbackMessage = chalk.dim(feedbackMessage) + } + + if (babelrc) { + unsupportedParts += `\n- Babel detected (${chalk.cyan( + babelrc + )})\n ${chalk.dim( + `Babel is not yet supported. To use Turbopack at the moment,\n you'll need to remove your usage of Babel.` + )}` + } + if (hasNonDefaultConfig) { + unsupportedParts += `\n\n- Unsupported Next.js configuration option(s) (${chalk.cyan( + 'next.config.js' + )})\n ${chalk.dim( + `The only configurations options supported are:\n - ${chalk.cyan( + 'experimental.serverComponentsExternalPackages' + )}\n - ${chalk.cyan( + 'experimental.transpilePackages' + )}\n To use Turbopack, remove other configuration options.` + )} ` + } + + if (postcssFile || tailwindFile) { + console.warn( + `${chalk.bold.yellow( + 'Warning:' + )} You are using configuration that may require additional\nsetup with Turbopack. If you already made these changes please\nignore this warning.\n` + ) + } + + if (postcssFile) { + console.warn( + `- PostCSS detected (${chalk.cyan(postcssFile)})\n` + + ` ${chalk.dim( + 'PostCSS is not yet supported by Next.js v13 with Turbopack.\n To use with Turbopack, see: https://nextjs.link/turbopack-postcss' + )}\n` + ) + } + + if (tailwindFile) { + console.warn( + `- Tailwind detected (${chalk.cyan(tailwindFile)})\n` + + ` ${chalk.dim( + 'Tailwind is not yet supported by Next.js v13 with Turbopack.\n To use with Turbopack, see: https://nextjs.link/turbopack-tailwind' + )}\n` + ) + } + + if (unsupportedParts) { + const pkgManager = getPkgManager(dir) + + console.error( + `${chalk.bold.red( + 'Error:' + )} You are using configuration and/or tools that are not yet\nsupported by Next.js v13 with Turbopack:\n${unsupportedParts}\n +If you cannot make the changes above, but still want to try out\nNext.js v13 with Turbopack, create the Next.js v13 playground app\nby running the following commands: + + ${chalk.bold.cyan( + `${ + pkgManager === 'npm' + ? 'npx create-next-app' + : `${pkgManager} create next-app` + } --example with-turbopack with-turbopack-app` + )}\n cd with-turbopack-app\n ${pkgManager} run dev + ` + ) + console.warn(feedbackMessage) + process.exit(1) + } + console.log(feedbackMessage) + + loadBindings() + .then((bindings: any) => { + // eslint-disable-next-line no-shadow + const findUp = + require('next/dist/compiled/find-up') as typeof import('next/dist/compiled/find-up') + const turboJson = findUp.sync('turbo.json', { cwd: dir }) + // eslint-disable-next-line no-shadow + const packagePath = findUp.sync('package.json', { cwd: dir }) + + let server = bindings.turbo.startDev({ + ...devServerOptions, + showAll: args['--show-all'] ?? false, + rootDir: + args['--root'] ?? + (turboJson + ? path.dirname(turboJson) + : packagePath + ? path.dirname(packagePath) + : undefined), + serverComponentsExternalPackages: + rawNextConfig.experimental?.serverComponentsExternalPackages, + }) + // Start preflight after server is listening and ignore errors: + preflight().catch(() => {}) + return server + }) + .then(resolve) }) } else { startServer(devServerOptions) diff --git a/packages/next/lib/find-config.ts b/packages/next/lib/find-config.ts index d53fe8fb2dfb8..4033452bb916d 100644 --- a/packages/next/lib/find-config.ts +++ b/packages/next/lib/find-config.ts @@ -6,12 +6,33 @@ type RecursivePartial = { [P in keyof T]?: RecursivePartial } +export function findConfigPath( + dir: string, + key: string +): Promise { + // If we didn't find the configuration in `package.json`, we should look for + // known filenames. + return findUp( + [ + `.${key}rc.json`, + `${key}.config.json`, + `.${key}rc.js`, + `${key}.config.js`, + `${key}.config.cjs`, + ], + { + cwd: dir, + } + ) +} + // We'll allow configuration to be typed, but we force everything provided to // become optional. We do not perform any schema validation. We should maybe // force all the types to be `unknown` as well. export async function findConfig( directory: string, - key: string + key: string, + _returnFile?: boolean ): Promise | null> { // `package.json` configuration always wins. Let's check that first. const packageJsonPath = await findUp('package.json', { cwd: directory }) @@ -22,20 +43,8 @@ export async function findConfig( } } - // If we didn't find the configuration in `package.json`, we should look for - // known filenames. - const filePath = await findUp( - [ - `.${key}rc.json`, - `${key}.config.json`, - `.${key}rc.js`, - `${key}.config.js`, - `${key}.config.cjs`, - ], - { - cwd: directory, - } - ) + const filePath = await findConfigPath(directory, key) + if (filePath) { if (filePath.endsWith('.js') || filePath.endsWith('.cjs')) { return require(filePath) diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index c6460aacec82b..149226f59da42 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -432,6 +432,33 @@ const configSchema = { enum: ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'], } as any, }, + turbotrace: { + type: 'object', + properties: { + logLevel: { + type: 'string', + enum: [ + 'bug', + 'fatal', + 'error', + 'warning', + 'hint', + 'note', + 'suggestions', + 'info', + ], + } as any, + logDetail: { + type: 'boolean', + }, + contextDirectory: { + type: 'string', + }, + processCwd: { + type: 'string', + }, + }, + }, }, type: 'object', }, diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index de58b999d6bcb..0c952bd7de22e 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -165,6 +165,20 @@ export interface ExperimentalConfig { fontLoaders?: Array<{ loader: string; options?: any }> webVitalsAttribution?: Array + turbotrace?: { + logLevel?: + | 'bug' + | 'fatal' + | 'error' + | 'warning' + | 'hint' + | 'note' + | 'suggestions' + | 'info' + logDetail?: boolean + contextDirectory?: string + processCwd?: string + } } export type ExportPathMap = { @@ -581,6 +595,7 @@ export const defaultConfig: NextConfig = { enableUndici: false, adjustFontFallbacks: false, adjustFontFallbacksWithSizeAdjust: false, + turbotrace: undefined, }, } diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index cdc4cc02620d6..018de2e5432d3 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -789,7 +789,8 @@ function assignDefaults(dir: string, userConfig: { [key: string]: any }) { export default async function loadConfig( phase: string, dir: string, - customConfig?: object | null + customConfig?: object | null, + rawConfig?: boolean ): Promise { await loadEnvConfig(dir, phase === PHASE_DEVELOPMENT_SERVER, Log) loadWebpackHook() @@ -823,6 +824,10 @@ export default async function loadConfig( } else { userConfigModule = await import(pathToFileURL(path).href) } + + if (rawConfig) { + return userConfigModule + } } catch (err) { Log.error( `Failed to load ${configFileName}, see more info here https://nextjs.org/docs/messages/next-config-error` diff --git a/test/integration/build-trace-extra-entries-turbo/app/content/hello.json b/test/integration/build-trace-extra-entries-turbo/app/content/hello.json new file mode 100644 index 0000000000000..f2a886f39de7d --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/content/hello.json @@ -0,0 +1,3 @@ +{ + "hello": "world" +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/include-me/hello.txt b/test/integration/build-trace-extra-entries-turbo/app/include-me/hello.txt new file mode 100644 index 0000000000000..fe4f02ad058b4 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/include-me/hello.txt @@ -0,0 +1 @@ +first \ No newline at end of file diff --git a/test/integration/build-trace-extra-entries-turbo/app/include-me/second.txt b/test/integration/build-trace-extra-entries-turbo/app/include-me/second.txt new file mode 100644 index 0000000000000..2147e41889511 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/include-me/second.txt @@ -0,0 +1 @@ +second \ No newline at end of file diff --git a/test/integration/build-trace-extra-entries-turbo/app/lib/fetch-data.js b/test/integration/build-trace-extra-entries-turbo/app/lib/fetch-data.js new file mode 100644 index 0000000000000..3134727c11c89 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/lib/fetch-data.js @@ -0,0 +1,12 @@ +import fs from 'fs' +import path from 'path' + +const getCmsData = require('some-cms') + +try { + fs.readdirSync(path.join(process.cwd(), 'public/exclude-me')) +} catch (_) {} + +export function fetchData() { + return getCmsData() +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/lib/get-data.js b/test/integration/build-trace-extra-entries-turbo/app/lib/get-data.js new file mode 100644 index 0000000000000..1ae25f9d78599 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/lib/get-data.js @@ -0,0 +1,8 @@ +import fs from 'fs' +import path from 'path' + +export function getData() { + return JSON.parse( + fs.readFileSync(path.join(process.cwd(), 'content/hello.json'), 'utf8') + ) +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/next.config.js b/test/integration/build-trace-extra-entries-turbo/app/next.config.js new file mode 100644 index 0000000000000..00d9a5ac4e363 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/next.config.js @@ -0,0 +1,27 @@ +const path = require('path') + +module.exports = { + webpack(cfg, { isServer, nextRuntime }) { + console.log(cfg.entry) + const origEntry = cfg.entry + cfg.entry = async () => { + const origEntries = await origEntry() + + if (isServer && nextRuntime === 'nodejs') { + const curEntry = origEntries['pages/_app'] + origEntries['pages/_app'] = [ + path.join(__dirname, 'lib/get-data.js'), + ...curEntry, + ] + console.log(origEntries) + } + return origEntries + } + return cfg + }, + experimental: { + turbotrace: { + contextDirectory: path.join(__dirname, '..', '..', '..', '..'), + }, + }, +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/constants/package.json b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/constants/package.json new file mode 100644 index 0000000000000..ecccc5d84801d --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/constants/package.json @@ -0,0 +1,4 @@ +{ + "name": "constants", + "main": "../dist/constants" +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/constants.js b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/constants.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/index.js b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/dist/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/package.json b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/package.json new file mode 100644 index 0000000000000..b2e7f4a5c8ca2 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/node_modules/nested-structure/package.json @@ -0,0 +1,4 @@ +{ + "name": "nested-structure", + "main": "./dist/index.js" +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/index.js b/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/index.js new file mode 100644 index 0000000000000..ce3b888523549 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/index.js @@ -0,0 +1,5 @@ +function getCmsData() { + return 'hello' +} + +module.exports = getCmsData diff --git a/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/package.json b/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/package.json new file mode 100644 index 0000000000000..5a348ce17878b --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/node_modules/some-cms/package.json @@ -0,0 +1,4 @@ +{ + "name": "some-cms", + "main": "./index.js" +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/pages/another.js b/test/integration/build-trace-extra-entries-turbo/app/pages/another.js new file mode 100644 index 0000000000000..7e102c0924652 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/pages/another.js @@ -0,0 +1,11 @@ +import 'nested-structure/constants' + +export default function Page() { + return

another page

+} + +export function getStaticProps() { + return { + props: {}, + } +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/pages/image-import.js b/test/integration/build-trace-extra-entries-turbo/app/pages/image-import.js new file mode 100644 index 0000000000000..65b93d1aaa4c4 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/pages/image-import.js @@ -0,0 +1,24 @@ +import fs from 'fs' +import path from 'path' +import Image from 'next/image' +import testImage from '../public/test.jpg' + +export default function Page(props) { + return ( +
+ test +
+ ) +} + +export function getServerSideProps() { + try { + // this should be included in the trace since it's not an + // import + fs.readFileSync(path.join(process.cwd(), 'public/another.jpg')) + } catch (_) {} + + return { + props: {}, + } +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/pages/index.js b/test/integration/build-trace-extra-entries-turbo/app/pages/index.js new file mode 100644 index 0000000000000..1c7cd2073a181 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/pages/index.js @@ -0,0 +1,18 @@ +import { fetchData } from '../lib/fetch-data' + +export const config = { + unstable_includeFiles: ['include-me/*'], + unstable_excludeFiles: ['public/exclude-me/**/*'], +} + +export default function Page() { + return 'index page' +} + +export function getStaticProps() { + fetchData() + + return { + props: {}, + } +} diff --git a/test/integration/build-trace-extra-entries-turbo/app/public/another.jpg b/test/integration/build-trace-extra-entries-turbo/app/public/another.jpg new file mode 100644 index 0000000000000..d536c882412ed Binary files /dev/null and b/test/integration/build-trace-extra-entries-turbo/app/public/another.jpg differ diff --git a/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/another.txt b/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/another.txt new file mode 100644 index 0000000000000..2147e41889511 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/another.txt @@ -0,0 +1 @@ +second \ No newline at end of file diff --git a/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/hello.txt b/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/hello.txt new file mode 100644 index 0000000000000..fe4f02ad058b4 --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/app/public/exclude-me/hello.txt @@ -0,0 +1 @@ +first \ No newline at end of file diff --git a/test/integration/build-trace-extra-entries-turbo/app/public/test.jpg b/test/integration/build-trace-extra-entries-turbo/app/public/test.jpg new file mode 100644 index 0000000000000..d536c882412ed Binary files /dev/null and b/test/integration/build-trace-extra-entries-turbo/app/public/test.jpg differ diff --git a/test/integration/build-trace-extra-entries-turbo/test/index.test.js b/test/integration/build-trace-extra-entries-turbo/test/index.test.js new file mode 100644 index 0000000000000..9cbcfc20ce0fa --- /dev/null +++ b/test/integration/build-trace-extra-entries-turbo/test/index.test.js @@ -0,0 +1,73 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { nextBuild } from 'next-test-utils' + +const appDir = join(__dirname, '../app') + +describe('build trace with extra entries', () => { + it('should build and trace correctly', async () => { + const result = await nextBuild(appDir, undefined, { + cwd: appDir, + stderr: true, + stdout: true, + }) + console.log(result) + expect(result.code).toBe(0) + + const appTrace = await fs.readJSON( + join(appDir, '.next/server/pages/_app.js.nft.json') + ) + const indexTrace = await fs.readJSON( + join(appDir, '.next/server/pages/index.js.nft.json') + ) + const anotherTrace = await fs.readJSON( + join(appDir, '.next/server/pages/another.js.nft.json') + ) + const imageTrace = await fs.readJSON( + join(appDir, '.next/server/pages/image-import.js.nft.json') + ) + + const tracedFiles = [ + ...appTrace.files, + ...indexTrace.files, + ...anotherTrace.files, + ...imageTrace.files, + ] + + expect(tracedFiles.some((file) => file.endsWith('hello.json'))).toBe(true) + expect(tracedFiles.some((file) => file.includes('some-cms/index.js'))).toBe( + true + ) + expect( + tracedFiles.some((file) => file === '../../../include-me/hello.txt') + ).toBe(true) + expect( + tracedFiles.some((file) => file === '../../../include-me/second.txt') + ).toBe(true) + expect(indexTrace.files.some((file) => file.includes('exclude-me'))).toBe( + false + ) + + expect( + tracedFiles.some((file) => + file.includes('nested-structure/constants/package.json') + ) + ).toBe(true) + expect( + tracedFiles.some((file) => file.includes('nested-structure/package.json')) + ).toBe(true) + expect( + tracedFiles.some((file) => + file.includes('nested-structure/dist/constants.js') + ) + ).toBe(true) + expect( + tracedFiles.some((file) => file.includes('public/another.jpg')) + ).toBe(true) + expect(tracedFiles.some((file) => file.includes('public/test.jpg'))).toBe( + false + ) + }) +})