mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## What is `pack-next`? This is a script we use for developing turbopack. It builds tarballs for all the next.js packages, including `next-swc`, and then provides instructions about how to modify a project's `package.json` to install from that tarball. ## What does this PR do? If you provide a `--project ~/path/to/directory` argument to `pnpm pack-next`, we'll auto-update the `package.json` there for you, instead of telling you how to do it manually! This also understands the React 19 RC peer dependencies, which is something that was missing from the generated instructions. ## Supported Package Managers I've tested this with `npm` and `pnpm`. - `bun` fails with `ENOTDIR` for unknown reasons (maybe it's trying to handle the local tarball as a directory instead of a tarball?). - `yarn` fails due to an upstream issue: https://github.com/yarnpkg/yarn/issues/6339 This does generate the entries for yarn's `resolutions` feature, assuming the upstream issue is eventually fixed. ## Why Rust and `cargo xtask`? https://github.com/matklad/cargo-xtask I'd love for these Turbopack-centric scripts to be written in Rust with xtask, since they're mostly touched by the turbopack team. However, it's also hard to justify putting too much effort into these development scripts, so I don't want to rewrite the whole thing right now. I figured I'd write the new functionality in Rust and call it from the old JS script. ## Test Plan Perform a fresh checkout of shadcn/ui, and then: ``` pnpm pack-next --project ~/shadcn-ui/apps/www/ ``` ``` Found workspace! Patching "/home/bgw.linux/shadcn-ui/package.json" with overrides. Successfully patched "/home/bgw.linux/shadcn-ui/package.json" to use local tarball files! Run `pnpm i` or `npm i` to install the overrides ``` Check the resulting changes to `~/shadcn-ui`: ```diff diff --git a/package.json b/package.json index be47ad7..262c13b 100644 --- a/package.json +++ b/package.json @@ -63,12 +63,31 @@ "tsx": "^4.1.4", "turbo": "^1.9.9", "vite-tsconfig-paths": "^4.2.0", - "vitest": "^0.31.1" + "vitest": "^0.31.1", + "@next/swc": "file:/home/bgw.linux/next.js/tarballs/next-swc.tar" }, "devDependencies": { "@types/node": "^20.11.27", "@types/react": "^18.2.65", "@types/react-dom": "^18.2.22", "typescript": "^5.5.3" + }, + "overrides": { + "next": "file:/home/bgw.linux/next.js/tarballs/next.tar", + "@next/mdx": "file:/home/bgw.linux/next.js/tarballs/next-mdx.tar", + "@next/env": "file:/home/bgw.linux/next.js/tarballs/next-env.tar", + "@next/bundle-analyzer": "file:/home/bgw.linux/next.js/tarballs/next-bundle-analyzer.tar", + "@next/swc": "file:/home/bgw.linux/next.js/tarballs/next-swc.tar", + "react": "19.0.0-rc-7771d3a7-20240827", + "react-dom": "19.0.0-rc-7771d3a7-20240827" + }, + "resolutions": { + "next": "file:/home/bgw.linux/next.js/tarballs/next.tar", + "@next/mdx": "file:/home/bgw.linux/next.js/tarballs/next-mdx.tar", + "@next/env": "file:/home/bgw.linux/next.js/tarballs/next-env.tar", + "@next/bundle-analyzer": "file:/home/bgw.linux/next.js/tarballs/next-bundle-analyzer.tar", + "@next/swc": "file:/home/bgw.linux/next.js/tarballs/next-swc.tar", + "react": "19.0.0-rc-7771d3a7-20240827", + "react-dom": "19.0.0-rc-7771d3a7-20240827" } } ```
172 lines
4.3 KiB
JavaScript
172 lines
4.3 KiB
JavaScript
const { execSync, execFileSync, spawn } = require('child_process')
|
|
const { existsSync } = require('fs')
|
|
const globOrig = require('glob')
|
|
const { join } = require('path')
|
|
const { promisify } = require('util')
|
|
|
|
const glob = promisify(globOrig)
|
|
exports.glob = glob
|
|
|
|
const NEXT_DIR = join(__dirname, '..')
|
|
|
|
exports.NEXT_DIR = NEXT_DIR
|
|
|
|
/**
|
|
* @param {string} title
|
|
* @param {string | string[]} command
|
|
* @param {ExecSyncOptions} [opts]
|
|
* @returns {string}
|
|
*/
|
|
function exec(title, command, opts) {
|
|
if (Array.isArray(command)) {
|
|
logCommand(title, command)
|
|
return execFileSync(command[0], command.slice(1), {
|
|
stdio: 'inherit',
|
|
cwd: NEXT_DIR,
|
|
...opts,
|
|
})
|
|
} else {
|
|
logCommand(title, command)
|
|
return execSync(command, {
|
|
stdio: 'inherit',
|
|
cwd: NEXT_DIR,
|
|
...opts,
|
|
})
|
|
}
|
|
}
|
|
|
|
exports.exec = exec
|
|
|
|
/**
|
|
* @param {string} title
|
|
* @param {string | string[]} command
|
|
* @param {SpawnOptions} [opts]
|
|
*/
|
|
function execAsyncWithOutput(title, command, opts) {
|
|
logCommand(title, command)
|
|
const proc = spawn(command[0], command.slice(1), {
|
|
encoding: 'utf8',
|
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
cwd: NEXT_DIR,
|
|
...opts,
|
|
})
|
|
const stdout = []
|
|
proc.stdout.on('data', (data) => {
|
|
process.stdout.write(data)
|
|
stdout.push(data)
|
|
})
|
|
const stderr = []
|
|
proc.stderr.on('data', (data) => {
|
|
process.stderr.write(data)
|
|
stderr.push(data)
|
|
})
|
|
return new Promise((resolve, reject) => {
|
|
proc.on('exit', (code) => {
|
|
if (code === 0) {
|
|
return resolve({
|
|
stdout: Buffer.concat(stdout),
|
|
stderr: Buffer.concat(stderr),
|
|
})
|
|
}
|
|
const err = new Error(
|
|
`Command failed with exit code ${code}: ${prettyCommand(command)}`
|
|
)
|
|
err.code = code
|
|
err.stdout = Buffer.concat(stdout)
|
|
err.stderr = Buffer.concat(stderr)
|
|
reject(err)
|
|
})
|
|
})
|
|
}
|
|
|
|
exports.execAsyncWithOutput = execAsyncWithOutput
|
|
|
|
/**
|
|
* @param {string | string[]} command
|
|
*/
|
|
function prettyCommand(command) {
|
|
if (Array.isArray(command)) command = command.join(' ')
|
|
return command.replace(/ -- .*/, ' -- …')
|
|
}
|
|
|
|
/**
|
|
* @param {string} title
|
|
* @param {string | string[]} [command]
|
|
*/
|
|
function logCommand(title, command) {
|
|
if (command) {
|
|
const pretty = prettyCommand(command)
|
|
console.log(`\n\x1b[1;4m${title}\x1b[0m\n> \x1b[1m${pretty}\x1b[0m\n`)
|
|
} else {
|
|
console.log(`\n\x1b[1;4m${title}\x1b[0m\n`)
|
|
}
|
|
}
|
|
|
|
exports.logCommand = logCommand
|
|
|
|
/**
|
|
* @param {string[]} args
|
|
* @param {string} name
|
|
* @returns {boolean}
|
|
*/
|
|
function booleanArg(args, name) {
|
|
const index = args.indexOf(name)
|
|
if (index === -1) return false
|
|
args.splice(index, 1)
|
|
return true
|
|
}
|
|
|
|
exports.booleanArg = booleanArg
|
|
|
|
/**
|
|
* @param {string[]} args
|
|
* @param {string} name
|
|
* @returns {?string}
|
|
*/
|
|
function namedValueArg(args, name) {
|
|
const index = args.indexOf(name)
|
|
if (index === -1) return null
|
|
return args.splice(index, 2)[1]
|
|
}
|
|
|
|
exports.namedValueArg = namedValueArg
|
|
|
|
const DEFAULT_GLOBS = ['**', '!target', '!node_modules', '!crates', '!.turbo']
|
|
const FORCED_GLOBS = ['package.json', 'README*', 'LICENSE*', 'LICENCE*']
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @returns {Promise<string[]>}
|
|
*/
|
|
async function packageFiles(path) {
|
|
const { files = DEFAULT_GLOBS, main, bin } = require(`${path}/package.json`)
|
|
|
|
const allFiles = files.concat(
|
|
FORCED_GLOBS,
|
|
main ?? [],
|
|
Object.values(bin ?? {})
|
|
)
|
|
const isGlob = (f) => f.includes('*') || f.startsWith('!')
|
|
const simpleFiles = allFiles
|
|
.filter((f) => !isGlob(f) && existsSync(join(path, f)))
|
|
.map((f) => f.replace(/^\.\//, ''))
|
|
const globFiles = allFiles.filter(isGlob)
|
|
const globbedFiles = await glob(`+(${globFiles.join('|')})`, { cwd: path })
|
|
const packageFiles = [...globbedFiles, ...simpleFiles].sort()
|
|
const set = new Set()
|
|
return packageFiles.filter((f) => {
|
|
if (set.has(f)) return false
|
|
// We add the full path, but check for parent directories too.
|
|
// This catches the case where the whole directory is added and then a single file from the directory.
|
|
// The sorting before ensures that the directory comes before the files inside of the directory.
|
|
set.add(f)
|
|
while (f.includes('/')) {
|
|
f = f.replace(/\/[^/]+$/, '')
|
|
if (set.has(f)) return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
exports.packageFiles = packageFiles
|