fix Next.rs API (#53456)

### What?

* fixes problems in Next.rs API introduced by #52846 
* adds test infrastructure for experimental turbo testing
* adds two test cases to verify the infrastructure
* add grouping of output logs in run-tests
* simplify template loading

### Why?

### How?
This commit is contained in:
Tobias Koppers 2023-08-02 14:31:52 +02:00 committed by GitHub
parent eecd8dc146
commit 61baae126f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1850 additions and 1745 deletions

View file

@ -125,6 +125,15 @@ jobs:
afterBuild: turbo run rust-check
secrets: inherit
test-experimental-turbopack-dev:
name: test experimental turbopack dev
needs: ['build-native', 'build-next']
uses: ./.github/workflows/build_reusable.yml
with:
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-tests-manifest.js" EXPERIMENTAL_TURBOPACK=1 NEXT_E2E_TEST_TIMEOUT=240000 NEXT_TEST_MODE=dev node run-tests.js --test-pattern '^(test\/development)/.*\.test\.(js|jsx|ts|tsx)$' --timings -c ${TEST_CONCURRENCY}
secrets: inherit
test-turbopack-dev:
name: test turbopack dev
needs: ['build-native', 'build-next']
@ -134,6 +143,21 @@ jobs:
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/packages/next-swc/crates/next-dev-tests/tests-manifest.js" TURBOPACK=1 __INTERNAL_CUSTOM_TURBOPACK_BINDINGS="$(pwd)/packages/next-swc/native/next-swc.linux-x64-gnu.node" NEXT_E2E_TEST_TIMEOUT=240000 NEXT_TEST_MODE=dev node run-tests.js --test-pattern '^(test\/development)/.*\.test\.(js|jsx|ts|tsx)$' --timings -c ${TEST_CONCURRENCY}
secrets: inherit
test-experimental-turbopack-integration:
name: test experimental turbopack integration
needs: ['build-native', 'build-next']
strategy:
fail-fast: false
matrix:
group: [1]
uses: ./.github/workflows/build_reusable.yml
with:
nodeVersion: 16
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-tests-manifest.js" EXPERIMENTAL_TURBOPACK=1 node run-tests.js --timings -g ${{ matrix.group }}/1 -c ${TEST_CONCURRENCY} --type integration
secrets: inherit
test-turbopack-integration:
name: test turbopack integration
needs: ['build-native', 'build-next']
@ -148,7 +172,6 @@ jobs:
skipForDocsOnly: 'yes'
afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/packages/next-swc/crates/next-dev-tests/tests-manifest.js" TURBOPACK=1 __INTERNAL_CUSTOM_TURBOPACK_BINDINGS="$(pwd)/packages/next-swc/native/next-swc.linux-x64-gnu.node" node run-tests.js --timings -g ${{ matrix.group }}/5 -c ${TEST_CONCURRENCY} --type integration
secrets: inherit
test-next-swc-wasm:
name: test next-swc wasm
needs: ['build-native', 'build-next']
@ -244,7 +267,9 @@ jobs:
'rust-check',
'test-next-swc-wasm',
'test-turbopack-dev',
'test-experimental-turbopack-dev',
'test-turbopack-integration',
'test-experimental-turbopack-integration',
]
if: always()

View file

@ -451,6 +451,7 @@ impl AppEndpoint {
loader_tree,
self.app_project.app_dir(),
self.pathname.clone(),
self.original_name.clone(),
self.app_project.project().project_path(),
)
}
@ -462,6 +463,7 @@ impl AppEndpoint {
self.app_project.edge_rsc_module_context(),
Vc::upcast(FileSource::new(path)),
self.pathname.clone(),
self.original_name.clone(),
self.app_project.project().project_path(),
)
}

View file

@ -641,7 +641,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_module_context(),
this.pages_project.edge_ssr_module_context(),
this.pages_project.project().ssr_chunking_context(),
@ -660,7 +660,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server-data".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_data_module_context(),
this.pages_project.edge_ssr_data_module_context(),
this.pages_project.project().ssr_data_chunking_context(),
@ -681,7 +681,7 @@ impl PageEndpoint {
.project()
.node_root()
.join("server".to_string()),
this.path.root(),
this.pages_project.project().project_path(),
this.pages_project.ssr_module_context(),
this.pages_project.edge_ssr_module_context(),
this.pages_project.project().ssr_chunking_context(),

View file

@ -190,7 +190,7 @@ pub async fn get_app_entries(
.map(|(pathname, entrypoint)| async move {
Ok(match entrypoint {
Entrypoint::AppPage {
original_name: _,
original_name,
loader_tree,
} => get_app_page_entry(
rsc_context,
@ -199,10 +199,11 @@ pub async fn get_app_entries(
*loader_tree,
app_dir,
pathname.clone(),
original_name.clone(),
project_root,
),
Entrypoint::AppRoute {
original_name: _,
original_name,
path,
} => get_app_route_entry(
rsc_context,
@ -210,6 +211,7 @@ pub async fn get_app_entries(
rsc_context,
Vc::upcast(FileSource::new(*path)),
pathname.clone(),
original_name.clone(),
project_root,
),
})

View file

@ -85,6 +85,7 @@ pub async fn get_app_route_favicon_entry(
Vc::upcast(source),
// TODO(alexkirsz) Get this from the metadata?
"/favicon.ico".to_string(),
"/favicon.ico".to_string(),
project_root,
))
}

View file

@ -6,7 +6,7 @@ use turbopack_binding::{
turbo::tasks_fs::{rope::RopeBuilder, File, FileSystemPath},
turbopack::{
core::{
asset::AssetContent, context::AssetContext, issue::IssueExt, module::Module,
asset::AssetContent, context::AssetContext, issue::IssueExt,
reference_type::ReferenceType, virtual_source::VirtualSource,
},
ecmascript::{chunk::EcmascriptChunkPlaceable, utils::StringifyJs},
@ -22,7 +22,7 @@ use crate::{
next_app::UnsupportedDynamicMetadataIssue,
next_server_component::NextServerComponentTransition,
parse_segment_config_from_loader_tree,
util::{load_next_js, resolve_next_module, NextRuntime},
util::{load_next_js_template, virtual_next_js_template_path, NextRuntime},
};
/// Computes the entry for a Next.js app page.
@ -33,6 +33,7 @@ pub async fn get_app_page_entry(
loader_tree: Vc<LoaderTree>,
app_dir: Vc<FileSystemPath>,
pathname: String,
original_name: String,
project_root: Vc<FileSystemPath>,
) -> Result<Vc<AppEntry>> {
let config = parse_segment_config_from_loader_tree(loader_tree, Vc::upcast(nodejs_context));
@ -77,12 +78,12 @@ pub async fn get_app_page_entry(
let pages = pages.iter().map(|page| page.to_string()).try_join().await?;
let original_name = get_original_page_name(&pathname);
let original_page_name = get_original_page_name(&original_name);
let template_file = "/dist/esm/build/webpack/loaders/next-route-loader/templates/app-page.js";
let template_file = "build/webpack/loaders/next-route-loader/templates/app-page.js";
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -96,7 +97,7 @@ pub async fn get_app_page_entry(
)
.replace(
"\"VAR_ORIGINAL_PATHNAME\"",
&StringifyJs(&original_name).to_string(),
&StringifyJs(&original_page_name).to_string(),
)
// TODO(alexkirsz) Support custom global error.
.replace(
@ -129,13 +130,7 @@ pub async fn get_app_page_entry(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
@ -152,7 +147,7 @@ pub async fn get_app_page_entry(
Ok(AppEntry {
pathname: pathname.to_string(),
original_name,
original_name: original_page_name,
rsc_entry,
config,
}

View file

@ -7,7 +7,6 @@ use turbopack_binding::{
core::{
asset::AssetContent,
context::AssetContext,
module::Module,
reference_type::{
EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType,
},
@ -22,7 +21,7 @@ use turbopack_binding::{
use crate::{
next_app::AppEntry,
parse_segment_config_from_source,
util::{load_next_js, resolve_next_module, NextRuntime},
util::{load_next_js_template, virtual_next_js_template_path, NextRuntime},
};
/// Computes the entry for a Next.js app route.
@ -32,6 +31,7 @@ pub async fn get_app_route_entry(
edge_context: Vc<ModuleAssetContext>,
source: Vc<Box<dyn Source>>,
pathname: String,
original_name: String,
project_root: Vc<FileSystemPath>,
) -> Result<Vc<AppEntry>> {
let config = parse_segment_config_from_source(
@ -49,13 +49,13 @@ pub async fn get_app_route_entry(
let mut result = RopeBuilder::default();
let original_name = get_original_route_name(&pathname);
let original_page_name = get_original_route_name(&original_name);
let path = source.ident().path();
let template_file = "/dist/esm/build/webpack/loaders/next-route-loader/templates/app-route.js";
let template_file = "build/webpack/loaders/next-route-loader/templates/app-route.js";
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -78,7 +78,7 @@ pub async fn get_app_route_entry(
)
.replace(
"\"VAR_ORIGINAL_PATHNAME\"",
&StringifyJs(&original_name).to_string(),
&StringifyJs(&original_page_name).to_string(),
)
.replace(
"\"VAR_RESOLVED_PAGE_PATH\"",
@ -98,13 +98,7 @@ pub async fn get_app_route_entry(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let virtual_source = VirtualSource::new(template_path, AssetContent::file(file.into()));
@ -132,7 +126,7 @@ pub async fn get_app_route_entry(
Ok(AppEntry {
pathname: pathname.to_string(),
original_name,
original_name: original_page_name,
rsc_entry,
config,
}

View file

@ -20,7 +20,7 @@ use crate::{
issue::NextFontIssue,
util::{get_scoped_font_family, FontFamilyType},
},
util::load_next_json,
util::load_next_js_templateon,
};
/// An entry in the Google fonts metrics map
@ -54,8 +54,11 @@ pub(super) async fn get_font_fallback(
Ok(match &options.fallback {
Some(fallback) => FontFallback::Manual(Vc::cell(fallback.clone())).cell(),
None => {
let metrics_json =
load_next_json(context, "/dist/server/capsize-font-metrics.json").await?;
let metrics_json = load_next_js_templateon(
context,
"dist/server/capsize-font-metrics.json".to_string(),
)
.await?;
let fallback = lookup_fallback(
&options.font_family,
metrics_json,

View file

@ -48,7 +48,7 @@ use super::{
get_request_hash, get_request_id, get_scoped_font_family, FontCssProperties, FontFamilyType,
},
};
use crate::{embed_js::next_js_file_path, util::load_next_json};
use crate::{embed_js::next_js_file_path, util::load_next_js_templateon};
pub mod font_fallback;
pub mod options;
@ -266,9 +266,9 @@ impl ImportMappingReplacement for NextFontGoogleCssModuleReplacer {
#[turbo_tasks::function]
async fn load_font_data(project_root: Vc<FileSystemPath>) -> Result<Vc<FontData>> {
let data: FontData = load_next_json(
let data: FontData = load_next_js_templateon(
project_root,
"/dist/compiled/@next/font/dist/google/font-data.json",
"dist/compiled/@next/font/dist/google/font-data.json".to_string(),
)
.await?;

View file

@ -229,6 +229,16 @@ pub async fn get_next_server_import_map(
ServerContextType::AppSSR { .. }
| ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. } => {
match mode {
NextMode::Development | NextMode::Build => {
import_map.insert_wildcard_alias("next/dist/server/", external);
import_map.insert_wildcard_alias("next/dist/shared/", external);
}
NextMode::DevServer => {
// The sandbox can't be bundled and needs to be external
import_map.insert_exact_alias("next/dist/server/web/sandbox", external);
}
}
import_map.insert_exact_alias(
"next/head",
request_to_import_mapping(project_path, "next/dist/client/components/noop-head"),
@ -237,9 +247,6 @@ pub async fn get_next_server_import_map(
"next/dynamic",
request_to_import_mapping(project_path, "next/dist/shared/lib/app-dynamic"),
);
// The sandbox can't be bundled and needs to be external
import_map.insert_exact_alias("next/dist/server/web/sandbox", external);
}
ServerContextType::Middleware => {}
}
@ -620,17 +627,19 @@ async fn package_lookup_resolve_options(
}
#[turbo_tasks::function]
pub async fn get_next_package(project_path: Vc<FileSystemPath>) -> Result<Vc<FileSystemPath>> {
pub async fn get_next_package(context_directory: Vc<FileSystemPath>) -> Result<Vc<FileSystemPath>> {
let result = resolve(
project_path,
context_directory,
Request::parse(Value::new(Pattern::Constant(
"next/package.json".to_string(),
))),
package_lookup_resolve_options(project_path),
package_lookup_resolve_options(context_directory),
);
let assets = result.primary_sources().await?;
let asset = *assets.first().context("Next.js package not found")?;
Ok(asset.ident().path().parent())
let source = result
.first_source()
.await?
.context("Next.js package not found")?;
Ok(source.ident().path().parent())
}
pub async fn insert_alias_option<const N: usize>(

View file

@ -13,7 +13,6 @@ use turbopack_binding::{
core::{
asset::AssetContent,
context::AssetContext,
module::Module,
reference_type::{EntryReferenceSubType, ReferenceType},
source::Source,
virtual_source::VirtualSource,
@ -22,7 +21,7 @@ use turbopack_binding::{
},
};
use crate::util::{load_next_js, resolve_next_module};
use crate::util::{load_next_js_template, virtual_next_js_template_path};
#[turbo_tasks::function]
pub async fn create_page_ssr_entry_module(
@ -43,17 +42,17 @@ pub async fn create_page_ssr_entry_module(
let template_file = match reference_type {
ReferenceType::Entry(EntryReferenceSubType::Page) => {
// Load the Page entry file.
"/dist/esm/build/webpack/loaders/next-route-loader/templates/pages.js"
"build/webpack/loaders/next-route-loader/templates/pages.js"
}
ReferenceType::Entry(EntryReferenceSubType::PagesApi) => {
// Load the Pages API entry file.
"/dist/esm/build/webpack/loaders/next-route-loader/templates/pages-api.js"
"build/webpack/loaders/next-route-loader/templates/pages-api.js"
}
_ => bail!("Invalid path type"),
};
// Load the file from the next.js codebase.
let file = load_next_js(project_root, template_file).await?.await?;
let file = load_next_js_template(project_root, template_file.to_string()).await?;
let mut file = file
.to_str()?
@ -103,13 +102,7 @@ pub async fn create_page_ssr_entry_module(
let file = File::from(result.build());
let resolve_result = resolve_next_module(project_root, template_file).await?;
let Some(template_path) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let template_path = template_path.ident().path();
let template_path = virtual_next_js_template_path(project_root, template_file.to_string());
let source = VirtualSource::new(template_path, AssetContent::file(file.into()));

View file

@ -2,22 +2,16 @@ use anyhow::{bail, Result};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use swc_core::ecma::ast::Program;
use turbo_tasks::{trace::TraceRawVcs, TaskInput, Value, ValueDefault, ValueToString, Vc};
use turbo_tasks::{trace::TraceRawVcs, TaskInput, ValueDefault, ValueToString, Vc};
use turbo_tasks_fs::rope::Rope;
use turbopack_binding::{
turbo::tasks_fs::{json::parse_json_rope_with_source_context, FileContent, FileSystemPath},
turbopack::{
core::{
asset::Asset,
environment::{ServerAddr, ServerInfo},
ident::AssetIdent,
issue::{Issue, IssueExt, IssueSeverity, OptionIssueSource},
issue::{Issue, IssueExt, IssueSeverity},
module::Module,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
resolve::{
self, handle_resolve_error, node::node_cjs_resolve_options, parse::Request,
ModuleResolveResult,
},
},
ecmascript::{
analyzer::{JsValue, ObjectPart},
@ -28,7 +22,10 @@ use turbopack_binding::{
},
};
use crate::next_config::{NextConfig, OutputType};
use crate::{
next_config::{NextConfig, OutputType},
next_import_map::get_next_package,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TaskInput)]
pub enum PathType {
@ -333,14 +330,16 @@ fn parse_config_from_js_value(module: Vc<Box<dyn Module>>, value: &JsValue) -> N
config
}
pub async fn load_next_js(context: Vc<FileSystemPath>, path: &str) -> Result<Vc<Rope>> {
let resolve_result = resolve_next_module(context, path).await?;
#[turbo_tasks::function]
pub async fn load_next_js_template(
project_path: Vc<FileSystemPath>,
path: String,
) -> Result<Vc<Rope>> {
let file_path = get_next_package(project_path)
.join("dist/esm".to_string())
.join(path);
let Some(js_asset) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let content = &*js_asset.content().file_content().await?;
let content = &*file_path.read().await?;
let FileContent::Content(file) = content else {
bail!("Expected file content for file");
@ -349,44 +348,23 @@ pub async fn load_next_js(context: Vc<FileSystemPath>, path: &str) -> Result<Vc<
Ok(file.content().to_owned().cell())
}
pub async fn resolve_next_module(
context: Vc<FileSystemPath>,
path: &str,
) -> Result<Vc<ModuleResolveResult>> {
let request = Request::module(
"next".to_owned(),
Value::new(path.to_string().into()),
Vc::cell(None),
);
let resolve_options = node_cjs_resolve_options(context.root());
let resolve_result = handle_resolve_error(
resolve::resolve(context, request, resolve_options).as_raw_module_result(),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
context,
request,
resolve_options,
OptionIssueSource::none(),
IssueSeverity::Error.cell(),
)
.await?;
Ok(resolve_result)
#[turbo_tasks::function]
pub fn virtual_next_js_template_path(
project_path: Vc<FileSystemPath>,
path: String,
) -> Vc<FileSystemPath> {
get_next_package(project_path)
.join("dist/esm".to_string())
.join(path)
}
pub async fn load_next_json<T: DeserializeOwned>(
context: Vc<FileSystemPath>,
path: &str,
pub async fn load_next_js_templateon<T: DeserializeOwned>(
project_path: Vc<FileSystemPath>,
path: String,
) -> Result<T> {
let resolve_result = resolve_next_module(context, path).await?;
let file_path = get_next_package(project_path).join(path);
let Some(metrics_asset) = *resolve_result.first_module().await? else {
bail!("Expected to find module");
};
let content = &*metrics_asset.content().file_content().await?;
let content = &*file_path.read().await?;
let FileContent::Content(file) = content else {
bail!("Expected file content for metrics data");

View file

@ -1,7 +1,11 @@
import type { LoaderTree } from '../../../../../server/lib/app-dir-module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/app-page/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { AppPageRouteModule } from '../../../../../server/future/route-modules/app-page/module'
const AppPageRouteModule =
module.AppPageRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-page/module').AppPageRouteModule
// These are injected by the loader afterwards.
declare const tree: LoaderTree

View file

@ -1,14 +1,16 @@
import '../../../../../server/node-polyfill-headers'
import {
AppRouteRouteModule,
type AppRouteRouteModuleOptions,
} from '../../../../../server/future/route-modules/app-route/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/app-route/module'
import type { AppRouteRouteModuleOptions } from '../../../../../server/future/route-modules/app-route/module'
import { RouteKind } from '../../../../../server/future/route-kind'
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'
const AppRouteRouteModule =
module.AppRouteRouteModule as unknown as typeof import('../../../../../server/future/route-modules/app-route/module').AppRouteRouteModule
// These are injected by the loader afterwards. This is injected as a variable
// instead of a replacement because this could also be `undefined` instead of
// an empty string.

View file

@ -1,7 +1,11 @@
import { PagesAPIRouteModule } from '../../../../../server/future/route-modules/pages-api/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/pages-api/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { hoist } from '../helpers'
const PagesAPIRouteModule =
module.PagesAPIRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages-api/module').PagesAPIRouteModule
// Import the userland code.
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'

View file

@ -1,4 +1,5 @@
import { PagesRouteModule } from '../../../../../server/future/route-modules/pages/module'
// @ts-ignore this need to be imported from next/dist to be external
import * as module from 'next/dist/server/future/route-modules/pages/module'
import { RouteKind } from '../../../../../server/future/route-kind'
import { hoist } from '../helpers'
@ -12,6 +13,9 @@ import App from 'VAR_MODULE_APP'
// @ts-expect-error - replaced by webpack/turbopack loader
import * as userland from 'VAR_USERLAND'
const PagesRouteModule =
module.PagesRouteModule as unknown as typeof import('../../../../../server/future/route-modules/pages/module').PagesRouteModule
// Re-export the component (should be the default export).
export default hoist(userland, 'default')

View file

@ -12,6 +12,9 @@ const { createNextInstall } = require('./test/lib/create-next-install')
const glob = promisify(_glob)
const exec = promisify(execOrig)
const GROUP = process.env.CI ? '##[group]' : ''
const ENDGROUP = process.env.CI ? '##[endgroup]' : ''
// Try to read an external array-based json to filter tests to be allowed / or disallowed.
// If process.argv contains a test to be executed, this'll append it to the list.
const externalTestsFilterLists = process.env.NEXT_EXTERNAL_TESTS_FILTERS
@ -272,7 +275,9 @@ async function main() {
return cleanUpAndExit(1)
}
console.log('Running tests:', '\n', ...testNames.map((name) => `${name}\n`))
console.log(`${GROUP}Running tests:
${testNames.join('\n')}
${ENDGROUP}`)
const hasIsolatedTests = testNames.some((test) => {
return configuredTestTypes.some(
@ -288,7 +293,7 @@ async function main() {
// for isolated next tests: e2e, dev, prod we create
// a starter Next.js install to re-use to speed up tests
// to avoid having to run yarn each time
console.log('Creating Next.js install for isolated tests')
console.log(`${GROUP}Creating Next.js install for isolated tests`)
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
const { installDir, pkgPaths, tmpRepoDir } = await createNextInstall({
parentSpan: mockTrace(),
@ -307,9 +312,11 @@ async function main() {
process.env.NEXT_TEST_PKG_PATHS = JSON.stringify(serializedPkgPaths)
process.env.NEXT_TEST_TEMP_REPO = tmpRepoDir
process.env.NEXT_TEST_STARTER = installDir
console.log(`${ENDGROUP}`)
}
const sema = new Sema(concurrency, { capacity: testNames.length })
const outputSema = new Sema(1, { capacity: testNames.length })
const children = new Set()
const jestPath = path.join(
__dirname,
@ -374,7 +381,7 @@ async function main() {
if (hideOutput) {
outputChunks.push({ type, chunk })
} else {
process.stderr.write(chunk)
process.stdout.write(chunk)
}
}
child.stdout.on('data', handleOutput('stdout'))
@ -386,20 +393,22 @@ async function main() {
children.delete(child)
if (code !== 0 || signal !== null) {
if (hideOutput) {
await outputSema.acquire()
process.stdout.write(`${GROUP}${test} output\n`)
// limit out to last 64kb so that we don't
// run out of log room in CI
outputChunks.forEach(({ type, chunk }) => {
if (type === 'stdout') {
for (const { chunk } of outputChunks) {
process.stdout.write(chunk)
} else {
process.stderr.write(chunk)
}
})
process.stdout.write(`end of ${test} output\n${ENDGROUP}\n`)
outputSema.release()
}
const err = new Error(
code ? `failed with code: ${code}` : `failed with signal: ${signal}`
)
err.output = outputChunks.map((chunk) => chunk.toString()).join('')
err.output = outputChunks
.map(({ chunk }) => chunk.toString())
.join('')
return reject(err)
}
@ -498,11 +507,15 @@ async function main() {
if ((!passed || shouldContinueTestsOnError) && isTestJob) {
try {
const testsOutput = await fs.readFile(`${test}${RESULTS_EXT}`, 'utf8')
await outputSema.acquire()
if (GROUP) console.log(`${GROUP}Result as JSON for tooling`)
console.log(
`--test output start--`,
testsOutput,
`--test output end--`
)
if (ENDGROUP) console.log(ENDGROUP)
outputSema.release()
} catch (err) {
console.log(`Failed to load test output`, err)
}

View file

@ -5,7 +5,9 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
// TODO-APP: Investigate snapshot mismatch
describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox app %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
@ -143,4 +145,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
await cleanup()
})
})
}
)

View file

@ -5,7 +5,9 @@ import { check, describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox app %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
@ -937,4 +939,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
await cleanup()
}
)
})
}
)

View file

@ -5,7 +5,9 @@ import { check, describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe.each(['default', 'turbo'])('Error recovery app %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'Error recovery app %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
@ -492,4 +494,5 @@ describe.each(['default', 'turbo'])('Error recovery app %s', () => {
await cleanup()
})
})
}
)

View file

@ -4,7 +4,9 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
@ -218,4 +220,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
expect(await session.hasRedbox(false)).toBe(false)
await cleanup()
})
})
}
)

View file

@ -4,7 +4,9 @@ import { describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
@ -146,4 +148,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
await cleanup()
})
})
}
)

View file

@ -5,7 +5,9 @@ import { describeVariants as describe } from 'next-test-utils'
import path from 'path'
import { outdent } from 'outdent'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
@ -722,4 +724,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
await cleanup()
})
})
}
)

View file

@ -5,7 +5,9 @@ import { check, describeVariants as describe } from 'next-test-utils'
import { outdent } from 'outdent'
import path from 'path'
describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
describe.each(['default', 'turbo', 'experimentalTurbo'])(
'ReactRefreshLogBox %s',
() => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
@ -474,4 +476,5 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
await cleanup()
})
})
}
)

View file

@ -26,7 +26,10 @@ import type { RequestInit, Response } from 'node-fetch'
import type { NextServer } from 'next/dist/server/next'
import type { BrowserInterface } from './browsers/base'
import { shouldRunTurboDevTest } from './turbo'
import {
shouldRunExperimentalTurboDevTest,
shouldRunTurboDevTest,
} from './turbo'
export { shouldRunTurboDevTest }
@ -327,6 +330,7 @@ export interface NextDevOptions {
bootupMarker?: RegExp
nextStart?: boolean
turbo?: boolean
experimentalTurbo?: boolean
stderr?: false
stdout?: false
@ -374,12 +378,19 @@ export function runNextCommandDev(
const bootupMarkers = {
dev: /compiled .*successfully/i,
turbo: /started server/i,
experimentalTurbo: /started server/i,
start: /started server/i,
}
if (
(opts.bootupMarker && opts.bootupMarker.test(message)) ||
bootupMarkers[
opts.nextStart || stdOut ? 'start' : opts?.turbo ? 'turbo' : 'dev'
opts.nextStart || stdOut
? 'start'
: opts?.experimentalTurbo
? 'experimentalTurbo'
: opts?.turbo
? 'turbo'
: 'dev'
].test(message)
) {
if (!didResolve) {
@ -434,6 +445,7 @@ export function launchApp(
) {
const options = opts ?? {}
const useTurbo = shouldRunTurboDevTest()
const useExperimentalTurbo = shouldRunExperimentalTurboDevTest()
return runNextCommandDev(
[useTurbo ? '--turbo' : undefined, dir, '-p', port as string].filter(
@ -443,6 +455,7 @@ export function launchApp(
{
...options,
turbo: useTurbo,
experimentalTurbo: useExperimentalTurbo,
}
)
}
@ -1008,23 +1021,30 @@ export function findAllTelemetryEvents(output: string, eventName: string) {
return events.filter((e) => e.eventName === eventName).map((e) => e.payload)
}
type TestVariants = 'default' | 'turbo'
type TestVariants = 'default' | 'turbo' | 'experimentalTurbo'
// WEB-168: There are some differences / incompletes in turbopack implementation enforces jest requires to update
// test snapshot when run against turbo. This fn returns describe, or describe.skip dependes on the running context
// to avoid force-snapshot update per each runs until turbopack update includes all the changes.
export function getSnapshotTestDescribe(variant: TestVariants) {
const runningEnv = variant ?? 'default'
if (runningEnv !== 'default' && runningEnv !== 'turbo') {
if (
runningEnv !== 'default' &&
runningEnv !== 'turbo' &&
runningEnv !== 'experimentalTurbo'
) {
throw new Error(
`An invalid test env was passed: ${variant} (only "default" and "turbo" are valid options)`
`An invalid test env was passed: ${variant} (only "default", "turbo" and "experimentalTurbo" are valid options)`
)
}
const shouldRunTurboDev = shouldRunTurboDevTest()
const shouldRunExperimentalTurboDev = shouldRunExperimentalTurboDevTest()
const shouldSkip =
(runningEnv === 'turbo' && !shouldRunTurboDev) ||
(runningEnv === 'default' && shouldRunTurboDev)
(runningEnv === 'experimentalTurbo' && !shouldRunExperimentalTurboDev) ||
(runningEnv === 'default' &&
(shouldRunTurboDev || shouldRunExperimentalTurboDev))
return shouldSkip ? describe.skip : describe
}

View file

@ -1,4 +1,5 @@
let loggedTurbopack = false
let loggedExperimentalTurbopack = false
/**
* Utility function to determine if a given test case needs to run with --turbo.
@ -24,3 +25,28 @@ export function shouldRunTurboDevTest(): boolean {
return shouldRunTurboDev
}
/**
* Utility function to determine if a given test case needs to run with --experimental-turbo.
*
* This is primarily for the gradual test enablement with latest turbopack upstream changes.
*
* Note: it could be possible to dynamically create test cases itself (createDevTest(): it.each([...])), but
* it makes hard to conform with existing lint rules. Instead, starting off from manual fixture setup and
* update test cases accordingly as turbopack changes enable more test cases.
*/
export function shouldRunExperimentalTurboDevTest(): boolean {
if (!!process.env.TEST_WASM) {
return false
}
const shouldRunExperimentalTurboDev = !!process.env.EXPERIMENTAL_TURBOPACK
if (shouldRunExperimentalTurboDev && !loggedExperimentalTurbopack) {
require('console').log(
`Running tests with experimental turbopack because environment variable EXPERIMENTAL_TURBOPACK is set`
)
loggedExperimentalTurbopack = true
}
return shouldRunExperimentalTurboDev
}

View file

@ -0,0 +1,9 @@
// Tests that are currently enabled with experimental Turbopack in CI.
// Only tests that are actively testing against Turbopack should
// be enabled here
const enabledTests = [
'test/development/api-cors-with-rewrite/index.test.ts',
'test/integration/bigint/test/index.test.js',
]
module.exports = { enabledTests }