@carlosv2/glue
Version:
Dependency injection library that stays out of the way
220 lines (219 loc) • 9.55 kB
JavaScript
import { Processor } from '../processor.js';
import { isString, library } from '../utils.js';
import { DiError } from '../error.js';
import { Instantiation } from '../compilable/instantiation.js';
import { Importation } from '../compilable/importation.js';
import ts from 'typescript';
import { readFileSync } from 'node:fs';
import { Importer } from '../importer.js';
import { fileExists, getFullPath, glob } from '../filesystem.js';
const { factory, NodeFlags, SyntaxKind, ScriptTarget, createPrinter, createSourceFile, isClassDeclaration, isVariableStatement, isFunctionDeclaration, } = ts;
export class Compiler extends Processor {
constructor() {
super(...arguments);
this.extensions = [];
this.parameters = {};
this.aliases = {};
this.services = {};
}
glob(pattern) {
return glob(pattern);
}
getFullPath(baseFile, relative, shouldExist = true) {
const path = getFullPath(baseFile, relative);
if (!shouldExist || fileExists(path)) {
return path;
}
return relative;
}
readContents(path) {
return readFileSync(path, { encoding: 'utf-8' });
}
addExtend(path) {
this.extensions.push(path);
}
addParameter(id, parameter) {
this.parameters[id] = parameter;
}
addAlias(id, alias) {
this.aliases[id] = alias;
}
addService(id, service) {
this.services[id] = service;
}
getCompilableContext(context) {
return new Instantiation(Importation.named(`${library}/context`, 'DefinitionContext'), [context.getPath(), context.getId(), context.getDefinition()]);
}
processEnvStrValue(context, name, fallback) {
return new Instantiation(Importation.named(`${library}/value`, 'EnvStr'), [
this.getCompilableContext(context),
name,
fallback,
]);
}
processEnvBoolValue(context, name, fallback) {
return new Instantiation(Importation.named(`${library}/value`, 'EnvBool'), [
this.getCompilableContext(context),
name,
fallback,
]);
}
processEnvNumValue(context, name, fallback) {
return new Instantiation(Importation.named(`${library}/value`, 'EnvNum'), [
this.getCompilableContext(context),
name,
fallback,
]);
}
processEvalValue(context, code) {
return new Instantiation(Importation.named(`${library}/value`, 'Eval'), [
this.getCompilableContext(context),
code,
]);
}
processLiteralValue(context, value) {
return new Instantiation(Importation.named(`${library}/value`, 'Literal'), [
this.getCompilableContext(context),
value,
]);
}
processParameterValue(context, id) {
return new Instantiation(Importation.named(`${library}/value`, 'Parameter'), [this.getCompilableContext(context), id]);
}
processServiceValue(context, id) {
return new Instantiation(Importation.named(`${library}/value`, 'Service'), [
this.getCompilableContext(context),
id,
]);
}
getExportedToken(context, path, constraint) {
var _a;
const filepath = this.getFullPath(context.getPath(), path);
const code = readFileSync(filepath, { encoding: 'utf8' });
let token = undefined;
for (const statement of createSourceFile(filepath, code, ScriptTarget.ES2022).statements) {
const isExported = ((_a = statement.modifiers) !== null && _a !== void 0 ? _a : []).find(modifier => modifier.kind === SyntaxKind.ExportKeyword);
if (isExported && constraint(statement)) {
if (token) {
throw new DiError('Multiple exported symbols where found.', context);
}
else if (isClassDeclaration(statement) ||
isFunctionDeclaration(statement)) {
token = statement.name.text;
}
else if (isVariableStatement(statement)) {
token = statement.declarationList.declarations[0].name
.escapedText;
}
else {
throw new DiError('Unable to determine token name from statement.', context);
}
}
}
if (token) {
return token;
}
throw new DiError('A suitable exported symbol was not found.', context);
}
processSymbolValue(context, path, name) {
// Importing module. For example: (my/path
if (!isString(name)) {
return new Instantiation(Importation.named(`${library}/value`, 'Symbol'), [
this.getCompilableContext(context),
Importation.module(this.getFullPath(context.getPath(), path)),
]);
}
// Importing the default symbol. For example: (my/path)
if (name.length === 0) {
return new Instantiation(Importation.named(`${library}/value`, 'Symbol'), [
this.getCompilableContext(context),
Importation.default(this.getFullPath(context.getPath(), path)),
]);
}
let token = name;
// Importing the only exported symbol. For example: (my/path)~
if (name === '~') {
token = this.getExportedToken(context, path, () => true);
}
// Importing the only exported class. For example: (my/path)~class
if (name === '~class') {
token = this.getExportedToken(context, path, isClassDeclaration);
}
// Importing the only exported function. For example: (my/path)~func
if (name === '~func') {
token = this.getExportedToken(context, path, isFunctionDeclaration);
}
// Imporint the symbol found before or a known one. For example: (my/path)MyObj
return new Instantiation(Importation.named(`${library}/value`, 'Symbol'), [
this.getCompilableContext(context),
Importation.named(this.getFullPath(context.getPath(), path), token),
]);
}
processTagListValue(context, name) {
return new Instantiation(Importation.named(`${library}/value`, 'TagList'), [
this.getCompilableContext(context),
name,
]);
}
processTagObjectValue(context, name) {
return new Instantiation(Importation.named(`${library}/value`, 'TagObject'), [this.getCompilableContext(context), name]);
}
processAlias(context, aliased) {
return new Instantiation(Importation.named(`${library}/alias`, 'Alias'), [
this.getCompilableContext(context),
aliased,
]);
}
processConstructorService(context, symbol, args, scope, tags, calls) {
return new Instantiation(Importation.named(`${library}/service`, 'Constructor'), [symbol, args, this.getCompilableContext(context), scope, tags, calls]);
}
processFactoryService(context, symbol, factory, args, scope, tags, calls) {
return new Instantiation(Importation.named(`${library}/service`, 'Factory'), [
symbol,
factory,
args,
this.getCompilableContext(context),
scope,
tags,
calls,
]);
}
processPropertyService(context, symbol, property, scope, tags, calls) {
return new Instantiation(Importation.named(`${library}/service`, 'Property'), [
symbol,
property,
this.getCompilableContext(context),
scope,
tags,
calls,
]);
}
compileCall(importer, loader, method, id, compilable) {
return factory.createExpressionStatement(factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier(loader), factory.createIdentifier(method)), undefined, [factory.createStringLiteral(id), compilable.compile(importer)]));
}
compile() {
const loader = 'loader';
const importer = new Importer();
const extensions = this.extensions.map(path => {
return factory.createExpressionStatement(factory.createCallExpression(factory.createIdentifier(importer.default(path)), undefined, [factory.createIdentifier(loader)]));
});
const parameters = Object.entries(this.parameters).map(([id, parameter]) => this.compileCall(importer, loader, 'addParameter', id, parameter));
const aliases = Object.entries(this.aliases).map(([id, alias]) => this.compileCall(importer, loader, 'addAlias', id, alias));
const services = Object.entries(this.services).map(([id, service]) => this.compileCall(importer, loader, 'addService', id, service));
const file = factory.createSourceFile([
...importer.compile(),
factory.createFunctionDeclaration([
factory.createToken(SyntaxKind.ExportKeyword),
factory.createToken(SyntaxKind.DefaultKeyword),
], undefined, undefined, undefined, [
factory.createParameterDeclaration(undefined, undefined, factory.createIdentifier(loader), undefined, undefined, undefined),
], undefined, factory.createBlock([
...extensions,
...parameters,
...aliases,
...services,
])),
], factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None);
return createPrinter().printFile(file);
}
}