UNPKG

alm

Version:

The best IDE for TypeScript

513 lines (464 loc) 18.1 kB
import * as fmc from "../../../disk/fileModelCache"; import * as fsu from "../../../utils/fsu"; import fs = require('fs'); import * as json from "../../../../common/json"; import { reverseKeysAndValues, uniq, extend, isJs } from "../../../../common/utils"; import { makeBlandError, PackageJsonParsed, TsconfigJsonParsed, TypeScriptConfigFileDetails } from "../../../../common/types"; import { increaseCompilationContext, getDefinitionsForNodeModules } from "./compilationContextExpander"; import { validate } from "./tsconfigValidation"; import * as types from '../../../../common/types'; /** * The CompilerOptions as read from a `tsconfig.json` file. * Most members copy pasted from ts.CompilerOptions * - With few (e.g. `module`) replaced with `string` * NOTE: see the changes in `types.ts` in the TypeScript sources to see what needs updating * * When adding you need to * 0 Add in this interface * 1 Add to `tsconfigValidation` * 2 If its an enum : Update `typescriptEnumMap` * 3 If its a path : Update `pathResolveTheseOptions` * 4 Update the tsconfig.json file `properties` in schema from https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/tsconfig.json */ interface CompilerOptions { allowJs?: boolean; allowNonTsExtensions?: boolean; allowSyntheticDefaultImports?: boolean; allowUnreachableCode?: boolean; allowUnusedLabels?: boolean; baseUrl?: string; charset?: string; checkJs?: boolean; codepage?: number; declaration?: boolean; declarationDir?: string; diagnostics?: boolean; emitBOM?: boolean; experimentalAsyncFunctions?: boolean; experimentalDecorators?: boolean; emitDecoratorMetadata?: boolean; forceConsistentCasingInFileNames?: boolean; help?: boolean; isolatedModules?: boolean; init?: boolean; inlineSourceMap?: boolean; inlineSources?: boolean; jsx?: string; locale?: string; lib?: string[]; list?: string[]; listEmittedFiles?: boolean; listFiles?: boolean; mapRoot?: string; module?: string; moduleResolution?: string; newLine?: string; noEmit?: boolean; noEmitHelpers?: boolean; noEmitOnError?: boolean; noErrorTruncation?: boolean; noFallthroughCasesInSwitch?: boolean; noImplicitAny?: boolean; noImplicitReturns?: boolean; noImplicitThis?: boolean noImplicitUseStrict?: boolean; noLib?: boolean; noLibCheck?: boolean; noResolve?: boolean; noUnusedParameters?: boolean; noUnusedLocals?: boolean; out?: string; outFile?: string; outDir?: string; paths?: ts.Map<string[]>; preserveConstEnums?: boolean; pretty?: string; reactNamespace?: string; removeComments?: boolean; rootDir?: string; rootDirs?: string[]; skipDefaultLibCheck?: boolean; skipLibCheck?: boolean; sourceMap?: boolean; sourceRoot?: string; strict?: boolean; strictNullChecks?: boolean; stripInternal?: boolean; suppressExcessPropertyErrors?: boolean; suppressImplicitAnyIndexErrors?: boolean; suppressOutputPathCheck?: boolean; target?: string; traceResolution?: boolean; types?: string[]; typeRoots?: string[]; typesSearchPaths?: string[]; version?: boolean; watch?: boolean; } /** * This is the JSON.parse result of a tsconfig.json */ interface TypeScriptProjectRawSpecification { compilerOptions?: CompilerOptions; exclude?: string[]; // optional: An array of 'glob' patterns to specify directories / files to exclude include?: string[]; // optional: An array of 'glob' patterns to specify directories / files to include files?: string[]; // optional: paths to files formatCodeOptions?: formatting.FormatCodeOptions; // optional: formatting options compileOnSave?: boolean; // optional: compile on save. Ignored to build tools. Used by IDEs buildOnSave?: boolean; } ////////////////////////////////////////////////////////////////////// export var errors = { GET_PROJECT_INVALID_PATH: 'The path used to query for tsconfig.json does not exist', GET_PROJECT_NO_PROJECT_FOUND: 'No Project Found', GET_PROJECT_FAILED_TO_OPEN_PROJECT_FILE: 'Failed to fs.readFileSync the project file', GET_PROJECT_PROJECT_FILE_INVALID_OPTIONS: 'Project file contains invalid options', CREATE_FOLDER_MUST_EXIST: 'The folder must exist on disk in order to create a tsconfig.json', CREATE_PROJECT_ALREADY_EXISTS: 'tsconfig.json file already exists', }; export interface ProjectFileErrorDetails { projectFilePath: string; error: types.CodeError; } import path = require('path'); import os = require('os'); import formatting = require('./formatCodeOptions'); const projectFileName = 'tsconfig.json'; /** * This is what we use when the user doesn't specify a files / include */ const invisibleFilesInclude = ["./**/*.ts", "./**/*.tsx"]; const invisibleFilesIncludeWithJS = ["./**/*.ts", "./**/*.tsx", "./**/*.js"]; /** * What we use to * - create a new tsconfig on disk * - create an in memory project * - default values for a tsconfig file read from disk. Therefore it must match ts defaults */ const defaultCompilerOptions: ts.CompilerOptions = { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeJs, jsx: ts.JsxEmit.None, experimentalDecorators: false, emitDecoratorMetadata: false, declaration: false, noImplicitAny: false, suppressImplicitAnyIndexErrors: false, strictNullChecks: false, }; /** * If you want to create a project on the fly */ export function getDefaultInMemoryProject(srcFile: string): TypeScriptConfigFileDetails { var dir = path.dirname(srcFile); const allowJs = isJs(srcFile); var files = [srcFile]; var typings = getDefinitionsForNodeModules(dir, files); files = increaseCompilationContext(files, allowJs); files = uniq(files.map(fsu.consistentPath)); let project: TsconfigJsonParsed = { compilerOptions: extend(defaultCompilerOptions, { allowJs, lib: [ 'dom', 'es2017' ] }), files, typings: typings.ours.concat(typings.implicit), formatCodeOptions: formatting.defaultFormatCodeOptions(), compileOnSave: true, buildOnSave: false, }; return { projectFileDirectory: dir, projectFilePath: srcFile, project: project, inMemory: true }; } /** Given an src (source file or directory) goes up the directory tree to find the project specifications. * Use this to bootstrap the UI for what project the user might want to work on. * Note: Definition files (.d.ts) are considered thier own project */ type GetProjectSyncResponse = { error?: types.CodeError, result?: TypeScriptConfigFileDetails }; export function getProjectSync(pathOrSrcFile: string): GetProjectSyncResponse { if (!fsu.existsSync(pathOrSrcFile)) { return { error: makeBlandError(pathOrSrcFile, errors.GET_PROJECT_INVALID_PATH, 'tsconfig') } } // Get the path directory var dir = fs.lstatSync(pathOrSrcFile).isDirectory() ? pathOrSrcFile : path.dirname(pathOrSrcFile); // Keep going up till we find the project file var projectFile = ''; try { projectFile = fsu.travelUpTheDirectoryTreeTillYouFind(dir, projectFileName); } catch (e) { let err: Error = e; if (err.message == "not found") { let bland = makeBlandError(fsu.consistentPath(pathOrSrcFile), errors.GET_PROJECT_NO_PROJECT_FOUND, 'tsconfig'); return { error: bland }; } } projectFile = path.normalize(projectFile); var projectFileDirectory = path.dirname(projectFile); let projectFilePath = fsu.consistentPath(projectFile); // We now have a valid projectFile. Parse it: var projectSpec: TypeScriptProjectRawSpecification; try { var projectFileTextContent = fmc.getOrCreateOpenFile(projectFile).getContents(); } catch (ex) { return { error: makeBlandError(pathOrSrcFile, errors.GET_PROJECT_FAILED_TO_OPEN_PROJECT_FILE, 'tsconfig') } } let res = json.parse(projectFileTextContent); if (res.data) { projectSpec = res.data; } else { let bland = json.parseErrorToCodeError(projectFilePath, res.error, 'tsconfig'); return { error: bland }; } // Setup default project options if (!projectSpec.compilerOptions) projectSpec.compilerOptions = {}; // Additional global level validations if (projectSpec.files && projectSpec.exclude) { let bland = makeBlandError(projectFilePath, 'You cannot use both "files" and "exclude" in tsconfig.json', 'tsconfig'); return { error: bland }; } if (projectSpec.compilerOptions.allowJs && !projectSpec.compilerOptions.outDir) { let bland = makeBlandError(projectFilePath, 'You must use an `outDir` if you are using `allowJs` in tsconfig.json', 'tsconfig'); return { error: bland }; } if (projectSpec.compilerOptions.allowJs && projectSpec.compilerOptions.allowNonTsExtensions) { // Bad because otherwise all `package.json`s in the `files` start to give *JavaScript parsing* errors. let bland = makeBlandError(projectFilePath, 'If you are using `allowJs` you should not specify `allowNonTsExtensions` in tsconfig.json', 'tsconfig'); return { error: bland }; } /** * Always add `outDir`(if any) to exclude */ if (projectSpec.compilerOptions.outDir) { projectSpec.exclude = (projectSpec.exclude || []).concat(projectSpec.compilerOptions.outDir); } /** * Finally expand whatever needs expanding * See : https://github.com/TypeStrong/tsconfig/issues/19 */ try { const tsResult = ts.parseJsonConfigFileContent(projectSpec, ts.sys, path.dirname(projectFile), null, projectFile); // console.log(tsResult); // DEBUG projectSpec.files = tsResult.fileNames || []; } catch (ex) { return { error: makeBlandError(projectFilePath, ex.message, 'tsconfig') } } var pkg: PackageJsonParsed = null; try { const packageJSONPath = fsu.travelUpTheDirectoryTreeTillYouFind(projectFileDirectory, 'package.json'); if (packageJSONPath) { const parsedPackage = JSON.parse(fmc.getOrCreateOpenFile(packageJSONPath).getContents()); pkg = { main: parsedPackage.main, name: parsedPackage.name, directory: path.dirname(packageJSONPath), definition: parsedPackage.typescript && parsedPackage.typescript.definition }; } } catch (ex) { // console.error('no package.json found', projectFileDirectory, ex.message); } var project: TsconfigJsonParsed = { compilerOptions: {}, files: projectSpec.files.map(x => path.resolve(projectFileDirectory, x)), formatCodeOptions: formatting.makeFormatCodeOptions(projectSpec.formatCodeOptions), compileOnSave: projectSpec.compileOnSave == undefined ? true : projectSpec.compileOnSave, package: pkg, typings: [], buildOnSave: !!projectSpec.buildOnSave, }; // Validate the raw compiler options before converting them to TS compiler options var validationResult = validate(projectSpec.compilerOptions); if (validationResult.errorMessage) { return { error: makeBlandError(projectFilePath, validationResult.errorMessage, 'tsconfig') }; } // Convert the raw options to TS options project.compilerOptions = rawToTsCompilerOptions(projectSpec.compilerOptions, projectFileDirectory); // Expand files to include references project.files = increaseCompilationContext(project.files, !!project.compilerOptions.allowJs); // Expand files to include node_modules / package.json / typescript.definition var typings = getDefinitionsForNodeModules(dir, project.files); project.files = project.files.concat(typings.implicit); project.typings = typings.ours.concat(typings.implicit); project.files = project.files.concat(typings.packagejson); // Normalize to "/" for all files // And take the uniq values project.files = uniq(project.files.map(fsu.consistentPath)); projectFileDirectory = fsu.consistentPath(projectFileDirectory); return { result: { projectFileDirectory: projectFileDirectory, projectFilePath: projectFileDirectory + '/' + projectFileName, project: project, inMemory: false } }; } /** Creates a project by source file location. Defaults are assumed unless overriden by the optional spec. */ export function createProjectRootSync( srcFolder: string, defaultOptions: ts.CompilerOptions = extend(defaultCompilerOptions, { jsx: ts.JsxEmit.React, declaration: true, experimentalDecorators: true, emitDecoratorMetadata: true, outDir: 'lib', lib: [ 'dom', 'es2017', ], }), overWrite = true) { if (!fs.existsSync(srcFolder)) { throw new Error(errors.CREATE_FOLDER_MUST_EXIST); } var projectFilePath = path.normalize(srcFolder + '/' + projectFileName); if (!overWrite && fs.existsSync(projectFilePath)) throw new Error(errors.CREATE_PROJECT_ALREADY_EXISTS); // We need to write the raw spec var projectSpec: TypeScriptProjectRawSpecification = {}; projectSpec.compilerOptions = tsToRawCompilerOptions(defaultOptions); projectSpec.compileOnSave = true; projectSpec.exclude = ["node_modules"]; projectSpec.include = ["src"]; fs.writeFileSync(projectFilePath, json.stringify(projectSpec, os.EOL)); return getProjectSync(srcFolder); } ////////////////////////////////////////////////////////////////////// /** * ENUM to String and String to ENUM */ const typescriptEnumMap = { target: { 'es3': ts.ScriptTarget.ES3, 'es5': ts.ScriptTarget.ES5, 'es6': ts.ScriptTarget.ES2015, 'es2015': ts.ScriptTarget.ES2015, 'es2016': ts.ScriptTarget.ES2016, 'es2017': ts.ScriptTarget.ES2017, 'esnext': ts.ScriptTarget.ESNext, 'next': ts.ScriptTarget.ESNext, 'latest': ts.ScriptTarget.Latest }, module: { 'none': ts.ModuleKind.None, 'commonjs': ts.ModuleKind.CommonJS, 'amd': ts.ModuleKind.AMD, 'umd': ts.ModuleKind.UMD, 'system': ts.ModuleKind.System, 'es6': ts.ModuleKind.ES2015, 'es2015': ts.ModuleKind.ES2015, }, moduleResolution: { 'node': ts.ModuleResolutionKind.NodeJs, 'classic': ts.ModuleResolutionKind.Classic }, jsx: { 'none': ts.JsxEmit.None, 'preserve': ts.JsxEmit.Preserve, 'react': ts.JsxEmit.React, 'react-native': ts.JsxEmit.ReactNative, }, newLine: { 'CRLF': ts.NewLineKind.CarriageReturnLineFeed, 'LF': ts.NewLineKind.LineFeed } }; /** * These are options that are relative paths to tsconfig.json * Note: There is also `rootDirs` that is handled manually */ const pathResolveTheseOptions = [ 'out', 'outFile', 'outDir', 'rootDir', 'baseUrl', ]; ////////////////////////////////////////////////////////////////////// /** * Raw To Compiler */ function rawToTsCompilerOptions(jsonOptions: CompilerOptions, projectDir: string): ts.CompilerOptions { const compilerOptions = extend(defaultCompilerOptions); /** Parse the enums */ for (var key in jsonOptions) { if (typescriptEnumMap[key]) { let name = jsonOptions[key]; let map = typescriptEnumMap[key]; compilerOptions[key] = map[name.toLowerCase()] || map[name.toUpperCase()]; } else { compilerOptions[key] = jsonOptions[key]; } } /** * Parse all paths to not be relative */ pathResolveTheseOptions.forEach(option => { if (compilerOptions[option] !== undefined) { compilerOptions[option] = fsu.resolve(projectDir, compilerOptions[option] as string); } }); /** * Support `rootDirs` * https://github.com/Microsoft/TypeScript-Handbook/blob/release-2.0/pages/Module%20Resolution.md#virtual-directories-with-rootdirs */ if (compilerOptions.rootDirs !== undefined && Array.isArray(compilerOptions.rootDirs)) { compilerOptions.rootDirs = compilerOptions.rootDirs.map(rd => { return fsu.resolve(projectDir, rd as string); }); } /** * Till `out` is removed. Support it by just copying it to `outFile` */ if (compilerOptions.out !== undefined) { compilerOptions.outFile = path.resolve(projectDir, compilerOptions.out); } /** * The default for moduleResolution as implemented by the compiler */ if (!jsonOptions.moduleResolution && compilerOptions.module !== ts.ModuleKind.CommonJS) { compilerOptions.moduleResolution = ts.ModuleResolutionKind.Classic; } return compilerOptions; } /** * Compiler to Raw */ function tsToRawCompilerOptions(compilerOptions: ts.CompilerOptions): CompilerOptions { const jsonOptions = extend({}, compilerOptions) as (ts.CompilerOptions & CompilerOptions); /** * Convert enums to raw */ Object.keys(compilerOptions).forEach((key) => { if (typescriptEnumMap[key] !== undefined && compilerOptions[key] !== undefined) { const value = compilerOptions[key] as string; const rawToTsMapForKey = typescriptEnumMap[key]; const reverseMap = reverseKeysAndValues(rawToTsMapForKey); jsonOptions[key] = reverseMap[value]; } }); return jsonOptions; }