Skip to content

Commit

Permalink
feat: add AOT domain discovery (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mararok committed Jul 21, 2024
1 parent 148d1fa commit a17327f
Show file tree
Hide file tree
Showing 69 changed files with 1,194 additions and 673 deletions.
8 changes: 3 additions & 5 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ const jestConfig: JestConfigWithTsJest = {
preset: "ts-jest",
runner: "groups",
roots: [__dirname],
modulePaths: [__dirname],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>" }),
transform: {
'^.+\\.ts?$': ['<rootDir>/lib/Compiler/Jest', {
rootDir,
tsconfig: 'tsconfig.json',
astTransformers: {
before: [{ path: './lib/Compiler/transformer', options: { sourceRoot: rootDir + "/src" } }]
}
tsconfig: compilerOptions,
diagnostics: false,
}]
},
testMatch: ["<rootDir>/test/**/*.test.ts"],
Expand Down
108 changes: 86 additions & 22 deletions src/Compiler/Jest/HcJestTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
import { FeatureModuleDiscoverer } from '../../Util/FeatureModuleDiscoverer';
import { FeatureModuleDiscoverer } from '../../Util/Feature/FeatureModuleDiscoverer';
import type { AsyncTransformer, TransformedSource } from '@jest/transform';
import { hash } from 'node:crypto';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { TsJestTransformer, type TsJestTransformerOptions, type TsJestTransformOptions } from 'ts-jest';
import { TsTransfromerHelper } from '../Transformer/TsTransformerHelper';
import ts from 'typescript';
import { FeatureTsTransformer } from '../Transformer/Feature/FeatureTsTransformer';
import { FsHelper } from '@/Util/Filesystem/FsHelper';

export type HcJestTransformerOptions = TsJestTransformerOptions & { rootDir: string; };
export type HcJestTransformerOptions = TsJestTransformerOptions & { rootDir: string; tmpDir: string; };
export const HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH = '@hexancore/core/compiler/transformer';

export class HcJestTransformer implements AsyncTransformer<HcJestTransformerOptions> {
private sourceRoot!: string;
private compilerOptions: any;
private tsJestTransformer: TsJestTransformer;

private featureModuleDiscoveryHashMap: Map<string, string>;
private featureTsTransformer!: FeatureTsTransformer;
private featuresHashMap: Map<string, string>;

private tmpDir!: string;

protected constructor(options: HcJestTransformerOptions) {
this.processOptions(options);
this.tsJestTransformer = new TsJestTransformer(options);
this.featureModuleDiscoveryHashMap = new Map();
this.featuresHashMap = new Map();
}

private processOptions(options: HcJestTransformerOptions) {
this.compilerOptions = options.tsconfig;

options.rootDir = options.rootDir.replaceAll("\\", "/");
this.sourceRoot = options.rootDir + "/src";
options.tsconfig = options.tsconfig ?? `${options.rootDir}/tsconfig.test.json`;
options.astTransformers = options.astTransformers ?? ({});
options.astTransformers.before = options.astTransformers.before ?? [];

if (!options.astTransformers.before.find((t) => typeof t !== 'string' && (t.path === HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH || t.path === './lib/Compiler/transformer'))) {
options.astTransformers.before.push({
path: HC_TYPESCRIPT_TRANSFORMER_MODULE_PATH,
options: {
sourceRoot: this.sourceRoot
}
});
}

private setupTransformedTmpDir(options: TsJestTransformOptions): void {
if (this.tmpDir) {
return;
}

const projectHash = hash('md5', this.sourceRoot);

this.tmpDir = options.config.cacheDirectory + '/hcjest-' + projectHash;
this.tmpDir = FsHelper.normalizePathSep(this.tmpDir);
if (!existsSync(this.tmpDir)) {
mkdirSync(this.tmpDir, { recursive: true });
}
}

Expand All @@ -45,24 +60,69 @@ export class HcJestTransformer implements AsyncTransformer<HcJestTransformerOpti
const discoverer = new FeatureModuleDiscoverer(this.sourceRoot);
const features = await discoverer.discoverAll();
for (const [name, discovery] of features.entries()) {
this.featureModuleDiscoveryHashMap.set(name, discovery.cacheKey);
this.featuresHashMap.set(name, discovery.cacheKey);
}

this.featureTsTransformer = FeatureTsTransformer.create(this.sourceRoot, undefined, features, false);
}

public get canInstrument(): boolean {
return false;
}

public process(sourceText: string, sourcePath: string, options: TsJestTransformOptions): TransformedSource {
return this.tsJestTransformer.process(sourceText, sourcePath, options);
sourcePath = FsHelper.normalizePathSep(sourcePath);
const featureName = this.extractFeatureNameFromPath(sourcePath);
if (!featureName || !this.featureTsTransformer.supports(sourcePath, featureName)) {
return this.tsJestTransformer.process(sourceText, sourcePath, options);
}

return this.processFeatureSourceFile(featureName, sourceText, sourcePath, options);
}

public processAsync(
public async processAsync(
sourceText: string,
sourcePath: string,
options: TsJestTransformOptions,
): Promise<TransformedSource> {
return this.tsJestTransformer.processAsync(sourceText, sourcePath, options as any) as any;
sourcePath = FsHelper.normalizePathSep(sourcePath);
const featureName = this.extractFeatureNameFromPath(sourcePath);
if (!featureName || !this.featureTsTransformer.supports(sourcePath, featureName)) {
return this.tsJestTransformer.processAsync(sourceText, sourcePath, options);
}

return this.processFeatureSourceFile(featureName, sourceText, sourcePath, options);
}

private processFeatureSourceFile(featureName: string, sourceText: string, sourcePath: string, options: TsJestTransformOptions): TransformedSource {
this.setupTransformedTmpDir(options);

const inSourceFile = ts.createSourceFile(
sourcePath,
sourceText,
this.compilerOptions.target ?? ts.ScriptTarget.Latest
);

const transformed = ts.transform(inSourceFile, [(context: ts.TransformationContext) => (source) => this.featureTsTransformer.transform(source, context)], this.compilerOptions);
const outSourceFile = transformed.transformed[0];

const printed = TsTransfromerHelper.printFile(outSourceFile);
const tmpPath = this.tmpDir + '/' + featureName + '-' + hash('md5', sourcePath, 'hex').substring(0, 8) + '-' + path.basename(sourcePath);
writeFileSync(tmpPath, printed);

const outTranspile = ts.transpileModule(printed, {
compilerOptions: this.compilerOptions,
fileName: tmpPath
});

const sourceMap = JSON.parse(outTranspile.sourceMapText!);
sourceMap.file = tmpPath;
sourceMap.sources = [tmpPath];

return {
code: outTranspile.outputText,
map: sourceMap
};
}

public getCacheKey(sourceText: string, sourcePath: string, transformOptions: TsJestTransformOptions): string {
Expand All @@ -80,13 +140,17 @@ export class HcJestTransformer implements AsyncTransformer<HcJestTransformerOpti
return this.getCacheKey(sourceText, sourcePath, transformOptions);
}

private getExtraCacheKey(sourcePath: string,): string | null {
sourcePath = sourcePath.replaceAll("\\", "/");
const extracted = FeatureModuleDiscoverer.extractFeatureNameFromPath(this.sourceRoot, sourcePath);
private getExtraCacheKey(sourcePath: string): string | null {
sourcePath = FsHelper.normalizePathSep(sourcePath);
const extracted = this.extractFeatureNameFromPath(sourcePath);
if (extracted) {
return this.featureModuleDiscoveryHashMap.get(extracted)!;
return this.featuresHashMap.get(extracted)!;
}

return null;
}

private extractFeatureNameFromPath(sourcePath: string): string | null {
return FeatureModuleDiscoverer.extractFeatureNameFromPath(this.sourceRoot, sourcePath);
}
}
17 changes: 17 additions & 0 deletions src/Compiler/Transformer/Feature/AbstractFeatureTsTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ts from 'typescript';
import type { FeatureModuleMeta } from "../../../Util/Feature/FeatureModuleMeta";
import { ModuleClassTsTransformer } from '../ModuleClassTsTransformer';
import { type ImportFromMapper } from '../TsTransformerHelper';

export abstract class AbstractFeatureTsTransformer {
protected moduleClassTransformer: ModuleClassTsTransformer;

public constructor(protected importFromMapper: ImportFromMapper, needFixImportAccess = true) {
this.moduleClassTransformer = new ModuleClassTsTransformer(importFromMapper, needFixImportAccess);
}

public abstract transform(feature: FeatureModuleMeta, source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile;

public abstract supports(sourceFilePath: string, feature: FeatureModuleMeta): boolean;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import ts from 'typescript';
import type { FeatureModuleMeta } from "../../../Util/Feature/FeatureModuleMeta";
import { AbstractFeatureTsTransformer } from './AbstractFeatureTsTransformer';
import { TsTransfromerHelper } from '../TsTransformerHelper';
import type { AddImportTransformDef } from '../ModuleClassTsTransformer';

export class FeatureInfraDomainModuleTsTransformer extends AbstractFeatureTsTransformer {

public transform(feature: FeatureModuleMeta, source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile {
const domainErrorsClassName = feature.name + "DomainErrors";

const imports: AddImportTransformDef[] = [
{ name: "DomainInfraModuleHelper", importModuleSpecifier: '@hexancore/core' },
{ name: domainErrorsClassName, importModuleSpecifier: '../../Domain'}
];

const repos: string[] = [];

for (const r of feature.domain.aggregateRoots) {
repos.push(r.infraRepositoryName);
imports.push(
{ name: r.name, importModuleSpecifier: `../../Domain/${r.name}/${r.name}` },
{ name: r.infraRepositoryName, importModuleSpecifier: `./${r.name}/${r.infraRepositoryName}` }
);

for (const e of r.entities) {
repos.push(e.infraRepositoryName);
imports.push({ name: e.infraRepositoryName, importModuleSpecifier: `./${r.name}/${e.infraRepositoryName}` });
}
}

return this.moduleClassTransformer.transform({
imports,
extraStatementProvider(importedIdentifierMapper) {
const classIdentifier = importedIdentifierMapper("DomainInfraModuleHelper");
const methodIdentifier = ts.factory.createIdentifier("createMeta");

const optionsObject = ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment("featureName", ts.factory.createStringLiteral(feature.name)),
ts.factory.createPropertyAssignment("aggregateRootCtrs", ts.factory.createArrayLiteralExpression(feature.domain.aggregateRoots.map((r) => ts.factory.createIdentifier(r.name)))),
ts.factory.createPropertyAssignment("domainErrors", ts.factory.createIdentifier(domainErrorsClassName)),
]);

const createMeta = TsTransfromerHelper.createConstStatement("HcDomainInfraModuleMetaExtra", ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(classIdentifier, methodIdentifier),
undefined,
[optionsObject]
));

return [
TsTransfromerHelper.createConstStatement("HcDomainInfraAggrgateRootRepositories",
ts.factory.createArrayLiteralExpression(repos.map((r) => ts.factory.createIdentifier(r)))
),
createMeta
];
},

extraMetaProvider() {
return ts.factory.createIdentifier("HcDomainInfraModuleMetaExtra");
},
source,
context
});
}

public supports(sourcefilePath: string, feature: FeatureModuleMeta): boolean {
return sourcefilePath.endsWith("DomainInfraModule.ts");
}
}
64 changes: 64 additions & 0 deletions src/Compiler/Transformer/Feature/FeatureModuleTsTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import ts from 'typescript';
import type { FeatureApplicationMessageMeta, FeatureModuleMeta } from "../../../Util/Feature/FeatureModuleMeta";
import { AbstractFeatureTsTransformer } from "./AbstractFeatureTsTransformer";
import type { ProviderModuleMetaTransformDef } from '../ModuleClassTsTransformer';

/**
* Adding automatic injection of message handlers, services, infra module to `[Feature]Module` source.
* Less write, more fun !
*/
export class FeatureModuleTsTransformer extends AbstractFeatureTsTransformer {

public supports(sourcefilePath: string, feature: FeatureModuleMeta): boolean {
return sourcefilePath.endsWith(feature.name + "Module.ts");
}

public transform(feature: FeatureModuleMeta, source: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile {

const messageHandlersProviders: ProviderModuleMetaTransformDef[] = [];
messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.commands));
messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.queries));
messageHandlersProviders.push(...this.createMessageHandlerProviders(feature.application.events));

return this.moduleClassTransformer.transform({
imports: [],
meta: {
imports: [],
providers: [...messageHandlersProviders, ...this.createServiceProviders(feature)],
},
source,
context
});
}

private createMessageHandlerProviders(messages: FeatureApplicationMessageMeta[]): ProviderModuleMetaTransformDef[] {
const providers: ProviderModuleMetaTransformDef[] = [];
for (const m of messages) {
const importPath = `./${m.path}/${m.handlerClassName}`;
providers.push({
addToExports: false,
name: m.handlerClassName,
importFrom: importPath
});
}

return providers;
}

private createServiceProviders(feature: FeatureModuleMeta): ProviderModuleMetaTransformDef[] {
const providers: ProviderModuleMetaTransformDef[] = [];
for (const s of feature.application.services) {
if (!s.isInjectable) {
continue;
}

providers.push({
addToExports: false,
name: s.className,
importFrom: `./${s.path}`
});
}

return providers;
}
}
Loading

0 comments on commit a17327f

Please sign in to comment.