UNPKG

@jfconley/di-compiler

Version:

A Custom Transformer for Typescript that enables compile-time Dependency Injection

1,023 lines (1,005 loc) 53.7 kB
import * as TS from 'typescript'; import TS__default from 'typescript'; import { ensureNodeFactory } from 'compatfactory'; import path from 'crosspath'; import { evaluate } from 'ts-evaluator'; import fs from 'fs'; import path$1 from 'path'; import os from 'os'; import crypto from 'crypto'; const CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER = `___CTOR_ARGS___`; const DI_CONTAINER_NAME = "DIContainer"; /** * A TypeNode such as IFoo<string> should still yield the service name "IFoo". * This helper generates a proper service name from a TypeNode */ function pickServiceOrImplementationName(node, context) { const { typescript } = context; if (typescript.isTypeReferenceNode(node)) { return pickServiceOrImplementationName(node.typeName, context); } else if (typescript.isIndexedAccessTypeNode(node)) { return `${pickServiceOrImplementationName(node.objectType, context)}[${pickServiceOrImplementationName(node.indexType, context)}]`; } else { return node.getText().trim(); } } function getModifierLikes(node) { var _a, _b; if ("decorators" in node && Array.isArray(node.decorators)) { return [...((_a = node.decorators) !== null && _a !== void 0 ? _a : []), ...((_b = node.modifiers) !== null && _b !== void 0 ? _b : [])]; } else { return node.modifiers; } } function visitClassLikeDeclaration(options) { const { node, childContinuation, continuation, context } = options; const { typescript, factory } = context; const constructorDeclaration = node.members.find(typescript.isConstructorDeclaration); // If there are no constructor declaration for the ClassLikeDeclaration, there's nothing to do if (constructorDeclaration == null) { return childContinuation(node); } const updatedClassMembers = [ ...node.members.map(continuation), factory.createGetAccessorDeclaration([factory.createModifier(typescript.SyntaxKind.StaticKeyword)], factory.createComputedPropertyName(factory.createIdentifier(`Symbol.for("${CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER}")`)), [], undefined, factory.createBlock([factory.createReturnStatement(getParameterTypeNamesAsArrayLiteral(constructorDeclaration.parameters, context))])) ]; const modifierLikes = getModifierLikes(node); if (typescript.isClassDeclaration(node)) { return factory.updateClassDeclaration(node, modifierLikes, node.name, node.typeParameters, node.heritageClauses, updatedClassMembers); } else { return factory.updateClassExpression(node, modifierLikes, node.name, node.typeParameters, node.heritageClauses, updatedClassMembers); } } /** * Takes ConstructorParams for the given NodeArray of ParameterDeclarations */ function getParameterTypeNamesAsArrayLiteral(parameters, context) { const { factory } = context; const constructorParams = []; for (let i = 0; i < parameters.length; i++) { const parameter = parameters[i]; // If the parameter has no type, there's nothing to extract if (parameter.type == null) { constructorParams[i] = factory.createIdentifier("undefined"); } else { constructorParams[i] = factory.createNoSubstitutionTemplateLiteral(pickServiceOrImplementationName(parameter.type, context)); } } return factory.createArrayLiteralExpression(constructorParams); } // For some TypeScript versions, such as 3.1, these helpers are not exposed by TypeScript, // so they will have to be duplicated and reused from here in these rare cases const HELPERS = { importDefaultHelper: { name: "typescript:commonjsimportdefault", scoped: false, text: '\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { "default": mod };\n};' }, importStarHelper: { name: "typescript:commonjsimportstar", scoped: false, text: '\nvar __importStar = (this && this.__importStar) || function (mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n result["default"] = mod;\n return result;\n};' } }; function needsImportPreservationLogic(typescriptOrContext, compilerOptionsOrUndefined) { const typescript = arguments.length >= 2 ? typescriptOrContext : typescriptOrContext.typescript; const compilerOptions = arguments.length >= 2 ? compilerOptionsOrUndefined : typescriptOrContext.compilerOptions; // If value imports shouldn't always be preserved, we'll have to perform import preservation logic if (!Boolean(compilerOptions.preserveValueImports)) return true; // Only TypeScript v4.5 and newer supports the `preserValueImports` Compiler option if (parseFloat(typescript.version) < 4.5) return true; switch (compilerOptions.module) { case typescript.ModuleKind.AMD: case typescript.ModuleKind.UMD: case typescript.ModuleKind.CommonJS: case typescript.ModuleKind.System: case typescript.ModuleKind.None: // None of these module systems support the `preserValueImports` Compiler option return true; default: return false; } } function getImportDefaultHelper(typescript) { var _a; return (_a = typescript.importDefaultHelper) !== null && _a !== void 0 ? _a : HELPERS.importDefaultHelper; } function getImportStarHelper(typescript) { var _a; return (_a = typescript.importStarHelper) !== null && _a !== void 0 ? _a : HELPERS.importStarHelper; } function moduleKindSupportsImportHelpers(moduleKind = TS.ModuleKind.CommonJS, typescript) { switch (moduleKind) { case typescript.ModuleKind.CommonJS: case typescript.ModuleKind.UMD: case typescript.ModuleKind.AMD: return true; default: return false; } } function moduleKindDefinesDependencies(moduleKind = TS.ModuleKind.CommonJS, typescript) { switch (moduleKind) { case typescript.ModuleKind.UMD: case typescript.ModuleKind.AMD: return true; default: return false; } } function getUnscopedHelperName(context, helperName) { const typescript = context.typescript; if ("getUnscopedHelperName" in typescript) { return typescript.getUnscopedHelperName(helperName); } else if ("createEmitHelperFactory" in typescript) { return typescript.createEmitHelperFactory(context.transformationContext).getUnscopedHelperName(helperName); } else { return typescript.getHelperName(helperName); } } function getRootBlockInsertionPosition(rootBlock, typescript) { let insertPosition = 0; for (let i = 0; i < rootBlock.statements.length; i++) { const statement = rootBlock.statements[i]; const isUseStrict = typescript.isExpressionStatement(statement) && typescript.isStringLiteralLike(statement.expression) && statement.expression.text === "use strict"; const isEsModuleSymbol = typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && typescript.isPropertyAccessExpression(statement.expression.expression) && typescript.isIdentifier(statement.expression.expression.expression) && typescript.isIdentifier(statement.expression.expression.name) && statement.expression.expression.expression.text === "Object" && statement.expression.expression.name.text === "defineProperty" && statement.expression.arguments.length >= 2 && typescript.isIdentifier(statement.expression.arguments[0]) && statement.expression.arguments[0].text === "exports" && typescript.isStringLiteralLike(statement.expression.arguments[1]) && statement.expression.arguments[1].text === "__esModule"; if (isUseStrict || isEsModuleSymbol) { insertPosition = Math.max(insertPosition, i + 1); } } return insertPosition; } function getDefineArrayLiteralExpression(sourceFile, context) { const { compilerOptions, typescript } = context; switch (compilerOptions.module) { case typescript.ModuleKind.ESNext: case typescript.ModuleKind.ES2015: case typescript.ModuleKind.ES2020: case typescript.ModuleKind.ES2022: case typescript.ModuleKind.NodeNext: // There are no such thing for these module types return undefined; // If we're targeting UMD, the root block won't be the root scope, but the Function Body of an iife case typescript.ModuleKind.UMD: { for (const statement of sourceFile.statements) { if (typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && typescript.isParenthesizedExpression(statement.expression.expression) && typescript.isFunctionExpression(statement.expression.expression.expression) && statement.expression.expression.expression.parameters.length === 1) { const [firstParameter] = statement.expression.expression.expression.parameters; if (typescript.isIdentifier(firstParameter.name)) { if (firstParameter.name.text === "factory") { for (const subStatement of statement.expression.expression.expression.body.statements) { if (typescript.isIfStatement(subStatement) && subStatement.elseStatement != null && typescript.isIfStatement(subStatement.elseStatement) && typescript.isBlock(subStatement.elseStatement.thenStatement)) { for (const subSubStatement of subStatement.elseStatement.thenStatement.statements) { if (typescript.isExpressionStatement(subSubStatement) && typescript.isCallExpression(subSubStatement.expression) && subSubStatement.expression.arguments.length === 2 && typescript.isIdentifier(subSubStatement.expression.expression) && subSubStatement.expression.expression.text === "define") { const [firstSubSubStatementExpressionArgument] = subSubStatement.expression.arguments; if (typescript.isArrayLiteralExpression(firstSubSubStatementExpressionArgument)) { return firstSubSubStatementExpressionArgument; } } } } } } } } } break; } case typescript.ModuleKind.AMD: { for (const statement of sourceFile.statements) { if (typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && typescript.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "define" && statement.expression.arguments.length === 2) { const [firstArgument, secondArgument] = statement.expression.arguments; if (typescript.isArrayLiteralExpression(firstArgument)) { if (typescript.isFunctionExpression(secondArgument) && secondArgument.parameters.length >= 2) { const [firstParameter, secondParameter] = secondArgument.parameters; if (typescript.isIdentifier(firstParameter.name) && typescript.isIdentifier(secondParameter.name) && firstParameter.name.text === "require" && secondParameter.name.text === "exports") { return firstArgument; } } } } } break; } } return undefined; } function getRootBlock(sourceFile, context) { const { compilerOptions, typescript } = context; switch (compilerOptions.module) { // If we're targeting UMD, the root block won't be the root scope, but the Function Body of an iife case typescript.ModuleKind.UMD: { for (const statement of sourceFile.statements) { if (typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && statement.expression.arguments.length === 1) { const [firstArgument] = statement.expression.arguments; if (typescript.isFunctionExpression(firstArgument) && firstArgument.parameters.length === 2) { const [firstParameter, secondParameter] = firstArgument.parameters; if (typescript.isIdentifier(firstParameter.name) && typescript.isIdentifier(secondParameter.name) && firstParameter.name.text === "require" && secondParameter.name.text === "exports") { return firstArgument.body; } } } } break; } // If we're targeting AMD, the root block won't be the root scope, but the Function Body of the // anonymous function provided as a second argument to the define() function case typescript.ModuleKind.AMD: { for (const statement of sourceFile.statements) { if (typescript.isExpressionStatement(statement) && typescript.isCallExpression(statement.expression) && typescript.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "define" && statement.expression.arguments.length === 2) { const [, secondArgument] = statement.expression.arguments; if (typescript.isFunctionExpression(secondArgument) && secondArgument.parameters.length >= 2) { const [firstParameter, secondParameter] = secondArgument.parameters; if (typescript.isIdentifier(firstParameter.name) && typescript.isIdentifier(secondParameter.name) && firstParameter.name.text === "require" && secondParameter.name.text === "exports") { return secondArgument.body; } } } } break; } } return sourceFile; } function isImportedSymbolImported(importedSymbol, rootBlock, context) { const { compilerOptions, typescript } = context; switch (compilerOptions.module) { case typescript.ModuleKind.ES2022: case typescript.ModuleKind.ES2020: case typescript.ModuleKind.ES2015: case typescript.ModuleKind.ESNext: case typescript.ModuleKind.NodeNext: { for (const statement of rootBlock.statements) { if (!typescript.isImportDeclaration(statement)) continue; if (!typescript.isStringLiteralLike(statement.moduleSpecifier)) { continue; } if (statement.moduleSpecifier.text !== importedSymbol.moduleSpecifier) { continue; } if (statement.importClause == null) { continue; } if ("isDefaultImport" in importedSymbol) { if (importedSymbol.isDefaultImport) { if (statement.importClause.name == null) { continue; } if (statement.importClause.name.text !== importedSymbol.name) { continue; } return true; } else { if (statement.importClause.namedBindings == null) continue; if (!typescript.isNamedImports(statement.importClause.namedBindings)) { continue; } for (const importSpecifier of statement.importClause.namedBindings.elements) { if (importSpecifier.name.text !== importedSymbol.name) continue; return true; } } } else if ("isNamespaceImport" in importedSymbol) { if (statement.importClause.namedBindings == null) continue; if (!typescript.isNamespaceImport(statement.importClause.namedBindings)) { continue; } if (statement.importClause.namedBindings.name.text !== importedSymbol.name) { continue; } return true; } } return false; } case typescript.ModuleKind.CommonJS: case typescript.ModuleKind.AMD: case typescript.ModuleKind.UMD: { for (const statement of rootBlock.statements) { if (!typescript.isVariableStatement(statement)) continue; for (const declaration of statement.declarationList.declarations) { if (!typescript.isIdentifier(declaration.name)) continue; if (declaration.name.text !== importedSymbol.name) continue; return true; } } } } // TODO: Add support for other module systems return false; } function generateImportStatementForImportedSymbolInContext(importedSymbol, context) { const { compilerOptions, typescript, factory } = context; switch (compilerOptions.module) { case typescript.ModuleKind.ES2022: case typescript.ModuleKind.ES2020: case typescript.ModuleKind.ES2015: case typescript.ModuleKind.ESNext: case typescript.ModuleKind.NodeNext: { return factory.createImportDeclaration(undefined, "isDefaultImport" in importedSymbol ? factory.createImportClause(false, !importedSymbol.isDefaultImport ? undefined : factory.createIdentifier(importedSymbol.name), importedSymbol.isDefaultImport ? undefined : factory.createNamedImports([ factory.createImportSpecifier(false, importedSymbol.propertyName === importedSymbol.name ? undefined : factory.createIdentifier(importedSymbol.propertyName), factory.createIdentifier(importedSymbol.name)) ])) : "isNamespaceImport" in importedSymbol ? factory.createImportClause(false, undefined, factory.createNamespaceImport(factory.createIdentifier(importedSymbol.name))) : undefined, factory.createStringLiteral(importedSymbol.moduleSpecifier)); } case typescript.ModuleKind.CommonJS: case typescript.ModuleKind.AMD: case typescript.ModuleKind.UMD: { const requireCall = factory.createCallExpression(factory.createIdentifier("require"), undefined, [factory.createStringLiteral(importedSymbol.moduleSpecifier)]); let wrappedRequireCall = requireCall; // We'll need to use a helper, '__importDefault', and wrap the require call with it if (compilerOptions.esModuleInterop === true && (("isDefaultImport" in importedSymbol && importedSymbol.isDefaultImport) || (!("isDefaultImport" in importedSymbol) && importedSymbol.isNamespaceImport))) { // If tslib is being used, we can do something like 'require("tslib").__import{Default|Star}(<requireCall>)' if (compilerOptions.importHelpers === true) { wrappedRequireCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createCallExpression(factory.createIdentifier("require"), undefined, [factory.createStringLiteral("tslib")]), getUnscopedHelperName(context, "isDefaultImport" in importedSymbol ? "__importDefault" : "__importStar")), undefined, [requireCall]); } // Otherwise, we'll have to make sure that the helper is being inlined in an transformation step later else { // We've already requested the __importDefault helper in the before transformer under these // circumstances wrappedRequireCall = factory.createCallExpression(getUnscopedHelperName(context, "isDefaultImport" in importedSymbol ? "__importDefault" : "__importStar"), undefined, [ requireCall ]); } } return factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(importedSymbol.name), undefined, undefined, wrappedRequireCall)], typescript.NodeFlags.Const)); } } // TODO: Handle other module types as well return undefined; } /** * Ensures that the given item is an array */ function ensureArray(item) { return Array.isArray(item) ? item : [item]; } /** * Converts the given string to a boolean */ function booleanize(str) { if (str == null) return false; if (typeof str === "boolean") return str; if (isTrueLike(str)) { return true; } else if (isFalseLike(str)) { return false; } else { return Boolean(str); } } function isTrueLike(str) { if (typeof str === "boolean") return str === true; if (str == null) return false; switch (str.toLowerCase().trim()) { case "true": case "yes": case "1": case "": return true; default: return false; } } function isFalseLike(str) { if (typeof str === "boolean") return str === false; if (str == null) return true; switch (str.toLowerCase().trim()) { case "false": case "no": case "0": return true; default: return false; } } const sha1 = (data) => crypto.createHash("sha1").update(data).digest("hex"); const NOOP = () => { // Noop }; function visitCallExpression(options) { var _a, _b, _c, _d, _e, _f, _g; const { node, childContinuation, continuation, context, addTslibDefinition, requireImportedSymbol } = options; const { typescript, factory, compilerOptions, transformationContext, needsImportPreservationLogic } = context; const diMethod = getDiMethodName(node.expression, context); if (diMethod != null) { switch (diMethod) { case "get": case "has": { // If no type arguments are given, don't modify the node at all if (node.typeArguments == null || node.typeArguments[0] == null) { return childContinuation(node); } const [firstTypeArgument] = node.typeArguments; return factory.updateCallExpression(node, node.expression, node.typeArguments, [ factory.createObjectLiteralExpression([ factory.createPropertyAssignment("identifier", factory.createStringLiteral(((_b = (_a = firstTypeArgument.getFirstToken()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : firstTypeArgument.getText()).trim())) ]) ]); } case "registerSingleton": case "registerTransient": { const [typeArg, secondTypeArg] = ((_c = node.typeArguments) !== null && _c !== void 0 ? _c : []); const [firstArgument] = (_d = node.arguments) !== null && _d !== void 0 ? _d : []; // The user may explicitly pass 'undefined' as a value here, which shouldn't count as a custom implementation const customImplementation = firstArgument == null || (typescript.isIdentifier(firstArgument) && firstArgument.text === "undefined") ? undefined : firstArgument; const implementationArg = // If another implementation is passed, used that one instead (_e = customImplementation !== null && customImplementation !== void 0 ? customImplementation : // If not implementation is provided, use the type argument *as* the implementation secondTypeArg) !== null && _e !== void 0 ? _e : typeArg; if (typeArg == null || implementationArg == null) { return childContinuation(node); } const typeArgText = pickServiceOrImplementationName(typeArg, context); const implementationArgText = pickServiceOrImplementationName(implementationArg, context); // If the Implementation is a TypeNode, and if it originates from an ImportDeclaration, it may be stripped from the file since Typescript won't Type-check the updates from // a CustomTransformer and such a node would normally be removed from the imports. // to fix it, add an ImportDeclaration if needed. This is only needed if `preserveValueImports` is falsy if (needsImportPreservationLogic && customImplementation == null) { const matchingImport = findMatchingImportDeclarationForIdentifier(implementationArgText, options); if (matchingImport != null && typescript.isStringLiteralLike(matchingImport.importDeclaration.moduleSpecifier)) { switch (matchingImport.kind) { case "default": { // Log a request for the __importDefault helper already if we will // need it in a later transformation step if (moduleKindSupportsImportHelpers(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers !== true) { transformationContext.requestEmitHelper(getImportDefaultHelper(typescript)); } // Log a request for adding 'tslib' to the define([...]) array for the current // module system if it relies on declaring dependencies (such as UMD, AMD, and SystemJS does) if (moduleKindDefinesDependencies(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers === true) { addTslibDefinition(); } requireImportedSymbol({ isDefaultImport: true, moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, name: matchingImport.identifier.text, propertyName: matchingImport.identifier.text }); break; } case "namedImport": { requireImportedSymbol({ isDefaultImport: false, moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, name: matchingImport.importSpecifier.name.text, propertyName: (_g = (_f = matchingImport.importSpecifier.propertyName) === null || _f === void 0 ? void 0 : _f.text) !== null && _g !== void 0 ? _g : matchingImport.importSpecifier.name.text }); break; } case "namespace": { // Log a request for the __importStar helper already if you will // need it in a later transformation step if (moduleKindSupportsImportHelpers(compilerOptions.module, typescript) && compilerOptions.esModuleInterop === true && compilerOptions.importHelpers !== true) { transformationContext.requestEmitHelper(getImportStarHelper(typescript)); } requireImportedSymbol({ isNamespaceImport: true, moduleSpecifier: matchingImport.importDeclaration.moduleSpecifier.text, name: matchingImport.identifier.text }); break; } } } } return factory.updateCallExpression(node, node.expression, node.typeArguments, [ customImplementation == null ? factory.createIdentifier("undefined") : continuation(implementationArg), factory.createObjectLiteralExpression([ factory.createPropertyAssignment("identifier", factory.createNoSubstitutionTemplateLiteral(typeArgText)), ...(customImplementation != null ? [] : [factory.createPropertyAssignment("implementation", factory.createIdentifier(rewriteImplementationName(implementationArgText, options)))]) ]) ]); } } } return childContinuation(node); } function findMatchingImportDeclarationForIdentifier(identifier, options) { var _a; const { sourceFile, context: { typescript } } = options; // Find the matching import const importDeclarations = sourceFile.statements.filter(typescript.isImportDeclaration); for (const importDeclaration of importDeclarations) { if (importDeclaration.importClause == null) continue; // Default import if (((_a = importDeclaration.importClause.name) === null || _a === void 0 ? void 0 : _a.text) === identifier) { return { importDeclaration, kind: "default", identifier: importDeclaration.importClause.name }; } else if (importDeclaration.importClause.namedBindings != null) { if (typescript.isNamespaceImport(importDeclaration.importClause.namedBindings)) { if (importDeclaration.importClause.namedBindings.name.text === identifier) { return { importDeclaration, kind: "namespace", identifier: importDeclaration.importClause.namedBindings.name }; } } else { for (const importSpecifier of importDeclaration.importClause.namedBindings.elements) { if (importSpecifier.name.text === identifier) { return { importDeclaration, kind: "namedImport", importSpecifier: importSpecifier }; } } } } } // No import was matched return undefined; } function rewriteImplementationName(name, options) { var _a; const { context: { typescript, compilerOptions } } = options; switch (compilerOptions.module) { case typescript.ModuleKind.ES2022: case typescript.ModuleKind.ES2020: case typescript.ModuleKind.ES2015: case typescript.ModuleKind.ESNext: return name; case typescript.ModuleKind.CommonJS: case typescript.ModuleKind.AMD: case typescript.ModuleKind.UMD: { // Find the matching import const match = findMatchingImportDeclarationForIdentifier(name, options); if (match == null) { return name; } switch (match.kind) { case "default": return `${name}.default`; case "namespace": return name; case "namedImport": return `${name}.${((_a = match.importSpecifier.propertyName) !== null && _a !== void 0 ? _a : match.importSpecifier.name).text}`; } // Fall back to returning the original name return name; } default: // TODO: Add support for SystemJS here return name; } } function getDiMethodName(node, context) { if (!context.typescript.isPropertyAccessExpression(node) && !context.typescript.isElementAccessExpression(node)) { return undefined; } // If it is an element access expression, evaluate the argument expression if (context.typescript.isElementAccessExpression(node)) { // Do nothing at this point if this isn't a DIContainer instance, as we can avoid invoking evaluate at this point if (!isDiContainerInstance(node, context)) { return undefined; } const evaluationResult = context.evaluate(node.argumentExpression); // If no value could be computed, or if the value isn't of type string, do nothing if (!evaluationResult.success || typeof evaluationResult.value !== "string") { return undefined; } else { return isDiContainerMethodName(evaluationResult.value) ? evaluationResult.value : undefined; } } else { // If the name is any of the relevant ones, assert that it is invoked on an instance of DIContainer return isDiContainerMethodName(node.name.text) && isDiContainerInstance(node, context) ? node.name.text : undefined; } } function isDiContainerMethodName(name) { switch (name) { case "get": case "has": case "registerSingleton": case "registerTransient": return true; default: return false; } } function isDiContainerInstance(node, context) { var _a, _b, _c; if ("typeChecker" in context) { // Don't proceed unless the left-hand expression is the DIServiceContainer const type = context.typeChecker.getTypeAtLocation(node.expression); if (type == null || type.symbol == null || type.symbol.escapedName !== DI_CONTAINER_NAME) { return false; } } else { // If one or more variable names were passed in, check those directly if (context.identifier != null && context.identifier.length > 0) { // Pick the left-hand side of the expression here const name = ((_b = (_a = node.expression.getFirstToken()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : node.expression.getText()).trim(); // If not a single matcher matches the text, this does not represent an instance of DIContainer. if (!ensureArray(context.identifier).some(matcher => name === matcher)) { return false; } } else { // Otherwise, attempt to resolve the value of the expression and check if it is an instance of DIContainer const evaluationResult = context.evaluate(node.expression); if (!evaluationResult.success || evaluationResult.value == null || typeof evaluationResult.value !== "object" || ((_c = evaluationResult.value.constructor) === null || _c === void 0 ? void 0 : _c.name) !== DI_CONTAINER_NAME) { return false; } } } return true; } function visitNode$1(options) { if (options.context.typescript.isClassLike(options.node)) { return visitClassLikeDeclaration({ ...options, node: options.node }); } else if (options.context.typescript.isCallExpression(options.node)) { return visitCallExpression({ ...options, node: options.node }); } return options.childContinuation(options.node); } function beforeTransformer(context) { return transformationContext => { var _a; const factory = ensureNodeFactory((_a = transformationContext.factory) !== null && _a !== void 0 ? _a : context.typescript); return sourceFile => transformSourceFile$1(sourceFile, { ...context, transformationContext, factory }); }; } function transformSourceFile$1(sourceFile, context) { const requiredImportedSymbolSet = new Set(); /** * An optimization in which every imported symbol is converted into * a string that can be matched against directly to guard against * duplicates */ const requiredImportedSymbolSetFlags = new Set(); if (context.needsImportPreservationLogic) { context.sourceFileToAddTslibDefinition.set(sourceFile.fileName, false); context.sourceFileToRequiredImportedSymbolSet.set(sourceFile.fileName, requiredImportedSymbolSet); } const computeImportedSymbolFlag = (symbol) => ["name", "propertyName", "moduleSpecifier", "isNamespaceImport", "isDefaultImport"] .map(property => { var _a; return `${property}:${(_a = symbol[property]) !== null && _a !== void 0 ? _a : false}`; }) .join("|"); const visitorOptions = { context, addTslibDefinition: () => { if (!context.needsImportPreservationLogic) return; context.sourceFileToAddTslibDefinition.set(sourceFile.fileName, true); }, requireImportedSymbol: (importedSymbol) => { if (!context.needsImportPreservationLogic) return; // Guard against duplicates and compute a string so we can do // constant time lookups to compare against existing symbols const flag = computeImportedSymbolFlag(importedSymbol); if (requiredImportedSymbolSetFlags.has(flag)) return; requiredImportedSymbolSetFlags.add(flag); requiredImportedSymbolSet.add(importedSymbol); }, continuation: node => visitNode$1({ ...visitorOptions, sourceFile, node }), childContinuation: node => context.typescript.visitEachChild(node, cbNode => visitNode$1({ ...visitorOptions, sourceFile, node: cbNode }), context.transformationContext) }; return visitorOptions.continuation(sourceFile); } function visitRootBlock(options) { var _a; const { node, sourceFile, context } = options; const { typescript } = context; const leadingExtraStatements = []; for (const importedSymbol of (_a = context.sourceFileToRequiredImportedSymbolSet.get(sourceFile.fileName)) !== null && _a !== void 0 ? _a : new Set()) { if (isImportedSymbolImported(importedSymbol, node, context)) continue; const missingImportStatement = generateImportStatementForImportedSymbolInContext(importedSymbol, context); if (missingImportStatement != null) { leadingExtraStatements.push(missingImportStatement); } } const insertPosition = getRootBlockInsertionPosition(node, typescript); return [...node.statements.slice(0, insertPosition), ...leadingExtraStatements, ...node.statements.slice(insertPosition)]; } function visitRootBlockSourceFile(options) { const { node, context } = options; const { factory } = context; return factory.updateSourceFile(node, visitRootBlock(options), node.isDeclarationFile, node.referencedFiles, node.typeReferenceDirectives, node.hasNoDefaultLib, node.libReferenceDirectives); } function visitRootBlockBlock(options) { const { node, context } = options; const { factory } = context; return factory.updateBlock(node, visitRootBlock(options)); } function visitDefineArrayLiteralExpression(options) { var _a; const { node, sourceFile, context } = options; const { typescript, factory } = context; const trailingExtraExpressions = []; for (const importedSymbol of (_a = context.sourceFileToRequiredImportedSymbolSet.get(sourceFile.fileName)) !== null && _a !== void 0 ? _a : new Set()) { // Skip the node if it is already declared as a dependency if (node.elements.some(element => typescript.isStringLiteralLike(element) && element.text === importedSymbol.moduleSpecifier)) { continue; } trailingExtraExpressions.push(factory.createStringLiteral(importedSymbol.moduleSpecifier)); } if (context.sourceFileToAddTslibDefinition.get(sourceFile.fileName) === true) { trailingExtraExpressions.push(factory.createStringLiteral("tslib")); } if (trailingExtraExpressions.length < 1) { return node; } return factory.updateArrayLiteralExpression(node, [...node.elements, ...trailingExtraExpressions]); } function visitNode(options) { const { node, childContinuation, defineArrayLiteralExpression, rootBlock, context: { typescript } } = options; if (typescript.isSourceFile(node) && rootBlock === node) { return visitRootBlockSourceFile({ ...options, node }); } else if (typescript.isBlock(node) && rootBlock === node) { return visitRootBlockBlock({ ...options, node }); } else if (typescript.isArrayLiteralExpression(node) && defineArrayLiteralExpression === node) { return visitDefineArrayLiteralExpression({ ...options, node }); } return childContinuation(options.node); } function afterTransformer(context) { return transformationContext => { var _a; const factory = ensureNodeFactory((_a = transformationContext.factory) !== null && _a !== void 0 ? _a : context.typescript); return sourceFile => transformSourceFile(sourceFile, { ...context, transformationContext, factory }); }; } function transformSourceFile(sourceFile, context) { // For TypeScript versions below 3.5, there may be instances // where EmitHelpers such as __importDefault or __importStar is duplicated. // For these TypeScript versions, well have to guard against this behavior if (sourceFile.emitNode != null && sourceFile.emitNode.helpers != null) { const seenNames = new Set(); const filtered = sourceFile.emitNode.helpers.filter(helper => { if (seenNames.has(helper.name)) return false; seenNames.add(helper.name); return true; }); // Reassign the emitNodes if they changed if (filtered.length !== sourceFile.emitNode.helpers.length) { sourceFile.emitNode.helpers = filtered; } } const visitorOptions = { context, defineArrayLiteralExpression: getDefineArrayLiteralExpression(sourceFile, context), rootBlock: getRootBlock(sourceFile, context), continuation: node => visitNode({ ...visitorOptions, sourceFile, node }), childContinuation: node => context.typescript.visitEachChild(node, cbNode => visitNode({ ...visitorOptions, sourceFile, node: cbNode }), context.transformationContext) }; return visitorOptions.continuation(sourceFile); } /** * Shim the @wessberg/di module */ const EVALUATE_MODULE_OVERRIDES = { "@wessberg/di": { [DI_CONTAINER_NAME]: class { } } }; function getBaseVisitorContext({ typescript = TS__default, ...rest } = {}) { var _a; // Prepare a VisitorContext const visitorContextShared = { sourceFileToAddTslibDefinition: new Map(), sourceFileToRequiredImportedSymbolSet: new Map() }; if ("program" in rest) { const typeChecker = rest.program.getTypeChecker(); const compilerOptions = rest.program.getCompilerOptions(); return { ...rest, ...visitorContextShared, needsImportPreservationLogic: needsImportPreservationLogic(typescript, compilerOptions), typescript, typeChecker, compilerOptions, evaluate: node => evaluate({ node, typeChecker, typescript, moduleOverrides: EVALUATE_MODULE_OVERRIDES }) }; } else { const compilerOptions = (_a = rest.compilerOptions) !== null && _a !== void 0 ? _a : typescript.getDefaultCompilerOptions(); return { identifier: [], ...rest, ...visitorContextShared, needsImportPreservationLogic: needsImportPreservationLogic(typescript, compilerOptions), typescript, compilerOptions, evaluate: node => evaluate({ node, typescript, moduleOverrides: EVALUATE_MODULE_OVERRIDES }) }; } } /** * CustomTransformer that associates constructor arguments with any given class declaration */ function di(options) { const baseVisitorContext = getBaseVisitorContext(options); return { before: [beforeTransformer(baseVisitorContext)], after: baseVisitorContext.needsImportPreservationLogic ? [afterTransformer(baseVisitorContext)] : [] }; } function transform(source, filenameOrOptions, optionsOrUndefined) { var _a, _b; const filename = typeof filenameOrOptions === "string" ? filenameOrOptions : "file.ts"; const options = typeof filenameOrOptions === "string" ? optionsOrUndefined : filenameOrOptions; const baseVisitorContext = getBaseVisitorContext(options); // By preserving value imports, we can avoid the `after` transformer entirely, // as well as adding/tracking imports,since nothing will be stripped away. baseVisitorContext.compilerOptions.preserveValueImports = true; const { compilerOptions } = baseVisitorContext; const typescript = baseVisitorContext.typescript; const hash = generateCacheKey(source, baseVisitorContext, options); const cacheHit = hash == null ? undefined : (_a = options === null || options === void 0 ? void 0 : options.cache) === null || _a === void 0 ? void 0 : _a.get(hash); if (cacheHit != null) { return cacheHit; } const newLine = typescript.sys.newLine; const printer = ((_b = options === null || options === void 0 ? void 0 : options.printer) !== null && _b !== void 0 ? _b : typescript.createPrinter()); const factory = ensureNodeFactory(typescript); // An undocumented internal helper can be leveraged here const transformationContext = typescript.nullTransformationContext; const visitorContext = { ...baseVisitorContext, transformationContext, factory }; const sourceFile = typescript.createSourceFile(filename, source, typescript.ScriptTarget.ESNext, true, typescript.ScriptKind.TS); const transformedSourceFile = transformSourceFile$1(sourceFile, visitorContext); let result; if (Boolean(compilerOptions.sourceMap)) { const sourceMapOptions = { sourceMap: Boolean(compilerOptions.sourceMap), sourceRoot: "", mapRoot: "", extendedDiagnostics: false }; const emitHost = { getCanonicalFileName: typescript.createGetCanonicalFileName(typescript.sys.useCaseSensitiveFileNames), getCompilerOptions: () => compilerOptions, getCurrentDirectory: () => path.dirname(filename) }; const sourceMapGenerator = typescript.createSourceMapGenerator(emitHost, path.basename(filename), sourceMapOptions.sourceRoot, path.dirname(filename), sourceMapOptions); const writer = typescript.createTextWriter(newLine); printer.writeFile(transformedSourceFile, writer, sourceMapGenerator); const sourceMappingUrl = getSourceMappingUrl(sourceMapGenerator, filename, Boolean(compilerOptions.inlineSourceMap)); if (sourceMappingUrl.length > 0) { if (!writer.isAtStartOfLine()) writer.rawWrite(newLine); writer.writeComment("//# ".concat("sourceMappingURL", "=").concat(sourceMappingUrl)); // Tools can sometimes see this line as a source mapping url comment } result = { code: writer.getText(), map: Boolean(compilerOptions.inlineSourceMap) ? undefined : sourceMapGenerator.toString() }; } else { result = { code: printer.printFile(transformedSourceFile) }; } if (hash != null && (options === null || options === void 0 ? void 0 : options.cache) != null) { options.cache.set(hash, result); } return result; }