Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add AOT domain discovery
Browse files Browse the repository at this point in the history
Mararok committed Jul 21, 2024

Verified

This commit was signed with the committer’s verified signature.
Mararok Mararok
1 parent 148d1fa commit 514bfca
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
@@ -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"],
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 });
}
}

@@ -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 {
@@ -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 514bfca

Please sign in to comment.