Initial commit

This commit is contained in:
Devin Zuczek
2026-06-08 18:23:40 -04:00
commit 8e644a5c20
141 changed files with 21414 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# hono-helpers
A package with shared helpers for Hono applications
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@repo/hono-helpers",
"version": "0.1.4",
"private": true,
"sideEffects": false,
"type": "module",
"main": "src/index.ts",
"scripts": {
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"test": "run-vitest"
},
"dependencies": {
"@hono/standard-validator": "0.2.2",
"hono": "4.12.9",
"http-codex": "0.6.6",
"workers-tagged-logger": "1.0.0",
"zod": "4.3.6"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.13.3",
"@cloudflare/workers-types": "4.20260317.1",
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"vitest": "4.1.0"
}
}
@@ -0,0 +1,18 @@
import { HTTPException } from 'hono/http-exception'
import type { ContentfulStatusCode } from 'hono/utils/http-status'
/** Generates a new HTTPException with the given status and message as a JSON response.
*
* **Example:** `throw newHTTPException(401, 'unauthorized')`
*/
export function newHTTPException(status: ContentfulStatusCode, message: string): HTTPException {
return new HTTPException(status, { message })
}
export interface APIError {
success: false
error: {
message: string
}
}
@@ -0,0 +1,9 @@
import { WorkersLogger } from 'workers-tagged-logger'
export type LogTagHints = {
// add common tags here so that they show up as hints
// in `logger.setTags()` and `logger.withTags()`
url: string
}
export const logger = new WorkersLogger<LogTagHints>()
@@ -0,0 +1,72 @@
import { redactUrl } from './url'
import type { Context } from 'hono'
import type { HonoApp } from '../types'
export interface LogDataRequest {
url: string
method: string
path: string
/** Hono route for the request */
routePath: string
/* URL search params */
searchParams: string
headers: string
/** Eyeball IP address of the request */
ip?: string
timestamp: string
}
/**
* Get logdata from request
*/
export function getRequestLogData<T extends HonoApp>(
c: Context<T>,
requestStartTimestamp: number
): LogDataRequest {
const redactedUrl = redactUrl(c.req.url)
return {
url: redactedUrl.toString(),
method: c.req.method,
path: c.req.path,
routePath: c.req.routePath,
searchParams: redactedUrl.searchParams.toString(),
headers: stringifyHeaders(c.req.raw.headers),
ip:
c.req.header('cf-connecting-ip') ||
c.req.header('x-real-ip') ||
c.req.header('x-forwarded-for'),
timestamp: new Date(requestStartTimestamp).toISOString(),
}
}
const SENSITIVE_HEADER_NAMES = new Set([
'authorization',
'proxy-authorization',
'cookie',
'set-cookie',
'cf-access-jwt-assertion',
])
function isSensitiveHeader(name: string): boolean {
const normalizedName = name.toLowerCase()
return (
SENSITIVE_HEADER_NAMES.has(normalizedName) ||
normalizedName.includes('token') ||
normalizedName.includes('secret') ||
normalizedName.includes('jwt') ||
normalizedName.includes('signature') ||
normalizedName.includes('session') ||
normalizedName.endsWith('-key') ||
normalizedName.endsWith('_key')
)
}
function stringifyHeaders(headers: Headers): string {
return JSON.stringify(
Object.fromEntries(
Array.from(headers, ([name, value]) => [name, isSensitiveHeader(name) ? 'REDACTED' : value])
)
)
}
+29
View File
@@ -0,0 +1,29 @@
/** Redacts keys from a url */
export function redactUrl(_url: URL | string): URL {
let url: URL
if (typeof _url === 'string') {
url = new URL(_url)
} else {
url = new URL(_url.toString()) // clone
}
for (const [key] of url.searchParams) {
if (/key|token|secret|password|passwd|auth|credential/i.test(key)) {
url.searchParams.set(key, 'REDACTED')
}
}
return url
}
/**
* Converts a URLSearchParams object into an array of "key=value" strings.
*
* @param searchParams - The URLSearchParams object to convert.
* @returns An array of strings, where each string is in the format "key=value".
*/
export function searchParamsToArray(searchParams: URLSearchParams): string[] {
const result: string[] = []
for (const [key, value] of searchParams.entries()) {
result.push(`${key}=${value}`)
}
return result
}
+9
View File
@@ -0,0 +1,9 @@
export type { HonoApp, SharedHonoEnv, SharedHonoVariables, SharedAppContext } from './types'
export { logger } from './helpers/logger'
export { getRequestLogData, type LogDataRequest } from './helpers/request'
export * from './helpers/errors'
export * from './helpers/url'
export * from './middleware/withCache'
export * from './middleware/withDefaultCors'
export * from './middleware/withNotFound'
export * from './middleware/withOnError'
@@ -0,0 +1,78 @@
import { httpStatus } from 'http-codex/status'
import type { Context, Next } from 'hono'
import type { StatusCode } from 'hono/utils/http-status'
import type { HonoApp } from '../types'
/** Caches status: 200 responses for given ttl */
export function withCache<T extends HonoApp>(ttl: number) {
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
const c = ctx as unknown as Context<HonoApp>
const cache = await caches.open('default')
const reqMatcher = new Request(c.req.url, { method: c.req.method })
const cachedRes = await cache.match(reqMatcher)
if (cachedRes) {
return c.newResponse(cachedRes.body, cachedRes)
}
await next()
if (c.res.status === httpStatus.OK) {
const clonedRes = c.res.clone()
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${ttl}`)
c.executionCtx.waitUntil(cache.put(reqMatcher, clonedRes))
}
}
}
/** Caches using default CF Cache behavior */
export function withCacheDefault<T extends HonoApp>(ttl: number) {
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
const c = ctx as unknown as Context<HonoApp>
const cache = await caches.open('default')
const cachedRes = await cache.match(c.req.raw)
if (cachedRes) {
return c.newResponse(cachedRes.body, cachedRes)
}
await next()
if (c.res.status === httpStatus.OK) {
const clonedRes = c.res.clone()
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${ttl}`)
c.executionCtx.waitUntil(cache.put(c.req.raw, clonedRes))
}
}
}
interface CacheByStatus {
status: StatusCode
/** Time in milliseconds to cache this status */
ttl: number
}
interface WithCacheByStatusOptions {
rules: CacheByStatus[]
/** Force caching rather than using default CF cache behavior */
force: boolean
}
/** Caches responses based on status */
export function withCacheByStatus<T extends HonoApp>(options: WithCacheByStatusOptions) {
return async (ctx: Context<T>, next: Next): Promise<Response | void> => {
const c = ctx as unknown as Context<HonoApp>
const cache = await caches.open('default')
const reqMatcher = options.force ? new Request(c.req.url, { method: c.req.method }) : c.req.raw
const cachedRes = await cache.match(reqMatcher)
if (cachedRes) {
return c.newResponse(cachedRes.body, cachedRes)
}
await next()
const opts = options.rules.find((o) => o.status === c.res.status)
if (opts) {
const clonedRes = c.res.clone()
clonedRes.headers.set('Cloudflare-CDN-Cache-Control', `max-age=${opts.ttl}`)
c.executionCtx.waitUntil(cache.put(reqMatcher, clonedRes))
}
}
}
@@ -0,0 +1,10 @@
import { cors } from 'hono/cors'
/** Default CORS handler */
export function withDefaultCors() {
return cors({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],
allowHeaders: ['Content-Type', 'Authorization'],
})
}
@@ -0,0 +1,19 @@
import { httpStatus } from 'http-codex/status'
import type { Context } from 'hono'
import type { APIError } from '../helpers/errors'
import type { HonoApp } from '../types'
/** Handles typical notFound hooks */
export function withNotFound<T extends HonoApp>() {
return async (ctx: Context<T>): Promise<Response> => {
const c = ctx as unknown as Context<HonoApp>
return c.json(notFoundResponse, httpStatus.NotFound)
}
}
export const notFoundResponse = {
success: false,
error: { message: 'not found' },
} satisfies APIError
@@ -0,0 +1,40 @@
import { HTTPException } from 'hono/http-exception'
import { httpStatus } from 'http-codex/status'
import { logger } from '../helpers/logger'
import type { Context } from 'hono'
import type { ContentfulStatusCode } from 'hono/utils/http-status'
import type { APIError } from '../helpers/errors'
import type { HonoApp } from '../types'
/** Handles typical onError hooks */
export function withOnError<T extends HonoApp>() {
return async (err: Error, ctx: Context<T>): Promise<Response> => {
const c = ctx as unknown as Context<HonoApp>
if (err instanceof HTTPException) {
const status = err.getResponse().status as ContentfulStatusCode
const body: APIError = { success: false, error: { message: err.message } }
if (status >= 500) {
// TODO: Capture to Sentry
// Log to Sentry
logger.error(err)
} else if (status === httpStatus.Unauthorized) {
body.error.message = 'unauthorized'
}
return c.json(body, status)
}
// TODO: Capture to Sentry
logger.error(err)
return c.json(
{
success: false,
error: { message: 'internal server error' },
} satisfies APIError,
500
)
}
}
+40
View File
@@ -0,0 +1,40 @@
import { z } from 'zod'
export type WorkersEnvironment = z.infer<typeof WorkersEnvironment>
export const WorkersEnvironment = z.enum(['VITEST', 'development', 'staging', 'production'])
/** Global bindings */
export type SharedHonoEnv = {
/**
* Name of the worker used in logging/etc.
* Automatically pulled from package.json
*/
NAME: string
/**
* Environment of the worker.
* All workers should specify env in wrangler.jsonc vars
*/
ENVIRONMENT: WorkersEnvironment
/**
* Release version of the Worker (based on the current git commit).
* Useful for logs, Sentry, etc.
*/
SENTRY_RELEASE: string
}
/** Global Hono variables */
export type SharedHonoVariables = {
// Things like Sentry, etc. that should be present on all Workers
}
/** Top-level Hono app */
export interface HonoApp {
Variables: SharedHonoVariables
Bindings: SharedHonoEnv
}
/** Context used for non-Hono things like Durable Objects */
export type SharedAppContext = {
var: SharedHonoVariables
env: SharedHonoEnv
executionCtx: Pick<ExecutionContext, 'waitUntil'>
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/workers-lib.json"
}
+44
View File
@@ -0,0 +1,44 @@
# `@repo/oxlint-config`
Collection of internal oxlint configurations.
## Adding additional configs
You may want to add additional configs with overrides for different frameworks.
For example, you can add a TanStack Start config by adding `src/tanstack.config.ts`:
```ts
// src/tanstack.config.ts
import { getConfig } from '@repo/oxlint-config'
import type { OxlintConfig } from 'oxlint'
const config = getConfig()
export function getTanstackConfig() {
return {
...config,
ignorePatterns: [...config.ignorePatterns, 'src/routeTree.gen.ts'],
} as const satisfies OxlintConfig
}
```
And then updating [package.json](./package.json) exports:
```json
"exports": {
".": "./src/default.config.ts",
"./tanstack": "./src/tanstack.config.ts"
},
```
Finally, import it to your TanStack Start app:
```ts
// apps/my-tanstack-project/oxlint.config.ts
import { defineConfig } from '@repo/oxlint-config'
import { getTanstackConfig } from '@repo/oxlint-config/tanstack'
export default defineConfig(getTanstackConfig())
```
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@repo/oxlint-config",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"exports": {
".": "./src/default.config.ts"
},
"dependencies": {
"oxlint": "1.56.0",
"oxlint-tsgolint": "0.17.1"
},
"devDependencies": {
"@types/node": "25.5.0",
"vitest": "4.1.0"
}
}
@@ -0,0 +1,75 @@
import { defineConfig } from 'oxlint'
import type { OxlintConfig } from 'oxlint'
export { defineConfig }
export function getConfig() {
return {
plugins: ['typescript', 'import', 'unicorn'],
env: {
builtin: true,
es2018: true,
},
ignorePatterns: [
'.astro/**',
'.next/**',
'.turbo/**',
'.vercel/**',
'.wrangler/**',
'dist/**',
'node_modules/**',
'out/**',
'worker-configuration.d.ts',
],
rules: {
'@typescript-eslint/no-floating-promises': 'warn',
'import/no-named-as-default': 'warn',
'import/no-named-as-default-member': 'warn',
'import/no-duplicates': 'warn',
'no-var': 'error',
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'eslint/prefer-const': 'warn',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'typescript/await-thenable': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/array-type': [
'warn',
{
default: 'array-simple',
},
],
'no-empty': 'warn',
},
overrides: [
{
files: ['**/dagger/*.ts', '**/dagger/**/*.ts'],
rules: {
'no-unused-vars': 'off',
},
},
{
files: ['tailwind.config.ts', 'postcss.config.mjs'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
],
} as const satisfies OxlintConfig
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@repo/typescript-config/lib.json",
"include": ["*.ts", "src/**/*.ts"],
"exclude": ["node_modules/"]
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
branch=$(git rev-parse --abbrev-ref HEAD)
echo "$branch"
+14
View File
@@ -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"
+20
View File
@@ -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"
+12
View File
@@ -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
+21
View File
@@ -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[@]}"
+6
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec vite build
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec vite dev
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec vite preview
+9
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec vitest run --silent --testTimeout=15000
+8
View File
@@ -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 "$@"
+14
View File
@@ -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 \
"$@"
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -eu
NAME=$(jq -r '.name' package.json)
exec wrangler dev \
--var NAME:"$NAME" \
"$@"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec wrangler types --include-env=false "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
set -eu
script_path="$(realpath "$(dirname "$0")/../src/bin/runx.cmd.ts")"
exec bun "$script_path" "$@"
+41
View File
@@ -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"
}
}
+32
View File
@@ -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())
+215
View File
@@ -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 })
}
})
+163
View File
@@ -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)
}
})
+22
View File
@@ -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`
})
+39
View File
@@ -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}`
}
})
+144
View File
@@ -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)
}
})
+80
View File
@@ -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)
}
}
}
+42
View File
@@ -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`
})
+9
View File
@@ -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, '../../..'))
})
})
+29
View File
@@ -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
})
+63
View File
@@ -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>
+20
View File
@@ -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
+4
View File
@@ -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'
+66
View File
@@ -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
}
}
+221
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "@repo/typescript-config/tools.json",
"compilerOptions": {
"types": ["@types/bun"]
}
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: [`${__dirname}/src/test/setup.ts`],
environment: 'node',
},
})
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"include": ["${configDir}/**/*.ts", "${configDir}/**/*.tsx"],
"exclude": ["${configDir}/node_modules/", "${configDir}/dist/", "${configDir}/oxlint.config.ts"],
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "bundler",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"noImplicitOverride": true,
"strict": true,
"noEmit": true,
"resolveJsonModule": true
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"module": "es2022",
"target": "es2022"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@repo/typescript-config",
"version": "0.1.0",
"private": true,
"sideEffects": false
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Tools",
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"noFallthroughCasesInSwitch": true
}
}
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/typescript-config/workers.json",
"include": ["${configDir}/**/*.ts", "${configDir}/**/*.tsx"],
"exclude": ["${configDir}/node_modules/", "${configDir}/dist/", "${configDir}/oxlint.config.ts"],
"compilerOptions": {
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers/types"]
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": [
"${configDir}/worker-configuration.d.ts",
"${configDir}/env.d.ts",
"${configDir}/**/*.ts",
"${configDir}/**/*.tsx"
],
"exclude": [
"${configDir}/node_modules/",
"${configDir}/dist/",
"${configDir}/**/opensrc/",
"${configDir}/oxlint.config.ts"
],
"compilerOptions": {
"target": "es2022",
"lib": ["ESNext"],
"jsx": "react",
"module": "es2022",
"moduleResolution": "bundler",
"types": ["./worker-configuration.d.ts", "@cloudflare/vitest-pool-workers/types"],
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"skipLibCheck": true
}
}
@@ -0,0 +1,5 @@
# @repo/workspace-dependencies
This package is for dependencies used at the root. This helps isolate these dependencies so that we aren't unintentionally using them within packages.
Other packages should never import from this package
@@ -0,0 +1,48 @@
{
"name": "@repo/workspace-dependencies",
"version": "0.1.3",
"private": true,
"sideEffects": false,
"type": "module",
"exports": {
"./zx": {
"import": "./src/zx.ts",
"require": {
"require": "./dist/zx.cjs",
"types": "./dist/zx.d.ts"
}
},
"./yaml": {
"import": "./src/yaml.ts",
"require": {
"require": "./dist/yaml.cjs",
"types": "./dist/yaml.d.ts"
}
},
"./zod": {
"import": "./src/zod.ts",
"require": {
"require": "./dist/zod.cjs",
"types": "./dist/zod.d.ts"
}
}
},
"scripts": {
"build": "runx build lib yaml.ts zod.ts zx.ts -d src -f cjs --platform node",
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"test": "run-vitest"
},
"dependencies": {
"esbuild": "0.27.4",
"slugify": "1.6.8",
"wrangler": "4.76.0",
"yaml": "2.8.3",
"zod": "4.3.6",
"zx": "8.8.5"
},
"devDependencies": {
"@repo/tools": "workspace:*",
"vitest": "4.1.0"
}
}
@@ -0,0 +1 @@
export { default as YAML } from 'yaml'
@@ -0,0 +1 @@
export { z } from 'zod'
@@ -0,0 +1 @@
export * from 'zx'
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/lib.json"
}