From 7b8248a486d43f5e59fe8261cf4731a18565a4d1 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sat, 29 Jun 2024 13:48:30 -0700 Subject: [PATCH] chore(multi): move to plugin.ts --- .../src/index.ts | 24 +- .../src/plugin.ts | 23 ++ packages/plugin-console-logger/src/index.ts | 57 +--- packages/plugin-console-logger/src/plugin.ts | 56 ++++ packages/plugin-launcher-spawn/src/index.ts | 285 +----------------- packages/plugin-launcher-spawn/src/plugin.ts | 283 +++++++++++++++++ 6 files changed, 365 insertions(+), 363 deletions(-) create mode 100644 packages/plugin-cloudflare-request-logger/src/plugin.ts create mode 100644 packages/plugin-console-logger/src/plugin.ts create mode 100644 packages/plugin-launcher-spawn/src/plugin.ts diff --git a/packages/plugin-cloudflare-request-logger/src/index.ts b/packages/plugin-cloudflare-request-logger/src/index.ts index 9498da52..9e1018b3 100644 --- a/packages/plugin-cloudflare-request-logger/src/index.ts +++ b/packages/plugin-cloudflare-request-logger/src/index.ts @@ -1,25 +1,3 @@ -import { info } from 'console' -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) || '' - const ip = (req.headers['x-forwarded-for'] as string) || '' - const method = req.method || '' - const sig = [ - method.padStart(10), - country.padStart(5), - ip.padEnd(45), - url.toString(), - ].join(' ') - info(`Incoming request ${sig}`) - res.locals.sig = sig - }) -} +import { plugin } from './plugin' export default plugin diff --git a/packages/plugin-cloudflare-request-logger/src/plugin.ts b/packages/plugin-cloudflare-request-logger/src/plugin.ts new file mode 100644 index 00000000..674277b6 --- /dev/null +++ b/packages/plugin-cloudflare-request-logger/src/plugin.ts @@ -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) || '' + const ip = (req.headers['x-forwarded-for'] as string) || '' + const method = req.method || '' + const sig = [ + method.padStart(10), + country.padStart(5), + ip.padEnd(45), + url.toString(), + ].join(' ') + info(`Incoming request ${sig}`) + res.locals.sig = sig + }) +} diff --git a/packages/plugin-console-logger/src/index.ts b/packages/plugin-console-logger/src/index.ts index 70d87fae..9e1018b3 100644 --- a/packages/plugin-console-logger/src/index.ts +++ b/packages/plugin-console-logger/src/index.ts @@ -1,58 +1,3 @@ -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 - -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(`<>`) - continue - } - finalArgs.push(arg) - } - LogLevelConsoleMap[levelIn](...finalArgs) - }) - - dbg(`initializing ${PLUGIN_NAME}`) -} +import { plugin } from './plugin' export default plugin diff --git a/packages/plugin-console-logger/src/plugin.ts b/packages/plugin-console-logger/src/plugin.ts new file mode 100644 index 00000000..2a71540b --- /dev/null +++ b/packages/plugin-console-logger/src/plugin.ts @@ -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(`<>`) + continue + } + finalArgs.push(arg) + } + LogLevelConsoleMap[levelIn](...finalArgs) + }) + + dbg(`initializing ${PLUGIN_NAME}`) +} diff --git a/packages/plugin-launcher-spawn/src/index.ts b/packages/plugin-launcher-spawn/src/index.ts index 3766f5be..9e1018b3 100644 --- a/packages/plugin-launcher-spawn/src/index.ts +++ b/packages/plugin-launcher-spawn/src/index.ts @@ -1,286 +1,3 @@ -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)) -} - -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 } = {} - - 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( - 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) - }) - }) - }) - }, - )) - }) -} +import { plugin } from './plugin' export default plugin diff --git a/packages/plugin-launcher-spawn/src/plugin.ts b/packages/plugin-launcher-spawn/src/plugin.ts new file mode 100644 index 00000000..2cfefb68 --- /dev/null +++ b/packages/plugin-launcher-spawn/src/plugin.ts @@ -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 } = {} + + 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( + 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) + }) + }) + }) + }, + )) + }) +}