@ttsc/lint 0.10.1 → 0.11.0-dev.20260517

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.
Files changed (113) hide show
  1. package/README.md +172 -33
  2. package/lib/defineConfig.d.ts +55 -0
  3. package/lib/defineConfig.js +39 -0
  4. package/lib/defineConfig.js.map +1 -0
  5. package/lib/index.d.ts +25 -1
  6. package/lib/index.js +622 -2
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintPlugin.d.ts +53 -0
  9. package/lib/structures/ITtscLintPlugin.js +3 -0
  10. package/lib/structures/ITtscLintPlugin.js.map +1 -0
  11. package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
  12. package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
  13. package/lib/structures/ITtscLintPluginMeta.js +3 -0
  14. package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
  15. package/lib/structures/PluginRuleNames.d.ts +14 -0
  16. package/lib/structures/PluginRuleNames.js +3 -0
  17. package/lib/structures/PluginRuleNames.js.map +1 -0
  18. package/lib/structures/TtscLintConfig.d.ts +24 -8
  19. package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
  20. package/lib/structures/TtscLintConfigEntry.js +3 -0
  21. package/lib/structures/TtscLintConfigEntry.js.map +1 -0
  22. package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
  23. package/lib/structures/TtscLintFormatConfig.js +3 -0
  24. package/lib/structures/TtscLintFormatConfig.js.map +1 -0
  25. package/lib/structures/TtscLintPlugins.d.ts +9 -0
  26. package/lib/structures/TtscLintPlugins.js +3 -0
  27. package/lib/structures/TtscLintPlugins.js.map +1 -0
  28. package/lib/structures/TtscLintRule.d.ts +1 -1
  29. package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
  30. package/lib/structures/TtscLintRuleEntry.js +3 -0
  31. package/lib/structures/TtscLintRuleEntry.js.map +1 -0
  32. package/lib/structures/TtscLintRuleMap.d.ts +44 -0
  33. package/lib/structures/TtscLintRuleMap.js +3 -0
  34. package/lib/structures/TtscLintRuleMap.js.map +1 -0
  35. package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
  36. package/lib/structures/TtscLintRuleOptions.js +3 -0
  37. package/lib/structures/TtscLintRuleOptions.js.map +1 -0
  38. package/lib/structures/defaultFormat.d.ts +31 -0
  39. package/lib/structures/defaultFormat.js +35 -0
  40. package/lib/structures/defaultFormat.js.map +1 -0
  41. package/lib/structures/index.d.ts +10 -0
  42. package/lib/structures/index.js +10 -0
  43. package/lib/structures/index.js.map +1 -1
  44. package/{plugin → linthost}/ast_helpers.go +92 -1
  45. package/{plugin → linthost}/compile.go +1 -1
  46. package/{plugin → linthost}/config.go +583 -66
  47. package/linthost/config_format.go +360 -0
  48. package/linthost/contrib_adapter.go +134 -0
  49. package/{plugin → linthost}/directives.go +1 -1
  50. package/linthost/dispatch.go +62 -0
  51. package/{plugin → linthost}/engine.go +156 -11
  52. package/{plugin → linthost}/eslint_runtime.go +76 -2
  53. package/linthost/fix.go +249 -0
  54. package/linthost/format.go +107 -0
  55. package/{plugin → linthost}/host.go +1 -1
  56. package/linthost/print_dispatch.go +118 -0
  57. package/linthost/print_doc.go +168 -0
  58. package/linthost/print_engine.go +343 -0
  59. package/linthost/print_nodes_array.go +46 -0
  60. package/linthost/print_nodes_call.go +180 -0
  61. package/linthost/print_nodes_imports.go +171 -0
  62. package/linthost/print_nodes_list.go +64 -0
  63. package/linthost/print_nodes_object.go +51 -0
  64. package/{plugin → linthost}/rules_arrays.go +1 -1
  65. package/{plugin → linthost}/rules_console.go +1 -1
  66. package/{plugin → linthost}/rules_debugger.go +1 -1
  67. package/{plugin → linthost}/rules_dupes.go +1 -1
  68. package/{plugin → linthost}/rules_empty.go +1 -1
  69. package/linthost/rules_escape.go +238 -0
  70. package/{plugin → linthost}/rules_eval.go +1 -1
  71. package/{plugin → linthost}/rules_finally.go +1 -1
  72. package/linthost/rules_format_jsdoc.go +230 -0
  73. package/linthost/rules_format_print_width.go +332 -0
  74. package/linthost/rules_format_quotes.go +219 -0
  75. package/linthost/rules_format_semi.go +190 -0
  76. package/linthost/rules_format_sort_imports.go +462 -0
  77. package/linthost/rules_format_trailing_comma.go +460 -0
  78. package/{plugin → linthost}/rules_gap.go +101 -6
  79. package/linthost/rules_imports.go +101 -0
  80. package/{plugin → linthost}/rules_logic.go +55 -3
  81. package/{plugin → linthost}/rules_loops.go +1 -1
  82. package/{plugin → linthost}/rules_misc.go +1 -1
  83. package/linthost/rules_params.go +64 -0
  84. package/{plugin → linthost}/rules_problems.go +10 -2
  85. package/linthost/rules_promise.go +111 -0
  86. package/{plugin → linthost}/rules_protos.go +1 -1
  87. package/{plugin → linthost}/rules_self.go +1 -1
  88. package/{plugin → linthost}/rules_strings.go +1 -1
  89. package/{plugin → linthost}/rules_suggestions.go +51 -9
  90. package/{plugin → linthost}/rules_throw.go +1 -1
  91. package/{plugin → linthost}/rules_ts.go +12 -2
  92. package/{plugin → linthost}/rules_ts_extra.go +90 -4
  93. package/{plugin → linthost}/rules_var.go +36 -5
  94. package/package.json +8 -6
  95. package/plugin/main.go +14 -27
  96. package/rule/astutil/astutil.go +148 -0
  97. package/rule/rule.go +291 -0
  98. package/src/defineConfig.ts +69 -0
  99. package/src/index.ts +807 -3
  100. package/src/structures/ITtscLintPlugin.ts +56 -0
  101. package/src/structures/ITtscLintPluginConfig.ts +86 -5
  102. package/src/structures/ITtscLintPluginMeta.ts +14 -0
  103. package/src/structures/PluginRuleNames.ts +19 -0
  104. package/src/structures/TtscLintConfig.ts +27 -8
  105. package/src/structures/TtscLintConfigEntry.ts +50 -0
  106. package/src/structures/TtscLintFormatConfig.ts +169 -0
  107. package/src/structures/TtscLintPlugins.ts +10 -0
  108. package/src/structures/TtscLintRule.ts +14 -1
  109. package/src/structures/TtscLintRuleEntry.ts +28 -0
  110. package/src/structures/TtscLintRuleMap.ts +52 -0
  111. package/src/structures/TtscLintRuleOptions.ts +162 -0
  112. package/src/structures/defaultFormat.ts +33 -0
  113. package/src/structures/index.ts +10 -0
package/lib/index.js CHANGED
@@ -18,13 +18,633 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.default = createTtscPlugin;
21
+ const node_child_process_1 = require("node:child_process");
22
+ const node_fs_1 = __importDefault(require("node:fs"));
23
+ const node_module_1 = require("node:module");
24
+ const node_os_1 = __importDefault(require("node:os"));
21
25
  const node_path_1 = __importDefault(require("node:path"));
26
+ __exportStar(require("./defineConfig"), exports);
22
27
  __exportStar(require("./structures/index"), exports);
23
- function createTtscPlugin(_context) {
24
- return {
28
+ // Namespace becomes the rule-name prefix (`<ns>/<rule>`). Mirrors ESLint
29
+ // plugin namespace conventions: lowercase ASCII, digits, hyphens, and
30
+ // underscores; leading character must be alphabetic so the prefix never
31
+ // collides with a rule name that itself starts with a digit. Hyphens
32
+ // are encoded into underscores for the Go sub-package name (see
33
+ // `goSubpackageName`); the user-facing prefix keeps the original form.
34
+ const NAMESPACE_PATTERN = /^[a-z][a-z0-9_-]*$/;
35
+ /**
36
+ * Map a user-facing namespace (`react-hooks`) to a Go-valid sub-package name
37
+ * (`react_hooks`). Required because ttsc's plugin builder uses the `name` field
38
+ * as a directory and import-path suffix, both of which must satisfy Go's
39
+ * stricter `[a-z][a-z0-9_]*` identifier rules. The function is total over
40
+ * namespaces that already passed `NAMESPACE_PATTERN`.
41
+ */
42
+ function goSubpackageName(namespace) {
43
+ return namespace.replace(/-/g, "_");
44
+ }
45
+ const LINT_CONFIG_FILENAMES = [
46
+ "lint.config.ts",
47
+ "lint.config.mts",
48
+ "lint.config.cts",
49
+ "lint.config.mjs",
50
+ "lint.config.cjs",
51
+ "lint.config.js",
52
+ "lint.config.json",
53
+ "ttsc-lint.config.ts",
54
+ "ttsc-lint.config.mts",
55
+ "ttsc-lint.config.cts",
56
+ "ttsc-lint.config.mjs",
57
+ "ttsc-lint.config.cjs",
58
+ "ttsc-lint.config.js",
59
+ "ttsc-lint.config.json",
60
+ "eslint.config.ts",
61
+ "eslint.config.mts",
62
+ "eslint.config.cts",
63
+ "eslint.config.mjs",
64
+ "eslint.config.cjs",
65
+ "eslint.config.js",
66
+ ];
67
+ /**
68
+ * Plugin descriptor factory for `@ttsc/lint`.
69
+ *
70
+ * Two discovery surfaces feed the descriptor's `contributors` field:
71
+ *
72
+ * 1. The tsconfig plugin entry's `plugins` map — namespace → npm specifier. Inline
73
+ * for projects that prefer to keep everything in `tsconfig.json`.
74
+ * 2. The companion `lint.config.{ts,cts,mts,js,cjs,mjs,json}` (or
75
+ * `eslint.config.*`) file — flat-config-style with an in-memory `plugins: {
76
+ * ns: pluginObject }` map. The factory evaluates the config (via ttsx for TS
77
+ * / ESM sources, `require` for CommonJS, `JSON.parse` for JSON) and walks
78
+ * every entry's `plugins` field.
79
+ *
80
+ * Contributions from both sources are merged with the tsconfig entry winning on
81
+ * namespace collisions, so a project can opt into a hand-curated subset of an
82
+ * external `lint.config.ts` by overriding specific namespaces in
83
+ * `tsconfig.json`.
84
+ */
85
+ function createTtscPlugin(context) {
86
+ const inline = resolveInlineContributors(context);
87
+ const fromConfig = resolveConfigFileContributors(context, inline.map((c) => c.name));
88
+ const contributors = [...inline, ...fromConfig];
89
+ // Build the descriptor without a `contributors` key when none were
90
+ // declared, so consumers (and the existing key-shape regression
91
+ // tests) see the same surface as before this feature shipped.
92
+ const descriptor = {
25
93
  name: "@ttsc/lint",
26
94
  source: node_path_1.default.resolve(__dirname, "..", "plugin"),
27
95
  stage: "check",
28
96
  };
97
+ if (contributors.length > 0) {
98
+ descriptor.contributors = contributors;
99
+ }
100
+ return descriptor;
101
+ }
102
+ // ────────────────────────────────────────────────────────────────────────────
103
+ // tsconfig-inline `plugins` map (the original MVP path)
104
+ // ────────────────────────────────────────────────────────────────────────────
105
+ function resolveInlineContributors(context) {
106
+ const declared = context.plugin.plugins;
107
+ if (declared === undefined)
108
+ return [];
109
+ if (typeof declared !== "object" ||
110
+ declared === null ||
111
+ Array.isArray(declared)) {
112
+ throw new Error(`@ttsc/lint: "plugins" in tsconfig plugin entry must be an object map of namespace → package specifier`);
113
+ }
114
+ const out = [];
115
+ // Track the post-`goSubpackageName` form so `a-b` and `a_b` are
116
+ // caught as colliding aliases before they reach the downstream
117
+ // contributor validator's opaque `duplicate name "a_b"` error.
118
+ // (`Object.entries` cannot itself surface duplicate string keys, so
119
+ // a verbatim-namespace guard is unreachable.)
120
+ const seenGoNames = new Map();
121
+ for (const [namespace, specifier] of Object.entries(declared)) {
122
+ if (!NAMESPACE_PATTERN.test(namespace)) {
123
+ throw new Error(`@ttsc/lint: contributor namespace ${JSON.stringify(namespace)} must match /^[a-z][a-z0-9_-]*$/`);
124
+ }
125
+ if (typeof specifier !== "string" || specifier.length === 0) {
126
+ throw new Error(`@ttsc/lint: contributor ${JSON.stringify(namespace)} must point at a non-empty package specifier or path`);
127
+ }
128
+ const goName = goSubpackageName(namespace);
129
+ const earlier = seenGoNames.get(goName);
130
+ if (earlier !== undefined) {
131
+ throw new Error(`@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)`);
132
+ }
133
+ seenGoNames.set(goName, namespace);
134
+ const plugin = loadContributorPluginViaRequire(specifier, context, namespace);
135
+ out.push({ name: goName, source: plugin.source });
136
+ }
137
+ return out;
138
+ }
139
+ function loadContributorPluginViaRequire(specifier, context, namespace, anchorFile) {
140
+ // Resolve relative paths and node_modules lookups from the file that
141
+ // declared the specifier, when one is available — `lint.config.json`
142
+ // / `lint.config.cjs` reach this code with their own path on disk,
143
+ // and ttsc's tsconfig plugin entry falls back to the project root.
144
+ const anchor = anchorFile ??
145
+ node_path_1.default.join(node_path_1.default.resolve(context.cwd ?? context.projectRoot), "__lint_contributor_resolve__.cjs");
146
+ const requireFromProject = (0, node_module_1.createRequire)(anchor);
147
+ let resolved;
148
+ try {
149
+ resolved = requireFromProject.resolve(specifier);
150
+ }
151
+ catch (error) {
152
+ throw new Error(`@ttsc/lint: failed to resolve contributor "${namespace}" via "${specifier}": ${error instanceof Error ? error.message : String(error)}`);
153
+ }
154
+ let mod;
155
+ try {
156
+ mod = requireFromProject(resolved);
157
+ }
158
+ catch (error) {
159
+ throw new Error(`@ttsc/lint: failed to load contributor "${namespace}" from ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ return validatePluginShape(unwrapDefault(mod), namespace, resolved);
162
+ }
163
+ function resolveConfigFileContributors(context, inlineNames) {
164
+ // Read the new `rules` / `extends` fields with a one-time fallback to
165
+ // the legacy `config` field. The legacy fallback warns once per
166
+ // ttsc invocation so existing tsconfigs keep working through the
167
+ // deprecation window without crashing CI.
168
+ const { hasInlineRules, extendsPath } = readSeverityConfig(context);
169
+ if (hasInlineRules) {
170
+ // Inline rules → no lint.config.* file involved. Skip discovery so
171
+ // we don't pull in plugins from an unrelated file.
172
+ return [];
173
+ }
174
+ const configPath = extendsPath !== undefined
175
+ ? node_path_1.default.resolve(tsconfigBaseDir(context), extendsPath)
176
+ : findLintConfigFile(context);
177
+ if (!configPath || !node_fs_1.default.existsSync(configPath))
178
+ return [];
179
+ const entries = readConfigPluginEntries(configPath, context);
180
+ // Dedup against the Go-subpackage form (post hyphen→underscore
181
+ // transform). The inline arm has already applied `goSubpackageName`
182
+ // when it produced `inlineNames`, so comparing on the original
183
+ // hyphenated namespace would always miss for hyphenated namespaces
184
+ // and emit a colliding contributor that `validatePluginContributors`
185
+ // later rejects as a duplicate name.
186
+ const occupied = new Set(inlineNames);
187
+ const out = [];
188
+ for (const entry of entries) {
189
+ const goName = goSubpackageName(entry.namespace);
190
+ if (occupied.has(goName))
191
+ continue; // tsconfig inline wins
192
+ occupied.add(goName);
193
+ out.push({ name: goName, source: entry.source });
194
+ }
195
+ return out;
196
+ }
197
+ /**
198
+ * Resolves the inline-rule vs file-path split between the new `rules` /
199
+ * `extends` fields and the legacy `config` field.
200
+ *
201
+ * - `rules` (object) routes the discovery loop away from any `lint.config.*` file
202
+ * — the inline map is authoritative.
203
+ * - `extends` (string) routes the file walk to a fixed path.
204
+ * - `config` (legacy) silently maps onto the equivalent new field. The
205
+ * user-facing deprecation notice is emitted by the Go sidecar so that a
206
+ * single ttsc invocation prints exactly one warning regardless of how many
207
+ * entry points (JS factory, Go binary) parse the same key.
208
+ * - Mixing legacy and new keys, or mixing `rules` with `extends`, is rejected
209
+ * outright so users don't end up with silent precedence surprises.
210
+ */
211
+ function readSeverityConfig(context) {
212
+ const entry = context.plugin;
213
+ const rules = entry.rules;
214
+ const extendsRaw = entry.extends;
215
+ const legacy = entry.config;
216
+ const hasNewRules = rules !== undefined;
217
+ const hasExtends = extendsRaw !== undefined;
218
+ const hasLegacy = legacy !== undefined;
219
+ if (hasLegacy && (hasNewRules || hasExtends)) {
220
+ throw new Error(`@ttsc/lint: tsconfig plugin entry mixes legacy "config" with the new "rules"/"extends" fields; remove "config" (deprecated)`);
221
+ }
222
+ if (hasNewRules && hasExtends) {
223
+ throw new Error(`@ttsc/lint: "rules" and "extends" cannot be combined on a single plugin entry; put base rules in the "extends" file and inline overrides in lint.config.ts itself`);
224
+ }
225
+ if (hasNewRules) {
226
+ if (typeof rules !== "object" || rules === null || Array.isArray(rules)) {
227
+ const actual = Array.isArray(rules)
228
+ ? "array"
229
+ : rules === null
230
+ ? "null"
231
+ : typeof rules;
232
+ throw new Error(`@ttsc/lint: "rules" must be a rule severity map, got ${actual}`);
233
+ }
234
+ return { hasInlineRules: true, extendsPath: undefined };
235
+ }
236
+ if (hasExtends) {
237
+ if (typeof extendsRaw !== "string" || extendsRaw.length === 0) {
238
+ throw new Error(`@ttsc/lint: "extends" must be a non-empty string path`);
239
+ }
240
+ return { hasInlineRules: false, extendsPath: extendsRaw };
241
+ }
242
+ if (hasLegacy) {
243
+ if (typeof legacy === "object" &&
244
+ legacy !== null &&
245
+ !Array.isArray(legacy)) {
246
+ return { hasInlineRules: true, extendsPath: undefined };
247
+ }
248
+ if (typeof legacy === "string" && legacy.length > 0) {
249
+ return { hasInlineRules: false, extendsPath: legacy };
250
+ }
251
+ throw new Error(`@ttsc/lint: legacy "config" must be a non-empty string path or a rule severity map, got ${typeof legacy}`);
252
+ }
253
+ return { hasInlineRules: false, extendsPath: undefined };
254
+ }
255
+ function findLintConfigFile(context) {
256
+ // Mirror the Go-side discovery loop: walk from the tsconfig directory
257
+ // upward, returning the first directory that has exactly one of the
258
+ // candidate filenames. Multiple files in the same directory is treated
259
+ // as ambiguous and skipped (the Go side raises a hard error on the
260
+ // duplicate; here we leave it to the binary's own discovery to surface
261
+ // the issue once with one canonical message).
262
+ let dir = tsconfigBaseDir(context);
263
+ while (true) {
264
+ const matches = LINT_CONFIG_FILENAMES.map((name) => node_path_1.default.join(dir, name)).filter((candidate) => node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile());
265
+ if (matches.length === 1) {
266
+ return matches[0];
267
+ }
268
+ if (matches.length > 1) {
269
+ return undefined; // ambiguous — defer to the Go side's error
270
+ }
271
+ const parent = node_path_1.default.dirname(dir);
272
+ if (parent === dir)
273
+ return undefined;
274
+ dir = parent;
275
+ }
276
+ }
277
+ function tsconfigBaseDir(context) {
278
+ const cwd = context.cwd ?? context.projectRoot;
279
+ if (context.tsconfig) {
280
+ return node_path_1.default.dirname(node_path_1.default.isAbsolute(context.tsconfig)
281
+ ? context.tsconfig
282
+ : node_path_1.default.join(cwd, context.tsconfig));
283
+ }
284
+ return node_path_1.default.resolve(cwd);
285
+ }
286
+ function readConfigPluginEntries(configPath, context) {
287
+ const ext = node_path_1.default.extname(configPath).toLowerCase();
288
+ if (ext === ".json") {
289
+ return readJsonConfigPlugins(configPath, context);
290
+ }
291
+ if (ext === ".js" || ext === ".cjs") {
292
+ return readCjsConfigPlugins(configPath);
293
+ }
294
+ // .ts, .cts, .mts, .mjs all need ttsx-side evaluation. .mjs sneaks in
295
+ // here because Node can't `require()` an ESM file synchronously.
296
+ return readTtsxConfigPlugins(configPath, context);
297
+ }
298
+ function readJsonConfigPlugins(configPath, context) {
299
+ let parsed;
300
+ try {
301
+ // Strip a leading UTF-8 BOM so files saved by Windows editors
302
+ // (Notepad++, some VS Code setups) round-trip through `JSON.parse`
303
+ // without an opaque "Unexpected token" failure.
304
+ const text = node_fs_1.default.readFileSync(configPath, "utf8").replace(/^\uFEFF/, "");
305
+ parsed = JSON.parse(text);
306
+ }
307
+ catch (error) {
308
+ throw new Error(`@ttsc/lint: failed to parse lint config ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
309
+ }
310
+ // In JSON, plugin values can only be strings (npm specifiers) — there
311
+ // is no way to attach an in-memory plugin object inside a JSON file.
312
+ return collectPluginObjectsFromConfig(parsed)
313
+ .flatMap((map) => Object.entries(map))
314
+ .map(([namespace, value]) => {
315
+ if (!NAMESPACE_PATTERN.test(namespace)) {
316
+ throw new Error(`@ttsc/lint: lint config ${configPath} namespace ${JSON.stringify(namespace)} must match /^[a-z][a-z0-9_-]*$/`);
317
+ }
318
+ if (typeof value !== "string" || value.length === 0) {
319
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(namespace)} must point at a package specifier string`);
320
+ }
321
+ const plugin = loadContributorPluginViaRequire(value, context, namespace, configPath);
322
+ return { namespace, source: plugin.source };
323
+ });
324
+ }
325
+ function readCjsConfigPlugins(configPath) {
326
+ let mod;
327
+ try {
328
+ const requireFromConfig = (0, node_module_1.createRequire)(configPath);
329
+ mod = requireFromConfig(configPath);
330
+ }
331
+ catch (error) {
332
+ throw new Error(`@ttsc/lint: failed to load lint config ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
333
+ }
334
+ return collectPluginObjectsFromConfig(unwrapDefault(mod))
335
+ .flatMap((map) => Object.entries(map))
336
+ .map(([namespace, value]) => normalizePluginValue(namespace, value, configPath));
337
+ }
338
+ const TTSX_EXTRACTOR_SCRIPT = `import * as importedConfig from %CONFIG_IMPORT%;
339
+
340
+ declare const process: {
341
+ argv: string[];
342
+ cwd(): string;
343
+ stdout: { write(value: string): void };
344
+ stderr: { write(value: string): void };
345
+ exit(code?: number): never;
346
+ };
347
+
348
+ try {
349
+ let current: unknown = importedConfig;
350
+ for (let i = 0; i < 8; i++) {
351
+ if (isObject(current) && hasOwn(current, "default")) {
352
+ current = (current as Record<string, unknown>).default;
353
+ continue;
354
+ }
355
+ break;
356
+ }
357
+ if (typeof current === "function") {
358
+ current = await (current as () => unknown | Promise<unknown>)();
359
+ }
360
+ const pluginMaps = collectPluginObjects(current);
361
+ const entries: Array<{ namespace: string; source: string }> = [];
362
+ for (const map of pluginMaps) {
363
+ for (const [namespace, value] of Object.entries(map)) {
364
+ const source = extractPluginSource(value);
365
+ if (source === undefined) continue;
366
+ entries.push({ namespace, source });
367
+ }
368
+ }
369
+ process.stdout.write(JSON.stringify({ entries }));
370
+ } catch (error) {
371
+ process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
372
+ process.exit(1);
373
+ }
374
+
375
+ function isObject(value: unknown): value is Record<string, unknown> {
376
+ return value !== null && typeof value === "object";
377
+ }
378
+
379
+ function hasOwn(value: Record<string, unknown>, key: string): boolean {
380
+ return Object.prototype.hasOwnProperty.call(value, key);
381
+ }
382
+
383
+ function collectPluginObjects(value: unknown): Array<Record<string, unknown>> {
384
+ const out: Array<Record<string, unknown>> = [];
385
+ visit(value);
386
+ return out;
387
+
388
+ function visit(node: unknown): void {
389
+ if (Array.isArray(node)) {
390
+ for (const item of node) visit(item);
391
+ return;
392
+ }
393
+ if (!isObject(node)) return;
394
+ if (hasOwn(node, "plugins") && isObject(node.plugins)) {
395
+ out.push(node.plugins as Record<string, unknown>);
396
+ }
397
+ }
398
+ }
399
+
400
+ function extractPluginSource(value: unknown): string | undefined {
401
+ if (typeof value === "string") return value;
402
+ if (!isObject(value)) return undefined;
403
+ // ESM-from-CJS interop wraps CJS modules' \`exports.default\` so the
404
+ // plugin object can land under a \`.default\` indirection. Walk a few
405
+ // hops so contributors authored as \`export default plugin\` and
406
+ // contributors authored as plain \`module.exports = plugin\` both
407
+ // resolve identically.
408
+ let current: Record<string, unknown> = value;
409
+ for (let i = 0; i < 4; i++) {
410
+ if (typeof current.source === "string") break;
411
+ const next = current.default;
412
+ if (!isObject(next)) break;
413
+ current = next;
414
+ }
415
+ const source = current.source;
416
+ return typeof source === "string" ? source : undefined;
417
+ }
418
+ `;
419
+ function readTtsxConfigPlugins(configPath, _context) {
420
+ const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsc-lint-cfg-"));
421
+ try {
422
+ linkNearestNodeModules(tempDir, node_path_1.default.dirname(configPath));
423
+ const loaderPath = node_path_1.default.join(tempDir, "loader.mts");
424
+ const tsconfigPath = node_path_1.default.join(tempDir, "tsconfig.json");
425
+ const importSpecifier = relativeImportSpecifier(tempDir, configPath);
426
+ const loaderSource = TTSX_EXTRACTOR_SCRIPT.replace("%CONFIG_IMPORT%", JSON.stringify(importSpecifier));
427
+ node_fs_1.default.writeFileSync(loaderPath, loaderSource, "utf8");
428
+ node_fs_1.default.writeFileSync(tsconfigPath, JSON.stringify({
429
+ compilerOptions: {
430
+ // `.mjs` and `.js` configs reach this loader because Node
431
+ // cannot `require()` ESM synchronously; ttsx's TypeScript
432
+ // project compiler refuses to admit them without `allowJs`,
433
+ // so set it alongside the strict bypass to make the loader
434
+ // tolerant of any of `lint.config.{ts,cts,mts,js,mjs,cjs}`.
435
+ allowImportingTsExtensions: true,
436
+ allowJs: true,
437
+ checkJs: false,
438
+ module: "ESNext",
439
+ moduleResolution: "bundler",
440
+ noImplicitAny: false,
441
+ outDir: node_path_1.default.join(tempDir, "out").replace(/\\/g, "/"),
442
+ rewriteRelativeImportExtensions: true,
443
+ rootDir: "/",
444
+ skipLibCheck: true,
445
+ strict: false,
446
+ target: "ES2022",
447
+ },
448
+ files: [
449
+ loaderPath.replace(/\\/g, "/"),
450
+ configPath.replace(/\\/g, "/"),
451
+ ],
452
+ }, null, 2), "utf8");
453
+ const ttsxBinary = process.env.TTSC_TTSX_BINARY ?? "ttsx";
454
+ const args = ["--project", tsconfigPath, "--cwd", tempDir, loaderPath];
455
+ if (process.env.TTSC_TSGO_BINARY) {
456
+ args.unshift("--binary", process.env.TTSC_TSGO_BINARY);
457
+ }
458
+ const env = nodeConfigLoaderEnv(configPath);
459
+ const command = ttsxThroughNodeIfNeeded(ttsxBinary);
460
+ const result = (0, node_child_process_1.spawnSync)(command.binary, [...command.prefix, ...args], {
461
+ cwd: tempDir,
462
+ env,
463
+ encoding: "utf8",
464
+ maxBuffer: 1024 * 1024 * 16,
465
+ // 60s cap so a runaway top-level await / infinite loop in the
466
+ // user's lint config can't hang the entire ttsc invocation.
467
+ timeout: 60_000,
468
+ windowsHide: true,
469
+ });
470
+ if (result.error) {
471
+ throw new Error(`@ttsc/lint: failed to spawn ttsx for ${configPath}: ${result.error.message}`);
472
+ }
473
+ if (result.signal) {
474
+ throw new Error(`@ttsc/lint: ttsx evaluation of ${configPath} was killed by signal ${result.signal} ` +
475
+ `(likely the 60s timeout). Simplify the config or move heavy work out of top-level.`);
476
+ }
477
+ if (result.status !== 0) {
478
+ throw new Error(`@ttsc/lint: lint config ${configPath} evaluation failed:\n${result.stderr || result.stdout}`);
479
+ }
480
+ let payload;
481
+ try {
482
+ payload = JSON.parse(result.stdout);
483
+ }
484
+ catch (error) {
485
+ throw new Error(`@ttsc/lint: lint config ${configPath} evaluator returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
486
+ }
487
+ const entries = payload.entries ?? [];
488
+ return entries.map((entry) => {
489
+ // The ttsx extractor already resolved each plugin object's
490
+ // `source` to an absolute directory path. Validate the shape but
491
+ // skip the specifier-resolution branch — re-routing a directory
492
+ // through `createRequire().resolve` would fail.
493
+ if (!NAMESPACE_PATTERN.test(entry.namespace)) {
494
+ throw new Error(`@ttsc/lint: lint config ${configPath} namespace ${JSON.stringify(entry.namespace)} must match /^[a-z][a-z0-9_-]*$/`);
495
+ }
496
+ if (typeof entry.source !== "string" || entry.source.length === 0) {
497
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(entry.namespace)} did not expose a "source" string`);
498
+ }
499
+ if (!node_path_1.default.isAbsolute(entry.source)) {
500
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(entry.namespace)} "source" must be absolute; got ${JSON.stringify(entry.source)}`);
501
+ }
502
+ if (!node_fs_1.default.existsSync(entry.source) ||
503
+ !node_fs_1.default.statSync(entry.source).isDirectory()) {
504
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(entry.namespace)} "source" must be an existing directory: ${entry.source}`);
505
+ }
506
+ return { namespace: entry.namespace, source: entry.source };
507
+ });
508
+ }
509
+ finally {
510
+ node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
511
+ }
512
+ }
513
+ // ────────────────────────────────────────────────────────────────────────────
514
+ // Shared helpers
515
+ // ────────────────────────────────────────────────────────────────────────────
516
+ function collectPluginObjectsFromConfig(value) {
517
+ const out = [];
518
+ const visit = (node) => {
519
+ if (Array.isArray(node)) {
520
+ for (const item of node)
521
+ visit(item);
522
+ return;
523
+ }
524
+ if (typeof node !== "object" || node === null)
525
+ return;
526
+ const obj = node;
527
+ const plugins = obj.plugins;
528
+ if (typeof plugins === "object" &&
529
+ plugins !== null &&
530
+ !Array.isArray(plugins)) {
531
+ out.push(plugins);
532
+ }
533
+ };
534
+ visit(value);
535
+ return out;
536
+ }
537
+ function normalizePluginValue(namespace, value, configPath) {
538
+ if (!NAMESPACE_PATTERN.test(namespace)) {
539
+ throw new Error(`@ttsc/lint: lint config ${configPath} namespace ${JSON.stringify(namespace)} must match /^[a-z][a-z0-9_-]*$/`);
540
+ }
541
+ if (typeof value === "string") {
542
+ // For .js/.cjs configs, a string value would be an npm specifier
543
+ // (matching JSON behavior). require it through the config's own
544
+ // module resolution.
545
+ const requireFromConfig = (0, node_module_1.createRequire)(configPath);
546
+ let resolved;
547
+ try {
548
+ resolved = requireFromConfig.resolve(value);
549
+ }
550
+ catch (error) {
551
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(namespace)} failed to resolve "${value}": ${error instanceof Error ? error.message : String(error)}`);
552
+ }
553
+ const mod = requireFromConfig(resolved);
554
+ const plugin = validatePluginShape(unwrapDefault(mod), namespace, resolved);
555
+ return { namespace, source: plugin.source };
556
+ }
557
+ if (typeof value === "object" && value !== null) {
558
+ const plugin = validatePluginShape(value, namespace, configPath);
559
+ return { namespace, source: plugin.source };
560
+ }
561
+ throw new Error(`@ttsc/lint: lint config ${configPath} plugin ${JSON.stringify(namespace)} must be a plugin object or specifier string; got ${typeof value}`);
562
+ }
563
+ function validatePluginShape(candidate, namespace, origin) {
564
+ if (typeof candidate !== "object" || candidate === null) {
565
+ throw new Error(`@ttsc/lint: contributor "${namespace}" loaded from ${origin} did not export an object`);
566
+ }
567
+ const obj = candidate;
568
+ if (typeof obj.source !== "string" || obj.source.length === 0) {
569
+ throw new Error(`@ttsc/lint: contributor "${namespace}" from ${origin} is missing a string "source" field`);
570
+ }
571
+ if (!node_path_1.default.isAbsolute(obj.source)) {
572
+ throw new Error(`@ttsc/lint: contributor "${namespace}" from ${origin} "source" must be an absolute path; got ${JSON.stringify(obj.source)}. Use path.resolve(__dirname, ...).`);
573
+ }
574
+ if (!node_fs_1.default.existsSync(obj.source) || !node_fs_1.default.statSync(obj.source).isDirectory()) {
575
+ throw new Error(`@ttsc/lint: contributor "${namespace}" from ${origin} "source" must be an existing directory: ${obj.source}`);
576
+ }
577
+ return obj;
578
+ }
579
+ function unwrapDefault(mod) {
580
+ let current = mod;
581
+ for (let i = 0; i < 8; i++) {
582
+ if (current !== null &&
583
+ typeof current === "object" &&
584
+ "default" in current) {
585
+ const next = current.default;
586
+ if (next !== undefined) {
587
+ current = next;
588
+ continue;
589
+ }
590
+ }
591
+ break;
592
+ }
593
+ return current;
594
+ }
595
+ function findNearestNodeModules(start) {
596
+ let dir = node_path_1.default.resolve(start);
597
+ while (true) {
598
+ const candidate = node_path_1.default.join(dir, "node_modules");
599
+ if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isDirectory()) {
600
+ return candidate;
601
+ }
602
+ const parent = node_path_1.default.dirname(dir);
603
+ if (parent === dir)
604
+ return undefined;
605
+ dir = parent;
606
+ }
607
+ }
608
+ function linkNearestNodeModules(tempDir, sourceDir) {
609
+ const nodeModules = findNearestNodeModules(sourceDir);
610
+ if (!nodeModules)
611
+ return;
612
+ const link = node_path_1.default.join(tempDir, "node_modules");
613
+ try {
614
+ node_fs_1.default.symlinkSync(nodeModules, link, "junction");
615
+ }
616
+ catch (error) {
617
+ const code = error.code;
618
+ if (code !== "EEXIST") {
619
+ throw new Error(`@ttsc/lint: failed to link node_modules from ${nodeModules}: ${error instanceof Error ? error.message : String(error)}`);
620
+ }
621
+ }
622
+ }
623
+ function relativeImportSpecifier(fromDir, target) {
624
+ let rel = node_path_1.default.relative(fromDir, target).replace(/\\/g, "/");
625
+ if (!rel.startsWith("./") && !rel.startsWith("../")) {
626
+ rel = "./" + rel;
627
+ }
628
+ return rel;
629
+ }
630
+ function nodeConfigLoaderEnv(configPath) {
631
+ const env = { ...process.env };
632
+ const parts = [];
633
+ const nodeModules = findNearestNodeModules(node_path_1.default.dirname(configPath));
634
+ if (nodeModules)
635
+ parts.push(nodeModules);
636
+ if (env.NODE_PATH)
637
+ parts.push(env.NODE_PATH);
638
+ if (parts.length > 0) {
639
+ env.NODE_PATH = parts.join(node_path_1.default.delimiter);
640
+ }
641
+ return env;
642
+ }
643
+ function ttsxThroughNodeIfNeeded(binary) {
644
+ const ext = node_path_1.default.extname(binary).toLowerCase();
645
+ if ([".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"].includes(ext)) {
646
+ return { binary: process.execPath, prefix: [binary] };
647
+ }
648
+ return { binary, prefix: [] };
29
649
  }
30
650
  //# sourceMappingURL=index.js.map