ttsc 0.27.0 → 0.28.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/cmd/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +17 -6
- package/cmd/ttscgraph/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +253 -25
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +57 -8
- package/driver/plugins.go +125 -14
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -2
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
- package/lib/launcher/internal/runtimeHooks.js +93 -2
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
- package/package.json +8 -8
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/shim.go +3 -0
- package/shim/compiler/emithook.go +10 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/index.ts +1 -0
- package/src/launcher/internal/runtimeHooks.ts +111 -2
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/structures/ITtscCompilerTransformation.ts +7 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +15 -6
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { resolveSourceBuildCachePaths } from "./buildSourcePlugin";
|
|
6
|
+
import {
|
|
7
|
+
hashHostInputPaths,
|
|
8
|
+
realpathHostInputPaths,
|
|
9
|
+
} from "./loadProjectPlugins";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The answer `resolveCapabilityPlugins` produced for one project, and the exact
|
|
13
|
+
* state that makes it still true.
|
|
14
|
+
*
|
|
15
|
+
* The answer is small and fully serializable — a binary path, a capability map,
|
|
16
|
+
* a manifest string — which is why it is cached here and not one layer down.
|
|
17
|
+
* `loadProjectPlugins` returns live plugin descriptors the compiler drives, and
|
|
18
|
+
* writing those to disk would be caching a different, much larger thing.
|
|
19
|
+
*/
|
|
20
|
+
export interface ITtscCapabilityResolutionEntry {
|
|
21
|
+
/** Cache format plus the ttsc build that wrote it. */
|
|
22
|
+
version: string;
|
|
23
|
+
/** Paths whose state decides whether this answer is still the answer. */
|
|
24
|
+
hostInputs: string[];
|
|
25
|
+
/** Content hash per host input, `null` for one that does not exist. */
|
|
26
|
+
hostInputHashes: Record<string, string | null>;
|
|
27
|
+
/** Physical identity per host input, so a retargeted link is a change. */
|
|
28
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Fingerprint per plugin Go source directory.
|
|
31
|
+
*
|
|
32
|
+
* The binary path is content-keyed on that source, so a source edit produces
|
|
33
|
+
* a different path — and the cached entry would keep naming the old one,
|
|
34
|
+
* which still exists because the build cache retains it. Nothing in the host
|
|
35
|
+
* inputs moves when a plugin's own Go changes, so this is what notices.
|
|
36
|
+
*/
|
|
37
|
+
sources: Record<string, string>;
|
|
38
|
+
/** The `--plugins-json` payload, verbatim. */
|
|
39
|
+
manifest: string;
|
|
40
|
+
/** The `--project-context-json` payload, or `null` when none was wanted. */
|
|
41
|
+
projectContext: string | null;
|
|
42
|
+
/** One entry per configured native plugin, in configured order. */
|
|
43
|
+
plugins: ITtscCapabilityResolutionPlugin[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One plugin as the cache records it. */
|
|
47
|
+
export interface ITtscCapabilityResolutionPlugin {
|
|
48
|
+
binary: string;
|
|
49
|
+
capabilities: Record<string, boolean>;
|
|
50
|
+
source: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cache format tag.
|
|
55
|
+
*
|
|
56
|
+
* Moves when the entry shape or the validation rule changes, so an older entry
|
|
57
|
+
* is discarded rather than read under new rules.
|
|
58
|
+
*/
|
|
59
|
+
const FORMAT = "ttsc-capability-resolution-v1";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The character that joins fields a path could otherwise forge.
|
|
63
|
+
*
|
|
64
|
+
* A path cannot contain it, which is the whole reason it is the separator: with
|
|
65
|
+
* a space, a file named `a 1 2` states the same string as a one-byte file named
|
|
66
|
+
* `a`, and an edit to either would read as no edit at all. Built rather than
|
|
67
|
+
* written literally, because a source file carrying a raw NUL is one Git
|
|
68
|
+
* classifies as binary — which silently exempts it from this repository's
|
|
69
|
+
* end-of-line contract and leaves it with no textual diff for a reviewer.
|
|
70
|
+
*/
|
|
71
|
+
const SEPARATOR = String.fromCharCode(0);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read the recorded answer for a project, or `null` when there is none that is
|
|
75
|
+
* still true.
|
|
76
|
+
*
|
|
77
|
+
* Fail-closed by construction: every path that cannot prove the entry — a
|
|
78
|
+
* missing file, a parse failure, a format bump, a moved input, a rebuilt plugin
|
|
79
|
+
* source, a binary that is gone — returns `null`, and `null` means "walk the
|
|
80
|
+
* project properly". A cache that guessed here would answer "no plugin declares
|
|
81
|
+
* this capability" for a project that had just configured one, which is a wrong
|
|
82
|
+
* answer indistinguishable from the correct answer for the common case.
|
|
83
|
+
*/
|
|
84
|
+
export function readCapabilityResolution(options: {
|
|
85
|
+
cwd: string;
|
|
86
|
+
tsconfig: string;
|
|
87
|
+
version: string;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
}): ITtscCapabilityResolutionEntry | null {
|
|
90
|
+
const file = resolutionFile(options);
|
|
91
|
+
if (file === null) return null;
|
|
92
|
+
let entry: ITtscCapabilityResolutionEntry;
|
|
93
|
+
try {
|
|
94
|
+
entry = JSON.parse(
|
|
95
|
+
fs.readFileSync(file, "utf8"),
|
|
96
|
+
) as ITtscCapabilityResolutionEntry;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!isEntry(entry) || entry.version !== formatVersion(options.version))
|
|
101
|
+
return null;
|
|
102
|
+
// An entry recording nothing proves nothing. Discovery always reads at least
|
|
103
|
+
// the project's own manifest, so an empty input set is a malformed entry
|
|
104
|
+
// rather than a project with no inputs.
|
|
105
|
+
if (entry.hostInputs.length === 0) return null;
|
|
106
|
+
if (
|
|
107
|
+
!sameMap(entry.hostInputHashes, hashHostInputPaths(entry.hostInputs)) ||
|
|
108
|
+
!sameMap(entry.hostInputRealpaths, realpathHostInputPaths(entry.hostInputs))
|
|
109
|
+
)
|
|
110
|
+
return null;
|
|
111
|
+
for (const [source, fingerprint] of Object.entries(entry.sources))
|
|
112
|
+
if (fingerprintDirectory(source) !== fingerprint) return null;
|
|
113
|
+
for (const plugin of entry.plugins)
|
|
114
|
+
if (!fs.existsSync(plugin.binary)) return null;
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Record the answer and the state it was true for.
|
|
120
|
+
*
|
|
121
|
+
* A write failure is not reported. The cache is an optimization over a walk
|
|
122
|
+
* that still works, and a read-only or full disk is a reason to be slower, not
|
|
123
|
+
* a reason for `resolveCapabilityPlugins` to start throwing at a caller whose
|
|
124
|
+
* contract is that it never does.
|
|
125
|
+
*/
|
|
126
|
+
export function writeCapabilityResolution(
|
|
127
|
+
options: {
|
|
128
|
+
cwd: string;
|
|
129
|
+
tsconfig: string;
|
|
130
|
+
version: string;
|
|
131
|
+
env?: NodeJS.ProcessEnv;
|
|
132
|
+
},
|
|
133
|
+
answer: {
|
|
134
|
+
hostInputs: readonly string[];
|
|
135
|
+
manifest: string;
|
|
136
|
+
projectContext: string | null;
|
|
137
|
+
plugins: readonly ITtscCapabilityResolutionPlugin[];
|
|
138
|
+
},
|
|
139
|
+
): void {
|
|
140
|
+
const file = resolutionFile(options);
|
|
141
|
+
if (file === null) return;
|
|
142
|
+
const hostInputs = [
|
|
143
|
+
...new Set(
|
|
144
|
+
answer.hostInputs
|
|
145
|
+
.filter((input) => input !== "")
|
|
146
|
+
.map((input) => path.resolve(input)),
|
|
147
|
+
),
|
|
148
|
+
].sort();
|
|
149
|
+
if (hostInputs.length === 0) return;
|
|
150
|
+
const entry: ITtscCapabilityResolutionEntry = {
|
|
151
|
+
hostInputHashes: hashHostInputPaths(hostInputs),
|
|
152
|
+
hostInputRealpaths: realpathHostInputPaths(hostInputs),
|
|
153
|
+
hostInputs,
|
|
154
|
+
manifest: answer.manifest,
|
|
155
|
+
plugins: [...answer.plugins],
|
|
156
|
+
projectContext: answer.projectContext,
|
|
157
|
+
sources: Object.fromEntries(
|
|
158
|
+
[...new Set(answer.plugins.map((plugin) => plugin.source))]
|
|
159
|
+
.filter((source) => source !== "")
|
|
160
|
+
.map((source) => [source, fingerprintDirectory(source)] as const),
|
|
161
|
+
),
|
|
162
|
+
version: formatVersion(options.version),
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
166
|
+
// Written beside the target and renamed, so a reader never sees half an
|
|
167
|
+
// entry: a truncated JSON parses as a failure and falls back, but a
|
|
168
|
+
// partially written one could parse and be believed.
|
|
169
|
+
const staging = `${file}.${String(process.pid)}.tmp`;
|
|
170
|
+
fs.writeFileSync(staging, JSON.stringify(entry), "utf8");
|
|
171
|
+
fs.renameSync(staging, file);
|
|
172
|
+
} catch {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatVersion(version: string): string {
|
|
178
|
+
return `${FORMAT}:${version}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Where the entry for this project lives, or `null` when no cache root can be
|
|
183
|
+
* resolved.
|
|
184
|
+
*
|
|
185
|
+
* Keyed on the project rather than on the capability: the walk it replaces
|
|
186
|
+
* discovers every configured plugin, so one entry answers for all of them and a
|
|
187
|
+
* second consumer asking about a different capability costs nothing.
|
|
188
|
+
*/
|
|
189
|
+
function resolutionFile(options: {
|
|
190
|
+
cwd: string;
|
|
191
|
+
tsconfig: string;
|
|
192
|
+
env?: NodeJS.ProcessEnv;
|
|
193
|
+
}): string | null {
|
|
194
|
+
let root: string;
|
|
195
|
+
try {
|
|
196
|
+
root = resolveSourceBuildCachePaths(
|
|
197
|
+
path.resolve(options.cwd),
|
|
198
|
+
undefined,
|
|
199
|
+
options.env ?? process.env,
|
|
200
|
+
).root;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const key = crypto
|
|
205
|
+
.createHash("sha256")
|
|
206
|
+
.update(path.resolve(options.cwd))
|
|
207
|
+
.update(SEPARATOR)
|
|
208
|
+
.update(options.tsconfig)
|
|
209
|
+
.digest("hex");
|
|
210
|
+
return path.join(root, "capabilities", `${key}.json`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether the parsed value has every field the validation reads. */
|
|
214
|
+
function isEntry(value: unknown): value is ITtscCapabilityResolutionEntry {
|
|
215
|
+
if (typeof value !== "object" || value === null) return false;
|
|
216
|
+
const entry = value as Partial<ITtscCapabilityResolutionEntry>;
|
|
217
|
+
return (
|
|
218
|
+
typeof entry.version === "string" &&
|
|
219
|
+
Array.isArray(entry.hostInputs) &&
|
|
220
|
+
entry.hostInputs.every((input) => typeof input === "string") &&
|
|
221
|
+
isRecord(entry.hostInputHashes) &&
|
|
222
|
+
isRecord(entry.hostInputRealpaths) &&
|
|
223
|
+
isRecord(entry.sources) &&
|
|
224
|
+
typeof entry.manifest === "string" &&
|
|
225
|
+
(entry.projectContext === null ||
|
|
226
|
+
typeof entry.projectContext === "string") &&
|
|
227
|
+
Array.isArray(entry.plugins) &&
|
|
228
|
+
entry.plugins.every(
|
|
229
|
+
(plugin) =>
|
|
230
|
+
typeof plugin === "object" &&
|
|
231
|
+
plugin !== null &&
|
|
232
|
+
typeof plugin.binary === "string" &&
|
|
233
|
+
typeof plugin.source === "string" &&
|
|
234
|
+
isRecord(plugin.capabilities),
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether two snapshots describe the same state.
|
|
245
|
+
*
|
|
246
|
+
* Compared both ways: an input that has appeared is as much a change as one
|
|
247
|
+
* that has moved, and a recorded key the fresh snapshot lacks means the two
|
|
248
|
+
* were taken over different input sets.
|
|
249
|
+
*/
|
|
250
|
+
function sameMap(
|
|
251
|
+
recorded: Record<string, string | null>,
|
|
252
|
+
current: Record<string, string | null>,
|
|
253
|
+
): boolean {
|
|
254
|
+
const keys = Object.keys(recorded);
|
|
255
|
+
if (keys.length !== Object.keys(current).length) return false;
|
|
256
|
+
return keys.every(
|
|
257
|
+
(key) =>
|
|
258
|
+
Object.prototype.hasOwnProperty.call(current, key) &&
|
|
259
|
+
recorded[key] === current[key],
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A directory's shape and the state of every file in it.
|
|
265
|
+
*
|
|
266
|
+
* Size and modification time rather than content: a plugin's Go source is
|
|
267
|
+
* hundreds of files, this runs on the fast path, and the question it answers is
|
|
268
|
+
* only whether the build cache would now key on something else. Bounded in
|
|
269
|
+
* depth, and blind to `node_modules` and dot directories, for the same reason
|
|
270
|
+
* the build that reads this source is.
|
|
271
|
+
*/
|
|
272
|
+
function fingerprintDirectory(directory: string, depth = 0): string {
|
|
273
|
+
const hash = crypto.createHash("sha256");
|
|
274
|
+
for (const line of directoryState(directory, depth).sort())
|
|
275
|
+
hash.update(line).update("\n");
|
|
276
|
+
return hash.digest("hex");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function directoryState(directory: string, depth: number): string[] {
|
|
280
|
+
if (depth > 16) return [];
|
|
281
|
+
let entries: fs.Dirent[];
|
|
282
|
+
try {
|
|
283
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
284
|
+
} catch {
|
|
285
|
+
return [`${directory}${SEPARATOR}absent`];
|
|
286
|
+
}
|
|
287
|
+
const states: string[] = [];
|
|
288
|
+
for (const entry of entries) {
|
|
289
|
+
const child = path.join(directory, entry.name);
|
|
290
|
+
if (entry.isDirectory()) {
|
|
291
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
292
|
+
states.push(...directoryState(child, depth + 1));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const stat = fs.statSync(child);
|
|
297
|
+
states.push(
|
|
298
|
+
[child, String(stat.size), String(stat.mtimeMs)].join(SEPARATOR),
|
|
299
|
+
);
|
|
300
|
+
} catch {
|
|
301
|
+
states.push(`${child}${SEPARATOR}absent`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return states;
|
|
305
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createNativeProjectContextJson } from "../compiler/internal/project/createNativeProjectContextArgs";
|
|
5
|
+
import { resolveBinary } from "../compiler/internal/resolveBinary";
|
|
6
|
+
import {
|
|
7
|
+
readCapabilityResolution,
|
|
8
|
+
writeCapabilityResolution,
|
|
9
|
+
} from "./internal/capabilityResolutionCache";
|
|
10
|
+
import { loadProjectPlugins } from "./internal/loadProjectPlugins";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One plugin that declared the requested capability.
|
|
14
|
+
*
|
|
15
|
+
* `manifest` is the `--plugins-json` payload its sidecar needs to find its own
|
|
16
|
+
* configured entry. Without it the sidecar loads an empty rule configuration
|
|
17
|
+
* and answers as though the project declared nothing — an empty answer that
|
|
18
|
+
* looks exactly like a project which genuinely publishes none.
|
|
19
|
+
*/
|
|
20
|
+
export interface ITtscCapabilityPlugin {
|
|
21
|
+
binary: string;
|
|
22
|
+
manifest: string;
|
|
23
|
+
/**
|
|
24
|
+
* The `--project-context-json` payload, or `undefined` when the plugin's
|
|
25
|
+
* descriptor does not declare it wants one.
|
|
26
|
+
*
|
|
27
|
+
* A sidecar is handed a project root, not asked to derive one. Without this a
|
|
28
|
+
* rule that resolves its own inputs — the documents an evidence claim reads,
|
|
29
|
+
* a Prisma schema, an OpenAPI file — has no base to resolve them against, and
|
|
30
|
+
* answers with an empty set rather than an error, because "this project
|
|
31
|
+
* declares nothing" is a legitimate answer it cannot distinguish from "I was
|
|
32
|
+
* not told where the project is".
|
|
33
|
+
*/
|
|
34
|
+
projectContext?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The built sidecars of a project's configured plugins that declare one
|
|
39
|
+
* capability.
|
|
40
|
+
*
|
|
41
|
+
* This is the seam a consumer outside the compiler needs to ask a plugin a
|
|
42
|
+
* question the plugin declared it can answer. `ttscserver` already does this
|
|
43
|
+
* for `capabilities.lsp`, but it does it from inside the launcher; a separate
|
|
44
|
+
* tool — `@ttsc/graph`, an editor integration, a script — had no way to reach
|
|
45
|
+
* the same answer without reimplementing plugin discovery, descriptor
|
|
46
|
+
* evaluation, and the Go source build cache.
|
|
47
|
+
*
|
|
48
|
+
* It is contributor-agnostic by construction: the caller names a capability,
|
|
49
|
+
* not a package. A project that configures no plugin, or none declaring that
|
|
50
|
+
* capability, gets an empty array — which is an answer, not a failure, and is
|
|
51
|
+
* the common case.
|
|
52
|
+
*
|
|
53
|
+
* Building a plugin is not free the first time. The Go source build is cached
|
|
54
|
+
* by content, so a warm project pays a lookup; a cold one pays the build the
|
|
55
|
+
* next `ttsc` invocation would have paid anyway.
|
|
56
|
+
*
|
|
57
|
+
* @param options.capability - Capability flag the plugin descriptor must
|
|
58
|
+
* declare.
|
|
59
|
+
* @param options.cwd - Project root. Defaults to the current directory.
|
|
60
|
+
* @param options.tsconfig - Project tsconfig path, relative to `cwd`.
|
|
61
|
+
* @returns One entry per declaring plugin, in configured plugin order.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveCapabilityPlugins(options: {
|
|
64
|
+
capability: string;
|
|
65
|
+
cwd?: string;
|
|
66
|
+
tsconfig?: string;
|
|
67
|
+
}): ITtscCapabilityPlugin[] {
|
|
68
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
69
|
+
const tsconfig = options.tsconfig ?? "tsconfig.json";
|
|
70
|
+
const cached = readCapabilityResolution({ cwd, tsconfig, version });
|
|
71
|
+
if (cached !== null) return select(cached, options.capability);
|
|
72
|
+
|
|
73
|
+
const binary = resolveBinary();
|
|
74
|
+
if (binary === null || binary === undefined) return [];
|
|
75
|
+
try {
|
|
76
|
+
const loaded = loadProjectPlugins({
|
|
77
|
+
binary,
|
|
78
|
+
cwd: options.cwd,
|
|
79
|
+
tsconfig: options.tsconfig,
|
|
80
|
+
});
|
|
81
|
+
// The manifest carries every configured plugin, not only the declaring one.
|
|
82
|
+
// A sidecar reads its OWN entry out of it — that entry is where its config
|
|
83
|
+
// file lives — and a manifest narrowed to the caller's capability would hand
|
|
84
|
+
// it a project it does not recognize, which is an empty answer rather than an
|
|
85
|
+
// error. This is the same string `runBuild` passes for a check-stage plugin.
|
|
86
|
+
const manifest = JSON.stringify(
|
|
87
|
+
loaded.nativePlugins.map((plugin) => ({
|
|
88
|
+
config: plugin.config,
|
|
89
|
+
name: plugin.name,
|
|
90
|
+
stage: plugin.stage,
|
|
91
|
+
})),
|
|
92
|
+
);
|
|
93
|
+
const wantsContext = loaded.nativePlugins.some(
|
|
94
|
+
(plugin) =>
|
|
95
|
+
(plugin.capabilities as Record<string, unknown> | undefined)
|
|
96
|
+
?.projectContextArgs === true,
|
|
97
|
+
);
|
|
98
|
+
const answer = {
|
|
99
|
+
hostInputs: loaded.hostInputs,
|
|
100
|
+
manifest,
|
|
101
|
+
plugins: loaded.nativePlugins.map((plugin) => ({
|
|
102
|
+
binary: plugin.binary,
|
|
103
|
+
capabilities: Object.fromEntries(
|
|
104
|
+
Object.entries(
|
|
105
|
+
(plugin.capabilities as Record<string, unknown> | undefined) ?? {},
|
|
106
|
+
).map(([name, declared]) => [name, declared === true] as const),
|
|
107
|
+
),
|
|
108
|
+
source: plugin.source,
|
|
109
|
+
})),
|
|
110
|
+
projectContext: wantsContext
|
|
111
|
+
? createNativeProjectContextJson(loaded.project)
|
|
112
|
+
: null,
|
|
113
|
+
};
|
|
114
|
+
writeCapabilityResolution({ cwd, tsconfig, version }, answer);
|
|
115
|
+
return select(answer, options.capability);
|
|
116
|
+
} catch {
|
|
117
|
+
// A project whose plugin configuration does not load is a project the user
|
|
118
|
+
// already sees an error for, from the command that compiles it. Failing here
|
|
119
|
+
// would turn "your lint config has a typo" into "the graph is broken", and
|
|
120
|
+
// the caller's own degraded answer is the honest one.
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The declaring plugins, from a resolution however it was obtained. */
|
|
126
|
+
function select(
|
|
127
|
+
resolution: {
|
|
128
|
+
manifest: string;
|
|
129
|
+
projectContext: string | null;
|
|
130
|
+
plugins: readonly {
|
|
131
|
+
binary: string;
|
|
132
|
+
capabilities: Record<string, boolean>;
|
|
133
|
+
}[];
|
|
134
|
+
},
|
|
135
|
+
capability: string,
|
|
136
|
+
): ITtscCapabilityPlugin[] {
|
|
137
|
+
return resolution.plugins
|
|
138
|
+
.filter(
|
|
139
|
+
(plugin) =>
|
|
140
|
+
plugin.binary !== "" && plugin.capabilities[capability] === true,
|
|
141
|
+
)
|
|
142
|
+
.map((plugin) => ({
|
|
143
|
+
binary: plugin.binary,
|
|
144
|
+
manifest: resolution.manifest,
|
|
145
|
+
...(plugin.capabilities.projectContextArgs === true &&
|
|
146
|
+
resolution.projectContext !== null
|
|
147
|
+
? { projectContext: resolution.projectContext }
|
|
148
|
+
: {}),
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* This build, as the resolution cache's key material.
|
|
154
|
+
*
|
|
155
|
+
* A ttsc upgrade can change what discovery finds — a new descriptor field, a
|
|
156
|
+
* different resolution order — so an entry written by another build is not this
|
|
157
|
+
* build's answer.
|
|
158
|
+
*/
|
|
159
|
+
const version = ((): string => {
|
|
160
|
+
try {
|
|
161
|
+
const manifest = JSON.parse(
|
|
162
|
+
fs.readFileSync(
|
|
163
|
+
path.resolve(__dirname, "..", "..", "package.json"),
|
|
164
|
+
"utf8",
|
|
165
|
+
),
|
|
166
|
+
) as { version?: unknown };
|
|
167
|
+
return typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
168
|
+
} catch {
|
|
169
|
+
return "0.0.0";
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
@@ -133,6 +133,13 @@ export namespace ITtscCompilerTransformation {
|
|
|
133
133
|
* declares completeness should stamp {@link graph} too; without it a listed
|
|
134
134
|
* file retains no universal input at all.
|
|
135
135
|
*
|
|
136
|
+
* Ttsc's own hosts stamp it for what they can prove: their source-to-source
|
|
137
|
+
* output is a syntactic re-print, so the built-in native host and the
|
|
138
|
+
* linked-plugin host list a file once every linked plugin able to
|
|
139
|
+
* contribute to it has declared its own contribution complete — which,
|
|
140
|
+
* where no plugin is active at all, is every file. An executable sidecar
|
|
141
|
+
* prints its own envelope and decides for itself.
|
|
142
|
+
*
|
|
136
143
|
* Optional, and never required for correctness: an envelope without this
|
|
137
144
|
* field keeps the sound host-owned bound. A file that is both listed here
|
|
138
145
|
* and in {@link volatile} keeps the union, since the two claims contradict
|
package/utility/host.go
CHANGED
|
@@ -46,12 +46,18 @@ type hostOptions struct {
|
|
|
46
46
|
// so cache layers can register every file whose content can influence a
|
|
47
47
|
// transformed module without per-plugin reporting.
|
|
48
48
|
type transformResult struct {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Dependencies and DependenciesComplete carry what the linked plugins
|
|
50
|
+
// declared about their own contribution to each file; the host prints the
|
|
51
|
+
// parsed AST syntactically, so it adds nothing of its own to either. See
|
|
52
|
+
// driver.Program.TransformDependenciesFor.
|
|
53
|
+
Dependencies map[string][]string `json:"dependencies,omitempty"`
|
|
54
|
+
DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
|
|
55
|
+
Diagnostics []any `json:"diagnostics,omitempty"`
|
|
56
|
+
Graph *driver.TransformGraph `json:"graph,omitempty"`
|
|
57
|
+
HostInputs []string `json:"hostInputs,omitempty"`
|
|
58
|
+
HostInputHashes map[string]*string `json:"hostInputHashes,omitempty"`
|
|
59
|
+
HostInputRealpaths map[string]*string `json:"hostInputRealpaths,omitempty"`
|
|
60
|
+
TypeScript map[string]string `json:"typescript"`
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
// RunCheck validates the project and linked plugin configuration without
|
|
@@ -153,6 +159,9 @@ func RunTransformWithIO(args []string, stdout, stderr io.Writer) int {
|
|
|
153
159
|
text := shimprinter.EmitSourceFile(printer, file)
|
|
154
160
|
out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = text
|
|
155
161
|
}
|
|
162
|
+
dependencies := prog.TransformDependenciesFor(opts.cwd)
|
|
163
|
+
out.Dependencies = dependencies.Dependencies
|
|
164
|
+
out.DependenciesComplete = dependencies.Complete
|
|
156
165
|
out.HostInputs = prog.PluginHostInputs()
|
|
157
166
|
out.HostInputHashes = prog.PluginHostInputHashes()
|
|
158
167
|
out.HostInputRealpaths = prog.PluginHostInputRealpaths()
|