@ttsc/lint 0.8.0-dev.20260505 → 0.8.0

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/README.md CHANGED
@@ -74,11 +74,11 @@ Keep `@ttsc/lint` as the first plugin entry — see [Plugin order](#plugin-order
74
74
  "no-var": "error",
75
75
  "prefer-const": "error",
76
76
  "no-explicit-any": "warning",
77
- "no-console": "off"
78
- }
79
- }
80
- ]
81
- }
77
+ "no-console": "off",
78
+ },
79
+ },
80
+ ],
81
+ },
82
82
  }
83
83
  ```
84
84
 
@@ -103,29 +103,97 @@ You can also keep the rules in a standalone file and reference it from `tsconfig
103
103
  "plugins": [
104
104
  {
105
105
  "transform": "@ttsc/lint",
106
- "config": "./ttsc-lint.config.ts"
107
- }
108
- ]
109
- }
106
+ "config": "./ttsc-lint.config.ts",
107
+ },
108
+ ],
109
+ },
110
110
  }
111
111
  ```
112
112
 
113
113
  ```ts
114
114
  // ttsc-lint.config.ts
115
+ import type { TtscLintConfig } from "@ttsc/lint/config";
116
+
115
117
  export default {
116
118
  "no-var": "error",
117
119
  "prefer-const": "error",
118
120
  "no-explicit-any": "warning",
119
- "no-console": "off"
120
- };
121
+ "no-console": "off",
122
+ } satisfies TtscLintConfig;
121
123
  ```
122
124
 
123
125
  The `config` field accepts:
124
126
 
125
127
  - An inline object (shown earlier), **or** a path to a file ending in `.json`, `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, or `.mts`.
126
128
  - For JS/TS configs: a `default` export, a named `config` export, a direct module export, or a function that returns the object.
129
+ - If `config` is omitted, `@ttsc/lint` discovers the nearest `eslint.config.js`, `eslint.config.mjs`, `eslint.config.cjs`, `eslint.config.ts`, `eslint.config.mts`, or `eslint.config.cts` from the owning `tsconfig.json` directory upward.
130
+ - Inline objects use the standard `@ttsc/lint` native rule map shape.
131
+ - Standalone config files may additionally export ESLint flat-config-style objects or arrays. `@ttsc/lint` lowers their `basePath`, `files`, `ignores`, object/array `extends`, and `rules` fields into native per-file rule maps, accepts ESLint severity tuples, and maps `@typescript-eslint/<rule>` keys to native rule names when a matching native rule exists.
127
132
  - Relative paths resolve from the directory of the owning `tsconfig.json`.
128
133
 
134
+ For example, a flat-config-style file can enable native `@ttsc/lint` rules with
135
+ ESLint or `@typescript-eslint/*` keys:
136
+
137
+ ```ts
138
+ // eslint.config.ts
139
+ import type { Linter } from "eslint";
140
+
141
+ const config = [
142
+ {
143
+ rules: {
144
+ "@typescript-eslint/no-explicit-any": "error",
145
+ "@typescript-eslint/consistent-type-imports": [
146
+ "warning",
147
+ { fixStyle: "inline-type-imports" },
148
+ ],
149
+ "no-console": "off",
150
+ },
151
+ },
152
+ ] satisfies Linter.Config[];
153
+
154
+ export default config;
155
+ ```
156
+
157
+ Config factories can be used when they return a resolved flat config object or
158
+ array. Current `typescript-eslint` versions prefer ESLint's `defineConfig(...)`;
159
+ existing configs that still use `typescript-eslint`'s `config(...)` are readable
160
+ when they export the resolved object/array shape:
161
+
162
+ ```ts
163
+ // eslint.config.ts
164
+ import tseslint, { type ConfigArray } from "typescript-eslint";
165
+
166
+ const config = tseslint.config({
167
+ extends: [tseslint.configs.recommended],
168
+ rules: {
169
+ "@typescript-eslint/no-explicit-any": "error",
170
+ },
171
+ }) satisfies ConfigArray;
172
+
173
+ export default config;
174
+ ```
175
+
176
+ When a project has `eslint` installed and the external config is an
177
+ `eslint.config.*` file, or the config uses runtime-only fields such as `plugins`,
178
+ `languageOptions`, `processor`, `settings`, or string `extends`, `@ttsc/lint`
179
+ delegates diagnostics to the installed ESLint runtime and forwards its rule ids,
180
+ severities, messages, and locations. This is the path that runs external
181
+ RuleModules such as `@typescript-eslint/no-floating-promises`; typed
182
+ typescript-eslint rules work when your ESLint config supplies the usual parser
183
+ and `parserOptions.project` / `projectService` settings.
184
+
185
+ When ESLint is not installed and the config can be represented as native rules,
186
+ `@ttsc/lint` falls back to the lowered native rule map. That fallback supports
187
+ `files`, `ignores`, `basePath`, object/array `extends`, `rules`, severity tuples,
188
+ and `@typescript-eslint/` or `typescript-eslint/` rule prefixes. Runtime-only
189
+ features such as `plugins`, `languageOptions`, `processor`, `settings`,
190
+ `linterOptions`, or string `extends` require ESLint to be installed.
191
+
192
+ Inline disable comments are respected in both paths. Use the ESLint forms
193
+ `eslint-disable-next-line`, `eslint-disable-line`, `eslint-disable`, and
194
+ `eslint-enable`; the native engine also accepts the `lint-*` aliases for the
195
+ same directives.
196
+
129
197
  ## Plugin order
130
198
 
131
199
  `@ttsc/lint` must be the first plugin in `compilerOptions.plugins`.
@@ -137,20 +205,25 @@ It inspects the source you wrote, so transform plugins such as `@ttsc/banner`, `
137
205
  "compilerOptions": {
138
206
  "plugins": [
139
207
  // Keep lint first.
140
- { "transform": "@ttsc/lint", "config": { "no-var": "error", "prefer-const": "error" } },
208
+ {
209
+ "transform": "@ttsc/lint",
210
+ "config": { "no-var": "error", "prefer-const": "error" },
211
+ },
141
212
 
142
- // First-party utilities use their documented source/emit hook order.
213
+ // First-party utilities use their documented transform order.
143
214
  { "transform": "@ttsc/banner", "banner": "License MIT" },
144
215
  { "transform": "@ttsc/paths" },
145
- { "transform": "@ttsc/strip", "calls": ["console.log"] }
146
- ]
147
- }
216
+ { "transform": "@ttsc/strip", "calls": ["console.log"] },
217
+ ],
218
+ },
148
219
  }
149
220
  ```
150
221
 
151
222
  ## Scope
152
223
 
153
- Diagnostic-only today: no autofix, no recommended preset, no custom rule loading, and no cross-file lint rules.
224
+ Diagnostic-only today: no autofix and no bundled recommended preset. The native
225
+ engine does not load custom Go rules; external ESLint RuleModules run through
226
+ the installed ESLint runtime path described above.
154
227
 
155
228
  ## Rules
156
229
 
@@ -161,7 +234,7 @@ Rules are off until you enable them:
161
234
  "no-var": "error",
162
235
  "eqeqeq": "error",
163
236
  "prefer-template": "warning",
164
- "no-non-null-assertion": "off"
237
+ "no-non-null-assertion": "off",
165
238
  }
166
239
  ```
167
240
 
@@ -0,0 +1,3 @@
1
+ export type { TtscLintConfig } from "./structures/TtscLintConfig";
2
+ export type { TtscLintRule } from "./structures/TtscLintRule";
3
+ export type { TtscLintSeverity } from "./structures/TtscLintSeverity";
package/lib/config.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":""}
@@ -3,10 +3,11 @@ import type { TtscLintConfig } from "./TtscLintConfig";
3
3
  /** `compilerOptions.plugins[]` entry shape consumed by `@ttsc/lint`. */
4
4
  export interface ITtscLintPluginConfig extends ITtscProjectPluginConfig {
5
5
  /**
6
- * Inline rule map or path to a JSON config file.
6
+ * Inline rule map or path to a standalone lint config file.
7
7
  *
8
8
  * Inline maps are passed to the native sidecar through `--plugins-json`.
9
- * String values are resolved by the sidecar from the project root.
9
+ * String values are resolved by the sidecar from the owning tsconfig
10
+ * directory and may point at JSON, JavaScript, or TypeScript config files.
10
11
  */
11
12
  config?: string | TtscLintConfig;
12
13
  }
@@ -3,9 +3,8 @@ import type { TtscLintSeverity } from "./TtscLintSeverity";
3
3
  /**
4
4
  * Inline rule map accepted by the `@ttsc/lint` tsconfig plugin entry.
5
5
  *
6
- * Each property key is a native lint rule name. Omitted rules use the sidecar's
7
- * default severity, while present rules override that severity for the current
8
- * project.
6
+ * Each property key is a native lint rule name. Omitted rules are disabled,
7
+ * while present rules enable or disable that rule for the current project.
9
8
  */
10
9
  export type TtscLintConfig = {
11
10
  [P in TtscLintRule]?: TtscLintSeverity;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.8.0-dev.20260505",
3
+ "version": "0.8.0",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -9,6 +9,10 @@
9
9
  "types": "./lib/index.d.ts",
10
10
  "default": "./lib/index.js"
11
11
  },
12
+ "./config": {
13
+ "types": "./lib/config.d.ts",
14
+ "default": "./lib/config.js"
15
+ },
12
16
  "./package.json": "./package.json"
13
17
  },
14
18
  "keywords": [
@@ -30,7 +34,7 @@
30
34
  "@typescript/native-preview": "7.0.0-dev.20260504.1",
31
35
  "@types/node": "^25.3.0",
32
36
  "rimraf": "^6.1.2",
33
- "ttsc": "0.8.0-dev.20260505"
37
+ "ttsc": "0.8.0"
34
38
  },
35
39
  "repository": {
36
40
  "type": "git",
package/plugin/compile.go CHANGED
@@ -95,10 +95,16 @@ func RunTransform(args []string) int {
95
95
  fmt.Fprintln(os.Stderr, err)
96
96
  return 2
97
97
  }
98
- engine := NewEngine(rules)
99
- warnUnknownRules(os.Stderr, engine.UnknownRules())
98
+ engine := NewEngineWithResolver(rules)
100
99
 
101
- astDiags, lintDiags := collectDiagnostics(prog, engine)
100
+ astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
101
+ if err != nil {
102
+ fmt.Fprintln(os.Stderr, err)
103
+ return 2
104
+ }
105
+ if !externalRan {
106
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
107
+ }
102
108
  if errors := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, resolvedCwd); errors > 0 {
103
109
  return 2
104
110
  }
@@ -225,10 +231,16 @@ func runProject(opts *subcommandOpts) int {
225
231
  fmt.Fprintln(os.Stderr, err)
226
232
  return 2
227
233
  }
228
- engine := NewEngine(rules)
229
- warnUnknownRules(os.Stderr, engine.UnknownRules())
234
+ engine := NewEngineWithResolver(rules)
230
235
 
231
- astDiags, lintDiags := collectDiagnostics(prog, engine)
236
+ astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
237
+ if err != nil {
238
+ fmt.Fprintln(os.Stderr, err)
239
+ return 2
240
+ }
241
+ if !externalRan {
242
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
243
+ }
232
244
  if errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
233
245
  return 2
234
246
  }
@@ -261,7 +273,7 @@ func runProject(opts *subcommandOpts) int {
261
273
  return 0
262
274
  }
263
275
 
264
- func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleConfig, error) {
276
+ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
265
277
  entries, err := ParsePlugins(pluginsJSON)
266
278
  if err != nil {
267
279
  return nil, err
@@ -273,7 +285,7 @@ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleConfig, error) {
273
285
  if entry == nil {
274
286
  return RuleConfig{}, nil
275
287
  }
276
- return LoadRuleConfig(entry, cwd, tsconfigPath)
288
+ return LoadConfigResolver(entry, cwd, tsconfigPath)
277
289
  }
278
290
 
279
291
  func warnUnknownRules(w io.Writer, unknown []string) {
@@ -314,9 +326,14 @@ func filterKnownFlags(args []string, known map[string]bool) []string {
314
326
  // collectDiagnostics merges tsgo typecheck diagnostics with the lint
315
327
  // engine's findings. The renderer takes the two slices and walks them in
316
328
  // source order, so we don't need to interleave here.
317
- func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic) {
329
+ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, bool, error) {
318
330
  astDiags := prog.programDiagnostics()
319
331
  files := prog.userSourceFiles()
332
+ if externalDiags, ran, err := runExternalESLintDiagnostics(engine.config, prog.cwd, files); err != nil {
333
+ return nil, nil, false, err
334
+ } else if ran {
335
+ return astDiags, externalDiags, true, nil
336
+ }
320
337
  findings := engine.Run(files, prog.checker)
321
338
  lintDiags := make([]*shimdw.LintDiagnostic, 0, len(findings))
322
339
  for _, finding := range findings {
@@ -333,7 +350,7 @@ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, [
333
350
  fmt.Sprintf("[%s] %s", finding.Rule, finding.Message),
334
351
  ))
335
352
  }
336
- return astDiags, lintDiags
353
+ return astDiags, lintDiags, false, nil
337
354
  }
338
355
 
339
356
  // RuleCode hashes a rule name into a stable, positive int32 so the