/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint @typescript-eslint/no-unsafe-function-type:0 */ import { YError } from 'yerror'; import initDebug from 'debug'; const debug = initDebug('knifecycle'); export const NO_PROVIDER = Symbol('NO_PROVIDER'); /* Architecture Note #1.2: Creating initializers `knifecycle` uses initializers at its a core. An initializer is basically an asynchronous function with some annotations: - name: it uniquely identifies the initializer so that it can be referred to as another initializer dependency. - type: an initializer can be of three types at the moment (constant, service or provider). The initializer annotations varies according to those types as we'll see later on. - injected dependencies: an array of dependencies declarations that declares which initializer this initializer depends on. Constants logically cannot have dependencies. - options: various options like for example, if the initializer implements the singleton pattern or not. - value: only used for constant, this property allows to know the value the initializer resolves to without actually executing it. - extra: an extra property for custom use that will be propagated by the various other decorators you'll find in this library. `Knifecycle` provides a set of decorators that allows you to simply create new initializers. */ export const DECLARATION_SEPARATOR = '>'; export const OPTIONAL_FLAG = '?'; export const ALLOWED_INITIALIZER_TYPES = [ 'provider', 'service', 'constant', ] as const; export type InitializerTypes = (typeof ALLOWED_INITIALIZER_TYPES)[number]; export type ServiceName = string; export type Service = any; export type Disposer = () => Promise; export type FatalErrorPromise = Promise; export interface Provider { service: S; dispose?: Disposer; fatalErrorPromise?: FatalErrorPromise; } export type Dependencies = Record; export type DependencyName = string; export type DependencyDeclaration = string; export interface LocationInformation { url: string; exportName: string; } export type ExtraInformation = any; export interface ParsedDependencyDeclaration { serviceName: string; mappedName: string; optional: boolean; } export interface ConstantProperties { $type: 'constant'; $name: DependencyName; $singleton: true; $location?: LocationInformation; } export type ConstantInitializer = ConstantProperties & { $value: S; }; export interface ProviderProperties { $type: 'provider'; $name: DependencyName; $inject?: DependencyDeclaration[]; $singleton?: boolean; $location?: LocationInformation; $extra?: ExtraInformation; } export interface ProviderInputProperties { type: 'provider'; name: DependencyName; inject?: DependencyDeclaration[]; singleton?: boolean; location?: LocationInformation; extra?: ExtraInformation; } export interface ProviderInitializerReqD< D extends Dependencies, S extends Service, > extends ProviderProperties { (dependencies: D): Promise>; } export interface ProviderInitializerOptD< D extends Dependencies, S extends Service, > extends ProviderProperties { (dependencies?: D): Promise>; } export type ProviderInitializer = | ProviderInitializerReqD | ProviderInitializerOptD; export type ProviderInitializerBuilder< D extends Dependencies, S extends Service, > = | ((dependencies: D) => Promise>) | ((dependencies?: D) => Promise>); export interface ServiceProperties { $type: 'service'; $name: DependencyName; $inject?: DependencyDeclaration[]; $singleton?: boolean; $location?: LocationInformation; $extra?: ExtraInformation; } export interface ServiceInputProperties { type: 'service'; name: DependencyName; inject?: DependencyDeclaration[]; singleton?: boolean; location?: LocationInformation; extra?: ExtraInformation; } export interface ServiceInitializerReqD< D extends Dependencies, S extends Service, > extends ServiceProperties { (dependencies: D): Promise; } export interface ServiceInitializerOptD< D extends Dependencies, S extends Service, > extends ServiceProperties { (dependencies?: D): Promise; } export type ServiceInitializer = | ServiceInitializerReqD | ServiceInitializerOptD; export type ServiceInitializerBuilder< D extends Dependencies, S extends Service, > = ((dependencies: D) => Promise) | ((dependencies?: D) => Promise); export type InitializerProperties = | ConstantProperties | ProviderProperties | ServiceProperties; export type InitializerBuilder = | ProviderInitializerBuilder | ServiceInitializerBuilder | Partial> | Partial>; export type AsyncInitializer = | ServiceInitializer | ProviderInitializer; export type Initializer = | ConstantInitializer | ServiceInitializer | ProviderInitializer; export type ServiceInitializerWrapper< S extends Service, D extends Dependencies, > = (dependencies: D, baseService: S) => Promise; export type ProviderInitializerWrapper< S extends Service, D extends Dependencies, > = (dependencies: D, baseService: Provider) => Promise>; export const SPECIAL_PROPS_PREFIX = '$'; export const SPECIAL_PROPS = { TYPE: `${SPECIAL_PROPS_PREFIX}type`, NAME: `${SPECIAL_PROPS_PREFIX}name`, INJECT: `${SPECIAL_PROPS_PREFIX}inject`, SINGLETON: `${SPECIAL_PROPS_PREFIX}singleton`, LOCATION: `${SPECIAL_PROPS_PREFIX}location`, EXTRA: `${SPECIAL_PROPS_PREFIX}extra`, VALUE: `${SPECIAL_PROPS_PREFIX}value`, } as const satisfies Record; export const ALLOWED_SPECIAL_PROPS = Object.keys(SPECIAL_PROPS).map( (key) => SPECIAL_PROPS[key as keyof typeof SPECIAL_PROPS], ); export type InitializerSpecialProp = (typeof ALLOWED_SPECIAL_PROPS)[number]; const E_BAD_INJECT_IN_CONSTANT = 'E_BAD_INJECT_IN_CONSTANT'; const E_CONSTANT_INJECTION = 'E_CONSTANT_INJECTION'; export function parseInjections( source: string, options?: { allowEmpty: boolean }, ): DependencyDeclaration[] { const matches = source.match( /^\s*(?:async\s+function(?:\s+\w+)?|async)\s*\(\s*\{\s*([^{}]+)(\s*\.\.\.[^{}]+|)\s*\}/, ); if (!matches) { if (!source.match(/^\s*async/)) { throw new YError('E_NON_ASYNC_INITIALIZER', [source]); } if ( options && options.allowEmpty && source.match(/^\s*(?:async\s+function(?:\s+\w+)?|async)\s*\(\s*\)/) ) { return []; } throw new YError('E_AUTO_INJECTION_FAILURE', [source]); } return matches[1] .trim() .replace(/,$/, '') .split(/\s*,\s*/) .map((s) => s.trim()) .filter((s) => !s.startsWith('...')) .map( (injection) => (injection.includes('=') ? '?' : '') + (injection.split(/\s*=\s*/).shift() as string).split(/\s*:\s*/).shift(), ) .filter((injection) => !/[)(\][]/.test(injection)); } export function readFunctionName(aFunction: Function): string { if (typeof aFunction !== 'function') { throw new YError('E_AUTO_NAMING_FAILURE', [typeof aFunction]); } const functionName = parseName(aFunction.name || ''); if (!functionName) { throw new YError('E_AUTO_NAMING_FAILURE', [aFunction.name]); } return functionName; } export function parseName(functionName: string): string { return (functionName.split(' ').pop() as string).replace( /^init(?:ialize)?([A-Z])/, (_, $1) => $1.toLowerCase(), ); } /** * Apply special props to the given initializer from another one * and optionally amend with new special props * @param {Function} from The initializer in which to pick the props * @param {Function} to The initializer from which to build the new one * @param {Object} [amend={}] Some properties to override * @return {Function} The newly built initializer */ export function reuseSpecialProps< FD extends Dependencies, TD extends Dependencies, S, >( from: InitializerBuilder, to: ProviderInitializerBuilder, amend?: Partial, ): ProviderInitializerBuilder; export function reuseSpecialProps< FD extends Dependencies, TD extends Dependencies, S, >( from: InitializerBuilder, to: ServiceInitializerBuilder, amend?: Partial, ): ServiceInitializerBuilder; export function reuseSpecialProps< FD extends Dependencies, TD extends Dependencies, S, >( from: InitializerBuilder, to: ProviderInitializerBuilder | ServiceInitializerBuilder, amend: Partial | Partial = {}, ): | ProviderInitializerBuilder | ServiceInitializerBuilder { const uniqueInitializer = (to as unknown as Function).bind(null); return [...new Set(Object.keys(from).concat(Object.keys(amend)))] .filter((prop): prop is (typeof ALLOWED_SPECIAL_PROPS)[number] => prop.startsWith(SPECIAL_PROPS_PREFIX), ) .reduce((fn, prop) => { const value = prop !== '$value' && prop in amend && 'undefined' !== typeof amend[prop] ? amend[prop] : prop !== '$value' && prop in from ? (from as Partial>)[prop] : undefined; if (value instanceof Array) { fn[prop] = value.concat(); } else if (value instanceof Object) { fn[prop] = Object.assign({}, value); } else { fn[prop] = value; } return fn; }, uniqueInitializer); } /** * Decorator that creates an initializer for a constant value * @param {String} name * The constant's name. * @param {any} value * The constant's value * @return {Function} * Returns a new constant initializer * @example * import Knifecycle, { constant, service } from 'knifecycle'; * * const { printAnswer } = new Knifecycle() * .register(constant('THE_NUMBER', value)) * .register(constant('log', console.log.bind(console))) * .register(service( * async ({ THE_NUMBER, log }) => () => log(THE_NUMBER), * 'printAnswer', * ['THE_NUMBER', 'log'], * )) * .run(['printAnswer']); * * printAnswer(); // 42 */ export function constant( name: DependencyName, value: V, ): ConstantInitializer { const contantLooksLikeAnInitializer = value instanceof Function && '$inject' in value; if (contantLooksLikeAnInitializer) { throw new YError(E_CONSTANT_INJECTION, [value.$inject as string[]]); } debug(`Created an initializer from a constant: ${name}.`); return { $type: 'constant', $name: name, $singleton: true, $value: value, }; } /** * Decorator that creates an initializer from a service builder * @param {Function} serviceBuilder * An async function to build the service * @param {String} [name] * The service's name * @param {Array} [dependencies] * The service's injected dependencies * @param {Boolean} [singleton] * Whether the service is a singleton or not * @param {any} [extra] * Eventual extra information * @return {Function} * Returns a new initializer * @example * import Knifecycle, { constant, service } from 'knifecycle'; * * const { printAnswer } = new Knifecycle() * .register(constant('THE_NUMBER', value)) * .register(constant('log', console.log.bind(console))) * .register(service( * async ({ THE_NUMBER, log }) => () => log(THE_NUMBER), * 'printAnswer', * ['THE_NUMBER', 'log'], * true * )) * .run(['printAnswer']); * * printAnswer(); // 42 */ export function service, S>( serviceBuilder: ServiceInitializerBuilder, name?: DependencyName, dependencies?: DependencyDeclaration[], singleton?: boolean, extra?: ExtraInformation, ): ServiceInitializer { if (!serviceBuilder) { throw new YError('E_NO_SERVICE_BUILDER'); } name = name || pickInitializerBuilderProp(serviceBuilder, '$name') || 'anonymous'; dependencies = dependencies || pickInitializerBuilderProp(serviceBuilder, '$inject') || []; singleton = typeof singleton === 'undefined' ? pickInitializerBuilderProp(serviceBuilder, '$singleton') || false : singleton; extra = extra || pickInitializerBuilderProp(serviceBuilder, '$extra') || []; debug(`Created an initializer from a service builder: ${name}.`); const uniqueInitializer = reuseSpecialProps(serviceBuilder, serviceBuilder, { $type: 'service', $name: name, $inject: dependencies, $singleton: singleton, $extra: extra, }) as ServiceInitializer; return uniqueInitializer; } /** * Decorator that creates an initializer from a service * builder by automatically detecting its name * and dependencies * @param {Function} serviceBuilder * An async function to build the service * @return {Function} * Returns a new initializer */ export function autoService, S>( serviceBuilder: ServiceInitializerBuilder, ): ServiceInitializer { const name = readFunctionName(serviceBuilder as Function); const source = serviceBuilder.toString(); const dependencies = parseInjections(source, { allowEmpty: true }); return service(serviceBuilder, name, dependencies); } /** * Decorator that creates an initializer for a provider * builder * @param {Function} providerBuilder * An async function to build the service provider * @param {String} [name] * The service's name * @param {Array} [dependencies] * The service's dependencies * @param {Boolean} [singleton] * Whether the service is a singleton or not * @param {any} [extra] * Eventual extra information * @return {Function} * Returns a new provider initializer * @example * * import Knifecycle, { provider } from 'knifecycle' * import fs from 'fs'; * * const $ = new Knifecycle(); * * $.register(provider(configProvider, 'config')); * * async function configProvider() { * return new Promise((resolve, reject) { * fs.readFile('config.js', function(err, data) { * let config; * * if(err) { * reject(err); * return; * } * * try { * config = JSON.parse(data.toString); * } catch (err) { * reject(err); * return; * } * * resolve({ * service: config, * }); * }); * }); * } */ export function provider, S>( providerBuilder: ProviderInitializerBuilder, name?: DependencyName, dependencies?: DependencyDeclaration[], singleton?: boolean, extra?: ExtraInformation, ): ProviderInitializer { if (!providerBuilder) { throw new YError('E_NO_PROVIDER_BUILDER'); } name = name || pickInitializerBuilderProp(providerBuilder, '$name') || 'anonymous'; dependencies = dependencies || pickInitializerBuilderProp(providerBuilder, '$inject') || []; singleton = typeof singleton === 'undefined' ? pickInitializerBuilderProp(providerBuilder, '$singleton') || false : singleton; extra = extra || pickInitializerBuilderProp(providerBuilder, '$extra') || []; debug( `Created an initializer from a provider builder: ${name || 'anonymous'}.`, ); const uniqueInitializer = reuseSpecialProps( providerBuilder, providerBuilder, { $type: 'provider', $name: name, $inject: dependencies, $singleton: singleton, $extra: extra, }, ) as ProviderInitializer; return uniqueInitializer; } /** * Decorator that creates an initializer from a provider * builder by automatically detecting its name * and dependencies * @param {Function} providerBuilder * An async function to build the service provider * @return {Function} * Returns a new provider initializer */ export function autoProvider, S>( providerBuilder: ProviderInitializerBuilder, ): ProviderInitializer { const name = readFunctionName(providerBuilder as Function); const source = providerBuilder.toString(); const dependencies = parseInjections(source, { allowEmpty: true }); return provider(providerBuilder, name, dependencies); } /** * Allows to wrap an initializer to add extra initialization steps * @param {Function} wrapper * A function taking dependencies and the base * service in arguments * @param {Function} baseInitializer * The initializer to decorate * @return {Function} * The new initializer */ export function wrapInitializer, S>( wrapper: ProviderInitializerWrapper, baseInitializer: ProviderInitializer, ): ProviderInitializer; export function wrapInitializer, S>( wrapper: ServiceInitializerWrapper, baseInitializer: ServiceInitializer, ): ServiceInitializer; export function wrapInitializer, S>( wrapper: ProviderInitializerWrapper | ServiceInitializerWrapper, baseInitializer: ProviderInitializer | ServiceInitializer, ): ProviderInitializer | ServiceInitializer { return reuseSpecialProps(baseInitializer, (async (services: D) => { const baseInstance = await baseInitializer(services); return (wrapper as ServiceInitializerWrapper)( services, baseInstance as S, ) as S; }) as unknown as ServiceInitializer) as ServiceInitializer; } /** * Decorator creating a new initializer with different * dependencies declarations set to it. * @param {Array} dependencies * List of dependencies declarations to declare which * services the initializer needs to provide its * own service * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { inject } from 'knifecycle' * import myServiceInitializer from './service'; * * new Knifecycle() * .register( * service( * inject(['ENV'], myServiceInitializer) * 'myService', * ) * ) * ); */ export function inject, S>( dependencies: DependencyDeclaration[], initializer: ProviderInitializer, ): ProviderInitializer; export function inject, S>( dependencies: DependencyDeclaration[], initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function inject, S>( dependencies: DependencyDeclaration[], initializer: ServiceInitializer, ): ServiceInitializer; export function inject, S>( dependencies: DependencyDeclaration[], initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function inject, S>( dependencies: DependencyDeclaration[], initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { if (initializerBuilderIsOfType('constant', initializer)) { throw new YError(E_BAD_INJECT_IN_CONSTANT, [ initializer.$name, dependencies, ]); } const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $inject: dependencies, }, ); debug('Wrapped an initializer with dependencies:', dependencies); return uniqueInitializer; } /** * Decorator creating a new initializer omitting * the given dependencies. * @param {Array} dependencies * List of dependencies to omit (also accept dependencies * declarations but omit the destination part) * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { unInject } from 'knifecycle' * import myServiceInitializer from './service'; * * new Knifecycle() * .register( * service( * unInject(['ENV'], myServiceInitializer) * 'myService', * ) * ) * ); */ export function unInject, S>( dependencies: DependencyDeclaration[], initializer: ProviderInitializer, ): ProviderInitializer; export function unInject, S>( dependencies: DependencyDeclaration[], initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function unInject, S>( dependencies: DependencyDeclaration[], initializer: ServiceInitializer, ): ServiceInitializer; export function unInject, S>( dependencies: DependencyDeclaration[], initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function unInject, S>( dependencies: DependencyDeclaration[], initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const filteredDependencies = dependencies.map(parseDependencyDeclaration); const originalDependencies = ( pickInitializerBuilderProp(initializer, '$inject') || [] ).map(parseDependencyDeclaration); return inject( originalDependencies .filter(({ serviceName }) => filteredDependencies.every( ({ serviceName: filteredServiceName }) => serviceName !== filteredServiceName, ), ) .map(stringifyDependencyDeclaration), initializer as ServiceInitializerBuilder, ); } /** * Apply injected dependencies from the given initializer to another one * @param {Function} from The initialization function in which to pick the dependencies * @param {Function} to The destination initialization function * @return {Function} The newly built initialization function */ export function useInject, S>( from: InitializerBuilder, to: ProviderInitializer, ): ProviderInitializer; export function useInject, S>( from: InitializerBuilder, to: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function useInject, S>( from: InitializerBuilder, to: ServiceInitializer, ): ServiceInitializer; export function useInject, S>( from: InitializerBuilder, to: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function useInject, S>( from: InitializerBuilder, to: ProviderInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { return inject(pickInitializerBuilderProp(from, '$inject') || [], to); } /** * Merge injected dependencies of the given initializer with another one * @param {Function} from The initialization function in which to pick the dependencies * @param {Function} to The destination initialization function * @return {Function} The newly built initialization function */ export function mergeInject< FD extends Dependencies, D extends Dependencies, S, >( from: InitializerBuilder, to: ProviderInitializer, ): ProviderInitializer; export function mergeInject< FD extends Dependencies, D extends Dependencies, S, >( from: InitializerBuilder, to: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function mergeInject< FD extends Dependencies, D extends Dependencies, S, >( from: InitializerBuilder, to: ServiceInitializer, ): ServiceInitializer; export function mergeInject< FD extends Dependencies, D extends Dependencies, S, >( from: InitializerBuilder, to: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function mergeInject< FD extends Dependencies, D extends Dependencies, S, >( from: InitializerBuilder, to: ProviderInitializerBuilder | ServiceInitializerBuilder, ): | ProviderInitializerBuilder | ServiceInitializerBuilder { return alsoInject( (from as Partial>).$inject || ([] as DependencyDeclaration[]), to as ServiceInitializerBuilder, ); } /** * Decorator creating a new initializer with different * dependencies declarations set to it according to the * given function signature. * @param {Function} initializer * The original initializer * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { autoInject, name } from 'knifecycle' * * new Knifecycle() * .register( * name( * 'application', * autoInject( * async ({ NODE_ENV, mysql: db }) => * async () => db.query('SELECT applicationId FROM applications WHERE environment=?', [NODE_ENV]) * ) * ) * ) * ) * ); */ export function autoInject, S>( initializer: ProviderInitializer, ): ProviderInitializer; export function autoInject, S>( initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function autoInject, S>( initializer: ServiceInitializer, ): ServiceInitializer; export function autoInject, S>( initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function autoInject, S>( initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const source = initializer.toString(); const dependencies = parseInjections(source); return inject( dependencies, initializer as ServiceInitializerBuilder, ); } /** * Decorator creating a new initializer with some * more dependencies declarations appended to it. * @param {Array} dependencies * List of dependencies declarations to append * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { alsoInject } from 'knifecycle' * import myServiceInitializer from './service'; * * new Knifecycle() * .register(service( * alsoInject(['ENV'], myServiceInitializer), * 'myService', * )); */ export function alsoInject< ND extends Dependencies, D extends Dependencies, S, >( dependencies: DependencyDeclaration[], to: ProviderInitializer, ): ProviderInitializer; export function alsoInject< ND extends Dependencies, D extends Dependencies, S, >( dependencies: DependencyDeclaration[], to: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function alsoInject< ND extends Dependencies, D extends Dependencies, S, >( dependencies: DependencyDeclaration[], to: ServiceInitializer, ): ServiceInitializer; export function alsoInject< ND extends Dependencies, D extends Dependencies, S, >( dependencies: DependencyDeclaration[], to: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function alsoInject< ND extends Dependencies, D extends Dependencies, S, >( dependencies: DependencyDeclaration[], initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): | ProviderInitializerBuilder | ServiceInitializerBuilder { const currentDependencies = ( pickInitializerBuilderProp(initializer, '$inject') || [] ).map(parseDependencyDeclaration); const addedDependencies = dependencies.map(parseDependencyDeclaration); const dedupedDependencies: DependencyDeclaration[] = currentDependencies .filter(({ serviceName }) => { const declarationIsOverridden = addedDependencies.some( ({ serviceName: addedServiceName }) => { return addedServiceName === serviceName; }, ); return !declarationIsOverridden; }) .concat( addedDependencies.map(({ serviceName, mappedName, optional }) => { const isOptionalEverywhere = optional && currentDependencies.every( ({ optional, mappedName: addedMappedName }) => { return addedMappedName !== mappedName || optional; }, ); return { serviceName, mappedName, optional: isOptionalEverywhere, }; }), ) .map(stringifyDependencyDeclaration); return inject( dedupedDependencies, initializer as ServiceInitializerBuilder, ); } /** * Decorator creating a new initializer with some * extra information appended to it. It is just * a way for user to store some additional * information but has no interaction with the * Knifecycle internals. * @param {Object} extraInformation * An object containing those extra information. * @param {Function} initializer * The initializer to tweak * @param {Boolean} [merge=false] * Whether the extra object should be merged * with the existing one or not * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { extra } from 'knifecycle' * import myServiceInitializer from './service'; * * new Knifecycle() * .register(service( * extra({ httpHandler: true }, myServiceInitializer), * 'myService', * )); */ export function extra, S>( extraInformation: ExtraInformation, initializer: ProviderInitializer, merge?: boolean, ): ProviderInitializer; export function extra, S>( extraInformation: ExtraInformation, initializer: ProviderInitializerBuilder, merge?: boolean, ): ProviderInitializerBuilder; export function extra, S>( extraInformation: ExtraInformation, initializer: ServiceInitializer, ): ServiceInitializer; export function extra, S>( extraInformation: ExtraInformation, initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function extra, S>( extraInformation: ExtraInformation, initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, merge = false, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $extra: merge ? Object.assign( pickInitializerBuilderProp(initializer, '$extra') || {}, extraInformation, ) : extraInformation, }, ); debug('Wrapped an initializer with extra information:', extraInformation); return uniqueInitializer; } /** * Decorator to set an initializer singleton option. * @param {Function} initializer * The initializer to tweak * @param {boolean} [isSingleton=true] * Define the initializer singleton option * (one instance for several runs if true) * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { inject, singleton } from 'knifecycle'; * import myServiceInitializer from './service'; * * new Knifecycle() * .register(service( * inject(['ENV'], * singleton(myServiceInitializer) * ), * 'myService', * )); */ export function singleton, S>( initializer: ProviderInitializer, isSingleton?: boolean, ): ProviderInitializer; export function singleton, S>( initializer: ProviderInitializerBuilder, isSingleton?: boolean, ): ProviderInitializerBuilder; export function singleton, S>( initializer: ServiceInitializer, isSingleton?: boolean, ): ServiceInitializer; export function singleton, S>( initializer: ServiceInitializerBuilder, isSingleton?: boolean, ): ServiceInitializerBuilder; export function singleton, S>( initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, isSingleton = true, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $singleton: isSingleton, }, ); debug('Marked an initializer as singleton:', isSingleton); return uniqueInitializer; } /** * Decorator to set an initializer location. * @param {Function} initializer * The initializer to tweak * @param {string} [url] * Define the initializer url (import.meta.url) in most situations * @param {string} [exportName] * Define the initializer export name * @return {Function} * Returns a new initializer * @example * * import { service, location } from 'knifecycle'; * * export const initMyService = location( * service(async () => {}, 'myService', []), * import.meta.url, * 'initMyService', * }); */ export function location, S>( initializer: ProviderInitializer, url: string, exportName?: string, ): ProviderInitializer; export function location, S>( initializer: ProviderInitializerBuilder, url: string, exportName?: string, ): ProviderInitializerBuilder; export function location, S>( initializer: ServiceInitializer, url: string, exportName?: string, ): ServiceInitializer; export function location, S>( initializer: ServiceInitializerBuilder, url: string, exportName?: string, ): ServiceInitializerBuilder; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function location, S>( initializer: ConstantInitializer, url: string, exportName?: string, ): ConstantInitializer; export function location, S>( initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder | ConstantInitializer, url: LocationInformation['url'], exportName: LocationInformation['exportName'] = 'default', ): | ProviderInitializerBuilder | ServiceInitializerBuilder | ConstantInitializer { if (initializerBuilderIsOfType('constant', initializer)) { return { $name: initializer.$name, $type: 'constant', $value: initializer.$value, $singleton: true, $location: { url, exportName }, }; } const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $location: { url, exportName }, }, ); debug('Set an initializer location as:', { url, exportName }); return uniqueInitializer; } /** * Decorator to set an initializer name. * @param {String} name * The name of the service the initializer resolves to. * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer with that name set * @example * * import Knifecycle, { name } from 'knifecycle'; * import myServiceInitializer from './service'; * * new Knifecycle() * .register(name('myService', myServiceInitializer)); */ export function name, S>( name: DependencyName, initializer: ProviderInitializer, ): ProviderInitializer; export function name, S>( name: DependencyName, initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function name, S>( name: DependencyName, initializer: ServiceInitializer, ): ServiceInitializer; export function name, S>( name: DependencyName, initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function name, S>( name: DependencyName, initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $name: name, }, ); debug('Wrapped an initializer with a name:', name); return uniqueInitializer; } /** * Decorator to set an initializer name from its function name. * @param {Function} initializer * The initializer to name * @return {Function} * Returns a new initializer with that name set * @example * * import Knifecycle, { autoName } from 'knifecycle'; * * new Knifecycle() * .register(autoName(async function myService() {})); */ export function autoName, S>( initializer: ProviderInitializer, ): ProviderInitializer; export function autoName, S>( initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function autoName, S>( initializer: ServiceInitializer, ): ServiceInitializer; export function autoName, S>( initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function autoName, S>( initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { return name( readFunctionName(initializer as unknown as Function), initializer as ServiceInitializerBuilder, ); } /** * Decorator to set an initializer type. * @param {String} type * The type to set to the initializer. * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { name, type } from 'knifecycle'; * import myServiceInitializer from './service'; * * new Knifecycle() * .register( * type('service', * name('myService', * myServiceInitializer * ) * ) * ); */ export function type, S>( type: 'provider', initializer: ProviderInitializer, ): ProviderInitializer; export function type, S>( type: 'provider', initializer: ProviderInitializerBuilder, ): ProviderInitializerBuilder; export function type, S>( type: 'service', initializer: ServiceInitializer, ): ServiceInitializer; export function type, S>( type: 'service', initializer: ServiceInitializerBuilder, ): ServiceInitializerBuilder; export function type, S>( type: 'service' | 'provider', initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializerBuilder | ServiceInitializerBuilder { const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, { $type: type as 'service', }, ); debug('Wrapped an initializer with a type:', type); return uniqueInitializer as ServiceInitializerBuilder; } /** * Decorator to set an initializer properties. * @param {Object} properties * Properties to set to the service. * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns a new initializer * @example * * import Knifecycle, { initializer } from 'knifecycle'; * import myServiceInitializer from './service'; * * new Knifecycle() * .register(initializer({ * name: 'myService', * type: 'service', * inject: ['ENV'], * singleton: true, * }, myServiceInitializer)); */ export function initializer, S>( properties: ProviderInputProperties, initializer: ProviderInitializerBuilder, ): ProviderInitializer; export function initializer, S>( properties: ServiceInputProperties, initializer: ServiceInitializerBuilder, ): ServiceInitializer; export function initializer, S>( properties: ServiceInputProperties | ProviderInputProperties, initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder, ): ProviderInitializer | ServiceInitializer { const uniqueInitializer = reuseSpecialProps( initializer, initializer as ServiceInitializerBuilder, Object.keys(properties).reduce((finalProperties, property) => { const finalProperty = (SPECIAL_PROPS_PREFIX + property) as keyof ServiceProperties; if (!ALLOWED_SPECIAL_PROPS.includes(finalProperty)) { throw new YError('E_BAD_PROPERTY', [property]); } finalProperties[finalProperty] = properties[property as keyof ServiceInputProperties]; return finalProperties; }, {} as Partial), ); debug('Wrapped an initializer with properties:', properties); return uniqueInitializer as ServiceInitializer; } /* Architecture Note #1.2.1: Dependencies declaration syntax The dependencies syntax is of the following form: `?serviceName>mappedName` The `?` flag indicates an optional dependency. `>mappedName` is optional and allows to inject `mappedName` as `serviceName`. It allows to write generic services with fixed dependencies and remap their name at injection time. */ /** * Explode a dependency declaration an returns its parts. * @param {String} dependencyDeclaration * A dependency declaration string * @return {Object} * The various parts of it * @example * parseDependencyDeclaration('pgsql>db'); * // Returns * { * serviceName: 'pgsql', * mappedName: 'db', * optional: false, * } */ export function parseDependencyDeclaration( dependencyDeclaration: DependencyDeclaration, ): ParsedDependencyDeclaration { const optional = dependencyDeclaration.startsWith(OPTIONAL_FLAG); const [serviceName, mappedName] = ( optional ? dependencyDeclaration.slice(1) : dependencyDeclaration ).split(DECLARATION_SEPARATOR); return { serviceName, mappedName: mappedName || serviceName, optional, }; } /** * Stringify a dependency declaration from its parts. * @param {Object} dependencyDeclarationParts * A dependency declaration string * @return {String} * The various parts of it * @example * stringifyDependencyDeclaration({ * serviceName: 'pgsql', * mappedName: 'db', * optional: false, * }); * * // Returns * 'pgsql>db' */ export function stringifyDependencyDeclaration( dependencyDeclarationParts: ParsedDependencyDeclaration, ): DependencyDeclaration { return `${dependencyDeclarationParts.optional ? '?' : ''}${ dependencyDeclarationParts.serviceName }${ dependencyDeclarationParts.mappedName !== dependencyDeclarationParts.serviceName ? '>' + dependencyDeclarationParts.mappedName : '' }`; } /* Architecture Note #3: TypeScript tweaks Sadly TypeScript does not allow to add generic types in all cases. This is why `(Service|Provider)Initializer` types do not embed the `(Service|Provider)Properties` directly. Instead, we use this utility function to reveal it to TypeScript and, by the way, check their completeness at execution time. For more details, see: https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184 */ /** * Utility function to check and reveal initializer properties. * @param {Function} initializer * The initializer to tweak * @return {Function} * Returns revealed initializer (with TypeScript types for properties) */ export function unwrapInitializerProperties>( initializer: ProviderInitializer, ): ProviderProperties; export function unwrapInitializerProperties>( initializer: ServiceInitializer, ): ServiceProperties; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function unwrapInitializerProperties>( initializer: ConstantInitializer, ): ConstantProperties; export function unwrapInitializerProperties>( initializer: Initializer, ): InitializerProperties; export function unwrapInitializerProperties>( initializer: | ProviderInitializerBuilder | ServiceInitializerBuilder | ConstantInitializer, ): ProviderProperties | ServiceProperties | ConstantProperties { if (typeof initializer !== 'function' && typeof initializer !== 'object') { throw new YError('E_BAD_INITIALIZER', [initializer]); } const properties = initializer as InitializerProperties; if (typeof properties.$name !== 'string' || properties.$name === '') { throw new YError('E_ANONYMOUS_ANALYZER', [properties.$name]); } if (!ALLOWED_INITIALIZER_TYPES.includes(properties.$type)) { throw new YError('E_BAD_INITIALIZER_TYPE', [ pickInitializerBuilderProp(initializer, '$name'), pickInitializerBuilderProp(initializer, '$type'), ALLOWED_INITIALIZER_TYPES, ]); } if ( pickInitializerBuilderProp(initializer, '$name') === '$autoload' && !pickInitializerBuilderProp(initializer, '$singleton') ) { throw new YError('E_BAD_AUTO_LOADER', [ pickInitializerBuilderProp(initializer, '$singleton') || false, ]); } if (properties.$type === 'constant') { if ('undefined' === typeof (initializer as ConstantInitializer).$value) { throw new YError('E_UNDEFINED_CONSTANT_INITIALIZER', [properties.$name]); } properties.$singleton = true; } else { if ('$value' in initializer && 'undefined' !== typeof initializer.$value) { throw new YError('E_BAD_VALUED_NON_CONSTANT_INITIALIZER', [ initializer.$name, ]); } properties.$inject = properties.$inject || []; properties.$singleton = properties.$singleton || false; properties.$extra = properties.$extra || undefined; } return initializer as | (ProviderInitializer & ProviderProperties) | (ServiceInitializer & ServiceProperties) | ConstantInitializer; } export function initializerBuilderIsOfType< T extends InitializerTypes, D extends Dependencies, S, >( type: T, initializer: InitializerBuilder | ConstantInitializer, ): initializer is T extends 'service' ? ServiceInitializerBuilder : T extends 'provider' ? ServiceInitializerBuilder : ConstantInitializer { return (initializer as ConstantInitializer).$type === type ? true : false; } export function pickInitializerBuilderProp< D extends Dependencies, S, T extends InitializerSpecialProp, >( initializer: InitializerBuilder | ConstantInitializer, prop: T, ): T extends keyof ServiceProperties ? ServiceProperties[T] : S { return (initializer as ConstantInitializer)[ prop as '$value' ] as T extends keyof ServiceProperties ? ServiceProperties[T] : S; }