mirror of
https://github.com/pockethost/pockethost.git
synced 2025-06-08 07:06:41 +00:00
chore(multi): move to plugin.ts
This commit is contained in:
parent
597d42d3ea
commit
7b8248a486
@ -1,25 +1,3 @@
|
|||||||
import { info } from 'console'
|
import { plugin } from './plugin'
|
||||||
import { PocketHostPlugin, onIncomingRequestAction } from 'pockethost'
|
|
||||||
import { PLUGIN_NAME } from './constants'
|
|
||||||
import { dbg } from './log'
|
|
||||||
|
|
||||||
const plugin: PocketHostPlugin = async ({}) => {
|
|
||||||
dbg(`initializing ${PLUGIN_NAME}`)
|
|
||||||
|
|
||||||
onIncomingRequestAction(async ({ req, res }) => {
|
|
||||||
const url = new URL(`http://${req.headers.host}${req.url}`)
|
|
||||||
const country = (req.headers['cf-ipcountry'] as string) || '<ct>'
|
|
||||||
const ip = (req.headers['x-forwarded-for'] as string) || '<ip>'
|
|
||||||
const method = req.method || '<m>'
|
|
||||||
const sig = [
|
|
||||||
method.padStart(10),
|
|
||||||
country.padStart(5),
|
|
||||||
ip.padEnd(45),
|
|
||||||
url.toString(),
|
|
||||||
].join(' ')
|
|
||||||
info(`Incoming request ${sig}`)
|
|
||||||
res.locals.sig = sig
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
|
23
packages/plugin-cloudflare-request-logger/src/plugin.ts
Normal file
23
packages/plugin-cloudflare-request-logger/src/plugin.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { info } from 'console'
|
||||||
|
import { PocketHostPlugin, onIncomingRequestAction } from 'pockethost'
|
||||||
|
import { PLUGIN_NAME } from './constants'
|
||||||
|
import { dbg } from './log'
|
||||||
|
|
||||||
|
export const plugin: PocketHostPlugin = async ({}) => {
|
||||||
|
dbg(`initializing ${PLUGIN_NAME}`)
|
||||||
|
|
||||||
|
onIncomingRequestAction(async ({ req, res }) => {
|
||||||
|
const url = new URL(`http://${req.headers.host}${req.url}`)
|
||||||
|
const country = (req.headers['cf-ipcountry'] as string) || '<ct>'
|
||||||
|
const ip = (req.headers['x-forwarded-for'] as string) || '<ip>'
|
||||||
|
const method = req.method || '<m>'
|
||||||
|
const sig = [
|
||||||
|
method.padStart(10),
|
||||||
|
country.padStart(5),
|
||||||
|
ip.padEnd(45),
|
||||||
|
url.toString(),
|
||||||
|
].join(' ')
|
||||||
|
info(`Incoming request ${sig}`)
|
||||||
|
res.locals.sig = sig
|
||||||
|
})
|
||||||
|
}
|
@ -1,58 +1,3 @@
|
|||||||
import stringify from 'json-stringify-safe'
|
import { plugin } from './plugin'
|
||||||
import {
|
|
||||||
LogLevelName,
|
|
||||||
PocketHostPlugin,
|
|
||||||
isLevelGte,
|
|
||||||
isLevelLte,
|
|
||||||
onLogAction,
|
|
||||||
} from 'pockethost'
|
|
||||||
import { PLUGIN_NAME } from './constants'
|
|
||||||
import { dbg } from './log'
|
|
||||||
|
|
||||||
export const LogLevelConsoleMap = {
|
|
||||||
[LogLevelName.Trace]: console.trace,
|
|
||||||
[LogLevelName.Raw]: console.log,
|
|
||||||
[LogLevelName.Debug]: console.debug,
|
|
||||||
[LogLevelName.Info]: console.info,
|
|
||||||
[LogLevelName.Warn]: console.warn,
|
|
||||||
[LogLevelName.Error]: console.error,
|
|
||||||
[LogLevelName.Abort]: console.error,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const plugin: PocketHostPlugin = async ({}) => {
|
|
||||||
onLogAction(async ({ currentLevel, levelIn, args }) => {
|
|
||||||
if (!isLevelGte(levelIn, currentLevel)) return
|
|
||||||
const finalArgs = []
|
|
||||||
if (args.length > 0) {
|
|
||||||
finalArgs.push(args.shift())
|
|
||||||
}
|
|
||||||
while (args.length > 0) {
|
|
||||||
let arg = args.shift()
|
|
||||||
if (arg instanceof Error) {
|
|
||||||
finalArgs.push(...[arg.name, arg.message.toString()])
|
|
||||||
if (isLevelLte(levelIn, LogLevelName.Debug) && arg.stack) {
|
|
||||||
finalArgs.push(...arg.stack.split(/\n/))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (typeof arg === 'string') {
|
|
||||||
finalArgs.push(arg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (typeof arg === 'object') {
|
|
||||||
finalArgs.push(stringify(arg, null, 2))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (typeof arg === 'function') {
|
|
||||||
finalArgs.push(`<<function ${stringify(arg.toString())}>>`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
finalArgs.push(arg)
|
|
||||||
}
|
|
||||||
LogLevelConsoleMap[levelIn](...finalArgs)
|
|
||||||
})
|
|
||||||
|
|
||||||
dbg(`initializing ${PLUGIN_NAME}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
|
56
packages/plugin-console-logger/src/plugin.ts
Normal file
56
packages/plugin-console-logger/src/plugin.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import stringify from 'json-stringify-safe'
|
||||||
|
import {
|
||||||
|
LogLevelName,
|
||||||
|
PocketHostPlugin,
|
||||||
|
isLevelGte,
|
||||||
|
isLevelLte,
|
||||||
|
onLogAction,
|
||||||
|
} from 'pockethost'
|
||||||
|
import { PLUGIN_NAME } from './constants'
|
||||||
|
import { dbg } from './log'
|
||||||
|
|
||||||
|
export const LogLevelConsoleMap = {
|
||||||
|
[LogLevelName.Trace]: console.trace,
|
||||||
|
[LogLevelName.Raw]: console.log,
|
||||||
|
[LogLevelName.Debug]: console.debug,
|
||||||
|
[LogLevelName.Info]: console.info,
|
||||||
|
[LogLevelName.Warn]: console.warn,
|
||||||
|
[LogLevelName.Error]: console.error,
|
||||||
|
[LogLevelName.Abort]: console.error,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const plugin: PocketHostPlugin = async ({}) => {
|
||||||
|
onLogAction(async ({ currentLevel, levelIn, args }) => {
|
||||||
|
if (!isLevelGte(levelIn, currentLevel)) return
|
||||||
|
const finalArgs = []
|
||||||
|
if (args.length > 0) {
|
||||||
|
finalArgs.push(args.shift())
|
||||||
|
}
|
||||||
|
while (args.length > 0) {
|
||||||
|
let arg = args.shift()
|
||||||
|
if (arg instanceof Error) {
|
||||||
|
finalArgs.push(...[arg.name, arg.message.toString()])
|
||||||
|
if (isLevelLte(levelIn, LogLevelName.Debug) && arg.stack) {
|
||||||
|
finalArgs.push(...arg.stack.split(/\n/))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof arg === 'string') {
|
||||||
|
finalArgs.push(arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
finalArgs.push(stringify(arg, null, 2))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof arg === 'function') {
|
||||||
|
finalArgs.push(`<<function ${stringify(arg.toString())}>>`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finalArgs.push(arg)
|
||||||
|
}
|
||||||
|
LogLevelConsoleMap[levelIn](...finalArgs)
|
||||||
|
})
|
||||||
|
|
||||||
|
dbg(`initializing ${PLUGIN_NAME}`)
|
||||||
|
}
|
@ -1,286 +1,3 @@
|
|||||||
import { Mutex } from 'async-mutex'
|
import { plugin } from './plugin'
|
||||||
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
||||||
import getPort from 'get-port'
|
|
||||||
import { globSync } from 'glob'
|
|
||||||
import { gobot } from 'gobot'
|
|
||||||
import path from 'path'
|
|
||||||
import {
|
|
||||||
Bind,
|
|
||||||
InstanceFields,
|
|
||||||
PocketHostPlugin,
|
|
||||||
doAfterInstanceStartedAction,
|
|
||||||
doAfterInstanceStoppedAction,
|
|
||||||
doInstanceConfigFilter,
|
|
||||||
doInstanceLogAction,
|
|
||||||
mkInstance,
|
|
||||||
onAfterInstanceFoundAction,
|
|
||||||
onAfterServerStartAction,
|
|
||||||
onGetInstanceByRequestInfoFilter,
|
|
||||||
onGetOrProvisionInstanceUrlFilter,
|
|
||||||
onNewInstanceRecordFilter,
|
|
||||||
} from 'pockethost'
|
|
||||||
import {
|
|
||||||
APEX_DOMAIN,
|
|
||||||
INSTANCE_DATA_DIR,
|
|
||||||
PORT,
|
|
||||||
exitHook,
|
|
||||||
tryFetch,
|
|
||||||
} from 'pockethost/core'
|
|
||||||
import { gte } from 'semver'
|
|
||||||
import { PLUGIN_NAME } from './constants'
|
|
||||||
import { dbg, info } from './log'
|
|
||||||
|
|
||||||
const deleteFiles = (globPattern: string) => {
|
|
||||||
const files = globSync(globPattern)
|
|
||||||
files.forEach((file) => {
|
|
||||||
dbg(`Deleting ${file}`)
|
|
||||||
fs.unlinkSync(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyFiles = (binds: Bind[], destination: string) => {
|
|
||||||
binds.forEach((bind) => {
|
|
||||||
const srcFiles = globSync(bind.src)
|
|
||||||
srcFiles.forEach((srcFile) => {
|
|
||||||
const relativePath = path.relative(bind.base, srcFile)
|
|
||||||
const destFile = path.join(destination, relativePath)
|
|
||||||
const destDir = path.dirname(destFile)
|
|
||||||
dbg(`Copying ${srcFile} to ${destFile}`, { relativePath, destDir, bind })
|
|
||||||
|
|
||||||
if (!fs.existsSync(destDir)) {
|
|
||||||
fs.mkdirSync(destDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.copyFileSync(srcFile, destFile)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const escape = (path: string) => `"${path}"`
|
|
||||||
|
|
||||||
const metaPath = (name: string) => {
|
|
||||||
const instanceRootDir = INSTANCE_DATA_DIR(name)
|
|
||||||
mkdirSync(instanceRootDir, { recursive: true })
|
|
||||||
const instanceMetaFile = INSTANCE_DATA_DIR(name, `meta.json`)
|
|
||||||
return instanceMetaFile
|
|
||||||
}
|
|
||||||
|
|
||||||
const readMeta = (path: string) =>
|
|
||||||
JSON.parse(readFileSync(path, 'utf-8').toString())
|
|
||||||
|
|
||||||
const getOrCreateMeta = async (name: string) => {
|
|
||||||
const instanceMetaFile = metaPath(name)
|
|
||||||
const meta = await (async () => {
|
|
||||||
if (!existsSync(instanceMetaFile)) {
|
|
||||||
const meta = await mkInstance(name)
|
|
||||||
writeFileSync(instanceMetaFile, JSON.stringify(meta, null, 2))
|
|
||||||
}
|
|
||||||
const meta = readMeta(instanceMetaFile)
|
|
||||||
return meta
|
|
||||||
})()
|
|
||||||
return meta
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeMeta = async (instance: InstanceFields) => {
|
|
||||||
const { subdomain } = instance
|
|
||||||
const instanceMetaFile = metaPath(subdomain)
|
|
||||||
writeFileSync(instanceMetaFile, JSON.stringify(instance, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin: PocketHostPlugin = async ({}) => {
|
|
||||||
dbg(`initializing ${PLUGIN_NAME}`)
|
|
||||||
|
|
||||||
/** Display some informational alerts to help the user get started. */
|
|
||||||
onAfterServerStartAction(async () => {
|
|
||||||
const protocol = PORT() === 443 ? 'https' : 'http'
|
|
||||||
{
|
|
||||||
const url = new URL(`${protocol}://*.${APEX_DOMAIN()}`)
|
|
||||||
url.port = `${PORT() === 80 || PORT() == 443 ? '' : PORT()}`
|
|
||||||
info(`Listening for requests on ${url}`)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const url = new URL(`${protocol}://hello.${APEX_DOMAIN()}`)
|
|
||||||
url.port = `${PORT() === 80 || PORT() == 443 ? '' : PORT()}`
|
|
||||||
info(`Try visiting ${url}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** When a request comes in, return an instance based on subdomain */
|
|
||||||
onGetInstanceByRequestInfoFilter(async (instance, context) => {
|
|
||||||
const { subdomain } = context
|
|
||||||
const meta = await getOrCreateMeta(subdomain)
|
|
||||||
return { ...instance, ...meta }
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a new instance model is instantiated, this filter gives listeners a
|
|
||||||
* chance to augment or update the instance data.
|
|
||||||
*
|
|
||||||
* In this case, the instance data is restored from a local db.
|
|
||||||
*/
|
|
||||||
onNewInstanceRecordFilter(async (instance) => {
|
|
||||||
const { subdomain } = instance
|
|
||||||
const path = metaPath(subdomain)
|
|
||||||
if (!existsSync(path)) return instance
|
|
||||||
const meta = await readMeta(path)
|
|
||||||
return { ...instance, ...meta }
|
|
||||||
})
|
|
||||||
|
|
||||||
/** After an instance has been found, store it to the db */
|
|
||||||
onAfterInstanceFoundAction(async (context) => {
|
|
||||||
const { instance } = context
|
|
||||||
await writeMeta(instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
const instances: { [_: string]: Promise<string> } = {}
|
|
||||||
|
|
||||||
const launchMutex = new Mutex()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The workhorse. This filter is responsible for launching PocketBase and
|
|
||||||
* returning an endpoint URL.
|
|
||||||
*/
|
|
||||||
onGetOrProvisionInstanceUrlFilter(async (url: string, { instance }) => {
|
|
||||||
const { dev, subdomain, version, secrets } = instance
|
|
||||||
|
|
||||||
if (subdomain in instances) return instances[subdomain]!
|
|
||||||
|
|
||||||
dbg({ instance })
|
|
||||||
return (instances[subdomain] = new Promise<string>(
|
|
||||||
async (resolve, reject) => {
|
|
||||||
const bot = await gobot(`pocketbase`, { version })
|
|
||||||
const realVersion = await bot.maxSatisfyingVersion(version)
|
|
||||||
if (!realVersion) {
|
|
||||||
throw new Error(`No PocketBase version satisfying ${version}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceConfig = await doInstanceConfigFilter({
|
|
||||||
env: {},
|
|
||||||
binds: {
|
|
||||||
data: [],
|
|
||||||
hooks: [],
|
|
||||||
migrations: [],
|
|
||||||
public: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
dbg(`instanceConfig`, { instanceConfig })
|
|
||||||
|
|
||||||
const dataDir = INSTANCE_DATA_DIR(subdomain, `pb_data`)
|
|
||||||
const hooksDir = INSTANCE_DATA_DIR(subdomain, `pb_hooks`)
|
|
||||||
const migrationsDir = INSTANCE_DATA_DIR(subdomain, `pb_migrations`)
|
|
||||||
const publicDir = INSTANCE_DATA_DIR(subdomain, `pb_public`)
|
|
||||||
|
|
||||||
;[dataDir, hooksDir, migrationsDir, publicDir].forEach((dir) => {
|
|
||||||
return mkdirSync(dir, { recursive: true })
|
|
||||||
})
|
|
||||||
const { binds, env } = instanceConfig
|
|
||||||
copyFiles(binds.data, dataDir)
|
|
||||||
copyFiles(binds.hooks, hooksDir)
|
|
||||||
copyFiles(binds.migrations, migrationsDir)
|
|
||||||
copyFiles(binds.public, publicDir)
|
|
||||||
|
|
||||||
return launchMutex.runExclusive(async () => {
|
|
||||||
dbg(`got lock`)
|
|
||||||
const port = await getPort()
|
|
||||||
const args = [
|
|
||||||
`serve`,
|
|
||||||
`--dir`,
|
|
||||||
escape(dataDir),
|
|
||||||
`--hooksDir`,
|
|
||||||
escape(hooksDir),
|
|
||||||
`--migrationsDir`,
|
|
||||||
escape(migrationsDir),
|
|
||||||
`--publicDir`,
|
|
||||||
escape(publicDir),
|
|
||||||
`--http`,
|
|
||||||
`0.0.0.0:${port}`,
|
|
||||||
]
|
|
||||||
if (dev && gte(realVersion, `0.20.1`)) args.push(`--dev`)
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: `Launching: ${await bot.getBinaryFilePath()} ${args.join(
|
|
||||||
' ',
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
bot.run(args, { env: { ...secrets, ...env } }, (proc) => {
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
data
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.split(`\n`)
|
|
||||||
.forEach((line: string) => {
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: line,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
data
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.split(`\n`)
|
|
||||||
.forEach((line: string) => {
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stderr',
|
|
||||||
data: line,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const unsub = exitHook(() => {
|
|
||||||
dbg(`killing ${subdomain}`)
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: `Forcibly killing PocketBase process`,
|
|
||||||
})
|
|
||||||
proc.kill()
|
|
||||||
})
|
|
||||||
proc.on('exit', (code) => {
|
|
||||||
unsub()
|
|
||||||
delete instances[subdomain]
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: `PocketBase process exited with code ${code}`,
|
|
||||||
})
|
|
||||||
doAfterInstanceStoppedAction({ instance, url })
|
|
||||||
dbg(`${subdomain} process exited with code ${code}`)
|
|
||||||
})
|
|
||||||
const url = `http://localhost:${port}`
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: `Waiting for PocketBase to start on ${url}`,
|
|
||||||
})
|
|
||||||
tryFetch(url)
|
|
||||||
.then(() => {
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stdout',
|
|
||||||
data: `PocketBase started on ${url}`,
|
|
||||||
})
|
|
||||||
doAfterInstanceStartedAction({ instance, url })
|
|
||||||
return resolve(url)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
doInstanceLogAction({
|
|
||||||
instance,
|
|
||||||
type: 'stderr',
|
|
||||||
data: `PocketBase failed to start on ${url}: ${e}`,
|
|
||||||
})
|
|
||||||
reject(e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
|
283
packages/plugin-launcher-spawn/src/plugin.ts
Normal file
283
packages/plugin-launcher-spawn/src/plugin.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import { Mutex } from 'async-mutex'
|
||||||
|
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
||||||
|
import getPort from 'get-port'
|
||||||
|
import { globSync } from 'glob'
|
||||||
|
import { gobot } from 'gobot'
|
||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
Bind,
|
||||||
|
InstanceFields,
|
||||||
|
PocketHostPlugin,
|
||||||
|
doAfterInstanceStartedAction,
|
||||||
|
doAfterInstanceStoppedAction,
|
||||||
|
doInstanceConfigFilter,
|
||||||
|
doInstanceLogAction,
|
||||||
|
mkInstance,
|
||||||
|
onAfterInstanceFoundAction,
|
||||||
|
onAfterServerStartAction,
|
||||||
|
onGetInstanceByRequestInfoFilter,
|
||||||
|
onGetOrProvisionInstanceUrlFilter,
|
||||||
|
onNewInstanceRecordFilter,
|
||||||
|
} from 'pockethost'
|
||||||
|
import {
|
||||||
|
APEX_DOMAIN,
|
||||||
|
INSTANCE_DATA_DIR,
|
||||||
|
PORT,
|
||||||
|
exitHook,
|
||||||
|
tryFetch,
|
||||||
|
} from 'pockethost/core'
|
||||||
|
import { gte } from 'semver'
|
||||||
|
import { PLUGIN_NAME } from './constants'
|
||||||
|
import { dbg, info } from './log'
|
||||||
|
|
||||||
|
const deleteFiles = (globPattern: string) => {
|
||||||
|
const files = globSync(globPattern)
|
||||||
|
files.forEach((file) => {
|
||||||
|
dbg(`Deleting ${file}`)
|
||||||
|
fs.unlinkSync(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyFiles = (binds: Bind[], destination: string) => {
|
||||||
|
binds.forEach((bind) => {
|
||||||
|
const srcFiles = globSync(bind.src)
|
||||||
|
srcFiles.forEach((srcFile) => {
|
||||||
|
const relativePath = path.relative(bind.base, srcFile)
|
||||||
|
const destFile = path.join(destination, relativePath)
|
||||||
|
const destDir = path.dirname(destFile)
|
||||||
|
dbg(`Copying ${srcFile} to ${destFile}`, { relativePath, destDir, bind })
|
||||||
|
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(srcFile, destFile)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const escape = (path: string) => `"${path}"`
|
||||||
|
|
||||||
|
const metaPath = (name: string) => {
|
||||||
|
const instanceRootDir = INSTANCE_DATA_DIR(name)
|
||||||
|
mkdirSync(instanceRootDir, { recursive: true })
|
||||||
|
const instanceMetaFile = INSTANCE_DATA_DIR(name, `meta.json`)
|
||||||
|
return instanceMetaFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const readMeta = (path: string) =>
|
||||||
|
JSON.parse(readFileSync(path, 'utf-8').toString())
|
||||||
|
|
||||||
|
const getOrCreateMeta = async (name: string) => {
|
||||||
|
const instanceMetaFile = metaPath(name)
|
||||||
|
const meta = await (async () => {
|
||||||
|
if (!existsSync(instanceMetaFile)) {
|
||||||
|
const meta = await mkInstance(name)
|
||||||
|
writeFileSync(instanceMetaFile, JSON.stringify(meta, null, 2))
|
||||||
|
}
|
||||||
|
const meta = readMeta(instanceMetaFile)
|
||||||
|
return meta
|
||||||
|
})()
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeMeta = async (instance: InstanceFields) => {
|
||||||
|
const { subdomain } = instance
|
||||||
|
const instanceMetaFile = metaPath(subdomain)
|
||||||
|
writeFileSync(instanceMetaFile, JSON.stringify(instance, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PocketHostPlugin = async ({}) => {
|
||||||
|
dbg(`initializing ${PLUGIN_NAME}`)
|
||||||
|
|
||||||
|
/** Display some informational alerts to help the user get started. */
|
||||||
|
onAfterServerStartAction(async () => {
|
||||||
|
const protocol = PORT() === 443 ? 'https' : 'http'
|
||||||
|
{
|
||||||
|
const url = new URL(`${protocol}://*.${APEX_DOMAIN()}`)
|
||||||
|
url.port = `${PORT() === 80 || PORT() == 443 ? '' : PORT()}`
|
||||||
|
info(`Listening for requests on ${url}`)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url = new URL(`${protocol}://hello.${APEX_DOMAIN()}`)
|
||||||
|
url.port = `${PORT() === 80 || PORT() == 443 ? '' : PORT()}`
|
||||||
|
info(`Try visiting ${url}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** When a request comes in, return an instance based on subdomain */
|
||||||
|
onGetInstanceByRequestInfoFilter(async (instance, context) => {
|
||||||
|
const { subdomain } = context
|
||||||
|
const meta = await getOrCreateMeta(subdomain)
|
||||||
|
return { ...instance, ...meta }
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a new instance model is instantiated, this filter gives listeners a
|
||||||
|
* chance to augment or update the instance data.
|
||||||
|
*
|
||||||
|
* In this case, the instance data is restored from a local db.
|
||||||
|
*/
|
||||||
|
onNewInstanceRecordFilter(async (instance) => {
|
||||||
|
const { subdomain } = instance
|
||||||
|
const path = metaPath(subdomain)
|
||||||
|
if (!existsSync(path)) return instance
|
||||||
|
const meta = await readMeta(path)
|
||||||
|
return { ...instance, ...meta }
|
||||||
|
})
|
||||||
|
|
||||||
|
/** After an instance has been found, store it to the db */
|
||||||
|
onAfterInstanceFoundAction(async (context) => {
|
||||||
|
const { instance } = context
|
||||||
|
await writeMeta(instance)
|
||||||
|
})
|
||||||
|
|
||||||
|
const instances: { [_: string]: Promise<string> } = {}
|
||||||
|
|
||||||
|
const launchMutex = new Mutex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workhorse. This filter is responsible for launching PocketBase and
|
||||||
|
* returning an endpoint URL.
|
||||||
|
*/
|
||||||
|
onGetOrProvisionInstanceUrlFilter(async (url: string, { instance }) => {
|
||||||
|
const { dev, subdomain, version, secrets } = instance
|
||||||
|
|
||||||
|
if (subdomain in instances) return instances[subdomain]!
|
||||||
|
|
||||||
|
dbg({ instance })
|
||||||
|
return (instances[subdomain] = new Promise<string>(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
const bot = await gobot(`pocketbase`, { version })
|
||||||
|
const realVersion = await bot.maxSatisfyingVersion(version)
|
||||||
|
if (!realVersion) {
|
||||||
|
throw new Error(`No PocketBase version satisfying ${version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceConfig = await doInstanceConfigFilter({
|
||||||
|
env: {},
|
||||||
|
binds: {
|
||||||
|
data: [],
|
||||||
|
hooks: [],
|
||||||
|
migrations: [],
|
||||||
|
public: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
dbg(`instanceConfig`, { instanceConfig })
|
||||||
|
|
||||||
|
const dataDir = INSTANCE_DATA_DIR(subdomain, `pb_data`)
|
||||||
|
const hooksDir = INSTANCE_DATA_DIR(subdomain, `pb_hooks`)
|
||||||
|
const migrationsDir = INSTANCE_DATA_DIR(subdomain, `pb_migrations`)
|
||||||
|
const publicDir = INSTANCE_DATA_DIR(subdomain, `pb_public`)
|
||||||
|
;[dataDir, hooksDir, migrationsDir, publicDir].forEach((dir) => {
|
||||||
|
return mkdirSync(dir, { recursive: true })
|
||||||
|
})
|
||||||
|
const { binds, env } = instanceConfig
|
||||||
|
copyFiles(binds.data, dataDir)
|
||||||
|
copyFiles(binds.hooks, hooksDir)
|
||||||
|
copyFiles(binds.migrations, migrationsDir)
|
||||||
|
copyFiles(binds.public, publicDir)
|
||||||
|
|
||||||
|
return launchMutex.runExclusive(async () => {
|
||||||
|
dbg(`got lock`)
|
||||||
|
const port = await getPort()
|
||||||
|
const args = [
|
||||||
|
`serve`,
|
||||||
|
`--dir`,
|
||||||
|
escape(dataDir),
|
||||||
|
`--hooksDir`,
|
||||||
|
escape(hooksDir),
|
||||||
|
`--migrationsDir`,
|
||||||
|
escape(migrationsDir),
|
||||||
|
`--publicDir`,
|
||||||
|
escape(publicDir),
|
||||||
|
`--http`,
|
||||||
|
`0.0.0.0:${port}`,
|
||||||
|
]
|
||||||
|
if (dev && gte(realVersion, `0.20.1`)) args.push(`--dev`)
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: `Launching: ${await bot.getBinaryFilePath()} ${args.join(
|
||||||
|
' ',
|
||||||
|
)}`,
|
||||||
|
})
|
||||||
|
bot.run(args, { env: { ...secrets, ...env } }, (proc) => {
|
||||||
|
proc.stdout.on('data', (data) => {
|
||||||
|
data
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.split(`\n`)
|
||||||
|
.forEach((line: string) => {
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: line,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
proc.stderr.on('data', (data) => {
|
||||||
|
data
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.split(`\n`)
|
||||||
|
.forEach((line: string) => {
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stderr',
|
||||||
|
data: line,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsub = exitHook(() => {
|
||||||
|
dbg(`killing ${subdomain}`)
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: `Forcibly killing PocketBase process`,
|
||||||
|
})
|
||||||
|
proc.kill()
|
||||||
|
})
|
||||||
|
proc.on('exit', (code) => {
|
||||||
|
unsub()
|
||||||
|
delete instances[subdomain]
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: `PocketBase process exited with code ${code}`,
|
||||||
|
})
|
||||||
|
doAfterInstanceStoppedAction({ instance, url })
|
||||||
|
dbg(`${subdomain} process exited with code ${code}`)
|
||||||
|
})
|
||||||
|
const url = `http://localhost:${port}`
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: `Waiting for PocketBase to start on ${url}`,
|
||||||
|
})
|
||||||
|
tryFetch(url)
|
||||||
|
.then(() => {
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stdout',
|
||||||
|
data: `PocketBase started on ${url}`,
|
||||||
|
})
|
||||||
|
doAfterInstanceStartedAction({ instance, url })
|
||||||
|
return resolve(url)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
doInstanceLogAction({
|
||||||
|
instance,
|
||||||
|
type: 'stderr',
|
||||||
|
data: `PocketBase failed to start on ${url}: ${e}`,
|
||||||
|
})
|
||||||
|
reject(e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user