@konvert7/klint 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/cli.ts +114 -34
- package/core/arch.ts +10 -11
- package/core/paths.ts +9 -0
- package/core/rule-helpers.ts +2 -2
- package/core/runner.ts +7 -7
- package/package.json +7 -5
- package/rules/no-consecutive-array-push.ts +2 -2
- package/rules/no-nested-template-literals.ts +2 -2
- package/rules/no-object-in-template.ts +1 -1
- package/rules/no-single-char-class.ts +2 -2
- package/rules/no-string-match.ts +2 -2
- package/rules/no-sync-in-async.ts +2 -2
- package/rules/no-unguarded-json-parse.ts +2 -2
- package/rules/prefer-at.ts +2 -2
- package/rules/prefer-nullish-coalescing-assign.ts +50 -12
- package/rules/prefer-string-raw-regexp.ts +2 -2
- package/rules/prefer-string-raw.ts +2 -2
- package/rules/prefer-string-replaceall.ts +3 -3
- package/tools/semantic-release-root-npm.js +15 -6
package/README.md
CHANGED
|
@@ -127,23 +127,30 @@ Maintainer migration notes live in [`docs/rust-engine.md`](docs/rust-engine.md).
|
|
|
127
127
|
| `ts` | Runs the full TypeScript implementation. This is the default. | Maximum compatibility |
|
|
128
128
|
| `rust` | Runs only rules supported by the Rust engine. Unsupported TS-only rules are rejected. | Strict Rust smoke tests and native-engine debugging |
|
|
129
129
|
| `compare` | Runs TS and Rust on the same supported config and fails if JSON output differs. Requires `--json`. | Parity verification while porting rules |
|
|
130
|
-
| `auto` | Runs Rust-supported rules in Rust, TS-only rules in TypeScript, then merges output. | Recommended experimental migration path |
|
|
130
|
+
| `auto` | Runs Rust-supported rules in Rust, TS-only rules and unsupported plugin defaults in TypeScript, then merges output. | Recommended experimental migration path |
|
|
131
131
|
|
|
132
132
|
Examples:
|
|
133
133
|
|
|
134
134
|
```sh
|
|
135
135
|
klint --engine auto
|
|
136
|
-
klint --engine rust --json # strict Rust-only run; rejects unsupported
|
|
137
|
-
klint --engine compare --json # parity check; rejects unsupported
|
|
136
|
+
klint --engine rust --json # strict Rust-only run; rejects unsupported rules
|
|
137
|
+
klint --engine compare --json # parity check; rejects unsupported rules
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
Rust currently supports:
|
|
141
141
|
|
|
142
142
|
- `arch` rules: imports, forbidden patterns, singleton locations
|
|
143
143
|
- `no-unguarded-json-parse`
|
|
144
|
+
- `no-sync-in-async`
|
|
144
145
|
- `no-nested-template-literals`
|
|
145
146
|
- `no-consecutive-array-push`
|
|
146
147
|
- `no-string-match`
|
|
148
|
+
- `sonar/no-single-char-class`
|
|
149
|
+
- `sonar/prefer-at`
|
|
150
|
+
- `sonar/prefer-nullish-coalescing-assign`
|
|
151
|
+
- `sonar/prefer-string-replaceall`
|
|
152
|
+
- `sonar/prefer-string-raw`
|
|
153
|
+
- `sonar/prefer-string-raw-regexp`
|
|
147
154
|
|
|
148
155
|
These rules remain TypeScript-owned because they need TypeScript semantic information:
|
|
149
156
|
|
|
@@ -280,8 +287,8 @@ arch:
|
|
|
280
287
|
| `no-sync-in-async` | No | Sync filesystem calls inside async functions |
|
|
281
288
|
| `no-floating-promise` | Yes | Promise-returning call whose result is discarded |
|
|
282
289
|
| `no-misused-promises` | Yes | Async function passed where a sync callback is expected |
|
|
283
|
-
| `no-async-predicate` |
|
|
284
|
-
| `no-date-equality` |
|
|
290
|
+
| `no-async-predicate` | Yes | Async predicate passed to array filtering/search methods |
|
|
291
|
+
| `no-date-equality` | Yes | Date values compared by object identity |
|
|
285
292
|
| `no-optional-chain-on-non-nullable` | Yes | Optional chaining on values TypeScript knows are non-nullable |
|
|
286
293
|
| `no-object-in-template` | Yes | Object values interpolated into template strings |
|
|
287
294
|
| `no-nested-template-literals` | No | Template literals nested inside template literals |
|
package/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type SpawnSyncReturns, spawnSync } from "node:child_process";
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
@@ -27,11 +27,18 @@ import type {
|
|
|
27
27
|
import { BUILT_IN_PLUGINS } from "./plugins/index";
|
|
28
28
|
import { BUILT_IN_RULES } from "./rules/index";
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const RUST_SUPPORTED_RULES = new Set([
|
|
31
31
|
"no-consecutive-array-push",
|
|
32
32
|
"no-nested-template-literals",
|
|
33
33
|
"no-string-match",
|
|
34
|
+
"no-sync-in-async",
|
|
34
35
|
"no-unguarded-json-parse",
|
|
36
|
+
"sonar/no-single-char-class",
|
|
37
|
+
"sonar/prefer-at",
|
|
38
|
+
"sonar/prefer-nullish-coalescing-assign",
|
|
39
|
+
"sonar/prefer-string-replaceall",
|
|
40
|
+
"sonar/prefer-string-raw",
|
|
41
|
+
"sonar/prefer-string-raw-regexp",
|
|
35
42
|
]);
|
|
36
43
|
|
|
37
44
|
interface CliOptions {
|
|
@@ -105,7 +112,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
105
112
|
const root = resolve(configDir, raw.root ?? ".");
|
|
106
113
|
|
|
107
114
|
if (engine === "rust") {
|
|
108
|
-
runRustEngine({
|
|
115
|
+
runRustEngine({ fix, json, raw, root, rulesFile, startedAt });
|
|
109
116
|
return;
|
|
110
117
|
}
|
|
111
118
|
if (
|
|
@@ -162,7 +169,14 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
162
169
|
);
|
|
163
170
|
|
|
164
171
|
if (engine === "compare") {
|
|
165
|
-
runCompareEngine({
|
|
172
|
+
runCompareEngine({
|
|
173
|
+
fix,
|
|
174
|
+
json,
|
|
175
|
+
raw,
|
|
176
|
+
root,
|
|
177
|
+
rulesFile,
|
|
178
|
+
tsViolations: violations,
|
|
179
|
+
});
|
|
166
180
|
return;
|
|
167
181
|
}
|
|
168
182
|
|
|
@@ -245,14 +259,13 @@ function writeTextOutput(
|
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
function runRustEngine({
|
|
248
|
-
configDir,
|
|
249
262
|
fix,
|
|
250
263
|
json,
|
|
251
264
|
raw,
|
|
265
|
+
root,
|
|
252
266
|
rulesFile,
|
|
253
267
|
startedAt,
|
|
254
268
|
}: {
|
|
255
|
-
configDir: string;
|
|
256
269
|
fix: boolean;
|
|
257
270
|
json: boolean;
|
|
258
271
|
raw: {
|
|
@@ -260,6 +273,7 @@ function runRustEngine({
|
|
|
260
273
|
rules?: Record<string, RuleConfigValue>;
|
|
261
274
|
arch?: unknown;
|
|
262
275
|
};
|
|
276
|
+
root: string;
|
|
263
277
|
rulesFile?: string;
|
|
264
278
|
startedAt: number;
|
|
265
279
|
}): void {
|
|
@@ -274,16 +288,22 @@ function runRustEngine({
|
|
|
274
288
|
process.exit(1);
|
|
275
289
|
}
|
|
276
290
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
)
|
|
291
|
+
const { configDir: rustConfigDir, cleanup } = writeRustEngineConfig({ raw, root });
|
|
292
|
+
const command = resolveRustEngineCommand(rustConfigDir);
|
|
293
|
+
let result: SpawnSyncReturns<string>;
|
|
294
|
+
try {
|
|
295
|
+
if (process.argv.includes("--debug") || process.argv.includes("-debug")) {
|
|
296
|
+
process.stderr.write(
|
|
297
|
+
`[klint:debug] rust engine: ${command.bin} ${command.args.join(" ")}\n`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
result = spawnSync(command.bin, command.args, {
|
|
301
|
+
cwd: import.meta.dir,
|
|
302
|
+
encoding: "utf-8",
|
|
303
|
+
});
|
|
304
|
+
} finally {
|
|
305
|
+
cleanup();
|
|
282
306
|
}
|
|
283
|
-
const result = spawnSync(command.bin, command.args, {
|
|
284
|
-
cwd: import.meta.dir,
|
|
285
|
-
encoding: "utf-8",
|
|
286
|
-
});
|
|
287
307
|
|
|
288
308
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
289
309
|
if (json) {
|
|
@@ -300,14 +320,13 @@ function runRustEngine({
|
|
|
300
320
|
}
|
|
301
321
|
|
|
302
322
|
function runCompareEngine({
|
|
303
|
-
configDir,
|
|
304
323
|
fix,
|
|
305
324
|
json,
|
|
306
325
|
raw,
|
|
326
|
+
root,
|
|
307
327
|
rulesFile,
|
|
308
328
|
tsViolations,
|
|
309
329
|
}: {
|
|
310
|
-
configDir: string;
|
|
311
330
|
fix: boolean;
|
|
312
331
|
json: boolean;
|
|
313
332
|
raw: {
|
|
@@ -315,6 +334,7 @@ function runCompareEngine({
|
|
|
315
334
|
rules?: Record<string, RuleConfigValue>;
|
|
316
335
|
arch?: unknown;
|
|
317
336
|
};
|
|
337
|
+
root: string;
|
|
318
338
|
rulesFile?: string;
|
|
319
339
|
tsViolations: ReturnType<typeof runKlint>;
|
|
320
340
|
}): void {
|
|
@@ -335,11 +355,17 @@ function runCompareEngine({
|
|
|
335
355
|
}
|
|
336
356
|
|
|
337
357
|
const tsResult = toJsonCliResult(tsViolations);
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
358
|
+
const { configDir: rustConfigDir, cleanup } = writeRustEngineConfig({ raw, root });
|
|
359
|
+
const rustCommand = resolveRustEngineCommand(rustConfigDir);
|
|
360
|
+
let rustResult: SpawnSyncReturns<string>;
|
|
361
|
+
try {
|
|
362
|
+
rustResult = spawnSync(rustCommand.bin, rustCommand.args, {
|
|
363
|
+
cwd: import.meta.dir,
|
|
364
|
+
encoding: "utf-8",
|
|
365
|
+
});
|
|
366
|
+
} finally {
|
|
367
|
+
cleanup();
|
|
368
|
+
}
|
|
343
369
|
|
|
344
370
|
const rustStatus = rustResult.status ?? 1;
|
|
345
371
|
if (rustResult.stderr) process.stderr.write(rustResult.stderr);
|
|
@@ -366,6 +392,35 @@ function runCompareEngine({
|
|
|
366
392
|
process.exit(tsResult.status);
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
function writeRustEngineConfig({
|
|
396
|
+
raw,
|
|
397
|
+
root,
|
|
398
|
+
}: {
|
|
399
|
+
raw: {
|
|
400
|
+
include?: string[];
|
|
401
|
+
plugins?: string[];
|
|
402
|
+
rules?: Record<string, RuleConfigValue>;
|
|
403
|
+
arch?: unknown;
|
|
404
|
+
};
|
|
405
|
+
root: string;
|
|
406
|
+
}): { configDir: string; cleanup: () => void } {
|
|
407
|
+
const tempDir = mkdtempSync(join(tmpdir(), "klint-rust-engine-"));
|
|
408
|
+
writeFileSync(
|
|
409
|
+
join(tempDir, "klint.config.json"),
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
root,
|
|
412
|
+
include: raw.include ?? ["."],
|
|
413
|
+
plugins: [],
|
|
414
|
+
rules: resolveEffectiveRules(raw.plugins, raw.rules ?? {}),
|
|
415
|
+
arch: raw.arch,
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
return {
|
|
419
|
+
configDir: tempDir,
|
|
420
|
+
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
369
424
|
function runAutoEngine({
|
|
370
425
|
fix,
|
|
371
426
|
json,
|
|
@@ -400,12 +455,13 @@ function runAutoEngine({
|
|
|
400
455
|
process.exit(1);
|
|
401
456
|
}
|
|
402
457
|
|
|
403
|
-
const
|
|
458
|
+
const effectiveRules = resolveEffectiveRules(raw.plugins, raw.rules ?? {});
|
|
459
|
+
const { rustRules, tsRules } = splitRulesForAuto(effectiveRules);
|
|
404
460
|
const tsViolations = runKlint(
|
|
405
461
|
{
|
|
406
462
|
root,
|
|
407
463
|
include: raw.include ?? ["."],
|
|
408
|
-
plugins:
|
|
464
|
+
plugins: [],
|
|
409
465
|
rules: { ...customRulesMap, ...tsRules },
|
|
410
466
|
},
|
|
411
467
|
customRules
|
|
@@ -433,7 +489,7 @@ function splitRulesForAuto(rules: Record<string, RuleConfigValue>): {
|
|
|
433
489
|
const tsRules: Record<string, RuleConfigValue> = {};
|
|
434
490
|
|
|
435
491
|
for (const [name, value] of Object.entries(rules)) {
|
|
436
|
-
if (
|
|
492
|
+
if (RUST_SUPPORTED_RULES.has(name)) rustRules[name] = value;
|
|
437
493
|
else tsRules[name] = value;
|
|
438
494
|
}
|
|
439
495
|
|
|
@@ -642,31 +698,51 @@ function rustEngineUnsupportedReason({
|
|
|
642
698
|
}): string | undefined {
|
|
643
699
|
if (fix) return "KLINT_ENGINE=rust does not support --fix";
|
|
644
700
|
if (rulesFile) return "KLINT_ENGINE=rust does not support --rules";
|
|
645
|
-
|
|
646
|
-
|
|
701
|
+
let effectiveRules: Record<string, RuleConfigValue>;
|
|
702
|
+
try {
|
|
703
|
+
effectiveRules = resolveEffectiveRules(raw.plugins, raw.rules ?? {});
|
|
704
|
+
} catch (error) {
|
|
705
|
+
return error instanceof Error
|
|
706
|
+
? error.message
|
|
707
|
+
: "KLINT_ENGINE=rust failed to load plugins";
|
|
708
|
+
}
|
|
709
|
+
const activeSupportedRules = Object.entries(effectiveRules).filter(
|
|
647
710
|
([name, value]) =>
|
|
648
|
-
ruleConfigSeverity(value) !== "off" &&
|
|
711
|
+
ruleConfigSeverity(value) !== "off" && RUST_SUPPORTED_RULES.has(name)
|
|
649
712
|
);
|
|
650
|
-
const unsupportedRules = Object.entries(
|
|
713
|
+
const unsupportedRules = Object.entries(effectiveRules)
|
|
651
714
|
.filter(
|
|
652
715
|
([name, value]) =>
|
|
653
|
-
ruleConfigSeverity(value) !== "off" && !
|
|
716
|
+
ruleConfigSeverity(value) !== "off" && !RUST_SUPPORTED_RULES.has(name)
|
|
654
717
|
)
|
|
655
718
|
.map(([name]) => name);
|
|
656
719
|
if (!raw.arch && activeSupportedRules.length === 0 && unsupportedRules.length === 0) {
|
|
657
|
-
return "KLINT_ENGINE=rust requires an arch config or supported
|
|
720
|
+
return "KLINT_ENGINE=rust requires an arch config or supported rule";
|
|
658
721
|
}
|
|
659
722
|
if (unsupportedRules.length > 0) {
|
|
660
723
|
return [
|
|
661
|
-
"Rust engine currently supports arch rules and selected
|
|
724
|
+
"Rust engine currently supports arch rules and selected rules only",
|
|
662
725
|
"",
|
|
663
|
-
"Unsupported
|
|
726
|
+
"Unsupported rules:",
|
|
664
727
|
...unsupportedRules.map((rule) => `- ${rule}`),
|
|
665
728
|
].join("\n");
|
|
666
729
|
}
|
|
667
730
|
return undefined;
|
|
668
731
|
}
|
|
669
732
|
|
|
733
|
+
function resolveEffectiveRules(
|
|
734
|
+
plugins: string[] | undefined,
|
|
735
|
+
rules: Record<string, RuleConfigValue>
|
|
736
|
+
): Record<string, RuleConfigValue> {
|
|
737
|
+
const pluginDefaults: Record<string, RuleConfigValue> = {};
|
|
738
|
+
for (const pluginName of plugins ?? []) {
|
|
739
|
+
const plugin = BUILT_IN_PLUGINS[pluginName];
|
|
740
|
+
if (!plugin) throw new Error(`Unknown klint plugin: "${pluginName}"`);
|
|
741
|
+
Object.assign(pluginDefaults, plugin.rules);
|
|
742
|
+
}
|
|
743
|
+
return { ...pluginDefaults, ...rules };
|
|
744
|
+
}
|
|
745
|
+
|
|
670
746
|
function ruleConfigSeverity(value: RuleConfigValue): string | undefined {
|
|
671
747
|
if (typeof value === "string") return value;
|
|
672
748
|
return value.severity;
|
|
@@ -685,7 +761,11 @@ function jsonPayloadEquals(a: unknown, b: unknown): boolean {
|
|
|
685
761
|
}
|
|
686
762
|
|
|
687
763
|
function canonicalJson(value: unknown): unknown {
|
|
688
|
-
if (Array.isArray(value))
|
|
764
|
+
if (Array.isArray(value)) {
|
|
765
|
+
return value
|
|
766
|
+
.map(canonicalJson)
|
|
767
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
768
|
+
}
|
|
689
769
|
if (!value || typeof value !== "object") return value;
|
|
690
770
|
return Object.fromEntries(
|
|
691
771
|
Object.entries(value)
|
package/core/arch.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { dirname, isAbsolute,
|
|
1
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
import { walkAst } from "./ast";
|
|
4
|
+
import { relativeSlashPath, toSlashPath } from "./paths";
|
|
4
5
|
import type { ArchConfig, Severity, Violation } from "./types";
|
|
5
6
|
|
|
6
|
-
const toSlash = (p: string) => p.replaceAll("\\", "/");
|
|
7
|
-
|
|
8
7
|
interface AliasEntry {
|
|
9
8
|
/** The prefix to match (pattern with `/*` stripped, e.g. `"@"` from `"@/*"`). */
|
|
10
9
|
prefix: string;
|
|
@@ -31,7 +30,7 @@ function loadPathAliases(root: string): AliasEntry[] {
|
|
|
31
30
|
const prefix = isWildcard ? pattern.slice(0, -2) : pattern;
|
|
32
31
|
const targetStr = targets[0];
|
|
33
32
|
const targetBase = targetStr.endsWith("/*") ? targetStr.slice(0, -2) : targetStr;
|
|
34
|
-
entries.push({ prefix, base:
|
|
33
|
+
entries.push({ prefix, base: toSlashPath(resolve(base, targetBase)), isWildcard });
|
|
35
34
|
}
|
|
36
35
|
return entries;
|
|
37
36
|
}
|
|
@@ -41,7 +40,7 @@ function resolveAlias(importPath: string, aliases: AliasEntry[]): string | undef
|
|
|
41
40
|
if (alias.isWildcard) {
|
|
42
41
|
const matchPrefix = `${alias.prefix}/`;
|
|
43
42
|
if (importPath.startsWith(matchPrefix)) {
|
|
44
|
-
return
|
|
43
|
+
return toSlashPath(resolve(alias.base, importPath.slice(matchPrefix.length)));
|
|
45
44
|
}
|
|
46
45
|
} else if (importPath === alias.prefix) {
|
|
47
46
|
return alias.base;
|
|
@@ -62,7 +61,7 @@ function isBareSpecifier(path: string): boolean {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
function globToPrefix(glob: string, root: string): string {
|
|
65
|
-
return
|
|
64
|
+
return toSlashPath(resolve(root, glob.split("/**")[0].split("/*")[0].split("*")[0]));
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
function resolveGlobs(
|
|
@@ -145,7 +144,7 @@ function scanImports(
|
|
|
145
144
|
if (isBareSpecifier(path)) {
|
|
146
145
|
resolved = resolveAlias(path, aliases) ?? path;
|
|
147
146
|
} else {
|
|
148
|
-
resolved =
|
|
147
|
+
resolved = toSlashPath(resolve(fileDir, path));
|
|
149
148
|
}
|
|
150
149
|
const { line } = src.getLineAndCharacterOfPosition(specifierNode.getStart());
|
|
151
150
|
records.push({ path, resolved, isTypeOnly, line: line + 1 });
|
|
@@ -176,7 +175,7 @@ export function runArchRules(
|
|
|
176
175
|
if (isBareSpecifier(imp.resolved)) continue;
|
|
177
176
|
if (rule["type-only"] === "allow" && imp.isTypeOnly) continue;
|
|
178
177
|
|
|
179
|
-
const relFile =
|
|
178
|
+
const relFile = relativeSlashPath(root, file);
|
|
180
179
|
|
|
181
180
|
if (rule.deny !== undefined) {
|
|
182
181
|
const denyPrefixes = resolveLayerPrefixes(rule.deny, layers, root);
|
|
@@ -235,7 +234,7 @@ export function runArchRules(
|
|
|
235
234
|
|
|
236
235
|
for (const rule of arch.singleton ?? []) {
|
|
237
236
|
const severity: Severity = rule.severity ?? "error";
|
|
238
|
-
const onlyFile =
|
|
237
|
+
const onlyFile = toSlashPath(resolve(root, rule.only));
|
|
239
238
|
const inFiles = rule.in
|
|
240
239
|
? resolveLayerFiles(rule.in, layers, root, allFiles)
|
|
241
240
|
: allFiles;
|
|
@@ -294,7 +293,7 @@ function scanJsxElements(
|
|
|
294
293
|
if (!targets.has(tagName.text)) return;
|
|
295
294
|
const { line } = src.getLineAndCharacterOfPosition(tagName.getStart());
|
|
296
295
|
violations.push({
|
|
297
|
-
file:
|
|
296
|
+
file: relativeSlashPath(root, file),
|
|
298
297
|
line: line + 1,
|
|
299
298
|
message,
|
|
300
299
|
rule: ruleName,
|
|
@@ -321,7 +320,7 @@ function scanLinesForPattern(
|
|
|
321
320
|
for (let i = 0; i < lines.length; i++) {
|
|
322
321
|
if (lines[i].includes(pattern)) {
|
|
323
322
|
violations.push({
|
|
324
|
-
file:
|
|
323
|
+
file: relativeSlashPath(root, file),
|
|
325
324
|
line: i + 1,
|
|
326
325
|
message,
|
|
327
326
|
rule: ruleName,
|
package/core/paths.ts
ADDED
package/core/rule-helpers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { createProgram } from "./ast";
|
|
3
|
+
import { relativeSlashPath } from "./paths";
|
|
4
4
|
import type { KlintRule, RawViolation, RuleMeta } from "./types";
|
|
5
5
|
import { defineRule } from "./types";
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ export function defineAstRule(opts: {
|
|
|
34
34
|
report(node, message) {
|
|
35
35
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
36
36
|
violations.push({
|
|
37
|
-
file:
|
|
37
|
+
file: relativeSlashPath(root, sourceFile.fileName),
|
|
38
38
|
line: line + 1,
|
|
39
39
|
message,
|
|
40
40
|
});
|
package/core/runner.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join,
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { BUILT_IN_PLUGINS } from "../plugins/index";
|
|
4
4
|
import { BUILT_IN_RULES } from "../rules/index";
|
|
5
5
|
import { runArchRules } from "./arch";
|
|
6
6
|
import { clearAstCache } from "./ast";
|
|
7
|
+
import { relativeSlashPath, toSlashPath } from "./paths";
|
|
7
8
|
import type {
|
|
8
9
|
KlintConfig,
|
|
9
10
|
KlintRule,
|
|
@@ -29,7 +30,7 @@ function walk(dir: string, root: string, excludes: string[] = []): string[] {
|
|
|
29
30
|
const out: string[] = [];
|
|
30
31
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
31
32
|
const full = join(dir, entry.name);
|
|
32
|
-
const rel =
|
|
33
|
+
const rel = relativeSlashPath(root, full);
|
|
33
34
|
if (entry.isDirectory()) {
|
|
34
35
|
if (
|
|
35
36
|
excludes.some(
|
|
@@ -38,8 +39,7 @@ function walk(dir: string, root: string, excludes: string[] = []): string[] {
|
|
|
38
39
|
)
|
|
39
40
|
continue;
|
|
40
41
|
out.push(...walk(full, root, excludes));
|
|
41
|
-
} else if (/\.(tsx?|jsx?|mts|cts)$/.test(entry.name))
|
|
42
|
-
out.push(full.replaceAll("\\", "/"));
|
|
42
|
+
} else if (/\.(tsx?|jsx?|mts|cts)$/.test(entry.name)) out.push(toSlashPath(full));
|
|
43
43
|
}
|
|
44
44
|
return out;
|
|
45
45
|
}
|
|
@@ -68,8 +68,8 @@ function resolveFiles(
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function matchPattern(relPath: string, pattern: string): boolean {
|
|
71
|
-
const norm = relPath
|
|
72
|
-
const p = pattern
|
|
71
|
+
const norm = toSlashPath(relPath);
|
|
72
|
+
const p = toSlashPath(pattern);
|
|
73
73
|
if (p === "." || p === "**") return true;
|
|
74
74
|
if (!p.includes("*")) return norm === p || norm.startsWith(`${p}/`);
|
|
75
75
|
return globToRegExp(p).test(norm);
|
|
@@ -107,7 +107,7 @@ function applyPatterns(files: string[], patterns: string[], root: string): strin
|
|
|
107
107
|
const includes = patterns.filter((p) => !p.startsWith("!"));
|
|
108
108
|
const excludes = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
|
|
109
109
|
return files.filter((file) => {
|
|
110
|
-
const rel =
|
|
110
|
+
const rel = relativeSlashPath(root, file);
|
|
111
111
|
const included = includes.length === 0 || includes.some((p) => matchPattern(rel, p));
|
|
112
112
|
const excluded = excludes.some((p) => matchPattern(rel, p));
|
|
113
113
|
return included && !excluded;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konvert7/klint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Architecture-as-Code linter for TypeScript. Enforce layer boundaries, import rules, and singleton patterns in YAML.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"pack:check": "bun tests/pack-check.ts",
|
|
41
41
|
"native:stage": "bun tests/stage-native-binary.ts",
|
|
42
42
|
"native:smoke": "bun tests/smoke-native-binary.ts",
|
|
43
|
+
"python:stage": "bun tests/stage-python-wheel.ts",
|
|
44
|
+
"python:smoke": "bun tests/smoke-python-wheel.ts",
|
|
43
45
|
"native:release:dry-run": "bun tools/prepare-native-release.ts --version 0.0.0-dry-run",
|
|
44
46
|
"check": "biome check",
|
|
45
47
|
"check-write": "biome check --write",
|
|
@@ -62,10 +64,10 @@
|
|
|
62
64
|
"zod": "^4.4.3"
|
|
63
65
|
},
|
|
64
66
|
"optionalDependencies": {
|
|
65
|
-
"@konvert7/klint-darwin-arm64": "0.
|
|
66
|
-
"@konvert7/klint-darwin-x64": "0.
|
|
67
|
-
"@konvert7/klint-linux-x64": "0.
|
|
68
|
-
"@konvert7/klint-win32-x64": "0.
|
|
67
|
+
"@konvert7/klint-darwin-arm64": "0.21.0",
|
|
68
|
+
"@konvert7/klint-darwin-x64": "0.21.0",
|
|
69
|
+
"@konvert7/klint-linux-x64": "0.21.0",
|
|
70
|
+
"@konvert7/klint-win32-x64": "0.21.0"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@biomejs/biome": "2.4.7",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const noConsecutiveArrayPush: KlintRule = {
|
|
@@ -26,7 +26,7 @@ export const noConsecutiveArrayPush: KlintRule = {
|
|
|
26
26
|
statements[runStart].getStart()
|
|
27
27
|
);
|
|
28
28
|
violations.push({
|
|
29
|
-
file:
|
|
29
|
+
file: relativeSlashPath(root, file),
|
|
30
30
|
line: line + 1,
|
|
31
31
|
message: `${upTo - runStart} consecutive .push() calls on \`${runReceiver}\` — combine into a single .push(a, b, …) call.`,
|
|
32
32
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const noNestedTemplateLiterals: KlintRule = {
|
|
@@ -32,7 +32,7 @@ function findNestedTemplate(
|
|
|
32
32
|
if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
33
33
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
34
34
|
violations.push({
|
|
35
|
-
file:
|
|
35
|
+
file: relativeSlashPath(root, file),
|
|
36
36
|
line: line + 1,
|
|
37
37
|
message:
|
|
38
38
|
"Nested template literal — extract the inner template to a variable to improve readability.",
|
|
@@ -87,7 +87,7 @@ function wouldRenderAsObjectObject(type: ts.Type, checker: ts.TypeChecker): bool
|
|
|
87
87
|
const fileName = d.getSourceFile().fileName;
|
|
88
88
|
return (
|
|
89
89
|
!fileName.includes("/typescript/lib/lib") &&
|
|
90
|
-
!fileName.includes(
|
|
90
|
+
!fileName.includes(String.raw`\typescript\lib\lib`)
|
|
91
91
|
);
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
5
|
import type { KlintRule } from "../core/types";
|
|
6
6
|
|
|
@@ -99,7 +99,7 @@ export const noSingleCharClass: KlintRule = {
|
|
|
99
99
|
const fix = buildNodeReplacementFix(src, node, fixedRegex);
|
|
100
100
|
|
|
101
101
|
violations.push({
|
|
102
|
-
file:
|
|
102
|
+
file: relativeSlashPath(root, file),
|
|
103
103
|
line: fix.startLine,
|
|
104
104
|
message: `Character class [${toFix[0].inner}] contains a single element — remove the brackets.`,
|
|
105
105
|
fix,
|
package/rules/no-string-match.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const noStringMatch: KlintRule = {
|
|
@@ -45,7 +45,7 @@ export const noStringMatch: KlintRule = {
|
|
|
45
45
|
})()
|
|
46
46
|
: undefined;
|
|
47
47
|
violations.push({
|
|
48
|
-
file:
|
|
48
|
+
file: relativeSlashPath(root, file),
|
|
49
49
|
line: s + 1,
|
|
50
50
|
message: `Use RegExp.exec() instead of String.match() for non-global regexes — use new RegExp(${regexText}).exec(${strText}) instead.`,
|
|
51
51
|
fix,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { nearestFunctionIsAsync, walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const noSyncInAsync: KlintRule = {
|
|
@@ -31,7 +31,7 @@ export const noSyncInAsync: KlintRule = {
|
|
|
31
31
|
) {
|
|
32
32
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
33
33
|
violations.push({
|
|
34
|
-
file:
|
|
34
|
+
file: relativeSlashPath(root, file),
|
|
35
35
|
line: line + 1,
|
|
36
36
|
message: `${name}() blocks the event loop inside an async function — use the async equivalent from node:fs/promises.`,
|
|
37
37
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { isInsideTry, walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const noUnguardedJsonParse: KlintRule = {
|
|
@@ -23,7 +23,7 @@ export const noUnguardedJsonParse: KlintRule = {
|
|
|
23
23
|
) {
|
|
24
24
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
25
25
|
violations.push({
|
|
26
|
-
file:
|
|
26
|
+
file: relativeSlashPath(root, file),
|
|
27
27
|
line: line + 1,
|
|
28
28
|
message:
|
|
29
29
|
"JSON.parse() called without a surrounding try/catch — a malformed payload will throw an unhandled exception.",
|
package/rules/prefer-at.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
5
|
import type { KlintRule } from "../core/types";
|
|
6
6
|
|
|
@@ -35,7 +35,7 @@ export const preferAt: KlintRule = {
|
|
|
35
35
|
const fix = buildNodeReplacementFix(src, node, fixedCall);
|
|
36
36
|
|
|
37
37
|
violations.push({
|
|
38
|
-
file:
|
|
38
|
+
file: relativeSlashPath(root, file),
|
|
39
39
|
line: fix.startLine,
|
|
40
40
|
message: `Prefer ${baseText}.at(-${n}) over ${baseText}[${baseText}.length - ${n}] for cleaner negative indexing.`,
|
|
41
41
|
fix,
|
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const preferNullishCoalescingAssign: KlintRule = {
|
|
7
7
|
meta: {
|
|
8
8
|
description:
|
|
9
|
-
"Flags
|
|
9
|
+
"Flags explicit nullish assignment guards — `x ??= y` is the dedicated nullish assignment operator.",
|
|
10
10
|
examples: ["sonar/prefer-nullish-coalescing-assign: warn"],
|
|
11
11
|
},
|
|
12
12
|
check({ files, root, fileContents }, violations) {
|
|
13
13
|
for (const file of files) {
|
|
14
14
|
const content = fileContents.get(file) ?? "";
|
|
15
15
|
walkAst(file, content, (node, src) => {
|
|
16
|
-
if (
|
|
17
|
-
!ts.isIfStatement(node) ||
|
|
18
|
-
node.elseStatement !== undefined ||
|
|
19
|
-
!ts.isPrefixUnaryExpression(node.expression) ||
|
|
20
|
-
node.expression.operator !== ts.SyntaxKind.ExclamationToken
|
|
21
|
-
)
|
|
22
|
-
return;
|
|
16
|
+
if (!ts.isIfStatement(node) || node.elseStatement !== undefined) return;
|
|
23
17
|
|
|
24
18
|
const assignExpr = extractAssignment(node.thenStatement);
|
|
25
19
|
if (!assignExpr) return;
|
|
26
20
|
|
|
27
|
-
const xText = node.expression
|
|
21
|
+
const xText = nullishGuardTarget(node.expression, src);
|
|
22
|
+
if (!xText) return;
|
|
28
23
|
if (assignExpr.left.getText(src) !== xText) return;
|
|
29
24
|
|
|
30
25
|
const yText = assignExpr.right.getText(src);
|
|
@@ -33,9 +28,9 @@ export const preferNullishCoalescingAssign: KlintRule = {
|
|
|
33
28
|
const indent = getIndent(src, node);
|
|
34
29
|
|
|
35
30
|
violations.push({
|
|
36
|
-
file:
|
|
31
|
+
file: relativeSlashPath(root, file),
|
|
37
32
|
line: s + 1,
|
|
38
|
-
message: `Prefer \`${xText} ??= ${yText}\` over
|
|
33
|
+
message: `Prefer \`${xText} ??= ${yText}\` over explicit nullish guard assignment — ??= only assigns when null or undefined.`,
|
|
39
34
|
fix: {
|
|
40
35
|
startLine: s + 1,
|
|
41
36
|
endLine: e + 1,
|
|
@@ -66,6 +61,49 @@ function isAssign(node: ts.Expression): node is ts.BinaryExpression {
|
|
|
66
61
|
);
|
|
67
62
|
}
|
|
68
63
|
|
|
64
|
+
function nullishGuardTarget(
|
|
65
|
+
expression: ts.Expression,
|
|
66
|
+
src: ts.SourceFile
|
|
67
|
+
): string | undefined {
|
|
68
|
+
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
69
|
+
|
|
70
|
+
const direct = equalityNullishTarget(expression, src);
|
|
71
|
+
if (direct) return direct.target;
|
|
72
|
+
|
|
73
|
+
if (expression.operatorToken.kind !== ts.SyntaxKind.BarBarToken) return undefined;
|
|
74
|
+
|
|
75
|
+
const left = equalityNullishTarget(expression.left, src);
|
|
76
|
+
const right = equalityNullishTarget(expression.right, src);
|
|
77
|
+
if (!left || !right || left.target !== right.target) return undefined;
|
|
78
|
+
|
|
79
|
+
const values = new Set([left.value, right.value]);
|
|
80
|
+
if (values.has("null") && values.has("undefined")) return left.target;
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function equalityNullishTarget(
|
|
85
|
+
expression: ts.Expression,
|
|
86
|
+
src: ts.SourceFile
|
|
87
|
+
): { target: string; value: "null" | "undefined" } | undefined {
|
|
88
|
+
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
89
|
+
|
|
90
|
+
const operator = expression.operatorToken.kind;
|
|
91
|
+
if (
|
|
92
|
+
operator !== ts.SyntaxKind.EqualsEqualsToken &&
|
|
93
|
+
operator !== ts.SyntaxKind.EqualsEqualsEqualsToken
|
|
94
|
+
)
|
|
95
|
+
return undefined;
|
|
96
|
+
|
|
97
|
+
const left = expression.left.getText(src);
|
|
98
|
+
const right = expression.right.getText(src);
|
|
99
|
+
|
|
100
|
+
if (right === "null") return { target: left, value: "null" };
|
|
101
|
+
if (right === "undefined") return { target: left, value: "undefined" };
|
|
102
|
+
if (left === "null") return { target: right, value: "null" };
|
|
103
|
+
if (left === "undefined") return { target: right, value: "undefined" };
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
function getIndent(src: ts.SourceFile, node: ts.Node): string {
|
|
70
108
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
71
109
|
const lineStart = src.getPositionOfLineAndCharacter(line, 0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import type { KlintRule } from "../core/types";
|
|
5
5
|
|
|
6
6
|
export const preferStringRawRegexp: KlintRule = {
|
|
@@ -47,7 +47,7 @@ export const preferStringRawRegexp: KlintRule = {
|
|
|
47
47
|
linesText.slice(0, argOffset) + fixedArg + linesText.slice(argEndOffset);
|
|
48
48
|
|
|
49
49
|
violations.push({
|
|
50
|
-
file:
|
|
50
|
+
file: relativeSlashPath(root, file),
|
|
51
51
|
line: line + 1,
|
|
52
52
|
message:
|
|
53
53
|
"Use String.raw`...` for RegExp template argument to avoid double backslashes (Sonar S7780).",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
5
|
import type { KlintRule } from "../core/types";
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ export const preferStringRaw: KlintRule = {
|
|
|
27
27
|
const fix = buildNodeReplacementFix(src, node, fixedNode);
|
|
28
28
|
|
|
29
29
|
violations.push({
|
|
30
|
-
file:
|
|
30
|
+
file: relativeSlashPath(root, file),
|
|
31
31
|
line: fix.startLine,
|
|
32
32
|
message:
|
|
33
33
|
"String literal with escaped backslashes — use String.raw`...` for clarity (Sonar S6535).",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
1
|
import ts from "typescript";
|
|
3
2
|
import { walkAst } from "../core/ast";
|
|
3
|
+
import { relativeSlashPath } from "../core/paths";
|
|
4
4
|
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
5
|
import type { KlintRule } from "../core/types";
|
|
6
6
|
|
|
@@ -35,13 +35,13 @@ export const preferStringReplaceall: KlintRule = {
|
|
|
35
35
|
).expression.getText(src);
|
|
36
36
|
const replacementText = node.arguments[1].getText(src);
|
|
37
37
|
const patternLit = pattern.includes('"')
|
|
38
|
-
? `'${pattern.replaceAll("'",
|
|
38
|
+
? `'${pattern.replaceAll("'", String.raw`\'`)}'`
|
|
39
39
|
: `"${pattern}"`;
|
|
40
40
|
const fixedCall = `${strText}.replaceAll(${patternLit}, ${replacementText})`;
|
|
41
41
|
const fix = buildNodeReplacementFix(src, node, fixedCall);
|
|
42
42
|
|
|
43
43
|
violations.push({
|
|
44
|
-
file:
|
|
44
|
+
file: relativeSlashPath(root, file),
|
|
45
45
|
line: fix.startLine,
|
|
46
46
|
message: `Prefer \`${strText}.replaceAll(${patternLit}, ...)\` over \`.replace(/${pattern}/g, ...)\` — replaceAll() with a string is clearer and avoids regex escaping pitfalls.`,
|
|
47
47
|
fix,
|
|
@@ -28,22 +28,31 @@ export async function prepare(_, context) {
|
|
|
28
28
|
|
|
29
29
|
const packageJson = readPackageJson(cwd);
|
|
30
30
|
packageJson.version = version;
|
|
31
|
-
packageJson.optionalDependencies ??= {};
|
|
32
|
-
for (const packageName of NATIVE_PACKAGE_NAMES) {
|
|
33
|
-
packageJson.optionalDependencies[packageName] = version;
|
|
34
|
-
}
|
|
35
31
|
writePackageJson(cwd, packageJson);
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
export async function publish(
|
|
34
|
+
export async function publish(pluginConfig = {}, context) {
|
|
39
35
|
const cwd = context.cwd ?? process.cwd();
|
|
36
|
+
const spawn = pluginConfig.spawnSync ?? spawnSync;
|
|
37
|
+
const version = context.nextRelease?.version;
|
|
40
38
|
|
|
41
39
|
if (context.options?.dryRun) {
|
|
42
40
|
context.logger?.log("Dry run: skipping npm publish");
|
|
43
41
|
return;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
if (!version) {
|
|
45
|
+
throw new Error("semantic-release did not provide nextRelease.version");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const packageJson = readPackageJson(cwd);
|
|
49
|
+
packageJson.optionalDependencies ??= {};
|
|
50
|
+
for (const packageName of NATIVE_PACKAGE_NAMES) {
|
|
51
|
+
packageJson.optionalDependencies[packageName] = version;
|
|
52
|
+
}
|
|
53
|
+
writePackageJson(cwd, packageJson);
|
|
54
|
+
|
|
55
|
+
const result = spawn("npm", ["publish", "--access", "public"], {
|
|
47
56
|
cwd,
|
|
48
57
|
encoding: "utf-8",
|
|
49
58
|
stdio: "inherit",
|