mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 23:21:30 -07:00
Initial commit
This commit is contained in:
@@ -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])
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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 }}"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/lib.json"
|
||||
}
|
||||
Reference in New Issue
Block a user