From 02a275114ce0fabb513594eb5405723fb95bd7ab Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 22:09:49 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(kernel):=20=F0=9F=90=9B=20Harden=20env?= =?UTF-8?q?=20and=20dependency=20injection=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/dependency-injection.md | 10 + docs/reference/kernel.md | 13 +- package.json | 1 + src/Kernel.ts | 83 ++++++-- .../AutowireWarningFilter.ts | 41 ++++ .../DependencyInjection.ts | 16 +- .../dependency-injection/index.ts | 1 + tests/dependency-injection.test.mjs | 99 ++++++++- tests/kernel.test.mjs | 56 ++++- tests/runtime-compatibility.test.mjs | 196 ++++++++++++++++++ yarn.lock | 97 ++++++++- 11 files changed, 589 insertions(+), 24 deletions(-) create mode 100644 src/infrastructure/dependency-injection/AutowireWarningFilter.ts create mode 100644 tests/runtime-compatibility.test.mjs diff --git a/docs/reference/dependency-injection.md b/docs/reference/dependency-injection.md index 35af5b0..c767c29 100644 --- a/docs/reference/dependency-injection.md +++ b/docs/reference/dependency-injection.md @@ -125,3 +125,13 @@ await kernel.dependencyInjection({ This avoids local bridge contracts or hand-written aliases when applications inject contracts exported by this package. + +## Autowire Noise + +Container generation ignores type-only default exports and other files that +look like TypeScript declarations but have no runtime default export. Those +files are skipped without warning so generated logs stay focused on actionable +container issues. + +Warnings that remain during autowire should describe a concrete service, +dependency symbol or import that needs to be fixed. diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index 5d3c0ed..36fb2ef 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -107,7 +107,18 @@ kernel.environment.SERVICE_NAME; // string | undefined Required variables throw `KernelEnvironmentValidationError` when they are missing. `number` and `boolean` values are parsed after `.env` files are loaded. Boolean values accept `true`, `false`, `1`, `0`, `yes`, `no`, `on` and `off`. -Blank numeric values are rejected instead of being coerced to `0`. + +Blank values are handled before parsing: + +| Schema | Environment value | Result | +| ---------------------------------------- | ----------------- | -------------------------------------- | +| `{ type: 'number' }` | `FOO=` | `kernel.environment.FOO === undefined` | +| `{ type: 'number', defaultValue: 3000 }` | `FOO=` | `kernel.environment.FOO === 3000` | +| `{ type: 'number', required: true }` | `FOO=` | `KernelEnvironmentValidationError` | +| `{ type: 'number' }` | `FOO=abc` | `KernelEnvironmentValidationError` | + +Validation errors distinguish missing required variables, blank required +variables and invalid parsed values. `choices` restricts the allowed runtime values and narrows the TypeScript type when the schema is declared `as const`: diff --git a/package.json b/package.json index d2e467a..f7aae5d 100644 --- a/package.json +++ b/package.json @@ -278,6 +278,7 @@ "prettier": "^3.9.1", "reflect-metadata": "^0.2.2", "routing-controllers": "^0.11.3", + "ts-node": "^10.9.2", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitepress": "^1.6.4", diff --git a/src/Kernel.ts b/src/Kernel.ts index a6e451a..123a1b9 100644 --- a/src/Kernel.ts +++ b/src/Kernel.ts @@ -137,6 +137,18 @@ export class Kernel< } } + private static assertRequiredEnvironmentVariableIsNotBlank( + name: string, + value: string, + schema: KernelEnvironmentSchema, + ): void { + if (schema[name]?.required === true && value.trim() === '') { + throw new KernelEnvironmentValidationError( + `Blank required environment variable "${name}".`, + ); + } + } + private static assertEnvironmentVariableChoice( name: string, value: KernelEnvironmentValue, @@ -176,7 +188,7 @@ export class Kernel< } throw new KernelEnvironmentValidationError( - `Environment variable "${name}" must be a boolean.`, + `Environment variable "${name}" has invalid boolean value "${value}".`, ); } @@ -184,12 +196,6 @@ export class Kernel< name: string, value: string, ): number { - if (value.trim() === '') { - throw new KernelEnvironmentValidationError( - `Environment variable "${name}" must be a number.`, - ); - } - const parsedValue = Number(value); if (Number.isFinite(parsedValue)) { @@ -197,7 +203,7 @@ export class Kernel< } throw new KernelEnvironmentValidationError( - `Environment variable "${name}" must be a number.`, + `Environment variable "${name}" has invalid number value "${value}".`, ); } @@ -229,22 +235,71 @@ export class Kernel< return value; } + private static getEnvironmentVariableValue( + name: string, + definition: KernelEnvironmentSchema[string], + schema: KernelEnvironmentSchema, + ): string | undefined { + const value = process.env[name]; + + Kernel.assertRequiredEnvironmentVariable(name, value, schema); + + if (value !== undefined) { + Kernel.assertRequiredEnvironmentVariableIsNotBlank(name, value, schema); + } + + if (value !== undefined && value.trim() !== '') { + return value; + } + + return definition.defaultValue?.toString(); + } + + private static shouldUnsetBlankOptionalEnvironmentVariable( + name: string, + value: string | undefined, + valueOrDefault: string | undefined, + schema: KernelEnvironmentSchema, + ): boolean { + return ( + value !== undefined && + value.trim() === '' && + valueOrDefault === undefined && + schema[name]?.required !== true + ); + } + private static validateEnvironmentVariables< TSchema extends KernelEnvironmentSchema, >(schema: TSchema): KernelEnvironmentForSchema { - const environmentVariables: Record = {}; + const environmentVariables: Record< + string, + KernelEnvironmentValue | undefined + > = {}; for (const [name, definition] of Object.entries(schema)) { - const value = process.env[name] ?? definition.defaultValue?.toString(); - - Kernel.assertRequiredEnvironmentVariable(name, value, schema); + const value = process.env[name]; + const valueOrDefault = Kernel.getEnvironmentVariableValue( + name, + definition, + schema, + ); - if (value !== undefined) { + if (valueOrDefault !== undefined) { environmentVariables[name] = Kernel.parseEnvironmentVariable( name, - value, + valueOrDefault, schema, ); + } else if ( + Kernel.shouldUnsetBlankOptionalEnvironmentVariable( + name, + value, + valueOrDefault, + schema, + ) + ) { + environmentVariables[name] = undefined; } } diff --git a/src/infrastructure/dependency-injection/AutowireWarningFilter.ts b/src/infrastructure/dependency-injection/AutowireWarningFilter.ts new file mode 100644 index 0000000..1dcfa00 --- /dev/null +++ b/src/infrastructure/dependency-injection/AutowireWarningFilter.ts @@ -0,0 +1,41 @@ +import type { Logger } from 'node-dependency-injection'; + +export class AutowireWarningFilter implements Logger { + constructor(private readonly logger: Logger) {} + + private isIgnorableAutowireWarning(message: string): boolean { + return ( + message.startsWith( + 'Autowire: file has export default declaration but no runtime default export:', + ) || + message.startsWith( + 'Autowire: failed to create definition for undefined:', + ) || + (message.startsWith('Autowire: failed to create definition for ') && + message.includes( + "Cannot read properties of undefined (reading 'body')", + )) + ); + } + + public warn(message?: unknown, ...optionalParams: unknown[]): void { + if ( + typeof message === 'string' && + this.isIgnorableAutowireWarning(message) + ) { + this.logger.debug?.(`Ignored ${message}`, ...optionalParams); + + return; + } + + this.logger.warn(message, ...optionalParams); + } + + public info(message?: unknown, ...optionalParams: unknown[]): void { + this.logger.info?.(message, ...optionalParams); + } + + public debug(message?: unknown, ...optionalParams: unknown[]): void { + this.logger.debug?.(message, ...optionalParams); + } +} diff --git a/src/infrastructure/dependency-injection/DependencyInjection.ts b/src/infrastructure/dependency-injection/DependencyInjection.ts index 137f372..eb7320b 100644 --- a/src/infrastructure/dependency-injection/DependencyInjection.ts +++ b/src/infrastructure/dependency-injection/DependencyInjection.ts @@ -13,6 +13,8 @@ import type { DefinitionMetadata } from './DefinitionMetadata.js'; import type { DependencyInjectionOptions } from './DependencyInjectionOptions.js'; import type { DependencyOverride } from './DependencyOverride.js'; +import { AutowireWarningFilter } from './AutowireWarningFilter.js'; + export class DependencyInjection implements ServiceResolver { private static configuredInstance: DependencyInjection | undefined; private autowire: Autowire | undefined; @@ -295,6 +297,18 @@ export class DependencyInjection implements ServiceResolver { } } + private async processAutowire(): Promise { + const previousLogger = this.container.logger; + + this.container.logger = new AutowireWarningFilter(previousLogger); + + try { + await this.autowire?.process(); + } finally { + this.container.logger = previousLogger; + } + } + private registerParentAliases(): void { for (const [id, definition] of this.definitions.entries()) { if (definition._abstract === true || !definition._parent) { @@ -313,7 +327,7 @@ export class DependencyInjection implements ServiceResolver { this.options.servicesYamlPath, false, ); - await this.autowire.process(); + await this.processAutowire(); } else { this.loader = new YamlFileLoader(this.container); await this.loader.load(this.options.servicesYamlPath); diff --git a/src/infrastructure/dependency-injection/index.ts b/src/infrastructure/dependency-injection/index.ts index b8abe1e..70e4e13 100644 --- a/src/infrastructure/dependency-injection/index.ts +++ b/src/infrastructure/dependency-injection/index.ts @@ -1,3 +1,4 @@ +export * from './AutowireWarningFilter.js'; export * from './ClassDependencyOverride.js'; export * from './ContainerDefinition.js'; export * from './ContainerInternals.js'; diff --git a/tests/dependency-injection.test.mjs b/tests/dependency-injection.test.mjs index 8ae0368..7d81aef 100644 --- a/tests/dependency-injection.test.mjs +++ b/tests/dependency-injection.test.mjs @@ -7,7 +7,11 @@ import test from 'node:test'; import { Reference } from 'node-dependency-injection'; -import { DependencyInjection } from '../dist/infrastructure/dependency-injection/index.js'; +import { DomainEventPublisher } from '../dist/domain/index.js'; +import { + AutowireWarningFilter, + DependencyInjection, +} from '../dist/infrastructure/dependency-injection/index.js'; class ContractRepository {} @@ -21,11 +25,42 @@ class AliasRepository {} class ConcreteService {} +class MessageBus extends DomainEventPublisher { + async publish() {} +} + const serviceIdFor = (ClassDefinition) => Buffer.from( `${ClassDefinition.name}.ts__${ClassDefinition.name}__${ClassDefinition.name}`, ).toString('base64'); +test('filters ignorable autowire warnings and forwards actionable logs', () => { + const calls = []; + const logger = new AutowireWarningFilter({ + debug: (message) => calls.push(`debug:${message}`), + info: (message) => calls.push(`info:${message}`), + warn: (message) => calls.push(`warn:${message}`), + }); + + logger.warn( + 'Autowire: file has export default declaration but no runtime default export: Contract.ts', + ); + logger.warn( + "Autowire: failed to create definition for HttpClient: Cannot read properties of undefined (reading 'body')", + ); + logger.warn('Autowire: could not resolve dependency "Repository"'); + logger.info('Autowire scan completed'); + logger.debug('Autowire debug message'); + + assert.deepEqual(calls, [ + 'debug:Ignored Autowire: file has export default declaration but no runtime default export: Contract.ts', + "debug:Ignored Autowire: failed to create definition for HttpClient: Cannot read properties of undefined (reading 'body')", + 'warn:Autowire: could not resolve dependency "Repository"', + 'info:Autowire scan completed', + 'debug:Autowire debug message', + ]); +}); + test('resolves a concrete class registered in the container', async () => { const dependencyInjection = new DependencyInjection(); const serviceId = serviceIdFor(ConcreteService); @@ -298,6 +333,36 @@ test('overrides unresolved argument references generated for external package im ); }); +test('overrides unresolved argument references generated for kernel subpath imports', async () => { + class ServiceThatNeedsDomainEventPublisher {} + + const dependencyInjection = new DependencyInjection({ + containerBuild: true, + overrides: [ + { + token: DomainEventPublisher, + useClass: MessageBus, + }, + ], + servicesYamlPath: '/tmp/services.yaml', + sourceDirectory: process.cwd(), + }); + const serviceId = serviceIdFor(ServiceThatNeedsDomainEventPublisher); + const externalReferenceId = Buffer.from( + 'src__application____haskou__ddd-kernel__domain__DomainEventPublisher', + ).toString('base64'); + + dependencyInjection.container + .register(serviceId, ServiceThatNeedsDomainEventPublisher) + .addArgument(new Reference(externalReferenceId)); + dependencyInjection.applyOverrides(); + await dependencyInjection.container.compile(); + + assert.ok( + dependencyInjection.getService(externalReferenceId) instanceof MessageBus, + ); +}); + test('generates services.yaml', async () => { const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'ddd-kernel-')); const sourceDirectory = temporaryDirectory; @@ -326,6 +391,38 @@ test('generates services.yaml', async () => { assert.match(servicesYaml, /GeneratedRepository/); }); +test('ignores type-only default exports during autowire', async () => { + const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'ddd-kernel-')); + const servicesYamlPath = path.join( + temporaryDirectory, + 'config', + 'container', + 'services.yaml', + ); + const warnings = []; + + await writeFile( + path.join(temporaryDirectory, 'IgnoredContract.ts'), + 'export default interface IgnoredContract {}\n', + ); + + const dependencyInjection = new DependencyInjection({ + containerBuild: true, + servicesYamlPath, + sourceDirectory: temporaryDirectory, + }); + + dependencyInjection.container.logger = { + warn(message) { + warnings.push(String(message)); + }, + }; + + await dependencyInjection.compile(); + + assert.deepEqual(warnings, []); +}); + test('reads services.yaml', async () => { const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'ddd-kernel-')); const loadedRepositoryPath = path.join( diff --git a/tests/kernel.test.mjs b/tests/kernel.test.mjs index 9f340ca..a26dada 100644 --- a/tests/kernel.test.mjs +++ b/tests/kernel.test.mjs @@ -641,7 +641,7 @@ test('throws when typed environment variables cannot be parsed', () => { assert.throws( () => kernel.loadEnvironmentVariables(), - /Environment variable "ENABLE_JOBS" must be a boolean|Environment variable "HTTP_PORT" must be a number/, + /Environment variable "ENABLE_JOBS" has invalid boolean value "maybe"|Environment variable "HTTP_PORT" has invalid number value "not-a-number"/, ); } finally { if (previousHttpPort === undefined) { @@ -658,7 +658,7 @@ test('throws when typed environment variables cannot be parsed', () => { } }); -test('throws when numeric typed environment variables are blank', () => { +test('treats blank optional typed environment variables as absent', () => { const previousHttpPort = process.env.HTTP_PORT; process.env.HTTP_PORT = ''; @@ -670,9 +670,57 @@ test('throws when numeric typed environment variables are blank', () => { }, }); + kernel.loadEnvironmentVariables(); + + assert.equal(kernel.environment.HTTP_PORT, undefined); + } finally { + if (previousHttpPort === undefined) { + delete process.env.HTTP_PORT; + } else { + process.env.HTTP_PORT = previousHttpPort; + } + } +}); + +test('uses defaults for blank optional typed environment variables', () => { + const previousHttpPort = process.env.HTTP_PORT; + + process.env.HTTP_PORT = ''; + + try { + const kernel = new Kernel({ + environmentSchema: { + HTTP_PORT: { defaultValue: 3000, type: 'number' }, + }, + }); + + kernel.loadEnvironmentVariables(); + + assert.equal(kernel.environment.HTTP_PORT, 3000); + } finally { + if (previousHttpPort === undefined) { + delete process.env.HTTP_PORT; + } else { + process.env.HTTP_PORT = previousHttpPort; + } + } +}); + +test('throws when required typed environment variables are blank', () => { + const previousHttpPort = process.env.HTTP_PORT; + + process.env.HTTP_PORT = ''; + + try { + const kernel = new Kernel({ + environmentSchema: { + HTTP_PORT: { required: true, type: 'number' }, + }, + }); + assert.throws( () => kernel.loadEnvironmentVariables(), - /Environment variable "HTTP_PORT" must be a number/, + /Blank required environment variable "HTTP_PORT"/, ); } finally { if (previousHttpPort === undefined) { @@ -697,7 +745,7 @@ test('throws when numeric typed environment variables are not finite numbers', ( assert.throws( () => kernel.loadEnvironmentVariables(), - /Environment variable "HTTP_PORT" must be a number/, + /Environment variable "HTTP_PORT" has invalid number value "not-a-number"/, ); } finally { if (previousHttpPort === undefined) { diff --git a/tests/runtime-compatibility.test.mjs b/tests/runtime-compatibility.test.mjs new file mode 100644 index 0000000..ec158fb --- /dev/null +++ b/tests/runtime-compatibility.test.mjs @@ -0,0 +1,196 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +async function createPackageFixture() { + const temporaryDirectory = await mkdtemp( + path.join(tmpdir(), 'ddd-kernel-runtime-'), + ); + const packageScopeDirectory = path.join( + temporaryDirectory, + 'node_modules', + '@haskou', + ); + + await mkdir(path.join(temporaryDirectory, 'src'), { recursive: true }); + await mkdir(packageScopeDirectory, { recursive: true }); + await symlink( + path.resolve('.'), + path.join(packageScopeDirectory, 'ddd-kernel'), + ); + + return temporaryDirectory; +} + +async function runNode(args, cwd, environmentVariables = {}) { + return new Promise((resolve) => { + const child = spawn(process.execPath, args, { + cwd, + env: { + ...process.env, + ...environmentVariables, + }, + }); + let stderr = ''; + let stdout = ''; + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.on('close', (code) => resolve({ code, stderr, stdout })); + }); +} + +test('runs a TypeScript runtime fixture through ts-node with public subpaths', async () => { + if (!existsSync(path.resolve('dist/index.js'))) { + return; + } + + const temporaryDirectory = await createPackageFixture(); + + await writeFile( + path.join(temporaryDirectory, 'package.json'), + JSON.stringify({ type: 'module' }), + ); + await writeFile( + path.join(temporaryDirectory, 'services.yaml'), + 'services: {}\n', + ); + await writeFile( + path.join(temporaryDirectory, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + skipLibCheck: true, + strict: true, + target: 'ES2022', + }, + }), + ); + await writeFile( + path.join(temporaryDirectory, 'index.ts'), + ` + import Kernel from '@haskou/ddd-kernel'; + import { DomainEventPublisher } from '@haskou/ddd-kernel/domain'; + import { Consumer } from '@haskou/ddd-kernel/adapters/pubsub'; + import { Scheduler } from '@haskou/ddd-kernel/scheduler'; + + class MessageBus extends DomainEventPublisher { + public async publish() {} + } + + const kernel = new Kernel({ + environmentSchema: { + HTTP_PORT: { defaultValue: 3000, type: 'number' }, + } as const, + servicesYamlPath: new URL('services.yaml', import.meta.url).pathname, + sourceDirectory: new URL('src', import.meta.url).pathname, + }); + + kernel.loadEnvironmentVariables(''); + await kernel.dependencyInjection({ + containerBuild: false, + overrides: [{ token: DomainEventPublisher, useClass: MessageBus }], + }); + + if (kernel.environment.HTTP_PORT !== 3000) { + throw new Error('environment schema default was not applied'); + } + + if (!(kernel.di.getService(DomainEventPublisher) instanceof MessageBus)) { + throw new Error('DomainEventPublisher override was not applied'); + } + + void Consumer; + void Scheduler; + `, + ); + + const result = await runNode( + [ + '--loader', + path.resolve('node_modules/ts-node/esm.mjs'), + path.join(temporaryDirectory, 'index.ts'), + ], + temporaryDirectory, + { HTTP_PORT: '', TS_NODE_TRANSPILE_ONLY: 'true' }, + ); + + assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`); +}); + +test('runs a CommonJS runtime fixture from dist with public subpaths', async () => { + if (!existsSync(path.resolve('dist/index.cjs'))) { + return; + } + + const temporaryDirectory = await createPackageFixture(); + + await writeFile( + path.join(temporaryDirectory, 'package.json'), + JSON.stringify({ type: 'commonjs' }), + ); + await writeFile( + path.join(temporaryDirectory, 'services.yaml'), + 'services: {}\n', + ); + await writeFile( + path.join(temporaryDirectory, 'index.cjs'), + ` + const KernelPackage = require('@haskou/ddd-kernel'); + const { DomainEventPublisher } = require('@haskou/ddd-kernel/domain'); + const { Consumer } = require('@haskou/ddd-kernel/adapters/pubsub'); + const { Scheduler } = require('@haskou/ddd-kernel/scheduler'); + + class MessageBus extends DomainEventPublisher { + async publish() {} + } + + (async () => { + const kernel = new KernelPackage.Kernel({ + environmentSchema: { + HTTP_PORT: { defaultValue: 3000, type: 'number' }, + }, + servicesYamlPath: require('node:path').join(process.cwd(), 'services.yaml'), + sourceDirectory: require('node:path').join(process.cwd(), 'src'), + }); + + kernel.loadEnvironmentVariables(''); + await kernel.dependencyInjection({ + containerBuild: false, + overrides: [{ token: DomainEventPublisher, useClass: MessageBus }], + }); + + if (kernel.environment.HTTP_PORT !== 3000) { + throw new Error('environment schema default was not applied'); + } + + if (!(kernel.di.getService(DomainEventPublisher) instanceof MessageBus)) { + throw new Error('DomainEventPublisher override was not applied'); + } + + void Consumer; + void Scheduler; + })().catch((error) => { + console.error(error); + process.exit(1); + }); + `, + ); + + const result = await runNode( + [path.join(temporaryDirectory, 'index.cjs')], + temporaryDirectory, + { HTTP_PORT: '' }, + ); + + assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`); +}); diff --git a/yarn.lock b/yarn.lock index 0314898..8783cf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -195,6 +195,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@dabh/diagnostics@^2.0.8": version "2.0.8" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz#ead97e72ca312cf0e6dd7af0d300b58993a31a5e" @@ -606,16 +613,24 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" @@ -874,6 +889,26 @@ color "^5.0.2" text-hex "1.0.x" +"@tsconfig/node10@^1.0.7": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/amqplib@^0.10.8": version "0.10.8" resolved "https://registry.yarnpkg.com/@types/amqplib/-/amqplib-0.10.8.tgz#23f2945d055e9fd583da672aa5ec0c7350b9e4e6" @@ -1356,7 +1391,14 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.16.0: +acorn-walk@^8.1.1: + version "8.3.5" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" + integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.16.0, acorn@^8.4.1: version "8.17.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== @@ -1423,6 +1465,11 @@ append-field@^1.0.0: resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1807,6 +1854,11 @@ cors@^2.8.6: object-assign "^4" vary "^1" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1884,6 +1936,11 @@ devlop@^1.0.0: dependencies: dequal "^2.0.0" +diff@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2931,6 +2988,11 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + mark.js@8.11.1: version "8.11.1" resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" @@ -3968,6 +4030,25 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -4124,6 +4205,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-to-istanbul@^9.0.0: version "9.3.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" @@ -4322,6 +4408,11 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 8897845ba0c1bb9e823cda6553adb0fea3f519b0 Mon Sep 17 00:00:00 2001 From: Hasko Date: Mon, 29 Jun 2026 22:20:06 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(di):=20=F0=9F=90=9B=20Keep=20runtime=20?= =?UTF-8?q?autowire=20warnings=20actionable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependency-injection/AutowireWarningFilter.ts | 8 +------- tests/dependency-injection.test.mjs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/infrastructure/dependency-injection/AutowireWarningFilter.ts b/src/infrastructure/dependency-injection/AutowireWarningFilter.ts index 1dcfa00..0eb8891 100644 --- a/src/infrastructure/dependency-injection/AutowireWarningFilter.ts +++ b/src/infrastructure/dependency-injection/AutowireWarningFilter.ts @@ -8,13 +8,7 @@ export class AutowireWarningFilter implements Logger { message.startsWith( 'Autowire: file has export default declaration but no runtime default export:', ) || - message.startsWith( - 'Autowire: failed to create definition for undefined:', - ) || - (message.startsWith('Autowire: failed to create definition for ') && - message.includes( - "Cannot read properties of undefined (reading 'body')", - )) + message.startsWith('Autowire: failed to create definition for undefined:') ); } diff --git a/tests/dependency-injection.test.mjs b/tests/dependency-injection.test.mjs index 7d81aef..db3b46b 100644 --- a/tests/dependency-injection.test.mjs +++ b/tests/dependency-injection.test.mjs @@ -54,7 +54,7 @@ test('filters ignorable autowire warnings and forwards actionable logs', () => { assert.deepEqual(calls, [ 'debug:Ignored Autowire: file has export default declaration but no runtime default export: Contract.ts', - "debug:Ignored Autowire: failed to create definition for HttpClient: Cannot read properties of undefined (reading 'body')", + "warn:Autowire: failed to create definition for HttpClient: Cannot read properties of undefined (reading 'body')", 'warn:Autowire: could not resolve dependency "Repository"', 'info:Autowire scan completed', 'debug:Autowire debug message',