diff --git a/cmd/rslint/api.go b/cmd/rslint/api.go index 0c88d3fa..7aa45e75 100644 --- a/cmd/rslint/api.go +++ b/cmd/rslint/api.go @@ -185,7 +185,7 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error) // Create programs from all tsconfig files found in rslint config programs := []*compiler.Program{} for _, configFileName := range tsConfigs { - program, err := utils.CreateProgram(false, fs, configDirectory, configFileName, host) + program, err := utils.CreateProgramWithOverrides(false, fs, configDirectory, configFileName, host, req.CompilerOptions) if err != nil { return nil, fmt.Errorf("error creating TS program for %s: %w", configFileName, err) } diff --git a/docs/api-compiler-options.md b/docs/api-compiler-options.md new file mode 100644 index 00000000..7a7f0287 --- /dev/null +++ b/docs/api-compiler-options.md @@ -0,0 +1,86 @@ +# Compiler Options Override Support + +The API mode now supports passing `compilerOptions` to override settings from the TypeScript configuration file. + +## Usage + +When making a lint request via the API, you can now include a `compilerOptions` field that will override any corresponding options in your `tsconfig.json`: + +```json +{ + "kind": "lint", + "id": 1, + "data": { + "files": ["src/**/*.ts"], + "config": "./rslint.json", + "compilerOptions": { + "strict": true, + "target": 7, + "noImplicitAny": true, + "skipLibCheck": true + }, + "ruleOptions": { + "no-unused-vars": "error" + } + } +} +``` + +## Notes + +- The `compilerOptions` field accepts a map of option name to value +- These options will override any corresponding options from your `tsconfig.json` +- Numeric options (like `target`) should be passed as numbers corresponding to the TypeScript enum values +- Boolean options can be passed as `true`/`false` +- String options should be passed as strings +- Array options should be passed as arrays + +## TypeScript Target Values + +Common target values: + +- `1` = ES3 +- `2` = ES5 +- `3` = ES2015 (ES6) +- `4` = ES2016 +- `5` = ES2017 +- `6` = ES2018 +- `7` = ES2019 +- `8` = ES2020 +- `9` = ES2021 +- `10` = ES2022 +- `99` = ESNext + +## Module Values + +Common module values: + +- `0` = None +- `1` = CommonJS +- `2` = AMD +- `3` = UMD +- `4` = System +- `5` = ES2015 (ES6) +- `6` = ES2020 +- `7` = ES2022 +- `99` = ESNext +- `100` = Node16 +- `199` = NodeNext + +## Example + +```json +{ + "compilerOptions": { + "target": 8, + "module": 1, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2020", "DOM"] + } +} +``` + +This will override the corresponding settings in your `tsconfig.json` for the duration of the linting session. diff --git a/internal/api/api.go b/internal/api/api.go index ee609cdd..fcf798b1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -60,8 +60,9 @@ type LintRequest struct { Format string `json:"format,omitempty"` WorkingDirectory string `json:"workingDirectory,omitempty"` // Supports both string level and array [level, options] format - RuleOptions map[string]interface{} `json:"ruleOptions,omitempty"` - FileContents map[string]string `json:"fileContents,omitempty"` // Map of file paths to their contents for VFS + RuleOptions map[string]interface{} `json:"ruleOptions,omitempty"` + FileContents map[string]string `json:"fileContents,omitempty"` // Map of file paths to their contents for VFS + CompilerOptions map[string]interface{} `json:"compilerOptions,omitempty"` // TypeScript compiler options to override config file settings } // LintResponse represents a lint response from Go to JS diff --git a/internal/utils/create_program.go b/internal/utils/create_program.go index b7d07fac..3112d0f4 100644 --- a/internal/utils/create_program.go +++ b/internal/utils/create_program.go @@ -21,12 +21,26 @@ func CreateCompilerHost(cwd string, fs vfs.FS) compiler.CompilerHost { } func CreateProgram(singleThreaded bool, fs vfs.FS, cwd string, tsconfigPath string, host compiler.CompilerHost) (*compiler.Program, error) { + return CreateProgramWithOverrides(singleThreaded, fs, cwd, tsconfigPath, host, nil) +} + +func CreateProgramWithOverrides(singleThreaded bool, fs vfs.FS, cwd string, tsconfigPath string, host compiler.CompilerHost, compilerOptionsOverrides map[string]interface{}) (*compiler.Program, error) { resolvedConfigPath := tspath.ResolvePath(cwd, tsconfigPath) if !fs.FileExists(resolvedConfigPath) { return nil, fmt.Errorf("couldn't read tsconfig at %v", resolvedConfigPath) } - configParseResult, _ := tsoptions.GetParsedCommandLineOfConfigFile(tsconfigPath, &core.CompilerOptions{}, host, nil) + // Start with empty existing options, we'll apply overrides later + existingOptions := &core.CompilerOptions{} + + // Apply compiler options overrides if provided + if compilerOptionsOverrides != nil { + for key, value := range compilerOptionsOverrides { + tsoptions.ParseCompilerOptions(key, value, existingOptions) + } + } + + configParseResult, _ := tsoptions.GetParsedCommandLineOfConfigFile(tsconfigPath, existingOptions, host, nil) opts := compiler.ProgramOptions{ Config: configParseResult, diff --git a/internal/utils/create_program_test.go b/internal/utils/create_program_test.go new file mode 100644 index 00000000..7ba47373 --- /dev/null +++ b/internal/utils/create_program_test.go @@ -0,0 +1,84 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/microsoft/typescript-go/shim/bundled" + "github.com/microsoft/typescript-go/shim/core" + "github.com/microsoft/typescript-go/shim/vfs/cachedvfs" + "github.com/microsoft/typescript-go/shim/vfs/osvfs" +) + +func TestCreateProgramWithOverrides(t *testing.T) { + // Create a temporary directory for our test + tempDir, err := os.MkdirTemp("", "rslint-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a basic tsconfig.json + tsconfigContent := `{ + "compilerOptions": { + "target": "ES2015", + "strict": false, + "noImplicitAny": false + } +}` + tsconfigPath := filepath.Join(tempDir, "tsconfig.json") + if err := os.WriteFile(tsconfigPath, []byte(tsconfigContent), 0644); err != nil { + t.Fatalf("Failed to write tsconfig.json: %v", err) + } + + // Create a simple TypeScript file + tsFileContent := `const x: any = 42;` + tsFilePath := filepath.Join(tempDir, "test.ts") + if err := os.WriteFile(tsFilePath, []byte(tsFileContent), 0644); err != nil { + t.Fatalf("Failed to write test.ts: %v", err) + } + + // Create compiler host with proper bundled FS for TypeScript libs + fs := bundled.WrapFS(cachedvfs.From(osvfs.FS())) + host := CreateCompilerHost(tempDir, fs) + + // Test 1: Create program without overrides + program1, err := CreateProgram(false, fs, tempDir, tsconfigPath, host) + if err != nil { + t.Fatalf("Failed to create program without overrides: %v", err) + } + + // Verify original config settings + options1 := program1.Options() + if options1.Target != core.ScriptTargetES2015 { + t.Errorf("Expected target ES2015, got %v", options1.Target) + } + if options1.Strict != core.TSFalse { + t.Errorf("Expected strict false, got %v", options1.Strict) + } + + // Test 2: Create program with overrides + overrides := map[string]interface{}{ + "target": float64(core.ScriptTargetES2020), // TypeScript expects numeric enum values + "strict": true, + "noImplicitAny": true, + } + + program2, err := CreateProgramWithOverrides(false, fs, tempDir, tsconfigPath, host, overrides) + if err != nil { + t.Fatalf("Failed to create program with overrides: %v", err) + } + + // Verify overrides applied + options2 := program2.Options() + if options2.Target != core.ScriptTargetES2020 { + t.Errorf("Expected target ES2020 after override, got %v", options2.Target) + } + if options2.Strict != core.TSTrue { + t.Errorf("Expected strict true after override, got %v", options2.Strict) + } + if options2.NoImplicitAny != core.TSTrue { + t.Errorf("Expected noImplicitAny true after override, got %v", options2.NoImplicitAny) + } +}