From c9c562afac4a115293b2e8d241ba74a03e6abcaf Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 24 Apr 2018 10:48:55 -0700 Subject: [PATCH] Reorganize and remove duplication --- src/harness/{core => }/collections.ts | 237 +------------------ src/harness/compiler.ts | 24 +- src/harness/compilerRunner.ts | 4 +- src/harness/core.ts | 4 - src/harness/core/comparers.ts | 43 ---- src/harness/core/functions.ts | 3 - src/harness/core/strings.ts | 130 ---------- src/harness/documents.ts | 8 +- src/harness/fakes.ts | 23 +- src/harness/harness.ts | 10 +- src/harness/harnessLanguageService.ts | 4 +- src/harness/projectsRunner.ts | 4 +- src/harness/sourceMapRecorder.ts | 2 +- src/harness/tsconfig.json | 7 +- src/harness/unittests/programMissingFiles.ts | 2 +- src/harness/unittests/publicApi.ts | 2 +- src/harness/utils.ts | 57 ++--- src/harness/vfs.ts | 124 +++++----- src/harness/vpath.ts | 82 ++++--- 19 files changed, 181 insertions(+), 589 deletions(-) rename src/harness/{core => }/collections.ts (55%) delete mode 100644 src/harness/core.ts delete mode 100644 src/harness/core/comparers.ts delete mode 100644 src/harness/core/functions.ts delete mode 100644 src/harness/core/strings.ts diff --git a/src/harness/core/collections.ts b/src/harness/collections.ts similarity index 55% rename from src/harness/core/collections.ts rename to src/harness/collections.ts index 7936b5a9323..7c71296409c 100644 --- a/src/harness/core/collections.ts +++ b/src/harness/collections.ts @@ -1,5 +1,4 @@ -/// -namespace core { +namespace collections { export interface SortOptions { comparer: (a: T, b: T) => number; sort: "insertion" | "comparison"; @@ -43,16 +42,16 @@ namespace core { } public has(key: K) { - return binarySearch(this._keys, key, identity, this._comparer) >= 0; + return ts.binarySearch(this._keys, key, ts.identity, this._comparer) >= 0; } public get(key: K) { - const index = binarySearch(this._keys, key, identity, this._comparer); + const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer); return index >= 0 ? this._values[index] : undefined; } public set(key: K, value: V) { - const index = binarySearch(this._keys, key, identity, this._comparer); + const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer); if (index >= 0) { this._values[index] = value; } @@ -67,12 +66,12 @@ namespace core { } public delete(key: K) { - const index = binarySearch(this._keys, key, identity, this._comparer); + const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer); if (index >= 0) { this.writePreamble(); - removeAt(this._keys, index); - removeAt(this._values, index); - if (this._order) removeAt(this._order, index); + ts.orderedRemoveItemAt(this._keys, index); + ts.orderedRemoveItemAt(this._values, index); + if (this._order) ts.orderedRemoveItemAt(this._order, index); this.writePostScript(); return true; } @@ -211,226 +210,6 @@ namespace core { } } - export class SortedSet { - private _comparer: (a: T, b: T) => number; - private _values: T[] = []; - private _order: number[] | undefined; - private _version = 0; - private _copyOnWrite = false; - - constructor(comparer: ((a: T, b: T) => number) | SortOptions, iterable?: Iterable) { - this._comparer = typeof comparer === "object" ? comparer.comparer : comparer; - this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined; - - if (iterable) { - const iterator = getIterator(iterable); - try { - for (let i = nextResult(iterator); i; i = nextResult(iterator)) { - const value = i.value; - this.add(value); - } - } - finally { - closeIterator(iterator); - } - } - } - - public get size() { - return this._values.length; - } - - public get comparer() { - return this._comparer; - } - - public get [Symbol.toStringTag]() { - return "SortedSet"; - } - - public has(value: T) { - return binarySearch(this._values, value, identity, this._comparer) >= 0; - } - - public add(value: T) { - const index = binarySearch(this._values, value, identity, this._comparer); - if (index < 0) { - this.writePreamble(); - insertAt(this._values, ~index, value); - if (this._order) insertAt(this._order, ~index, this._version); - this.writePostScript(); - } - return this; - } - - public delete(value: T) { - const index = binarySearch(this._values, value, identity, this._comparer); - if (index >= 0) { - this.writePreamble(); - removeAt(this._values, index); - if (this._order) removeAt(this._order, index); - this.writePostScript(); - return true; - } - return false; - } - - public clear() { - if (this.size > 0) { - this.writePreamble(); - this._values.length = 0; - if (this._order) this._order.length = 0; - this.writePostScript(); - } - } - - public forEach(callback: (value: T, key: T, collection: this) => void, thisArg?: any) { - const values = this._values; - const indices = this.getIterationOrder(); - const version = this._version; - this._copyOnWrite = true; - try { - if (indices) { - for (const i of indices) { - callback.call(thisArg, values[i], values[i], this); - } - } - else { - for (const value of values) { - callback.call(thisArg, value, value, this); - } - } - } - finally { - if (version === this._version) { - this._copyOnWrite = false; - } - } - } - - public keys() { - return this.values(); - } - - public * values() { - const values = this._values; - const indices = this.getIterationOrder(); - const version = this._version; - this._copyOnWrite = true; - try { - if (indices) { - for (const i of indices) { - yield values[i]; - } - } - else { - for (const value of values) { - yield value; - } - } - } - finally { - if (version === this._version) { - this._copyOnWrite = false; - } - } - } - - public * entries() { - const values = this._values; - const indices = this.getIterationOrder(); - const version = this._version; - this._copyOnWrite = true; - try { - if (indices) { - for (const i of indices) { - yield [values[i], values[i]] as [T, T]; - } - } - else { - for (const value of values) { - yield [value, value] as [T, T]; - } - } - } - finally { - if (version === this._version) { - this._copyOnWrite = false; - } - } - } - - public [Symbol.iterator]() { - return this.values(); - } - - private writePreamble() { - if (this._copyOnWrite) { - this._values = this._values.slice(); - if (this._order) this._order = this._order.slice(); - this._copyOnWrite = false; - } - } - - private writePostScript() { - this._version++; - } - - private getIterationOrder() { - if (this._order) { - const order = this._order; - return this._order - .map((_, i) => i) - .sort((x, y) => order[x] - order[y]); - } - return undefined; - } - } - - export function binarySearch(array: ReadonlyArray, value: T, keySelector: (v: T) => U, keyComparer: (a: U, b: U) => number, offset?: number): number { - if (!array || array.length === 0) { - return -1; - } - - let low = offset || 0; - let high = array.length - 1; - const key = keySelector(value); - while (low <= high) { - const middle = low + ((high - low) >> 1); - const midKey = keySelector(array[middle]); - const result = keyComparer(midKey, key); - if (result < 0) { - low = middle + 1; - } - else if (result > 0) { - high = middle - 1; - } - else { - return middle; - } - } - - return ~low; - } - - export function removeAt(array: T[], index: number): void { - if (index < 0 || index >= array.length) { - return; - } - else if (index === 0) { - array.shift(); - } - else if (index === array.length - 1) { - array.pop(); - } - else { - for (let i = index; i < array.length - 1; i++) { - array[i] = array[i + 1]; - } - array.length--; - } - } - export function insertAt(array: T[], index: number, value: T): void { if (index === 0) { array.unshift(value); diff --git a/src/harness/compiler.ts b/src/harness/compiler.ts index f9df4167276..8308074c444 100644 --- a/src/harness/compiler.ts +++ b/src/harness/compiler.ts @@ -1,14 +1,6 @@ -/// -/// -/// -/// -/// -/// -/// - -// NOTE: The contents of this file are all exported from the namespace 'compiler'. This is to -// support the eventual conversion of harness into a modular system. - +/** + * Test harness compiler functionality. + */ namespace compiler { export interface Project { file: string; @@ -64,7 +56,7 @@ namespace compiler { public readonly maps: ReadonlyMap; private _inputs: documents.TextDocument[] = []; - private _inputsAndOutputs: core.SortedMap; + private _inputsAndOutputs: collections.SortedMap; constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: ts.Diagnostic[]) { this.host = host; @@ -74,9 +66,9 @@ namespace compiler { this.options = program ? program.getCompilerOptions() : options; // collect outputs - const js = this.js = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - const dts = this.dts = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - const maps = this.maps = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + const js = this.js = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + const dts = this.dts = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + const maps = this.maps = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); for (const document of this.host.outputs) { if (vpath.isJavaScript(document.file)) { js.set(document.file, document); @@ -90,7 +82,7 @@ namespace compiler { } // correlate inputs and outputs - this._inputsAndOutputs = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + this._inputsAndOutputs = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); if (program) { if (this.options.out || this.options.outFile) { const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out); diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 8ccf756a9d7..1ffa2fc63a7 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -273,8 +273,8 @@ class CompilerTest { } private makeUnitName(name: string, root: string) { - const path = ts.toPath(name, root, core.identity); - const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", core.identity); + const path = ts.toPath(name, root, ts.identity); + const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.identity); return pathStart ? path.replace(pathStart, "/") : path; } diff --git a/src/harness/core.ts b/src/harness/core.ts deleted file mode 100644 index cb94af50bde..00000000000 --- a/src/harness/core.ts +++ /dev/null @@ -1,4 +0,0 @@ -/// -/// -/// -/// \ No newline at end of file diff --git a/src/harness/core/comparers.ts b/src/harness/core/comparers.ts deleted file mode 100644 index c0846437308..00000000000 --- a/src/harness/core/comparers.ts +++ /dev/null @@ -1,43 +0,0 @@ -namespace core { - export function compareNumbers(a: number, b: number): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return +1; - return a < b ? -1 : +1; - } - - export function compareStrings(a: string, b: string, ignoreCase: boolean): number { - return ignoreCase - ? compareStringsCaseInsensitive(a, b) - : compareStringsCaseSensitive(a, b); - } - - // NOTE: This is a duplicate of `compareNumbers` above, but is intended to be used only with - // strings to reduce polymorphism. - export function compareStringsCaseSensitive(a: string, b: string): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return +1; - return a < b ? -1 : +1; - } - - export function compareStringsCaseInsensitive(a: string, b: string): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return +1; - a = a.toUpperCase(); - b = b.toUpperCase(); - return a < b ? -1 : a > b ? +1 : 0; - } - - export function equateStringsCaseSensitive(a: string, b: string): boolean { - return a === b; - } - - export function equateStringsCaseInsensitive(a: string, b: string): boolean { - return a === b - || a !== undefined - && b !== undefined - && a.toUpperCase() === b.toUpperCase(); - } -} \ No newline at end of file diff --git a/src/harness/core/functions.ts b/src/harness/core/functions.ts deleted file mode 100644 index 870ea844b32..00000000000 --- a/src/harness/core/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -namespace core { - export function identity(v: T): T { return v; } -} diff --git a/src/harness/core/strings.ts b/src/harness/core/strings.ts deleted file mode 100644 index 593ef23c83f..00000000000 --- a/src/harness/core/strings.ts +++ /dev/null @@ -1,130 +0,0 @@ -namespace core { - export function padLeft(text: string, size: number, ch = " "): string { - while (text.length < size) text = ch + text; - return text; - } - - export function padRight(text: string, size: number, ch = " "): string { - while (text.length < size) text += ch; - return text; - } - - export function getByteOrderMark(text: string): string { - const length = getByteOrderMarkLength(text); - return length > 0 ? text.slice(0, length) : ""; - } - - export function getByteOrderMarkLength(text: string): number { - if (text.length >= 1) { - const ch0 = text.charCodeAt(0); - if (ch0 === 0xfeff) return 1; - if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0; - if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0; - if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0; - } - return 0; - } - - export function removeByteOrderMark(text: string): string { - const length = getByteOrderMarkLength(text); - return length ? text.slice(length) : text; - } - - export function addUTF8ByteOrderMark(text: string) { - return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text; - } - - function splitLinesWorker(text: string, lineStarts: number[] | undefined, lines: string[] | undefined, removeEmptyElements: boolean) { - let pos = 0; - let end = 0; - let lineStart = 0; - let nonWhiteSpace = false; - while (pos < text.length) { - const ch = text.charCodeAt(pos); - end = pos; - pos++; - switch (ch) { - // LineTerminator - case 0x000d: // carriage return - if (pos < text.length && text.charCodeAt(pos) === 0x000a) { - pos++; - } - // falls through - - case 0x000a: // line feed - case 0x2028: // line separator - case 0x2029: // paragraph separator - if (lineStarts) { - lineStarts.push(lineStart); - } - if (lines && (!removeEmptyElements || nonWhiteSpace)) { - lines.push(text.slice(lineStart, end)); - } - lineStart = pos; - nonWhiteSpace = false; - break; - - // WhiteSpace - case 0x0009: // tab - case 0x000b: // vertical tab - case 0x000c: // form feed - case 0x0020: // space - case 0x00a0: // no-break space - case 0xfeff: // zero width no-break space - case 0x1680: // ogham space mark - case 0x2000: // en quad - case 0x2001: // em quad - case 0x2002: // en space - case 0x2003: // em space - case 0x2004: // three-per-em space - case 0x2005: // four-per-em space - case 0x2006: // six-per-em space - case 0x2007: // figure space - case 0x2008: // punctuation space - case 0x2009: // thin space - case 0x200a: // hair space - case 0x202f: // narrow no-break space - case 0x205f: // medium mathematical space - case 0x3000: // ideographic space - case 0x0085: // next-line (not strictly per spec, but used by the compiler) - break; - - default: - nonWhiteSpace = true; - break; - } - } - if (lineStarts) { - lineStarts.push(lineStart); - } - if (lines && (!removeEmptyElements || nonWhiteSpace)) { - lines.push(text.slice(lineStart, text.length)); - } - } - - export type LineStarts = ReadonlyArray; - - export interface LinesAndLineStarts { - readonly lines: ReadonlyArray; - readonly lineStarts: LineStarts; - } - - export function getLinesAndLineStarts(text: string): LinesAndLineStarts { - const lines: string[] = []; - const lineStarts: number[] = []; - splitLinesWorker(text, lineStarts, lines, /*removeEmptyElements*/ false); - return { lines, lineStarts }; - } - - export function splitLines(text: string, removeEmptyElements = false): string[] { - const lines: string[] = []; - splitLinesWorker(text, /*lineStarts*/ undefined, lines, removeEmptyElements); - return lines; - } - - export function computeLineStarts(text: string): LineStarts { - const lineStarts: number[] = []; - splitLinesWorker(text, lineStarts, /*lines*/ undefined, /*removeEmptyElements*/ false); - return lineStarts; - } -} \ No newline at end of file diff --git a/src/harness/documents.ts b/src/harness/documents.ts index fc4893ef4e8..f5219029109 100644 --- a/src/harness/documents.ts +++ b/src/harness/documents.ts @@ -1,5 +1,3 @@ -/// - // NOTE: The contents of this file are all exported from the namespace 'documents'. This is to // support the eventual conversion of harness into a modular system. @@ -9,7 +7,7 @@ namespace documents { public readonly file: string; public readonly text: string; - private _lineStarts: core.LineStarts | undefined; + private _lineStarts: ReadonlyArray | undefined; private _testFile: Harness.Compiler.TestFile | undefined; constructor(file: string, text: string, meta?: Map) { @@ -18,8 +16,8 @@ namespace documents { this.meta = meta || new Map(); } - public get lineStarts(): core.LineStarts { - return this._lineStarts || (this._lineStarts = core.computeLineStarts(this.text)); + public get lineStarts(): ReadonlyArray { + return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text)); } public static fromTestFile(file: Harness.Compiler.TestFile) { diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 9b1b83666cf..669cb33c0c4 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -1,11 +1,6 @@ -/// -/// -/// - -// NOTE: The contents of this file are all exported from the namespace 'fakes'. This is to -// support the eventual conversion of harness into a modular system. - -// harness fakes +/** + * Fake implementations of various compiler dependencies. + */ namespace fakes { const processExitSentinel = new Error("System exit"); @@ -45,8 +40,8 @@ namespace fakes { try { const content = this.vfs.readFileSync(path, "utf8"); return content === undefined ? undefined : - vpath.extname(path) === ".json" ? utils.removeComments(core.removeByteOrderMark(content), utils.CommentRemoval.leadingAndTrailing) : - core.removeByteOrderMark(content); + vpath.extname(path) === ".json" ? utils.removeComments(utils.removeByteOrderMark(content), utils.CommentRemoval.leadingAndTrailing) : + utils.removeByteOrderMark(content); } catch { return undefined; @@ -55,7 +50,7 @@ namespace fakes { public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void { this.vfs.mkdirpSync(vpath.dirname(path)); - this.vfs.writeFileSync(path, writeByteOrderMark ? core.addUTF8ByteOrderMark(data) : data); + this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data); } public fileExists(path: string) { @@ -212,7 +207,7 @@ namespace fakes { public readonly shouldAssertInvariants = !Harness.lightMode; private _setParentNodes: boolean; - private _sourceFiles: core.SortedMap; + private _sourceFiles: collections.SortedMap; private _parseConfigHost: ParseConfigHost; private _newLine: string; @@ -221,7 +216,7 @@ namespace fakes { this.sys = sys; this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || ""; this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine); - this._sourceFiles = new core.SortedMap({ comparer: sys.vfs.stringComparer, sort: "insertion" }); + this._sourceFiles = new collections.SortedMap({ comparer: sys.vfs.stringComparer, sort: "insertion" }); this._setParentNodes = setParentNodes; } @@ -266,7 +261,7 @@ namespace fakes { } public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) { - if (writeByteOrderMark) content = core.addUTF8ByteOrderMark(content); + if (writeByteOrderMark) content = utils.addUTF8ByteOrderMark(content); this.sys.writeFile(fileName, content); const document = new documents.TextDocument(fileName, content); diff --git a/src/harness/harness.ts b/src/harness/harness.ts index d7db8eb8cfe..2a477b07399 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -678,9 +678,9 @@ namespace Harness { function createBrowserIO(): IO { const serverRoot = new URL("http://localhost:8888/"); - class HttpHeaders extends core.SortedMap { + class HttpHeaders extends collections.SortedMap { constructor(template?: Record) { - super(core.compareStringsCaseInsensitive); + super(ts.compareStringsCaseInsensitive); if (template) { for (const key in template) { if (ts.hasProperty(template, key)) { @@ -1237,7 +1237,7 @@ namespace Harness { } const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile); - const fs = vfs.FileSystem.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory }); + const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory }); const host = new fakes.CompilerHost(fs, options); return compiler.compileFiles(host, programFileNames, options); } @@ -1286,7 +1286,7 @@ namespace Harness { else if (vpath.isTypeScript(file.unitName)) { const declFile = findResultCodeFile(file.unitName); if (declFile && !findUnit(declFile.file, declInputFiles) && !findUnit(declFile.file, declOtherFiles)) { - dtsFiles.push({ unitName: declFile.file, content: core.removeByteOrderMark(declFile.text) }); + dtsFiles.push({ unitName: declFile.file, content: utils.removeByteOrderMark(declFile.text) }); } } } @@ -1733,7 +1733,7 @@ namespace Harness { const dupeCase = ts.createMap(); // Yield them for (const outputFile of files) { - yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + core.removeByteOrderMark(outputFile.text)]; + yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + utils.removeByteOrderMark(outputFile.text)]; } function cleanName(fn: string) { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 9f1587b85b5..34d6f775a74 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -119,11 +119,11 @@ namespace Harness.LanguageService { export abstract class LanguageServiceAdapterHost { public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); public typesRegistry: ts.Map | undefined; - private scriptInfos: core.SortedMap; + private scriptInfos: collections.SortedMap; constructor(protected cancellationToken = DefaultHostCancellationToken.instance, protected settings = ts.getDefaultCompilerOptions()) { - this.scriptInfos = new core.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); } public get vfs() { diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index e7d9b387b46..3aa18b1ba27 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -193,7 +193,7 @@ namespace project { assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); } - const fs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); + const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO)); fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot)); fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot)); @@ -393,7 +393,7 @@ namespace project { } }); - const _vfs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { + const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: allInputFiles, cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot) }); diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts index 68ac32a5687..1e13e3833a4 100644 --- a/src/harness/sourceMapRecorder.ts +++ b/src/harness/sourceMapRecorder.ts @@ -317,7 +317,7 @@ namespace Harness.SourceMapRecorder { const startPos = lineMap[line]; const endPos = lineMap[line + 1]; const text = code.substring(startPos, endPos); - return line === 0 ? core.removeByteOrderMark(text) : text; + return line === 0 ? utils.removeByteOrderMark(text) : text; } function writeJsFileLines(endJsLine: number) { diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index e9674d7e452..67eeeb14f37 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -134,12 +134,7 @@ "../server/session.ts", "../server/scriptVersionCache.ts", - "./core/functions.ts", - "./core/comparers.ts", - "./core/collections.ts", - "./core/strings.ts", - "core.ts", - + "collections.ts", "utils.ts", "documents.ts", "vpath.ts", diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index efbf42e6004..66b2ab4cf7f 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -37,7 +37,7 @@ namespace ts { ); const testCompilerHost = new fakes.CompilerHost( - vfs.FileSystem.createFromFileSystem( + vfs.createFromFileSystem( Harness.IO, /*ignoreCase*/ true, { documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }), diff --git a/src/harness/unittests/publicApi.ts b/src/harness/unittests/publicApi.ts index e1a0b6ef673..b99557b67bd 100644 --- a/src/harness/unittests/publicApi.ts +++ b/src/harness/unittests/publicApi.ts @@ -14,7 +14,7 @@ describe("Public APIs", () => { }); it("should compile", () => { - const fs = vfs.FileSystem.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); + const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); fs.linkSync(`${vfs.builtFolder}/${fileName}`, `${vfs.srcFolder}/${fileName}`); const sys = new fakes.System(fs); const host = new fakes.CompilerHost(sys); diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 2e53c3a5cb0..81da505cd90 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -1,8 +1,6 @@ -/// - -// NOTE: The contents of this file are all exported from the namespace 'utils'. This is to -// support the eventual conversion of harness into a modular system. - +/** + * Common utilities + */ namespace utils { const leadingCommentRegExp = /^(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+/; const trailingCommentRegExp = /(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+$/; @@ -34,28 +32,6 @@ namespace utils { return text !== undefined ? text.replace(testPathPrefixRegExp, "") : undefined; } - /** - * SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===". - */ - export function is(x: any, y: any) { - return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y); - } - - export interface Theory { - title: string; - args: any[]; - } - - export function theory(name: string, data: (Theory | any[])[], callback: (...args: any[]) => any) { - describe(name, () => { - for (const theory of data) { - const title = Array.isArray(theory) ? theory.toString() : theory.title; - const args = Array.isArray(theory) ? theory : theory.args; - it(title, () => callback(...args)); - } - }); - } - /** * Removes leading indentation from a template literal string. */ @@ -112,14 +88,27 @@ namespace utils { return indentation; } - export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${expectedFileNames}, got ${actualFileNames}`); - for (const f of expectedFileNames) { - assert.isTrue(actualFileNames.indexOf(f) >= 0, `${caption}: expected to find ${f} in ${actualFileNames}`); - } - } - export function toUtf8(text: string): string { return new Buffer(text).toString("utf8"); } + + export function getByteOrderMarkLength(text: string): number { + if (text.length >= 1) { + const ch0 = text.charCodeAt(0); + if (ch0 === 0xfeff) return 1; + if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0; + if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0; + if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0; + } + return 0; + } + + export function removeByteOrderMark(text: string): string { + const length = getByteOrderMarkLength(text); + return length ? text.slice(length) : text; + } + + export function addUTF8ByteOrderMark(text: string) { + return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text; + } } \ No newline at end of file diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 49c72bfde58..cbd3f5a11d2 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -40,9 +40,9 @@ namespace vfs { // lazy-initialized state that should be mutable even if the FileSystem is frozen. private _lazy: { - links?: core.SortedMap; + links?: collections.SortedMap; shadows?: Map; - meta?: core.Metadata; + meta?: collections.Metadata; } = {}; private _cwd: string; // current working directory @@ -68,16 +68,16 @@ namespace vfs { let cwd = options.cwd; if ((!cwd || !vpath.isRoot(cwd)) && this._lazy.links) { - const iterator = core.getIterator(this._lazy.links.keys()); + const iterator = collections.getIterator(this._lazy.links.keys()); try { - for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) { + for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { const name = i.value; cwd = cwd ? vpath.resolve(name, cwd) : name; break; } } finally { - core.closeIterator(iterator); + collections.closeIterator(iterator); } } @@ -92,9 +92,9 @@ namespace vfs { /** * Gets metadata for this `FileSystem`. */ - public get meta(): core.Metadata { + public get meta(): collections.Metadata { if (!this._lazy.meta) { - this._lazy.meta = new core.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined); + this._lazy.meta = new collections.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined); } return this._lazy.meta; } @@ -121,40 +121,6 @@ namespace vfs { return this._shadowRoot; } - /** - * Create a virtual file system from a physical file system using the following path mappings: - * - * - `/.ts` is a directory mapped to `${workspaceRoot}/built/local` - * - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib` - * - `/.src` is a virtual directory to be used for tests. - * - * Unless overridden, `/.src` will be the current working directory for the virtual file system. - */ - public static createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) { - const fs = getBuiltLocal(host, ignoreCase).shadow(); - if (cwd) { - fs.mkdirpSync(cwd); - fs.chdir(cwd); - } - if (documents) { - for (const document of documents) { - fs.mkdirpSync(vpath.dirname(document.file)); - fs.writeFileSync(document.file, document.text, "utf8"); - fs.filemeta(document.file).set("document", document); - // Add symlinks - const symlink = document.meta.get("symlink"); - if (symlink) { - for (const link of symlink.split(",").map(link => link.trim())) { - fs.mkdirpSync(vpath.dirname(link)); - fs.symlinkSync(document.file, link); - fs.filemeta(link).set("document", document); - } - } - } - } - return fs; - } - /** * Gets a shadow copy of this file system. Changes to the shadow copy do not affect the * original, allowing multiple copies of the same core file system without multiple copies @@ -190,16 +156,16 @@ namespace vfs { * Gets the metadata object for a path. * @param path */ - public filemeta(path: string): core.Metadata { + public filemeta(path: string): collections.Metadata { const { node } = this._walk(this._resolve(path)); if (!node) throw createIOError("ENOENT"); return this._filemeta(node); } - private _filemeta(node: Inode): core.Metadata { + private _filemeta(node: Inode): collections.Metadata { if (!node.meta) { const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot); - node.meta = new core.Metadata(parentMeta); + node.meta = new collections.Metadata(parentMeta); } return node.meta; } @@ -390,10 +356,10 @@ namespace vfs { */ public debugPrint(): void { let result = ""; - const printLinks = (dirname: string | undefined, links: core.SortedMap) => { - const iterator = core.getIterator(links); + const printLinks = (dirname: string | undefined, links: collections.SortedMap) => { + const iterator = collections.getIterator(links); try { - for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) { + for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { const [name, node] = i.value; const path = dirname ? vpath.combine(dirname, name) : name; const marker = vpath.compare(this._cwd, path, this.ignoreCase) === 0 ? "*" : " "; @@ -412,7 +378,7 @@ namespace vfs { } } finally { - core.closeIterator(iterator); + collections.closeIterator(iterator); } }; printLinks(/*dirname*/ undefined, this._getRootLinks()); @@ -686,7 +652,7 @@ namespace vfs { }; } - private _addLink(parent: DirectoryInode | undefined, links: core.SortedMap, name: string, node: Inode, time = this.time()) { + private _addLink(parent: DirectoryInode | undefined, links: collections.SortedMap, name: string, node: Inode, time = this.time()) { links.set(name, node); node.nlink++; node.ctimeMs = time; @@ -694,14 +660,14 @@ namespace vfs { if (!parent && !this._cwd) this._cwd = name; } - private _removeLink(parent: DirectoryInode | undefined, links: core.SortedMap, name: string, node: Inode, time = this.time()) { + private _removeLink(parent: DirectoryInode | undefined, links: collections.SortedMap, name: string, node: Inode, time = this.time()) { links.delete(name); node.nlink--; node.ctimeMs = time; if (parent) parent.mtimeMs = time; } - private _replaceLink(oldParent: DirectoryInode, oldLinks: core.SortedMap, oldName: string, newParent: DirectoryInode, newLinks: core.SortedMap, newName: string, node: Inode, time: number) { + private _replaceLink(oldParent: DirectoryInode, oldLinks: collections.SortedMap, oldName: string, newParent: DirectoryInode, newLinks: collections.SortedMap, newName: string, node: Inode, time: number) { if (oldParent !== newParent) { this._removeLink(oldParent, oldLinks, oldName, node, time); this._addLink(newParent, newLinks, newName, node, time); @@ -716,7 +682,7 @@ namespace vfs { private _getRootLinks() { if (!this._lazy.links) { - this._lazy.links = new core.SortedMap(this.stringComparer); + this._lazy.links = new collections.SortedMap(this.stringComparer); if (this._shadowRoot) { this._copyShadowLinks(this._shadowRoot._getRootLinks(), this._lazy.links); } @@ -727,7 +693,7 @@ namespace vfs { private _getLinks(node: DirectoryInode) { if (!node.links) { - const links = new core.SortedMap(this.stringComparer); + const links = new collections.SortedMap(this.stringComparer); const { source, resolver } = node; if (source && resolver) { node.source = undefined; @@ -786,16 +752,16 @@ namespace vfs { return shadow; } - private _copyShadowLinks(source: ReadonlyMap, target: core.SortedMap) { - const iterator = core.getIterator(source); + private _copyShadowLinks(source: ReadonlyMap, target: collections.SortedMap) { + const iterator = collections.getIterator(source); try { - for (let i = core.nextResult(iterator); i; i = core.nextResult(iterator)) { + for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) { const [name, root] = i.value; target.set(name, this._getShadow(root)); } } finally { - core.closeIterator(iterator); + collections.closeIterator(iterator); } } @@ -1035,6 +1001,40 @@ namespace vfs { }; } + /** + * Create a virtual file system from a physical file system using the following path mappings: + * + * - `/.ts` is a directory mapped to `${workspaceRoot}/built/local` + * - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib` + * - `/.src` is a virtual directory to be used for tests. + * + * Unless overridden, `/.src` will be the current working directory for the virtual file system. + */ + export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) { + const fs = getBuiltLocal(host, ignoreCase).shadow(); + if (cwd) { + fs.mkdirpSync(cwd); + fs.chdir(cwd); + } + if (documents) { + for (const document of documents) { + fs.mkdirpSync(vpath.dirname(document.file)); + fs.writeFileSync(document.file, document.text, "utf8"); + fs.filemeta(document.file).set("document", document); + // Add symlinks + const symlink = document.meta.get("symlink"); + if (symlink) { + for (const link of symlink.split(",").map(link => link.trim())) { + fs.mkdirpSync(vpath.dirname(link)); + fs.symlinkSync(document.file, link); + fs.filemeta(link).set("document", document); + } + } + } + } + return fs; + } + export class Stats { public dev: number; public ino: number; @@ -1188,7 +1188,7 @@ namespace vfs { source?: string; resolver?: FileSystemResolver; shadowRoot?: FileInode; - meta?: core.Metadata; + meta?: collections.Metadata; } interface DirectoryInode { @@ -1200,11 +1200,11 @@ namespace vfs { ctimeMs: number; // status change time birthtimeMs: number; // creation time nlink: number; // number of hard links - links?: core.SortedMap; + links?: collections.SortedMap; source?: string; resolver?: FileSystemResolver; shadowRoot?: DirectoryInode; - meta?: core.Metadata; + meta?: collections.Metadata; } interface SymlinkInode { @@ -1218,7 +1218,7 @@ namespace vfs { nlink: number; // number of hard links symlink?: string; shadowRoot?: SymlinkInode; - meta?: core.Metadata; + meta?: collections.Metadata; } function isFile(node: Inode | undefined): node is FileInode { @@ -1237,7 +1237,7 @@ namespace vfs { realpath: string; basename: string; parent: DirectoryInode | undefined; - links: core.SortedMap | undefined; + links: collections.SortedMap | undefined; node: Inode | undefined; } diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts index 72c3f7d7bea..02675ccd95a 100644 --- a/src/harness/vpath.ts +++ b/src/harness/vpath.ts @@ -1,17 +1,16 @@ -/// -/// namespace vpath { /** * Virtual path separator. */ - export const sep = "/"; + export import sep = ts.directorySeparator; /** * Normalize path separators. */ - export function normalizeSeparators(path: string): string { - return path.replace(/\s*[\\/]\s*/g, sep).trim(); - } + export import normalizeSeparators = ts.normalizeSlashes; + // export function normalizeSeparators(path: string): string { + // return ts.normalizeSlashes(path); + // } const invalidRootComponentRegExp = /^(?!(\/|\/\/\w+\/|[a-zA-Z]:\/?|)$)/; const invalidNavigableComponentRegExp = /[:*?"<>|]/; @@ -96,6 +95,7 @@ namespace vpath { const absolutePathRegExp = /^[\\/]([\\/](.*?[\\/](.*?[\\/])?)?)?|^[a-zA-Z]:[\\/]?|^\w+:\/{2}[^\\/]*\/?/; + // NOTE: this differs from `ts.getRootLength` in that it doesn't support URIs. function getRootLength(path: string) { const match = absolutePathRegExp.exec(path); return match ? match[0].length : 0; @@ -105,9 +105,10 @@ namespace vpath { * Determines whether a path is an absolute path (e.g. starts with `/`, `\\`, or a dos path * like `c:`). */ - export function isAbsolute(path: string) { - return absolutePathRegExp.test(path); - } + export import isAbsolute = ts.isRootedDiskPath; + // export function isAbsolute(path: string) { + // return absolutePathRegExp.test(path); + // } /** * Determines whether a path consists only of a path root. @@ -129,16 +130,18 @@ namespace vpath { /** * Adds a trailing separator (`/`) to a path if it doesn't have one. */ - export function addTrailingSeparator(path: string) { - return !trailingSeperatorRegExp.test(path) && path ? path + "/" : path; - } + export import addTrailingSeparator = ts.ensureTrailingDirectorySeparator; + // export function addTrailingSeparator(path: string) { + // return !trailingSeperatorRegExp.test(path) && path ? path + "/" : path; + // } /** * Removes a trailing separator (`/`) from a path if it has one. */ - export function removeTrailingSeparator(path: string) { - return trailingSeperatorRegExp.test(path) && !isRoot(path) ? path.slice(0, -1) : path; - } + export import removeTrailingSeparator = ts.removeTrailingDirectorySeparator; + // export function removeTrailingSeparator(path: string) { + // return trailingSeperatorRegExp.test(path) && !isRoot(path) ? path.slice(0, -1) : path; + // } function reduce(components: ReadonlyArray) { const normalized = [components[0]]; @@ -172,11 +175,12 @@ namespace vpath { */ export function combine(path: string, ...paths: string[]) { path = normalizeSeparators(path); - for (let name of paths) { - name = normalizeSeparators(name); - if (name.length === 0) continue; - path = path.length === 0 || isAbsolute(name) ? name : - addTrailingSeparator(path) + name; + for (const name of paths) { + path = ts.combinePaths(path, normalizeSeparators(name)); + // name = normalizeSeparators(name); + // if (name.length === 0) continue; + // path = path.length === 0 || isAbsolute(name) ? name : + // addTrailingSeparator(path) + name; } return path; } @@ -188,6 +192,8 @@ namespace vpath { return normalize(combine(path, ...paths)); } + // NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths + // are already absolute and does not perform "canonicalization". function relativeWorker(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean) { if (!isAbsolute(from)) throw new Error("Path not absolute"); if (!isAbsolute(to)) throw new Error("Path not absolute"); @@ -215,20 +221,23 @@ namespace vpath { } function relativeCaseSensitive(from: string, to: string) { - return relativeWorker(from, to, core.equateStringsCaseSensitive); + return relativeWorker(from, to, ts.equateStringsCaseSensitive); } function relativeCaseInsensitive(from: string, to: string) { - return relativeWorker(from, to, core.equateStringsCaseInsensitive); + return relativeWorker(from, to, ts.equateStringsCaseInsensitive); } /** * Gets a relative path that can be used to traverse between `from` and `to`. */ + // NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths + // are already absolute and does not perform "canonicalization". export function relative(from: string, to: string, ignoreCase: boolean) { return ignoreCase ? relativeCaseInsensitive(from, to) : relativeCaseSensitive(from, to); } + // NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`. function compareWorker(a: string, b: string, stringComparer: (a: string, b: string) => number) { if (a === b) return 0; a = removeTrailingSeparator(a); @@ -241,32 +250,33 @@ namespace vpath { const result = stringComparer(aComponents[i], bComponents[i]); if (result !== 0) return result; } - return core.compareNumbers(aComponents.length, bComponents.length); + return ts.compareValues(aComponents.length, bComponents.length); } /** * Performs a case-sensitive comparison of two paths. */ export function compareCaseSensitive(a: string, b: string) { - return compareWorker(a, b, core.compareStringsCaseSensitive); + return compareWorker(a, b, ts.compareStringsCaseSensitive); } /** * Performs a case-insensitive comparison of two paths. */ export function compareCaseInsensitive(a: string, b: string) { - return compareWorker(a, b, core.compareStringsCaseInsensitive); + return compareWorker(a, b, ts.compareStringsCaseInsensitive); } /** * Compare two paths. */ + // NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`. export function compare(a: string, b: string, ignoreCase: boolean) { return ignoreCase ? compareCaseInsensitive(a, b) : compareCaseSensitive(a, b); } /** - * Determines whether two strings are equal. + * Determines whether two paths are equal. */ export function equals(a: string, b: string, ignoreCase: boolean) { if (!isAbsolute(a)) throw new Error("Path not absolute"); @@ -281,6 +291,7 @@ namespace vpath { return ignoreCase && a.toUpperCase() === b.toUpperCase(); } + // NOTE: this differs from `ts.containsPath` due to the behavior of `parse`. function beneathWorker(ancestor: string, descendant: string, stringEqualityComparer: (a: string, b: string) => boolean) { if (!isAbsolute(ancestor)) throw new Error("Path not absolute"); if (!isAbsolute(descendant)) throw new Error("Path not absolute"); @@ -296,16 +307,17 @@ namespace vpath { } function beneathCaseSensitive(ancestor: string, descendant: string) { - return beneathWorker(ancestor, descendant, core.equateStringsCaseSensitive); + return beneathWorker(ancestor, descendant, ts.equateStringsCaseSensitive); } function beneathCaseInsensitive(ancestor: string, descendant: string) { - return beneathWorker(ancestor, descendant, core.equateStringsCaseInsensitive); + return beneathWorker(ancestor, descendant, ts.equateStringsCaseInsensitive); } /** * Determines whether the path `descendant` is beneath the path `ancestor`. */ + // NOTE: this differs from `containsPath` in compiler/core.ts due to the behavior of `parse`. export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { return ignoreCase ? beneathCaseInsensitive(ancestor, descendant) : beneathCaseSensitive(ancestor, descendant); } @@ -315,6 +327,9 @@ namespace vpath { * If the path is relative, the root component is `""`. * If the path is absolute, the root component includes the first path separator (`/`). */ + // NOTE: this differs from `ts.getNormalizedPathComponents` due to the fact that `parse` does + // not automatically normalize relative paths and does not perform path normalization. This is + // necessary to support proper path navigation in `vfs`. export function parse(path: string) { path = normalizeSeparators(path); const rootLength = getRootLength(path); @@ -327,6 +342,8 @@ namespace vpath { /** * Formats a parsed path consisting of a root component and zero or more path segments. */ + // NOTE: this differs from `ts.getNormalizedPathFromPathComponents` in that this function + // always returns a string. export function format(components: ReadonlyArray) { return components.length ? components[0] + components.slice(1).join(sep) : ""; } @@ -334,6 +351,7 @@ namespace vpath { /** * Gets the parent directory name of a path. */ + // NOTE: this differs from `ts.getDirectoryPath` due to the behavior of `getRootLength`. export function dirname(path: string) { path = normalizeSeparators(path); path = removeTrailingSeparator(path); @@ -349,6 +367,8 @@ namespace vpath { * If the base name has any one of the provided extensions, it is removed. */ export function basename(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + // NOTE: this differs from `ts.getBaseFileName` in that this function handles extensions in a + // fashion similar to the NodeJS `path.basename` function as well as handles case sensitivity. export function basename(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { path = normalizeSeparators(path); path = removeTrailingSeparator(path); @@ -384,9 +404,11 @@ namespace vpath { * Gets the file extension for a path, provided it is one of the provided extensions. */ export function extname(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + // NOTE: this differs from `ts.getAnyExtensionFromPath` in that this function allows you to + // restrict extensions and handle case sensitivity export function extname(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { if (extensions) { - return extnameWorker(path, extensions, ignoreCase ? core.equateStringsCaseInsensitive : core.equateStringsCaseSensitive); + return extnameWorker(path, extensions, ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive); } const match = extRegExp.exec(path); @@ -395,6 +417,8 @@ namespace vpath { export function changeExtension(path: string, ext: string): string; export function changeExtension(path: string, ext: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + // NOTE: this differs from `ts.changeExtension` in that this function allows you to + // specify extensions and handle case sensitivity export function changeExtension(path: string, ext: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { const pathext = extensions !== undefined && ignoreCase !== undefined ? extname(path, extensions, ignoreCase) : extname(path); return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path;