From 5e2c3f4501c93122480ede4be1652fd8d03b4737 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 4 Aug 2016 15:50:04 -0700 Subject: [PATCH 1/3] Extension loading and management tools --- Gulpfile.ts | 2 +- Jakefile.js | 5 +- src/compiler/commandLineParser.ts | 8 +- src/compiler/core.ts | 90 +++- src/compiler/diagnosticMessages.json | 19 +- src/compiler/extensions.ts | 189 +++++++ src/compiler/performance.ts | 3 +- src/compiler/program.ts | 116 +++-- src/compiler/sys.ts | 4 + src/compiler/tsc.ts | 13 +- src/compiler/tsconfig.json | 1 + src/compiler/types.ts | 11 +- src/harness/extensionRunner.ts | 492 ++++++++++++++++++ src/harness/fourslash.ts | 12 +- src/harness/harness.ts | 7 +- src/harness/harnessLanguageService.ts | 5 +- src/harness/runner.ts | 9 + src/harness/runnerbase.ts | 2 +- src/server/client.ts | 4 + src/server/editorServices.ts | 2 +- src/services/services.ts | 39 +- src/services/shims.ts | 14 +- src/services/tsconfig.json | 1 + .../reportsFailedLoads/test.errors.txt | 8 + .../CompilerHost/reportsFailedLoads/test.js | 5 + .../reportsFailedLoads/test.errors.txt | 8 + .../reportsFailedLoads/test.js | 5 + .../reference/library-reference-12.trace.json | 1 + .../reference/library-reference-2.trace.json | 2 + .../available/extension-api/index.ts | 2 + .../available/extension-api/package.json | 8 + .../cases/extensions/available/tsconfig.json | 9 + .../available/typescript/package.json | 3 + .../scenarios/reportsFailedLoads/test.json | 9 + tests/cases/extensions/source/hello.ts | 1 + 35 files changed, 1018 insertions(+), 91 deletions(-) create mode 100644 src/compiler/extensions.ts create mode 100644 src/harness/extensionRunner.ts create mode 100644 tests/baselines/reference/CompilerHost/reportsFailedLoads/test.errors.txt create mode 100644 tests/baselines/reference/CompilerHost/reportsFailedLoads/test.js create mode 100644 tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.errors.txt create mode 100644 tests/baselines/reference/LanguageServiceHost/reportsFailedLoads/test.js create mode 100644 tests/cases/extensions/available/extension-api/index.ts create mode 100644 tests/cases/extensions/available/extension-api/package.json create mode 100644 tests/cases/extensions/available/tsconfig.json create mode 100644 tests/cases/extensions/available/typescript/package.json create mode 100644 tests/cases/extensions/scenarios/reportsFailedLoads/test.json create mode 100644 tests/cases/extensions/source/hello.ts 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..31f942af31a91 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 = 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 c6a3698ea16a3..e08a406c4e8c4 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,25 @@ "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 }, "Property '{0}' is declared but never used.": { "category": "Error", "code": 6138 }, + + "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..071d3e7f3ec3d --- /dev/null +++ b/src/compiler/extensions.ts @@ -0,0 +1,189 @@ +namespace ts { + + export namespace ExtensionKind { + } + export type ExtensionKind = string; + + export interface ExtensionCollectionMap { + [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 type Extension = ExtensionBase; + + 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); + } + + 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) { + 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 e8f064e81cdf3..3ddaf2cd19ddc 100644 --- a/src/compiler/performance.ts +++ b/src/compiler/performance.ts @@ -46,8 +46,9 @@ namespace ts.performance { if (enabled) { const end = endMarkName && marks[endMarkName] || timestamp(); const start = startMarkName && marks[startMarkName] || profilerStart; - measures[measureName] = (measures[measureName] || 0) + (end - start); + return measures[measureName] = (measures[measureName] || 0) + (end - start); } + return 0; } /** 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..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 7594d2c3fb8af..cf61b75b34264 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[] | Map; [option: string]: CompilerOptionsValue | undefined; } @@ -2985,6 +2986,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 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..56b08f2731b79 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()) { @@ -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..b57c242562039 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/services.ts b/src/services/services.ts index ea85caf1332ae..ecb217dbe50f7 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); } }); @@ -3026,6 +3033,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; @@ -3116,10 +3124,12 @@ 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"); 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()); }, @@ -3128,6 +3138,9 @@ namespace ts { }, getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; + }, + loadExtension: path => { + return host.loadExtension ? host.loadExtension(path) : undefined; } }; if (host.trace) { @@ -3143,8 +3156,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. @@ -3305,7 +3323,7 @@ namespace ts { return concatenate(semanticDiagnostics, declarationDiagnostics); } - function getCompilerOptionsDiagnostics() { + function getProgramDiagnostics() { synchronizeHostData(); return program.getOptionsDiagnostics(cancellationToken).concat( program.getGlobalDiagnostics(cancellationToken)); @@ -8273,7 +8291,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 ff57dd9cf7ab4..af0c19430ba1e 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 From a900aa36624af4ae03944c171fec8bcb06f7dbf5 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 8 Aug 2016 16:10:10 -0700 Subject: [PATCH 2/3] Start extension search from config location preferentially to current directory --- src/compiler/extensions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index 071d3e7f3ec3d..8f2d07b3d7af7 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -114,10 +114,10 @@ namespace ts { return cache; function resolveExtensionNames(): Map { - const currentDirectory = host.getCurrentDirectory ? host.getCurrentDirectory() : ""; + const basePath = options.configFilePath || combinePaths(host.getCurrentDirectory ? host.getCurrentDirectory() : "", "tsconfig.json"); const extMap: Map = {}; forEach(extensionNames, name => { - const resolved = resolveModuleName(name, combinePaths(currentDirectory, "tsconfig.json"), options, host, /*loadJs*/true).resolvedModule; + const resolved = resolveModuleName(name, basePath, options, host, /*loadJs*/true).resolvedModule; if (resolved) { extMap[name] = resolved.resolvedFileName; } From d8aec9907aef9ef2185afa4f256d64f9fa498b7e Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 8 Aug 2016 17:01:19 -0700 Subject: [PATCH 3/3] Use resolveModuleNames when making an extension cache, if available. Implement loadExtension on LSHost --- src/compiler/core.ts | 4 +- src/compiler/diagnosticMessages.json | 9 ++- src/compiler/extensions.ts | 70 ++++++++----------- src/compiler/performance.ts | 3 +- src/compiler/tsconfig.json | 2 +- src/compiler/types.ts | 4 +- src/harness/extensionRunner.ts | 52 +++++++------- src/harness/harnessLanguageService.ts | 6 +- src/server/editorServices.ts | 17 +++-- src/services/services.ts | 10 +-- src/services/shims.ts | 8 +-- .../loadsExtensions/test-normal.js | 5 ++ .../loadsExtensions/test-normal.js | 5 ++ .../cases/extensions/available/dummy/index.ts | 1 + .../extensions/available/dummy/package.json | 7 ++ .../loadsExtensions/test-normal.json | 9 +++ 16 files changed, 119 insertions(+), 93 deletions(-) create mode 100644 tests/baselines/reference/CompilerHost/loadsExtensions/test-normal.js create mode 100644 tests/baselines/reference/LanguageServiceHost/loadsExtensions/test-normal.js create mode 100644 tests/cases/extensions/available/dummy/index.ts create mode 100644 tests/cases/extensions/available/dummy/package.json create mode 100644 tests/cases/extensions/scenarios/loadsExtensions/test-normal.json diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 31f942af31a91..516b3545958d8 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1542,8 +1542,8 @@ namespace ts { memo = []; } - const aKeys = ts.getKeys(a); - const bKeys = ts.getKeys(b); + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); aKeys.sort(); bKeys.sort(); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e08a406c4e8c4..6936a4385cb95 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2824,6 +2824,10 @@ "category": "Message", "code": 6136 }, + "Property '{0}' is declared but never used.": { + "category": "Error", + "code": 6138 + }, "List of compiler extensions to require.": { "category": "Message", @@ -2833,11 +2837,6 @@ "category": "Error", "code": 6151 }, - "Property '{0}' is declared but never used.": { - "category": "Error", - "code": 6138 - }, - "Extension '{0}' exported member '{1}' has extension kind '{2}', but was type '{3}' when type '{4}' was expected.": { "category": "Error", "code": 6152 diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index 8f2d07b3d7af7..082aeb66f4a18 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -10,15 +10,11 @@ namespace ts { export interface ExtensionBase { name: string; - args: any; + args: {}; kind: ExtensionKind; - } - - export interface ProfileData { - globalBucket: string; - task: string; - start: number; - length?: number; + // 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; @@ -30,6 +26,7 @@ namespace ts { export interface ExtensionHost extends ModuleResolutionHost { loadExtension?(name: string): any; + resolveModuleNames?(moduleNames: string[], containingFile: string, loadJs?: boolean): ResolvedModule[]; } export interface Program { @@ -49,8 +46,6 @@ namespace ts { getCompilerExtensions(): ExtensionCollectionMap; } - export const perfTraces: Map = {}; - function getExtensionRootName(qualifiedName: string) { return qualifiedName.substring(0, qualifiedName.indexOf("[")) || qualifiedName; } @@ -59,41 +54,33 @@ namespace ts { return `${task}|${qualifiedName}`; } - export function startProfile(enabled: boolean, key: string, bucket?: string) { + function startProfile(enabled: boolean, key: string) { if (!enabled) return; - performance.emit(`start|${key}`); - perfTraces[key] = { - task: key, - start: performance.mark(), - length: undefined, - globalBucket: bucket - }; + performance.mark(`start|${key}`); } - export function completeProfile(enabled: boolean, key: string) { + function completeProfile(enabled: boolean, key: string, bucket: 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}`); + 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, getExtensionRootName(qualifiedName)); + 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); + 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 : getKeys(extOptions); + 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; @@ -113,11 +100,22 @@ namespace ts { }; 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: Map = {}; + const extMap = createMap(); forEach(extensionNames, name => { - const resolved = resolveModuleName(name, basePath, options, host, /*loadJs*/true).resolvedModule; + const resolved = resolveModuleName(name, basePath); if (resolved) { extMap[name] = resolved.resolvedFileName; } @@ -136,9 +134,9 @@ namespace ts { } if (resolved && host.loadExtension) { try { - startProfile(profilingEnabled, name, name); + startProfile(profilingEnabled, name); result = host.loadExtension(resolved); - completeProfile(profilingEnabled, name); + completeProfile(profilingEnabled, name, name); } catch (e) { error = e; @@ -158,7 +156,7 @@ namespace ts { return []; } const aggregate: Extension[] = []; - forEachKey(res.result, key => { + 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?) @@ -169,17 +167,11 @@ namespace ts { } const ext: ExtensionBase = { name: key !== "default" ? `${res.name}[${key}]` : res.name, - args: extensionNames === extOptions ? undefined : (extOptions as Map)[res.name], + args: extensionNames === extOptions ? undefined : (extOptions as MapLike)[res.name], kind: annotatedKind as ExtensionKind, + extension: potentialExtension }; - switch (ext.kind) { - 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); + aggregate.push(ext); }); return aggregate; }); diff --git a/src/compiler/performance.ts b/src/compiler/performance.ts index 3ddaf2cd19ddc..e8f064e81cdf3 100644 --- a/src/compiler/performance.ts +++ b/src/compiler/performance.ts @@ -46,9 +46,8 @@ namespace ts.performance { if (enabled) { const end = endMarkName && marks[endMarkName] || timestamp(); const start = startMarkName && marks[startMarkName] || profilerStart; - return measures[measureName] = (measures[measureName] || 0) + (end - start); + measures[measureName] = (measures[measureName] || 0) + (end - start); } - return 0; } /** diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 9d98da46cd1a1..5f751856bfb13 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -24,7 +24,7 @@ "declarationEmitter.ts", "emitter.ts", "program.ts", - "extensions.ts", + "extensions.ts", "commandLineParser.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" diff --git a/src/compiler/types.ts b/src/compiler/types.ts index cf61b75b34264..9422608ec5f3d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2652,7 +2652,7 @@ namespace ts { typeRoots?: string[]; /*@internal*/ version?: boolean; /*@internal*/ watch?: boolean; - extensions?: string[] | Map; + extensions?: string[] | MapLike; [option: string]: CompilerOptionsValue | undefined; } @@ -2981,7 +2981,7 @@ 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 */ diff --git a/src/harness/extensionRunner.ts b/src/harness/extensionRunner.ts index ca7ab8c4a6d9b..873dc6d760697 100644 --- a/src/harness/extensionRunner.ts +++ b/src/harness/extensionRunner.ts @@ -17,10 +17,10 @@ class ExtensionRunner extends RunnerBase { 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 = {}; + 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"); @@ -41,12 +41,12 @@ class ExtensionRunner extends RunnerBase { 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])); + 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.forEachKey(set, key => void (this.virtualFs[this.getCanonicalFileName(key)] = set[key])); + ts.forEach(Object.keys(set), key => void (this.virtualFs[this.getCanonicalFileName(key)] = set[key])); } private traces: string[] = []; @@ -62,7 +62,7 @@ class ExtensionRunner extends RunnerBase { }, directoryExists: (path) => { const fullPath = this.mockHost.getCanonicalFileName(path); - return ts.forEach(ts.getKeys(this.virtualFs), key => ts.startsWith(key, fullPath)); + return ts.forEach(Object.keys(this.virtualFs), key => ts.startsWith(key, fullPath)); }, getCurrentDirectory(): string { return "/"; }, getSourceFile: (path, languageVersion, onError): ts.SourceFile => { @@ -76,7 +76,7 @@ class ExtensionRunner extends RunnerBase { getCanonicalFileName: this.getCanonicalFileName, getDirectories: (path) => { path = this.mockHost.getCanonicalFileName(path); - return ts.filter(ts.map(ts.filter(ts.getKeys(this.virtualFs), + 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); @@ -118,7 +118,7 @@ class ExtensionRunner extends RunnerBase { }; host.getScriptInfo = (fileName: string) => { fileName = this.getCanonicalFileName(fileName); - return ts.lookUp(host.fileNameToScript, fileName); + return host.fileNameToScript[fileName]; }; host.getDirectories = (s: string) => this.mockHost.getDirectories(s); host.addScript = (fileName: string, content: string, isRootFile: boolean): void => { @@ -149,7 +149,7 @@ class ExtensionRunner extends RunnerBase { languageServiceCompile(typescriptFiles: string[], options: ts.CompilerOptions): Harness.Compiler.CompilerResult { const self = this; - const host = this.makeMockLSHost(ts.getKeys(this.virtualFs), options); + const host = this.makeMockLSHost(Object.keys(this.virtualFs), options); const service = ts.createLanguageService(host); const fileResults: Harness.Compiler.GeneratedFile[] = []; @@ -199,7 +199,7 @@ class ExtensionRunner extends RunnerBase { 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)); + const typescriptFiles = ts.filter(Object.keys(fileset), name => ts.endsWith(name, ".ts") && !(name.indexOf("node_modules") >= 0)); return compileFunc(typescriptFiles, options); } @@ -212,22 +212,24 @@ class ExtensionRunner extends RunnerBase { } throw new Error("Compiling test harness extension API code resulted in errors."); } - ts.copyMap(this.virtualFs, out); - this.virtualFs = {}; + for (const key in this.virtualFs) { + out[key] = this.virtualFs[key]; + } + this.virtualFs = ts.createMap(); return results; } private loadExtensions() { - this.extensionAPI = { + 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.Map = {}; + const extensionFileset = ts.createMap(); const extensionFiles = this.enumerateFiles(packageDir, /*regex*/ undefined, { recursive: true }); ts.forEach(extensionFiles, name => { const shortName = name.substring(packageDir.length + 1); @@ -244,10 +246,10 @@ class ExtensionRunner extends RunnerBase { super(); const {content: libContent} = Harness.getDefaultLibraryFile(Harness.IO); const tsLibContents = Harness.IO.readFile("built/local/typescript.d.ts"); - this.virtualLib = { + this.virtualLib = ts.createMap({ "/lib/lib.d.ts": libContent, "/node_modules/typescript/index.d.ts": tsLibContents - }; + }); this.loadExtensions(); } @@ -304,7 +306,7 @@ class ExtensionRunner extends RunnerBase { shortCasePath = caseName.substring(this.scenarioPath.length + 1).replace(/\.json$/, ""); testConfigText = Harness.IO.readFile(caseName); testConfig = JSON.parse(testConfigText); - inputSources = {}; + inputSources = ts.createMap(); inputTestFiles = []; ts.forEach(testConfig.inputFiles, name => { inputSources[name] = Harness.IO.readFile(ts.combinePaths(this.sourcePath, name)); @@ -329,9 +331,11 @@ class ExtensionRunner extends RunnerBase { 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); + 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); }); @@ -465,7 +469,7 @@ class ExtensionRunner extends RunnerBase { it("passes fourslash verification", () => { if (testConfig.fourslashTest) { - this.virtualFs = {}; + this.virtualFs = ts.createMap(); const testFile = `${this.fourslashPath}/${testConfig.fourslashTest}`; let testFileContents = Harness.IO.readFile(testFile); testFileContents = testFileContents.replace(`/// `, ""); @@ -482,7 +486,7 @@ class ExtensionRunner extends RunnerBase { 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); + const adapterFactory = (token: ts.HostCancellationToken) => this.makeLSMockAdapter(Object.keys(this.virtualFs), testConfig.compilerOptions, token); FourSlash.runFourSlashTestContent(shortCasePath, adapterFactory, finishedTestContent, testFile); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 56b08f2731b79..39952b47129d7 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b57c242562039..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>; private resolvedTypeReferenceDirectives: ts.FileMap>; @@ -118,14 +119,18 @@ namespace ts.server { if (this.host.realpath) { this.moduleResolutionHost.realpath = path => this.host.realpath(path); } + if (this.host.loadExtension) { + this.loadExtension = name => this.host.loadExtension(name); + } } private resolveNamesWithLocalCache( names: string[], containingFile: string, cache: ts.FileMap>, - loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R): R[] { + loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost, loadJs?: boolean) => T, + getResult: (s: T) => R, + loadJs: boolean): R[] { const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); const currentResolutionsInFile = cache.get(path); @@ -144,7 +149,7 @@ namespace ts.server { resolution = existingResolution; } else { - resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost); + resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost, loadJs); resolution.lastCheckTime = Date.now(); newResolutions[name] = resolution; } @@ -177,11 +182,11 @@ namespace ts.server { } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective); + return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, /*loadJs*/false); } - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { - return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule); + resolveModuleNames(moduleNames: string[], containingFile: string, loadJs?: boolean): ResolvedModule[] { + return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule, loadJs); } getDefaultLibFileName() { diff --git a/src/services/services.ts b/src/services/services.ts index ecb217dbe50f7..6a4e092d4edda 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1163,7 +1163,7 @@ namespace ts { * if implementation is omitted then language service will use built-in module resolution logic and get answers to * host specific questions using 'getScriptSnapshot'. */ - resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; + resolveModuleNames?(moduleNames: string[], containingFile: string, loadJs?: boolean): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; @@ -3138,17 +3138,17 @@ namespace ts { }, getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; - }, - loadExtension: path => { - return host.loadExtension ? host.loadExtension(path) : undefined; } }; if (host.trace) { compilerHost.trace = message => host.trace(message); } + if (host.loadExtension) { + compilerHost.loadExtension = name => host.loadExtension(name); + } if (host.resolveModuleNames) { - compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + compilerHost.resolveModuleNames = (moduleNames, containingFile, loadJs) => host.resolveModuleNames(moduleNames, containingFile, loadJs); } if (host.resolveTypeReferenceDirectives) { compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { diff --git a/src/services/shims.ts b/src/services/shims.ts index af0c19430ba1e..55649d9e6dfe3 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -67,7 +67,7 @@ namespace ts { getProjectVersion?(): string; useCaseSensitiveFileNames?(): boolean; - getModuleResolutionsForFile?(fileName: string): string; + getModuleResolutionsForFile?(fileName: string, loadJs?: boolean): string; getTypeReferenceDirectiveResolutionsForFile?(fileName: string): string; directoryExists(directoryName: string): boolean; } @@ -303,7 +303,7 @@ namespace ts { private loggingEnabled = false; private tracingEnabled = false; - public resolveModuleNames: (moduleName: string[], containingFile: string) => ResolvedModule[]; + public resolveModuleNames: (moduleName: string[], containingFile: string, loadJs?: boolean) => ResolvedModule[]; public resolveTypeReferenceDirectives: (typeDirectiveNames: string[], containingFile: string) => ResolvedTypeReferenceDirective[]; public directoryExists: (directoryName: string) => boolean; @@ -311,8 +311,8 @@ namespace ts { // if shimHost is a COM object then property check will become method call with no arguments. // 'in' does not have this effect. if ("getModuleResolutionsForFile" in this.shimHost) { - this.resolveModuleNames = (moduleNames: string[], containingFile: string) => { - const resolutionsInFile = >JSON.parse(this.shimHost.getModuleResolutionsForFile(containingFile)); + this.resolveModuleNames = (moduleNames: string[], containingFile: string, loadJs?: boolean) => { + const resolutionsInFile = >JSON.parse(this.shimHost.getModuleResolutionsForFile(containingFile, loadJs)); return map(moduleNames, name => { const result = getProperty(resolutionsInFile, name); return result ? { resolvedFileName: result } : undefined; diff --git a/tests/baselines/reference/CompilerHost/loadsExtensions/test-normal.js b/tests/baselines/reference/CompilerHost/loadsExtensions/test-normal.js new file mode 100644 index 0000000000000..21b4721f1a6ed --- /dev/null +++ b/tests/baselines/reference/CompilerHost/loadsExtensions/test-normal.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/loadsExtensions/test-normal.js b/tests/baselines/reference/LanguageServiceHost/loadsExtensions/test-normal.js new file mode 100644 index 0000000000000..b9f140e37bdde --- /dev/null +++ b/tests/baselines/reference/LanguageServiceHost/loadsExtensions/test-normal.js @@ -0,0 +1,5 @@ +//// [hello.ts] +console.log("Hello, world!");/*EOL*/ + +//// [hello.js] +console.log("Hello, world!"); /*EOL*/ diff --git a/tests/cases/extensions/available/dummy/index.ts b/tests/cases/extensions/available/dummy/index.ts new file mode 100644 index 0000000000000..26f98f77f6d17 --- /dev/null +++ b/tests/cases/extensions/available/dummy/index.ts @@ -0,0 +1 @@ +console.log("loaded"); \ No newline at end of file diff --git a/tests/cases/extensions/available/dummy/package.json b/tests/cases/extensions/available/dummy/package.json new file mode 100644 index 0000000000000..96abe59cfd984 --- /dev/null +++ b/tests/cases/extensions/available/dummy/package.json @@ -0,0 +1,7 @@ +{ + "name": "dummy", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +} \ No newline at end of file diff --git a/tests/cases/extensions/scenarios/loadsExtensions/test-normal.json b/tests/cases/extensions/scenarios/loadsExtensions/test-normal.json new file mode 100644 index 0000000000000..fafffb866d178 --- /dev/null +++ b/tests/cases/extensions/scenarios/loadsExtensions/test-normal.json @@ -0,0 +1,9 @@ +{ + "inputFiles": [ + "hello.ts" + ], + "availableExtensions": ["dummy"], + "compilerOptions": { + "extensions": ["dummy"] + } +} \ No newline at end of file