mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# hono-helpers
|
||||
|
||||
A package with shared helpers for Hono applications
|
||||
@@ -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])
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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'>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/workers-lib.json"
|
||||
}
|
||||
@@ -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())
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/lib.json",
|
||||
"include": ["*.ts", "src/**/*.ts"],
|
||||
"exclude": ["node_modules/"]
|
||||
}
|
||||
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',
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "es2022",
|
||||
"target": "es2022"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"sideEffects": false
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user