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
+29
View File
@@ -0,0 +1,29 @@
import { z } from 'zod'
export type Paths = z.infer<typeof Paths>
export const Paths = z.object({
cwd: z.string(),
root: z.string(),
workspace: z.string(),
})
export type Turbo = z.infer<typeof Turbo>
export const Turbo = z.object({
paths: Paths,
})
export type NewWorkerAnswers = z.infer<typeof NewWorkerAnswers>
export const NewWorkerAnswers = z.object({
name: z.string(),
turbo: Turbo,
})
export type NewPackageAnswers = z.infer<typeof NewPackageAnswers>
export const NewPackageAnswers = z.object({
name: z.string(),
turbo: Turbo,
usedInWorkers: z.boolean().optional(),
})
export type Answers = z.infer<typeof Answers>
export const Answers = z.union([NewWorkerAnswers, NewPackageAnswers])
+166
View File
@@ -0,0 +1,166 @@
import { NewPackageAnswers, NewWorkerAnswers } from './answers'
import {
pascalText,
pascalTextPlural,
pascalTextSingular,
slugifyText,
slugifyTextPlural,
slugifyTextSingular,
} from './helpers/slugify'
import { nameValidator } from './helpers/validate'
import { fixAll } from './plugins/fix-all'
import { fixDepsAndFormat } from './plugins/fix-deps-and-format'
import { pnpmInstall } from './plugins/pnpm-install'
import type { PlopTypes } from '@turbo/gen'
import type { PnpmInstallData } from './plugins/pnpm-install'
export default function generator(plop: PlopTypes.NodePlopAPI): void {
plop.setActionType('pnpmInstall', pnpmInstall as PlopTypes.CustomActionFunction)
plop.setActionType('fixAll', fixAll as PlopTypes.CustomActionFunction)
plop.setActionType('fixDepsAndFormat', fixDepsAndFormat as PlopTypes.CustomActionFunction)
plop.setHelper('slug', slugifyText)
plop.setHelper('slug-s', slugifyTextSingular)
plop.setHelper('slug-p', slugifyTextPlural)
plop.setHelper('pascal', pascalText)
plop.setHelper('pascal-s', pascalTextSingular)
plop.setHelper('pascal-p', pascalTextPlural)
plop.setGenerator('new-worker', {
description: 'Create a new Cloudflare Worker using Hono',
prompts: [
{
type: 'input',
name: 'name',
message: 'name of worker',
validate: nameValidator,
},
],
actions: (data: unknown) => {
const answers = NewWorkerAnswers.parse(data)
process.chdir(answers.turbo.paths.root)
const destination = `apps/${slugifyText(answers.name)}`
const actions: PlopTypes.Actions = [
{
type: 'addMany',
base: 'templates/fetch-worker',
destination,
templateFiles: ['templates/fetch-worker/**/**.hbs'],
data: answers,
},
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
{ type: 'fixAll' },
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
]
return actions
},
})
plop.setGenerator('new-worker-vite', {
description: 'Create a new Cloudflare Worker using Hono and Vite',
prompts: [
{
type: 'input',
name: 'name',
message: 'name of worker',
validate: nameValidator,
},
],
actions: (data: unknown) => {
const answers = NewWorkerAnswers.parse(data)
process.chdir(answers.turbo.paths.root)
const destination = `apps/${slugifyText(answers.name)}`
const actions: PlopTypes.Actions = [
{
type: 'addMany',
base: 'templates/fetch-worker-vite',
destination,
templateFiles: ['templates/fetch-worker-vite/**/**.hbs'],
data: answers,
},
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
{ type: 'fixAll' },
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
]
return actions
},
})
plop.setGenerator('new-worker-minimal', {
description: 'Create a new Cloudflare Worker with a minimal fetch handler',
prompts: [
{
type: 'input',
name: 'name',
message: 'name of worker',
validate: nameValidator,
},
],
actions: (data: unknown) => {
const answers = NewWorkerAnswers.parse(data)
process.chdir(answers.turbo.paths.root)
const destination = `apps/${slugifyText(answers.name)}`
const actions: PlopTypes.Actions = [
{
type: 'addMany',
base: 'templates/fetch-worker-minimal',
destination,
templateFiles: ['templates/fetch-worker-minimal/**/**.hbs'],
data: answers,
},
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
{ type: 'fixAll' },
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
]
return actions
},
})
plop.setGenerator('new-package', {
description: 'Create a new shared package',
prompts: [
{
type: 'input',
name: 'name',
message: 'name of package',
validate: nameValidator,
},
{
type: 'confirm',
name: 'usedInWorkers',
message: 'Will this package be used within Cloudflare Workers?',
default: true,
},
],
actions: (data: unknown) => {
const answers = NewPackageAnswers.parse(data)
process.chdir(answers.turbo.paths.root)
const destination = `packages/${slugifyText(answers.name)}`
const actions: PlopTypes.Actions = [
{
type: 'addMany',
base: 'templates/package',
destination,
templateFiles: ['templates/package/**/**.hbs'],
data: {
...answers,
tsconfigType: answers.usedInWorkers ? 'workers-lib.json' : 'lib.json',
},
},
{ type: 'fixDepsAndFormat' },
{ type: 'pnpmInstall', data: { ...answers, destination } satisfies PnpmInstallData },
]
return actions
},
})
}
+19
View File
@@ -0,0 +1,19 @@
import type { ProcessOutput } from 'zx'
export function onProcSuccess(
name: string,
resolve: (value: unknown) => void,
reject: (reason?: unknown) => void
) {
return (proc: ProcessOutput) => {
if (proc.exitCode === 0) {
resolve(`${name} ran correctly`)
} else {
reject(`${name} exited with ${proc.exitCode}`)
}
}
}
export function catchError(reject: (reason?: unknown) => void) {
return () => reject('unknown error')
}
+328
View File
@@ -0,0 +1,328 @@
import slugify from 'slugify'
export function slugifyText(text: string) {
const slug = slugify(text, {
lower: true,
remove: /['.]/g,
})
return slug
}
export function slugifyTextSingular(text: string) {
const slug = slugify(text, {
lower: true,
remove: /['.]/g,
})
if (slug.endsWith('s')) {
return slug.slice(0, slug.length - 1)
}
return slug
}
export function slugifyTextPlural(text: string) {
const slug = slugify(text, {
lower: true,
remove: /['.]/g,
})
if (slug.endsWith('s')) {
return slug
}
return `${slug}s`
}
export function pascalText(text: string) {
return pascalCase(text)
}
export function pascalTextSingular(text: string) {
const slug = pascalCase(text)
if (slug.endsWith('s')) {
return slug.slice(0, slug.length - 1)
}
return slug
}
export function pascalTextPlural(text: string) {
const slug = pascalCase(text)
if (slug.endsWith('s')) {
return slug
}
return `${slug}s`
}
// =========================== //
// ========= VENDOR ========== //
// =========================== //
// https://github.com/blakeembrey/change-case/blob/main/packages/change-case/src/index.ts
// Regexps involved with splitting words in various case formats.
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu
// Used to iterate over the initial split result and separate numbers.
const SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u
// Regexp involved with stripping non-word characters from the result.
const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu
// The replacement value for splits.
const SPLIT_REPLACE_VALUE = '$1\0$2'
// The default characters to keep after transforming case.
const DEFAULT_PREFIX_SUFFIX_CHARACTERS = ''
/**
* Supported locale values. Use `false` to ignore locale.
* Defaults to `undefined`, which uses the host environment.
*/
export type Locale = string[] | string | false | undefined
/**
* Options used for converting strings to pascal/camel case.
*/
export interface PascalCaseOptions extends Options {
mergeAmbiguousCharacters?: boolean
}
/**
* Options used for converting strings to any case.
*/
export interface Options {
locale?: Locale
split?: (value: string) => string[]
/** @deprecated Pass `split: splitSeparateNumbers` instead. */
separateNumbers?: boolean
delimiter?: string
prefixCharacters?: string
suffixCharacters?: string
}
/**
* Split any cased input strings into an array of words.
*/
export function split(value: string) {
let result = value.trim()
result = result
.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE)
.replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE)
result = result.replace(DEFAULT_STRIP_REGEXP, '\0')
let start = 0
let end = result.length
// Trim the delimiter from around the output string.
while (result.charAt(start) === '\0') start++
if (start === end) return []
while (result.charAt(end - 1) === '\0') end--
// oxlint-disable-next-line no-control-regex -- intentional null character split
return result.slice(start, end).split(/\0/g)
}
/**
* Split the input string into an array of words, separating numbers.
*/
export function splitSeparateNumbers(value: string) {
const words = split(value)
for (let i = 0; i < words.length; i++) {
const word = words[i]
const match = SPLIT_SEPARATE_NUMBER_RE.exec(word)
if (match) {
const offset = match.index + (match[1] ?? match[2]).length
words.splice(i, 1, word.slice(0, offset), word.slice(offset))
}
}
return words
}
/**
* Convert a string to space separated lower case (`foo bar`).
*/
export function noCase(input: string, options?: Options) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
return prefix + words.map(lowerFactory(options?.locale)).join(options?.delimiter ?? ' ') + suffix
}
/**
* Convert a string to camel case (`fooBar`).
*/
export function camelCase(input: string, options?: PascalCaseOptions) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
const lower = lowerFactory(options?.locale)
const upper = upperFactory(options?.locale)
const transform = options?.mergeAmbiguousCharacters
? capitalCaseTransformFactory(lower, upper)
: pascalCaseTransformFactory(lower, upper)
return (
prefix +
words
.map((word, index) => {
if (index === 0) return lower(word)
return transform(word, index)
})
.join(options?.delimiter ?? '') +
suffix
)
}
/**
* Convert a string to pascal case (`FooBar`).
*/
export function pascalCase(input: string, options?: PascalCaseOptions) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
const lower = lowerFactory(options?.locale)
const upper = upperFactory(options?.locale)
const transform = options?.mergeAmbiguousCharacters
? capitalCaseTransformFactory(lower, upper)
: pascalCaseTransformFactory(lower, upper)
return prefix + words.map(transform).join(options?.delimiter ?? '') + suffix
}
/**
* Convert a string to pascal snake case (`Foo_Bar`).
*/
export function pascalSnakeCase(input: string, options?: Options) {
return capitalCase(input, { delimiter: '_', ...options })
}
/**
* Convert a string to capital case (`Foo Bar`).
*/
export function capitalCase(input: string, options?: Options) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
const lower = lowerFactory(options?.locale)
const upper = upperFactory(options?.locale)
return (
prefix +
words.map(capitalCaseTransformFactory(lower, upper)).join(options?.delimiter ?? ' ') +
suffix
)
}
/**
* Convert a string to constant case (`FOO_BAR`).
*/
export function constantCase(input: string, options?: Options) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
return prefix + words.map(upperFactory(options?.locale)).join(options?.delimiter ?? '_') + suffix
}
/**
* Convert a string to dot case (`foo.bar`).
*/
export function dotCase(input: string, options?: Options) {
return noCase(input, { delimiter: '.', ...options })
}
/**
* Convert a string to kebab case (`foo-bar`).
*/
export function kebabCase(input: string, options?: Options) {
return noCase(input, { delimiter: '-', ...options })
}
/**
* Convert a string to path case (`foo/bar`).
*/
export function pathCase(input: string, options?: Options) {
return noCase(input, { delimiter: '/', ...options })
}
/**
* Convert a string to path case (`Foo bar`).
*/
export function sentenceCase(input: string, options?: Options) {
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
const lower = lowerFactory(options?.locale)
const upper = upperFactory(options?.locale)
const transform = capitalCaseTransformFactory(lower, upper)
return (
prefix +
words
.map((word, index) => {
if (index === 0) return transform(word)
return lower(word)
})
.join(options?.delimiter ?? ' ') +
suffix
)
}
/**
* Convert a string to snake case (`foo_bar`).
*/
export function snakeCase(input: string, options?: Options) {
return noCase(input, { delimiter: '_', ...options })
}
/**
* Convert a string to header case (`Foo-Bar`).
*/
export function trainCase(input: string, options?: Options) {
return capitalCase(input, { delimiter: '-', ...options })
}
function lowerFactory(locale: Locale): (input: string) => string {
return locale === false
? (input: string) => input.toLowerCase()
: (input: string) => input.toLocaleLowerCase(locale)
}
function upperFactory(locale: Locale): (input: string) => string {
return locale === false
? (input: string) => input.toUpperCase()
: (input: string) => input.toLocaleUpperCase(locale)
}
function capitalCaseTransformFactory(
lower: (input: string) => string,
upper: (input: string) => string
) {
return (word: string) => `${upper(word[0])}${lower(word.slice(1))}`
}
function pascalCaseTransformFactory(
lower: (input: string) => string,
upper: (input: string) => string
) {
return (word: string, index: number) => {
const char0 = word[0]
const initial = index > 0 && char0 >= '0' && char0 <= '9' ? '_' + char0 : upper(char0)
return initial + lower(word.slice(1))
}
}
function splitPrefixSuffix(input: string, options: Options = {}): [string, string[], string] {
const splitFn = options.split ?? (options.separateNumbers ? splitSeparateNumbers : split)
const prefixCharacters = options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS
const suffixCharacters = options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS
let prefixIndex = 0
let suffixIndex = input.length
while (prefixIndex < input.length) {
const char = input.charAt(prefixIndex)
if (!prefixCharacters.includes(char)) break
prefixIndex++
}
while (suffixIndex > prefixIndex) {
const index = suffixIndex - 1
const char = input.charAt(index)
if (!suffixCharacters.includes(char)) break
suffixIndex = index
}
return [
input.slice(0, prefixIndex),
splitFn(input.slice(prefixIndex, suffixIndex)),
input.slice(suffixIndex),
]
}
+6
View File
@@ -0,0 +1,6 @@
export function nameValidator(value: string) {
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
return 'Must start with a letter and contain only lowercase letters, numbers, and hyphens'
}
return true
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@repo/turbo-generators",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"test": "run-vitest"
},
"dependencies": {
"@turbo/gen": "2.8.20",
"slugify": "1.6.8",
"zod": "4.3.6",
"zx": "8.8.5"
},
"devDependencies": {
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"vitest": "4.1.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
import { $ } from 'zx'
import { catchError, onProcSuccess } from '../helpers/proc'
import { slugifyText } from '../helpers/slugify'
import type { PlopTypes } from '@turbo/gen'
import type { Answers } from '../answers'
export function fixAll(answers: Answers, _config: any, _plop: PlopTypes.NodePlopAPI) {
return new Promise((resolve, reject) => {
console.log('🌀 running pnpm fix...')
$({
cwd: answers.turbo.paths.root,
nothrow: true,
quiet: true,
})`pnpm -F ${slugifyText(answers.name)} check:lint --fix && pnpm runx fix --deps --format --workers-types`
.then(onProcSuccess('pnpm fix', resolve, reject))
.catch(catchError(reject))
})
}
@@ -0,0 +1,20 @@
import { $ } from 'zx'
import { catchError, onProcSuccess } from '../helpers/proc'
import type { PlopTypes } from '@turbo/gen'
import type { Answers } from '../answers'
export function fixDepsAndFormat(answers: Answers, _config: any, _plop: PlopTypes.NodePlopAPI) {
return new Promise((resolve, reject) => {
console.log('🌀 running pnpm runx fix --deps --format')
$({
cwd: answers.turbo.paths.root,
nothrow: true,
quiet: true,
})`pnpm runx fix --deps --format`
.then(onProcSuccess('pnpm runx fix', resolve, reject))
.catch(catchError(reject))
})
}
+21
View File
@@ -0,0 +1,21 @@
import { $ } from 'zx'
import { catchError, onProcSuccess } from '../helpers/proc'
import type { PlopTypes } from '@turbo/gen'
import type { Answers } from '../answers'
export type PnpmInstallData = Answers & { destination: string }
export function pnpmInstall(data: PnpmInstallData, _config: any, _plop: PlopTypes.NodePlopAPI) {
return new Promise((resolve, reject) => {
console.log('🌀 running pnpm install')
$({
cwd: data.turbo.paths.root,
nothrow: true,
})`pnpm install --child-concurrency=10 -F ./${data.destination}`
.then(onProcSuccess('pnpm install', resolve, reject))
.catch(catchError(reject))
})
}
@@ -0,0 +1,23 @@
# {{ slug name }}
A Cloudflare Workers application with a minimal fetch handler
## Development
### Run in dev mode
```sh
pnpm dev
```
### Run tests
```sh
pnpm test
```
### Deploy
```sh
pnpm turbo deploy
```
@@ -0,0 +1,12 @@
// oxlint-disable @typescript-eslint/consistent-type-imports
type LocalEnv = import('./src/context').Env
type MainModule = typeof import('./src/{{ slug name }}.app')
// Add Env to Cloudflare namespace so that we can access it via
// import { env } from 'cloudflare:workers'
declare namespace Cloudflare {
interface Env extends LocalEnv {}
interface GlobalProps {
mainModule: MainModule
}
}
@@ -0,0 +1,25 @@
{
"name": "{{ slug name }}",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build:wrangler": "run-wrangler-build",
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"check:workers-types": "run-wrangler-types --check",
"deploy": "run-wrangler-deploy",
"dev": "run-wrangler-dev",
"fix:workers-types": "run-wrangler-types",
"test": "run-vitest"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.9.12",
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "22.15.27",
"vitest": "3.2.4",
"wrangler": "4.42.2"
}
}
@@ -0,0 +1,3 @@
export type Env = {
// add bindings here
}
@@ -0,0 +1,8 @@
import { SELF } from 'cloudflare:test'
import { expect, it } from 'vitest'
it('response with hello world', async () => {
const res = await SELF.fetch('https://example.com')
expect(res.status).toBe(200)
expect(await res.text()).toMatchInlineSnapshot(`"hello, world!"`)
})
@@ -0,0 +1,7 @@
import type { Env } from './context'
export default {
fetch: (_request, _env, _ctx) => {
return new Response('hello, world!')
},
} satisfies ExportedHandler<Env>
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/workers.json"
}
@@ -0,0 +1,15 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: `${__dirname}/wrangler.jsonc` },
miniflare: {
bindings: {
ENVIRONMENT: 'VITEST',
},
},
}),
],
})
@@ -0,0 +1,15 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "{{ slug name }}",
"main": "src/{{ slug name }}.app.ts",
"compatibility_date": "2025-09-20",
"compatibility_flags": ["nodejs_compat"],
"routes": [],
"upload_source_maps": true,
"observability": {
"logs": {
"enabled": true,
"head_sampling_rate": 1 // 100%
}
}
}
@@ -0,0 +1,29 @@
# {{ slug name }}
A Cloudflare Workers application using Hono and Vite
## Development
### Run in dev mode
```sh
pnpm turbo dev
```
### Run in preview mode
```sh
pnpm turbo preview
```
### Run tests
```sh
pnpm test
```
### Deploy
```sh
pnpm turbo deploy
```
@@ -0,0 +1,12 @@
// oxlint-disable @typescript-eslint/consistent-type-imports
type LocalEnv = import('./src/context').Env
type MainModule = typeof import('./src/{{ slug name }}.app')
// Add Env to Cloudflare namespace so that we can access it via
// import { env } from 'cloudflare:workers'
declare namespace Cloudflare {
interface Env extends LocalEnv {}
interface GlobalProps {
mainModule: MainModule
}
}
@@ -0,0 +1,33 @@
{
"name": "{{ slug name }}",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "run-vite-build",
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"check:workers-types": "run-wrangler-types --check",
"deploy": "run-wrangler-deploy",
"dev": "run-vite-dev",
"fix:workers-types": "run-wrangler-types",
"preview": "run-vite-preview",
"test": "run-vitest"
},
"dependencies": {
"@repo/hono-helpers": "workspace:*",
"hono": "4.7.8",
"workers-tagged-logger": "0.10.0"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.1.0",
"@cloudflare/vitest-pool-workers": "0.8.24",
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "22.15.3",
"vite": "^6.3.4",
"vitest": "3.1.2",
"wrangler": "4.14.1"
}
}
@@ -0,0 +1,14 @@
import type { HonoApp } from '@repo/hono-helpers'
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
export type Env = SharedHonoEnv & {
// add additional Bindings here
}
/** Variables can be extended */
export type Variables = SharedHonoVariables
export interface App extends HonoApp {
Bindings: Env
Variables: Variables
}
@@ -0,0 +1,8 @@
import { SELF } from 'cloudflare:test'
import { expect, it } from 'vitest'
it('response with hello world', async () => {
const res = await SELF.fetch('https://example.com')
expect(res.status).toBe(200)
expect(await res.text()).toMatchInlineSnapshot(`"hello, world!"`)
})
@@ -0,0 +1,26 @@
import { Hono } from 'hono'
import { useWorkersLogger } from 'workers-tagged-logger'
import { withNotFound, withOnError } from '@repo/hono-helpers'
import type { App } from './context'
const app = new Hono<App>()
.use(
'*',
// middleware
(c, next) =>
useWorkersLogger(c.env.NAME, {
environment: c.env.ENVIRONMENT,
release: c.env.SENTRY_RELEASE,
})(c, next)
)
.onError(withOnError())
.notFound(withNotFound())
.get('/', async (c) => {
return c.text('hello, world!')
})
export default app
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/workers.json"
}
@@ -0,0 +1,6 @@
import { cloudflare } from '@cloudflare/vite-plugin'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [cloudflare()],
})
@@ -0,0 +1,15 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: `${__dirname}/wrangler.jsonc` },
miniflare: {
bindings: {
ENVIRONMENT: 'VITEST',
},
},
}),
],
})
@@ -0,0 +1,19 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "{{ slug name }}",
"main": "src/{{ slug name }}.app.ts",
"compatibility_date": "2025-09-20",
"compatibility_flags": ["nodejs_compat"],
"routes": [],
"upload_source_maps": true,
"observability": {
"logs": {
"enabled": true,
"head_sampling_rate": 1 // 100%
}
},
"vars": {
"ENVIRONMENT": "development", // overridden during deployment
"SENTRY_RELEASE": "unknown" // overridden during deployment
}
}
@@ -0,0 +1,23 @@
# {{ slug name }}
A Cloudflare Workers application using Hono
## Development
### Run in dev mode
```sh
pnpm dev
```
### Run tests
```sh
pnpm test
```
### Deploy
```sh
pnpm turbo deploy
```
@@ -0,0 +1,12 @@
// oxlint-disable @typescript-eslint/consistent-type-imports
type LocalEnv = import('./src/context').Env
type MainModule = typeof import('./src/{{ slug name }}.app')
// Add Env to Cloudflare namespace so that we can access it via
// import { env } from 'cloudflare:workers'
declare namespace Cloudflare {
interface Env extends LocalEnv {}
interface GlobalProps {
mainModule: MainModule
}
}
@@ -0,0 +1,30 @@
{
"name": "{{ slug name }}",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build:wrangler": "run-wrangler-build",
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"check:workers-types": "run-wrangler-types --check",
"deploy": "run-wrangler-deploy",
"dev": "run-wrangler-dev",
"fix:workers-types": "run-wrangler-types",
"test": "run-vitest"
},
"dependencies": {
"@repo/hono-helpers": "workspace:*",
"hono": "4.7.8",
"workers-tagged-logger": "0.10.0"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.8.24",
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "22.15.3",
"vitest": "3.1.2",
"wrangler": "4.14.1"
}
}
@@ -0,0 +1,14 @@
import type { HonoApp } from '@repo/hono-helpers'
import type { SharedHonoEnv, SharedHonoVariables } from '@repo/hono-helpers/src/types'
export type Env = SharedHonoEnv & {
// add additional Bindings here
}
/** Variables can be extended */
export type Variables = SharedHonoVariables
export interface App extends HonoApp {
Bindings: Env
Variables: Variables
}
@@ -0,0 +1,8 @@
import { SELF } from 'cloudflare:test'
import { expect, it } from 'vitest'
it('response with hello world', async () => {
const res = await SELF.fetch('https://example.com')
expect(res.status).toBe(200)
expect(await res.text()).toMatchInlineSnapshot(`"hello, world!"`)
})
@@ -0,0 +1,26 @@
import { Hono } from 'hono'
import { useWorkersLogger } from 'workers-tagged-logger'
import { withNotFound, withOnError } from '@repo/hono-helpers'
import type { App } from './context'
const app = new Hono<App>()
.use(
'*',
// middleware
(c, next) =>
useWorkersLogger(c.env.NAME, {
environment: c.env.ENVIRONMENT,
release: c.env.SENTRY_RELEASE,
})(c, next)
)
.onError(withOnError())
.notFound(withNotFound())
.get('/', async (c) => {
return c.text('hello, world!')
})
export default app
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/workers.json"
}
@@ -0,0 +1,15 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: `${__dirname}/wrangler.jsonc` },
miniflare: {
bindings: {
ENVIRONMENT: 'VITEST',
},
},
}),
],
})
@@ -0,0 +1,19 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "{{ slug name }}",
"main": "src/{{ slug name }}.app.ts",
"compatibility_date": "2025-09-20",
"compatibility_flags": ["nodejs_compat"],
"routes": [],
"upload_source_maps": true,
"observability": {
"logs": {
"enabled": true,
"head_sampling_rate": 1 // 100%
}
},
"vars": {
"ENVIRONMENT": "development", // overridden during deployment
"SENTRY_RELEASE": "unknown" // overridden during deployment
}
}
@@ -0,0 +1,10 @@
# @repo/{{ slug name }}
A shared package that can be used from other apps / packages.
To add it as as a dependency in another package, run:
```sh
cd apps/example-worker-echoback # or whatever app/package you want to add it to
pnpm add '@repo/{{ slug name }}@workspace:*'
```
@@ -0,0 +1,21 @@
{
"name": "@repo/{{ slug name }}",
"version": "0.1.0",
"private": true,
"sideEffects": false,
"type": "module",
"main": "src/index.ts",
"scripts": {
"check:lint": "run-oxlint",
"check:types": "run-tsc",
"test": "run-vitest"
},
"dependencies": {},
"devDependencies": {
"@repo/tools": "workspace:*",
"@repo/typescript-config": "workspace:*",
"vitest": "3.1.3"{{#if usedInWorkers}},
"@cloudflare/vitest-pool-workers": "0.8.24",
"@cloudflare/workers-types": "4.20250503.0"{{/if}}
}
}
@@ -0,0 +1,16 @@
/**
* Example function.
*
* @example
*
* Use from other apps/packages:
*
* ```ts
* import { hello } from '@repo/{{ slug name }}'
*
* hello()
* ```
*/
export function hello() {
return 'Hello, world!'
}
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/{{ tsconfigType }}"
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/lib.json"
}