diff --git a/package-lock.json b/package-lock.json index ee0062bf3..a4794e0aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -623,6 +623,15 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, + "@types/fs-extra": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.1.tgz", + "integrity": "sha512-J00cVDALmi/hJOYsunyT52Hva5TnJeKP5yd1r+mH/ZU0mbYZflR0Z5kw5kITtKTRYMhm1JMClOFYdHnQszEvqw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", @@ -1861,6 +1870,25 @@ "map-cache": "^0.2.2" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3493,6 +3521,15 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -5221,6 +5258,12 @@ "set-value": "^2.0.1" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index 3c08b8fa1..5ea65f798 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,13 @@ "typescript": "^3.6.2" }, "devDependencies": { + "@types/fs-extra": "^8.0.1", "@types/glob": "^7.1.1", "@types/jest": "^24.0.15", "@types/node": "^11.13.14", "@types/resolve": "0.0.8", "fengari": "^0.1.4", + "fs-extra": "^8.1.0", "javascript-stringify": "^2.0.0", "jest": "^24.8.0", "jest-circus": "^24.8.0", diff --git a/src/Emit.ts b/src/Emit.ts index 50d5c1fdf..ecb1453f9 100644 --- a/src/Emit.ts +++ b/src/Emit.ts @@ -1,10 +1,10 @@ import * as path from "path"; import * as ts from "typescript"; import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; -import { TranspiledFile, EmitHost } from "./Transpile"; +import { EmitHost, TranspiledFile } from "./Transpile"; +import { normalizeSlashes } from "./utils"; const trimExt = (filePath: string) => filePath.slice(0, -path.extname(filePath).length); -const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); export interface OutputFile { name: string; diff --git a/src/TSTransformers.ts b/src/TSTransformers.ts index 8cc38356c..710e3ac0e 100644 --- a/src/TSTransformers.ts +++ b/src/TSTransformers.ts @@ -1,6 +1,7 @@ import * as path from "path"; import * as resolve from "resolve"; import * as ts from "typescript"; +import * as cliDiagnostics from "./cli/diagnostics"; import { CompilerOptions, TransformerImport } from "./CompilerOptions"; import * as diagnosticFactories from "./diagnostics"; import { noImplicitSelfTransformer } from "./NoImplicitSelfTransformer"; @@ -111,7 +112,7 @@ function resolveTransformerFactory( ): { error?: ts.Diagnostic; factory?: TransformerFactory } { if (typeof transform !== "string") { const optionName = `${transformerOptionPath}.transform`; - return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "string") }; + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") }; } let resolved: string; @@ -167,18 +168,18 @@ function loadTransformer( break; default: { const optionName = `--${transformerOptionPath}.type`; - return { error: diagnosticFactories.argumentForOptionMustBe(optionName, "program") }; + return { error: cliDiagnostics.argumentForOptionMustBe(optionName, "program") }; } } if (typeof after !== "boolean") { const optionName = `${transformerOptionPath}.after`; - return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "boolean") }; + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") }; } if (typeof afterDeclarations !== "boolean") { const optionName = `${transformerOptionPath}.afterDeclarations`; - return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "boolean") }; + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") }; } if (typeof transformer === "function") { diff --git a/src/cli/diagnostics.ts b/src/cli/diagnostics.ts new file mode 100644 index 000000000..9e950710f --- /dev/null +++ b/src/cli/diagnostics.ts @@ -0,0 +1,81 @@ +import * as ts from "typescript"; + +export const tstlOptionsAreMovingToTheTstlObject = (tstl: Record): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Warning, + code: 0, + source: "typescript-to-lua", + messageText: + 'TSTL options are moving to the "tstl" object. Adjust your tsconfig to look like\n' + + `"tstl": ${JSON.stringify(tstl, undefined, 4)}`, +}); + +export const watchErrorSummary = (errorCount: number): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Message, + code: errorCount === 1 ? 6193 : 6194, + messageText: + errorCount === 1 + ? "Found 1 error. Watching for file changes." + : `Found ${errorCount} errors. Watching for file changes.`, +}); + +const createCommandLineError = (code: number, getMessage: (...args: Args) => string) => ( + ...args: Args +): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Error, + code, + messageText: getMessage(...args), +}); + +export const unknownCompilerOption = createCommandLineError( + 5023, + (name: string) => `Unknown compiler option '${name}'.` +); + +export const compilerOptionRequiresAValueOfType = createCommandLineError( + 5024, + (name: string, type: string) => `Compiler option '${name}' requires a value of type ${type}.` +); + +export const optionProjectCannotBeMixedWithSourceFilesOnACommandLine = createCommandLineError( + 5042, + () => "Option 'project' cannot be mixed with source files on a command line." +); + +export const cannotFindATsconfigJsonAtTheSpecifiedDirectory = createCommandLineError( + 5057, + (dir: string) => `Cannot find a tsconfig.json file at the specified directory: '${dir}'.` +); + +export const theSpecifiedPathDoesNotExist = createCommandLineError( + 5058, + (dir: string) => `The specified path does not exist: '${dir}'.` +); + +export const compilerOptionExpectsAnArgument = createCommandLineError( + 6044, + (name: string) => `Compiler option '${name}' expects an argument.` +); + +export const argumentForOptionMustBe = createCommandLineError( + 6046, + (name: string, values: string) => `Argument for '${name}' option must be: ${values}.` +); + +export const optionCanOnlyBeSpecifiedInTsconfigJsonFile = createCommandLineError( + 6064, + (name: string) => `Option '${name}' can only be specified in 'tsconfig.json' file.` +); + +export const optionBuildMustBeFirstCommandLineArgument = createCommandLineError( + 6369, + () => "Option '--build' must be the first command line argument." +); diff --git a/src/cli/information.ts b/src/cli/information.ts new file mode 100644 index 000000000..df39c0ecd --- /dev/null +++ b/src/cli/information.ts @@ -0,0 +1,32 @@ +import { optionDeclarations } from "./parse"; + +export const { version } = require("../../package.json"); +export const versionString = `Version ${version}`; + +const helpString = ` +Syntax: tstl [options] [files...] + +Examples: tstl path/to/file.ts [...] + tstl -p path/to/tsconfig.json + +In addition to the options listed below you can also pass options +for the typescript compiler (For a list of options use tsc -h). +Some tsc options might have no effect. +`.trim(); + +export function getHelpString(): string { + let result = helpString + "\n\n"; + + result += "Options:\n"; + for (const option of optionDeclarations) { + const aliasStrings = (option.aliases || []).map(a => "-" + a); + const optionString = aliasStrings.concat(["--" + option.name]).join("|"); + + const valuesHint = option.type === "enum" ? option.choices.join("|") : option.type; + const spacing = " ".repeat(Math.max(1, 45 - optionString.length - valuesHint.length)); + + result += `\n ${optionString} <${valuesHint}>${spacing}${option.description}\n`; + } + + return result; +} diff --git a/src/CommandLineParser.ts b/src/cli/parse.ts similarity index 67% rename from src/CommandLineParser.ts rename to src/cli/parse.ts index d657e0127..8bbaad42b 100644 --- a/src/CommandLineParser.ts +++ b/src/cli/parse.ts @@ -1,7 +1,6 @@ -import * as path from "path"; import * as ts from "typescript"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; -import * as diagnosticFactories from "./diagnostics"; +import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; +import * as cliDiagnostics from "./diagnostics"; export interface ParsedCommandLine extends ts.ParsedCommandLine { options: CompilerOptions; @@ -24,7 +23,7 @@ interface CommandLineOptionOfBoolean extends CommandLineOptionBase { type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfBoolean; -const optionDeclarations: CommandLineOption[] = [ +export const optionDeclarations: CommandLineOption[] = [ { name: "luaLibImport", description: "Specifies how js standard features missing in lua are imported.", @@ -60,36 +59,6 @@ const optionDeclarations: CommandLineOption[] = [ }, ]; -export const version = `Version ${require("../package.json").version}`; - -const helpString = ` -Syntax: tstl [options] [files...] - -Examples: tstl path/to/file.ts [...] - tstl -p path/to/tsconfig.json - -In addition to the options listed below you can also pass options -for the typescript compiler (For a list of options use tsc -h). -Some tsc options might have no effect. -`.trim(); - -export function getHelpString(): string { - let result = helpString + "\n\n"; - - result += "Options:\n"; - for (const option of optionDeclarations) { - const aliasStrings = (option.aliases || []).map(a => "-" + a); - const optionString = aliasStrings.concat(["--" + option.name]).join("|"); - - const valuesHint = option.type === "enum" ? option.choices.join("|") : option.type; - const spacing = " ".repeat(Math.max(1, 45 - optionString.length - valuesHint.length)); - - result += `\n ${optionString} <${valuesHint}>${spacing}${option.description}\n`; - } - - return result; -} - export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine): ParsedCommandLine { let hasRootLevelOptions = false; for (const key in parsedConfigFile.raw) { @@ -103,15 +72,13 @@ export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine): if (parsedConfigFile.raw.tstl) { if (hasRootLevelOptions) { - parsedConfigFile.errors.push( - diagnosticFactories.tstlOptionsAreMovingToTheTstlObject(parsedConfigFile.raw.tstl) - ); + parsedConfigFile.errors.push(cliDiagnostics.tstlOptionsAreMovingToTheTstlObject(parsedConfigFile.raw.tstl)); } for (const key in parsedConfigFile.raw.tstl) { const option = optionDeclarations.find(option => option.name === key); if (!option) { - parsedConfigFile.errors.push(diagnosticFactories.unknownCompilerOption(key)); + parsedConfigFile.errors.push(cliDiagnostics.unknownCompilerOption(key)); continue; } @@ -179,7 +146,7 @@ function readCommandLineArgument(option: CommandLineOption, value: any): Command if (value === undefined) { return { - error: diagnosticFactories.compilerOptionExpectsAnArgument(option.name), + error: cliDiagnostics.compilerOptionExpectsAnArgument(option.name), value: undefined, increment: 0, }; @@ -201,7 +168,7 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult { if (typeof value !== "boolean") { return { value: undefined, - error: diagnosticFactories.compilerOptionRequiresAValueOfType(option.name, "boolean"), + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, "boolean"), }; } @@ -212,7 +179,7 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult { if (typeof value !== "string") { return { value: undefined, - error: diagnosticFactories.compilerOptionRequiresAValueOfType(option.name, "string"), + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, "string"), }; } @@ -221,7 +188,7 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult { const optionChoices = option.choices.join(", "); return { value: undefined, - error: diagnosticFactories.argumentForOptionMustBe(`--${option.name}`, optionChoices), + error: cliDiagnostics.argumentForOptionMustBe(`--${option.name}`, optionChoices), }; } @@ -229,30 +196,3 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult { } } } - -export function parseConfigFileWithSystem( - configFileName: string, - commandLineOptions?: CompilerOptions, - system = ts.sys -): ParsedCommandLine { - const parsedConfigFile = ts.parseJsonSourceFileConfigFileContent( - ts.readJsonConfigFile(configFileName, system.readFile), - system, - path.dirname(configFileName), - commandLineOptions, - configFileName - ); - - return updateParsedConfigFile(parsedConfigFile); -} - -export function createDiagnosticReporter(pretty: boolean, system = ts.sys): ts.DiagnosticReporter { - const reporter: ts.DiagnosticReporter = (ts as any).createDiagnosticReporter(system, pretty); - return diagnostic => { - if (diagnostic.source === "typescript-to-lua") { - diagnostic = { ...diagnostic, code: ("TL" + diagnostic.code) as any }; - } - - reporter(diagnostic); - }; -} diff --git a/src/cli/report.ts b/src/cli/report.ts new file mode 100644 index 000000000..039ca069c --- /dev/null +++ b/src/cli/report.ts @@ -0,0 +1,12 @@ +import * as ts from "typescript"; + +export function createDiagnosticReporter(pretty: boolean, system = ts.sys): ts.DiagnosticReporter { + const reporter = ts.createDiagnosticReporter(system, pretty); + return diagnostic => { + if (diagnostic.source === "typescript-to-lua") { + diagnostic = { ...diagnostic, code: ("TL" + diagnostic.code) as any }; + } + + reporter(diagnostic); + }; +} diff --git a/src/cli/tsconfig.ts b/src/cli/tsconfig.ts new file mode 100644 index 000000000..369c14363 --- /dev/null +++ b/src/cli/tsconfig.ts @@ -0,0 +1,82 @@ +import * as path from "path"; +import * as ts from "typescript"; +import { CompilerOptions } from "../CompilerOptions"; +import { normalizeSlashes } from "../utils"; +import * as cliDiagnostics from "./diagnostics"; +import { ParsedCommandLine, updateParsedConfigFile } from "./parse"; + +export function locateConfigFile(commandLine: ParsedCommandLine): ts.Diagnostic | string | undefined { + const { project } = commandLine.options; + if (!project) { + if (commandLine.fileNames.length > 0) { + return undefined; + } + + const searchPath = normalizeSlashes(ts.sys.getCurrentDirectory()); + return ts.findConfigFile(searchPath, ts.sys.fileExists); + } + + if (commandLine.fileNames.length !== 0) { + return cliDiagnostics.optionProjectCannotBeMixedWithSourceFilesOnACommandLine(); + } + + // TODO: Unlike tsc, this resolves `.` to absolute path + const fileOrDirectory = normalizeSlashes(path.resolve(ts.sys.getCurrentDirectory(), project)); + if (ts.sys.directoryExists(fileOrDirectory)) { + const configFileName = path.posix.join(fileOrDirectory, "tsconfig.json"); + if (ts.sys.fileExists(configFileName)) { + return configFileName; + } else { + return cliDiagnostics.cannotFindATsconfigJsonAtTheSpecifiedDirectory(project); + } + } else if (ts.sys.fileExists(fileOrDirectory)) { + return fileOrDirectory; + } else { + return cliDiagnostics.theSpecifiedPathDoesNotExist(project); + } +} + +export function parseConfigFileWithSystem( + configFileName: string, + commandLineOptions?: CompilerOptions, + system = ts.sys +): ParsedCommandLine { + const parsedConfigFile = ts.parseJsonSourceFileConfigFileContent( + ts.readJsonConfigFile(configFileName, system.readFile), + system, + path.dirname(configFileName), + commandLineOptions, + configFileName + ); + + return updateParsedConfigFile(parsedConfigFile); +} + +export function createConfigFileUpdater( + optionsToExtend: CompilerOptions +): (options: ts.CompilerOptions) => ts.Diagnostic[] { + const configFileMap = new WeakMap(); + return options => { + const configFile = options.configFile as ts.TsConfigSourceFile | undefined; + const configFilePath = options.configFilePath as string | undefined; + if (!configFile || !configFilePath) return []; + + if (!configFileMap.has(configFile)) { + const parsedConfigFile = updateParsedConfigFile( + ts.parseJsonSourceFileConfigFileContent( + configFile, + ts.sys, + path.dirname(configFilePath), + optionsToExtend, + configFilePath + ) + ); + + configFileMap.set(configFile, parsedConfigFile); + } + + const parsedConfigFile = configFileMap.get(configFile)!; + Object.assign(options, parsedConfigFile.options); + return parsedConfigFile.errors; + }; +} diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 2f052d272..5e614a380 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -11,18 +11,6 @@ export const transpileError = (error: TranspileError): ts.Diagnostic => ({ messageText: error.message, }); -export const tstlOptionsAreMovingToTheTstlObject = (tstl: Record): ts.Diagnostic => ({ - file: undefined, - start: undefined, - length: undefined, - category: ts.DiagnosticCategory.Warning, - code: 0, - source: "typescript-to-lua", - messageText: - 'TSTL options are moving to the "tstl" object. Adjust your tsconfig to look like\n' + - `"tstl": ${JSON.stringify(tstl, undefined, 4)}`, -}); - export const toLoadTransformerItShouldBeTranspiled = (transform: string): ts.Diagnostic => ({ file: undefined, start: undefined, @@ -62,71 +50,3 @@ export const transformerShouldBeATsTransformerFactory = (transform: string): ts. source: "typescript-to-lua", messageText: `"${transform}" transformer should be a ts.TransformerFactory or an object with ts.TransformerFactory values`, }); - -export const watchErrorSummary = (errorCount: number): ts.Diagnostic => ({ - file: undefined, - start: undefined, - length: undefined, - category: ts.DiagnosticCategory.Message, - code: errorCount === 1 ? 6193 : 6194, - messageText: - errorCount === 1 - ? "Found 1 error. Watching for file changes." - : `Found ${errorCount} errors. Watching for file changes.`, -}); - -const createCommandLineError = (code: number, getMessage: (...args: Args) => string) => ( - ...args: Args -): ts.Diagnostic => ({ - file: undefined, - start: undefined, - length: undefined, - category: ts.DiagnosticCategory.Error, - code, - messageText: getMessage(...args), -}); - -export const unknownCompilerOption = createCommandLineError( - 5023, - (name: string) => `Unknown compiler option '${name}'.` -); - -export const compilerOptionRequiresAValueOfType = createCommandLineError( - 5024, - (name: string, type: string) => `Compiler option '${name}' requires a value of type ${type}.` -); - -export const optionProjectCannotBeMixedWithSourceFilesOnACommandLine = createCommandLineError( - 5042, - () => "Option 'project' cannot be mixed with source files on a command line." -); - -export const cannotFindATsconfigJsonAtTheSpecifiedDirectory = createCommandLineError( - 5057, - (dir: string) => `Cannot find a tsconfig.json file at the specified directory: '${dir}'.` -); - -export const theSpecifiedPathDoesNotExist = createCommandLineError( - 5058, - (dir: string) => `The specified path does not exist: '${dir}'.` -); - -export const compilerOptionExpectsAnArgument = createCommandLineError( - 6044, - (name: string) => `Compiler option '${name}' expects an argument.` -); - -export const argumentForOptionMustBe = createCommandLineError( - 6046, - (name: string, values: string) => `Argument for '${name}' option must be: ${values}.` -); - -export const optionCanOnlyBeSpecifiedInTsconfigJsonFile = createCommandLineError( - 6064, - (name: string) => `Option '${name}' can only be specified in 'tsconfig.json' file.` -); - -export const optionBuildMustBeFirstCommandLineArgument = createCommandLineError( - 6369, - () => "Option '--build' must be the first command line argument." -); diff --git a/src/index.ts b/src/index.ts index 17226cf7f..f9a514fb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,14 @@ import * as fs from "fs"; import * as path from "path"; import * as ts from "typescript"; -import { parseConfigFileWithSystem } from "./CommandLineParser"; +import { parseConfigFileWithSystem } from "./cli/tsconfig"; import { CompilerOptions } from "./CompilerOptions"; import { emitTranspiledFiles, OutputFile } from "./Emit"; import { transpile, TranspiledFile, TranspileResult } from "./Transpile"; -export { - createDiagnosticReporter, - parseCommandLine, - ParsedCommandLine, - updateParsedConfigFile, -} from "./CommandLineParser"; +export { version } from "./cli/information"; +export { parseCommandLine, ParsedCommandLine, updateParsedConfigFile } from "./cli/parse"; +export * from "./cli/report"; export * from "./CompilerOptions"; export * from "./Emit"; export * from "./LuaAST"; diff --git a/src/tstl.ts b/src/tstl.ts index 8860b8929..e3c416f6e 100644 --- a/src/tstl.ts +++ b/src/tstl.ts @@ -1,13 +1,11 @@ #!/usr/bin/env node -import * as path from "path"; import * as ts from "typescript"; import * as tstl from "."; -import * as CommandLineParser from "./CommandLineParser"; -import * as diagnosticFactories from "./diagnostics"; - -function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { - return (ts as any).createWatchStatusReporter(ts.sys, shouldBePretty(options)); -} +import * as cliDiagnostics from "./cli/diagnostics"; +import { getHelpString, versionString } from "./cli/information"; +import { parseCommandLine } from "./cli/parse"; +import { createDiagnosticReporter } from "./cli/report"; +import { createConfigFileUpdater, locateConfigFile, parseConfigFileWithSystem } from "./cli/tsconfig"; function shouldBePretty(options?: ts.CompilerOptions): boolean { return !options || options.pretty === undefined @@ -15,49 +13,13 @@ function shouldBePretty(options?: ts.CompilerOptions): boolean { : Boolean(options.pretty); } -let reportDiagnostic = tstl.createDiagnosticReporter(false); +let reportDiagnostic = createDiagnosticReporter(false); function updateReportDiagnostic(options?: ts.CompilerOptions): void { - reportDiagnostic = tstl.createDiagnosticReporter(shouldBePretty(options)); + reportDiagnostic = createDiagnosticReporter(shouldBePretty(options)); } -function locateConfigFile(commandLine: tstl.ParsedCommandLine): string | undefined { - const { project } = commandLine.options; - if (!project) { - if (commandLine.fileNames.length === 0) { - const searchPath = path.posix.normalize(ts.sys.getCurrentDirectory()); - return ts.findConfigFile(searchPath, ts.sys.fileExists); - } - return; - } - - if (commandLine.fileNames.length !== 0) { - reportDiagnostic(diagnosticFactories.optionProjectCannotBeMixedWithSourceFilesOnACommandLine()); - ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - - let fileOrDirectory = path.posix.normalize(project); - if (!path.isAbsolute(fileOrDirectory)) { - fileOrDirectory = path.posix.join(ts.sys.getCurrentDirectory(), fileOrDirectory); - } - - if (!fileOrDirectory || ts.sys.directoryExists(fileOrDirectory)) { - const configFileName = path.posix.join(fileOrDirectory, "tsconfig.json"); - if (ts.sys.fileExists(configFileName)) { - return configFileName; - } else { - reportDiagnostic(diagnosticFactories.cannotFindATsconfigJsonAtTheSpecifiedDirectory(project)); - - ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } else { - if (ts.sys.fileExists(fileOrDirectory)) { - return fileOrDirectory; - } else { - reportDiagnostic(diagnosticFactories.theSpecifiedPathDoesNotExist(project)); - ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } +function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { + return ts.createWatchStatusReporter(ts.sys, shouldBePretty(options)); } function executeCommandLine(args: string[]): void { @@ -68,10 +30,10 @@ function executeCommandLine(args: string[]): void { } } - const commandLine = CommandLineParser.parseCommandLine(args); + const commandLine = parseCommandLine(args); if (commandLine.options.build) { - reportDiagnostic(diagnosticFactories.optionBuildMustBeFirstCommandLineArgument()); + reportDiagnostic(cliDiagnostics.optionBuildMustBeFirstCommandLineArgument()); return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -83,20 +45,25 @@ function executeCommandLine(args: string[]): void { } if (commandLine.options.version) { - console.log(CommandLineParser.version); + console.log(versionString); return ts.sys.exit(ts.ExitStatus.Success); } if (commandLine.options.help) { - console.log(CommandLineParser.version); - console.log(CommandLineParser.getHelpString()); + console.log(versionString); + console.log(getHelpString()); return ts.sys.exit(ts.ExitStatus.Success); } const configFileName = locateConfigFile(commandLine); + if (typeof configFileName === "object") { + reportDiagnostic(configFileName); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + const commandLineOptions = commandLine.options; if (configFileName) { - const configParseResult = CommandLineParser.parseConfigFileWithSystem(configFileName, commandLineOptions); + const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions); updateReportDiagnostic(configParseResult.options); if (configParseResult.options.watch) { @@ -191,34 +158,12 @@ function updateWatchCompilationHost( optionsToExtend: tstl.CompilerOptions ): void { let fullRecompile = true; - const configFileMap = new WeakMap(); + const updateConfigFile = createConfigFileUpdater(optionsToExtend); host.afterProgramCreate = builderProgram => { const program = builderProgram.getProgram(); const options = builderProgram.getCompilerOptions() as tstl.CompilerOptions; - - let configFileParsingDiagnostics: ts.Diagnostic[] = []; - const configFile = options.configFile as ts.TsConfigSourceFile | undefined; - const configFilePath = options.configFilePath as string | undefined; - if (configFile && configFilePath) { - if (!configFileMap.has(configFile)) { - const parsedConfigFile = CommandLineParser.updateParsedConfigFile( - ts.parseJsonSourceFileConfigFileContent( - configFile, - ts.sys, - path.dirname(configFilePath), - optionsToExtend, - configFilePath - ) - ); - - configFileMap.set(configFile, parsedConfigFile); - } - - const parsedConfigFile = configFileMap.get(configFile)!; - Object.assign(options, parsedConfigFile.options); - configFileParsingDiagnostics = parsedConfigFile.errors; - } + const configFileParsingDiagnostics: ts.Diagnostic[] = updateConfigFile(options); let sourceFiles: ts.SourceFile[] | undefined; if (!fullRecompile) { @@ -255,10 +200,12 @@ function updateWatchCompilationHost( // do a full recompile after an error fullRecompile = errors.length > 0; - host.onWatchStatusChange!(diagnosticFactories.watchErrorSummary(errors.length), host.getNewLine(), options); + host.onWatchStatusChange!(cliDiagnostics.watchErrorSummary(errors.length), host.getNewLine(), options); }; } -if ((ts.sys as any).setBlocking) (ts.sys as any).setBlocking(); +if (ts.sys.setBlocking) { + ts.sys.setBlocking(); +} executeCommandLine(ts.sys.args); diff --git a/src/typescript-internal.ts b/src/typescript-internal.ts index ddff96eaf..6f5819c38 100644 --- a/src/typescript-internal.ts +++ b/src/typescript-internal.ts @@ -1,6 +1,13 @@ import * as ts from "typescript"; declare module "typescript" { + function createDiagnosticReporter(system: ts.System, pretty?: boolean): ts.DiagnosticReporter; + function createWatchStatusReporter(system: ts.System, pretty?: boolean): ts.WatchStatusReporter; + + interface System { + setBlocking?(): void; + } + interface Statement { jsDoc?: ts.JSDoc[]; } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..0fe3f68b6 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1 @@ +export const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); diff --git a/test/unit/commandLineParser.spec.ts b/test/cli/parse.spec.ts similarity index 100% rename from test/unit/commandLineParser.spec.ts rename to test/cli/parse.spec.ts diff --git a/test/cli/tsconfig.spec.ts b/test/cli/tsconfig.spec.ts new file mode 100644 index 000000000..b4fac5cbf --- /dev/null +++ b/test/cli/tsconfig.spec.ts @@ -0,0 +1,93 @@ +import * as fs from "fs-extra"; +import * as os from "os"; +import * as path from "path"; +import { locateConfigFile } from "../../src/cli/tsconfig"; +import { normalizeSlashes } from "../../src/utils"; + +let temp: string; +beforeEach(async () => { + temp = await fs.mkdtemp(path.join(os.tmpdir(), "tstl-test-")); + process.chdir(temp); +}); + +const originalWorkingDirectory = process.cwd(); +afterEach(async () => { + process.chdir(originalWorkingDirectory); + // TODO [node@12]: `rmdir` has `recursive` option + await fs.remove(temp); +}); + +const locate = (project: string | undefined, fileNames: string[] = []) => + locateConfigFile({ errors: [], fileNames, options: { project } }); + +const normalize = (name: string) => normalizeSlashes(path.resolve(temp, name)); + +describe("specified", () => { + for (const separator of process.platform === "win32" ? ["/", "\\"] : ["/"]) { + for (const pointsTo of ["file", "directory"] as const) { + const findAndExpect = (project: string, expected: string) => { + project = project.replace(/[\\/]/g, separator); + if (pointsTo === "directory") { + project = path.dirname(project); + } + + expect(locate(project)).toBe(normalize(expected)); + }; + + test(`relative to ${pointsTo} separated with '${separator}'`, async () => { + await fs.outputFile("tsconfig.json", ""); + await fs.mkdir("src"); + process.chdir("src"); + findAndExpect("../tsconfig.json", "tsconfig.json"); + }); + + test(`absolute to ${pointsTo} separated with '${separator}'`, async () => { + await fs.outputFile("tsconfig.json", ""); + findAndExpect(path.resolve("tsconfig.json"), "tsconfig.json"); + }); + } + } + + test.each(["", ".", "./"])("current directory (%p)", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(".")).toBe(normalize("tsconfig.json")); + }); +}); + +describe("inferred", () => { + test("in current directory", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(undefined)).toBe(normalize("tsconfig.json")); + }); + + test("in parent directory", async () => { + await fs.outputFile("tsconfig.json", ""); + await fs.mkdir("src"); + process.chdir("src"); + expect(locate(undefined)).toBe(normalize("tsconfig.json")); + }); + + test("not found", () => { + expect(locate(undefined)).toBe(undefined); + }); + + test("does not attempt when has files", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(undefined, [""])).toBe(undefined); + }); +}); + +describe("errors", () => { + test("specified file does not exist", () => { + expect([locate("tsconfig.json")]).toHaveDiagnostics(); + }); + + test("specified directory does not exist", () => { + expect([locate("project")]).toHaveDiagnostics(); + }); + + test("cannot be mixed", async () => { + await fs.outputFile("tsconfig.json", ""); + expect([locate("tsconfig.json", [""])]).toHaveDiagnostics(); + }); +});