diff --git a/Gulpfile.ts b/Gulpfile.ts
index 6c91ef52cbb49..2a0e7092931e5 100644
--- a/Gulpfile.ts
+++ b/Gulpfile.ts
@@ -411,7 +411,7 @@ gulp.task(servicesFile, false, ["lib", "generate-diagnostics"], () => {
completedDts.pipe(clone())
.pipe(insert.transform((content, file) => {
file.path = nodeStandaloneDefinitionsFile;
- return content.replace(/declare (namespace|module) ts/g, 'declare module "typescript"');
+ return content.replace(/declare (namespace|module) ts {/g, 'declare module "typescript" {\n import * as ts from "typescript";');
}))
]).pipe(gulp.dest(builtLocalDirectory));
});
diff --git a/Jakefile.js b/Jakefile.js
index 174be5e702f52..868c3cd53e2ae 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -46,6 +46,7 @@ var compilerSources = [
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
+ "extensions.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"
@@ -67,6 +68,7 @@ var servicesSources = [
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
+ "extensions.ts",
"commandLineParser.ts",
"diagnosticInformationMap.generated.ts"
].map(function (f) {
@@ -131,6 +133,7 @@ var harnessCoreSources = [
"typeWriter.ts",
"fourslashRunner.ts",
"projectsRunner.ts",
+ "extensionRunner.ts",
"loggedIO.ts",
"rwcRunner.ts",
"test262Runner.ts",
@@ -158,7 +161,7 @@ var harnessSources = harnessCoreSources.concat([
"convertCompilerOptionsFromJson.ts",
"convertTypingOptionsFromJson.ts",
"tsserverProjectSystem.ts",
- "matchFiles.ts"
+ "matchFiles.ts",
].map(function (f) {
return path.join(unittestsDirectory, f);
})).concat([
@@ -524,7 +527,7 @@ compileFile(servicesFile, servicesSources,[builtLocalDirectory, copyright].conca
// Node package definition file to be distributed without the package. Created by replacing
// 'ts' namespace with '"typescript"' as a module.
- var nodeStandaloneDefinitionsFileContents = definitionFileContents.replace(/declare (namespace|module) ts/g, 'declare module "typescript"');
+ var nodeStandaloneDefinitionsFileContents = definitionFileContents.replace(/declare (namespace|module) ts {/g, 'declare module "typescript" {\n import * as ts from "typescript";');
fs.writeFileSync(nodeStandaloneDefinitionsFile, nodeStandaloneDefinitionsFileContents);
});
diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts
index 7e2c6eb8d334e..8dd35b033f031 100644
--- a/src/compiler/commandLineParser.ts
+++ b/src/compiler/commandLineParser.ts
@@ -282,6 +282,12 @@ namespace ts {
experimental: true,
description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators
},
+ {
+ name: "extensions",
+ type: "object",
+ isTSConfigOnly: true,
+ description: Diagnostics.List_of_compiler_extensions_to_require
+ },
{
name: "moduleResolution",
type: {
@@ -429,7 +435,7 @@ namespace ts {
name: "strictNullChecks",
type: "boolean",
description: Diagnostics.Enable_strict_null_checks
- }
+ },
];
/* @internal */
diff --git a/src/compiler/core.ts b/src/compiler/core.ts
index 6c87ad82955c3..f12f110e3e39f 100644
--- a/src/compiler/core.ts
+++ b/src/compiler/core.ts
@@ -2,6 +2,17 @@
///
+namespace ts {
+ export function startsWith(str: string, prefix: string): boolean {
+ return str.lastIndexOf(prefix, 0) === 0;
+ }
+
+ export function endsWith(str: string, suffix: string): boolean {
+ const expectedPos = str.length - suffix.length;
+ return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos;
+ }
+}
+
/* @internal */
namespace ts {
/**
@@ -178,6 +189,26 @@ namespace ts {
return array1.concat(array2);
}
+ export function flatten(array1: T[][]): T[] {
+ if (!array1 || !array1.length) return array1;
+ return [].concat(...array1);
+ }
+
+ export function groupBy(array: T[], classifier: (item: T) => string): {[index: string]: T[]};
+ export function groupBy(array: T[], classifier: (item: T) => number): {[index: number]: T[]};
+ export function groupBy(array: T[], classifier: (item: T) => (string | number)): {[index: string]: T[], [index: number]: T[]} {
+ if (!array || !array.length) return undefined;
+ const ret: {[index: string]: T[], [index: number]: T[]} = {};
+ for (const elem of array) {
+ const key = classifier(elem);
+ if (!ret[key]) {
+ ret[key] = [];
+ }
+ ret[key].push(elem);
+ }
+ return ret;
+ }
+
export function deduplicate(array: T[], areEqual?: (a: T, b: T) => boolean): T[] {
let result: T[];
if (array) {
@@ -908,17 +939,6 @@ namespace ts {
return true;
}
- /* @internal */
- export function startsWith(str: string, prefix: string): boolean {
- return str.lastIndexOf(prefix, 0) === 0;
- }
-
- /* @internal */
- export function endsWith(str: string, suffix: string): boolean {
- const expectedPos = str.length - suffix.length;
- return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos;
- }
-
export function fileExtensionIs(path: string, extension: string): boolean {
return path.length > extension.length && endsWith(path, extension);
}
@@ -1195,7 +1215,8 @@ namespace ts {
export const supportedJavascriptExtensions = [".js", ".jsx"];
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
- export function getSupportedExtensions(options?: CompilerOptions): string[] {
+ export function getSupportedExtensions(options?: CompilerOptions, loadJS?: boolean): string[] {
+ if (loadJS) return supportedJavascriptExtensions;
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;
}
@@ -1373,4 +1394,49 @@ namespace ts {
: ((fileName) => fileName.toLowerCase());
}
+ /**
+ * This isn't the strictest deep equal, but it's good enough for us
+ * - +0 === -0 (though who really wants to consider them different?)
+ * - arguments and arrays can be equal (both typeof === object, both have enumerable keys)
+ * - doesn't inspect es6 iterables (not that they're used in this code base)
+ * - doesn't inspect regex toString value (so only references to the same regex are equal)
+ * - doesn't inspect date primitive number value (so only references to the same date are equal)
+ */
+ export function deepEqual(a: any, b: any, memo?: [any, any][]): boolean {
+ if (a === b) return true;
+ if (typeof a !== typeof b) return false;
+ // Special case NaN
+ if (typeof a === "number" && isNaN(a) && isNaN(b)) return true;
+ // We can't know if function arguments are deep equal, so we say they're equal if they look alike
+ if (typeof a === "object" || typeof a === "function") {
+ if (memo) {
+ for (let i = 0; i < memo.length; i++) {
+ if (memo[i][0] === a && memo[i][1] === b) return true;
+ if (memo[i][0] === b && memo[i][1] === a) return true;
+ }
+ }
+ else {
+ memo = [];
+ }
+
+ const aKeys = ts.getKeys(a);
+ const bKeys = ts.getKeys(b);
+ aKeys.sort();
+ bKeys.sort();
+
+ if (aKeys.length !== bKeys.length) return false;
+
+ for (let i = 0; i < aKeys.length; i++) {
+ if (aKeys[i] !== bKeys[i]) return false;
+ }
+
+ memo.push([a, b]);
+
+ for (const key of aKeys) {
+ if (!deepEqual(a[key], b[key], memo)) return false;
+ }
+ return true;
+ }
+ return false;
+ }
}
diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json
index 8126d5c605ea7..2a73331b91b0e 100644
--- a/src/compiler/diagnosticMessages.json
+++ b/src/compiler/diagnosticMessages.json
@@ -2676,7 +2676,7 @@
"category": "Message",
"code": 6099
},
- "'package.json' does not have 'types' field.": {
+ "'package.json' does not have '{0}' field.": {
"category": "Message",
"code": 6100
},
@@ -2696,7 +2696,7 @@
"category": "Message",
"code": 6104
},
- "Expected type of '{0}' field in 'package.json' to be 'string', got '{1}'.": {
+ "Expected type of '{0}' field in 'package.json' to be '{1}', got '{2}'.": {
"category": "Message",
"code": 6105
},
@@ -2824,10 +2824,20 @@
"category": "Message",
"code": 6136
},
- "No types specified in 'package.json' but 'allowJs' is set, so returning 'main' value of '{0}'": {
+
+ "List of compiler extensions to require.": {
"category": "Message",
- "code": 6137
+ "code": 6150
+ },
+ "Extension loading failed with error '{0}'.": {
+ "category": "Error",
+ "code": 6151
},
+ "Extension '{0}' exported member '{1}' has extension kind '{2}', but was type '{3}' when type '{4}' was expected.": {
+ "category": "Error",
+ "code": 6152
+ },
+
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts
new file mode 100644
index 0000000000000..c156cf16a9835
--- /dev/null
+++ b/src/compiler/extensions.ts
@@ -0,0 +1,222 @@
+namespace ts {
+
+ export interface BaseProviderStatic {
+ readonly ["extension-kind"]: ExtensionKind;
+ }
+
+ export namespace ExtensionKind {
+ export const TypeDiscovery: "type-discovery" = "type-discovery";
+ export type TypeDiscovery = "type-discovery";
+ }
+ export type ExtensionKind = ExtensionKind.TypeDiscovery;
+
+ export interface ExtensionCollectionMap {
+ "type-discovery"?: TypeDiscoveryExtension[];
+ [index: string]: Extension[] | undefined;
+ }
+
+ export interface ExtensionBase {
+ name: string;
+ args: any;
+ kind: ExtensionKind;
+ }
+
+ export interface ProfileData {
+ globalBucket: string;
+ task: string;
+ start: number;
+ length?: number;
+ }
+
+ export interface TypeDiscoveryExtension extends ExtensionBase {
+ kind: ExtensionKind.TypeDiscovery;
+ lookup: (searchDir: string, args: any) => string[];
+ }
+
+ export type Extension = TypeDiscoveryExtension;
+
+ export interface ExtensionCache {
+ getCompilerExtensions(): ExtensionCollectionMap;
+ getExtensionLoadingDiagnostics(): Diagnostic[];
+ }
+
+ export interface ExtensionHost extends ModuleResolutionHost {
+ loadExtension?(name: string): any;
+ }
+
+ export interface Program {
+ /**
+ * Gets a map of loaded compiler extensions
+ */
+ getCompilerExtensions(): ExtensionCollectionMap;
+
+ /**
+ * Gets only diagnostics reported while loading extensions
+ */
+ getExtensionLoadingDiagnostics(): Diagnostic[];
+ }
+
+ /* @internal */
+ export interface TypeCheckerHost {
+ getCompilerExtensions(): ExtensionCollectionMap;
+ }
+
+ export const perfTraces: Map = {};
+
+ function getExtensionRootName(qualifiedName: string) {
+ return qualifiedName.substring(0, qualifiedName.indexOf("[")) || qualifiedName;
+ }
+
+ function createTaskName(qualifiedName: string, task: string) {
+ return `${task}|${qualifiedName}`;
+ }
+
+ export function startProfile(enabled: boolean, key: string, bucket?: string) {
+ if (!enabled) return;
+ performance.emit(`start|${key}`);
+ perfTraces[key] = {
+ task: key,
+ start: performance.mark(),
+ length: undefined,
+ globalBucket: bucket
+ };
+ }
+
+ export function completeProfile(enabled: boolean, key: string) {
+ if (!enabled) return;
+ Debug.assert(!!perfTraces[key], "Completed profile did not have a corresponding start.");
+ perfTraces[key].length = performance.measure(perfTraces[key].globalBucket, perfTraces[key].start);
+ performance.emit(`end|${key}`);
+ }
+
+ export function startExtensionProfile(enabled: boolean, qualifiedName: string, task: string) {
+ if (!enabled) return;
+ const longTask = createTaskName(qualifiedName, task);
+ startProfile(/*enabled*/true, longTask, getExtensionRootName(qualifiedName));
+ }
+
+ export function completeExtensionProfile(enabled: boolean, qualifiedName: string, task: string) {
+ if (!enabled) return;
+ const longTask = createTaskName(qualifiedName, task);
+ completeProfile(/*enabled*/true, longTask);
+ }
+
+ function verifyType(thing: any, type: string, diagnostics: Diagnostic[], extName: string, extMember: string, extKind: ExtensionKind) {
+ if (typeof thing !== type) {
+ diagnostics.push(createCompilerDiagnostic(
+ Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected,
+ extName,
+ extMember,
+ extKind,
+ typeof thing,
+ type
+ ));
+ return false;
+ }
+ return true;
+ }
+
+ export function createExtensionCache(options: CompilerOptions, host: ExtensionHost, resolvedExtensionNames?: Map): ExtensionCache {
+
+ const diagnostics: Diagnostic[] = [];
+ const extOptions = options.extensions;
+ const extensionNames = (extOptions instanceof Array) ? extOptions : getKeys(extOptions);
+ // Eagerly evaluate extension paths, but lazily execute their contents
+ resolvedExtensionNames = resolvedExtensionNames || resolveExtensionNames();
+ let extensions: ExtensionCollectionMap;
+
+ const cache: ExtensionCache = {
+ getCompilerExtensions: () => {
+ if (!extensions) {
+ extensions = collectCompilerExtensions();
+ }
+ return extensions;
+ },
+ getExtensionLoadingDiagnostics: () => {
+ // To get extension loading diagnostics, we need to make sure we've actually loaded them
+ cache.getCompilerExtensions();
+ return diagnostics;
+ },
+ };
+ return cache;
+
+ function resolveExtensionNames(): Map {
+ const currentDirectory = host.getCurrentDirectory ? host.getCurrentDirectory() : "";
+ const extMap: Map = {};
+ forEach(extensionNames, name => {
+ const resolved = resolveModuleName(name, combinePaths(currentDirectory, "tsconfig.json"), options, host, /*loadJs*/true).resolvedModule;
+ if (resolved) {
+ extMap[name] = resolved.resolvedFileName;
+ }
+ });
+ return extMap;
+ }
+
+ function collectCompilerExtensions(): ExtensionCollectionMap {
+ const profilingEnabled = options.extendedDiagnostics;
+ const extensionLoadResults = map(extensionNames, (name) => {
+ const resolved = resolvedExtensionNames[name];
+ let result: any;
+ let error: any;
+ if (!resolved) {
+ error = new Error(`Host could not locate extension '${name}'.`);
+ }
+ if (resolved && host.loadExtension) {
+ try {
+ startProfile(profilingEnabled, name, name);
+ result = host.loadExtension(resolved);
+ completeProfile(profilingEnabled, name);
+ }
+ catch (e) {
+ error = e;
+ }
+ }
+ else if (!host.loadExtension) {
+ error = new Error("Extension loading not implemented in host!");
+ }
+ if (error) {
+ diagnostics.push(createCompilerDiagnostic(Diagnostics.Extension_loading_failed_with_error_0, error));
+ }
+ return { name, result, error };
+ });
+ const successfulExtensionLoadResults = filter(extensionLoadResults, res => !res.error);
+ const preparedExtensionObjects = map(successfulExtensionLoadResults, res => {
+ if (!res.result) {
+ return [];
+ }
+ const aggregate: Extension[] = [];
+ forEachKey(res.result, key => {
+ const potentialExtension = res.result[key];
+ if (!potentialExtension) {
+ return; // Avoid errors on explicitly exported null/undefined (why would someone do that, though?)
+ }
+ const annotatedKind = potentialExtension["extension-kind"];
+ if (typeof annotatedKind !== "string") {
+ return;
+ }
+ const ext: ExtensionBase = {
+ name: key !== "default" ? `${res.name}[${key}]` : res.name,
+ args: extensionNames === extOptions ? undefined : (extOptions as Map)[res.name],
+ kind: annotatedKind as ExtensionKind,
+ };
+ switch (ext.kind) {
+ case ExtensionKind.TypeDiscovery: {
+ const verified = verifyType(potentialExtension, "function", diagnostics, res.name, key, annotatedKind);
+ if (!verified) return aggregate;
+ (ext as TypeDiscoveryExtension).lookup = potentialExtension as any;
+ break;
+ }
+ default:
+ // Include a default case which just puts the extension unchecked onto the base extension
+ // This can allow language service extensions to query for custom extension kinds
+ (ext as any).__extension = potentialExtension;
+ break;
+ }
+ aggregate.push(ext as Extension);
+ });
+ return aggregate;
+ });
+ return groupBy(flatten(preparedExtensionObjects), elem => elem.kind) || {};
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/compiler/performance.ts b/src/compiler/performance.ts
index 89db876ae5e48..71f3dbd74854d 100644
--- a/src/compiler/performance.ts
+++ b/src/compiler/performance.ts
@@ -58,10 +58,11 @@ namespace ts.performance {
* @param measureName The name of the performance measurement.
* @param marker The timestamp of the starting mark.
*/
- export function measure(measureName: string, marker: number) {
+ export function measure(measureName: string, marker: number): number {
if (measures) {
- measures[measureName] = (getProperty(measures, measureName) || 0) + (timestamp() - marker);
+ return measures[measureName] = (getProperty(measures, measureName) || 0) + (timestamp() - marker);
}
+ return 0;
}
/**
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index 7d40b2f3219fc..9ba91f41bac81 100644
--- a/src/compiler/program.ts
+++ b/src/compiler/program.ts
@@ -1,6 +1,8 @@
///
///
///
+///
+
namespace ts {
/** The version of the TypeScript compiler release */
@@ -118,57 +120,51 @@ namespace ts {
skipTsx: boolean;
}
- function tryReadTypesSection(packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string {
- let jsonContent: { typings?: string, types?: string, main?: string };
- try {
- const jsonText = state.host.readFile(packageJsonPath);
- jsonContent = jsonText ? <{ typings?: string, types?: string, main?: string }>JSON.parse(jsonText) : {};
+ function getPackageEntry(packageJson: any, key: string, tag: string, state: ModuleResolutionState) {
+ const value = packageJson[key];
+ if (typeof value === tag) {
+ return value;
}
- catch (e) {
- // gracefully handle if readFile fails or returns not JSON
- jsonContent = {};
+ if (state.traceEnabled) {
+ trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, key, tag, typeof value);
}
+ return undefined;
+ }
- let typesFile: string;
- let fieldName: string;
- // first try to read content of 'typings' section (backward compatibility)
- if (jsonContent.typings) {
- if (typeof jsonContent.typings === "string") {
- fieldName = "typings";
- typesFile = jsonContent.typings;
- }
- else {
- if (state.traceEnabled) {
- trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "typings", typeof jsonContent.typings);
- }
- }
+ function getPackageEntryAsPath(packageJson: any, packageJsonPath: string, key: string, state: ModuleResolutionState) {
+ const value = getPackageEntry(packageJson, key, "string", state);
+ const path = value ? normalizePath(combinePaths(getDirectoryPath(packageJsonPath), value)) : undefined;
+ if (path && state.traceEnabled) {
+ trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, key, value, path);
}
- // then read 'types'
- if (!typesFile && jsonContent.types) {
- if (typeof jsonContent.types === "string") {
- fieldName = "types";
- typesFile = jsonContent.types;
- }
- else {
- if (state.traceEnabled) {
- trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "types", typeof jsonContent.types);
- }
- }
+ return path;
+ }
+
+ function getPackageTypes(packageJsonPath: string, state: ModuleResolutionState) {
+ const { config } = readConfigFile(packageJsonPath, state.host.readFile);
+ if (config) {
+ return getPackageEntryAsPath(config, packageJsonPath, "typings", state)
+ || getPackageEntryAsPath(config, packageJsonPath, "types", state)
+ // Use the main module for inferring types if no types package specified and the allowJs is set
+ || (state.compilerOptions.allowJs && getPackageEntryAsPath(config, packageJsonPath, "main", state));
}
- if (typesFile) {
- const typesFilePath = normalizePath(combinePaths(baseDirectory, typesFile));
+ else {
if (state.traceEnabled) {
- trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, typesFile, typesFilePath);
+ trace(state.host, Diagnostics.package_json_does_not_have_0_field, "types");
}
- return typesFilePath;
}
- // Use the main module for inferring types if no types package specified and the allowJs is set
- if (state.compilerOptions.allowJs && jsonContent.main && typeof jsonContent.main === "string") {
+ return undefined;
+ }
+
+ function getPackageMain(packageJsonPath: string, state: ModuleResolutionState) {
+ const { config } = readConfigFile(packageJsonPath, state.host.readFile);
+ if (config) {
+ return getPackageEntryAsPath(config, packageJsonPath, "main", state);
+ }
+ else {
if (state.traceEnabled) {
- trace(state.host, Diagnostics.No_types_specified_in_package_json_but_allowJs_is_set_so_returning_main_value_of_0, jsonContent.main);
+ trace(state.host, Diagnostics.package_json_does_not_have_0_field, "main");
}
- const mainFilePath = normalizePath(combinePaths(baseDirectory, jsonContent.main));
- return mainFilePath;
}
return undefined;
}
@@ -293,7 +289,7 @@ namespace ts {
};
}
- export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
+ export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, loadJs?: boolean): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
if (traceEnabled) {
trace(host, Diagnostics.Resolving_module_0_from_1, moduleName, containingFile);
@@ -315,7 +311,7 @@ namespace ts {
let result: ResolvedModuleWithFailedLookupLocations;
switch (moduleResolution) {
case ModuleResolutionKind.NodeJs:
- result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host);
+ result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, loadJs);
break;
case ModuleResolutionKind.Classic:
result = classicNameResolver(moduleName, containingFile, compilerOptions, host);
@@ -610,7 +606,7 @@ namespace ts {
};
}
- export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
+ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, loadJs?: boolean): ResolvedModuleWithFailedLookupLocations {
const containingDirectory = getDirectoryPath(containingFile);
const supportedExtensions = getSupportedExtensions(compilerOptions);
const traceEnabled = isTraceEnabled(compilerOptions, host);
@@ -626,7 +622,7 @@ namespace ts {
if (traceEnabled) {
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName);
}
- resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state);
+ resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state, loadJs);
isExternalLibraryImport = resolvedFileName !== undefined;
}
else {
@@ -716,25 +712,20 @@ namespace ts {
}
}
- function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): string {
+ function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState, loadJS?: boolean): string {
const packageJsonPath = combinePaths(candidate, "package.json");
const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host);
if (directoryExists && state.host.fileExists(packageJsonPath)) {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
}
- const typesFile = tryReadTypesSection(packageJsonPath, candidate, state);
+ const typesFile = loadJS ? getPackageMain(packageJsonPath, state) : getPackageTypes(packageJsonPath, state);
if (typesFile) {
const result = loadModuleFromFile(typesFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typesFile), state.host), state);
if (result) {
return result;
}
}
- else {
- if (state.traceEnabled) {
- trace(state.host, Diagnostics.package_json_does_not_have_types_field);
- }
- }
}
else {
if (state.traceEnabled) {
@@ -747,30 +738,31 @@ namespace ts {
return loadModuleFromFile(combinePaths(candidate, "index"), extensions, failedLookupLocation, !directoryExists, state);
}
- function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string {
+ function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, loadJS?: boolean): string {
const nodeModulesFolder = combinePaths(directory, "node_modules");
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
- const supportedExtensions = getSupportedExtensions(state.compilerOptions);
+
+ const supportedExtensions = getSupportedExtensions(state.compilerOptions, loadJS);
let result = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
if (result) {
return result;
}
- result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
+ result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state, loadJS);
if (result) {
return result;
}
}
- function loadModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string {
+ function loadModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState, loadJS?: boolean): string {
directory = normalizeSlashes(directory);
while (true) {
const baseName = getBaseFileName(directory);
if (baseName !== "node_modules") {
// Try to load source from the package
- const packageResult = loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state);
- if (packageResult && hasTypeScriptFileExtension(packageResult)) {
+ const packageResult = loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state, loadJS);
+ if (packageResult && (hasTypeScriptFileExtension(packageResult) || loadJS)) {
// Always prefer a TypeScript (.ts, .tsx, .d.ts) file shipped with the package
return packageResult;
}
@@ -954,6 +946,7 @@ namespace ts {
const newLine = getNewLineCharacter(options);
const realpath = sys.realpath && ((path: string) => sys.realpath(path));
+ const loadExtension = sys.loadExtension && ((name: string) => sys.loadExtension(name));
return {
getSourceFile,
@@ -969,7 +962,8 @@ namespace ts {
trace: (s: string) => sys.write(s + newLine),
directoryExists: directoryName => sys.directoryExists(directoryName),
getDirectories: (path: string) => sys.getDirectories(path),
- realpath
+ realpath,
+ loadExtension
};
}
@@ -1004,7 +998,8 @@ namespace ts {
}
const category = DiagnosticCategory[diagnostic.category].toLowerCase();
- output += `${ category } TS${ diagnostic.code }: ${ flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()) }${ host.getNewLine() }`;
+ const code = typeof diagnostic.code === "string" ? diagnostic.code : `TS${ diagnostic.code }`;
+ output += `${ category } ${ code }: ${ flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()) }${ host.getNewLine() }`;
}
return output;
}
@@ -1087,7 +1082,7 @@ namespace ts {
return result;
}
- export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program {
+ export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, extensionCache?: ExtensionCache): Program {
let program: Program;
let files: SourceFile[] = [];
let commonSourceDirectory: string;
@@ -1187,6 +1182,8 @@ namespace ts {
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
oldProgram = undefined;
+ extensionCache = extensionCache || createExtensionCache(options, host);
+
program = {
getRootFileNames: () => rootNames,
getSourceFile,
@@ -1209,7 +1206,13 @@ namespace ts {
getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(),
getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(),
getFileProcessingDiagnostics: () => fileProcessingDiagnostics,
- getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives
+ getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives,
+ getCompilerExtensions() {
+ return extensionCache.getCompilerExtensions();
+ },
+ getExtensionLoadingDiagnostics() {
+ return extensionCache.getExtensionLoadingDiagnostics();
+ },
};
verifyCompilerOptions();
@@ -1753,6 +1756,7 @@ namespace ts {
const allDiagnostics: Diagnostic[] = [];
addRange(allDiagnostics, fileProcessingDiagnostics.getGlobalDiagnostics());
addRange(allDiagnostics, programDiagnostics.getGlobalDiagnostics());
+ allDiagnostics.push(...extensionCache.getExtensionLoadingDiagnostics());
return sortAndDeduplicateDiagnostics(allDiagnostics);
}
diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts
index 29ae2c60af165..251c22fd43ade 100644
--- a/src/compiler/sys.ts
+++ b/src/compiler/sys.ts
@@ -32,6 +32,7 @@ namespace ts {
getMemoryUsage?(): number;
exit(exitCode?: number): void;
realpath?(path: string): string;
+ loadExtension?(name: string): any;
}
export interface FileWatcher {
@@ -548,6 +549,9 @@ namespace ts {
},
realpath(path: string): string {
return _fs.realpathSync(path);
+ },
+ loadExtension(name) {
+ return require(name);
}
};
return nodeSystem;
diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts
index 10538d0c009ee..9ecde9262f5f2 100644
--- a/src/compiler/tsc.ts
+++ b/src/compiler/tsc.ts
@@ -607,13 +607,18 @@ namespace ts {
// First get and report any syntactic errors.
diagnostics = program.getSyntacticDiagnostics();
+ // Count warnings/messages and ignore them for determining continued error reporting
+ const nonErrorCount = countWhere(diagnostics, d => d.category !== DiagnosticCategory.Error);
+
// If we didn't have any syntactic errors, then also try getting the global and
// semantic errors.
- if (diagnostics.length === 0) {
- diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics());
+ if (diagnostics.length === nonErrorCount) {
+ diagnostics = diagnostics.concat(program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()));
+
+ const nonErrorCount = countWhere(diagnostics, d => d.category !== DiagnosticCategory.Error);
- if (diagnostics.length === 0) {
- diagnostics = program.getSemanticDiagnostics();
+ if (diagnostics.length === nonErrorCount) {
+ diagnostics = diagnostics.concat(program.getSemanticDiagnostics());
}
}
diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json
index cc9bfddcece78..9d98da46cd1a1 100644
--- a/src/compiler/tsconfig.json
+++ b/src/compiler/tsconfig.json
@@ -24,6 +24,7 @@
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
+ "extensions.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index 28ededebb0de1..dc750e2c9dff5 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -2540,7 +2540,7 @@ namespace ts {
length: number;
messageText: string | DiagnosticMessageChain;
category: DiagnosticCategory;
- code: number;
+ code: number | string;
}
export enum DiagnosticCategory {
@@ -2633,6 +2633,7 @@ namespace ts {
typeRoots?: string[];
/*@internal*/ version?: boolean;
/*@internal*/ watch?: boolean;
+ extensions?: string[] | Map;
[option: string]: CompilerOptionsValue | undefined;
}
@@ -2966,6 +2967,14 @@ namespace ts {
* This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files
*/
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
+
+ /**
+ * Delegates the loading of compiler extensions to the compiler host.
+ * The function should return the result of executing the code of an extension
+ * - its exported members. These members will be searched for objects who have been decorated with
+ * specific flags.
+ */
+ loadExtension?(extension: string): any;
}
export interface TextSpan {
diff --git a/src/harness/extensionRunner.ts b/src/harness/extensionRunner.ts
new file mode 100644
index 0000000000000..ca7ab8c4a6d9b
--- /dev/null
+++ b/src/harness/extensionRunner.ts
@@ -0,0 +1,492 @@
+///
+///
+///
+
+interface ExtensionTestConfig {
+ inputFiles: string[]; // Files from the source directory to include in the compilation
+ fourslashTest?: string; // File from the fourslash directory to test this compilation with
+ availableExtensions: string[]; // Extensions from the available directory to make available to the test
+ compilerOptions?: ts.CompilerOptions; // Optional compiler options to run with (usually at least "extensions" is specified)
+}
+
+type VirtualCompilationFunction = (files: string[], options: ts.CompilerOptions) => Harness.Compiler.CompilerResult;
+
+class ExtensionRunner extends RunnerBase {
+ private basePath = "tests/cases/extensions";
+ private scenarioPath = ts.combinePaths(this.basePath, "scenarios");
+ private extensionPath = ts.combinePaths(this.basePath, "available");
+ private sourcePath = ts.combinePaths(this.basePath, "source");
+ private fourslashPath = ts.combinePaths(this.basePath, "fourslash");
+ private extensionAPI: ts.Map = {};
+ private extensions: ts.Map> = {};
+ private virtualLib: ts.Map = {};
+ private virtualFs: ts.Map = {};
+
+ prettyPrintDiagnostic(diagnostic: ts.Diagnostic): string {
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
+ if (diagnostic.file) {
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
+ return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
+ }
+ else {
+ return `!!!${message}`;
+ }
+ }
+
+ private innerCanonicalName = ts.createGetCanonicalFileName(true);
+ private getCanonicalFileName = (fileName: string) => ts.toPath(fileName, "/", this.innerCanonicalName);
+
+ loadSetIntoFsAt(set: ts.Map, prefix: string) {
+ ts.Debug.assert(!!prefix, "Prefix must exist");
+ ts.Debug.assert(set !== this.virtualFs, "You cannot try to load the fs into itself.");
+
+ // Load a fileset at the given location, but exclude the 'lib' kind files from the added set (they'll be reloaded at the top level before compilation)
+ ts.forEachKey(set, key => ts.forEachKey(this.virtualLib, path => key === path) ? void 0 : void (this.virtualFs[this.getCanonicalFileName(`${prefix}/${key}`)] = set[key]));
+ }
+
+ loadSetIntoFs(set: ts.Map) {
+ ts.Debug.assert(set !== this.virtualFs, "You cannot try to load the fs into itself.");
+ ts.forEachKey(set, key => void (this.virtualFs[this.getCanonicalFileName(key)] = set[key]));
+ }
+
+ private traces: string[] = [];
+ private mockHost: ts.CompilerHost = {
+ useCaseSensitiveFileNames: () => true,
+ getNewLine: () => "\n",
+ readFile: (path) => this.virtualFs[this.mockHost.getCanonicalFileName(path)],
+ writeFile: (path, content, foo, bar, baz) => {
+ this.virtualFs[this.mockHost.getCanonicalFileName(path)] = content;
+ },
+ fileExists: (path) => {
+ return !!this.virtualFs[this.mockHost.getCanonicalFileName(path)];
+ },
+ directoryExists: (path) => {
+ const fullPath = this.mockHost.getCanonicalFileName(path);
+ return ts.forEach(ts.getKeys(this.virtualFs), key => ts.startsWith(key, fullPath));
+ },
+ getCurrentDirectory(): string { return "/"; },
+ getSourceFile: (path, languageVersion, onError): ts.SourceFile => {
+ const fullPath = this.mockHost.getCanonicalFileName(path);
+ return ts.createSourceFile(fullPath, this.virtualFs[fullPath], languageVersion);
+ },
+ getDefaultLibLocation: () => "/lib/",
+ getDefaultLibFileName: (options) => {
+ return ts.combinePaths(this.mockHost.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
+ },
+ getCanonicalFileName: this.getCanonicalFileName,
+ getDirectories: (path) => {
+ path = this.mockHost.getCanonicalFileName(path);
+ return ts.filter(ts.map(ts.filter(ts.getKeys(this.virtualFs),
+ fullpath => ts.startsWith(fullpath, path) && fullpath.substr(path.length, 1) === "/"),
+ fullpath => fullpath.substr(path.length + 1).indexOf("/") >= 0 ? fullpath.substr(0, 1 + path.length + fullpath.substr(path.length + 1).indexOf("/")) : fullpath),
+ fullpath => fullpath.lastIndexOf(".") === -1);
+ },
+ loadExtension: (path) => this.mockLoadExtension(path),
+ trace: (s) => {
+ this.traces.push(s);
+ }
+ };
+
+ mockLoadExtension(path: string) {
+ const fullPath = this.getCanonicalFileName(path);
+ const m = { exports: {} };
+ ((module, exports, require) => { eval(this.virtualFs[fullPath]); })(
+ m,
+ m.exports,
+ (name: string) => {
+ return this.mockLoadExtension(
+ this.getCanonicalFileName(
+ ts.resolveModuleName(name, fullPath, { module: ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeJs }, this.mockHost, true).resolvedModule.resolvedFileName
+ )
+ );
+ }
+ );
+ return m.exports;
+ }
+
+ makeLSMockAdapter(files: string[], options: ts.CompilerOptions, token?: ts.HostCancellationToken) {
+ const adapter = new Harness.LanguageService.NativeLanguageServiceAdapter(token, options);
+ // The host returned by the harness is _mostly_ suitable for use here
+ // it just needs to be monkeypatched to load extensions, report directories, and canonicalize script paths
+ const host = adapter.getHost();
+ host.getDefaultLibFileName = () => "/lib/lib.d.ts";
+ host.getCurrentDirectory = () => "/";
+ (host as ts.LanguageServiceHost).loadExtension = (path) => this.mockLoadExtension(path);
+ (host as ts.LanguageServiceHost).useCaseSensitiveFileNames = () => true;
+ host.trace = (s) => {
+ this.traces.push(s);
+ };
+ host.getScriptInfo = (fileName: string) => {
+ fileName = this.getCanonicalFileName(fileName);
+ return ts.lookUp(host.fileNameToScript, fileName);
+ };
+ host.getDirectories = (s: string) => this.mockHost.getDirectories(s);
+ host.addScript = (fileName: string, content: string, isRootFile: boolean): void => {
+ const canonical = this.getCanonicalFileName(fileName);
+ host.fileNameToScript[canonical] = new Harness.LanguageService.ScriptInfo(canonical, content, isRootFile);
+ };
+ ts.forEach(files, file => {
+ host.addScript(file, this.virtualFs[file], looksLikeRootFile(file));
+ });
+
+ return adapter;
+
+ function looksLikeRootFile(file: string) {
+ return ts.endsWith(file, ".ts") && !ts.endsWith(file, ".d.ts") && (file.indexOf("node_modules") === -1);
+ }
+ }
+
+ makeMockLSHost(files: string[], options: ts.CompilerOptions) {
+ const adapter = this.makeLSMockAdapter(files, options);
+ return adapter.getHost();
+ };
+
+ getTraces(): string[] {
+ const traces = this.traces;
+ this.traces = [];
+ return traces.map(t => t.replace(/\([0-9\.e\+\-]+ ms\)$/, "(REDACTED ms)"));
+ }
+
+ languageServiceCompile(typescriptFiles: string[], options: ts.CompilerOptions): Harness.Compiler.CompilerResult {
+ const self = this;
+ const host = this.makeMockLSHost(ts.getKeys(this.virtualFs), options);
+ const service = ts.createLanguageService(host);
+ const fileResults: Harness.Compiler.GeneratedFile[] = [];
+
+ const diagnostics = ts.concatenate(ts.concatenate(
+ service.getProgramDiagnostics(),
+ ts.flatten(ts.map(typescriptFiles, fileName => service.getSyntacticDiagnostics(this.getCanonicalFileName(fileName))))),
+ ts.flatten(ts.map(typescriptFiles, fileName => service.getSemanticDiagnostics(this.getCanonicalFileName(fileName)))));
+
+ const emitResult = service.getProgram().emit(/*targetSourceFile*/undefined, writeFile);
+
+ const allDiagnostics = ts.sortAndDeduplicateDiagnostics(ts.concatenate(diagnostics, emitResult.diagnostics));
+
+ return new Harness.Compiler.CompilerResult(fileResults, allDiagnostics, /*program*/undefined, host.getCurrentDirectory(), emitResult.sourceMaps, this.getTraces());
+
+ function writeFile(fileName: string, code: string, writeByteOrderMark: boolean, onError: (message: string) => void, sourceFiles: ts.SourceFile[]) {
+ fileResults.push({
+ fileName,
+ writeByteOrderMark,
+ code
+ });
+ self.mockHost.writeFile(fileName, code, writeByteOrderMark, onError, sourceFiles);
+ }
+ }
+
+ programCompile(typescriptFiles: string[], options: ts.CompilerOptions): Harness.Compiler.CompilerResult {
+ const self = this;
+ const program = ts.createProgram(typescriptFiles, options, this.mockHost);
+ const fileResults: Harness.Compiler.GeneratedFile[] = [];
+ const diagnostics = ts.getPreEmitDiagnostics(program);
+ const emitResult = program.emit(/*targetSourceFile*/undefined, writeFile);
+
+ const allDiagnostics = ts.sortAndDeduplicateDiagnostics(ts.concatenate(diagnostics, emitResult.diagnostics));
+
+ return new Harness.Compiler.CompilerResult(fileResults, allDiagnostics, /*program*/undefined, this.mockHost.getCurrentDirectory(), emitResult.sourceMaps, this.getTraces());
+ function writeFile(fileName: string, code: string, writeByteOrderMark: boolean, onError: (message: string) => void, sourceFiles: ts.SourceFile[]) {
+ fileResults.push({
+ fileName,
+ writeByteOrderMark,
+ code
+ });
+ self.mockHost.writeFile(fileName, code, writeByteOrderMark, onError, sourceFiles);
+ }
+ }
+
+ compile(fileset: ts.Map, options: ts.CompilerOptions, compileFunc: VirtualCompilationFunction): Harness.Compiler.CompilerResult {
+ this.loadSetIntoFs(this.virtualLib);
+ this.loadSetIntoFs(fileset);
+
+ // Consider all TS files in the passed fileset as the root files, but not any under a node_modules folder
+ const typescriptFiles = ts.filter(ts.getKeys(fileset), name => ts.endsWith(name, ".ts") && !(name.indexOf("node_modules") >= 0));
+ return compileFunc(typescriptFiles, options);
+ }
+
+ buildMap(compileFunc: VirtualCompilationFunction, map: ts.Map, out: ts.Map, compilerOptions?: ts.CompilerOptions, shouldError?: boolean): Harness.Compiler.CompilerResult {
+ const results = this.compile(map, compilerOptions ? compilerOptions : { module: ts.ModuleKind.CommonJS, declaration: true }, compileFunc);
+ const diagnostics = results.errors;
+ if (shouldError && diagnostics && diagnostics.length) {
+ for (let i = 0; i < diagnostics.length; i++) {
+ console.log(this.prettyPrintDiagnostic(diagnostics[i]));
+ }
+ throw new Error("Compiling test harness extension API code resulted in errors.");
+ }
+ ts.copyMap(this.virtualFs, out);
+ this.virtualFs = {};
+ return results;
+ }
+
+ private loadExtensions() {
+ this.extensionAPI = {
+ "package.json": Harness.IO.readFile(ts.combinePaths(this.extensionPath, "extension-api/package.json")),
+ "index.ts": Harness.IO.readFile(ts.combinePaths(this.extensionPath, "extension-api/index.ts")),
+ };
+ this.buildMap((str, opts) => this.programCompile(str, opts), this.extensionAPI, this.extensionAPI, { module: ts.ModuleKind.CommonJS, declaration: true }, /*shouldError*/true);
+
+ ts.forEach(Harness.IO.getDirectories(this.extensionPath), path => {
+ if (path === "extension-api" || path === "typescript") return; // Since these are dependencies of every actual test extension, we handle them specially
+ const packageDir = ts.combinePaths(this.extensionPath, path);
+ const extensionFileset: ts.Map = {};
+ const extensionFiles = this.enumerateFiles(packageDir, /*regex*/ undefined, { recursive: true });
+ ts.forEach(extensionFiles, name => {
+ const shortName = name.substring(packageDir.length + 1);
+ extensionFileset[shortName] = Harness.IO.readFile(name);
+ });
+ this.loadSetIntoFsAt(this.extensionAPI, "/node_modules/extension-api");
+
+ this.buildMap((str, opts) => this.programCompile(str, opts), extensionFileset, extensionFileset, { module: ts.ModuleKind.CommonJS, declaration: true }, /*shouldError*/true);
+ this.extensions[path] = extensionFileset;
+ });
+ }
+
+ constructor() {
+ super();
+ const {content: libContent} = Harness.getDefaultLibraryFile(Harness.IO);
+ const tsLibContents = Harness.IO.readFile("built/local/typescript.d.ts");
+ this.virtualLib = {
+ "/lib/lib.d.ts": libContent,
+ "/node_modules/typescript/index.d.ts": tsLibContents
+ };
+ this.loadExtensions();
+ }
+
+ kind(): "extension" {
+ return "extension";
+ }
+
+ enumerateTestFiles(): string[] {
+ return this.enumerateFiles(this.scenarioPath, /\.json$/, { recursive: true });
+ }
+
+ /** Setup the runner's tests so that they are ready to be executed by the harness
+ * The first test should be a describe/it block that sets up the harness's compiler instance appropriately
+ */
+ public initializeTests(): void {
+ describe("Compiler Extensions", () => {
+ if (this.tests.length === 0) {
+ const testFiles = this.enumerateTestFiles();
+ testFiles.forEach(fn => {
+ this.runTest(fn);
+ });
+ }
+ else {
+ this.tests.forEach(test => this.runTest(test));
+ }
+ });
+ }
+
+ getByteOrderMarkText(file: Harness.Compiler.GeneratedFile): string {
+ return file.writeByteOrderMark ? "\u00EF\u00BB\u00BF" : "";
+ }
+
+ private compileTargets: [string, VirtualCompilationFunction][] = [["CompilerHost", (str, opts) => this.programCompile(str, opts)], ["LanguageServiceHost", (str, opts) => this.languageServiceCompile(str, opts)]];
+ /**
+ * Extensions tests are complete end-to-end tests with multiple compilations to prepare a test
+ *
+ * Tests need to be:
+ * Run under both `compilerHost` and `languageServiceHost` environments
+ * - When under LSHost, verify all fourslash test-type results included in the test
+ * - Verify output baseline
+ * - Verify error baseline
+ * - Verify sourcemaps if need be
+ * - Verify traces if need be
+ */
+ private runTest(caseName: string) {
+ const caseNameNoExtension = caseName.replace(/\.json$/, "");
+ describe(caseNameNoExtension, () => {
+ let shortCasePath: string;
+ let testConfigText: string;
+ let testConfig: ExtensionTestConfig;
+ let inputSources: ts.Map;
+ let inputTestFiles: Harness.Compiler.TestFile[];
+ before(() => {
+ shortCasePath = caseName.substring(this.scenarioPath.length + 1).replace(/\.json$/, "");
+ testConfigText = Harness.IO.readFile(caseName);
+ testConfig = JSON.parse(testConfigText);
+ inputSources = {};
+ inputTestFiles = [];
+ ts.forEach(testConfig.inputFiles, name => {
+ inputSources[name] = Harness.IO.readFile(ts.combinePaths(this.sourcePath, name));
+ inputTestFiles.push({
+ unitName: this.getCanonicalFileName(name),
+ content: inputSources[name]
+ });
+ });
+ });
+
+ after(() => {
+ shortCasePath = undefined;
+ testConfigText = undefined;
+ testConfig = undefined;
+ inputSources = undefined;
+ inputTestFiles = undefined;
+ });
+
+ ts.forEach(this.compileTargets, ([name, compileCb]) => {
+ describe(`${name}`, () => {
+ let sources: ts.Map;
+ let result: Harness.Compiler.CompilerResult;
+ before(() => {
+ this.traces = []; // Clear out any traces from tests which made traces, but didn't specify traceResolution
+ this.virtualFs = {}; // In case a fourslash test was run last (which doesn't clear FS on end like buildMap does), clear the FS
+ sources = {};
+ ts.copyMap(inputSources, sources);
+ ts.forEach(testConfig.availableExtensions, ext => this.loadSetIntoFsAt(this.extensions[ext], `/node_modules/${ext}`));
+ result = this.buildMap(compileCb, sources, sources, testConfig.compilerOptions, /*shouldError*/false);
+ });
+
+ after(() => {
+ sources = undefined;
+ result = undefined;
+ });
+
+ const errorsTestName = `Correct errors`;
+ it(errorsTestName, () => {
+ Harness.Baseline.runBaseline(errorsTestName, `${name}/${shortCasePath}.errors.txt`, () => {
+ /* tslint:disable:no-null-keyword */
+ if (result.errors.length === 0) return null;
+ /* tslint:enable:no-null-keyword */
+ return Harness.Compiler.getErrorBaseline(inputTestFiles, result.errors);
+ });
+ });
+
+ const traceTestName = `Correct traces`;
+ it(traceTestName, () => {
+ if (!(testConfig.compilerOptions.traceResolution)) {
+ return;
+ }
+ Harness.Baseline.runBaseline(traceTestName, `${name}/${shortCasePath}.trace.txt`, (): string => {
+ return (result.traceResults || []).join("\n");
+ });
+ });
+
+ const sourcemapTestName = `Correct sourcemap content`;
+ it(sourcemapTestName, () => {
+ if (!(testConfig.compilerOptions.sourceMap || testConfig.compilerOptions.inlineSourceMap)) {
+ return;
+ }
+ Harness.Baseline.runBaseline(sourcemapTestName, `${name}/${shortCasePath}.sourcemap.txt`, () => {
+ const record = result.getSourceMapRecord();
+ if (testConfig.compilerOptions.noEmitOnError && result.errors.length !== 0 && record === undefined) {
+ // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required.
+ /* tslint:disable:no-null-keyword */
+ return null;
+ /* tslint:enable:no-null-keyword */
+ }
+ return record;
+ });
+ });
+
+ const sourcemapOutputTestName = `Correct sourcemap output`;
+ it(sourcemapOutputTestName, () => {
+ if (testConfig.compilerOptions.inlineSourceMap) {
+ if (result.sourceMaps.length > 0) {
+ throw new Error("No sourcemap files should be generated if inlineSourceMaps was set.");
+ }
+ return;
+ }
+ else if (!testConfig.compilerOptions.sourceMap) {
+ return;
+ }
+ if (result.sourceMaps.length !== result.files.length) {
+ throw new Error("Number of sourcemap files should be same as js files.");
+ }
+
+ Harness.Baseline.runBaseline(sourcemapOutputTestName, `${name}/${shortCasePath}.js.map`, () => {
+ if (testConfig.compilerOptions.noEmitOnError && result.errors.length !== 0 && result.sourceMaps.length === 0) {
+ // We need to return null here or the runBaseLine will actually create a empty file.
+ // Baselining isn't required here because there is no output.
+ /* tslint:disable:no-null-keyword */
+ return null;
+ /* tslint:enable:no-null-keyword */
+ }
+
+ let sourceMapCode = "";
+ for (let i = 0; i < result.sourceMaps.length; i++) {
+ sourceMapCode += "//// [" + Harness.Path.getFileName(result.sourceMaps[i].fileName) + "]\r\n";
+ sourceMapCode += this.getByteOrderMarkText(result.sourceMaps[i]);
+ sourceMapCode += result.sourceMaps[i].code;
+ }
+
+ return sourceMapCode;
+ });
+ });
+
+ const emitOutputTestName = `Correct emit (JS/DTS)`;
+ it(emitOutputTestName, () => {
+ if (!ts.forEach(testConfig.inputFiles, name => !ts.endsWith(name, ".d.ts"))) {
+ return;
+ }
+ if (!testConfig.compilerOptions.noEmit && result.files.length === 0 && result.errors.length === 0) {
+ throw new Error("Expected at least one js file to be emitted or at least one error to be created.");
+ }
+
+ // check js output
+ Harness.Baseline.runBaseline(emitOutputTestName, `${name}/${shortCasePath}.js`, () => {
+ let tsCode = "";
+ const tsSources = inputTestFiles;
+ if (tsSources.length > 1) {
+ tsCode += "//// [" + caseNameNoExtension + "] ////\r\n\r\n";
+ }
+ for (let i = 0; i < tsSources.length; i++) {
+ tsCode += "//// [" + Harness.Path.getFileName(tsSources[i].unitName) + "]\r\n";
+ tsCode += tsSources[i].content + (i < (tsSources.length - 1) ? "\r\n" : "");
+ }
+
+ let jsCode = "";
+ for (let i = 0; i < result.files.length; i++) {
+ jsCode += "//// [" + Harness.Path.getFileName(result.files[i].fileName) + "]\r\n";
+ jsCode += this.getByteOrderMarkText(result.files[i]);
+ jsCode += result.files[i].code;
+ }
+
+ if (result.declFilesCode.length > 0) {
+ jsCode += "\r\n\r\n";
+ for (let i = 0; i < result.declFilesCode.length; i++) {
+ jsCode += "//// [" + Harness.Path.getFileName(result.declFilesCode[i].fileName) + "]\r\n";
+ jsCode += this.getByteOrderMarkText(result.declFilesCode[i]);
+ jsCode += result.declFilesCode[i].code;
+ }
+ }
+
+ if (jsCode.length > 0) {
+ return tsCode + "\r\n\r\n" + jsCode;
+ }
+ else {
+ /* tslint:disable:no-null-keyword */
+ return null;
+ /* tslint:enable:no-null-keyword */
+ }
+ });
+ });
+ });
+ });
+
+ it("passes fourslash verification", () => {
+ if (testConfig.fourslashTest) {
+ this.virtualFs = {};
+ const testFile = `${this.fourslashPath}/${testConfig.fourslashTest}`;
+ let testFileContents = Harness.IO.readFile(testFile);
+ testFileContents = testFileContents.replace(`/// `, "");
+ const testContent = [`/// `, ""];
+ ts.forEach(inputTestFiles, testFile => {
+ testContent.push(`// @Filename: ${testFile.unitName.substring(1)}`); // Drop leading /
+ testContent.push(...testFile.content.split("\n").map(s => `////${s}`));
+ });
+ testContent.push("// @Filename: tsconfig.json");
+ testContent.push(`////${JSON.stringify(testConfig.compilerOptions)}`);
+ testContent.push(testFileContents);
+ const finishedTestContent = testContent.join("\n");
+
+ this.loadSetIntoFs(this.virtualLib);
+ ts.forEach(testConfig.availableExtensions, ext => this.loadSetIntoFsAt(this.extensions[ext], `/node_modules/${ext}`));
+
+ const adapterFactory = (token: ts.HostCancellationToken) => this.makeLSMockAdapter(ts.getKeys(this.virtualFs), testConfig.compilerOptions, token);
+
+ FourSlash.runFourSlashTestContent(shortCasePath, adapterFactory, finishedTestContent, testFile);
+ }
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts
index a42abbbc60909..14f16eb09f2d6 100644
--- a/src/harness/fourslash.ts
+++ b/src/harness/fourslash.ts
@@ -242,7 +242,7 @@ namespace FourSlash {
}
}
- constructor(private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) {
+ constructor(private basePath: string, private testType: FourSlashTestType | ((token: ts.HostCancellationToken) => Harness.LanguageService.LanguageServiceAdapter), public testData: FourSlashData) {
// Create a new Services Adapter
this.cancellationToken = new TestCancellationToken();
const compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
@@ -250,7 +250,13 @@ namespace FourSlash {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}
- const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
+ let languageServiceAdapter: Harness.LanguageService.LanguageServiceAdapter;
+ if (typeof testType === "number") {
+ languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
+ }
+ else {
+ languageServiceAdapter = testType(this.cancellationToken);
+ }
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
this.languageService = languageServiceAdapter.getLanguageService();
@@ -2266,7 +2272,7 @@ namespace FourSlash {
runFourSlashTestContent(basePath, testType, content, fileName);
}
- export function runFourSlashTestContent(basePath: string, testType: FourSlashTestType, content: string, fileName: string): void {
+ export function runFourSlashTestContent(basePath: string, testType: FourSlashTestType | ((token: ts.HostCancellationToken) => Harness.LanguageService.LanguageServiceAdapter), content: string, fileName: string): void {
// Parse out the files and their metadata
const testData = parseTestData(basePath, content, fileName);
diff --git a/src/harness/harness.ts b/src/harness/harness.ts
index f27e7e1c1749a..4579dbe4aae59 100644
--- a/src/harness/harness.ts
+++ b/src/harness/harness.ts
@@ -1209,6 +1209,11 @@ namespace Harness {
return normalized;
}
+ function getDiagnosticCodeString(code: string | number) {
+ if (typeof code === "number") return `TS${code}`;
+ return code;
+ }
+
export function minimalDiagnosticsToString(diagnostics: ts.Diagnostic[]) {
return ts.formatDiagnostics(diagnostics, { getCanonicalFileName, getCurrentDirectory: () => "", getNewLine: () => Harness.IO.newLine() });
}
@@ -1226,7 +1231,7 @@ namespace Harness {
.split("\n")
.map(s => s.length > 0 && s.charAt(s.length - 1) === "\r" ? s.substr(0, s.length - 1) : s)
.filter(s => s.length > 0)
- .map(s => "!!! " + ts.DiagnosticCategory[error.category].toLowerCase() + " TS" + error.code + ": " + s);
+ .map(s => "!!! " + ts.DiagnosticCategory[error.category].toLowerCase() + " " + getDiagnosticCodeString(error.code) + ": " + s);
errLines.forEach(e => outputLines.push(e));
// do not count errors from lib.d.ts here, they are computed separately as numLibraryDiagnostics
diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts
index d7ed04b627f4f..fdd2f6602ecb5 100644
--- a/src/harness/harnessLanguageService.ts
+++ b/src/harness/harnessLanguageService.ts
@@ -123,7 +123,7 @@ namespace Harness.LanguageService {
}
export class LanguageServiceAdapterHost {
- protected fileNameToScript: ts.Map = {};
+ public fileNameToScript: ts.Map = {};
constructor(protected cancellationToken = DefaultHostCancellationToken.Instance,
protected settings = ts.getDefaultCompilerOptions()) {
@@ -366,6 +366,9 @@ namespace Harness.LanguageService {
getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics());
}
+ getProgramDiagnostics(): ts.Diagnostic[] {
+ return unwrapJSONCallResult(this.shim.getProgramDiagnostics());
+ }
getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] {
return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length));
}
diff --git a/src/harness/runner.ts b/src/harness/runner.ts
index 4b1945f5baa33..942ee516e9476 100644
--- a/src/harness/runner.ts
+++ b/src/harness/runner.ts
@@ -17,6 +17,7 @@
///
///
///
+///
///
///
@@ -58,6 +59,8 @@ function createRunner(kind: TestRunnerKind): RunnerBase {
return new RWCRunner();
case "test262":
return new Test262BaselineRunner();
+ case "extension":
+ return new ExtensionRunner();
}
}
@@ -155,6 +158,9 @@ if (testConfigContent !== "") {
case "test262":
runners.push(new Test262BaselineRunner());
break;
+ case "extension":
+ runners.push(new ExtensionRunner());
+ break;
}
}
}
@@ -176,6 +182,9 @@ if (runners.length === 0) {
runners.push(new FourSlashRunner(FourSlashTestType.ShimsWithPreprocess));
runners.push(new FourSlashRunner(FourSlashTestType.Server));
// runners.push(new GeneratedFourslashRunner());
+
+ // extension
+ runners.push(new ExtensionRunner());
}
if (taskConfigsFolder) {
diff --git a/src/harness/runnerbase.ts b/src/harness/runnerbase.ts
index 346382b7a5721..35d345463fde7 100644
--- a/src/harness/runnerbase.ts
+++ b/src/harness/runnerbase.ts
@@ -1,7 +1,7 @@
///
-type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project" | "rwc" | "test262";
+type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project" | "rwc" | "test262" | "extension";
type CompilerTestKind = "conformance" | "compiler";
type FourslashTestKind = "fourslash" | "fourslash-shims" | "fourslash-shims-pp" | "fourslash-server";
diff --git a/src/server/client.ts b/src/server/client.ts
index f04dbd8dc0253..d12c50459b6fe 100644
--- a/src/server/client.ts
+++ b/src/server/client.ts
@@ -395,6 +395,10 @@ namespace ts.server {
}
getCompilerOptionsDiagnostics(): Diagnostic[] {
+ return this.getProgramDiagnostics();
+ }
+
+ getProgramDiagnostics(): Diagnostic[] {
throw new Error("Not Implemented Yet.");
}
diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts
index 6e9adad7f472f..acc12116ecb57 100644
--- a/src/server/editorServices.ts
+++ b/src/server/editorServices.ts
@@ -1150,7 +1150,7 @@ namespace ts.server {
info.setFormatOptions(this.getFormatCodeOptions());
this.filenameToScriptInfo[fileName] = info;
if (!info.isOpen) {
- info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); });
+ info.fileWatcher = this.host.watchFile && this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); });
}
}
}
diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts
index 3b013a4a924ac..7e63e20720c8a 100644
--- a/src/services/jsTyping.ts
+++ b/src/services/jsTyping.ts
@@ -6,7 +6,7 @@
/* @internal */
namespace ts.JsTyping {
- export interface TypingResolutionHost {
+ export interface TypingResolutionHost extends ExtensionHost {
directoryExists: (path: string) => boolean;
fileExists: (fileName: string) => boolean;
readFile: (path: string, encoding?: string) => string;
@@ -35,6 +35,7 @@ namespace ts.JsTyping {
* @param packageNameToTypingLocation is the map of package names to their cached typing locations
* @param typingOptions are used to customize the typing inference process
* @param compilerOptions are used as a source for typing inference
+ * @param cache is the optional extension cache to lookup type discovery extensions in - one will be made if it cannot be provided
*/
export function discoverTypings(
host: TypingResolutionHost,
@@ -43,7 +44,8 @@ namespace ts.JsTyping {
safeListPath: Path,
packageNameToTypingLocation: Map,
typingOptions: TypingOptions,
- compilerOptions: CompilerOptions):
+ compilerOptions: CompilerOptions,
+ cache: ExtensionCache = createExtensionCache(compilerOptions, host)):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
// A typing name to typing file path mapping
@@ -88,6 +90,8 @@ namespace ts.JsTyping {
const nodeModulesPath = combinePaths(searchDir, "node_modules");
getTypingNamesFromNodeModuleFolder(nodeModulesPath);
+
+ getTypingNamesFromExtensions(searchDir, cache);
}
getTypingNamesFromSourceFileNames(fileNames);
@@ -223,5 +227,24 @@ namespace ts.JsTyping {
mergeTypings(typingNames);
}
+
+ function getTypingNamesFromExtensions(searchPath: string, cache: ExtensionCache) {
+ // We don't report issues loading type discovery extensions (if the host cares about them, it should load them in advance and pass in a cache)
+ const extensions = cache.getCompilerExtensions()["type-discovery"];
+ for (const extension of extensions) {
+ let results: string[];
+ try {
+ startExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup");
+ results = extension.lookup(searchPath, extension.args);
+ completeExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup");
+ }
+ catch (e) {
+ // There's presently no way to report errors during discover typings other than a hard failure (which is to be avoided)
+ }
+ if (results) {
+ mergeTypings(results);
+ }
+ }
+ }
}
}
diff --git a/src/services/services.ts b/src/services/services.ts
index ab1e30a44a607..7805fd19e03b9 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -1167,6 +1167,8 @@ namespace ts {
resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
directoryExists?(directoryName: string): boolean;
getDirectories?(directoryName: string): string[];
+
+ loadExtension?(path: string): any;
}
//
@@ -1179,10 +1181,13 @@ namespace ts {
getSyntacticDiagnostics(fileName: string): Diagnostic[];
getSemanticDiagnostics(fileName: string): Diagnostic[];
- // TODO: Rename this to getProgramDiagnostics to better indicate that these are any
- // diagnostics present for the program level, and not just 'options' diagnostics.
+ /**
+ * @deprecated Use getProgramDiagnostics instead.
+ */
getCompilerOptionsDiagnostics(): Diagnostic[];
+ getProgramDiagnostics(): Diagnostic[];
+
/**
* @deprecated Use getEncodedSyntacticClassifications instead.
*/
@@ -1826,6 +1831,7 @@ namespace ts {
version: string;
scriptSnapshot: IScriptSnapshot;
scriptKind: ScriptKind;
+ isRoot: boolean;
}
interface DocumentRegistryEntry {
@@ -1902,7 +1908,7 @@ namespace ts {
// Initialize the list with the root file names
const rootFileNames = host.getScriptFileNames();
for (const fileName of rootFileNames) {
- this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
+ this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName), /*isRoot*/true);
}
// store the compilation settings
@@ -1913,7 +1919,7 @@ namespace ts {
return this._compilationSettings;
}
- private createEntry(fileName: string, path: Path) {
+ private createEntry(fileName: string, path: Path, isRoot: boolean) {
let entry: HostFileInformation;
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
@@ -1921,7 +1927,8 @@ namespace ts {
hostFileName: fileName,
version: this.host.getScriptVersion(fileName),
scriptSnapshot: scriptSnapshot,
- scriptKind: getScriptKind(fileName, this.host)
+ scriptKind: getScriptKind(fileName, this.host),
+ isRoot
};
}
@@ -1945,14 +1952,14 @@ namespace ts {
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
return this.contains(path)
? this.getEntry(path)
- : this.createEntry(fileName, path);
+ : this.createEntry(fileName, path, /*isRoot*/false);
}
public getRootFileNames(): string[] {
const fileNames: string[] = [];
this.fileNameToEntry.forEachValue((path, value) => {
- if (value) {
+ if (value && value.isRoot) {
fileNames.push(value.hostFileName);
}
});
@@ -3024,6 +3031,7 @@ namespace ts {
const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host);
let ruleProvider: formatting.RulesProvider;
let program: Program;
+ let extensionCache: ExtensionCache;
let lastProjectVersion: string;
const useCaseSensitivefileNames = false;
@@ -3114,11 +3122,13 @@ namespace ts {
getCurrentDirectory: () => currentDirectory,
fileExists: (fileName): boolean => {
// stub missing host functionality
+ Debug.assert(!!hostCache, "LS CompilerHost may not persist beyond the execution of a synchronize call");
Debug.assert(!host.resolveModuleNames || !host.resolveTypeReferenceDirectives);
return hostCache.getOrCreateEntry(fileName) !== undefined;
},
readFile: (fileName): string => {
// stub missing host functionality
+ Debug.assert(!!hostCache, "LS CompilerHost may not persist beyond the execution of a synchronize call");
const entry = hostCache.getOrCreateEntry(fileName);
return entry && entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength());
},
@@ -3127,6 +3137,9 @@ namespace ts {
},
getDirectories: path => {
return host.getDirectories ? host.getDirectories(path) : [];
+ },
+ loadExtension: path => {
+ return host.loadExtension ? host.loadExtension(path) : undefined;
}
};
if (host.trace) {
@@ -3142,8 +3155,13 @@ namespace ts {
};
}
+ const changesInCompilationSettingsAffectExtensions = oldSettings && !deepEqual(oldSettings.extensions, newSettings.extensions);
+ if (!extensionCache || changesInCompilationSettingsAffectExtensions) {
+ extensionCache = createExtensionCache(newSettings, compilerHost);
+ }
+
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
- const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program);
+ const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program, extensionCache);
// Release any files we have acquired in the old program but are
// not part of the new program.
@@ -3304,7 +3322,7 @@ namespace ts {
return concatenate(semanticDiagnostics, declarationDiagnostics);
}
- function getCompilerOptionsDiagnostics() {
+ function getProgramDiagnostics() {
synchronizeHostData();
return program.getOptionsDiagnostics(cancellationToken).concat(
program.getGlobalDiagnostics(cancellationToken));
@@ -8265,7 +8283,8 @@ namespace ts {
cleanupSemanticCache,
getSyntacticDiagnostics,
getSemanticDiagnostics,
- getCompilerOptionsDiagnostics,
+ getCompilerOptionsDiagnostics: getProgramDiagnostics,
+ getProgramDiagnostics,
getSyntacticClassifications,
getSemanticClassifications,
getEncodedSyntacticClassifications,
diff --git a/src/services/shims.ts b/src/services/shims.ts
index 45c4b284ae744..a73133940bd6e 100644
--- a/src/services/shims.ts
+++ b/src/services/shims.ts
@@ -129,6 +129,7 @@ namespace ts {
getSyntacticDiagnostics(fileName: string): string;
getSemanticDiagnostics(fileName: string): string;
getCompilerOptionsDiagnostics(): string;
+ getProgramDiagnostics(): string;
getSyntacticClassifications(fileName: string, start: number, length: number): string;
getSemanticClassifications(fileName: string, start: number, length: number): string;
@@ -562,11 +563,11 @@ namespace ts {
}
}
- export function realizeDiagnostics(diagnostics: Diagnostic[], newLine: string): { message: string; start: number; length: number; category: string; code: number; }[] {
+ export function realizeDiagnostics(diagnostics: Diagnostic[], newLine: string): { message: string; start: number; length: number; category: string; code: number | string; }[] {
return diagnostics.map(d => realizeDiagnostic(d, newLine));
}
- function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): { message: string; start: number; length: number; category: string; code: number; } {
+ function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): { message: string; start: number; length: number; category: string; code: number | string; } {
return {
message: flattenDiagnosticMessageText(diagnostic.messageText, newLine),
start: diagnostic.start,
@@ -699,6 +700,15 @@ namespace ts {
});
}
+ public getProgramDiagnostics(): string {
+ return this.forwardJSONCall(
+ "getProgramDiagnostics()",
+ () => {
+ const diagnostics = this.languageService.getProgramDiagnostics();
+ return this.realizeDiagnostics(diagnostics);
+ });
+ }
+
/// QUICKINFO
/**
diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json
index cfeb7c2fcd582..7d8d5893212bf 100644
--- a/src/services/tsconfig.json
+++ b/src/services/tsconfig.json
@@ -25,6 +25,7 @@
"../compiler/declarationEmitter.ts",
"../compiler/emitter.ts",
"../compiler/program.ts",
+ "../compiler/extensions.ts",
"../compiler/commandLineParser.ts",
"../compiler/diagnosticInformationMap.generated.ts",
"breakpoints.ts",
diff --git a/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.errors.txt b/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.errors.txt
new file mode 100644
index 0000000000000..7d6ee017a2a6b
--- /dev/null
+++ b/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.errors.txt
@@ -0,0 +1,8 @@
+error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-semantic-lint'.'.
+error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-syntactic-lint'.'.
+
+
+!!! error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-semantic-lint'.'.
+!!! error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-syntactic-lint'.'.
+==== /hello.ts (0 errors) ====
+ console.log("Hello, world!");/*EOL*/
\ No newline at end of file
diff --git a/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.js b/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.js
new file mode 100644
index 0000000000000..21b4721f1a6ed
--- /dev/null
+++ b/tests/baselines/reference/CompilerHost/reportsFailedLoads/test.js
@@ -0,0 +1,5 @@
+//// [hello.ts]
+console.log("Hello, world!");/*EOL*/
+
+//// [hello.js]
+console.log("Hello, world!"); /*EOL*/
diff --git a/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.errors.txt b/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.errors.txt
new file mode 100644
index 0000000000000..7d6ee017a2a6b
--- /dev/null
+++ b/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.errors.txt
@@ -0,0 +1,8 @@
+error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-semantic-lint'.'.
+error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-syntactic-lint'.'.
+
+
+!!! error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-semantic-lint'.'.
+!!! error TS6151: Extension loading failed with error 'Error: Host could not locate extension 'test-syntactic-lint'.'.
+==== /hello.ts (0 errors) ====
+ console.log("Hello, world!");/*EOL*/
\ No newline at end of file
diff --git a/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.js b/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.js
new file mode 100644
index 0000000000000..b9f140e37bdde
--- /dev/null
+++ b/tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.js
@@ -0,0 +1,5 @@
+//// [hello.ts]
+console.log("Hello, world!");/*EOL*/
+
+//// [hello.js]
+console.log("Hello, world!"); /*EOL*/
diff --git a/tests/baselines/reference/library-reference-12.trace.json b/tests/baselines/reference/library-reference-12.trace.json
index 84144f82729c6..25fb7f618cd70 100644
--- a/tests/baselines/reference/library-reference-12.trace.json
+++ b/tests/baselines/reference/library-reference-12.trace.json
@@ -17,6 +17,7 @@
"File '/a/node_modules/jquery.ts' does not exist.",
"File '/a/node_modules/jquery.d.ts' does not exist.",
"Found 'package.json' at '/a/node_modules/jquery/package.json'.",
+ "Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'dist/jquery.d.ts' that references '/a/node_modules/jquery/dist/jquery.d.ts'.",
"File '/a/node_modules/jquery/dist/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/a/node_modules/jquery/dist/jquery.d.ts', primary: false. ========"
diff --git a/tests/baselines/reference/library-reference-2.trace.json b/tests/baselines/reference/library-reference-2.trace.json
index 64cdd8091832f..f8119ea65dbb9 100644
--- a/tests/baselines/reference/library-reference-2.trace.json
+++ b/tests/baselines/reference/library-reference-2.trace.json
@@ -2,12 +2,14 @@
"======== Resolving type reference directive 'jquery', containing file '/consumer.ts', root directory '/types'. ========",
"Resolving with primary search path '/types'",
"Found 'package.json' at '/types/jquery/package.json'.",
+ "Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========",
"======== Resolving type reference directive 'jquery', containing file 'test/__inferred type names__.ts', root directory '/types'. ========",
"Resolving with primary search path '/types'",
"Found 'package.json' at '/types/jquery/package.json'.",
+ "Expected type of 'typings' field in 'package.json' to be 'string', got 'undefined'.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========"
diff --git a/tests/cases/extensions/available/extension-api/index.ts b/tests/cases/extensions/available/extension-api/index.ts
new file mode 100644
index 0000000000000..086fc445a1c38
--- /dev/null
+++ b/tests/cases/extensions/available/extension-api/index.ts
@@ -0,0 +1,2 @@
+import * as tsi from "typescript";
+// No APIs exposed
\ No newline at end of file
diff --git a/tests/cases/extensions/available/extension-api/package.json b/tests/cases/extensions/available/extension-api/package.json
new file mode 100644
index 0000000000000..cbd379499d2ec
--- /dev/null
+++ b/tests/cases/extensions/available/extension-api/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "extension-api",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "author": "",
+ "types": "index.d.ts"
+}
\ No newline at end of file
diff --git a/tests/cases/extensions/available/tsconfig.json b/tests/cases/extensions/available/tsconfig.json
new file mode 100644
index 0000000000000..d36035609aafc
--- /dev/null
+++ b/tests/cases/extensions/available/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ // This baseUrl option is useful while writing tests - it lets us
+ // pretend all these modules can see one another (as if they were in a node_modules folder)
+ // since when they're loaded into the virtual fs the test host provides, they _will_ be in a
+ // node_modules folder
+ "baseUrl": "./"
+ }
+}
\ No newline at end of file
diff --git a/tests/cases/extensions/available/typescript/package.json b/tests/cases/extensions/available/typescript/package.json
new file mode 100644
index 0000000000000..14adb10419ff1
--- /dev/null
+++ b/tests/cases/extensions/available/typescript/package.json
@@ -0,0 +1,3 @@
+{
+ "types": "../../../../built/local/typescript.d.ts"
+}
\ No newline at end of file
diff --git a/tests/cases/extensions/scenarios/reportsFailedLoads/test.json b/tests/cases/extensions/scenarios/reportsFailedLoads/test.json
new file mode 100644
index 0000000000000..221b4e3a867c0
--- /dev/null
+++ b/tests/cases/extensions/scenarios/reportsFailedLoads/test.json
@@ -0,0 +1,9 @@
+{
+ "inputFiles": [
+ "hello.ts"
+ ],
+ "availableExtensions": [],
+ "compilerOptions": {
+ "extensions": ["test-syntactic-lint", "test-semantic-lint"]
+ }
+}
\ No newline at end of file
diff --git a/tests/cases/extensions/source/hello.ts b/tests/cases/extensions/source/hello.ts
new file mode 100644
index 0000000000000..97d87624465a4
--- /dev/null
+++ b/tests/cases/extensions/source/hello.ts
@@ -0,0 +1 @@
+console.log("Hello, world!");/*EOL*/
\ No newline at end of file