Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@ coverage
*.tsbuildinfo
.eslintcache

# Added by coconfig
.eslintignore
.npmignore
tsconfig.json
tsconfig.build.json
jest.config.js
.commitlintrc.json
vitest.config.ts
.eslintrc.cjs
.prettierrc.cjs
# Managed by cpconfig
/.commitlintrc.yaml
/eslint.config.mts
/tsconfig.json
/tsconfig.build.json
/.prettierrc.yaml
/vitest.config.ts
3 changes: 2 additions & 1 deletion __tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { fileURLToPath } from 'url';

import { describe, expect, test } from 'vitest';

import { ConfigurationSchema, insertConfigurationBefore, loadConfiguration } from '../src/config/index.js';
import type { ConfigurationSchema} from '../src/config/index.js';
import { insertConfigurationBefore, loadConfiguration } from '../src/config/index.js';
import { shortstops } from '../src/config/shortstops.js';

interface CustomConfig extends ConfigurationSchema {
Expand Down
4 changes: 3 additions & 1 deletion __tests__/fake-serv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { fileURLToPath } from 'url';
import { describe, expect, test } from 'vitest';
import request from 'supertest';

import type {
ServiceStartOptions} from '../src/index.js';
import {
listen,
ServiceStartOptions,
startApp,
} from '../src/index.js';

Expand Down Expand Up @@ -45,6 +46,7 @@ describe('fake-serv', () => {
};

const app = await startApp(options).catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
throw error;
});
Expand Down
5 changes: 3 additions & 2 deletions __tests__/fake-serv/src/handlers/hello.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { repl$, ServiceHandler } from '../../../../src/index';
import { FakeServLocals } from '../index';
import type { ServiceHandler } from '../../../../src/index';
import { repl$ } from '../../../../src/index';
import type { FakeServLocals } from '../index';

export const get: ServiceHandler<FakeServLocals> = async (req, res) => {
res.json({ greeting: req.query.greeting || 'Hello World' });
Expand Down
4 changes: 2 additions & 2 deletions __tests__/fake-serv/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useService } from '../../../src/index.js';
export interface FakeServLocals extends ServiceLocals {
services: {
fakeServ: {
get_something(): { things: string[] } | Error;
get_something(): Promise<{ things: string[] } | Error>;
};
};
}
Expand All @@ -17,7 +17,7 @@ export function service(): Service<FakeServLocals> {
await base.start(app);
app.locals.services = app.locals.services || {};
app.locals.services.fakeServ = {
get_something() {
async get_something() {
throw new Error('Should not be called.');
},
};
Expand Down
2 changes: 1 addition & 1 deletion __tests__/fake-serv/src/routes/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ServiceRouter } from '../../../../src/index';
import type { ServiceRouter } from '../../../../src/index';
import { ServiceError } from '../../../../src/error';

export function route(router: ServiceRouter) {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/fake-serv/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ServiceExpress, ServiceRouter } from '../../../../src/index';
import { FakeServLocals } from '../index';
import type { FakeServLocals } from '../index';

export function route(router: ServiceRouter<FakeServLocals>, app: ServiceExpress<FakeServLocals>) {
const worldRequests = app.locals.meter.createCounter('world_requests', {
Expand Down
61 changes: 31 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openapi-typescript-infra/service",
"version": "4.10.2",
"version": "0.0.0",
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version has been reset to '0.0.0', which suggests this may be a mistake or placeholder. Ensure this is updated to the correct version before publishing.

Suggested change
"version": "0.0.0",
"version": "0.1.0",

Copilot uses AI. Check for mistakes.
"description": "An opinionated framework for building configuration driven services - web, api, or ob. Uses OpenAPI, pino logging, express, confit, Typescript and vitest.",
"exports": {
".": {
Expand All @@ -10,7 +10,8 @@
"./telemetry": {
"import": "./build/telemetry/index.js",
"types": "./build/telemetry/index.d.ts"
}
},
"./Makefile": "./Makefile"
},
"type": "module",
"scripts": {
Expand All @@ -20,7 +21,7 @@
"watch": "tsc -p tsconfig.json -w --preserveWatchOutput",
"clean": "npx rimraf ./build",
"prepublishOnly": "yarn build",
"postinstall": "coconfig"
"postinstall": "cpconfig"
},
"repository": {
"type": "git",
Expand All @@ -30,7 +31,7 @@
"start-service": "./build/bin/start-service.js"
},
"config": {
"coconfig": "@openapi-typescript-infra/coconfig"
"cpconfig": "@openapi-typescript-infra/cpconfig"
},
"engines": {
"node": ">=22"
Expand Down Expand Up @@ -70,26 +71,26 @@
"dependencies": {
"@godaddy/terminus": "^4.12.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
"@opentelemetry/exporter-prometheus": "^0.207.0",
"@opentelemetry/instrumentation-dns": "^0.51.0",
"@opentelemetry/instrumentation-express": "^0.56.0",
"@opentelemetry/instrumentation-generic-pool": "^0.51.0",
"@opentelemetry/instrumentation-graphql": "^0.55.0",
"@opentelemetry/instrumentation-http": "^0.207.0",
"@opentelemetry/instrumentation-net": "^0.51.0",
"@opentelemetry/instrumentation-pg": "^0.60.0",
"@opentelemetry/instrumentation-pino": "^0.54.0",
"@opentelemetry/instrumentation-redis": "^0.56.0",
"@opentelemetry/instrumentation-undici": "^0.18.0",
"@opentelemetry/resource-detector-container": "^0.7.10",
"@opentelemetry/resource-detector-gcp": "^0.42.0",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@opentelemetry/auto-instrumentations-node": "^0.67.0",
"@opentelemetry/exporter-prometheus": "^0.208.0",
"@opentelemetry/instrumentation-dns": "^0.52.0",
"@opentelemetry/instrumentation-express": "^0.57.0",
"@opentelemetry/instrumentation-generic-pool": "^0.52.0",
"@opentelemetry/instrumentation-graphql": "^0.56.0",
"@opentelemetry/instrumentation-http": "^0.208.0",
"@opentelemetry/instrumentation-net": "^0.52.0",
"@opentelemetry/instrumentation-pg": "^0.61.0",
"@opentelemetry/instrumentation-pino": "^0.55.0",
"@opentelemetry/instrumentation-redis": "^0.57.0",
"@opentelemetry/instrumentation-undici": "^0.19.0",
"@opentelemetry/resource-detector-container": "^0.7.11",
"@opentelemetry/resource-detector-gcp": "^0.43.0",
"@opentelemetry/sdk-node": "^0.208.0",
"@opentelemetry/semantic-conventions": "^1.38.0",
"@sesamecare-oss/confit": "^2.2.1",
"@sesamecare-oss/opentelemetry-node-metrics": "^1.1.0",
"ajv": "^8.17.1",
"clean-stack": "^5.3.0",
"clean-stack": "^6.0.0",
"cookie-parser": "^1.4.7",
"dotenv": "^17.2.3",
"express": "^5.1.0",
Expand All @@ -100,27 +101,27 @@
"moderndash": "^4.0.0",
"opentelemetry-resource-detector-sync-api": "^0.30.0",
"pino": "^10.1.0",
"read-package-up": "^11.0.0",
"read-package-up": "^12.0.0",
"request-ip": "^3.3.0"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@openapi-typescript-infra/coconfig": "^4.7.1",
"@openapi-typescript-infra/cpconfig": "^1.1.0",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/github": "^12.0.1",
"@semantic-release/github": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.1.0",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.5",
"@types/minimist": "^1.2.5",
"@types/node": "^24.10.0",
"@types/node": "^24.10.1",
"@types/request-ip": "^0.0.41",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.46.3",
"@typescript-eslint/parser": "^8.46.3",
"coconfig": "^1.6.2",
"eslint": "^8.57.1",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"cpconfig": "^1.4.4",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
Expand All @@ -131,7 +132,7 @@
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"vitest": "^4.0.7"
"vitest": "^4.0.9"
},
"resolutions": {
"qs": "^6.11.0"
Expand Down
2 changes: 1 addition & 1 deletion src/bin/start-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if (argv.telemetry) {
}

const noTelemetry = (argv.repl || isDev()) && !argv.telemetry;
bootstrap({
void bootstrap({
...argv,
telemetry: !noTelemetry,
}).then(({ app, codepath, server }) => {
Expand Down
12 changes: 6 additions & 6 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from 'node:path';
import assert from 'node:assert';

import { config, DotenvConfigOptions } from 'dotenv';
import { config } from 'dotenv';
import { readPackageUp } from 'read-package-up';
import type { NormalizedPackageJson } from 'read-package-up';
import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';

import type {
AnyServiceLocals,
Expand All @@ -14,7 +14,7 @@ import type {
} from './types.js';
import { isDev } from './env.js';
import { startWithTelemetry } from './telemetry/index.js';
import { ConfigurationSchema } from './config/schema.js';
import type { ConfigurationSchema } from './config/schema.js';

interface BootstrapArguments {
// The name of the service, else discovered via read-package-up
Expand Down Expand Up @@ -102,7 +102,7 @@ export async function bootstrap<
entrypoint = './build/index.js';
}

config({ quiet: true } as DotenvConfigOptions);
config({ quiet: true });

const absoluteEntrypoint = path.resolve(rootDirectory, entrypoint);
if (argv?.telemetry) {
Expand All @@ -128,7 +128,7 @@ export async function bootstrap<
}

// This needs to be required for TS on-the-fly to work
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires

const impl = await import(absoluteEntrypoint);
const opts: ServiceStartOptions<SLocals, RLocals> = {
name,
Expand All @@ -137,7 +137,7 @@ export async function bootstrap<
service: impl.default || impl.service,
codepath,
};
// eslint-disable-next-line import/no-unresolved

const { startApp, listen } = await import('./express-app/app.js');
const app = await startApp<SLocals, RLocals>(opts);
const server = argv?.nobind ? undefined : await listen(app);
Expand Down
9 changes: 2 additions & 7 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

import {
BaseConfitSchema,
Confit,
Factory,
ShortstopHandler,
confit,
} from '@sesamecare-oss/confit';
import type { BaseConfitSchema, Confit, Factory, ShortstopHandler } from '@sesamecare-oss/confit';
import { confit } from '@sesamecare-oss/confit';

import { getAvailablePort } from '../development/port-finder.js';

Expand Down
2 changes: 1 addition & 1 deletion src/config/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BaseConfitSchema } from '@sesamecare-oss/confit';
import type { middleware } from 'express-openapi-validator';
import type { Level } from 'pino';
import bodyParser from 'body-parser';
import type bodyParser from 'body-parser';

export interface ConfigurationItemEnabled {
enabled?: boolean;
Expand Down
6 changes: 3 additions & 3 deletions src/config/shortstops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ export function shortstops(service: { name: string }, sourcedir: string) {
servicetype: serviceTypeFactory(service.name),
servicename: (v: string) => v.replace(/\$\{name\}/g, service.name),

os(p: keyof typeof osMethods) {
return osMethods[p]();
os(p: string) {
return osMethods[p as keyof typeof osMethods]();
},
// No-op in case you have values that start with a shortstop handler name (and colon)
literal(v: string) {
return v;
},
} as Record<string, ShortstopHandler<string, unknown>>;
} satisfies Record<string, ShortstopHandler<string, unknown>>;
}
2 changes: 1 addition & 1 deletion src/config/validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigurationSchema } from './schema.js';
import type { ConfigurationSchema } from './schema.js';

export interface ConfigValidationError {
path: string;
Expand Down
1 change: 0 additions & 1 deletion src/development/port-finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ async function isAvailable(port: number) {

async function findPort(start: number) {
for (let p = start; p < start + 1000; p += 1) {
// eslint-disable-next-line no-await-in-loop
if (await isAvailable(p)) {
return p;
}
Expand Down
24 changes: 14 additions & 10 deletions src/development/repl.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import repl, { REPLServer } from 'repl';
import fs from 'fs';
import path from 'path';
import type { REPLServer } from 'node:repl';
import repl from 'node:repl';
import fs from 'node:fs';
import path from 'node:path';

import { glob } from 'glob';
import { set } from 'moderndash';

import { AnyServiceLocals, ServiceExpress, ServiceLocals } from '../types.js';
import { ConfigurationSchema } from '../config/schema.js';
import type { AnyServiceLocals, ServiceExpress, ServiceLocals } from '../types.js';
import type { ConfigurationSchema } from '../config/schema.js';

const REPL_PROP = '$$repl$$';

Expand Down Expand Up @@ -37,7 +38,6 @@ export function serviceRepl<SLocals extends AnyServiceLocals = ServiceLocals<Con
app,
req: new FakeReq('/'),
dump(o: unknown) {
// eslint-disable-next-line no-console
console.log(JSON.stringify(o, null, '\t'));
},
// Use iTerm2's escape code to copy to clipboard
Expand All @@ -54,7 +54,10 @@ export function serviceRepl<SLocals extends AnyServiceLocals = ServiceLocals<Con
});
app.locals.service.attachRepl?.(app, rl);

loadReplFunctions(app, codepath, rl);
loadReplFunctions(app, codepath, rl).catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to load REPL functions', error);
});

rl.on('exit', onExit);
}
Expand All @@ -78,19 +81,20 @@ async function loadReplFunctions<
const module = await import(path.resolve(file));

// Look for functions with the REPL_PROP marker
Object.values(module).forEach((exported) => {
for (const exported of Object.values(module as Record<string, unknown>)) {
if (!exported) {
return;
continue;
}
if (typeof exported === 'function') {
const replName = (exported as WithReplProp)[REPL_PROP];
if (replName) {
set(rl.context, replName, exported.bind(null, app));
}
}
});
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(`Failed to load REPL functions from ${file}:`, err);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function getNodeEnv(): ValidEnv {
case 'staging':
case 'test':
return (process.env.APP_ENV || process.env.NODE_ENV) as ValidEnv;
case undefined:
default:
return 'development';
}
Expand Down
Loading
Loading