diff --git a/Gulpfile.ts b/Gulpfile.ts
index 1b90256283264..fae5c6948c9a8 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 441f6aef4f935..3ad403b04d87a 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -68,6 +68,7 @@ var compilerSources = [
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
+ "extensions.ts",
"commandLineParser.ts",
"tsc.ts",
"diagnosticInformationMap.generated.ts"
@@ -89,6 +90,7 @@ var servicesSources = [
"declarationEmitter.ts",
"emitter.ts",
"program.ts",
+ "extensions.ts",
"commandLineParser.ts",
"diagnosticInformationMap.generated.ts"
].map(function (f) {
@@ -153,6 +155,7 @@ var harnessCoreSources = [
"typeWriter.ts",
"fourslashRunner.ts",
"projectsRunner.ts",
+ "extensionRunner.ts",
"loggedIO.ts",
"rwcRunner.ts",
"test262Runner.ts",
@@ -550,7 +553,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 6406455d71328..ce0e52084caa0 100644
--- a/src/compiler/commandLineParser.ts
+++ b/src/compiler/commandLineParser.ts
@@ -286,6 +286,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: createMap({
@@ -433,7 +439,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 40341929c02f7..516b3545958d8 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 {
/**
@@ -236,6 +247,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) {
@@ -1031,17 +1062,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);
}
@@ -1318,7 +1338,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;
}
@@ -1496,4 +1517,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 = Object.keys(a);
+ const bKeys = Object.keys(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 c6a3698ea16a3..6936a4385cb95 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,14 +2824,24 @@
"category": "Message",
"code": 6136
},
- "No types specified in 'package.json' but 'allowJs' is set, so returning 'main' value of '{0}'": {
- "category": "Message",
- "code": 6137
- },
"Property '{0}' is declared but never used.": {
"category": "Error",
"code": 6138
},
+
+ "List of compiler extensions to require.": {
+ "category": "Message",
+ "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..082aeb66f4a18
--- /dev/null
+++ b/src/compiler/extensions.ts
@@ -0,0 +1,181 @@
+namespace ts {
+
+ export namespace ExtensionKind {
+ }
+ export type ExtensionKind = string;
+
+ export interface ExtensionCollectionMap {
+ [index: string]: Extension[] | undefined;
+ }
+
+ export interface ExtensionBase {
+ name: string;
+ args: {};
+ kind: ExtensionKind;
+ // 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
+ extension: {};
+ }
+
+ export type Extension = ExtensionBase;
+
+ export interface ExtensionCache {
+ getCompilerExtensions(): ExtensionCollectionMap;
+ getExtensionLoadingDiagnostics(): Diagnostic[];
+ }
+
+ export interface ExtensionHost extends ModuleResolutionHost {
+ loadExtension?(name: string): any;
+ resolveModuleNames?(moduleNames: string[], containingFile: string, loadJs?: boolean): ResolvedModule[];
+ }
+
+ 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;
+ }
+
+ function getExtensionRootName(qualifiedName: string) {
+ return qualifiedName.substring(0, qualifiedName.indexOf("[")) || qualifiedName;
+ }
+
+ function createTaskName(qualifiedName: string, task: string) {
+ return `${task}|${qualifiedName}`;
+ }
+
+ function startProfile(enabled: boolean, key: string) {
+ if (!enabled) return;
+ performance.mark(`start|${key}`);
+ }
+
+ function completeProfile(enabled: boolean, key: string, bucket: string) {
+ if (!enabled) return;
+ performance.measure(bucket, `start|${key}`);
+ }
+
+ export function startExtensionProfile(enabled: boolean, qualifiedName: string, task: string) {
+ if (!enabled) return;
+ const longTask = createTaskName(qualifiedName, task);
+ startProfile(/*enabled*/true, longTask);
+ }
+
+ export function completeExtensionProfile(enabled: boolean, qualifiedName: string, task: string) {
+ if (!enabled) return;
+ const longTask = createTaskName(qualifiedName, task);
+ completeProfile(/*enabled*/true, longTask, getExtensionRootName(qualifiedName));
+ }
+
+ export function createExtensionCache(options: CompilerOptions, host: ExtensionHost, resolvedExtensionNames?: Map): ExtensionCache {
+
+ const diagnostics: Diagnostic[] = [];
+ const extOptions = options.extensions;
+ const extensionNames = (extOptions instanceof Array) ? extOptions : extOptions ? Object.keys(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;
+
+ // Defer to the host's `resolveModuleName` method if it has it, otherwise use it as a ModuleResolutionHost.
+ function resolveModuleName(name: string, fromLocation: string) {
+ if (host.resolveModuleNames) {
+ const results = host.resolveModuleNames([name], fromLocation, /*loadJs*/true);
+ return results && results[0];
+ }
+ else {
+ return ts.resolveModuleName(name, fromLocation, options, host, /*loadJs*/true).resolvedModule;
+ }
+ }
+
+ function resolveExtensionNames(): Map {
+ const basePath = options.configFilePath || combinePaths(host.getCurrentDirectory ? host.getCurrentDirectory() : "", "tsconfig.json");
+ const extMap = createMap();
+ forEach(extensionNames, name => {
+ const resolved = resolveModuleName(name, basePath);
+ 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);
+ result = host.loadExtension(resolved);
+ completeProfile(profilingEnabled, name, 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[] = [];
+ forEach(Object.keys(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 MapLike)[res.name],
+ kind: annotatedKind as ExtensionKind,
+ extension: potentialExtension
+ };
+ aggregate.push(ext);
+ });
+ return aggregate;
+ });
+ return groupBy(flatten(preparedExtensionObjects), elem => elem.kind) || {};
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index e732fe218726e..f860ef2895c3d 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,39 +120,51 @@ namespace ts {
skipTsx: boolean;
}
- function tryReadTypesSection(packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string {
- const jsonContent = readJson(packageJsonPath, state.host);
+ function getPackageEntry(packageJson: any, key: string, tag: string, state: ModuleResolutionState) {
+ const value = packageJson[key];
+ if (typeof value === tag) {
+ return value;
+ }
+ 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;
+ }
- function tryReadFromField(fieldName: string) {
- if (hasProperty(jsonContent, fieldName)) {
- const typesFile = (jsonContent)[fieldName];
- if (typeof typesFile === "string") {
- const typesFilePath = normalizePath(combinePaths(baseDirectory, typesFile));
- if (state.traceEnabled) {
- trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, typesFile, typesFilePath);
- }
- return typesFilePath;
- }
- else {
- if (state.traceEnabled) {
- trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof typesFile);
- }
- }
- }
+ 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);
}
+ return path;
+ }
- const typesFilePath = tryReadFromField("typings") || tryReadFromField("types");
- if (typesFilePath) {
- return typesFilePath;
+ 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));
}
+ else {
+ if (state.traceEnabled) {
+ trace(state.host, Diagnostics.package_json_does_not_have_0_field, "types");
+ }
+ }
+ return undefined;
+ }
- // 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") {
+ 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;
}
@@ -286,7 +300,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);
@@ -308,7 +322,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);
@@ -603,7 +617,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);
@@ -619,7 +633,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 {
@@ -709,25 +723,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 = pathToPackageJson(candidate);
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) {
@@ -744,30 +753,31 @@ namespace ts {
return combinePaths(directory, "package.json");
}
- 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;
}
@@ -945,6 +955,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,
@@ -960,7 +971,8 @@ namespace ts {
trace: (s: string) => sys.write(s + newLine),
directoryExists: directoryName => sys.directoryExists(directoryName),
getDirectories: (path: string) => sys.getDirectories(path),
- realpath
+ realpath,
+ loadExtension
};
}
@@ -995,7 +1007,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;
}
@@ -1079,7 +1092,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;
@@ -1179,6 +1192,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,
@@ -1201,7 +1216,13 @@ namespace ts {
getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(),
getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(),
getFileProcessingDiagnostics: () => fileProcessingDiagnostics,
- getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives
+ getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives,
+ getCompilerExtensions() {
+ return extensionCache.getCompilerExtensions();
+ },
+ getExtensionLoadingDiagnostics() {
+ return extensionCache.getExtensionLoadingDiagnostics();
+ },
};
verifyCompilerOptions();
@@ -1746,6 +1767,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 350d75429b754..942c05944d4fd 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 {
@@ -543,6 +544,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 62b58609086e6..5a302d2126684 100644
--- a/src/compiler/tsc.ts
+++ b/src/compiler/tsc.ts
@@ -606,13 +606,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..5f751856bfb13 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 7594d2c3fb8af..9422608ec5f3d 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -2559,7 +2559,7 @@ namespace ts {
length: number;
messageText: string | DiagnosticMessageChain;
category: DiagnosticCategory;
- code: number;
+ code: number | string;
}
export enum DiagnosticCategory {
@@ -2652,6 +2652,7 @@ namespace ts {
typeRoots?: string[];
/*@internal*/ version?: boolean;
/*@internal*/ watch?: boolean;
+ extensions?: string[] | MapLike;
[option: string]: CompilerOptionsValue | undefined;
}
@@ -2980,11 +2981,19 @@ namespace ts {
* If resolveModuleNames is implemented then implementation for members from ModuleResolutionHost can be just
* 'throw new Error("NotImplemented")'
*/
- resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
+ resolveModuleNames?(moduleNames: string[], containingFile: string, loadJs?: boolean): ResolvedModule[];
/**
* 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..873dc6d760697
--- /dev/null
+++ b/src/harness/extensionRunner.ts
@@ -0,0 +1,496 @@
+///
+///
+///
+
+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.createMap();
+ private extensions = ts.createMap>();
+ private virtualLib = ts.createMap();
+ private virtualFs = ts.createMap();
+
+ 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.forEach(Object.keys(set), key => ts.forEach(Object.keys(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.forEach(Object.keys(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(Object.keys(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(Object.keys(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 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(Object.keys(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(Object.keys(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.");
+ }
+ for (const key in this.virtualFs) {
+ out[key] = this.virtualFs[key];
+ }
+ this.virtualFs = ts.createMap();
+ return results;
+ }
+
+ private loadExtensions() {
+ this.extensionAPI = ts.createMap({
+ "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.createMap();
+ 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 = ts.createMap({
+ "/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 = ts.createMap();
+ 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 = ts.createMap(); // In case a fourslash test was run last (which doesn't clear FS on end like buildMap does), clear the FS
+ sources = ts.createMap();
+ for (const key in inputSources) {
+ sources[key] = inputSources[key];
+ }
+ 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 = ts.createMap();
+ 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(Object.keys(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 b5fa53763cb9d..90c3270347112 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);
@@ -251,7 +251,13 @@ namespace FourSlash {
}
compilationOptions.skipDefaultLibCheck = true;
- 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();
@@ -2260,7 +2266,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);
const state = new TestState(basePath, testType, testData);
diff --git a/src/harness/harness.ts b/src/harness/harness.ts
index 9ed817eae499e..18fc6a93d3277 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 9412443278002..39952b47129d7 100644
--- a/src/harness/harnessLanguageService.ts
+++ b/src/harness/harnessLanguageService.ts
@@ -123,7 +123,7 @@ namespace Harness.LanguageService {
}
export class LanguageServiceAdapterHost {
- protected fileNameToScript = ts.createMap();
+ public fileNameToScript = ts.createMap();
constructor(protected cancellationToken = DefaultHostCancellationToken.Instance,
protected settings = ts.getDefaultCompilerOptions()) {
@@ -216,7 +216,7 @@ namespace Harness.LanguageService {
class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost {
private nativeHost: NativeLanguageServiceHost;
- public getModuleResolutionsForFile: (fileName: string) => string;
+ public getModuleResolutionsForFile: (fileName: string, loadJs?: boolean) => string;
public getTypeReferenceDirectiveResolutionsForFile: (fileName: string) => string;
constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
@@ -232,12 +232,12 @@ namespace Harness.LanguageService {
return scriptInfo && scriptInfo.content;
}
};
- this.getModuleResolutionsForFile = (fileName) => {
+ this.getModuleResolutionsForFile = (fileName, loadJs) => {
const scriptInfo = this.getScriptInfo(fileName);
const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true);
const imports = ts.createMap();
for (const module of preprocessInfo.importedFiles) {
- const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost);
+ const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost, loadJs);
if (resolutionInfo.resolvedModule) {
imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName;
}
@@ -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 88177e91fad7b..a9f598b49bea5 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 db9bdd5043bf6..94c905d1430fa 100644
--- a/src/server/editorServices.ts
+++ b/src/server/editorServices.ts
@@ -99,6 +99,7 @@ namespace ts.server {
compilationSettings: ts.CompilerOptions;
filenameToScript: ts.FileMap;
roots: ScriptInfo[] = [];
+ loadExtension?: (path: string) => any;
private resolvedModuleNames: ts.FileMap