From 27b294e2c1e8629d498e1372fe086ee6c0858f82 Mon Sep 17 00:00:00 2001 From: Dan Kadera Date: Wed, 13 Nov 2024 12:09:35 +0100 Subject: [PATCH] v1 rewrite --- .github/workflows/ci.yml | 2 + Makefile | 4 + README.md | 100 ++- core/cli/Makefile | 10 +- core/cli/dicc.yaml | 18 +- core/cli/package.json | 2 +- core/cli/src/analysis/autowiring.ts | 258 +++++++ core/cli/src/analysis/containerAnalyser.ts | 194 +++++ core/cli/src/analysis/di.ts | 10 + core/cli/src/analysis/index.ts | 5 + .../analysis/reflection/builderReflection.ts | 34 + .../analysis/reflection/containerReflector.ts | 21 + .../analysis/reflection/externalReflection.ts | 34 + .../reflection/index.ts | 2 +- core/cli/src/analysis/reflection/types.ts | 15 + core/cli/src/analysis/results/arguments.ts | 102 +++ core/cli/src/analysis/results/async.ts | 25 + core/cli/src/analysis/results/call.ts | 8 + core/cli/src/analysis/results/container.ts | 23 + core/cli/src/analysis/results/index.ts | 5 + core/cli/src/analysis/results/service.ts | 106 +++ core/cli/src/analysis/serviceAnalyser.ts | 571 +++++++++++++++ core/cli/src/autowiring.ts | 400 ----------- core/cli/src/bootstrap.ts | 117 --- core/cli/src/checker.ts | 162 ----- core/cli/src/cli.ts | 31 - core/cli/src/{ => cli}/argv.ts | 0 core/cli/src/cli/bootstrap/container.ts | 296 ++++++++ core/cli/src/cli/bootstrap/definitions.ts | 68 ++ core/cli/src/cli/bootstrap/index.ts | 1 + core/cli/src/cli/di.ts | 1 + core/cli/src/cli/dicc.ts | 13 + core/cli/src/compiler.ts | 673 ------------------ core/cli/src/compiler/compiler.ts | 66 ++ core/cli/src/compiler/containerCompiler.ts | 113 +++ core/cli/src/compiler/di.ts | 3 + core/cli/src/compiler/index.ts | 4 + core/cli/src/compiler/serviceCompiler.ts | 602 ++++++++++++++++ core/cli/src/compiler/utils.ts | 46 ++ core/cli/src/compiler/writerFactory.ts | 11 + core/cli/src/{v2 => }/config/configLoader.ts | 0 core/cli/src/{v2 => }/config/index.ts | 0 core/cli/src/{v2 => }/config/types.ts | 0 core/cli/src/configLoader.ts | 57 -- core/cli/src/container/builderMap.ts | 43 ++ core/cli/src/container/containerBuilder.ts | 253 +++++++ core/cli/src/container/di.ts | 2 + core/cli/src/container/events.ts | 24 + core/cli/src/container/index.ts | 4 + core/cli/src/containerBuilder.ts | 175 ----- core/cli/src/definitionScanner.ts | 484 ------------- core/cli/src/definitions.ts | 50 -- .../cli/src/definitions/argumentDefinition.ts | 21 + .../src/definitions/autoImplementedMethod.ts | 20 + .../cli/src/definitions/callableDefinition.ts | 14 + .../definitions/decoratorDefinition.ts | 18 +- core/cli/src/definitions/di.ts | 1 + core/cli/src/definitions/events.ts | 15 + core/cli/src/definitions/factoryDefinition.ts | 18 + core/cli/src/{v2 => }/definitions/index.ts | 6 +- .../resourceScanner.ts | 23 +- .../service/abstractLocalServiceDefinition.ts | 9 +- .../service/abstractServiceDefinition.ts | 3 - .../service/explicitServiceDefinition.ts | 15 +- .../service/foreignServiceDefinition.ts | 14 +- .../service/implicitServiceDefinition.ts | 0 .../src/{v2 => }/definitions/service/index.ts | 0 .../src/{v2 => }/definitions/service/types.ts | 10 +- core/cli/src/definitions/types.ts | 178 +++++ core/cli/src/dicc.ts | 118 --- core/cli/src/errors.ts | 21 - core/cli/src/{v2 => }/errors/errors.ts | 0 core/cli/src/{v2 => }/errors/helpers.ts | 0 core/cli/src/{v2 => }/errors/index.ts | 0 core/cli/src/{v2 => }/errors/reporters.ts | 9 +- core/cli/src/{v2 => }/errors/types.ts | 0 core/cli/src/{v2 => }/events/asyncEvent.ts | 0 core/cli/src/{v2 => }/events/event.ts | 0 .../src/{v2 => }/events/eventDispatcher.ts | 0 core/cli/src/{v2 => }/events/index.ts | 0 core/cli/src/{v2 => }/events/types.ts | 0 .../{v2 => }/extensions/compilerExtension.ts | 3 - .../extensions/decoratorsExtension.ts | 10 +- core/cli/src/extensions/di.ts | 3 + .../{v2 => }/extensions/extensionLoader.ts | 11 +- core/cli/src/{v2 => }/extensions/helpers.ts | 4 + core/cli/src/{v2 => }/extensions/index.ts | 0 .../{v2 => }/extensions/servicesExtension.ts | 60 +- core/cli/src/{v2 => }/extensions/types.ts | 3 +- core/cli/src/referenceResolver.ts | 70 -- core/cli/src/sourceFiles.ts | 78 -- core/cli/src/typeHelper.ts | 406 ----------- core/cli/src/types.ts | 130 ---- core/cli/src/utils/di.ts | 3 + core/cli/src/{v2 => }/utils/helpers.ts | 33 + core/cli/src/{v2 => }/utils/index.ts | 0 core/cli/src/{v2 => }/utils/moduleResolver.ts | 0 .../src/{v2 => }/utils/referenceResolver.ts | 10 +- core/cli/src/{v2 => }/utils/typeHelper.ts | 79 +- core/cli/src/{v2 => }/utils/types.ts | 0 core/cli/src/v2/compiler/autowiring.ts | 530 -------------- core/cli/src/v2/compiler/compilation.ts | 44 -- core/cli/src/v2/compiler/compiler.ts | 70 -- core/cli/src/v2/compiler/containerCompiler.ts | 103 --- .../src/v2/compiler/foreignServiceCompiler.ts | 66 -- core/cli/src/v2/compiler/importMapCompiler.ts | 26 - core/cli/src/v2/compiler/index.ts | 11 - core/cli/src/v2/compiler/lazy/index.ts | 5 - core/cli/src/v2/compiler/lazy/lazyArgs.ts | 55 -- core/cli/src/v2/compiler/lazy/lazyCompiler.ts | 56 -- core/cli/src/v2/compiler/lazy/lazyTemplate.ts | 14 - core/cli/src/v2/compiler/lazy/lazyWriter.ts | 55 -- core/cli/src/v2/compiler/lazy/types.ts | 5 - .../src/v2/compiler/localServiceCompiler.ts | 422 ----------- core/cli/src/v2/compiler/serviceCompiler.ts | 123 ---- core/cli/src/v2/compiler/typeMapCompiler.ts | 132 ---- core/cli/src/v2/container/containerBuilder.ts | 21 - core/cli/src/v2/container/decoratorMap.ts | 54 -- core/cli/src/v2/container/importMap.ts | 65 -- core/cli/src/v2/container/index.ts | 7 - .../container/reflection/builderReflection.ts | 42 -- .../reflection/containerReflector.ts | 22 - .../reflection/externalReflection.ts | 49 -- core/cli/src/v2/container/reflection/types.ts | 16 - core/cli/src/v2/container/serviceMap.ts | 171 ----- core/cli/src/v2/container/typeMap.ts | 35 - core/cli/src/v2/definitions/argument.ts | 12 - .../v2/definitions/autoImplementedMethod.ts | 18 - core/cli/src/v2/definitions/callable.ts | 14 - .../src/v2/definitions/factoryDefinition.ts | 16 - core/cli/src/v2/definitions/types.ts | 125 ---- core/cli/src/v2/definitions/values.ts | 0 core/cli/tests/.gitignore | 1 + core/cli/tests/explicit/definitions.ts | 86 +++ core/cli/tests/explicit/dicc.yaml | 5 + core/cli/tests/explicit/expectedContainer.ts | 59 ++ core/cli/tests/explicit/test.mjs | 8 + core/cli/tests/explicit/tsconfig.json | 6 + core/cli/tests/generators/definitions.ts | 38 + core/cli/tests/generators/dicc.yaml | 5 + .../cli/tests/generators/expectedContainer.ts | 32 + core/cli/tests/generators/test.mjs | 8 + core/cli/tests/generators/tsconfig.json | 6 + core/cli/tests/hooks/definitions.ts | 57 ++ core/cli/tests/hooks/dicc.yaml | 5 + core/cli/tests/hooks/expectedContainer.ts | 92 +++ core/cli/tests/hooks/test.mjs | 8 + core/cli/tests/hooks/tsconfig.json | 6 + core/cli/tests/implicit/definitions.ts | 100 +++ core/cli/tests/implicit/dicc.yaml | 5 + core/cli/tests/implicit/expectedContainer.ts | 93 +++ core/cli/tests/implicit/test.mjs | 8 + core/cli/tests/implicit/tsconfig.json | 6 + core/cli/tests/iterables/definitions.ts | 126 ++++ core/cli/tests/iterables/dicc.yaml | 5 + core/cli/tests/iterables/expectedContainer.ts | 179 +++++ core/cli/tests/iterables/test.mjs | 8 + core/cli/tests/iterables/tsconfig.json | 6 + core/cli/tests/merging/childDefinitions.ts | 8 + core/cli/tests/merging/common.ts | 19 + core/cli/tests/merging/dicc.yaml | 9 + .../tests/merging/expectedChildContainer.ts | 42 ++ .../tests/merging/expectedParentContainer.ts | 67 ++ core/cli/tests/merging/parentDefinitions.ts | 35 + core/cli/tests/merging/test.mjs | 23 + core/cli/tests/merging/tsconfig.json | 6 + core/cli/tests/utils.mjs | 82 +++ core/dicc/src/abstractContainer.ts | 16 +- core/dicc/src/asyncContext.ts | 24 + core/dicc/src/index.ts | 1 + core/dicc/src/types.ts | 6 +- core/dicc/src/utils.ts | 14 + docs/README.md | 15 +- docs/recipes/01-express.md | 20 +- docs/user/01-intro-to-di.md | 5 +- docs/user/02-intro-to-dicc.md | 306 +++----- docs/user/03-implicit-services.md | 98 +++ docs/user/03-simple-services.md | 37 - docs/user/04-explicit-definitions.md | 30 +- docs/user/05-injecting-dependencies.md | 19 +- docs/user/06-auto-factories.md | 45 +- docs/user/07-service-decorators.md | 28 +- docs/user/08-container-parameters.md | 85 --- docs/user/08-merging-containers.md | 41 ++ ...lation.md => 09-config-and-compilation.md} | 29 +- docs/user/09-merging-containers.md | 33 - 186 files changed, 5125 insertions(+), 6024 deletions(-) create mode 100644 core/cli/src/analysis/autowiring.ts create mode 100644 core/cli/src/analysis/containerAnalyser.ts create mode 100644 core/cli/src/analysis/di.ts create mode 100644 core/cli/src/analysis/index.ts create mode 100644 core/cli/src/analysis/reflection/builderReflection.ts create mode 100644 core/cli/src/analysis/reflection/containerReflector.ts create mode 100644 core/cli/src/analysis/reflection/externalReflection.ts rename core/cli/src/{v2/container => analysis}/reflection/index.ts (100%) create mode 100644 core/cli/src/analysis/reflection/types.ts create mode 100644 core/cli/src/analysis/results/arguments.ts create mode 100644 core/cli/src/analysis/results/async.ts create mode 100644 core/cli/src/analysis/results/call.ts create mode 100644 core/cli/src/analysis/results/container.ts create mode 100644 core/cli/src/analysis/results/index.ts create mode 100644 core/cli/src/analysis/results/service.ts create mode 100644 core/cli/src/analysis/serviceAnalyser.ts delete mode 100644 core/cli/src/autowiring.ts delete mode 100644 core/cli/src/bootstrap.ts delete mode 100644 core/cli/src/checker.ts delete mode 100644 core/cli/src/cli.ts rename core/cli/src/{ => cli}/argv.ts (100%) create mode 100644 core/cli/src/cli/bootstrap/container.ts create mode 100644 core/cli/src/cli/bootstrap/definitions.ts create mode 100644 core/cli/src/cli/bootstrap/index.ts create mode 100644 core/cli/src/cli/di.ts create mode 100644 core/cli/src/cli/dicc.ts delete mode 100644 core/cli/src/compiler.ts create mode 100644 core/cli/src/compiler/compiler.ts create mode 100644 core/cli/src/compiler/containerCompiler.ts create mode 100644 core/cli/src/compiler/di.ts create mode 100644 core/cli/src/compiler/index.ts create mode 100644 core/cli/src/compiler/serviceCompiler.ts create mode 100644 core/cli/src/compiler/utils.ts create mode 100644 core/cli/src/compiler/writerFactory.ts rename core/cli/src/{v2 => }/config/configLoader.ts (100%) rename core/cli/src/{v2 => }/config/index.ts (100%) rename core/cli/src/{v2 => }/config/types.ts (100%) delete mode 100644 core/cli/src/configLoader.ts create mode 100644 core/cli/src/container/builderMap.ts create mode 100644 core/cli/src/container/containerBuilder.ts create mode 100644 core/cli/src/container/di.ts create mode 100644 core/cli/src/container/events.ts create mode 100644 core/cli/src/container/index.ts delete mode 100644 core/cli/src/containerBuilder.ts delete mode 100644 core/cli/src/definitionScanner.ts delete mode 100644 core/cli/src/definitions.ts create mode 100644 core/cli/src/definitions/argumentDefinition.ts create mode 100644 core/cli/src/definitions/autoImplementedMethod.ts create mode 100644 core/cli/src/definitions/callableDefinition.ts rename core/cli/src/{v2 => }/definitions/decoratorDefinition.ts (66%) create mode 100644 core/cli/src/definitions/di.ts create mode 100644 core/cli/src/definitions/events.ts create mode 100644 core/cli/src/definitions/factoryDefinition.ts rename core/cli/src/{v2 => }/definitions/index.ts (55%) rename core/cli/src/{v2/compiler => definitions}/resourceScanner.ts (92%) rename core/cli/src/{v2 => }/definitions/service/abstractLocalServiceDefinition.ts (74%) rename core/cli/src/{v2 => }/definitions/service/abstractServiceDefinition.ts (90%) rename core/cli/src/{v2 => }/definitions/service/explicitServiceDefinition.ts (73%) rename core/cli/src/{v2 => }/definitions/service/foreignServiceDefinition.ts (61%) rename core/cli/src/{v2 => }/definitions/service/implicitServiceDefinition.ts (100%) rename core/cli/src/{v2 => }/definitions/service/index.ts (100%) rename core/cli/src/{v2 => }/definitions/service/types.ts (85%) create mode 100644 core/cli/src/definitions/types.ts delete mode 100644 core/cli/src/dicc.ts delete mode 100644 core/cli/src/errors.ts rename core/cli/src/{v2 => }/errors/errors.ts (100%) rename core/cli/src/{v2 => }/errors/helpers.ts (100%) rename core/cli/src/{v2 => }/errors/index.ts (100%) rename core/cli/src/{v2 => }/errors/reporters.ts (97%) rename core/cli/src/{v2 => }/errors/types.ts (100%) rename core/cli/src/{v2 => }/events/asyncEvent.ts (100%) rename core/cli/src/{v2 => }/events/event.ts (100%) rename core/cli/src/{v2 => }/events/eventDispatcher.ts (100%) rename core/cli/src/{v2 => }/events/index.ts (100%) rename core/cli/src/{v2 => }/events/types.ts (100%) rename core/cli/src/{v2 => }/extensions/compilerExtension.ts (91%) rename core/cli/src/{v2 => }/extensions/decoratorsExtension.ts (83%) create mode 100644 core/cli/src/extensions/di.ts rename core/cli/src/{v2 => }/extensions/extensionLoader.ts (90%) rename core/cli/src/{v2 => }/extensions/helpers.ts (94%) rename core/cli/src/{v2 => }/extensions/index.ts (100%) rename core/cli/src/{v2 => }/extensions/servicesExtension.ts (80%) rename core/cli/src/{v2 => }/extensions/types.ts (95%) delete mode 100644 core/cli/src/referenceResolver.ts delete mode 100644 core/cli/src/sourceFiles.ts delete mode 100644 core/cli/src/typeHelper.ts delete mode 100644 core/cli/src/types.ts create mode 100644 core/cli/src/utils/di.ts rename core/cli/src/{v2 => }/utils/helpers.ts (63%) rename core/cli/src/{v2 => }/utils/index.ts (100%) rename core/cli/src/{v2 => }/utils/moduleResolver.ts (100%) rename core/cli/src/{v2 => }/utils/referenceResolver.ts (89%) rename core/cli/src/{v2 => }/utils/typeHelper.ts (83%) rename core/cli/src/{v2 => }/utils/types.ts (100%) delete mode 100644 core/cli/src/v2/compiler/autowiring.ts delete mode 100644 core/cli/src/v2/compiler/compilation.ts delete mode 100644 core/cli/src/v2/compiler/compiler.ts delete mode 100644 core/cli/src/v2/compiler/containerCompiler.ts delete mode 100644 core/cli/src/v2/compiler/foreignServiceCompiler.ts delete mode 100644 core/cli/src/v2/compiler/importMapCompiler.ts delete mode 100644 core/cli/src/v2/compiler/index.ts delete mode 100644 core/cli/src/v2/compiler/lazy/index.ts delete mode 100644 core/cli/src/v2/compiler/lazy/lazyArgs.ts delete mode 100644 core/cli/src/v2/compiler/lazy/lazyCompiler.ts delete mode 100644 core/cli/src/v2/compiler/lazy/lazyTemplate.ts delete mode 100644 core/cli/src/v2/compiler/lazy/lazyWriter.ts delete mode 100644 core/cli/src/v2/compiler/lazy/types.ts delete mode 100644 core/cli/src/v2/compiler/localServiceCompiler.ts delete mode 100644 core/cli/src/v2/compiler/serviceCompiler.ts delete mode 100644 core/cli/src/v2/compiler/typeMapCompiler.ts delete mode 100644 core/cli/src/v2/container/containerBuilder.ts delete mode 100644 core/cli/src/v2/container/decoratorMap.ts delete mode 100644 core/cli/src/v2/container/importMap.ts delete mode 100644 core/cli/src/v2/container/index.ts delete mode 100644 core/cli/src/v2/container/reflection/builderReflection.ts delete mode 100644 core/cli/src/v2/container/reflection/containerReflector.ts delete mode 100644 core/cli/src/v2/container/reflection/externalReflection.ts delete mode 100644 core/cli/src/v2/container/reflection/types.ts delete mode 100644 core/cli/src/v2/container/serviceMap.ts delete mode 100644 core/cli/src/v2/container/typeMap.ts delete mode 100644 core/cli/src/v2/definitions/argument.ts delete mode 100644 core/cli/src/v2/definitions/autoImplementedMethod.ts delete mode 100644 core/cli/src/v2/definitions/callable.ts delete mode 100644 core/cli/src/v2/definitions/factoryDefinition.ts delete mode 100644 core/cli/src/v2/definitions/types.ts delete mode 100644 core/cli/src/v2/definitions/values.ts create mode 100644 core/cli/tests/.gitignore create mode 100644 core/cli/tests/explicit/definitions.ts create mode 100644 core/cli/tests/explicit/dicc.yaml create mode 100644 core/cli/tests/explicit/expectedContainer.ts create mode 100644 core/cli/tests/explicit/test.mjs create mode 100644 core/cli/tests/explicit/tsconfig.json create mode 100644 core/cli/tests/generators/definitions.ts create mode 100644 core/cli/tests/generators/dicc.yaml create mode 100644 core/cli/tests/generators/expectedContainer.ts create mode 100644 core/cli/tests/generators/test.mjs create mode 100644 core/cli/tests/generators/tsconfig.json create mode 100644 core/cli/tests/hooks/definitions.ts create mode 100644 core/cli/tests/hooks/dicc.yaml create mode 100644 core/cli/tests/hooks/expectedContainer.ts create mode 100644 core/cli/tests/hooks/test.mjs create mode 100644 core/cli/tests/hooks/tsconfig.json create mode 100644 core/cli/tests/implicit/definitions.ts create mode 100644 core/cli/tests/implicit/dicc.yaml create mode 100644 core/cli/tests/implicit/expectedContainer.ts create mode 100644 core/cli/tests/implicit/test.mjs create mode 100644 core/cli/tests/implicit/tsconfig.json create mode 100644 core/cli/tests/iterables/definitions.ts create mode 100644 core/cli/tests/iterables/dicc.yaml create mode 100644 core/cli/tests/iterables/expectedContainer.ts create mode 100644 core/cli/tests/iterables/test.mjs create mode 100644 core/cli/tests/iterables/tsconfig.json create mode 100644 core/cli/tests/merging/childDefinitions.ts create mode 100644 core/cli/tests/merging/common.ts create mode 100644 core/cli/tests/merging/dicc.yaml create mode 100644 core/cli/tests/merging/expectedChildContainer.ts create mode 100644 core/cli/tests/merging/expectedParentContainer.ts create mode 100644 core/cli/tests/merging/parentDefinitions.ts create mode 100644 core/cli/tests/merging/test.mjs create mode 100644 core/cli/tests/merging/tsconfig.json create mode 100644 core/cli/tests/utils.mjs create mode 100644 core/dicc/src/asyncContext.ts create mode 100644 docs/user/03-implicit-services.md delete mode 100644 docs/user/03-simple-services.md delete mode 100644 docs/user/08-container-parameters.md create mode 100644 docs/user/08-merging-containers.md rename docs/user/{10-config-and-compilation.md => 09-config-and-compilation.md} (72%) delete mode 100644 docs/user/09-merging-containers.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55e153f..4ef891d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: run: npm ci - name: Build packages run: make + - name: Run tests + run: make tests - name: Publish packages if: github.ref_name == 'main' run: | diff --git a/Makefile b/Makefile index 4b0dec8..616214f 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,10 @@ build: cd core/dicc && make cd core/cli && make +.PHONY: tests +tests: + cd core/cli && make tests + .PHONY: rebuild rebuild: cd core/dicc && make rebuild diff --git a/README.md b/README.md index 66988d4..9129e28 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,10 @@ ## About -This is a project to _end_ all current TypeScript DI implementations. -I mean it. **All of them**. With extreme prejudice. - -Why? Because they are all based on decorators. (Well, there is _one_ exception, -but that one doesn't - and cannot, properly - support async services, hence -this project.) Don't get me wrong - decorators are awesome! I love decorators. -I've built a pretty big library based _entirely_ on decorators. - -But - and I _cannot stress this enough_ - decorator-based dependency injection -breaks one of the most sacred dependency injection principles: **your code -should almost NEVER know or care that dependency injection even exists**, and it -most certainly shouldn't know anything about the specifics of the DI -_implementation_ - in other words, your code _should not depend on your -preferred dependency injection solution_. Because if it does, then it's not -portable, you can't just easily extract parts of it into a shared library, -you cannot test it independently of the DI framework, you're simply locked in, -you're alone in the dark, you're locked in and there are drums in the deep and -they are coming and you cannot get out and you don't have Gandalf and - -</rant> - -<zen> - -DICC can't do the pointy hat trick, but it does offer an alternative solution -to dependency injection. Using a simple YAML config file, you specify one or -more _resource files_, which are regular TypeScript files inside your project. -From these resource files you export some classes, interfaces, and possibly some -constant expressions, and then you point the DICC CLI to your config file and -DICC will produce a _compiled file_, which exports a fully typed and autowired -dependency injection container. - +The last Dependency Injection solution for TypeScript you will ever use. +No `@decorators` or any other bloat needed: works off of TypeScript types. The only place in your code you will ever `import { anything } from 'dicc'` -will be inside the resource file (or files). +will be inside a handful of _resource files_. ## Highlights - type-based autowiring, doesn't care about type or argument names @@ -51,13 +22,12 @@ will be inside the resource file (or files). registered manually in order to be available as dependencies to other services - supports _auto-generated service factories_ from interface declarations + and abstract classes - supports _service decorators_ (not the same thing as `@decorators`) which allow some modifications to service definitions without needing to alter the definitions themselves - supports merging multiple containers, such that public services from a merged container are available for injection into services in the parent container - - allows you to define _container parameters_, which are useful to inject - runtime configuration e.g. from `.env` files - compiles to regular TypeScript which you can easily examine to see what's going on under the hood - cyclic dependency checks run on compile time, preventing possible deadlocks @@ -88,6 +58,7 @@ Writing services and specifying dependencies: ```typescript // services.ts +import type { ServiceDefinition } from 'dicc'; // simple service with no dependencies: export class ServiceOne { @@ -107,37 +78,58 @@ export class ServiceThree { private readonly two: ServiceTwo, ) {} } + +class Entrypoint { + constructor( + readonly one: ServiceOne, + readonly two: ServiceTwo, + readonly three: ServiceThree, + ) {} +} + +export const entrypoint = Entrypoint satisfies ServiceDefinition; ``` Compiled container generated by running `dicc` with the previous code snippet as its input: ```typescript -import { Container } from 'dicc'; +import { Container, type ServiceType } from 'dicc'; import * as services0 from './services.ts'; -export interface Services { - '#ServiceOne.0': services0.ServiceOne, - '#ServiceTwo.0': Promise, - '#ServiceThree.0': Promise, +export interface PublicServices { + entrypoint: ServiceType; +} + +export interface AnonymousServices { + '#ServiceOne0.0': services0.ServiceOne; + '#ServiceTwo0.0': Promise; + '#ServiceThree0.0': Promise; } -export class AppContainer extends Container { +export class AppContainer extends Container { constructor() { - super({}, { - '#ServiceOne.0': { + super({ + 'entrypoint': { + factory: async (di) => new services0.entrypoint( + di.get('#ServiceOne0.0'), + await di.get('#ServiceTwo0.0'), + await di.get('#ServiceThree0.0'), + ), + }, + '#ServiceOne0.0': { factory: () => new services0.ServiceOne(), }, - '#ServiceTwo.0': { - async: true, + '#ServiceTwo0.0': { factory: async () => services0.ServiceTwo.create(), - }, - '#ServiceThree.0': { async: true, + }, + '#ServiceThree0.0': { factory: async (di) => new services0.ServiceThree( - di.get('#ServiceOne.0'), - await di.get('#ServiceTwo.0'), + di.get('#ServiceOne0.0'), + await di.get('#ServiceTwo0.0'), ), + async: true, }, }); } @@ -145,8 +137,9 @@ export class AppContainer extends Container { ``` The DICC compiler actually uses DICC itself, so you can look at its source code -to see a simple real-world example of [service definitions][2] and the resulting -[compiled container][3], as well as of how the container is [used][4]. +to see a simple real-world example of its [configuration][2], some +[explicit service definitions][3] and the resulting [compiled container][4], +as well as of how the container is [used][5]. ## Contributing @@ -162,6 +155,7 @@ indentation or something, I'll just fix it. [1]: https://cdn77.github.io/dicc/ -[2]: https://github.com/cdn77/dicc/blob/main/core/cli/src/definitions.ts -[3]: https://github.com/cdn77/dicc/blob/main/core/cli/src/bootstrap.ts -[4]: https://github.com/cdn77/dicc/blob/main/core/cli/src/cli.ts +[2]: https://github.com/cdn77/dicc/blob/main/core/cli/dicc.yaml +[3]: https://github.com/cdn77/dicc/blob/main/core/cli/src/cli/bootstrap/definitions.ts +[4]: https://github.com/cdn77/dicc/blob/main/core/cli/src/cli/bootstrap/container.ts +[5]: https://github.com/cdn77/dicc/blob/main/core/cli/src/cli/dicc.ts diff --git a/core/cli/Makefile b/core/cli/Makefile index 905acae..e50a457 100644 --- a/core/cli/Makefile +++ b/core/cli/Makefile @@ -9,12 +9,16 @@ clean: rm -rf dist .PHONY: compile -compile: - dist/cli.js +compile: dist + dist/cli/dicc.js dist: ../../node_modules/.bin/tsc - chmod +x dist/cli.js + chmod +x dist/cli/dicc.js + +.PHONY: tests +tests: dist + node --test tests .PHONY: major-release major-release: diff --git a/core/cli/dicc.yaml b/core/cli/dicc.yaml index b756e58..3dc5ab8 100644 --- a/core/cli/dicc.yaml +++ b/core/cli/dicc.yaml @@ -1,15 +1,7 @@ containers: - src/bootstrap2.ts: - className: ThiccContainer - lazyImports: false + src/cli/bootstrap/container.ts: + className: DiccContainer + #lazyImports: false resources: -# src/argv.ts: ~ -# src/autowiring.ts: ~ -# src/checker.ts: ~ -# src/configLoader.ts: ~ -# src/definitions.ts: ~ - src/definitions2.ts: ~ - src/decco.ts: ~ -# src/definitionScanner.ts: ~ -# src/sourceFiles.ts: ~ -# src/typeHelper.ts: ~ + src/cli/bootstrap/definitions.ts: ~ + src/*/di.ts: ~ diff --git a/core/cli/package.json b/core/cli/package.json index a8d0772..9403356 100644 --- a/core/cli/package.json +++ b/core/cli/package.json @@ -29,7 +29,7 @@ "node": ">=18" }, "bin": { - "dicc": "dist/cli.js" + "dicc": "dist/cli/dicc.js" }, "dependencies": { "@debugr/console": "^3.0.0-rc.10", diff --git a/core/cli/src/analysis/autowiring.ts b/core/cli/src/analysis/autowiring.ts new file mode 100644 index 0000000..0d40182 --- /dev/null +++ b/core/cli/src/analysis/autowiring.ts @@ -0,0 +1,258 @@ +import { ServiceScope } from 'dicc'; +import { ContainerBuilder } from '../container'; +import { + AccessorType, + ArgumentDefinition, + InjectableType, + InjectorType, + IterableType, + ListType, + LocalServiceDefinition, + PromiseType, + ReturnType, + ScopedRunnerType, + ServiceDefinition, +} from '../definitions'; +import { AutowiringError, ContainerContext } from '../errors'; +import { getFirst, mapMap } from '../utils'; +import { ContainerReflector } from './reflection'; +import { + AccessorInjectedArgument, + Argument, + AsyncMode, + ChildServiceRegistrations, + getAsyncMode, + InjectedArgument, + InjectorInjectedArgument, + IterableInjectedArgument, + ListInjectedArgument, + SingleInjectedArgument, + withAsync, +} from './results'; +import { ServiceAnalyser } from './serviceAnalyser'; + +export interface AutowiringFactory { + create(serviceAnalyser: ServiceAnalyser): Autowiring; +} + +export class Autowiring { + constructor( + private readonly reflector: ContainerReflector, + private readonly serviceAnalyser: ServiceAnalyser, + ) {} + + resolveArgumentInjection(ctx: ContainerContext, arg: ArgumentDefinition, scope: ServiceScope): Argument | undefined { + if (arg.type instanceof ScopedRunnerType) { + return { kind: 'injected', mode: 'scoped-runner' }; + } + + for (const injectable of arg.type.getInjectableTypes()) { + const candidates = ctx.builder.findByType(injectable.serviceType); + + if (!candidates.size) { + continue; + } + + const argIsPromise = injectable instanceof PromiseType; + const argIsList = (argIsPromise ? injectable.value : injectable) instanceof ListType; + const argIsIterable = (argIsPromise ? injectable.value : injectable) instanceof IterableType; + + if (candidates.size > 1 && !argIsList && !argIsIterable) { + throw new AutowiringError('Multiple services of matching type found', ctx); + } + + if (injectable instanceof InjectorType) { + return this.resolveInjector(ctx, getFirst(candidates)); + } + + return this.resolveInjection(ctx, arg, injectable, candidates, scope); + } + + if (arg.optional) { + return undefined; + } else if (arg.type.nullable) { + return { kind: 'literal', source: 'undefined', async: 'none' }; + } + + throw new AutowiringError( + arg.type instanceof InjectorType + ? 'Unknown service type in injector' + : 'Unable to autowire non-optional argument', + ctx, + ); + } + + private resolveInjector(ctx: ContainerContext, candidate: ServiceDefinition): InjectorInjectedArgument { + if (candidate.isForeign()) { + // or can we? + throw new AutowiringError('Cannot inject injector for a service from a foreign container', ctx); + } else if (candidate.scope === 'private') { + throw new AutowiringError(`Cannot inject injector for privately-scoped service '${candidate.path}'`, ctx); + } else if (candidate.factory) { + throw new AutowiringError(`Cannot inject injector for non-dynamic service '${candidate.path}'`, ctx); + } + + this.serviceAnalyser.analyseServiceDefinition(candidate, true); + return { kind: 'injected', mode: 'injector', id: candidate.id }; + } + + private resolveInjection( + ctx: ContainerContext, + arg: ArgumentDefinition, + injectable: InjectableType, + candidates: Set, + scope: ServiceScope, + ): InjectedArgument { + const argIsPromise = injectable instanceof PromiseType; + const argIsAccessor = injectable instanceof AccessorType; + const accessorIsAsync = argIsAccessor && injectable.returnType instanceof PromiseType; + const argIsIterable = injectable instanceof IterableType; + const alias = candidates.size > 1 + ? ctx.builder.getTypeName(injectable.aliasType) + : getFirst(candidates).id; + const async: (() => boolean)[] = []; + + for (const candidate of candidates) { + const service = this.serviceAnalyser.analyseServiceDefinition(candidate, argIsAccessor); + candidates.size > 1 && service.aliases.add(alias); + + if (scope === 'global' && service.scope === 'local' && !argIsAccessor) { + throw new AutowiringError( + `Cannot inject locally-scoped dependency '${service.id}' into globally-scoped service`, + ctx, + ); + } + + if (argIsPromise) { + continue; + } + + async.push(() => { + if (!service.async) { + return false; + } + + if (argIsAccessor && !accessorIsAsync) { + throw new AutowiringError( + `Cannot inject synchronous accessor for async service '${service.id}'`, + ctx, + ); + } + + return true; + }); + } + + if (argIsAccessor) { + return this.accessor(injectable.returnType, alias, () => async.some((cb) => cb())); + } else if (argIsIterable) { + return this.iterable(arg.rest, alias, getAsyncCb(injectable.async)); + } else if ((argIsPromise ? injectable.value : injectable) instanceof ListType) { + return this.list(arg.rest, alias, getAsyncCb()); + } else { + return this.single(injectable, arg.optional, alias, getAsyncCb()); + } + + function getAsyncCb(targetIsAsync: boolean = argIsPromise): () => AsyncMode { + return () => { + const hasAsyncCandidate = async.some((cb) => cb()); + return getAsyncMode(hasAsyncCandidate, targetIsAsync); + }; + } + } + + private single(type: InjectableType, optional: boolean, alias: string, async: () => AsyncMode): SingleInjectedArgument { + return withAsync(async, { + kind: 'injected', + mode: 'single', + alias, + need: !optional && !type.nullable, + spread: false, + }); + } + + private list(spread: boolean, alias: string, async: () => AsyncMode): ListInjectedArgument { + return withAsync(async, { + kind: 'injected', + mode: 'list', + alias, + spread, + }); + } + + private iterable(spread: boolean, alias: string, async: () => AsyncMode): IterableInjectedArgument { + return withAsync(async, { + kind: 'injected', + mode: 'iterable', + alias, + spread, + }); + } + + private accessor(returnType: ReturnType, alias: string, async: () => boolean): AccessorInjectedArgument { + return withAsync(async, { + kind: 'injected', + mode: 'accessor', + alias, + need: !returnType.nullable, + target: returnType instanceof ListType ? 'list' : 'single', + }); + } + + resolveChildServiceRegistrations( + ctx: ContainerContext, + definition: ServiceDefinition, + ): ChildServiceRegistrations | undefined { + if (!definition.isLocal() || !definition.container) { + return undefined; + } + + const definitions = this.getInjectableDynamicServices( + ctx.builder, + definition, + ); + + const services = mapMap(definitions, (foreignId, definition) => { + const service = this.serviceAnalyser.analyseServiceDefinition(definition); + const arg: SingleInjectedArgument = withAsync(() => getAsyncMode(service.async, false), { + kind: 'injected', + mode: 'single', + alias: service.id, + need: true, + spread: false, + }); + + return [foreignId, arg]; + }); + + return withAsync(hasAsync, { services }); + + function hasAsync(): boolean { + for (const arg of services.values()) { + if (arg.async === 'await') { + return true; + } + } + + return false; + } + } + + private getInjectableDynamicServices( + parent: ContainerBuilder, + child: LocalServiceDefinition, + ): Map { + const reflection = this.reflector.getContainerReflection(child); + const services: Map = new Map(); + + for (const service of reflection.getDynamicServices()) { + const [candidate, extra] = parent.findByType(service.type); + + if (candidate && !extra && candidate.isLocal() && candidate.factory) { + services.set(service.id, candidate); + } + } + + return services; + } +} diff --git a/core/cli/src/analysis/containerAnalyser.ts b/core/cli/src/analysis/containerAnalyser.ts new file mode 100644 index 0000000..532e5ab --- /dev/null +++ b/core/cli/src/analysis/containerAnalyser.ts @@ -0,0 +1,194 @@ +import { SourceFile } from 'ts-morph'; +import { ContainerBuilder } from '../container'; +import { LocalServiceDefinition } from '../definitions'; +import { getOrCreate } from '../utils'; +import { ContainerReflector } from './reflection'; +import { Call, Container, Service, TypeSpecifierWithAsync, withAsync } from './results'; +import { ServiceAnalyser } from './serviceAnalyser'; + +export class ContainerAnalyser { + private readonly builders: Map = new Map(); + private readonly results: Map = new Map(); + + constructor( + private readonly reflector: ContainerReflector, + private readonly serviceAnalyser: ServiceAnalyser, + ) { + } + + analyse(builders: Iterable): Iterable<[ContainerBuilder, Container]> { + for (const builder of builders) { + this.builders.set(builder.sourceFile, builder); + this.results.set(builder, createEmptyResult(builder.options.className, builder.options.preamble)); + } + + this.mergeChildContainers(); + this.analyseServices(); + this.analyseContainers(); + + return this.results; + } + + private mergeChildContainers(): void { + for (const container of this.builders.values()) { + for (const child of container.getChildContainers()) { + this.mergeChildServices(container, child); + } + } + } + + private mergeChildServices(parent: ContainerBuilder, child: LocalServiceDefinition): void { + const reflection = this.reflector.getContainerReflection(child); + + for (const svc of reflection.getPublicServices()) { + parent.addForeignDefinition(child, svc.id, svc.type, svc.aliases, svc.definition, svc.async); + } + } + + private analyseServices(): void { + for (const builder of this.builders.values()) { + for (const service of builder.getPublicServices()) { + this.serviceAnalyser.analyseServiceDefinition(service); + } + } + + withAsync.stopWarnings(); + + for (const [builder, service] of this.serviceAnalyser.getAnalysedServices()) { + this.results.get(builder)?.services.add(service); + } + } + + private analyseContainers(): void { + for (const [builder, result] of this.results) { + this.analyseContainer(builder, result); + } + } + + private analyseContainer(builder: ContainerBuilder, container: Container): void { + this.registerResources(builder, container); + + for (const service of container.services) { + this.analyseService(container, service, builder); + } + } + + private registerResources(builder: ContainerBuilder, container: Container): void { + for (const [alias, staticImport, dynamicImport] of builder.getResourceMap()) { + container.resources.set(alias, { + staticImport, + dynamicImport, + needsType: false, + needsValue: !builder.options.lazyImports, + }); + } + } + + private analyseService(container: Container, service: Service, builder: ContainerBuilder): void { + this.registerServiceTypes(container, service); + + if (builder.options.lazyImports) { + this.analyseServiceResources(container, service); + } + } + + private registerServiceTypes(container: Container, service: Service): void { + const type: TypeSpecifierWithAsync = { ...service.type, async: service.async }; + + if (type.kind === 'local') { + const resource = container.resources.get(type.resource); + resource && (resource.needsType = true); + type.indirect && container.imports.add('type ServiceType'); + } else { + const resource = container.resources.get(type.container.resource); + resource && (resource.needsType = true); + type.container.indirect && container.imports.add('type ServiceType'); + container.imports.add('type ForeignServiceType'); + } + + const map = service.factory ? container.anonymousTypes : container.dynamicTypes; + + if (!service.id.startsWith('#')) { + container.publicTypes.set(service.id, type); + } else { + getOrCreate(map, service.id, () => new Set()).add(type); + } + + for (const alias of service.aliases) { + getOrCreate(map, alias, () => new Set()).add(type); + } + } + + private analyseServiceResources(container: Container, service: Service): void { + if (service.factory) { + if (service.factory.kind === 'local' || service.factory.kind === 'auto-class') { + this.analyseCallResources(container, service.factory.call, service.async); + } + + if (service.factory.kind === 'auto-class' || service.factory.kind === 'auto-interface') { + if (service.factory.method.name === 'create' && !service.factory.method.async) { + this.analyseServiceResources(container, service.factory.method.service); + } + } + } + + if (service.decorate) { + this.analyseCallListResources(container, service.decorate.calls, service.async); + } + + if (service.onCreate) { + this.analyseCallListResources(container, service.onCreate.calls, service.async); + } + + if (service.onFork) { + if (service.onFork.serviceCall) { + this.analyseCallResources(container, service.onFork.serviceCall, true); + } + + this.analyseCallListResources(container, service.onFork.calls, true); + } + + if (service.onDestroy) { + this.analyseCallListResources(container, service.onDestroy.calls, service.onDestroy.async); + } + } + + private analyseCallListResources(container: Container, calls: Call[], async?: boolean): void { + for (const call of calls) { + this.analyseCallResources(container, call, async); + } + } + + private analyseCallResources(container: Container, call: Call, async: boolean = call.async): void { + if (!async) { + const resource = container.resources.get(call.resource); + resource && (resource.needsValue = true); + } + + if (container.imports.has('toAsyncIterable') && container.imports.has('toSyncIterable')) { + return; + } + + for (const arg of call.args) { + if (arg.kind === 'injected' && arg.mode === 'iterable') { + switch (arg.async) { + case 'await': container.imports.add('toSyncIterable'); break; + case 'wrap': container.imports.add('toAsyncIterable'); break; + } + } + } + } +} + +function createEmptyResult(className: string, preamble?: string): Container { + return { + resources: new Map(), + imports: new Set(), + publicTypes: new Map(), + dynamicTypes: new Map(), + anonymousTypes: new Map(), + services: new Set(), + className, + preamble, + }; +} diff --git a/core/cli/src/analysis/di.ts b/core/cli/src/analysis/di.ts new file mode 100644 index 0000000..fee2dc1 --- /dev/null +++ b/core/cli/src/analysis/di.ts @@ -0,0 +1,10 @@ +export { Autowiring, AutowiringFactory } from './autowiring'; +export { ContainerAnalyser } from './containerAnalyser'; +export { ServiceAnalyser } from './serviceAnalyser'; +export { + ContainerReflector, + BuilderReflection, + BuilderReflectionFactory, + ExternalReflection, + ExternalReflectionFactory, +} from './reflection'; diff --git a/core/cli/src/analysis/index.ts b/core/cli/src/analysis/index.ts new file mode 100644 index 0000000..40b2c7f --- /dev/null +++ b/core/cli/src/analysis/index.ts @@ -0,0 +1,5 @@ +export * from './reflection'; +export * from './results'; +export * from './autowiring'; +export * from './containerAnalyser'; +export * from './serviceAnalyser'; diff --git a/core/cli/src/analysis/reflection/builderReflection.ts b/core/cli/src/analysis/reflection/builderReflection.ts new file mode 100644 index 0000000..a3f9c54 --- /dev/null +++ b/core/cli/src/analysis/reflection/builderReflection.ts @@ -0,0 +1,34 @@ +import { ServiceDefinition } from '../../definitions'; +import { ContainerBuilder } from '../../container'; +import { ContainerReflection, ForeignServiceReflection } from './types'; + +export interface BuilderReflectionFactory { + create(container: ContainerBuilder): BuilderReflection; +} + +export class BuilderReflection implements ContainerReflection { + constructor( + private readonly container: ContainerBuilder, + ) {} + + * getPublicServices(): Iterable { + for (const definition of this.container.getPublicServices()) { + yield this.reflect(definition); + } + } + + * getDynamicServices(): Iterable { + for (const definition of this.container.getDynamicServices()) { + yield this.reflect(definition); + } + } + + private reflect(definition: ServiceDefinition): ForeignServiceReflection { + return { + id: definition.id, + type: definition.type, + aliases: definition.aliases, + definition, + }; + } +} diff --git a/core/cli/src/analysis/reflection/containerReflector.ts b/core/cli/src/analysis/reflection/containerReflector.ts new file mode 100644 index 0000000..aa6b74e --- /dev/null +++ b/core/cli/src/analysis/reflection/containerReflector.ts @@ -0,0 +1,21 @@ +import { BuilderMap } from '../../container'; +import { LocalServiceDefinition } from '../../definitions'; +import { BuilderReflectionFactory } from './builderReflection'; +import { ExternalReflectionFactory } from './externalReflection'; +import { ContainerReflection } from './types'; + +export class ContainerReflector { + constructor( + private readonly builderReflectionFactory: BuilderReflectionFactory, + private readonly externalReflectionFactory: ExternalReflectionFactory, + private readonly builders: BuilderMap, + ) {} + + getContainerReflection(containerService: LocalServiceDefinition): ContainerReflection { + const resource = containerService.declaration?.getSourceFile(); + const builder = resource && this.builders.getByResource(resource); + return builder + ? this.builderReflectionFactory.create(builder) + : this.externalReflectionFactory.create(containerService.type); + } +} diff --git a/core/cli/src/analysis/reflection/externalReflection.ts b/core/cli/src/analysis/reflection/externalReflection.ts new file mode 100644 index 0000000..30027e7 --- /dev/null +++ b/core/cli/src/analysis/reflection/externalReflection.ts @@ -0,0 +1,34 @@ +import { Type } from 'ts-morph'; +import { mapIterable, TypeHelper } from '../../utils'; +import { ContainerReflection, ForeignServiceReflection } from './types'; + +export interface ExternalReflectionFactory { + create(container: Type): ExternalReflection; +} + +export class ExternalReflection implements ContainerReflection { + private publicServices?: Set; + private dynamicServices?: Set; + + constructor( + private readonly typeHelper: TypeHelper, + private readonly container: Type, + ) {} + + getPublicServices(): Iterable { + this.publicServices ??= new Set(this.resolveServices('public')); + return this.publicServices; + } + + getDynamicServices(): Iterable { + this.dynamicServices ??= new Set(this.resolveServices('dynamic')); + return this.dynamicServices; + } + + private resolveServices(map: 'public' | 'dynamic'): Iterable { + return mapIterable( + this.typeHelper.resolveExternalContainerServices(this.container, map), + ([id, type, aliases, async]) => ({ id, type, aliases, async }), + ); + } +} diff --git a/core/cli/src/v2/container/reflection/index.ts b/core/cli/src/analysis/reflection/index.ts similarity index 100% rename from core/cli/src/v2/container/reflection/index.ts rename to core/cli/src/analysis/reflection/index.ts index f94e8ee..528dbe8 100644 --- a/core/cli/src/v2/container/reflection/index.ts +++ b/core/cli/src/analysis/reflection/index.ts @@ -1,4 +1,4 @@ +export * from './containerReflector'; export * from './builderReflection'; export * from './externalReflection'; -export * from './containerReflector'; export * from './types'; diff --git a/core/cli/src/analysis/reflection/types.ts b/core/cli/src/analysis/reflection/types.ts new file mode 100644 index 0000000..351221d --- /dev/null +++ b/core/cli/src/analysis/reflection/types.ts @@ -0,0 +1,15 @@ +import { Type } from 'ts-morph'; +import { ServiceDefinition } from '../../definitions'; + +export type ForeignServiceReflection = { + id: string; + type: Type; + aliases: Iterable; + definition?: ServiceDefinition; + async?: boolean; +}; + +export interface ContainerReflection { + getPublicServices(): Iterable; + getDynamicServices(): Iterable; +} diff --git a/core/cli/src/analysis/results/arguments.ts b/core/cli/src/analysis/results/arguments.ts new file mode 100644 index 0000000..d6b54ae --- /dev/null +++ b/core/cli/src/analysis/results/arguments.ts @@ -0,0 +1,102 @@ +import { Call } from './call'; + +export type AsyncMode = + | 'await' + | 'wrap' + | 'none'; + +export function getAsyncMode(valueIsAsync: boolean, targetWantsAsync: boolean): AsyncMode { + return valueIsAsync === targetWantsAsync + ? 'none' + : valueIsAsync ? 'await' : 'wrap'; +} + +export type RawArgument = { + kind: 'raw'; + value: any; +}; + +export type LiteralArgument = { + kind: 'literal'; + source: string; + async: AsyncMode; +}; + +export type OverrideCall = Call & { + kind: 'call'; +}; + +export type OverrideValue = { + kind: 'value'; + resource: string; + path: string; + async: boolean; +}; + +export type OverriddenArgument = { + kind: 'overridden'; + value: OverrideCall | OverrideValue; + async: AsyncMode; + spread: boolean; +}; + +type InjectedArgumentOptions = { + kind: 'injected'; +}; + +type InjectedServiceArgumentOptions = InjectedArgumentOptions & { + alias: string; + async: AsyncMode; + spread: boolean; +}; + +export type SingleInjectedArgument = InjectedServiceArgumentOptions & { + mode: 'single'; + need: boolean; +}; + +export type ListInjectedArgument = InjectedServiceArgumentOptions & { + mode: 'list'; +}; + +export type IterableInjectedArgument = InjectedServiceArgumentOptions & { + mode: 'iterable'; +}; + +export type AccessorInjectedArgument = InjectedArgumentOptions & { + mode: 'accessor'; + alias: string; + target: 'single' | 'list'; + async: boolean; + need: boolean; +}; + +export type InjectorInjectedArgument = InjectedArgumentOptions & { + mode: 'injector'; + id: string; +}; + +export type ScopedRunnerInjectedArgument = InjectedArgumentOptions & { + mode: 'scoped-runner'; +}; + +export type InjectedArgument = + | SingleInjectedArgument + | ListInjectedArgument + | IterableInjectedArgument + | AccessorInjectedArgument + | InjectorInjectedArgument + | ScopedRunnerInjectedArgument; + +export type Argument = + | RawArgument + | LiteralArgument + | OverriddenArgument + | InjectedArgument; + +export type ArgumentList = Iterable & { + length: number; + inject: boolean; + async: boolean; + replace(index: number, arg: Argument): void; +}; diff --git a/core/cli/src/analysis/results/async.ts b/core/cli/src/analysis/results/async.ts new file mode 100644 index 0000000..3c18dd4 --- /dev/null +++ b/core/cli/src/analysis/results/async.ts @@ -0,0 +1,25 @@ +import { AsyncMode } from './arguments'; + +let warn = true; + +export function withAsync( + cb: () => A, + value: T, +): T & { async: A } { + let async: A | undefined; + + return { + ...value, + get async(): A { + if (warn) { + console.trace('Warning: async getter called prematurely!'); + } + + return async ??= cb(); + }, + }; +} + +withAsync.stopWarnings = () => { + warn = false; +}; diff --git a/core/cli/src/analysis/results/call.ts b/core/cli/src/analysis/results/call.ts new file mode 100644 index 0000000..6af6b8a --- /dev/null +++ b/core/cli/src/analysis/results/call.ts @@ -0,0 +1,8 @@ +import { ArgumentList } from './arguments'; + +export type Call = { + resource: string; + statement: string; + args: ArgumentList; + async: boolean; +}; diff --git a/core/cli/src/analysis/results/container.ts b/core/cli/src/analysis/results/container.ts new file mode 100644 index 0000000..cabb157 --- /dev/null +++ b/core/cli/src/analysis/results/container.ts @@ -0,0 +1,23 @@ +import { Service, TypeSpecifier } from './service'; + +export type Resource = { + staticImport: string; + dynamicImport: string; + needsType: boolean; + needsValue: boolean; +}; + +export type TypeSpecifierWithAsync = TypeSpecifier & { + async: boolean +}; + +export type Container = { + resources: Map; + imports: Set; + publicTypes: Map; + dynamicTypes: Map>; + anonymousTypes: Map>; + services: Set; + className: string; + preamble?: string; +}; diff --git a/core/cli/src/analysis/results/index.ts b/core/cli/src/analysis/results/index.ts new file mode 100644 index 0000000..91262af --- /dev/null +++ b/core/cli/src/analysis/results/index.ts @@ -0,0 +1,5 @@ +export * from './async'; +export * from './arguments'; +export * from './call'; +export * from './container'; +export * from './service'; diff --git a/core/cli/src/analysis/results/service.ts b/core/cli/src/analysis/results/service.ts new file mode 100644 index 0000000..7e7d67c --- /dev/null +++ b/core/cli/src/analysis/results/service.ts @@ -0,0 +1,106 @@ +import { ServiceScope } from 'dicc'; +import { Argument, SingleInjectedArgument } from './arguments'; +import { Call } from './call'; + +export type Service = { + id: string; + type: TypeSpecifier; + aliases: Set; + factory?: Factory | AutoImplement; + async: boolean; + scope: ServiceScope; + anonymous: boolean; + register?: ChildServiceRegistrations; + decorate?: HookInfo; + onCreate?: HookInfo; + onFork?: ForkHookInfo; + onDestroy?: HookInfo; +}; + +export type LocalTypeSpecifier = { + kind: 'local'; + resource: string; + path: string; + indirect: boolean; +}; + +export type ForeignTypeSpecifier = { + kind: 'foreign'; + container: LocalTypeSpecifier; + id: string; +}; + +export type TypeSpecifier = + | LocalTypeSpecifier + | ForeignTypeSpecifier; + +export type LocalFactory = { + kind: 'local'; + call: Call; +}; + +export type ForeignContainer = { + id: string; + async: boolean; +}; + +export type ForeignFactory = { + kind: 'foreign'; + container: ForeignContainer; + id: string; + async: boolean; +}; + +export type Factory = LocalFactory | ForeignFactory; + +export type AutoImplementAccessorMethod = { + name: 'get'; + async: boolean; + target: string; + need: boolean; +}; + +export type AutoImplementFactoryMethod = { + name: 'create'; + args: string[]; + async: boolean; + service: Service; + eagerDeps: Map; +}; + +export type AutoImplementMethod = + | AutoImplementAccessorMethod + | AutoImplementFactoryMethod; + +export type AutoClass = { + kind: 'auto-class'; + call: Call; + method: AutoImplementMethod; +}; + +export type AutoInterface = { + kind: 'auto-interface'; + method: AutoImplementMethod; +}; + +export type AutoImplement = + | AutoClass + | AutoInterface; + +export type ChildServiceRegistrations = { + services: Map; + async: boolean; +}; + +export type HookInfo = { + async: boolean; + args: number; + calls: Call[]; +}; + +export type ForkHookInfo = { + args: number; + containerCall: boolean; + serviceCall?: Call; + calls: Call[]; +}; diff --git a/core/cli/src/analysis/serviceAnalyser.ts b/core/cli/src/analysis/serviceAnalyser.ts new file mode 100644 index 0000000..75ec8c3 --- /dev/null +++ b/core/cli/src/analysis/serviceAnalyser.ts @@ -0,0 +1,571 @@ +import { ServiceScope } from 'dicc'; +import { ContainerBuilder } from '../container'; +import { + ArgumentDefinition, + ArgumentOverride, + AutoImplementedMethod, + CallableDefinition, + DecoratorDefinition, + FactoryDefinition, + ForcedArgument, + ForeignServiceDefinition, + LocalServiceDefinition, + PromiseType, + ServiceDefinition, +} from '../definitions'; +import { ContainerContext, CyclicDependencyError, DefinitionError, InternalError } from '../errors'; +import { mergeMaps } from '../utils'; +import { Autowiring, AutowiringFactory } from './autowiring'; +import { + Argument, + ArgumentList, + AutoImplement, + AutoImplementMethod, + Call, + Factory, + ForeignTypeSpecifier, + ForkHookInfo, + getAsyncMode, + HookInfo, + IterableInjectedArgument, + ListInjectedArgument, + LiteralArgument, + LocalTypeSpecifier, + OverriddenArgument, + OverrideCall, + OverrideValue, + Service, + SingleInjectedArgument, + TypeSpecifier, + withAsync, +} from './results'; + +export class ServiceAnalyser { + private readonly autowiring: Autowiring; + private readonly visitedServices: Map = new Map(); + private readonly excludedServices: Set = new Set(); + private readonly eagerCalls: Set = new Set(); + private readonly dependencyChains: ServiceDefinition[][] = []; + private currentDependencyChain: ServiceDefinition[] = []; + + constructor( + autowiringFactory: AutowiringFactory, + ) { + this.autowiring = autowiringFactory.create(this); + } + + * getAnalysedServices(): Iterable<[ContainerBuilder, Service]> { + for (const [definition, service] of this.visitedServices) { + if (!this.excludedServices.has(service)) { + yield [definition.builder, service]; + } + } + } + + analyseServiceDefinition( + definition: ServiceDefinition, + startDependencyChain: boolean = false, + externalArgs?: Map, + ): Service { + this.checkCyclicDependencies(definition, startDependencyChain); + + const visited = this.visitedServices.get(definition); + + if (visited) { + this.releaseCyclicDependencyCheck(definition, startDependencyChain); + return visited; + } + + const decorators = definition.builder.decorate(definition); + const scope = this.resolveScope(definition, decorators); + const service: Service = withAsync(() => isServiceAsync(service), { + id: definition.id, + type: this.resolveTypeSpecifier(definition), + aliases: new Set(), + async: false, + scope, + anonymous: isAnonymous(definition), + }); + + this.visitedServices.set(definition, service); + + const ctx: ContainerContext = { + builder: definition.builder, + definition, + }; + + service.factory = this.analyseFactory(ctx, definition, scope, externalArgs); + service.register = this.autowiring.resolveChildServiceRegistrations(ctx, definition); + service.decorate = this.analyseHook(ctx, 'decorate', definition, decorators, scope); + service.onCreate = this.analyseHook(ctx, 'onCreate', definition, decorators, scope); + service.onFork = this.analyseForkHook(ctx, definition, decorators); + service.onDestroy = this.analyseHook(ctx, 'onDestroy', definition, decorators, scope); + + this.releaseCyclicDependencyCheck(definition, startDependencyChain); + return service; + } + + private analyseFactory( + ctx: ContainerContext, + definition: ServiceDefinition, + scope: ServiceScope, + externalArgs?: Map, + ): Factory | AutoImplement | undefined { + if (definition.isForeign()) { + const parent = this.analyseServiceDefinition(definition.parent); + const service = definition.definition && this.analyseServiceDefinition(definition.definition); + + return withAsync(() => service ? service.async : definition.async, { + kind: 'foreign', + container: withAsync(() => parent.async, { + id: definition.parent.id, + }), + id: definition.foreignId, + }); + } + + const overrides = mergeMaps( + definition.isExplicit() ? definition.args : new Map(), + externalArgs ?? new Map(), + ); + + const call = definition.factory + ? this.analyseCall({ ...ctx, method: 'factory' }, definition.factory, scope, overrides) + : undefined; + + if (!definition.autoImplement) { + return call ? { kind: 'local', call } : undefined; + } + + const method = this.analyseAutoImplementedMethod( + ctx, + definition.autoImplement.method, + definition.autoImplement.service, + ); + + if (!call) { + return { kind: 'auto-interface', method }; + } + + call.statement = call.statement.replace(/^new(?=\s)/, 'new class extends'); + return { kind: 'auto-class', call, method }; + } + + private analyseHook( + ctx: ContainerContext, + hook: 'decorate' | 'onCreate' | 'onDestroy', + definition: ServiceDefinition, + decorators: DecoratorDefinition[], + scope: ServiceScope, + ): HookInfo | undefined { + const info: Omit = { + calls: [], + args: 0, + }; + + const overrides = indexedArgs('service'); + + if (hook !== 'decorate' && definition.isExplicit() && definition[hook]) { + const call = this.analyseCall({ ...ctx, method: hook }, definition[hook], scope, overrides); + info.calls.push(call); + info.args = Math.max(info.args, call.args.inject ? 2 : call.args.length ? 1 : 0); + } + + for (const decorator of decorators) { + if (decorator[hook]) { + const call = this.analyseCall( + { ...ctx, definition: decorator, method: hook }, + decorator[hook], + scope, + overrides, + ); + + info.calls.push(call); + info.args = Math.max(info.args, call.args.inject ? 2 : call.args.length ? 1 : 0); + } + } + + return info.calls.length ? withAsync(hasAsyncCall, info) : undefined; + + function hasAsyncCall(): boolean { + for (const call of info.calls) { + if (call.async || call.args.async) { + return true; + } + } + + return false; + } + } + + private analyseForkHook( + ctx: ContainerContext, + definition: ServiceDefinition, + decorators: DecoratorDefinition[], + ): ForkHookInfo | undefined { + const containerCall = definition.isLocal() && definition.container; + const serviceCall = definition.isExplicit() && definition.onFork + ? this.analyseCall( + { ...ctx, method: 'onFork' }, + definition.onFork, + 'global', + indexedArgs('callback', 'service'), + ) + : undefined; + + const args: number[] = [ + 1, // at least (callback) must always be provided + containerCall ? 2 : 0, // container call needs (callback, service) + serviceCall // injected service call needs (callback, service, di), otherwise at most (callback, service) + ? serviceCall.args.inject ? 3 : Math.min(serviceCall.args.length, 2) + : 0, + ]; + + const info: ForkHookInfo = { + args: 0, + containerCall, + serviceCall, + calls: [], + }; + + for (const decorator of decorators) { + if (decorator.onFork) { + const call = this.analyseCall( + { ...ctx, method: 'onFork', definition: decorator }, + decorator.onFork, + 'global', + indexedArgs(serviceCall ? 'fork ?? service' : 'service'), + ); + + // injected decorator onFork hooks need the compiled call to have (cb, service, di), + // otherwise (cb, service) if they have at least 1 argument + args.push(call.args.inject ? 3 : call.args.length ? 2 : 0); + info.calls.push(call); + } + } + + info.args = Math.max(...args); + return info.containerCall || info.serviceCall || info.calls.length ? info : undefined; + } + + private analyseAutoImplementedMethod( + ctx: ContainerContext, + method: AutoImplementedMethod, + target: LocalServiceDefinition, + ): AutoImplementMethod { + const args = [...method.args.keys()]; + const service = this.analyseServiceDefinition(target, true, namedArgs(...args)); + const returnsPromise = method.returnType instanceof PromiseType; + + if (method.name === 'create') { + this.excludedServices.add(service); + + if (returnsPromise) { + return { name: 'create', args, async: true, service, eagerDeps: new Map() }; + } + + let eagerDeps: Map | undefined; + const getEagerDeps = () => eagerDeps ??= this.analyseAutoFactoryEagerDeps(service); + + return { + name: 'create', + args, + async: false, + get service() { + getEagerDeps(); + return service; + }, + get eagerDeps() { + return getEagerDeps(); + }, + }; + } + + return withAsync( + () => { + if (service.async && !returnsPromise) { + throw new DefinitionError(`Auto-implemented getter of async service doesn't return Promise`, { + builder: ctx.builder, + resource: method.resource, + path: method.path, + node: method.node, + }); + } + + return returnsPromise; + }, + { name: 'get', target: service.id, need: !method.returnType.nullable } + ); + } + + private analyseAutoFactoryEagerDeps(service: Service): Map { + if (!service.factory || (service.factory.kind !== 'local' && service.factory.kind !== 'auto-class')) { + throw new InternalError('This should not happen'); + } + + const deps: Map = new Map(this.extractEagerArgs(service.factory.call)); + + if ( + service.factory.kind === 'auto-class' + && service.factory.method.name === 'create' + && service.factory.method.eagerDeps.size + ) { + for (const [name, arg] of service.factory.method.eagerDeps) { + deps.set(name, arg); + } + + service.factory.method.eagerDeps.clear(); + } + + if (service.decorate && service.decorate.async) { + Object.defineProperty(service.decorate, 'async', { get() { return false; } }); + + for (const call of service.decorate.calls) { + for (const [name, arg] of this.extractEagerArgs(call)) { + deps.set(name, arg); + } + } + } + + return deps; + } + + private * extractEagerArgs(call: Call): Iterable<[string, Argument]> { + if (call.async) { + throw new Error(`Async call '${call.statement}(...)' in auto-implemented factory method cannot be resolved eagerly`); + } + + Object.defineProperty(call.args, 'async', { get() { return false; } }); + let i = 0; + + for (const arg of call.args) { + if (hasAsyncMode(arg) && arg.async === 'await') { + this.eagerCalls.add(call); + const name = `call${this.eagerCalls.size - 1}Arg${i}`; + call.args.replace(i, { kind: 'literal', source: name, async: 'none' }); + yield [name, { ...arg, async: 'none' }]; + } + + ++i; + } + } + + private analyseCall( + ctx: ContainerContext, + callable: CallableDefinition, + scope: ServiceScope, + overrides?: Map, + ): Call { + const [pre, post] = callable instanceof FactoryDefinition && callable.method + ? callable.method === 'constructor' ? ['new ', ''] : ['', `.${callable.method}`] + : ['', '']; + const resource = ctx.builder.getResourceAlias(callable.resource); + + return { + resource, + statement: `${pre}${resource}.${callable.path}${post}`, + args: this.analyseArgs(ctx, callable.args, scope, overrides), + async: callable.returnType instanceof PromiseType, + }; + } + + private analyseArgs( + ctx: ContainerContext, + args: Map, + scope: ServiceScope, + overrides?: Map, + ): ArgumentList { + const items: Argument[] = []; + const result: Omit = { + inject: false, + get length() { return items.length; }, + *[Symbol.iterator]() { yield * items; }, + replace(index: number, arg: Argument) { items[index] = arg; }, + }; + const undefs: Argument[] = []; + + for (const [name, arg] of args) { + const override = overrides?.get(name) ?? overrides?.get(result.length); + + if (override) { + const overridden = this.resolveOverride(ctx, arg, name, scope, override); + items.push(...undefs.splice(0, undefs.length), overridden); + + if (overridden.kind === 'overridden' && overridden.value.kind === 'call') { + overridden.value.args.inject && (result.inject = true); + } + continue; + } + + const argument = this.autowiring.resolveArgumentInjection({ ...ctx, argument: name }, arg, scope); + + if (!argument) { + undefs.push({ kind: 'literal', source: 'undefined', async: 'none' }); + continue; + } + + items.push(...undefs.splice(0, undefs.length), argument); + argument.kind === 'injected' && (result.inject = true); + } + + return withAsync(hasAsyncArg, result); + + function hasAsyncArg(): boolean { + for (const arg of result) { + if (hasAsyncMode(arg) && arg.async === 'await') { + return true; + } + } + + return false; + } + } + + private resolveOverride( + ctx: ContainerContext, + arg: ArgumentDefinition, + name: string, + scope: ServiceScope, + override: ArgumentOverride, + ): LiteralArgument | OverriddenArgument { + if (override instanceof ForcedArgument) { + return { kind: 'literal', source: override.value, async: 'none' }; + } + + let value: OverrideCall | OverrideValue; + + if (override instanceof CallableDefinition) { + const call = this.analyseCall({ ...ctx, method: 'override', argument: name }, override, scope); + value = { kind: 'call', ...call }; + } else { + const resource = ctx.builder.getResourceAlias(override.resource); + const path = `${resource}.${override.path}`; + value = { kind: 'value', resource, path, async: override.type instanceof PromiseType }; + } + + // async reflects the (return) type of the override in code, so it can be resolved here: + const async = getAsyncMode(value.async, arg.type instanceof PromiseType); + return { kind: 'overridden', value, async, spread: arg.rest }; + } + + private resolveScope(definition: ServiceDefinition, decorators: DecoratorDefinition[]): ServiceScope { + const decoratorWithScope = decorators.findLast((decorator) => decorator.scope !== undefined); + return decoratorWithScope?.scope ?? (definition.isLocal() ? definition.scope : 'global'); + } + + private resolveTypeSpecifier(definition: ForeignServiceDefinition): ForeignTypeSpecifier; + private resolveTypeSpecifier(definition: LocalServiceDefinition): LocalTypeSpecifier; + private resolveTypeSpecifier(definition: ServiceDefinition): TypeSpecifier; + private resolveTypeSpecifier(definition: ServiceDefinition): TypeSpecifier { + if (definition.isForeign()) { + return { + kind: 'foreign', + container: this.resolveTypeSpecifier(definition.parent), + id: definition.foreignId, + }; + } + + const resource = definition.builder.getResourceAlias(definition.resource); + + return { + kind: 'local', + resource, + path: `${resource}.${definition.path}`, + indirect: definition.isExplicit() || !!(definition.factory && !definition.factory.method), + }; + } + + private checkCyclicDependencies(definition: ServiceDefinition, startNewChain: boolean): void { + if (startNewChain) { + this.dependencyChains.push(this.currentDependencyChain); + this.currentDependencyChain = []; + } + + const idx = this.currentDependencyChain.indexOf(definition); + + if (idx < 0) { + this.currentDependencyChain.push(definition); + return; + } + + throw new CyclicDependencyError(...this.currentDependencyChain.slice(idx), definition); + } + + private releaseCyclicDependencyCheck(definition: ServiceDefinition, startedNewChain: boolean): void { + if (definition !== this.currentDependencyChain.pop()) { + throw new InternalError('Cyclic dependency checker is broken'); + } + + if (startedNewChain) { + const previous = this.dependencyChains.pop(); + + if (!previous || this.currentDependencyChain.length) { + throw new InternalError('Cyclic dependency checker is broken'); + } + + this.currentDependencyChain = previous; + } + } +} + +function indexedArgs(...values: string[]): Map { + return new Map(values.map((value, i) => [i, new ForcedArgument(value)])); +} + +function namedArgs(...names: string[]): Map { + return new Map(names.map((name) => [name, new ForcedArgument(name)])); +} + +function isServiceAsync(service: Service): boolean { + switch (service.factory?.kind) { + case 'local': + case 'auto-class': + if (service.factory.call.async || service.factory.call.args.async) { + return true; + } + break; + case 'foreign': + if (service.factory.async || service.factory.container.async) { + return true; + } + break; + } + + if (service.factory?.kind === 'auto-class' || service.factory?.kind === 'auto-interface') { + if (service.factory.method.name === 'create' && service.factory.method.eagerDeps.size) { + return true; + } + } + + return service.register?.async + || service.decorate?.async + || service.onCreate?.async + || false; +} + +function isAnonymous(definition: ServiceDefinition): boolean { + if (definition.isLocal()) { + return !definition.isExplicit() || definition.anonymous; + } + + return !definition.parent.isExplicit() || definition.parent.anonymous; +} + +function hasAsyncMode( + argument: Argument, +): argument is OverriddenArgument | SingleInjectedArgument | ListInjectedArgument | IterableInjectedArgument { + switch (argument.kind) { + case 'raw': + return false; + case 'injected': + switch (argument.mode) { + case 'scoped-runner': + case 'injector': + case 'accessor': + return false; + } + break; + } + + return true; +} diff --git a/core/cli/src/autowiring.ts b/core/cli/src/autowiring.ts deleted file mode 100644 index f3898dc..0000000 --- a/core/cli/src/autowiring.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Logger } from '@debugr/core'; -import { ServiceScope } from 'dicc'; -import { SourceFile } from 'ts-morph'; -import { ContainerBuilder } from './containerBuilder'; -import { DefinitionError, TypeError, UserError } from './errors'; -import { - ArgumentInfo, - ArgumentOverrideMap, - ServiceDecoratorInfo, - ServiceDefinitionInfo, - ServiceHooks, - TypeFlag, -} from './types'; - -export interface AutowiringFactory { - create(containers: Map): Autowiring; -} - -export class Autowiring { - private readonly visitedContainers: Set = new Set(); - private readonly visitedServices: Set = new Set(); - private readonly visitedDecorators: Map> = new Map(); - private readonly resolving: string[] = []; - - constructor( - private readonly logger: Logger, - private readonly containers: Map, - ) {} - - checkDependencies(): void { - for (const builder of this.containers.values()) { - this.checkContainer(builder); - } - } - - private checkContainer(builder: ContainerBuilder): void { - if (!this.visitContainer(builder)) { - return; - } - - this.logger.debug(`Checking container '${builder.path}'...`); - - for (const definition of builder.getDefinitions()) { - this.checkServiceDependencies(builder, definition); - } - - // needs to run after all dependencies have been fully resolved - for (const definition of builder.getDefinitions()) { - this.checkCyclicDependencies(builder, definition); - } - } - - private checkServiceDependencies(builder: ContainerBuilder, info: ServiceDefinitionInfo): void { - if (!this.visitService(info)) { - return; - } - - const desc = info.parent ? `'${info.path}.${info.id}'` : `'${info.path}'`; - - this.logger.debug(`Checking ${desc} dependencies...`); - const scope = this.resolveScope(info); - - if (info.container) { - const src = info.type.getSymbolOrThrow().getValueDeclarationOrThrow().getSourceFile(); - const params = this.containers.get(src)?.getParametersInfo(); - - if (params && info.factory?.method === 'constructor' && !info.factory.args.length) { - info.factory.args = [ - { name: 'parameters', type: params.type, flags: TypeFlag.None }, - ]; - } - } - - if (info.factory) { - if (info.parent) { - const parentSvc = builder.get(info.parent); - this.checkServiceDependencies(builder, parentSvc); - - if (parentSvc.async && !info.async) { - this.logger.trace(`Marking ${desc} as async because parent container needs to be awaited`); - info.async = true; - } - - const parentSrc = parentSvc.type.getSymbolOrThrow().getValueDeclarationOrThrow().getSourceFile(); - const parentBuilder = this.containers.get(parentSrc); - - if (parentBuilder) { - this.checkContainer(parentBuilder); - - if (parentBuilder.isAsync(info.type) && !info.factory.async) { - this.logger.trace(`Marking ${desc} factory as async because service is async in parent container`); - info.factory.async = true; - } - } - } else if (this.checkArguments(builder, info.factory.args, `service ${desc}`, scope, info.args) && !info.factory.async) { - this.logger.trace(`Marking ${desc} factory as async because one or more arguments need to be awaited`); - info.factory.async = true; - } - - if (info.factory.async && !info.async) { - this.logger.trace(`Marking ${desc} as async because factory is async`); - info.async = true; - } - } - - if (info.creates) { - this.logger.trace(`Checking auto-factory ${desc} target arguments...`); - const injectedArgs = info.creates.factory.args.filter((p) => !info.creates!.manualArgs.includes(p.name)); - - if (this.checkArguments(builder, injectedArgs, `auto-factory ${desc}`, scope, info.creates.args) && !info.creates.async) { - throw new DefinitionError(`Auto-factory ${desc} must return a Promise because target has async dependencies`); - } - } - - if (this.checkHooks(builder, info.hooks, `service ${desc}`, scope) && !info.async) { - this.logger.trace(`Marking ${desc} as async because its 'onCreate' hook is async`); - info.async = true; - } - - const flags = this.checkDecorators(builder, info.decorators, scope); - - if (flags.asyncDecorate && info.factory && !info.factory.async) { - this.logger.trace(`Marking ${desc} factory as async because it has an async decorator`); - info.factory.async = true; - } - - if ((flags.asyncDecorate || flags.asyncOnCreate) && !info.async) { - this.logger.trace(`Marking ${desc} as async because it has an async decorator`); - info.async = true; - } - } - - private resolveScope(definition: ServiceDefinitionInfo): ServiceScope { - const decoratorWithScope = definition.decorators.findLast((decorator) => decorator.scope !== undefined); - return decoratorWithScope?.scope ?? definition.scope ?? 'global'; - } - - private checkHooks(builder: ContainerBuilder, hooks: ServiceHooks, target: string, scope: ServiceScope): boolean { - for (const hook of ['onCreate', 'onFork', 'onDestroy'] as const) { - const info = hooks[hook]; - - if (!info) { - continue; - } - - this.logger.debug(`Checking ${target} '${hook}' hook...`); - - if (this.checkArguments(builder, info.args, `'${hook}' hook of ${target}`, scope) && !info.async) { - this.logger.trace(`Marking '${hook}' hook of ${target} as async because one or more arguments need to be awaited`); - info.async = true; - } - } - - return hooks.onCreate?.async ?? false; - } - - private checkDecorators(builder: ContainerBuilder, decorators: ServiceDecoratorInfo[], scope: ServiceScope): DecoratorFlags { - const flags: DecoratorFlags = {}; - - for (const decorator of decorators) { - this.checkDecorator(builder, decorator, scope, flags); - } - - return flags; - } - - private checkDecorator(builder: ContainerBuilder, decorator: ServiceDecoratorInfo, scope: ServiceScope, flags: DecoratorFlags): void { - if (!this.visitDecorator(decorator, scope)) { - decorator.decorate?.async && (flags.asyncDecorate = true); - decorator.hooks.onCreate?.async && (flags.asyncOnCreate = true); - return; - } - - if (decorator.decorate) { - if (this.checkArguments(builder, decorator.decorate.args, `decorator '${decorator.path}'`, scope)) { - this.logger.trace(`Marking decorator '${decorator.path}' as async because one or more arguments need to be awaited`); - decorator.decorate.async = true; - } - - if (decorator.decorate.async) { - flags.asyncDecorate = true; - } - } - - if (this.checkHooks(builder, decorator.hooks, `decorator '${decorator.path}'`, scope)) { - this.logger.trace(`Marking decorator '${decorator.path}' as async because its 'onCreate' hook is async`); - flags.asyncOnCreate = true; - } - } - - private checkArguments( - builder: ContainerBuilder, - args: ArgumentInfo[], - target: string, - scope: ServiceScope, - argOverrides?: ArgumentOverrideMap, - ): boolean { - let async = false; - - for (const arg of args) { - if (argOverrides && arg.name in argOverrides) { - const override = argOverrides[arg.name]; - - if (typeof override === 'object' && this.checkArguments(builder, override.args, `argument '${arg.name}' of ${target}`, scope)) { - this.logger.trace(`Argument '${arg.name}' of ${target} is async`); - async = true; - } - } else if (this.checkArgument(builder, arg, target, scope)) { - this.logger.trace(`Argument '${arg.name}' of ${target} is async`); - async = true; - } - } - - return async; - } - - private checkArgument(builder: ContainerBuilder, arg: ArgumentInfo, target: string, scope: ServiceScope): boolean { - if (arg.flags & TypeFlag.Container) { - return false; - } - - if (!arg.type) { - return this.checkOptional(arg, target); - } - - const services = builder.getByType(arg.type); - - if (services.length) { - return this.checkServiceCandidates(builder, arg, target, scope, services); - } - - const parameters = builder.getParametersByType(arg.type); - - if (!parameters) { - return this.checkOptional(arg, target); - } - - if (arg.flags & ~(TypeFlag.Optional | TypeFlag.Array)) { - throw new TypeError( - `Container parameters cannot be injected into iterables, async arguments, accessors, or injectors`, - arg.type, - ); - } - - if ('nestedTypes' in parameters) { - if (this.checkMultiple(arg)) { - throw new TypeError( - `Cannot inject container parameters into array argument '${arg.name}' of ${target}`, - arg.type, - ); - } - - return false; - } - - if (Boolean(parameters.flags & TypeFlag.Array) !== this.checkMultiple(arg)) { - const [p, a] = parameters.flags & TypeFlag.Array ? ['', 'non-'] : ['non-', '']; - - throw new TypeError( - `Cannot inject ${p}array parameter '${parameters.path}' into ${a}array argument '${arg.name}' of ${target}`, - arg.type, - ); - } - - return false; - } - - private checkServiceCandidates(builder: ContainerBuilder, arg: ArgumentInfo, target: string, scope: ServiceScope, candidates: ServiceDefinitionInfo[]): boolean { - if (candidates.length > 1 && !this.checkMultiple(arg)) { - throw new TypeError(`Multiple services for argument '${arg.name}' of ${target} found`, arg.type); - } else if (arg.flags & TypeFlag.Injector) { - if (candidates[0].scope === 'private') { - throw new TypeError(`Cannot inject injector for privately-scoped service '${candidates[0].id}' into ${target}`, arg.type); - } - - return false; - } - - let async = false; - - for (const candidate of candidates) { - this.checkServiceDependencies(builder, candidate); - - if (scope === 'global' && candidate.scope === 'local' && !(arg.flags & TypeFlag.Accessor)) { - throw new TypeError(`Cannot inject locally-scoped service '${candidate.id}' into globally-scoped ${target}`, arg.type); - } - - if (candidate.async && !(arg.flags & TypeFlag.Async)) { - if (arg.flags & (TypeFlag.Accessor | TypeFlag.Iterable)) { - throw new TypeError(`Cannot inject async service '${candidate.id}' into synchronous accessor or iterable argument '${arg.name}' of ${target}`, arg.type); - } - - async = true; - } - } - - return async; - } - - private checkOptional(arg: ArgumentInfo, target: string): boolean { - if (arg.flags & TypeFlag.Optional) { - this.logger.trace(`Skipping argument '${arg.name}' of ${target}: unknown argument type`); - return false; - } - - throw new TypeError( - arg.flags & TypeFlag.Injector - ? `Unknown service type in injector '${arg.name}' of ${target}` - : `Unable to autowire non-optional argument '${arg.name}' of ${target}`, - arg.type, - ); - } - - private checkMultiple(arg: ArgumentInfo): boolean { - return Boolean(arg.flags & (TypeFlag.Array | TypeFlag.Iterable)); - } - - private checkCyclicDependencies(builder: ContainerBuilder, definition: ServiceDefinitionInfo): void { - this.checkCyclicDependency(definition.id); - - for (const arg of definition.factory?.args ?? []) { - this.checkArgumentDependencies(builder, arg); - } - - for (const arg of definition.hooks?.onCreate?.args ?? []) { - this.checkArgumentDependencies(builder, arg); - } - - for (const arg of definition.decorators.flatMap((d) => [...d.decorate?.args ?? [], ...d.hooks.onCreate?.args ?? []])) { - this.checkArgumentDependencies(builder, arg); - } - - this.releaseCyclicDependencyCheck(definition.id); - } - - private checkArgumentDependencies(builder: ContainerBuilder, arg: ArgumentInfo): void { - if (arg.flags & (TypeFlag.Async | TypeFlag.Accessor | TypeFlag.Iterable)) { - return; - } - - const candidates = arg.type && builder.getByType(arg.type); - - for (const candidate of candidates ?? []) { - this.checkCyclicDependencies(builder, candidate); - } - } - - private checkCyclicDependency(id: string): void { - const idx = this.resolving.indexOf(id); - - if (idx > -1) { - throw new UserError(`Cyclic dependency detected: ${this.resolving.join(' → ')} → ${id}`); - } - - this.resolving.push(id); - } - - private releaseCyclicDependencyCheck(id: string): void { - const last = this.resolving.pop(); - - if (last !== id) { - throw new Error(`Dependency chain checker broken`); - } - } - - private visitContainer(builder: ContainerBuilder): boolean { - if (this.visitedContainers.has(builder)) { - return false; - } - - this.visitedContainers.add(builder); - return true; - } - - private visitService(definition: ServiceDefinitionInfo): boolean { - if (this.visitedServices.has(definition)) { - return false; - } - - this.visitedServices.add(definition); - return true; - } - - private visitDecorator(decorator: ServiceDecoratorInfo, scope: ServiceScope): boolean { - const scopes = this.visitedDecorators.get(decorator) ?? new Set(); - this.visitedDecorators.set(decorator, scopes); - - if (scopes.has(scope)) { - return false; - } - - scopes.add(scope); - return true; - } -} - -type DecoratorFlags = { - asyncDecorate?: boolean; - asyncOnCreate?: boolean; -}; diff --git a/core/cli/src/bootstrap.ts b/core/cli/src/bootstrap.ts deleted file mode 100644 index 15946fb..0000000 --- a/core/cli/src/bootstrap.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Container, ServiceType } from 'dicc'; -import * as argv0 from './argv'; -import * as autowiring0 from './autowiring'; -import * as checker0 from './checker'; -import * as configLoader0 from './configLoader'; -import * as definitionScanner0 from './definitionScanner'; -import * as definitions0 from './definitions'; -import * as iffy0 from './iffy'; -import * as sourceFiles0 from './sourceFiles'; -import * as typeHelper0 from './typeHelper'; - -interface PublicServices { - 'debug.logger': ServiceType; - 'dicc': Promise>; -} - -interface DynamicServices { - '#Iffy.0': iffy0.Iffy; -} - -interface AnonymousServices { - '#Argv.0': argv0.Argv; - '#AutowiringFactory.0': autowiring0.AutowiringFactory; - '#Checker.0': Promise; - '#ConfigLoader.0': configLoader0.ConfigLoader; - '#ConsoleHandler.0': ServiceType; - '#DefinitionScanner.0': Promise; - '#Dicc.0': Promise>; - '#DiccConfig.0': Promise>; - '#Logger.0': ServiceType; - '#Plugin.0': ServiceType; - '#Project.0': Promise>; - '#SourceFiles.0': Promise; - '#TypeHelper.0': Promise; -} - -export class DiccContainer extends Container{ - constructor() { - super({ - 'debug.logger': { - ...definitions0.debug.logger, - aliases: ['#Logger.0'], - factory: (di) => definitions0.debug.logger.factory(di.find('#Plugin.0')), - }, - 'dicc': { - aliases: ['#Dicc.0'], - async: true, - factory: async (di) => new definitions0.dicc( - await di.get('#SourceFiles.0'), - await di.get('#DefinitionScanner.0'), - di.get('#AutowiringFactory.0'), - await di.get('#Checker.0'), - await di.get('#DiccConfig.0'), - di.get('#Logger.0'), - ), - }, - '#Argv.0': { - factory: () => new argv0.Argv(), - }, - '#AutowiringFactory.0': { - factory: (di) => ({ - create: (containers) => new autowiring0.Autowiring( - di.get('#Logger.0'), - containers, - ), - }), - }, - '#Checker.0': { - async: true, - factory: async (di) => new checker0.Checker( - await di.get('#TypeHelper.0'), - di.get('#Logger.0'), - ), - }, - '#ConfigLoader.0': { - factory: (di) => new configLoader0.ConfigLoader(di.get('#Argv.0')), - }, - '#ConsoleHandler.0': { - ...definitions0.debug.console, - aliases: ['#Plugin.0'], - factory: (di) => definitions0.debug.console.factory(di.get('#Argv.0')), - }, - '#DefinitionScanner.0': { - async: true, - factory: async (di) => new definitionScanner0.DefinitionScanner( - await di.get('#TypeHelper.0'), - di.get('#Logger.0'), - ), - }, - '#DiccConfig.0': { - ...definitions0.config, - async: true, - factory: async (di) => definitions0.config.factory(di.get('#ConfigLoader.0')), - }, - '#Project.0': { - ...definitions0.project, - async: true, - factory: async (di) => definitions0.project.factory(await di.get('#DiccConfig.0')), - }, - '#SourceFiles.0': { - async: true, - factory: async (di) => new sourceFiles0.SourceFiles( - await di.get('#Project.0'), - await di.get('#DiccConfig.0'), - di.get('#Logger.0'), - ), - }, - '#TypeHelper.0': { - async: true, - factory: async (di) => new typeHelper0.TypeHelper(await di.get('#Project.0')), - }, - }); - } -} - - - diff --git a/core/cli/src/checker.ts b/core/cli/src/checker.ts deleted file mode 100644 index a546410..0000000 --- a/core/cli/src/checker.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Logger, LogLevel } from '@debugr/core'; -import { DiagnosticCategory, DiagnosticMessageChain, Node, SourceFile } from 'ts-morph'; -import { ContainerBuilder } from './containerBuilder'; -import { DefinitionError } from './errors'; -import { TypeHelper } from './typeHelper'; -import { TypeFlag } from './types'; - -export class Checker { - constructor( - private readonly helper: TypeHelper, - private readonly logger: Logger, - ) {} - - checkAutoFactories(builder: ContainerBuilder): void { - for (const definition of builder.getDefinitions()) { - if (definition.factory) { - definition.id === '#HelloFactory.0' && this.logger.warning(`${definition.id} has factory`); - continue; - } - - const [signature, method] = this.helper.resolveAutoFactorySignature(definition.type); - - if (!signature) { - definition.id === '#HelloFactory.0' && this.logger.warning(`${definition.id} has no signature`); - continue; - } - - const [serviceType, async] = this.helper.unwrapAsyncType(signature.getReturnType()); - const [serviceDef, ...rest] = builder.getByType(serviceType); - - if (!serviceDef) { - definition.id === '#HelloFactory.0' && this.logger.warning(`${definition.id} has no target service`); - continue; - } else if (rest.length) { - throw new DefinitionError(`Multiple services satisfy return type of auto factory '${definition.id}'`); - } else if (!serviceDef.factory) { - throw new DefinitionError(`Cannot auto-implement factory '${definition.id}': unable to resolve target service factory`); - } - - this.logger.debug(`Promoting '${definition.id}' to auto-factory of '${serviceDef.id}'`); - - const manualArgs = signature.getParameters().map((p) => p.getName()); - - definition.creates = { - method, - manualArgs, - async, - source: serviceDef.source, - path: serviceDef.path, - type: serviceDef.type, - object: serviceDef.object, - explicit: serviceDef.explicit, - factory: serviceDef.factory, - args: serviceDef.args, - }; - - if (method !== 'get') { - builder.unregister(serviceDef.id); - } - } - } - - removeExtraneousImplicitRegistrations(builder: ContainerBuilder): void { - for (const def of builder.getDefinitions()) { - if (def.explicit) { - continue; - } - - const others = builder.getByType(def.type).filter((d) => d !== def); - - if ((!def.factory && others.find((d) => d.factory)) || others.find((d) => d.explicit)) { - this.logger.debug(`Unregistered extraneous service '${def.path}'`); - builder.unregister(def.id); - } - } - } - - scanUsages(builder: ContainerBuilder): void { - for (const method of ['get', 'find', 'iterate']) { - for (const call of this.helper.getContainerMethodCalls(method)) { - const [id] = call.getArguments(); - - if (Node.isStringLiteral(id) && !builder.has(id.getLiteralValue())) { - const sf = id.getSourceFile(); - const ln = id.getStartLineNumber(); - this.logger.warning(`Unknown service '${id.getLiteralValue()}' in call to Container.${method}() in '${sf.getFilePath()}' on line ${ln}`); - } - } - } - - const registrations: Set = new Set(); - - for (const call of this.helper.getContainerMethodCalls('register')) { - const [id] = call.getArguments(); - - if (Node.isStringLiteral(id)) { - registrations.add(id.getLiteralValue()); - } - } - - const injectors: Set = new Set(); - const dynamic: Set = new Set(); - - for (const definition of builder.getDefinitions()) { - if (!definition.factory) { - dynamic.add(definition.id); - } else { - for (const arg of definition.factory.args) { - if (arg.flags & TypeFlag.Injector) { - const [id] = arg.type ? builder.getIdsByType(arg.type) : [] - injectors.add(id); - } - } - } - } - - for (const id of dynamic) { - if (!registrations.has(id) && !injectors.has(id)) { - this.logger.warning(`No Container.register() call found for dynamic service '${id}'`); - } - } - } - - checkOutput(output: SourceFile): boolean { - let ok = true; - - for (const diagnostic of output.getPreEmitDiagnostics()) { - this.logger.log( - this.getDiagnosticCategoryLogLevel(diagnostic.getCategory()), - this.formatDiagnostic(diagnostic.getMessageText(), diagnostic.getLineNumber()), - ); - - diagnostic.getCategory() === DiagnosticCategory.Error && (ok = false); - } - - return ok; - } - - private getDiagnosticCategoryLogLevel(category: DiagnosticCategory): LogLevel { - switch (category) { - case DiagnosticCategory.Warning: return LogLevel.WARNING; - case DiagnosticCategory.Error: return LogLevel.ERROR; - default: return LogLevel.INFO; - } - } - - private formatDiagnostic(message: DiagnosticMessageChain | string, line?: number): string { - return line !== undefined - ? `line ${line} in compiled container: ${this.formatDiagnosticMessage(message)}` - : `in compiled container: ${this.formatDiagnosticMessage(message)}`; - } - - private formatDiagnosticMessage(...messages: (DiagnosticMessageChain | string)[]): string { - return messages.map((message) => { - if (typeof message === 'string') { - return message; - } - - return this.formatDiagnosticMessage(message.getMessageText(), ...message.getNext() ?? []); - }).join('\n'); - } -} diff --git a/core/cli/src/cli.ts b/core/cli/src/cli.ts deleted file mode 100644 index d0599a0..0000000 --- a/core/cli/src/cli.ts +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node - -import { DiccContainer } from './bootstrap'; -import { UserError } from './errors'; - -const isVerbose = process.argv.slice(2).find((arg) => /^(-v+|--verbose)/.test(arg)); - -(async () => { - try { - const container = new DiccContainer(); - - try { - const dicc = await container.get('dicc'); - await dicc.compile(); - } catch (e: any) { - const isUserError = e instanceof UserError; - const logger = container.get('debug.logger'); - logger.error(isVerbose && !isUserError ? e : e.message); - process.exit(1); - } - } catch (e: any) { - const isUserError = e instanceof UserError; - - if (isVerbose && !isUserError) { - throw e; - } else { - console.log(`Error: ${e.message}`); - process.exit(1); - } - } -})(); diff --git a/core/cli/src/argv.ts b/core/cli/src/cli/argv.ts similarity index 100% rename from core/cli/src/argv.ts rename to core/cli/src/cli/argv.ts diff --git a/core/cli/src/cli/bootstrap/container.ts b/core/cli/src/cli/bootstrap/container.ts new file mode 100644 index 0000000..a6219e5 --- /dev/null +++ b/core/cli/src/cli/bootstrap/container.ts @@ -0,0 +1,296 @@ +import { Container, type ServiceType } from 'dicc'; +import * as di4 from '../../analysis/di'; +import type * as di6 from '../../compiler/di'; +import type * as di5 from '../../container/di'; +import type * as di3 from '../../definitions/di'; +import type * as di0 from '../../extensions/di'; +import type * as di1 from '../../utils/di'; +import * as di2 from '../di'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'compiler': Promise>; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#Argv0.0': di2.Argv; + '#AutowiringFactory0.0': Promise; + '#BuilderMap0.0': Promise; + '#BuilderReflectionFactory0.0': di4.BuilderReflectionFactory; + '#CompilerConfig0.0': Promise>; + '#CompilerExtension0': Promise< + | di0.DecoratorsExtension + | di0.ServicesExtension + >; + '#ConfigLoader0.0': ServiceType; + '#ConsoleHandler0.0': ServiceType; + '#ContainerAnalyser0.0': Promise; + '#ContainerBuilderFactory0.0': Promise; + '#ContainerCompiler0.0': Promise; + '#ContainerReflector0.0': Promise; + '#DecoratorsExtension0.0': Promise; + '#EventDispatcher0.0': Promise>; + '#EventSubscriber0': Promise< + | di0.DecoratorsExtension + | di0.ServicesExtension + >; + '#ExtensionLoader0.0': Promise; + '#ExternalReflectionFactory0.0': Promise; + '#Logger0.0': ServiceType; + '#ModuleResolver0.0': Promise; + '#Project0.0': Promise>; + '#ReferenceResolverFactory0.0': Promise; + '#ResourceScanner0.0': Promise; + '#ServiceAnalyser0.0': Promise; + '#ServiceCompiler0.0': Promise; + '#ServicesExtension0.0': Promise; + '#TypeHelper0.0': Promise; + '#WriterFactory0.0': Promise; +} + +export class DiccContainer extends Container { + constructor() { + super({ + 'compiler': { + factory: async (di) => new definitions0.compiler( + await di.get('#ExtensionLoader0.0'), + di.iterate('#CompilerExtension0'), + await di.get('#ResourceScanner0.0'), + await di.get('#ContainerAnalyser0.0'), + await di.get('#ContainerCompiler0.0'), + await di.get('#BuilderMap0.0'), + ), + async: true, + }, + '#Argv0.0': { + factory: () => new di2.Argv(), + }, + '#AutowiringFactory0.0': { + factory: async (di) => { + const call2Arg0 = await di.get('#ContainerReflector0.0'); + return { + create: (serviceAnalyser) => new di4.Autowiring( + call2Arg0, + serviceAnalyser, + ), + }; + }, + async: true, + }, + '#BuilderMap0.0': { + factory: async (di) => { + const di5 = await import('../../container/di.js'); + return new di5.BuilderMap( + await di.get('#Project0.0'), + await di.get('#ContainerBuilderFactory0.0'), + await di.get('#CompilerConfig0.0'), + ); + }, + async: true, + }, + '#BuilderReflectionFactory0.0': { + factory: () => ({ + create: (container) => new di4.BuilderReflection( + container, + ), + }), + }, + '#CompilerConfig0.0': { + factory: async (di) => definitions0.config.compilerConfig.factory( + di.get('#ConfigLoader0.0'), + ), + async: true, + }, + '#ConfigLoader0.0': { + factory: (di) => new definitions0.config.loader.factory( + definitions0.config.loader.args.configFile( + di.get('#Argv0.0'), + ), + ), + }, + '#ConsoleHandler0.0': { + factory: (di) => new definitions0.debug.console.factory( + definitions0.debug.console.args.options( + di.get('#Argv0.0'), + ), + ), + scope: 'private', + }, + '#ContainerAnalyser0.0': { + factory: async (di) => new di4.ContainerAnalyser( + await di.get('#ContainerReflector0.0'), + await di.get('#ServiceAnalyser0.0'), + ), + async: true, + }, + '#ContainerBuilderFactory0.0': { + factory: async (di) => { + const di5 = await import('../../container/di.js'); + const call1Arg0 = await di.get('#EventDispatcher0.0'); + return { + create: (sourceFile, options) => new di5.ContainerBuilder( + call1Arg0, + sourceFile, + options, + ), + }; + }, + async: true, + }, + '#ContainerCompiler0.0': { + factory: async (di) => { + const di6 = await import('../../compiler/di.js'); + return new di6.ContainerCompiler( + await di.get('#ServiceCompiler0.0'), + await di.get('#WriterFactory0.0'), + ); + }, + async: true, + }, + '#ContainerReflector0.0': { + factory: async (di) => new di4.ContainerReflector( + di.get('#BuilderReflectionFactory0.0'), + await di.get('#ExternalReflectionFactory0.0'), + await di.get('#BuilderMap0.0'), + ), + async: true, + }, + '#DecoratorsExtension0.0': { + aliases: [ + '#EventSubscriber0', + '#CompilerExtension0', + ], + factory: async (di) => { + const di0 = await import('../../extensions/di.js'); + return new di0.DecoratorsExtension( + await di.get('#TypeHelper0.0'), + ); + }, + async: true, + }, + '#EventDispatcher0.0': { + factory: () => new definitions0.eventDispatcher.factory(), + async: true, + onCreate: async (service, di) => { + definitions0.eventDispatcher.onCreate( + service, + await di.find('#EventSubscriber0'), + ); + }, + }, + '#ExtensionLoader0.0': { + factory: async (di) => { + const di0 = await import('../../extensions/di.js'); + return new di0.ExtensionLoader( + await di.get('#EventDispatcher0.0'), + async () => di.get('#ModuleResolver0.0'), + async () => di.get('#TypeHelper0.0'), + await di.get('#ReferenceResolverFactory0.0'), + di.get('#Logger0.0'), + await di.get('#CompilerConfig0.0'), + ); + }, + async: true, + }, + '#ExternalReflectionFactory0.0': { + factory: async (di) => { + const call0Arg0 = await di.get('#TypeHelper0.0'); + return { + create: (container) => new di4.ExternalReflection( + call0Arg0, + container, + ), + }; + }, + async: true, + }, + '#Logger0.0': { + factory: (di) => definitions0.debug.logger.factory( + di.find('#ConsoleHandler0.0'), + ), + }, + '#ModuleResolver0.0': { + factory: async (di) => { + const di1 = await import('../../utils/di.js'); + return new di1.ModuleResolver( + await di.get('#Project0.0'), + ); + }, + async: true, + }, + '#Project0.0': { + factory: async (di) => definitions0.project.factory( + await di.get('#CompilerConfig0.0'), + ), + async: true, + }, + '#ReferenceResolverFactory0.0': { + factory: async (di) => { + const di1 = await import('../../utils/di.js'); + return new di1.ReferenceResolverFactory( + await di.get('#ModuleResolver0.0'), + ); + }, + async: true, + }, + '#ResourceScanner0.0': { + factory: async (di) => { + const di3 = await import('../../definitions/di.js'); + return new di3.ResourceScanner( + await di.get('#Project0.0'), + await di.get('#EventDispatcher0.0'), + ); + }, + async: true, + }, + '#ServiceAnalyser0.0': { + factory: async (di) => new di4.ServiceAnalyser( + await di.get('#AutowiringFactory0.0'), + ), + async: true, + }, + '#ServiceCompiler0.0': { + factory: async (di) => { + const di6 = await import('../../compiler/di.js'); + return new di6.ServiceCompiler( + await di.get('#WriterFactory0.0'), + ); + }, + async: true, + }, + '#ServicesExtension0.0': { + aliases: [ + '#EventSubscriber0', + '#CompilerExtension0', + ], + factory: async (di) => { + const di0 = await import('../../extensions/di.js'); + return new di0.ServicesExtension( + await di.get('#TypeHelper0.0'), + ); + }, + async: true, + }, + '#TypeHelper0.0': { + factory: async (di) => { + const di1 = await import('../../utils/di.js'); + return new di1.TypeHelper( + await di.get('#ReferenceResolverFactory0.0'), + ); + }, + async: true, + }, + '#WriterFactory0.0': { + factory: async (di) => { + const di6 = await import('../../compiler/di.js'); + return new di6.WriterFactory( + await di.get('#Project0.0'), + ); + }, + async: true, + }, + }); + } +} diff --git a/core/cli/src/cli/bootstrap/definitions.ts b/core/cli/src/cli/bootstrap/definitions.ts new file mode 100644 index 0000000..c8f2332 --- /dev/null +++ b/core/cli/src/cli/bootstrap/definitions.ts @@ -0,0 +1,68 @@ +import { ConsoleHandler } from '@debugr/console'; +import { Logger, Plugin } from '@debugr/core'; +import { ServiceDefinition } from 'dicc'; +import { IndentationText, NewLineKind, Project, QuoteKind } from 'ts-morph'; +import { Compiler } from '../../compiler'; +import { CompilerConfig, ConfigLoader } from '../../config'; +import { EventDispatcher, EventSubscriber } from '../../events'; +import { Argv } from '../argv'; + +export namespace debug { + export const logger = { + factory: (plugins: Plugin[]) => new Logger({ + globalContext: {}, + plugins, + }), + anonymous: true, + } satisfies ServiceDefinition; + + export const console = { + factory: ConsoleHandler, + args: { + options: (argv: Argv) => ({ threshold: argv.logLevel, timestamp: false }), + }, + scope: 'private', + anonymous: true, + } satisfies ServiceDefinition; +} + +export namespace config { + export const loader = { + factory: ConfigLoader, + args: { + configFile: (argv: Argv) => argv.configFile, + }, + anonymous: true, + } satisfies ServiceDefinition; + + export const compilerConfig = { + factory: async (loader: ConfigLoader) => loader.load(), + anonymous: true, + } satisfies ServiceDefinition; +} + +export const project = { + factory: (config: CompilerConfig) => new Project({ + tsConfigFilePath: config.project, + skipAddingFilesFromTsConfig: true, + manipulationSettings: { + indentationText: IndentationText.TwoSpaces, + newLineKind: NewLineKind.LineFeed, + quoteKind: QuoteKind.Single, + useTrailingCommas: true, + }, + }), + anonymous: true, +} satisfies ServiceDefinition; + +export const eventDispatcher = { + factory: EventDispatcher, + anonymous: true, + onCreate: (dispatcher, subscribers: EventSubscriber[]) => { + for (const subscriber of subscribers) { + dispatcher.addSubscriber(subscriber); + } + }, +} satisfies ServiceDefinition; + +export const compiler = Compiler satisfies ServiceDefinition; diff --git a/core/cli/src/cli/bootstrap/index.ts b/core/cli/src/cli/bootstrap/index.ts new file mode 100644 index 0000000..85ee15b --- /dev/null +++ b/core/cli/src/cli/bootstrap/index.ts @@ -0,0 +1 @@ +export * from './container'; diff --git a/core/cli/src/cli/di.ts b/core/cli/src/cli/di.ts new file mode 100644 index 0000000..59c8658 --- /dev/null +++ b/core/cli/src/cli/di.ts @@ -0,0 +1 @@ +export { Argv } from './argv'; diff --git a/core/cli/src/cli/dicc.ts b/core/cli/src/cli/dicc.ts new file mode 100644 index 0000000..a9ea661 --- /dev/null +++ b/core/cli/src/cli/dicc.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import { formatErrorReport } from '../errors'; +import { DiccContainer } from './bootstrap'; + +async function main(): Promise { + const container = new DiccContainer(); + const compiler = await container.get('compiler'); + await compiler.compile(); +} + +main().catch((e: unknown) => { + process.stderr.write([...formatErrorReport(e)].join('')); +}); diff --git a/core/cli/src/compiler.ts b/core/cli/src/compiler.ts deleted file mode 100644 index fe1c74e..0000000 --- a/core/cli/src/compiler.ts +++ /dev/null @@ -1,673 +0,0 @@ -import { ServiceScope } from 'dicc'; -import { CodeBlockWriter, SourceFile, Type } from 'ts-morph'; -import { ContainerBuilder } from './containerBuilder'; -import { - ArgumentInfo, - ArgumentOverrideMap, - AutoFactoryTarget, - CallbackInfo, - ContainerParametersInfo, - ServiceDecoratorInfo, - ServiceDefinitionInfo, - TypeFlag, -} from './types'; - -export class Compiler { - constructor( - private readonly builder: ContainerBuilder, - ) {} - - compile(): void { - const definitions = [...this.builder.getDefinitions()].sort( - (a, b) => compareIDs(a.parent ?? '', b.parent ?? '') || compareIDs(a.id, b.id), - ); - const sources = extractSources(definitions, this.builder.getParametersInfo()); - - this.builder.output.replaceWithText(''); - - this.writeHeader(sources); - this.writeMap(definitions, sources); - this.writeDefinitions(definitions, sources); - - if (this.builder.options.preamble !== undefined) { - this.builder.output.insertText(0, this.builder.options.preamble.replace(/\s*$/, '\n\n')); - } - } - - private writeHeader(sources: Map): void { - const imports = [...sources].map(([source, name]) => [ - name, - this.builder.output.getRelativePathAsModuleSpecifierTo(source), - ]); - - this.builder.output.addImportDeclaration({ - moduleSpecifier: 'dicc', - namedImports: [ - { name: 'Container' }, - ], - }); - - for (const [namespaceImport, moduleSpecifier] of imports) { - this.builder.output.addImportDeclaration({ moduleSpecifier, namespaceImport }); - } - } - - private writeMap(definitions: ServiceDefinitionInfo[], sources: Map): void { - this.builder.output.addStatements((writer) => { - const aliasMap: Map> = new Map(); - let useForeignServiceType = false; - let useServiceType = false; - - writer.writeLine(`\ninterface Services {`); - - writer.indent(() => { - for (const { source, id, path, type, aliases, factory, async, explicit, parent } of definitions) { - const method = !explicit && factory?.method !== 'constructor' && factory?.method; - const fullPath = join('.', sources.get(source), path, method); - const serviceType = !explicit && !method && !path.includes('.') - ? fullPath - : `ServiceType`; - const fullType = parent - ? maybeAsync(`ForeignServiceType<${serviceType}, '${id}'>`, async && !factory?.async) - : maybeAsync(serviceType, async); - - if (!id.startsWith('#')) { - writer.writeLine(`'${parent ? `${parent}.` : ''}${id}': ${fullType};`); - } - - for (const typeAlias of [type, ...aliases]) { - const alias = this.builder.getTypeId(typeAlias); - - if (alias !== undefined) { - aliasMap.has(alias) || aliasMap.set(alias, new Set()); - aliasMap.get(alias)!.add(fullType); - } - } - - if (parent) { - useForeignServiceType = true; - } else if (serviceType !== fullPath) { - useServiceType = true; - } - } - - for (const [alias, ids] of [...aliasMap].sort((a, b) => compareIDs(a[0], b[0]))) { - if (ids.size > 1) { - writer.writeLine(`'${alias}':`); - writer.indent(() => { - let n = ids.size; - - for (const id of ids) { - writer.writeLine(`| ${id}${--n ? '' : ';'}`); - } - }); - } else { - writer.writeLine(`'${alias}': ${[...ids].join('')};`); - } - } - }); - - writer.writeLine('}'); - - if (useForeignServiceType) { - this.builder.output.getImportDeclarationOrThrow('dicc').addNamedImport('ForeignServiceType'); - } - - if (useServiceType) { - this.builder.output.getImportDeclarationOrThrow('dicc').addNamedImport('ServiceType'); - } - }); - } - - private writeDefinitions( - definitions: ServiceDefinitionInfo[], - sources: Map, - ): void { - this.builder.output.addStatements((writer) => { - const declaration = this.builder.options.className === 'default' - ? 'default class' - : `class ${this.builder.options.className}`; - const [paramType, ctorArgs, superArgs] = this.compileContainerParameters(sources); - - writer.writeLine(`\nexport ${declaration} extends Container{`); - - writer.indent(() => { - writer.writeLine(`constructor(${ctorArgs}) {`); - writer.indent(() => { - writer.writeLine(`super(${superArgs}, {`); - writer.indent(() => { - for (const definition of definitions) { - this.compileDefinition(writer, definition, sources); - } - }); - writer.writeLine('});'); - }); - writer.writeLine('}'); - }); - - writer.writeLine('}\n'); - }); - } - - private compileContainerParameters(sources: Map): [type: string, ctorArgs: string, superArgs: string] { - const info = this.builder.getParametersInfo(); - - if (!info) { - return ['', '', '{}']; - } - - const type = join('.', sources.get(info.source), info.path); - - return [ - `, ${type}`, - `parameters: ${type}`, - 'parameters', - ]; - } - - private compileDefinition( - writer: CodeBlockWriter, - { source, id, path, type, factory, args, scope = 'global', async, container, parent, object, creates, hooks, aliases, decorators }: ServiceDefinitionInfo, - sources: Map, - ): void { - const decoratorMap = getDecoratorMap(decorators, sources); - const src = sources.get(source)!; - writer.writeLine(`'${parent ? `${parent}.` : ''}${id}': {`); - - writer.indent(() => { - !parent && object && writer.writeLine(`...${src}.${path},`); - - const types = [!/^#/.test(id) ? type : undefined, ...aliases] - .filter((v): v is Type => v !== undefined) - .map((t) => `'${this.builder.getTypeId(t)}'`); - - writer.conditionalWriteLine(types.length > 0, `aliases: [${types.join(`, `)}],`); - writer.conditionalWriteLine(async, `async: true,`); - writer.conditionalWriteLine(container, `container: true,`); - - if (parent) { - writer.writeLine(`scope: 'private',`); - } else if (decoratorMap.scope && decoratorMap.scope !== scope) { - writer.writeLine(`scope: '${decoratorMap.scope}',`); - } - - if (factory) { - if (parent) { - this.compileForeignFactory(writer, parent, id, async); - } else if (!object && factory.method !== 'constructor' && !factory.args.length && !decoratorMap.decorate.length) { - writer.writeLine(`factory: ${join('.', src, path, factory.method)},`); - } else if (!object || factory.method === 'constructor' || factory.args.length || decoratorMap.decorate.length) { - this.compileFactory( - writer, - src, - path, - factory.async, - factory.method, - object, - factory.returnType.isNullable(), - factory.args, - args, - decoratorMap.decorate, - ); - } - // else definition is an object with a factory function with zero arguments and no decorators, - // so it is already included in the compiled definition courtesy of object spread - } else if (creates) { - if (creates.method === 'get') { - this.compileAutoClassAccessor(writer, src, path, creates, object); - } else { - this.compileAutoFactory(writer, src, path, creates, sources, type, object); - } - } else { - writer.writeLine(`factory: undefined,`); - } - - for (const hook of ['onCreate', 'onFork', 'onDestroy'] as const) { - const info = hooks[hook]; - - if (info?.args.length || decoratorMap[hook].length) { - this.compileHook(writer, src, path, hook, info, decoratorMap[hook]); - } - } - }); - - writer.writeLine('},'); - } - - private compileForeignFactory( - writer: CodeBlockWriter, - parent: string, - id: string, - async?: boolean, - ): void { - const parentIsAsync = this.builder.get(parent).async; - - writer.write(`factory: `); - writer.conditionalWrite(async || parentIsAsync, 'async '); - - if (!parentIsAsync) { - writer.write(`(di) => di.get('${parent}').get('${id}'),\n`); - } else { - writer.write('(di) => {\n'); - writer.indent(() => { - writer.writeLine(`const src = await di.get('${parent}');`); - writer.writeLine(`return src.get('${id}');`); - }); - writer.write('},\n'); - } - } - - private compileFactory( - writer: CodeBlockWriter, - source: string, - path: string, - async: boolean | undefined, - method: string | undefined, - object: boolean | undefined, - optional: boolean, - args: ArgumentInfo[], - argOverrides: ArgumentOverrideMap | undefined, - decorators: DecoratorInfo[], - ): void { - const argValues = this.compileArguments(args, argOverrides && this.compileOverrides(writer, source, path, argOverrides)); - const decArgValues = decorators.map(([,, info]) => this.compileArguments(info.args)); - const inject = argValues.length > 0 || decArgValues.some((p) => p.length > 0); - - writer.write(`factory: `); - writer.conditionalWrite(async, 'async '); - writer.write(inject ? '(di) => ' : '() => '); - - const writeFactoryCall = () => { - this.compileCall( - writer, - join( - ' ', - method === 'constructor' && 'new', - async && decorators.length && 'await', - join('.', source, path, object && 'factory', method !== 'constructor' && method), - ), - argValues, - ); - }; - - if (!decorators.length) { - writeFactoryCall(); - writer.write(',\n'); - return; - } - - writer.write(`{\n`); - - writer.indent(() => { - writer.write(`${decorators.length > 1 ? 'let' : 'const'} service = `); - writeFactoryCall(); - writer.write(';\n'); - - if (optional) { - writer.write('\nif (service === undefined) {'); - writer.indent(() => writer.writeLine('return undefined;')); - writer.write('}\n'); - } - - for (const [i, [source, path, info]] of decorators.entries()) { - const last = i + 1 >= decorators.length; - writer.conditionalWrite(optional || (i > 0 ? decArgValues[i - 1] : argValues).length > 0 || decArgValues[i].length > 0, '\n'); - writer.write(last ? 'return ' : 'service = '); - this.compileCall(writer, join(' ', info.async && !last && 'await', join('.', source, path, 'decorate')), ['service', ...decArgValues[i]]); - writer.write(';\n'); - } - }); - - writer.write('},\n'); - } - - private compileAutoClassAccessor( - writer: CodeBlockWriter, - source: string, - path: string, - creates: AutoFactoryTarget, - object?: boolean, - ): void { - writer.writeLine(`factory: (di) => new class extends ${join('.', source, path, object && 'factory')} {`); - writer.indent(() => { - writer.conditionalWrite(creates.async, 'async '); - writer.write('get() {'); - writer.indent(() => writer.write(`return di.get('${this.builder.getTypeId(creates.type)}');`)); - writer.write('}'); - }); - writer.write('},\n'); - } - - private compileAutoFactory( - writer: CodeBlockWriter, - source: string, - path: string, - creates: AutoFactoryTarget, - sources: Map, - type: Type, - object?: boolean, - ): void { - const args = this.compileArguments(creates.factory.args, { - ...(creates.args ? this.compileOverrides(writer, source, path, creates.args) : {}), - ...Object.fromEntries(creates.manualArgs.map((p) => [p, p])), - }); - - const inject = args.length > 0; - const extend = type.isClass(); - - const writeAsync = () => { - writer.conditionalWrite(creates.async, 'async '); - }; - - const writeArgs = () => { - writer.write(`(${creates.manualArgs.join(', ')})`); - }; - - const writeFactory = () => { - this.compileCall( - writer, - join( - ' ', - creates.factory.method === 'constructor' && 'new', - join('.', sources.get(creates.source), creates.path, creates.object && 'factory', creates.factory.method !== 'constructor' && creates.factory.method), - ), - args, - ); - }; - - const writeArrow = () => { - writeAsync(); - writeArgs(); - writer.write(' => '); - writeFactory(); - }; - - writer.write(`factory: `); - writer.write(inject ? '(di) => ' : '() => '); - - if (!creates.method) { - writeArrow(); - } else if (extend) { - writer.write(`new class extends ${join('.', source, path, object && 'factory')} {`); - writer.indent(() => { - writeAsync(); - writer.write(creates.method!); - writeArgs(); - writer.block(() => { - writer.write('return '); - writeFactory(); - }); - }); - writer.write('}'); - } else { - writer.write('({\n'); - writer.indent(() => { - writer.write(`${creates.method}: `); - writeArrow(); - writer.write(',\n'); - }); - writer.write('})'); - } - - writer.write(',\n'); - } - - private compileHook( - writer: CodeBlockWriter, - source: string, - path: string, - hook: string, - info: CallbackInfo | undefined, - decorators: DecoratorInfo[], - ): void { - const args = ['service', ...this.compileArguments(info?.args ?? [])]; - const decoratorArgs = decorators.map(([,, info]) => this.compileArguments(info.args)); - const inject = args.length > 1 || decoratorArgs.some((p) => p.length > 0); - const async = info?.async || decorators.some(([,, info]) => info.async); - - writer.write(`${hook}: `); - writer.conditionalWrite(async, 'async '); - writer.write(`(`); - writer.conditionalWrite(hook === 'onFork', 'callback, '); - writer.write(`service`); - writer.conditionalWrite(inject, ', di'); - writer.write(') => '); - - if (!decorators.length) { - this.compileCall(writer, join('.', source, path, hook), hook === 'onFork' ? ['callback', ...args] : args); - writer.write(',\n'); - return; - } - - writer.write('{\n'); - - writer.indent(() => { - if (info) { - if (hook === 'onFork') { - const tmp = new CodeBlockWriter(writer.getOptions()); - tmp.write('async (fork) => {\n'); - tmp.indent(() => { - this.compileDecoratorCalls(tmp, decorators, 'fork ?? service', hook, decoratorArgs); - tmp.write('return callback(fork);\n'); - }); - tmp.write('}'); - args.unshift(tmp.toString()); - } - - this.compileCall(writer, join(' ', hook === 'onFork' ? 'return' : info.async && 'await', join('.', source, path, hook)), args); - writer.write(';\n'); - } - - if (!info || hook !== 'onFork') { - writer.conditionalWrite(args.length > 1 || decoratorArgs[0].length > 0, '\n'); - this.compileDecoratorCalls(writer, decorators, 'service', hook, decoratorArgs); - } - - if (!info && hook === 'onFork') { - writer.write('return callback();\n'); - } - }); - - writer.write('},\n'); - } - - private compileDecoratorCalls(writer: CodeBlockWriter, decorators: DecoratorInfo[], service: string, hook: string, decoratorArgs: string[][]): void { - for (const [i, [source, path, info]] of decorators.entries()) { - writer.conditionalWrite(i > 0 && (decoratorArgs[i - 1].length > 0 || decoratorArgs[i].length > 0), '\n'); - this.compileCall(writer, join(' ', info.async && 'await', join('.', source, path, hook)), [service, ...decoratorArgs[i]]); - writer.write(';\n'); - } - } - - private compileCall(writer: CodeBlockWriter, expression: string, args: string[]): void { - writer.write(expression); - writer.write('('); - - if (args.length > 1) { - writer.indent(() => { - for (const arg of args) { - writer.writeLine(`${arg},`); - } - }); - } else if (args.length) { - writer.write(args[0]); - } - - writer.write(')'); - } - - private compileOverrides(writer: CodeBlockWriter, source: string, path: string, overrides: ArgumentOverrideMap): Record { - return Object.fromEntries(Object.entries(overrides).map(([name, info]) => { - switch (typeof info) { - case 'object': { - const args = this.compileArguments(info.args); - const tmp = new CodeBlockWriter(writer.getOptions()); - this.compileCall(tmp, join(' ', info.async && 'await', join('.', source, path, 'args', name)), args) - return [name, tmp.toString()]; - } - case 'string': { - const [method, arg] = /^%[a-z0-9_.]+$/i.test(info) - ? ['resolve', info.slice(1, -1)] - : ['expand', info]; - return [name, `di.parameters.${method}('${arg}')`]; - } - default: - return [name, join('.', source, path, 'args', name)]; - } - })); - } - - private compileArguments(args: ArgumentInfo[], overrides?: Record): string[] { - const stmts: string[] = []; - const undefs: string[] = []; - - for (const arg of args) { - const stmt = overrides && arg.name in overrides ? overrides[arg.name] : this.compileArgument(arg); - - if (stmt === undefined) { - undefs.push(`undefined`); - } else { - stmts.push(...undefs.splice(0, undefs.length), stmt); - } - } - - return stmts; - } - - private compileArgument(arg: ArgumentInfo): string | undefined { - if (arg.flags & TypeFlag.Container) { - return 'di'; - } else if (!arg.type) { - return undefined; - } - - const id = this.builder.getTypeId(arg.type); - - if (id !== undefined) { - return this.compileServiceInjection(arg.type, arg.flags, id); - } - - const parameters = this.builder.getParametersByType(arg.type); - - if (!parameters) { - return undefined; - } - - if ('nestedTypes' in parameters) { - return 'di.parameters.getAll()'; - } - - const optional = arg.flags & TypeFlag.Optional ? `, false` : ''; - return `di.parameters.resolve('${parameters.path}'${optional})`; - } - - private compileServiceInjection(type: Type, flags: TypeFlag, id: string): string { - if (flags & TypeFlag.Injector) { - return `(service) => di.register('${id}', service)`; - } - - const wantsPromise = Boolean(flags & TypeFlag.Async); - const wantsArray = Boolean(flags & TypeFlag.Array); - const wantsAccessor = Boolean(flags & TypeFlag.Accessor); - const wantsIterable = Boolean(flags & TypeFlag.Iterable); - const isOptional = Boolean(flags & TypeFlag.Optional); - const valueIsAsync = this.builder.isAsync(type); - let method: string = wantsArray ? 'find' : 'get'; - let prefix: string = ''; - let need: string = ''; - let postfix: string = ''; - - if (!wantsArray && !wantsIterable && isOptional) { - need = ', false'; - } - - if (wantsAccessor) { - prefix = `${wantsPromise ? 'async ' : ''}() => `; - } else if (wantsIterable) { - method = 'iterate'; - } else if (!wantsPromise && valueIsAsync) { - prefix = 'await '; - } else if (wantsPromise && !valueIsAsync && !wantsArray) { - prefix = 'Promise.resolve().then(() => '; - postfix = ')'; - } - - return `${prefix}di.${method}('${id}'${need})${postfix}`; - } -} - -type DecoratorInfo = [source: string, path: string, info: CallbackInfo]; - -type DecoratorMap = { - scope?: ServiceScope; - decorate: DecoratorInfo[]; - onCreate: DecoratorInfo[]; - onFork: DecoratorInfo[]; - onDestroy: DecoratorInfo[]; -}; - -function getDecoratorMap(decorators: ServiceDecoratorInfo[], sources: Map): DecoratorMap { - const map: DecoratorMap = { - decorate: [], - onCreate: [], - onFork: [], - onDestroy: [], - }; - - for (const decorator of decorators) { - const source = sources.get(decorator.source)!; - decorator.scope && (map.scope = decorator.scope); - decorator.decorate && map.decorate.push([source, decorator.path, decorator.decorate]); - decorator.hooks.onCreate && map.onCreate.push([source, decorator.path, decorator.hooks.onCreate]); - decorator.hooks.onFork && map.onFork.push([source, decorator.path, decorator.hooks.onFork]); - decorator.hooks.onDestroy && map.onDestroy.push([source, decorator.path, decorator.hooks.onDestroy]); - } - - return map; -} - -function compareIDs(a: string, b: string): number { - return (a.indexOf('#') - b.indexOf('#')) || a.localeCompare(b, 'en', { sensitivity: 'base', numeric: true }); -} - -function extractSources(definitions: ServiceDefinitionInfo[], parameters?: ContainerParametersInfo): Map { - const sources = new Set( - definitions - .flatMap((d) => [d.source, ...d.decorators.map((o) => o.source), d.creates?.source]) - .filter((s): s is SourceFile => s !== undefined) - ); - - if (parameters) { - sources.add(parameters.source); - } - - const list = [...sources].sort(compareSourceFiles); - const aliases: Record = {}; - return new Map(list.map((s) => [s, extractSourceAlias(s, aliases)])); -} - -function compareSourceFiles(a: SourceFile, b: SourceFile): number { - const pa = a.getFilePath(); - const pb = b.getFilePath(); - return pa < pb ? -1 : pa > pb ? 1 : 0; -} - -function extractSourceAlias(source: SourceFile, map: Record): string { - const alias = source.getFilePath() - .replace(/^(?:.*\/)?([^\/]+)(?:\/index)?(?:\.d)?\.tsx?$/i, '$1') - .replace(/[^a-z0-9]+/gi, '') - || 'anon'; - map[alias] ??= 0; - const idx = map[alias]++; - return `${alias}${idx}`; -} - -function join(separator: string, ...tokens: (string | 0 | false | undefined)[]): string { - return tokens.filter((t) => typeof t === 'string').join(separator); -} - -function maybeAsync(type: string, async?: boolean): string { - return async - ? `Promise<${type}>` - : type; -} diff --git a/core/cli/src/compiler/compiler.ts b/core/cli/src/compiler/compiler.ts new file mode 100644 index 0000000..a32f2ac --- /dev/null +++ b/core/cli/src/compiler/compiler.ts @@ -0,0 +1,66 @@ +import { ContainerAnalyser } from '../analysis'; +import { BuilderMap } from '../container'; +import { ResourceScanner } from '../definitions'; +import { CompilerExtension, ExtensionLoader } from '../extensions'; +import { ContainerCompiler } from './containerCompiler'; + +export class Compiler { + private readonly extensions: Set = new Set(); + + constructor( + private readonly extensionLoader: ExtensionLoader, + private readonly defaultExtensions: AsyncIterable, + private readonly resourceScanner: ResourceScanner, + private readonly containerAnalyser: ContainerAnalyser, + private readonly containerCompiler: ContainerCompiler, + private readonly builders: BuilderMap, + ) {} + + async compile(): Promise { + await this.loadExtensions(); + + this.loadResources(); + + for (const [builder, container] of this.containerAnalyser.analyse(this.builders)) { + builder.sourceFile.replaceWithText(this.containerCompiler.compile(container)); + } + + for (const builder of this.builders) { + await builder.sourceFile.save(); + } + } + + private async loadExtensions(): Promise { + for await (const extension of this.defaultExtensions) { + this.extensions.add(extension); + } + + for await (const extension of this.extensionLoader.load()) { + this.extensions.add(extension); + } + } + + private loadResources(): void { + for (const builder of this.builders) { + for (const [resource, options] of Object.entries(builder.options.resources)) { + this.resourceScanner.enqueueResources( + builder, + createResourceGlobs(resource, options?.excludePaths ?? []), + options?.excludeExports ?? [], + ); + } + + for (const extension of this.extensions) { + extension.loadResources(builder, (resources, excludeExports, resolveFrom) => { + this.resourceScanner.enqueueResources(builder, resources, excludeExports, resolveFrom); + }); + } + } + + this.resourceScanner.scanEnqueuedResources(); + } +} + +function createResourceGlobs(resource: string, exclude: string[]): string[] { + return [resource, ...exclude.filter((p) => /(\/|\.tsx?$)/i.test(p)).map((e) => `!${e}`)]; +} diff --git a/core/cli/src/compiler/containerCompiler.ts b/core/cli/src/compiler/containerCompiler.ts new file mode 100644 index 0000000..30b7885 --- /dev/null +++ b/core/cli/src/compiler/containerCompiler.ts @@ -0,0 +1,113 @@ +import { Container, TypeSpecifierWithAsync } from '../analysis'; +import { getFirst, sortMap } from '../utils'; +import { ServiceCompiler } from './serviceCompiler'; +import { formatType, compareKeys, compareResources, compareTypes } from './utils'; +import { WriterFactory } from './writerFactory'; + +const diccImports = [ + 'Container', + 'type ForeignServiceType', + 'type ServiceType', + 'toAsyncIterable', + 'toSyncIterable', +]; + +export class ContainerCompiler { + constructor( + private readonly serviceCompiler: ServiceCompiler, + private readonly writerFactory: WriterFactory, + ) {} + + compile(container: Container): string { + const writer = this.writerFactory.create(); + + if (container.preamble) { + writer.write(container.preamble.trimEnd()); + writer.write('\n\n'); + } + + writer.write(this.compileImports(container)); + writer.write('\n'); + writer.write(this.compileTypeMaps(container)); + writer.write('\n'); + writer.write(this.compileContainerClass(container)); + writer.write('\n'); + return writer.toString(); + } + + private compileImports(container: Container): string { + const dicc: string[] = diccImports.filter((i) => i === 'Container' || container.imports.has(i)); + const imports: string[] = [`import { ${dicc.join(', ')} } from 'dicc';\n`]; + + for (const [alias, resource] of sortMap(container.resources, compareResources)) { + if (!resource.needsType && !resource.needsValue) { + continue; + } + + const typeOnly = !resource.needsValue ? 'type ' : ''; + imports.push(`import ${typeOnly}* as ${alias} from '${resource.staticImport}';\n`); + } + + return imports.join(''); + } + + private compileTypeMaps(container: Container): string { + const writer = this.writerFactory.create(); + writer.write('interface PublicServices {'); + writer.indent(() => writer.write(this.compileTypeMap(container.publicTypes))); + writer.write('}\n\ninterface DynamicServices {'); + if (container.dynamicTypes.size) { + writer.indent(() => writer.write(this.compileTypeMap(container.dynamicTypes))); + } + writer.write('}\n\ninterface AnonymousServices {'); + if (container.anonymousTypes.size) { + writer.indent(() => writer.write(this.compileTypeMap(container.anonymousTypes))); + } + writer.write('}\n'); + return writer.toString(); + } + + private compileContainerClass(container: Container): string { + const writer = this.writerFactory.create(); + const declaration = container.className === 'default' ? 'default class' : `class ${container.className}`; + writer.write(`export ${declaration} extends Container {`); + writer.indent(() => { + writer.write('constructor() {'); + writer.indent(() => { + writer.write(`super(${this.serviceCompiler.compileDefinitions(container.services, container.resources)});`); + }); + writer.write('}'); + }); + writer.write('}'); + return writer.toString(); + } + + private compileTypeMap(map: Map>): string { + const writer = this.writerFactory.create(); + + for (const [alias, typeOrTypes] of sortMap(map, compareKeys)) { + let async = false; + const types = (typeOrTypes instanceof Set ? [...typeOrTypes] : [typeOrTypes]) + .sort(compareTypes) + .map((type) => { + type.async && (async = true); + return formatType(type); + }); + + const [pre, post] = async ? [' Promise<', '>'] : [' ', '']; + + if (types.length === 1) { + writer.write(`'${alias}':${pre}${getFirst(types)}${post};\n`); + continue; + } + + writer.write(`'${alias}':${pre.trimEnd()}`); + writer.indent(() => writer.write( + types.map((type, i) => `| ${type}${i + 1 >= types.length && !post ? ';' : ''}`).join('\n'), + )); + post && writer.write(`${post};\n`); + } + + return writer.toString(); + } +} diff --git a/core/cli/src/compiler/di.ts b/core/cli/src/compiler/di.ts new file mode 100644 index 0000000..b05ebb7 --- /dev/null +++ b/core/cli/src/compiler/di.ts @@ -0,0 +1,3 @@ +export { ServiceCompiler } from './serviceCompiler'; +export { ContainerCompiler } from './containerCompiler'; +export { WriterFactory } from './writerFactory'; diff --git a/core/cli/src/compiler/index.ts b/core/cli/src/compiler/index.ts new file mode 100644 index 0000000..df496b5 --- /dev/null +++ b/core/cli/src/compiler/index.ts @@ -0,0 +1,4 @@ +export * from './compiler'; +export * from './serviceCompiler'; +export * from './containerCompiler'; +export * from './writerFactory'; diff --git a/core/cli/src/compiler/serviceCompiler.ts b/core/cli/src/compiler/serviceCompiler.ts new file mode 100644 index 0000000..f1fb782 --- /dev/null +++ b/core/cli/src/compiler/serviceCompiler.ts @@ -0,0 +1,602 @@ +import { + Argument, + ArgumentList, + AsyncMode, + AutoImplement, + Call, ChildServiceRegistrations, + Factory, ForkHookInfo, + HookInfo, + InjectedArgument, + OverriddenArgument, + Resource, + Service, +} from '../analysis'; +import { InternalError } from '../errors'; +import { getFirst, mapMap } from '../utils'; +import { compareServiceIds, formatType } from './utils'; +import { WriterFactory } from './writerFactory'; + +export class ServiceCompiler { + constructor( + private readonly writerFactory: WriterFactory, + ) {} + + compileDefinitions(services: Iterable, resources: Map): string { + const writer = this.writerFactory.create(); + const orderedServices = [...services].sort(compareServiceIds); + + writer.write('{'); + writer.indent(() => { + for (const service of orderedServices) { + writer.write(`'${service.id}': {`); + writer.indent(() => { + writer.write(this.compileDefinition(service, resources)); + }); + writer.write('},\n'); + } + }); + writer.write('}'); + + return writer.toString(); + } + + private compileDefinition(service: Service, resources: Map): string { + const writer = this.writerFactory.create(); + writer.conditionalWrite(service.aliases.size > 0, `aliases: ${this.compileAliases(service)},\n`); + writer.write(`factory: ${this.compileFactory(service, resources)},\n`); + writer.conditionalWrite(service.async, `async: true,\n`); + + if (service.factory?.kind === 'foreign') { + writer.write(`scope: 'private',\n`); + } else { + writer.conditionalWrite(service.scope !== 'global', `scope: '${service.scope}',\n`); + } + + writer.write(this.compileHook('onCreate', service.onCreate, resources)); + writer.write(this.compileForkHook(service.onFork, resources)); + writer.write(this.compileHook('onDestroy', service.onDestroy, resources)); + + return writer.toString(); + } + + private compileAliases(service: Service): string { + if (service.aliases.size === 1) { + return `['${getFirst(service.aliases)}']`; + } + + const writer = this.writerFactory.create(); + writer.write('['); + writer.indent(() => { + for (const alias of service.aliases) { + writer.write(`'${alias}',\n`); + } + }); + writer.write(']'); + return writer.toString(); + } + + private compileFactory( + service: Service, + resources: Map, + scope: Set = new Set(), + ): string { + if (!service.factory) { + return 'undefined'; + } + + const [async, inject] = resolveFactorySignature(service); + const body = this.compileFactoryBody(service, service.factory, resources, scope); + return `${async ? 'async ' : ''}(${inject ? 'di' : ''}) => ${body}`; + } + + private compileFactoryBody( + service: Service, + factory: Factory | AutoImplement, + resources: Map, + parentScope: Set, + ): string { + const [imports, scope] = this.compileDynamicImports( + resources, + parentScope, + factory.kind !== 'foreign' && factory.kind !== 'auto-interface' && factory.call, + ...service.decorate?.calls ?? [], + ...this.getAutoFactoryCallsIfEager(service), + ); + + const eagerArgs = factory.kind === 'auto-class' || factory.kind === 'auto-interface' + ? this.compileEagerAsyncArgs(factory) + : ''; + + const register = this.compileChildServiceRegistrations(service.register); + + const decorate = service.decorate + ? this.compileCalls( + service.decorate.calls, + (stmt, call, i, n) => i + 1 < n ? `service = ${awaitKw(call)}${stmt}` : `return ${stmt}`, + ) + : register ? 'return service;' : ''; + const multipleDecorate = (service.decorate?.calls.length ?? 0) > 1; + + const needsBlock = + !!imports + || !!eagerArgs + || !!register + || !!decorate + || (factory.kind === 'foreign' && factory.container.async); + + const createService = this.compileCreateService( + service, + factory, + resources, + scope, + multipleDecorate ? 'let' + : (decorate || register) ? 'const' + : needsBlock ? 'return' + : 'inline', + ); + + if (!needsBlock) { + return createService; + } + + const writer = this.writerFactory.create(); + writer.write(`{`); + writer.indent(() => { + writer.write(imports); + writer.write(eagerArgs); + writer.write(createService); + writer.write(register); + writer.write(decorate); + }); + writer.write('}'); + return writer.toString(); + } + + private compileCreateService( + service: Service, + factory: Factory | AutoImplement, + resources: Map, + scope: Set, + mode: 'const' | 'let' | 'return' | 'inline', + ): string { + if (factory.kind === 'local') { + const call = this.compileCall(factory.call); + return this.compileCreateServiceStmt(factory.call, call, mode); + } else if (factory.kind === 'auto-class') { + const body = this.compileAutoImplement(service, factory, resources, scope); + const args = factory.call.args.length ? this.compileArgs(factory.call.args) : ''; + const stmt = `${factory.call.statement} ${body}${args}`; + return this.compileCreateServiceStmt(factory.call, stmt, mode); + } else if (factory.kind === 'auto-interface') { + const [pre, post] = mode === 'inline' ? ['(', ')'] : ['', '']; + const body = `${pre}${this.compileAutoImplement(service, factory, resources, scope)}${post}`; + return this.compileCreateServiceStmt({ async: false }, body, mode); + } + + const writer = this.writerFactory.create(); + + if (factory.container.async) { + writer.write(`const parent = await di.get('${factory.container.id}');\n`); + writer.write( + mode === 'const' || mode === 'let' + ? `${mode} service = ${awaitKw(factory)}parent.get('${factory.id}');\n` + : `return parent.get('${factory.id}');`, + ); + } else { + const get = `di.get('${factory.container.id}').get('${factory.id}')`; + writer.write(this.compileCreateServiceStmt(factory, get, mode)); + } + + return writer.toString(); + } + + private compileCreateServiceStmt( + call: Call, + expr: string, + mode: 'const' | 'let' | 'return' | 'inline', + ): string { + switch (mode) { + case 'const': + case 'let': + return `${mode} service = ${awaitKw(call)}${expr};\n`; + case 'return': + return `return ${expr};`; + case 'inline': + return expr; + } + } + + private compileAutoImplement(service: Service, factory: AutoImplement, resources: Map, scope: Set): string { + const method = factory.method; + const [assign, eos] = factory.kind === 'auto-class' ? [' =', ';'] : [':', ',']; + const writer = this.writerFactory.create(); + writer.write('{'); + writer.indent(() => { + if (method.name === 'get') { + writer.write(`${asyncKw(method)}get() {`); + writer.indent(() => { + writer.write(`return di.get('${method.target}'${needKw(method)});`); + }); + writer.write('}'); + return; + } + + writer.write('create'); + + if (factory.kind === 'auto-class') { + if (method.service.type.kind !== 'local') { + throw new InternalError('This should not happen'); + } + + writer.write(`: ${formatType(service.type)}['create']`); + } + + writer.write(`${assign} (${method.args.join(', ')}) => `); + writer.write( + method.service.factory + ? this.compileFactoryBody(method.service, method.service.factory, resources, scope) + : 'undefined', + ); + writer.write(eos); + }); + writer.write('}'); + return writer.toString(); + } + + private compileHook(name: string, hook: HookInfo | undefined, resources: Map): string { + if (!hook || !hook.calls.length) { + return ''; + } + + const writer = this.writerFactory.create(); + const args = ['service', 'di'].slice(0, hook.args); + writer.write(`${name}: ${asyncKw(hook)}(${args.join(', ')}) => {`); + writer.indent(() => { + const [imports] = this.compileDynamicImports(resources, new Set(), ...hook.calls); + writer.write(imports); + writer.write(this.compileCalls(hook.calls, (stmt, call) => `${awaitKw(call)}${stmt}`)); + }); + writer.write('},\n'); + return writer.toString(); + } + + private compileForkHook(hook: ForkHookInfo | undefined, resources: Map): string { + if (!hook || (!hook.containerCall && !hook.serviceCall && !hook.calls.length)) { + return ''; + } + + const [imports] = this.compileDynamicImports(resources, new Set(), hook.serviceCall, ...hook.calls); + const args = ['callback', 'service', 'di'].slice(0, hook.args); + + let body = this.compileForkHookDecoratorCalls( + hook, + hook.serviceCall || hook.containerCall ? '' : imports, + hook.serviceCall ? ['fork'] : hook.containerCall ? [] : args, + hook.serviceCall ? 'fork' : '', + ); + + if (hook.serviceCall) { + const pre = hook.containerCall ? 'async () => ' : ''; + hook.serviceCall.args.replace(0, { kind: 'literal', async: 'none', source: body }); + body = `${pre}${this.compileCall(hook.serviceCall)}`; + } + + if (hook.containerCall) { + body = `service.run(${body})`; + } + + const writer = this.writerFactory.create(); + writer.write('onFork: '); + + if (!hook.serviceCall && !hook.containerCall) { + writer.write(body); + writer.write(',\n'); + return writer.toString(); + } + + writer.write(`async (${args.join(', ')}) => `); + + if (!imports) { + writer.write(`${body},\n`); + return writer.toString(); + } + + writer.write('{'); + writer.indent(() => { + writer.write(imports); + writer.write(`return ${body};`); + }); + writer.write('},\n'); + return writer.toString(); + } + + private compileForkHookDecoratorCalls(hook: ForkHookInfo, imports: string, args: string[], cbArg: string): string { + if (!hook.calls.length) { + return 'callback'; + } + + const calls = this.compileCalls(hook.calls, (stmt, call) => `${awaitKw(call)}${stmt}`); + const writer = this.writerFactory.create(); + + writer.write(`async (${args.join(', ')}) => {`); + writer.indent(() => { + writer.write(imports); + writer.write(calls); + writer.write(`return callback(${cbArg});`); + }); + writer.write('}'); + + return writer.toString(); + } + + private compileCalls( + calls: Call[], + format?: (stmt: string, call: Call, index: number, total: number) => string, + ): string { + const writer = this.writerFactory.create(); + + for (let i = 0; i < calls.length; ++i) { + const stmt = this.compileCall(calls[i]); + writer.write(format ? format(stmt, calls[i], i, calls.length) : stmt); + writer.write(';\n'); + } + + return writer.toString(); + } + + private compileCall(call: Call): string { + return `${call.statement}${this.compileArgs(call.args)}`; + } + + private compileArgs(args: ArgumentList): string { + if (!args.length) { + return '()'; + } + + const writer = this.writerFactory.create(); + writer.write('('); + writer.indent(() => { + for (const arg of args) { + writer.write(`${this.compileArg(arg)},\n`); + } + }); + writer.write(')'); + return writer.toString(); + } + + private compileArg(arg: Argument): string { + switch (arg.kind) { + case 'raw': return JSON.stringify(arg.value); + case 'literal': return withAsyncMode(arg, arg.source); + case 'overridden': return withSpread(arg, withAsyncMode(arg, this.compileOverriddenArg(arg))); + case 'injected': return this.compileInjectedArgument(arg); + } + } + + private compileOverriddenArg(arg: OverriddenArgument): string { + return arg.value.kind === 'value' + ? withSpread(arg, withAsyncMode(arg, arg.value.path)) + : this.compileCall(arg.value); + } + + private compileInjectedArgument(arg: InjectedArgument): string { + switch (arg.mode) { + case 'scoped-runner': return `{ async run(cb) { return di.run(cb); } }`; + case 'injector': return `(service) => di.register('${arg.id}', service)`; + case 'accessor': + return `${asyncKw(arg)}() => di.${method(arg.target)}('${arg.alias}'${needKw(arg)})`; + } + + const need = arg.mode === 'single' ? needKw(arg) : ''; + const asyncMode = arg.mode === 'iterable' ? withIterableMode : withAsyncMode; + return withSpread(arg, asyncMode(arg, `di.${method(arg.mode)}('${arg.alias}'${need})`)); + } + + private compileDynamicImports( + resources: Map, + scope: Set, + ...targets: (Call | false | undefined | null)[] + ): [string, Set] { + const imports: Map = new Map(); + const queue: Call[] = targets.filter((target) => !!target); + let target: Call | undefined; + + while (target = queue.shift()) { + checkResource(target.resource); + + for (const arg of target.args) { + if (arg.kind !== 'overridden') { + continue; + } + + if (arg.value.kind === 'call') { + queue.push(arg.value); + } else { + checkResource(arg.value.resource); + } + } + } + + const stmt = this.compilePromiseAll(imports); + return [stmt, new Set([...scope, ...imports.keys()])]; + + function checkResource(alias: string): void { + if (scope.has(alias) || imports.has(alias)) { + return; + } + + const resource = resources.get(alias); + + if (!resource) { + throw new InternalError('This should never happen'); + } + + if (!resource.needsValue) { + imports.set(alias, `import('${resource.dynamicImport}')`); + } + } + } + + private * getAutoFactoryCallsIfEager(service: Service): Iterable { + if ( + !service.factory + || (service.factory.kind !== 'auto-class' && service.factory.kind !== 'auto-interface') + || service.factory.method.name !== 'create' + || service.factory.method.async + ) { + return; + } + + const target = service.factory.method.service; + + if (target.factory && (target.factory.kind === 'local' || target.factory.kind === 'auto-class')) { + yield target.factory.call; + } + + if (target.decorate) { + yield * target.decorate.calls; + } + } + + private compileEagerAsyncArgs(factory: AutoImplement): string { + if (factory.method.name === 'get') { + return ''; + } + + return this.compilePromiseAll(mapMap( + factory.method.eagerDeps, + (name, arg) => [name, this.compileArg(arg)], + )); + } + + private compileChildServiceRegistrations(registrations?: ChildServiceRegistrations): string { + if (!registrations) { + return ''; + } + + const writer = this.writerFactory.create(); + + for (const [id, arg] of registrations.services) { + writer.write(`service.register('${id}', ${this.compileArg(arg)});\n`); + } + + return writer.toString(); + } + + private compilePromiseAll(stmts: Map): string { + if (stmts.size < 2) { + return [...stmts] + .map(([alias, stmt]) => `const ${alias} = await ${stmt};\n`) + .join(''); + } + + const writer = this.writerFactory.create(); + writer.write(`const [${[...stmts.keys()].join(', ')}] = await Promise.all([`); + writer.indent(() => { + for (const stmt of stmts.values()) { + writer.write(`${stmt},`); + } + }); + writer.write(']);\n'); + return writer.toString(); + } +} + + +function withAsyncMode(o: O, value: string): string { + switch (o.async) { + case 'none': return value; + case 'await': return `await ${value}`; + case 'wrap': return `Promise.resolve(${value})`; + } +} + +function withIterableMode(o: O, value: string): string { + switch (o.async) { + case 'none': return value; + case 'await': return `await toSyncIterable(${value})`; + case 'wrap': return `toAsyncIterable(${value})`; + } +} + +function withSpread(o: O, value: string): string { + return o.spread ? `...${value}` : value; +} + +function asyncKw(value: Value): string { + return value.async ? 'async ' : ''; +} + +function awaitKw(value: Value): string { + return value.async ? 'await ' : ''; +} + +function needKw(value: Value): string { + return value.need ? '' : ', false'; +} + +function method(mode: 'single' | 'list' | 'iterable'): string { + switch (mode) { + case 'single': return 'get'; + case 'list': return 'find'; + case 'iterable': return 'iterate'; + } +} + +function resolveFactorySignature(service: Service): [async: boolean, inject: boolean] { + let async = false; + let inject = false; + + if (service.register) { + inject = true; + + if (service.register.async) { + async = true; + } + } + + if (service.decorate && service.decorate.args > 1) { + inject = true; + } + + if (service.decorate?.async) { + async = true; + } + + if (async && inject) { + return [async, inject]; + } + + if (!service.factory) { + return [false, false]; + } + + switch (service.factory.kind) { + case 'foreign': + return [async || service.factory.async, true]; + case 'local': + return [async || service.factory.call.async || service.factory.call.args.async, inject || service.factory.call.args.inject]; + case 'auto-class': + if (service.factory.call.args.async) { + async = true; + } + + if (service.factory.call.args.inject) { + inject = true; + } + + if (async && inject) { + return [async, inject]; + } + break; + } + + if (service.factory.method.name === 'get') { + return [async, true]; + } else if (service.factory.method.eagerDeps.size) { + return [true, true]; + } + + const [afAsync, afInject] = resolveFactorySignature(service.factory.method.service); + return [async || afAsync, inject || afInject]; +} diff --git a/core/cli/src/compiler/utils.ts b/core/cli/src/compiler/utils.ts new file mode 100644 index 0000000..28fad2b --- /dev/null +++ b/core/cli/src/compiler/utils.ts @@ -0,0 +1,46 @@ +import { + ForeignTypeSpecifier, + LocalTypeSpecifier, Resource, Service, + TypeSpecifier, +} from '../analysis'; +import { MapEntry } from '../utils'; + +export function formatLocalType(type: LocalTypeSpecifier): string { + return type.indirect ? `ServiceType` : type.path; +} + +export function formatForeignType(type: ForeignTypeSpecifier): string { + return `ForeignServiceType<${formatLocalType(type.container)}, '${type.id}'>`; +} + +export function formatType(type: TypeSpecifier): string { + return type.kind === 'foreign' ? formatForeignType(type) : formatLocalType(type); +} + +function strcmp(a: string, b: string): number { + return a < b ? -1 : a > b ? 1 : 0; +} + +export function compareKeys(a: MapEntry, b: MapEntry): number { + return strcmp(a.k, b.k); +} + +function pathcmp(a: string, b: string): number { + const upa = a.match(/^(?:\.\/)?((?:\.\.\/)*)(.+)$/)!; + const upb = b.match(/^(?:\.\/)?((?:\.\.\/)*)(.+)$/)!; + return (upb[1].length - upa[1].length) || strcmp(upa[2], upb[2]); +} + +export function compareResources(a: MapEntry, b: MapEntry): number { + return pathcmp(a.v.staticImport, b.v.staticImport) || strcmp(a.k, b.k); +} + +export function compareServiceIds(a: Service, b: Service): number { + return (a.id.indexOf('#') - b.id.indexOf('#')) || strcmp(a.id, b.id); +} + +export function compareTypes(a: TypeSpecifier, b: TypeSpecifier): number { + const [na, ka] = a.kind === 'foreign' ? [a.container.path, a.id] : [a.path, '']; + const [nb, kb] = b.kind === 'foreign' ? [b.container.path, b.id] : [b.path, '']; + return strcmp(na, nb) || strcmp(ka, kb); +} diff --git a/core/cli/src/compiler/writerFactory.ts b/core/cli/src/compiler/writerFactory.ts new file mode 100644 index 0000000..7dbefd3 --- /dev/null +++ b/core/cli/src/compiler/writerFactory.ts @@ -0,0 +1,11 @@ +import { CodeBlockWriter, Project } from 'ts-morph'; + +export class WriterFactory { + constructor( + private readonly project: Project, + ) {} + + create(): CodeBlockWriter { + return this.project.createWriter(); + } +} diff --git a/core/cli/src/v2/config/configLoader.ts b/core/cli/src/config/configLoader.ts similarity index 100% rename from core/cli/src/v2/config/configLoader.ts rename to core/cli/src/config/configLoader.ts diff --git a/core/cli/src/v2/config/index.ts b/core/cli/src/config/index.ts similarity index 100% rename from core/cli/src/v2/config/index.ts rename to core/cli/src/config/index.ts diff --git a/core/cli/src/v2/config/types.ts b/core/cli/src/config/types.ts similarity index 100% rename from core/cli/src/v2/config/types.ts rename to core/cli/src/config/types.ts diff --git a/core/cli/src/configLoader.ts b/core/cli/src/configLoader.ts deleted file mode 100644 index 7e2a4bd..0000000 --- a/core/cli/src/configLoader.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { readFile } from 'fs/promises'; -import { resolve } from 'path'; -import { parse } from 'yaml'; -import { Argv } from './argv'; -import { ConfigError } from './errors'; -import { DiccConfig, diccConfigSchema } from './types'; - -const defaultConfigFiles = [ - 'dicc.yaml', - 'dicc.yml', - '.dicc.yaml', - '.dicc.yml', -]; - -export class ConfigLoader { - private readonly configFile?: string; - - constructor(argv: Argv) { - this.configFile = argv.configFile; - } - - async load(): Promise { - const [file, data] = await this.loadConfigFile(); - - try { - const config = parse(data); - return diccConfigSchema.parse(config); - } catch (e: any) { - throw new ConfigError(`Error in config file '${file}': ${e.message}`); - } - } - - private async loadConfigFile(): Promise<[file: string, config: string]> { - const candidates = this.configFile === undefined ? defaultConfigFiles : [this.configFile]; - - for (const file of candidates) { - const fullPath = resolve(file); - - try { - const config = await readFile(fullPath, 'utf-8'); - return [fullPath, config]; - } catch (e: any) { - if (e.code === 'ENOENT') { - continue; - } - - throw new ConfigError(`Error reading config file '${file}': ${e.message}`); - } - } - - throw new ConfigError( - this.configFile === undefined - ? 'Config file not specified and none of the default config files exists' - : `Config file '${this.configFile}' doesn't exist`, - ); - } -} diff --git a/core/cli/src/container/builderMap.ts b/core/cli/src/container/builderMap.ts new file mode 100644 index 0000000..4664194 --- /dev/null +++ b/core/cli/src/container/builderMap.ts @@ -0,0 +1,43 @@ +import { Project, SourceFile, ScriptKind } from 'ts-morph'; +import { CompilerConfig } from '../config'; +import { ContainerBuilder, ContainerBuilderFactory } from './containerBuilder'; + +export class BuilderMap implements Iterable { + private readonly builders: Map = new Map(); + + constructor( + project: Project, + builderFactory: ContainerBuilderFactory, + config: CompilerConfig, + ) { + for (const [path, options] of Object.entries(config.containers)) { + const resource = project.createSourceFile(path, createEmptyContent(options.className), { + scriptKind: ScriptKind.TS, + overwrite: true, + }); + + this.builders.set(resource, builderFactory.create(resource, options)); + } + } + + getByResource(resource: SourceFile): ContainerBuilder | undefined { + return this.builders.get(resource); + } + + [Symbol.iterator](): Iterator { + return this.builders.values(); + } +} + +function createEmptyContent(className: string): string { + const declaration = className === 'default' ? 'default class' : `class ${className}`; + return ` +import { Container } from 'dicc'; + +export ${declaration} extends Container { + constructor() { + super({}); + } +} +`; +} diff --git a/core/cli/src/container/containerBuilder.ts b/core/cli/src/container/containerBuilder.ts new file mode 100644 index 0000000..963661a --- /dev/null +++ b/core/cli/src/container/containerBuilder.ts @@ -0,0 +1,253 @@ +import { SourceFile, Type } from 'ts-morph'; +import { ContainerOptions } from '../config'; +import { + DecoratorDefinition, + DecoratorOptions, + ExplicitServiceDefinition, + ExplicitServiceDefinitionOptions, + ForeignServiceDefinition, + ImplicitServiceDefinition, + LocalServiceDefinition, + LocalServiceDefinitionOptions, + ServiceDefinition, +} from '../definitions'; +import { DefinitionError, InternalError } from '../errors'; +import { EventDispatcher } from '../events'; +import { allocateInSet, getFirstIfOnly, getOrCreate, throwIfUndef } from '../utils'; +import { DecoratorAdded, DecoratorRemoved, ServiceAdded, ServiceRemoved } from './events'; + +export interface ContainerBuilderFactory { + create(sourceFile: SourceFile, options: ContainerOptions): ContainerBuilder; +} + +export class ContainerBuilder { + private readonly services: Map = new Map(); + private readonly publicServices: Set = new Set(); + private readonly dynamicServices: Set = new Set(); + private readonly types: Map> = new Map(); + private readonly typeNames: Map = new Map(); + private readonly uniqueTypeNames: Set = new Set(); + private readonly childContainers: Set = new Set(); + private readonly decorators: Map> = new Map(); + private readonly resources: Map = new Map(); + private readonly uniqueResourceAliases: Set = new Set(); + + constructor( + private readonly eventDispatcher: EventDispatcher, + readonly sourceFile: SourceFile, + readonly options: ContainerOptions, + ) {} + + addImplicitDefinition( + resource: SourceFile, + path: string, + type: Type, + options: LocalServiceDefinitionOptions = {}, + ): void { + const id = this.allocateServiceId(options.factory?.returnType?.aliasType ?? type); + this.addService(new ImplicitServiceDefinition(this, resource, path, id, type, options)); + } + + addExplicitDefinition( + resource: SourceFile, + path: string, + type: Type, + options: ExplicitServiceDefinitionOptions = {}, + ): void { + const id = options.anonymous + ? this.allocateServiceId(options.factory?.returnType?.aliasType ?? type) + : path; + const definition = new ExplicitServiceDefinition(this, resource, path, id, type, options); + + if (!definition.anonymous) { + this.addPublicService(definition); + } + + this.addService(definition); + } + + addForeignDefinition( + container: LocalServiceDefinition, + foreignId: string, + type: Type, + aliases?: Iterable, + definition?: ServiceDefinition, + async?: boolean, + ): void { + const anonymous = !container.isExplicit() || container.anonymous; + + const id = anonymous + ? this.allocateServiceId(type) + : `${container.id}.${foreignId}`; + + const def = new ForeignServiceDefinition(this, container, foreignId, id, type, aliases, definition, async); + + if (!anonymous) { + this.addPublicService(def); + } + + this.addService(def); + } + + private addService(definition: ServiceDefinition): void { + this.services.set(definition.id, definition); + getOrCreate(this.types, definition.type, () => new Set()).add(definition); + + for (const alias of definition.aliases) { + getOrCreate(this.types, alias, () => new Set()).add(definition); + } + + if (definition.isLocal()) { + if (!definition.factory) { + this.dynamicServices.add(definition); + } + + if (definition.container) { + this.childContainers.add(definition); + } + } + + this.eventDispatcher.dispatch(new ServiceAdded(definition)); + } + + getTypeName(type: Type): string { + return getOrCreate(this.typeNames, type, () => { + const typeName = type.getSymbol()?.getName() ?? 'Anonymous'; + return allocateInSet(this.uniqueTypeNames, `#${typeName}{i}`); + }); + } + + getTypeNamesIfExist(types: Iterable): string[] { + return [...types].map((type) => this.typeNames.get(type)).filter((name) => name !== undefined); + } + + private allocateServiceId(type: Type): string { + const typeName = this.getTypeName(type); + const existing = getOrCreate(this.types, type, () => new Set()); + return `${typeName}.${existing.size}`; + } + + private addPublicService(definition: ServiceDefinition): void { + const existing = this.services.get(definition.id); + + if (existing) { + const source = definition.isForeign() + ? `merged foreign service '${definition.foreignId}' from container '${definition.parent.id}'` + : `service '${definition.id}' exported from '${definition.resource.getFilePath()}'`; + const collision = existing.isForeign() + ? `merged foreign service '${existing.foreignId}' from container '${existing.parent.id}'` + : `definition exported from '${existing.resource.getFilePath()}'` + + const local = existing.isForeign() ? existing.parent : existing; + + throw new DefinitionError( + `Public service ID of ${source} collides with ${collision}`, + { builder: local.builder, resource: local.resource, path: local.path, node: local.node }, + ); + } + + this.publicServices.add(definition); + } + + removeService(definition: ServiceDefinition): void { + if (!this.services.delete(definition.id)) { + return; + } + + this.types.get(definition.type)?.delete(definition); + + for (const alias of definition.aliases) { + this.types.get(alias)?.delete(definition); + } + + this.publicServices.delete(definition); + definition.isLocal() && this.dynamicServices.delete(definition); + this.eventDispatcher.dispatch(new ServiceRemoved(definition)); + } + + getById(id: string): ServiceDefinition { + return throwIfUndef( + this.services.get(id), + () => new InternalError(`Service '${id}' does not exist`), + ); + } + + getPublicServices(): Iterable { + return this.publicServices; + } + + getDynamicServices(): Iterable { + return this.dynamicServices; + } + + getAllServices(): Iterable { + return this.services.values(); + } + + getChildContainers(): Iterable { + return this.childContainers; + } + + getByTypeIfSingle(type: Type): ServiceDefinition | undefined { + return getFirstIfOnly(this.types.get(type) ?? []); + } + + findByType(type: Type): Set { + return this.types.get(type) ?? new Set(); + } + + findByAnyType(...types: Type[]): Set { + return new Set(types.flatMap((type) => [...this.types.get(type) ?? []])); + } + + addDecorator( + resource: SourceFile, + path: string, + targetType: Type, + options: DecoratorOptions = {}, + ): void { + const definition = new DecoratorDefinition(resource, path, targetType, options); + getOrCreate(this.decorators, definition.targetType, () => new Set()).add(definition); + this.eventDispatcher.dispatch(new DecoratorAdded(definition)); + } + + removeDecorator(definition: DecoratorDefinition): void { + const definitions = this.decorators.get(definition.targetType); + + if (!definitions?.delete(definition)) { + return; + } + + this.eventDispatcher.dispatch(new DecoratorRemoved(definition)); + } + + decorate(service: ServiceDefinition): DecoratorDefinition[] { + const decorators: DecoratorDefinition[] = []; + + for (const target of [service.type, ...service.aliases]) { + decorators.push(...this.decorators.get(target) ?? []); + } + + return decorators.sort((a, b) => b.priority - a.priority); + } + + getResourceAlias(resource: SourceFile): string { + return getOrCreate(this.resources, resource, () => { + const alias = resource.getFilePath() + .replace(/^(?:.*?\/)?([^\/]+)(?:\/index)?(?:\.d)?\.tsx?$/i, '$1') + .replace(/^[^a-z]+|[^a-z0-9]+/gi, '') + .replace(/^$/, 'anon'); + + return allocateInSet(this.uniqueResourceAliases, `${alias}{i}`); + }); + } + + * getResourceMap(): Iterable<[alias: string, staticImport: string, dynamicImport: string]> { + for (const [resource, alias] of this.resources) { + const ext = resource.getFilePath().match(/\.([mc]?)[jt]sx?$/i); + const staticImport = this.sourceFile.getRelativePathAsModuleSpecifierTo(resource); + const dynamicImport = `${staticImport}.${ext ? ext[1] : ''}js`; + yield [alias, staticImport, dynamicImport]; + } + } +} diff --git a/core/cli/src/container/di.ts b/core/cli/src/container/di.ts new file mode 100644 index 0000000..b6c290c --- /dev/null +++ b/core/cli/src/container/di.ts @@ -0,0 +1,2 @@ +export { BuilderMap } from './builderMap'; +export { ContainerBuilder, ContainerBuilderFactory } from './containerBuilder'; diff --git a/core/cli/src/container/events.ts b/core/cli/src/container/events.ts new file mode 100644 index 0000000..1e41466 --- /dev/null +++ b/core/cli/src/container/events.ts @@ -0,0 +1,24 @@ +import { DecoratorDefinition, ServiceDefinition } from '../definitions'; +import { Event } from '../events'; + +export abstract class ServiceEvent extends Event { + constructor( + public readonly service: ServiceDefinition, + ) { + super(); + } +} + +export class ServiceAdded extends ServiceEvent {} +export class ServiceRemoved extends ServiceEvent {} + +export abstract class DecoratorEvent extends Event { + constructor( + public readonly decorator: DecoratorDefinition, + ) { + super(); + } +} + +export class DecoratorAdded extends DecoratorEvent {} +export class DecoratorRemoved extends DecoratorEvent {} diff --git a/core/cli/src/container/index.ts b/core/cli/src/container/index.ts new file mode 100644 index 0000000..70f41fe --- /dev/null +++ b/core/cli/src/container/index.ts @@ -0,0 +1,4 @@ +export * from './builderMap'; +export * from './containerBuilder'; +export * from './events'; + diff --git a/core/cli/src/containerBuilder.ts b/core/cli/src/containerBuilder.ts deleted file mode 100644 index 0ee7f12..0000000 --- a/core/cli/src/containerBuilder.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { SourceFile, Type } from 'ts-morph'; -import { DefinitionError } from './errors'; -import { - ContainerOptions, - ContainerParametersInfo, - NestedParameterInfo, - ServiceDecoratorInfo, - ServiceDefinitionInfo, - ServiceRegistrationInfo, -} from './types'; - -export class ContainerBuilder { - private readonly definitions: Map = new Map(); - private readonly types: Map = new Map(); - private readonly typeIds: Set = new Set(); - private readonly aliases: Map> = new Map(); - private readonly decorators: Map = new Map(); - private parameters?: ContainerParametersInfo; - - constructor( - readonly path: string, - readonly options: ContainerOptions, - readonly output: SourceFile, - ) {} - - register({ id, type, ...registration }: ServiceRegistrationInfo): void { - const typeId = this.registerType(type); - id ??= typeId; - const definition = { id, type, ...registration, references: 0, decorators: [] }; - this.definitions.set(id, definition); - this.registerAlias(id, id); - this.registerAlias(id, typeId); - - for (const alias of definition.aliases) { - this.registerAlias(id, this.registerType(alias)); - } - } - - unregister(id: string): void { - const definition = this.definitions.get(id); - - if (!definition) { - return; - } - - this.definitions.delete(id); - this.aliases.get(id)?.delete(id); - - for (const type of [definition.type, ...definition.aliases]) { - const typeId = this.types.get(type); - - if (typeId !== undefined) { - this.aliases.get(typeId)?.delete(id); - } - } - } - - decorate(decorator: ServiceDecoratorInfo): void { - const typeId = this.registerType(decorator.type); - this.decorators.has(typeId) || this.decorators.set(typeId, []); - this.decorators.get(typeId)!.push(decorator); - } - - applyDecorators(): void { - const decorated: Set = new Set(); - - for (const [id, decorators] of this.decorators) { - const definitions = [...this.aliases.get(id) ?? []].map((id) => this.get(id)); - - for (const definition of definitions) { - definition.decorators.push(...decorators); - decorated.add(definition); - } - } - - for (const definition of decorated) { - definition.decorators.sort((a, b) => b.priority - a.priority); - } - } - - setParametersInfo(info: ContainerParametersInfo): void { - if (this.parameters) { - throw new DefinitionError( - 'Multiple container parameter type definitions', - info.type.getSymbol()?.getValueDeclaration(), - ); - } - - this.parameters = info; - } - - getParametersInfo(): ContainerParametersInfo | undefined { - return this.parameters; - } - - getParametersByType(type: Type): ContainerParametersInfo | NestedParameterInfo | undefined { - if (!this.parameters) { - return undefined; - } - - if (type === this.parameters.type) { - return this.parameters; - } - - return this.parameters.nestedTypes.get(type); - } - - has(id: string): boolean { - return this.definitions.has(id); - } - - get(id: string): ServiceDefinitionInfo { - return this.definitions.get(id)!; - } - - getDefinitions(): Iterable { - return this.definitions.values(); - } - - * getPublicServices(): Iterable<[id: string, type: Type, async?: boolean]> { - for (const definition of this.getDefinitions()) { - if (!definition.id.startsWith('#')) { - yield [definition.id, definition.type, definition.async]; - } - } - } - - getTypeId(type: Type): string | undefined { - return this.types.get(type); - } - - getIdsByType(type: Type): string[] { - const alias = this.types.get(type); - - if (alias === undefined) { - return []; - } - - return [...this.aliases.get(alias) ?? []]; - } - - getByType(type: Type): ServiceDefinitionInfo[] { - return this.getIdsByType(type).map((id) => this.definitions.get(id)!); - } - - isAsync(type: Type): boolean { - return this.getByType(type).some((def) => def.async); - } - - private registerType(type: Type): string { - const existing = this.types.get(type); - - if (existing !== undefined) { - return existing; - } - - const name = type.getSymbol()?.getName() ?? 'Anonymous'; - - for (let idx = 0; true; ++idx) { - const id = `#${name}.${idx}`; - - if (!this.typeIds.has(id)) { - this.types.set(type, id); - this.typeIds.add(id); - return id; - } - } - } - - private registerAlias(id: string, alias: string): void { - const ids = this.aliases.get(alias) ?? new Set(); - this.aliases.set(alias, ids); - ids.add(id); - } -} diff --git a/core/cli/src/definitionScanner.ts b/core/cli/src/definitionScanner.ts deleted file mode 100644 index 7afb2c7..0000000 --- a/core/cli/src/definitionScanner.ts +++ /dev/null @@ -1,484 +0,0 @@ -import { Logger } from '@debugr/core'; -import { ServiceScope } from 'dicc'; -import { relative } from 'path'; -import { - ClassDeclaration, - ExportedDeclarations, - Expression, - Identifier, - InterfaceDeclaration, - ModuleDeclaration, - Node, - ObjectLiteralExpression, - SatisfiesExpression, - Signature, - SourceFile, - SyntaxKind, - Type, - TypeReferenceNode, - VariableDeclaration, -} from 'ts-morph'; -import { ContainerBuilder } from './containerBuilder'; -import { DefinitionError } from './errors'; -import { TypeHelper } from './typeHelper'; -import { - ArgumentOverrideMap, - CallbackInfo, - ResourceOptions, - ServiceFactoryInfo, - ServiceHooks, - TypeFlag, -} from './types'; - -export class DefinitionScanner { - constructor( - private readonly helper: TypeHelper, - private readonly logger: Logger, - ) {} - - scanDefinitions(builder: ContainerBuilder, source: SourceFile, options: ResourceOptions = {}): void { - const exclude = createExcludeRegex(options.exclude); - const ctx: ScanContext = { - builder, - source, - exclude, - path: '', - describe() { - return `'${this.path.replace(/\.$/, '')}' found in '${relative(process.cwd(), this.source.getFilePath())}'`; - }, - }; - - this.scanNode(ctx, source); - } - - * resolvePublicServices(container: Type): Iterable<[id: string, type: Type, async?: boolean]> { - for (const [id, svcType] of this.helper.resolveContainerPublicServices(container)) { - const [type, async] = this.helper.unwrapAsyncType(svcType); - yield [id, type, async]; - } - } - - private scanNode(ctx: ScanContext, node?: Node): void { - if (ctx.exclude?.test(ctx.path)) { - this.logger.trace(`Ignored ${ctx.describe()}`); - return; - } - - if (Node.isSourceFile(node)) { - this.scanModule(ctx, node); - } else if (Node.isModuleDeclaration(node) && node.hasNamespaceKeyword()) { - this.scanModule(ctx, node); - } else if (Node.isObjectLiteralExpression(node)) { - this.scanObject(ctx, node); - } else if (Node.isVariableDeclaration(node)) { - this.scanVariableDeclaration(ctx, node); - } else if (Node.isIdentifier(node)) { - this.scanIdentifier(ctx, node); - } else if (Node.isClassDeclaration(node)) { - this.scanClassDeclaration(ctx, node); - } else if (Node.isInterfaceDeclaration(node)) { - this.scanInterfaceDeclaration(ctx, node); - } else if (Node.isSatisfiesExpression(node)) { - this.scanSatisfiesExpression(ctx, node); - } - } - - private scanModule(ctx: ScanContext, node: SourceFile | ModuleDeclaration): void { - for (const [name, declarations] of node.getExportedDeclarations()) { - this.scanExportedDeclarations({ ...ctx, path: `${ctx.path}${name}.` }, declarations); - } - } - - private scanExportedDeclarations(ctx: ScanContext, declarations?: ExportedDeclarations[]): void { - for (const declaration of declarations ?? []) { - this.scanNode(ctx, declaration); - } - } - - private scanObject(ctx: ScanContext, node: ObjectLiteralExpression): void { - for (const prop of node.getProperties()) { - if (Node.isSpreadAssignment(prop)) { - this.scanNode(ctx, prop.getExpression()); - } else if (Node.isShorthandPropertyAssignment(prop)) { - this.scanNode({ ...ctx, path: `${ctx.path}${prop.getName()}.` }, prop.getNameNode()); - } else if (Node.isPropertyAssignment(prop)) { - const name = this.helper.resolveLiteralPropertyName(prop.getNameNode()); - - if (name !== undefined) { - this.scanNode({ ...ctx, path: `${ctx.path}${name}.` }, prop.getInitializerOrThrow()); - } - } - } - } - - private scanIdentifier(ctx: ScanContext, node: Identifier): void { - for (const definition of node.getDefinitionNodes()) { - if (Node.isNamespaceImport(definition)) { - this.scanModule( - ctx, - definition - .getFirstAncestorByKindOrThrow(SyntaxKind.ImportDeclaration) - .getModuleSpecifierSourceFileOrThrow(), - ); - } else if (Node.isImportClause(definition)) { - this.scanExportedDeclarations( - ctx, - definition - .getFirstAncestorByKindOrThrow(SyntaxKind.ImportDeclaration) - .getModuleSpecifierSourceFileOrThrow() - .getExportedDeclarations() - .get('default'), - ); - } else if (Node.isImportSpecifier(definition)) { - this.scanExportedDeclarations( - ctx, - definition - .getFirstAncestorByKindOrThrow(SyntaxKind.ImportDeclaration) - .getModuleSpecifierSourceFileOrThrow() - .getExportedDeclarations() - .get(definition.getAliasNode()?.getText() ?? definition.getName()), - ); - } else if (Node.isVariableDeclaration(definition)) { - this.scanNode(ctx, definition.getInitializer()); - } - } - } - - private scanClassDeclaration(ctx: ScanContext, node: ClassDeclaration): void { - if (node.getTypeParameters().length) { - return; - } - - if (node.isAbstract()) { - if (node.getStaticMembers().length || node.getConstructors().length) { - this.logger.warning(`Abstract class ${node.getName()} has static members or a constructor`); - return; - } - - for (const member of node.getInstanceMembers()) { - if (Node.isPropertyDeclaration(member)) { - if (!member.isReadonly() || !member.hasInitializer()) { - return; - } - } else if (!Node.isMethodDeclaration(member) || !member.isAbstract()) { - return; - } else { - const name = member.getName(); - const params = member.getParameters(); - - if (name !== 'create' && (name !== 'get' || params.length !== 0)) { - return; - } - } - } - } - - this.registerService(ctx, node.getType(), this.helper.resolveClassTypes(node)); - } - - private scanInterfaceDeclaration(ctx: ScanContext, node: InterfaceDeclaration): void { - if (node.getTypeParameters().length) { - return; - } - - const type = node.getType(); - const aliases = this.helper.resolveInterfaceTypes(node); - const nestedTypes = this.helper.resolveNestedContainerParameters(node, type, aliases); - - if (nestedTypes) { - ctx.builder.setParametersInfo({ - source: ctx.source, - path: ctx.path.replace(/\.$/, ''), - type, - nestedTypes, - }); - } else { - this.registerService(ctx, type, aliases); - } - } - - private scanVariableDeclaration(ctx: ScanContext, node: VariableDeclaration): void { - this.scanNode(ctx, node.getInitializer()); - } - - private scanSatisfiesExpression(ctx: ScanContext, node: SatisfiesExpression): void { - const satisfies = node.getTypeNode(); - - if (this.helper.isServiceDefinition(satisfies)) { - const [typeArg, aliasArg] = satisfies.getTypeArguments(); - this.registerService(ctx, typeArg.getType(), this.helper.resolveAliases(aliasArg), node.getExpression()); - } else if (this.helper.isServiceDecorator(satisfies)) { - this.registerDecorator(ctx, node.getExpression(), satisfies); - } - } - - private registerService( - ctx: ScanContext, - type: Type, - aliases: Type[], - definition?: Expression, - ): void { - const source = ctx.source; - const path = ctx.path.replace(/\.$/, ''); - const [factory, object] = this.resolveFactory(type, definition); - const args = this.resolveServiceArgs(definition); - const scope = this.resolveServiceScope(definition); - const hooks = this.resolveServiceHooks(definition); - const anonymous = this.resolveAnonymousFlag(definition); - const container = this.helper.isContainer(type); - const id = definition && !anonymous ? path : undefined; - const explicit = !!definition; - - this.logger.debug(`Register service ${ctx.describe()}`); - - ctx.builder.register({ - source, - path, - id, - type, - aliases, - object, - explicit, - anonymous, - container, - factory, - args, - scope, - hooks, - }); - } - - private registerDecorator(ctx: ScanContext, definition: Expression, nodeType: TypeReferenceNode): void { - if (!Node.isObjectLiteralExpression(definition)) { - return; - } - - const source = ctx.source; - const path = ctx.path.replace(/\.$/, ''); - const [typeArg] = nodeType.getTypeArguments(); - const type = typeArg.getType(); - const priority = this.resolveDecoratorPriority(definition); - const decorate = this.resolveServiceHook(definition, 'decorate'); - const scope = this.resolveServiceScope(definition); - const hooks = this.resolveServiceHooks(definition); - this.logger.debug(`Register decorator ${ctx.describe()}`); - ctx.builder.decorate({ source, path, type, priority, decorate, scope, hooks }); - } - - private resolveFactory(type: Type, definition?: Expression): [factory?: ServiceFactoryInfo, object?: boolean] { - if (!definition && type.isClass()) { - const symbol = type.getSymbolOrThrow(); - const declaration = symbol.getValueDeclarationOrThrow(); - - return Node.isClassDeclaration(declaration) && declaration.isAbstract() - ? [undefined, false] - : [this.resolveFactoryInfo(symbol.getTypeAtLocation(declaration)), false]; - } - - const [factory, object] = Node.isObjectLiteralExpression(definition) - ? [definition.getPropertyOrThrow('factory'), true] - : [definition, false]; - - if (!factory) { - return [undefined, object]; - } - - const factoryType = factory.getType(); - const declaration = factoryType.getSymbol()?.getValueDeclaration(); - - return Node.isClassDeclaration(declaration) && declaration.isAbstract() - ? [undefined, object] - : [this.resolveFactoryInfo(factoryType), object]; - } - - private resolveFactoryInfo(factoryType: Type): ServiceFactoryInfo | undefined { - if (factoryType.isUndefined()) { - return undefined; - } - - const [signature, method] = this.helper.resolveFactorySignature(factoryType); - const [returnType, async] = this.helper.unwrapAsyncType(signature.getReturnType()); - const args = signature.getParameters().map((arg) => this.helper.resolveArgumentInfo(arg)); - return { args, returnType, method, async }; - } - - private resolveServiceArgs(definition?: Expression): ArgumentOverrideMap | undefined { - if (!Node.isObjectLiteralExpression(definition)) { - return undefined; - } - - const argsProp = definition.getProperty('args'); - - if (!argsProp) { - return undefined; - } else if (!Node.isPropertyAssignment(argsProp)) { - throw new DefinitionError(`Invalid 'args', must be a property assignment`, argsProp); - } - - const argsInit = argsProp.getInitializer(); - - if (!Node.isObjectLiteralExpression(argsInit)) { - throw new DefinitionError(`Invalid 'args', must be an object literal`, argsInit ?? argsProp); - } - - const args: ArgumentOverrideMap = {}; - - for (const arg of argsInit.getProperties()) { - if (!Node.isPropertyAssignment(arg)) { - throw new DefinitionError(`Invalid 'args' property, 'args' must be a plain object literal`, arg ?? argsInit); - } - - const name = arg.getName(); - const initializer = arg.getInitializer(); - - if (Node.isStringLiteral(initializer)) { - const value = initializer.getLiteralValue(); - args[name] = /%[a-z0-9_.]+%/i.test(value) ? value : undefined; - } else { - args[name] = this.resolveCallbackInfo(arg); - } - } - - return args; - } - - private resolveServiceHooks(definition?: Expression): ServiceHooks { - if (!Node.isObjectLiteralExpression(definition)) { - return {}; - } - - const hooks: ServiceHooks = {}; - - for (const hook of ['onCreate', 'onFork', 'onDestroy'] as const) { - hooks[hook] = this.resolveServiceHook(definition, hook); - } - - return hooks; - } - - private resolveServiceHook(definition: ObjectLiteralExpression, hook: string): CallbackInfo | undefined { - const hookProp = definition.getProperty(hook); - const info = this.resolveCallbackInfo(hookProp, hook === 'onFork' ? 2 : 1); - - if (!info && hookProp) { - throw new DefinitionError(`Invalid '${hook}' hook, must be a method declaration or property assignment`, hookProp); - } - - return info; - } - - private resolveCallbackInfo(node?: Node, skip: number = 0): CallbackInfo | undefined { - const signature = this.resolveCallSignature(node); - - if (!signature) { - return undefined; - } - - const args = signature.getParameters().slice(skip); - const [, flags] = this.helper.resolveType(signature.getReturnType()); - - return { - args: args.map((p) => this.helper.resolveArgumentInfo(p)), - async: Boolean(flags & TypeFlag.Async), - }; - } - - private resolveCallSignature(node?: Node): Signature | undefined { - if (Node.isMethodDeclaration(node)) { - return node.getSignature(); - } else if (Node.isPropertyAssignment(node)) { - const value = node.getInitializer(); - - if (Node.isFunctionExpression(value) || Node.isArrowFunction(value)) { - return value.getSignature(); - } - } - - return undefined; - } - - private resolveServiceScope(definition?: Expression): ServiceScope | undefined { - const initializer = this.resolvePropertyInitializer(definition, 'scope'); - - if (!initializer) { - return undefined; - } else if (!Node.isStringLiteral(initializer)) { - throw new DefinitionError(`The 'scope' option must be a string literal`, initializer); - } - - const scope = initializer.getLiteralValue(); - - switch (scope) { - case 'global': - case 'local': - case 'private': - return scope; - default: - throw new DefinitionError(`Invalid value for 'scope', must be one of 'global', 'local' or 'private'`, initializer); - } - } - - private resolveDecoratorPriority(definition?: Expression): number { - const initializer = this.resolvePropertyInitializer(definition, 'priority'); - - if (!initializer) { - return 0; - } else if (Node.isNumericLiteral(initializer)) { - return initializer.getLiteralValue(); - } else { - throw new DefinitionError(`The 'priority' option must be a numeric literal`, initializer); - } - } - - private resolveAnonymousFlag(definition?: Expression): boolean | undefined { - const initializer = this.resolvePropertyInitializer(definition, 'anonymous'); - - if (!initializer) { - return undefined; - } else if (Node.isTrueLiteral(initializer) || Node.isFalseLiteral(initializer)) { - return initializer.getLiteralValue(); - } else { - throw new DefinitionError(`The 'anonymous' option must be a boolean literal`, initializer); - } - } - - private resolvePropertyInitializer(definition: Expression | undefined, name: string): Node | undefined { - if (!Node.isObjectLiteralExpression(definition)) { - return undefined; - } - - const prop = definition.getProperty(name); - - if (!prop) { - return undefined; - } else if (!Node.isPropertyAssignment(prop)) { - throw new DefinitionError(`The '${name}' option must be a simple property assignment`, prop); - } - - return prop.getInitializerOrThrow(`Missing initializer for option '${name}'`); - } -} - -type ScanContext = { - builder: ContainerBuilder, - source: SourceFile; - path: string; - exclude?: RegExp; - describe(): string; -}; - -function createExcludeRegex(patterns?: string[]): RegExp | undefined { - if (!patterns || !patterns.length) { - return undefined; - } - - patterns = patterns - .filter((p) => !/(\/|\.tsx?$)/i.test(p)) - .map((p) => p - .replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') - .replace(/\\\*\\\*/g, '.*') - .replace(/\\\*/g, '[^.]*') - ); - - return new RegExp(`^(?:${patterns.join('|')})\.$`); -} diff --git a/core/cli/src/definitions.ts b/core/cli/src/definitions.ts deleted file mode 100644 index b1864bb..0000000 --- a/core/cli/src/definitions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ConsoleHandler } from '@debugr/console'; -import { Logger, Plugin } from '@debugr/core'; -import { ServiceDefinition } from 'dicc'; -import { IndentationText, NewLineKind, Project, QuoteKind } from 'ts-morph'; -import { Argv } from './argv'; -import { ConfigLoader } from './configLoader'; -import { Dicc } from './dicc'; -import { DiccConfig } from './types'; - -export namespace debug { - export const logger = { - factory: (plugins: Plugin[]) => new Logger({ - globalContext: {}, - plugins, - }), - } satisfies ServiceDefinition; - - export const console = { - factory: (argv: Argv) => new ConsoleHandler({ threshold: argv.logLevel, timestamp: false }), - scope: 'private', - anonymous: true, - } satisfies ServiceDefinition; - - export const otherConsole = { - factory: (argv: Argv) => new ConsoleHandler({ threshold: argv.logLevel, timestamp: false }), - scope: 'private', - anonymous: true, - } satisfies ServiceDefinition; -} - -export const config = { - factory: async (loader: ConfigLoader) => loader.load(), - anonymous: true, -} satisfies ServiceDefinition; - -export const project = { - factory: (config: DiccConfig) => new Project({ - tsConfigFilePath: config.project, - skipAddingFilesFromTsConfig: true, - manipulationSettings: { - indentationText: IndentationText.TwoSpaces, - newLineKind: NewLineKind.LineFeed, - quoteKind: QuoteKind.Single, - useTrailingCommas: true, - }, - }), - anonymous: true, -} satisfies ServiceDefinition; - -export const dicc = Dicc satisfies ServiceDefinition; diff --git a/core/cli/src/definitions/argumentDefinition.ts b/core/cli/src/definitions/argumentDefinition.ts new file mode 100644 index 0000000..78e146d --- /dev/null +++ b/core/cli/src/definitions/argumentDefinition.ts @@ -0,0 +1,21 @@ +import { Node, SourceFile, Type } from 'ts-morph'; +import { ValueType } from './types'; + +export class ArgumentDefinition { + constructor( + public readonly rawType: Type, + public readonly type: ValueType, + public readonly optional: boolean = false, + public readonly rest: boolean = false, + public readonly node?: Node, + ) {} +} + +export class OverrideDefinition { + constructor( + public readonly resource: SourceFile, + public readonly path: string, + public readonly type: ValueType, + public readonly node?: Node, + ) {} +} diff --git a/core/cli/src/definitions/autoImplementedMethod.ts b/core/cli/src/definitions/autoImplementedMethod.ts new file mode 100644 index 0000000..f6f6367 --- /dev/null +++ b/core/cli/src/definitions/autoImplementedMethod.ts @@ -0,0 +1,20 @@ +import { Node, SourceFile, Type } from 'ts-morph'; +import { ArgumentDefinition } from './argumentDefinition'; +import { CallableDefinition } from './callableDefinition'; +import { ReturnType } from './types'; + +export class AutoImplementedMethod extends CallableDefinition { + declare public readonly returnType: ReturnType; + + constructor( + resource: SourceFile, + path: string, + public readonly name: string, + args: Map, + rawReturnType: Type, + returnType: ReturnType, + node?: Node, + ) { + super(resource, path, args, rawReturnType, returnType, node); + } +} diff --git a/core/cli/src/definitions/callableDefinition.ts b/core/cli/src/definitions/callableDefinition.ts new file mode 100644 index 0000000..7735b45 --- /dev/null +++ b/core/cli/src/definitions/callableDefinition.ts @@ -0,0 +1,14 @@ +import { Node, SourceFile, Type } from 'ts-morph'; +import { ArgumentDefinition } from './argumentDefinition'; +import { ReturnType } from './types'; + +export class CallableDefinition { + constructor( + public readonly resource: SourceFile, + public readonly path: string, + public readonly args: Map, + public readonly rawReturnType: Type, + public readonly returnType?: ReturnType, // undefined means void + public readonly node?: Node, + ) {} +} diff --git a/core/cli/src/v2/definitions/decoratorDefinition.ts b/core/cli/src/definitions/decoratorDefinition.ts similarity index 66% rename from core/cli/src/v2/definitions/decoratorDefinition.ts rename to core/cli/src/definitions/decoratorDefinition.ts index 97fc3e8..48ab326 100644 --- a/core/cli/src/v2/definitions/decoratorDefinition.ts +++ b/core/cli/src/definitions/decoratorDefinition.ts @@ -1,23 +1,23 @@ import { ServiceScope } from 'dicc'; import { Node, SourceFile, Type } from 'ts-morph'; -import { Callable } from './callable'; +import { CallableDefinition } from './callableDefinition'; export type DecoratorOptions = { scope?: ServiceScope; - decorate?: Callable; - onCreate?: Callable; - onFork?: Callable; - onDestroy?: Callable; + decorate?: CallableDefinition; + onCreate?: CallableDefinition; + onFork?: CallableDefinition; + onDestroy?: CallableDefinition; priority?: number; node?: Node; }; export class DecoratorDefinition { public readonly scope?: ServiceScope; - public readonly decorate?: Callable; - public readonly onCreate?: Callable; - public readonly onFork?: Callable; - public readonly onDestroy?: Callable; + public readonly decorate?: CallableDefinition; + public readonly onCreate?: CallableDefinition; + public readonly onFork?: CallableDefinition; + public readonly onDestroy?: CallableDefinition; public readonly priority: number; public readonly node?: Node; diff --git a/core/cli/src/definitions/di.ts b/core/cli/src/definitions/di.ts new file mode 100644 index 0000000..82a649c --- /dev/null +++ b/core/cli/src/definitions/di.ts @@ -0,0 +1 @@ +export { ResourceScanner } from './resourceScanner'; diff --git a/core/cli/src/definitions/events.ts b/core/cli/src/definitions/events.ts new file mode 100644 index 0000000..f77e753 --- /dev/null +++ b/core/cli/src/definitions/events.ts @@ -0,0 +1,15 @@ +import { SourceFile } from 'ts-morph'; +import { ContainerBuilder } from '../container'; +import { Event } from '../events'; +import { DeclarationNode } from '../utils'; + +export class DeclarationNodeDiscovered extends Event { + constructor( + public readonly resource: SourceFile, + public readonly path: string, + public readonly node: DeclarationNode, + public readonly builder: ContainerBuilder, + ) { + super(); + } +} diff --git a/core/cli/src/definitions/factoryDefinition.ts b/core/cli/src/definitions/factoryDefinition.ts new file mode 100644 index 0000000..e671bbf --- /dev/null +++ b/core/cli/src/definitions/factoryDefinition.ts @@ -0,0 +1,18 @@ +import { Node, SourceFile, Type } from 'ts-morph'; +import { ArgumentDefinition } from './argumentDefinition'; +import { CallableDefinition } from './callableDefinition'; +import { ReturnType } from './types'; + +export class FactoryDefinition extends CallableDefinition { + constructor( + resource: SourceFile, + path: string, + args: Map, + rawReturnType: Type, + returnType?: ReturnType, + node?: Node, + public readonly method?: string, + ) { + super(resource, path, args, rawReturnType, returnType, node); + } +} diff --git a/core/cli/src/v2/definitions/index.ts b/core/cli/src/definitions/index.ts similarity index 55% rename from core/cli/src/v2/definitions/index.ts rename to core/cli/src/definitions/index.ts index a6c961c..54d18a5 100644 --- a/core/cli/src/v2/definitions/index.ts +++ b/core/cli/src/definitions/index.ts @@ -1,7 +1,9 @@ -export * from './argument'; +export * from './argumentDefinition'; export * from './autoImplementedMethod'; -export * from './callable'; +export * from './callableDefinition'; export * from './decoratorDefinition'; +export * from './events'; export * from './factoryDefinition'; +export * from './resourceScanner'; export * from './service'; export * from './types'; diff --git a/core/cli/src/v2/compiler/resourceScanner.ts b/core/cli/src/definitions/resourceScanner.ts similarity index 92% rename from core/cli/src/v2/compiler/resourceScanner.ts rename to core/cli/src/definitions/resourceScanner.ts index 51d7ec0..c9cc987 100644 --- a/core/cli/src/v2/compiler/resourceScanner.ts +++ b/core/cli/src/definitions/resourceScanner.ts @@ -1,8 +1,11 @@ import { resolve } from 'path'; import { ExportedDeclarations, - Identifier, ImportClause, ImportSpecifier, - ModuleDeclaration, NamespaceImport, + Identifier, + ImportClause, + ImportSpecifier, + ModuleDeclaration, + NamespaceImport, Node, ObjectLiteralExpression, Project, @@ -12,19 +15,9 @@ import { VariableDeclaration, } from 'ts-morph'; import { ContainerBuilder } from '../container'; -import { Event, EventDispatcher } from '../events'; +import { EventDispatcher } from '../events'; import { DeclarationNode } from '../utils'; - -export class ResourceDeclarationDiscovered extends Event { - constructor( - public readonly resource: SourceFile, - public readonly path: string, - public readonly node: DeclarationNode, - public readonly builder: ContainerBuilder, - ) { - super(); - } -} +import { DeclarationNodeDiscovered } from './events'; type EnqueuedResource = { builder: ContainerBuilder; @@ -179,7 +172,7 @@ export class ResourceScanner { } private emitDeclaration(ctx: ScanContext, node: DeclarationNode): void { - this.eventDispatcher.dispatch(new ResourceDeclarationDiscovered( + this.eventDispatcher.dispatch(new DeclarationNodeDiscovered( ctx.resource, ctx.path.replace(/\.$/, ''), node, diff --git a/core/cli/src/v2/definitions/service/abstractLocalServiceDefinition.ts b/core/cli/src/definitions/service/abstractLocalServiceDefinition.ts similarity index 74% rename from core/cli/src/v2/definitions/service/abstractLocalServiceDefinition.ts rename to core/cli/src/definitions/service/abstractLocalServiceDefinition.ts index 9c99260..e80621b 100644 --- a/core/cli/src/v2/definitions/service/abstractLocalServiceDefinition.ts +++ b/core/cli/src/definitions/service/abstractLocalServiceDefinition.ts @@ -1,15 +1,12 @@ import { ServiceScope } from 'dicc'; import { ClassDeclaration, InterfaceDeclaration, Node, SourceFile, Type } from 'ts-morph'; import { ContainerBuilder } from '../../container'; -import { DecoratorDefinition } from '../decoratorDefinition'; import { FactoryDefinition } from '../factoryDefinition'; -import type { ExplicitServiceDefinition } from './explicitServiceDefinition'; -import type { ImplicitServiceDefinition } from './implicitServiceDefinition'; import { AbstractServiceDefinition } from './abstractServiceDefinition'; import type { AutoImplementationInfo, + LocalServiceDefinition, LocalServiceDefinitionOptions, - ServiceDefinition, } from './types'; export class AbstractLocalServiceDefinition extends AbstractServiceDefinition { @@ -19,8 +16,6 @@ export class AbstractLocalServiceDefinition extends AbstractServiceDefinition { public readonly declaration?: ClassDeclaration | InterfaceDeclaration; public readonly container: boolean; public autoImplement?: AutoImplementationInfo; - public decorators?: DecoratorDefinition[]; - public autoRegister?: Map; constructor( builder: ContainerBuilder, @@ -38,7 +33,7 @@ export class AbstractLocalServiceDefinition extends AbstractServiceDefinition { this.container = options.container ?? false; } - isLocal(): this is ImplicitServiceDefinition | ExplicitServiceDefinition { + isLocal(): this is LocalServiceDefinition { return true; } } diff --git a/core/cli/src/v2/definitions/service/abstractServiceDefinition.ts b/core/cli/src/definitions/service/abstractServiceDefinition.ts similarity index 90% rename from core/cli/src/v2/definitions/service/abstractServiceDefinition.ts rename to core/cli/src/definitions/service/abstractServiceDefinition.ts index 87938eb..3eb8048 100644 --- a/core/cli/src/v2/definitions/service/abstractServiceDefinition.ts +++ b/core/cli/src/definitions/service/abstractServiceDefinition.ts @@ -1,5 +1,4 @@ import type { Type } from 'ts-morph'; -import { Lazy } from '../../compiler'; import { ContainerBuilder } from '../../container'; import type { ExplicitServiceDefinition } from './explicitServiceDefinition'; import type { ForeignServiceDefinition } from './foreignServiceDefinition'; @@ -8,8 +7,6 @@ import type { LocalServiceDefinition } from './types'; export abstract class AbstractServiceDefinition { public readonly aliases: Set; - public async: boolean = false; - public source?: Lazy; constructor( public readonly builder: ContainerBuilder, diff --git a/core/cli/src/v2/definitions/service/explicitServiceDefinition.ts b/core/cli/src/definitions/service/explicitServiceDefinition.ts similarity index 73% rename from core/cli/src/v2/definitions/service/explicitServiceDefinition.ts rename to core/cli/src/definitions/service/explicitServiceDefinition.ts index d1447bf..18d21a9 100644 --- a/core/cli/src/v2/definitions/service/explicitServiceDefinition.ts +++ b/core/cli/src/definitions/service/explicitServiceDefinition.ts @@ -1,18 +1,17 @@ import { SourceFile, Type } from 'ts-morph'; import { ContainerBuilder } from '../../container'; -import { Callable } from '../callable'; +import { CallableDefinition } from '../callableDefinition'; import { ArgumentOverride } from '../types'; import { AbstractLocalServiceDefinition } from './abstractLocalServiceDefinition'; import { ExplicitServiceDefinitionOptions } from './types'; export class ExplicitServiceDefinition extends AbstractLocalServiceDefinition { - public args: Map; - public object: boolean; - public anonymous: boolean; - public parent?: string; - public onCreate?: Callable; - public onFork?: Callable; - public onDestroy?: Callable; + public readonly args: Map; + public readonly object: boolean; + public readonly anonymous: boolean; + public readonly onCreate?: CallableDefinition; + public readonly onFork?: CallableDefinition; + public readonly onDestroy?: CallableDefinition; constructor( builder: ContainerBuilder, diff --git a/core/cli/src/v2/definitions/service/foreignServiceDefinition.ts b/core/cli/src/definitions/service/foreignServiceDefinition.ts similarity index 61% rename from core/cli/src/v2/definitions/service/foreignServiceDefinition.ts rename to core/cli/src/definitions/service/foreignServiceDefinition.ts index 70f033c..a95eb04 100644 --- a/core/cli/src/v2/definitions/service/foreignServiceDefinition.ts +++ b/core/cli/src/definitions/service/foreignServiceDefinition.ts @@ -1,26 +1,20 @@ import { Type } from 'ts-morph'; import { ContainerBuilder } from '../../container'; -import { AbstractLocalServiceDefinition } from './abstractLocalServiceDefinition'; import { AbstractServiceDefinition } from './abstractServiceDefinition'; -import { ForeignFactoryInfo } from './types'; - +import { LocalServiceDefinition, ServiceDefinition } from './types'; export class ForeignServiceDefinition extends AbstractServiceDefinition { - public readonly factory: ForeignFactoryInfo = { - async: false, - }; - constructor( builder: ContainerBuilder, - public readonly container: AbstractLocalServiceDefinition, + public readonly parent: LocalServiceDefinition, public readonly foreignId: string, id: string, type: Type, aliases?: Iterable, - async: boolean = false, + public readonly definition?: ServiceDefinition, + public readonly async: boolean = false, ) { super(builder, id, type, aliases); - this.async = async; } isForeign(): this is ForeignServiceDefinition { diff --git a/core/cli/src/v2/definitions/service/implicitServiceDefinition.ts b/core/cli/src/definitions/service/implicitServiceDefinition.ts similarity index 100% rename from core/cli/src/v2/definitions/service/implicitServiceDefinition.ts rename to core/cli/src/definitions/service/implicitServiceDefinition.ts diff --git a/core/cli/src/v2/definitions/service/index.ts b/core/cli/src/definitions/service/index.ts similarity index 100% rename from core/cli/src/v2/definitions/service/index.ts rename to core/cli/src/definitions/service/index.ts diff --git a/core/cli/src/v2/definitions/service/types.ts b/core/cli/src/definitions/service/types.ts similarity index 85% rename from core/cli/src/v2/definitions/service/types.ts rename to core/cli/src/definitions/service/types.ts index 7a63080..9ff1299 100644 --- a/core/cli/src/v2/definitions/service/types.ts +++ b/core/cli/src/definitions/service/types.ts @@ -1,7 +1,7 @@ import { ServiceScope } from 'dicc'; import { ClassDeclaration, InterfaceDeclaration, Node, Type } from 'ts-morph'; import { AutoImplementedMethod } from '../autoImplementedMethod'; -import { Callable } from '../callable'; +import { CallableDefinition } from '../callableDefinition'; import { FactoryDefinition } from '../factoryDefinition'; import { ArgumentOverride } from '../types'; import { ExplicitServiceDefinition } from './explicitServiceDefinition'; @@ -16,7 +16,6 @@ export type AutoImplementationInfo = { export type LocalServiceDefinitionOptions = { aliases?: Iterable; factory?: FactoryDefinition; - args?: Map; scope?: ServiceScope; node?: Node; declaration?: ClassDeclaration | InterfaceDeclaration; @@ -25,10 +24,11 @@ export type LocalServiceDefinitionOptions = { export type ExplicitServiceDefinitionOptions = LocalServiceDefinitionOptions & { object?: boolean; + args?: Map; anonymous?: boolean; - onCreate?: Callable; - onFork?: Callable; - onDestroy?: Callable; + onCreate?: CallableDefinition; + onFork?: CallableDefinition; + onDestroy?: CallableDefinition; }; export type ForeignFactoryInfo = { diff --git a/core/cli/src/definitions/types.ts b/core/cli/src/definitions/types.ts new file mode 100644 index 0000000..b6c97d1 --- /dev/null +++ b/core/cli/src/definitions/types.ts @@ -0,0 +1,178 @@ +import { Type } from 'ts-morph'; +import { OverrideDefinition } from './argumentDefinition'; +import { CallableDefinition } from './callableDefinition'; + +export type ArgumentOverride = CallableDefinition | OverrideDefinition | ForcedArgument; + +export class ForcedArgument { + constructor( + public readonly value: string, + ) {} +} + +export class SingleType { + constructor( + public readonly type: Type, + public readonly nullable: boolean = false, + public readonly aliasType: Type = type, + ) {} + + * getInjectableTypes(): Iterable { + yield this; + } + + get serviceType(): Type { + return this.type; + } +} + +export class ListType { + private readonly asSingleType: SingleType; + + constructor( + public readonly type: Type, + public readonly value: SingleType, + public readonly nullable: boolean = false, + ) { + this.asSingleType = new SingleType(this.type, this.nullable, this.aliasType); + } + + * getInjectableTypes(): Iterable { + yield this.asSingleType; + yield this; + } + + get serviceType(): Type { + return this.value.serviceType; + } + + get aliasType(): Type { + return this.value.aliasType; + } +} + +export class PromiseType { + private readonly asSingleType: SingleType; + + constructor( + public readonly type: Type, + public readonly value: SingleType | ListType, + public readonly nullable: boolean = false, + ) { + this.asSingleType = new SingleType(this.type, this.nullable, this.aliasType); + } + + * getInjectableTypes(): Iterable { + yield this.asSingleType; + yield this; + } + + get serviceType(): Type { + return this.value.serviceType; + } + + get aliasType(): Type { + return this.value.aliasType; + } +} + +export class IterableType { + private readonly asSingleType: SingleType; + + constructor( + public readonly type: Type, + public readonly value: SingleType, + public readonly nullable: boolean = false, + public readonly async: boolean = false, + ) { + this.asSingleType = new SingleType(this.type, this.nullable, this.aliasType); + } + + * getInjectableTypes(): Iterable { + yield this.asSingleType; + yield this; + } + + get serviceType(): Type { + return this.value.serviceType; + } + + get aliasType(): Type { + return this.value.aliasType; + } +} + +export class AccessorType { + private readonly asSingleType: SingleType; + + constructor( + public readonly type: Type, + public readonly returnType: SingleType | ListType | PromiseType, + public readonly nullable: boolean = false, + ) { + this.asSingleType = new SingleType(this.type, this.nullable, this.aliasType); + } + + * getInjectableTypes(): Iterable { + yield this.asSingleType; + yield this; + } + + get serviceType(): Type { + return this.returnType.serviceType; + } + + get aliasType(): Type { + return this.returnType.aliasType; + } +} + +export class InjectorType { + private readonly asSingleType: SingleType; + + constructor( + public readonly type: Type, + public readonly serviceType: Type, + public readonly nullable: boolean = false, + ) { + this.asSingleType = new SingleType(this.type, this.nullable, this.aliasType); + } + + * getInjectableTypes(): Iterable { + yield this.asSingleType; + yield this; + } + + get aliasType(): Type { + return this.serviceType; + } +} + +export class ScopedRunnerType { + constructor( + public readonly nullable: boolean = false, + ) {} +} + +export type ValueType = + | SingleType + | ListType + | PromiseType + | IterableType + | AccessorType + | InjectorType + | ScopedRunnerType; + +export type InjectableType = + | SingleType + | ListType + | PromiseType + | IterableType + | AccessorType + | InjectorType; + +export type ReturnType = + | SingleType + | ListType + | PromiseType + | IterableType; diff --git a/core/cli/src/dicc.ts b/core/cli/src/dicc.ts deleted file mode 100644 index 25b3846..0000000 --- a/core/cli/src/dicc.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Logger } from '@debugr/core'; -import { SourceFile } from 'ts-morph'; -import { AutowiringFactory } from './autowiring'; -import { Checker } from './checker'; -import { Compiler } from './compiler'; -import { ContainerBuilder } from './containerBuilder'; -import { DefinitionScanner } from './definitionScanner'; -import { UserError } from './errors'; -import { SourceFiles } from './sourceFiles'; -import { DiccConfig } from './types'; - -export class Dicc { - constructor( - private readonly sourceFiles: SourceFiles, - private readonly scanner: DefinitionScanner, - private readonly autowiringFactory: AutowiringFactory, - private readonly checker: Checker, - private readonly config: DiccConfig, - private readonly logger: Logger, - ) {} - - async compile(): Promise { - const containers: Map = new Map(); - - for (const [path, options] of Object.entries(this.config.containers)) { - this.logger.debug(`Scanning resources for '${path}'...`); - const builder = new ContainerBuilder(path, options, this.sourceFiles.getOutput(path)); - - for (const [resource, opts] of Object.entries(options.resources)) { - for (const input of this.sourceFiles.getInputs(path, resource)) { - this.logger.trace(`Scanning '${input.getFilePath()}'`); - this.scanner.scanDefinitions(builder, input, opts ?? undefined); - } - } - - containers.set(builder.output, builder); - } - - for (const builder of containers.values()) { - this.logger.debug(`Post-processing '${builder.path}'...`); - this.checker.checkAutoFactories(builder); - this.logger.trace('Cleaning up...'); - this.checker.removeExtraneousImplicitRegistrations(builder); - this.logger.trace('Applying decorators...'); - builder.applyDecorators(); - } - - this.mergeForeignServices(containers); - - this.logger.trace('Autowiring dependencies...'); - this.autowiringFactory.create(containers).checkDependencies(); - - for (const builder of containers.values()) { - this.logger.debug(`Compiling '${builder.path}'...`); - const compiler = new Compiler(builder); - compiler.compile(); - } - - this.logger.debug(`Writing output files...`); - - for (const output of containers.keys()) { - await output.save(); - } - - this.logger.debug(`Type-checking compiled containers...`); - let errors = false; - - for (const [output, builder] of containers) { - if (!builder.options.typeCheck) { - this.logger.debug(`Type-checking disabled for '${builder.path}', skipping.`); - } else if (!this.checker.checkOutput(output)) { - errors = true; - } - } - - if (errors) { - throw new UserError(`One or more compiled containers have TypeScript errors`); - } - } - - private mergeForeignServices(containers: Map): void { - for (const builder of containers.values()) { - for (const def of builder.getDefinitions()) { - if (!def.container) { - continue; - } - - const source = def.type.getSymbol()?.getValueDeclaration()?.getSourceFile(); - - if (!source) { - continue; - } - - const services = containers.get(source)?.getPublicServices() ?? this.scanner.resolvePublicServices(def.type); - - for (const [id, type, async] of services) { - builder.register({ - id, - type, - factory: { - args: [], - returnType: type, - async, - }, - source: def.source, - path: def.path, - object: def.object, - explicit: def.explicit, - parent: def.id, - scope: 'private', - aliases: [], - hooks: {}, - }); - } - } - } - } -} diff --git a/core/cli/src/errors.ts b/core/cli/src/errors.ts deleted file mode 100644 index 6e22c95..0000000 --- a/core/cli/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Node, Type } from 'ts-morph'; - -export class UserError extends Error {} - -export class ConfigError extends UserError {} - -export class DefinitionError extends UserError { - constructor(message: string, node?: Node) { - const location = node - ? ` in ${node.getSourceFile().getFilePath()} on line ${node.getStartLineNumber()}` - : ''; - super(`${message}${location}`); - } -} - -export class TypeError extends DefinitionError { - constructor(message: string, type?: Type) { - const [node] = type?.getSymbol()?.getDeclarations() ?? []; - super(message, node); - } -} diff --git a/core/cli/src/v2/errors/errors.ts b/core/cli/src/errors/errors.ts similarity index 100% rename from core/cli/src/v2/errors/errors.ts rename to core/cli/src/errors/errors.ts diff --git a/core/cli/src/v2/errors/helpers.ts b/core/cli/src/errors/helpers.ts similarity index 100% rename from core/cli/src/v2/errors/helpers.ts rename to core/cli/src/errors/helpers.ts diff --git a/core/cli/src/v2/errors/index.ts b/core/cli/src/errors/index.ts similarity index 100% rename from core/cli/src/v2/errors/index.ts rename to core/cli/src/errors/index.ts diff --git a/core/cli/src/v2/errors/reporters.ts b/core/cli/src/errors/reporters.ts similarity index 97% rename from core/cli/src/v2/errors/reporters.ts rename to core/cli/src/errors/reporters.ts index 2feaa2d..3a0ad70 100644 --- a/core/cli/src/v2/errors/reporters.ts +++ b/core/cli/src/errors/reporters.ts @@ -129,9 +129,10 @@ export class CyclicDependencyErrorReporter implements ErrorReporter { - yield line('Cyclic dependency detected:'); + yield 'Cyclic dependency detected:'; for (const def of error.definitions) { + yield '\n'; yield * definition(def); } } @@ -144,7 +145,7 @@ function line(...chunks: string[]): string { } function builder(builder: ContainerBuilder): string { - return line('Container:', filePath(builder.sourceFile), `(${builder.className})`); + return line('Container:', filePath(builder.sourceFile), `(${builder.options.className})`); } function file(label: string, file: SourceFile): string { @@ -182,9 +183,9 @@ function * definition(definition?: ServiceDefinition | DecoratorDefinition): Ite 'Service:', definition.foreignId, 'merged from container', - definition.container.path, + definition.parent.path, 'exported from', - filePath(definition.container.resource), + filePath(definition.parent.resource), ); } else { yield line( diff --git a/core/cli/src/v2/errors/types.ts b/core/cli/src/errors/types.ts similarity index 100% rename from core/cli/src/v2/errors/types.ts rename to core/cli/src/errors/types.ts diff --git a/core/cli/src/v2/events/asyncEvent.ts b/core/cli/src/events/asyncEvent.ts similarity index 100% rename from core/cli/src/v2/events/asyncEvent.ts rename to core/cli/src/events/asyncEvent.ts diff --git a/core/cli/src/v2/events/event.ts b/core/cli/src/events/event.ts similarity index 100% rename from core/cli/src/v2/events/event.ts rename to core/cli/src/events/event.ts diff --git a/core/cli/src/v2/events/eventDispatcher.ts b/core/cli/src/events/eventDispatcher.ts similarity index 100% rename from core/cli/src/v2/events/eventDispatcher.ts rename to core/cli/src/events/eventDispatcher.ts diff --git a/core/cli/src/v2/events/index.ts b/core/cli/src/events/index.ts similarity index 100% rename from core/cli/src/v2/events/index.ts rename to core/cli/src/events/index.ts diff --git a/core/cli/src/v2/events/types.ts b/core/cli/src/events/types.ts similarity index 100% rename from core/cli/src/v2/events/types.ts rename to core/cli/src/events/types.ts diff --git a/core/cli/src/v2/extensions/compilerExtension.ts b/core/cli/src/extensions/compilerExtension.ts similarity index 91% rename from core/cli/src/v2/extensions/compilerExtension.ts rename to core/cli/src/extensions/compilerExtension.ts index 0db08a4..c88dbbc 100644 --- a/core/cli/src/v2/extensions/compilerExtension.ts +++ b/core/cli/src/extensions/compilerExtension.ts @@ -17,7 +17,4 @@ export abstract class CompilerExtension implements EventSubscriber { loadResources(builder: ContainerBuilder, enqueue: EnqueueResourcesCb): void { } - - autowireDependencies(builder: ContainerBuilder): void { - } } diff --git a/core/cli/src/v2/extensions/decoratorsExtension.ts b/core/cli/src/extensions/decoratorsExtension.ts similarity index 83% rename from core/cli/src/v2/extensions/decoratorsExtension.ts rename to core/cli/src/extensions/decoratorsExtension.ts index 32852a6..58b2350 100644 --- a/core/cli/src/v2/extensions/decoratorsExtension.ts +++ b/core/cli/src/extensions/decoratorsExtension.ts @@ -1,11 +1,11 @@ import { Node, SourceFile } from 'ts-morph'; -import { ResourceDeclarationDiscovered } from '../compiler'; import { ContainerBuilder } from '../container'; +import { DeclarationNodeDiscovered } from '../definitions'; import { DefinitionError, UserCodeContext } from '../errors'; import { EventSubscription } from '../events'; import { DeclarationNode, TypeHelper } from '../utils'; import { CompilerExtension } from './compilerExtension'; -import { getPropertyLiteralValueIfKind, validateServiceScope } from './helpers'; +import { getPropertyLiteralValueIfKind, subpath, validateServiceScope } from './helpers'; export class DecoratorsExtension extends CompilerExtension { constructor( @@ -15,7 +15,7 @@ export class DecoratorsExtension extends CompilerExtension { } * getSubscribedEvents(): Iterable> { - yield ResourceDeclarationDiscovered.sub((evt) => this.scanNode(evt.resource, evt.path, evt.node, evt.builder)); + yield DeclarationNodeDiscovered.sub((evt) => this.scanNode(evt.resource, evt.path, evt.node, evt.builder)); } private scanNode(resource: SourceFile, path: string, node: DeclarationNode, builder: ContainerBuilder): void { @@ -38,14 +38,14 @@ export class DecoratorsExtension extends CompilerExtension { const [typeArg] = typeNode.getTypeArguments(); const targetType = typeArg.getType(); - const scope = getPropertyLiteralValueIfKind(expression, 'scope', 'string', ctx, validateServiceScope); + const scope = getPropertyLiteralValueIfKind(expression, 'scope', 'string', subpath(ctx, 'scope'), validateServiceScope); const decorate = this.typeHelper.resolveCallableProperty(expression, 'decorate', ctx); const onCreate = this.typeHelper.resolveCallableProperty(expression, 'onCreate', ctx); const onFork = this.typeHelper.resolveCallableProperty(expression, 'onFork', ctx); const onDestroy = this.typeHelper.resolveCallableProperty(expression, 'onDestroy', ctx); const priority = getPropertyLiteralValueIfKind(expression, 'priority', 'number'); - builder.decorators.add(resource, path, targetType, { + builder.addDecorator(resource, path, targetType, { scope, decorate, onCreate, diff --git a/core/cli/src/extensions/di.ts b/core/cli/src/extensions/di.ts new file mode 100644 index 0000000..3b774cd --- /dev/null +++ b/core/cli/src/extensions/di.ts @@ -0,0 +1,3 @@ +export { ExtensionLoader } from './extensionLoader'; +export { ServicesExtension } from './servicesExtension'; +export { DecoratorsExtension } from './decoratorsExtension'; diff --git a/core/cli/src/v2/extensions/extensionLoader.ts b/core/cli/src/extensions/extensionLoader.ts similarity index 90% rename from core/cli/src/v2/extensions/extensionLoader.ts rename to core/cli/src/extensions/extensionLoader.ts index 2775d84..3c02c08 100644 --- a/core/cli/src/v2/extensions/extensionLoader.ts +++ b/core/cli/src/extensions/extensionLoader.ts @@ -1,4 +1,6 @@ +import { Logger } from '@debugr/core'; import { dirname, resolve } from 'path'; +import { CompilerConfig } from '../config'; import { ConfigError, ExtensionError } from '../errors'; import { EventDispatcher } from '../events'; import { ModuleResolver, ReferenceResolverFactory, TypeHelper } from '../utils'; @@ -15,11 +17,13 @@ export class ExtensionLoader { private readonly getModuleResolver: () => Promise, private readonly getTypeHelper: () => Promise, private readonly referenceResolverFactory: ReferenceResolverFactory, + private readonly logger: Logger, + private readonly config: CompilerConfig, ) {} - async * load(extensions: Record, configPath: string): AsyncIterable { - for (const [id, config] of Object.entries(extensions)) { - const [path, constructor] = await this.resolveExtension(id, configPath); + async * load(): AsyncIterable { + for (const [id, config] of Object.entries(this.config.extensions)) { + const [path, constructor] = await this.resolveExtension(id, this.config.configFile); const extension = new constructor( ...await this.resolveInjections(id, path, constructor.inject ?? [], constructor.references), @@ -45,6 +49,7 @@ export class ExtensionLoader { case 'eventDispatcher': args.push(this.eventDispatcher); break; case 'moduleResolver': args.push(await this.getModuleResolver()); break; case 'typeHelper': args.push(await this.getTypeHelper()); break; + case 'logger': args.push(this.logger); break; case 'referenceResolver': if (!references) { throw new ExtensionError(`Extension requires injection of reference resolver but doesn't specify a reference module`, extensionId); diff --git a/core/cli/src/v2/extensions/helpers.ts b/core/cli/src/extensions/helpers.ts similarity index 94% rename from core/cli/src/v2/extensions/helpers.ts rename to core/cli/src/extensions/helpers.ts index bce00dc..a33203a 100644 --- a/core/cli/src/v2/extensions/helpers.ts +++ b/core/cli/src/extensions/helpers.ts @@ -61,3 +61,7 @@ export function validateServiceScope(scope: string, ctx: UserCodeContext): Servi throw new DefinitionError(`Invalid service scope '${scope}'`, ctx); } + +export function subpath(ctx: UserCodeContext, sub: string): UserCodeContext { + return { ...ctx, path: `${ctx.path}.${sub}` }; +} diff --git a/core/cli/src/v2/extensions/index.ts b/core/cli/src/extensions/index.ts similarity index 100% rename from core/cli/src/v2/extensions/index.ts rename to core/cli/src/extensions/index.ts diff --git a/core/cli/src/v2/extensions/servicesExtension.ts b/core/cli/src/extensions/servicesExtension.ts similarity index 80% rename from core/cli/src/v2/extensions/servicesExtension.ts rename to core/cli/src/extensions/servicesExtension.ts index 79710e1..8cb5c54 100644 --- a/core/cli/src/v2/extensions/servicesExtension.ts +++ b/core/cli/src/extensions/servicesExtension.ts @@ -12,23 +12,24 @@ import { Type, TypeNode, } from 'ts-morph'; -import { ResourceDeclarationDiscovered } from '../compiler'; import { ContainerBuilder, ServiceAdded } from '../container'; import { ArgumentOverride, - Callable, + CallableDefinition, + DeclarationNodeDiscovered, PromiseType, - ImplicitServiceDefinition, SingleType, ExplicitServiceDefinitionOptions, ServiceDefinition, AutoImplementedMethod, + OverrideDefinition, + LocalServiceDefinition, } from '../definitions'; import { DefinitionError, UserCodeContext } from '../errors'; import { EventSubscription } from '../events'; import { DeclarationNode, TypeHelper } from '../utils'; import { CompilerExtension } from './compilerExtension'; -import { getPropertyLiteralValueIfKind, validateServiceScope } from './helpers'; +import { getPropertyLiteralValueIfKind, subpath, validateServiceScope } from './helpers'; export class ServicesExtension extends CompilerExtension { private readonly pendingAutoImplements: Map = new Map(); @@ -40,7 +41,7 @@ export class ServicesExtension extends CompilerExtension { } * getSubscribedEvents(): Iterable> { - yield ResourceDeclarationDiscovered.sub((evt) => this.scanNode(evt.resource, evt.path, evt.node, evt.builder)); + yield DeclarationNodeDiscovered.sub((evt) => this.scanNode(evt.resource, evt.path, evt.node, evt.builder)); yield ServiceAdded.sub((evt) => this.tryAutoImplement(evt.service)); } @@ -66,7 +67,7 @@ export class ServicesExtension extends CompilerExtension { const type = node.getType(); const aliases = this.typeHelper.resolveAliases(type); - ctx.builder.services.addImplicitDefinition(ctx.builder, ctx.resource, ctx.path, type, { + ctx.builder.addImplicitDefinition(ctx.resource, ctx.path, type, { aliases, factory: this.typeHelper.resolveFactory(type, ctx), node, @@ -82,7 +83,7 @@ export class ServicesExtension extends CompilerExtension { const type = node.getType(); - ctx.builder.services.addImplicitDefinition(ctx.builder, ctx.resource, ctx.path, type, { + ctx.builder.addImplicitDefinition(ctx.resource, ctx.path, type, { aliases: this.typeHelper.resolveAliases(type), node, declaration: node, @@ -110,7 +111,7 @@ export class ServicesExtension extends CompilerExtension { ? [this.typeHelper.resolveAliases(rtn.type), this.typeHelper.resolveDeclaration(rtn.type)] : []; - ctx.builder.services.addImplicitDefinition(ctx.builder, ctx.resource, ctx.path, rtn.type, { + ctx.builder.addImplicitDefinition(ctx.resource, ctx.path, rtn.type, { aliases, factory, node, @@ -134,9 +135,12 @@ export class ServicesExtension extends CompilerExtension { const expression = node.getExpression(); const [factoryType, options] = this.resolveExplicitDefinition(expression, { ...ctx, node: expression }); let declaration = factoryType ? this.typeHelper.resolveDeclaration(factoryType) : undefined; - const factory = declaration ? this.typeHelper.resolveFactory(declaration.getType(), ctx) - : factoryType ? this.typeHelper.resolveFactory(factoryType, ctx) - : undefined; + const factoryCtx = options.object ? subpath(ctx, 'factory') : ctx; + const factory = declaration + ? this.typeHelper.resolveFactory(declaration.getType(), factoryCtx) + : factoryType + ? this.typeHelper.resolveFactory(factoryType, factoryCtx) + : undefined; if (!declaration && factory && factory.returnType && factory.returnType) { const returnType = factory.returnType instanceof PromiseType @@ -145,7 +149,7 @@ export class ServicesExtension extends CompilerExtension { declaration = this.typeHelper.resolveDeclaration(returnType); } - ctx.builder.services.addExplicitDefinition(ctx.builder, ctx.resource, ctx.path, type, { + ctx.builder.addExplicitDefinition(ctx.resource, ctx.path, type, { factory, node, declaration, @@ -175,8 +179,8 @@ export class ServicesExtension extends CompilerExtension { factoryType = undefined; } - const args = this.resolveArgumentOverrides(getPropertyLiteralValueIfKind(node, 'args', 'object'), ctx); - const scope = getPropertyLiteralValueIfKind(node, 'scope', 'string', ctx, validateServiceScope); + const args = this.resolveArgumentOverrides(getPropertyLiteralValueIfKind(node, 'args', 'object'), subpath(ctx, 'args')); + const scope = getPropertyLiteralValueIfKind(node, 'scope', 'string', subpath(ctx, 'scope'), validateServiceScope); const anonymous = getPropertyLiteralValueIfKind(node, 'anonymous', 'boolean'); const onCreate = this.typeHelper.resolveCallableProperty(node, 'onCreate', ctx); const onFork = this.typeHelper.resolveCallableProperty(node, 'onFork', ctx); @@ -187,12 +191,12 @@ export class ServicesExtension extends CompilerExtension { private resolveArgumentOverrides( args: ObjectLiteralExpression | undefined, ctx: UserCodeContext, - ): Map | undefined { + ): Map | undefined { if (!args) { return undefined; } - const map: Map = new Map(); + const map: Map = new Map(); for (const prop of args.getProperties()) { if (!Node.isPropertyNamed(prop)) { @@ -200,16 +204,24 @@ export class ServicesExtension extends CompilerExtension { } const type = prop.getType(); - const signature = this.typeHelper.resolveCallSignature(type, { ...ctx, node: prop }); + const name = prop.getName(); + const signature = this.typeHelper.resolveCallSignature(type, subpath({ ...ctx, node: prop }, name)); + const arg = /^\d+$/.test(name) ? parseInt(name, 10) : name; if (signature) { - map.set(prop.getName(), new Callable( + map.set(arg, new CallableDefinition( + ctx.resource, + `${ctx.path}.${name}`, this.typeHelper.resolveArguments(signature, prop), ...this.typeHelper.resolveReturnType(signature), prop, )); } else { - map.set(prop.getName(), this.typeHelper.resolveValueType(type, prop)); + map.set(arg, new OverrideDefinition( + ctx.resource, + `${ctx.path}.${name}`, + this.typeHelper.resolveValueType(type, prop), + )); } } @@ -223,7 +235,7 @@ export class ServicesExtension extends CompilerExtension { const pending = this.pendingAutoImplements.get(definition.type); - if (pending) { + if (pending && pending.factory.builder === definition.builder) { this.pendingAutoImplements.delete(definition.type); pending.factory.autoImplement = { @@ -232,7 +244,7 @@ export class ServicesExtension extends CompilerExtension { }; if (pending.method.name === 'create') { - definition.builder.services.remove(definition); + definition.builder.removeService(definition); } } @@ -255,7 +267,7 @@ export class ServicesExtension extends CompilerExtension { ? method.returnType.value.type : method.returnType.type; - const [service, nonUnique] = definition.builder.services.findByType(targetType); + const [service, nonUnique] = definition.builder.findByType(targetType); if (nonUnique) { return; @@ -267,7 +279,7 @@ export class ServicesExtension extends CompilerExtension { } if (method.name === 'create') { - definition.builder.services.remove(service); + definition.builder.removeService(service); } definition.autoImplement = { method, service }; @@ -276,5 +288,5 @@ export class ServicesExtension extends CompilerExtension { type PendingAutoImplement = { method: AutoImplementedMethod; - factory: ImplicitServiceDefinition; + factory: LocalServiceDefinition; }; diff --git a/core/cli/src/v2/extensions/types.ts b/core/cli/src/extensions/types.ts similarity index 95% rename from core/cli/src/v2/extensions/types.ts rename to core/cli/src/extensions/types.ts index 8e2dc3a..90d1148 100644 --- a/core/cli/src/v2/extensions/types.ts +++ b/core/cli/src/extensions/types.ts @@ -6,7 +6,8 @@ export type CompilerExtensionInjectSpecifier = | 'eventDispatcher' | 'moduleResolver' | 'referenceResolver' - | 'typeHelper'; + | 'typeHelper' + | 'logger'; export type CompilerExtensionReferences = { module: string; diff --git a/core/cli/src/referenceResolver.ts b/core/cli/src/referenceResolver.ts deleted file mode 100644 index f564840..0000000 --- a/core/cli/src/referenceResolver.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - ExportedDeclarations, - Node, - Project, - SyntaxKind, - Type, - TypeAliasDeclaration, - ts, -} from 'ts-morph'; -import { TypeHelper } from './typeHelper'; -import { ResolvedReference } from './types'; - -export class ReferenceResolver { - private readonly cache: Map = new Map(); - private readonly exports: ReadonlyMap; - - constructor( - private readonly project: Project, - private readonly typeHelper: TypeHelper, - private readonly moduleName: string, - ) { - this.exports = this.resolveModule(); - } - - get(name: string, kind: K): ResolvedReference { - const cached = this.cache.get(name); - - if (cached) { - return cached as ResolvedReference; - } - - const declarations = this.exports.get(name); - - if (!declarations) { - throw new Error(`Unable to resolve reference '${name}': module '${this.moduleName}' has no such export`); - } - - const node = declarations.find(Node.is(kind)); - - if (!node) { - throw new Error(`Unable to resolve reference '${name}': module '${this.moduleName}' has no export of the required kind`); - } - - const reference = node instanceof TypeAliasDeclaration - ? this.typeHelper.resolveRootType(node.getType()) - : node; - - this.cache.set(name, reference); - return reference as ResolvedReference; - } - - private resolveModule(): ReadonlyMap { - const fs = this.project.getFileSystem(); - - const result = ts.resolveModuleName( - this.moduleName, - `${fs.getCurrentDirectory()}/dummy.ts`, - this.project.getCompilerOptions(), - this.project.getModuleResolutionHost(), - ); - - if (!result.resolvedModule) { - throw new Error(`Unable to resolve module '${this.moduleName}'`); - } - - const file = this.project.addSourceFileAtPath(result.resolvedModule.resolvedFileName); - - return file.getExportedDeclarations(); - } -} diff --git a/core/cli/src/sourceFiles.ts b/core/cli/src/sourceFiles.ts deleted file mode 100644 index 0f26b74..0000000 --- a/core/cli/src/sourceFiles.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Logger } from '@debugr/core'; -import { Project, ScriptKind, SourceFile } from 'ts-morph'; -import { DiccConfig } from './types'; - -type ContainerFiles = { - inputs: Map; - output: SourceFile; -}; - -export class SourceFiles { - private readonly logger: Logger; - private readonly containers: Map = new Map(); - - constructor(project: Project, config: DiccConfig, logger: Logger) { - this.logger = logger; - - for (const [outputPath, options] of Object.entries(config.containers)) { - const inputs = new Map(Object.entries(options.resources).map(([resource, opts]) => [ - resource, - project.addSourceFilesAtPaths(createSourceGlobs(resource, opts?.exclude ?? [])), - ])); - - const output = project.createSourceFile(outputPath, createEmptyOutput(options.className), { - scriptKind: ScriptKind.TS, - overwrite: true, - }); - - this.containers.set(outputPath, { inputs, output }); - } - } - - getInputs(container: string, resource: string): SourceFile[] { - const inputs = this.getContainer(container).inputs.get(resource); - - if (!inputs) { - throw new Error(`Unknown resource: '${resource}'`); - } else if (!inputs.length) { - if (resource.includes('*')) { - this.logger.warning(`Resource '${resource}' didn't match any files`); - } else { - throw new Error(`Resource '${resource}' doesn't exist`); - } - } - - return inputs; - } - - getOutput(container: string): SourceFile { - return this.getContainer(container).output; - } - - private getContainer(container: string): ContainerFiles { - const files = this.containers.get(container); - - if (!files) { - throw new Error(`Unknown container: '${container}'`); - } - - return files; - } -} - -function createSourceGlobs(resource: string, exclude: string[]): string[] { - return [resource].concat(exclude.filter((p) => /(\/|\.tsx?$)/i.test(p)).map((e) => `!${e}`)); -} - -function createEmptyOutput(className: string): string { - const declaration = className === 'default' ? 'default class' : `class ${className}`; - return ` -import { Container } from 'dicc'; - -export ${declaration} extends Container<{}> { - constructor() { - super({}); - } -} -`; -} diff --git a/core/cli/src/typeHelper.ts b/core/cli/src/typeHelper.ts deleted file mode 100644 index a9928fa..0000000 --- a/core/cli/src/typeHelper.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { - CallExpression, - ClassDeclaration, - InterfaceDeclaration, - Node, - Project, - PropertyName, - Signature, - Symbol, - SyntaxKind, - Type, - TypeAliasDeclaration, - TypeNode, - TypeReferenceNode, -} from 'ts-morph'; -import { TypeError } from './errors'; -import { ReferenceResolver } from './referenceResolver'; -import { ArgumentInfo, NestedParameterInfo, TypeFlag } from './types'; - -export class TypeHelper { - private readonly refs: ReferenceResolver; - private containerType?: Type; - private containerParametersType?: Type; - private serviceMapSymbolType?: Type; - - constructor(private readonly project: Project) { - this.refs = this.createReferenceResolver('dicc/refs'); - } - - isServiceDefinition(node?: TypeNode): node is TypeReferenceNode { - return Node.isTypeReference(node) - && this.resolveRootType(node.getTypeName().getType()) === this.refs.get('ServiceDefinition', SyntaxKind.TypeAliasDeclaration); - } - - isServiceDecorator(node?: TypeNode): node is TypeReferenceNode { - return Node.isTypeReference(node) - && this.resolveRootType(node.getTypeName().getType()) === this.refs.get('ServiceDecorator', SyntaxKind.TypeAliasDeclaration); - } - - resolveLiteralPropertyName(name: PropertyName): string | number | undefined { - if (Node.isIdentifier(name)) { - return name.getText(); - } else if (Node.isStringLiteral(name) || Node.isNumericLiteral(name)) { - return name.getLiteralValue(); - } else { - return undefined; - } - } - - unwrapAsyncType(type: Type): [type: Type, async: boolean] { - const target = this.resolveRootType(type); - let async = false; - - if (target === this.refs.get('GlobalPromise', SyntaxKind.TypeAliasDeclaration)) { - async = true; - type = type.getTypeArguments()[0]; - } - - return [type, async]; - } - - resolveType(type: Type): [type: Type, flags: TypeFlag] { - const originalType = type; - let flags: TypeFlag = TypeFlag.None; - - [type, flags] = this.resolveNullable(type, flags); - - const signatures = type.getCallSignatures(); - - if (signatures.length === 1) { - const args = signatures[0].getParameters(); - const returnType = signatures[0].getReturnType(); - - if (args.length === 0) { - flags |= TypeFlag.Accessor; - type = returnType; - } else if (args.length === 1 && returnType.isVoid()) { - flags |= TypeFlag.Injector; - type = args[0].getValueDeclarationOrThrow().getType(); - } - } - - const target = this.resolveRootType(type); - - if (target === this.refs.get('GlobalPromise', SyntaxKind.TypeAliasDeclaration)) { - [type, flags] = this.resolveNullable(type.getTypeArguments()[0], flags | TypeFlag.Async); - } else if (target === this.refs.get('GlobalIterable', SyntaxKind.TypeAliasDeclaration)) { - flags |= TypeFlag.Iterable; - [type] = type.getTypeArguments(); - } else if (target === this.refs.get('GlobalAsyncIterable', SyntaxKind.TypeAliasDeclaration)) { - flags |= TypeFlag.Async | TypeFlag.Iterable; - [type] = type.getTypeArguments(); - } else if (this.isContainer(target)) { - flags |= TypeFlag.Container; - type = target; - } - - if (type.isArray()) { - flags |= TypeFlag.Array; - type = type.getArrayElementTypeOrThrow(); - } - - if ((flags & TypeFlag.Iterable) && (flags & (TypeFlag.Accessor | TypeFlag.Array))) { - throw new TypeError(`Iterable services are mutually exclusive with accessors and arrays`, originalType); - } else if ((flags & TypeFlag.Injector) && flags !== TypeFlag.Injector) { - throw new TypeError(`Injectors must accept a single resolved service instance`, originalType); - } else if ((flags & TypeFlag.Container) && flags !== TypeFlag.Container) { - throw new TypeError(`A dependency on the container can only be a direct dependency`, originalType); - } - - return [type, flags]; - } - - resolveAliases(aliases?: TypeNode): Type[] { - if (!aliases) { - return []; - } else if (Node.isUndefinedKeyword(aliases)) { - return []; - } else if (Node.isTupleTypeNode(aliases)) { - return aliases.getElements().map((el) => el.getType()); - } else { - return [aliases.getType()]; - } - } - - resolveNullable(type: Type, flags: TypeFlag): [type: Type, flags: TypeFlag] { - const nonNullable = type.getNonNullableType(); - return nonNullable !== type ? [nonNullable, flags | TypeFlag.Optional] : [type, flags]; - } - - resolveClassTypes(declaration: ClassDeclaration): Type[] { - const types: Set = new Set(); - let cursor: ClassDeclaration | undefined = declaration; - - while (cursor) { - for (const ifc of cursor.getImplements()) { - types.add(ifc.getType()); - - const impl = ifc.getExpression(); - - if (Node.isIdentifier(impl)) { - const parents = impl.getDefinitionNodes().flatMap((node) => - Node.isClassDeclaration(node) - ? this.resolveClassTypes(node) - : Node.isInterfaceDeclaration(node) - ? this.resolveInterfaceTypes(node) - : [] - ); - - for (const parent of parents) { - types.add(parent); - } - } - } - - const parent: ClassDeclaration | undefined = cursor.getBaseClass(); - parent && types.add(parent.getType()); - cursor = parent; - } - - return [...types]; - } - - resolveInterfaceTypes(declaration: InterfaceDeclaration): Type[] { - const types: Set = new Set(); - const queue: (ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration)[] = [declaration]; - let cursor: ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | undefined; - - while (cursor = queue.shift()) { - if (Node.isClassDeclaration(cursor)) { - for (const classType of this.resolveClassTypes(cursor)) { - types.add(classType); - } - } else if (Node.isInterfaceDeclaration(cursor)) { - for (const ifc of cursor.getBaseDeclarations()) { - types.add(ifc.getType()); - queue.push(ifc); - } - } else { - types.add(cursor.getType()); - } - } - - return [...types]; - } - - resolveFactorySignature(factory: Type): [signature: Signature, method?: string] { - const ctors = factory.getConstructSignatures(); - - if (!ctors.length) { - return [this.getFirstSignature(factory.getCallSignatures(), factory)]; - } - - const publicCtors = ctors.filter((ctor) => { - try { - const declaration = ctor.getDeclaration(); - - return Node.isConstructorDeclaration(declaration) - && !declaration.hasModifier(SyntaxKind.PrivateKeyword) - && !declaration.hasModifier(SyntaxKind.ProtectedKeyword) - } catch { - // This would happen if a class has no explicit constructor - - // in that case we'd get a construct signature, but no declaration. - return true; - } - }); - - if (!publicCtors.length) { - const cprop = factory.getProperty('create'); - const csig = cprop?.getTypeAtLocation(cprop.getValueDeclarationOrThrow()).getCallSignatures(); - return [this.getFirstSignature(csig ?? [], factory), 'create']; - } - - return [this.getFirstSignature(publicCtors, factory), 'constructor']; - } - - resolveAutoFactorySignature(type: Type): [signature?: Signature, method?: string] { - const props = type.getProperties(); - - if (!props.length) { - return [this.getFirstSignature(type.getCallSignatures(), type, false)]; - } - - const method = props.find((prop) => /^(get|create)$/.test(prop.getName())); - - if (!method) { - console.log(`NO GET/CREATE: ${type.getSymbol()?.getFullyQualifiedName()}`); - return []; - } - - const name = method.getName(); - const signature = this.getFirstSignature( - method.getTypeAtLocation(method.getDeclarations()[0]).getCallSignatures(), - type, - false, - ); - - if (!signature || (name === 'get' && signature.getParameters().length > 0)) { - return []; - } - - return [signature, name]; - } - - resolveArgumentInfo(symbol: Symbol): ArgumentInfo { - const name = symbol.getName(); - const declaration = symbol.getValueDeclarationOrThrow(); - let [type, flags] = this.resolveType(declaration.getType()); - - if (Node.isParameterDeclaration(declaration) && declaration.hasInitializer()) { - flags |= TypeFlag.Optional; - } - - return type.isClassOrInterface() || type.isObject() - ? { name, type, flags } - : { name, flags }; - } - - * resolveContainerPublicServices(type: Type): Iterable<[string, Type]> { - const declaration = type.getSymbol()?.getValueDeclaration(); - - if (!declaration) { - return; - } - - const prop = type.getProperty(findSymbolProp(this.getServiceMapSymbolType())); - const map = prop?.getTypeAtLocation(declaration).getNonNullableType(); - - if (!map) { - return; - } - - for (const prop of map.getProperties()) { - if (!prop.getName().startsWith('#')) { - yield [prop.getName(), prop.getTypeAtLocation(declaration)]; - } - } - } - - private getFirstSignature(signatures: Signature[], ctx: Type, need?: true): Signature; - private getFirstSignature(signatures: Signature[], ctx: Type, need: false): Signature | undefined; - private getFirstSignature([first, ...rest]: Signature[], ctx: Type, need?: boolean): Signature | undefined { - if (!first) { - if (need === false) { - return undefined; - } - - throw new TypeError(`No call or construct signatures found on service factory`, ctx); - } else if (rest.length) { - throw new TypeError(`Multiple overloads on service factories aren't supported`, ctx); - } - - return first; - } - - resolveRootType(type: Type): Type { - let target: Type | undefined; - - while ((target = type.getTargetType()) && target !== type) { - type = target; - } - - return target ?? type; - } - - createReferenceResolver(moduleName: string): ReferenceResolver { - return new ReferenceResolver(this.project, this, moduleName); - } - - * getContainerMethodCalls(methodName: string): Iterable { - const method = this.refs - .get('Container', SyntaxKind.ClassDeclaration) - .getInstanceMethodOrThrow(methodName); - - for (const r1 of method.findReferences()) { - for (const r2 of r1.getReferences()) { - if (!r2.isDefinition()) { - const call = r2.getNode().getFirstAncestorByKind(SyntaxKind.CallExpression); - - if (call) { - yield call; - } - } - } - } - } - - isContainer(type: Type): boolean { - if (!type.isClass()) { - return false; - } - - this.containerType ??= this.resolveRootType(this.refs.get('Container', SyntaxKind.ClassDeclaration).getType()); - const queue: Type[] = [type]; - - for (let t = queue.shift(); t !== undefined; t = queue.shift()) { - if (this.resolveRootType(t) === this.containerType) { - return true; - } - - queue.push(...t.getBaseTypes()); - } - - return false; - } - - resolveNestedContainerParameters(declaration: InterfaceDeclaration, type: Type, aliases: Type[]): Map | undefined { - this.containerParametersType ??= this.resolveRootType(this.refs.get('ContainerParameters', SyntaxKind.InterfaceDeclaration).getType()); - - if (type !== this.containerParametersType && !aliases.includes(this.containerParametersType)) { - return undefined; - } - - const map: Map = new Map(); - const queue: [type: Type, path: string, flags: TypeFlag][] = [[type, '', TypeFlag.None]]; - - while (queue.length) { - const [parent, path, flags] = queue.shift()!; - - for (const prop of parent.getProperties()) { - let [ptype, pflags] = this.resolveNullable(prop.getTypeAtLocation(declaration), flags); - const ppath = `${path}${prop.getName()}`; - - if (ptype.isArray()) { - ptype = ptype.getArrayElementTypeOrThrow(); - pflags |= TypeFlag.Array; - } - - if (!ptype.isAnonymous()) { - map.set(ptype, { - path: ppath, - flags: pflags, - }); - } - - if (!(pflags & TypeFlag.Array) && (ptype.isObject() || ptype.isClass() || ptype.isInterface())) { - queue.push([ptype, `${ppath}.`, pflags]); - } - } - } - - return map; - } - - private getServiceMapSymbolType(): Type { - return this.serviceMapSymbolType ??= this.refs.get('ServiceMap', SyntaxKind.VariableDeclaration).getType(); - } -} - -function findSymbolProp(symbol: Type) { - return (prop: Symbol) => { - const d = prop.getValueDeclaration(); - - if (!Node.isPropertyDeclaration(d)) { - return false; - } - - const n = d.getNameNode(); - - if (!Node.isComputedPropertyName(n)) { - return false; - } - - return n.getExpression().getType() === symbol; - }; -} diff --git a/core/cli/src/types.ts b/core/cli/src/types.ts deleted file mode 100644 index fa9c039..0000000 --- a/core/cli/src/types.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { ServiceScope } from 'dicc'; -import { KindToNodeMappings, SourceFile, SyntaxKind, Type } from 'ts-morph'; -import { z } from 'zod'; - -const resourceSchema = z.strictObject({ - exclude: z.array(z.string()).optional(), -}); - -const containerConfigSchema = z.strictObject({ - preamble: z.string().optional(), - className: z.string().regex(/^[a-z$_][a-z0-9$_]*$/i, 'Invalid identifier').default('AppContainer'), - typeCheck: z.boolean().default(true), - resources: z.record(resourceSchema.optional().nullable()), -}); - -export const diccConfigSchema = z.strictObject({ - project: z.string().default('./tsconfig.json'), - containers: z.record(containerConfigSchema), -}); - -type ResourceConfigSchema = z.infer; -type ContainerConfigSchema = z.infer; -type DiccConfigSchema = z.infer; - -export interface ResourceOptions extends ResourceConfigSchema {} -export interface ContainerOptions extends ContainerConfigSchema {} -export interface DiccConfig extends DiccConfigSchema {} - -export type ServiceRegistrationInfo = { - source: SourceFile; - path: string; - id?: string; - type: Type; - aliases: Type[]; - object?: boolean; - explicit?: boolean; - anonymous?: boolean; - container?: boolean; - parent?: string; - factory?: ServiceFactoryInfo; - args?: ArgumentOverrideMap; - hooks: ServiceHooks; - scope?: ServiceScope; -}; - -export type AutoFactoryTarget = { - method?: string; - manualArgs: string[]; - async?: boolean; - source: SourceFile; - path: string; - type: Type; - object?: boolean; - explicit?: boolean; - factory: ServiceFactoryInfo; - args?: ArgumentOverrideMap; -}; - -export type ServiceDefinitionInfo = Omit & { - id: string; - async?: boolean; - creates?: AutoFactoryTarget; - decorators: ServiceDecoratorInfo[]; -}; - -export type ServiceDecoratorInfo = { - source: SourceFile; - path: string; - type: Type; - priority: number; - decorate?: CallbackInfo; - hooks: ServiceHooks; - scope?: ServiceScope; -}; - -export type ServiceFactoryInfo = { - args: ArgumentInfo[]; - returnType: Type; - method?: string; - async?: boolean; -}; - -export type ServiceHooks = { - onCreate?: CallbackInfo; - onFork?: CallbackInfo; - onDestroy?: CallbackInfo; -}; - -export type CallbackInfo = { - args: ArgumentInfo[]; - async?: boolean; -}; - -export type ArgumentOverrideMap = { - [arg: string]: CallbackInfo | string | undefined; -}; - -export type ArgumentInfo = { - name: string; - type?: Type; - flags: TypeFlag; -}; - -export enum TypeFlag { - None = 0b0000000, - Optional = 0b0000001, - Array = 0b0000010, - Iterable = 0b0000100, - Async = 0b0001000, - Accessor = 0b0010000, - Injector = 0b0100000, - Container = 0b1000000, -} - -export type ContainerParametersInfo = { - source: SourceFile; - path: string; - type: Type; - nestedTypes: Map; -}; - -export type NestedParameterInfo = { - path: string; - flags: TypeFlag; -}; - -export type ResolvedReference = - K extends SyntaxKind.TypeAliasDeclaration - ? Type - : KindToNodeMappings[K]; diff --git a/core/cli/src/utils/di.ts b/core/cli/src/utils/di.ts new file mode 100644 index 0000000..86446ac --- /dev/null +++ b/core/cli/src/utils/di.ts @@ -0,0 +1,3 @@ +export { ModuleResolver } from './moduleResolver'; +export { ReferenceResolver, ReferenceResolverFactory } from './referenceResolver'; +export { TypeHelper } from './typeHelper'; diff --git a/core/cli/src/v2/utils/helpers.ts b/core/cli/src/utils/helpers.ts similarity index 63% rename from core/cli/src/v2/utils/helpers.ts rename to core/cli/src/utils/helpers.ts index 6ac8e36..a340026 100644 --- a/core/cli/src/v2/utils/helpers.ts +++ b/core/cli/src/utils/helpers.ts @@ -10,6 +10,29 @@ export function getOrCreate(map: Map, key: K, factory: () => V): V { return value; } +export function allocateInSet(set: Set, format: string, cb?: (key: string) => void): string { + for (let i = 0; ; ++i) { + const value = format.replace(/\{i}/, i.toString()); + + if (!set.has(value)) { + set.add(value); + cb && cb(value); + return value; + } + } +} + +export function allocateInMap(map: Map, format: string, value: V): string { + for (let i = 0; ; ++i) { + const key = format.replace(/\{i}/, i.toString()); + + if (!map.has(key)) { + map.set(key, value); + return key; + } + } +} + export function getFirst(items: Iterable): T { const [first] = items; return first; @@ -55,6 +78,16 @@ export function filterMap(map: Map, predicate: (v: V, k: K) => boole return new Map([...map].filter(([k, v]) => predicate(v, k))); } +export type MapEntry = { k: K, v: V }; + +export function sortMap(map: Map, callback: (a: MapEntry, b: MapEntry) => number): Map { + return new Map([...map].sort(([ak, av], [bk, bv]) => callback({ k: ak, v: av }, { k: bk, v: bv }))); +} + +export function mergeMaps(a: Map, b: Map): Map { + return new Map([...a, ...b]); +} + export function find(items: Iterable, cb: (value: T) => boolean): T | undefined { for (const item of items) { if (cb(item)) { diff --git a/core/cli/src/v2/utils/index.ts b/core/cli/src/utils/index.ts similarity index 100% rename from core/cli/src/v2/utils/index.ts rename to core/cli/src/utils/index.ts diff --git a/core/cli/src/v2/utils/moduleResolver.ts b/core/cli/src/utils/moduleResolver.ts similarity index 100% rename from core/cli/src/v2/utils/moduleResolver.ts rename to core/cli/src/utils/moduleResolver.ts diff --git a/core/cli/src/v2/utils/referenceResolver.ts b/core/cli/src/utils/referenceResolver.ts similarity index 89% rename from core/cli/src/v2/utils/referenceResolver.ts rename to core/cli/src/utils/referenceResolver.ts index 7b39e23..276fa45 100644 --- a/core/cli/src/v2/utils/referenceResolver.ts +++ b/core/cli/src/utils/referenceResolver.ts @@ -17,8 +17,14 @@ export type ResolvedMap = { [N in keyof M]: ResolvedReference; }; -export interface ReferenceResolverFactory { - create(moduleName: string, map: M, resolveFrom?: string): ReferenceResolver; +export class ReferenceResolverFactory { + constructor( + private readonly moduleResolver: ModuleResolver, + ) {} + + create(moduleName: string, map: M, resolveFrom?: string): ReferenceResolver { + return new ReferenceResolver(this.moduleResolver, moduleName, map, resolveFrom); + } } export class ReferenceResolver { diff --git a/core/cli/src/v2/utils/typeHelper.ts b/core/cli/src/utils/typeHelper.ts similarity index 83% rename from core/cli/src/v2/utils/typeHelper.ts rename to core/cli/src/utils/typeHelper.ts index c0eff84..0dd4b4d 100644 --- a/core/cli/src/v2/utils/typeHelper.ts +++ b/core/cli/src/utils/typeHelper.ts @@ -8,14 +8,14 @@ import { Signature, Symbol, SyntaxKind, - Type, TypeAliasDeclaration, + Type, + TypeAliasDeclaration, } from 'ts-morph'; -import { ForeignServiceReflection } from '../container'; import { AccessorType, - Argument, + ArgumentDefinition, AutoImplementedMethod, - Callable, + CallableDefinition, FactoryDefinition, InjectorType, IterableType, @@ -180,6 +180,8 @@ export class TypeHelper { } return new FactoryDefinition( + ctx.resource, + ctx.path, this.resolveArguments(signature, ctx.node), ...this.resolveReturnType(signature), ctx.node, @@ -212,13 +214,15 @@ export class TypeHelper { return createSignature ? [createSignature, 'create'] : []; } - resolveCallable(type: Type, ctx: UserCodeContext): Callable { + resolveCallable(type: Type, ctx: UserCodeContext): CallableDefinition { const signature = throwIfUndef( this.resolveCallSignature(type, ctx), () => new DefinitionError(`Unable to resolve call signature`, ctx), ); - return new Callable( + return new CallableDefinition( + ctx.resource, + ctx.path, this.resolveArguments(signature, ctx.node), ...this.resolveReturnType(signature), ctx.node, @@ -229,9 +233,9 @@ export class TypeHelper { object: ObjectLiteralExpression, property: string, ctx: UserCodeContext, - ): Callable | undefined { + ): CallableDefinition | undefined { const node = object.getProperty(property); - return node && this.resolveCallable(node.getType(), { ...ctx, node }); + return node && this.resolveCallable(node.getType(), { ...ctx, path: `${ctx.path}.${property}`, node }); } resolveCallSignature(type: Type, ctx: UserCodeContext): Signature | undefined { @@ -267,8 +271,8 @@ export class TypeHelper { } } - resolveArguments(signature: Signature, ctx?: Node): Map { - const args: Map = new Map(); + resolveArguments(signature: Signature, ctx?: Node): Map { + const args: Map = new Map(); ctx ??= signature.getDeclaration(); for (const arg of signature.getParameters()) { @@ -276,7 +280,7 @@ export class TypeHelper { const type = arg.getTypeAtLocation(node); const declaration = node.asKind(SyntaxKind.Parameter); - args.set(arg.getName(), new Argument( + args.set(arg.getName(), new ArgumentDefinition( type, this.resolveValueType(type, node), declaration?.isOptional() ?? false, @@ -294,19 +298,19 @@ export class TypeHelper { if (this.refs.isType(type, 'ScopedRunner')) { return new ScopedRunnerType(nullable); } else if (this.isIterable(type)) { - return new IterableType(rawType, getFirst(type.getTypeArguments()), nullable); + return new IterableType(type, this.resolveSingleType(getFirst(type.getTypeArguments())), nullable); } else if (this.isAsyncIterable(rawType)) { - return new IterableType(rawType, getFirst(type.getTypeArguments()), nullable, true); + return new IterableType(type, this.resolveSingleType(getFirst(type.getTypeArguments())), nullable, true); } else if (this.isPromise(type)) { - return this.resolvePromiseType(rawType); + return this.resolvePromiseType(type, nullable); } else { - return this.resolveListType(rawType) - ?? this.resolveCallableType(rawType, node) - ?? this.resolveSingleType(rawType); + return this.resolveListType(type, nullable) + ?? this.resolveCallableType(type, nullable, node) + ?? this.resolveSingleType(type, nullable); } } - private resolveCallableType(type: Type, node: Node): ValueType | undefined { + private resolveCallableType(type: Type, nullable: boolean, node: Node): ValueType | undefined { const signature = getFirstIfOnly(type.getCallSignatures()); if (!signature) { @@ -319,14 +323,21 @@ export class TypeHelper { return undefined; } - let returnType = signature.getReturnType(); + const rawRtnType = signature.getReturnType(); + const [rtnType, rtnNullable] = this.unwrapNullable(rawRtnType); if (p1) { - return returnType.isVoid() ? new InjectorType(type, p1.getTypeAtLocation(node)) : undefined; - } else if (this.isPromise(returnType)) { - return new AccessorType(type, this.resolvePromiseType(returnType)); + return rawRtnType.isVoid() + ? new InjectorType(type, p1.getTypeAtLocation(node).getNonNullableType(), nullable) + : undefined; + } else if (this.isPromise(rtnType)) { + return new AccessorType(type, this.resolvePromiseType(rtnType, rtnNullable), nullable); } else { - return new AccessorType(type, this.resolveListType(returnType) ?? this.resolveSingleType(type)); + return new AccessorType( + type, + this.resolveListType(rtnType, rtnNullable) ?? this.resolveSingleType(rtnType, rtnNullable), + nullable, + ); } } @@ -345,12 +356,12 @@ export class TypeHelper { const [type, nullable] = this.unwrapNullable(rawType, nullable_); return type.isArray() || type.isReadonlyArray() - ? new ListType(rawType, type.getArrayElementType()!, nullable) + ? new ListType(rawType, this.resolveSingleType(type.getArrayElementType()!), nullable) : undefined; } private resolveSingleType(rawType: Type, nullable?: boolean): SingleType { - return new SingleType(rawType, ...this.unwrapNullable(rawType, nullable)); + return new SingleType(...this.unwrapNullable(rawType, nullable)); } resolveReturnType(signature: Signature): [Type, ReturnType | undefined] { @@ -363,9 +374,9 @@ export class TypeHelper { let [type, nullable] = this.unwrapNullable(raw); if (this.isIterable(type)) { - return [raw, new IterableType(raw, getFirst(type.getTypeArguments()), nullable)]; - } else if (this.isAsyncIterable(raw)) { - return [raw, new IterableType(raw, getFirst(type.getTypeArguments()), nullable, true)]; + return [raw, new IterableType(type, this.resolveSingleType(getFirst(type.getTypeArguments())), nullable)]; + } else if (this.isAsyncIterable(type)) { + return [raw, new IterableType(type, this.resolveSingleType(getFirst(type.getTypeArguments())), nullable, true)]; } else if (this.isPromise(type)) { return [raw, this.resolvePromiseType(type, nullable)]; } @@ -402,7 +413,7 @@ export class TypeHelper { return undefined; } - return new AutoImplementedMethod(name, args, rawReturnType, returnType, method); + return new AutoImplementedMethod(ctx.resource, ctx.path, name, args, rawReturnType, returnType, method); } private resolveFactoryMethodCandidates( @@ -420,7 +431,7 @@ export class TypeHelper { * resolveExternalContainerServices( container: Type, map: 'public' | 'dynamic', - ): Iterable { + ): Iterable<[id: string, type: Type, aliases: Iterable, async: boolean]> { const declaration = container.getSymbol()?.getValueDeclaration(); if (!declaration) { @@ -439,7 +450,7 @@ export class TypeHelper { private * resolveExternalContainerServiceMap( map: Type, declaration: Node, - ): Iterable { + ): Iterable<[id: string, type: Type, aliases: Iterable, async: boolean]> { for (const prop of map.getProperties()) { let type = prop.getTypeAtLocation(declaration).getNonNullableType(); let aliases: Type[] = []; @@ -454,7 +465,11 @@ export class TypeHelper { [type, ...aliases] = type.getIntersectionTypes(); } - yield { id: prop.getName(), type, aliases, async }; + if (!aliases.length) { + aliases = this.resolveAliases(type); + } + + yield [prop.getName(), type, aliases, async]; } } } diff --git a/core/cli/src/v2/utils/types.ts b/core/cli/src/utils/types.ts similarity index 100% rename from core/cli/src/v2/utils/types.ts rename to core/cli/src/utils/types.ts diff --git a/core/cli/src/v2/compiler/autowiring.ts b/core/cli/src/v2/compiler/autowiring.ts deleted file mode 100644 index 2789b13..0000000 --- a/core/cli/src/v2/compiler/autowiring.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { ServiceScope } from 'dicc'; -import { ContainerBuilder, ContainerReflector } from '../container'; -import { - AccessorType, - Argument, - ArgumentOverride, - Callable, - DecoratorDefinition, - ExplicitServiceDefinition, - ForeignServiceDefinition, - InjectorType, - IterableType, - ListType, - LocalServiceDefinition, - PromiseType, - ScopedRunnerType, - ServiceDefinition, -} from '../definitions'; -import { - AutowiringError, - ContainerContext, - CyclicDependencyError, - DefinitionError, - InternalError, -} from '../errors'; -import { filterMap, getFirst, getOrCreate, skip } from '../utils'; - -export class Autowiring { - private readonly visitedContainers: Set = new Set(); - private readonly visitedServices: Set = new Set(); - private readonly visitedDecorators: Map> = new Map(); - private readonly resolving: ServiceDefinition[] = []; - - constructor( - private readonly reflector: ContainerReflector, - ) {} - - checkDependencies(containers: Iterable): void { - const reiterable = new Set(containers); - - this.mergeForeignContainers(reiterable); - - for (const builder of reiterable) { - this.checkContainer(builder); - } - } - - private mergeForeignContainers(containers: Iterable): void { - for (const builder of containers) { - for (const child of builder.services) { - if (child.isLocal() && child.container) { - this.mergeForeignServices(builder, child); - } - } - } - } - - private mergeForeignServices(builder: ContainerBuilder, definition: LocalServiceDefinition): void { - const container = this.reflector.getContainerReflection(definition.type); - - for (const { id, type, aliases, async } of container.getPublicServices()) { - builder.services.addForeignDefinition(builder, definition, id, type, aliases, async); - } - } - - private checkContainer(builder: ContainerBuilder): void { - if (!this.visitContainer(builder)) { - return; - } - - const ctx: ContainerContext = { builder }; - - for (const definition of builder.services) { - this.checkServiceDependencies({ ...ctx, definition }, definition); - } - - // needs to run after all dependencies have been fully resolved - for (const definition of builder.services) { - this.checkCyclicDependencies(builder, definition); - } - } - - private checkServiceDependencies(ctx: ContainerContext, definition: ServiceDefinition): void { - if (!this.visitService(definition)) { - return; - } - - if (definition.isLocal()) { - definition.decorators ??= ctx.builder.decorators.decorate(definition); - } - - const scope = this.resolveScope(definition); - - if (definition.isLocal()) { - this.checkLocalServiceDependencies(ctx, definition, scope); - } else { - this.checkForeignServiceDependencies(ctx, definition); - return; - } - - const flags = this.checkDecorators(ctx, definition.decorators!, scope); - - if (flags.asyncDecorate && definition.factory) { - definition.factory.async = true; - } - - if (flags.asyncDecorate || flags.asyncOnCreate) { - definition.async = true; - } - } - - private checkLocalServiceDependencies(ctx: ContainerContext, definition: LocalServiceDefinition, scope: ServiceScope): void { - if (definition.factory) { - const overrides = definition.isExplicit() ? definition.args : undefined; - // todo check for extraneous overrides - - if (this.checkArguments({ ...ctx, method: 'factory' }, definition.factory.args, scope, overrides)) { - definition.factory.async = true; - } - - if (definition.factory.returnType instanceof PromiseType) { - definition.factory.async = true; - } - - if (definition.factory.async) { - definition.async = true; - } - } - - if (definition.autoImplement) { - const { service: target, method } = definition.autoImplement; - - // todo: - // tady to vyjebat do metody; - // pokud je to auto-getter, neres (je to totez jako accessor); - // pokud je to auto-factory, mozna pouzij check service deps..? - - if (target.factory) { - for (const [name, arg] of method.args) { - const dst = target.factory.args.get(name); - - if (!dst || !arg.rawType.isAssignableTo(dst.rawType)) { - // todo behave better towards async values we might be able to await - const msg = dst - ? `doesn't exist in target factory` - : `is not assignable to target factory argument of the same name`; - - throw new DefinitionError(`Manual argument of auto-implemented method ${msg}`, { - builder: ctx.builder, - resource: definition.resource, - path: definition.path, - node: definition.autoImplement.method.node, - }); - } - } - - const injectedArgs = filterMap(target.factory.args, (_, name) => !method.args.has(name)); - const overrides = target.isExplicit() ? target.args : undefined; - - if (this.checkArguments({ ...ctx, method: 'auto-implement' }, injectedArgs, scope, overrides) && !target.async) { - throw new AutowiringError(`Auto-implemented method must return a Promise because target has async dependencies`, ctx); - } - } - } - - if (definition.container && this.checkChildContainerDependencies(ctx.builder, definition)) { - definition.factory && (definition.factory.async = true); - definition.async = true; - } - - if (definition.isExplicit() && this.checkHooks(ctx, definition, scope)) { - definition.async = true; - } - } - - private checkChildContainerDependencies(builder: ContainerBuilder, definition: LocalServiceDefinition): boolean { - definition.autoRegister ??= new Map(this.resolveChildDynamicServices(builder, definition)); - - let async = false; - - for (const injectable of definition.autoRegister.values()) { - this.checkServiceDependencies({ builder, definition: injectable }, injectable); - - if (injectable.async) { - async = true; - } - } - - return async; - } - - private checkForeignServiceDependencies(ctx: ContainerContext, definition: ForeignServiceDefinition): void { - this.checkServiceDependencies({ builder: ctx.builder, definition: definition.container }, definition.container); - - if (definition.container.async) { - definition.async = true; - } - - const container = this.reflector.getContainerReflection(definition.container.type); - const builder = container.getBuilder(); - - if (builder) { - this.checkContainer(builder); - } - - const reflection = container.getPublicServiceById(definition.foreignId); - - if (reflection?.async) { - definition.factory.async = true; - } - } - - private checkHooks( - ctx: ContainerContext, - definition: ExplicitServiceDefinition | DecoratorDefinition, - scope: ServiceScope, - ): boolean { - for (const name of ['onCreate', 'onFork', 'onDestroy'] as const) { - const hook = definition[name]; - - if (!hook) { - continue; - } - - const args = new Map(skip(name === 'onFork' ? 2 : 1, hook.args)); - - if (this.checkArguments({ ...ctx, method: name }, args, scope)) { - hook.async = true; - } - - if (hook.returnType instanceof PromiseType) { - hook.async = true; - } - } - - return definition.onCreate?.async ?? false; - } - - private checkDecorators( - ctx: ContainerContext, - decorators: DecoratorDefinition[], - scope: ServiceScope, - ): DecoratorFlags { - const flags: DecoratorFlags = {}; - - for (const decorator of decorators) { - this.checkDecorator({ ...ctx, definition: decorator }, decorator, scope, flags); - } - - return flags; - } - - private checkDecorator( - ctx: ContainerContext, - decorator: DecoratorDefinition, - scope: ServiceScope, - flags: DecoratorFlags, - ): void { - if (!this.visitDecorator(decorator, scope)) { - decorator.decorate?.async && (flags.asyncDecorate = true); - decorator.onCreate?.async && (flags.asyncOnCreate = true); - return; - } - - if (decorator.decorate) { - const args = new Map(skip(1, decorator.decorate.args)); - - if (this.checkArguments({ ...ctx, method: 'decorate' }, args, scope)) { - decorator.decorate.async = true; - } - - if (decorator.decorate.returnType instanceof PromiseType) { - decorator.decorate.async = true; - } - - if (decorator.decorate.async) { - flags.asyncDecorate = true; - } - } - - if (this.checkHooks(ctx, decorator, scope)) { - flags.asyncOnCreate = true; - } - } - - private checkArguments( - ctx: ContainerContext, - args: Map, - scope: ServiceScope, - overrides?: Map, - ): boolean { - let async = false; - - for (const [name, arg] of args) { - const override = overrides?.get(name); - - if (override) { - if (override instanceof Callable) { - if (this.checkArguments({ ...ctx, method: 'override', argument: name }, override.args, scope)) { - async = true; - } - } else { - // todo check assignable & async - } - } else if (this.checkInjectableArgument({ ...ctx, argument: name }, arg, scope)) { - async = true; - } - } - - return async; - } - - private checkInjectableArgument(ctx: ContainerContext, arg: Argument, scope: ServiceScope): boolean { - if (arg.type instanceof ScopedRunnerType) { - return false; - } - - for (const serviceType of arg.type.getInjectableTypes()) { - const candidates = ctx.builder.services.findByType(serviceType); - - if (!candidates.size) { - continue; - } - - if (candidates.size > 1 && !(arg.type instanceof ListType || arg.type instanceof IterableType)) { - throw new AutowiringError('Multiple services of matching type found', ctx); - } - - if (arg.type instanceof InjectorType) { - return this.checkInjector(ctx, getFirst(candidates)); - } - - return this.checkInjectionCandidates(ctx, arg, scope, candidates); - } - - if (arg.optional || arg.type.nullable) { - return false; - } - - throw new AutowiringError( - arg.type instanceof InjectorType - ? 'Unknown service type in injector' - : 'Unable to autowire non-optional argument', - ctx, - ); - } - - private checkInjector(ctx: ContainerContext, candidate: ServiceDefinition): boolean { - if (candidate.isForeign()) { - throw new AutowiringError('Cannot inject injector for a service from a foreign container', ctx); - } else if (candidate.scope === 'private') { - throw new AutowiringError(`Cannot inject injector for privately-scoped service '${candidate.path}'`, ctx); - } - - ctx.builder.types.add(candidate.type); - return false; - } - - private checkInjectionCandidates(ctx: ContainerContext, arg: Argument, scope: ServiceScope, candidates: Iterable): boolean { - let async = false; - - for (const candidate of candidates) { - this.checkServiceDependencies({ builder: ctx.builder, definition: candidate }, candidate); - - if (candidate.isLocal() && scope === 'global' && candidate.scope === 'local' && !(arg.type instanceof AccessorType)) { - throw new AutowiringError( - `Cannot inject locally-scoped dependency '${candidate.path}' into globally-scoped service`, - ctx, - ); - } - - if (candidate.async && !(arg.type instanceof PromiseType)) { - if (arg.type instanceof AccessorType || arg.type instanceof IterableType) { - const desc = candidate.isLocal() - ? `dependency '${candidate.path}'` - : `foreign dependency '${candidate.foreignId}'`; - - throw new AutowiringError( - `Cannot inject async ${desc} into synchronous accessor or iterable argument`, - ctx, - ); - } - - async = true; - } - - ctx.builder.types.add(candidate.type); - } - - return async; - } - - private resolveScope(definition: ServiceDefinition): ServiceScope { - if (!definition.isLocal()) { - return 'global'; - } - - const decoratorWithScope = definition.decorators?.findLast((decorator) => decorator.scope !== undefined); - return decoratorWithScope?.scope ?? definition.scope; - } - - private * resolveChildDynamicServices( - builder: ContainerBuilder, - definition: LocalServiceDefinition, - ): Iterable<[foreignId: string, localDefinition: ServiceDefinition]> { - const container = this.reflector.getContainerReflection(definition.type); - - for (const service of container.getDynamicServices()) { - const [candidate, more] = builder.services.findByAnyType(service.type, ...service.aliases); - - if (more) { - throw new AutowiringError( - `Multiple services can be autowired into merged container's dynamic service`, - { builder, definition }, - ); - } else if (candidate) { - yield [service.id, candidate]; - } - } - } - - private checkCyclicDependencies(builder: ContainerBuilder, definition: ServiceDefinition): void { - this.checkCyclicDependency(definition); - - if (definition.isForeign()) { - this.checkCyclicDependencies(builder, definition.container); - } else { - if (definition.factory) { - for (const arg of definition.factory.args.values()) { - this.checkArgumentDependencies(builder, arg); - } - } - - if (definition.isExplicit() && definition.onCreate) { - for (const arg of skip(1, definition.onCreate.args.values())) { - this.checkArgumentDependencies(builder, arg); - } - } - - if (definition.autoRegister) { - for (const injectable of definition.autoRegister.values()) { - this.checkCyclicDependencies(builder, injectable); - } - } - - if (definition.decorators) { - for (const arg of definition.decorators.flatMap(getDecoratorArguments)) { - this.checkArgumentDependencies(builder, arg); - } - } - } - - this.releaseCyclicDependencyCheck(definition); - } - - private checkArgumentDependencies(builder: ContainerBuilder, arg: Argument): void { - if ( - arg.type instanceof PromiseType - || arg.type instanceof AccessorType - || arg.type instanceof IterableType - || arg.type instanceof InjectorType - || arg.type instanceof ScopedRunnerType - ) { - return; - } - - for (const candidate of builder.services.findByType(arg.type.type)) { - this.checkCyclicDependencies(builder, candidate); - } - } - - private checkCyclicDependency(definition: ServiceDefinition): void { - const idx = this.resolving.indexOf(definition); - - if (idx > -1) { - throw new CyclicDependencyError(...this.resolving.slice(idx), definition); - } - - this.resolving.push(definition); - } - - private releaseCyclicDependencyCheck(definition: ServiceDefinition): void { - const last = this.resolving.pop(); - - if (last !== definition) { - throw new InternalError(`Dependency chain checker broken`); - } - } - - private visitContainer(builder: ContainerBuilder): boolean { - if (this.visitedContainers.has(builder)) { - return false; - } - - this.visitedContainers.add(builder); - return true; - } - - private visitService(definition: ServiceDefinition): boolean { - if (this.visitedServices.has(definition)) { - return false; - } - - this.visitedServices.add(definition); - return true; - } - - private visitDecorator(decorator: DecoratorDefinition, scope: ServiceScope): boolean { - const scopes = getOrCreate(this.visitedDecorators, decorator, () => new Set()); - - if (scopes.has(scope)) { - return false; - } - - scopes.add(scope); - return true; - } -} - -type DecoratorFlags = { - asyncDecorate?: boolean; - asyncOnCreate?: boolean; -}; - -function getDecoratorArguments(decorator: DecoratorDefinition): Argument[] { - return [ - ...skip(1, decorator.decorate?.args.values() ?? []), - ...skip(1, decorator.onCreate?.args.values() ?? []), - ]; -} diff --git a/core/cli/src/v2/compiler/compilation.ts b/core/cli/src/v2/compiler/compilation.ts deleted file mode 100644 index 911b487..0000000 --- a/core/cli/src/v2/compiler/compilation.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Project, ScriptKind, Type } from 'ts-morph'; -import { CompilerConfig, ContainerOptions } from '../config'; -import { ContainerBuilder, ContainerBuilderFactory } from '../container'; - -export class Compilation { - public readonly containers: Map = new Map(); - private readonly types: Map = new Map(); - - constructor( - public readonly config: CompilerConfig, - builderFactory: ContainerBuilderFactory, - project: Project, - ) { - for (const [path, options] of Object.entries(this.config.containers)) { - const sourceFile = project.createSourceFile(path, createEmptyOutput(options.className), { - scriptKind: ScriptKind.TS, - overwrite: true, - }); - - this.containers.set(builderFactory.create(sourceFile, options.className, options.lazyImports), options); - } - - for (const container of this.containers.keys()) { - this.types.set(container.sourceFile.getClassOrThrow(container.className).getType(), container); - } - } - - getContainerByType(type: Type): ContainerBuilder | undefined { - return this.types.get(type); - } -} - -function createEmptyOutput(className: string): string { - const declaration = className === 'default' ? 'default class' : `class ${className}`; - return ` -import { Container } from 'dicc'; - -export ${declaration} extends Container { - constructor() { - super({}); - } -} -`; -} diff --git a/core/cli/src/v2/compiler/compiler.ts b/core/cli/src/v2/compiler/compiler.ts deleted file mode 100644 index bde0ab1..0000000 --- a/core/cli/src/v2/compiler/compiler.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { CompilerConfig } from '../config'; -import { CompilerExtension, ExtensionLoader } from '../extensions'; -import { Autowiring } from './autowiring'; -import { Compilation } from './compilation'; -import { ContainerCompilerFactory } from './containerCompiler'; -import { ResourceScanner } from './resourceScanner'; - -export class Compiler { - private readonly extensions: Set; - - constructor( - private readonly extensionLoader: ExtensionLoader, - private readonly resourceScanner: ResourceScanner, - private readonly autowiring: Autowiring, - private readonly containerCompilerFactory: ContainerCompilerFactory, - extensions: Iterable, - private readonly config: CompilerConfig, - private readonly compilation: Compilation, - ) { - this.extensions = new Set(extensions); - } - - async loadExtensions(): Promise { - for await (const extension of this.extensionLoader.load(this.config.extensions, this.config.configFile)) { - this.extensions.add(extension); - } - } - - loadResources(): void { - for (const [builder, config] of this.compilation.containers) { - for (const [resource, options] of Object.entries(config.resources)) { - this.resourceScanner.enqueueResources( - builder, - createResourceGlobs(resource, options?.excludePaths ?? []), - options?.excludeExports ?? [], - ); - } - - for (const extension of this.extensions) { - extension.loadResources(builder, (resources, excludeExports, resolveFrom) => { - this.resourceScanner.enqueueResources(builder, resources, excludeExports, resolveFrom); - }); - } - } - - this.resourceScanner.scanEnqueuedResources(); - } - - autowireDependencies(): void { - for (const builder of this.compilation.containers.keys()) { - for (const extension of this.extensions) { - extension.autowireDependencies(builder); - } - } - - this.autowiring.checkDependencies(this.compilation.containers.keys()); - } - - compile(): string { - for (const builder of this.compilation.containers.keys()) { - return this.containerCompilerFactory.create(builder).compile(); - } - - return ''; - } -} - -function createResourceGlobs(resource: string, exclude: string[]): string[] { - return [resource, ...exclude.filter((p) => /(\/|\.tsx?$)/i.test(p)).map((e) => `!${e}`)]; -} diff --git a/core/cli/src/v2/compiler/containerCompiler.ts b/core/cli/src/v2/compiler/containerCompiler.ts deleted file mode 100644 index 6c8d1ee..0000000 --- a/core/cli/src/v2/compiler/containerCompiler.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Type } from 'ts-morph'; -import { ContainerBuilder, ImportMode } from '../container'; -import { DecoratorDefinition, LocalServiceDefinition, ServiceDefinition } from '../definitions'; -import { find, mapSet } from '../utils'; -import { ForeignServiceCompilerFactory } from './foreignServiceCompiler'; -import { ImportMapCompiler } from './importMapCompiler'; -import { $lazy as $, Lazy, LazyCompiler, LazyWriter } from './lazy'; -import { LocalServiceCompilerFactory } from './localServiceCompiler'; -import { TypeMapCompiler } from './typeMapCompiler'; - -export interface ContainerCompilerFactory { - create(builder: ContainerBuilder): ContainerCompiler; -} - -export class ContainerCompiler { - private readonly compiledDefinitions: Set = new Set(); - - constructor( - private readonly localServiceCompilerFactory: LocalServiceCompilerFactory, - private readonly foreignServiceCompilerFactory: ForeignServiceCompilerFactory, - private readonly typeMapCompiler: TypeMapCompiler, - private readonly importMapCompiler: ImportMapCompiler, - private readonly lazyCompiler: LazyCompiler, - private readonly builder: ContainerBuilder, - ) {} - - compile(): string { - const definitions = this.compileDefinitions(); - const containerClass = this.compileClass(definitions); - const typeMaps = this.typeMapCompiler.compile(this.builder.types, this.builder.imports, this.compiledDefinitions); - const imports = this.importMapCompiler.compile(this.builder); - - return this.lazyCompiler.compile($`${imports}\n${typeMaps}\n${containerClass}`); - } - - private compileDefinitions(): Lazy[] { - for (const definition of this.builder.services.getPublicServices()) { - this.compiledDefinitions.add(definition); - definition.source ??= this.compileService(definition); - } - - return [...this.compiledDefinitions] - .sort((a, b) => compareIDs(a.id, b.id)) - .map((def) => $`'${def.id}': ${def.source},\n`); - } - - private compileClass(definitions: Lazy[]): Lazy { - const writer = new LazyWriter(); - const declaration = this.builder.className === 'default' - ? 'default class' - : `class ${this.builder.className}`; - - writer.writeLine(`export ${declaration} extends Container {`); - writer.indent(() => { - writer.writeLine('constructor() {'); - writer.indent(() => { - writer.writeLine('super({'); - writer.indent(() => writer.write(...definitions)); - writer.writeLine('});'); - }) - writer.writeLine('}'); - }); - writer.writeLine('}'); - - return writer; - } - - resolveServiceInjection(type: Type): [id: string | undefined, async: boolean] { - const definitions = this.builder.services.findByType(type); - const [id] = mapSet(definitions, (definition) => { - this.compiledDefinitions.add(definition); - definition.source ??= this.compileService(definition); - return definition.id; - }); - - const async = !!find(definitions, (definition) => definition.async); - - switch (definitions.size) { - case 0: return [undefined, false]; - case 1: return [id, async]; - default: return [`#${this.builder.types.getTypeName(type)}`, async]; - } - } - - resolveDefinitionPath(definition: LocalServiceDefinition | DecoratorDefinition, mode?: ImportMode): string { - const info = this.builder.imports.getInfo(this.builder, definition.resource, mode); - return `${info.alias}.${definition.path}`; - } - - private compileService(definition: ServiceDefinition): Lazy { - if (definition.isLocal()) { - return this.localServiceCompilerFactory.create(this, this.builder, definition).compile(); - } else { - return this.foreignServiceCompilerFactory.create(this, this.builder, definition).compile(); - } - } -} - -function compareIDs(a: string, b: string): number { - return (a.indexOf('#') - b.indexOf('#')) || ( - a < b ? -1 : a > b ? 1 : 0 - ); -} diff --git a/core/cli/src/v2/compiler/foreignServiceCompiler.ts b/core/cli/src/v2/compiler/foreignServiceCompiler.ts deleted file mode 100644 index 0812212..0000000 --- a/core/cli/src/v2/compiler/foreignServiceCompiler.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ContainerBuilder } from '../container'; -import { ForeignServiceDefinition } from '../definitions'; -import { ContainerCompiler } from './containerCompiler'; -import { Lazy, LazyWriter } from './lazy'; -import { ServiceCompiler } from './serviceCompiler'; - -export interface ForeignServiceCompilerFactory { - create( - container: ContainerCompiler, - builder: ContainerBuilder, - definition: ForeignServiceDefinition, - ): ForeignServiceCompiler; -} - -export class ForeignServiceCompiler extends ServiceCompiler { - constructor( - container: ContainerCompiler, - builder: ContainerBuilder, - definition: ForeignServiceDefinition, - ) { - super(container, builder, definition); - } - - compile(): Lazy { - const [parentId, parentAsync] = this.container.resolveServiceInjection(this.definition.container.type); - - if (parentId === undefined) { - throw 'unreachable'; - } - - const writer = new LazyWriter(); - - writer.block(() => { - writer.writeLine(this.compileFactory(parentId, parentAsync)); - writer.writeLine(`scope: 'private',`); - - if (this.definition.async) { - writer.writeLine('async: true,'); - } - }); - - return writer; - } - - private compileFactory(parentId: string, parentAsync: boolean): Lazy { - const writer = new LazyWriter(); - - const async = this.definition.async ? 'async ' : ''; - writer.write(`factory: ${async}(di) => `); - - if (parentAsync) { - writer.write('{\n'); - writer.indent(() => { - writer.writeLine(`const parent = await di.get('${parentId}');`); - writer.writeLine(`return parent.get('${this.definition.foreignId}');`); - }); - writer.write('}'); - } else { - writer.write(`di.get('${parentId}').get('${this.definition.foreignId}')`); - } - - writer.write(','); - - return writer; - } -} diff --git a/core/cli/src/v2/compiler/importMapCompiler.ts b/core/cli/src/v2/compiler/importMapCompiler.ts deleted file mode 100644 index 9439ad1..0000000 --- a/core/cli/src/v2/compiler/importMapCompiler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ContainerBuilder, ImportMode } from '../container'; -import { Lazy, LazyWriter } from './lazy'; - -export class ImportMapCompiler { - compile(builder: ContainerBuilder): Lazy { - const writer = new LazyWriter(); - const dicc = [ - 'Container', - builder.imports.useForeignServiceType && 'type ForeignServiceType', - builder.imports.useServiceType && 'type ServiceType', - ]; - - writer.writeLine(`import { ${dicc.filter((v) => !!v).join(', ')} } from 'dicc';`); - - for (const info of builder.imports) { - if (info.mode === ImportMode.None) { - continue; - } - - const typeOnly = builder.lazyImports && !Boolean(info.mode & ImportMode.Value) ? 'type ' : ''; - writer.writeLine(`import ${typeOnly}* as ${info.alias} from '${info.specifier}';`); - } - - return writer; - } -} diff --git a/core/cli/src/v2/compiler/index.ts b/core/cli/src/v2/compiler/index.ts deleted file mode 100644 index dde2c92..0000000 --- a/core/cli/src/v2/compiler/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './autowiring'; -export * from './compilation'; -export * from './containerCompiler'; -export * from './compiler'; -export * from './foreignServiceCompiler'; -export * from './importMapCompiler'; -export * from './lazy'; -export * from './localServiceCompiler'; -export * from './resourceScanner'; -export * from './serviceCompiler'; -export * from './typeMapCompiler'; diff --git a/core/cli/src/v2/compiler/lazy/index.ts b/core/cli/src/v2/compiler/lazy/index.ts deleted file mode 100644 index 9de775b..0000000 --- a/core/cli/src/v2/compiler/lazy/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './lazyArgs'; -export * from './lazyCompiler'; -export * from './lazyTemplate'; -export * from './lazyWriter'; -export * from './types'; diff --git a/core/cli/src/v2/compiler/lazy/lazyArgs.ts b/core/cli/src/v2/compiler/lazy/lazyArgs.ts deleted file mode 100644 index 7c593bf..0000000 --- a/core/cli/src/v2/compiler/lazy/lazyArgs.ts +++ /dev/null @@ -1,55 +0,0 @@ -const stack: (LazyArgs | undefined)[] = []; -let current: LazyArgs | undefined = undefined; - -export class LazyArgs { - private last: number = -1; - - constructor( - private readonly args: string[], - ) {} - - use(arg: string): void { - const idx = this.args.indexOf(arg); - - if (idx > this.last) { - this.last = idx; - } - } - - watch(cb: () => R): R { - try { - stack.push(current); - current = this; - return cb(); - } finally { - current = stack.pop(); - } - } - - get value(): () => string { - return () => this.args.slice(0, this.last + 1).join(', '); - } - - toString(): symbol { - return Symbol(`Do not stringify LazyArgs!`); - } -} - -export function $args(...args: string[]): LazyArgs { - return new LazyArgs(args); -} - -$args.use = (arg: string, value?: R): R | undefined => { - current?.use(arg); - return value; -}; - -$args.unwatch = (cb: () => R): R => { - try { - stack.push(current); - current = undefined; - return cb(); - } finally { - current = stack.pop(); - } -}; diff --git a/core/cli/src/v2/compiler/lazy/lazyCompiler.ts b/core/cli/src/v2/compiler/lazy/lazyCompiler.ts deleted file mode 100644 index 65004e2..0000000 --- a/core/cli/src/v2/compiler/lazy/lazyCompiler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CodeBlockWriter } from 'ts-morph'; -import { LazyTemplate } from './lazyTemplate'; -import { LazyWriter, LazyWriterAction } from './lazyWriter'; -import { Lazy } from './types'; - -export class LazyCompiler { - constructor( - private readonly createWriter: () => CodeBlockWriter, - ) {} - - compile(content: Lazy): string { - if (content instanceof LazyTemplate) { - return this.compileTemplate(content); - } else if (content instanceof LazyWriter) { - const writer = this.createWriter(); - this.compileWriter(writer, content.actions); - return writer.toString(); - } else if (typeof content === 'function') { - return this.compile(content()); - } else { - return content; - } - } - - private compileTemplate(template: LazyTemplate): string { - const result: string[] = []; - - for (let i = 0; i < template.args.length; ++i) { - result.push(template.tokens[i], this.compile(template.args[i])); - } - - result.push(template.tokens[template.tokens.length - 1]); - return result.join(''); - } - - private compileWriter(writer: CodeBlockWriter, actions: LazyWriterAction[]): void { - for (const action of actions) { - switch (action.type) { - case 'write': writer.write(this.compile(action.text)); break; - case 'line': writer.writeLine(this.compile(action.text)); break; - case 'conditional-write': - action.condition() && writer.write(this.compile(action.text)); - break; - case 'conditional-line': - action.condition() && writer.writeLine(this.compile(action.text)); - break; - case 'indent': - writer.indent(() => this.compileWriter(writer, action.actions)); - break; - case 'block': - writer.block(() => this.compileWriter(writer, action.actions)); - break; - } - } - } -} diff --git a/core/cli/src/v2/compiler/lazy/lazyTemplate.ts b/core/cli/src/v2/compiler/lazy/lazyTemplate.ts deleted file mode 100644 index 88950b9..0000000 --- a/core/cli/src/v2/compiler/lazy/lazyTemplate.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class LazyTemplate { - constructor( - readonly tokens: TemplateStringsArray, - readonly args: any[], - ) {} - - toString(): symbol { - return Symbol('Do not stringify a LazyTemplate!'); - } -} - -export function $lazy(tokens: TemplateStringsArray, ...args: any[]): LazyTemplate { - return new LazyTemplate(tokens, args); -} diff --git a/core/cli/src/v2/compiler/lazy/lazyWriter.ts b/core/cli/src/v2/compiler/lazy/lazyWriter.ts deleted file mode 100644 index 19c7e1d..0000000 --- a/core/cli/src/v2/compiler/lazy/lazyWriter.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Lazy } from './types'; - -export type LazyWriterAction = - | { type: 'write', text: Lazy } - | { type: 'conditional-write', condition: () => boolean, text: Lazy } - | { type: 'line', text: Lazy } - | { type: 'conditional-line', condition: () => boolean, text: Lazy } - | { type: 'indent', actions: LazyWriterAction[] } - | { type: 'block', actions: LazyWriterAction[] }; - -export class LazyWriter { - readonly actions: LazyWriterAction[] = []; - private stack: LazyWriterAction[][] = []; - private cursor: LazyWriterAction[] = this.actions; - - write(...entries: Lazy[]): void { - this.cursor.push(...entries.map((text): LazyWriterAction => ({ type: 'write', text }))); - } - - writeLine(text: Lazy): void { - this.cursor.push({ type: 'line', text }); - } - - conditionalWrite(condition: () => boolean, text: Lazy): void { - this.cursor.push({ type: 'conditional-write', condition, text }); - } - - conditionalWriteLine(condition: () => boolean, text: Lazy): void { - this.cursor.push({ type: 'conditional-line', condition, text }); - } - - indent(cb: () => void): void { - this.nest('indent', cb); - } - - block(cb: () => void): void { - this.nest('block', cb); - } - - toString(): symbol { - return Symbol('Do not stringify a LazyWriter!'); - } - - private nest(type: 'indent' | 'block', cb: () => void): void { - try { - const actions: LazyWriterAction[] = []; - this.cursor.push({ type, actions }); - this.stack.push(this.cursor); - this.cursor = actions; - cb(); - } finally { - this.cursor = this.stack.pop()!; - } - } -} diff --git a/core/cli/src/v2/compiler/lazy/types.ts b/core/cli/src/v2/compiler/lazy/types.ts deleted file mode 100644 index bb752b1..0000000 --- a/core/cli/src/v2/compiler/lazy/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { LazyTemplate } from './lazyTemplate'; -import { LazyWriter } from './lazyWriter'; - -export type LazyCallback = () => Lazy; -export type Lazy = LazyWriter | LazyTemplate | LazyCallback | string; diff --git a/core/cli/src/v2/compiler/localServiceCompiler.ts b/core/cli/src/v2/compiler/localServiceCompiler.ts deleted file mode 100644 index 37b178a..0000000 --- a/core/cli/src/v2/compiler/localServiceCompiler.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { Node, SourceFile } from 'ts-morph'; -import { ContainerBuilder, ImportMode } from '../container'; -import { - AutoImplementationInfo, - Callable, - LocalServiceDefinition, - PromiseType, -} from '../definitions'; -import { getFirst, mapMap, mapSet } from '../utils'; -import { ContainerCompiler } from './containerCompiler'; -import { $args, $lazy, Lazy, LazyWriter } from './lazy'; -import { ServiceCompiler } from './serviceCompiler'; - -export interface LocalServiceCompilerFactory { - create( - container: ContainerCompiler, - builder: ContainerBuilder, - definition: LocalServiceDefinition, - ): LocalServiceCompiler; -} - -export class LocalServiceCompiler extends ServiceCompiler { - private readonly path: string; - - constructor( - container: ContainerCompiler, - builder: ContainerBuilder, - definition: LocalServiceDefinition, - private readonly externalArgs: Set = new Set(), - ) { - super(container, builder, definition); - - this.path = this.container.resolveDefinitionPath( - this.definition, - this.definition.async ? ImportMode.None : ImportMode.Value, - ); - } - - compile(): Lazy { - const writer = new LazyWriter(); - - writer.block(() => { - writer.write($lazy`factory: ${this.compileFactory()},\n`); - writer.conditionalWrite(() => this.definition.scope !== 'global', `scope: '${this.definition.scope}',`); - writer.conditionalWrite(() => this.definition.async, 'async: true,'); - - for (const hook of ['onCreate', 'onFork', 'onDestroy'] as const) { - const stmt = this.compileHook(hook); - stmt !== undefined && writer.writeLine(stmt); - } - }); - - return writer; - } - - private compileFactory(): Lazy { - if (!this.definition.factory && !this.definition.autoImplement) { - return 'undefined'; - } - - const async = this.definition.factory?.async ? 'async ' : ''; - const args = $args('di'); - - return $lazy`${async}(${args.value}) => ${args.watch(() => this.compileFactoryBody())}`; - } - - private compileFactoryBody(): Lazy { - const call = this.compileCall(this.compileFactoryStatement(), this.compileArguments( - this.definition.factory?.args ?? new Map(), - this.compileOverrides(), - )); - - const register = this.compileChildContainerInjections(); - const resources = new Set([this.definition.resource]); - const decorate = this.compileDecorateHooks(resources); - - return () => { - const import_ = this.compileDynamicImports(...resources); - - if (import_ === undefined && register === undefined && decorate === undefined) { - return call; - } - - const writer = new LazyWriter(); - writer.write('{'); - writer.indent(() => { - if (import_ !== undefined) { - writer.writeLine(import_); - } - - if (register === undefined && decorate === undefined) { - writer.write($lazy`return ${call};`); - return; - } - - const declaration = decorate !== undefined ? 'let' : 'const'; - const await_ = this.definition.factory?.async && this.definition.factory.method !== 'constructor' - ? 'await ' - : ''; - writer.writeLine($lazy`${declaration} service = ${await_}${call};`); - register !== undefined && writer.write(register); - writer.write(decorate ?? 'return service;'); - }); - writer.write(`}`); - return writer; - }; - } - - private compileFactoryStatement(): Lazy { - const method = this.definition.factory?.method; - const new_ = method === 'constructor' ? 'new ' : ''; - const path = this.definition.isExplicit() && this.definition.object - ? `${this.path}.factory` - : method - ? `${this.path}.${method}` - : this.path; - - if (this.definition.autoImplement) { - return $lazy`${new_}${this.compileAutoImplementedFactory(path, this.definition.autoImplement)}`; - } else { - return `${new_}${path}`; - } - } - - private compileAutoImplementedFactory( - path: string, - ai: AutoImplementationInfo, - ): Lazy { - const writer = new LazyWriter(); - const extend = Node.isClassDeclaration(this.definition.declaration); - const [prefix, postfix, assign, comma] = extend - ? [`class extends ${path} {`, '}', ' = ', ';'] - : ['({', '})', ': ', ',']; - - writer.write(prefix); - - writer.indent(() => { - const async = ai.method.async ? 'async ' : ''; - const args = new Set(ai.method.args.keys()); - const useServiceType = this.definition.isExplicit() || this.definition.factory?.method === 'constructor'; - const typeHint = extend - ? useServiceType - ? `: ServiceType['${ai.method.name}']` - : `: ${path}['${ai.method.name}']` - : ''; - - if (useServiceType) { - this.builder.imports.useServiceType = true; - } - - writer.write(`${async}${ai.method.name}${typeHint}${assign}(${[...args].join(', ')}) => `); - - if (ai.method.name === 'get') { - $args.use('di'); - const id = $args.unwatch(() => this.container.resolveServiceInjection(ai.service.type)); - const need = ai.method.returnType.nullable ? ', false' : ''; - writer.write(`di.get('${id}'${need})${comma}`); - } else { - const compiler = new LocalServiceCompiler( - this.container, - this.builder, - ai.service, - args, - ); - - writer.write(compiler.compileFactoryBody()); - } - - writer.write(comma); - }); - - writer.write(postfix); - - return writer; - } - - private compileOverrides(): Map { - if (!this.definition.isExplicit()) { - return new Map(mapSet(this.externalArgs, (n) => [n, n])); - } - - return mapMap(this.definition.args, (name, value) => { - if (this.externalArgs.has(name)) { - return [name, name]; - } - - const arg = this.definition.factory?.args.get(name); - const argPath = `${this.path}.args.${name}`; - - if (!(value instanceof Callable)) { - return [name, argPath]; - } - - const isAsync = value.returnType instanceof PromiseType; - const wantsAsync = arg?.type instanceof PromiseType; - - const source = this.ensureAsyncAwaited( - this.compileCall(argPath, this.compileArguments(value.args)), - isAsync, - wantsAsync, - ); - - return [name, source]; - }); - } - - private compileChildContainerInjections(): Lazy | undefined { - if (!this.definition.autoRegister?.size) { - return undefined; - } - - $args.use('di'); - - const writer = new LazyWriter(); - - for (const [foreignId, definition] of this.definition.autoRegister) { - const [id, async] = $args.unwatch(() => this.container.resolveServiceInjection(definition.type)); - const get = this.ensureAsyncAwaited(`di.get('${id}')`, async, false); - writer.writeLine($lazy`service.register('${foreignId}', ${get});`); - } - - return writer; - } - - private compileDecorateHooks(resources: Set): Lazy | undefined { - const [callables] = this.resolveHookCallables('decorate'); - - if (!callables.length) { - return undefined; - } - - const writer = new LazyWriter(); - const overrides = new Map(['service'].entries()); - let idx = 0; - - for (const [callable, resource, path] of callables) { - const await_ = callable.async ? 'await ' : ''; - const call = this.compileCall(`${path}.decorate`, this.compileArguments(callable.args, overrides)); - const stmt = ++idx >= callables.length ? 'return ' : `service = ${await_}`; - writer.writeLine($lazy`${stmt}${call} ?? service;`); - resources.add(resource); - } - - return writer; - } - - private compileHook(hook: 'onCreate' | 'onFork' | 'onDestroy'): Lazy | undefined { - const [callables, async] = this.resolveHookCallables(hook); - const isFork = hook === 'onFork'; - const isContainerFork = isFork && this.definition.container; - - if (!callables.length && !isContainerFork) { - return undefined; - } - - const writer = new LazyWriter(); - const args = isFork ? $args('callback', 'service', 'di') : $args('service', 'di'); - isFork && args.use('callback'); - - writer.write($lazy`${hook}: ${async || isContainerFork ? 'async ' : ''}(${args.value}) => `); - - const body = callables.length - ? args.watch(() => this.compileHookBody(hook, callables)) - : undefined; - - if (!isContainerFork) { - writer.write($lazy`${body},`); - return writer; - } - - const callback = body ? $lazy`async () => ${body}` : 'callback'; - args.use('service'); - writer.write($lazy`service.run(${callback}),`); - return writer; - } - - private compileHookBody( - hook: 'onCreate' | 'onFork' | 'onDestroy', - callables: [callable: Callable, resource: SourceFile, path: string][], - ): Lazy { - const isFork = hook === 'onFork'; - const hasOwnForkHook = isFork && callables[0][2] === this.path; - - if (!hasOwnForkHook) { - const writer = new LazyWriter(); - writer.write('{'); - writer.indent(() => { - const resources = new Set(callables.map(([, resource]) => resource)); - writer.write(() => this.compileDynamicImports(...resources) ?? ''); - writer.write(this.compileHookCalls(hook, callables, new Map(['service'].entries()))); - isFork && writer.writeLine('return callback();'); - }); - writer.write('}'); - return writer; - } - - const [ownForkHook] = callables.shift()!; - const callback = callables.length ? this.compileServiceForkHookCallback(callables) : 'callback'; - const call = this.compileCall(`${this.path}.onFork`, this.compileArguments( - ownForkHook.args, - new Map([callback, 'service'].entries()), - )); - - if (ownForkHook.args.size > 1) { - $args.use('service'); - } - - return () => { - const imports = this.compileDynamicImports(this.definition.resource); - - if (imports === undefined) { - return call; - } - - const writer = new LazyWriter(); - writer.write('{'); - writer.indent(() => { - writer.writeLine(imports); - writer.writeLine($lazy`return ${call};`); - }); - writer.write('}'); - return writer; - }; - } - - private compileServiceForkHookCallback(callables: [callable: Callable, resource: SourceFile, path: string][]): Lazy { - const overrides = new Map(['fork ?? service'].entries()); - const writer = new LazyWriter(); - writer.write('async (fork) => {'); - writer.indent(() => { - const resources = new Set(callables.map(([, resource]) => resource)); - writer.write(() => this.compileDynamicImports(...resources) ?? ''); - writer.write(this.compileHookCalls('onFork', callables, overrides)); - writer.writeLine('return callback();'); - }); - writer.write('}'); - return writer; - } - - private compileHookCalls( - hook: string, - callables: [callable: Callable, resource: SourceFile, path: string][], - overrides: Map, - ): Lazy { - const writer = new LazyWriter(); - - for (const [callable, /*resource*/, path] of callables) { - if (callable.args.size) { - $args.use('service'); - } - - const await_ = callable.async ? `await ` : ''; - const call = this.compileCall(`${path}.${hook}`, this.compileArguments( - callable.args, - overrides, - )); - - writer.writeLine($lazy`${await_}${call};`); - } - - return writer; - } - - private resolveHookCallables( - hook: 'decorate' | 'onCreate' | 'onFork' | 'onDestroy', - ): [callables: [callable: Callable, resource: SourceFile, path: string][], async: boolean] { - const callables: [callable: Callable, resource: SourceFile, path: string][] = []; - let async: boolean = false; - - if (hook !== 'decorate' && this.definition.isExplicit() && this.definition[hook]) { - callables.push([this.definition[hook], this.definition.resource, this.path]); - this.definition[hook].async && (async = true); - } - - for (const decorator of this.definition.decorators ?? []) { - if (decorator[hook]) { - const importMode = this.builder.lazyImports && this.definition.async - ? ImportMode.None - : ImportMode.Value; - - callables.push([ - decorator[hook], - decorator.resource, - this.container.resolveDefinitionPath(decorator, importMode), - ]); - - decorator[hook].async && (async = true); - } - } - - return [callables, async]; - } - - private compileDynamicImports(...resources: SourceFile[]): Lazy | undefined { - if (!this.builder.lazyImports) { - return undefined; - } - - const imports = resources - .map((resource) => this.builder.imports.getInfo(this.builder, resource)); - - if (!imports.length) { - return undefined; - } else if (imports.length === 1) { - const info = getFirst(imports); - return `const ${info.alias} = await import('${info.dynamicSpecifier}');`; - } - - const writer = new LazyWriter(); - const aliases = imports.map((info) => info.alias); - - writer.write(`const [${aliases.join(', ')}] = await Promise.all([`); - writer.indent(() => { - for (const info of imports) { - writer.writeLine(`import('${info.dynamicSpecifier}'),`); - } - }); - writer.write(']);'); - - return writer; - } -} diff --git a/core/cli/src/v2/compiler/serviceCompiler.ts b/core/cli/src/v2/compiler/serviceCompiler.ts deleted file mode 100644 index 371de0a..0000000 --- a/core/cli/src/v2/compiler/serviceCompiler.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ContainerBuilder } from '../container'; -import { - AccessorType, - Argument, - InjectorType, - IterableType, - ListType, - PromiseType, - ScopedRunnerType, - ServiceDefinition, -} from '../definitions'; -import { getFirst } from '../utils'; -import { ContainerCompiler } from './containerCompiler'; -import { $args, $lazy, Lazy, LazyWriter } from './lazy'; - -export abstract class ServiceCompiler { - constructor( - protected readonly container: ContainerCompiler, - protected readonly builder: ContainerBuilder, - protected readonly definition: Definition, - ) {} - - abstract compile(): Lazy; - - protected compileCall(statement: Lazy, args: Lazy[]): Lazy { - if (args.length < 2) { - return $lazy`${statement}(${getFirst(args)})`; - } - - const writer = new LazyWriter(); - - writer.writeLine($lazy`${statement}(`); - writer.indent(() => { - for (const arg of args) { - writer.writeLine($lazy`${arg},`); - } - }); - writer.write(')'); - - return writer; - } - - protected compileArguments(args: Map, overrides?: Map): Lazy[] { - const stmts: Lazy[] = []; - const undefs: Lazy[] = []; - let idx = 0; - - for (const [name, arg] of args) { - const stmt = overrides?.get(name) ?? overrides?.get(idx) ?? this.compileArgument(arg); - ++idx; - - if (stmt === undefined) { - undefs.push(`undefined`); - } else { - stmts.push(...undefs.splice(0, undefs.length), stmt); - } - } - - return stmts; - } - - protected compileArgument(arg: Argument): Lazy | undefined { - if (!arg.type) { - return undefined; - } if (arg.type instanceof ScopedRunnerType) { - return $args.use('di', '{ run: async (cb) => di.run(cb) }'); - } else if (arg.type instanceof InjectorType) { - const type = arg.type.type; - const [id] = $args.unwatch(() => this.container.resolveServiceInjection(type)); - return id === undefined ? undefined : $args.use('di', `(service) => di.register('${id}', service)`); - } else if (arg.type instanceof IterableType) { - const type = arg.type.type; - const [id] = $args.unwatch(() => this.container.resolveServiceInjection(type)); - return id === undefined ? undefined : $args.use('di', `${arg.rest ? '...' : ''}di.iterate('${id}')`); - } - - let value = arg.type; - let wantsAccessor: boolean = false; - let wantsAsync: boolean = false; - - if (value instanceof AccessorType) { - value = value.returnType; - wantsAccessor = true; - } - - if (value instanceof PromiseType) { - value = value.value; - wantsAsync = true; - } - - for (const type of value.getInjectableTypes()) { - const [id, async] = $args.unwatch(() => this.container.resolveServiceInjection(type)); - - if (id === undefined) { - continue; - } - - const method = value instanceof ListType && type === value.type ? 'find' : 'get'; - const need = method === 'get' && (arg.optional || value.nullable) ? ', false' : ''; - const source = `di.${method}('${id}'${need})`; - - $args.use('di'); - - if (wantsAccessor) { - return `${wantsAsync ? 'async ' : ''}() => ${source}`; - } - - return $lazy`${arg.rest ? '...' : ''}${this.ensureAsyncAwaited(source, async, wantsAsync)}`; - } - - return undefined; - } - - protected ensureAsyncAwaited(source: Lazy, isAsync: boolean, wantsAsync: boolean): Lazy { - if (isAsync && !wantsAsync) { - return $lazy`await ${source}`; - } else if (!isAsync && wantsAsync) { - return $lazy`Promise.resolve().then(() => ${source})`; - } else { - return source; - } - } -} diff --git a/core/cli/src/v2/compiler/typeMapCompiler.ts b/core/cli/src/v2/compiler/typeMapCompiler.ts deleted file mode 100644 index 09c92e9..0000000 --- a/core/cli/src/v2/compiler/typeMapCompiler.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Type } from 'ts-morph'; -import { ImportMap, ImportMode, TypeMap } from '../container'; -import { - ForeignServiceDefinition, - LocalServiceDefinition, - ServiceDefinition, -} from '../definitions'; -import { getFirst, getOrCreate } from '../utils'; -import { Lazy, LazyWriter } from './lazy'; - -export class TypeMapCompiler { - compile(types: TypeMap, imports: ImportMap, definitions: Iterable): Lazy { - const [publicTypes, dynamicMap, anonymousMap, async] = this.mapTypes(types, imports, definitions); - - const writer = new LazyWriter(); - writer.write(this.compileMap('Public', publicTypes.join('\n'))); - writer.write(this.compileMap('Dynamic', this.compileTypes(dynamicMap, async))); - writer.write(this.compileMap('Anonymous', this.compileTypes(anonymousMap, async))); - return writer; - } - - private compileMap(name: string, content: Lazy): Lazy { - if (!content) { - return `interface ${name}Services {}\n\n`; - } - - const writer = new LazyWriter(); - writer.writeLine(`interface ${name}Services {`); - writer.indent(() => writer.write(content)); - writer.writeLine('}\n'); - return writer; - } - - private compileTypes(map: Map>, async: Set): Lazy { - if (!map.size) { - return ''; - } - - const writer = new LazyWriter(); - - for (const [alias, types] of map) { - const [pre, post] = async.has(alias) ? ['Promise<', '>'] : ['', '']; - - if (types.size === 1) { - writer.writeLine(`'${alias}': ${pre}${getFirst(types)}${post};`); - continue; - } - - writer.writeLine(`'${alias}':${pre ? ' ' : ''}${pre}`); - writer.indent(() => { - writer.write(`| ${[...types].join('\n| ')};`); - }); - } - - return writer; - } - - private mapTypes( - types: TypeMap, - imports: ImportMap, - definitions: Iterable, - ): [string[], Map>, Map>, Set] { - const publicTypes: string[] = []; - const dynamicMap: Map> = new Map(); - const anonymousMap: Map> = new Map(); - const async: Set = new Set(); - - for (const definition of definitions) { - const type = this.compileType(definition, imports); - - if (!definition.id.startsWith('#')) { - const [pre, post] = definition.async ? ['Promise<', '>'] : ['', '']; - publicTypes.push(`'${definition.id}': ${pre}${type}${post};`); - continue; - } - - const map = definition.isLocal() && !definition.factory && !definition.autoImplement - ? dynamicMap - : anonymousMap; - - this.addTypesToMap(definition.id, type, definition.aliases, types, map, definition.async ? async : undefined); - } - - return [publicTypes, dynamicMap, anonymousMap, async]; - } - - private addTypesToMap( - id: string, - type: string, - aliases: Iterable, - types: TypeMap, - map: Map>, - async?: Set, - ): void { - getOrCreate(map, id, () => new Set).add(type); - async?.add(id); - - for (const name of types.getTypeNamesIfExist(aliases)) { - const alias = `#${name}`; - getOrCreate(map, alias, () => new Set).add(type); - async?.add(alias); - } - } - - private compileType(definition: ServiceDefinition, importMap: ImportMap): string { - if (definition.isLocal()) { - return this.compileLocalType(definition, importMap); - } else if (definition.isForeign()) { - return this.compileForeignType(definition, importMap); - } else { - throw 'unreachable'; - } - } - - private compileLocalType(definition: LocalServiceDefinition, importMap: ImportMap): string { - const info = importMap.getInfo(definition.builder, definition.resource, ImportMode.Type); - const path = `${info.alias}.${definition.path}`; - - if (!definition.isExplicit() && (!definition.factory || definition.factory.method === 'constructor')) { - return path; - } - - importMap.useServiceType = true; - return `ServiceType`; - } - - private compileForeignType(definition: ForeignServiceDefinition, importMap: ImportMap): string { - importMap.useForeignServiceType = true; - const containerType = this.compileLocalType(definition.container, importMap); - return `ForeignServiceType<${containerType}, '${definition.foreignId}'>`; - } -} diff --git a/core/cli/src/v2/container/containerBuilder.ts b/core/cli/src/v2/container/containerBuilder.ts deleted file mode 100644 index 4625861..0000000 --- a/core/cli/src/v2/container/containerBuilder.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SourceFile } from 'ts-morph'; -import { DecoratorMap } from './decoratorMap'; -import { ImportMap } from './importMap'; -import { ServiceMap } from './serviceMap'; -import { TypeMap } from './typeMap'; - -export interface ContainerBuilderFactory { - create(sourceFile: SourceFile, className: string, lazyImports: boolean): ContainerBuilder; -} - -export class ContainerBuilder { - constructor( - readonly services: ServiceMap, - readonly decorators: DecoratorMap, - readonly types: TypeMap, - readonly imports: ImportMap, - readonly sourceFile: SourceFile, - readonly className: string, - readonly lazyImports: boolean, - ) {} -} diff --git a/core/cli/src/v2/container/decoratorMap.ts b/core/cli/src/v2/container/decoratorMap.ts deleted file mode 100644 index 78b8688..0000000 --- a/core/cli/src/v2/container/decoratorMap.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SourceFile, Type } from 'ts-morph'; -import { DecoratorDefinition, DecoratorOptions, ServiceDefinition } from '../definitions'; -import { Event, EventDispatcher } from '../events'; -import { getOrCreate } from '../utils'; - -export abstract class DecoratorEvent extends Event { - constructor( - public readonly decorator: DecoratorDefinition, - ) { - super(); - } -} - -export class DecoratorAdded extends DecoratorEvent {} -export class DecoratorRemoved extends DecoratorEvent {} - -export class DecoratorMap { - private readonly decorators: Map> = new Map(); - - constructor( - private readonly eventDispatcher: EventDispatcher, - ) {} - - add( - resource: SourceFile, - path: string, - targetType: Type, - options: DecoratorOptions = {}, - ): void { - const definition = new DecoratorDefinition(resource, path, targetType, options); - getOrCreate(this.decorators, definition.targetType, () => new Set()).add(definition); - this.eventDispatcher.dispatch(new DecoratorAdded(definition)); - } - - remove(definition: DecoratorDefinition): void { - const definitions = this.decorators.get(definition.targetType); - - if (!definitions?.delete(definition)) { - return; - } - - this.eventDispatcher.dispatch(new DecoratorRemoved(definition)); - } - - decorate(service: ServiceDefinition): DecoratorDefinition[] { - const decorators: DecoratorDefinition[] = []; - - for (const target of [service.type, ...service.aliases]) { - decorators.push(...this.decorators.get(target) ?? []); - } - - return decorators.sort((a, b) => b.priority - a.priority); - } -} diff --git a/core/cli/src/v2/container/importMap.ts b/core/cli/src/v2/container/importMap.ts deleted file mode 100644 index eb6a301..0000000 --- a/core/cli/src/v2/container/importMap.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { SourceFile } from 'ts-morph'; -import { getOrCreate } from '../utils'; -import { ContainerBuilder } from './containerBuilder'; - -export enum ImportMode { - None = 0b00, - Type = 0b01, - Value = 0b10, - Both = 0b11, -} - -export type ImportInfo = { - specifier: string; - dynamicSpecifier: string; - alias: string; - mode: ImportMode; -}; - -export class ImportMap implements Iterable { - public useServiceType: boolean = false; - public useForeignServiceType: boolean = false; - - private readonly resources: Map = new Map(); - private readonly aliases: Set = new Set(); - - getInfo(builder: ContainerBuilder, resource: SourceFile, mode: ImportMode = ImportMode.None): ImportInfo { - const info = getOrCreate(this.resources, resource, () => { - const name = resource.getFilePath() - .replace(/^(?:.*?\/)?([^\/]+)(?:\/index)?(?:\.d)?\.tsx?$/i, '$1') - .replace(/^[^a-z]+|[^a-z0-9]+/gi, '') - .replace(/^$/, 'anon'); - - for (let i = 0; ; ++i) { - const alias = `${name}${i}`; - - if (!this.aliases.has(alias)) { - const info: ImportInfo = { - ...this.resolveSpecifier(builder, resource), - alias, - mode, - }; - - this.aliases.add(alias); - return info; - } - } - }); - - info.mode |= mode; - return info; - } - - private resolveSpecifier( - builder: ContainerBuilder, - resource: SourceFile, - ): { specifier: string, dynamicSpecifier: string } { - const specifier = builder.sourceFile.getRelativePathAsModuleSpecifierTo(resource); - const ext = resource.getFilePath().match(/\.([mc]?)[jt]sx?$/i); - return { specifier, dynamicSpecifier: `${specifier}.${ext ? ext[1] : ''}js` }; - } - - * [Symbol.iterator](): Iterator { - yield * this.resources.values(); - } -} diff --git a/core/cli/src/v2/container/index.ts b/core/cli/src/v2/container/index.ts deleted file mode 100644 index a99a989..0000000 --- a/core/cli/src/v2/container/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './containerBuilder'; -export * from './decoratorMap'; -export * from './importMap'; -export * from './reflection'; -export * from './serviceMap'; -export * from './typeMap'; - diff --git a/core/cli/src/v2/container/reflection/builderReflection.ts b/core/cli/src/v2/container/reflection/builderReflection.ts deleted file mode 100644 index a0cb810..0000000 --- a/core/cli/src/v2/container/reflection/builderReflection.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ServiceDefinition } from '../../definitions'; -import { ContainerBuilder } from '../containerBuilder'; -import { ContainerReflection, ForeignServiceReflection } from './types'; - -export class BuilderReflection implements ContainerReflection { - constructor( - private readonly builder: ContainerBuilder, - ) {} - - getBuilder(): ContainerBuilder | undefined { - return this.builder; - } - - getPublicServiceById(id: string): ForeignServiceReflection { - return this.reflect(this.builder.services.getById(id)); - } - - * getPublicServices(): Iterable { - for (const definition of this.builder.services.getPublicServices()) { - yield this.reflect(definition); - } - } - - * getDynamicServices(): Iterable { - for (const definition of this.builder.services) { - if (definition.isExplicit() && !definition.factory && !definition.autoImplement) { - yield this.reflect(definition); - } - } - } - - private reflect(definition: ServiceDefinition): ForeignServiceReflection { - return { - id: definition.id, - type: definition.type, - aliases: definition.aliases, - get async() { - return definition.async; - }, - }; - } -} diff --git a/core/cli/src/v2/container/reflection/containerReflector.ts b/core/cli/src/v2/container/reflection/containerReflector.ts deleted file mode 100644 index b342821..0000000 --- a/core/cli/src/v2/container/reflection/containerReflector.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Type } from 'ts-morph'; -import { Compilation } from '../../compiler'; -import { getOrCreate } from '../../utils'; -import { BuilderReflection } from './builderReflection'; -import { ExternalReflectionFactory } from './externalReflection'; -import { ContainerReflection } from './types'; - -export class ContainerReflector { - private readonly reflections: Map = new Map(); - - constructor( - private readonly compilation: Compilation, - private readonly externalReflectionFactory: ExternalReflectionFactory, - ) {} - - getContainerReflection(type: Type): ContainerReflection { - return getOrCreate(this.reflections, type, () => { - const builder = this.compilation.getContainerByType(type); - return builder ? new BuilderReflection(builder) : this.externalReflectionFactory.create(type); - }); - } -} diff --git a/core/cli/src/v2/container/reflection/externalReflection.ts b/core/cli/src/v2/container/reflection/externalReflection.ts deleted file mode 100644 index 81452ba..0000000 --- a/core/cli/src/v2/container/reflection/externalReflection.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Type } from 'ts-morph'; -import { ContainerBuilder } from '../containerBuilder'; -import { InternalError } from '../../errors'; -import { mapIterable, throwIfUndef, TypeHelper } from '../../utils'; -import { ContainerReflection, ForeignServiceReflection } from './types'; - -export interface ExternalReflectionFactory { - create(container: Type): ExternalReflection; -} - -export class ExternalReflection implements ContainerReflection { - private publicServices?: Map; - private dynamicServices?: Map; - - constructor( - private readonly typeHelper: TypeHelper, - private readonly container: Type, - ) {} - - getBuilder(): ContainerBuilder | undefined { - return undefined; - } - - getPublicServiceById(id: string): ForeignServiceReflection { - this.publicServices ??= new Map(this.resolveServices('public')); - - return throwIfUndef( - this.publicServices.get(id), - () => new InternalError(`Unknown service: '${id}'`), - ); - } - - getPublicServices(): Iterable { - this.publicServices ??= new Map(this.resolveServices('public')); - return this.publicServices.values(); - } - - getDynamicServices(): Iterable { - this.dynamicServices ??= new Map(this.resolveServices('dynamic')); - return this.dynamicServices.values(); - } - - private resolveServices(map: 'public' | 'dynamic'): Iterable<[string, ForeignServiceReflection]> { - return mapIterable( - this.typeHelper.resolveExternalContainerServices(this.container, map), - (reflection) => [reflection.id, reflection] - ); - } -} diff --git a/core/cli/src/v2/container/reflection/types.ts b/core/cli/src/v2/container/reflection/types.ts deleted file mode 100644 index cf2f649..0000000 --- a/core/cli/src/v2/container/reflection/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Type } from 'ts-morph'; -import { ContainerBuilder } from '../containerBuilder'; - -export type ForeignServiceReflection = { - readonly id: string; - readonly type: Type; - readonly aliases: Iterable; - readonly async: boolean; -}; - -export interface ContainerReflection { - getBuilder(): ContainerBuilder | undefined; - getPublicServices(): Iterable; - getPublicServiceById(id: string): ForeignServiceReflection; - getDynamicServices(): Iterable; -} diff --git a/core/cli/src/v2/container/serviceMap.ts b/core/cli/src/v2/container/serviceMap.ts deleted file mode 100644 index 268ee55..0000000 --- a/core/cli/src/v2/container/serviceMap.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { SourceFile, Type } from 'ts-morph'; -import { - ExplicitServiceDefinition, - ExplicitServiceDefinitionOptions, - ForeignServiceDefinition, - ImplicitServiceDefinition, - LocalServiceDefinition, - LocalServiceDefinitionOptions, - ServiceDefinition, -} from '../definitions'; -import { DefinitionError, InternalError } from '../errors'; -import { Event, EventDispatcher } from '../events'; -import { getFirstIfOnly, getOrCreate, throwIfUndef } from '../utils'; -import { ContainerBuilder } from './containerBuilder'; - -export abstract class ServiceEvent extends Event { - constructor( - public readonly service: ServiceDefinition, - ) { - super(); - } -} - -export class ServiceAdded extends ServiceEvent {} -export class ServiceRemoved extends ServiceEvent {} - -export class ServiceMap implements Iterable { - private readonly definitions: Map = new Map(); - private readonly types: Map> = new Map(); - private readonly publicServices: Set = new Set(); - - constructor( - private readonly eventDispatcher: EventDispatcher, - ) {} - - addImplicitDefinition( - builder: ContainerBuilder, - resource: SourceFile, - path: string, - type: Type, - options: LocalServiceDefinitionOptions = {}, - ): void { - const id = this.allocateServiceId(type); - this.add(new ImplicitServiceDefinition(builder, resource, path, id, type, options)); - } - - addExplicitDefinition( - builder: ContainerBuilder, - resource: SourceFile, - path: string, - type: Type, - options: ExplicitServiceDefinitionOptions = {}, - ): void { - const id = options.anonymous ? this.allocateServiceId(type) : path; - const definition = new ExplicitServiceDefinition(builder, resource, path, id, type, options); - - if (!definition.anonymous) { - this.addPublicService(definition); - } - - this.add(definition); - } - - addForeignDefinition( - builder: ContainerBuilder, - container: LocalServiceDefinition, - foreignId: string, - type: Type, - aliases?: Iterable, - async: boolean = false, - ): void { - const anonymous = !container.isExplicit() || container.anonymous; - - const id = anonymous - ? this.allocateServiceId(type) - : `${container.id}.${foreignId}`; - - const definition = new ForeignServiceDefinition(builder, container, foreignId, id, type, aliases, async); - - if (!anonymous) { - this.addPublicService(definition); - } - - this.add(definition); - } - - private add(definition: ServiceDefinition): void { - this.definitions.set(definition.id, definition); - getOrCreate(this.types, definition.type, () => new Set()).add(definition); - - for (const alias of definition.aliases) { - getOrCreate(this.types, alias, () => new Set()).add(definition); - } - - this.eventDispatcher.dispatch(new ServiceAdded(definition)); - } - - private allocateServiceId(type: Type): string { - const typeName = type.getSymbol()?.getName() ?? 'Anonymous'; - const existing = getOrCreate(this.types, type, () => new Set()); - return `#${typeName}.${existing.size}`; - } - - private addPublicService(definition: ServiceDefinition): void { - const existing = this.definitions.get(definition.id); - - if (existing) { - const source = definition.isForeign() - ? `merged foreign service '${definition.foreignId}' from container '${definition.container.id}'` - : `service '${definition.id}' exported from '${definition.resource.getFilePath()}'`; - const collision = existing.isForeign() - ? `merged foreign service '${existing.foreignId}' from container '${existing.container.id}'` - : `definition exported from '${existing.resource.getFilePath()}'` - - const local = existing.isForeign() ? existing.container : existing; - - throw new DefinitionError( - `Public service ID of ${source} collides with ${collision}`, - { builder: local.builder, resource: local.resource, path: local.path, node: local.node }, - ); - } - - this.publicServices.add(definition); - } - - remove(definition: ServiceDefinition): void { - if (!this.definitions.delete(definition.id)) { - return; - } - - this.types.get(definition.type)?.delete(definition); - - for (const alias of definition.aliases) { - this.types.get(alias)?.delete(definition); - } - - this.publicServices.delete(definition); - this.eventDispatcher.dispatch(new ServiceRemoved(definition)); - } - - getById(id: string): ServiceDefinition { - return throwIfUndef( - this.definitions.get(id), - () => new InternalError(`Service '${id}' does not exist`), - ); - } - - getPublicServices(): Iterable { - return this.publicServices; - } - - getByTypeIfSingle(type: Type): ServiceDefinition | undefined { - return getFirstIfOnly(this.types.get(type) ?? []); - } - - findByType(type: Type): Set { - return this.types.get(type) ?? new Set(); - } - - findByAnyType(...types: Type[]): Set { - return new Set(types.flatMap((type) => [...this.types.get(type) ?? []])); - } - - get size(): number { - return this.definitions.size; - } - - * [Symbol.iterator](): Iterator { - yield * this.definitions.values(); - } -} diff --git a/core/cli/src/v2/container/typeMap.ts b/core/cli/src/v2/container/typeMap.ts deleted file mode 100644 index b3a24c1..0000000 --- a/core/cli/src/v2/container/typeMap.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Type } from 'ts-morph'; -import { getOrCreate } from '../utils'; - -export class TypeMap { - private readonly types: Map = new Map(); - private readonly names: Set = new Set(); - - add(type: Type): void { - this.getTypeName(type); - } - - getTypeName(type: Type): string { - return getOrCreate(this.types, type, () => { - const typeName = type.getSymbol()?.getName() ?? 'Anonymous'; - - for (let i = 0; ; ++i) { - const name = `${typeName}.${i}`; - - if (!this.names.has(name)) { - this.types.set(type, name); - this.names.add(name); - return name; - } - } - }); - } - - getTypeNamesIfExist(types: Iterable): string[] { - return [...types].map((type) => this.types.get(type)).filter((name) => name !== undefined); - } - - [Symbol.iterator](): Iterable<[Type, string]> { - return this.types; - } -} diff --git a/core/cli/src/v2/definitions/argument.ts b/core/cli/src/v2/definitions/argument.ts deleted file mode 100644 index be5127a..0000000 --- a/core/cli/src/v2/definitions/argument.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Node, Type } from 'ts-morph'; -import { ValueType } from './types'; - -export class Argument { - constructor( - public readonly rawType: Type, - public readonly type: ValueType, - public readonly optional: boolean = false, - public readonly rest: boolean = false, - public readonly node?: Node, - ) {} -} diff --git a/core/cli/src/v2/definitions/autoImplementedMethod.ts b/core/cli/src/v2/definitions/autoImplementedMethod.ts deleted file mode 100644 index 977546d..0000000 --- a/core/cli/src/v2/definitions/autoImplementedMethod.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Node, Type } from 'ts-morph'; -import { Argument } from './argument'; -import { Callable } from './callable'; -import { ReturnType } from './types'; - -export class AutoImplementedMethod extends Callable { - declare public readonly returnType: ReturnType; - - constructor( - public readonly name: string, - args: Map, - rawReturnType: Type, - returnType: ReturnType, - node?: Node, - ) { - super(args, rawReturnType, returnType, node); - } -} diff --git a/core/cli/src/v2/definitions/callable.ts b/core/cli/src/v2/definitions/callable.ts deleted file mode 100644 index d5d6bb1..0000000 --- a/core/cli/src/v2/definitions/callable.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Node, Type } from 'ts-morph'; -import { Argument } from './argument'; -import { ReturnType } from './types'; - -export class Callable { - public async?: boolean; - - constructor( - public readonly args: Map, - public readonly rawReturnType: Type, - public readonly returnType?: ReturnType, // undefined means void - public readonly node?: Node, - ) {} -} diff --git a/core/cli/src/v2/definitions/factoryDefinition.ts b/core/cli/src/v2/definitions/factoryDefinition.ts deleted file mode 100644 index 781c948..0000000 --- a/core/cli/src/v2/definitions/factoryDefinition.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Node, Type } from 'ts-morph'; -import { Argument } from './argument'; -import { Callable } from './callable'; -import { ReturnType } from './types'; - -export class FactoryDefinition extends Callable { - constructor( - args: Map, - rawReturnType: Type, - returnType?: ReturnType, - node?: Node, - public readonly method?: string, - ) { - super(args, rawReturnType, returnType, node); - } -} diff --git a/core/cli/src/v2/definitions/types.ts b/core/cli/src/v2/definitions/types.ts deleted file mode 100644 index 3e3b3ad..0000000 --- a/core/cli/src/v2/definitions/types.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Type } from 'ts-morph'; -import { Callable } from './callable'; - -export type ArgumentOverride = Callable | ValueType; - -export class SingleType { - constructor( - public readonly rawType: Type, - public readonly type: Type, - public readonly nullable: boolean = false, - ) {} - - get serviceType(): Type { - return this.type; - } - - * getInjectableTypes(): Iterable { - yield this.type; - } -} - -export class ListType { - constructor( - public readonly rawType: Type, - public readonly type: Type, - public readonly nullable: boolean = false, - ) {} - - get serviceType(): Type { - return this.type; - } - - * getInjectableTypes(): Iterable { - yield this.type; - yield this.rawType; - } -} - -export class PromiseType { - constructor( - public readonly rawType: Type, - public readonly value: SingleType | ListType, - public readonly nullable: boolean = false, - ) {} - - get serviceType(): Type { - return this.value.type; - } - - * getInjectableTypes(): Iterable { - yield * this.value.getInjectableTypes(); - yield this.rawType; - } -} - -export class IterableType { - constructor( - public readonly rawType: Type, - public readonly type: Type, - public readonly nullable: boolean = false, - public readonly async: boolean = false, - ) {} - - get serviceType(): Type { - return this.type; - } - - * getInjectableTypes(): Iterable { - yield this.type; - yield this.rawType; - } -} - -export class AccessorType { - constructor( - public readonly rawType: Type, - public readonly returnType: SingleType | ListType | PromiseType, - public readonly nullable: boolean = false, - ) {} - - get serviceType(): Type { - return this.returnType.serviceType; - } - - * getInjectableTypes(): Iterable { - yield * this.returnType.getInjectableTypes(); - } -} - -export class InjectorType { - constructor( - public readonly rawType: Type, - public readonly type: Type, - public readonly nullable: boolean = false, - ) {} - - get serviceType(): Type { - return this.type; - } - - * getInjectableTypes(): Iterable { - yield this.type; - } -} - -export class ScopedRunnerType { - constructor( - public readonly nullable: boolean = false, - ) {} -} - -export type ValueType = - | SingleType - | ListType - | PromiseType - | IterableType - | AccessorType - | InjectorType - | ScopedRunnerType; - -export type ReturnType = - | SingleType - | ListType - | PromiseType - | IterableType; diff --git a/core/cli/src/v2/definitions/values.ts b/core/cli/src/v2/definitions/values.ts deleted file mode 100644 index e69de29..0000000 diff --git a/core/cli/tests/.gitignore b/core/cli/tests/.gitignore new file mode 100644 index 0000000..e3f1541 --- /dev/null +++ b/core/cli/tests/.gitignore @@ -0,0 +1 @@ +*/generated*.ts diff --git a/core/cli/tests/explicit/definitions.ts b/core/cli/tests/explicit/definitions.ts new file mode 100644 index 0000000..c754cdd --- /dev/null +++ b/core/cli/tests/explicit/definitions.ts @@ -0,0 +1,86 @@ +import { ServiceDefinition } from 'dicc'; + +interface AnAlias { + sayHi(): void; +} + +class TestWithImplicitAlias implements AnAlias { + sayHi() { + console.log('Hi!'); + } +} + +export const testWithImplicitAlias = { + factory: TestWithImplicitAlias, +} satisfies ServiceDefinition; + +class TestWithExplicitAlias { + sayHi() { + console.log('Ciao!'); + } +} + +export const testWithExplicitAlias = { + factory: TestWithExplicitAlias, + anonymous: true, + scope: 'private', +} satisfies ServiceDefinition; + +class TestWithDisabledImplicitAlias implements AnAlias { + sayHi() { + console.log('Not today!'); + } +} + +export const testWithDisabledImplicitAlias = { + factory: TestWithDisabledImplicitAlias, +} satisfies ServiceDefinition; + +interface AnotherAlias { + sayBye(): void; +} + +class TestWithOverriddenAlias implements AnAlias, AnotherAlias { + sayHi() { + console.log('Not me either!'); + } + + sayBye() { + console.log('Servus!'); + } +} + +export const testWithOverriddenAlias = { + factory: TestWithOverriddenAlias, +} satisfies ServiceDefinition; + +class TestAliasInjection { + constructor( + readonly dependencies: AnAlias[], + ) {} +} + +export const testAliasInjection = { + factory: TestAliasInjection, +} satisfies ServiceDefinition; + +class TestNonObjectExplicit {} + +export const testNonObjectExplicit + = TestNonObjectExplicit satisfies ServiceDefinition; + +class TestArgumentOverrides { + constructor( + readonly alias: AnAlias, + readonly another: AnotherAlias, + readonly runtime: string, + ) {} +} + +export const testArgumentOverrides = { + factory: TestArgumentOverrides, + args: { + alias: (service: TestWithImplicitAlias) => service, + runtime: 'some runtime value', + }, +} satisfies ServiceDefinition; diff --git a/core/cli/tests/explicit/dicc.yaml b/core/cli/tests/explicit/dicc.yaml new file mode 100644 index 0000000..a46fedc --- /dev/null +++ b/core/cli/tests/explicit/dicc.yaml @@ -0,0 +1,5 @@ +containers: + generatedContainer.ts: + className: TestContainer + resources: + definitions.ts: ~ diff --git a/core/cli/tests/explicit/expectedContainer.ts b/core/cli/tests/explicit/expectedContainer.ts new file mode 100644 index 0000000..1c58ed7 --- /dev/null +++ b/core/cli/tests/explicit/expectedContainer.ts @@ -0,0 +1,59 @@ +import { Container, type ServiceType } from 'dicc'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'testAliasInjection': ServiceType; + 'testArgumentOverrides': ServiceType; + 'testNonObjectExplicit': ServiceType; + 'testWithDisabledImplicitAlias': ServiceType; + 'testWithImplicitAlias': ServiceType; + 'testWithOverriddenAlias': ServiceType; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#AnAlias0': + | ServiceType + | ServiceType; + '#TestWithExplicitAlias0.0': ServiceType; +} + +export class TestContainer extends Container { + constructor() { + super({ + 'testAliasInjection': { + factory: (di) => new definitions0.testAliasInjection.factory( + di.find('#AnAlias0'), + ), + }, + 'testArgumentOverrides': { + factory: (di) => new definitions0.testArgumentOverrides.factory( + definitions0.testArgumentOverrides.args.alias( + di.get('testWithImplicitAlias'), + ), + di.get('testWithOverriddenAlias'), + definitions0.testArgumentOverrides.args.runtime, + ), + }, + 'testNonObjectExplicit': { + factory: () => new definitions0.testNonObjectExplicit(), + }, + 'testWithDisabledImplicitAlias': { + factory: () => new definitions0.testWithDisabledImplicitAlias.factory(), + }, + 'testWithImplicitAlias': { + aliases: ['#AnAlias0'], + factory: () => new definitions0.testWithImplicitAlias.factory(), + }, + 'testWithOverriddenAlias': { + factory: () => new definitions0.testWithOverriddenAlias.factory(), + }, + '#TestWithExplicitAlias0.0': { + aliases: ['#AnAlias0'], + factory: () => new definitions0.testWithExplicitAlias.factory(), + scope: 'private', + }, + }); + } +} diff --git a/core/cli/tests/explicit/test.mjs b/core/cli/tests/explicit/test.mjs new file mode 100644 index 0000000..afd9e3d --- /dev/null +++ b/core/cli/tests/explicit/test.mjs @@ -0,0 +1,8 @@ +import { it, test } from 'node:test'; +import { runTestSuite } from '../utils.mjs'; + +test('Explicit definitions', async () => { + await it('compiles explicitly defined services', async () => { + await runTestSuite(import.meta.url); + }); +}); diff --git a/core/cli/tests/explicit/tsconfig.json b/core/cli/tests/explicit/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/explicit/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/generators/definitions.ts b/core/cli/tests/generators/definitions.ts new file mode 100644 index 0000000..acb5acb --- /dev/null +++ b/core/cli/tests/generators/definitions.ts @@ -0,0 +1,38 @@ +import { ServiceDefinition } from 'dicc'; + +interface AnAlias {} + +class List1 implements AnAlias {} +class List2 implements AnAlias {} +class List3 implements AnAlias {} + +interface AnotherAlias {} + +class Iterable1 implements AnotherAlias {} +class Iterable2 implements AnotherAlias {} +class Iterable3 implements AnotherAlias {} + +export function listGenerator(): AnAlias[] { + return [ + new List1(), + new List2(), + new List3(), + ]; +} + +export function * iterableGenerator(): Iterable { + yield new Iterable1(); + yield new Iterable2(); + yield new Iterable3(); +} + +class TestDependingOnGenerators { + constructor( + readonly alias: AnAlias[], + readonly another: Iterable, + ) {} +} + +export const testDependingOnGenerators = { + factory: TestDependingOnGenerators, +} satisfies ServiceDefinition; diff --git a/core/cli/tests/generators/dicc.yaml b/core/cli/tests/generators/dicc.yaml new file mode 100644 index 0000000..a46fedc --- /dev/null +++ b/core/cli/tests/generators/dicc.yaml @@ -0,0 +1,5 @@ +containers: + generatedContainer.ts: + className: TestContainer + resources: + definitions.ts: ~ diff --git a/core/cli/tests/generators/expectedContainer.ts b/core/cli/tests/generators/expectedContainer.ts new file mode 100644 index 0000000..ea2d92b --- /dev/null +++ b/core/cli/tests/generators/expectedContainer.ts @@ -0,0 +1,32 @@ +import { Container, type ServiceType } from 'dicc'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'testDependingOnGenerators': ServiceType; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#AnAlias0.0': ServiceType; + '#AnotherAlias0.0': ServiceType; +} + +export class TestContainer extends Container { + constructor() { + super({ + 'testDependingOnGenerators': { + factory: (di) => new definitions0.testDependingOnGenerators.factory( + di.get('#AnAlias0.0'), + di.get('#AnotherAlias0.0'), + ), + }, + '#AnAlias0.0': { + factory: () => definitions0.listGenerator(), + }, + '#AnotherAlias0.0': { + factory: () => definitions0.iterableGenerator(), + }, + }); + } +} diff --git a/core/cli/tests/generators/test.mjs b/core/cli/tests/generators/test.mjs new file mode 100644 index 0000000..25d7fed --- /dev/null +++ b/core/cli/tests/generators/test.mjs @@ -0,0 +1,8 @@ +import { it, test } from 'node:test'; +import { runTestSuite } from '../utils.mjs'; + +test('Service generators', async () => { + await it('generate lists or iterables of services', async () => { + await runTestSuite(import.meta.url); + }); +}); diff --git a/core/cli/tests/generators/tsconfig.json b/core/cli/tests/generators/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/generators/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/hooks/definitions.ts b/core/cli/tests/hooks/definitions.ts new file mode 100644 index 0000000..fdd4891 --- /dev/null +++ b/core/cli/tests/hooks/definitions.ts @@ -0,0 +1,57 @@ +import { ServiceDecorator, ServiceDefinition } from 'dicc'; + +export class ServiceDependency {} +export class OnCreateHookDependency {} +export class OnForkHookDependency {} +export class OnDestroyHookDependency {} + +class TestServiceHooks { + constructor( + readonly dep: ServiceDependency, + ) {} +} + +export const testServiceHooks = { + factory: TestServiceHooks, + onCreate: (service, dep: OnCreateHookDependency) => {}, + onFork: (cb, service, dep: OnForkHookDependency) => cb(), + onDestroy: (service, dep: OnDestroyHookDependency) => {}, +} satisfies ServiceDefinition; + +interface AnAlias { + sayHi(): void; +} + +class TestServiceDecorators implements AnAlias { + sayHi() { + console.log('Hi!'); + } +} + +class TestAliasDecorators implements AnAlias { + sayHi() { + console.log('Hi!'); + } +} + +export const testServiceDecorators = { + factory: TestServiceDecorators, + onFork: (cb) => cb(), +} satisfies ServiceDefinition; + +export const testAliasDecorators = { + factory: TestAliasDecorators, +} satisfies ServiceDefinition; + +export const serviceDecorators = { + decorate: (service) => service, + onCreate: async () => {}, + onFork: (service) => {}, + priority: 1, +} satisfies ServiceDecorator; + +export const aliasDecorators = { + onFork: () => {}, + onDestroy: () => {}, + priority: 2, +} satisfies ServiceDecorator; diff --git a/core/cli/tests/hooks/dicc.yaml b/core/cli/tests/hooks/dicc.yaml new file mode 100644 index 0000000..a46fedc --- /dev/null +++ b/core/cli/tests/hooks/dicc.yaml @@ -0,0 +1,5 @@ +containers: + generatedContainer.ts: + className: TestContainer + resources: + definitions.ts: ~ diff --git a/core/cli/tests/hooks/expectedContainer.ts b/core/cli/tests/hooks/expectedContainer.ts new file mode 100644 index 0000000..27a7867 --- /dev/null +++ b/core/cli/tests/hooks/expectedContainer.ts @@ -0,0 +1,92 @@ +import { Container, type ServiceType } from 'dicc'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'testAliasDecorators': ServiceType; + 'testServiceDecorators': Promise>; + 'testServiceHooks': ServiceType; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#OnCreateHookDependency0.0': definitions0.OnCreateHookDependency; + '#OnDestroyHookDependency0.0': definitions0.OnDestroyHookDependency; + '#OnForkHookDependency0.0': definitions0.OnForkHookDependency; + '#ServiceDependency0.0': definitions0.ServiceDependency; +} + +export class TestContainer extends Container { + constructor() { + super({ + 'testAliasDecorators': { + factory: () => new definitions0.testAliasDecorators.factory(), + onFork: async (callback) => { + definitions0.aliasDecorators.onFork(); + return callback(); + }, + onDestroy: () => { + definitions0.aliasDecorators.onDestroy(); + }, + }, + 'testServiceDecorators': { + factory: () => { + const service = new definitions0.testServiceDecorators.factory(); + return definitions0.serviceDecorators.decorate( + service, + ); + }, + async: true, + onCreate: async () => { + await definitions0.serviceDecorators.onCreate(); + }, + onFork: async (callback, service) => definitions0.testServiceDecorators.onFork( + async (fork) => { + definitions0.aliasDecorators.onFork(); + definitions0.serviceDecorators.onFork( + fork ?? service, + ); + return callback(fork); + }, + ), + onDestroy: () => { + definitions0.aliasDecorators.onDestroy(); + }, + }, + 'testServiceHooks': { + factory: (di) => new definitions0.testServiceHooks.factory( + di.get('#ServiceDependency0.0'), + ), + onCreate: (service, di) => { + definitions0.testServiceHooks.onCreate( + service, + di.get('#OnCreateHookDependency0.0'), + ); + }, + onFork: async (callback, service, di) => definitions0.testServiceHooks.onFork( + callback, + service, + di.get('#OnForkHookDependency0.0'), + ), + onDestroy: (service, di) => { + definitions0.testServiceHooks.onDestroy( + service, + di.get('#OnDestroyHookDependency0.0'), + ); + }, + }, + '#OnCreateHookDependency0.0': { + factory: () => new definitions0.OnCreateHookDependency(), + }, + '#OnDestroyHookDependency0.0': { + factory: () => new definitions0.OnDestroyHookDependency(), + }, + '#OnForkHookDependency0.0': { + factory: () => new definitions0.OnForkHookDependency(), + }, + '#ServiceDependency0.0': { + factory: () => new definitions0.ServiceDependency(), + }, + }); + } +} diff --git a/core/cli/tests/hooks/test.mjs b/core/cli/tests/hooks/test.mjs new file mode 100644 index 0000000..e68e613 --- /dev/null +++ b/core/cli/tests/hooks/test.mjs @@ -0,0 +1,8 @@ +import { it, test } from 'node:test'; +import { runTestSuite } from '../utils.mjs'; + +test('Service hooks', async () => { + await it('compiles service hooks', async () => { + await runTestSuite(import.meta.url); + }); +}); diff --git a/core/cli/tests/hooks/tsconfig.json b/core/cli/tests/hooks/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/hooks/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/implicit/definitions.ts b/core/cli/tests/implicit/definitions.ts new file mode 100644 index 0000000..afaac7b --- /dev/null +++ b/core/cli/tests/implicit/definitions.ts @@ -0,0 +1,100 @@ +import { ScopedRunner, ServiceDefinition } from 'dicc'; + +export class TestNoDependencies {} + +interface AliasInterface { + sayHello(): void; +} + +export class TestSingleDependency implements AliasInterface { + constructor( + readonly dependency: TestNoDependencies, + ) {} + + sayHello() { + console.log('Hello!'); + } +} + +export class TestMultipleDependencies { + constructor( + readonly dependencyOne: TestNoDependencies, + readonly dependencyTwo: AliasInterface, + ) {} +} + +interface AnotherAlias { + sayBye(): void; +} + +export class OneWayToSayBye implements AnotherAlias { + sayBye() { + console.log('Bye!'); + } +} + +export class AnotherWayToSayBye implements AnotherAlias { + sayBye() { + console.log('Ciao!'); + } +} + +export class TestListDependency { + constructor( + readonly services: AnotherAlias[], + ) {} +} + +export interface DynamicServiceOfSomeKind { + beGayDoCrime(): void; +} + +export class TestInjectionModes { + constructor( + readonly accessor: () => TestSingleDependency, + readonly injector: (service: DynamicServiceOfSomeKind) => void, + readonly runner: ScopedRunner, + ) {} +} + +export class TestFactoryMethod { + static create(dependency: TestListDependency): TestFactoryMethod { + return new TestFactoryMethod(dependency); + } + + private constructor( + readonly dependency: TestListDependency, + ) {} +} + +export class TestAsyncFactoryMethod { + static async create(dependency: TestListDependency): Promise { + return new TestAsyncFactoryMethod(dependency); + } + + private constructor( + readonly dependency: TestListDependency, + ) {} +} + +export class TestAsyncDependency { + constructor( + readonly dependency: TestAsyncFactoryMethod, + ) {} +} + +class Entrypoint { + constructor( + readonly testSingle: TestSingleDependency, + readonly testMultiple: TestMultipleDependencies, + readonly testList: TestListDependency, + readonly testInjectionModes: TestInjectionModes, + readonly testFactoryMethod: TestAsyncFactoryMethod, + readonly testAsyncFactoryMethod: TestAsyncFactoryMethod, + readonly testAsyncDependency: TestAsyncDependency, + ) {} +} + +export const entrypoint = { + factory: Entrypoint, +} satisfies ServiceDefinition; diff --git a/core/cli/tests/implicit/dicc.yaml b/core/cli/tests/implicit/dicc.yaml new file mode 100644 index 0000000..a46fedc --- /dev/null +++ b/core/cli/tests/implicit/dicc.yaml @@ -0,0 +1,5 @@ +containers: + generatedContainer.ts: + className: TestContainer + resources: + definitions.ts: ~ diff --git a/core/cli/tests/implicit/expectedContainer.ts b/core/cli/tests/implicit/expectedContainer.ts new file mode 100644 index 0000000..f6b118e --- /dev/null +++ b/core/cli/tests/implicit/expectedContainer.ts @@ -0,0 +1,93 @@ +import { Container, type ServiceType } from 'dicc'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'entrypoint': Promise>; +} + +interface DynamicServices { + '#DynamicServiceOfSomeKind0.0': definitions0.DynamicServiceOfSomeKind; +} + +interface AnonymousServices { + '#AnotherAlias0': + | definitions0.AnotherWayToSayBye + | definitions0.OneWayToSayBye; + '#AnotherWayToSayBye0.0': definitions0.AnotherWayToSayBye; + '#OneWayToSayBye0.0': definitions0.OneWayToSayBye; + '#TestAsyncDependency0.0': Promise; + '#TestAsyncFactoryMethod0.0': Promise; + '#TestInjectionModes0.0': definitions0.TestInjectionModes; + '#TestListDependency0.0': definitions0.TestListDependency; + '#TestMultipleDependencies0.0': definitions0.TestMultipleDependencies; + '#TestNoDependencies0.0': definitions0.TestNoDependencies; + '#TestSingleDependency0.0': definitions0.TestSingleDependency; +} + +export class TestContainer extends Container { + constructor() { + super({ + 'entrypoint': { + factory: async (di) => new definitions0.entrypoint.factory( + di.get('#TestSingleDependency0.0'), + di.get('#TestMultipleDependencies0.0'), + di.get('#TestListDependency0.0'), + di.get('#TestInjectionModes0.0'), + await di.get('#TestAsyncFactoryMethod0.0'), + await di.get('#TestAsyncFactoryMethod0.0'), + await di.get('#TestAsyncDependency0.0'), + ), + async: true, + }, + '#AnotherWayToSayBye0.0': { + aliases: ['#AnotherAlias0'], + factory: () => new definitions0.AnotherWayToSayBye(), + }, + '#DynamicServiceOfSomeKind0.0': { + factory: undefined, + }, + '#OneWayToSayBye0.0': { + aliases: ['#AnotherAlias0'], + factory: () => new definitions0.OneWayToSayBye(), + }, + '#TestAsyncDependency0.0': { + factory: async (di) => new definitions0.TestAsyncDependency( + await di.get('#TestAsyncFactoryMethod0.0'), + ), + async: true, + }, + '#TestAsyncFactoryMethod0.0': { + factory: async (di) => definitions0.TestAsyncFactoryMethod.create( + di.get('#TestListDependency0.0'), + ), + async: true, + }, + '#TestInjectionModes0.0': { + factory: (di) => new definitions0.TestInjectionModes( + () => di.get('#TestSingleDependency0.0'), + (service) => di.register('#DynamicServiceOfSomeKind0.0', service), + { async run(cb) { return di.run(cb); } }, + ), + }, + '#TestListDependency0.0': { + factory: (di) => new definitions0.TestListDependency( + di.find('#AnotherAlias0'), + ), + }, + '#TestMultipleDependencies0.0': { + factory: (di) => new definitions0.TestMultipleDependencies( + di.get('#TestNoDependencies0.0'), + di.get('#TestSingleDependency0.0'), + ), + }, + '#TestNoDependencies0.0': { + factory: () => new definitions0.TestNoDependencies(), + }, + '#TestSingleDependency0.0': { + factory: (di) => new definitions0.TestSingleDependency( + di.get('#TestNoDependencies0.0'), + ), + }, + }); + } +} diff --git a/core/cli/tests/implicit/test.mjs b/core/cli/tests/implicit/test.mjs new file mode 100644 index 0000000..ada0d63 --- /dev/null +++ b/core/cli/tests/implicit/test.mjs @@ -0,0 +1,8 @@ +import { it, test } from 'node:test'; +import { runTestSuite } from '../utils.mjs'; + +test('Implicit definitions', async () => { + await it('compiles implicitly defined services', async () => { + await runTestSuite(import.meta.url); + }); +}); diff --git a/core/cli/tests/implicit/tsconfig.json b/core/cli/tests/implicit/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/implicit/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/iterables/definitions.ts b/core/cli/tests/iterables/definitions.ts new file mode 100644 index 0000000..03a40b1 --- /dev/null +++ b/core/cli/tests/iterables/definitions.ts @@ -0,0 +1,126 @@ +import { ServiceDefinition } from 'dicc'; + +interface AsyncServices {} +interface SyncServices {} +interface MixedServices {} + +export class AsyncService1 implements AsyncServices, MixedServices { + static async create(): Promise { + return new AsyncService1(); + } + + private constructor() {} +} + +export class AsyncService2 implements AsyncServices, MixedServices { + static async create(): Promise { + return new AsyncService2(); + } + + private constructor() {} +} + +export class AsyncService3 implements AsyncServices, MixedServices { + static async create(): Promise { + return new AsyncService3(); + } + + private constructor() {} +} + +export class SyncService1 implements SyncServices, MixedServices {} +export class SyncService2 implements SyncServices, MixedServices {} +export class SyncService3 implements SyncServices, MixedServices {} + +export class TestInjectSyncListOfSyncServices { + constructor( + readonly value: SyncServices[], + ) {} +} + +export class TestInjectSyncListOfAsyncServices { + constructor( + readonly value: AsyncServices[], + ) {} +} + +export class TestInjectSyncListOfMixedServices { + constructor( + readonly value: MixedServices[], + ) {} +} + +export class TestInjectSyncIterableOfSyncServices { + constructor( + readonly value: Iterable, + ) {} +} + +export class TestInjectSyncIterableOfAsyncServices { + constructor( + readonly value: Iterable, + ) {} +} + +export class TestInjectSyncIterableOfMixedServices { + constructor( + readonly value: Iterable, + ) {} +} + +export class TestInjectAsyncListOfSyncServices { + constructor( + readonly value: Promise, + ) {} +} + +export class TestInjectAsyncListOfAsyncServices { + constructor( + readonly value: Promise, + ) {} +} + +export class TestInjectAsyncListOfMixedServices { + constructor( + readonly value: Promise, + ) {} +} + +export class TestInjectAsyncIterableOfSyncServices { + constructor( + readonly value: AsyncIterable, + ) {} +} + +export class TestInjectAsyncIterableOfAsyncServices { + constructor( + readonly value: AsyncIterable, + ) {} +} + +export class TestInjectAsyncIterableOfMixedServices { + constructor( + readonly value: AsyncIterable, + ) {} +} + +class Entrypoint { + constructor( + readonly syncListOfSync: TestInjectSyncListOfSyncServices, + readonly syncListOfAsync: TestInjectSyncListOfAsyncServices, + readonly syncListOfMixed: TestInjectSyncListOfMixedServices, + readonly syncIterableOfSync: TestInjectSyncIterableOfSyncServices, + readonly syncIterableOfAsync: TestInjectSyncIterableOfAsyncServices, + readonly syncIterableOfMixed: TestInjectSyncIterableOfMixedServices, + readonly asyncListOfSync: TestInjectAsyncListOfSyncServices, + readonly asyncListOfAsync: TestInjectAsyncListOfAsyncServices, + readonly asyncListOfMixed: TestInjectAsyncListOfMixedServices, + readonly asyncIterableOfSync: TestInjectAsyncIterableOfSyncServices, + readonly asyncIterableOfAsync: TestInjectAsyncIterableOfAsyncServices, + readonly asyncIterableOfMixed: TestInjectAsyncIterableOfMixedServices, + ) {} +} + +export const entrypoint = { + factory: Entrypoint, +} satisfies ServiceDefinition; diff --git a/core/cli/tests/iterables/dicc.yaml b/core/cli/tests/iterables/dicc.yaml new file mode 100644 index 0000000..a46fedc --- /dev/null +++ b/core/cli/tests/iterables/dicc.yaml @@ -0,0 +1,5 @@ +containers: + generatedContainer.ts: + className: TestContainer + resources: + definitions.ts: ~ diff --git a/core/cli/tests/iterables/expectedContainer.ts b/core/cli/tests/iterables/expectedContainer.ts new file mode 100644 index 0000000..4494758 --- /dev/null +++ b/core/cli/tests/iterables/expectedContainer.ts @@ -0,0 +1,179 @@ +import { Container, type ServiceType, toAsyncIterable, toSyncIterable } from 'dicc'; +import * as definitions0 from './definitions'; + +interface PublicServices { + 'entrypoint': Promise>; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#AsyncService10.0': Promise; + '#AsyncService20.0': Promise; + '#AsyncService30.0': Promise; + '#AsyncServices0': Promise< + | definitions0.AsyncService1 + | definitions0.AsyncService2 + | definitions0.AsyncService3 + >; + '#MixedServices0': Promise< + | definitions0.AsyncService1 + | definitions0.AsyncService2 + | definitions0.AsyncService3 + | definitions0.SyncService1 + | definitions0.SyncService2 + | definitions0.SyncService3 + >; + '#SyncService10.0': definitions0.SyncService1; + '#SyncService20.0': definitions0.SyncService2; + '#SyncService30.0': definitions0.SyncService3; + '#SyncServices0': + | definitions0.SyncService1 + | definitions0.SyncService2 + | definitions0.SyncService3; + '#TestInjectAsyncIterableOfAsyncServices0.0': definitions0.TestInjectAsyncIterableOfAsyncServices; + '#TestInjectAsyncIterableOfMixedServices0.0': definitions0.TestInjectAsyncIterableOfMixedServices; + '#TestInjectAsyncIterableOfSyncServices0.0': definitions0.TestInjectAsyncIterableOfSyncServices; + '#TestInjectAsyncListOfAsyncServices0.0': definitions0.TestInjectAsyncListOfAsyncServices; + '#TestInjectAsyncListOfMixedServices0.0': definitions0.TestInjectAsyncListOfMixedServices; + '#TestInjectAsyncListOfSyncServices0.0': definitions0.TestInjectAsyncListOfSyncServices; + '#TestInjectSyncIterableOfAsyncServices0.0': Promise; + '#TestInjectSyncIterableOfMixedServices0.0': Promise; + '#TestInjectSyncIterableOfSyncServices0.0': definitions0.TestInjectSyncIterableOfSyncServices; + '#TestInjectSyncListOfAsyncServices0.0': Promise; + '#TestInjectSyncListOfMixedServices0.0': Promise; + '#TestInjectSyncListOfSyncServices0.0': definitions0.TestInjectSyncListOfSyncServices; +} + +export class TestContainer extends Container { + constructor() { + super({ + 'entrypoint': { + factory: async (di) => new definitions0.entrypoint.factory( + di.get('#TestInjectSyncListOfSyncServices0.0'), + await di.get('#TestInjectSyncListOfAsyncServices0.0'), + await di.get('#TestInjectSyncListOfMixedServices0.0'), + di.get('#TestInjectSyncIterableOfSyncServices0.0'), + await di.get('#TestInjectSyncIterableOfAsyncServices0.0'), + await di.get('#TestInjectSyncIterableOfMixedServices0.0'), + di.get('#TestInjectAsyncListOfSyncServices0.0'), + di.get('#TestInjectAsyncListOfAsyncServices0.0'), + di.get('#TestInjectAsyncListOfMixedServices0.0'), + di.get('#TestInjectAsyncIterableOfSyncServices0.0'), + di.get('#TestInjectAsyncIterableOfAsyncServices0.0'), + di.get('#TestInjectAsyncIterableOfMixedServices0.0'), + ), + async: true, + }, + '#AsyncService10.0': { + aliases: [ + '#AsyncServices0', + '#MixedServices0', + ], + factory: async () => definitions0.AsyncService1.create(), + async: true, + }, + '#AsyncService20.0': { + aliases: [ + '#AsyncServices0', + '#MixedServices0', + ], + factory: async () => definitions0.AsyncService2.create(), + async: true, + }, + '#AsyncService30.0': { + aliases: [ + '#AsyncServices0', + '#MixedServices0', + ], + factory: async () => definitions0.AsyncService3.create(), + async: true, + }, + '#SyncService10.0': { + aliases: [ + '#SyncServices0', + '#MixedServices0', + ], + factory: () => new definitions0.SyncService1(), + }, + '#SyncService20.0': { + aliases: [ + '#SyncServices0', + '#MixedServices0', + ], + factory: () => new definitions0.SyncService2(), + }, + '#SyncService30.0': { + aliases: [ + '#SyncServices0', + '#MixedServices0', + ], + factory: () => new definitions0.SyncService3(), + }, + '#TestInjectAsyncIterableOfAsyncServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncIterableOfAsyncServices( + di.iterate('#AsyncServices0'), + ), + }, + '#TestInjectAsyncIterableOfMixedServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncIterableOfMixedServices( + di.iterate('#MixedServices0'), + ), + }, + '#TestInjectAsyncIterableOfSyncServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncIterableOfSyncServices( + toAsyncIterable(di.iterate('#SyncServices0')), + ), + }, + '#TestInjectAsyncListOfAsyncServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncListOfAsyncServices( + Promise.resolve(di.find('#AsyncServices0')), + ), + }, + '#TestInjectAsyncListOfMixedServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncListOfMixedServices( + Promise.resolve(di.find('#MixedServices0')), + ), + }, + '#TestInjectAsyncListOfSyncServices0.0': { + factory: (di) => new definitions0.TestInjectAsyncListOfSyncServices( + Promise.resolve(di.find('#SyncServices0')), + ), + }, + '#TestInjectSyncIterableOfAsyncServices0.0': { + factory: async (di) => new definitions0.TestInjectSyncIterableOfAsyncServices( + await toSyncIterable(di.iterate('#AsyncServices0')), + ), + async: true, + }, + '#TestInjectSyncIterableOfMixedServices0.0': { + factory: async (di) => new definitions0.TestInjectSyncIterableOfMixedServices( + await toSyncIterable(di.iterate('#MixedServices0')), + ), + async: true, + }, + '#TestInjectSyncIterableOfSyncServices0.0': { + factory: (di) => new definitions0.TestInjectSyncIterableOfSyncServices( + di.iterate('#SyncServices0'), + ), + }, + '#TestInjectSyncListOfAsyncServices0.0': { + factory: async (di) => new definitions0.TestInjectSyncListOfAsyncServices( + await di.find('#AsyncServices0'), + ), + async: true, + }, + '#TestInjectSyncListOfMixedServices0.0': { + factory: async (di) => new definitions0.TestInjectSyncListOfMixedServices( + await di.find('#MixedServices0'), + ), + async: true, + }, + '#TestInjectSyncListOfSyncServices0.0': { + factory: (di) => new definitions0.TestInjectSyncListOfSyncServices( + di.find('#SyncServices0'), + ), + }, + }); + } +} diff --git a/core/cli/tests/iterables/test.mjs b/core/cli/tests/iterables/test.mjs new file mode 100644 index 0000000..da76a6e --- /dev/null +++ b/core/cli/tests/iterables/test.mjs @@ -0,0 +1,8 @@ +import { it, test } from 'node:test'; +import { runTestSuite } from '../utils.mjs'; + +test('Injection of iterables', async () => { + await it('injects appropriately adjusted iterables', async () => { + await runTestSuite(import.meta.url); + }); +}); diff --git a/core/cli/tests/iterables/tsconfig.json b/core/cli/tests/iterables/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/iterables/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/merging/childDefinitions.ts b/core/cli/tests/merging/childDefinitions.ts new file mode 100644 index 0000000..9d3e0ec --- /dev/null +++ b/core/cli/tests/merging/childDefinitions.ts @@ -0,0 +1,8 @@ +import { ServiceDefinition } from 'dicc'; +import { DynamicChildService, ImplicitChildService, ChildPublicService } from './common'; + +export { DynamicChildService, ImplicitChildService }; + +export const childPublicService = { + factory: ChildPublicService, +} satisfies ServiceDefinition; diff --git a/core/cli/tests/merging/common.ts b/core/cli/tests/merging/common.ts new file mode 100644 index 0000000..ce19548 --- /dev/null +++ b/core/cli/tests/merging/common.ts @@ -0,0 +1,19 @@ +export interface DynamicChildService {} + +export class ImplicitChildService { + static async create(dynamic: DynamicChildService): Promise { + return new ImplicitChildService(dynamic); + } + + private constructor( + readonly dynamic: DynamicChildService, + ) {} +} + +export interface ChildPublicInterface {} + +export class ChildPublicService implements ChildPublicInterface { + constructor( + readonly implicit: ImplicitChildService, + ) {} +} diff --git a/core/cli/tests/merging/dicc.yaml b/core/cli/tests/merging/dicc.yaml new file mode 100644 index 0000000..f221c54 --- /dev/null +++ b/core/cli/tests/merging/dicc.yaml @@ -0,0 +1,9 @@ +containers: + generatedParentContainer.ts: + className: TestParentContainer + resources: + parentDefinitions.ts: ~ + generatedChildContainer.ts: + className: TestChildContainer + resources: + childDefinitions.ts: ~ diff --git a/core/cli/tests/merging/expectedChildContainer.ts b/core/cli/tests/merging/expectedChildContainer.ts new file mode 100644 index 0000000..1719f16 --- /dev/null +++ b/core/cli/tests/merging/expectedChildContainer.ts @@ -0,0 +1,42 @@ +import { Container, type ServiceType } from 'dicc'; +import type * as childDefinitions0 from './childDefinitions'; + +interface PublicServices { + 'childPublicService': Promise>; +} + +interface DynamicServices { + '#DynamicChildService0.0': childDefinitions0.DynamicChildService; +} + +interface AnonymousServices { + '#ImplicitChildService0.0': Promise; +} + +export class TestChildContainer extends Container { + constructor() { + super({ + 'childPublicService': { + factory: async (di) => { + const childDefinitions0 = await import('./childDefinitions.js'); + return new childDefinitions0.childPublicService.factory( + await di.get('#ImplicitChildService0.0'), + ); + }, + async: true, + }, + '#DynamicChildService0.0': { + factory: undefined, + }, + '#ImplicitChildService0.0': { + factory: async (di) => { + const childDefinitions0 = await import('./childDefinitions.js'); + return childDefinitions0.ImplicitChildService.create( + di.get('#DynamicChildService0.0'), + ); + }, + async: true, + }, + }); + } +} diff --git a/core/cli/tests/merging/expectedParentContainer.ts b/core/cli/tests/merging/expectedParentContainer.ts new file mode 100644 index 0000000..aa45a3e --- /dev/null +++ b/core/cli/tests/merging/expectedParentContainer.ts @@ -0,0 +1,67 @@ +import { Container, type ForeignServiceType, type ServiceType } from 'dicc'; +import type * as parentDefinitions0 from './parentDefinitions'; + +interface PublicServices { + 'parentServiceDependingOnChildPublicService': Promise>; +} + +interface DynamicServices {} + +interface AnonymousServices { + '#ChildPublicService0.0': Promise, 'childPublicService'>>; + '#ParentImplementationOfChildDynamicService0.0': Promise; + '#TestChildContainer0.0': Promise>; +} + +export class TestParentContainer extends Container { + constructor() { + super({ + 'parentServiceDependingOnChildPublicService': { + factory: async (di) => { + const parentDefinitions0 = await import('./parentDefinitions.js'); + return new parentDefinitions0.parentServiceDependingOnChildPublicService.factory( + await di.get('#ChildPublicService0.0'), + ); + }, + async: true, + }, + '#ChildPublicService0.0': { + factory: async (di) => { + const parentDefinitions0 = await import('./parentDefinitions.js'); + const parent = await di.get('#TestChildContainer0.0'); + const service = await parent.get('childPublicService'); + return parentDefinitions0.childServiceDecorators.decorate( + service, + ); + }, + async: true, + scope: 'private', + }, + '#ParentImplementationOfChildDynamicService0.0': { + factory: async () => { + const parentDefinitions0 = await import('./parentDefinitions.js'); + return parentDefinitions0.ParentImplementationOfChildDynamicService.create(); + }, + async: true, + }, + '#TestChildContainer0.0': { + factory: async (di) => { + const parentDefinitions0 = await import('./parentDefinitions.js'); + const service = new parentDefinitions0.childContainer.factory(); + service.register('#DynamicChildService0.0', await di.get('#ParentImplementationOfChildDynamicService0.0')); + return service; + }, + async: true, + onFork: async (callback, service) => { + const parentDefinitions0 = await import('./parentDefinitions.js'); + return service.run(async () => parentDefinitions0.childContainer.onFork( + async (fork) => { + parentDefinitions0.childContainerDecorators.onFork(); + return callback(fork); + }, + )); + }, + }, + }); + } +} diff --git a/core/cli/tests/merging/parentDefinitions.ts b/core/cli/tests/merging/parentDefinitions.ts new file mode 100644 index 0000000..7a9c322 --- /dev/null +++ b/core/cli/tests/merging/parentDefinitions.ts @@ -0,0 +1,35 @@ +import { ServiceDecorator, ServiceDefinition } from 'dicc'; +import { ChildPublicInterface, DynamicChildService } from './common'; +import { TestChildContainer } from './generatedChildContainer'; + +export class ParentImplementationOfChildDynamicService implements DynamicChildService { + static async create(): Promise { + return new ParentImplementationOfChildDynamicService(); + } + + private constructor() {} +} + +class ParentServiceDependingOnChildPublicService { + constructor( + readonly service: ChildPublicInterface, + ) {} +} + +export const parentServiceDependingOnChildPublicService = { + factory: ParentServiceDependingOnChildPublicService, +} satisfies ServiceDefinition; + +export const childContainer = { + factory: TestChildContainer, + onFork: (cb) => cb(), + anonymous: true, +} satisfies ServiceDefinition; + +export const childContainerDecorators = { + onFork: () => {}, +} satisfies ServiceDecorator; + +export const childServiceDecorators = { + decorate: (service) => service, +} satisfies ServiceDecorator; diff --git a/core/cli/tests/merging/test.mjs b/core/cli/tests/merging/test.mjs new file mode 100644 index 0000000..503f776 --- /dev/null +++ b/core/cli/tests/merging/test.mjs @@ -0,0 +1,23 @@ +import { it, test } from 'node:test'; +import { compareGeneratedContainer, ensureFileExists, generateTestContainer } from '../utils.mjs'; + +test('Container merging', async () => { + await it('merges containers', async () => { + await ensureFileExists(import.meta.url, 'generatedChildContainer.ts', generateEmptyChildContainer); + const path = await generateTestContainer(import.meta.url); + await compareGeneratedContainer(path, 'ParentContainer'); + await compareGeneratedContainer(path, 'ChildContainer'); + }); +}); + +function generateEmptyChildContainer() { + return ` +import { Container } from 'dicc'; + +export class TestChildContainer extends Container { + constructor() { + super({}); + } +} +`; +} diff --git a/core/cli/tests/merging/tsconfig.json b/core/cli/tests/merging/tsconfig.json new file mode 100644 index 0000000..212cefb --- /dev/null +++ b/core/cli/tests/merging/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/node18", + "compilerOptions": { + "moduleResolution": "NodeNext" + } +} diff --git a/core/cli/tests/utils.mjs b/core/cli/tests/utils.mjs new file mode 100644 index 0000000..6305dd1 --- /dev/null +++ b/core/cli/tests/utils.mjs @@ -0,0 +1,82 @@ +import assert from 'node:assert'; +import { spawn } from 'node:child_process'; +import { readFile, unlink, stat, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export async function generateTestContainer(url) { + const suite = dirname(fileURLToPath(url)); + const configFile = resolve(suite, 'dicc.yaml'); + const diccExecutable = resolve(dirname(fileURLToPath(import.meta.url)), '../dist/cli/dicc.js'); + + const dicc = spawn(diccExecutable, ['--config', configFile], { + cwd: suite, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const output = []; + + dicc.stdout.on('data', (chunk) => { + output.push({ stream: 'stdout', chunk }); + }); + + dicc.stderr.on('data', (chunk) => { + output.push({ stream: 'stderr', chunk }); + }); + + const compilation = new Promise((resolve, reject) => { + dicc.on('error', reject); + dicc.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Compiler terminated with non-zero exit code: ${code}`)); + } + }); + }); + + try { + await compilation; + } catch (e) { + for (const { stream, chunk } of output) { + process[stream].write(chunk); + } + + throw e; + } + + return suite; +} + +export async function compareGeneratedContainer(path, name) { + const expectedFile = resolve(path, `expected${name}.ts`); + const generatedFile = resolve(path, `generated${name}.ts`); + + const expected = await readFile(expectedFile, 'utf-8'); + const generated = await readFile(generatedFile, 'utf-8'); + assert.strictEqual(generated, expected, `Generated container doesn't match expected code`); + + try { + await unlink(generatedFile); + } catch { /* noop */ } +} + +export async function runTestSuite(url) { + const suite = await generateTestContainer(url); + await compareGeneratedContainer(suite, 'Container'); +} + +export async function ensureFileExists(url, file, generate) { + const suite = dirname(fileURLToPath(url)); + const path = resolve(suite, file); + + try { + await stat(path); + } catch (e) { + if (e.code === 'ENOENT') { + await writeFile(path, generate()); + } else { + throw e; + } + } +} diff --git a/core/dicc/src/abstractContainer.ts b/core/dicc/src/abstractContainer.ts index b33f3b8..9b4227b 100644 --- a/core/dicc/src/abstractContainer.ts +++ b/core/dicc/src/abstractContainer.ts @@ -1,4 +1,4 @@ -import { AsyncLocalStorage } from 'async_hooks'; +import { AsyncContextShim as AsyncContext } from './asyncContext'; import { ServiceStore } from './serviceStore'; import { CompiledAsyncServiceDefinition, @@ -22,7 +22,7 @@ export abstract class AbstractContainer = { private readonly definitions: Map> = new Map(); private readonly aliases: Map = new Map(); private readonly globalServices: ServiceStore = new ServiceStore(); - private readonly localServices: AsyncLocalStorage = new AsyncLocalStorage(); + private readonly localServices: AsyncContext.Variable = new AsyncContext.Variable(); private readonly forkHooks: [string, CompiledServiceForkHook][] = []; private readonly creating: Set = new Set(); @@ -95,8 +95,8 @@ export abstract class AbstractContainer = { const store = new ServiceStore(parent); const chain = this.forkHooks.reduceRight((next, [id, hook]) => { return async () => { - const callback = async (fork?: any) => { - fork && store.set(id, fork); + const callback = async (localService?: any) => { + localService && store.set(id, localService); return next(); }; @@ -117,7 +117,7 @@ export abstract class AbstractContainer = { async reset(): Promise { if (this.currentStore !== this.globalServices) { - throw new Error(`Only the global container instance can be reset, this is a fork`); + throw new Error(`The container can only be reset from the global scope, this call is running inside an async local scope`); } for (const [id, definition] of this.definitions) { @@ -175,7 +175,7 @@ export abstract class AbstractContainer = { } return undefined; - } else if (definition.scope === 'local' && !this.localServices.getStore()) { + } else if (definition.scope === 'local' && !this.localServices.get()) { throw new Error(`Cannot create local service '${id}' in global scope`); } else if (definition.scope !== 'private') { if (this.creating.has(id)) { @@ -222,14 +222,14 @@ export abstract class AbstractContainer = { } private get currentStore(): ServiceStore { - return this.localServices.getStore() ?? this.globalServices; + return this.localServices.get() ?? this.globalServices; } private getStore(scope: ServiceScope = 'global'): ServiceStore | undefined { if (scope === 'global') { return this.globalServices; } else if (scope === 'local') { - const store = this.localServices.getStore(); + const store = this.localServices.get(); if (!store) { throw new Error('Cannot access local store in global context'); diff --git a/core/dicc/src/asyncContext.ts b/core/dicc/src/asyncContext.ts new file mode 100644 index 0000000..d17700c --- /dev/null +++ b/core/dicc/src/asyncContext.ts @@ -0,0 +1,24 @@ +import type { AsyncLocalStorage } from 'async_hooks'; + +export namespace AsyncContextShim { + let async_hooks: typeof import('async_hooks') | undefined; + + export class Variable { + private store?: AsyncLocalStorage; + + get(): T | undefined { + return this.store?.getStore(); + } + + async run(value: T, fn: (...args: Args)=> Return, ...args: Args): Promise { + try { + async_hooks ??= await import('async_hooks'); + } catch { + throw new Error("Your environment doesn't support AsyncLocalStorage"); + } + + this.store ??= new async_hooks.AsyncLocalStorage(); + return this.store.run(value, fn, ...args); + } + } +} diff --git a/core/dicc/src/index.ts b/core/dicc/src/index.ts index 71b234e..92fab38 100644 --- a/core/dicc/src/index.ts +++ b/core/dicc/src/index.ts @@ -1,2 +1,3 @@ export * from './container'; export * from './types'; +export { toAsyncIterable, toSyncIterable } from './utils'; diff --git a/core/dicc/src/types.ts b/core/dicc/src/types.ts index 2e71208..63ecca3 100644 --- a/core/dicc/src/types.ts +++ b/core/dicc/src/types.ts @@ -22,8 +22,8 @@ export type IterateResult, K extends keyof export type ServiceScope = 'global' | 'local' | 'private'; export type ServiceHook = (service: T, ...args: any[]) => Promise | void; -export type ServiceForkHook = (callback: ServiceForkCallback, service: T, ...args: any[]) => Promise | unknown; -export type ServiceForkCallback = (fork?: T | undefined) => Promise | R; +export type ServiceForkHook = (callback: ServiceForkHookCallback, service: T, ...args: any[]) => Promise | unknown; +export type ServiceForkHookCallback = (localService?: T | undefined) => Promise | R; export type ServiceDefinitionOptions = { factory: Constructor | Factory | T | undefined>; @@ -77,7 +77,7 @@ export type CompiledAsyncServiceHook = { }; export type CompiledServiceForkHook = {}> = { - (callback: ServiceForkCallback, service: T, container: AbstractContainer): Promise | unknown; + (callback: ServiceForkHookCallback, service: T, container: AbstractContainer): Promise | unknown; }; export type CompiledServiceDefinitionOptions = {}> = { diff --git a/core/dicc/src/utils.ts b/core/dicc/src/utils.ts index 3cb069e..64a5af0 100644 --- a/core/dicc/src/utils.ts +++ b/core/dicc/src/utils.ts @@ -29,3 +29,17 @@ export function createAsyncIterator(input: T[], transform: (value: T) => P }, }; } + +export async function * toAsyncIterable(iterable: Iterable): AsyncIterable { + yield * iterable; +} + +export async function toSyncIterable(iterable: AsyncIterable): Promise> { + const items: T[] = []; + + for await (const item of iterable) { + items.push(item); + } + + return items; +} diff --git a/docs/README.md b/docs/README.md index fb79f24..37dad4e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,16 +34,14 @@ npm i --save dicc - [Injecting dependencies][5] - all the ways services can depend on each other - [Auto-generated factories][6] - save yourself some keystrokes - [Service decorators][7] - augment multiple service definitions at once - - [Container parameters][8] - define runtime parameters for the entire - container - - [Merging containers][9] - split your application container into multiple + - [Merging containers][8] - split your application container into multiple parts and then merge them back together - - [Config and compilation][10] - how to configure the compiler, how to compile + - [Config and compilation][9] - how to configure the compiler, how to compile a container and how to use the container at runtime ## Integration recipes - - [Express][11] + - [Express][10] ## Developer documentation @@ -58,7 +56,6 @@ to DICC or to extend it with custom functionality. Coming soon! [5]: user/05-injecting-dependencies.md [6]: user/06-auto-factories.md [7]: user/07-service-decorators.md -[8]: user/08-container-parameters.md -[9]: user/09-merging-containers.md -[10]: user/10-config-and-compilation.md -[11]: recipes/01-express.md +[8]: user/08-merging-containers.md +[9]: user/09-config-and-compilation.md +[10]: recipes/01-express.md diff --git a/docs/recipes/01-express.md b/docs/recipes/01-express.md index 24a002d..517a76b 100644 --- a/docs/recipes/01-express.md +++ b/docs/recipes/01-express.md @@ -1,9 +1,9 @@ -# Using `container.fork()` with Express +# Using `container.run()` with Express The sadly ubiquitous Express HTTP server doesn't mesh well with async code. This means some care must be taken when integrating DICC into an Express-based -application, otherwise container forking and local services won't work as -expected. +application, otherwise the async context tracking within the `container.run()` +callback required to make locally-scoped services possible simply won't work. ## TL;DR - how do I use DICC with Express? @@ -11,7 +11,7 @@ Just use the following code snippet as your very first middleware: ```typescript app.use((req, res, next) => { - container.fork(async () => { + container.run(async () => { next(); if (!res.closed) { @@ -75,14 +75,14 @@ Notice the first and second middlewares log the end of their execution _before_ the last middleware finishes executing, even though each middleware awaits the `next()` call. This means that `mw 1` has no direct way of telling when `mw 3` (or any other middleware) finished handling the request. If we were to naively -use something like `container.fork(next)` in `mw 1`, the forked context would +use something like `container.run(next)` in `mw 1`, the local context would only be available during the synchronous part of the subsequent middlewares, -because the `fork()` method awaits the provided callback and cleans up the -forked context when the callback resolves - but as we've seen, `next()` doesn't +because the `run()` method awaits the provided callback and cleans up the +local context when the callback resolves - but as we've seen, `next()` doesn't return a Promise, so it will resolve immediately when all synchronous code has been executed. The snippet at the beginning of this recipe works by waiting for the response -stream to be closed before returning from the fork callback. Unless the app -crashes catastrophically, this will ensure that the forked DI context will stay -alive for the entire duration of the request handling pipeline. +stream to be closed before returning from the callback. Unless the app crashes +catastrophically, this will ensure that the local DI context will stay alive +for the entire duration of the request handling pipeline. diff --git a/docs/user/01-intro-to-di.md b/docs/user/01-intro-to-di.md index 137b4f6..1034bb2 100644 --- a/docs/user/01-intro-to-di.md +++ b/docs/user/01-intro-to-di.md @@ -75,8 +75,9 @@ switch to e.g. Slack or text messages in the future, and then the API wouldn't make sense). The second thing is that it's generally a good idea to specify dependencies using _interfaces_, rather than depending on a specific implementation of that interface - so instead of depending on e.g. -`EmailAlertSender`, you'd depend on something like `AlertSenderInterface`, and -the `EmailAlertSender` class would implement this interface. +`class EmailAlertSender`, you'd depend on something like +`interface AlertSender`, and the `EmailAlertSender` class would implement this +interface. **Next**: [Intro to DICC][1] diff --git a/docs/user/02-intro-to-dicc.md b/docs/user/02-intro-to-dicc.md index 07db030..a57284b 100644 --- a/docs/user/02-intro-to-dicc.md +++ b/docs/user/02-intro-to-dicc.md @@ -4,233 +4,99 @@ In [the previous chapter][1] we've talked about dependency injection in general terms; in this chapter, we'll tackle the basics of how dependency injection works in DICC. +Let's begin with defining some terminology that we'll use throughout the rest +of this documentation: + + - A **service** can be almost anything, but typically they will be classes + or objects. Scalar values explicitly cannot be services; and values which + have a call signature (typically functions) may in some cases introduce + issues with dependency detection, so they're best avoided as services, too. + - A **service factory** is either a constructor or a function which creates + an instance of a particular service. + - A **dependency** is any argument of a service factory. + - **Autowiring** is the process of analysing the dependencies of each service + and looking up other services within the same container which would satisfy + those dependencies. + - A **service definition** is a piece of TypeScript code that describes the + identity and the dependencies of a particular service. Service definitions + can be either _implicit_ or _explicit_. + - **Implicit** service definitions are classes, interfaces, or functions which + return a service. In other words, most of your regular code is already a + service definition! + - **Explicit** definitions are special `satisfies` expressions which allow you + to specify extra options for service definitions. + - Services can be either **public** or **anonymous**. Public services are those + with a known _service ID_. All services have a unique service ID, but for + anonymous services this ID is generated and should be considered opaque. Each + container must have at least one public service. Implicit service definitions + are always anonymous; explicit definitions are public by default, but can be + made anonymous using an optional flag. + - **Dynamic** services are services which don't have a factory. The container + won't be able to create instances of such services at runtime; instead, you + will need to register them manually. But other services can still depend on + them. + - A **resource file** (or just "resource" for short) is any TypeScript file + within your project that exports one or more service definitions. + - Service **aliases** are extra types which you want DICC to take into account + while autowiring service dependencies. By default, if a service is a class or + an interface, all of its ancestor classes and extended / implemented + interfaces will automatically be added as the service's aliases. + - Service **decorators** (not to be confused with `@decorators`) are + a mechanism which you can use to augment the definitions of all services + matching a given type. + - A service's **scope** dictates when will the runtime container create + new instances of the service: + - _Global_ services will be instantiated _at most once_ per container, making + them akin to _singletons_ (although unlike singletons, this is enforced by + the container, and not the service itself). All services which depend on a + global service will receive the same instance of that service. + - _Private_ services are the polar opposite: they are instantiated every time + they are requested from the container, meaning each service which depends + on a private service will receive a fresh instance of that service. + - _Local_ services are services which are only available within + an asynchronous execution context started by calling the container's + `.run()` method. Each such execution context (also called a _fork_) will + have its own instances of local services, and these instances will be + destroyed and discarded at the end of the `.run()` call. This is useful for + things like HTTP requests, where each request can have a set of + request-specific services not shared with other concurrent requests. + - Service **hooks** are callbacks defined using explicit service definitions + and / or service decorators which are run at specific points in a service's + lifecycle. + - An **async** service is one which cannot be instantiated synchronously. This + may be due to the service factory being async, or due to the factory + depending on another service which is itself async (and must therefore be + awaited before it can be passed to the factory), or due to some of the hooks, + which can be attached to the service, being async. Most of the time you don't + need to care too much about which services are async, because DICC will take + care of awaiting them as appropriate. -## DICC is a DI Container _Compiler_ - -Most existing DI frameworks in TypeScript use metadata about service types -emitted during transpilation and then do most of the heavy lifting at runtime. -DICC is different: it requires an additional step before the actual TypeScript -transpilation, during which it will analyse your service definitions and produce -a _compiled_ DI container, which only has limited information about the actual -dependencies of each service, but which includes factories capable of obtaining -the dependencies required to create the services. It depends on static analysis -of your code in order to achieve this. This approach has some benefits, as well -as some drawbacks: on the one hand in allows your service code to remain almost -entirely agnostic to the DI implementation (e.g. no `@Decorators` in service -code, no special types outside service definitions etc.), but on the other hand -it imposes some requirements on how you write code. These, however, shouldn't -be far outside what you'd probably do anyway, so the cost of using DICC should -be relatively small, and far outweighed by the benefits: - - - DICC doesn't require you to enable `experimentalDecorators` and - `emitDecoratorMetadata`; nor does it require usage of `reflect-metadata`. - - As mentioned, services don't need to include any kind of special code in - order to work with DICC (apart from TypeScript types which you'd provide - anyway). Among other things, this means that you don't even need to use DICC - in tests - you can still construct services and inject their dependencies - manually if needed. - - DICC supports some features that few, if any, other frameworks currently - offer, including async services, service accessors and injectors, and lists / - iterables of services. - - The compiled container is a regular TypeScript file which you can easily - examine to figure out exactly what DICC is doing. - -The main pain point of using DICC is writing service definitions. The goal of -DICC is to help you keep DI-related code separate from the actual service code; -this usually means creating one or more extra files somewhere in your project. -In the simplest cases (which should also be the most frequent), DICC can -extrapolate a service definition from the service's class declaration, and you -can simply re-export these from within your resource files (or even point to -them directly from within the DICC config file); in more complex cases you need -to write a _service definition_, which is a simple `satisfies` expression. - - -## Dependency injection using DICC - -As mentioned in the previous chapter, DICC only supports _constructor -injection_. This is not unintentional - it is the author's opinion that setter -and property injection bring more problems than they solve, and implementing -them correctly in DICC without introducing a lot of issues would be extremely -hard. For example, how would the compiler distinguish between a property -which should be injected and another which shouldn't, unless the service code -is changed to label injected properties somehow? - -One thing which DICC does support as a natural extension of the constructor -injection pattern is _service factories_ - functions or static class methods -which create the service instance. Service factories are injected the same way -a class constructor would be, but they allow some powerful stuff which isn't -possible with constructors. For starters, unlike a class constructor, a factory -function can be `async`; this means not only that a factory can e.g. return a -database connection _after_ the connection has already been established, but -also that the factory can properly `await` the resolution of any dependencies -which themselves are `async` - in fact, DICC does this part automatically under -the hood, and most of the time you don't need to know or care whether a -dependency is async or not. -Another thing which service factories allow is defining _non-class_ services - -for example, an object literal can be a service (and still have dependencies). -This is useful e.g. for injecting configuration into services. - -Service factories also make it possible for services to be _optional_ - a -factory function can decide, based on e.g. its dependencies, that a particular -service can't or shouldn't be created, and return `undefined` instead. This -can be used to define things like different logger backends based on local vs. -production environment and similar. - - -## Service and dependency types - -TypeScript is a _structurally_-typed language, meaning that one thing is -assignable to another if its structure matches the structure of the target. -Other languages, for example PHP, employ _nominal_ typing, where one thing is -assignable to another if and only if its type corresponds to the same type -declaration as the target - for example, imagine you have declared two -interfaces with the exact same shape, and a class which implements one of them - -then instances of that class will only be assignable to e.g. function arguments -which reference the interface explicitly listed in the class's `implements` -clause, and not to arguments which reference the other interface, even though -the interfaces have the same shape. Nominal typing is therefore stricter than -structural typing. - -DICC uses a mixture of the two systems: it obtains a reference to each service -and injectable argument's type from the TypeScript type checker, and it will -inject services into arguments which reference the same type - meaning that the -decision of what to inject doesn't consider the _structure_ of the services, but -rather the _identity_ of the types. But services can be given _aliases_ - extra -types that the actual service type must conform to _structurally_ (but doesn't -need to explicitly list in e.g. `implements` clauses), which are then also -considered when resolving injection. - -When the DICC compiler encounters a class declaration in a resource file, it -will add any interfaces explicitly implemented by the class as aliases; it will -also add all ancestor classes and their explicitly listed interfaces. When a -service has an explicit service definition, only aliases explicitly listed -in the definition will be applied, regardless of which interfaces and ancestor -classes the service implements or extends. +## DICC is a DI Container _Compiler_ +DICC works by statically analysing your resource files to discover service +definitions. It then analyses these definitions, starting with public services +and working its way through their dependencies. Then it generates a container +class, which includes all the code needed to instantiate services and their +dependencies at runtime. -## DICC speak +Due to the fact that service analysis begins with public services and follows +their dependency chains, any anonymous service definitions which aren't part of +a public service's dependency chain are excluded from the compiled container, +because they are effectively unreachable at runtime. -In this section we'll look at some terms used in the rest of the documentation: +Typically, your container will only need a handful of public services, which +will serve as entrypoints for your application. Outside application entrypoints, +you should never need to know service IDs or have direct access to the container +instance. - - A **service** can be almost anything that has a type. Due to the way type - resolution works in the TypeScript compiler and the way that the DICC - compiler leverages that mechanism, it isn't very useful to register simple - types such as `string` and `number` as services, because even if you create - a type alias such as `type DbHost = string`, the compiler will only see - `string`, and therefore you'd have a bunch of services of type `string` which - the DICC compiler wouldn't be able to distinguish between. Furthermore, it is - recommended (but not required) that you use interfaces, rather than type - aliases, for service aliases - again, due to the way the DICC compiler now - works, the _name_ of a type alias (unlike an interface's name) is lost during - the compilation - and the names of service and alias types are used in the - compiled container code (suffixed with a number to ensure two distinct types - with the same name don't conflict). Using type aliases would therefore result - in a lot of `#Anonymous.` strings in your compiled container, making - it much less readable. - - A **factory** is a callable or constructable which returns a service. - - A **service ID** is a unique identifier of a service. Service IDs are derived - from the path to the service definition in the definition tree exported from - a definition file. Services which don't have an explicit service definition - don't have service IDs. - - **Public services** are those which we can explicitly address in application - code using their service IDs. Your app will typically only have a couple of - these. - - **Anonymous services** are the opposite of public services: although they do - have an ID internally, it should be considered an opaque string and not used - by application code. - - An **alias**, as mentioned, is an extra type that you want the DICC compiler - to consider when resolving injection. Each service can have zero or more - aliases and the service type must structurally conform to each of them. - Aliases can be specified in the service definition, rather than directly in - the service code (e.g. in the service class declaration), meaning that you - can add extra aliases to e.g. 3rd-party services without touching their code - or needing to extend them to add an `implements` clause. - - A **service definition** is a mechanism by which you tell DICC about a - service which exists in your application. Service definitions must be - exported (or re-exported) from a _resource file_, which serves as the main - input to the DICC compiler. Each definition corresponds to exactly one - service (meaning it isn't possible to e.g. define a bunch of services in a - batch by iterating over an array). The definition itself is any value - declaration reachable by traversing the exports of the definition file whose - initializer is a special `satisfies` expression. We'll unpack what this - somewhat convoluted sentence means later. - - A **dependency** is any constructor or factory argument of a defined service. - The DICC compiler doesn't understand everything you can do in TypeScript, not - by a long shot; the full list of things the compiler _does_ understand will - be elaborated in a later section. If an argument's type cannot be resolved - to something that DICC understands, DICC will either inject `undefined` if - the argument is optional, or throw an error during compilation if it's not. - Similarly, if DICC does understand an argument's type, but no service of the - requested type exists, DICC will inject `undefined` if the argument is - optional, and throw an error during compilation if it's not. - - A service's **scope** dictates when the container will create a new instance - of the service and where the instance will be available from. There are three - options: - - The _global_ scope is the default; a service defined in this scope will be - instantiated at most once and the instance will be shared among all other - services which depend on it. - - Services in the _private_ scope are the polar opposite: each time such a - service is requested from the container, a new instance will be created; - an instance of a private service will never be shared among multiple - services which depend on it. - - A _local_ service can only be instantiated inside an isolated asynchronous - context created by calling the container's `.fork()` method. Each forked - context gets its own instances of any local services. See the next entry - for more details about forking the container. - - An isolated forked context, or **fork**, of a container can be created by - calling its `.fork()` method. Inside the callback passed to the `.fork()` - method and the asynchronous call chain spawned from within the callback the - application can access _locally-scoped_ services. When the async call chain - terminates (i.e. when the value returned from the callback is not a promise, - or when the promise is resolved), the local scope and all its services are - destroyed. Global and private services are available inside a fork as usual, - and local services can depend on them, but global services cannot _directly_ - depend on local ones (although they can depend on e.g. accessors for them). - - A **dynamic service** is one which the compiler knows about, but which the - container cannot create at runtime - in other words, it is a service without - a factory. Such a service must be registered manually at runtime in order to - be available for injection. This can be useful especially in combination with - other features - e.g. a locally-scoped dynamic service which represents an - HTTP request. - - A **hook** is one of a few optional callbacks you can specify in service - definitions; they are executed by the container at important points in a - service's lifecycle. The available hooks are: - - `onCreate(service: T, ...args: any[]): Promise | void` - This hook - will be called when the service has been created and registered. Note it - works with dynamic services as well - the hook will be called when the - service is registered. An async `onCreate` hook will make the service - itself async as well. - - `onFork(callback: (forkedService?: T) => Promise | R, service: T, ...args: any[]): Promise | R` - - This hook will be called at the start of a `container.fork()` call. The - hook should do its thing and then call `return callback()` at the end, - optionally passing a new instance of the service into the callback; this - instance will be used inside the forked async execution chain, as if the - service was defined in the `local` scope. MikroORM's `EntityManager` comes - to mind here. - - `onDestroy(service: T, ...args: any[]): void` - This hook will be called - when a service is being destroyed. This will happen at the end of any - `container.fork()` call for all services in the `local` scope, as well as - for any instances returned from an `onFork` hook within the same fork - call. +Since autowiring is resolved during container compilation, any unmet service +dependencies will result in an error at compile time. Similarly, unbroken cyclic +dependencies can be detected at compile time and will likewise result in an +error. - The `...args: any[]` rest argument of each hook callback will be injected the - same way service factories are, so hooks can use other services. - - An **async service** is a service whose factory returns a Promise, or whose - `onCreate` hook returns a Promise, or which depends on another async service. - It is impossible to obtain a resolved instance of an async service from the - container directly; rather, you will always obtain a Promise which you will - need to await. DICC will do it for you automatically when injecting the - service as a dependency. - - A **service decorator** is a special definition you can export from one of - your resource files; similarly to a service definition, it is a `satisfies` - expression. A service decorator always targets a specific service type, and - will be applied by the compiler to all services which match that type. - Service decorators can modify a service's scope, they can register additional - hooks, and they can even wrap the service factory and alter the service - instance itself, if needed. -**Next**: [Simple services][2] +**Next**: [Implicit services][2] [1]: ./01-intro-to-di.md -[2]: ./03-simple-services.md +[2]: ./03-implicit-services.md diff --git a/docs/user/03-implicit-services.md b/docs/user/03-implicit-services.md new file mode 100644 index 0000000..b8670b0 --- /dev/null +++ b/docs/user/03-implicit-services.md @@ -0,0 +1,98 @@ +# Implicit services + +Simple services, which don't require any special options and whose dependencies +can be resolved automatically by the compiler, need only to be discoverable by +the compiler as exports from one of the resource files: + +```typescript +// this will register the ServiceOne class as a service and it will also +// automatically include any interfaces ServiceOne implements, as well as +// its ancestors and their interfaces, as aliases: +export class ServiceOne { + // ... +} + +// this will register a dynamic service of the type 'ServiceTwo'; any other +// interfaces which ServiceTwo extends will be registered as its aliases: +export interface ServiceTwo { + // ... +} + +// aside from classes and interfaces you can also export factory functions: +export function createServiceThree(): ServiceThree { + // ... +} +``` + +Services registered this way will not have a public service ID. They will have +autogenerated string identifiers beginning with a `#` character, but you're +strongly discouraged from using these, because they're a product of the +compilation process and can change at any time, even between compilations in +some cases. Instead, you should rely on injection to get instances of these +services. + +> There is a special case for classes which don't have any public constructors, +> but which have a static `create()` method. If such a class is found, the +> `create()` method will be used as its factory: +> +> ```typescript +> export class ServiceFour { +> static create(): ServiceFour { +> // ... +> } +> +> private constructor() { +> // ... +> } +> } +> ``` + +Service factories, whether they're functions or static `create()` methods, can +be `async`: + +```typescript +export async function loadConfig(): Promise { + return JSON.parse(await readFile('config.json', 'utf-8')); +} +``` + +Factories can also return `undefined` if a service cannot be instantiated at +runtime, allowing services to be _optional_: + +```typescript +interface LogWriter { + write(message: string): void; +} + +export function fileLogWriter(): LogWriter { + return process.env.LOG_FILE ? new FileLogWriter(process.env.LOG_FILE) : undefined; +} + +export function elasticLogWriter(): LogWriter { + return process.env.ELASTIC_DSN ? new ElasticLogWriter(process.env.ELASTIC_DSN) : undefined; +} +``` + +A service can also be a _list_ or an _iterable_: + +```typescript +export function * logWriterFactory(): Iterable { + if (process.env.LOG_FILE) { + yield new FileLogWriter(process.env.LOG_FILE); + } + + if (process.env.ELASTIC_DSN) { + yield new ElasticLogWriter(process.env.ELASTIC_DSN); + } +} +``` + + +When you need to do some more complex logic to create a service instance, or +when you want to provide other service configuration such as a scope or one or +more service hooks, you can export an _explicit service definition_ from +a resource file. + +**Next**: [Explicit service definitions][1] + +[1]: ./04-explicit-definitions.md diff --git a/docs/user/03-simple-services.md b/docs/user/03-simple-services.md deleted file mode 100644 index f4713be..0000000 --- a/docs/user/03-simple-services.md +++ /dev/null @@ -1,37 +0,0 @@ -# Simple services - -Simple services, which don't require any special options and whose dependencies -can be resolved automatically by the compiler, need only to be discoverable by -the compiler as exports from one of the resource files: - -```typescript -// this will register the ServiceOne class as a service and it will also -// automatically include any interfaces ServiceOne implements, as well as -// its ancestors and their interfaces, as aliases: -export { ServiceOne } from '../services'; - -// assuming ServiceTwoInterface is actually an interface, this will register -// a dynamic service of the ServiceTwoInterface type; any other interfaces which -// ServiceTwoInterface extends will be registered as its aliases: -export { ServiceTwoInterface } from '../services'; -``` - -Services registered this way will not have a public service ID. They will have -autogenerated string identifiers beginning with a `#` character, but you're -strongly discouraged from using these, because they're a product of the -compilation process and can change at any time, even between compilations in -some cases. Instead, you should rely on injection to get instances of these -services. You know, the thing you're using this library for. - -> There is a special case for classes which don't have any public constructors, -> but which have a static `create()` method: if such a class is found, it will -> be registered and the `create()` method will be used at runtime as the service -> factory. - -When you need to do some more complex logic to create a service instance, or -when you want to provide other service configuration such as a scope or one or -more service hooks, you can export a _service definition_ from a resource file. - -**Next**: [Explicit service definitions][1] - -[1]: ./04-explicit-definitions.md diff --git a/docs/user/04-explicit-definitions.md b/docs/user/04-explicit-definitions.md index f01184d..bb22aa3 100644 --- a/docs/user/04-explicit-definitions.md +++ b/docs/user/04-explicit-definitions.md @@ -7,14 +7,17 @@ As briefly mentioned before, an explicit service definition is a special import { ServiceDefinition } from 'dicc'; // the simplest kind of definition - an instantiable class service; -// note that this would be almost equivalent to just `export { ServiceOne }`, -// except that no aliases are automatically registered: +// note that the only difference between this and directly exporting +// ServiceOne is that the service will be public when defined like this: export const one = ServiceOne satisfies ServiceDefinition; -// a definition with an alias: +// a definition with an explicit alias: export const twoWithOneAlias = ServiceTwo satisfies ServiceDefinition; -// multiple aliases can be specified as a tuple: -export const twoWithMultipleAliases = ServiceTwo satisfies ServiceDefinition; +// multiple aliases can be specified as an intersection type: +export const twoWithMultipleAliases = ServiceTwo satisfies ServiceDefinition; +// implicit aliases from ancestor classes and interfaces implemented +// by a service class can be disabled entirely by specifying 'unknown': +export const twoWithNoAliases = ServiceTwo satisfies ServiceDefinition; // a definition using a factory function: export const three = (() => new ServiceThree()) satisfies ServiceDefinition; @@ -83,16 +86,6 @@ export const maybeSix = ( () => process.env.WITH_SIX ? new ServiceSix() : undefined ) satisfies ServiceDefinition; -// factories themselves can be undefined - this makes the service dynamic, -// that is, the compiler can include code to inject it as a dependency to other -// services, but the runtime container can't create it and instead you need to -// register it manually: -export const seven = undefined satisfies ServiceDefinition; -export const alsoSeven = { - factory: undefined, - onCreate() { console.log('Seven registered in the container!') }, -} satisfies ServiceDefinition; - // an explicit service definition can override factory arguments by name; // this is useful if you just need to override one or two arguments and // avoid repeating the full factory signature: @@ -165,13 +158,6 @@ container.get('controllers.listBooks'); // etc ``` -> An explicit service definition overrides a simple class or interface export of -> the same type, so you can simply `export * from '../services'` at the start of -> a resource file, and then follow that with explicit definitions for services -> which need some tweaks - such services won't be registered twice. If you do -> need to register a given service multiple times, you must provide explicit -> definitions for each registration. - Now that we know how to tell DICC about services, let's see how we can tell it what those services depend on. diff --git a/docs/user/05-injecting-dependencies.md b/docs/user/05-injecting-dependencies.md index 2616050..105ee7c 100644 --- a/docs/user/05-injecting-dependencies.md +++ b/docs/user/05-injecting-dependencies.md @@ -159,7 +159,7 @@ export class Logger { } ``` -Last, but not least, you can inject _iterables_ - this also allows you to inject +Similarly to arrays, you can inject _iterables_ - this also allows you to inject a bunch of services of the same type, but unlike injecting an array (or an accessor for an array), each service in the iterable will be lazily resolved when the iterable reaches it. Works for sync and async services: @@ -193,6 +193,23 @@ _think_ about it too much, because DICC will throw an error during compilation if you try to inject a non-async accessor or iterable for something which _is_ async. + +## Injecting the container + +Simply put, injecting the container into a service is intentionally impossible +in DICC. "Container-aware" services are a direct breach of the entire reason +for DICC to exist. But you don't need that: you can inject service accessors +and injectors instead of using container methods manually. The only remaining +reason for accessing the container directly in application code outside of +application entrypoints would be to call the `container.run()` method at the +start of e.g. HTTP requests; and to that end, you can instead declare +a dependency on a service implementing the `ScopedRunner` interface exported +from `dicc`; this interface declares the `run()` method with the same signature +as the container's, and the compiler will inject an appropriate implementation +into such a dependency, leaving the container safely separate from your code. + + **Next**: [Auto-generated service factories][1] + [1]: ./06-auto-factories.md diff --git a/docs/user/06-auto-factories.md b/docs/user/06-auto-factories.md index 30da0d9..6b0b44f 100644 --- a/docs/user/06-auto-factories.md +++ b/docs/user/06-auto-factories.md @@ -41,11 +41,6 @@ implementation for you and inject it where appropriate: export interface UserNotificationChannelFactory { create(username: string): UserNotificationChannel; } - -// or alternatively just as a call signature: -export interface UserNotificationChannelFactory { - (username: string): UserNotificationChannel; -} ``` Some notes on how it works: @@ -57,11 +52,41 @@ Some notes on how it works: service's arguments _by name_ during compilation. This means that the factory's arguments' positions and order can be arbitrary - they don't have to be in the same order as the target service's arguments. -- The generated factory will resolve dependencies of the target service lazily - when creating an instance of the target service; this means that if either - the target service itself or any of its dependencies is async, the factory - needs to return a Promise for the target service. An error will be thrown - during compilation if this requirement is not satisfied. +- The generated factory will attempt to resolve the target service's + dependencies as lazily as possible; but if the `create()` method doesn't + return a Promise and one or more of the target service's dependencies ends up + being async, the factory service itself will be made async and the async + dependencies will be resolved eagerly when creating an instance of the factory + class, so that the `create()` method can stay synchronous. + +Auto-generated factories can also be generated from abstract classes: if the +compiler encounters an abstract class with a single abstract `create()` method, +it will create a factory service by extending the class and implementing the +`create()` method using the same mechanism as for an interface. Such a factory +class can therefore serve other purposes than just creating an instance of the +target service: it can e.g. carry some metadata about the target service, so +that you can inject a list of factories and then determine at runtime which +target service you want to instantiate based on the metadata. This can be useful +e.g. for GraphQL class resolvers, where each resolver could have a corresponding +factory class which knows which operation the resolver belongs to, so that a +root resolver can examine the factory classes and instantiate the appropriate +resolver for an incoming GraphQL operation. + +## Auto-implemented accessor services + +A special case of auto-implemented factories are _accessor services_, which +are interfaces (or abstract classes) with a single (abstract) `get()` method +with no arguments. They behave exactly the same as auto-implemented factory +services, with the core distinction being that the target service isn't +unregistered from the container, and therefore its lifecycle can be managed +using the `scope` option as usual, instead of effectively making it `private` +as auto-factories do. + +Since auto-implemented factories and accessors can be derived from abstract +classes (and therefore can carry additional data available before accessing the +target service), you can use them for similar purposes as you would use service +tags in other DI implementations, while retaining strict typing. + **Next**: [Service decorators][1] diff --git a/docs/user/07-service-decorators.md b/docs/user/07-service-decorators.md index a9fc5a2..f56f857 100644 --- a/docs/user/07-service-decorators.md +++ b/docs/user/07-service-decorators.md @@ -37,14 +37,13 @@ export const notifyCreated = { } satisfies ServiceDecorator; ``` -Service decorators can add any of the three service lifecycle hooks; the +Service decorators can add any of the three service lifecycle hooks. The `onCreate` and `onDestroy` hooks follow the same semantics as if they were -registered on the service definitions, but the `onFork` hook works slightly -differently: if the service definition has an `onFork` hook which returns a -forked instance of the service, all the service's decorators will receive that -instance instead of the original service, and the decorators' `onFork` hooks' -return values are ignored, meaning a decorator's `onFork` hook cannot be used -to create a forked service instance. +registered on the service definitions. The `onFork` hook works slightly +differently: if the service definition has an `onFork` hook which passes +a forked instance of the service to the provided callback, all the service's +decorators' `onFork` hooks will receive that instance instead of +the original service. Service decorators can also be applied to the output of service factories: @@ -56,11 +55,9 @@ export const withLoggedMethodCalls = { } satisfies ServiceDecorator; ``` -Currently, the `decorate` hook must return either an instance of the same class -as the original service which was passed in, or a `Proxy` for the same. In the -future, decorators might possibly be given the ability to completely override -the service's type, although I will have to think hard on all the ramifications -of such a capability. +The `decorate` hook must return either an instance of the same class as the +original service which was passed in, or a descendant class, or a `Proxy` for +the same. Decorators may be given a numeric _priority_ to influence the order in which they are applied to decorated services. The `priority` option defaults to zero, @@ -68,9 +65,10 @@ and decorators are applied in descending order of priority. There are no predetermined priority levels, you can simply choose whichever numbers make sense for your use case. You can e.g. use negative numbers for decorators which need to run later than any default-priority decorators etc. The order of -decorators with the same priority is undefined. +decorators with the same priority is undefined. A service's own hooks are always +executed first, followed by any decorator hooks ordered by their priority. -**Next**: [Container parameters][1] +**Next**: [Merging containers][1] -[1]: ./08-container-parameters.md +[1]: ./08-merging-containers.md diff --git a/docs/user/08-container-parameters.md b/docs/user/08-container-parameters.md deleted file mode 100644 index 4eac294..0000000 --- a/docs/user/08-container-parameters.md +++ /dev/null @@ -1,85 +0,0 @@ -# Container parameters - -It may be useful to define a set of parameters which must be provided at runtime -when creating a container, for example to pass environment configuration to -services. One way to do this is to simply define services which hold the -parameters, resolve the actual parameter values in the service factories, and -then inject these services wherever you need access to the parameters. But DICC -has another mechanism built in for this purpose: _container parameters_. - -This feature is opt-in; to start using it, simply declare (and export from -one of your resource files) an interface which extends from the -`ContainerParameters` interface exported by `dicc`: - -```typescript -// src/bootstrap/definitions/parameters.ts -import { ContainerParameters } from 'dicc'; -import { DatabaseParameters } from '../../services/database/config'; -import { MailerParameters } from '../../services/mailer/config'; - -export interface AppParameters extends ContainerParameters { - database: DatabaseParameters; - mailer: MailerParameters; - runtime: { - env: 'production' | 'development' | 'test'; - debug: boolean; - cacheDir: string; - }; -} -``` - -> If you use other interfaces to define nested parameter types, be careful not -> to export those interfaces from your resource files, otherwise they will be -> registered as dynamic services, which will break injection. - -The compiled Container class will now require a single constructor argument of -the type `AppParameters`: - -```typescript -// src/bootstrap/container.ts -import * as parameters0 from './definitions/parameters'; - -interface Services { - // ... -} - -export class AppContainer extends Container { - constructor(parameters: parameters0.AppParameters) { - super(parameters, { /* ... */ }); - } -} -``` - -When creating an instance of the container, you'll need to pass in an -appropriate object manually. - -**But**: the DICC compiler will now be able to inject the parameters into your -services, as if they were themselves services - not only the root -`AppParameters` object, but also all named object types nested inside it (so -`DatabaseParameters` and `MailerParameters` in this example). And what's more, -you can use container parameters when overriding service factory arguments in -explicit service definitions: - -```typescript -// src/bootstrap/definitions/caching.ts -import { ServiceDefinition } from 'dicc'; -import { FileCache } from '../../services/caching'; - -export const imageCache = { - factory: FileCache, - args: { - basePath: '%runtime.cacheDir%/images', - }, -} satisfies ServiceDefinition; -``` - -If the argument value is a string containing a single parameter expansion, -e.g. `'%runtime.debug%'`, the result will be strictly typed according to the -parameter interface declaration. If there are multiple parameter expansions, -or any other characters, the result will be a `string`. Parameter expansion -works recursively, so `%runtime.cacheDir%` could itself be set to something -like `%runtime.tempDir%/cache/%runtime.env%`. - -**Next**: [Merging containers][1] - -[1]: ./09-merging-containers.md diff --git a/docs/user/08-merging-containers.md b/docs/user/08-merging-containers.md new file mode 100644 index 0000000..e0940c6 --- /dev/null +++ b/docs/user/08-merging-containers.md @@ -0,0 +1,41 @@ +# Merging containers + +You can easily create multiple containers simply by defining them as appropriate +in your DICC config file; this will be covered in the next chapter. And while +that in an of itself can be pretty useful, DICC also allows you to _merge_ +containers, wiring up some services in-between them. + +To merge two containers in this manner, simply define the container you wish to +merge _from_ (the _child container_) as a service within the container you wish +to merge _into_ (the _parent container_). The DICC compiler will recognise that +the service is a container, and on top of its regular autowiring duties, the +following will happen: + - DICC will set up an `onFork` hook for the child container service, so + that the child container's lifecycle is kept in sync with the parent. + - Any public services of the _child container_ will become available for + injection into services inside the _parent container_; if the child container + itself is registered as a public service of the parent, its public services + will also become public in the parent. Inside the parent container, public + services of public child container services will have their IDs prefixed with + the ID of the child container service. + - Any dynamic services in the _child container_ will be automatically injected + with any matching services from the _parent container_. Thus, the child + container can specify its external dependencies using dynamic services. + +Public services merged into the parent container will have their scope set to +`private` in the parent container, which will cause the parent container to +never store instances of those services and instead always ask the child +container for them, thus delegating the management of merged services' scope to +their original container. Starting an async local scope using `parent.run()` +will also propagate to all of its child containers. + +Merging containers allows you to e.g. define separate containers for DDD bounded +contexts and then merge them into a core container, propagating globally shared +services such as a global event dispatcher down into the BC containers, and +well-defined surface services (such as controllers / GraphQL resolvers) back +into the core container. + + +**Next**: [DICC config and compilation][1] + +[1]: ./09-config-and-compilation.md diff --git a/docs/user/10-config-and-compilation.md b/docs/user/09-config-and-compilation.md similarity index 72% rename from docs/user/10-config-and-compilation.md rename to docs/user/09-config-and-compilation.md index ba3b098..93763b6 100644 --- a/docs/user/10-config-and-compilation.md +++ b/docs/user/09-config-and-compilation.md @@ -14,6 +14,11 @@ applicable: # path to your project's tsconfig.json: project: './tsconfig.json' +# an optional map of compiler extensions, which run during compilation +# and can be used to do some advanced stuff; this will be expanded +# in a future chapter: +extensions: ~ + # required; a map of containers you wish to compile, # with path to the generated file as the key and the # container options as the value: @@ -27,8 +32,9 @@ containers: # use 'default' to make the class the default export: className: 'AppContainer' - # whether to type-check the generated container after compilation: - typeCheck: true + # whether to use dynamic imports inside service factories + # of async services, allowing faster application startup: + lazyImports: true # required; a map of : [options] pairs: resources: @@ -37,8 +43,9 @@ containers: # multiple files can be selected using globs: 'src/examples/**/*.ts': # exclude files or exported paths from scanning: - exclude: + excludePaths: - '**/__tests__/**' # you can exclude by file path or glob + excludeExports: - 'path.to.ExcludedClass' # or by object path ``` @@ -46,7 +53,7 @@ containers: Whenever you change service definitions you must re-run the compiler in order to get a matching container. This is done using the `dicc` executable shipped -with DICC. The executable has the following options: +with `dicc-cli`. The executable has the following options: ``` dicc [-v|--verbose] [-c |--config ] @@ -61,10 +68,13 @@ dicc [-v|--verbose] [-c |--config ] '.dicc.yml', in this order. ``` -The compiled container should be a deterministic product of your definitions, -so you can safely exclude it from version control. But versioning it probably -won't hurt anything, either; that way, you can speed up build times, and you'll -be able to see changes between compilations. +Whether you keep the compiled container file(s) in your VCS or not depends on +which compiler extensions (if any) you use. The default DICC compiler produces +deterministic output based on your service definitions, so the decision mostly +comes down to whether you prefer shorter build times or keeping generated code +outside VCS; but if you use extensions which produce different output based e.g. +on the environment, then you'll probably want to exclude the compiled containers +from VCS. ## Obtaining services @@ -85,4 +95,5 @@ Remember, a key attribute of dependency injection is that code doesn't know that there _is_ a DI container, and that includes obtaining services from the container. So the ideal way to write code is to wrap everything in services, specify inter-service dependencies as constructor arguments and have DICC -inject the dependencies wherever you need them. +inject the dependencies wherever you need them. Your code shouldn't be littered +with `container.get()` calls. diff --git a/docs/user/09-merging-containers.md b/docs/user/09-merging-containers.md deleted file mode 100644 index d566448..0000000 --- a/docs/user/09-merging-containers.md +++ /dev/null @@ -1,33 +0,0 @@ -# Merging containers - -You can easily create multiple containers simply by defining them as appropriate -in your DICC config file; this will be covered in the next chapter. And while -that in an of itself can be pretty useful, DICC also allows you to _merge_ -multiple containers, exposing public services of the merged container to the -parent container, which can then inject them into its own services. - -To merge two containers in this manner, simply define the container you wish to -merge _from_ as a service within the container you wish to merge _into_; DICC -will take care of the rest. If the merged container is registered as a _public -service_ (that is, using an explicit definition without setting the `anonymous` -flag to `true`), its public services will also become public in the parent -container; their service IDs will be prefixed with the merged container's own -service ID. If the merged container is registered as an anonymous service, its -public services will be registered as anonymous in the parent container. - -Merged services will have their scope set to `private` in the parent container, -which will cause the parent container to never store instances of those services -and instead always ask the merged container for them, thus delegating the -management of merged services' scope to their original container. Forking a -container will also fork all of its child containers. - -Since the merged container is just another service from the parent container's -perspective, its factory will be injected as usual, whether it's the container -class itself, or a factory function - so if the merged container declares -runtime parameters as described in the previous chapter, you can either register -a service factory which provides the parameters in the parent container, or you -can include the parameters in the parent container's own parameter tree. - -**Next**: [DICC config and compilation][1] - -[1]: ./10-config-and-compilation.md