From 33899c67e3923064eb9302becfb9fbc8d1d20ec8 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sat, 12 Sep 2026 10:54:15 -0400 Subject: [PATCH] fix(ci): install+build deps for file-processing/media-processing, wire jest transform for feature-flags sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nself-ci's node gate surfaced three more pre-existing reds beyond PR #93's scope (same root cause class: nothing installs a nested package's own dependencies before typecheck/test/build runs): - free/file-processing/ts and free/media-processing/ts both had every declared dependency (commander, fastify, @types/node, @nself/plugin-utils, etc.) present in package.json but never installed. Added the same `pnpm install --frozen-lockfile --dir ` pattern PR #93 introduced. Their @nself/plugin-utils dependency is a file: path to shared/, whose dist/ output pnpm never builds on link — added a build step for shared ahead of the gate so the type/JS output exists. - Installing file-processing surfaced a genuine bug once tsc could actually resolve @nself/plugin-utils: src/server.ts imports createMetrics, which was never implemented in the shared package. Added a minimal dependency-free Prometheus-text-format counter (shared/src/metrics.ts) matching the incrementRequest/incrementError/format surface the plugin already calls. - free/feature-flags/sdk-ts had ts-jest installed but no jest config at all, so jest ran with zero TypeScript transform. Added jest.config.js (ts-jest preset + a moduleNameMapper for the NodeNext-style `.js` import extensions). That surfaced two more real gaps: the tsconfig's Node16/NodeNext module kind needs isolatedModules for ts-jest's per-file transpile, and the test file's `global.fetch` mock needed @types/node (never a declared dependency here). scripts/nself-ci.sh --check --no-gitleaks -v . now reports PASS for every node:typecheck, node:test, and node:build entry across all 5 discovered workspace members. --- .github/workflows/nself-ci.yml | 12 +++++ free/feature-flags/sdk-ts/jest.config.js | 24 +++++++++ free/feature-flags/sdk-ts/package.json | 1 + free/feature-flags/sdk-ts/pnpm-lock.yaml | 69 ++++++++++++------------ free/feature-flags/sdk-ts/tsconfig.json | 3 +- shared/src/index.ts | 2 + shared/src/metrics.ts | 46 ++++++++++++++++ 7 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 free/feature-flags/sdk-ts/jest.config.js create mode 100644 shared/src/metrics.ts diff --git a/.github/workflows/nself-ci.yml b/.github/workflows/nself-ci.yml index 3965eae1..0e354d17 100644 --- a/.github/workflows/nself-ci.yml +++ b/.github/workflows/nself-ci.yml @@ -57,6 +57,18 @@ jobs: run: | pnpm install --frozen-lockfile --dir .workers/plugins-registry pnpm install --frozen-lockfile --dir free/feature-flags/sdk-ts + pnpm install --frozen-lockfile --dir shared + pnpm install --frozen-lockfile --dir free/file-processing/ts + pnpm install --frozen-lockfile --dir free/media-processing/ts + + # free/file-processing/ts and free/media-processing/ts both depend on + # @nself/plugin-utils via a "file:../../../shared" path dependency. + # pnpm links the directory but never runs its build script, so + # shared/dist (what "@nself/plugin-utils"'s main/types fields point + # at) does not exist yet — build it before the two plugins are + # typechecked/built against it. + - name: Build shared plugin-utils package + run: pnpm --dir shared run build - name: Run nself ci gate env: diff --git a/free/feature-flags/sdk-ts/jest.config.js b/free/feature-flags/sdk-ts/jest.config.js new file mode 100644 index 00000000..a763e031 --- /dev/null +++ b/free/feature-flags/sdk-ts/jest.config.js @@ -0,0 +1,24 @@ +/** + * Jest config for @nself/feature-flags-client. + * + * The package has no jest.config previously — jest ran with zero + * TypeScript transform and every test failed at the `import` statement. + * ts-jest was already an installed devDependency but never wired up. + * + * Source uses NodeNext module resolution and imports its own sibling + * modules with an explicit `.js` extension (e.g. `./index.js`) even though + * the file on disk is `index.ts` — the standard NodeNext-ESM-style + * convention. Jest's CommonJS resolver does not do that rewrite on its + * own, so moduleNameMapper strips the `.js` suffix back off before + * resolution, letting ts-jest's transform pick up the matching `.ts` + * file. + */ + +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, +}; diff --git a/free/feature-flags/sdk-ts/package.json b/free/feature-flags/sdk-ts/package.json index 7f1febf0..e9781133 100644 --- a/free/feature-flags/sdk-ts/package.json +++ b/free/feature-flags/sdk-ts/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@types/jest": "^29.0.0", + "@types/node": "^20.10.0", "@types/react": "^19.0.0", "jest": "^29.0.0", "ts-jest": "^29.0.0", diff --git a/free/feature-flags/sdk-ts/pnpm-lock.yaml b/free/feature-flags/sdk-ts/pnpm-lock.yaml index 6e152640..9b9792c4 100644 --- a/free/feature-flags/sdk-ts/pnpm-lock.yaml +++ b/free/feature-flags/sdk-ts/pnpm-lock.yaml @@ -15,15 +15,18 @@ importers: '@types/jest': specifier: ^29.0.0 version: 29.5.14 + '@types/node': + specifier: ^20.10.0 + version: 20.19.43 '@types/react': specifier: ^19.0.0 version: 19.3.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@22.20.2) + version: 29.7.0(@types/node@20.19.43) ts-jest: specifier: ^29.0.0 - version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.2))(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -321,8 +324,8 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - '@types/node@22.20.2': - resolution: {integrity: sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==} + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} @@ -1397,7 +1400,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1410,14 +1413,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.20.2) + jest-config: 29.7.0(@types/node@20.19.43) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1442,7 +1445,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1460,7 +1463,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.20.2 + '@types/node': 20.19.43 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1482,7 +1485,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -1552,7 +1555,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.20.2 + '@types/node': 20.19.43 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -1608,7 +1611,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.20.2 + '@types/node': 20.19.43 '@types/istanbul-lib-coverage@2.0.6': {} @@ -1625,7 +1628,7 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@22.20.2': + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -1787,13 +1790,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.20.2): + create-jest@29.7.0(@types/node@20.19.43): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.20.2) + jest-config: 29.7.0(@types/node@20.19.43) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2005,7 +2008,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -2025,16 +2028,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.20.2): + jest-cli@29.7.0(@types/node@20.19.43): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.20.2) + create-jest: 29.7.0(@types/node@20.19.43) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.20.2) + jest-config: 29.7.0(@types/node@20.19.43) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.3 @@ -2044,7 +2047,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.20.2): + jest-config@29.7.0(@types/node@20.19.43): dependencies: '@babel/core': 7.29.7 '@jest/test-sequencer': 29.7.0 @@ -2069,7 +2072,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.20.2 + '@types/node': 20.19.43 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2098,7 +2101,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2108,7 +2111,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.20.2 + '@types/node': 20.19.43 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2147,7 +2150,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2182,7 +2185,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2210,7 +2213,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -2256,7 +2259,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -2275,7 +2278,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.2 + '@types/node': 20.19.43 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -2284,17 +2287,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.20.2 + '@types/node': 20.19.43 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.20.2): + jest@29.7.0(@types/node@20.19.43): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.20.2) + jest-cli: 29.7.0(@types/node@20.19.43) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -2524,12 +2527,12 @@ snapshots: dependencies: is-number: 7.0.0 - ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.2))(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 29.7.0(@types/node@22.20.2) + jest: 29.7.0(@types/node@20.19.43) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 diff --git a/free/feature-flags/sdk-ts/tsconfig.json b/free/feature-flags/sdk-ts/tsconfig.json index 2cd9a2ff..218ef894 100644 --- a/free/feature-flags/sdk-ts/tsconfig.json +++ b/free/feature-flags/sdk-ts/tsconfig.json @@ -10,7 +10,8 @@ "sourceMap": true, "strict": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "isolatedModules": true }, "include": ["src/**/*.ts"], "exclude": ["src/**/*.test.ts", "dist"] diff --git a/shared/src/index.ts b/shared/src/index.ts index 90c61ffa..44e82aab 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -11,6 +11,7 @@ export * from './http.js'; export * from './validation.js'; export * from './security.js'; export * from './app-context.js'; +export * from './metrics.js'; // Re-export commonly used items at top level export { createLogger, Logger } from './logger.js'; @@ -51,3 +52,4 @@ export { parseCsvList, buildAccountConfigs, } from './app-context.js'; +export { createMetrics, type PluginMetrics } from './metrics.js'; diff --git a/shared/src/metrics.ts b/shared/src/metrics.ts new file mode 100644 index 00000000..cfa8eb11 --- /dev/null +++ b/shared/src/metrics.ts @@ -0,0 +1,46 @@ +/** + * Minimal Prometheus-text-format request/error counters for nself plugins. + * + * createMetrics(name) returns a per-plugin counter pair exposed at a + * plugin's `/metrics` route (see free/file-processing/ts/src/server.ts and + * free/media-processing/ts/src/server.ts, which both hook Fastify's + * onRequest/onError and serve `metrics.format()` as `text/plain; + * version=0.0.4`). Deliberately dependency-free (no prom-client) since a + * plugin only needs two monotonic counters, not full histogram/summary + * support. + */ + +export interface PluginMetrics { + /** Increment the request counter. Call once per inbound request. */ + incrementRequest: () => void; + /** Increment the error counter. Call once per request that errors. */ + incrementError: () => void; + /** Render both counters in Prometheus text exposition format. */ + format: () => string; +} + +export function createMetrics(name: string): PluginMetrics { + const metricName = name.replace(/[^a-zA-Z0-9_]/g, '_'); + let requestCount = 0; + let errorCount = 0; + + return { + incrementRequest(): void { + requestCount += 1; + }, + incrementError(): void { + errorCount += 1; + }, + format(): string { + return [ + `# HELP ${metricName}_requests_total Total number of requests handled.`, + `# TYPE ${metricName}_requests_total counter`, + `${metricName}_requests_total ${requestCount}`, + `# HELP ${metricName}_errors_total Total number of requests that errored.`, + `# TYPE ${metricName}_errors_total counter`, + `${metricName}_errors_total ${errorCount}`, + '', + ].join('\n'); + }, + }; +}