ts-builds 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +93 -0
  2. package/dist/cli.js +288 -9
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -254,6 +254,99 @@ Wire-up:
254
254
  aggregate a different root. Finding zero sidecars exits nonzero — an empty sweep
255
255
  must not silently pass CI.
256
256
 
257
+ The sidecar also has a `rules` field with per-rule counts: errors, warnings, findings
258
+ silenced by an `eslint-disable` comment.
259
+
260
+ ### Lint telemetry (opt-in)
261
+
262
+ `ts-builds lint` can send each run's per-rule counts to an endpoint you run. That shows which
263
+ rules fire, which ones people silence, and how both change between releases. It's off unless you
264
+ turn it on. ts-builds reads these settings from the environment only, so `ts-builds.config.json`
265
+ has no switch for it. A committed script, workflow or config command that sets them still turns it
266
+ on for everyone who runs it, so set them only where you mean to, such as your own CI.
267
+
268
+ | Env var | Effect |
269
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
270
+ | `TS_BUILDS_TELEMETRY=1` | Turns it on. |
271
+ | `TS_BUILDS_TELEMETRY_URL` | Where to POST. Must be `https`, or `http` to localhost. With telemetry on and no URL, `lint` warns once and sends nothing. |
272
+ | `TS_BUILDS_TELEMETRY_DEBUG=1` | Prints the payload to stderr and sends nothing, whether or not telemetry is on. |
273
+ | `DO_NOT_TRACK=1` | Turns it off, whatever else is set. |
274
+
275
+ Each run sends one JSON object:
276
+
277
+ ```json
278
+ {
279
+ "schema": 1,
280
+ "package": "@acme/api",
281
+ "project": "3f9c2a1b7d4e6f80",
282
+ "ci": true,
283
+ "agent": "claude-code",
284
+ "fix": false,
285
+ "durationMs": 1840,
286
+ "files": 212,
287
+ "fatal": 0,
288
+ "versions": {
289
+ "node": "24.20.0",
290
+ "ts-builds": "4.2.0",
291
+ "eslint": "10.14.0",
292
+ "typescript": "6.0.3",
293
+ "eslint-plugin-functype": "2.125.1"
294
+ },
295
+ "enabled": { "functype/no-let": "error", "functype/prefer-option": "warn" },
296
+ "rules": { "functype/prefer-option": { "errors": 0, "warnings": 3, "silenced": 2 } }
297
+ }
298
+ ```
299
+
300
+ - **Never sent:** file paths, source code or message text.
301
+ - **Sent as-is:** the `package.json` name, left out when there isn't one, and rule ids as your config
302
+ names them. Plugin names and versions are sent only when they look like an npm package name and a
303
+ semver version.
304
+ - **`project`:** a salted hash of the git remote, so one repo's packages group together. It's a
305
+ stable pseudonym, not anonymity: anyone who knows the remote URL can compute it. It's left out
306
+ when there's no remote.
307
+ - **`agent`:** set when a coding agent runs the lint. Today that's Claude Code, from `CLAUDECODE=1`.
308
+ - **`versions`:** Node, ts-builds, ESLint, TypeScript, and each configured plugin. A plugin's
309
+ installed version wins over the `meta.version` it declares, which is hand-written and can go stale.
310
+ When the installed package can't be found, the `meta.version` is used.
311
+ - **`enabled`:** the plugin rules that are on, with their severity, read from the first linted file's
312
+ config. It tells a rule with no findings apart from a rule that's off. Core rules are left out.
313
+
314
+ It never changes a run's result. It sends after lint finishes and its output is printed, waits at
315
+ most 2 seconds, and ignores every error, so the exit code and output stay the same. A run that
316
+ fails before linting, such as one with a broken config, sends nothing.
317
+
318
+ It doesn't cover `lint.useProjectEslint`, which spawns your own ESLint so ts-builds never sees the
319
+ results. It also doesn't cover scripts that call `eslint` directly instead of `ts-builds lint`.
320
+
321
+ To turn it on in GitHub Actions, set both variables on the job:
322
+
323
+ ```yaml
324
+ env:
325
+ TS_BUILDS_TELEMETRY: "1"
326
+ TS_BUILDS_TELEMETRY_URL: ${{ vars.TS_BUILDS_TELEMETRY_URL }}
327
+ ```
328
+
329
+ Under Turbo's strict env mode, a task only sees the variables Turbo is told about, plus a built-in
330
+ set that includes `CI`. List these so `ts-builds lint` sees them. Adding `globalPassThroughEnv` also
331
+ turns on strict mode for every task, so check that your other tasks still get the variables they need:
332
+
333
+ ```jsonc
334
+ // turbo.json
335
+ {
336
+ "globalPassThroughEnv": [
337
+ "TS_BUILDS_TELEMETRY",
338
+ "TS_BUILDS_TELEMETRY_URL",
339
+ "TS_BUILDS_TELEMETRY_DEBUG",
340
+ "DO_NOT_TRACK",
341
+ "GITHUB_ACTIONS",
342
+ "CLAUDECODE",
343
+ ],
344
+ }
345
+ ```
346
+
347
+ Pass-through variables don't change Turbo's cache key. A cached lint task doesn't run, so it sends
348
+ nothing.
349
+
257
350
  ## Extendable Configs
258
351
 
259
352
  ts-builds exports base configurations you can extend:
package/dist/cli.js CHANGED
@@ -5,7 +5,8 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { $, Do, List, Map, Option, Set, Try } from "functype";
7
7
  import { Fs, Process } from "functype-os";
8
- import { spawn, spawnSync } from "node:child_process";
8
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
9
+ import { createHash } from "node:crypto";
9
10
  import { createInterface } from "node:readline";
10
11
  import { gzipSync } from "node:zlib";
11
12
 
@@ -68,6 +69,270 @@ function loadConfig() {
68
69
  };
69
70
  }
70
71
 
72
+ // src/cli/telemetry.ts
73
+ /**
74
+ * Opt-in lint telemetry: per-rule counts from a `ts-builds lint` run, sent to an endpoint you
75
+ * choose so you can see which rules fire, which get silenced, and whether that changes over time.
76
+ *
77
+ * Off unless `TS_BUILDS_TELEMETRY=1` is set, and `DO_NOT_TRACK=1` always turns it off. ts-builds
78
+ * reads these from the environment only. A committed script, workflow or config command that sets
79
+ * them still turns it on for everyone who runs it. It never sends file paths, source or messages,
80
+ * the send waits at most `TELEMETRY_TIMEOUT_MS`, and it never changes the run's exit code.
81
+ */
82
+ /** Bump when the payload's shape changes incompatibly, so the receiver can tell versions apart. */
83
+ const TELEMETRY_SCHEMA_VERSION = 1;
84
+ /** The most a send can add to a lint run. A send that takes longer is dropped. */
85
+ const TELEMETRY_TIMEOUT_MS = 2e3;
86
+ /** A fixed salt, so the repo id is a stable pseudonym. Anyone who knows the remote URL can recompute it. */
87
+ const PROJECT_SALT = "ts-builds-telemetry-v1";
88
+ const LOG_PREFIX = "[ts-builds telemetry]";
89
+ const NO_COUNTS = {
90
+ errors: 0,
91
+ warnings: 0,
92
+ silenced: 0
93
+ };
94
+ const isOn = (value) => value !== void 0 && /^(1|true|yes)$/i.test(value.trim());
95
+ /** One hit per message that has a rule id. Messages without one are fatal, and counted apart. */
96
+ function hits(result) {
97
+ const reported = result.messages.flatMap((m) => m.ruleId === null ? [] : [{
98
+ ruleId: m.ruleId,
99
+ field: m.severity === 2 ? "errors" : "warnings"
100
+ }]);
101
+ const suppressed = result.suppressedMessages.flatMap((m) => m.ruleId === null ? [] : [{
102
+ ruleId: m.ruleId,
103
+ field: "silenced"
104
+ }]);
105
+ return [...reported, ...suppressed];
106
+ }
107
+ /** Per-rule counts for a lint run. Pure: the same results always give the same, key-sorted output. */
108
+ function ruleStats(results) {
109
+ const unsorted = results.flatMap(hits).reduce((acc, h) => {
110
+ const current = acc[h.ruleId] ?? NO_COUNTS;
111
+ return {
112
+ ...acc,
113
+ [h.ruleId]: {
114
+ ...current,
115
+ [h.field]: current[h.field] + 1
116
+ }
117
+ };
118
+ }, {});
119
+ return {
120
+ rules: Object.fromEntries(Object.keys(unsorted).toSorted().map((id) => [id, unsorted[id] ?? NO_COUNTS])),
121
+ fatal: results.reduce((n, r) => n + r.messages.filter((m) => m.ruleId === null).length, 0)
122
+ };
123
+ }
124
+ const LOCAL_HOSTS = [
125
+ "localhost",
126
+ "127.0.0.1",
127
+ "[::1]"
128
+ ];
129
+ /** An https URL, or http only to this machine (for tests and a local receiver). */
130
+ function isAllowedEndpoint(endpoint) {
131
+ return Try(() => new URL(endpoint)).map((url) => url.protocol === "https:" || url.protocol === "http:" && LOCAL_HOSTS.includes(url.hostname)).orElse(false);
132
+ }
133
+ /**
134
+ * What to do with this run's telemetry, from the environment alone. `DO_NOT_TRACK` wins over
135
+ * everything. `TS_BUILDS_TELEMETRY_DEBUG` prints the payload and sends nothing, so you can see
136
+ * exactly what would go out before turning it on.
137
+ */
138
+ function telemetryMode(env) {
139
+ if (isOn(env.DO_NOT_TRACK)) return { kind: "off" };
140
+ if (isOn(env.TS_BUILDS_TELEMETRY_DEBUG)) return { kind: "debug" };
141
+ if (!isOn(env.TS_BUILDS_TELEMETRY)) return { kind: "off" };
142
+ const endpoint = env.TS_BUILDS_TELEMETRY_URL?.trim() ?? "";
143
+ if (endpoint === "") return {
144
+ kind: "off",
145
+ warning: "TS_BUILDS_TELEMETRY is on but TS_BUILDS_TELEMETRY_URL isn't set, so nothing was sent."
146
+ };
147
+ if (!isAllowedEndpoint(endpoint)) return {
148
+ kind: "off",
149
+ warning: "TS_BUILDS_TELEMETRY_URL must be an https URL (http only to localhost), so nothing was sent."
150
+ };
151
+ return {
152
+ kind: "send",
153
+ endpoint
154
+ };
155
+ }
156
+ /**
157
+ * A git remote reduced to `host/owner/repo`, lowercase: no protocol, credentials, port or `.git`.
158
+ * `git@github.com:a/b.git`, `https://user:token@github.com/a/b` and `ssh://git@github.com/a/b`
159
+ * all give `github.com/a/b`.
160
+ */
161
+ function normalizeRemote(remote) {
162
+ const trimmed = remote.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
163
+ const scp = /^[^@/\s]+@([^:/\s]+):(.+)$/.exec(trimmed);
164
+ if (scp) return `${scp[1]}/${scp[2]}`.replace(/^\/+/, "").toLowerCase();
165
+ return Try(() => new URL(trimmed)).map((url) => `${url.hostname}${url.pathname}`.replace(/\/+$/, "")).orElse(trimmed).toLowerCase();
166
+ }
167
+ /** The salted id for a remote. Stable across machines and clones, so one repo's packages group together. */
168
+ function projectId(remote) {
169
+ return createHash("sha256").update(`${PROJECT_SALT}:${normalizeRemote(remote)}`).digest("hex").slice(0, 16);
170
+ }
171
+ /** The coding agent driving this run, when its env marker is set. */
172
+ function detectAgent(env) {
173
+ return env.CLAUDECODE === "1" ? Option("claude-code") : Option.none();
174
+ }
175
+ function isCi(env) {
176
+ return isOn(env.CI) || env.GITHUB_ACTIONS === "true";
177
+ }
178
+ /**
179
+ * The npm package that provides a rule's plugin, splitting the rule id the way ESLint does: at the
180
+ * first slash, or the second for a scoped plugin. `functype/x` is `eslint-plugin-functype`,
181
+ * `@typescript-eslint/x` is `@typescript-eslint/eslint-plugin`, `@scope/name/x` is
182
+ * `@scope/eslint-plugin-name`, and `a/b/c` is `eslint-plugin-a`. Core rules have no plugin.
183
+ */
184
+ function pluginPackage(ruleId) {
185
+ const parts = ruleId.split("/");
186
+ const first = parts.at(0) ?? "";
187
+ if (parts.length < 2 || first === "") return Option.none();
188
+ if (!first.startsWith("@")) return Option(`eslint-plugin-${first}`);
189
+ const name = parts.length > 2 ? parts.at(1) : void 0;
190
+ return Option(name === void 0 ? `${first}/eslint-plugin` : `${first}/eslint-plugin-${name}`);
191
+ }
192
+ /** An npm package name, so a plugin's `meta.name` can't carry a path or other free text. */
193
+ const NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
194
+ /** A semver version, so a plugin's `meta.version` can't carry free text. */
195
+ const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
196
+ const ancestors = (dir) => {
197
+ const parent = dirname(dir);
198
+ return parent === dir ? [dir] : [dir, ...ancestors(parent)];
199
+ };
200
+ const readJson = (path) => Try(() => JSON.parse(readFileSync(path, "utf-8"))).toOption();
201
+ const versionIn = (path) => readJson(path).flatMap((pkg) => typeof pkg.version === "string" ? Option(pkg.version) : Option.none());
202
+ /**
203
+ * A package's installed version, found the way Node finds it: `node_modules/<name>` in `fromDir` or
204
+ * any ancestor. Inside a `node_modules` folder it also checks `<name>` beside it, which is where pnpm
205
+ * puts a package's dependencies (`.pnpm/ts-builds@x/node_modules/eslint-plugin-x`).
206
+ */
207
+ function installedVersion(name, fromDir) {
208
+ const found = ancestors(fromDir).flatMap((dir) => [join(dir, "node_modules", name, "package.json"), ...basename(dir) === "node_modules" ? [join(dir, name, "package.json")] : []]).find((path) => existsSync(path));
209
+ return found === void 0 ? Option.none() : versionIn(found);
210
+ }
211
+ /** ts-builds' own root: the nearest ancestor of this module whose `package.json` is ts-builds. */
212
+ function tsBuildsRoot() {
213
+ const here = dirname(fileURLToPath(import.meta.url));
214
+ const root = ancestors(here).find((dir) => readJson(join(dir, "package.json")).map((pkg) => pkg.name === "ts-builds").orElse(false));
215
+ return root === void 0 ? Option.none() : Option(root);
216
+ }
217
+ /** The git remote of the repo holding `dir`, if there is one. */
218
+ function gitRemote(dir) {
219
+ return Try(() => execFileSync("git", [
220
+ "config",
221
+ "--get",
222
+ "remote.origin.url"
223
+ ], {
224
+ cwd: dir,
225
+ encoding: "utf-8",
226
+ stdio: [
227
+ "ignore",
228
+ "pipe",
229
+ "ignore"
230
+ ],
231
+ timeout: 2e3
232
+ }).trim()).toOption().filter((out) => out !== "");
233
+ }
234
+ const severityOf = (entry) => {
235
+ const level = Array.isArray(entry) ? entry[0] : entry;
236
+ if (level === 2 || level === "error") return Option("error");
237
+ if (level === 1 || level === "warn") return Option("warn");
238
+ return Option.none();
239
+ };
240
+ const asRecord = (value) => typeof value === "object" && value !== null ? value : {};
241
+ const asString = (value) => typeof value === "string" && value !== "" ? Option(value) : Option.none();
242
+ /**
243
+ * The plugins and the plugin rules that are on, from `eslint.calculateConfigForFile(...)`. Core
244
+ * rules are left out, and so is ESLint's own `@` namespace.
245
+ */
246
+ function activeConfig(calculated) {
247
+ const config = asRecord(calculated);
248
+ return {
249
+ plugins: Object.entries(asRecord(config.plugins)).filter(([namespace]) => namespace !== "@").flatMap(([namespace, plugin]) => {
250
+ const meta = asRecord(asRecord(plugin).meta);
251
+ return asString(meta.name).filter((name) => name.length <= 214 && NPM_NAME.test(name)).or(pluginPackage(`${namespace}/rule`)).map((name) => ({
252
+ name,
253
+ metaVersion: asString(meta.version).filter((v) => SEMVER.test(v))
254
+ })).toArray();
255
+ }),
256
+ enabled: Object.fromEntries(Object.entries(asRecord(config.rules)).filter(([id]) => id.includes("/")).flatMap(([id, entry]) => severityOf(entry).map((severity) => [id, severity]).toArray()).toSorted(([a], [b]) => a.localeCompare(b)))
257
+ };
258
+ }
259
+ /**
260
+ * Versions worth comparing across runs: the toolchain, plus every configured plugin and every plugin
261
+ * whose rules reported. A plugin's installed version wins over the one its `meta` claims, because a
262
+ * hand-written `meta.version` can go stale.
263
+ */
264
+ function versionsFor(run, rules, active) {
265
+ const root = tsBuildsRoot();
266
+ const lookup = (name) => installedVersion(name, run.dir).or(root.flatMap((dir) => installedVersion(name, dir)));
267
+ const configured = active.map((a) => a.plugins).orElse([]);
268
+ const claimed = (name) => Option(configured.find((p) => p.name === name)).flatMap((p) => p.metaVersion);
269
+ const plugins = [...configured.map((p) => p.name), ...Object.keys(rules).flatMap((id) => pluginPackage(id).toArray())].filter((name, i, all) => all.indexOf(name) === i).toSorted();
270
+ const typescript = lookup("typescript").map((v) => [["typescript", v]]).orElse([]);
271
+ const pluginVersions = plugins.flatMap((name) => lookup(name).or(claimed(name)).map((v) => [name, v]).toArray());
272
+ return {
273
+ node: process.versions.node,
274
+ "ts-builds": root.flatMap((dir) => versionIn(join(dir, "package.json"))).orElse("unknown"),
275
+ eslint: run.eslintVersion,
276
+ ...Object.fromEntries([...typescript, ...pluginVersions])
277
+ };
278
+ }
279
+ /** The payload for one lint run. Holds counts and versions only: no file paths, source or messages. */
280
+ function buildTelemetryPayload(run, env, active = Option.none()) {
281
+ const stats = ruleStats(run.results);
282
+ const project = gitRemote(run.dir).map(projectId);
283
+ const agent = detectAgent(env);
284
+ return {
285
+ schema: 1,
286
+ ...run.packageName.map((name) => ({ package: name })).orElse({}),
287
+ ...project.map((id) => ({ project: id })).orElse({}),
288
+ ci: isCi(env),
289
+ ...agent.map((name) => ({ agent: name })).orElse({}),
290
+ fix: run.fix,
291
+ durationMs: Math.round(run.durationMs),
292
+ files: run.results.length,
293
+ fatal: stats.fatal,
294
+ versions: versionsFor(run, stats.rules, active),
295
+ ...active.map((a) => ({ enabled: a.enabled })).orElse({}),
296
+ rules: stats.rules
297
+ };
298
+ }
299
+ /**
300
+ * POST the payload. Resolves `true` on a 2xx and `false` otherwise; never rejects. The send is
301
+ * awaited rather than left in flight, so the run's exit can't cut it off or be held open by it.
302
+ */
303
+ async function sendTelemetry(payload, endpoint, timeoutMs = TELEMETRY_TIMEOUT_MS) {
304
+ return (await Try.async(async () => {
305
+ const res = await fetch(endpoint, {
306
+ method: "POST",
307
+ headers: { "content-type": "application/json" },
308
+ body: JSON.stringify(payload),
309
+ signal: AbortSignal.timeout(timeoutMs)
310
+ });
311
+ await res.body?.cancel();
312
+ return res.ok;
313
+ })).orElse(false);
314
+ }
315
+ /**
316
+ * Report one lint run, as the environment asks. Never throws and never changes the run's result:
317
+ * a failure anywhere in here is dropped.
318
+ */
319
+ async function reportLintTelemetry(run, env = process.env, log = (line) => process.stderr.write(`${line}\n`)) {
320
+ await Try.async(async () => {
321
+ const mode = telemetryMode(env);
322
+ if (mode.kind === "off") {
323
+ if (mode.warning !== void 0) log(`${LOG_PREFIX} ${mode.warning}`);
324
+ return;
325
+ }
326
+ const payload = buildTelemetryPayload(run, env, (run.configFor === void 0 ? Option.none() : (await Try.async(run.configFor)).toOption()).map(activeConfig));
327
+ if (mode.kind === "debug") {
328
+ log(`${LOG_PREFIX} TS_BUILDS_TELEMETRY_DEBUG is set, so this was printed and not sent:`);
329
+ log(JSON.stringify(payload, null, 2));
330
+ return;
331
+ }
332
+ await sendTelemetry(payload, mode.endpoint);
333
+ });
334
+ }
335
+
71
336
  // src/cli/lint-report.ts
72
337
  /**
73
338
  * Current sidecar schema version. Bump when the shape changes incompatibly so
@@ -78,14 +343,15 @@ const LINT_REPORT_VERSION = 1;
78
343
  function lintReportPath(dir = targetDir) {
79
344
  return join(dir, ".ts-builds", "lint-report.json");
80
345
  }
81
- /** Package name from `<dir>/package.json`, falling back to the directory name. */
82
- function readPackageName(dir = targetDir) {
346
+ /** The `name` in `<dir>/package.json`, when the file exists, parses and has a non-empty name. */
347
+ function packageJsonName(dir = targetDir) {
83
348
  const pkgPath = join(dir, "package.json");
84
- if (existsSync(pkgPath)) try {
85
- const { name } = JSON.parse(readFileSync(pkgPath, "utf-8"));
86
- if (typeof name === "string" && name.length > 0) return name;
87
- } catch {}
88
- return basename(dir);
349
+ if (!existsSync(pkgPath)) return Option.none();
350
+ return Try(() => JSON.parse(readFileSync(pkgPath, "utf-8")).name).toOption().flatMap((name) => typeof name === "string" && name.length > 0 ? Option(name) : Option.none());
351
+ }
352
+ /** Package name from `<dir>/package.json`, falling back to the directory name. For local use only. */
353
+ function readPackageName(dir = targetDir) {
354
+ return packageJsonName(dir).orElse(basename(dir));
89
355
  }
90
356
  /** Build a report from ESLint results. */
91
357
  function buildLintReport(results, fix, pkg) {
@@ -111,7 +377,8 @@ function buildLintReport(results, fix, pkg) {
111
377
  filePath: r.filePath,
112
378
  errorCount: r.errorCount,
113
379
  warningCount: r.warningCount
114
- }))
380
+ })),
381
+ rules: ruleStats(results).rules
115
382
  };
116
383
  }
117
384
  /** Build a fatal report for a run that threw before producing results. */
@@ -435,8 +702,10 @@ async function runLint(check = false) {
435
702
  fix,
436
703
  cwd: targetDir
437
704
  });
705
+ const startedAt = performance.now();
438
706
  const results = await eslint.lintFiles(lintTargets(config, targetDir));
439
707
  if (fix) await ESLint.outputFixes(results);
708
+ const durationMs = performance.now() - startedAt;
440
709
  const report = buildLintReport(results, fix, pkg);
441
710
  await writeLintReport(report, targetDir);
442
711
  try {
@@ -446,6 +715,16 @@ async function runLint(check = false) {
446
715
  if (output) await writeAll(process.stdout, output);
447
716
  if (unparsed.length > 0) await writeAll(process.stdout, `\n✗ ${unparsed.length} file(s) in ${config.testDir} aren't in a tsconfig, so type-aware rules can't read them.\n Add "${relative(targetDir, testDir).split(sep).join("/")}/**/*" to "include" in tsconfig.json, or point parserOptions.project at a tsconfig.eslint.json that has it.\n`);
448
717
  } catch {}
718
+ const firstFile = results.at(0)?.filePath;
719
+ await reportLintTelemetry({
720
+ results,
721
+ fix,
722
+ durationMs,
723
+ packageName: packageJsonName(targetDir),
724
+ eslintVersion: ESLint.version,
725
+ dir: targetDir,
726
+ configFor: () => firstFile === void 0 ? Promise.resolve(void 0) : eslint.calculateConfigForFile(firstFile)
727
+ });
449
728
  return report.errorCount > 0 ? 1 : 0;
450
729
  } catch (err) {
451
730
  console.error(err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-builds",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Shared TypeScript configuration files for library templates. Provides standardized ESLint, Prettier, Vitest, TypeScript, and build configs.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -66,9 +66,9 @@
66
66
  "@vitest/coverage-v8": "^5.0.3",
67
67
  "@vitest/ui": "^5.0.3",
68
68
  "eslint": "^10.10.0",
69
- "eslint-config-functype": "^2.124.0",
69
+ "eslint-config-functype": "^2.124.1",
70
70
  "eslint-config-prettier": "^10.1.8",
71
- "eslint-plugin-functype": "^2.124.0",
71
+ "eslint-plugin-functype": "^2.124.1",
72
72
  "eslint-plugin-prettier": "^5.5.6",
73
73
  "eslint-plugin-simple-import-sort": "^14.0.0",
74
74
  "functype": "^1.11.0",