@ttsc/lint 0.24.0 → 0.25.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/lib/index.d.ts +1 -1
- package/lib/index.js +161 -46
- package/lib/index.js.map +1 -1
- package/lib/internal/configEvaluatorFailure.d.ts +24 -20
- package/lib/internal/configEvaluatorFailure.js +41 -54
- package/lib/internal/configEvaluatorFailure.js.map +1 -1
- package/linthost/config.go +154 -64
- package/package.json +2 -2
- package/src/index.ts +163 -53
- package/src/internal/configEvaluatorFailure.ts +35 -64
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
export declare const CONFIG_EVALUATOR_MAX_BUFFER: number;
|
|
2
|
-
export declare const CONFIG_EVALUATOR_TIMEOUT_MS = 60000;
|
|
3
|
-
export declare const CONFIG_EVALUATOR_TEARDOWN_GRACE_MS = 5000;
|
|
4
|
-
export declare const CONFIG_EVALUATOR_STATUS_FD = 3;
|
|
5
|
-
export declare const CONFIG_EVALUATOR_PROCESS_OPTIONS: Readonly<{
|
|
6
|
-
killSignal: "SIGKILL";
|
|
7
|
-
maxBuffer: number;
|
|
8
|
-
timeout: number;
|
|
9
|
-
}>;
|
|
10
1
|
interface ConfigEvaluatorProcessResult {
|
|
11
2
|
error?: Error;
|
|
12
|
-
output?: readonly (string | null)[] | null;
|
|
13
3
|
signal: NodeJS.Signals | null;
|
|
14
4
|
status: number | null;
|
|
15
|
-
stderr: string | null | undefined;
|
|
16
5
|
}
|
|
17
6
|
/**
|
|
18
7
|
* Classify the ways the isolated lint-config evaluator can stop.
|
|
19
8
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
9
|
+
* The evaluator writes the child's own output straight to this process's
|
|
10
|
+
* stderr, so a diagnostic has already reached the user by the time anything
|
|
11
|
+
* here runs. What is left to say is only how the process ended: it never
|
|
12
|
+
* launched, something outside killed it, or it exited non-zero after printing
|
|
13
|
+
* its own reason.
|
|
14
|
+
*
|
|
15
|
+
* Nothing is bounded here — not time, not output. Both were the compiler
|
|
16
|
+
* deciding, on numbers nobody chose for this machine, that a user's own config
|
|
17
|
+
* had run too long or said too much. A slow config is a slow build the user can
|
|
18
|
+
* watch and interrupt; a loud one is output they asked for. Neither is this
|
|
19
|
+
* process's memory to spend either, because the child's streams are no longer
|
|
20
|
+
* collected into it.
|
|
26
21
|
*/
|
|
27
22
|
export declare function configEvaluatorProcessFailure(result: ConfigEvaluatorProcessResult, configPath: string): Error | undefined;
|
|
28
23
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
24
|
+
* Read the failure envelope the evaluator writes to its result file when it
|
|
25
|
+
* stops on an error it can name.
|
|
26
|
+
*
|
|
27
|
+
* This is the other half of classifying how the evaluation ended, which is why
|
|
28
|
+
* it lives beside {@link configEvaluatorProcessFailure} rather than at the call
|
|
29
|
+
* site: the status says that it failed, and this says why.
|
|
30
|
+
*
|
|
31
|
+
* Only a well-formed envelope is honoured. A real evaluation payload never
|
|
32
|
+
* carries this key, and every other shape — an absent file, a build that failed
|
|
33
|
+
* before the loader ran, a half-written result, a payload written before a
|
|
34
|
+
* later non-zero exit — leaves the process status to speak for itself.
|
|
31
35
|
*/
|
|
32
|
-
export declare function
|
|
36
|
+
export declare function configEvaluatorFailureReason(outputPath: string): string;
|
|
33
37
|
export {};
|
|
@@ -1,40 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CONFIG_EVALUATOR_PROCESS_OPTIONS = exports.CONFIG_EVALUATOR_STATUS_FD = exports.CONFIG_EVALUATOR_TEARDOWN_GRACE_MS = exports.CONFIG_EVALUATOR_TIMEOUT_MS = exports.CONFIG_EVALUATOR_MAX_BUFFER = void 0;
|
|
4
6
|
exports.configEvaluatorProcessFailure = configEvaluatorProcessFailure;
|
|
5
|
-
exports.
|
|
6
|
-
|
|
7
|
-
exports.CONFIG_EVALUATOR_TIMEOUT_MS = 60_000;
|
|
8
|
-
exports.CONFIG_EVALUATOR_TEARDOWN_GRACE_MS = 5_000;
|
|
9
|
-
exports.CONFIG_EVALUATOR_STATUS_FD = 3;
|
|
10
|
-
exports.CONFIG_EVALUATOR_PROCESS_OPTIONS = Object.freeze({
|
|
11
|
-
killSignal: "SIGKILL",
|
|
12
|
-
maxBuffer: exports.CONFIG_EVALUATOR_MAX_BUFFER,
|
|
13
|
-
timeout: exports.CONFIG_EVALUATOR_TIMEOUT_MS + exports.CONFIG_EVALUATOR_TEARDOWN_GRACE_MS,
|
|
14
|
-
});
|
|
7
|
+
exports.configEvaluatorFailureReason = configEvaluatorFailureReason;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
9
|
/**
|
|
16
10
|
* Classify the ways the isolated lint-config evaluator can stop.
|
|
17
11
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
12
|
+
* The evaluator writes the child's own output straight to this process's
|
|
13
|
+
* stderr, so a diagnostic has already reached the user by the time anything
|
|
14
|
+
* here runs. What is left to say is only how the process ended: it never
|
|
15
|
+
* launched, something outside killed it, or it exited non-zero after printing
|
|
16
|
+
* its own reason.
|
|
17
|
+
*
|
|
18
|
+
* Nothing is bounded here — not time, not output. Both were the compiler
|
|
19
|
+
* deciding, on numbers nobody chose for this machine, that a user's own config
|
|
20
|
+
* had run too long or said too much. A slow config is a slow build the user can
|
|
21
|
+
* watch and interrupt; a loud one is output they asked for. Neither is this
|
|
22
|
+
* process's memory to spend either, because the child's streams are no longer
|
|
23
|
+
* collected into it.
|
|
24
24
|
*/
|
|
25
25
|
function configEvaluatorProcessFailure(result, configPath) {
|
|
26
|
-
const code = result.error?.code;
|
|
27
|
-
const nestedCode = result.output?.[exports.CONFIG_EVALUATOR_STATUS_FD]?.trim() ?? "";
|
|
28
|
-
if (code === "ETIMEDOUT" || nestedCode === "ETIMEDOUT") {
|
|
29
|
-
return new Error(`@ttsc/lint: ttsx evaluation of ${configPath} timed out after ` +
|
|
30
|
-
`${exports.CONFIG_EVALUATOR_TIMEOUT_MS / 1_000} seconds. ` +
|
|
31
|
-
"Simplify the config or move heavy work out of top-level.");
|
|
32
|
-
}
|
|
33
|
-
if (code === "ENOBUFS" || nestedCode === "ENOBUFS") {
|
|
34
|
-
return new Error(`@ttsc/lint: ttsx evaluation of ${configPath} exceeded the ` +
|
|
35
|
-
`${exports.CONFIG_EVALUATOR_MAX_BUFFER / (1024 * 1024)} MiB output limit. ` +
|
|
36
|
-
"Reduce console output from the config and its dependencies.");
|
|
37
|
-
}
|
|
38
26
|
if (result.error) {
|
|
39
27
|
return new Error(`@ttsc/lint: failed to spawn ttsx for ${configPath}: ${result.error.message}`);
|
|
40
28
|
}
|
|
@@ -42,35 +30,34 @@ function configEvaluatorProcessFailure(result, configPath) {
|
|
|
42
30
|
return new Error(`@ttsc/lint: ttsx evaluation of ${configPath} was killed by signal ${result.signal}.`);
|
|
43
31
|
}
|
|
44
32
|
if (result.status !== 0) {
|
|
45
|
-
|
|
46
|
-
return new Error(`@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}` +
|
|
47
|
-
(reason === "" ? "" : "\n" + reason));
|
|
33
|
+
return new Error(`@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}`);
|
|
48
34
|
}
|
|
49
35
|
return undefined;
|
|
50
36
|
}
|
|
51
37
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* Return the useful tail of evaluator stderr without turning an exception into
|
|
64
|
-
* an unbounded duplicate of the already-forwarded child stream.
|
|
38
|
+
* Read the failure envelope the evaluator writes to its result file when it
|
|
39
|
+
* stops on an error it can name.
|
|
40
|
+
*
|
|
41
|
+
* This is the other half of classifying how the evaluation ended, which is why
|
|
42
|
+
* it lives beside {@link configEvaluatorProcessFailure} rather than at the call
|
|
43
|
+
* site: the status says that it failed, and this says why.
|
|
44
|
+
*
|
|
45
|
+
* Only a well-formed envelope is honoured. A real evaluation payload never
|
|
46
|
+
* carries this key, and every other shape — an absent file, a build that failed
|
|
47
|
+
* before the loader ran, a half-written result, a payload written before a
|
|
48
|
+
* later non-zero exit — leaves the process status to speak for itself.
|
|
65
49
|
*/
|
|
66
|
-
function configEvaluatorFailureReason(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
50
|
+
function configEvaluatorFailureReason(outputPath) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(outputPath, "utf8"));
|
|
53
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
54
|
+
return "";
|
|
55
|
+
const message = parsed
|
|
56
|
+
.__ttscLoaderError;
|
|
57
|
+
return typeof message === "string" ? message.trim() : "";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
73
62
|
}
|
|
74
|
-
const CONFIG_EVALUATOR_REASON_LINES = 5;
|
|
75
|
-
const CONFIG_EVALUATOR_REASON_MAX_CHARS = 8 * 1024;
|
|
76
63
|
//# sourceMappingURL=configEvaluatorFailure.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configEvaluatorFailure.js","sourceRoot":"","sources":["../../src/internal/configEvaluatorFailure.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"configEvaluatorFailure.js","sourceRoot":"","sources":["../../src/internal/configEvaluatorFailure.ts"],"names":[],"mappings":";;;;;;;AAAA,sDAAyB;AAQzB;;;;;;;;;;;;;;;GAeG;AACH,uCACE,MAAoC,EACpC,UAAkB;IAElB,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,IAAI,KAAK,CACd,wCAAwC,UAAU,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAC9E,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,IAAI,KAAK,CACd,kCAAkC,UAAU,yBAAyB,MAAM,CAAC,MAAM,GAAG,CACtF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,KAAK,CACd,2BAA2B,UAAU,qCAAqC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAClG,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,sCAA6C,UAAkB;IAC7D,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,iBAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;QACxE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QAC7D,MAAM,OAAO,GAAI,MAA0C;aACxD,iBAAiB,CAAC;QACrB,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/linthost/config.go
CHANGED
|
@@ -16,20 +16,10 @@ import (
|
|
|
16
16
|
"sort"
|
|
17
17
|
"strings"
|
|
18
18
|
"sync"
|
|
19
|
-
"time"
|
|
20
19
|
|
|
21
20
|
"github.com/samchon/ttsc/packages/ttsc/driver/windowsjunction"
|
|
22
21
|
)
|
|
23
22
|
|
|
24
|
-
// configLoaderTimeout caps every `ttsx`/`node -e` subprocess that
|
|
25
|
-
// evaluates a user-supplied lint config. The JS factory imposes the
|
|
26
|
-
// same 60 s budget on its mirroring spawnSync; without the Go-side cap
|
|
27
|
-
// a runaway user config would hang `ttsc-lint` forever, while
|
|
28
|
-
// `ttsc`/`pnpm` upstream of it stays responsive. 60 s is generous
|
|
29
|
-
// enough for cold ttsx starts on CI runners and tight enough to keep
|
|
30
|
-
// user-visible feedback under a minute.
|
|
31
|
-
const configLoaderTimeout = 60 * time.Second
|
|
32
|
-
|
|
33
23
|
// Severity is the `error | warning | off` ladder.
|
|
34
24
|
type Severity int
|
|
35
25
|
|
|
@@ -1781,39 +1771,31 @@ func serializableConfigKeysLiteral() string {
|
|
|
1781
1771
|
|
|
1782
1772
|
// runConfigLoaderCommand runs a prepared config-loader subprocess (`cmd`),
|
|
1783
1773
|
// then turns its result into a parsed config object. It owns the shared tail
|
|
1784
|
-
// of both subprocess-backed loaders: discarding user stdout,
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
// for error messages; `label` is the human-readable subject (e.g. "config
|
|
1774
|
+
// of both subprocess-backed loaders: discarding user stdout, streaming user
|
|
1775
|
+
// stderr through, reading the private result file, JSON-parsing its envelope,
|
|
1776
|
+
// and rejecting a non-object result. `location` is the config file
|
|
1777
|
+
// path for error messages; `label` is the human-readable subject (e.g. "config
|
|
1789
1778
|
// file" or "TypeScript config file") spliced into the load/parse error
|
|
1790
1779
|
// prefixes so each loader keeps its own wording.
|
|
1791
1780
|
func runConfigLoaderCommand(
|
|
1792
|
-
ctx context.Context,
|
|
1793
1781
|
cmd *exec.Cmd,
|
|
1794
1782
|
location string,
|
|
1795
1783
|
label string,
|
|
1796
1784
|
outputPath string,
|
|
1797
1785
|
) (evaluatedConfigFile, error) {
|
|
1798
|
-
|
|
1786
|
+
// The child's stderr is human output and goes straight to this process's
|
|
1787
|
+
// stderr as it is written. Collecting it only to replay it afterwards is what
|
|
1788
|
+
// made a long evaluation print nothing at all, and what would make a loud one
|
|
1789
|
+
// grow this process's memory without bound.
|
|
1799
1790
|
cmd.Stdout = io.Discard
|
|
1800
|
-
cmd.Stderr =
|
|
1791
|
+
cmd.Stderr = os.Stderr
|
|
1801
1792
|
err := cmd.Run()
|
|
1802
|
-
// A loader diagnostic is only useful when the load succeeds, because a
|
|
1803
|
-
// failure already carries the same text in its message. Forward it so an
|
|
1804
|
-
// assertion about what the loader recorded can name what it resolved.
|
|
1805
|
-
if err == nil && os.Getenv("TTSC_LINT_DEBUG_CONFIG_GRAPH") != "" {
|
|
1806
|
-
if text := strings.TrimSpace(stderr.String()); text != "" {
|
|
1807
|
-
fmt.Fprintln(os.Stderr, text)
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
1793
|
if err != nil {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: %s", label, location, stderrText)
|
|
1794
|
+
// The loader's stack already reached the user's stderr as it was written.
|
|
1795
|
+
// What it could not put there is a reason a caller can act on, so that
|
|
1796
|
+
// arrives through the result file instead.
|
|
1797
|
+
if reason := loaderFailureReason(outputPath); reason != "" {
|
|
1798
|
+
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: %s", label, location, reason)
|
|
1817
1799
|
}
|
|
1818
1800
|
return evaluatedConfigFile{}, fmt.Errorf("@ttsc/lint: load %s %s: %w", label, location, err)
|
|
1819
1801
|
}
|
|
@@ -1907,8 +1889,7 @@ func normalizeConfigDependencyFingerprints(
|
|
|
1907
1889
|
// loadScriptConfigFile evaluates a .js/.cjs/.mjs config file by running a
|
|
1908
1890
|
// Node subprocess that dynamic-imports the file, resolves the exported config
|
|
1909
1891
|
// through the same 8-hop default/config normalization used by the TS loader,
|
|
1910
|
-
// and serializes the result into a private result file.
|
|
1911
|
-
// configLoaderTimeout deadline to prevent user code from hanging indefinitely.
|
|
1892
|
+
// and serializes the result into a private result file.
|
|
1912
1893
|
func loadScriptConfigFile(location string) (any, error) {
|
|
1913
1894
|
evaluated, err := loadScriptConfigEvaluation(location)
|
|
1914
1895
|
return evaluated.value, err
|
|
@@ -1933,7 +1914,7 @@ func loadScriptConfigEvaluationWithin(
|
|
|
1933
1914
|
if node == "" {
|
|
1934
1915
|
node = "node"
|
|
1935
1916
|
}
|
|
1936
|
-
ctx, cancel := context.
|
|
1917
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
1937
1918
|
defer cancel()
|
|
1938
1919
|
cmd := exec.CommandContext(
|
|
1939
1920
|
ctx,
|
|
@@ -1944,7 +1925,7 @@ func loadScriptConfigEvaluationWithin(
|
|
|
1944
1925
|
outputPath,
|
|
1945
1926
|
resolutionRoot,
|
|
1946
1927
|
)
|
|
1947
|
-
return runConfigLoaderCommand(
|
|
1928
|
+
return runConfigLoaderCommand(cmd, location, "config file", outputPath)
|
|
1948
1929
|
}
|
|
1949
1930
|
|
|
1950
1931
|
// scriptConfigLoaderSource returns the CommonJS source of the loader script
|
|
@@ -2072,6 +2053,12 @@ const hooks = registerHooks({
|
|
|
2072
2053
|
}
|
|
2073
2054
|
})().catch((error) => {
|
|
2074
2055
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
2056
|
+
// The stack above is for the reader. This is for the caller: the parent reads
|
|
2057
|
+
// the result file either way, so a failure reason travels as data rather than
|
|
2058
|
+
// as text scraped back out of a captured stream.
|
|
2059
|
+
try {
|
|
2060
|
+
fs.writeFileSync(outputPath, JSON.stringify({ __ttscLoaderError: error && error.message ? String(error.message) : String(error) }), "utf8");
|
|
2061
|
+
} catch {}
|
|
2075
2062
|
process.exit(1);
|
|
2076
2063
|
});
|
|
2077
2064
|
|
|
@@ -2965,7 +2952,7 @@ function toSerializableConfig(value) {
|
|
|
2965
2952
|
|
|
2966
2953
|
// loadTypeScriptConfigFile evaluates a .ts/.cts/.mts config file by writing
|
|
2967
2954
|
// an ephemeral loader script and tsconfig into a temp directory, symlinking the
|
|
2968
|
-
// nearest node_modules, then running `ttsx
|
|
2955
|
+
// nearest node_modules, then running `ttsx`.
|
|
2969
2956
|
// The loader script imports the config file, resolves it through the same
|
|
2970
2957
|
// normalization chain used by loadScriptConfigFile, and writes a private JSON
|
|
2971
2958
|
// result file so user stdout cannot corrupt the protocol.
|
|
@@ -3046,11 +3033,11 @@ func loadTypeScriptConfigEvaluationWithin(
|
|
|
3046
3033
|
}
|
|
3047
3034
|
args = append(args, loader)
|
|
3048
3035
|
|
|
3049
|
-
ctx, cancel := context.
|
|
3036
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
3050
3037
|
defer cancel()
|
|
3051
3038
|
cmd := ttsxCommandContext(ctx, args...)
|
|
3052
3039
|
cmd.Env = nodeConfigLoaderEnv(location)
|
|
3053
|
-
return runConfigLoaderCommand(
|
|
3040
|
+
return runConfigLoaderCommand(cmd, location, "TypeScript config file", outputPath)
|
|
3054
3041
|
}
|
|
3055
3042
|
|
|
3056
3043
|
// isConfigObject reports whether `value` is a top-level config object. A lint
|
|
@@ -3239,22 +3226,36 @@ const hooks = registerHooks({
|
|
|
3239
3226
|
},
|
|
3240
3227
|
});
|
|
3241
3228
|
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
value
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
}
|
|
3229
|
+
// Wrapped rather than written as a top-level await: the loader tsconfig's
|
|
3230
|
+
// "module" follows the config's own package, and TS1378 rejects top-level await
|
|
3231
|
+
// under a CommonJS module option however this .mts file emits. The body's own
|
|
3232
|
+
// catch is the only failure path — it ends the process — so there is nothing
|
|
3233
|
+
// left for a trailing handler to settle.
|
|
3234
|
+
(async () => {
|
|
3235
|
+
try {
|
|
3236
|
+
const importedConfig = await import(configUrl);
|
|
3237
|
+
const value = await resolveConfig(importedConfig, true);
|
|
3238
|
+
if (!isObject(value) || Array.isArray(value)) {
|
|
3239
|
+
throw new Error("config file must export an ITtscLintConfig object");
|
|
3240
|
+
}
|
|
3241
|
+
fs.writeFileSync(outputPath, JSON.stringify({
|
|
3242
|
+
dependencies: finalizeDependencies(),
|
|
3243
|
+
value: toSerializableConfig(value),
|
|
3244
|
+
}), "utf8");
|
|
3245
|
+
} catch (error) {
|
|
3246
|
+
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
3247
|
+
// The stack above is for the reader. This is for the caller: the parent
|
|
3248
|
+
// reads the result file either way, so a failure reason travels as data
|
|
3249
|
+
// rather than as text scraped back out of a captured stream. A write that
|
|
3250
|
+
// itself fails leaves the process status to speak.
|
|
3251
|
+
try {
|
|
3252
|
+
fs.writeFileSync(outputPath, JSON.stringify({ __ttscLoaderError: error instanceof Error ? error.message : String(error) }), "utf8");
|
|
3253
|
+
} catch {}
|
|
3254
|
+
process.exit(1);
|
|
3255
|
+
} finally {
|
|
3256
|
+
hooks.deregister();
|
|
3257
|
+
}
|
|
3258
|
+
})();
|
|
3258
3259
|
|
|
3259
3260
|
async function resolveConfig(value: unknown, allowNamedConfig: boolean): Promise<unknown> {
|
|
3260
3261
|
let current = value;
|
|
@@ -4238,10 +4239,16 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
4238
4239
|
// false` is the right baseline.
|
|
4239
4240
|
content := map[string]any{
|
|
4240
4241
|
"compilerOptions": map[string]any{
|
|
4241
|
-
"allowImportingTsExtensions":
|
|
4242
|
-
"allowJs":
|
|
4243
|
-
"checkJs":
|
|
4244
|
-
|
|
4242
|
+
"allowImportingTsExtensions": true,
|
|
4243
|
+
"allowJs": true,
|
|
4244
|
+
"checkJs": false,
|
|
4245
|
+
// The config is a Node module, so Node's rule decides its format: the
|
|
4246
|
+
// nearest package.json "type" above it. Hardcoding one answer ran every
|
|
4247
|
+
// ambiguous `.ts` config as ESM and broke __dirname in an ordinary
|
|
4248
|
+
// CommonJS package (#1068). moduleResolution stays "bundler", which tsgo
|
|
4249
|
+
// accepts for both kinds, so extensionless relative imports keep
|
|
4250
|
+
// resolving either way.
|
|
4251
|
+
"module": configModuleOption(location),
|
|
4245
4252
|
"moduleResolution": "bundler",
|
|
4246
4253
|
"noImplicitAny": false,
|
|
4247
4254
|
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
@@ -4250,6 +4257,12 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
4250
4257
|
"skipLibCheck": true,
|
|
4251
4258
|
"strict": false,
|
|
4252
4259
|
"target": "ES2022",
|
|
4260
|
+
// TypeScript 7 includes no ambient type package unless "types" asks for
|
|
4261
|
+
// it, and this Program extends nothing, so without the wildcard a config
|
|
4262
|
+
// could not name a single Node global (#1068). The loader directory links
|
|
4263
|
+
// the config's nearest node_modules, so the default typeRoots walk finds
|
|
4264
|
+
// exactly what the project installed.
|
|
4265
|
+
"types": []string{"*"},
|
|
4253
4266
|
},
|
|
4254
4267
|
"files": []string{
|
|
4255
4268
|
filepath.ToSlash(loader),
|
|
@@ -4263,6 +4276,56 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
4263
4276
|
return string(body)
|
|
4264
4277
|
}
|
|
4265
4278
|
|
|
4279
|
+
// configModuleOption returns the loader tsconfig's "module" for a config file:
|
|
4280
|
+
// the module kind Node itself would give that file.
|
|
4281
|
+
//
|
|
4282
|
+
// An explicit .cts/.cjs or .mts/.mjs extension already decides the emit format
|
|
4283
|
+
// on its own, so those keep the ES-module setting and let the extension win —
|
|
4284
|
+
// the same precedence tsgo applies. Everything ambiguous walks up for the
|
|
4285
|
+
// nearest package.json "type", exactly as Node does when it loads the file.
|
|
4286
|
+
func configModuleOption(location string) string {
|
|
4287
|
+
switch strings.ToLower(filepath.Ext(location)) {
|
|
4288
|
+
case ".ts", ".tsx", ".js":
|
|
4289
|
+
if nearestPackageType(location) == "commonjs" {
|
|
4290
|
+
return "CommonJS"
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return "ESNext"
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// nearestPackageType mirrors Node's package-scope lookup for the nearest
|
|
4297
|
+
// package.json above location: the walk stops at the FIRST manifest it finds,
|
|
4298
|
+
// and a manifest declaring no "type" means CommonJS rather than a reason to
|
|
4299
|
+
// keep climbing. Reaching the filesystem root without any manifest also means
|
|
4300
|
+
// CommonJS. The location is made absolute first, so a relative config path
|
|
4301
|
+
// cannot end the walk at "." after a single step.
|
|
4302
|
+
func nearestPackageType(location string) string {
|
|
4303
|
+
absolute, err := filepath.Abs(location)
|
|
4304
|
+
if err != nil {
|
|
4305
|
+
absolute = location
|
|
4306
|
+
}
|
|
4307
|
+
dir := filepath.Dir(absolute)
|
|
4308
|
+
for {
|
|
4309
|
+
raw, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
|
4310
|
+
if err == nil {
|
|
4311
|
+
var manifest struct {
|
|
4312
|
+
Type string `json:"type"`
|
|
4313
|
+
}
|
|
4314
|
+
// A manifest that does not parse still bounds the package scope; Node
|
|
4315
|
+
// refuses to look past it, and CommonJS is the format it defaults to.
|
|
4316
|
+
if json.Unmarshal(raw, &manifest) == nil && manifest.Type == "module" {
|
|
4317
|
+
return "module"
|
|
4318
|
+
}
|
|
4319
|
+
return "commonjs"
|
|
4320
|
+
}
|
|
4321
|
+
parent := filepath.Dir(dir)
|
|
4322
|
+
if parent == dir {
|
|
4323
|
+
return "commonjs"
|
|
4324
|
+
}
|
|
4325
|
+
dir = parent
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4266
4329
|
// loaderRootDir returns the widest rootDir that still contains the loader
|
|
4267
4330
|
// tsconfig's inputs: the volume root of the loader temp dir (`C:/` on
|
|
4268
4331
|
// Windows, `/` elsewhere). A literal "/" is not an ancestor of drive-letter
|
|
@@ -4344,15 +4407,15 @@ func resolveDirLink(dir string) string {
|
|
|
4344
4407
|
}
|
|
4345
4408
|
|
|
4346
4409
|
// ttsxCommand returns a ttsx exec.Cmd bound to a background context. Use
|
|
4347
|
-
// ttsxCommandContext when
|
|
4410
|
+
// ttsxCommandContext when the caller owns a cancellable context.
|
|
4348
4411
|
func ttsxCommand(args ...string) *exec.Cmd {
|
|
4349
4412
|
return ttsxCommandContext(context.Background(), args...)
|
|
4350
4413
|
}
|
|
4351
4414
|
|
|
4352
|
-
// ttsxCommandContext is the
|
|
4353
|
-
//
|
|
4354
|
-
//
|
|
4355
|
-
//
|
|
4415
|
+
// ttsxCommandContext is the cancellable variant, used by the config loaders so
|
|
4416
|
+
// their subprocess is torn down with the call that started it. It carries no
|
|
4417
|
+
// deadline: evaluating a user config is the user's own code running, and how
|
|
4418
|
+
// long that is allowed to take is not this binary's decision.
|
|
4356
4419
|
func ttsxCommandContext(ctx context.Context, args ...string) *exec.Cmd {
|
|
4357
4420
|
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
4358
4421
|
if ttsx == "" {
|
|
@@ -4721,3 +4784,30 @@ func (c RuleConfig) Severity(name string) Severity {
|
|
|
4721
4784
|
}
|
|
4722
4785
|
return SeverityOff
|
|
4723
4786
|
}
|
|
4787
|
+
|
|
4788
|
+
// loaderFailureReason reads the failure envelope a config loader writes to its
|
|
4789
|
+
// private result file when it stops on an error it can name.
|
|
4790
|
+
//
|
|
4791
|
+
// The loader's stack goes to this process's stderr as it runs, which is where a
|
|
4792
|
+
// reader wants it. But the reason — "config file must export an ITtscLintConfig
|
|
4793
|
+
// object" — is a fact about the user's config, and a caller deserves it in the
|
|
4794
|
+
// error rather than having to go find it in the log. Only a well-formed
|
|
4795
|
+
// envelope is honoured; anything else leaves the process status to speak.
|
|
4796
|
+
//
|
|
4797
|
+
// The key is `__ttscLoaderError` — the same spelling every other ttsc loader
|
|
4798
|
+
// writes, and namespaced so it cannot collide with a payload field. This file
|
|
4799
|
+
// spends "error" on rule severity, which is exactly the confusion a shared,
|
|
4800
|
+
// prefixed key avoids.
|
|
4801
|
+
func loaderFailureReason(outputPath string) string {
|
|
4802
|
+
raw, err := os.ReadFile(outputPath)
|
|
4803
|
+
if err != nil {
|
|
4804
|
+
return ""
|
|
4805
|
+
}
|
|
4806
|
+
var envelope struct {
|
|
4807
|
+
Error string `json:"__ttscLoaderError"`
|
|
4808
|
+
}
|
|
4809
|
+
if json.Unmarshal(raw, &envelope) != nil {
|
|
4810
|
+
return ""
|
|
4811
|
+
}
|
|
4812
|
+
return strings.TrimSpace(envelope.Error)
|
|
4813
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"ttsc": "0.
|
|
40
|
+
"ttsc": "0.25.0"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|