Skip to content

Commit

Permalink
Merge branch 'canary' into docs-typo-doubled-space
Browse files Browse the repository at this point in the history
  • Loading branch information
lumirlumir authored Oct 11, 2024
2 parents 6cecea0 + fd552c5 commit 6231fbe
Show file tree
Hide file tree
Showing 82 changed files with 1,296 additions and 734 deletions.
315 changes: 85 additions & 230 deletions Cargo.lock

Large diffs are not rendered by default.

140 changes: 82 additions & 58 deletions crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use turbopack_core::{
diagnostics::PlainDiagnostic,
error::PrettyPrintError,
issue::PlainIssue,
source_map::Token,
source_map::{SourceMap, Token},
version::{PartialUpdate, TotalUpdate, Update, VersionState},
SOURCE_MAP_PREFIX,
};
Expand Down Expand Up @@ -1002,74 +1002,76 @@ pub struct StackFrame {
pub method_name: Option<String>,
}

pub async fn get_source_map(
container: Vc<ProjectContainer>,
file_path: String,
) -> Result<Option<Vc<SourceMap>>> {
let (file, module) = match Url::parse(&file_path) {
Ok(url) => match url.scheme() {
"file" => {
let path = urlencoding::decode(url.path())?.to_string();
let module = url.query_pairs().find(|(k, _)| k == "id");
(
path,
match module {
Some(module) => Some(urlencoding::decode(&module.1)?.into_owned().into()),
None => None,
},
)
}
_ => bail!("Unknown url scheme"),
},
Err(_) => (file_path.to_string(), None),
};

let Some(chunk_base) = file.strip_prefix(
&(format!(
"{}/{}/",
container.project().await?.project_path,
container.project().dist_dir().await?
)),
) else {
// File doesn't exist within the dist dir
return Ok(None);
};

let server_path = container.project().node_root().join(chunk_base.into());

let client_path = container
.project()
.client_relative_path()
.join(chunk_base.into());

let mut map = container
.get_source_map(server_path, module.clone())
.await?;

if map.is_none() {
// If the chunk doesn't exist as a server chunk, try a client chunk.
// TODO: Properly tag all server chunks and use the `isServer` query param.
// Currently, this is inaccurate as it does not cover RSC server
// chunks.
map = container.get_source_map(client_path, module).await?;
}

let map = map.context("chunk/module is missing a sourcemap")?;

Ok(Some(map))
}

#[napi]
pub async fn project_trace_source(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
frame: StackFrame,
) -> napi::Result<Option<StackFrame>> {
let turbo_tasks = project.turbo_tasks.clone();
let container = project.container;
let traced_frame = turbo_tasks
.run_once(async move {
let (file, module) = match Url::parse(&frame.file) {
Ok(url) => match url.scheme() {
"file" => {
let path = urlencoding::decode(url.path())?.to_string();
let module = url.query_pairs().find(|(k, _)| k == "id");
(
path,
match module {
Some(module) => {
Some(urlencoding::decode(&module.1)?.into_owned().into())
}
None => None,
},
)
}
_ => bail!("Unknown url scheme"),
},
Err(_) => (frame.file.to_string(), None),
};

let Some(chunk_base) = file.strip_prefix(
&(format!(
"{}/{}/",
project.container.project().await?.project_path,
project.container.project().dist_dir().await?
)),
) else {
// File doesn't exist within the dist dir
let Some(map) = get_source_map(container, frame.file).await? else {
return Ok(None);
};

let server_path = project
.container
.project()
.node_root()
.join(chunk_base.into());

let client_path = project
.container
.project()
.client_relative_path()
.join(chunk_base.into());

let mut map = project
.container
.get_source_map(server_path, module.clone())
.await?;

if map.is_none() {
// If the chunk doesn't exist as a server chunk, try a client chunk.
// TODO: Properly tag all server chunks and use the `isServer` query param.
// Currently, this is inaccurate as it does not cover RSC server
// chunks.
map = project
.container
.get_source_map(client_path, module)
.await?;
}
let map = map.context("chunk/module is missing a sourcemap")?;

let Some(line) = frame.line else {
return Ok(None);
};
Expand Down Expand Up @@ -1152,6 +1154,28 @@ pub async fn project_get_source_for_asset(
Ok(source)
}

#[napi]
pub async fn project_get_source_map(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
file_path: String,
) -> napi::Result<Option<String>> {
let turbo_tasks = project.turbo_tasks.clone();
let container = project.container;

let source_map = turbo_tasks
.run_once(async move {
let Some(map) = get_source_map(container, file_path).await? else {
return Ok(None);
};

Ok(Some(map.to_rope().await?.to_str()?.to_string()))
})
.await
.map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;

Ok(source_map)
}

/// Runs exit handlers for the project registered using the [`ExitHandler`] API.
#[napi]
pub async fn project_on_exit(
Expand Down
127 changes: 123 additions & 4 deletions packages/next-codemod/bin/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,44 @@ export async function runUpgrade(
return
}

const installedReactVersion = getInstalledReactVersion()
console.log(`Current React version: v${installedReactVersion}`)
let shouldStayOnReact18 = false
if (
// From release v14.3.0-canary.45, Next.js expects the React version to be 19.0.0-beta.0
// If the user is on a version higher than this but is still on React 18, we ask them
// if they still want to stay on React 18 after the upgrade.
// IF THE USER USES APP ROUTER, we expect them to upgrade React to > 19.0.0-beta.0,
// we should only let the user stay on React 18 if they are using pure Pages Router.
// x-ref(PR): https://github.com/vercel/next.js/pull/65058
// x-ref(release): https://github.com/vercel/next.js/releases/tag/v14.3.0-canary.45
compareVersions(installedNextVersion, '14.3.0-canary.45') >= 0 &&
installedReactVersion.startsWith('18')
) {
const shouldStayOnReact18Res = await prompts(
{
type: 'confirm',
name: 'shouldStayOnReact18',
message: `Are you using ${pc.underline('only the Pages Router')} (no App Router) and prefer to stay on React 18?`,
initial: false,
active: 'Yes',
inactive: 'No',
},
{ onCancel }
)
shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18
}

// We're resolving a specific version here to avoid including "ugly" version queries
// in the manifest.
// E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
// If we'd just `npm add` that, the manifest would read the same version query.
// This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
const targetReactVersion = await loadHighestNPMVersionMatching(
`react@${targetNextPackageJson.peerDependencies['react']}`
)
const targetReactVersion = shouldStayOnReact18
? '18.3.1'
: await loadHighestNPMVersionMatching(
`react@${targetNextPackageJson.peerDependencies['react']}`
)

if (compareVersions(targetNextVersion, '15.0.0-canary') >= 0) {
await suggestTurbopack(appPackageJson)
Expand All @@ -91,10 +121,30 @@ export async function runUpgrade(
installedNextVersion,
targetNextVersion
)
const packageManager: PackageManager = getPkgManager(process.cwd())

let shouldRunReactCodemods = false
let shouldRunReactTypesCodemods = false
let execCommand = 'npx'
// The following React codemods are for React 19
if (
!shouldStayOnReact18 &&
compareVersions(targetReactVersion, '19.0.0-beta.0') >= 0
) {
shouldRunReactCodemods = await suggestReactCodemods()
shouldRunReactTypesCodemods = await suggestReactTypesCodemods()

const execCommandMap = {
yarn: 'yarn dlx',
pnpm: 'pnpx',
bun: 'bunx',
npm: 'npx',
}
execCommand = execCommandMap[packageManager]
}

fs.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2))

const packageManager: PackageManager = getPkgManager(process.cwd())
const nextDependency = `next@${targetNextVersion}`
const reactDependencies = [
`react@${targetReactVersion}`,
Expand Down Expand Up @@ -134,6 +184,26 @@ export async function runUpgrade(
await runTransform(codemod, process.cwd(), { force: true, verbose })
}

// To reduce user-side burden of selecting which codemods to run as it needs additional
// understanding of the codemods, we run all of the applicable codemods.
if (shouldRunReactCodemods) {
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
execSync(
// `--no-interactive` skips the interactive prompt that asks for confirmation
// https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
`${execCommand} codemod@latest react/19/migration-recipe --no-interactive`,
{ stdio: 'inherit' }
)
}
if (shouldRunReactTypesCodemods) {
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
// `--yes` skips prompts and applies all codemods automatically
// https://github.com/eps1lon/types-react-codemod/blob/8463103233d6b70aad3cd6bee1814001eae51b28/README.md?plain=1#L52
execSync(`${execCommand} types-react-codemod@latest --yes preset-19 .`, {
stdio: 'inherit',
})
}

console.log() // new line
if (codemods.length > 0) {
console.log(`${pc.green('✔')} Codemods have been applied successfully.`)
Expand All @@ -160,6 +230,23 @@ function getInstalledNextVersion(): string {
}
}

function getInstalledReactVersion(): string {
try {
return require(
require.resolve('react/package.json', {
paths: [process.cwd()],
})
).version
} catch (error) {
throw new Error(
`Failed to detect the installed React version in "${process.cwd()}".\nIf you're working in a monorepo, please run this command from the Next.js app directory.`,
{
cause: error,
}
)
}
}

/*
* Heuristics are used to determine whether to Turbopack is enabled or not and
* to determine how to update the dev script.
Expand Down Expand Up @@ -263,3 +350,35 @@ async function suggestCodemods(

return codemods
}

async function suggestReactCodemods(): Promise<boolean> {
const { runReactCodemod } = await prompts(
{
type: 'toggle',
name: 'runReactCodemod',
message: 'Would you like to run the React 19 upgrade codemod?',
initial: true,
active: 'Yes',
inactive: 'No',
},
{ onCancel }
)

return runReactCodemod
}

async function suggestReactTypesCodemods(): Promise<boolean> {
const { runReactTypesCodemod } = await prompts(
{
type: 'toggle',
name: 'runReactTypesCodemod',
message: 'Would you like to run the React 19 Types upgrade codemod?',
initial: true,
active: 'Yes',
inactive: 'No',
},
{ onCancel }
)

return runReactTypesCodemod
}
5 changes: 3 additions & 2 deletions packages/next-codemod/lib/cra-to-next/global-css-transform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import nodePath from 'path'
import type { API, FileInfo, Options } from 'jscodeshift'
import { createParserFromPath } from '../parser'

export const globalCssContext = {
cssImports: new Set<string>(),
Expand All @@ -9,10 +10,10 @@ const globalStylesRegex = /(?<!\.module)\.(css|scss|sass)$/i

export default function transformer(
file: FileInfo,
api: API,
_api: API,
options: Options
) {
const j = api.jscodeshift.withParser('tsx')
const j = createParserFromPath(file.path)
const root = j(file.source)
let hasModifications = false

Expand Down
5 changes: 3 additions & 2 deletions packages/next-codemod/lib/cra-to-next/index-to-component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { API, FileInfo, JSXElement, Options } from 'jscodeshift'
import { createParserFromPath } from '../parser'

export const indexContext = {
multipleRenderRoots: false,
Expand All @@ -7,10 +8,10 @@ export const indexContext = {

export default function transformer(
file: FileInfo,
api: API,
_api: API,
options: Options
) {
const j = api.jscodeshift.withParser('tsx')
const j = createParserFromPath(file.path)
const root = j(file.source)
let hasModifications = false
let foundReactRender = 0
Expand Down
Loading

0 comments on commit 6231fbe

Please sign in to comment.