@ttsc/lint 0.10.2 → 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.
- package/README.md +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +62 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- 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
|
-
|
|
24
|
-
|
|
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
|