ts-simple-ast
Version:
TypeScript compiler wrapper for AST navigation and code generation.
416 lines (414 loc) • 17.5 kB
JavaScript
"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