@ttsc/lint 0.7.2 → 0.8.0-dev.20260505

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/plugin/config.go CHANGED
@@ -1,33 +1,33 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "encoding/json"
5
- "fmt"
6
- "os"
7
- "os/exec"
8
- "path/filepath"
9
- "strings"
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
10
  )
11
11
 
12
12
  // Severity is the `error | warning | off` ladder.
13
13
  type Severity int
14
14
 
15
15
  const (
16
- SeverityOff Severity = iota
17
- SeverityWarn
18
- SeverityError
16
+ SeverityOff Severity = iota
17
+ SeverityWarn
18
+ SeverityError
19
19
  )
20
20
 
21
21
  func (s Severity) String() string {
22
- switch s {
23
- case SeverityError:
24
- return "error"
25
- case SeverityWarn:
26
- return "warning"
27
- case SeverityOff:
28
- return "off"
29
- }
30
- return "unknown"
22
+ switch s {
23
+ case SeverityError:
24
+ return "error"
25
+ case SeverityWarn:
26
+ return "warning"
27
+ case SeverityOff:
28
+ return "off"
29
+ }
30
+ return "unknown"
31
31
  }
32
32
 
33
33
  // PluginEntry mirrors the shape ttsc serializes into `--plugins-json`.
@@ -35,36 +35,36 @@ func (s Severity) String() string {
35
35
  // `Config` carries the original tsconfig plugin entry. `Name` and `Stage`
36
36
  // come from the JS plugin descriptor returned to the ttsc host.
37
37
  type PluginEntry struct {
38
- Config map[string]any `json:"config"`
39
- Name string `json:"name"`
40
- Stage string `json:"stage"`
38
+ Config map[string]any `json:"config"`
39
+ Name string `json:"name"`
40
+ Stage string `json:"stage"`
41
41
  }
42
42
 
43
43
  // ParsePlugins decodes the `--plugins-json` payload.
44
44
  func ParsePlugins(text string) ([]PluginEntry, error) {
45
- if strings.TrimSpace(text) == "" {
46
- return nil, nil
47
- }
48
- var entries []PluginEntry
49
- if err := json.Unmarshal([]byte(text), &entries); err != nil {
50
- return nil, fmt.Errorf("@ttsc/lint: invalid --plugins-json: %w", err)
51
- }
52
- return entries, nil
45
+ if strings.TrimSpace(text) == "" {
46
+ return nil, nil
47
+ }
48
+ var entries []PluginEntry
49
+ if err := json.Unmarshal([]byte(text), &entries); err != nil {
50
+ return nil, fmt.Errorf("@ttsc/lint: invalid --plugins-json: %w", err)
51
+ }
52
+ return entries, nil
53
53
  }
54
54
 
55
55
  // FindLintEntry returns the lint entry only when it is the first active
56
56
  // plugin. Linting after a source-transforming plugin would inspect mutated
57
57
  // source, which is not a meaningful user-code lint result.
58
58
  func FindLintEntry(entries []PluginEntry) (*PluginEntry, error) {
59
- for i := range entries {
60
- if entries[i].Name == "@ttsc/lint" {
61
- if i != 0 {
62
- return nil, fmt.Errorf("@ttsc/lint must be the first active compilerOptions.plugins entry")
63
- }
64
- return &entries[i], nil
65
- }
66
- }
67
- return nil, nil
59
+ for i := range entries {
60
+ if entries[i].Name == "@ttsc/lint" {
61
+ if i != 0 {
62
+ return nil, fmt.Errorf("@ttsc/lint must be the first active compilerOptions.plugins entry")
63
+ }
64
+ return &entries[i], nil
65
+ }
66
+ }
67
+ return nil, nil
68
68
  }
69
69
 
70
70
  // RuleConfig captures the resolved per-rule severity. The map is keyed by
@@ -81,22 +81,22 @@ type RuleConfig map[string]Severity
81
81
  // Anything else returns an error (no silent fallback — typos in a rule
82
82
  // severity should be loud).
83
83
  func ParseRules(raw any) (RuleConfig, error) {
84
- if raw == nil {
85
- return RuleConfig{}, nil
86
- }
87
- dict, ok := raw.(map[string]any)
88
- if !ok {
89
- return nil, fmt.Errorf("@ttsc/lint: \"config\" must be an object, got %T", raw)
90
- }
91
- out := make(RuleConfig, len(dict))
92
- for name, value := range dict {
93
- sev, err := parseSeverity(value)
94
- if err != nil {
95
- return nil, fmt.Errorf("@ttsc/lint: rule %q: %w", name, err)
96
- }
97
- out[name] = sev
98
- }
99
- return out, nil
84
+ if raw == nil {
85
+ return RuleConfig{}, nil
86
+ }
87
+ dict, ok := raw.(map[string]any)
88
+ if !ok {
89
+ return nil, fmt.Errorf("@ttsc/lint: \"config\" must be an object, got %T", raw)
90
+ }
91
+ out := make(RuleConfig, len(dict))
92
+ for name, value := range dict {
93
+ sev, err := parseSeverity(value)
94
+ if err != nil {
95
+ return nil, fmt.Errorf("@ttsc/lint: rule %q: %w", name, err)
96
+ }
97
+ out[name] = sev
98
+ }
99
+ return out, nil
100
100
  }
101
101
 
102
102
  // LoadRuleConfig resolves the lint config for one plugin entry. The only
@@ -104,86 +104,86 @@ func ParseRules(raw any) (RuleConfig, error) {
104
104
  // rules object or a string path to a standalone config file. Relative config
105
105
  // paths are resolved from the tsconfig directory.
106
106
  func LoadRuleConfig(entry *PluginEntry, cwd, tsconfigPath string) (RuleConfig, error) {
107
- if entry == nil {
108
- return RuleConfig{}, nil
109
- }
110
- inline := entry.Config
111
- if inline == nil {
112
- inline = map[string]any{}
113
- }
114
- for _, key := range []string{"rules", "configFile", "configPath"} {
115
- if _, ok := inline[key]; ok {
116
- return nil, fmt.Errorf("@ttsc/lint: %q is not supported; use \"config\"", key)
117
- }
118
- }
119
-
120
- value, ok := inline["config"]
121
- if !ok {
122
- return RuleConfig{}, nil
123
- }
124
- switch typed := value.(type) {
125
- case string:
126
- if strings.TrimSpace(typed) == "" {
127
- return nil, fmt.Errorf("@ttsc/lint: \"config\" must not be empty")
128
- }
129
- rules, err := loadConfigFile(resolveConfigFilePath(typed, cwd, tsconfigPath))
130
- if err != nil {
131
- return nil, err
132
- }
133
- return ParseRules(rules)
134
- case map[string]any:
135
- return ParseRules(typed)
136
- default:
137
- return nil, fmt.Errorf("@ttsc/lint: \"config\" must be a string path or object, got %T", value)
138
- }
107
+ if entry == nil {
108
+ return RuleConfig{}, nil
109
+ }
110
+ inline := entry.Config
111
+ if inline == nil {
112
+ inline = map[string]any{}
113
+ }
114
+ for _, key := range []string{"rules", "configFile", "configPath"} {
115
+ if _, ok := inline[key]; ok {
116
+ return nil, fmt.Errorf("@ttsc/lint: %q is not supported; use \"config\"", key)
117
+ }
118
+ }
119
+
120
+ value, ok := inline["config"]
121
+ if !ok {
122
+ return RuleConfig{}, nil
123
+ }
124
+ switch typed := value.(type) {
125
+ case string:
126
+ if strings.TrimSpace(typed) == "" {
127
+ return nil, fmt.Errorf("@ttsc/lint: \"config\" must not be empty")
128
+ }
129
+ rules, err := loadConfigFile(resolveConfigFilePath(typed, cwd, tsconfigPath))
130
+ if err != nil {
131
+ return nil, err
132
+ }
133
+ return ParseRules(rules)
134
+ case map[string]any:
135
+ return ParseRules(typed)
136
+ default:
137
+ return nil, fmt.Errorf("@ttsc/lint: \"config\" must be a string path or object, got %T", value)
138
+ }
139
139
  }
140
140
 
141
141
  func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
142
- if filepath.IsAbs(configPath) {
143
- return configPath
144
- }
145
- base := cwd
146
- if tsconfigPath != "" {
147
- resolvedTsconfig := tsconfigPath
148
- if !filepath.IsAbs(resolvedTsconfig) {
149
- resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
150
- }
151
- base = filepath.Dir(resolvedTsconfig)
152
- }
153
- return filepath.Join(base, configPath)
142
+ if filepath.IsAbs(configPath) {
143
+ return configPath
144
+ }
145
+ base := cwd
146
+ if tsconfigPath != "" {
147
+ resolvedTsconfig := tsconfigPath
148
+ if !filepath.IsAbs(resolvedTsconfig) {
149
+ resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
150
+ }
151
+ base = filepath.Dir(resolvedTsconfig)
152
+ }
153
+ return filepath.Join(base, configPath)
154
154
  }
155
155
 
156
156
  func loadConfigFile(location string) (map[string]any, error) {
157
- ext := strings.ToLower(filepath.Ext(location))
158
- switch ext {
159
- case ".json":
160
- return loadJSONConfigFile(location)
161
- case ".js", ".cjs", ".mjs":
162
- return loadScriptConfigFile(location)
163
- case ".ts", ".cts", ".mts":
164
- return loadTypeScriptConfigFile(location)
165
- default:
166
- return nil, fmt.Errorf("@ttsc/lint: unsupported config file extension %q for %s", ext, location)
167
- }
157
+ ext := strings.ToLower(filepath.Ext(location))
158
+ switch ext {
159
+ case ".json":
160
+ return loadJSONConfigFile(location)
161
+ case ".js", ".cjs", ".mjs":
162
+ return loadScriptConfigFile(location)
163
+ case ".ts", ".cts", ".mts":
164
+ return loadTypeScriptConfigFile(location)
165
+ default:
166
+ return nil, fmt.Errorf("@ttsc/lint: unsupported config file extension %q for %s", ext, location)
167
+ }
168
168
  }
169
169
 
170
170
  func loadJSONConfigFile(location string) (map[string]any, error) {
171
- body, err := os.ReadFile(location)
172
- if err != nil {
173
- return nil, fmt.Errorf("@ttsc/lint: read config file %s: %w", location, err)
174
- }
175
- var out map[string]any
176
- if err := json.Unmarshal(body, &out); err != nil {
177
- return nil, fmt.Errorf("@ttsc/lint: parse config file %s: %w", location, err)
178
- }
179
- if out == nil {
180
- return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
181
- }
182
- return out, nil
171
+ body, err := os.ReadFile(location)
172
+ if err != nil {
173
+ return nil, fmt.Errorf("@ttsc/lint: read config file %s: %w", location, err)
174
+ }
175
+ var out map[string]any
176
+ if err := json.Unmarshal(body, &out); err != nil {
177
+ return nil, fmt.Errorf("@ttsc/lint: parse config file %s: %w", location, err)
178
+ }
179
+ if out == nil {
180
+ return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
181
+ }
182
+ return out, nil
183
183
  }
184
184
 
185
185
  func loadScriptConfigFile(location string) (map[string]any, error) {
186
- const script = `
186
+ const script = `
187
187
  const { pathToFileURL } = require("node:url");
188
188
 
189
189
  (async () => {
@@ -199,107 +199,107 @@ const { pathToFileURL } = require("node:url");
199
199
  process.exit(1);
200
200
  });
201
201
  `
202
- node := os.Getenv("TTSC_NODE_BINARY")
203
- if node == "" {
204
- node = "node"
205
- }
206
- cmd := exec.Command(node, "-e", script, location)
207
- output, err := cmd.Output()
208
- if err != nil {
209
- stderr := ""
210
- if exit, ok := err.(*exec.ExitError); ok {
211
- stderr = strings.TrimSpace(string(exit.Stderr))
212
- }
213
- if stderr != "" {
214
- return nil, fmt.Errorf("@ttsc/lint: load config file %s: %s", location, stderr)
215
- }
216
- return nil, fmt.Errorf("@ttsc/lint: load config file %s: %w", location, err)
217
- }
218
- var out map[string]any
219
- if err := json.Unmarshal(output, &out); err != nil {
220
- return nil, fmt.Errorf("@ttsc/lint: parse config file %s output: %w", location, err)
221
- }
222
- if out == nil {
223
- return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
224
- }
225
- return out, nil
202
+ node := os.Getenv("TTSC_NODE_BINARY")
203
+ if node == "" {
204
+ node = "node"
205
+ }
206
+ cmd := exec.Command(node, "-e", script, location)
207
+ output, err := cmd.Output()
208
+ if err != nil {
209
+ stderr := ""
210
+ if exit, ok := err.(*exec.ExitError); ok {
211
+ stderr = strings.TrimSpace(string(exit.Stderr))
212
+ }
213
+ if stderr != "" {
214
+ return nil, fmt.Errorf("@ttsc/lint: load config file %s: %s", location, stderr)
215
+ }
216
+ return nil, fmt.Errorf("@ttsc/lint: load config file %s: %w", location, err)
217
+ }
218
+ var out map[string]any
219
+ if err := json.Unmarshal(output, &out); err != nil {
220
+ return nil, fmt.Errorf("@ttsc/lint: parse config file %s output: %w", location, err)
221
+ }
222
+ if out == nil {
223
+ return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
224
+ }
225
+ return out, nil
226
226
  }
227
227
 
228
228
  func loadTypeScriptConfigFile(location string) (map[string]any, error) {
229
- tempDir, err := os.MkdirTemp("", "ttsc-lint-config-")
230
- if err != nil {
231
- return nil, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
232
- }
233
- defer os.RemoveAll(tempDir)
234
-
235
- if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
236
- return nil, err
237
- }
238
-
239
- loader := filepath.Join(tempDir, "loader.mts")
240
- tsconfig := filepath.Join(tempDir, "tsconfig.json")
241
- importSpecifier, err := relativeImportSpecifier(tempDir, location)
242
- if err != nil {
243
- return nil, err
244
- }
245
- importLiteral, err := json.Marshal(importSpecifier)
246
- if err != nil {
247
- return nil, fmt.Errorf("@ttsc/lint: encode config import %s: %w", location, err)
248
- }
249
- if err := os.WriteFile(loader, []byte(typeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
250
- return nil, fmt.Errorf("@ttsc/lint: write config loader: %w", err)
251
- }
252
- if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
253
- return nil, fmt.Errorf("@ttsc/lint: write config loader tsconfig: %w", err)
254
- }
255
-
256
- args := []string{
257
- "--project", tsconfig,
258
- "--cwd", tempDir,
259
- "--cache-dir", filepath.Join(tempDir, "cache"),
260
- }
261
- if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
262
- args = append(args, "--binary", tsgo)
263
- }
264
- args = append(args, loader)
265
-
266
- cmd := ttsxCommand(args...)
267
- cmd.Env = nodeConfigLoaderEnv(location)
268
- output, err := cmd.Output()
269
- if err != nil {
270
- stderr := ""
271
- if exit, ok := err.(*exec.ExitError); ok {
272
- stderr = strings.TrimSpace(string(exit.Stderr))
273
- }
274
- if stderr != "" {
275
- return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %s", location, stderr)
276
- }
277
- return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %w", location, err)
278
- }
279
- var out map[string]any
280
- if err := json.Unmarshal(output, &out); err != nil {
281
- return nil, fmt.Errorf("@ttsc/lint: parse TypeScript config file %s output: %w", location, err)
282
- }
283
- if out == nil {
284
- return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
285
- }
286
- return out, nil
229
+ tempDir, err := os.MkdirTemp("", "ttsc-lint-config-")
230
+ if err != nil {
231
+ return nil, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
232
+ }
233
+ defer os.RemoveAll(tempDir)
234
+
235
+ if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
236
+ return nil, err
237
+ }
238
+
239
+ loader := filepath.Join(tempDir, "loader.mts")
240
+ tsconfig := filepath.Join(tempDir, "tsconfig.json")
241
+ importSpecifier, err := relativeImportSpecifier(tempDir, location)
242
+ if err != nil {
243
+ return nil, err
244
+ }
245
+ importLiteral, err := json.Marshal(importSpecifier)
246
+ if err != nil {
247
+ return nil, fmt.Errorf("@ttsc/lint: encode config import %s: %w", location, err)
248
+ }
249
+ if err := os.WriteFile(loader, []byte(typeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
250
+ return nil, fmt.Errorf("@ttsc/lint: write config loader: %w", err)
251
+ }
252
+ if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
253
+ return nil, fmt.Errorf("@ttsc/lint: write config loader tsconfig: %w", err)
254
+ }
255
+
256
+ args := []string{
257
+ "--project", tsconfig,
258
+ "--cwd", tempDir,
259
+ "--cache-dir", filepath.Join(tempDir, "cache"),
260
+ }
261
+ if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
262
+ args = append(args, "--binary", tsgo)
263
+ }
264
+ args = append(args, loader)
265
+
266
+ cmd := ttsxCommand(args...)
267
+ cmd.Env = nodeConfigLoaderEnv(location)
268
+ output, err := cmd.Output()
269
+ if err != nil {
270
+ stderr := ""
271
+ if exit, ok := err.(*exec.ExitError); ok {
272
+ stderr = strings.TrimSpace(string(exit.Stderr))
273
+ }
274
+ if stderr != "" {
275
+ return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %s", location, stderr)
276
+ }
277
+ return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %w", location, err)
278
+ }
279
+ var out map[string]any
280
+ if err := json.Unmarshal(output, &out); err != nil {
281
+ return nil, fmt.Errorf("@ttsc/lint: parse TypeScript config file %s output: %w", location, err)
282
+ }
283
+ if out == nil {
284
+ return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
285
+ }
286
+ return out, nil
287
287
  }
288
288
 
289
289
  func relativeImportSpecifier(fromDir, location string) (string, error) {
290
- relative, err := filepath.Rel(fromDir, location)
291
- if err != nil {
292
- return "", fmt.Errorf("@ttsc/lint: resolve relative config import %s: %w", location, err)
293
- }
294
- relative = filepath.ToSlash(relative)
295
- if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
296
- return relative, nil
297
- }
298
- return "./" + relative, nil
290
+ relative, err := filepath.Rel(fromDir, location)
291
+ if err != nil {
292
+ return "", fmt.Errorf("@ttsc/lint: resolve relative config import %s: %w", location, err)
293
+ }
294
+ relative = filepath.ToSlash(relative)
295
+ if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
296
+ return relative, nil
297
+ }
298
+ return "./" + relative, nil
299
299
  }
300
300
 
301
301
  func typeScriptConfigLoaderSource(importLiteral string) string {
302
- return fmt.Sprintf(`import * as importedConfig from %s;
302
+ return fmt.Sprintf(`import * as importedConfig from %s;
303
303
 
304
304
  declare const process: {
305
305
  stdout: { write(value: string): void };
@@ -350,138 +350,138 @@ function hasOwn(value: Record<string, unknown>, key: string): boolean {
350
350
  }
351
351
 
352
352
  func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
353
- content := map[string]any{
354
- "compilerOptions": map[string]any{
355
- "allowImportingTsExtensions": true,
356
- "module": "ESNext",
357
- "moduleResolution": "bundler",
358
- "outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
359
- "rewriteRelativeImportExtensions": true,
360
- "rootDir": "/",
361
- "skipLibCheck": true,
362
- "strict": true,
363
- "target": "ES2022",
364
- },
365
- "files": []string{
366
- filepath.ToSlash(loader),
367
- filepath.ToSlash(location),
368
- },
369
- }
370
- body, err := json.MarshalIndent(content, "", " ")
371
- if err != nil {
372
- panic(err)
373
- }
374
- return string(body)
353
+ content := map[string]any{
354
+ "compilerOptions": map[string]any{
355
+ "allowImportingTsExtensions": true,
356
+ "module": "ESNext",
357
+ "moduleResolution": "bundler",
358
+ "outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
359
+ "rewriteRelativeImportExtensions": true,
360
+ "rootDir": "/",
361
+ "skipLibCheck": true,
362
+ "strict": true,
363
+ "target": "ES2022",
364
+ },
365
+ "files": []string{
366
+ filepath.ToSlash(loader),
367
+ filepath.ToSlash(location),
368
+ },
369
+ }
370
+ body, err := json.MarshalIndent(content, "", " ")
371
+ if err != nil {
372
+ panic(err)
373
+ }
374
+ return string(body)
375
375
  }
376
376
 
377
377
  func ttsxCommand(args ...string) *exec.Cmd {
378
- ttsx := os.Getenv("TTSC_TTSX_BINARY")
379
- if ttsx == "" {
380
- ttsx = "ttsx"
381
- }
382
- if shouldRunTtsxThroughNode(ttsx) {
383
- node := os.Getenv("TTSC_NODE_BINARY")
384
- if node == "" {
385
- node = "node"
386
- }
387
- return exec.Command(node, append([]string{ttsx}, args...)...)
388
- }
389
- return exec.Command(ttsx, args...)
378
+ ttsx := os.Getenv("TTSC_TTSX_BINARY")
379
+ if ttsx == "" {
380
+ ttsx = "ttsx"
381
+ }
382
+ if shouldRunTtsxThroughNode(ttsx) {
383
+ node := os.Getenv("TTSC_NODE_BINARY")
384
+ if node == "" {
385
+ node = "node"
386
+ }
387
+ return exec.Command(node, append([]string{ttsx}, args...)...)
388
+ }
389
+ return exec.Command(ttsx, args...)
390
390
  }
391
391
 
392
392
  func shouldRunTtsxThroughNode(binary string) bool {
393
- switch strings.ToLower(filepath.Ext(binary)) {
394
- case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
395
- return true
396
- default:
397
- return false
398
- }
393
+ switch strings.ToLower(filepath.Ext(binary)) {
394
+ case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
395
+ return true
396
+ default:
397
+ return false
398
+ }
399
399
  }
400
400
 
401
401
  func nodeConfigLoaderEnv(location string) []string {
402
- env := os.Environ()
403
- parts := make([]string, 0, 2)
404
- if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
405
- parts = append(parts, nodeModules)
406
- }
407
- if existing := os.Getenv("NODE_PATH"); existing != "" {
408
- parts = append(parts, existing)
409
- }
410
- if len(parts) == 0 {
411
- return env
412
- }
413
- return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
402
+ env := os.Environ()
403
+ parts := make([]string, 0, 2)
404
+ if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
405
+ parts = append(parts, nodeModules)
406
+ }
407
+ if existing := os.Getenv("NODE_PATH"); existing != "" {
408
+ parts = append(parts, existing)
409
+ }
410
+ if len(parts) == 0 {
411
+ return env
412
+ }
413
+ return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
414
414
  }
415
415
 
416
416
  func linkNearestNodeModules(tempDir, sourceDir string) error {
417
- nodeModules := findNearestNodeModules(sourceDir)
418
- if nodeModules == "" {
419
- return nil
420
- }
421
- link := filepath.Join(tempDir, "node_modules")
422
- if err := os.Symlink(nodeModules, link); err != nil {
423
- return fmt.Errorf("@ttsc/lint: link config node_modules %s: %w", nodeModules, err)
424
- }
425
- return nil
417
+ nodeModules := findNearestNodeModules(sourceDir)
418
+ if nodeModules == "" {
419
+ return nil
420
+ }
421
+ link := filepath.Join(tempDir, "node_modules")
422
+ if err := os.Symlink(nodeModules, link); err != nil {
423
+ return fmt.Errorf("@ttsc/lint: link config node_modules %s: %w", nodeModules, err)
424
+ }
425
+ return nil
426
426
  }
427
427
 
428
428
  func findNearestNodeModules(start string) string {
429
- dir := filepath.Clean(start)
430
- for {
431
- candidate := filepath.Join(dir, "node_modules")
432
- if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
433
- return candidate
434
- }
435
- parent := filepath.Dir(dir)
436
- if parent == dir {
437
- return ""
438
- }
439
- dir = parent
440
- }
429
+ dir := filepath.Clean(start)
430
+ for {
431
+ candidate := filepath.Join(dir, "node_modules")
432
+ if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
433
+ return candidate
434
+ }
435
+ parent := filepath.Dir(dir)
436
+ if parent == dir {
437
+ return ""
438
+ }
439
+ dir = parent
440
+ }
441
441
  }
442
442
 
443
443
  func setEnv(env []string, key, value string) []string {
444
- prefix := key + "="
445
- for i, entry := range env {
446
- if strings.HasPrefix(entry, prefix) {
447
- env[i] = prefix + value
448
- return env
449
- }
450
- }
451
- return append(env, prefix+value)
444
+ prefix := key + "="
445
+ for i, entry := range env {
446
+ if strings.HasPrefix(entry, prefix) {
447
+ env[i] = prefix + value
448
+ return env
449
+ }
450
+ }
451
+ return append(env, prefix+value)
452
452
  }
453
453
 
454
454
  func parseSeverity(v any) (Severity, error) {
455
- switch x := v.(type) {
456
- case string:
457
- switch x {
458
- case "off":
459
- return SeverityOff, nil
460
- case "warning", "warn":
461
- return SeverityWarn, nil
462
- case "error":
463
- return SeverityError, nil
464
- }
465
- return SeverityOff, fmt.Errorf("unknown severity %q (want off | warning | error)", x)
466
- case float64:
467
- switch x {
468
- case 0:
469
- return SeverityOff, nil
470
- case 1:
471
- return SeverityWarn, nil
472
- case 2:
473
- return SeverityError, nil
474
- }
475
- return SeverityOff, fmt.Errorf("unknown severity %v (want off | warning | error)", x)
476
- }
477
- return SeverityOff, fmt.Errorf("severity must be one of: off | warning | error, got %T", v)
455
+ switch x := v.(type) {
456
+ case string:
457
+ switch x {
458
+ case "off":
459
+ return SeverityOff, nil
460
+ case "warning", "warn":
461
+ return SeverityWarn, nil
462
+ case "error":
463
+ return SeverityError, nil
464
+ }
465
+ return SeverityOff, fmt.Errorf("unknown severity %q (want off | warning | error)", x)
466
+ case float64:
467
+ switch x {
468
+ case 0:
469
+ return SeverityOff, nil
470
+ case 1:
471
+ return SeverityWarn, nil
472
+ case 2:
473
+ return SeverityError, nil
474
+ }
475
+ return SeverityOff, fmt.Errorf("unknown severity %v (want off | warning | error)", x)
476
+ }
477
+ return SeverityOff, fmt.Errorf("severity must be one of: off | warning | error, got %T", v)
478
478
  }
479
479
 
480
480
  // Severity returns the configured level for a rule, defaulting to
481
481
  // `SeverityOff`. Rules opt in explicitly — silent on missing entries.
482
482
  func (c RuleConfig) Severity(name string) Severity {
483
- if c == nil {
484
- return SeverityOff
485
- }
486
- return c[name]
483
+ if c == nil {
484
+ return SeverityOff
485
+ }
486
+ return c[name]
487
487
  }