UNPKG

@ts-ast-parser/core

Version:

Reflects a simplified version of the TypeScript AST for generating documentation

378 lines 12.2 kB
import { knownLibFiles } from './known-lib-files.js'; import { isBrowser } from './is-browser.js'; import ts from 'typescript'; // The root directory of the in memory file system const ROOT_DIR = 'root'; // The separator used in the in memory file system const SEP = '/'; var FSNodeType; (function (FSNodeType) { FSNodeType[FSNodeType["File"] = 0] = "File"; FSNodeType[FSNodeType["Directory"] = 1] = "Directory"; })(FSNodeType || (FSNodeType = {})); /** * System interface to interact with an in memory file system. * * All interaction of the TypeScript compiler with the operating system goes * through a System interface. * * This is a very simplistic approach. For complex use cases, * this may not be enough. */ export class InMemorySystem { constructor(files) { Object.defineProperty(this, "newLine", { enumerable: true, configurable: true, writable: true, value: '\n' }); Object.defineProperty(this, "useCaseSensitiveFileNames", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "args", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "_fs", { enumerable: true, configurable: true, writable: true, value: { type: FSNodeType.Directory, name: ROOT_DIR, children: [], parent: null, } }); const entries = files?.entries() ?? []; for (const [name, data] of entries) { this.writeFile(name, data); } } static async create(files) { const system = new InMemorySystem(files); if (isBrowser) { await system._writeLibFilesFromCDN(ts.version); } else { await system._writeLibFilesFromNodeModules(); } return system; } /** * This method is not implemented and calling it, will throw * an exception * * @param _content */ // eslint-disable-next-line @typescript-eslint/no-unused-vars write(_content) { throw new Error('Method \'write\' is not supported.'); } /** * Reads the data encoded inside a file * * @param path */ readFile(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); if (node?.type === FSNodeType.File) { return node.data; } return undefined; } writeFile(path, data) { const absolutePath = this.getAbsolutePath(path); this._setFile(absolutePath, data); } resolvePath(path) { return this.getAbsolutePath(path); } /** * Checks whether the file exists * * @param path * @returns True if the file exists, otherwise false */ fileExists(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); return node?.type === FSNodeType.File; } directoryExists(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); return node?.type === FSNodeType.Directory; } createDirectory(path) { const absolutePath = this.getAbsolutePath(path); this._setDirectory(absolutePath); } /** * This method is not implemented and calling it, will throw * an exception */ getExecutingFilePath() { throw new Error('Method \'getExecutingFilePath\' is not supported.'); } /** * The root directory name */ getCurrentDirectory() { return ROOT_DIR; } /** * Returns the directory names (not the absolute path) * * @param path - The path from where to search */ getDirectories(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); return node?.type === FSNodeType.Directory ? node.children.filter(d => d.type === FSNodeType.Directory).map(d => d.name) : []; } readDirectory(path, extensions) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); if (!node || node.type === FSNodeType.File) { return []; } const basePath = this._getPath(node); const fileNames = node.children.filter(f => f.type === FSNodeType.File).map(f => this.join(basePath, f.name)); if (!extensions) { return fileNames; } return fileNames.filter(f => extensions.some(ext => f.endsWith(ext))); } deleteFile(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); if (!node) { return; } if (node.type === FSNodeType.Directory) { throw new Error(`Unable to delete the file "${absolutePath}" because it's a directory.`); } const index = node.parent.children.findIndex(c => c.name === node.name); if (index < -1) { return; } node.parent.children.splice(index, 1); } /** * This method is not implemented and calling it, will throw * an exception */ exit() { throw new Error('Method \'exit\' is not supported.'); } /** * Normalizes the path based on the OS and makes it * relative to the current working directory. * * @param path */ normalizePath(path) { const absolutePath = this.getAbsolutePath(path); return absolutePath.split(SEP).slice(1).join(SEP); } /** * Returns the path of the directory * * @param path */ getDirectoryName(path) { return this.getAbsolutePath(path).split(SEP).slice(0, -1).join(SEP); } /** * Returns a string with the filename portion of the path * * @param path */ getBaseName(path) { const absolutePath = this.getAbsolutePath(path); const node = this._get(absolutePath); if (!node || node.type === FSNodeType.Directory) { return ''; } return node.name; } /** * Joins the segments using the path separator of the OS/Browser * * @param segments */ join(...segments) { return segments.join(SEP); } /** * Checks if the path is an absolute path. An absolute * path is a path that starts with the ROOT directory. * * @param path * @returns True if the path is absolute */ isAbsolute(path) { return path.startsWith(ROOT_DIR); } realpath(path) { return this.getAbsolutePath(path); } /** * Transforms the path to an absolute path. * * It imposes some restrictions about how path are handled to simplify things. * * The following restrictions will apply: * * - No directories with only dots in its name * - Paths are converted to lowercase * - In the name of the file you can have dots * * A few examples to show how the functions transforms the path: * * - "./foo.js" -> "<ROOT_DIR>/foo.js" * - "." -> "<ROOT_DIR>" * - "/foo.js" -> "<ROOT_DIR>/foo.js" * - "foo.js" -> "<ROOT_DIR>/foo.js" * - "Bar/foo.js" or "./Bar/foo.js" -> "<ROOT_DIR>/bar/foo.js" * * @param path */ getAbsolutePath(path) { // Convert segment to lowercase and remove dots const segments = path .toLowerCase() .split(SEP) .filter(segment => !!segment && !/^\.+$/.test(segment)); if (segments[0] === ROOT_DIR) { return segments.join(SEP); } return [ROOT_DIR, ...segments].join(SEP); } async _writeLibFilesFromNodeModules() { const m = await import('./get-lib-files-from-node.js'); const libFiles = m.getLibFilesFromNode(); const typeFiles = m.getTypesFromNode(); for (const [name, content] of libFiles.entries()) { this.writeFile(name, content); } for (const [name, content] of typeFiles.entries()) { this.writeFile(name, content); } } _writeLibFilesFromCDN(version) { const cdn = `https://typescript.azureedge.net/cdn/${version}/typescript/lib/`; const promises = []; for (const lib of knownLibFiles) { const promise = fetch(cdn + lib) .then(resp => resp.text()) .then(text => this.writeFile(lib, text)) .catch(err => console.error(err)); promises.push(promise); } return Promise.all(promises); } _get(absolutePath) { const segments = absolutePath.split(SEP).slice(1); // The first directory is ROOT if (!segments.length) { return this._fs; } let node = this._fs; for (const segment of segments) { if (!node) { break; } // There are still segments to process and // we are in a file node if (node.type === FSNodeType.File) { node = undefined; break; } node = node.children.find(c => c.name === segment); } return node; } _setFile(absolutePath, data) { const segments = absolutePath.split(SEP).slice(1); // The first directory is ROOT const parentDirectories = segments.slice(0, -1); const newFileName = segments[segments.length - 1] ?? ''; if (!newFileName) { throw new Error(`Empty file name found when trying to write to: "${absolutePath}".`); } let node = this._fs; for (const directoryName of parentDirectories) { const child = node.children.find(c => c.name === directoryName); if (child?.type === FSNodeType.File) { throw new Error(`Expected a directory but found a file when trying to write to: "${absolutePath}".`); } if (child?.type === FSNodeType.Directory) { node = child; continue; } const newChild = { type: FSNodeType.Directory, name: directoryName, children: [], parent: node, }; node.children.push(newChild); node = newChild; } const existingFileNode = node.children.find((f) => { return f.name === newFileName && f.type === FSNodeType.File; }); if (existingFileNode) { existingFileNode.data = data; return; } const newFileNode = { data, type: FSNodeType.File, name: newFileName, parent: node, }; node.children.push(newFileNode); } _setDirectory(absolutePath) { const segments = absolutePath.split(SEP).slice(1); // The first directory is ROOT let node = this._fs; for (const segment of segments) { const child = node.children.find(c => c.name === segment); if (child?.type === FSNodeType.File) { throw new Error(`Expected a directory but found a file when trying to write to: "${absolutePath}".`); } if (child?.type === FSNodeType.Directory) { node = child; continue; } const newChild = { type: FSNodeType.Directory, name: segment, children: [], parent: node, }; node.children.push(newChild); node = newChild; } } _getPath(node) { let curr = node; const path = []; while (curr) { path.push(curr.name); curr = curr.parent; } return path.reverse().join(SEP); } } //# sourceMappingURL=in-memory-system.js.map