package tsoptions_test import ( "fmt" "io" "io/fs" "path/filepath" "strings" "testing" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnosticwriter" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsonutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/baseline" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions/tsoptionstest" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/osvfs" "github.com/google/go-cmp/cmp/cmpopts" "gotest.tools/v3/assert" ) type testConfig struct { jsonText string configFileName string basePath string allFileList map[string]string } var parseConfigFileTextToJsonTests = []struct { title string input []string }{ { title: "returns empty config for file with only whitespaces", input: []string{ "", " ", }, }, { title: "returns empty config for file with comments only", input: []string{ "// Comment", "/* Comment*/", }, }, { title: "returns empty config when config is empty object", input: []string{ `{}`, }, }, { title: "returns config object without comments", input: []string{ `{ // Excluded files "exclude": [ // Exclude d.ts "file.d.ts" ] }`, `{ /* Excluded Files */ "exclude": [ /* multiline comments can be in the middle of a line */"file.d.ts" ] }`, }, }, { title: "keeps string content untouched", input: []string{ `{ "exclude": [ "xx//file.d.ts" ] }`, `{ "exclude": [ "xx/*file.d.ts*/" ] }`, }, }, { title: "handles escaped characters in strings correctly", input: []string{ `{ "exclude": [ "xx\"//files" ] }`, `{ "exclude": [ "xx\\" // end of line comment ] }`, }, }, { title: "returns object when users correctly specify library", input: []string{ `{ "compilerOptions": { "lib": ["es5"] } }`, `{ "compilerOptions": { "lib": ["es5", "es6"] } }`, }, }, } func TestParseConfigFileTextToJson(t *testing.T) { t.Parallel() repo.SkipIfNoTypeScriptSubmodule(t) for _, rec := range parseConfigFileTextToJsonTests { t.Run(rec.title, func(t *testing.T) { t.Parallel() var baselineContent strings.Builder for i, jsonText := range rec.input { baselineContent.WriteString("Input::\n") baselineContent.WriteString(jsonText + "\n") parsed, errors := tsoptions.ParseConfigFileTextToJson("/apath/tsconfig.json", "/apath", jsonText) baselineContent.WriteString("Config::\n") assert.NilError(t, writeJsonReadableText(&baselineContent, parsed), "Failed to write JSON text") baselineContent.WriteString("\n") baselineContent.WriteString("Errors::\n") diagnosticwriter.FormatDiagnosticsWithColorAndContext(&baselineContent, errors, &diagnosticwriter.FormattingOptions{ NewLine: "\n", ComparePathsOptions: tspath.ComparePathsOptions{ CurrentDirectory: "/", UseCaseSensitiveFileNames: true, }, }) baselineContent.WriteString("\n") if i != len(rec.input)-1 { baselineContent.WriteString("\n") } } baseline.RunAgainstSubmodule(t, rec.title+" jsonParse.js", baselineContent.String(), baseline.Options{Subfolder: "config/tsconfigParsing"}) }) } } type parseJsonConfigTestCase struct { title string noSubmoduleBaseline bool input []testConfig } var parseJsonConfigFileTests = []parseJsonConfigTestCase{ { title: "ignore dotted files and folders", input: []testConfig{{ jsonText: `{}`, configFileName: "tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/test.ts": "", "/apath/.git/a.ts": "", "/apath/.b.ts": "", "/apath/..c.ts": ""}, }}, }, { title: "allow dotted files and folders when explicitly requested", input: []testConfig{{ jsonText: `{ "files": ["/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"] }`, configFileName: "tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/test.ts": "", "/apath/.git/a.ts": "", "/apath/.b.ts": "", "/apath/..c.ts": ""}, }}, }, { title: "implicitly exclude common package folders", input: []testConfig{{ jsonText: `{}`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/node_modules/a.ts": "", "/bower_components/b.ts": "", "/jspm_packages/c.ts": "", "/d.ts": "", "/folder/e.ts": ""}, }}, }, { title: "generates errors for empty files list", input: []testConfig{{ jsonText: `{ "files": [] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "generates errors for empty files list when no references are provided", input: []testConfig{{ jsonText: `{ "files": [], "references": [] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "generates errors for directory with no .ts files", input: []testConfig{{ jsonText: `{ }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.js": ""}, }}, }, { title: "generates errors for empty include", input: []testConfig{{ jsonText: `{ "include": [] }`, configFileName: "/apath/tsconfig.json", basePath: "tests/cases/unittests", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "parses tsconfig with compilerOptions, files, include, and exclude", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "compilerOptions": { "outDir": "./dist", "strict": true, "noImplicitAny": true, "target": "ES2017", "module": "ESNext", "moduleResolution": "bundler", "moduleDetection": "auto", "jsx": "react", "maxNodeModuleJsDepth": 1, "paths": { "jquery": ["./vendor/jquery/dist/jquery"] } }, "files": ["/apath/src/index.ts", "/apath/src/app.ts"], "include": ["/apath/src/**/*"], "exclude": ["/apath/node_modules", "/apath/dist"] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/src/index.ts": "", "/apath/src/app.ts": "", "/apath/node_modules/module.ts": "", "/apath/dist/output.js": ""}, }}, }, { title: "generates errors when commandline option is in tsconfig", input: []testConfig{{ jsonText: `{ "compilerOptions": { "help": true } }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "does not generate errors for empty files list when one or more references are provided", input: []testConfig{{ jsonText: `{ "files": [], "references": [{ "path": "/apath" }] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "exclude outDir unless overridden", input: []testConfig{{ jsonText: `{ "compilerOptions": { "outDir": "bin" } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/bin/a.ts": "", "/b.ts": ""}, }, { jsonText: `{ "compilerOptions": { "outDir": "bin" }, "exclude": [ "obj" ] }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/bin/a.ts": "", "/b.ts": ""}, }}, }, { title: "exclude declarationDir unless overridden", input: []testConfig{{ jsonText: `{ "compilerOptions": { "declarationDir": "declarations" } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/declarations/a.d.ts": "", "/a.ts": ""}, }, { jsonText: `{ "compilerOptions": { "declarationDir": "declarations" }, "exclude": [ "types" ] }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/declarations/a.d.ts": "", "/a.ts": ""}, }}, }, { title: "generates errors for empty directory", input: []testConfig{{ jsonText: `{ "compilerOptions": { "allowJs": true } }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{}, }}, }, { title: "generates errors for includes with outDir", input: []testConfig{{ jsonText: `{ "compilerOptions": { "outDir": "./" }, "include": ["**/*"] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "generates errors when include is not string", input: []testConfig{{ jsonText: `{ "include": [ [ "./**/*.ts" ] ] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "generates errors when files is not string", input: []testConfig{{ jsonText: `{ "files": [ { "compilerOptions": { "experimentalDecorators": true, "allowJs": true } } ] }`, configFileName: "/apath/tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/a.ts": ""}, }}, }, { title: "with outDir from base tsconfig", input: []testConfig{ { jsonText: `{ "extends": "./tsconfigWithoutConfigDir.json" }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfigWithoutConfigDir.json": tsconfigWithoutConfigDir, "/bin/a.ts": "", "/b.ts": "", }, }, { jsonText: `{ "extends": "./tsconfigWithConfigDir.json" }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfigWithConfigDir.json": tsconfigWithConfigDir, "/bin/a.ts": "", "/b.ts": "", }, }, }, }, { title: "returns error when tsconfig have excludes", input: []testConfig{{ jsonText: `{ "compilerOptions": { "lib": ["es5"] }, "excludes": [ "foge.ts" ] }`, configFileName: "tsconfig.json", basePath: "/apath", allFileList: map[string]string{"/apath/test.ts": "", "/apath/foge.ts": ""}, }}, }, { title: "parses tsconfig with extends, files, include and other options", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfigWithExtends.json", "compilerOptions": { "outDir": "./dist", "strict": true, "noImplicitAny": true, "baseUrl": "", }, }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/tsconfigWithExtends.json": tsconfigWithExtends, "/src/index.ts": "", "/src/app.ts": "", "/node_modules/module.ts": "", "/dist/output.js": ""}, }}, }, { title: "parses tsconfig with extends and configDir", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig.base.json" }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/tsconfig.base.json": tsconfigWithExtendsAndConfigDir, "/src/index.ts": "", "/src/app.ts": "", "/node_modules/module.ts": "", "/dist/output.js": ""}, }}, }, { title: "reports error for an unknown option", input: []testConfig{{ jsonText: `{ "compilerOptions": { "unknown": true } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/app.ts": ""}, }}, }, { title: "reports errors for wrong type option and invalid enum value", input: []testConfig{{ jsonText: `{ "compilerOptions": { "target": "invalid value", "removeComments": "should be a boolean", "moduleResolution": "invalid value" } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/app.ts": ""}, }}, }, { title: "handles empty types array", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "compilerOptions": { "types": [] } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{"/app.ts": ""}, }}, }, { title: "issue 1267 scenario - extended files not picked up", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-base/backend.json", "compilerOptions": { "baseUrl": "./", "outDir": "dist", "rootDir": "src", "resolveJsonModule": true }, "exclude": ["node_modules", "dist"], "include": ["src/**/*"] }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-base/backend.json": `{ "$schema": "https://json.schemastore.org/tsconfig", "display": "Backend", "compilerOptions": { "allowJs": true, "module": "nodenext", "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "esnext", "lib": ["ESNext"], "incremental": false, "esModuleInterop": true, "noImplicitAny": true, "moduleResolution": "nodenext", "types": ["node", "vitest/globals"], "sourceMap": true, "strictPropertyInitialization": false }, "files": [ "types/ical2json.d.ts", "types/express.d.ts", "types/multer.d.ts", "types/reset.d.ts", "types/stripe-custom-typings.d.ts", "types/nestjs-modules.d.ts", "types/luxon.d.ts", "types/nestjs-pino.d.ts" ], "ts-node": { "files": true } }`, "/tsconfig-base/types/ical2json.d.ts": "export {}", "/tsconfig-base/types/express.d.ts": "export {}", "/tsconfig-base/types/multer.d.ts": "export {}", "/tsconfig-base/types/reset.d.ts": "export {}", "/tsconfig-base/types/stripe-custom-typings.d.ts": "export {}", "/tsconfig-base/types/nestjs-modules.d.ts": "export {}", "/tsconfig-base/types/luxon.d.ts": `declare module 'luxon' { interface TSSettings { throwOnInvalid: true } } export {}`, "/tsconfig-base/types/nestjs-pino.d.ts": "export {}", "/src/main.ts": "export {}", "/src/utils.ts": "export {}", }, }}, }, { title: "null overrides in extended tsconfig - array fields", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-base.json", "compilerOptions": { "types": null, "lib": null, "typeRoots": null } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-base.json": `{ "compilerOptions": { "types": ["node", "@types/jest"], "lib": ["es2020", "dom"], "typeRoots": ["./types", "./node_modules/@types"] } }`, "/app.ts": "", }, }}, }, { title: "null overrides in extended tsconfig - string fields", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-base.json", "compilerOptions": { "outDir": null, "baseUrl": null, "rootDir": null } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-base.json": `{ "compilerOptions": { "outDir": "./dist", "baseUrl": "./src", "rootDir": "./src" } }`, "/app.ts": "", }, }}, }, { title: "null overrides in extended tsconfig - mixed field types", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-base.json", "compilerOptions": { "types": null, "outDir": null, "strict": false, "lib": ["es2022"], "allowJs": null } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-base.json": `{ "compilerOptions": { "types": ["node"], "lib": ["es2020", "dom"], "outDir": "./dist", "strict": true, "allowJs": true, "target": "es2020" } }`, "/app.ts": "", }, }}, }, { title: "null overrides with multiple extends levels", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-middle.json", "compilerOptions": { "types": null, "lib": null } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-middle.json": `{ "extends": "./tsconfig-base.json", "compilerOptions": { "types": ["jest"], "outDir": "./build" } }`, "/tsconfig-base.json": `{ "compilerOptions": { "types": ["node"], "lib": ["es2020"], "outDir": "./dist", "strict": true } }`, "/app.ts": "", }, }}, }, { title: "null overrides in middle level of extends chain", noSubmoduleBaseline: true, input: []testConfig{{ jsonText: `{ "extends": "./tsconfig-middle.json", "compilerOptions": { "outDir": "./final" } }`, configFileName: "tsconfig.json", basePath: "/", allFileList: map[string]string{ "/tsconfig-middle.json": `{ "extends": "./tsconfig-base.json", "compilerOptions": { "types": null, "lib": null, "outDir": "./middle" } }`, "/tsconfig-base.json": `{ "compilerOptions": { "types": ["node"], "lib": ["es2020"], "outDir": "./base", "strict": true } }`, "/app.ts": "", }, }}, }, } var tsconfigWithExtends = `{ "files": ["/src/index.ts", "/src/app.ts"], "include": ["/src/**/*"], "exclude": [], "ts-node": { "compilerOptions": { "module": "commonjs" }, "transpileOnly": true } }` var tsconfigWithoutConfigDir = `{ "compilerOptions": { "outDir": "bin" } }` var tsconfigWithConfigDir = `{ "compilerOptions": { "outDir": "${configDir}/bin" } }` var tsconfigWithExtendsAndConfigDir = `{ "compilerOptions": { "outFile": "${configDir}/outFile", "outDir": "${configDir}/outDir", "rootDir": "${configDir}/rootDir", "tsBuildInfoFile": "${configDir}/tsBuildInfoFile", "baseUrl": "${configDir}/baseUrl", "declarationDir": "${configDir}/declarationDir", } }` func TestParseJsonConfigFileContent(t *testing.T) { t.Parallel() repo.SkipIfNoTypeScriptSubmodule(t) for _, rec := range parseJsonConfigFileTests { t.Run(rec.title+" with json api", func(t *testing.T) { t.Parallel() baselineParseConfigWith(t, rec.title+" with json api.js", rec.noSubmoduleBaseline, rec.input, getParsedWithJsonApi) }) } } func getParsedWithJsonApi(config testConfig, host tsoptions.ParseConfigHost, basePath string) *tsoptions.ParsedCommandLine { configFileName := tspath.GetNormalizedAbsolutePath(config.configFileName, basePath) path := tspath.ToPath(config.configFileName, basePath, host.FS().UseCaseSensitiveFileNames()) parsed, _ := tsoptions.ParseConfigFileTextToJson(configFileName, path, config.jsonText) return tsoptions.ParseJsonConfigFileContent( parsed, host, basePath, nil, configFileName, /*resolutionStack*/ nil, /*extraFileExtensions*/ nil, /*extendedConfigCache*/ nil, ) } func TestParseJsonSourceFileConfigFileContent(t *testing.T) { t.Parallel() repo.SkipIfNoTypeScriptSubmodule(t) for _, rec := range parseJsonConfigFileTests { t.Run(rec.title+" with jsonSourceFile api", func(t *testing.T) { t.Parallel() baselineParseConfigWith(t, rec.title+" with jsonSourceFile api.js", rec.noSubmoduleBaseline, rec.input, getParsedWithJsonSourceFileApi) }) } } func getParsedWithJsonSourceFileApi(config testConfig, host tsoptions.ParseConfigHost, basePath string) *tsoptions.ParsedCommandLine { configFileName := tspath.GetNormalizedAbsolutePath(config.configFileName, basePath) path := tspath.ToPath(config.configFileName, basePath, host.FS().UseCaseSensitiveFileNames()) parsed := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: configFileName, Path: path, }, config.jsonText, core.ScriptKindJSON) tsConfigSourceFile := &tsoptions.TsConfigSourceFile{ SourceFile: parsed, } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, host, host.GetCurrentDirectory(), nil, configFileName, /*resolutionStack*/ nil, /*extraFileExtensions*/ nil, /*extendedConfigCache*/ nil, ) } func baselineParseConfigWith(t *testing.T, baselineFileName string, noSubmoduleBaseline bool, input []testConfig, getParsed func(config testConfig, host tsoptions.ParseConfigHost, basePath string) *tsoptions.ParsedCommandLine) { noSubmoduleBaseline = true var baselineContent strings.Builder for i, config := range input { basePath := config.basePath if basePath == "" { basePath = tspath.GetNormalizedAbsolutePath(tspath.GetDirectoryPath(config.configFileName), "") } configFileName := tspath.CombinePaths(basePath, config.configFileName) allFileLists := make(map[string]string, len(config.allFileList)+1) for file, content := range config.allFileList { allFileLists[file] = content } allFileLists[configFileName] = config.jsonText host := tsoptionstest.NewVFSParseConfigHost(allFileLists, config.basePath, true /*useCaseSensitiveFileNames*/) parsedConfigFileContent := getParsed(config, host, basePath) baselineContent.WriteString("Fs::\n") if err := printFS(&baselineContent, host.FS(), "/"); err != nil { t.Fatal(err) } baselineContent.WriteString("\n") baselineContent.WriteString("configFileName:: " + config.configFileName + "\n") if noSubmoduleBaseline { baselineContent.WriteString("CompilerOptions::\n") assert.NilError(t, jsonutil.MarshalIndentWrite(&baselineContent, parsedConfigFileContent.ParsedConfig.CompilerOptions, "", " ")) baselineContent.WriteString("\n") baselineContent.WriteString("\n") if parsedConfigFileContent.ParsedConfig.TypeAcquisition != nil { baselineContent.WriteString("TypeAcquisition::\n") assert.NilError(t, jsonutil.MarshalIndentWrite(&baselineContent, parsedConfigFileContent.ParsedConfig.TypeAcquisition, "", " ")) baselineContent.WriteString("\n") baselineContent.WriteString("\n") } } baselineContent.WriteString("FileNames::\n") baselineContent.WriteString(strings.Join(parsedConfigFileContent.ParsedConfig.FileNames, ",") + "\n") baselineContent.WriteString("Errors::\n") diagnosticwriter.FormatDiagnosticsWithColorAndContext(&baselineContent, parsedConfigFileContent.Errors, &diagnosticwriter.FormattingOptions{ NewLine: "\r\n", ComparePathsOptions: tspath.ComparePathsOptions{ CurrentDirectory: basePath, UseCaseSensitiveFileNames: true, }, }) baselineContent.WriteString("\n") if i != len(input)-1 { baselineContent.WriteString("\n") } } if noSubmoduleBaseline { baseline.Run(t, baselineFileName, baselineContent.String(), baseline.Options{Subfolder: "config/tsconfigParsing"}) } else { baseline.RunAgainstSubmodule(t, baselineFileName, baselineContent.String(), baseline.Options{Subfolder: "config/tsconfigParsing"}) } } func writeJsonReadableText(output io.Writer, input any) error { return jsonutil.MarshalIndentWrite(output, input, "", " ") } func TestParseTypeAcquisition(t *testing.T) { t.Parallel() // repo.SkipIfNoTypeScriptSubmodule(t) cases := []struct { title string configName string config string }{ { title: "Convert correctly format tsconfig.json to typeAcquisition ", config: `{ "typeAcquisition": { "enable": true, "include": ["0.d.ts", "1.d.ts"], "exclude": ["0.js", "1.js"], }, }`, configName: "tsconfig.json", }, { title: "Convert incorrect format tsconfig.json to typeAcquisition ", config: `{ "typeAcquisition": { "enableAutoDiscovy": true, } }`, configName: "tsconfig.json", }, { title: "Convert default tsconfig.json to typeAcquisition ", config: `{}`, configName: "tsconfig.json", }, { title: "Convert tsconfig.json with only enable property to typeAcquisition ", config: `{ "typeAcquisition": { "enable": true, }, }`, configName: "tsconfig.json", }, // jsconfig.json { title: "Convert jsconfig.json to typeAcquisition ", config: `{ "typeAcquisition": { "enable": false, "include": ["0.d.ts"], "exclude": ["0.js"], }, }`, configName: "jsconfig.json", }, {title: "Convert default jsconfig.json to typeAcquisition ", config: `{}`, configName: "jsconfig.json"}, { title: "Convert incorrect format jsconfig.json to typeAcquisition ", config: `{ "typeAcquisition": { "enableAutoDiscovy": true, }, }`, configName: "jsconfig.json", }, { title: "Convert jsconfig.json with only enable property to typeAcquisition ", config: `{ "typeAcquisition": { "enable": false, }, }`, configName: "jsconfig.json", }, } for _, test := range cases { withJsonApiName := test.title + " with json api" input := []testConfig{ { jsonText: test.config, configFileName: test.configName, basePath: "/apath", allFileList: map[string]string{ "/apath/a.ts": "", "/apath/b.ts": "", }, }, } t.Run(withJsonApiName, func(t *testing.T) { t.Parallel() baselineParseConfigWith(t, withJsonApiName+".js", true, input, getParsedWithJsonApi) }) withJsonSourceFileApiName := test.title + " with jsonSourceFile api" t.Run(withJsonSourceFileApiName, func(t *testing.T) { t.Parallel() baselineParseConfigWith(t, withJsonSourceFileApiName+".js", true, input, getParsedWithJsonSourceFileApi) }) } } func printFS(output io.Writer, files vfs.FS, root string) error { return files.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type().IsRegular() { if content, ok := files.ReadFile(path); !ok { return fmt.Errorf("failed to read file %s", path) } else { if _, err := fmt.Fprintf(output, "//// [%s]\r\n%s\r\n\r\n", path, content); err != nil { return err } } } return nil }) } func TestParseSrcCompiler(t *testing.T) { t.Parallel() repo.SkipIfNoTypeScriptSubmodule(t) compilerDir := tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath, "src", "compiler")) tsconfigFileName := tspath.CombinePaths(compilerDir, "tsconfig.json") fs := osvfs.FS() host := &tsoptionstest.VfsParseConfigHost{ Vfs: fs, CurrentDirectory: compilerDir, } jsonText, ok := fs.ReadFile(tsconfigFileName) assert.Assert(t, ok) tsconfigPath := tspath.ToPath(tsconfigFileName, compilerDir, fs.UseCaseSensitiveFileNames()) parsed := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: tsconfigFileName, Path: tsconfigPath, }, jsonText, core.ScriptKindJSON) if len(parsed.Diagnostics()) > 0 { for _, error := range parsed.Diagnostics() { t.Log(error.Message()) } t.FailNow() } tsConfigSourceFile := &tsoptions.TsConfigSourceFile{ SourceFile: parsed, } parseConfigFileContent := tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, host, host.GetCurrentDirectory(), nil, tsconfigFileName, /*resolutionStack*/ nil, /*extraFileExtensions*/ nil, /*extendedConfigCache*/ nil, ) if len(parseConfigFileContent.Errors) > 0 { for _, error := range parseConfigFileContent.Errors { t.Log(error.Message()) } t.FailNow() } opts := parseConfigFileContent.CompilerOptions() assert.DeepEqual(t, opts, &core.CompilerOptions{ Lib: []string{"lib.es2020.d.ts"}, Module: core.ModuleKindNodeNext, ModuleResolution: core.ModuleResolutionKindNodeNext, NewLine: core.NewLineKindLF, OutDir: tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath, "built", "local")), Target: core.ScriptTargetES2020, Types: []string{"node"}, ConfigFilePath: tsconfigFileName, Declaration: core.TSTrue, DeclarationMap: core.TSTrue, EmitDeclarationOnly: core.TSTrue, AlwaysStrict: core.TSTrue, Composite: core.TSTrue, IsolatedDeclarations: core.TSTrue, NoImplicitOverride: core.TSTrue, PreserveConstEnums: core.TSTrue, RootDir: tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath, "src")), SkipLibCheck: core.TSTrue, Strict: core.TSTrue, StrictBindCallApply: core.TSFalse, SourceMap: core.TSTrue, UseUnknownInCatchVariables: core.TSFalse, Pretty: core.TSTrue, }, cmpopts.IgnoreUnexported(core.CompilerOptions{})) fileNames := parseConfigFileContent.ParsedConfig.FileNames relativePaths := make([]string, 0, len(fileNames)) for _, fileName := range fileNames { if strings.Contains(fileName, ".generated.") { continue } relativePaths = append(relativePaths, tspath.ConvertToRelativePath(fileName, tspath.ComparePathsOptions{ CurrentDirectory: compilerDir, UseCaseSensitiveFileNames: fs.UseCaseSensitiveFileNames(), })) } assert.DeepEqual(t, relativePaths, []string{ "binder.ts", "builder.ts", "builderPublic.ts", "builderState.ts", "builderStatePublic.ts", "checker.ts", "commandLineParser.ts", "core.ts", "corePublic.ts", "debug.ts", "emitter.ts", "executeCommandLine.ts", "expressionToTypeNode.ts", "moduleNameResolver.ts", "moduleSpecifiers.ts", "parser.ts", "path.ts", "performance.ts", "performanceCore.ts", "program.ts", "programDiagnostics.ts", "resolutionCache.ts", "scanner.ts", "semver.ts", "sourcemap.ts", "symbolWalker.ts", "sys.ts", "tracing.ts", "transformer.ts", "tsbuild.ts", "tsbuildPublic.ts", "types.ts", "utilities.ts", "utilitiesPublic.ts", "visitorPublic.ts", "watch.ts", "watchPublic.ts", "watchUtilities.ts", "_namespaces/ts.moduleSpecifiers.ts", "_namespaces/ts.performance.ts", "_namespaces/ts.ts", "factory/baseNodeFactory.ts", "factory/emitHelpers.ts", "factory/emitNode.ts", "factory/nodeChildren.ts", "factory/nodeConverters.ts", "factory/nodeFactory.ts", "factory/nodeTests.ts", "factory/parenthesizerRules.ts", "factory/utilities.ts", "factory/utilitiesPublic.ts", "transformers/classFields.ts", "transformers/classThis.ts", "transformers/declarations.ts", "transformers/destructuring.ts", "transformers/es2016.ts", "transformers/es2017.ts", "transformers/es2018.ts", "transformers/es2019.ts", "transformers/es2020.ts", "transformers/es2021.ts", "transformers/esDecorators.ts", "transformers/esnext.ts", "transformers/jsx.ts", "transformers/legacyDecorators.ts", "transformers/namedEvaluation.ts", "transformers/taggedTemplate.ts", "transformers/ts.ts", "transformers/typeSerializer.ts", "transformers/utilities.ts", "transformers/declarations/diagnostics.ts", "transformers/module/esnextAnd2015.ts", "transformers/module/impliedNodeFormatDependent.ts", "transformers/module/module.ts", "transformers/module/system.ts", }) } func BenchmarkParseSrcCompiler(b *testing.B) { repo.SkipIfNoTypeScriptSubmodule(b) compilerDir := tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath, "src", "compiler")) tsconfigFileName := tspath.CombinePaths(compilerDir, "tsconfig.json") fs := osvfs.FS() host := &tsoptionstest.VfsParseConfigHost{ Vfs: fs, CurrentDirectory: compilerDir, } jsonText, ok := fs.ReadFile(tsconfigFileName) assert.Assert(b, ok) tsconfigPath := tspath.ToPath(tsconfigFileName, compilerDir, fs.UseCaseSensitiveFileNames()) parsed := parser.ParseSourceFile(ast.SourceFileParseOptions{ FileName: tsconfigFileName, Path: tsconfigPath, }, jsonText, core.ScriptKindJSON) b.ReportAllocs() for b.Loop() { tsoptions.ParseJsonSourceFileConfigFileContent( &tsoptions.TsConfigSourceFile{ SourceFile: parsed, }, host, host.GetCurrentDirectory(), nil, tsconfigFileName, /*resolutionStack*/ nil, /*extraFileExtensions*/ nil, /*extendedConfigCache*/ nil, ) } } // memoCache is a minimal memoizing ExtendedConfigCache used by tests to simulate // cache hits across multiple parses of configs that extend a common base. type memoCache struct { m map[tspath.Path]*tsoptions.ExtendedConfigCacheEntry } func (mc *memoCache) GetExtendedConfig(fileName string, path tspath.Path, parse func() *tsoptions.ExtendedConfigCacheEntry) *tsoptions.ExtendedConfigCacheEntry { if mc.m == nil { mc.m = make(map[tspath.Path]*tsoptions.ExtendedConfigCacheEntry) } if e, ok := mc.m[path]; ok { return e } e := parse() mc.m[path] = e return e } var _ tsoptions.ExtendedConfigCache = (*memoCache)(nil) // TestExtendedConfigErrorsAppearOnCacheHit verifies that diagnostics produced while parsing an // extended config are still reported when the extended config comes from the cache. func TestExtendedConfigErrorsAppearOnCacheHit(t *testing.T) { t.Parallel() t.Run("single config parsed twice", func(t *testing.T) { t.Parallel() files := map[string]string{ "/tsconfig.json": `{ "extends": "./base.json" }`, // 'excludes' instead of 'exclude' triggers diagnostic "/base.json": `{ "excludes": ["**/*.ts"] }`, "/app.ts": "export {}", } host := tsoptionstest.NewVFSParseConfigHost(files, "/", true /*useCaseSensitiveFileNames*/) parseConfig := func(configFileName string, cache tsoptions.ExtendedConfigCache) *tsoptions.ParsedCommandLine { cfgPath := tspath.ToPath(configFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) jsonText, ok := host.FS().ReadFile(configFileName) assert.Assert(t, ok, "missing %s in test fs", configFileName) tsConfigSourceFile := &tsoptions.TsConfigSourceFile{ SourceFile: parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: configFileName, Path: cfgPath}, jsonText, core.ScriptKindJSON), } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, host, host.GetCurrentDirectory(), nil, configFileName, nil, nil, cache, ) } cache := &memoCache{} first := parseConfig("/tsconfig.json", cache) assert.Assert(t, len(first.Errors) > 0, "expected diagnostics on first parse, got 0") second := parseConfig("/tsconfig.json", cache) assert.Assert(t, len(second.Errors) > 0, "expected diagnostics on second parse (cache hit), got 0") }) t.Run("two configs share same base", func(t *testing.T) { t.Parallel() files := map[string]string{ "/base.json": `{ "excludes": ["**/*.ts"] }`, "/projA/tsconfig.json": `{ "extends": "../base.json" }`, "/projB/tsconfig.json": `{ "extends": "../base.json" }`, "/projA/app.ts": "export {}", "/projB/app.ts": "export {}", } host := tsoptionstest.NewVFSParseConfigHost(files, "/", true /*useCaseSensitiveFileNames*/) parseConfig := func(configFileName string, cache tsoptions.ExtendedConfigCache) *tsoptions.ParsedCommandLine { cfgPath := tspath.ToPath(configFileName, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) jsonText, ok := host.FS().ReadFile(configFileName) assert.Assert(t, ok, "missing %s in test fs", configFileName) tsConfigSourceFile := &tsoptions.TsConfigSourceFile{ SourceFile: parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: configFileName, Path: cfgPath}, jsonText, core.ScriptKindJSON), } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, host, host.GetCurrentDirectory(), nil, configFileName, nil, nil, cache, ) } cache := &memoCache{} first := parseConfig("/projA/tsconfig.json", cache) assert.Assert(t, len(first.Errors) > 0, "expected diagnostics for projA parse, got 0") second := parseConfig("/projB/tsconfig.json", cache) assert.Assert(t, len(second.Errors) > 0, "expected diagnostics for projB parse (cache hit on base), got 0") }) }