Files
next.js/scripts/pack-util.cjs
T
Benjamin WoodruffandGitHub 882d88381c build(pack-next): Add an option for the pack-next script to automate patching package.json (#69396)
## 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"
   }
 }
```
2024-08-29 10:25:34 -07:00

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