diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 26eaa20..8bfc176 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -37,7 +37,7 @@ It is designed to have a low footprint on services code. In fact, the Knifecycle API is aimed to allow to statically build its services load/unload code once in production. -[See in context](./src/index.ts#L213-L232) +[See in context](./src/index.ts#L207-L226) @@ -52,7 +52,7 @@ A service provider is full of state since its concern is [encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) your application global states. -[See in context](./src/index.ts#L234-L243) +[See in context](./src/index.ts#L228-L237) @@ -92,7 +92,7 @@ The `?` flag indicates an optional dependency. It allows to write generic services with fixed dependencies and remap their name at injection time. -[See in context](./src/util.ts#L1372-L1381) +[See in context](./src/util.ts#L1462-L1471) @@ -121,7 +121,7 @@ Initializers can be of three types: instanciated once for all for each executions silos using them (we will cover this topic later on). -[See in context](./src/index.ts#L332-L356) +[See in context](./src/index.ts#L326-L350) @@ -137,7 +137,7 @@ Depending on your application design, you could run it in only one execution silo or into several ones according to the isolation level your wish to reach. -[See in context](./src/index.ts#L688-L698) +[See in context](./src/index.ts#L682-L692) @@ -157,7 +157,7 @@ For the build to work, we need: - the dependencies list you want to initialize -[See in context](./src/build.ts#L37-L52) +[See in context](./src/build.ts#L39-L54) @@ -173,5 +173,5 @@ Sadly TypeScript does not allow to add generic types For more details, see: https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184 -[See in context](./src/util.ts#L1442-L1453) +[See in context](./src/util.ts#L1532-L1543) diff --git a/src/build.test.ts b/src/build.test.ts index 8e600ef..e40d573 100644 --- a/src/build.test.ts +++ b/src/build.test.ts @@ -21,6 +21,10 @@ describe('buildInitializer', () => { inject: [], type: 'service', name: 'dep1', + location: { + url: `file://services/dep1`, + exportName: 'default', + }, }, aProvider, ), @@ -29,6 +33,10 @@ describe('buildInitializer', () => { inject: ['dep1', 'NODE_ENV'], type: 'provider', name: 'dep2', + location: { + url: `file://services/dep2`, + exportName: 'default', + }, }, aProvider, ), @@ -37,6 +45,10 @@ describe('buildInitializer', () => { inject: ['dep5', 'dep2', 'dep1', '?depOpt'], type: 'service', name: 'dep3', + location: { + url: `file://services/dep3`, + exportName: 'default', + }, }, aProvider, ), @@ -45,6 +57,10 @@ describe('buildInitializer', () => { inject: ['dep5', 'dep2', 'dep1', '?depOpt'], type: 'service', name: 'dep4', + location: { + url: `file://services/dep4`, + exportName: 'default', + }, }, aProvider, ), @@ -53,6 +69,10 @@ describe('buildInitializer', () => { inject: ['$ready'], type: 'service', name: 'dep5', + location: { + url: `file://services/dep5`, + exportName: 'initDep5', + }, }, aProvider, ), @@ -61,6 +81,10 @@ describe('buildInitializer', () => { inject: [], type: 'service', name: 'dep6', + location: { + url: `file://services/dep6`, + exportName: 'aDep6', + }, }, aProvider, ), @@ -75,10 +99,7 @@ describe('buildInitializer', () => { async () => { return async function $autoload(name) { return mockedDepsHash[name] - ? Promise.resolve({ - path: `./services/${name}`, - initializer: mockedDepsHash[name], - }) + ? Promise.resolve(mockedDepsHash[name]) : Promise.reject(new YError('E_UNMATCHED_DEPENDENCY', name)); }; }, @@ -121,15 +142,15 @@ const $instance = { // Definition batch #0 -import initDep1 from './services/dep1'; +import initDep1 from 'file://services/dep1'; const NODE_ENV = "development"; // Definition batch #1 -import initDep5 from './services/dep5'; -import initDep2 from './services/dep2'; +import { initDep5 } from 'file://services/dep5'; +import initDep2 from 'file://services/dep2'; // Definition batch #2 -import initDep3 from './services/dep3'; +import initDep3 from 'file://services/dep3'; export async function initialize(services = {}) { const $fatalError = await initFatalError(); @@ -255,15 +276,15 @@ const $instance = { // Definition batch #0 -import initDep1 from './services/dep1'; -import initDep6 from './services/dep6'; +import initDep1 from 'file://services/dep1'; +import { aDep6 as initDep6 } from 'file://services/dep6'; const NODE_ENV = "development"; // Definition batch #1 -import initDep2 from './services/dep2'; +import initDep2 from 'file://services/dep2'; // Definition batch #2 -import initDep4 from './services/dep4'; +import initDep4 from 'file://services/dep4'; export async function initialize(services = {}) { const $fatalError = await initFatalError(); @@ -384,16 +405,16 @@ const $instance = { // Definition batch #0 -import initDep1 from './services/dep1'; +import initDep1 from 'file://services/dep1'; const NODE_ENV = "development"; const $siloContext = undefined; // Definition batch #1 -import initDep5 from './services/dep5'; -import initDep2 from './services/dep2'; +import { initDep5 } from 'file://services/dep5'; +import initDep2 from 'file://services/dep2'; // Definition batch #2 -import initDep3 from './services/dep3'; +import initDep3 from 'file://services/dep3'; export async function initialize(services = {}) { const $fatalError = await initFatalError(); diff --git a/src/build.ts b/src/build.ts index a9c3271..ddd981e 100644 --- a/src/build.ts +++ b/src/build.ts @@ -5,15 +5,17 @@ import { parseDependencyDeclaration, initializer, READY, + location, } from './util.js'; import { buildInitializationSequence } from './sequence.js'; import { FATAL_ERROR } from './fatalError.js'; import { DISPOSE } from './dispose.js'; import { type Overrides, type Autoloader } from './index.js'; -import type { - DependencyDeclaration, - Initializer, - Dependencies, +import { + type DependencyDeclaration, + type Initializer, + type Dependencies, + type LocationInformation, } from './util.js'; import { OVERRIDES, pickOverridenName } from './overrides.js'; @@ -26,7 +28,7 @@ type DependencyTreeNode = { __inject: DependencyDeclaration[]; __type: 'provider' | 'constant' | 'service'; __initializerName: string; - __path: string; + __location: LocationInformation | 'managed' | 'no_location'; __parentsNames: string[]; }; @@ -51,13 +53,16 @@ For the build to work, we need: initialize */ -export default initializer( - { - name: 'buildInitializer', - type: 'service', - inject: [AUTOLOAD, OVERRIDES], - }, - initInitializerBuilder, +export default location( + initializer( + { + name: 'buildInitializer', + type: 'service', + inject: [AUTOLOAD, OVERRIDES], + }, + initInitializerBuilder, + ), + import.meta.url, ); /** @@ -146,23 +151,36 @@ ${batches (batch, index) => ` // Definition batch #${index}${batch .map((name) => { - if (MANAGED_SERVICES.includes(name)) { + if (dependenciesHash[name].__location === 'managed') { return ''; } - if ( - 'constant' === - dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE] - ) { - return ` + if (dependenciesHash[name].__location === 'no_location') { + if ( + 'constant' === + dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE] && + dependenciesHash[name].__location === 'no_location' + ) { + return ` const ${name} = ${JSON.stringify( - dependenciesHash[name].__initializer[SPECIAL_PROPS.VALUE], - null, - 2, - )};`; + dependenciesHash[name].__initializer[SPECIAL_PROPS.VALUE], + null, + 2, + )};`; + } + return ` +// No location for "${name}" service +const ${name} = undefined;`; } return ` -import ${dependenciesHash[name].__initializerName} from '${dependenciesHash[name].__path}';`; +import ${ + dependenciesHash[name].__location.exportName === 'default' + ? dependenciesHash[name].__initializerName + : dependenciesHash[name].__location.exportName === + dependenciesHash[name].__initializerName + ? `{ ${dependenciesHash[name].__initializerName} }` + : `{ ${dependenciesHash[name].__location.exportName} as ${dependenciesHash[name].__initializerName} }` + } from '${dependenciesHash[name].__location.url}';`; }) .join('')}`, ) @@ -268,22 +286,21 @@ async function buildDependencyTree( mappedName, ]); - if(MANAGED_SERVICES.includes(finalName)) { + if (MANAGED_SERVICES.includes(finalName)) { return { - __name: finalName, - __initializer: async() => {}, + __initializer: async () => {}, __inject: [], __type: 'constant', __initializerName: 'init' + upperCaseFirst(finalName.slice(1)), - __path: `internal://managed/${finalName}`, + __location: 'managed', __childNodes: [], __parentsNames: [...parentsNames, finalName], }; } try { - const { path, initializer } = await $autoload(finalName); + const initializer = await $autoload(finalName); const node: DependencyTreeNode = { __name: finalName, __initializer: initializer, @@ -296,7 +313,7 @@ async function buildDependencyTree( ? initializer[SPECIAL_PROPS.TYPE] : 'provider', __initializerName: 'init' + upperCaseFirst(finalName), - __path: path, + __location: initializer[SPECIAL_PROPS.LOCATION] || 'no_location', __childNodes: [], __parentsNames: [...parentsNames, finalName], }; diff --git a/src/dispose.ts b/src/dispose.ts index 71276f6..e08d545 100644 --- a/src/dispose.ts +++ b/src/dispose.ts @@ -3,6 +3,7 @@ import { NO_PROVIDER, SILO_CONTEXT, SPECIAL_PROPS, + location, parseDependencyDeclaration, service, } from './util.js'; @@ -151,4 +152,7 @@ async function initDispose({ }; } -export default service(initDispose, DISPOSE, [INSTANCE, SILO_CONTEXT]); +export default location( + service(initDispose, DISPOSE, [INSTANCE, SILO_CONTEXT]), + import.meta.url, +); diff --git a/src/fatalError.ts b/src/fatalError.ts index 9d91098..ae018ae 100644 --- a/src/fatalError.ts +++ b/src/fatalError.ts @@ -1,5 +1,5 @@ import { printStackTrace } from 'yerror'; -import { service } from './util.js'; +import { location, service } from './util.js'; import initDebug from 'debug'; const debug = initDebug('knifecycle'); @@ -57,4 +57,7 @@ async function initFatalError(): Promise { }; } -export default service(initFatalError, FATAL_ERROR, [], true); +export default location( + service(initFatalError, FATAL_ERROR, [], true), + import.meta.url, +); diff --git a/src/index.test.ts b/src/index.test.ts index 025ee28..4aff842 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -14,7 +14,7 @@ import { singleton, } from './index.js'; import type { Provider, FatalErrorService } from './index.js'; -import { ALLOWED_INITIALIZER_TYPES } from './util.js'; +import { ALLOWED_INITIALIZER_TYPES, location } from './util.js'; describe('Knifecycle', () => { let $: Knifecycle; @@ -27,7 +27,7 @@ describe('Knifecycle', () => { return time; } - async function hashProvider(hash: Record) { + async function hashProvider(hash: Record) { return { service: hash, }; @@ -957,10 +957,11 @@ describe('Knifecycle', () => { inject: [], singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/of/${serviceName}`, - initializer: constant(serviceName, `value_of:${serviceName}`), - }), + async () => async (serviceName) => + location( + constant(serviceName, `value_of:${serviceName}`), + `/path/of/${serviceName}`, + ), ); const wrappedProvider = provider(hashProvider, 'hash', [ 'ENV', @@ -987,17 +988,19 @@ describe('Knifecycle', () => { inject: [], singleton: true, }, - async () => async (serviceName) => ({ - path: '/path/of/debug', - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: 'DEBUG', inject: [], + location: { + url: 'file://path/of/debug', + exportName: 'default', + }, }, async () => 'THE_DEBUG:' + serviceName, ), - }), ); const wrappedProvider = provider(hashProvider, 'hash', [ 'ENV', @@ -1026,9 +1029,8 @@ describe('Knifecycle', () => { type: 'service', singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/to/${serviceName}`, - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'provider', name: serviceName, @@ -1038,10 +1040,13 @@ describe('Knifecycle', () => { : 'hash4' === serviceName ? ['hash3'] : [], + location: { + url: `file://path/to/${serviceName}`, + exportName: 'default', + }, }, hashProvider, ), - }), ), ); $.register(constant('ENV', ENV)); @@ -1073,9 +1078,8 @@ describe('Knifecycle', () => { throw new YError('E_UNMATCHED_DEPENDENCY'); } - return { - path: '/path/of/debug', - initializer: initializer( + return location( + initializer( { type: 'service', name: 'hash2', @@ -1083,7 +1087,9 @@ describe('Knifecycle', () => { }, async () => 'THE_HASH:' + serviceName, ), - }; + 'file://path/of/debug', + 'default', + ); }, ), ); @@ -1101,17 +1107,12 @@ describe('Knifecycle', () => { type: 'service', singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/to/${serviceName}`, - initializer: initializer( - { - type: 'provider', - name: serviceName, - inject: ['ENV', 'time'], - }, - hashProvider, + async () => async (serviceName) => + location( + provider(hashProvider, serviceName, ['ENV', 'time']), + `file://path/to/${serviceName}`, + 'default', ), - }), ), ); const timeServiceStub = jest.fn(timeService); @@ -1141,10 +1142,8 @@ describe('Knifecycle', () => { type: 'service', singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/to/${serviceName}`, - initializer: nullService, - }), + async () => async (serviceName) => + location(nullService, `file://path/to/${serviceName}`, 'default'), ), ); @@ -1168,10 +1167,12 @@ describe('Knifecycle', () => { type: 'service', singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/to/${serviceName}`, - initializer: nullProvider, - }), + async () => async (serviceName) => + location( + nullProvider, + `file://path/to/${serviceName}`, + 'default', + ), ), ); @@ -1193,13 +1194,14 @@ describe('Knifecycle', () => { type: 'service', singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/to/${serviceName}`, - initializer: + async () => async (serviceName) => + location( serviceName === 'undefinedService' ? undefinedService : undefinedProvider, - }), + `file://path/to/${serviceName}`, + 'default', + ), ), ); @@ -1229,17 +1231,19 @@ describe('Knifecycle', () => { inject: ['?ENV'], singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/of/${serviceName}`, - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: serviceName, inject: [], + location: { + url: `file://path/of/${serviceName}`, + exportName: 'default', + }, }, async () => `THE_${serviceName.toUpperCase()}:` + serviceName, ), - }), ), ); @@ -1272,17 +1276,19 @@ describe('Knifecycle', () => { inject: ['?ENV', '?log'], singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/of/${serviceName}`, - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: serviceName, inject: [], + location: { + url: `file://path/of/${serviceName}`, + exportName: 'default', + }, }, async () => `THE_${serviceName.toUpperCase()}:` + serviceName, ), - }), ), ); @@ -1359,17 +1365,19 @@ describe('Knifecycle', () => { inject: ['?ENV', '?log'], singleton: true, }, - async () => async (serviceName) => ({ - path: `/path/of/${serviceName}`, - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: serviceName, inject: ['parentService1'], + location: { + url: `file://path/of/${serviceName}`, + exportName: 'default', + }, }, async () => `THE_${serviceName.toUpperCase()}:` + serviceName, ), - }), ), ); @@ -1428,10 +1436,7 @@ describe('Knifecycle', () => { inject: [], singleton: true, }, - async () => async () => ({ - initializer: 'not_an_initializer', - path: '/path/to/initializer', - }), + async () => async () => 'not_an_initializer', ), ); @@ -1442,11 +1447,11 @@ describe('Knifecycle', () => { expect((err as YError).code).toEqual('E_BAD_AUTOLOADED_INITIALIZER'); expect((err as YError).params).toEqual(['test']); expect(((err as YError).wrappedErrors[0] as YError).code).toEqual( - 'E_AUTOLOADED_INITIALIZER_MISMATCH', + 'E_BAD_AUTOLOADER_RESULT', ); expect(((err as YError).wrappedErrors[0] as YError).params).toEqual([ 'test', - undefined, + 'not_an_initializer', ]); } }); @@ -1460,17 +1465,19 @@ describe('Knifecycle', () => { inject: [], singleton: true, }, - async () => async (serviceName) => ({ - path: '/path/of/debug', - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: 'not-' + serviceName, inject: [], + location: { + url: 'file://path/of/debug', + exportName: 'default', + }, }, async () => 'THE_TEST:' + serviceName, ), - }), ), ); @@ -1499,17 +1506,19 @@ describe('Knifecycle', () => { inject: ['ENV'], singleton: true, }, - async () => async (serviceName) => ({ - path: '/path/of/debug', - initializer: initializer( + async () => async (serviceName) => + initializer( { type: 'service', name: 'DEBUG', inject: [], + location: { + url: 'file://path/of/debug', + exportName: 'default', + }, }, async () => 'THE_DEBUG:' + serviceName, ), - }), ), ); diff --git a/src/index.ts b/src/index.ts index 9fd56ae..083e6d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import { autoService, provider, autoProvider, + location, wrapInitializer, handler, autoHandler, @@ -54,6 +55,7 @@ import type { Provider, Dependencies, DependencyDeclaration, + LocationInformation, ExtraInformation, ParsedDependencyDeclaration, ConstantProperties, @@ -61,11 +63,9 @@ import type { ProviderInitializerBuilder, ProviderProperties, ProviderInitializer, - ProviderInputProperties, ServiceInitializerBuilder, ServiceProperties, ServiceInitializer, - ServiceInputProperties, AsyncInitializerBuilder, AsyncInitializer, PartialAsyncInitializer, @@ -77,7 +77,7 @@ import type { } from './util.js'; import type { BuildInitializer } from './build.js'; import type { FatalErrorService } from './fatalError.js'; -export { initFatalError, initDispose }; + export type { ServiceName, Service, @@ -86,6 +86,7 @@ export type { Provider, Dependencies, DependencyDeclaration, + LocationInformation, ExtraInformation, ParsedDependencyDeclaration, ConstantProperties, @@ -93,11 +94,9 @@ export type { ProviderInitializerBuilder, ProviderProperties, ProviderInitializer, - ProviderInputProperties, ServiceInitializerBuilder, ServiceProperties, ServiceInitializer, - ServiceInputProperties, AsyncInitializerBuilder, AsyncInitializer, PartialAsyncInitializer, @@ -111,6 +110,47 @@ export type { Overrides, }; +export { + SPECIAL_PROPS, + SPECIAL_PROPS_PREFIX, + DECLARATION_SEPARATOR, + OPTIONAL_FLAG, + ALLOWED_INITIALIZER_TYPES, + ALLOWED_SPECIAL_PROPS, + parseInjections, + readFunctionName, + parseName, + Knifecycle, + initializer, + name, + autoName, + type, + inject, + useInject, + mergeInject, + autoInject, + alsoInject, + unInject, + extra, + singleton, + reuseSpecialProps, + wrapInitializer, + constant, + service, + autoService, + provider, + autoProvider, + location, + handler, + autoHandler, + parseDependencyDeclaration, + stringifyDependencyDeclaration, + unwrapInitializerProperties, + initInitializerBuilder, + initFatalError, + initDispose, +}; + export const RUN_DEPENDENT_NAME = '__run__'; export const SYSTEM_DEPENDENT_NAME = '__system__'; export const AUTOLOAD_DEPENDENT_NAME = '__autoloader__'; @@ -126,10 +166,7 @@ export interface Injector> { export interface Autoloader< T extends Initializer>, > { - (name: DependencyDeclaration): Promise<{ - initializer: T; - path: string; - }>; + (name: DependencyDeclaration): Promise; } export type SiloIndex = string; export type BaseInitializerStateDescriptor = { @@ -1176,22 +1213,22 @@ class Knifecycle { return; } - const result = await autoloader(serviceName); + const initializer = await autoloader(serviceName); - if ( - typeof result !== 'object' || - !('initializer' in result) || - !('path' in result) - ) { - throw new YError('E_BAD_AUTOLOADER_RESULT', serviceName, result); + if (!['object', 'function'].includes(typeof initializer)) { + throw new YError( + 'E_BAD_AUTOLOADER_RESULT', + serviceName, + initializer, + ); } - const { initializer, path } = result; - debug( `${[...parentsNames, serviceName].join( '->', - )}: Loaded the initializer at path ${path}...`, + )}: Loaded the initializer in location ${ + initializer[SPECIAL_PROPS.LOCATION]?.url || 'no_location' + }...`, ); if (initializer[SPECIAL_PROPS.NAME] !== serviceName) { @@ -1363,44 +1400,6 @@ class Knifecycle { } } -export { - SPECIAL_PROPS, - SPECIAL_PROPS_PREFIX, - DECLARATION_SEPARATOR, - OPTIONAL_FLAG, - ALLOWED_INITIALIZER_TYPES, - ALLOWED_SPECIAL_PROPS, - parseInjections, - readFunctionName, - parseName, - Knifecycle, - initializer, - name, - autoName, - type, - inject, - useInject, - mergeInject, - autoInject, - alsoInject, - unInject, - extra, - singleton, - reuseSpecialProps, - wrapInitializer, - constant, - service, - autoService, - provider, - autoProvider, - handler, - autoHandler, - parseDependencyDeclaration, - stringifyDependencyDeclaration, - unwrapInitializerProperties, - initInitializerBuilder, -}; - function _applyShapes(shapes, serviceName) { return shapes.reduce((shapedService, shape) => { if (shapedService) { diff --git a/src/util.test.ts b/src/util.test.ts index c4325fa..2753c33 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -26,6 +26,7 @@ import { autoHandler, SPECIAL_PROPS, unInject, + location, } from './util.js'; import type { Provider } from './util.js'; import type { Dependencies, ServiceInitializer } from './index.js'; @@ -346,6 +347,36 @@ describe('autoInject', () => { expect(newInitializer[SPECIAL_PROPS.INJECT]).toEqual(dependencies); }); + test('should allow to decorate a service initializer with its location', () => { + async function baseService({ ENV, mysql: db }) { + return { my: 'service' }; + } + + const newInitializer = location( + autoService(baseService), + 'file://here', + 'prop', + ); + + expect(newInitializer).not.toEqual(baseService); + expect(newInitializer[SPECIAL_PROPS.LOCATION]).toEqual({ + url: 'file://here', + exportName: 'prop', + }); + }); + + test('should allow to decorate a constant initializer with its location', () => { + const baseConstant = constant('test', 'test'); + const newConstant = location(baseConstant, 'file://here'); + + expect(newConstant).not.toEqual(baseConstant); + expect(newConstant[SPECIAL_PROPS.TYPE]).toEqual('constant'); + expect(newConstant[SPECIAL_PROPS.LOCATION]).toEqual({ + url: 'file://here', + exportName: 'default', + }); + }); + test('should allow to decorate an initializer with optional dependencies', () => { const noop = () => undefined; const baseProvider = @@ -709,9 +740,7 @@ describe('extra', () => { ); expect(newInitializer).not.toEqual(aProviderInitializer); - expect(newInitializer[SPECIAL_PROPS.EXTRA]).not.toBe( - baseExtraInformations, - ); + expect(newInitializer[SPECIAL_PROPS.EXTRA]).not.toBe(baseExtraInformations); expect(newInitializer[SPECIAL_PROPS.EXTRA]).not.toEqual( additionalExtraInformations, ); diff --git a/src/util.ts b/src/util.ts index ac4b3f6..466cbfc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -56,6 +56,10 @@ export type Provider = { export type Dependencies = { [name: string]: S }; export type DependencyName = string; export type DependencyDeclaration = string; +export type LocationInformation = { + url: string; + exportName: string; +}; export type ExtraInformation = any; export type ParsedDependencyDeclaration = { serviceName: string; @@ -67,6 +71,7 @@ export type ConstantProperties = { $type: 'constant'; $name: DependencyName; $singleton: true; + $location?: LocationInformation; }; export type ConstantInitializer = ConstantProperties & { $value: S; @@ -83,6 +88,7 @@ export type ProviderProperties = { $name: DependencyName; $inject?: DependencyDeclaration[]; $singleton?: boolean; + $location?: LocationInformation; $extra?: ExtraInformation; }; export type ProviderInitializer = @@ -93,6 +99,7 @@ export type ProviderInputProperties = { name: DependencyName; inject?: DependencyDeclaration[]; singleton?: boolean; + location?: LocationInformation; extra?: ExtraInformation; }; @@ -105,6 +112,7 @@ export type ServiceProperties = { $name: DependencyName; $inject?: DependencyDeclaration[]; $singleton?: boolean; + $location?: LocationInformation; $extra?: ExtraInformation; }; export type ServiceInitializer = @@ -115,6 +123,7 @@ export type ServiceInputProperties = { name: DependencyName; inject?: DependencyDeclaration[]; singleton?: boolean; + location?: LocationInformation; extra?: ExtraInformation; }; @@ -166,6 +175,7 @@ export const SPECIAL_PROPS = { 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`, }; @@ -1072,6 +1082,86 @@ export function singleton, S>( 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 (initializer[SPECIAL_PROPS.TYPE] === 'constant') { + return { + $name: initializer[SPECIAL_PROPS.NAME], + $type: 'constant', + $value: initializer[SPECIAL_PROPS.VALUE], + $singleton: true, + $location: { url, exportName }, + }; + } + + const uniqueInitializer = reuseSpecialProps( + initializer, + initializer as ServiceInitializerBuilder, + { + [SPECIAL_PROPS.LOCATION]: { url, exportName }, + }, + ); + + debug('Set an initializer location as:', { url, exportName }); + + return uniqueInitializer; +} + /** * Decorator to set an initializer name. * @param {String} name