Initial commit from create-turbo
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import fs from 'node:fs'
|
||||
import { BunPlugin } from 'bun'
|
||||
import path from 'node:path'
|
||||
|
||||
export function globSync(
|
||||
globPattern: string,
|
||||
globOptions: { cwd?: string },
|
||||
) {
|
||||
return Array.from(new Bun.Glob(globPattern).scanSync(globOptions))
|
||||
}
|
||||
|
||||
const fsUsageMock = /* ts */ `{
|
||||
default: {
|
||||
existsSync() {
|
||||
return true;
|
||||
},
|
||||
statSync() {
|
||||
return {
|
||||
isDirectory() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const fileSourcesString = (files: string[], directory: string) => `
|
||||
const fileSources = {
|
||||
${files
|
||||
.map(
|
||||
(file) => /* ts */ `
|
||||
"${file.replaceAll('\\', '/')}": await import("${path
|
||||
.resolve(directory, file)
|
||||
.replace(/\\/gi, '\\\\')}"),`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`
|
||||
|
||||
const filerouter = () => {
|
||||
return {
|
||||
name: 'file-router',
|
||||
setup: (build) => {
|
||||
const directory = './src/routes'
|
||||
|
||||
build.onLoad(
|
||||
{ filter: /(.*)file-router(\/|\\)(.*).(ts)/i },
|
||||
async (args) => {
|
||||
const pattern = '**/*.{ts,tsx,js,jsx,mjs,cjs}'
|
||||
let content = String(await fs.promises.readFile(args.path))
|
||||
|
||||
const files = globSync(pattern, { cwd: directory })
|
||||
|
||||
content = content.replace('watch()', '')
|
||||
content = content.replace(
|
||||
'delete require.cache[require.resolve(pathToFileURL(fullPath).href)]',
|
||||
'',
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
'const files = globSync(globPattern, globOptions)',
|
||||
/* ts */ `const files = [${files
|
||||
.map((file) => `"${file.replaceAll('\\', '/')}"`)
|
||||
.join(',')}];`,
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
`import fs from 'node:fs'`,
|
||||
`var { default: fs} = ${fsUsageMock}`,
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
'// insert imports',
|
||||
fileSourcesString(files, directory),
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
/const file = (.*)/i,
|
||||
'const file = fileSources[filepath]',
|
||||
)
|
||||
|
||||
return { contents: content }
|
||||
},
|
||||
)
|
||||
},
|
||||
} satisfies BunPlugin
|
||||
}
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: ['file-router/index.ts'],
|
||||
target: 'bun',
|
||||
outdir: 'build',
|
||||
sourcemap: true,
|
||||
plugins: [filerouter()],
|
||||
compile: true,
|
||||
}).then(console.log)
|
||||
@@ -0,0 +1,85 @@
|
||||
import { getParamsFromFilePath } from '../utils/get-params-from-file-path'
|
||||
import { transformToUrl } from '../utils/transform-to-url'
|
||||
|
||||
type Stats = {
|
||||
isFile(): boolean
|
||||
isDirectory(): boolean
|
||||
isBlockDevice(): boolean
|
||||
isCharacterDevice(): boolean
|
||||
isSymbolicLink(): boolean
|
||||
isFIFO(): boolean
|
||||
isSocket(): boolean
|
||||
dev: number
|
||||
ino: number
|
||||
mode: number
|
||||
nlink: number
|
||||
uid: number
|
||||
gid: number
|
||||
rdev: number
|
||||
size: number
|
||||
blksize: number
|
||||
blocks: number
|
||||
atimeMs: number
|
||||
mtimeMs: number
|
||||
ctimeMs: number
|
||||
birthtimeMs: number
|
||||
atime: Date
|
||||
mtime: Date
|
||||
ctime: Date
|
||||
birthtime: Date
|
||||
}
|
||||
|
||||
const generateTemplate = ({ params }: { params?: string }) => {
|
||||
let config = ''
|
||||
if (params) {
|
||||
config += `{ params: ${params} }`
|
||||
}
|
||||
|
||||
if (config.length > 0) {
|
||||
config = ',' + config
|
||||
}
|
||||
|
||||
return `import Elysia from 'elysia'
|
||||
import { betterAuth } from '$lib/plugin/better-auth'
|
||||
|
||||
export const route = new Elysia()
|
||||
.use(betterAuth)
|
||||
.get('/', async () => {
|
||||
return 'hello'
|
||||
}${config})
|
||||
`
|
||||
}
|
||||
|
||||
export const onFileCreate = async (path: string, stats?: Stats) => {
|
||||
const file = Bun.file(path)
|
||||
|
||||
const params = getParamsFromFilePath(path)
|
||||
|
||||
const paramsTemplate = getParamsTemplate(params)
|
||||
|
||||
let template: string
|
||||
|
||||
template = generateTemplate({ params: paramsTemplate })
|
||||
|
||||
if ((await file.text()) === '') {
|
||||
await file.write(template)
|
||||
|
||||
console.log(await Bun.$`bunx prettier --write ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getParamsTemplate = (params: string[]) => {
|
||||
let result = ''
|
||||
|
||||
if (params.length > 0) {
|
||||
result += `t.Object({`
|
||||
|
||||
params.map((param) => {
|
||||
result += `"${param}": t.String(),`
|
||||
})
|
||||
|
||||
result += `})`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { createApp } from '$routes/index'
|
||||
import { Elysia } from 'elysia'
|
||||
import path from 'node:path'
|
||||
import readline from 'node:readline'
|
||||
import { pathToFileURL } from 'bun'
|
||||
import chokidar from 'chokidar'
|
||||
import { Cron } from 'croner'
|
||||
import { onFileCreate } from './hooks/on-file-create'
|
||||
import { transformToUrl } from './utils/transform-to-url'
|
||||
|
||||
export function addRelativeIfNotDot(path: string) {
|
||||
if (path.charAt(0) !== '.') return `./${path}`
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export function globSync(
|
||||
globPattern: string,
|
||||
globOptions: { cwd?: string },
|
||||
) {
|
||||
return Array.from(new Bun.Glob(globPattern).scanSync(globOptions))
|
||||
}
|
||||
|
||||
export function getPath(dir: string) {
|
||||
if (path.isAbsolute(dir)) return dir
|
||||
if (path.isAbsolute(process.argv[1]))
|
||||
return path.join(process.argv[1], '..', dir)
|
||||
|
||||
return path.join(process.cwd(), process.argv[1], '..', dir)
|
||||
}
|
||||
|
||||
const fileRouter = async () => {
|
||||
const directoryPath = getPath('../src/routes')
|
||||
|
||||
const globPattern = '**/*.{ts,tsx,js,jsx,mjs,cjs}'
|
||||
const globOptions = { cwd: directoryPath }
|
||||
|
||||
const files = globSync(globPattern, globOptions)
|
||||
|
||||
const plugin = new Elysia({ name: 'file-router' })
|
||||
|
||||
// insert imports
|
||||
|
||||
const paths: [path: string, exportName: string][] = []
|
||||
|
||||
for (const filepath of files) {
|
||||
const fullPath = path.join(directoryPath, filepath)
|
||||
const file = await import(pathToFileURL(fullPath).href)
|
||||
|
||||
// prettier-ignore
|
||||
delete require.cache[require.resolve(pathToFileURL(fullPath).href)]
|
||||
|
||||
const url = transformToUrl(filepath)
|
||||
|
||||
if (!file['route']) continue
|
||||
|
||||
const importedValue = file['route']
|
||||
|
||||
plugin.group(url, {}, (app) => app.use(importedValue))
|
||||
|
||||
paths.push([fullPath.replace(directoryPath, ''), 'route'])
|
||||
}
|
||||
|
||||
const outputAbsolutePath = './src/route-types.ts'
|
||||
|
||||
const imports: string[] = paths.map(
|
||||
([filePath, exportName], index) => {
|
||||
const importPath = filePath.replace(/\.(ts|tsx)$/, '')
|
||||
|
||||
return `import type ${
|
||||
exportName === 'default'
|
||||
? `Route${index}`
|
||||
: `{ ${exportName} as Route${index} }`
|
||||
} from "${addRelativeIfNotDot(
|
||||
path
|
||||
.relative(
|
||||
path.dirname(outputAbsolutePath),
|
||||
path.join(directoryPath, importPath),
|
||||
)
|
||||
.replaceAll('\\', '/'),
|
||||
)}";`
|
||||
},
|
||||
)
|
||||
|
||||
const input = [
|
||||
`import type { ElysiaWithBaseUrl } from "../file-router/types.ts";`,
|
||||
imports.join('\n'),
|
||||
'',
|
||||
`export type Route = ${paths
|
||||
.map(
|
||||
([x], index) =>
|
||||
`ElysiaWithBaseUrl<"${
|
||||
transformToUrl(x) || '/'
|
||||
}", typeof Route${index}>`,
|
||||
)
|
||||
.join('\n\t\t\t& ')}`,
|
||||
].join('\n')
|
||||
|
||||
await Bun.file(outputAbsolutePath).write(input)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
function clearScreen() {
|
||||
const repeatCount = process.stdout.rows - 2
|
||||
const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : ''
|
||||
console.log(blank)
|
||||
readline.cursorTo(process.stdout, 0, 0)
|
||||
readline.clearScreenDown(process.stdout)
|
||||
}
|
||||
|
||||
let app = createApp()
|
||||
app.use(await fileRouter())
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: app.fetch,
|
||||
port: Bun.env.PORT ?? 3000,
|
||||
})
|
||||
|
||||
const reloadServer = async () => {
|
||||
if ('cron' in app.store) {
|
||||
// @ts-ignore
|
||||
const cron = app.store.cron as Record<string, Cron>
|
||||
for (const [key, value] of Object.entries(cron)) {
|
||||
// @ts-ignore
|
||||
app.store.cron[key].stop()
|
||||
}
|
||||
}
|
||||
app = createApp()
|
||||
app.use(await fileRouter())
|
||||
|
||||
server.reload({ fetch: app.fetch })
|
||||
|
||||
clearScreen()
|
||||
console.log(
|
||||
`🦊 Elysia is running at http://${server.hostname}:${server.port}`,
|
||||
)
|
||||
}
|
||||
|
||||
const watch = () => {
|
||||
chokidar
|
||||
.watch('./src/routes', { persistent: true })
|
||||
.on('all', async (event, path) => {
|
||||
console.log(event, path)
|
||||
|
||||
for (const path of Object.keys(require.cache)) {
|
||||
delete require.cache[path]
|
||||
}
|
||||
|
||||
try {
|
||||
await reloadServer()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.on('add', async (path, stats) => {
|
||||
console.log(path, stats)
|
||||
|
||||
try {
|
||||
await onFileCreate(path, stats)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
chokidar
|
||||
.watch('./lib', { persistent: true })
|
||||
.on('all', async (event, path) => {
|
||||
console.log(event, path)
|
||||
|
||||
for (const path of Object.keys(require.cache)) {
|
||||
delete require.cache[path]
|
||||
}
|
||||
|
||||
try {
|
||||
await reloadServer()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch()
|
||||
@@ -0,0 +1,65 @@
|
||||
import type Elysia from 'elysia'
|
||||
import type { AnyElysia, RouteBase, RouteSchema } from 'elysia'
|
||||
|
||||
type PathToObject<
|
||||
Path extends string,
|
||||
Type extends RouteBase,
|
||||
> = Path extends `${infer Head}/${infer Rest}`
|
||||
? Head extends ''
|
||||
? PathToObject<Rest, Type>
|
||||
: { [K in Head]: PathToObject<Rest, Type> }
|
||||
: { [K in Path]: Type }
|
||||
|
||||
namespace ElysiaMatch {
|
||||
export type RouteEnd = Record<string, RouteSchema>
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
export type Fx = (...args: any[]) => AnyElysia
|
||||
export type All = AnyElysia | Fx
|
||||
|
||||
export type Extract<T extends All> = T extends Fx
|
||||
? ReturnType<T>
|
||||
: T
|
||||
}
|
||||
|
||||
type FlattenIndexRoutes<T> = T extends object
|
||||
? {
|
||||
[K in keyof T as K extends 'index'
|
||||
? T[K] extends ElysiaMatch.RouteEnd
|
||||
? never
|
||||
: K
|
||||
: K]: FlattenIndexRoutes<T[K]>
|
||||
} & (T extends { index: infer I }
|
||||
? I extends ElysiaMatch.RouteEnd
|
||||
? FlattenIndexRoutes<I>
|
||||
: T
|
||||
: T)
|
||||
: T
|
||||
|
||||
export type ElysiaWithBaseUrl<
|
||||
BaseUrl extends string,
|
||||
ElysiaType extends ElysiaMatch.All,
|
||||
> =
|
||||
ElysiaMatch.Extract<ElysiaType> extends Elysia<
|
||||
infer BasePath,
|
||||
// infer Scoped,
|
||||
infer Singleton,
|
||||
infer Definitions,
|
||||
infer Metadata,
|
||||
infer Routes,
|
||||
infer Ephemeral,
|
||||
infer Volatile
|
||||
>
|
||||
? Elysia<
|
||||
BasePath,
|
||||
// Scoped,
|
||||
Singleton,
|
||||
Definitions,
|
||||
Metadata,
|
||||
FlattenIndexRoutes<PathToObject<BaseUrl, Routes>>,
|
||||
Ephemeral,
|
||||
Volatile
|
||||
>
|
||||
: never
|
||||
|
||||
export type SoftString<T extends string> = T | (string & {})
|
||||
@@ -0,0 +1,9 @@
|
||||
export const getParamsFromFilePath = (path: string) => {
|
||||
const params = path.match(/\[(.*?)\]/gu) || []
|
||||
return params.map((param) => {
|
||||
return param.replaceAll(
|
||||
/\[(.*?)\]/gu,
|
||||
(_: string, match: string) => `${match}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// https://github.com/wobsoriano/elysia-autoroutes/blob/main/src/utils/transformPathToUrl.ts#L4C31-L4C31
|
||||
export function transformToUrl(path: string) {
|
||||
return (
|
||||
path
|
||||
// Clean the url extensions
|
||||
.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/u, '')
|
||||
// Fix windows slashes
|
||||
.replaceAll('\\', '/')
|
||||
// Handle wild card based routes - users/[...id]/profile.ts -> users/*/profile
|
||||
.replaceAll(/\[\.\.\..*\]/gu, '*')
|
||||
// Handle generic square bracket based routes - users/[id]/index.ts -> users/:id
|
||||
.replaceAll(
|
||||
/\[(.*?)\]/gu,
|
||||
(_: string, match: string) => `:${match}`,
|
||||
)
|
||||
.replace(/\/?\((.*)\)/, '')
|
||||
// Handle the case when multiple parameters are present in one file
|
||||
// users / [id] - [name].ts to users /: id -:name and users / [id] - [name] / [age].ts to users /: id -: name /: age
|
||||
.replaceAll(']-[', '-:')
|
||||
.replaceAll(']/', '/')
|
||||
.replaceAll(/\[|\]/gu, '')
|
||||
// remove potential route methods in file name
|
||||
.replace(
|
||||
/(\.(?<method>connect|delete|get|head|options|patch|post|put|trace))?(\.(?<env>dev|prod|prerender))?$/,
|
||||
'',
|
||||
)
|
||||
// remove index from end of path
|
||||
.replace(/\/?index$/, '')
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user