diff --git a/packages/serve-instrument/package.json b/packages/serve-instrument/package.json index 231726d2b..98506300a 100644 --- a/packages/serve-instrument/package.json +++ b/packages/serve-instrument/package.json @@ -20,6 +20,7 @@ "esbuild": "catalog:" }, "devDependencies": { + "@douglasneuroinformatics/libjs": "catalog:", "@douglasneuroinformatics/libui": "catalog:", "@opendatacapture/instrument-bundler": "workspace:", "@opendatacapture/react-core": "workspace:*", diff --git a/packages/serve-instrument/src/cli.ts b/packages/serve-instrument/src/cli.ts index e4d0cf288..3875323b0 100755 --- a/packages/serve-instrument/src/cli.ts +++ b/packages/serve-instrument/src/cli.ts @@ -33,9 +33,10 @@ program .allowExcessArguments(false) .argument('', 'the directory containing the instrument', parseTarget) .option('-p --port ', 'the port to run the dev server on', parsePort, 3000) + .option('-v, --verbose', 'enable verbose logging (includes request logs and build timing)') .action(async (target: string) => { - const { port } = program.opts<{ port: number }>(); - const server = await Server.create({ port, target }); + const { port, verbose } = program.opts<{ port: number; verbose: boolean }>(); + const server = await Server.create({ port, target, verbose: verbose ?? false }); await server.start(); }); diff --git a/packages/serve-instrument/src/server.tsx b/packages/serve-instrument/src/server.tsx index 58e6d64fa..18abb43b3 100644 --- a/packages/serve-instrument/src/server.tsx +++ b/packages/serve-instrument/src/server.tsx @@ -6,12 +6,8 @@ import * as fs from 'node:fs'; import * as http from 'node:http'; import * as path from 'node:path'; -import { - bundle, - BUNDLER_FILE_EXT_REGEX, - inferLoader, - InstrumentBundlerError -} from '@opendatacapture/instrument-bundler'; +import { formatError } from '@douglasneuroinformatics/libjs'; +import { bundle, BUNDLER_FILE_EXT_REGEX, inferLoader } from '@opendatacapture/instrument-bundler'; import type { BundlerInput } from '@opendatacapture/instrument-bundler'; import { encodeUnicodeToBase64 } from '@opendatacapture/runtime-internal'; import { generateMetadata, resolveRuntimeAsset } from '@opendatacapture/runtime-meta'; @@ -24,13 +20,28 @@ import type { RootProps } from './root'; declare const __TAILWIND_STYLES__: string; +function timestamp(): string { + return chalk.dim(new Date().toLocaleTimeString('en', { hour12: false })); +} + +function log(message: string): void { + console.log(`${timestamp()} ${message}`); +} + +function logError(message: string): void { + console.error(`${timestamp()} ${message}`); +} + class InstrumentLoader { private encodedBundle: null | string; - constructor(private readonly target: string) { + constructor( + private readonly target: string, + private readonly verbose: boolean + ) { this.encodedBundle = null; fs.watch(target, { recursive: false }, () => { - console.log(chalk.yellow('↺') + chalk.dim(' File changed, rebuilding instrument...')); + log(chalk.yellow('↺') + chalk.dim(' File changed, rebuilding...')); void this.updateEncodedBundle(); }); } @@ -58,14 +69,16 @@ class InstrumentLoader { name: path.basename(filepath) }); } + const start = Date.now(); try { this.encodedBundle = encodeUnicodeToBase64(await bundle({ inputs })); - console.log(chalk.green('✓') + chalk.dim(' Bundle ready')); + const elapsed = Date.now() - start; + const timing = this.verbose ? chalk.dim(` (${elapsed}ms)`) : ''; + log(chalk.green('✓') + chalk.dim(' Bundle ready') + timing); } catch (err) { - if (!(err instanceof InstrumentBundlerError)) { - console.error(err); - } - console.error(chalk.red('✘ Failed to compile instrument')); + const formattedError = err instanceof Error ? formatError(err) : err; + logError(chalk.dim(String(formattedError)) + '\n'); + logError(chalk.red('✘') + chalk.bold.red(' Error: Failed to compile instrument')); } } } @@ -73,20 +86,19 @@ class InstrumentLoader { class RequestHandler { private instrumentLoader: InstrumentLoader; private runtimeMetadata: RuntimeMetadataMap; + private verbose: boolean; - constructor(params: { instrumentLoader: InstrumentLoader; runtimeMetadata: RuntimeMetadataMap }) { + constructor(params: { instrumentLoader: InstrumentLoader; runtimeMetadata: RuntimeMetadataMap; verbose: boolean }) { this.instrumentLoader = params.instrumentLoader; this.runtimeMetadata = params.runtimeMetadata; + this.verbose = params.verbose; } async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise { if (req.method !== 'GET') { res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('Method Not Allowed'); - return; - } - - if (req.url === '/') { + } else if (req.url === '/') { const encodedBundle = await this.instrumentLoader.getEncodedBundle(); if (encodedBundle) { res.writeHead(200, { 'Content-Type': 'text/html' }); @@ -145,11 +157,12 @@ export class Server { this.server = http.createServer((...args) => void this.handler.handle(...args)); } - static async create({ port, target }: { port: number; target: string }) { + static async create({ port, target, verbose }: { port: number; target: string; verbose: boolean }) { return new this({ handler: new RequestHandler({ - instrumentLoader: new InstrumentLoader(target), - runtimeMetadata: await generateMetadata({ rootDir: import.meta.dirname }) + instrumentLoader: new InstrumentLoader(target, verbose), + runtimeMetadata: await generateMetadata({ rootDir: import.meta.dirname }), + verbose }), port }); @@ -158,7 +171,14 @@ export class Server { async start(): Promise { return new Promise((resolve) => { this.server.listen(this.port, () => { - console.log(chalk.green('✓') + ' Listening on ' + chalk.cyan.underline(`http://localhost:${this.port}`)); + log( + chalk.green('✓') + + chalk.bold(' Ready') + + ' ' + + chalk.dim('➜') + + ' ' + + chalk.cyan.underline(`http://localhost:${this.port}`) + ); resolve(); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56341f1c5..5ea9d9d0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ catalogs: specifier: ^0.0.4 version: 0.0.4 '@douglasneuroinformatics/libjs': - specifier: ^3.1.1 - version: 3.1.1 + specifier: ^3.2.0 + version: 3.2.0 '@douglasneuroinformatics/libpasswd': specifier: latest version: 0.0.3 @@ -208,7 +208,7 @@ importers: version: 0.0.4 '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@douglasneuroinformatics/libnest': specifier: ^8.2.0 version: 8.2.1(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-fastify@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14))(@nestjs/testing@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14))(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.6.3))(typescript@5.6.3))(@swc/types@0.1.25)(fastify@5.7.4)(neverthrow@8.2.0)(reflect-metadata@0.1.14)(rollup@4.58.0)(rxjs@7.8.2)(typescript@5.6.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)(zod@vendor+zod@3.x) @@ -517,7 +517,7 @@ importers: version: 0.0.4 '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@douglasneuroinformatics/libui': specifier: 'catalog:' version: 6.1.2(immer@10.2.0)(neverthrow@8.2.0)(react-dom@vendor+react-dom@19.x)(react@vendor+react@19.x)(tailwindcss@4.2.0)(use-sync-external-store@1.6.0(react@vendor+react@19.x))(zod@vendor+zod@3.x) @@ -641,7 +641,7 @@ importers: version: 6.8.0 '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@douglasneuroinformatics/libpasswd': specifier: 'catalog:' version: 0.0.3(typescript@5.6.3) @@ -804,7 +804,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@4.3.6) + version: 3.2.0(neverthrow@8.2.0)(zod@4.3.6) '@opendatacapture/schemas': specifier: workspace:* version: link:../schemas @@ -888,7 +888,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@opendatacapture/runtime-core': specifier: workspace:* version: link:../runtime-core @@ -900,7 +900,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@4.3.6) + version: 3.2.0(neverthrow@8.2.0)(zod@4.3.6) '@opendatacapture/runtime-core': specifier: workspace:* version: link:../runtime-core @@ -920,7 +920,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@douglasneuroinformatics/libui': specifier: 'catalog:' version: 6.1.2(immer@10.2.0)(neverthrow@8.2.0)(react-dom@vendor+react-dom@19.x)(react@vendor+react@19.x)(tailwindcss@4.2.0)(use-sync-external-store@1.6.0(react@vendor+react@19.x))(zod@vendor+zod@3.x) @@ -1017,7 +1017,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) esbuild: specifier: 'catalog:' version: 0.23.1 @@ -1061,7 +1061,7 @@ importers: version: 6.8.0 '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@opendatacapture/licenses': specifier: workspace:* version: link:../licenses @@ -1097,6 +1097,9 @@ importers: specifier: 'catalog:' version: 0.23.1 devDependencies: + '@douglasneuroinformatics/libjs': + specifier: 'catalog:' + version: 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@douglasneuroinformatics/libui': specifier: 'catalog:' version: 6.1.2(immer@10.2.0)(neverthrow@8.2.0)(react-dom@vendor+react-dom@19.x)(react@vendor+react@19.x)(tailwindcss@4.2.0)(use-sync-external-store@1.6.0(react@vendor+react@19.x))(zod@vendor+zod@3.x) @@ -1161,7 +1164,7 @@ importers: devDependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@4.3.6) + version: 3.2.0(neverthrow@8.2.0)(zod@4.3.6) runtime/v1: devDependencies: @@ -1357,7 +1360,7 @@ importers: version: 6.8.0 '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@4.3.6) + version: 3.2.0(neverthrow@8.2.0)(zod@4.3.6) '@faker-js/faker': specifier: ^9.4.0 version: 9.9.0 @@ -1376,7 +1379,7 @@ importers: dependencies: '@douglasneuroinformatics/libjs': specifier: 'catalog:' - version: 3.1.1(neverthrow@8.2.0)(zod@4.3.6) + version: 3.2.0(neverthrow@8.2.0)(zod@4.3.6) '@opendatacapture/schemas': specifier: workspace:* version: link:../../packages/schemas @@ -1954,6 +1957,13 @@ packages: neverthrow: ^8.2.0 zod: ^3.25.67 + '@douglasneuroinformatics/libjs@3.2.0': + resolution: + { integrity: sha512-hHOqDvKWjhRovpGobEag/BT9xaQNyjDBHbxvwOadlepMbnpyk+RCbcGW9OSRidfvs96wq7KSg8tGpzXyYn2J6Q== } + peerDependencies: + neverthrow: ^8.2.0 + zod: ^3.25.67 + '@douglasneuroinformatics/libnest@8.2.1': resolution: { integrity: sha512-Mxhv7Fyxhe4qJDUNFUh6m59cchuLhcXtRnykH/3Dv0PcAm6p9siRjbFT28rKUAGk4g1NVgJal5XT4An4Mi/90Q== } @@ -13679,7 +13689,17 @@ snapshots: dependencies: '@hpke/core': 1.7.5 - '@douglasneuroinformatics/libjs@3.1.1(neverthrow@8.2.0)(zod@4.3.6)': + '@douglasneuroinformatics/libjs@3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x)': + dependencies: + clean-stack: 5.2.0 + extract-stack: 3.0.0 + neverthrow: 8.2.0 + serialize-error: 12.0.0 + stringify-object: 5.0.0 + type-fest: 4.41.0 + zod: link:vendor/zod@3.x + + '@douglasneuroinformatics/libjs@3.2.0(neverthrow@8.2.0)(zod@4.3.6)': dependencies: clean-stack: 5.2.0 extract-stack: 3.0.0 @@ -13689,7 +13709,7 @@ snapshots: type-fest: 4.41.0 zod: 4.3.6 - '@douglasneuroinformatics/libjs@3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x)': + '@douglasneuroinformatics/libjs@3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x)': dependencies: clean-stack: 5.2.0 extract-stack: 3.0.0 @@ -13701,7 +13721,7 @@ snapshots: '@douglasneuroinformatics/libnest@8.2.1(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-fastify@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14))(@nestjs/testing@11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14))(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.6.3))(typescript@5.6.3))(@swc/types@0.1.25)(fastify@5.7.4)(neverthrow@8.2.0)(reflect-metadata@0.1.14)(rollup@4.58.0)(rxjs@7.8.2)(typescript@5.6.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4)(zod@vendor+zod@3.x)': dependencies: - '@douglasneuroinformatics/libjs': 3.1.1(neverthrow@8.2.0)(zod@vendor+zod@3.x) + '@douglasneuroinformatics/libjs': 3.2.0(neverthrow@8.2.0)(zod@vendor+zod@3.x) '@nestjs/common': 11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-fastify': 11.1.14(@nestjs/common@11.1.14(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.14) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f9132c96c..7c3e1be3f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,7 +4,7 @@ catalog: '@casl/prisma': '^1.4.1' '@douglasneuroinformatics/esbuild-plugin-prisma': 'latest' '@douglasneuroinformatics/libcrypto': '^0.0.4' - '@douglasneuroinformatics/libjs': ^3.1.1 + '@douglasneuroinformatics/libjs': ^3.2.0 '@douglasneuroinformatics/libpasswd': 'latest' '@douglasneuroinformatics/libstats': 'latest' '@douglasneuroinformatics/libui': ^6.1.2