rajt
Version:
A serverless bundler layer, fully typed for AWS Lambda (Node.js and LLRT) and Cloudflare Workers.
234 lines (209 loc) • 6.3 kB
text/typescript
import { join } from 'pathe'
import { spawn, type ChildProcess } from 'node:child_process'
import { defineCommand } from 'citty'
import type { Miniflare } from 'miniflare'
import {
_root, build, wait, watch, normalizePlatform, platformError, getRuntime,
wranglerConfig, createMiniflare, localflareManifest,
getDockerHost
} from '../utils'
import { error, event, log, rn, warn } from '../../utils/log'
import { withPort } from '../../utils/port'
import shutdown from '../../utils/shutdown'
export default defineCommand({
meta: {
name: 'dev',
description: '💻 Start the localhost server\n',
},
args: {
port: {
description: 'Port to listen on',
type: 'number',
default: 3000,
},
host: {
description: 'Host to forward requests to, defaults to the zone of project',
type: 'string',
default: 'localhost',
},
platform: {
alias: 'p',
description: 'Environment platform',
type: 'enum',
options: ['aws', 'cf', 'node'] as const,
// required: true,
},
},
async run({ args }) { // @ts-ignore
const platform = normalizePlatform(args.p || args.platform || args._[0] || 'node')
if (!platform)
return platformError()
const desiredPort = args.port ? Number(args.port) : 3000
const host = args.host ? String(args.host) : 'localhost'
let isBuilding = false
const startApp = async (start: Function, stop: Function|undefined = undefined, building: boolean = true) => {
if (building) {
if (isBuilding) return
isBuilding = true
event('Building..')
}
const fn = async () => {
building && await build(platform)
await start()
}
try {
await fn()
watch(async () => {
event('Restarting..')
await fn()
// event('Restarted...')
})
// @ts-ignore
stop && shutdown(stop)
} catch (e: any) {
error(e)
process.exit(0)
} finally {
isBuilding = false
}
}
const applyExit = async (app: ChildProcess | null) => {
if (!app) return null
app //?.on('exit', code => process.exit(code ?? 0))
.on('message', msg => {
process.send && process.send(msg)
}).on('disconnect', () => {
process.disconnect && process.disconnect()
})
}
const killProcess = async (app: ChildProcess | null) => {
if (!app) return null
// event('Stopping..')
try {
if (!app?.killed) {
app.kill('SIGTERM')
await wait(1000)
if (!app?.killed) { // force kill
app.kill('SIGKILL')
await wait(1000)
}
}
return null
} catch (e) {
error('Error stopping:', e)
}
return null
}
const started = (port: number) => {
log(`Starting API on http://${host}:${port}`)
if (platform == 'cf')
log(`Localflare on https://studio.localflare.dev`)
rn()
}
switch (platform) {
case 'aws':
return withPort(desiredPort, async (port) => {
started(port)
let lambda: ChildProcess | null = null
const stopLambda = async () => {
lambda = await killProcess(lambda)
}
const startLambda = async () => {
if (lambda) await stopLambda()
lambda = spawn(
'sam',
[
'local', 'start-api',
'--warm-containers', 'LAZY',
'--debug', '--template-file', join(_root, 'template-dev.yaml'),
'--port', String(port),
],
{
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
// stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
shell: process.platform == 'win32',
env: {...process.env, DOCKER_HOST: getDockerHost()},
}
)
//.on('exit', code => {
// warn(`Lambda process exited with code ${code ?? 0}`)
// if (code != 0 && code != null)
// error('Lambda process crashed, waiting for restart...')
// lambda = null
// }).on('message', msg => {
// process.send && process.send(msg)
// }).on('disconnect', () => {
// process.disconnect && process.disconnect()
// }).on('error', e => {
// error('Lambda process error:', e)
// lambda = null
// })
applyExit(lambda)
await wait(2000)
}
await startApp(startLambda, stopLambda)
})
case 'cf':
return withPort(desiredPort, async (port) => {
started(port)
let worker: Miniflare | null = null
let localflare: Miniflare | null = null
const startWorker = async () => {
if (worker) await worker.dispose()
if (localflare) await localflare.dispose()
const workerConfig = await wranglerConfig()
workerConfig.host = host
workerConfig.liveReload = false
worker = createMiniflare({ ...workerConfig, port })
await worker.ready
localflare = createMiniflare({
...workerConfig,
vars: {
...workerConfig.vars,
LOCALFLARE_MANIFEST: JSON.stringify(localflareManifest(workerConfig)),
},
main: 'node_modules/localflare-api/dist/worker/index.js',
port: 8788,
inspectorPort: 9230,
})
await localflare.ready
}
await startApp(startWorker)
})
default:
case 'node':
return withPort(desiredPort, async (port) => {
started(port)
const isBun = getRuntime() == 'bun'
const isWin32 = process.platform == 'win32'
const params = isBun
? ['run', '--port=' + port, '--hot', '--silent', '--no-clear-screen', '--no-summary', join(_root, 'node_modules/rajt/src/dev.ts')]
: [join(_root, 'node_modules/.bin/tsx' + (isWin32 ? '.exe' : '')), 'watch', join(_root, 'node_modules/rajt/src/dev-node.ts')]
let nodeApp: ChildProcess | null = null
const stopNode = async () => {
nodeApp = await killProcess(nodeApp)
}
const startNode = async () => {
if (nodeApp) await stopNode()
nodeApp = spawn(
isBun && isWin32 ? 'bun' : process.execPath,
params,
{
stdio: ['inherit', isBun ? 'pipe' : 'inherit', 'inherit', 'ipc'],
env: {...process.env, PORT: port},
}
)
if (isBun && nodeApp?.stdout) {
nodeApp.stdout?.on('data', data => {
const output = data.toString()
if (!output.includes('Started development server'))
process.stdout.write(output)
})
}
applyExit(nodeApp)
}
await startApp(startNode, stopNode, false)
})
}
},
})