import { SPECIAL_PROPS, parseDependencyDeclaration, initializer, location, constant, type InitializerTypes, initializerBuilderIsOfType, pickInitializerBuilderProp, type Service, } from './util.js'; import { buildInitializationSequence } from './sequence.js'; import { type Overrides, type Autoloader } from './index.js'; import { type DependencyDeclaration, type Initializer, type Dependencies, type LocationInformation, } from './util.js'; import { pickOverridenName } from './overrides.js'; import { type Injector } from './injector.js'; export const MANAGED_SERVICES = [ '$fatalError', '$dispose', '$instance', '$ready', '$injector', ]; export const DEFAULT_BUILD_CONSTANT_FILTER: BuildConstantFilter = () => false; export const DEFAULT_BUILD_INJECTED_SERVICE_FILTER: BuildInjectedServiceFilter = () => false; export type BuildConstantFilter = (name: string) => boolean; export type BuildInjectedServiceFilter = (name: string) => boolean; interface DependencyTreeNode { __name: string; __childNodes?: DependencyTreeNode[]; __initializer: Initializer>; __inject: DependencyDeclaration[]; __type: InitializerTypes; __initializerName: string; __location: LocationInformation | 'managed' | 'no_location'; __parentsNames: string[]; } export type BuildInitializer = ( dependencies: DependencyDeclaration[], ) => Promise; /* Architecture Note #2: Build Using Knifecycle only makes sense for monoliths. For some targets like serverless functions, a better approach is to simply build a raw initialization function. For the build to work, we need: - a hash of various constants that may be used. - an autoloader that resolves dependencies names to its actual initializer - the dependencies list you want to initialize */ export default location( initializer( { name: 'buildInitializer', type: 'service', inject: [ '$autoload', '$overrides', '$injector', '?BUILD_CONSTANT_FILTER', '?BUILD_INJECTED_SERVICE_FILTER', ], }, initInitializerBuilder, ), import.meta.url, ); /** * Instantiate the initializer builder service * @param {Object} services * The services to inject * @param {Object} services.$autoload * The dependencies autoloader * @return {Promise} * A promise of the buildInitializer function * @example * import initInitializerBuilder from 'knifecycle/dist/build'; * * const buildInitializer = await initInitializerBuilder({ * $autoload: async () => {}, * }); */ async function initInitializerBuilder({ $autoload, $overrides, $injector, BUILD_CONSTANT_FILTER = DEFAULT_BUILD_CONSTANT_FILTER, BUILD_INJECTED_SERVICE_FILTER = DEFAULT_BUILD_INJECTED_SERVICE_FILTER, }: { $autoload: Autoloader>>; $overrides: Overrides; $injector: Injector>; BUILD_CONSTANT_FILTER?: BuildConstantFilter; BUILD_INJECTED_SERVICE_FILTER?: BuildInjectedServiceFilter; }) { return buildInitializer; /** * Create a JavaScript module that initialize * a set of dependencies with hardcoded * import/awaits. * @param {String[]} dependencies * The main dependencies * @return {Promise} * The JavaScript module content * @example * import initInitializerBuilder from 'knifecycle/dist/build'; * * const buildInitializer = await initInitializerBuilder({ * $autoload: async () => {}, * }); * * const content = await buildInitializer(['entryPoint']); */ async function buildInitializer( dependencies: DependencyDeclaration[], ): Promise { const dependencyTrees = await Promise.all( dependencies.map((dependency) => buildDependencyTree( { BUILD_CONSTANT_FILTER, BUILD_INJECTED_SERVICE_FILTER, $autoload, $overrides, $injector, }, dependency, [], ), ), ); const dependenciesHash = buildDependenciesHash( dependencyTrees.filter(identity) as DependencyTreeNode[], ); const batches = buildInitializationSequence({ __name: 'main', __childNodes: dependencyTrees.filter(identity) as DependencyTreeNode[], }); batches.pop(); return ` // Automatically generated by \`knifecycle\` import { initFatalError } from 'knifecycle'; const batchsDisposers = []; async function $dispose() { for(const batchDisposers of batchsDisposers.reverse()) { await Promise.all( batchDisposers .map(batchDisposer => batchDisposer()) ); } } let finalServicesHash = {}; let resolveReady; const $ready = new Promise((resolve) => { resolveReady = resolve; }); const $instance = { destroy: $dispose, register: async () => { throw new Error('E_NO_REGISTER_FOR_BUILT_CODE'); }, toMermaidGraph: () => { throw new Error('E_GRAPH_NOT_IMPLEMENTED_FOR_BUILT_CODE'); }, }; const $injector = async (services) => { await $ready; return services.reduce((acc, service) => ({ ...acc, [service]: finalServicesHash[service], }), {}); }; ${batches .map( (batch, index) => ` // Definition batch #${index}${batch .map((name) => { if (BUILD_INJECTED_SERVICE_FILTER(name)) { return ` // The service "${name}" will be injected later`; } if (dependenciesHash[name].__location === 'managed') { return ` // The service "${name}" is a managed service`; } if (dependenciesHash[name].__location === 'no_location') { if ( initializerBuilderIsOfType( 'constant', dependenciesHash[name].__initializer, ) && dependenciesHash[name].__location === 'no_location' ) { return ` const ${name} = ${JSON.stringify( dependenciesHash[name].__initializer.$value, null, 2, )};`; } return ` // No location for "${name}" service const ${name} = undefined;`; } return ` 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('')}`, ) .join('\n')} export async function initialize(services = {}) { const $fatalError = await initFatalError(); ${batches .map( (batch, index) => ` // Initialization batch #${index} batchsDisposers[${index}] = []; const batch${index} = {${batch .map((name) => { if (BUILD_INJECTED_SERVICE_FILTER(name)) { return ` ${name}: '${name}' in services ? Promise.resolve(services['${name}']) : Promise.reject(new Error('E_INJECTED_SERVICE_LACKS')),`; } if ( MANAGED_SERVICES.includes(name) || initializerBuilderIsOfType( 'constant', dependenciesHash[name].__initializer, ) ) { return ` ${name}: Promise.resolve(${name}),`; } return ` ${name}: ${dependenciesHash[name].__initializerName}({${ dependenciesHash[name].__inject ? `${dependenciesHash[name].__inject .map(parseDependencyDeclaration) .map( ({ serviceName, mappedName }) => ` ${serviceName}: services['${pickOverridenName($overrides, [ ...dependenciesHash[name].__parentsNames, mappedName, ])}'],`, ) .join('')}` : '' } })${ 'provider' === dependenciesHash[name].__type ? `.then(provider => { if(provider.dispose) { batchsDisposers[${index}].push(provider.dispose); } if(provider.fatalErrorPromise) { $fatalError.registerErrorPromise(provider.fatalErrorPromise); } return provider.service; })` : '' },`; }) .join('')} }; await Promise.all( Object.keys(batch${index}) .map(key => batch${index}[key]) ); ${batch .map((name) => { return ` services['${name}'] = await batch${index}['${name}'];`; }) .join('')} `, ) .join('')} finalServicesHash = {${dependencies .map(parseDependencyDeclaration) .map( ({ serviceName, mappedName }) => ` ${serviceName}: services['${pickOverridenName($overrides, [ mappedName, ])}'],`, ) .join('')} }; $instance.registered = () => Object.keys(finalServicesHash); resolveReady(); return finalServicesHash; } `; } } async function buildDependencyTree( { $autoload, $overrides, $injector, BUILD_CONSTANT_FILTER, BUILD_INJECTED_SERVICE_FILTER, }: { $autoload: Autoloader>>; $overrides: Overrides; $injector: Injector>; BUILD_CONSTANT_FILTER: BuildConstantFilter; BUILD_INJECTED_SERVICE_FILTER: BuildInjectedServiceFilter; }, dependencyDeclaration: string, parentsNames: string[], ): Promise { const { mappedName, optional } = parseDependencyDeclaration( dependencyDeclaration, ); const finalName = pickOverridenName($overrides, [ ...parentsNames, mappedName, ]); if (BUILD_INJECTED_SERVICE_FILTER(finalName)) { return null; } if (BUILD_CONSTANT_FILTER(finalName)) { return { __name: finalName, __initializer: constant( finalName, (await $injector([finalName]))[finalName], ), __inject: [], __type: 'constant', __initializerName: 'init' + upperCaseFirst(finalName.slice(1)), __location: 'no_location', __childNodes: [], __parentsNames: [...parentsNames, finalName], }; } if (MANAGED_SERVICES.includes(finalName)) { return { __name: finalName, __initializer: (async () => undefined) as Initializer< Service, Dependencies >, __inject: [], __type: 'constant', __initializerName: 'init' + upperCaseFirst(finalName.slice(1)), __location: 'managed', __childNodes: [], __parentsNames: [...parentsNames, finalName], }; } try { const initializer = await $autoload(finalName); const inject = pickInitializerBuilderProp(initializer, '$inject') || []; const node: DependencyTreeNode = { __name: finalName, __initializer: initializer, __inject: inject, __type: initializer && initializer[SPECIAL_PROPS.TYPE] ? initializer[SPECIAL_PROPS.TYPE] : 'provider', __initializerName: 'init' + upperCaseFirst(finalName), __location: initializer[SPECIAL_PROPS.LOCATION] || 'no_location', __childNodes: [], __parentsNames: [...parentsNames, finalName], }; if (inject.length) { const childNodes: (DependencyTreeNode | null)[] = await Promise.all( inject.map((childDependencyDeclaration) => buildDependencyTree( { BUILD_CONSTANT_FILTER, BUILD_INJECTED_SERVICE_FILTER, $autoload, $overrides, $injector, }, childDependencyDeclaration, [...parentsNames, finalName], ), ), ); node.__childNodes = childNodes.filter(identity) as DependencyTreeNode[]; return node; } else { return node; } } catch (err) { if (optional) { return null; } throw err; } } function buildDependenciesHash( dependencyTrees: DependencyTreeNode[], hash: Record = {}, ): Record { return dependencyTrees.reduce( (hash, tree) => buildHashFromNode(tree, hash), hash, ); } function buildHashFromNode( node: DependencyTreeNode, hash: Record = {}, ): Record { const nodeIsALeaf = !(node.__childNodes && node.__childNodes.length); hash[node.__name] = node; if (nodeIsALeaf) { return hash; } (node?.__childNodes || []).forEach((childNode) => { hash = buildHashFromNode(childNode, hash); }); return hash; } function identity(a: T): T { return a; } function upperCaseFirst(str: string): string { return str[0].toUpperCase() + str.slice(1); }