| | import fs from 'node:fs/promises' |
| | import path from 'node:path' |
| | import { fileURLToPath } from 'node:url' |
| | import os from 'node:os' |
| |
|
| | export interface DependencyPaths { |
| | nextTarball: string |
| | nextMdxTarball: string |
| | nextEnvTarball: string |
| | nextBundleAnalyzerTarball: string |
| | nextSwcTarball: string |
| | } |
| |
|
| | interface NextPeerDeps { |
| | react: string |
| | reactDom: string |
| | [key: string]: unknown |
| | } |
| |
|
| | export interface PackageJson { |
| | dependencies?: Record<string, string> |
| | peerDependencies?: Record<string, string> |
| | overrides?: Record<string, string> |
| | resolutions?: Record<string, string> |
| | workspaces?: string[] | { packages: string[] } |
| | [key: string]: unknown |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export default async function patchPackageJson( |
| | targetProjectPath: string, |
| | paths: DependencyPaths |
| | ): Promise<string> { |
| | try { |
| | const root = await findWorkspaceRoot(targetProjectPath) |
| | const packageJsonPath = root |
| | ? path.join(root, 'package.json') |
| | : path.join(targetProjectPath, 'package.json') |
| |
|
| | const packageJsonValue = await readJsonValue(packageJsonPath) |
| | await patchWorkspacePackageJsonMap(paths, packageJsonValue) |
| | await writeJsonValue(packageJsonPath, packageJsonValue) |
| |
|
| | return packageJsonPath |
| | } catch (error) { |
| | throw new Error('Error patching package.json', { cause: error }) |
| | } |
| | } |
| |
|
| | async function readJsonValue(filePath: string): Promise<PackageJson> { |
| | try { |
| | const content = await fs.readFile(filePath, 'utf8') |
| | return JSON.parse(content) as PackageJson |
| | } catch (error) { |
| | throw new Error(`Could not read or parse ${filePath}`, { cause: error }) |
| | } |
| | } |
| |
|
| | async function writeJsonValue( |
| | filePath: string, |
| | value: PackageJson |
| | ): Promise<void> { |
| | try { |
| | const content = JSON.stringify(value, null, 2) + os.EOL |
| | await fs.writeFile(filePath, content) |
| | } catch (error) { |
| | throw new Error(`Failed to write ${filePath}`, { cause: error }) |
| | } |
| | } |
| |
|
| | async function patchWorkspacePackageJsonMap( |
| | paths: DependencyPaths, |
| | packageJsonMap: PackageJson |
| | ): Promise<PackageJson> { |
| | const nextPeerDeps = await getNextPeerDeps() |
| |
|
| | const overrides: [string, string][] = [ |
| | ['next', `file:${paths.nextTarball}`], |
| | ['@next/mdx', `file:${paths.nextMdxTarball}`], |
| | ['@next/env', `file:${paths.nextEnvTarball}`], |
| | ['@next/bundle-analyzer', `file:${paths.nextBundleAnalyzerTarball}`], |
| | ['@next/swc', `file:${paths.nextSwcTarball}`], |
| | ['react', nextPeerDeps.react], |
| | ['react-dom', nextPeerDeps.reactDom], |
| | ] |
| |
|
| | |
| | packageJsonMap.overrides = packageJsonMap.overrides || {} |
| | insertMapEntries(packageJsonMap.overrides, overrides) |
| |
|
| | |
| | packageJsonMap.resolutions = packageJsonMap.resolutions || {} |
| | insertMapEntries(packageJsonMap.resolutions, overrides) |
| |
|
| | |
| | packageJsonMap.dependencies = packageJsonMap.dependencies || {} |
| | insertMapEntries(packageJsonMap.dependencies, [ |
| | ['@next/swc', `file:${paths.nextSwcTarball}`], |
| | ]) |
| |
|
| | |
| | updateMapEntriesIfExists(packageJsonMap.dependencies, overrides) |
| |
|
| | return packageJsonMap |
| | } |
| |
|
| | |
| | |
| | |
| | async function getNextPeerDeps(): Promise<NextPeerDeps> { |
| | try { |
| | |
| | const currentFilePath = fileURLToPath(import.meta.url) |
| | const scriptDir = path.dirname(currentFilePath) |
| | const packageJsonPath = path.resolve( |
| | scriptDir, |
| | '../../packages/next/package.json' |
| | ) |
| |
|
| | const content = await fs.readFile(packageJsonPath, 'utf8') |
| | const nextPackageJson = JSON.parse(content) as PackageJson |
| |
|
| | if (!nextPackageJson.peerDependencies) { |
| | throw new Error('Next.js package.json is missing peerDependencies') |
| | } |
| |
|
| | return { |
| | react: nextPackageJson.peerDependencies.react || '', |
| | reactDom: nextPackageJson.peerDependencies['react-dom'] || '', |
| | } |
| | } catch (error) { |
| | throw new Error('Failed to get Next.js peer dependencies', { cause: error }) |
| | } |
| | } |
| |
|
| | function insertMapEntries( |
| | map: Record<string, string>, |
| | entries: [string, string][] |
| | ): void { |
| | for (const [key, value] of entries) { |
| | map[key] = value |
| | } |
| | } |
| |
|
| | function updateMapEntriesIfExists( |
| | map: Record<string, string>, |
| | entries: [string, string][] |
| | ): void { |
| | for (const [key, value] of entries) { |
| | if (map[key] !== undefined) { |
| | map[key] = value |
| | } |
| | } |
| | } |
| |
|
| | async function fileExists(filePath: string): Promise<boolean> { |
| | try { |
| | await fs.access(filePath) |
| | return true |
| | } catch { |
| | return false |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | async function findWorkspaceRoot(projectPath: string): Promise<string | null> { |
| | |
| | const envVars = ['NPM_CONFIG_WORKSPACE_DIR', 'npm_config_workspace_dir'] |
| | for (const ev of envVars) { |
| | if (process.env[ev]) { |
| | return process.env[ev] |
| | } |
| | } |
| |
|
| | try { |
| | const canonicalPath = await fs.realpath(projectPath) |
| | let currentDir = canonicalPath |
| |
|
| | |
| | while (currentDir !== path.parse(currentDir).root) { |
| | |
| | if (await fileExists(path.join(currentDir, 'pnpm-workspace.yaml'))) { |
| | return currentDir |
| | } |
| |
|
| | |
| | const packageJsonPath = path.join(currentDir, 'package.json') |
| | if (await fileExists(packageJsonPath)) { |
| | const packageJson = await readJsonValue(packageJsonPath) |
| | if (packageJson.workspaces) { |
| | return currentDir |
| | } |
| | } |
| |
|
| | |
| | currentDir = path.dirname(currentDir) |
| | } |
| |
|
| | |
| | return null |
| | } catch (error) { |
| | throw new Error('Failed to find workspace root', { cause: error }) |
| | } |
| | } |
| |
|