UNPKG

@carlosv2/glue

Version:

Dependency injection library that stays out of the way

220 lines (219 loc) 9.55 kB
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); } }