UNPKG

ts-simple-ast

Version:

TypeScript compiler wrapper for AST navigation and code generation.

416 lines (414 loc) 17.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const ts = require("typescript"); const errors = require("./../../errors"); const manipulation_1 = require("./../../manipulation"); const textSeek_1 = require("./../../manipulation/textSeek"); const utils_1 = require("./../../utils"); const callBaseFill_1 = require("./../callBaseFill"); const base_1 = require("./../base"); const common_1 = require("./../common"); const statement_1 = require("./../statement"); // todo: not sure why I need to explicitly type this in order to get VS to not complain... (TS 2.4.1) exports.SourceFileBase = base_1.TextInsertableNode(statement_1.StatementedNode(common_1.Node)); class SourceFile extends exports.SourceFileBase { /** * Initializes a new instance. * @internal * @param global - Global container. * @param node - Underlying node. */ constructor(global, node) { // start hack :( super(global, node, undefined); /** @internal */ this._isSaved = false; this.sourceFile = this; // end hack } /** * Fills the node from a structure. * @param structure - Structure to fill. */ fill(structure) { callBaseFill_1.callBaseFill(exports.SourceFileBase.prototype, this, structure); if (structure.imports != null) this.addImports(structure.imports); if (structure.exports != null) this.addExports(structure.exports); return this; } /** * @internal */ replaceCompilerNode(compilerNode) { super.replaceCompilerNode(compilerNode); this.global.resetProgram(); // make sure the program has the latest source file this._isSaved = false; } /** * Gets the file path. */ getFilePath() { return this.compilerNode.fileName; } /** * Copy this source file to a new file. * @param filePath - A new file path. Can be relative to the original file or an absolute path. */ copy(filePath) { const absoluteFilePath = utils_1.FileUtils.getAbsoluteOrRelativePathFromPath(filePath, utils_1.FileUtils.getDirPath(this.getFilePath())); return this.global.compilerFactory.addSourceFileFromText(absoluteFilePath, this.getFullText()); } /** * Asynchronously saves this file with any changes. */ save() { return __awaiter(this, void 0, void 0, function* () { yield utils_1.FileUtils.ensureDirectoryExists(this.global.fileSystem, utils_1.FileUtils.getDirPath(this.getFilePath())); yield this.global.fileSystem.writeFile(this.getFilePath(), this.getFullText()); this._isSaved = true; }); } /** * Synchronously saves this file with any changes. */ saveSync() { utils_1.FileUtils.ensureDirectoryExistsSync(this.global.fileSystem, utils_1.FileUtils.getDirPath(this.getFilePath())); this.global.fileSystem.writeFileSync(this.getFilePath(), this.getFullText()); this._isSaved = true; } /** * Gets any referenced files. */ getReferencedFiles() { // todo: add tests const dirPath = utils_1.FileUtils.getDirPath(this.getFilePath()); return (this.compilerNode.referencedFiles || []) .map(f => this.global.compilerFactory.getSourceFileFromFilePath(utils_1.FileUtils.pathJoin(dirPath, f.fileName))) .filter(f => f != null); } /** * Gets the source files for any type reference directives. */ getTypeReferenceDirectives() { // todo: add tests const dirPath = utils_1.FileUtils.getDirPath(this.getFilePath()); return (this.compilerNode.typeReferenceDirectives || []) .map(f => this.global.compilerFactory.getSourceFileFromFilePath(utils_1.FileUtils.pathJoin(dirPath, f.fileName))) .filter(f => f != null); } /** * Gets the source file language variant. */ getLanguageVariant() { return this.compilerNode.languageVariant; } /** * Gets if this is a declaration file. */ isDeclarationFile() { return this.compilerNode.isDeclarationFile; } /** * Gets if this source file has been saved or if the latest changes have been saved. */ isSaved() { return this._isSaved; } /** * Sets if this source file has been saved. * @internal */ setIsSaved(value) { this._isSaved = value; } /** * Add an import. * @param structure - Structure that represents the import. */ addImport(structure) { return this.addImports([structure])[0]; } /** * Add imports. * @param structures - Structures that represent the imports. */ addImports(structures) { const imports = this.getImports(); const insertIndex = imports.length === 0 ? 0 : imports[imports.length - 1].getChildIndex() + 1; return this.insertImports(insertIndex, structures); } /** * Insert an import. * @param index - Index to insert at. * @param structure - Structure that represents the import. */ insertImport(index, structure) { return this.insertImports(index, [structure])[0]; } /** * Insert imports into a file. * @param index - Index to insert at. * @param structures - Structures that represent the imports to insert. */ insertImports(index, structures) { const newLineChar = this.global.manipulationSettings.getNewLineKind(); const indentationText = this.getChildIndentationText(); const texts = structures.map(structure => { const hasNamedImport = structure.namedImports != null && structure.namedImports.length > 0; let code = `${indentationText}import`; // validation if (hasNamedImport && structure.namespaceImport != null) throw new errors.InvalidOperationError("An import declaration cannot have both a namespace import and a named import."); // default import if (structure.defaultImport != null) { code += ` ${structure.defaultImport}`; if (hasNamedImport || structure.namespaceImport != null) code += ","; } // namespace import if (structure.namespaceImport != null) code += ` * as ${structure.namespaceImport}`; // named imports if (structure.namedImports != null && structure.namedImports.length > 0) { const namedImportsCode = structure.namedImports.map(n => { let namedImportCode = n.name; if (n.alias != null) namedImportCode += ` as ${n.alias}`; return namedImportCode; }).join(", "); code += ` {${namedImportsCode}}`; } // from keyword if (structure.defaultImport != null || hasNamedImport || structure.namespaceImport != null) code += " from"; code += ` "${structure.moduleSpecifier}";`; return code; }); return this._insertMainChildren(index, texts, structures, ts.SyntaxKind.ImportDeclaration, undefined, { previousBlanklineWhen: previousMember => !(utils_1.TypeGuards.isImportDeclaration(previousMember)), nextBlanklineWhen: nextMember => !(utils_1.TypeGuards.isImportDeclaration(nextMember)), separatorNewlineWhen: () => false }); } /** * Gets the first import declaration that matches a condition, or undefined if it doesn't exist. * @param condition - Condition to get the import by. */ getImport(condition) { return this.getImports().find(condition); } /** * Gets the first import declaration that matches a condition, or throws if it doesn't exist. * @param condition - Condition to get the import by. */ getImportOrThrow(condition) { return errors.throwIfNullOrUndefined(this.getImport(condition), "Expected to find an import with the provided condition."); } /** * Get the file's import declarations. */ getImports() { // todo: remove type assertion return this.getChildSyntaxListOrThrow().getChildrenOfKind(ts.SyntaxKind.ImportDeclaration); } /** * Add an export. * @param structure - Structure that represents the export. */ addExport(structure) { return this.addExports([structure])[0]; } /** * Add exports. * @param structures - Structures that represent the exports. */ addExports(structures) { // always insert at end of file because of export {Identifier}; statements return this.insertExports(this.getChildSyntaxListOrThrow().getChildCount(), structures); } /** * Insert an export. * @param index - Index to insert at. * @param structure - Structure that represents the export. */ insertExport(index, structure) { return this.insertExports(index, [structure])[0]; } /** * Insert exports into a file. * @param index - Index to insert at. * @param structures - Structures that represent the exports to insert. */ insertExports(index, structures) { const newLineChar = this.global.manipulationSettings.getNewLineKind(); const stringChar = this.global.manipulationSettings.getStringChar(); const indentationText = this.getChildIndentationText(); const texts = structures.map(structure => { const hasModuleSpecifier = structure.moduleSpecifier != null && structure.moduleSpecifier.length > 0; let code = `${indentationText}export`; if (structure.namedExports != null && structure.namedExports.length > 0) { const namedExportsCode = structure.namedExports.map(n => { let namedExportCode = n.name; if (n.alias != null) namedExportCode += ` as ${n.alias}`; return namedExportCode; }).join(", "); code += ` {${namedExportsCode}}`; } else if (!hasModuleSpecifier) code += " {}"; else code += " *"; if (hasModuleSpecifier) code += ` from ${stringChar}${structure.moduleSpecifier}${stringChar}`; code += `;`; return code; }); return this._insertMainChildren(index, texts, structures, ts.SyntaxKind.ExportDeclaration, undefined, { previousBlanklineWhen: previousMember => !(utils_1.TypeGuards.isExportDeclaration(previousMember)), nextBlanklineWhen: nextMember => !(utils_1.TypeGuards.isExportDeclaration(nextMember)), separatorNewlineWhen: () => false }); } /** * Gets the first export declaration that matches a condition, or undefined if it doesn't exist. * @param condition - Condition to get the export by. */ getExport(condition) { return this.getExports().find(condition); } /** * Gets the first export declaration that matches a condition, or throws if it doesn't exist. * @param condition - Condition to get the export by. */ getExportOrThrow(condition) { return errors.throwIfNullOrUndefined(this.getExport(condition), "Expected to find an export with the provided condition."); } /** * Get the file's export declarations. */ getExports() { // todo: remove type assertion return this.getChildSyntaxListOrThrow().getChildrenOfKind(ts.SyntaxKind.ExportDeclaration); } /** * Gets the default export symbol of the file. */ getDefaultExportSymbol() { const sourceFileSymbol = this.getSymbol(); // will be undefined when the source file doesn't have an export if (sourceFileSymbol == null) return undefined; return sourceFileSymbol.getExportByName("default"); } /** * Gets the default export symbol of the file or throws if it doesn't exist. */ getDefaultExportSymbolOrThrow() { return errors.throwIfNullOrUndefined(this.getDefaultExportSymbol(), "Expected to find a default export symbol"); } /** * Gets the compiler diagnostics. */ getDiagnostics() { // todo: implement cancellation token const compilerDiagnostics = ts.getPreEmitDiagnostics(this.global.program.compilerObject, this.compilerNode); return compilerDiagnostics.map(d => this.global.compilerFactory.getDiagnostic(d)); } /** * Removes any "export default"; */ removeDefaultExport(defaultExportSymbol) { defaultExportSymbol = defaultExportSymbol || this.getDefaultExportSymbol(); if (defaultExportSymbol == null) return this; const declaration = defaultExportSymbol.getDeclarations()[0]; if (declaration.compilerNode.kind === ts.SyntaxKind.ExportAssignment) manipulation_1.removeChildrenWithFormatting({ children: [declaration], getSiblingFormatting: () => manipulation_1.FormattingKind.Newline }); else if (utils_1.TypeGuards.isModifierableNode(declaration)) { declaration.toggleModifier("default", false); declaration.toggleModifier("export", false); } return this; } unindent(positionRangeOrPos, times = 1) { return this.indent(positionRangeOrPos, times * -1); } indent(positionRangeOrPos, times = 1) { if (times === 0) return this; const sourceFileText = this.getFullText(); const positionRange = typeof positionRangeOrPos === "number" ? [positionRangeOrPos, positionRangeOrPos] : positionRangeOrPos; errors.throwIfRangeOutOfRange(positionRange, [0, sourceFileText.length], "positionRange"); const startLinePos = textSeek_1.getPreviousMatchingPos(sourceFileText, positionRange[0], char => char === "\n"); const endLinePos = textSeek_1.getNextMatchingPos(sourceFileText, positionRange[1], char => char === "\r" || char === "\n"); const stringNodeRanges = this.getDescendants().filter(n => utils_1.isStringNode(n)).map(n => [n.getStart(), n.getEnd()]); const indentText = this.global.manipulationSettings.getIndentationText(); const unindentRegex = times > 0 ? undefined : new RegExp(getDeindentRegexText()); let pos = startLinePos; const newLines = []; for (const line of sourceFileText.substring(startLinePos, endLinePos).split("\n")) { if (stringNodeRanges.some(n => n[0] < pos && n[1] > pos)) newLines.push(line); else if (times > 0) newLines.push(indentText.repeat(times) + line); else newLines.push(line.replace(unindentRegex, "")); pos += line.length; } manipulation_1.replaceSourceFileTextForFormatting({ sourceFile: this, newText: sourceFileText.substring(0, startLinePos) + newLines.join("\n") + sourceFileText.substring(endLinePos) }); return this; function getDeindentRegexText() { const isSpaces = /^ +$/; let text = "^"; for (let i = 0; i < Math.abs(times); i++) { text += "("; if (isSpaces.test(indentText)) { // the optional string makes it possible to unindent when a line doesn't have the full number of spaces for (let j = 0; j < indentText.length; j++) text += " ?"; } else text += indentText; text += "|\t)?"; } return text; } } /** * Emits the source file. */ emit(options) { return this.global.program.emit(Object.assign({ targetSourceFile: this }, options)); } /** * Formats the source file text using the internal typescript printer. * * WARNING: This will dispose any previously navigated descendant nodes. */ formatText(opts = {}) { const printer = ts.createPrinter({ newLine: utils_1.newLineKindToTs(this.global.manipulationSettings.getNewLineKind()), removeComments: opts.removeComments || false }); const newText = printer.printFile(this.compilerNode); const replacementSourceFile = this.global.compilerFactory.createTempSourceFileFromText(newText, this.getFilePath()); this.getChildren().forEach(d => d.dispose()); // this will dispose all the descendants this.replaceCompilerNode(replacementSourceFile.compilerNode); } } exports.SourceFile = SourceFile; //# sourceMappingURL=SourceFile.js.map