@ttsc/lint 0.12.4 → 0.13.1
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/lib/index.js +225 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +68 -16
- package/linthost/compile.go +117 -119
- package/linthost/config.go +518 -707
- package/linthost/config_format.go +16 -4
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +44 -0
- package/linthost/engine.go +152 -44
- package/linthost/fix.go +24 -33
- package/linthost/flags_gen.go +33 -0
- package/linthost/format.go +96 -3
- package/linthost/host.go +144 -8
- package/linthost/print_dispatch.go +121 -23
- package/linthost/print_doc.go +19 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +129 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_escape.go +20 -3
- package/linthost/rules_format_print_width.go +267 -15
- package/linthost/rules_gap.go +55 -3
- package/linthost/rules_logic.go +64 -5
- package/linthost/rules_problems.go +65 -21
- package/linthost/rules_promise.go +3 -0
- package/linthost/rules_suggestions.go +160 -5
- package/linthost/rules_var.go +7 -1
- package/package.json +3 -3
- package/src/index.ts +243 -168
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -351
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import os from "node:os";
|
|
@@ -17,10 +18,12 @@ type TtscPluginContributor = {
|
|
|
17
18
|
|
|
18
19
|
/** Descriptor shape returned to ttsc's plugin builder by the factory. */
|
|
19
20
|
type TtscPluginDescriptor = {
|
|
21
|
+
capabilities?: { diagnosticsTiming?: boolean; threadingArgs?: boolean };
|
|
22
|
+
contributors?: TtscPluginContributor[];
|
|
20
23
|
name: string;
|
|
24
|
+
reportsTypeScriptDiagnostics?: boolean;
|
|
21
25
|
source: string;
|
|
22
26
|
stage?: "check" | "transform";
|
|
23
|
-
contributors?: TtscPluginContributor[];
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -69,48 +72,50 @@ const LINT_CONFIG_FILENAMES = [
|
|
|
69
72
|
"ttsc-lint.config.cjs",
|
|
70
73
|
"ttsc-lint.config.js",
|
|
71
74
|
"ttsc-lint.config.json",
|
|
72
|
-
"eslint.config.ts",
|
|
73
|
-
"eslint.config.mts",
|
|
74
|
-
"eslint.config.cts",
|
|
75
|
-
"eslint.config.mjs",
|
|
76
|
-
"eslint.config.cjs",
|
|
77
|
-
"eslint.config.js",
|
|
78
75
|
];
|
|
79
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Tsconfig plugin-entry keys owned by the ttsc host framework. They are
|
|
79
|
+
* accepted alongside the single lint-specific `configFile` key; every other key
|
|
80
|
+
* is rejected so a stale inline option (`rules`, `format`, `extends`, legacy
|
|
81
|
+
* `config`, `plugins`) surfaces as a clear migration error instead of being
|
|
82
|
+
* silently ignored. Mirrors `@ttsc/banner` and `@ttsc/strip`.
|
|
83
|
+
*/
|
|
84
|
+
const FRAMEWORK_KEYS = new Set<string>([
|
|
85
|
+
"enabled",
|
|
86
|
+
"name",
|
|
87
|
+
"stage",
|
|
88
|
+
"transform",
|
|
89
|
+
]);
|
|
90
|
+
|
|
80
91
|
/**
|
|
81
92
|
* Plugin descriptor factory consumed by ttsc package discovery.
|
|
82
93
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `eslint.config.*`) file — an object with an in-memory `plugins: { ns:
|
|
89
|
-
* pluginObject }` map. The factory evaluates the config (via ttsx for TS /
|
|
90
|
-
* ESM sources, `require` for CommonJS, `JSON.parse` for JSON) and reads the
|
|
91
|
-
* `plugins` field.
|
|
94
|
+
* Contributor lint plugins come from one place: the project's lint config file
|
|
95
|
+
* (`lint.config.{ts,cts,mts,js,cjs,mjs,json}` or `ttsc-lint.config.*`). The
|
|
96
|
+
* tsconfig plugin entry carries no rule or plugin surface — it optionally names
|
|
97
|
+
* the config file via `configFile`, otherwise the file is discovered by walking
|
|
98
|
+
* upward from the tsconfig directory.
|
|
92
99
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `
|
|
100
|
+
* The factory locates the config file, evaluates it (via ttsx for TS / ESM
|
|
101
|
+
* sources, `require` for CommonJS, `JSON.parse` for JSON), reads its `plugins`
|
|
102
|
+
* map, and forwards each contributor's Go source directory to ttsc's plugin
|
|
103
|
+
* builder via the descriptor's `contributors` field.
|
|
97
104
|
*
|
|
98
105
|
* @internal
|
|
99
106
|
*/
|
|
100
107
|
export default function createTtscPlugin(
|
|
101
108
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
102
109
|
): TtscPluginDescriptor {
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
context,
|
|
106
|
-
inline.map((c) => c.name),
|
|
107
|
-
);
|
|
108
|
-
const contributors = [...inline, ...fromConfig];
|
|
110
|
+
rejectUnsupportedEntryKeys(context.plugin);
|
|
111
|
+
const contributors = resolveConfigFileContributors(context);
|
|
109
112
|
// Build the descriptor without a `contributors` key when none were
|
|
110
113
|
// declared, so consumers (and the existing key-shape regression
|
|
111
114
|
// tests) see the same surface as before this feature shipped.
|
|
112
115
|
const descriptor: TtscPluginDescriptor = {
|
|
116
|
+
capabilities: { diagnosticsTiming: true, threadingArgs: true },
|
|
113
117
|
name: "@ttsc/lint",
|
|
118
|
+
reportsTypeScriptDiagnostics: true,
|
|
114
119
|
source: path.resolve(__dirname, "..", "plugin"),
|
|
115
120
|
stage: "check",
|
|
116
121
|
};
|
|
@@ -120,60 +125,6 @@ export default function createTtscPlugin(
|
|
|
120
125
|
return descriptor;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
124
|
-
// tsconfig-inline `plugins` map (the original MVP path)
|
|
125
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
function resolveInlineContributors(
|
|
128
|
-
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
129
|
-
): TtscPluginContributor[] {
|
|
130
|
-
const declared = (context.plugin as { plugins?: unknown }).plugins;
|
|
131
|
-
if (declared === undefined) return [];
|
|
132
|
-
if (
|
|
133
|
-
typeof declared !== "object" ||
|
|
134
|
-
declared === null ||
|
|
135
|
-
Array.isArray(declared)
|
|
136
|
-
) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
`@ttsc/lint: "plugins" in tsconfig plugin entry must be an object map of namespace → package specifier`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
const out: TtscPluginContributor[] = [];
|
|
142
|
-
// Track the post-`goSubpackageName` form so `a-b` and `a_b` are
|
|
143
|
-
// caught as colliding aliases before they reach the downstream
|
|
144
|
-
// contributor validator's opaque `duplicate name "a_b"` error.
|
|
145
|
-
// (`Object.entries` cannot itself surface duplicate string keys, so
|
|
146
|
-
// a verbatim-namespace guard is unreachable.)
|
|
147
|
-
const seenGoNames = new Map<string, string>();
|
|
148
|
-
for (const [namespace, specifier] of Object.entries(declared)) {
|
|
149
|
-
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`@ttsc/lint: contributor namespace ${JSON.stringify(namespace)} must match /^[a-z][a-z0-9_-]*$/`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`@ttsc/lint: contributor ${JSON.stringify(namespace)} must point at a non-empty package specifier or path`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
const goName = goSubpackageName(namespace);
|
|
160
|
-
const earlier = seenGoNames.get(goName);
|
|
161
|
-
if (earlier !== undefined) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
`@ttsc/lint: contributor namespaces ${JSON.stringify(earlier)} and ${JSON.stringify(namespace)} both map to Go sub-package ${JSON.stringify(goName)}; pick one form (hyphens collapse to underscores for the Go identifier)`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
seenGoNames.set(goName, namespace);
|
|
167
|
-
const plugin = loadContributorPluginViaRequire(
|
|
168
|
-
specifier,
|
|
169
|
-
context,
|
|
170
|
-
namespace,
|
|
171
|
-
);
|
|
172
|
-
out.push({ name: goName, source: plugin.source });
|
|
173
|
-
}
|
|
174
|
-
return out;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
128
|
function loadContributorPluginViaRequire(
|
|
178
129
|
specifier: string,
|
|
179
130
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
@@ -218,42 +169,41 @@ function loadContributorPluginViaRequire(
|
|
|
218
169
|
// lint.config.* discovery + evaluation
|
|
219
170
|
// ────────────────────────────────────────────────────────────────────────────
|
|
220
171
|
|
|
221
|
-
/** Plugin entries observed in
|
|
172
|
+
/** Plugin entries observed in a lint config file, normalized per file. */
|
|
222
173
|
type ConfigPluginEntry = { namespace: string; source: string };
|
|
223
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Resolves the contributor lint plugins declared in the project's lint config
|
|
177
|
+
* file.
|
|
178
|
+
*
|
|
179
|
+
* - When the tsconfig plugin entry sets `configFile`, that exact file is loaded.
|
|
180
|
+
* - Otherwise a `lint.config.*` / `ttsc-lint.config.*` file is discovered by
|
|
181
|
+
* walking upward from the tsconfig directory.
|
|
182
|
+
*
|
|
183
|
+
* Returns an empty array when no config file is set or discovered — the Go
|
|
184
|
+
* sidecar surfaces the missing-config error; the factory only needs to forward
|
|
185
|
+
* contributors when a config file is present.
|
|
186
|
+
*/
|
|
224
187
|
function resolveConfigFileContributors(
|
|
225
188
|
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
226
|
-
inlineNames: readonly string[],
|
|
227
189
|
): TtscPluginContributor[] {
|
|
228
|
-
|
|
229
|
-
// the legacy `config` field. The legacy fallback warns once per
|
|
230
|
-
// ttsc invocation so existing tsconfigs keep working through the
|
|
231
|
-
// deprecation window without crashing CI.
|
|
232
|
-
const { hasInlineRules, extendsPath } = readSeverityConfig(context);
|
|
233
|
-
if (hasInlineRules) {
|
|
234
|
-
// Inline rules → no lint.config.* file involved. Skip discovery so
|
|
235
|
-
// we don't pull in plugins from an unrelated file.
|
|
236
|
-
return [];
|
|
237
|
-
}
|
|
238
|
-
|
|
190
|
+
const configFile = readConfigFileOption(context);
|
|
239
191
|
const configPath =
|
|
240
|
-
|
|
241
|
-
? path.resolve(tsconfigBaseDir(context),
|
|
192
|
+
configFile !== undefined
|
|
193
|
+
? path.resolve(tsconfigBaseDir(context), configFile)
|
|
242
194
|
: findLintConfigFile(context);
|
|
243
195
|
if (!configPath || !fs.existsSync(configPath)) return [];
|
|
244
196
|
|
|
245
197
|
const entries = readConfigPluginEntries(configPath, context);
|
|
246
|
-
// Dedup
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
// later rejects as a duplicate name.
|
|
252
|
-
const occupied = new Set(inlineNames);
|
|
198
|
+
// Dedup on the Go-subpackage form (post hyphen→underscore transform)
|
|
199
|
+
// so two namespaces that collapse to the same Go identifier surface
|
|
200
|
+
// here instead of as the contributor validator's opaque
|
|
201
|
+
// `duplicate name "a_b"` error.
|
|
202
|
+
const occupied = new Set<string>();
|
|
253
203
|
const out: TtscPluginContributor[] = [];
|
|
254
204
|
for (const entry of entries) {
|
|
255
205
|
const goName = goSubpackageName(entry.namespace);
|
|
256
|
-
if (occupied.has(goName)) continue;
|
|
206
|
+
if (occupied.has(goName)) continue;
|
|
257
207
|
occupied.add(goName);
|
|
258
208
|
out.push({ name: goName, source: entry.source });
|
|
259
209
|
}
|
|
@@ -261,76 +211,39 @@ function resolveConfigFileContributors(
|
|
|
261
211
|
}
|
|
262
212
|
|
|
263
213
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* — the inline map is authoritative.
|
|
269
|
-
* - `extends` (string) routes the file walk to a fixed path.
|
|
270
|
-
* - `config` (legacy) silently maps onto the equivalent new field. The
|
|
271
|
-
* user-facing deprecation notice is emitted by the Go sidecar so that a
|
|
272
|
-
* single ttsc invocation prints exactly one warning regardless of how many
|
|
273
|
-
* entry points (JS factory, Go binary) parse the same key.
|
|
274
|
-
* - Mixing legacy and new keys, or mixing `rules` with `extends`, is rejected
|
|
275
|
-
* outright so users don't end up with silent precedence surprises.
|
|
214
|
+
* Rejects any tsconfig plugin-entry key that is neither a host framework key
|
|
215
|
+
* nor the single lint-specific `configFile` key. Rule, format, and plugin
|
|
216
|
+
* settings live only in the lint config file, so a leftover inline key is
|
|
217
|
+
* surfaced as an explicit migration error rather than silently ignored.
|
|
276
218
|
*/
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const extendsRaw = entry.extends;
|
|
283
|
-
const legacy = entry.config;
|
|
284
|
-
const hasNewRules = rules !== undefined;
|
|
285
|
-
const hasExtends = extendsRaw !== undefined;
|
|
286
|
-
const hasLegacy = legacy !== undefined;
|
|
287
|
-
|
|
288
|
-
if (hasLegacy && (hasNewRules || hasExtends)) {
|
|
289
|
-
throw new Error(
|
|
290
|
-
`@ttsc/lint: tsconfig plugin entry mixes legacy "config" with the new "rules"/"extends" fields; remove "config" (deprecated)`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
if (hasNewRules && hasExtends) {
|
|
219
|
+
function rejectUnsupportedEntryKeys(entry: ITtscLintPluginConfig): void {
|
|
220
|
+
for (const key of Object.keys(entry as Record<string, unknown>)) {
|
|
221
|
+
if (FRAMEWORK_KEYS.has(key) || key === "configFile") {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
294
224
|
throw new Error(
|
|
295
|
-
`@ttsc/lint:
|
|
225
|
+
`@ttsc/lint: tsconfig plugin entry contains unsupported key ${JSON.stringify(key)}. ` +
|
|
226
|
+
`Rules, format, and plugin settings must live in a ` +
|
|
227
|
+
`lint.config.{ts,cts,mts,js,cjs,mjs,json} file. The only accepted key ` +
|
|
228
|
+
`in the tsconfig entry is "configFile" (optional path to the config file).`,
|
|
296
229
|
);
|
|
297
230
|
}
|
|
231
|
+
}
|
|
298
232
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
if (hasExtends) {
|
|
313
|
-
if (typeof extendsRaw !== "string" || extendsRaw.length === 0) {
|
|
314
|
-
throw new Error(`@ttsc/lint: "extends" must be a non-empty string path`);
|
|
315
|
-
}
|
|
316
|
-
return { hasInlineRules: false, extendsPath: extendsRaw };
|
|
317
|
-
}
|
|
318
|
-
if (hasLegacy) {
|
|
319
|
-
if (
|
|
320
|
-
typeof legacy === "object" &&
|
|
321
|
-
legacy !== null &&
|
|
322
|
-
!Array.isArray(legacy)
|
|
323
|
-
) {
|
|
324
|
-
return { hasInlineRules: true, extendsPath: undefined };
|
|
325
|
-
}
|
|
326
|
-
if (typeof legacy === "string" && legacy.length > 0) {
|
|
327
|
-
return { hasInlineRules: false, extendsPath: legacy };
|
|
328
|
-
}
|
|
329
|
-
throw new Error(
|
|
330
|
-
`@ttsc/lint: legacy "config" must be a non-empty string path or a rule severity map, got ${typeof legacy}`,
|
|
331
|
-
);
|
|
233
|
+
/**
|
|
234
|
+
* Reads the optional `configFile` key from the tsconfig plugin entry. It is the
|
|
235
|
+
* only lint-specific key the entry accepts; when present it overrides
|
|
236
|
+
* auto-discovery of the lint config file.
|
|
237
|
+
*/
|
|
238
|
+
function readConfigFileOption(
|
|
239
|
+
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
240
|
+
): string | undefined {
|
|
241
|
+
const value = (context.plugin as { configFile?: unknown }).configFile;
|
|
242
|
+
if (value === undefined) return undefined;
|
|
243
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
244
|
+
throw new Error(`@ttsc/lint: "configFile" must be a non-empty string path`);
|
|
332
245
|
}
|
|
333
|
-
return
|
|
246
|
+
return value;
|
|
334
247
|
}
|
|
335
248
|
|
|
336
249
|
function findLintConfigFile(
|
|
@@ -542,7 +455,61 @@ function extractPluginSource(value: unknown): string | undefined {
|
|
|
542
455
|
}
|
|
543
456
|
`;
|
|
544
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Resolves the contributor plugin entries declared in a .ts/.mjs lint config,
|
|
460
|
+
* memoized through the shared on-disk config cache.
|
|
461
|
+
*
|
|
462
|
+
* Evaluating such a config spawns a full `ttsx` subprocess. A monorepo build
|
|
463
|
+
* runs one `ttsc` process per package, and each would otherwise re-spawn `ttsx`
|
|
464
|
+
* for the same shared config; the cache collapses that to a single evaluation.
|
|
465
|
+
* The cache is keyed by the config file's path and exact contents (see
|
|
466
|
+
* `configCacheKey`), so an edit re-evaluates cleanly.
|
|
467
|
+
*/
|
|
545
468
|
function readTtsxConfigPlugins(
|
|
469
|
+
configPath: string,
|
|
470
|
+
context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
471
|
+
): ConfigPluginEntry[] {
|
|
472
|
+
const cacheKey = configCacheKey("plugins", configPath);
|
|
473
|
+
if (cacheKey) {
|
|
474
|
+
const cached = readConfigPluginCache(cacheKey);
|
|
475
|
+
// Re-validate cached entries before trusting them: a contributor's
|
|
476
|
+
// resolved `source` directory may have moved since the entry was
|
|
477
|
+
// written. A stale entry falls through to a fresh evaluation rather
|
|
478
|
+
// than being forwarded to ttsc's plugin builder as a dead path.
|
|
479
|
+
if (cached && cached.every(isValidConfigPluginEntry)) return cached;
|
|
480
|
+
}
|
|
481
|
+
const entries = evaluateTtsxConfigPlugins(configPath, context);
|
|
482
|
+
if (cacheKey) writeConfigPluginCache(cacheKey, entries);
|
|
483
|
+
return entries;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Reports whether a cached plugin entry is still usable: a well-formed
|
|
488
|
+
* namespace and an absolute `source` that still points at a directory. Pure
|
|
489
|
+
* predicate — never throws — so a malformed cache entry simply triggers
|
|
490
|
+
* re-evaluation instead of aborting plugin discovery.
|
|
491
|
+
*/
|
|
492
|
+
function isValidConfigPluginEntry(entry: unknown): entry is ConfigPluginEntry {
|
|
493
|
+
if (
|
|
494
|
+
entry == null ||
|
|
495
|
+
typeof entry !== "object" ||
|
|
496
|
+
typeof (entry as ConfigPluginEntry).namespace !== "string" ||
|
|
497
|
+
typeof (entry as ConfigPluginEntry).source !== "string"
|
|
498
|
+
) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const { namespace, source } = entry as ConfigPluginEntry;
|
|
502
|
+
if (!NAMESPACE_PATTERN.test(namespace) || !path.isAbsolute(source)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
return fs.statSync(source).isDirectory();
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function evaluateTtsxConfigPlugins(
|
|
546
513
|
configPath: string,
|
|
547
514
|
_context: TtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
548
515
|
): ConfigPluginEntry[] {
|
|
@@ -592,7 +559,20 @@ function readTtsxConfigPlugins(
|
|
|
592
559
|
);
|
|
593
560
|
|
|
594
561
|
const ttsxBinary = process.env.TTSC_TTSX_BINARY ?? "ttsx";
|
|
595
|
-
|
|
562
|
+
// `--no-plugins` keeps this build hermetic: the loader only needs to
|
|
563
|
+
// type-check and run the user's lint config to extract its plugin
|
|
564
|
+
// entries. Loading the host project's transform/check plugins
|
|
565
|
+
// (`@nestia/core`, `typia`, …) would run their project checks
|
|
566
|
+
// against this deliberately lenient loader tsconfig and fail the
|
|
567
|
+
// build — e.g. `@nestia/core` rejects the loader's `strict: false`.
|
|
568
|
+
const args = [
|
|
569
|
+
"--project",
|
|
570
|
+
tsconfigPath,
|
|
571
|
+
"--cwd",
|
|
572
|
+
tempDir,
|
|
573
|
+
"--no-plugins",
|
|
574
|
+
loaderPath,
|
|
575
|
+
];
|
|
596
576
|
if (process.env.TTSC_TSGO_BINARY) {
|
|
597
577
|
args.unshift("--binary", process.env.TTSC_TSGO_BINARY);
|
|
598
578
|
}
|
|
@@ -670,6 +650,101 @@ function readTtsxConfigPlugins(
|
|
|
670
650
|
}
|
|
671
651
|
}
|
|
672
652
|
|
|
653
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
654
|
+
// Config cache (shared with the Go sidecar — packages/lint/linthost/config.go)
|
|
655
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Namespaces the on-disk config cache. Kept in lockstep with the Go sidecar's
|
|
659
|
+
* `configCacheVersion`; bump both when the cached shape changes.
|
|
660
|
+
*/
|
|
661
|
+
const CONFIG_CACHE_VERSION = "v1";
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Directory shared by this factory and the Go sidecar for cached lint configs.
|
|
665
|
+
* The two write different files (the `kind` segment of the cache key keeps
|
|
666
|
+
* their namespaces apart), so they coexist without collision.
|
|
667
|
+
*/
|
|
668
|
+
function configCacheDir(): string {
|
|
669
|
+
return path.join(os.tmpdir(), "ttsc-lint-config-cache");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Env opt-out, mirroring the Go sidecar's `TTSC_LINT_DISABLE_CONFIG_CACHE`. */
|
|
673
|
+
function configCacheDisabled(): boolean {
|
|
674
|
+
return Boolean(process.env.TTSC_LINT_DISABLE_CONFIG_CACHE);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Content-addressed cache key for a lint config file. Mirrors the Go sidecar's
|
|
679
|
+
* `configCacheKey`: a version tag, a namespace `kind`, the config's absolute
|
|
680
|
+
* path, and its exact bytes. Returns "" — a "do not cache" signal — when the
|
|
681
|
+
* file cannot be read or the env opt-out is set.
|
|
682
|
+
*/
|
|
683
|
+
function configCacheKey(kind: string, configPath: string): string {
|
|
684
|
+
if (configCacheDisabled()) return "";
|
|
685
|
+
let content: Buffer;
|
|
686
|
+
try {
|
|
687
|
+
content = fs.readFileSync(configPath);
|
|
688
|
+
} catch {
|
|
689
|
+
return "";
|
|
690
|
+
}
|
|
691
|
+
return createHash("sha256")
|
|
692
|
+
.update(CONFIG_CACHE_VERSION)
|
|
693
|
+
.update("\0")
|
|
694
|
+
.update(kind)
|
|
695
|
+
.update("\0")
|
|
696
|
+
.update(path.resolve(configPath))
|
|
697
|
+
.update("\0")
|
|
698
|
+
.update(content)
|
|
699
|
+
.digest("hex");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Returns the cached plugin-entry list for `cacheKey`, or undefined on any miss
|
|
704
|
+
* — a missing file, an unreadable file, or content that is not a JSON array.
|
|
705
|
+
* Every failure is a soft miss: the caller re-evaluates.
|
|
706
|
+
*/
|
|
707
|
+
function readConfigPluginCache(
|
|
708
|
+
cacheKey: string,
|
|
709
|
+
): ConfigPluginEntry[] | undefined {
|
|
710
|
+
let body: string;
|
|
711
|
+
try {
|
|
712
|
+
body = fs.readFileSync(
|
|
713
|
+
path.join(configCacheDir(), `${cacheKey}.json`),
|
|
714
|
+
"utf8",
|
|
715
|
+
);
|
|
716
|
+
} catch {
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
const parsed: unknown = JSON.parse(body);
|
|
721
|
+
return Array.isArray(parsed) ? (parsed as ConfigPluginEntry[]) : undefined;
|
|
722
|
+
} catch {
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Writes `entries` to the config cache under `cacheKey`. Best-effort: any
|
|
729
|
+
* failure leaves the cache cold rather than aborting plugin discovery. The
|
|
730
|
+
* temp-file + rename keeps a concurrent reader in a sibling `ttsc` process from
|
|
731
|
+
* observing a half-written file.
|
|
732
|
+
*/
|
|
733
|
+
function writeConfigPluginCache(
|
|
734
|
+
cacheKey: string,
|
|
735
|
+
entries: ConfigPluginEntry[],
|
|
736
|
+
): void {
|
|
737
|
+
try {
|
|
738
|
+
const dir = configCacheDir();
|
|
739
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
740
|
+
const tmp = path.join(dir, `${cacheKey}.${process.pid}.tmp`);
|
|
741
|
+
fs.writeFileSync(tmp, JSON.stringify(entries), "utf8");
|
|
742
|
+
fs.renameSync(tmp, path.join(dir, `${cacheKey}.json`));
|
|
743
|
+
} catch {
|
|
744
|
+
// Cold cache on failure — the next invocation re-evaluates.
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
673
748
|
// ────────────────────────────────────────────────────────────────────────────
|
|
674
749
|
// Shared helpers
|
|
675
750
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { ITtscLintFormatConfig } from "./ITtscLintFormatConfig";
|
|
2
|
-
import type { TtscLintRuleMap } from "./TtscLintRuleMap";
|
|
3
|
-
|
|
4
1
|
/** `compilerOptions.plugins[]` entry shape consumed by `@ttsc/lint`. */
|
|
5
2
|
export interface ITtscLintPluginConfig {
|
|
6
3
|
/** Set to `false` to keep the entry while disabling this plugin. */
|
|
@@ -10,94 +7,24 @@ export interface ITtscLintPluginConfig {
|
|
|
10
7
|
transform?: string;
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Mirrors the `rules` field of an ESLint flat-config entry. When set, the
|
|
16
|
-
* sidecar uses this map directly and does NOT consult any `lint.config.*`
|
|
17
|
-
* file (use `extends` for that). Combine with `plugins` to register
|
|
18
|
-
* contributor rule namespaces in the same entry.
|
|
19
|
-
*
|
|
20
|
-
* ```jsonc
|
|
21
|
-
* {
|
|
22
|
-
* "transform": "@ttsc/lint",
|
|
23
|
-
* "rules": { "no-var": "error", "prefer-const": "warning" }
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
rules?: TtscLintRuleMap;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Path to a standalone lint config file whose rules should be applied to this
|
|
31
|
-
* project. Relative paths are resolved from the tsconfig directory. Accepts
|
|
32
|
-
* the usual `lint.config.*` / `ttsc-lint.config.*` / `eslint.config.*`
|
|
33
|
-
* extensions.
|
|
34
|
-
*
|
|
35
|
-
* Mirrors the `extends` field of an ESLint flat-config entry — "inherit this
|
|
36
|
-
* file's configuration".
|
|
37
|
-
*
|
|
38
|
-
* ```jsonc
|
|
39
|
-
* {
|
|
40
|
-
* "transform": "@ttsc/lint",
|
|
41
|
-
* "extends": "./lint.config.ts"
|
|
42
|
-
* }
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* `rules` and `extends` are mutually exclusive on a single plugin entry; the
|
|
46
|
-
* sidecar surfaces a loud error when both are set.
|
|
47
|
-
*/
|
|
48
|
-
extends?: string;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Contributor lint plugins to compile into the `@ttsc/lint` binary.
|
|
10
|
+
* Path to the lint config file, overriding auto-discovery.
|
|
52
11
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* ttsc's plugin builder via the `contributors` field.
|
|
12
|
+
* Relative paths are resolved from the tsconfig directory; absolute paths are
|
|
13
|
+
* used as-is. Accepts the usual `lint.config.*` / `ttsc-lint.config.*`
|
|
14
|
+
* extensions (`.ts`, `.cts`, `.mts`, `.js`, `.cjs`, `.mjs`, `.json`).
|
|
57
15
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* "transform": "@ttsc/lint",
|
|
61
|
-
* "plugins": { "demo": "ttsc-lint-plugin-demo" },
|
|
62
|
-
* "rules": { "demo/no-todo-comment": "error" }
|
|
63
|
-
* }
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
plugins?: Record<string, string>;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Prettier-style flat configuration for the `format/*` rules. Sibling of
|
|
70
|
-
* `rules`. See {@link ITtscLintFormatConfig} for the full surface.
|
|
16
|
+
* When omitted, `@ttsc/lint` discovers a `lint.config.*` /
|
|
17
|
+
* `ttsc-lint.config.*` file by walking upward from the tsconfig directory.
|
|
71
18
|
*
|
|
72
19
|
* ```jsonc
|
|
73
20
|
* {
|
|
74
21
|
* "transform": "@ttsc/lint",
|
|
75
|
-
* "
|
|
22
|
+
* "configFile": "./lint.config.ts"
|
|
76
23
|
* }
|
|
77
24
|
* ```
|
|
78
25
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* inside the extends-target lint.config.ts instead.
|
|
26
|
+
* Every rule, format, and plugin setting lives in the config file — the
|
|
27
|
+
* tsconfig plugin entry carries nothing but this pointer.
|
|
82
28
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Inline rule map or path to a standalone lint config file.
|
|
87
|
-
*
|
|
88
|
-
* @deprecated Use `rules` for inline severity maps or `extends` for a config
|
|
89
|
-
* file path. The sidecar maps a legacy `config` entry onto the appropriate
|
|
90
|
-
* new field and emits a one-time stderr deprecation notice. Removed in a
|
|
91
|
-
* future minor.
|
|
92
|
-
*
|
|
93
|
-
* The legacy shape is intentionally narrower than `rules`/`extends`: a string
|
|
94
|
-
* (file path) or a flat rule-name → severity map. The Go-side parser only
|
|
95
|
-
* accepts those two shapes; widening the TS type to the full config object
|
|
96
|
-
* would silently let unsupported nested shapes pass type-checking and fail
|
|
97
|
-
* at runtime.
|
|
98
|
-
*/
|
|
99
|
-
config?: string | TtscLintRuleMap;
|
|
100
|
-
|
|
101
|
-
/** Extra plugin-owned fields are passed through unchanged. */
|
|
102
|
-
[key: string]: unknown;
|
|
29
|
+
configFile?: string;
|
|
103
30
|
}
|
|
@@ -99,6 +99,21 @@ export interface ITtscLintPrintWidthRuleOptions {
|
|
|
99
99
|
* @default "lf"
|
|
100
100
|
*/
|
|
101
101
|
endOfLine?: "lf" | "crlf";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Trailing-comma policy the reflow honors when it breaks a list across
|
|
105
|
+
* lines. Mirrors prettier's `trailingComma` and must match the
|
|
106
|
+
* `format/trailing-comma` rule's `mode`; otherwise the two rules
|
|
107
|
+
* disagree on every cascade pass and oscillate against each other.
|
|
108
|
+
*
|
|
109
|
+
* When a `format` block is configured, `format.trailingComma` is mirrored
|
|
110
|
+
* into this option automatically. Set it directly only when overriding
|
|
111
|
+
* the print-width rule via a `rules` tuple — see the conflict-resolution
|
|
112
|
+
* notes in the README.
|
|
113
|
+
*
|
|
114
|
+
* @default "all"
|
|
115
|
+
*/
|
|
116
|
+
trailingComma?: "all" | "es5" | "none";
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
/** `format/jsdoc` rule options. */
|