mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
Initial commit
This commit is contained in:
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "$branch"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# Script to get version for Workers apps
|
||||
|
||||
if ! command -v jq >/dev/null; then
|
||||
echo "ERROR: jq is required for getting the version of the Worker from package.json. Please install jq."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pkg_json_version=$(jq -r '.version' package.json)
|
||||
gitsha=$(git log -1 --pretty=format:%h)
|
||||
|
||||
echo "$pkg_json_version-$gitsha"
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
cd "$repo_root"
|
||||
|
||||
# Stage changes so that changeset can see them
|
||||
git add .
|
||||
|
||||
changeset
|
||||
|
||||
# Make sure a changeset was created
|
||||
if ! git status --porcelain | grep '.changeset/.*\.md' >/dev/null; then
|
||||
echo "🚨 No changeset created"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new_changeset=$(find .changeset -name "*.md" -type f -exec ls -t {} + | head -n 1)
|
||||
echo "📝 New changeset: $new_changeset"
|
||||
git add "$new_changeset"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
cd "$repo_root"
|
||||
|
||||
syncpack fix-mismatches
|
||||
|
||||
# Update lockfile if there were any changes to package.json files
|
||||
if git status --porcelain | grep -q 'package.json'; then
|
||||
exec pnpm install --child-concurrency=10
|
||||
fi
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
args=(
|
||||
--type-aware
|
||||
.
|
||||
)
|
||||
|
||||
if [[ -n "${CI:-}" ]]; then
|
||||
args+=("--max-warnings=0")
|
||||
else
|
||||
args+=("--max-warnings=1000")
|
||||
fi
|
||||
|
||||
# get additional args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
args+=("$1")
|
||||
shift
|
||||
done
|
||||
|
||||
exec oxlint "${args[@]}"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# TODO: migrate back to tsc once typescript 7.0 comes out
|
||||
# (which will be based on tsgo)
|
||||
exec tsgo --noEmit -p tsconfig.json
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec vite build
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec vite dev
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec vite preview
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if test -f ./vitest.config.ts; then
|
||||
vitest --testTimeout=15000 "$@"
|
||||
fi
|
||||
if test -f ./vitest.config.node.ts; then
|
||||
vitest --testTimeout=15000 -c ./vitest.config.node.ts "$@"
|
||||
fi
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec vitest run --silent --testTimeout=15000
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Extract name and version from package.json using jq
|
||||
NAME=$(jq -r '.name' package.json)
|
||||
|
||||
echo "Building worker $NAME"
|
||||
exec wrangler build "$@"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Extract name and version from package.json using jq
|
||||
NAME=$(jq -r '.name' package.json)
|
||||
VERSION=$(get-version)
|
||||
|
||||
# Deploy with wrangler using the extracted values as binding variables
|
||||
echo "Deploying worker $NAME version $VERSION"
|
||||
exec wrangler deploy \
|
||||
--var NAME:"$NAME" \
|
||||
--var SENTRY_RELEASE:"$VERSION" \
|
||||
--minify \
|
||||
"$@"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
NAME=$(jq -r '.name' package.json)
|
||||
|
||||
exec wrangler dev \
|
||||
--var NAME:"$NAME" \
|
||||
"$@"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec wrangler types --include-env=false "$@"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_path="$(realpath "$(dirname "$0")/../src/bin/runx.cmd.ts")"
|
||||
|
||||
exec bun "$script_path" "$@"
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@repo/tools",
|
||||
"version": "0.3.2",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/lib/index.ts",
|
||||
"./zx": "./src/lib/zx.ts"
|
||||
},
|
||||
"directories": {
|
||||
"bin": "bin"
|
||||
},
|
||||
"scripts": {
|
||||
"check:lint": "run-oxlint",
|
||||
"check:types": "run-tsc",
|
||||
"test": "run-vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@commander-js/extra-typings": "14.0.0",
|
||||
"@jahands/cli-tools": "0.11.4",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "25.5.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260323.1",
|
||||
"cli-table3": "0.6.5",
|
||||
"commander": "14.0.3",
|
||||
"empathic": "2.0.0",
|
||||
"esbuild": "0.27.4",
|
||||
"memoize-one": "6.0.0",
|
||||
"p-map": "7.0.4",
|
||||
"smol-toml": "1.6.0",
|
||||
"ts-pattern": "5.9.0",
|
||||
"tsx": "4.21.0",
|
||||
"zod": "4.3.6",
|
||||
"zx": "8.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "1.3.11",
|
||||
"vitest": "4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'zx/globals'
|
||||
|
||||
import { program } from '@commander-js/extra-typings'
|
||||
import { catchProcessError } from '@jahands/cli-tools/proc'
|
||||
|
||||
import { buildCmd } from '../cmd/build.cmd'
|
||||
import { checkCmd } from '../cmd/check.cmd'
|
||||
import { ciCmd } from '../cmd/ci.cmd'
|
||||
import { devCmd } from '../cmd/dev.cmd'
|
||||
import { fixCmd } from '../cmd/fix.cmd'
|
||||
import { shfmtCmd } from '../cmd/shfmt.cmd'
|
||||
import { updateCmd } from '../cmd/update.cmd'
|
||||
|
||||
program
|
||||
.name('runx')
|
||||
.description('A CLI for scripts that automate this repo')
|
||||
|
||||
// While `packages/tools/bin` scripts work well for simple tasks,
|
||||
// a typescript CLI is nicer for more complex things.
|
||||
|
||||
.addCommand(fixCmd)
|
||||
.addCommand(buildCmd)
|
||||
.addCommand(checkCmd)
|
||||
.addCommand(devCmd)
|
||||
.addCommand(ciCmd)
|
||||
.addCommand(updateCmd)
|
||||
.addCommand(shfmtCmd)
|
||||
|
||||
// Don't hang for unresolved promises
|
||||
.hook('postAction', () => process.exit(0))
|
||||
.parseAsync()
|
||||
.catch(catchProcessError())
|
||||
@@ -0,0 +1,215 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
import { validateArg } from '@jahands/cli-tools/args'
|
||||
import * as esbuild from 'esbuild'
|
||||
import pMap from 'p-map'
|
||||
import { match } from 'ts-pattern'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { TSHelpers } from '../tsconfig'
|
||||
|
||||
import type { CompilerOptions as TSCompilerOptions } from 'typescript'
|
||||
|
||||
export const buildCmd = new Command('build').description('Scripts to build things')
|
||||
|
||||
type Format = z.infer<typeof Format>
|
||||
const Format = z.enum(['esm', 'cjs'])
|
||||
|
||||
buildCmd
|
||||
.command('tsc')
|
||||
.description('Build a library with tsc')
|
||||
.argument('<entrypoint...>', 'Entrypoint(s) for the program')
|
||||
.option('-r, --root-dir <string>', 'Root dir for tsc to use (overrides tsconfig.json)')
|
||||
.action(async (entrypoints, { rootDir }) => {
|
||||
const tsHelpers = await new TSHelpers().init()
|
||||
await $`rm -rf ./dist`
|
||||
|
||||
const tsOptions: TSCompilerOptions = {
|
||||
...tsHelpers.getTSConfig(),
|
||||
...(rootDir !== undefined && { rootDir }),
|
||||
}
|
||||
|
||||
tsHelpers.ts.createProgram(entrypoints, tsOptions).emit()
|
||||
})
|
||||
|
||||
buildCmd
|
||||
.command('bundle-lib')
|
||||
.alias('lib')
|
||||
.description('Bundle library with esbuild')
|
||||
|
||||
.argument('<entrypoints...>', 'Entrypoint(s) of the app. e.g. src/index.ts')
|
||||
.option('-d, --root-dir <string>', 'Root directory to look for entrypoints')
|
||||
.option('-f, --format <format...>', 'Formats to use (options: esm, cjs)', ['esm'])
|
||||
.option('--no-minify', `Don't minify output`)
|
||||
.option(
|
||||
'--sourcemap <string>',
|
||||
`Include sourcemaps in the output. (options: both, linked, inline, external, true, false)`,
|
||||
validateArg(z.union([z.enum(['both', 'linked', 'inline', 'external']), z.coerce.boolean()])),
|
||||
'both'
|
||||
)
|
||||
.option(
|
||||
'--platform <string>',
|
||||
'Optional platform to target. (options: cloudflare_workers,browser, node, neutral)',
|
||||
validateArg(z.enum(['cloudflare_workers', 'browser', 'node', 'neutral'])),
|
||||
'cloudflare_workers'
|
||||
)
|
||||
.option('--no-types', `Don't include .d.ts types in output (usually not recommended)`)
|
||||
|
||||
.action(
|
||||
async (entryPoints, { format: moduleFormats, platform, rootDir, minify, sourcemap, types }) => {
|
||||
entryPoints = z
|
||||
.string()
|
||||
.array()
|
||||
.min(1)
|
||||
.decode(entryPoints)
|
||||
.map((d) => path.join(rootDir ?? '.', d))
|
||||
|
||||
const formats = Format.array().parse(moduleFormats)
|
||||
|
||||
await fs.rm('./dist/', { force: true, recursive: true })
|
||||
|
||||
const maybeOutputTypes = types
|
||||
? $({
|
||||
stdio: 'inherit',
|
||||
})`runx build bundle-lib-build-types ${entryPoints}`
|
||||
: undefined
|
||||
|
||||
await Promise.all([
|
||||
maybeOutputTypes,
|
||||
...formats.map(async (outFormat) => {
|
||||
type Config = {
|
||||
format: Format
|
||||
outExt: string
|
||||
}
|
||||
|
||||
const { format, outExt } = match<'esm' | 'cjs', Config>(outFormat)
|
||||
.with('esm', () => ({
|
||||
format: 'esm',
|
||||
outExt: '.mjs',
|
||||
}))
|
||||
.with('cjs', () => ({
|
||||
format: 'cjs',
|
||||
outExt: '.cjs',
|
||||
}))
|
||||
.exhaustive()
|
||||
|
||||
const external: string[] = []
|
||||
if (platform === 'cloudflare_workers') {
|
||||
external.push('node:events', 'node:async_hooks', 'node:buffer', 'cloudflare:test')
|
||||
}
|
||||
|
||||
const opts: esbuild.BuildOptions = {
|
||||
entryPoints,
|
||||
outdir: './dist/',
|
||||
logLevel: 'warning',
|
||||
outExtension: {
|
||||
'.js': outExt,
|
||||
},
|
||||
target: 'es2022',
|
||||
bundle: true,
|
||||
minify,
|
||||
format,
|
||||
sourcemap,
|
||||
treeShaking: true,
|
||||
external,
|
||||
}
|
||||
|
||||
if (platform !== 'cloudflare_workers') {
|
||||
opts.platform = platform
|
||||
}
|
||||
|
||||
await esbuild.build(opts)
|
||||
}),
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
buildCmd
|
||||
.command('bundle-lib-build-types')
|
||||
.description('Separate command to build types (so that we can run them concurrently)')
|
||||
.argument('<entrypoints...>', 'Entrypoint(s) of the app. e.g. src/index.ts')
|
||||
.action(async (entryPoints) => {
|
||||
const tsHelpers = await new TSHelpers().init()
|
||||
const { ts } = tsHelpers
|
||||
z.string().array().min(1).decode(entryPoints)
|
||||
|
||||
const tsCompOpts = {
|
||||
...tsHelpers.getTSConfig(),
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
emitDeclarationOnly: true,
|
||||
noEmit: false,
|
||||
outDir: './dist/',
|
||||
} satisfies TSCompilerOptions
|
||||
|
||||
const program = ts.createProgram(entryPoints, tsCompOpts)
|
||||
program.emit()
|
||||
})
|
||||
|
||||
buildCmd
|
||||
.command('bun')
|
||||
.description('Bundle with Bun')
|
||||
|
||||
.argument('<entrypoints...>', 'Entrypoint(s) of the app. e.g. src/index.ts')
|
||||
.option('-f, --format <format...>', 'Formats to use (options: esm, cjs)', ['esm'])
|
||||
.option('--no-minify', `Don't minify output`)
|
||||
.option('--no-sourcemap', `Don't include sourcemaps`)
|
||||
|
||||
.action(async (entryPoints, { format, minify, sourcemap }) => {
|
||||
await fs.rm('./dist/', { force: true, recursive: true })
|
||||
|
||||
const formats = await z
|
||||
.array(Format)
|
||||
.parseAsync(format)
|
||||
.catch((e) => {
|
||||
throw new Error(`Invalid format: ${z.prettifyError(e)}`)
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
$({
|
||||
stdio: 'inherit',
|
||||
})`runx build bundle-lib-build-types ${entryPoints}`,
|
||||
|
||||
...formats.map(async (fmt) => {
|
||||
const distDir = `./dist/${fmt}`
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: entryPoints,
|
||||
outdir: distDir,
|
||||
target: 'node',
|
||||
minify,
|
||||
format: fmt,
|
||||
})
|
||||
|
||||
const outExt = match(fmt)
|
||||
.with('esm', () => '.mjs')
|
||||
.with('cjs', () => '.cjs')
|
||||
.exhaustive()
|
||||
|
||||
// change output files to mjs/cjs
|
||||
await pMap(await glob(`${distDir}/**/*.js`), async (file) => {
|
||||
await fs.rename(file, file.replace(/\.js$/, outExt))
|
||||
})
|
||||
}),
|
||||
])
|
||||
|
||||
const cleanupSourcemaps = async () => {
|
||||
if (sourcemap === false) {
|
||||
const files = await glob('dist/**/*.map')
|
||||
await Promise.all(files.map((file) => fs.rm(file)))
|
||||
}
|
||||
}
|
||||
|
||||
// executables don't need declaration files
|
||||
const cleanupBin = async () => {
|
||||
const files = await glob('dist/bin/*.d.ts')
|
||||
await Promise.all(files.map((file) => fs.rm(file)))
|
||||
}
|
||||
|
||||
await Promise.all([cleanupSourcemaps(), cleanupBin()])
|
||||
|
||||
// check if bin is empty
|
||||
const files = await glob('dist/bin/*')
|
||||
if (files.length === 0) {
|
||||
await fs.rm('dist/bin', { recursive: true })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
import Table from 'cli-table3'
|
||||
|
||||
import { getRepoRoot } from '../path'
|
||||
import { getOutcome, SHFMT_SKIPPED_EXIT_CODE } from '../proc'
|
||||
|
||||
export const checkCmd = new Command('check')
|
||||
.description(
|
||||
'Check for issues with deps/lint/types/format. If no options are provided, all checks are run.'
|
||||
)
|
||||
|
||||
.option('-r, --root', 'Run checks from root of repo. Defaults to cwd', false)
|
||||
.option('-d, --deps', 'Check for dependency issues with Syncpack')
|
||||
.option('-l, --lint', 'Check for oxlint issues')
|
||||
.option('-t, --types', 'Check for TypeScript issues')
|
||||
.option(
|
||||
'-f, --format',
|
||||
'Check for formatting issues with prettier. Also checks shell scripts if shfmt and rg (ripgrep) are available'
|
||||
)
|
||||
|
||||
.option('--continue', 'Use --continue when executing turbo commands', false)
|
||||
|
||||
.action(async ({ root, deps, lint, types, format, continue: useContinue }) => {
|
||||
const repoRoot = getRepoRoot()
|
||||
if (root) {
|
||||
cd(repoRoot)
|
||||
}
|
||||
|
||||
// Run all if none are selected
|
||||
if (!deps && !lint && !types && !format) {
|
||||
deps = true
|
||||
lint = true
|
||||
types = true
|
||||
format = true
|
||||
}
|
||||
|
||||
const cwd = process.cwd()
|
||||
const runFromRoot = cwd === repoRoot
|
||||
const cwdName = path.basename(cwd)
|
||||
|
||||
const turboFlags: string[] = []
|
||||
|
||||
if (useContinue) {
|
||||
turboFlags.push('--continue')
|
||||
}
|
||||
|
||||
const checks = {
|
||||
deps: ['syncpack', 'lint'],
|
||||
// oxlint can be run from anywhere and it'll automatically only lint the current dir and children
|
||||
lint: ['run-oxlint'],
|
||||
types: ['turbo', ...turboFlags, 'check:types'],
|
||||
format: ['prettier', '.', '--cache', '--check', '--log-level=warn'],
|
||||
formatShell: ['runx', 'shfmt', 'check', '--skip-if-unavailable'],
|
||||
workersTypes: ['turbo', ...turboFlags, 'check:workers-types'],
|
||||
} as const satisfies { [key: string]: string[] }
|
||||
|
||||
type TableRow = [string, string, string, string]
|
||||
const table = new Table({
|
||||
head: [
|
||||
chalk.whiteBright('Name'),
|
||||
chalk.whiteBright('Command'),
|
||||
chalk.whiteBright('Outcome'),
|
||||
chalk.whiteBright('Ran From'),
|
||||
] satisfies TableRow,
|
||||
})
|
||||
|
||||
$.stdio = 'inherit'
|
||||
$.verbose = true
|
||||
$.nothrow = true
|
||||
|
||||
let didErr = false
|
||||
function getAndCheckOutcome({
|
||||
exitCode,
|
||||
skippedCode,
|
||||
}: {
|
||||
exitCode: number | null
|
||||
skippedCode?: number
|
||||
}): string {
|
||||
if (exitCode !== 0 && exitCode !== skippedCode) {
|
||||
didErr = true
|
||||
}
|
||||
return getOutcome({ exitCode, skippedCode })
|
||||
}
|
||||
|
||||
if (deps) {
|
||||
const exitCode = await $({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${checks.deps}`.exitCode
|
||||
table.push([
|
||||
'deps',
|
||||
checks.deps.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
'Root',
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (lint) {
|
||||
const exitCode = await $`${checks.lint}`.exitCode
|
||||
table.push([
|
||||
'lint',
|
||||
checks.lint.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
runFromRoot ? 'Root' : `cwd (${cwdName})`,
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (types) {
|
||||
const exitCode = await $`${checks.types}`.exitCode
|
||||
table.push([
|
||||
'types',
|
||||
checks.types.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
runFromRoot ? 'Root' : `cwd (${cwdName})`,
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (format) {
|
||||
echo(chalk.dim('checking formatting with prettier (and shfmt if available)...'))
|
||||
|
||||
const [prettierProc, shfmtProc] = await Promise.all([
|
||||
$({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${checks.format}`,
|
||||
|
||||
$({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${checks.formatShell}`,
|
||||
])
|
||||
|
||||
table.push(
|
||||
[
|
||||
'format',
|
||||
checks.format.join(' '),
|
||||
getAndCheckOutcome({ exitCode: prettierProc.exitCode }),
|
||||
'Root',
|
||||
] satisfies TableRow,
|
||||
[
|
||||
'format shell',
|
||||
checks.formatShell.join(' '),
|
||||
getAndCheckOutcome({
|
||||
exitCode: shfmtProc.exitCode,
|
||||
skippedCode: SHFMT_SKIPPED_EXIT_CODE,
|
||||
}),
|
||||
'Root',
|
||||
] satisfies TableRow
|
||||
)
|
||||
|
||||
const workersTypesExitCode = await $({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${checks.workersTypes}`.exitCode
|
||||
table.push([
|
||||
'workers types',
|
||||
checks.workersTypes.join(' '),
|
||||
getAndCheckOutcome({ exitCode: workersTypesExitCode }),
|
||||
'Root',
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
echo(table.toString())
|
||||
if (didErr) {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
|
||||
import type { Options as ZXOptions } from 'zx'
|
||||
|
||||
export const ciCmd = new Command('ci').description('Scripts used in CI')
|
||||
|
||||
function opts(): Partial<ZXOptions> {
|
||||
return {
|
||||
verbose: true,
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
...process.env,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ciCmd
|
||||
.command('check')
|
||||
.description('Run CI checks')
|
||||
.action(async () => {
|
||||
await $(opts())`bun turbo check:ci`
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
|
||||
import { getRepoRoot } from '../path'
|
||||
|
||||
export const devCmd = new Command('dev')
|
||||
.description(
|
||||
'Run development server for Workers projects, etc. Use --runx-help to see all options.'
|
||||
)
|
||||
.argument(
|
||||
'[args...]',
|
||||
'Arguments to pass to the dev script. May need to use -- to pass options to the dev script.'
|
||||
)
|
||||
.allowUnknownOption()
|
||||
// allow passing --help to the dev script
|
||||
.helpOption('--runx-help')
|
||||
.action(async (args) => {
|
||||
const cwd = process.cwd()
|
||||
const repoRoot = getRepoRoot()
|
||||
const isRepoRoot = cwd === repoRoot
|
||||
|
||||
const [hasDevScript, hasWranglerJsonc] = await Promise.all([
|
||||
fs
|
||||
.readJson('./package.json')
|
||||
.then((packageJson) => packageJson.scripts?.dev !== undefined)
|
||||
.catch(() => {
|
||||
return false
|
||||
}),
|
||||
fs.pathExists('./wrangler.jsonc'),
|
||||
])
|
||||
|
||||
$.stdio = 'inherit'
|
||||
|
||||
if (!isRepoRoot && (hasWranglerJsonc || hasDevScript)) {
|
||||
await $`pnpm dev ${args}`
|
||||
} else {
|
||||
const argsWithSeparator = args.length > 0 ? ['--', ...args] : args
|
||||
await $`pnpm turbo dev ${argsWithSeparator}`
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
import Table from 'cli-table3'
|
||||
|
||||
import { getRepoRoot } from '../path'
|
||||
import { getOutcome, SHFMT_SKIPPED_EXIT_CODE } from '../proc'
|
||||
|
||||
export const fixCmd = new Command('fix')
|
||||
.description('Fix deps/lint/format issues. If no options are provided, all fixes are run.')
|
||||
|
||||
.option('-r, --root', 'Run fixes from root of repo. Defaults to cwd', false)
|
||||
.option('-d, --deps', 'Fix dependency versions with syncpack')
|
||||
.option('-l, --lint', 'Fix oxlint issues')
|
||||
.option('-f, --format', 'Format code with prettier')
|
||||
.option(
|
||||
'-w, --workers-types',
|
||||
'Generate Workers runtime types (worker-configuration.d.ts) via wrangler types'
|
||||
)
|
||||
|
||||
.action(async ({ root, deps, lint, format, workersTypes }) => {
|
||||
const repoRoot = getRepoRoot()
|
||||
if (root) {
|
||||
cd(repoRoot)
|
||||
}
|
||||
|
||||
// Run all if none are selected
|
||||
if (!deps && !lint && !format && !workersTypes) {
|
||||
deps = true
|
||||
lint = true
|
||||
format = true
|
||||
workersTypes = true
|
||||
}
|
||||
|
||||
const cwd = process.cwd()
|
||||
const runFromRoot = cwd === repoRoot
|
||||
const cwdName = path.basename(cwd)
|
||||
|
||||
const fixes = {
|
||||
deps: ['run-fix-deps'],
|
||||
lint: ['run-oxlint', '--fix'],
|
||||
workersTypes: ['turbo', 'fix:workers-types'],
|
||||
format: ['prettier', '.', '--cache', '--write', '--log-level=warn'],
|
||||
formatShell: ['runx', 'shfmt', 'fix', '--skip-if-unavailable'],
|
||||
} as const satisfies { [key: string]: string[] }
|
||||
|
||||
type TableRow = [string, string, string, string]
|
||||
const table = new Table({
|
||||
head: [
|
||||
chalk.whiteBright('Name'),
|
||||
chalk.whiteBright('Command'),
|
||||
chalk.whiteBright('Outcome'),
|
||||
chalk.whiteBright('Ran From'),
|
||||
] satisfies TableRow,
|
||||
})
|
||||
|
||||
$.stdio = 'inherit'
|
||||
$.verbose = true
|
||||
$.nothrow = true
|
||||
|
||||
let didErr = false
|
||||
function getAndCheckOutcome({
|
||||
exitCode,
|
||||
skippedCode,
|
||||
}: {
|
||||
exitCode: number | null
|
||||
skippedCode?: number
|
||||
}): string {
|
||||
if (exitCode !== 0 && exitCode !== skippedCode) {
|
||||
didErr = true
|
||||
}
|
||||
return getOutcome({ exitCode, skippedCode })
|
||||
}
|
||||
|
||||
if (deps) {
|
||||
const exitCode = await $({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${fixes.deps}`.exitCode
|
||||
table.push([
|
||||
'deps',
|
||||
fixes.deps.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
'Root',
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (lint) {
|
||||
const exitCode = await $`${fixes.lint}`.exitCode
|
||||
table.push([
|
||||
'lint',
|
||||
fixes.lint.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
runFromRoot ? 'Root' : `cwd (${cwdName})`,
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (workersTypes) {
|
||||
const exitCode = await $({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${fixes.workersTypes}`.exitCode
|
||||
table.push([
|
||||
'workers types',
|
||||
fixes.workersTypes.join(' '),
|
||||
getAndCheckOutcome({ exitCode }),
|
||||
runFromRoot ? 'Root' : `cwd (${cwdName})`,
|
||||
] satisfies TableRow)
|
||||
}
|
||||
|
||||
if (format) {
|
||||
echo(chalk.dim('formatting with prettier (and shfmt if available)...'))
|
||||
|
||||
const [prettierProc, shfmtProc] = await Promise.all([
|
||||
$({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${fixes.format}`,
|
||||
|
||||
$({
|
||||
cwd: repoRoot, // Must be run from root
|
||||
})`${fixes.formatShell}`,
|
||||
])
|
||||
|
||||
table.push(
|
||||
[
|
||||
'format',
|
||||
fixes.format.join(' '),
|
||||
getAndCheckOutcome({ exitCode: prettierProc.exitCode }),
|
||||
'Root',
|
||||
] satisfies TableRow,
|
||||
[
|
||||
'format shell',
|
||||
fixes.formatShell.join(' '),
|
||||
getAndCheckOutcome({
|
||||
exitCode: shfmtProc.exitCode,
|
||||
skippedCode: SHFMT_SKIPPED_EXIT_CODE,
|
||||
}),
|
||||
'Root',
|
||||
] satisfies TableRow
|
||||
)
|
||||
}
|
||||
|
||||
echo(table.toString())
|
||||
|
||||
if (didErr) {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
|
||||
import { SHFMT_SKIPPED_EXIT_CODE } from '../proc'
|
||||
|
||||
export const shfmtCmd = new Command('shfmt').description('Format shell scripts with shfmt')
|
||||
|
||||
shfmtCmd
|
||||
.command('fix')
|
||||
.description('Format shell scripts with shfmt')
|
||||
.option(
|
||||
'--skip-if-unavailable',
|
||||
'Only run if shfmt and ripgrep are available, otherwise skip with a warning.',
|
||||
false
|
||||
)
|
||||
.action(async ({ skipIfUnavailable }) => {
|
||||
const mode: Mode = 'write'
|
||||
|
||||
await checkShfmtAvailable(skipIfUnavailable, mode)
|
||||
|
||||
await runShfmt(mode)
|
||||
})
|
||||
|
||||
shfmtCmd
|
||||
.command('check')
|
||||
.description('Check shell scripts with shfmt')
|
||||
.option(
|
||||
'--skip-if-unavailable',
|
||||
'Only run if shfmt and ripgrep are available, otherwise skip with a warning.',
|
||||
false
|
||||
)
|
||||
.action(async ({ skipIfUnavailable }) => {
|
||||
const mode: Mode = 'diff'
|
||||
|
||||
await checkShfmtAvailable(skipIfUnavailable, mode)
|
||||
|
||||
await runShfmt(mode)
|
||||
})
|
||||
|
||||
type Mode = 'write' | 'diff'
|
||||
|
||||
async function runShfmt(mode: Mode) {
|
||||
await Promise.all([
|
||||
$`rg --files-with-matches '^#!.*\\b(sh|bash|zsh|fish|dash|ksh|csh)\\b' -g '!*.*' .`
|
||||
.pipe($`xargs shfmt --case-indent ${`--${mode}`}`)
|
||||
.pipe(process.stderr),
|
||||
$({
|
||||
nothrow: true, // may not be any .sh files
|
||||
})`rg --files-with-matches '^#!.*\\b(sh|bash|zsh|fish|dash|ksh|csh)\\b' -g '*.sh' .`
|
||||
.pipe($`xargs shfmt --case-indent ${`--${mode}`}`)
|
||||
.pipe(process.stderr),
|
||||
])
|
||||
}
|
||||
|
||||
async function checkShfmtAvailable(skipIfUnavailable: boolean, mode: Mode): Promise<void> {
|
||||
const [shfmtExit, rgExit] = await Promise.all([
|
||||
which('shfmt', { nothrow: true }),
|
||||
which('rg', { nothrow: true }),
|
||||
])
|
||||
|
||||
const missing: string[] = []
|
||||
if (shfmtExit === null) {
|
||||
missing.push('shfmt')
|
||||
}
|
||||
if (rgExit === null) {
|
||||
missing.push('rg (ripgrep)')
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
const missingStr = `${missing.join(' and ')} ${missing.length === 1 ? 'is' : 'are'} unavailable`
|
||||
|
||||
if (skipIfUnavailable) {
|
||||
echo(chalk.yellow(`warning: ${missingStr}, skipping shell formatting`))
|
||||
process.exit(SHFMT_SKIPPED_EXIT_CODE)
|
||||
} else {
|
||||
const action = mode === 'write' ? 'fix' : 'check'
|
||||
echo(chalk.red(`error: ${missingStr}, unable to ${action} shell formatting`))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Command } from '@commander-js/extra-typings'
|
||||
|
||||
import { getRepoRoot } from '../path'
|
||||
import { updatePnpm } from '../update-pnpm'
|
||||
|
||||
export const updateCmd = new Command('update')
|
||||
.description('Update things in the repo')
|
||||
.hook('preAction', () => {
|
||||
cd(getRepoRoot())
|
||||
$.verbose = true
|
||||
$.stdio = 'inherit'
|
||||
$.env.FORCE_COLOR = '1'
|
||||
})
|
||||
|
||||
updateCmd
|
||||
.command('deps')
|
||||
.description('Update dependencies via syncpack')
|
||||
.action(async () => {
|
||||
await $`syncpack update`
|
||||
|
||||
// Run fix if there are any changes
|
||||
const status = await $({
|
||||
stdio: 'pipe',
|
||||
})`git status --porcelain`.text()
|
||||
if (status.includes('package.json') || status.includes('pnpm-lock.yaml')) {
|
||||
await $`just fix --deps`
|
||||
}
|
||||
})
|
||||
|
||||
updateCmd
|
||||
.command('pnpm')
|
||||
.description('Update pnpm version')
|
||||
.action(async () => {
|
||||
await updatePnpm()
|
||||
})
|
||||
|
||||
updateCmd
|
||||
.command('turbo')
|
||||
.description('Update turbo version (must have clean working tree)')
|
||||
.action(async () => {
|
||||
await $`pnpm dlx @turbo/codemod@latest update`
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getRepoRoot } from './path'
|
||||
|
||||
describe('getRepoRoot()', () => {
|
||||
it('should return the root of the repo', () => {
|
||||
expect(getRepoRoot()).toBe(path.resolve(__dirname, '../../..'))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as find from 'empathic/find'
|
||||
import * as pkg from 'empathic/package'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const getRepoRoot = memoizeOne(() => {
|
||||
const pnpmLock = z
|
||||
.string()
|
||||
.trim()
|
||||
.startsWith('/')
|
||||
.endsWith('/pnpm-lock.yaml')
|
||||
.parse(find.up('pnpm-lock.yaml'))
|
||||
return path.dirname(pnpmLock)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the package name of the nearest package.json
|
||||
*/
|
||||
export const getPackageName = memoizeOne(async (): Promise<string> => {
|
||||
const pkgJsonPath = pkg.up()
|
||||
if (!pkgJsonPath) {
|
||||
throw new Error(`unable to locate package.json from ${process.cwd()}`)
|
||||
}
|
||||
const pkgJson = z.object({ name: z.string() }).safeParse(await fs.readJson(pkgJsonPath))
|
||||
if (!pkgJson.success) {
|
||||
throw new Error(`unable to parse package.json: ${pkgJsonPath}`)
|
||||
}
|
||||
return pkgJson.data.name
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Represents a pakcage.json file
|
||||
*/
|
||||
export const PackageJson = z.object({
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
type: z.enum(['module', 'commonjs']).optional(),
|
||||
module: z.string().optional(),
|
||||
main: z.string().optional(),
|
||||
types: z.string().optional(),
|
||||
scripts: z.record(z.string(), z.string()).optional(),
|
||||
dependencies: z.record(z.string(), z.string()).optional(),
|
||||
devDependencies: z.record(z.string(), z.string()).optional(),
|
||||
peerDependencies: z.record(z.string(), z.string()).optional(),
|
||||
optionalDependencies: z.record(z.string(), z.string()).optional(),
|
||||
packageManager: z.string().optional(),
|
||||
engines: z
|
||||
.object({
|
||||
node: z.string().optional(),
|
||||
npm: z.string().optional(),
|
||||
pnpm: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
repository: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
type: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
keywords: z.array(z.string()).optional(),
|
||||
author: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
email: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
license: z.string().optional(),
|
||||
bugs: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
url: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
homepage: z.string().optional(),
|
||||
private: z.boolean().optional(),
|
||||
publishConfig: z.record(z.string(), z.unknown()).optional(),
|
||||
workspaces: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export type PackageJson = z.infer<typeof PackageJson>
|
||||
@@ -0,0 +1,20 @@
|
||||
export function getOutcome({
|
||||
exitCode,
|
||||
skippedCode,
|
||||
}: {
|
||||
exitCode: number | null
|
||||
skippedCode?: number
|
||||
}) {
|
||||
if (exitCode === 0) {
|
||||
return chalk.green('Success!')
|
||||
} else if (exitCode === skippedCode) {
|
||||
return chalk.yellow('Skipped')
|
||||
} else {
|
||||
return chalk.red(`Failed with code: ${exitCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-zero exit code used to indicate "skipped due to shfmt/rg unavailable"
|
||||
*/
|
||||
export const SHFMT_SKIPPED_EXIT_CODE = 113
|
||||
@@ -0,0 +1,4 @@
|
||||
// runx uses zx/globals imported in bin/runx.cmd.ts
|
||||
// This import ensures that tests work without
|
||||
// needing to import this manually.
|
||||
import 'zx/globals'
|
||||
@@ -0,0 +1,66 @@
|
||||
import path from 'node:path'
|
||||
import { inspect } from 'node:util'
|
||||
|
||||
import type {
|
||||
createProgram,
|
||||
parseJsonConfigFileContent,
|
||||
readConfigFile,
|
||||
sys,
|
||||
CompilerOptions as TSCompilerOptions,
|
||||
} from 'typescript'
|
||||
|
||||
export type { TSCompilerOptions }
|
||||
|
||||
interface TSModule {
|
||||
readConfigFile: typeof readConfigFile
|
||||
parseJsonConfigFileContent: typeof parseJsonConfigFileContent
|
||||
sys: typeof sys
|
||||
createProgram: typeof createProgram
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeScript helpers. This is a class so that we can dynamically import the TypeScript module
|
||||
* to reduce runx start time for commands that don't use the typescript package.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const tsHelpers = await new TSHelpers().init()
|
||||
* const { ts } = tsHelpers
|
||||
* const tsConfig = tsHelpers.getTSConfig()
|
||||
* ts.createProgram(entryPoints, tsConfig).emit()
|
||||
* ```
|
||||
*/
|
||||
export class TSHelpers {
|
||||
#ts: TSModule | undefined
|
||||
public get ts(): TSModule {
|
||||
if (!this.#ts) {
|
||||
throw new Error('TSHelpers not initialized. Call init() first.')
|
||||
}
|
||||
return this.#ts
|
||||
}
|
||||
|
||||
async init(): Promise<TSHelpers> {
|
||||
this.#ts = (await import('typescript')) as TSModule
|
||||
return this
|
||||
}
|
||||
|
||||
getTSConfig(configPath = 'tsconfig.json'): TSCompilerOptions {
|
||||
const absolutePath = path.resolve(configPath)
|
||||
const configFile = this.ts.readConfigFile(absolutePath, (p) => this.ts.sys.readFile(p))
|
||||
if (configFile.error) {
|
||||
throw new Error(`Failed to read tsconfig: ${inspect(configFile.error)}`)
|
||||
}
|
||||
|
||||
const parsed = this.ts.parseJsonConfigFileContent(
|
||||
configFile.config,
|
||||
this.ts.sys,
|
||||
path.dirname(absolutePath)
|
||||
)
|
||||
if (parsed.errors.length > 0) {
|
||||
throw new Error(`Failed to parse tsconfig: ${inspect(parsed.errors)}`)
|
||||
}
|
||||
|
||||
return parsed.options
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { cliError } from '@jahands/cli-tools'
|
||||
import Table from 'cli-table3'
|
||||
import * as toml from 'smol-toml'
|
||||
import { match } from 'ts-pattern'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { getRepoRoot } from './path'
|
||||
import { PackageJson } from './pkg'
|
||||
|
||||
type UpdateResult = {
|
||||
type: string
|
||||
updated: number
|
||||
failed: number
|
||||
files: string[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export async function updatePnpm() {
|
||||
const $$ = $({
|
||||
verbose: false,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
const repoRoot = getRepoRoot()
|
||||
cd(repoRoot)
|
||||
const miseAvailable = await which('mise')
|
||||
echo(chalk.white(`Checking for pnpm updates...`))
|
||||
const res = await fetch('https://registry.npmjs.org/pnpm')
|
||||
if (!res.ok) {
|
||||
throw cliError(`Failed to fetch pnpm registry: ${res.status}`)
|
||||
}
|
||||
const body = await res.json()
|
||||
const pnpm = NpmRegistryPnpmResponse.parse(body)
|
||||
const latest = pnpm['dist-tags'].latest
|
||||
echo(chalk.blue(`Latest pnpm version: ${latest}`))
|
||||
|
||||
const results: UpdateResult[] = []
|
||||
|
||||
if (miseAvailable) {
|
||||
const miseResult = await updateMiseTomlFiles(repoRoot, latest)
|
||||
results.push(miseResult)
|
||||
}
|
||||
|
||||
const packageJsonResult = await updatePackageJsonFiles(repoRoot, latest)
|
||||
results.push(packageJsonResult)
|
||||
|
||||
if (results.some((r) => r.updated > 0)) {
|
||||
echo(chalk.blue(`Fixing formatting...`))
|
||||
await $$`runx fix --format`
|
||||
}
|
||||
|
||||
// Display summary table
|
||||
const table = new Table({
|
||||
head: [
|
||||
chalk.whiteBright('File Type'),
|
||||
chalk.whiteBright('Updated'),
|
||||
chalk.whiteBright('Failed'),
|
||||
chalk.whiteBright('Status'),
|
||||
],
|
||||
})
|
||||
|
||||
let totalUpdated = 0
|
||||
let totalFailed = 0
|
||||
for (const result of results) {
|
||||
totalUpdated += result.updated
|
||||
totalFailed += result.failed
|
||||
|
||||
const status = match(result)
|
||||
.when(
|
||||
(r) => r.failed > 0,
|
||||
() => chalk.red('Failed')
|
||||
)
|
||||
.when(
|
||||
(r) => r.updated > 0,
|
||||
() => chalk.green('Updated')
|
||||
)
|
||||
.otherwise(() => chalk.gray('No changes'))
|
||||
|
||||
table.push([result.type, result.updated.toString(), result.failed.toString(), status])
|
||||
}
|
||||
|
||||
// Show detailed errors if any
|
||||
for (const result of results) {
|
||||
if (result.errors.length > 0) {
|
||||
echo(chalk.red(`\nErrors in ${result.type}:`))
|
||||
for (const error of result.errors) {
|
||||
echo(chalk.red(` ${error}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalUpdated > 0) {
|
||||
if (miseAvailable) {
|
||||
echo(chalk.blue('\nRunning mise up...'))
|
||||
await $$`mise up`
|
||||
}
|
||||
|
||||
echo(chalk.blue(`Fixing formatting...`))
|
||||
await $$`runx fix --format`
|
||||
|
||||
echo(chalk.greenBright(`\nSuccessfully updated pnpm to ${latest}`))
|
||||
} else if (totalFailed > 0) {
|
||||
echo(chalk.red(`\nFailed to update some files. See errors above.`))
|
||||
process.exit(1)
|
||||
} else {
|
||||
echo(chalk.green(`\nNo pnpm updates needed, already on ${latest}`))
|
||||
}
|
||||
|
||||
echo('\n' + table.toString())
|
||||
}
|
||||
|
||||
const Semver = z.string().regex(/^\d+\.\d+\.\d+$/)
|
||||
const NpmRegistryPnpmResponse = z.object({
|
||||
_id: z.literal('pnpm'),
|
||||
name: z.literal('pnpm'),
|
||||
'dist-tags': z.object({
|
||||
latest: Semver.describe('pnpm latest version, e.g. 9.5.0'),
|
||||
}),
|
||||
})
|
||||
|
||||
type MiseToml = z.infer<typeof MiseToml>
|
||||
const MiseToml = z
|
||||
.object({
|
||||
tools: z
|
||||
.object({
|
||||
pnpm: Semver.describe('pnpm version, e.g. 9.5.0').optional(),
|
||||
})
|
||||
.catchall(z.string()),
|
||||
alias: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.loose()
|
||||
|
||||
/**
|
||||
* Update package.json files with the new pnpm version
|
||||
* @param repoRoot - The root directory of the repository
|
||||
* @param newVersion - The new pnpm version
|
||||
* @returns UpdateResult with details about the operation
|
||||
*/
|
||||
async function updatePackageJsonFiles(repoRoot: string, newVersion: string): Promise<UpdateResult> {
|
||||
const result: UpdateResult = {
|
||||
type: 'package.json',
|
||||
updated: 0,
|
||||
failed: 0,
|
||||
files: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
const packageJsonFiles = await glob('**/package.json', {
|
||||
cwd: repoRoot,
|
||||
gitignore: true,
|
||||
ignore: ['**/node_modules/**', 'turbo/generators/**'],
|
||||
})
|
||||
|
||||
for (const file of packageJsonFiles) {
|
||||
const filePath = `${repoRoot}/${file}`
|
||||
|
||||
try {
|
||||
const packageJson = PackageJson.loose().parse(await Bun.file(filePath).json())
|
||||
|
||||
if (packageJson.packageManager?.startsWith('pnpm@')) {
|
||||
const currentVersion = packageJson.packageManager.replace('pnpm@', '')
|
||||
if (currentVersion !== newVersion) {
|
||||
echo(chalk.blue(`Updating ${file} packageManager to pnpm@${newVersion}`))
|
||||
packageJson.packageManager = `pnpm@${newVersion}`
|
||||
await Bun.file(filePath).write(JSON.stringify(packageJson, null, 2) + '\n')
|
||||
result.updated++
|
||||
result.files.push(file)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
result.failed++
|
||||
result.errors.push(`Failed to update ${file}: ${e instanceof Error ? e.message : String(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mise.toml and .mise.toml files with the new pnpm version
|
||||
* @param repoRoot - The root directory of the repository
|
||||
* @param newVersion - The new pnpm version
|
||||
* @returns UpdateResult with details about the operation
|
||||
*/
|
||||
async function updateMiseTomlFiles(repoRoot: string, newVersion: string): Promise<UpdateResult> {
|
||||
const result: UpdateResult = {
|
||||
type: 'mise.toml',
|
||||
updated: 0,
|
||||
failed: 0,
|
||||
files: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
const miseTomlFiles = await glob('**/{.mise.toml,mise.toml}', {
|
||||
cwd: repoRoot,
|
||||
gitignore: true,
|
||||
ignore: ['**/node_modules/**'],
|
||||
})
|
||||
|
||||
for (const file of miseTomlFiles) {
|
||||
const filePath = `${repoRoot}/${file}`
|
||||
|
||||
try {
|
||||
const miseToml = MiseToml.parse(toml.parse(await Bun.file(filePath).text()))
|
||||
|
||||
if (miseToml.tools.pnpm && miseToml.tools.pnpm !== newVersion) {
|
||||
echo(chalk.blue(`Updating ${file} to pnpm@${newVersion}`))
|
||||
miseToml.tools.pnpm = newVersion
|
||||
const miseString = toml.stringify(miseToml) + '\n'
|
||||
await Bun.file(filePath).write(miseString.replaceAll('"', "'"))
|
||||
result.updated++
|
||||
result.files.push(file)
|
||||
}
|
||||
} catch (e) {
|
||||
result.failed++
|
||||
result.errors.push(`Failed to update ${file}: ${e instanceof Error ? e.message : String(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/tools.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@types/bun"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
setupFiles: [`${__dirname}/src/test/setup.ts`],
|
||||
environment: 'node',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user