diff --git a/.github/actions/next-stats-action/src/prepare/repo-setup.js b/.github/actions/next-stats-action/src/prepare/repo-setup.js index 65dafedd0da10..72196d21cf6dc 100644 --- a/.github/actions/next-stats-action/src/prepare/repo-setup.js +++ b/.github/actions/next-stats-action/src/prepare/repo-setup.js @@ -13,23 +13,23 @@ module.exports = (actionInfo) => { `git clone ${actionInfo.gitRoot}${repoPath} --single-branch --branch ${branch} --depth=${depth} ${dest}` ) }, - async getLastStable(repoDir = '') { - const { stdout } = await exec(`cd ${repoDir} && git describe`) - const tag = stdout.trim() + async getLastStable() { + const res = await fetch( + `https://api.github.com/repos/vercel/next.js/releases/latest`, + { + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + } + ) - if (!tag || !tag.startsWith('v')) { - throw new Error(`Failed to get tag info: "${stdout}"`) - } - const [major, minor, patch] = tag.split('-canary')[0].split('.') - if (!major || !minor || !patch) { + if (!res.ok) { throw new Error( - `Failed to split tag into major/minor/patch: "${stdout}"` + `Failed to get latest stable tag ${res.status}: ${await res.text()}` ) } - // last stable tag will always be 1 patch less than canary - return `${major}.${minor}.${ - Number(patch) - tag.includes('-canary') ? 1 : 0 - }` + const data = await res.json() + return data.tag_name }, async getCommitId(repoDir = '') { const { stdout } = await exec(`cd ${repoDir} && git rev-parse HEAD`) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 197f426c387e0..27212502893d3 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -50,7 +50,9 @@ jobs: node-version: 18 check-latest: true - - run: git clone https://github.com/vercel/next.js.git --depth=25 . + - uses: actions/checkout@v4 + with: + fetch-depth: 25 - name: Get commit of the latest tag run: echo "LATEST_TAG_COMMIT=$(git rev-list -n 1 $(git describe --tags --abbrev=0))" >> $GITHUB_ENV diff --git a/Cargo.lock b/Cargo.lock index ab7de4e7227d6..b1a0230eaee9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3341,13 +3341,11 @@ dependencies = [ name = "next-custom-transforms" version = "0.0.0" dependencies = [ - "anyhow", "chrono", "easy-error", "either", "fxhash", "hex", - "lazy_static", "once_cell", "pathdiff", "preset_env_base", @@ -3382,12 +3380,9 @@ dependencies = [ "next-build", "next-core", "next-custom-transforms", - "once_cell", - "regex", "serde", "serde_json", "shadow-rs", - "swc_core", "tokio", "tracing", "tracing-chrome", @@ -8541,9 +8536,6 @@ dependencies = [ "getrandom", "js-sys", "next-custom-transforms", - "once_cell", - "regex", - "serde", "serde-wasm-bindgen", "serde_json", "swc_core", diff --git a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx index 136bf509bdf29..f92068a78d4b1 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx @@ -171,7 +171,7 @@ export default async function sitemap({ id }) { `SELECT id, date FROM products WHERE id BETWEEN ${start} AND ${end}` ) return products.map((product) => ({ - url: `${BASE_URL}/product/${id}` + url: `${BASE_URL}/product/${id}`, lastModified: product.date, })) } diff --git a/lerna.json b/lerna.json index 04ce63885adea..1d84ff116d770 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.1.2-canary.5" + "version": "14.2.0-canary.0" } diff --git a/package.json b/package.json index acb8f21608b5e..f17f2e58cf42c 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,6 @@ "types": "lerna run types --stream", "check-precompiled": "./scripts/check-pre-compiled.sh", "prepublishOnly": "turbo run build", - "release-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --force-publish && release --pre --skip-questions --show-url", - "release-stable": "lerna version --force-publish", - "publish-canary": "node -e 'import(\"open\").then(open => open.default(\"https://github.com/vercel/next.js/actions/workflows/trigger_release.yml\"))'", - "publish-stable": "pnpm publish-canary", "lint-staged": "lint-staged", "next-with-deps": "./scripts/next-with-deps.sh", "next": "cross-env NEXT_TELEMETRY_DISABLED=1 node --trace-deprecation --enable-source-maps packages/next/dist/bin/next", @@ -182,7 +178,6 @@ "npm-run-all": "4.1.5", "nprogress": "0.2.0", "octokit": "3.1.0", - "open": "9.0.0", "outdent": "0.8.0", "pixrem": "5.0.0", "playwright": "1.41.2", diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 0d75a7aefdebf..b48f573e661d6 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index fc4ec2578bf2f..f8d7459d62f3a 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "14.1.2-canary.5", + "@next/eslint-plugin-next": "14.2.0-canary.0", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.1", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 615fb9b3eff96..aca108de4311f 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 7c9819b3241bd..4dc8393fcde4c 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 2966ba2578bae..975f07d80c7e9 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-bundle-analyzer/readme.md b/packages/next-bundle-analyzer/readme.md index c2d78720d0206..020578ec0f0c5 100644 --- a/packages/next-bundle-analyzer/readme.md +++ b/packages/next-bundle-analyzer/readme.md @@ -42,7 +42,7 @@ Then you can run the command below: ANALYZE=true yarn build ``` -When enabled two HTML files (client.html and server.html) will be outputted to `/analyze/`. One will be for the server bundle, one for the browser bundle. +When enabled three HTML files (client.html, edge.html and nodejs.html) will be outputted to `/analyze/`. One will be for the nodejs server bundle, one for the edge server bundle, and one for the browser bundle. #### Options diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 17905abed9bf5..551cd3874e449 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index d7bb158abc391..1e30522dabfc2 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index be7bb8bc14c05..c035f9abad47d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index ae24bce16526e..0056ee3a9d368 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 7503cfb0ab2f7..608a4d8b44b3c 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 9f3b2ad5cbb0e..df0b4d8ef5e0f 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 94fbdb3284881..e37bcad685e4d 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -76,8 +76,6 @@ napi = { version = "2", default-features = false, features = [ napi-derive = "2" next-custom-transforms = { workspace = true } -once_cell = { workspace = true } -regex = { workspace = true } serde = "1" serde_json = "1" shadow-rs = { workspace = true } @@ -111,7 +109,6 @@ turbopack-binding = { workspace = true, features = [ [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.9", default-features = false, features = ["js"] } iana-time-zone = { version = "*", features = ["fallback"] } -swc_core = { workspace = true, features = ["ecma_visit_path"] } turbopack-binding = { workspace = true, features = [ "__swc_core_binding_napi", "__swc_core_serde", diff --git a/packages/next-swc/crates/napi/src/parse.rs b/packages/next-swc/crates/napi/src/parse.rs index d510da23cb857..0df7489849ff8 100644 --- a/packages/next-swc/crates/napi/src/parse.rs +++ b/packages/next-swc/crates/napi/src/parse.rs @@ -1,250 +1,92 @@ -use std::collections::HashMap; +use std::sync::Arc; use anyhow::Context as _; use napi::bindgen_prelude::*; -use next_custom_transforms::transforms::page_static_info::{ - build_ast_from_source, collect_exports, collect_rsc_module_info, extract_expored_const_values, - Const, ExportInfo, ExportInfoWarning, RscModuleInfo, +use turbopack_binding::swc::core::{ + base::{config::ParseOptions, try_with_handler}, + common::{ + comments::Comments, errors::ColorConfig, FileName, FilePathMapping, SourceMap, GLOBALS, + }, }; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use serde_json::Value; use crate::util::MapErr; -/// wrap read file to suppress errors conditionally. -/// [NOTE] currently next.js passes _every_ file in the paths regardless of if -/// it's an asset or an ecmascript, So skipping non-utf8 read errors. Probably -/// should skip based on file extension. -fn read_file_wrapped_err(path: &str, raise_err: bool) -> Result { - let buf = std::fs::read(path).map_err(|e| { - napi::Error::new( - Status::GenericFailure, - format!("Next.js ERROR: Failed to read file {}:\n{:#?}", path, e), - ) - }); - - match buf { - Ok(buf) => Ok(String::from_utf8(buf).ok().unwrap_or("".to_string())), - Err(e) if raise_err => Err(e), - _ => Ok("".to_string()), - } -} - -/// A regex pattern to determine if is_dynamic_metadata_route should continue to -/// parse the page or short circuit and return false. -static DYNAMIC_METADATA_ROUTE_SHORT_CURCUIT: Lazy = - Lazy::new(|| Regex::new("generateImageMetadata|generateSitemaps").unwrap()); - -/// A regex pattern to determine if get_page_static_info should continue to -/// parse the page or short circuit and return default. -static PAGE_STATIC_INFO_SHORT_CURCUIT: Lazy = Lazy::new(|| { - Regex::new( - r#"runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const"#, - ) - .unwrap() -}); - -pub struct DetectMetadataRouteTask { - page_file_path: String, - file_content: Option, +pub struct ParseTask { + pub filename: FileName, + pub src: String, + pub options: Buffer, } #[napi] -impl Task for DetectMetadataRouteTask { - type Output = Option; - type JsValue = Object; +impl Task for ParseTask { + type Output = String; + type JsValue = String; fn compute(&mut self) -> napi::Result { - let file_content = if let Some(file_content) = &self.file_content { - file_content.clone() - } else { - read_file_wrapped_err(self.page_file_path.as_str(), true)? - }; - - if !DYNAMIC_METADATA_ROUTE_SHORT_CURCUIT.is_match(file_content.as_str()) { - return Ok(None); - } - - let parsed = if let Ok(parsed) = build_ast_from_source(&file_content, &self.page_file_path) - { - parsed - } else { - return Ok(None); - }; - - let (source_ast, _) = parsed; - collect_exports(&source_ast).convert_err() - } - - fn resolve(&mut self, env: Env, exports_info: Self::Output) -> napi::Result { - let mut ret = env.create_object()?; - - let mut warnings = env.create_array(0)?; - - match exports_info { - Some(exports_info) => { - let is_dynamic_metadata_route = - !exports_info.generate_image_metadata.unwrap_or_default() - || !exports_info.generate_sitemaps.unwrap_or_default(); - ret.set_named_property( - "isDynamicMetadataRoute", - env.get_boolean(is_dynamic_metadata_route), - )?; - - for ExportInfoWarning { key, message } in exports_info.warnings { - let mut warning_obj = env.create_object()?; - warning_obj.set_named_property("key", env.create_string(&key)?)?; - warning_obj.set_named_property("message", env.create_string(&message)?)?; - warnings.insert(warning_obj)?; - } - ret.set_named_property("warnings", warnings)?; - } - None => { - ret.set_named_property("warnings", warnings)?; - ret.set_named_property("isDynamicMetadataRoute", env.get_boolean(false))?; - } - } - - Ok(ret) - } -} - -/// Detect if metadata routes is a dynamic route, which containing -/// generateImageMetadata or generateSitemaps as export -#[napi] -pub fn is_dynamic_metadata_route( - page_file_path: String, - file_content: Option, -) -> AsyncTask { - AsyncTask::new(DetectMetadataRouteTask { - page_file_path, - file_content, - }) -} - -#[napi(object, object_to_js = false)] -pub struct CollectPageStaticInfoOption { - pub page_file_path: String, - pub is_dev: Option, - pub page: Option, - pub page_type: String, //'pages' | 'app' | 'root' -} - -pub struct CollectPageStaticInfoTask { - option: CollectPageStaticInfoOption, - file_content: Option, -} - -#[napi] -impl Task for CollectPageStaticInfoTask { - type Output = Option<( - ExportInfo, - HashMap, - RscModuleInfo, - Vec, - )>; - type JsValue = Option; - - fn compute(&mut self) -> napi::Result { - let CollectPageStaticInfoOption { - page_file_path, - is_dev, - .. - } = &self.option; - let file_content = if let Some(file_content) = &self.file_content { - file_content.clone() - } else { - read_file_wrapped_err(page_file_path.as_str(), !is_dev.unwrap_or_default())? - }; - - if !PAGE_STATIC_INFO_SHORT_CURCUIT.is_match(file_content.as_str()) { - return Ok(None); - } - - // [NOTE] currently get-page-static-info parses file outside of bundler's scope, - // and puts every possible input other than valid ecmascripts. If parse - // fails, consider there's no static info to read. We may want to - // consolidate whole logic into turbopack itself later. - let parsed = if let Ok(parsed) = build_ast_from_source(&file_content, page_file_path) { - parsed - } else { - return Ok(None); - }; - - let (source_ast, comments) = parsed; - let exports_info = collect_exports(&source_ast)?; - match exports_info { - None => Ok(None), - Some(exports_info) => { - let rsc_info = collect_rsc_module_info(&comments, true); - - let mut properties_to_extract = exports_info.extra_properties.clone(); - properties_to_extract.insert("config".to_string()); - - let mut exported_const_values = - extract_expored_const_values(&source_ast, properties_to_extract); - - let mut extracted_values = HashMap::new(); - let mut warnings = vec![]; - - for (key, value) in exported_const_values.drain() { - match value { - Some(Const::Value(v)) => { - extracted_values.insert(key.clone(), v); - } - Some(Const::Unsupported(msg)) => { - warnings.push(msg); - } - _ => {} - } - } - - Ok(Some((exports_info, extracted_values, rsc_info, warnings))) - } - } - } - - fn resolve(&mut self, _env: Env, result: Self::Output) -> napi::Result { - if let Some((exports_info, extracted_values, rsc_info, warnings)) = result { - // [TODO] this is stopgap; there are some non n-api serializable types in the - // nested result. However, this is still much smaller than passing whole ast. - // Should go away once all of logics in the getPageStaticInfo is internalized. - let ret = StaticPageInfo { - exports_info: Some(exports_info), - extracted_values, - rsc_info: Some(rsc_info), - warnings, + GLOBALS.set(&Default::default(), || { + let c = turbopack_binding::swc::core::base::Compiler::new(Arc::new(SourceMap::new( + FilePathMapping::empty(), + ))); + + let options: ParseOptions = serde_json::from_slice(self.options.as_ref())?; + let comments = c.comments().clone(); + let comments: Option<&dyn Comments> = if options.comments { + Some(&comments) + } else { + None }; - - let ret = serde_json::to_string(&ret) - .context("failed to serialize static info result") + let fm = + c.cm.new_source_file(self.filename.clone(), self.src.clone()); + let program = try_with_handler( + c.cm.clone(), + turbopack_binding::swc::core::base::HandlerOpts { + color: ColorConfig::Never, + skip_filename: false, + }, + |handler| { + c.parse_js( + fm, + handler, + options.target, + options.syntax, + options.is_module, + comments, + ) + }, + ) + .convert_err()?; + + let ast_json = serde_json::to_string(&program) + .context("failed to serialize Program") .convert_err()?; - Ok(Some(ret)) - } else { - Ok(None) - } + Ok(ast_json) + }) } -} -#[derive(Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StaticPageInfo { - pub exports_info: Option, - pub extracted_values: HashMap, - pub rsc_info: Option, - pub warnings: Vec, + fn resolve(&mut self, _env: Env, result: Self::Output) -> napi::Result { + Ok(result) + } } #[napi] -pub fn get_page_static_info( - option: CollectPageStaticInfoOption, - file_content: Option, -) -> AsyncTask { - AsyncTask::new(CollectPageStaticInfoTask { - option, - file_content, - }) +pub fn parse( + src: String, + options: Buffer, + filename: Option, + signal: Option, +) -> AsyncTask { + let filename = if let Some(value) = filename { + FileName::Real(value.into()) + } else { + FileName::Anon + }; + AsyncTask::with_optional_signal( + ParseTask { + filename, + src, + options, + }, + signal, + ) } diff --git a/packages/next-swc/crates/next-custom-transforms/Cargo.toml b/packages/next-swc/crates/next-custom-transforms/Cargo.toml index 02019123a3c22..2f65671fd49e7 100644 --- a/packages/next-swc/crates/next-custom-transforms/Cargo.toml +++ b/packages/next-swc/crates/next-custom-transforms/Cargo.toml @@ -16,8 +16,6 @@ easy-error = "1.0.0" either = "1" fxhash = "0.2.1" hex = "0.4.3" -anyhow = { workspace = true } -lazy_static = { workspace = true } once_cell = { workspace = true } pathdiff = { workspace = true } regex = "1.5" diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/mod.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/mod.rs index f3c62e7c1d398..b35d569825461 100644 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/mod.rs +++ b/packages/next-swc/crates/next-custom-transforms/src/transforms/mod.rs @@ -8,7 +8,6 @@ pub mod import_analyzer; pub mod next_ssg; pub mod optimize_server_react; pub mod page_config; -pub mod page_static_info; pub mod pure; pub mod react_server_components; pub mod server_actions; diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exported_const_visitor.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exported_const_visitor.rs deleted file mode 100644 index ebf4c3401ce4b..0000000000000 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exported_const_visitor.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use serde_json::{Map, Number, Value}; -use swc_core::{ - common::{pass::AstNodePath, Mark, SyntaxContext}, - ecma::{ - ast::{ - BindingIdent, Decl, ExportDecl, Expr, Lit, Pat, Prop, PropName, PropOrSpread, VarDecl, - VarDeclKind, VarDeclarator, - }, - utils::{ExprCtx, ExprExt}, - visit::{AstParentNodeRef, VisitAstPath, VisitWithPath}, - }, -}; - -/// The values extracted for the corresponding AST node. -/// refer extract_expored_const_values for the supported value types. -/// Undefined / null is treated as None. -pub enum Const { - Value(Value), - Unsupported(String), -} - -pub(crate) struct CollectExportedConstVisitor { - pub properties: HashMap>, - expr_ctx: ExprCtx, -} - -impl CollectExportedConstVisitor { - pub fn new(properties_to_extract: HashSet) -> Self { - Self { - properties: properties_to_extract - .into_iter() - .map(|p| (p, None)) - .collect(), - expr_ctx: ExprCtx { - unresolved_ctxt: SyntaxContext::empty().apply_mark(Mark::new()), - is_unresolved_ref_safe: false, - }, - } - } -} - -impl VisitAstPath for CollectExportedConstVisitor { - fn visit_export_decl<'ast: 'r, 'r>( - &mut self, - export_decl: &'ast ExportDecl, - ast_path: &mut AstNodePath>, - ) { - match &export_decl.decl { - Decl::Var(box VarDecl { kind, decls, .. }) if kind == &VarDeclKind::Const => { - for decl in decls { - if let VarDeclarator { - name: Pat::Ident(BindingIdent { id, .. }), - init: Some(init), - .. - } = decl - { - let id = id.sym.as_ref(); - if let Some(prop) = self.properties.get_mut(id) { - *prop = extract_value(&self.expr_ctx, init, id.to_string()); - }; - } - } - } - _ => {} - } - - export_decl.visit_children_with_path(self, ast_path); - } -} - -/// Coerece the actual value of the given ast node. -fn extract_value(ctx: &ExprCtx, init: &Expr, id: String) -> Option { - match init { - init if init.is_undefined(ctx) => Some(Const::Value(Value::Null)), - Expr::Ident(ident) => Some(Const::Unsupported(format!( - "Unknown identifier \"{}\" at \"{}\".", - ident.sym, id - ))), - Expr::Lit(lit) => match lit { - Lit::Num(num) => Some(Const::Value(Value::Number( - Number::from_f64(num.value).expect("Should able to convert f64 to Number"), - ))), - Lit::Null(_) => Some(Const::Value(Value::Null)), - Lit::Str(s) => Some(Const::Value(Value::String(s.value.to_string()))), - Lit::Bool(b) => Some(Const::Value(Value::Bool(b.value))), - Lit::Regex(r) => Some(Const::Value(Value::String(format!( - "/{}/{}", - r.exp, r.flags - )))), - _ => Some(Const::Unsupported("Unsupported Literal".to_string())), - }, - Expr::Array(arr) => { - let mut a = vec![]; - - for elem in &arr.elems { - match elem { - Some(elem) => { - if elem.spread.is_some() { - return Some(Const::Unsupported(format!( - "Unsupported spread operator in the Array Expression at \"{}\"", - id - ))); - } - - match extract_value(ctx, &elem.expr, id.clone()) { - Some(Const::Value(value)) => a.push(value), - Some(Const::Unsupported(message)) => { - return Some(Const::Unsupported(format!( - "Unsupported value in the Array Expression: {message}" - ))) - } - _ => { - return Some(Const::Unsupported( - "Unsupported value in the Array Expression".to_string(), - )) - } - } - } - None => { - a.push(Value::Null); - } - } - } - - Some(Const::Value(Value::Array(a))) - } - Expr::Object(obj) => { - let mut o = Map::new(); - - for prop in &obj.props { - let (key, value) = match prop { - PropOrSpread::Prop(box Prop::KeyValue(kv)) => ( - match &kv.key { - PropName::Ident(i) => i.sym.as_ref(), - PropName::Str(s) => s.value.as_ref(), - _ => { - return Some(Const::Unsupported(format!( - "Unsupported key type in the Object Expression at \"{}\"", - id - ))) - } - }, - &kv.value, - ), - _ => { - return Some(Const::Unsupported(format!( - "Unsupported spread operator in the Object Expression at \"{}\"", - id - ))) - } - }; - let new_value = extract_value(ctx, value, format!("{}.{}", id, key)); - if let Some(Const::Unsupported(msg)) = new_value { - return Some(Const::Unsupported(msg)); - } - - if let Some(Const::Value(value)) = new_value { - o.insert(key.to_string(), value); - } - } - - Some(Const::Value(Value::Object(o))) - } - Expr::Tpl(tpl) => { - // [TODO] should we add support for `${'e'}d${'g'}'e'`? - if !tpl.exprs.is_empty() { - Some(Const::Unsupported(format!( - "Unsupported template literal with expressions at \"{}\".", - id - ))) - } else { - Some( - tpl.quasis - .first() - .map(|q| { - // When TemplateLiteral has 0 expressions, the length of quasis is - // always 1. Because when parsing - // TemplateLiteral, the parser yields the first quasi, - // then the first expression, then the next quasi, then the next - // expression, etc., until the last quasi. - // Thus if there is no expression, the parser ends at the frst and also - // last quasis - // - // A "cooked" interpretation where backslashes have special meaning, - // while a "raw" interpretation where - // backslashes do not have special meaning https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw - let cooked = q.cooked.as_ref(); - let raw = q.raw.as_ref(); - - Const::Value(Value::String( - cooked.map(|c| c.to_string()).unwrap_or(raw.to_string()), - )) - }) - .unwrap_or(Const::Unsupported(format!( - "Unsupported node type at \"{}\"", - id - ))), - ) - } - } - _ => Some(Const::Unsupported(format!( - "Unsupported node type at \"{}\"", - id - ))), - } -} diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exports_visitor.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exports_visitor.rs deleted file mode 100644 index 9d10a302defc4..0000000000000 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/collect_exports_visitor.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::collections::HashSet; - -use lazy_static::lazy_static; -use swc_core::ecma::{ - ast::{ - Decl, ExportDecl, ExportNamedSpecifier, ExportSpecifier, Expr, ExprOrSpread, ExprStmt, Lit, - ModuleExportName, ModuleItem, NamedExport, Pat, Stmt, Str, VarDeclarator, - }, - visit::{Visit, VisitWith}, -}; - -use super::{ExportInfo, ExportInfoWarning}; - -lazy_static! { - static ref EXPORTS_SET: HashSet<&'static str> = HashSet::from([ - "getStaticProps", - "getServerSideProps", - "generateImageMetadata", - "generateSitemaps", - "generateStaticParams", - ]); -} - -pub(crate) struct CollectExportsVisitor { - pub export_info: Option, -} - -impl CollectExportsVisitor { - pub fn new() -> Self { - Self { - export_info: Default::default(), - } - } -} - -impl Visit for CollectExportsVisitor { - fn visit_module_items(&mut self, stmts: &[swc_core::ecma::ast::ModuleItem]) { - let mut is_directive = true; - - for stmt in stmts { - if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { - expr: box Expr::Lit(Lit::Str(Str { value, .. })), - .. - })) = stmt - { - if is_directive { - if value == "use server" { - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.directives.insert("server".to_string()); - } - if value == "use client" { - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.directives.insert("client".to_string()); - } - } - } else { - is_directive = false; - } - - stmt.visit_children_with(self); - } - } - - fn visit_export_decl(&mut self, export_decl: &ExportDecl) { - match &export_decl.decl { - Decl::Var(box var_decl) => { - if let Some(VarDeclarator { - name: Pat::Ident(name), - .. - }) = var_decl.decls.first() - { - if EXPORTS_SET.contains(&name.sym.as_str()) { - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.ssg = name.sym == "getStaticProps"; - export_info.ssr = name.sym == "getServerSideProps"; - export_info.generate_image_metadata = - Some(name.sym == "generateImageMetadata"); - export_info.generate_sitemaps = Some(name.sym == "generateSitemaps"); - export_info.generate_static_params = name.sym == "generateStaticParams"; - } - } - - for decl in &var_decl.decls { - if let Pat::Ident(id) = &decl.name { - if id.sym == "runtime" { - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.runtime = decl.init.as_ref().and_then(|init| { - if let Expr::Lit(Lit::Str(Str { value, .. })) = &**init { - Some(value.to_string()) - } else { - None - } - }) - } else if id.sym == "preferredRegion" { - if let Some(init) = &decl.init { - if let Expr::Array(arr) = &**init { - for expr in arr.elems.iter().flatten() { - if let ExprOrSpread { - expr: box Expr::Lit(Lit::Str(Str { value, .. })), - .. - } = expr - { - let export_info = - self.export_info.get_or_insert(Default::default()); - export_info.preferred_region.push(value.to_string()); - } - } - } else if let Expr::Lit(Lit::Str(Str { value, .. })) = &**init { - let export_info = - self.export_info.get_or_insert(Default::default()); - export_info.preferred_region.push(value.to_string()); - } - } - } else { - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.extra_properties.insert(id.sym.to_string()); - } - } - } - } - Decl::Fn(fn_decl) => { - let id = &fn_decl.ident; - - let export_info = self.export_info.get_or_insert(Default::default()); - export_info.ssg = id.sym == "getStaticProps"; - export_info.ssr = id.sym == "getServerSideProps"; - export_info.generate_image_metadata = Some(id.sym == "generateImageMetadata"); - export_info.generate_sitemaps = Some(id.sym == "generateSitemaps"); - export_info.generate_static_params = id.sym == "generateStaticParams"; - } - _ => {} - } - - export_decl.visit_children_with(self); - } - - fn visit_named_export(&mut self, named_export: &NamedExport) { - for specifier in &named_export.specifiers { - if let ExportSpecifier::Named(ExportNamedSpecifier { - orig: ModuleExportName::Ident(value), - .. - }) = specifier - { - let export_info = self.export_info.get_or_insert(Default::default()); - - if !export_info.ssg && value.sym == "getStaticProps" { - export_info.ssg = true; - } - - if !export_info.ssr && value.sym == "getServerSideProps" { - export_info.ssr = true; - } - - if !export_info.generate_image_metadata.unwrap_or_default() - && value.sym == "generateImageMetadata" - { - export_info.generate_image_metadata = Some(true); - } - - if !export_info.generate_sitemaps.unwrap_or_default() - && value.sym == "generateSitemaps" - { - export_info.generate_sitemaps = Some(true); - } - - if !export_info.generate_static_params && value.sym == "generateStaticParams" { - export_info.generate_static_params = true; - } - - if export_info.runtime.is_none() && value.sym == "runtime" { - export_info.warnings.push(ExportInfoWarning::new( - value.sym.to_string(), - "it was not assigned to a string literal".to_string(), - )); - } - - if export_info.preferred_region.is_empty() && value.sym == "preferredRegion" { - export_info.warnings.push(ExportInfoWarning::new( - value.sym.to_string(), - "it was not assigned to a string literal or an array of string literals" - .to_string(), - )); - } - } - } - - named_export.visit_children_with(self); - } -} diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/mod.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/mod.rs deleted file mode 100644 index 031ac76e2b2d9..0000000000000 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/page_static_info/mod.rs +++ /dev/null @@ -1,375 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, - sync::Arc, -}; - -use anyhow::Result; -pub use collect_exported_const_visitor::Const; -use collect_exports_visitor::CollectExportsVisitor; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use swc_core::{ - base::{ - config::{IsModule, ParseOptions}, - try_with_handler, Compiler, HandlerOpts, SwcComments, - }, - common::{errors::ColorConfig, FilePathMapping, SourceMap, GLOBALS}, - ecma::{ - ast::Program, - parser::{EsConfig, Syntax, TsConfig}, - visit::{VisitWith, VisitWithPath}, - }, -}; - -pub mod collect_exported_const_visitor; -pub mod collect_exports_visitor; - -/// Parse given contents of the file as ecmascript via swc's parser. -/// [NOTE] this is being used outside of turbopack (next.js's analysis phase) -/// currently, so we can't use turbopack-ecmascript's parse. -pub fn build_ast_from_source(contents: &str, file_path: &str) -> Result<(Program, SwcComments)> { - GLOBALS.set(&Default::default(), || { - let c = Compiler::new(Arc::new(SourceMap::new(FilePathMapping::empty()))); - - let options = ParseOptions { - is_module: IsModule::Unknown, - syntax: if file_path.ends_with(".ts") || file_path.ends_with(".tsx") { - Syntax::Typescript(TsConfig { - tsx: true, - decorators: true, - ..Default::default() - }) - } else { - Syntax::Es(EsConfig { - jsx: true, - decorators: true, - ..Default::default() - }) - }, - ..Default::default() - }; - - let fm = c.cm.new_source_file( - swc_core::common::FileName::Real(PathBuf::from(file_path.to_string())), - contents.to_string(), - ); - - let comments = c.comments().clone(); - - try_with_handler( - c.cm.clone(), - HandlerOpts { - color: ColorConfig::Never, - skip_filename: false, - }, - |handler| { - c.parse_js( - fm, - handler, - options.target, - options.syntax, - options.is_module, - Some(&comments), - ) - }, - ) - .map(|p| (p, comments)) - }) -} - -#[derive(Debug, Default)] -pub struct MiddlewareConfig {} - -#[derive(Debug)] -pub enum Amp { - Boolean(bool), - Hybrid, -} - -#[derive(Debug, Default)] -pub struct PageStaticInfo { - // [TODO] next-core have NextRuntime type, but the order of dependency won't allow to import - // Since this value is being passed into JS context anyway, we can just use string for now. - pub runtime: Option, // 'nodejs' | 'experimental-edge' | 'edge' - pub preferred_region: Vec, - pub ssg: Option, - pub ssr: Option, - pub rsc: Option, // 'server' | 'client' - pub generate_static_params: Option, - pub middleware: Option, - pub amp: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExportInfoWarning { - pub key: String, - pub message: String, -} - -impl ExportInfoWarning { - pub fn new(key: String, message: String) -> Self { - Self { key, message } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExportInfo { - pub ssr: bool, - pub ssg: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub runtime: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub preferred_region: Vec, - pub generate_image_metadata: Option, - pub generate_sitemaps: Option, - pub generate_static_params: bool, - pub extra_properties: HashSet, - pub directives: HashSet, - /// extra properties to bubble up warning messages from visitor, - /// since this isn't a failure to abort the process. - pub warnings: Vec, -} - -/// Collects static page export information for the next.js from given source's -/// AST. This is being used for some places like detecting page -/// is a dynamic route or not, or building a PageStaticInfo object. -pub fn collect_exports(program: &Program) -> Result> { - let mut collect_export_visitor = CollectExportsVisitor::new(); - program.visit_with(&mut collect_export_visitor); - - Ok(collect_export_visitor.export_info) -} - -static CLIENT_MODULE_LABEL: Lazy = Lazy::new(|| { - Regex::new(" __next_internal_client_entry_do_not_use__ ([^ ]*) (cjs|auto) ").unwrap() -}); -static ACTION_MODULE_LABEL: Lazy = - Lazy::new(|| Regex::new(r#" __next_internal_action_entry_do_not_use__ (\{[^}]+\}) "#).unwrap()); - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RscModuleInfo { - #[serde(rename = "type")] - pub module_type: String, - pub actions: Option>, - pub is_client_ref: bool, - pub client_refs: Option>, - pub client_entry_type: Option, -} - -impl RscModuleInfo { - pub fn new(module_type: String) -> Self { - Self { - module_type, - actions: None, - is_client_ref: false, - client_refs: None, - client_entry_type: None, - } - } -} - -/// Parse comments from the given source code and collect the RSC module info. -/// This doesn't use visitor, only read comments to parse necessary information. -pub fn collect_rsc_module_info( - comments: &SwcComments, - is_react_server_layer: bool, -) -> RscModuleInfo { - let mut captured = None; - - for comment in comments.leading.iter() { - let parsed = comment.iter().find_map(|c| { - let actions_json = ACTION_MODULE_LABEL.captures(&c.text); - let client_info_match = CLIENT_MODULE_LABEL.captures(&c.text); - - if actions_json.is_none() && client_info_match.is_none() { - return None; - } - - let actions = if let Some(actions_json) = actions_json { - if let Ok(serde_json::Value::Object(map)) = - serde_json::from_str::(&actions_json[1]) - { - Some( - map.iter() - // values for the action json should be a string - .map(|(_, v)| v.as_str().unwrap_or_default().to_string()) - .collect::>(), - ) - } else { - None - } - } else { - None - }; - - let is_client_ref = client_info_match.is_some(); - let client_info = client_info_match.map(|client_info_match| { - ( - client_info_match[1] - .split(',') - .map(|s| s.to_string()) - .collect::>(), - client_info_match[2].to_string(), - ) - }); - - Some((actions, is_client_ref, client_info)) - }); - - if captured.is_none() { - captured = parsed; - break; - } - } - - match captured { - Some((actions, is_client_ref, client_info)) => { - if !is_react_server_layer { - let mut module_info = RscModuleInfo::new("client".to_string()); - module_info.actions = actions; - module_info.is_client_ref = is_client_ref; - module_info - } else { - let mut module_info = RscModuleInfo::new(if client_info.is_some() { - "client".to_string() - } else { - "server".to_string() - }); - module_info.actions = actions; - module_info.is_client_ref = is_client_ref; - if let Some((client_refs, client_entry_type)) = client_info { - module_info.client_refs = Some(client_refs); - module_info.client_entry_type = Some(client_entry_type); - } - - module_info - } - } - None => RscModuleInfo::new(if !is_react_server_layer { - "client".to_string() - } else { - "server".to_string() - }), - } -} - -/// Extracts the value of an exported const variable named `exportedName` -/// (e.g. "export const config = { runtime: 'edge' }") from swc's AST. -/// The value must be one of -/// - string -/// - boolean -/// - number -/// - null -/// - undefined -/// - array containing values listed in this list -/// - object containing values listed in this list -/// -/// Returns a map of the extracted values, or either contains corresponding -/// error. -pub fn extract_expored_const_values( - source_ast: &Program, - properties_to_extract: HashSet, -) -> HashMap> { - GLOBALS.set(&Default::default(), || { - let mut visitor = - collect_exported_const_visitor::CollectExportedConstVisitor::new(properties_to_extract); - - source_ast.visit_with_path(&mut visitor, &mut Default::default()); - - visitor.properties - }) -} - -#[cfg(test)] -mod tests { - use super::{build_ast_from_source, collect_rsc_module_info, RscModuleInfo}; - - #[test] - fn should_parse_server_info() { - let input = r#"export default function Page() { - return

app-edge-ssr

- } - - export const runtime = 'edge' - export const maxDuration = 4 - "#; - - let (_, comments) = build_ast_from_source(input, "some-file.js") - .expect("Should able to parse test fixture input"); - - let module_info = collect_rsc_module_info(&comments, true); - let expected = RscModuleInfo { - module_type: "server".to_string(), - actions: None, - is_client_ref: false, - client_refs: None, - client_entry_type: None, - }; - - assert_eq!(module_info, expected); - } - - #[test] - fn should_parse_actions_json() { - let input = r#" - /* __next_internal_action_entry_do_not_use__ {"ab21efdafbe611287bc25c0462b1e0510d13e48b":"foo"} */ import { createActionProxy } from "private-next-rsc-action-proxy"; - import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; - export function foo() {} - import { ensureServerEntryExports } from "private-next-rsc-action-validate"; - ensureServerEntryExports([ - foo - ]); - createActionProxy("ab21efdafbe611287bc25c0462b1e0510d13e48b", foo); - "#; - - let (_, comments) = build_ast_from_source(input, "some-file.js") - .expect("Should able to parse test fixture input"); - - let module_info = collect_rsc_module_info(&comments, true); - let expected = RscModuleInfo { - module_type: "server".to_string(), - actions: Some(vec!["foo".to_string()]), - is_client_ref: false, - client_refs: None, - client_entry_type: None, - }; - - assert_eq!(module_info, expected); - } - - #[test] - fn should_parse_client_refs() { - let input = r#" - // This is a comment. - /* __next_internal_client_entry_do_not_use__ default,a,b,c,*,f auto */ const { createProxy } = require("private-next-rsc-mod-ref-proxy"); - module.exports = createProxy("/some-project/src/some-file.js"); - "#; - - let (_, comments) = build_ast_from_source(input, "some-file.js") - .expect("Should able to parse test fixture input"); - - let module_info = collect_rsc_module_info(&comments, true); - - let expected = RscModuleInfo { - module_type: "client".to_string(), - actions: None, - is_client_ref: true, - client_refs: Some(vec![ - "default".to_string(), - "a".to_string(), - "b".to_string(), - "c".to_string(), - "*".to_string(), - "f".to_string(), - ]), - client_entry_type: Some("auto".to_string()), - }; - - assert_eq!(module_info, expected); - } -} diff --git a/packages/next-swc/crates/wasm/Cargo.toml b/packages/next-swc/crates/wasm/Cargo.toml index 0f32023b0edb4..bc3bf0bc680c1 100644 --- a/packages/next-swc/crates/wasm/Cargo.toml +++ b/packages/next-swc/crates/wasm/Cargo.toml @@ -17,9 +17,7 @@ plugin = ["turbopack-binding/__swc_core_binding_wasm_plugin"] workspace = true [dependencies] -anyhow = { workspace = true } -once_cell = { workspace = true } -serde = { workspace = true } +anyhow = "1.0.66" console_error_panic_hook = "0.1.6" next-custom-transforms = { workspace = true } serde_json = "1" @@ -32,8 +30,8 @@ turbopack-binding = { workspace = true, features = [ "__swc_core_binding_wasm", "__feature_mdx_rs", ] } -swc_core = { workspace = true, features = ["ecma_ast_serde", "common", "ecma_visit_path"] } -regex = "1.5" +swc_core = { workspace = true, features = ["ecma_ast_serde", "common"] } + # Workaround a bug [package.metadata.wasm-pack.profile.release] diff --git a/packages/next-swc/crates/wasm/src/lib.rs b/packages/next-swc/crates/wasm/src/lib.rs index 1692a9742973a..4c53eeb2c517a 100644 --- a/packages/next-swc/crates/wasm/src/lib.rs +++ b/packages/next-swc/crates/wasm/src/lib.rs @@ -1,23 +1,18 @@ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use anyhow::{Context, Error}; use js_sys::JsString; -use next_custom_transforms::{ - chain_transforms::{custom_before_pass, TransformOptions}, - transforms::page_static_info::{ - build_ast_from_source, collect_exports, collect_rsc_module_info, - extract_expored_const_values, Const, ExportInfo, RscModuleInfo, - }, -}; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserialize, Serialize}; +use next_custom_transforms::chain_transforms::{custom_before_pass, TransformOptions}; use swc_core::common::Mark; use turbopack_binding::swc::core::{ - base::{config::JsMinifyOptions, try_with_handler, Compiler}, + base::{ + config::{JsMinifyOptions, ParseOptions}, + try_with_handler, Compiler, + }, common::{ - comments::SingleThreadedComments, errors::ColorConfig, FileName, FilePathMapping, - SourceMap, GLOBALS, + comments::{Comments, SingleThreadedComments}, + errors::ColorConfig, + FileName, FilePathMapping, SourceMap, GLOBALS, }, ecma::transforms::base::pass::noop, }; @@ -26,21 +21,6 @@ use wasm_bindgen_futures::future_to_promise; pub mod mdx; -/// A regex pattern to determine if is_dynamic_metadata_route should continue to -/// parse the page or short circuit and return false. -static DYNAMIC_METADATA_ROUTE_SHORT_CURCUIT: Lazy = - Lazy::new(|| Regex::new("generateImageMetadata|generateSitemaps").unwrap()); - -/// A regex pattern to determine if get_page_static_info should continue to -/// parse the page or short circuit and return default. -static PAGE_STATIC_INFO_SHORT_CURCUIT: Lazy = Lazy::new(|| { - Regex::new( - "runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export \ - const", - ) - .unwrap() -}); - fn convert_err(err: Error) -> JsValue { format!("{:?}", err).into() } @@ -157,107 +137,57 @@ pub fn transform(s: JsValue, opts: JsValue) -> js_sys::Promise { future_to_promise(async { transform_sync(s, opts) }) } -/// Detect if metadata routes is a dynamic route, which containing -/// generateImageMetadata or generateSitemaps as export -/// Unlike native bindings, caller should provide the contents of the pages -/// sine our wasm bindings does not have access to the file system -#[wasm_bindgen(js_name = "isDynamicMetadataRoute")] -pub fn is_dynamic_metadata_route(page_file_path: String, page_contents: String) -> js_sys::Promise { - // Returning promise to conform existing interfaces - future_to_promise(async move { - if !DYNAMIC_METADATA_ROUTE_SHORT_CURCUIT.is_match(&page_contents) { - return Ok(JsValue::from(false)); - } +#[wasm_bindgen(js_name = "parseSync")] +pub fn parse_sync(s: JsString, opts: JsValue) -> Result { + console_error_panic_hook::set_once(); - let parsed = if let Ok(parsed) = build_ast_from_source(&page_contents, &page_file_path) { - parsed - } else { - return Ok(JsValue::null()); - }; + let c = turbopack_binding::swc::core::base::Compiler::new(Arc::new(SourceMap::new( + FilePathMapping::empty(), + ))); + let opts: ParseOptions = serde_wasm_bindgen::from_value(opts)?; - let (source_ast, _) = parsed; - collect_exports(&source_ast) - .map(|exports_info| { - exports_info - .map(|exports_info| { - JsValue::from( - !exports_info.generate_image_metadata.unwrap_or_default() - || !exports_info.generate_sitemaps.unwrap_or_default(), + try_with_handler( + c.cm.clone(), + turbopack_binding::swc::core::base::HandlerOpts { + ..Default::default() + }, + |handler| { + c.run(|| { + GLOBALS.set(&Default::default(), || { + let fm = c.cm.new_source_file(FileName::Anon, s.into()); + + let cmts = c.comments().clone(); + let comments = if opts.comments { + Some(&cmts as &dyn Comments) + } else { + None + }; + + let program = c + .parse_js( + fm, + handler, + opts.target, + opts.syntax, + opts.is_module, + comments, ) - }) - .unwrap_or_default() - }) - .map_err(|e| JsValue::from_str(format!("{:?}", e).as_str())) - }) -} + .context("failed to parse code")?; -#[derive(Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StaticPageInfo { - pub exports_info: Option, - pub extracted_values: HashMap, - pub rsc_info: Option, - pub warnings: Vec, + let s = serde_json::to_string(&program).unwrap(); + Ok(JsValue::from_str(&s)) + }) + }) + }, + ) + .map_err(convert_err) } -#[wasm_bindgen(js_name = "getPageStaticInfo")] -pub fn get_page_static_info(page_file_path: String, page_contents: String) -> js_sys::Promise { - future_to_promise(async move { - if !PAGE_STATIC_INFO_SHORT_CURCUIT.is_match(&page_contents) { - return Ok(JsValue::null()); - } - - let parsed = if let Ok(parsed) = build_ast_from_source(&page_contents, &page_file_path) { - parsed - } else { - return Ok(JsValue::null()); - }; - - let (source_ast, comments) = parsed; - let exports_info = collect_exports(&source_ast) - .map_err(|e| JsValue::from_str(format!("{:?}", e).as_str()))?; - - match exports_info { - None => Ok(JsValue::null()), - Some(exports_info) => { - let rsc_info = collect_rsc_module_info(&comments, true); - - let mut properties_to_extract = exports_info.extra_properties.clone(); - properties_to_extract.insert("config".to_string()); - - let mut exported_const_values = - extract_expored_const_values(&source_ast, properties_to_extract); - - let mut extracted_values = HashMap::new(); - let mut warnings = vec![]; - - for (key, value) in exported_const_values.drain() { - match value { - Some(Const::Value(v)) => { - extracted_values.insert(key.clone(), v); - } - Some(Const::Unsupported(msg)) => { - warnings.push(msg); - } - _ => {} - } - } - - let ret = StaticPageInfo { - exports_info: Some(exports_info), - extracted_values, - rsc_info: Some(rsc_info), - warnings, - }; - - let s = serde_json::to_string(&ret) - .map(|s| JsValue::from_str(&s)) - .unwrap_or(JsValue::null()); - - Ok(s) - } - } - }) +#[wasm_bindgen(js_name = "parse")] +pub fn parse(s: JsString, opts: JsValue) -> js_sys::Promise { + // TODO: This'll be properly scheduled once wasm have standard backed thread + // support. + future_to_promise(async { parse_sync(s, opts) }) } /// Get global sourcemap diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 7ef7780f0a523..9511aae21d3f2 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 146ed41eaa97c..eb62670402f87 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.1.2-canary.5", + "@next/env": "14.2.0-canary.0", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -145,10 +145,10 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/polyfill-module": "14.1.2-canary.5", - "@next/polyfill-nomodule": "14.1.2-canary.5", - "@next/react-refresh-utils": "14.1.2-canary.5", - "@next/swc": "14.1.2-canary.5", + "@next/polyfill-module": "14.2.0-canary.0", + "@next/polyfill-nomodule": "14.2.0-canary.0", + "@next/react-refresh-utils": "14.2.0-canary.0", + "@next/swc": "14.2.0-canary.0", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/build/analysis/extract-const-value.ts b/packages/next/src/build/analysis/extract-const-value.ts new file mode 100644 index 0000000000000..0ae1d5fc7993a --- /dev/null +++ b/packages/next/src/build/analysis/extract-const-value.ts @@ -0,0 +1,249 @@ +import type { + ArrayExpression, + BooleanLiteral, + ExportDeclaration, + Identifier, + KeyValueProperty, + Module, + Node, + NullLiteral, + NumericLiteral, + ObjectExpression, + RegExpLiteral, + StringLiteral, + TemplateLiteral, + VariableDeclaration, +} from '@swc/core' + +export class NoSuchDeclarationError extends Error {} + +function isExportDeclaration(node: Node): node is ExportDeclaration { + return node.type === 'ExportDeclaration' +} + +function isVariableDeclaration(node: Node): node is VariableDeclaration { + return node.type === 'VariableDeclaration' +} + +function isIdentifier(node: Node): node is Identifier { + return node.type === 'Identifier' +} + +function isBooleanLiteral(node: Node): node is BooleanLiteral { + return node.type === 'BooleanLiteral' +} + +function isNullLiteral(node: Node): node is NullLiteral { + return node.type === 'NullLiteral' +} + +function isStringLiteral(node: Node): node is StringLiteral { + return node.type === 'StringLiteral' +} + +function isNumericLiteral(node: Node): node is NumericLiteral { + return node.type === 'NumericLiteral' +} + +function isArrayExpression(node: Node): node is ArrayExpression { + return node.type === 'ArrayExpression' +} + +function isObjectExpression(node: Node): node is ObjectExpression { + return node.type === 'ObjectExpression' +} + +function isKeyValueProperty(node: Node): node is KeyValueProperty { + return node.type === 'KeyValueProperty' +} + +function isRegExpLiteral(node: Node): node is RegExpLiteral { + return node.type === 'RegExpLiteral' +} + +function isTemplateLiteral(node: Node): node is TemplateLiteral { + return node.type === 'TemplateLiteral' +} + +export class UnsupportedValueError extends Error { + /** @example `config.runtime[0].value` */ + path?: string + + constructor(message: string, paths?: string[]) { + super(message) + + // Generating "path" that looks like "config.runtime[0].value" + let codePath: string | undefined + if (paths) { + codePath = '' + for (const path of paths) { + if (path[0] === '[') { + // "array" + "[0]" + codePath += path + } else { + if (codePath === '') { + codePath = path + } else { + // "object" + ".key" + codePath += `.${path}` + } + } + } + } + + this.path = codePath + } +} + +function extractValue(node: Node, path?: string[]): any { + if (isNullLiteral(node)) { + return null + } else if (isBooleanLiteral(node)) { + // e.g. true / false + return node.value + } else if (isStringLiteral(node)) { + // e.g. "abc" + return node.value + } else if (isNumericLiteral(node)) { + // e.g. 123 + return node.value + } else if (isRegExpLiteral(node)) { + // e.g. /abc/i + return new RegExp(node.pattern, node.flags) + } else if (isIdentifier(node)) { + switch (node.value) { + case 'undefined': + return undefined + default: + throw new UnsupportedValueError( + `Unknown identifier "${node.value}"`, + path + ) + } + } else if (isArrayExpression(node)) { + // e.g. [1, 2, 3] + const arr = [] + for (let i = 0, len = node.elements.length; i < len; i++) { + const elem = node.elements[i] + if (elem) { + if (elem.spread) { + // e.g. [ ...a ] + throw new UnsupportedValueError( + 'Unsupported spread operator in the Array Expression', + path + ) + } + + arr.push(extractValue(elem.expression, path && [...path, `[${i}]`])) + } else { + // e.g. [1, , 2] + // ^^ + arr.push(undefined) + } + } + return arr + } else if (isObjectExpression(node)) { + // e.g. { a: 1, b: 2 } + const obj: any = {} + for (const prop of node.properties) { + if (!isKeyValueProperty(prop)) { + // e.g. { ...a } + throw new UnsupportedValueError( + 'Unsupported spread operator in the Object Expression', + path + ) + } + + let key + if (isIdentifier(prop.key)) { + // e.g. { a: 1, b: 2 } + key = prop.key.value + } else if (isStringLiteral(prop.key)) { + // e.g. { "a": 1, "b": 2 } + key = prop.key.value + } else { + throw new UnsupportedValueError( + `Unsupported key type "${prop.key.type}" in the Object Expression`, + path + ) + } + + obj[key] = extractValue(prop.value, path && [...path, key]) + } + + return obj + } else if (isTemplateLiteral(node)) { + // e.g. `abc` + if (node.expressions.length !== 0) { + // TODO: should we add support for `${'e'}d${'g'}'e'`? + throw new UnsupportedValueError( + 'Unsupported template literal with expressions', + path + ) + } + + // When TemplateLiteral has 0 expressions, the length of quasis is always 1. + // Because when parsing TemplateLiteral, the parser yields the first quasi, + // then the first expression, then the next quasi, then the next expression, etc., + // until the last quasi. + // Thus if there is no expression, the parser ends at the frst and also last quasis + // + // A "cooked" interpretation where backslashes have special meaning, while a + // "raw" interpretation where backslashes do not have special meaning + // https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw + const [{ cooked, raw }] = node.quasis + + return cooked ?? raw + } else { + throw new UnsupportedValueError( + `Unsupported node type "${node.type}"`, + path + ) + } +} + +/** + * Extracts the value of an exported const variable named `exportedName` + * (e.g. "export const config = { runtime: 'edge' }") from swc's AST. + * The value must be one of (or throws UnsupportedValueError): + * - string + * - boolean + * - number + * - null + * - undefined + * - array containing values listed in this list + * - object containing values listed in this list + * + * Throws NoSuchDeclarationError if the declaration is not found. + */ +export function extractExportedConstValue( + module: Module, + exportedName: string +): any { + for (const moduleItem of module.body) { + if (!isExportDeclaration(moduleItem)) { + continue + } + + const declaration = moduleItem.declaration + if (!isVariableDeclaration(declaration)) { + continue + } + + if (declaration.kind !== 'const') { + continue + } + + for (const decl of declaration.declarations) { + if ( + isIdentifier(decl.id) && + decl.id.value === exportedName && + decl.init + ) { + return extractValue(decl.init, [exportedName]) + } + } + } + + throw new NoSuchDeclarationError() +} diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 4fd75bc5481bd..4420c266144c3 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -1,9 +1,15 @@ import type { NextConfig } from '../../server/config-shared' import type { Middleware, RouteHas } from '../../lib/load-custom-routes' +import { promises as fs } from 'fs' import LRUCache from 'next/dist/compiled/lru-cache' import picomatch from 'next/dist/compiled/picomatch' import type { ServerRuntime } from 'next/types' +import { + extractExportedConstValue, + UnsupportedValueError, +} from './extract-const-value' +import { parseModule } from './parse-module' import * as Log from '../output/log' import { SERVER_RUNTIME } from '../../lib/constants' import { checkCustomRoutes } from '../../lib/load-custom-routes' @@ -64,6 +70,9 @@ const CLIENT_MODULE_LABEL = const ACTION_MODULE_LABEL = /\/\* __next_internal_action_entry_do_not_use__ (\{[^}]+\}) \*\// +const CLIENT_DIRECTIVE = 'use client' +const SERVER_ACTION_DIRECTIVE = 'use server' + export type RSCModuleType = 'server' | 'client' export function getRSCModuleInformation( source: string, @@ -98,6 +107,211 @@ export function getRSCModuleInformation( } } +const warnedInvalidValueMap = { + runtime: new Map(), + preferredRegion: new Map(), +} as const +function warnInvalidValue( + pageFilePath: string, + key: keyof typeof warnedInvalidValueMap, + message: string +): void { + if (warnedInvalidValueMap[key].has(pageFilePath)) return + + Log.warn( + `Next.js can't recognize the exported \`${key}\` field in "${pageFilePath}" as ${message}.` + + '\n' + + 'The default runtime will be used instead.' + ) + + warnedInvalidValueMap[key].set(pageFilePath, true) +} +/** + * Receives a parsed AST from SWC and checks if it belongs to a module that + * requires a runtime to be specified. Those are: + * - Modules with `export function getStaticProps | getServerSideProps` + * - Modules with `export { getStaticProps | getServerSideProps } ` + * - Modules with `export const runtime = ...` + */ +function checkExports( + swcAST: any, + pageFilePath: string +): { + ssr: boolean + ssg: boolean + runtime?: string + preferredRegion?: string | string[] + generateImageMetadata?: boolean + generateSitemaps?: boolean + generateStaticParams: boolean + extraProperties?: Set + directives?: Set +} { + const exportsSet = new Set([ + 'getStaticProps', + 'getServerSideProps', + 'generateImageMetadata', + 'generateSitemaps', + 'generateStaticParams', + ]) + if (Array.isArray(swcAST?.body)) { + try { + let runtime: string | undefined + let preferredRegion: string | string[] | undefined + let ssr: boolean = false + let ssg: boolean = false + let generateImageMetadata: boolean = false + let generateSitemaps: boolean = false + let generateStaticParams = false + let extraProperties = new Set() + let directives = new Set() + let hasLeadingNonDirectiveNode = false + + for (const node of swcAST.body) { + // There should be no non-string literals nodes before directives + if ( + node.type === 'ExpressionStatement' && + node.expression.type === 'StringLiteral' + ) { + if (!hasLeadingNonDirectiveNode) { + const directive = node.expression.value + if (CLIENT_DIRECTIVE === directive) { + directives.add('client') + } + if (SERVER_ACTION_DIRECTIVE === directive) { + directives.add('server') + } + } + } else { + hasLeadingNonDirectiveNode = true + } + if ( + node.type === 'ExportDeclaration' && + node.declaration?.type === 'VariableDeclaration' + ) { + for (const declaration of node.declaration?.declarations) { + if (declaration.id.value === 'runtime') { + runtime = declaration.init.value + } else if (declaration.id.value === 'preferredRegion') { + if (declaration.init.type === 'ArrayExpression') { + const elements: string[] = [] + for (const element of declaration.init.elements) { + const { expression } = element + if (expression.type !== 'StringLiteral') { + continue + } + elements.push(expression.value) + } + preferredRegion = elements + } else { + preferredRegion = declaration.init.value + } + } else { + extraProperties.add(declaration.id.value) + } + } + } + + if ( + node.type === 'ExportDeclaration' && + node.declaration?.type === 'FunctionDeclaration' && + exportsSet.has(node.declaration.identifier?.value) + ) { + const id = node.declaration.identifier.value + ssg = id === 'getStaticProps' + ssr = id === 'getServerSideProps' + generateImageMetadata = id === 'generateImageMetadata' + generateSitemaps = id === 'generateSitemaps' + generateStaticParams = id === 'generateStaticParams' + } + + if ( + node.type === 'ExportDeclaration' && + node.declaration?.type === 'VariableDeclaration' + ) { + const id = node.declaration?.declarations[0]?.id.value + if (exportsSet.has(id)) { + ssg = id === 'getStaticProps' + ssr = id === 'getServerSideProps' + generateImageMetadata = id === 'generateImageMetadata' + generateSitemaps = id === 'generateSitemaps' + generateStaticParams = id === 'generateStaticParams' + } + } + + if (node.type === 'ExportNamedDeclaration') { + const values = node.specifiers.map( + (specifier: any) => + specifier.type === 'ExportSpecifier' && + specifier.orig?.type === 'Identifier' && + specifier.orig?.value + ) + + for (const value of values) { + if (!ssg && value === 'getStaticProps') ssg = true + if (!ssr && value === 'getServerSideProps') ssr = true + if (!generateImageMetadata && value === 'generateImageMetadata') + generateImageMetadata = true + if (!generateSitemaps && value === 'generateSitemaps') + generateSitemaps = true + if (!generateStaticParams && value === 'generateStaticParams') + generateStaticParams = true + if (!runtime && value === 'runtime') + warnInvalidValue( + pageFilePath, + 'runtime', + 'it was not assigned to a string literal' + ) + if (!preferredRegion && value === 'preferredRegion') + warnInvalidValue( + pageFilePath, + 'preferredRegion', + 'it was not assigned to a string literal or an array of string literals' + ) + } + } + } + + return { + ssr, + ssg, + runtime, + preferredRegion, + generateImageMetadata, + generateSitemaps, + generateStaticParams, + extraProperties, + directives, + } + } catch (err) {} + } + + return { + ssg: false, + ssr: false, + runtime: undefined, + preferredRegion: undefined, + generateImageMetadata: false, + generateSitemaps: false, + generateStaticParams: false, + extraProperties: undefined, + directives: undefined, + } +} + +async function tryToReadFile(filePath: string, shouldThrow: boolean) { + try { + return await fs.readFile(filePath, { + encoding: 'utf8', + }) + } catch (error: any) { + if (shouldThrow) { + error.message = `Next.js ERROR: Failed to read file ${filePath}:\n${error.message}` + throw error + } + } +} + export function getMiddlewareMatchers( matcherOrMatchers: unknown, nextConfig: NextConfig @@ -230,11 +444,10 @@ function warnAboutExperimentalEdge(apiRoute: string | null) { const warnedUnsupportedValueMap = new LRUCache({ max: 250 }) -// [TODO] next-swc does not returns path where unsupported value is found yet. function warnAboutUnsupportedValue( pageFilePath: string, page: string | undefined, - message: string + error: UnsupportedValueError ) { if (warnedUnsupportedValueMap.has(pageFilePath)) { return @@ -244,8 +457,9 @@ function warnAboutUnsupportedValue( `Next.js can't recognize the exported \`config\` field in ` + (page ? `route "${page}"` : `"${pageFilePath}"`) + ':\n' + - message + - '\n' + + error.message + + (error.path ? ` at "${error.path}"` : '') + + '.\n' + 'The default config will be used instead.\n' + 'Read More - https://nextjs.org/docs/messages/invalid-page-config' ) @@ -253,6 +467,20 @@ function warnAboutUnsupportedValue( warnedUnsupportedValueMap.set(pageFilePath, true) } +// Detect if metadata routes is a dynamic route, which containing +// generateImageMetadata or generateSitemaps as export +export async function isDynamicMetadataRoute( + pageFilePath: string +): Promise { + const fileContent = (await tryToReadFile(pageFilePath, true)) || '' + if (/generateImageMetadata|generateSitemaps/.test(fileContent)) { + const swcAST = await parseModule(pageFilePath, fileContent) + const exportsInfo = checkExports(swcAST, pageFilePath) + return !!(exportsInfo.generateImageMetadata || exportsInfo.generateSitemaps) + } + return false +} + /** * For a given pageFilePath and nextConfig, if the config supports it, this * function will read the file and return the runtime that should be used. @@ -269,12 +497,13 @@ export async function getPageStaticInfo(params: { }): Promise { const { isDev, pageFilePath, nextConfig, page, pageType } = params - const binding = await require('../swc').loadBindings() - const pageStaticInfo = await binding.analysis.getPageStaticInfo(params) - - if (pageStaticInfo) { - const { exportsInfo, extractedValues, rscInfo, warnings } = pageStaticInfo - + const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || '' + if ( + /(? { - warnAboutUnsupportedValue(pageFilePath, page, warning) - }) - // default / failsafe value for config - let config = extractedValues.config + let config: any // TODO: type this as unknown + try { + config = extractExportedConstValue(swcAST, 'config') + } catch (e) { + if (e instanceof UnsupportedValueError) { + warnAboutUnsupportedValue(pageFilePath, page, e) + } + // `export config` doesn't exist, or other unknown error thrown by swc, silence them + } const extraConfig: Record = {} if (extraProperties && pageType === PAGE_TYPES.APP) { for (const prop of extraProperties) { if (!AUTHORIZED_EXTRA_ROUTER_PROPS.includes(prop)) continue - extraConfig[prop] = extractedValues[prop] + try { + extraConfig[prop] = extractExportedConstValue(swcAST, prop) + } catch (e) { + if (e instanceof UnsupportedValueError) { + warnAboutUnsupportedValue(pageFilePath, page, e) + } + } } } else if (pageType === PAGE_TYPES.PAGES) { for (const key in config) { diff --git a/packages/next/src/build/analysis/parse-module.ts b/packages/next/src/build/analysis/parse-module.ts new file mode 100644 index 0000000000000..5ba1dd24a15c3 --- /dev/null +++ b/packages/next/src/build/analysis/parse-module.ts @@ -0,0 +1,15 @@ +import LRUCache from 'next/dist/compiled/lru-cache' +import { withPromiseCache } from '../../lib/with-promise-cache' +import { createHash } from 'crypto' +import { parse } from '../swc' + +/** + * Parses a module with SWC using an LRU cache where the parsed module will + * be indexed by a sha of its content holding up to 500 entries. + */ +export const parseModule = withPromiseCache( + new LRUCache({ max: 500 }), + async (filename: string, content: string) => + parse(content, { isModule: 'unknown', filename }).catch(() => null), + (_, content) => createHash('sha1').update(content).digest('hex') +) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 4ff581d098a74..6a27607e82909 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -99,7 +99,10 @@ import { } from '../telemetry/events' import type { EventBuildFeatureUsage } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' -import { getPageStaticInfo } from './analysis/get-page-static-info' +import { + isDynamicMetadataRoute, + getPageStaticInfo, +} from './analysis/get-page-static-info' import { createPagesMapping, getPageFilePath, sortByPageExts } from './entries' import { PAGE_TYPES } from '../lib/page-types' import { generateBuildId } from './generate-build-id' @@ -748,6 +751,7 @@ export default async function build( const cacheDir = getCacheDir(distDir) const telemetry = new Telemetry({ distDir }) + setGlobal('telemetry', telemetry) const publicDir = path.join(dir, 'public') @@ -755,8 +759,6 @@ export default async function build( NextBuildContext.pagesDir = pagesDir NextBuildContext.appDir = appDir - const binding = await loadBindings(config?.experimental?.useWasmBinary) - const enabledDirectories: NextEnabledDirectories = { app: typeof appDir === 'string', pages: typeof pagesDir === 'string', @@ -946,10 +948,7 @@ export default async function build( rootDir, }) - const isDynamic = await binding.analysis.isDynamicMetadataRoute( - pageFilePath - ) - + const isDynamic = await isDynamicMetadataRoute(pageFilePath) if (!isDynamic) { delete mappedAppPages[pageKey] mappedAppPages[pageKey.replace('[[...__metadata_id__]]/', '')] = diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index a1bdd11d14c04..35200812420e3 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -2,9 +2,9 @@ import path from 'path' import { pathToFileURL } from 'url' import { arch, platform } from 'os' -import { promises as fs } from 'fs' import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples' import * as Log from '../output/log' +import { getParserOptions } from './options' import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure' import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile' import { downloadNativeNextSwc, downloadWasmSwc } from '../../lib/download-swc' @@ -15,7 +15,6 @@ import { getDefineEnv } from '../webpack/plugins/define-env-plugin' import type { PageExtensions } from '../page-extensions-type' const nextVersion = process.env.__NEXT_VERSION as string -const isYarnPnP = !!process?.versions?.pnp const ArchName = arch() const PlatformName = platform() @@ -109,19 +108,6 @@ function checkVersionMismatch(pkgData: any) { } } -async function tryToReadFile(filePath: string, shouldThrow: boolean) { - try { - return await fs.readFile(filePath, { - encoding: 'utf8', - }) - } catch (error: any) { - if (shouldThrow) { - error.message = `Next.js ERROR: Failed to read file ${filePath}:\n${error.message}` - throw error - } - } -} - // These are the platforms we'll try to load wasm bindings first, // only try to load native bindings if loading wasm binding somehow fails. // Fallback to native binding is for migration period only, @@ -174,13 +160,12 @@ export interface Binding { turboEngineOptions?: TurboEngineOptions ) => Promise } - analysis: { - isDynamicMetadataRoute(pageFilePath: string): Promise - } minify: any minifySync: any transform: any transformSync: any + parse: any + parseSync: any getTargetTriple(): string | undefined @@ -213,11 +198,11 @@ export async function loadBindings( // and https://github.com/nodejs/node/blob/main/doc/api/process.md#a-note-on-process-io if (process.stdout._handle != null) { // @ts-ignore - process.stdout._handle.setBlocking(true) + process.stdout._handle.setBlocking?.(true) } if (process.stderr._handle != null) { // @ts-ignore - process.stderr._handle.setBlocking(true) + process.stderr._handle.setBlocking?.(true) } pendingBindings = new Promise(async (resolve, _reject) => { @@ -1194,26 +1179,6 @@ function bindingToApi(binding: any, _wasm: boolean) { return createProject } -const warnedInvalidValueMap = { - runtime: new Map(), - preferredRegion: new Map(), -} as const -function warnInvalidValue( - pageFilePath: string, - key: keyof typeof warnedInvalidValueMap, - message: string -): void { - if (warnedInvalidValueMap[key].has(pageFilePath)) return - - Log.warn( - `Next.js can't recognize the exported \`${key}\` field in "${pageFilePath}" as ${message}.` + - '\n' + - 'The default runtime will be used instead.' - ) - - warnedInvalidValueMap[key].set(pageFilePath, true) -} - async function loadWasm(importPath = '') { if (wasmBindings) { return wasmBindings @@ -1255,33 +1220,13 @@ async function loadWasm(importPath = '') { minifySync(src: string, options: any) { return bindings.minifySync(src.toString(), options) }, - analysis: { - isDynamicMetadataRoute: async (pageFilePath: string) => { - const fileContent = (await tryToReadFile(pageFilePath, true)) || '' - const { isDynamicMetadataRoute, warnings } = - await bindings.isDynamicMetadataRoute(pageFilePath, fileContent) - - warnings?.forEach( - ({ - key, - message, - }: { - key: keyof typeof warnedInvalidValueMap - message: string - }) => warnInvalidValue(pageFilePath, key, message) - ) - return isDynamicMetadataRoute - }, - getPageStaticInfo: async (params: Record) => { - const fileContent = - (await tryToReadFile(params.pageFilePath, !params.isDev)) || '' - - const raw = await bindings.getPageStaticInfo( - params.pageFilePath, - fileContent - ) - return coercePageStaticInfo(params.pageFilePath, raw) - }, + parse(src: string, options: any) { + return bindings?.parse + ? bindings.parse(src.toString(), options) + : Promise.resolve(bindings.parseSync(src.toString(), options)) + }, + parseSync(src: string, options: any) { + return bindings.parseSync(src.toString(), options) }, getTargetTriple() { return undefined @@ -1446,40 +1391,8 @@ function loadNative(importPath?: string) { return bindings.minifySync(toBuffer(src), toBuffer(options ?? {})) }, - analysis: { - isDynamicMetadataRoute: async (pageFilePath: string) => { - let fileContent: string | undefined = undefined - if (isYarnPnP) { - fileContent = (await tryToReadFile(pageFilePath, true)) || '' - } - - const { isDynamicMetadataRoute, warnings } = - await bindings.isDynamicMetadataRoute(pageFilePath, fileContent) - - // Instead of passing js callback into napi's context, bindings bubble up the warning messages - // and let next.js logger handles it. - warnings?.forEach( - ({ - key, - message, - }: { - key: keyof typeof warnedInvalidValueMap - message: string - }) => warnInvalidValue(pageFilePath, key, message) - ) - return isDynamicMetadataRoute - }, - - getPageStaticInfo: async (params: Record) => { - let fileContent: string | undefined = undefined - if (isYarnPnP) { - fileContent = - (await tryToReadFile(params.pageFilePath, !params.isDev)) || '' - } - - const raw = await bindings.getPageStaticInfo(params, fileContent) - return coercePageStaticInfo(params.pageFilePath, raw) - }, + parse(src: string, options: any) { + return bindings.parse(src, toBuffer(options ?? {})) }, getTargetTriple: bindings.getTargetTriple, @@ -1568,31 +1481,6 @@ function toBuffer(t: any) { return Buffer.from(JSON.stringify(t)) } -function coercePageStaticInfo(pageFilePath: string, raw?: string) { - if (!raw) return raw - - const parsed = JSON.parse(raw) - - parsed?.exportsInfo?.warnings?.forEach( - ({ - key, - message, - }: { - key: keyof typeof warnedInvalidValueMap - message: string - }) => warnInvalidValue(pageFilePath, key, message) - ) - - return { - ...parsed, - exportsInfo: { - ...parsed.exportsInfo, - directives: new Set(parsed?.exportsInfo?.directives ?? []), - extraProperties: new Set(parsed?.exportsInfo?.extraProperties ?? []), - }, - } -} - export async function isWasm(): Promise { let bindings = await loadBindings() return bindings.isWasm @@ -1618,6 +1506,14 @@ export function minifySync(src: string, options: any): string { return bindings.minifySync(src, options) } +export async function parse(src: string, options: any): Promise { + let bindings = await loadBindings() + let parserOptions = getParserOptions(options) + return bindings + .parse(src, parserOptions) + .then((astStr: any) => JSON.parse(astStr)) +} + export function getBinaryMetadata() { let bindings try { diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/server-reference.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/server-reference.ts index 52d2f6cab8d3b..642d0293568bd 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/server-reference.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/server-reference.ts @@ -1,17 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { registerServerReference as flightRegisterServerReference } from 'react-server-dom-webpack/server.edge' -const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference') - -function isServerReference(reference: any) { - return reference && reference.$$typeof === SERVER_REFERENCE_TAG -} - export function registerServerReference(id: string, action: any) { - // Avoid registering the same action twice - if (isServerReference(action)) { - return action - } - return flightRegisterServerReference(action, id, null) } diff --git a/packages/next/src/cli/next-lint.ts b/packages/next/src/cli/next-lint.ts index 1875dd0c06e6c..bd3563679efe1 100755 --- a/packages/next/src/cli/next-lint.ts +++ b/packages/next/src/cli/next-lint.ts @@ -99,7 +99,7 @@ const nextLint = async (options: NextLintOptions, directory?: string) => { await verifyTypeScriptSetup({ dir: baseDir, - distDir: distDir, + distDir: nextConfig.distDir, intentDirs: [pagesDir, appDir].filter(Boolean) as string[], typeCheckPreflight: false, tsconfigPath: nextConfig.typescript.tsconfigPath, diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx deleted file mode 100644 index dadf679a28a6d..0000000000000 --- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx +++ /dev/null @@ -1,1984 +0,0 @@ -import React from 'react' -import type { fetchServerResponse as fetchServerResponseType } from '../fetch-server-response' -import type { FlightData } from '../../../../server/app-render/types' -import type { FlightRouterState } from '../../../../server/app-render/types' -import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' -import { createInitialRouterState } from '../create-initial-router-state' -import { - ACTION_NAVIGATE, - ACTION_PREFETCH, - PrefetchKind, -} from '../router-reducer-types' -import type { NavigateAction, PrefetchAction } from '../router-reducer-types' -import { navigateReducer } from './navigate-reducer' -import { prefetchReducer } from './prefetch-reducer' -import { handleMutable } from '../handle-mutable' - -const buildId = 'development' - -const flightData: FlightData = [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['__PAGE__', {}], - }, - ], - ['about', {},

About Page!

], - <> - About page! - , - ], -] - -const demographicsFlightData: FlightData = [ - [ - [ - '', - { - children: [ - 'parallel-tab-bar', - { - audience: [ - 'demographics', - { - children: ['__PAGE__', {}], - }, - ], - }, - ], - }, - null, - null, - true, - ], - [ - '', - {}, - - - Root layout from response - , - ], - <> - Demographics Head - , - ], -] - -jest.mock('../fetch-server-response', () => { - return { - fetchServerResponse: ( - url: URL - ): ReturnType => { - if (url.pathname === '/linking' && url.hash === '#hash') { - return Promise.resolve(['', undefined]) - } - if (url.pathname === '/linking/about') { - return Promise.resolve([flightData, undefined]) - } - - if (url.pathname === '/parallel-tab-bar/demographics') { - return Promise.resolve([demographicsFlightData, undefined]) - } - - throw new Error('unknown url in mock') - }, - } -}) - -const getInitialRouterStateTree = (): FlightRouterState => [ - '', - { - children: [ - 'linking', - { - children: ['__PAGE__', {}], - }, - ], - }, - undefined, - undefined, - true, -] - -describe('navigateReducer', () => { - beforeAll(() => { - jest.useFakeTimers() - jest.setSystemTime(new Date('2023-07-26')) - }) - - afterAll(() => { - jest.useRealTimers() - }) - - it('should apply navigation', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking/about', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "about" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "head": - - About page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- About Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about", - "focusAndScrollRef": { - "apply": true, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "linking", - "children", - "about", - "children", - "__PAGE__", - ], - ], - }, - "nextUrl": "/linking/about", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "temporary", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply navigation when called twice (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking/about', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "about" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "head": - - About page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- About Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about", - "focusAndScrollRef": { - "apply": true, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "linking", - "children", - "about", - "children", - "__PAGE__", - ], - ], - }, - "nextUrl": "/linking/about", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "temporary", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply navigation for external url (push)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const url = new URL('https://example.vercel.sh', 'https://localhost') - const isExternalUrl = url.origin !== 'localhost' - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url, - isExternalUrl, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "https://example.vercel.sh/", - "focusAndScrollRef": { - "apply": false, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [], - }, - "nextUrl": "/linking", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": true, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - } - `) - }) - - it('should apply navigation for external url (replace)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const url = new URL('https://example.vercel.sh', 'https://localhost') - const isExternalUrl = url.origin !== 'localhost' - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url, - isExternalUrl, - locationSearch: '', - navigateType: 'replace', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "https://example.vercel.sh/", - "focusAndScrollRef": { - "apply": false, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [], - }, - "nextUrl": "/linking", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": true, - "pendingPush": false, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - } - `) - }) - - it('should apply navigation for scroll', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking#hash', 'https://localhost') as any, - }) - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking#hash', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: false, // should not scroll - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking#hash", - "focusAndScrollRef": { - "apply": false, - "hashFragment": null, - "onlyHashChange": true, - "segmentPaths": [], - }, - "nextUrl": "/linking", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply navigation with prefetched data', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const url = new URL('/linking/about', 'https://localhost') - const prefetchAction: PrefetchAction = { - type: ACTION_PREFETCH, - url, - kind: PrefetchKind.AUTO, - } - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - await prefetchReducer(state, prefetchAction) - - await state.prefetchCache.get(url.pathname + url.search)?.data - - await prefetchReducer(state, prefetchAction) - await state.prefetchCache.get(url.pathname + url.search)?.data - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking/about', 'https://localhost'), - isExternalUrl: false, - navigateType: 'push', - locationSearch: '', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - const prom = Promise.resolve([ - [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['__PAGE__', {}], - }, - ], -

About Page!

, - - About page! - , - ], - ], - undefined, - ] as any) - await prom - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "about" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "head": - - About page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- About Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about", - "focusAndScrollRef": { - "apply": true, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "linking", - "children", - "about", - "children", - "__PAGE__", - ], - ], - }, - "nextUrl": "/linking/about", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "auto", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply parallel routes navigation (concurrent)', async () => { - const initialTree: FlightRouterState = [ - '', - { - children: [ - 'parallel-tab-bar', - { - audience: ['__PAGE__', {}], - views: ['__PAGE__', {}], - children: ['__PAGE__', {}], - }, - ], - }, - null, - null, - true, - ] - - const initialCanonicalUrl = '/parallel-tab-bar' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'parallel-tab-bar', - { - parallelRoutes: new Map([ - [ - 'audience', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Audience Page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - [ - 'views', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Views Page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Children Page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/parallel-tab-bar', 'https://localhost') as any, - }) - - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/parallel-tab-bar/demographics', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "parallel-tab-bar" => { - "lazyData": null, - "parallelRoutes": Map { - "audience" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Audience Page - , - }, - "demographics" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "head": - - Demographics Head - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc": null, - }, - }, - "views" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Views Page - , - }, - }, - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Children Page - , - }, - }, - }, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout from response - - , - }, - "canonicalUrl": "/parallel-tab-bar/demographics", - "focusAndScrollRef": { - "apply": true, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "parallel-tab-bar", - "audience", - "demographics", - "children", - "__PAGE__", - ], - ], - }, - "nextUrl": "/parallel-tab-bar/demographics", - "prefetchCache": Map { - "/parallel-tab-bar" => { - "data": Promise {}, - "key": "/parallel-tab-bar", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "parallel-tab-bar", - { - "audience": [ - "__PAGE__", - {}, - ], - "children": [ - "__PAGE__", - {}, - ], - "views": [ - "__PAGE__", - {}, - ], - }, - ], - }, - null, - null, - true, - ], - }, - "/parallel-tab-bar/demographics" => { - "data": Promise {}, - "key": "/parallel-tab-bar/demographics", - "kind": "temporary", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "parallel-tab-bar", - { - "audience": [ - "__PAGE__", - {}, - ], - "children": [ - "__PAGE__", - {}, - ], - "views": [ - "__PAGE__", - {}, - ], - }, - ], - }, - null, - null, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "parallel-tab-bar", - { - "audience": [ - "demographics", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - "children": [ - "__PAGE__", - {}, - ], - "views": [ - "__PAGE__", - {}, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply navigation for hash fragments within the same tree', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking#hash', 'https://localhost') as any, - }) - - const mutable = { - canonicalUrl: '/linking#hash', - previousTree: initialTree, - hashFragment: '#hash', - pendingPush: true, - shouldScroll: true, - preserveCustomHistoryState: false, - } - - const newState = handleMutable(state, mutable) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking#hash", - "focusAndScrollRef": { - "apply": false, - "hashFragment": "hash", - "onlyHashChange": true, - "segmentPaths": [], - }, - "nextUrl": "/linking", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - } - `) - }) - - it('should apply navigation for hash fragments within a different tree', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '__PAGE__', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - const action: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking/about#hash', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const newState = await navigateReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "about" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "__PAGE__" => { - "head": - - About page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- About Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about#hash", - "focusAndScrollRef": { - "apply": true, - "hashFragment": "hash", - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "linking", - "children", - "about", - "children", - "__PAGE__", - ], - ], - }, - "nextUrl": "/linking/about", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "temporary", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "__PAGE__", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) -}) diff --git a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx deleted file mode 100644 index 66149b39a1f40..0000000000000 --- a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import React from 'react' -import type { fetchServerResponse as fetchServerResponseType } from '../fetch-server-response' -import type { FlightData } from '../../../../server/app-render/types' -import type { FlightRouterState } from '../../../../server/app-render/types' -import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' -import { createInitialRouterState } from '../create-initial-router-state' -import { - ACTION_PREFETCH, - PrefetchCacheEntryStatus, - PrefetchKind, -} from '../router-reducer-types' -import type { - PrefetchAction, - PrefetchCacheEntry, -} from '../router-reducer-types' -import { prefetchReducer } from './prefetch-reducer' -import { fetchServerResponse } from '../fetch-server-response' - -jest.mock('../fetch-server-response', () => { - const flightData: FlightData = [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['', {}], - }, - ], - ['about', {},

About Page!

], - <> - About page! - , - ], - ] - return { - fetchServerResponse: ( - url: URL - ): ReturnType => { - if (url.pathname === '/linking/about') { - return Promise.resolve([flightData, undefined]) - } - - throw new Error('unknown url in mock') - }, - } -}) - -const getInitialRouterStateTree = (): FlightRouterState => [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, -] - -async function runPromiseThrowChain(fn: any): Promise { - try { - return await fn() - } catch (err) { - if (err instanceof Promise) { - await err - return await runPromiseThrowChain(fn) - } - - throw err - } -} - -describe('prefetchReducer', () => { - it('should apply navigation', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId: 'development', - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const url = new URL('/linking/about', 'https://localhost') - const serverResponse = await fetchServerResponse( - url, - initialTree, - null, - state.buildId, - PrefetchKind.AUTO - ) - const action: PrefetchAction = { - type: ACTION_PREFETCH, - url, - kind: PrefetchKind.AUTO, - } - - const newState = await runPromiseThrowChain(() => - prefetchReducer(state, action) - ) - - const prom = Promise.resolve(serverResponse) - await prom - - const expectedState: ReturnType = { - buildId: 'development', - prefetchCache: new Map([ - [ - '/linking', - { - key: '/linking', - data: expect.any(Promise), - prefetchTime: expect.any(Number), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - treeAtTimeOfPrefetch: initialTree, - status: PrefetchCacheEntryStatus.fresh, - }, - ], - [ - '/linking/about', - { - key: '/linking/about', - data: prom, - kind: PrefetchKind.AUTO, - lastUsedTime: null, - prefetchTime: expect.any(Number), - status: PrefetchCacheEntryStatus.fresh, - treeAtTimeOfPrefetch: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - ], - ]), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: true, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - Root layout - - ), - prefetchRsc: null, - parallelRoutes: initialParallelRoutes, - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - nextUrl: '/linking', - } - - expect(newState).toMatchObject(expectedState) - }) - - it('should apply navigation (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId: 'development', - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const state2 = createInitialRouterState({ - buildId: 'development', - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const url = new URL('/linking/about', 'https://localhost') - const serverResponse = await fetchServerResponse( - url, - initialTree, - null, - state.buildId, - PrefetchKind.AUTO - ) - const action: PrefetchAction = { - type: ACTION_PREFETCH, - url, - kind: PrefetchKind.AUTO, - } - - await runPromiseThrowChain(() => prefetchReducer(state, action)) - - const newState = await runPromiseThrowChain(() => - prefetchReducer(state2, action) - ) - - const prom = Promise.resolve(serverResponse) - await prom - - const prefetchCache = new Map() - prefetchCache.set('/linking', { - data: expect.any(Promise), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - prefetchTime: expect.any(Number), - treeAtTimeOfPrefetch: initialTree, - key: '/linking', - status: PrefetchCacheEntryStatus.fresh, - }) - - const expectedState: ReturnType = { - buildId: 'development', - prefetchCache: new Map([ - [ - '/linking', - { - key: '/linking', - data: expect.any(Promise), - prefetchTime: expect.any(Number), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - treeAtTimeOfPrefetch: initialTree, - status: PrefetchCacheEntryStatus.fresh, - }, - ], - [ - '/linking/about', - { - key: '/linking/about', - data: prom, - prefetchTime: expect.any(Number), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - status: PrefetchCacheEntryStatus.fresh, - treeAtTimeOfPrefetch: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - ], - ]), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: true, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - Root layout - - ), - prefetchRsc: null, - parallelRoutes: initialParallelRoutes, - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - nextUrl: '/linking', - } - - expect(newState).toMatchObject(expectedState) - }) -}) diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx deleted file mode 100644 index 40d6700e8c711..0000000000000 --- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx +++ /dev/null @@ -1,738 +0,0 @@ -import React from 'react' -import type { fetchServerResponse } from '../fetch-server-response' -import type { FlightData } from '../../../../server/app-render/types' -import type { FlightRouterState } from '../../../../server/app-render/types' -import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' -import { createInitialRouterState } from '../create-initial-router-state' -import { ACTION_REFRESH } from '../router-reducer-types' -import type { RefreshAction } from '../router-reducer-types' -import { refreshReducer } from './refresh-reducer' -const buildId = 'development' - -jest.mock('../fetch-server-response', () => { - const flightData: FlightData = [ - [ - [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - null, - null, - true, - ], - [ - '', - {}, - - - -

Linking Page!

- - , - ], - <> - Linking page! - , - ], - ] - return { - fetchServerResponse: (url: URL): ReturnType => { - if (url.pathname === '/linking') { - return Promise.resolve([flightData, undefined]) - } - - throw new Error('unknown url in mock') - }, - } -}) - -const getInitialRouterStateTree = (): FlightRouterState => [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, -] - -async function runPromiseThrowChain(fn: any): Promise { - try { - return await fn() - } catch (err) { - if (err instanceof Promise) { - await err - return await runPromiseThrowChain(fn) - } - - throw err - } -} - -describe('refreshReducer', () => { - it('should apply refresh', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - const action: RefreshAction = { - type: ACTION_REFRESH, - origin: new URL('/linking', 'https://localhost').origin, - } - - const newState = await runPromiseThrowChain(() => - refreshReducer(state, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map(), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: false, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - nextUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - -

Linking Page!

- - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: null, - prefetchRsc: null, - parallelRoutes: new Map(), - head: ( - <> - Linking page! - - ), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: null, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) - - it('should apply refresh (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const state2 = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const action: RefreshAction = { - type: ACTION_REFRESH, - origin: new URL('/linking', 'https://localhost').origin, - } - - await runPromiseThrowChain(() => refreshReducer(state, action)) - - const newState = await runPromiseThrowChain(() => - refreshReducer(state2, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map(), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: false, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - nextUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - -

Linking Page!

- - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: null, - prefetchRsc: null, - parallelRoutes: new Map(), - head: ( - <> - Linking page! - - ), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: null, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) - - it('should invalidate all segments (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - [ - 'about', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>About page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>About layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const state2 = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const action: RefreshAction = { - type: ACTION_REFRESH, - origin: new URL('/linking', 'https://localhost').origin, - } - - await runPromiseThrowChain(() => refreshReducer(state, action)) - - const newState = await runPromiseThrowChain(() => - refreshReducer(state2, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map(), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: false, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - nextUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - -

Linking Page!

- - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: null, - prefetchRsc: null, - parallelRoutes: new Map(), - head: ( - <> - Linking page! - - ), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: null, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) - - it('should invalidate prefetchCache (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - [ - 'about', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>About page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>About layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const state2 = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const action: RefreshAction = { - type: ACTION_REFRESH, - origin: new URL('/linking', 'https://localhost').origin, - } - - await runPromiseThrowChain(() => refreshReducer(state, action)) - - const newState = await runPromiseThrowChain(() => - refreshReducer(state2, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map(), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: false, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking', - nextUrl: '/linking', - cache: { - lazyData: null, - rsc: ( - - - -

Linking Page!

- - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: null, - prefetchRsc: null, - parallelRoutes: new Map(), - head: ( - <> - Linking page! - - ), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: null, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['', {}], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) -}) diff --git a/packages/next/src/client/components/router-reducer/reducers/restore-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/restore-reducer.test.tsx deleted file mode 100644 index ebd5ec8b19ef2..0000000000000 --- a/packages/next/src/client/components/router-reducer/reducers/restore-reducer.test.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import React from 'react' -import type { FlightRouterState } from '../../../../server/app-render/types' -import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' -import { createInitialRouterState } from '../create-initial-router-state' -import { - ACTION_RESTORE, - PrefetchCacheEntryStatus, - PrefetchKind, -} from '../router-reducer-types' -import type { RestoreAction } from '../router-reducer-types' -import { restoreReducer } from './restore-reducer' - -const buildId = 'development' - -const getInitialRouterStateTree = (): FlightRouterState => [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - undefined, - undefined, - true, -] - -async function runPromiseThrowChain(fn: any): Promise { - try { - return await fn() - } catch (err) { - if (err instanceof Promise) { - await err - return await runPromiseThrowChain(fn) - } - - throw err - } -} - -describe('serverPatchReducer', () => { - it('should apply server patch', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - const action: RestoreAction = { - type: ACTION_RESTORE, - url: new URL('/linking/about', 'https://localhost'), - tree: [ - '', - { - children: [ - 'linking', - { - children: [ - 'about', - { - children: ['', {}], - }, - ], - }, - ], - }, - null, - null, - true, - ], - } - - const newState = await runPromiseThrowChain(() => - restoreReducer(state, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map([ - [ - '/linking', - { - key: '/linking', - data: expect.any(Promise), - prefetchTime: expect.any(Number), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - treeAtTimeOfPrefetch: initialTree, - status: PrefetchCacheEntryStatus.fresh, - }, - ], - ]), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: true, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking/about', - nextUrl: '/linking/about', - cache: { - lazyData: null, - rsc: ( - - - Root layout - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - null, - null, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) - - it('should apply server patch (concurrent)', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - const state2 = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: new URL('/linking', 'https://localhost') as any, - }) - - const action: RestoreAction = { - type: ACTION_RESTORE, - url: new URL('/linking/about', 'https://localhost'), - tree: [ - '', - { - children: [ - 'linking', - { - children: [ - 'about', - { - children: ['', {}], - }, - ], - }, - ], - }, - null, - null, - true, - ], - } - - await runPromiseThrowChain(() => restoreReducer(state, action)) - - const newState = await runPromiseThrowChain(() => - restoreReducer(state2, action) - ) - - const expectedState: ReturnType = { - buildId, - prefetchCache: new Map([ - [ - '/linking', - { - key: '/linking', - data: expect.any(Promise), - prefetchTime: expect.any(Number), - kind: PrefetchKind.AUTO, - lastUsedTime: null, - treeAtTimeOfPrefetch: initialTree, - status: PrefetchCacheEntryStatus.fresh, - }, - ], - ]), - pushRef: { - mpaNavigation: false, - pendingPush: false, - preserveCustomHistoryState: true, - }, - focusAndScrollRef: { - apply: false, - onlyHashChange: false, - hashFragment: null, - segmentPaths: [], - }, - canonicalUrl: '/linking/about', - nextUrl: '/linking/about', - cache: { - lazyData: null, - rsc: ( - - - Root layout - - ), - prefetchRsc: null, - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]), - }, - tree: [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - null, - null, - true, - ], - } - - expect(newState).toMatchObject(expectedState) - }) -}) diff --git a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx deleted file mode 100644 index 7471b082cf588..0000000000000 --- a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx +++ /dev/null @@ -1,627 +0,0 @@ -import React from 'react' -import type { fetchServerResponse as fetchServerResponseType } from '../fetch-server-response' -import type { - FlightData, - FlightRouterState, -} from '../../../../server/app-render/types' -import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' -import { createInitialRouterState } from '../create-initial-router-state' -import { ACTION_SERVER_PATCH, ACTION_NAVIGATE } from '../router-reducer-types' -import type { ServerPatchAction, NavigateAction } from '../router-reducer-types' -import { navigateReducer } from './navigate-reducer' -import { serverPatchReducer } from './server-patch-reducer' -const buildId = 'development' - -jest.mock('../fetch-server-response', () => { - const flightData: FlightData = [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['', {}], - }, - ], - ['about', {},

About Page!

], - <> - About page! - , - ], - ] - return { - fetchServerResponse: ( - url: URL - ): ReturnType => { - if (url.pathname === '/linking/about') { - return Promise.resolve([flightData, undefined]) - } - - throw new Error('unknown url in mock') - }, - } -}) - -const flightDataForPatch: FlightData = [ - [ - 'children', - 'linking', - 'children', - 'somewhere-else', - [ - 'somewhere-else', - { - children: ['', {}], - }, - ], - ['somewhere-else', {},

Somewhere Page!

], - <> - Somewhere page! - , - ], -] - -const getInitialRouterStateTree = (): FlightRouterState => [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - undefined, - undefined, - true, -] - -describe('serverPatchReducer', () => { - beforeAll(() => { - jest.useFakeTimers() - jest.setSystemTime(new Date('2023-07-26')) - }) - - afterAll(() => { - jest.useRealTimers() - }) - - it('should apply server patch', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const url = new URL('/linking/about', 'https://localhost') as any - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: url, - }) - const action: ServerPatchAction = { - type: ACTION_SERVER_PATCH, - serverResponse: [flightDataForPatch, undefined], - previousTree: [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - const newState = await serverPatchReducer(state, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "somewhere-else" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "" => { - "head": - - Somewhere page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- Somewhere Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about", - "focusAndScrollRef": { - "apply": false, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [], - }, - "nextUrl": "/linking/somewhere-else", - "prefetchCache": Map { - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "", - {}, - ], - }, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": false, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "somewhere-else", - { - "children": [ - "", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it('should apply server patch without affecting focusAndScrollRef', async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ - [ - 'children', - new Map([ - [ - 'linking', - { - parallelRoutes: new Map([ - [ - 'children', - new Map([ - [ - '', - { - lazyData: null, - rsc: <>Linking page, - prefetchRsc: null, - parallelRoutes: new Map(), - }, - ], - ]), - ], - ]), - lazyData: null, - rsc: <>Linking layout level, - prefetchRsc: null, - }, - ], - ]), - ], - ]) - - const navigateAction: NavigateAction = { - type: ACTION_NAVIGATE, - url: new URL('/linking/about', 'https://localhost'), - isExternalUrl: false, - locationSearch: '', - navigateType: 'push', - shouldScroll: true, - } - - const url = new URL(initialCanonicalUrl, 'https://localhost') as any - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes, - location: url, - }) - - const stateAfterNavigate = await navigateReducer(state, navigateAction) - - const action: ServerPatchAction = { - type: ACTION_SERVER_PATCH, - serverResponse: [flightDataForPatch, undefined], - previousTree: [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - const newState = await serverPatchReducer(stateAfterNavigate, action) - - expect(newState).toMatchInlineSnapshot(` - { - "buildId": "development", - "cache": { - "lazyData": null, - "lazyDataResolved": false, - "parallelRoutes": Map { - "children" => Map { - "linking" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "" => { - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": - Linking page - , - }, - "about" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "" => { - "head": - - About page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- About Page! -

, - }, - "somewhere-else" => { - "lazyData": null, - "parallelRoutes": Map { - "children" => Map { - "" => { - "head": - - Somewhere page! - - , - "lazyData": null, - "parallelRoutes": Map {}, - "prefetchRsc": null, - "rsc": null, - }, - }, - }, - "prefetchRsc": null, - "rsc":

- Somewhere Page! -

, - }, - }, - }, - "prefetchRsc": null, - "rsc": - Linking layout level - , - }, - }, - }, - "prefetchRsc": null, - "rsc": - - - Root layout - - , - }, - "canonicalUrl": "/linking/about", - "focusAndScrollRef": { - "apply": true, - "hashFragment": null, - "onlyHashChange": false, - "segmentPaths": [ - [ - "children", - "linking", - "children", - "about", - "children", - "", - ], - ], - }, - "nextUrl": "/linking/somewhere-else", - "prefetchCache": Map { - "/linking" => { - "data": Promise {}, - "key": "/linking", - "kind": "auto", - "lastUsedTime": null, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "", - {}, - ], - }, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - "/linking/about" => { - "data": Promise {}, - "key": "/linking/about", - "kind": "temporary", - "lastUsedTime": 1690329600000, - "prefetchTime": 1690329600000, - "status": "fresh", - "treeAtTimeOfPrefetch": [ - "", - { - "children": [ - "linking", - { - "children": [ - "about", - { - "children": [ - "", - {}, - ], - }, - ], - }, - ], - }, - undefined, - undefined, - true, - ], - }, - }, - "pushRef": { - "mpaNavigation": false, - "pendingPush": true, - "preserveCustomHistoryState": false, - }, - "tree": [ - "", - { - "children": [ - "linking", - { - "children": [ - "somewhere-else", - { - "children": [ - "", - {}, - ], - }, - ], - }, - ], - }, - , - , - true, - ], - } - `) - }) - - it("should gracefully recover if the server patch doesn't match the current tree", async () => { - const initialTree = getInitialRouterStateTree() - const initialCanonicalUrl = '/linking' - const children = ( - - - Root layout - - ) - - const url = new URL('/linking/about', 'https://localhost') as any - const state = createInitialRouterState({ - buildId, - initialTree, - initialHead: null, - initialCanonicalUrl, - initialSeedData: ['', {}, children], - initialParallelRoutes: new Map(), - location: url, - }) - - const action: ServerPatchAction = { - type: ACTION_SERVER_PATCH, - // this flight data is intentionally completely unrelated to the existing tree - serverResponse: [ - [ - [ - 'children', - 'tree-patch-failure', - 'children', - 'new-page', - ['new-page', { children: ['__PAGE__', {}] }], - null, - null, - ], - ], - undefined, - ], - previousTree: [ - '', - { - children: [ - 'linking', - { - children: ['about', { children: ['', {}] }], - }, - ], - }, - undefined, - undefined, - true, - ], - } - - const newState = await serverPatchReducer(state, action) - expect(newState.pushRef.pendingPush).toBe(true) - expect(newState.pushRef.mpaNavigation).toBe(true) - expect(newState.canonicalUrl).toBe('/linking/about') - }) -}) diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 813684b23290b..361f9fd9b5854 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 176fd50ac6a0d..e6ef47c4d56a3 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.1.2-canary.5", + "version": "14.2.0-canary.0", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.1.2-canary.5", + "next": "14.2.0-canary.0", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4d199c16889e..ad1b9fcf643a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,9 +392,6 @@ importers: octokit: specifier: 3.1.0 version: 3.1.0 - open: - specifier: 9.0.0 - version: 9.0.0 outdent: specifier: 0.8.0 version: 0.8.0 @@ -750,7 +747,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -797,7 +794,7 @@ importers: dependencies: next: specifier: '*' - version: 14.1.1(@babel/core@7.22.5)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.54.0) + version: link:../next devDependencies: '@types/fontkit': specifier: 2.0.0 @@ -812,7 +809,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../next-env '@swc/helpers': specifier: 0.5.5 @@ -933,16 +930,16 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/polyfill-module': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../react-refresh-utils '@next/swc': - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1557,7 +1554,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.1.2-canary.5 + specifier: 14.2.0-canary.0 version: link:../next outdent: specifier: 0.8.0 @@ -5360,91 +5357,6 @@ packages: resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==} dev: true - /@next/env@14.1.1: - resolution: {integrity: sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA==} - dev: false - - /@next/swc-darwin-arm64@14.1.1: - resolution: {integrity: sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-darwin-x64@14.1.1: - resolution: {integrity: sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-gnu@14.1.1: - resolution: {integrity: sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-arm64-musl@14.1.1: - resolution: {integrity: sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-gnu@14.1.1: - resolution: {integrity: sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-linux-x64-musl@14.1.1: - resolution: {integrity: sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-arm64-msvc@14.1.1: - resolution: {integrity: sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-ia32-msvc@14.1.1: - resolution: {integrity: sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-x64-msvc@14.1.1: - resolution: {integrity: sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3: resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} requiresBuild: true @@ -5930,6 +5842,7 @@ packages: /@opentelemetry/api@1.4.1: resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'} + dev: true /@opentelemetry/api@1.6.0: resolution: {integrity: sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==} @@ -6590,12 +6503,6 @@ packages: tslib: 2.6.2 dev: true - /@swc/helpers@0.5.2: - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} - dependencies: - tslib: 2.6.2 - dev: false - /@swc/helpers@0.5.5: resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: @@ -8731,11 +8638,6 @@ packages: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} dev: true - /big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} - dev: true - /big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} dev: true @@ -8813,13 +8715,6 @@ packages: widest-line: 3.1.0 dev: true - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - dependencies: - big-integer: 1.6.51 - dev: true - /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -9006,13 +8901,6 @@ packages: semver: 7.3.7 dev: true - /bundle-name@3.0.0: - resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} - engines: {node: '>=12'} - dependencies: - run-applescript: 5.0.0 - dev: true - /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -10160,6 +10048,7 @@ packages: dependencies: lru-cache: 4.1.5 which: 1.3.1 + dev: false /cross-spawn@4.0.2: resolution: {integrity: sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==} @@ -10812,24 +10701,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} - dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 - dev: true - - /default-browser@3.1.0: - resolution: {integrity: sha512-SOHecvSoairSAWxEHP/0qcsld/KtI3DargfEuELQDyHIYmS2EMgdGhHOTC1GxaYr+NLUV6kDroeiSBfnNHnn8w==} - engines: {node: '>=12'} - dependencies: - bundle-name: 3.0.0 - default-browser-id: 3.0.0 - execa: 5.0.0 - xdg-default-browser: 2.1.0 - dev: true - /defaults@1.0.3: resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} dependencies: @@ -10849,11 +10720,6 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} - /define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - dev: true - /define-properties@1.1.3: resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==} engines: {node: '>= 0.4'} @@ -12324,17 +12190,6 @@ packages: safe-buffer: 5.2.1 dev: true - /execa@0.2.2: - resolution: {integrity: sha512-zmBGzLd3nhA/NB9P7VLoceAO6vyYPftvl809Vjwe5U2fYI9tYWbeKqP3wZlAw9WS+znnkogf/bhSU+Gcn2NbkQ==} - engines: {node: '>=0.12'} - dependencies: - cross-spawn-async: 2.2.5 - npm-run-path: 1.0.0 - object-assign: 4.1.1 - path-key: 1.0.0 - strip-eof: 1.0.0 - dev: true - /execa@0.4.0: resolution: {integrity: sha1-TrZGejaglfq7KXD/nV4/t7zm68M=} engines: {node: '>=0.12'} @@ -14697,12 +14552,6 @@ packages: engines: {node: '>=8'} hasBin: true - /is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - dev: true - /is-empty@1.2.0: resolution: {integrity: sha1-3pu1snhzigWgsJpX4ftNSjQan2s=} dev: true @@ -14789,14 +14638,6 @@ packages: /is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - /is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - dependencies: - is-docker: 3.0.0 - dev: true - /is-installed-globally@0.3.2: resolution: {integrity: sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==} engines: {node: '>=8'} @@ -18175,47 +18016,6 @@ packages: resolution: {integrity: sha1-yobR/ogoFpsBICCOPchCS524NCw=} dev: true - /next@14.1.1(@babel/core@7.22.5)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.54.0): - resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true - dependencies: - '@next/env': 14.1.1 - '@opentelemetry/api': 1.4.1 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001579 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - sass: 1.54.0 - styled-jsx: 5.1.1(@babel/core@7.22.5)(react@18.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 14.1.1 - '@next/swc-darwin-x64': 14.1.1 - '@next/swc-linux-arm64-gnu': 14.1.1 - '@next/swc-linux-arm64-musl': 14.1.1 - '@next/swc-linux-x64-gnu': 14.1.1 - '@next/swc-linux-x64-musl': 14.1.1 - '@next/swc-win32-arm64-msvc': 14.1.1 - '@next/swc-win32-ia32-msvc': 14.1.1 - '@next/swc-win32-x64-msvc': 14.1.1 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - /nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true @@ -18549,6 +18349,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: path-key: 1.0.0 + dev: false /npm-run-path@3.1.0: resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} @@ -18797,16 +18598,6 @@ packages: is-docker: 2.2.1 is-wsl: 2.2.0 - /open@9.0.0: - resolution: {integrity: sha512-yerrN5WPzgwuE3T6rxAkT1UuMLDzs4Szpug7hy9s4gru3iOTnaU0yKc1AYOVYrBzvykce5gUdr9RPNB4R+Zc/A==} - engines: {node: '>=14.16'} - dependencies: - default-browser: 3.1.0 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 2.2.0 - dev: true - /opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -19335,6 +19126,7 @@ packages: /path-key@1.0.0: resolution: {integrity: sha512-T3hWy7tyXlk3QvPFnT+o2tmXRzU4GkitkUWLp/WZ0S/FXd7XMx176tRurgTvHTNMJOQzTcesHNpBqetH86mQ9g==} engines: {node: '>=0.10.0'} + dev: false /path-key@2.0.1: resolution: {integrity: sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=} @@ -22265,13 +22057,6 @@ packages: fsevents: 2.1.3 dev: true - /run-applescript@5.0.0: - resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} - engines: {node: '>=12'} - dependencies: - execa: 5.0.0 - dev: true - /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -23375,6 +23160,7 @@ packages: /strip-eof@1.0.0: resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} engines: {node: '>=0.10.0'} + dev: false /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} @@ -23952,11 +23738,6 @@ packages: tslib: 2.6.2 dev: true - /titleize@1.0.1: - resolution: {integrity: sha512-rUwGDruKq1gX+FFHbTl5qjI7teVO7eOe+C8IcQ7QT+1BK3eEUXJqbZcBOeaRP4FwSC/C1A5jDoIVta0nIQ9yew==} - engines: {node: '>=0.10.0'} - dev: true - /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -24762,11 +24543,6 @@ packages: isobject: 3.0.1 dev: false - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - dev: true - /upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -25572,14 +25348,6 @@ packages: engines: {node: '>=8'} dev: true - /xdg-default-browser@2.1.0: - resolution: {integrity: sha512-HY4G725+IDQr16N8XOjAms5qJGArdJaWIuC7Q7A8UXIwj2mifqnPXephazyL7sIkQPvmEoPX3E0v2yFv6hQUNg==} - engines: {node: '>=4'} - dependencies: - execa: 0.2.2 - titleize: 1.0.1 - dev: true - /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} diff --git a/scripts/start-release.js b/scripts/start-release.js index fbf0c2ac18da7..f72df96d43e8f 100644 --- a/scripts/start-release.js +++ b/scripts/start-release.js @@ -1,79 +1,6 @@ const path = require('path') const execa = require('execa') const resolveFrom = require('resolve-from') -const ansiEscapes = require('ansi-escapes') - -function getPromptErrorDetails(rawAssertion, mostRecentChunk) { - const assertion = rawAssertion.toString().trim() - const mostRecent = (mostRecentChunk || '').trim() - return `Waiting for:\n "${assertion}"\nmost recent chunk was:\n "${mostRecent}"` -} - -async function waitForPrompt(cp, rawAssertion, timeout = 3000) { - let assertion - if (typeof rawAssertion === 'string') { - assertion = (chunk) => chunk.includes(rawAssertion) - } else if (rawAssertion instanceof RegExp) { - assertion = (chunk) => rawAssertion.test(chunk) - } else { - assertion = rawAssertion - } - - return new Promise((resolve, reject) => { - let mostRecentChunk = 'NO CHUNKS SO FAR' - - console.log('Waiting for prompt...') - const handleTimeout = setTimeout(() => { - cleanup() - const promptErrorDetails = getPromptErrorDetails( - rawAssertion, - mostRecentChunk - ) - reject( - new Error( - `Timed out after ${timeout}ms in waitForPrompt. ${promptErrorDetails}` - ) - ) - }, timeout) - - const onComplete = () => { - cleanup() - const promptErrorDetails = getPromptErrorDetails( - rawAssertion, - mostRecentChunk - ) - reject( - new Error( - `Process exited before prompt was found in waitForPrompt. ${promptErrorDetails}` - ) - ) - } - - const onData = (rawChunk) => { - const chunk = rawChunk.toString() - - mostRecentChunk = chunk - console.log('> ' + chunk) - if (assertion(chunk)) { - cleanup() - resolve() - } - } - - const cleanup = () => { - cp.stdout?.off('data', onData) - cp.stderr?.off('data', onData) - cp.off('close', onComplete) - cp.off('exit', onComplete) - clearTimeout(handleTimeout) - } - - cp.stdout?.on('data', onData) - cp.stderr?.on('data', onData) - cp.on('close', onComplete) - cp.on('exit', onComplete) - }) -} const SEMVER_TYPES = ['patch', 'minor', 'major'] @@ -126,41 +53,18 @@ async function main() { }) console.log(`Running pnpm release-${isCanary ? 'canary' : 'stable'}...`) - const child = execa(`pnpm release-${isCanary ? 'canary' : 'stable'}`, { - stdio: 'pipe', - shell: true, - }) + const child = execa( + isCanary + ? `pnpm lerna version preminor --preid canary --force-publish -y && pnpm release --pre --skip-questions --show-url` + : `pnpm lerna version ${semverType} --force-publish -y`, + { + stdio: 'pipe', + shell: true, + } + ) child.stdout.pipe(process.stdout) child.stderr.pipe(process.stderr) - - if (isCanary) { - console.log("Releasing canary: enter 'y'\n") - child.stdin.write('y\n') - } else { - console.log('Wait for the version prompt to show up') - await waitForPrompt(child, 'Select a new version') - console.log('Releasing stable') - if (semverType === 'minor') { - console.log('Releasing minor: cursor down > 1\n') - child.stdin.write(ansiEscapes.cursorDown(1)) - } - if (semverType === 'major') { - console.log('Releasing major: curser down > 1') - child.stdin.write(ansiEscapes.cursorDown(1)) - console.log('Releasing major: curser down > 2') - child.stdin.write(ansiEscapes.cursorDown(1)) - } - if (semverType === 'patch') { - console.log('Releasing patch: cursor stay\n') - } - console.log('Enter newline') - child.stdin.write('\n') - await waitForPrompt(child, 'Changes:') - console.log('Enter y') - child.stdin.write('y\n') - } - console.log('Await child process...') await child console.log('Release process is finished') } diff --git a/test/development/middleware-errors/index.test.ts b/test/development/middleware-errors/index.test.ts index b3c4b14910972..89a768b6b91df 100644 --- a/test/development/middleware-errors/index.test.ts +++ b/test/development/middleware-errors/index.test.ts @@ -250,8 +250,6 @@ createNextDescribe( await next.fetch('/') await check(async () => { expect(next.cliOutput).toContain(`Expected '{', got '}'`) - // [NOTE] [Flaky] expect at least 2 occurrences of the error message, - // on CI sometimes have more message appended somehow expect( next.cliOutput.split(`Expected '{', got '}'`).length ).toBeGreaterThanOrEqual(2) diff --git a/test/production/exported-runtimes-value-validation/index.test.ts b/test/production/exported-runtimes-value-validation/index.test.ts index 94013f520f37a..f99474d7b6070 100644 --- a/test/production/exported-runtimes-value-validation/index.test.ts +++ b/test/production/exported-runtimes-value-validation/index.test.ts @@ -46,7 +46,9 @@ describe('Exported runtimes value validation', () => { ) ) expect(result.stderr).toEqual( - expect.stringContaining('Unsupported node type at "config.runtime"') + expect.stringContaining( + 'Unsupported node type "BinaryExpression" at "config.runtime"' + ) ) // Spread Operator within Object Expression expect(result.stderr).toEqual( @@ -88,7 +90,9 @@ describe('Exported runtimes value validation', () => { ) ) expect(result.stderr).toEqual( - expect.stringContaining('Unsupported node type at "config.runtime"') + expect.stringContaining( + 'Unsupported node type "CallExpression" at "config.runtime"' + ) ) // Unknown Object Key expect(result.stderr).toEqual( @@ -98,7 +102,7 @@ describe('Exported runtimes value validation', () => { ) expect(result.stderr).toEqual( expect.stringContaining( - 'Unsupported key type in the Object Expression at "config.runtime"' + 'Unsupported key type "Computed" in the Object Expression at "config.runtime"' ) ) })