ttsc 0.31.2 → 0.31.3
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/compiler/internal/build/BuildExecution.d.ts +2 -1
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -1
- package/lib/launcher/internal/runTtsc.js +2 -2
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/watch/WatchTopology.d.ts +20 -3
- package/lib/launcher/internal/watch/WatchTopology.js +31 -9
- package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
- package/lib/plugin/internal/load/loadProjectPlugins.d.ts +5 -2
- package/lib/plugin/internal/load/loadProjectPlugins.js +6 -3
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +13 -5
- package/lib/plugin/internal/source/SourcePluginWorkspace.js +139 -5
- package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -1
- package/package.json +8 -8
- package/src/compiler/internal/build/BuildExecution.ts +2 -1
- package/src/compiler/internal/build/RunBuildOptions.ts +2 -1
- package/src/launcher/internal/runTtsc.ts +6 -2
- package/src/launcher/internal/watch/WatchTopology.ts +38 -9
- package/src/plugin/internal/load/loadProjectPlugins.ts +14 -4
- package/src/plugin/internal/source/SourcePluginWorkspace.ts +175 -6
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttsc",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.3",
|
|
4
4
|
"description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
},
|
|
62
62
|
"optionalDependencies": {
|
|
63
63
|
"fsevents": "^2.3.3",
|
|
64
|
-
"@ttsc/linux-
|
|
65
|
-
"@ttsc/
|
|
66
|
-
"@ttsc/linux-arm64": "0.31.
|
|
67
|
-
"@ttsc/
|
|
68
|
-
"@ttsc/
|
|
69
|
-
"@ttsc/
|
|
70
|
-
"@ttsc/
|
|
64
|
+
"@ttsc/linux-arm": "0.31.3",
|
|
65
|
+
"@ttsc/linux-x64": "0.31.3",
|
|
66
|
+
"@ttsc/linux-arm64": "0.31.3",
|
|
67
|
+
"@ttsc/darwin-arm64": "0.31.3",
|
|
68
|
+
"@ttsc/darwin-x64": "0.31.3",
|
|
69
|
+
"@ttsc/win32-x64": "0.31.3",
|
|
70
|
+
"@ttsc/win32-arm64": "0.31.3"
|
|
71
71
|
},
|
|
72
72
|
"sideEffects": [
|
|
73
73
|
"./lib/register.js",
|
|
@@ -12,6 +12,7 @@ import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITt
|
|
|
12
12
|
import type { TtscBuildOptions } from "../../../structures/internal/TtscBuildOptions";
|
|
13
13
|
import type { TtscBuildResult } from "../../../structures/internal/TtscBuildResult";
|
|
14
14
|
import type { TtscCommonOptions } from "../../../structures/internal/TtscCommonOptions";
|
|
15
|
+
import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
|
|
15
16
|
import { outputText } from "../outputText";
|
|
16
17
|
import { readProjectConfig } from "../project/readProjectConfig";
|
|
17
18
|
import { CompilerProjectSelection } from "../project/CompilerProjectSelection";
|
|
@@ -1190,7 +1191,7 @@ export namespace BuildExecution {
|
|
|
1190
1191
|
export function resolveExecutionContext(
|
|
1191
1192
|
options: TtscCommonOptions & {
|
|
1192
1193
|
emit?: boolean;
|
|
1193
|
-
onWatchInputs?:
|
|
1194
|
+
onWatchInputs?: TtscPluginSourceInputsHandler;
|
|
1194
1195
|
resolvedProject?: ITtscParsedProjectConfig;
|
|
1195
1196
|
tsconfig?: string;
|
|
1196
1197
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
|
|
2
2
|
import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITtscProjectInputSnapshot";
|
|
3
3
|
import type { TtscBuildOptions } from "../../../structures/internal/TtscBuildOptions";
|
|
4
|
+
import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Options of one {@link runBuild} call: the public build options plus the knobs
|
|
@@ -120,7 +121,7 @@ export type RunBuildOptions = TtscBuildOptions & {
|
|
|
120
121
|
* The watch launcher uses these roots to invalidate a sidecar when its Go
|
|
121
122
|
* implementation changes between rebuilds.
|
|
122
123
|
*/
|
|
123
|
-
onWatchInputs?:
|
|
124
|
+
onWatchInputs?: TtscPluginSourceInputsHandler;
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
127
|
* Receives the reconciled project-rule filesystem dependency snapshot. Called
|
|
@@ -19,6 +19,7 @@ import { createFilesystemPathIdentityContext } from "../../internal/pathIdentity
|
|
|
19
19
|
import { isFilesystemPathIdentityWithin } from "../../internal/pathIdentity/isFilesystemPathIdentityWithin";
|
|
20
20
|
import { resolvePhysicalPath } from "../../internal/pathIdentity/resolvePhysicalPath";
|
|
21
21
|
import { resolveSafeCacheCleanupTargets } from "../../internal/resolveSafeCacheCleanupTargets";
|
|
22
|
+
import type { PluginContentIdentities } from "../../plugin/internal/source/PluginContentIdentities";
|
|
22
23
|
import { SourceBuildCacheLayout } from "../../plugin/internal/source/SourceBuildCacheLayout";
|
|
23
24
|
import { legacyGlobalCacheTargets } from "../../plugin/internal/source/legacyGlobalCacheTargets";
|
|
24
25
|
import { resolveCleanTargets } from "../../plugin/internal/source/resolveCleanTargets";
|
|
@@ -634,8 +635,11 @@ function runWatch(
|
|
|
634
635
|
const invocation = {
|
|
635
636
|
...(checkOnly ? { ...options, emit: false } : options),
|
|
636
637
|
cwd,
|
|
637
|
-
onWatchInputs: (
|
|
638
|
-
|
|
638
|
+
onWatchInputs: (
|
|
639
|
+
inputs: readonly string[],
|
|
640
|
+
identities?: PluginContentIdentities.Store,
|
|
641
|
+
) => {
|
|
642
|
+
topology?.setExtraInputs(inputs, identities);
|
|
639
643
|
},
|
|
640
644
|
onProjectInputs: (inputs: ITtscProjectInputSnapshot) => {
|
|
641
645
|
topology?.setProjectInputs(inputs);
|
|
@@ -14,9 +14,9 @@ import { type ProjectInputPathIdentityContext } from "../../../internal/pathIden
|
|
|
14
14
|
import { createProjectInputPathIdentityContext } from "../../../internal/pathIdentity/createProjectInputPathIdentityContext";
|
|
15
15
|
import { isProjectInputPathIdentityWithin } from "../../../internal/pathIdentity/isProjectInputPathIdentityWithin";
|
|
16
16
|
import { resolveProjectInputPath } from "../../../internal/pathIdentity/resolveProjectInputPath";
|
|
17
|
+
import { PluginContentIdentities } from "../../../plugin/internal/source/PluginContentIdentities";
|
|
17
18
|
import { collectPluginSourceDirectories } from "../../../plugin/internal/source/collectPluginSourceDirectories";
|
|
18
19
|
import { pluginSourceCovers } from "../../../plugin/internal/source/pluginSourceCovers";
|
|
19
|
-
import { pluginSourceDigest } from "../../../plugin/internal/source/pluginSourceDigest";
|
|
20
20
|
import { prunesPluginSourceDirectory } from "../../../plugin/internal/source/prunesPluginSourceDirectory";
|
|
21
21
|
import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
|
|
22
22
|
import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITtscProjectInputSnapshot";
|
|
@@ -84,6 +84,13 @@ export class WatchTopology {
|
|
|
84
84
|
*/
|
|
85
85
|
private pluginInputStates = new Map<string, string>();
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The record store of the latest plugin load that reported the inputs, which
|
|
89
|
+
* proves their states from the plugin cache's records (#1728). Absent before
|
|
90
|
+
* a load reports one, or when it opened none; the states are read then.
|
|
91
|
+
*/
|
|
92
|
+
private pluginInputIdentities: PluginContentIdentities.Store | undefined;
|
|
93
|
+
|
|
87
94
|
/**
|
|
88
95
|
* The plugin inputs notifications named since the last decision, by path key,
|
|
89
96
|
* each with the locations noted for it (`notePluginNotification`).
|
|
@@ -331,17 +338,33 @@ export class WatchTopology {
|
|
|
331
338
|
* observation. This does not freeze the source tree or prove every transient
|
|
332
339
|
* edit was seen.
|
|
333
340
|
*
|
|
341
|
+
* WARNING (#1728): take each baseline through the reporting load's record
|
|
342
|
+
* store (`identities`), never by reading the sources again. The load proves
|
|
343
|
+
* the same directories from the plugin cache's records a moment later, and a
|
|
344
|
+
* content read here cost every watch session start 0.3-0.6 s for typia's Go
|
|
345
|
+
* module. The store proves only a digest whose metadata still holds and is
|
|
346
|
+
* separable from its reference, so the baseline is the digest a content read
|
|
347
|
+
* would return at this moment.
|
|
348
|
+
*
|
|
349
|
+
* @param inputs Plugin source directories and files the load reported.
|
|
350
|
+
* @param identities The reporting load's record store, if it opened one.
|
|
334
351
|
* @evidence contracts/common.md#principled-implementation Published plugin inputs define the source corpus, and retaining existing pre-build baselines keeps unreported edits visible.
|
|
335
352
|
* @evidence contracts/common.md#clear-and-simple-design Deduplication, baseline retention and compiler/watch reconciliation remain ordered phases.
|
|
336
353
|
* @evidence contracts/common.md#prohibited-implementation-shortcuts The actual plugin source digest applies its owning source-selection and pruning policy; named plugins or fixture output do not replace observed input state.
|
|
337
354
|
* @evidence contracts/common.md#meaningful-documentation Native prose explains baseline timing and why existing inputs are not restamped following the documentation skill.
|
|
338
355
|
* @evidence contracts/portability.md#os-neutral-implementation Native path resolution and canonical backend registration govern source trees; lexical keys preserve declarations and unknown names, folding ASCII only under measured insensitive parents rather than an OS-derived rule.
|
|
339
|
-
* @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests
|
|
340
|
-
* @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state.
|
|
356
|
+
* @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests, each proven from the selected files' metadata when the load's records hold and read from the selected pruned corpus otherwise; refresh(false) also pays project/compiler listing, directory traversal and watcher reconciliation; fixed publication count does not bound delegated bytes or backend work.
|
|
357
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state; a new input's digest is the one the load recorded while its metadata holds.
|
|
341
358
|
* @evidence contracts/performance.md#bound-retention-and-release-resources Publications replace input/digest populations and case-observation caches without a population cap; synchronization attempts obsolete watcher closure. Native/supplied closure has its own lifetime and can throw; owner close does not prove every backend has stopped or every retained snapshot byte was released.
|
|
342
359
|
*/
|
|
343
|
-
public setExtraInputs(
|
|
360
|
+
public setExtraInputs(
|
|
361
|
+
inputs: readonly string[],
|
|
362
|
+
identities?: PluginContentIdentities.Store,
|
|
363
|
+
): void {
|
|
344
364
|
this.beginLexicalTransaction();
|
|
365
|
+
// The latest load's store also serves later notifications; its references
|
|
366
|
+
// only grow older, which can make fewer stamps separable but never more.
|
|
367
|
+
this.pluginInputIdentities = identities;
|
|
345
368
|
const next = uniqueExistingPaths(inputs, this.lexicalIdentities);
|
|
346
369
|
if (arraysEqual(this.extraInputs, next)) return;
|
|
347
370
|
this.extraInputs = next;
|
|
@@ -354,7 +377,8 @@ export class WatchTopology {
|
|
|
354
377
|
const key = this.pathKey(input);
|
|
355
378
|
states.set(
|
|
356
379
|
key,
|
|
357
|
-
this.pluginInputStates.get(key) ??
|
|
380
|
+
this.pluginInputStates.get(key) ??
|
|
381
|
+
pluginInputState(input, this.pluginInputIdentities),
|
|
358
382
|
);
|
|
359
383
|
}
|
|
360
384
|
this.pluginInputStates = states;
|
|
@@ -1664,7 +1688,7 @@ export class WatchTopology {
|
|
|
1664
1688
|
const key = this.pathKey(input);
|
|
1665
1689
|
const locations = pending.get(key);
|
|
1666
1690
|
if (locations === undefined) continue;
|
|
1667
|
-
const state = pluginInputState(input);
|
|
1691
|
+
const state = pluginInputState(input, this.pluginInputIdentities);
|
|
1668
1692
|
if (this.pluginInputStates.get(key) === state) continue;
|
|
1669
1693
|
this.pluginInputStates.set(key, state);
|
|
1670
1694
|
for (const location of locations) {
|
|
@@ -2739,12 +2763,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
2739
2763
|
* What a plugin build keys a plugin input on: the digest of the files its build
|
|
2740
2764
|
* reads from a source directory (`pluginSourceDigest`), or a file's bytes. An
|
|
2741
2765
|
* input that cannot be read has a state of its own, so the change that made it
|
|
2742
|
-
* unreadable is reported and the build names the failure.
|
|
2766
|
+
* unreadable is reported and the build names the failure. A directory's digest
|
|
2767
|
+
* comes from the load's record store while the selected files' metadata holds
|
|
2768
|
+
* there (#1728), and from the files otherwise.
|
|
2743
2769
|
*/
|
|
2744
|
-
function pluginInputState(
|
|
2770
|
+
function pluginInputState(
|
|
2771
|
+
input: string,
|
|
2772
|
+
identities: PluginContentIdentities.Store | undefined,
|
|
2773
|
+
): string {
|
|
2745
2774
|
try {
|
|
2746
2775
|
return WatchPaths.isDirectory(input)
|
|
2747
|
-
? `directory:${
|
|
2776
|
+
? `directory:${PluginContentIdentities.sourceDirectory(identities, input)}`
|
|
2748
2777
|
: `file:${fingerprintProjectInputFile(input)}`;
|
|
2749
2778
|
} catch (error) {
|
|
2750
2779
|
return `unreadable:${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -130,8 +130,14 @@ export function loadProjectPlugins(options: {
|
|
|
130
130
|
*/
|
|
131
131
|
identities?: PluginContentIdentities.Store;
|
|
132
132
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Observe plugin build roots before building, including failed builds, with
|
|
135
|
+
* the record store that proves their states (#1728).
|
|
136
|
+
*/
|
|
137
|
+
onWatchInputs?: (
|
|
138
|
+
inputs: readonly string[],
|
|
139
|
+
identities?: PluginContentIdentities.Store,
|
|
140
|
+
) => void;
|
|
135
141
|
|
|
136
142
|
/** Caller-selected anchor for plugin-owned configuration discovery. */
|
|
137
143
|
pluginConfigDir?: string;
|
|
@@ -425,7 +431,10 @@ export function loadProjectPlugins(options: {
|
|
|
425
431
|
])
|
|
426
432
|
.filter(reportsPluginSource),
|
|
427
433
|
);
|
|
428
|
-
|
|
434
|
+
// WARNING (#1728): hand the watch owner this load's store with the roots.
|
|
435
|
+
// It baselines each root before any build reads it; without the store it
|
|
436
|
+
// read every source the load proves from records a moment later.
|
|
437
|
+
options.onWatchInputs([...interests].sort(), sourceIdentities);
|
|
429
438
|
const initialSize = interests.size;
|
|
430
439
|
for (const candidate of candidates) {
|
|
431
440
|
// Nested source-only proposals replace the outer root manifests. Do not
|
|
@@ -440,7 +449,7 @@ export function loadProjectPlugins(options: {
|
|
|
440
449
|
if (reportsPluginSource(directory)) interests.add(directory);
|
|
441
450
|
}
|
|
442
451
|
if (interests.size !== initialSize)
|
|
443
|
-
options.onWatchInputs([...interests].sort());
|
|
452
|
+
options.onWatchInputs([...interests].sort(), sourceIdentities);
|
|
444
453
|
}
|
|
445
454
|
const proposals = NativeSourcePackages.propose(
|
|
446
455
|
candidates.map((candidate) => ({
|
|
@@ -529,6 +538,7 @@ export function loadProjectPlugins(options: {
|
|
|
529
538
|
watchReplacements,
|
|
530
539
|
packageReaders,
|
|
531
540
|
),
|
|
541
|
+
sourceIdentities,
|
|
532
542
|
);
|
|
533
543
|
const linkedContributors = records
|
|
534
544
|
.filter((record) => record.stage === "transform")
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProcess";
|
|
7
|
+
import { runtimeExecutableIdentity } from "../../../internal/runtimeExecutableIdentity";
|
|
6
8
|
import { GoEnvironmentReading } from "./GoEnvironmentReading";
|
|
7
9
|
import { GoSourceInputs } from "./GoSourceInputs";
|
|
8
10
|
import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
|
|
9
|
-
import
|
|
11
|
+
import { PluginContentIdentities } from "./PluginContentIdentities";
|
|
12
|
+
import { PluginLoadAnswers } from "./PluginLoadAnswers";
|
|
10
13
|
import { SourcePluginAdmission } from "./SourcePluginAdmission";
|
|
11
14
|
import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
|
|
12
15
|
import { formatGoWorkPath } from "./formatGoWorkPath";
|
|
@@ -239,9 +242,17 @@ export namespace SourcePluginWorkspace {
|
|
|
239
242
|
* registry shares native reader state from the first watch query onward; its
|
|
240
243
|
* owner must keep one fixed effective environment and discard it on retry.
|
|
241
244
|
* Caller views share that native state and syntax, while each default
|
|
242
|
-
* diagnostic label remains with its current caller.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
+
* diagnostic label remains with its current caller.
|
|
246
|
+
*
|
|
247
|
+
* An optional record store carries the Go environment-file discovery hint
|
|
248
|
+
* across processes, and the parse of identical bytes by the same Go command
|
|
249
|
+
* (#1729): every launch otherwise ran `go mod edit -json` on an unchanged
|
|
250
|
+
* `go.mod`. A recorded parse is keyed by the manifest bytes, the content
|
|
251
|
+
* identities of the launcher and of the `GOROOT/bin/go` the live `go env
|
|
252
|
+
* -json GOROOT` reports, the `GOENV` file's bytes, and every `GO*`/`CGO_*`
|
|
253
|
+
* variable the command receives. It is consulted and written only under the
|
|
254
|
+
* same qualified tool witness that admits in-load sharing, so the live
|
|
255
|
+
* witness query still runs on every load.
|
|
245
256
|
*
|
|
246
257
|
* @evidence contracts/common.md#principled-implementation Actual go mod edit JSON determines module/replacement identities; current bytes, selected/dispatched-tool metadata and Go's environment-file witness qualify reuse under the existing metadata-distinguishability premise. Environment values are copied at reader creation. Missing go.mod returns the existing empty module observation, and moving tool/manifest inputs cannot publish a parsed record.
|
|
247
258
|
* @evidence contracts/common.md#clear-and-simple-design One factory acquires load-owned native reader state by selected tool; a thin caller view forwards its diagnostic label without duplicating the byte-content map. Current reads and post-parse byte/tool comparisons qualify insertion.
|
|
@@ -249,7 +260,7 @@ export namespace SourcePluginWorkspace {
|
|
|
249
260
|
* @evidence contracts/common.md#meaningful-documentation Native prose states this operation's authority and delegated boundaries; fields retain separate native comments.
|
|
250
261
|
* @evidence contracts/portability.md#os-neutral-implementation Native path resolution and temporary cwd avoid deep Windows working-directory limits; the selected tool handles manifest syntax.
|
|
251
262
|
* @evidence contracts/performance.md#efficient-algorithms Reader acquisition performs a selected-tool registry lookup; every nonmissing read qualifies tool/environment metadata and reads current manifest bytes. A cold or stale witness first observes the selected launcher; an unavailable or nonfile launcher skips optional environment acquisition and permits only the actual uncached parse attempt. An observed file lends its same pre-read stat to the witness before acquiring current Go values and GOENV evidence through the common reader, without a GOROOT walk. Only complete native GOENV/GOROOT locations qualify syntax sharing; a dispatched tool is witnessed before parsing. Unavailable native environment acquisition disables syntax sharing while preserving actual parse success or failure. Each distinct byte string that earns stable successful syntax in that tool epoch runs Go mod edit and complete JSON parsing once, then rechecks bytes/tool metadata before caching; copied manifests share that parse. Failed queries remain uncached and report the current caller's diagnostic label.
|
|
252
|
-
* @evidence contracts/performance.md#reuse-equivalent-work The caller may share this reader only within one load and the same pinned tool/effective environment; parsed syntax is
|
|
263
|
+
* @evidence contracts/performance.md#reuse-equivalent-work The caller may share this reader only within one load and the same pinned tool/effective environment; parsed syntax is never native containment authority. Across loads only a parse recorded under the content identities of the bytes, both Go executables, the GOENV file and the Go variables is reused, and only under a qualified live tool witness. Caller views share native state while preserving failure attribution; diagnostic labels are not syntax cache inputs.
|
|
253
264
|
* @evidence contracts/performance.md#bound-retention-and-release-resources The map retains distinct manifest bytes and parsed syntax for its caller's load/build lifetime; process/output capture cleanup remains delegated.
|
|
254
265
|
*/
|
|
255
266
|
export function createGoModReader(
|
|
@@ -279,6 +290,9 @@ export namespace SourcePluginWorkspace {
|
|
|
279
290
|
const cache = new Map<string, GoModInfo>();
|
|
280
291
|
const environment = { ...env };
|
|
281
292
|
let toolWitness: PluginBuildEnvironmentWitness.Record | undefined;
|
|
293
|
+
// The tool part of a recorded parse's identity, formed with each qualified
|
|
294
|
+
// witness; undefined without a store or a provable tool.
|
|
295
|
+
let toolIdentity: unknown;
|
|
282
296
|
const reader: GoModReader = {
|
|
283
297
|
read(dir, diagnosticPlugin = pluginName) {
|
|
284
298
|
OwnedSynchronousProcess.checkpoint();
|
|
@@ -343,6 +357,16 @@ export namespace SourcePluginWorkspace {
|
|
|
343
357
|
complete && PluginBuildEnvironmentWitness.holds(witness)
|
|
344
358
|
? witness
|
|
345
359
|
: undefined;
|
|
360
|
+
toolIdentity =
|
|
361
|
+
toolWitness === undefined || identities === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: goModToolIdentity(
|
|
364
|
+
goBinary,
|
|
365
|
+
observed!.GOROOT as string,
|
|
366
|
+
observed!.GOENV as string,
|
|
367
|
+
environment,
|
|
368
|
+
identities,
|
|
369
|
+
);
|
|
346
370
|
}
|
|
347
371
|
// UTF-8 decoding is lossy for malformed input. Only identical bytes
|
|
348
372
|
// can share Go syntax, even when Go serializes two inputs identically.
|
|
@@ -351,6 +375,26 @@ export namespace SourcePluginWorkspace {
|
|
|
351
375
|
if (cached !== undefined) {
|
|
352
376
|
return cached;
|
|
353
377
|
}
|
|
378
|
+
// WARNING (#1729): a new process asks the record store before running
|
|
379
|
+
// Go on bytes this exact command already parsed. Never key this on a
|
|
380
|
+
// path or on the reader's metadata witness alone.
|
|
381
|
+
const answer =
|
|
382
|
+
toolIdentity === undefined
|
|
383
|
+
? undefined
|
|
384
|
+
: [
|
|
385
|
+
toolIdentity,
|
|
386
|
+
crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
387
|
+
];
|
|
388
|
+
const recalled =
|
|
389
|
+
answer === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: recallGoModInfo(
|
|
392
|
+
PluginLoadAnswers.read(identities, GO_MOD_SYNTAX, answer),
|
|
393
|
+
);
|
|
394
|
+
if (recalled !== undefined) {
|
|
395
|
+
cache.set(byteKey, recalled);
|
|
396
|
+
return recalled;
|
|
397
|
+
}
|
|
354
398
|
const info = readGoModInfo(
|
|
355
399
|
resolved,
|
|
356
400
|
goBinary,
|
|
@@ -370,7 +414,14 @@ export namespace SourcePluginWorkspace {
|
|
|
370
414
|
);
|
|
371
415
|
// An unavailable environment observation can still execute a fresh Go
|
|
372
416
|
// parse, but it cannot authorize sharing that result on another call.
|
|
373
|
-
if (toolWitness !== undefined)
|
|
417
|
+
if (toolWitness !== undefined) {
|
|
418
|
+
cache.set(byteKey, info);
|
|
419
|
+
if (answer !== undefined)
|
|
420
|
+
PluginLoadAnswers.write(identities, GO_MOD_SYNTAX, answer, {
|
|
421
|
+
modulePath: info.modulePath,
|
|
422
|
+
directives: info.directives,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
374
425
|
return info;
|
|
375
426
|
},
|
|
376
427
|
};
|
|
@@ -450,6 +501,124 @@ export namespace SourcePluginWorkspace {
|
|
|
450
501
|
};
|
|
451
502
|
}
|
|
452
503
|
|
|
504
|
+
/** Answer family of `go mod edit -json` parses (`PluginLoadAnswers`). */
|
|
505
|
+
const GO_MOD_SYNTAX = "go-mod-syntax";
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Everything besides the manifest bytes that decides how `go mod edit -json`
|
|
509
|
+
* parses them, by content: the launcher, the `GOROOT/bin/go` the live query
|
|
510
|
+
* reported, the `GOENV` file that can select another toolchain, and the Go
|
|
511
|
+
* variables the command receives. `undefined` when an executable cannot be
|
|
512
|
+
* identified or the file cannot be read, so the parse runs.
|
|
513
|
+
*/
|
|
514
|
+
function goModToolIdentity(
|
|
515
|
+
goBinary: string,
|
|
516
|
+
goRoot: string,
|
|
517
|
+
goEnv: string,
|
|
518
|
+
env: NodeJS.ProcessEnv,
|
|
519
|
+
identities: PluginContentIdentities.Store,
|
|
520
|
+
): unknown {
|
|
521
|
+
const dispatchedPath = path.join(
|
|
522
|
+
goRoot,
|
|
523
|
+
"bin",
|
|
524
|
+
process.platform === "win32" ? "go.exe" : "go",
|
|
525
|
+
);
|
|
526
|
+
// Only an executable the store can prove from metadata may key the record:
|
|
527
|
+
// streaming both executables on a device without a reference would cost
|
|
528
|
+
// more than the parse it replaces.
|
|
529
|
+
if (
|
|
530
|
+
!provableFromMetadata(goBinary, identities) ||
|
|
531
|
+
!provableFromMetadata(dispatchedPath, identities)
|
|
532
|
+
)
|
|
533
|
+
return undefined;
|
|
534
|
+
const launcher = runtimeExecutableIdentity(goBinary, identities);
|
|
535
|
+
const dispatched = runtimeExecutableIdentity(dispatchedPath, identities);
|
|
536
|
+
if (launcher === undefined || dispatched === undefined) return undefined;
|
|
537
|
+
let goEnvFile: string | null;
|
|
538
|
+
if (goEnv === "off") goEnvFile = "off";
|
|
539
|
+
else
|
|
540
|
+
try {
|
|
541
|
+
goEnvFile = crypto
|
|
542
|
+
.createHash("sha256")
|
|
543
|
+
.update(fs.readFileSync(goEnv))
|
|
544
|
+
.digest("hex");
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
547
|
+
return undefined;
|
|
548
|
+
goEnvFile = null;
|
|
549
|
+
}
|
|
550
|
+
return [
|
|
551
|
+
1,
|
|
552
|
+
launcher,
|
|
553
|
+
goRoot,
|
|
554
|
+
dispatched,
|
|
555
|
+
goEnv,
|
|
556
|
+
goEnvFile,
|
|
557
|
+
Object.entries(GoSourceInputs.goBuildEnv(goBinary, undefined, env))
|
|
558
|
+
.filter(
|
|
559
|
+
(entry): entry is [string, string] =>
|
|
560
|
+
entry[1] !== undefined && /^(GO|CGO_)/i.test(entry[0]),
|
|
561
|
+
)
|
|
562
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Whether the file's stamp is separable from the store's reference on its
|
|
568
|
+
* device, the precondition for proving its bytes from a record.
|
|
569
|
+
*/
|
|
570
|
+
function provableFromMetadata(
|
|
571
|
+
file: string,
|
|
572
|
+
identities: PluginContentIdentities.Store,
|
|
573
|
+
): boolean {
|
|
574
|
+
try {
|
|
575
|
+
return PluginContentIdentities.separable(
|
|
576
|
+
identities.references,
|
|
577
|
+
fs.statSync(file, { bigint: true }),
|
|
578
|
+
);
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* A recorded parse in the shape `readGoModInfo` returns, or `undefined` when
|
|
586
|
+
* the record does not have it.
|
|
587
|
+
*/
|
|
588
|
+
function recallGoModInfo(value: unknown): GoModInfo | undefined {
|
|
589
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
590
|
+
const record = value as { modulePath?: unknown; directives?: unknown };
|
|
591
|
+
if (
|
|
592
|
+
(record.modulePath !== null && typeof record.modulePath !== "string") ||
|
|
593
|
+
!Array.isArray(record.directives) ||
|
|
594
|
+
!record.directives.every(isReplaceDirective)
|
|
595
|
+
)
|
|
596
|
+
return undefined;
|
|
597
|
+
const directives = record.directives as NonNullable<GoModJson["Replace"]>;
|
|
598
|
+
return {
|
|
599
|
+
modulePath: record.modulePath,
|
|
600
|
+
directives,
|
|
601
|
+
replacements: directives
|
|
602
|
+
.map(jsonReplacementToGoModReplacement)
|
|
603
|
+
.filter((replacement) => replacement !== null),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function isReplaceDirective(value: unknown): boolean {
|
|
608
|
+
if (typeof value !== "object" || value === null) return false;
|
|
609
|
+
const directive = value as Record<string, unknown>;
|
|
610
|
+
return [directive.Old, directive.New].every(
|
|
611
|
+
(side) =>
|
|
612
|
+
side === undefined ||
|
|
613
|
+
(typeof side === "object" &&
|
|
614
|
+
side !== null &&
|
|
615
|
+
["Path", "Version"].every((key) => {
|
|
616
|
+
const field = (side as Record<string, unknown>)[key];
|
|
617
|
+
return field === undefined || typeof field === "string";
|
|
618
|
+
})),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
453
622
|
function jsonReplacementToGoModReplacement(
|
|
454
623
|
replacement: NonNullable<GoModJson["Replace"]>[number],
|
|
455
624
|
): GoModReplacement | null {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PluginContentIdentities } from "../../plugin/internal/source/PluginContentIdentities";
|
|
1
2
|
import type { ITtscProjectInputSnapshot } from "./ITtscProjectInputSnapshot";
|
|
2
3
|
import type { TtscCommonOptions } from "./TtscCommonOptions";
|
|
3
4
|
|
|
@@ -39,6 +40,9 @@ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
|
|
|
39
40
|
* Executable modules, linked source roots, contributors and external local
|
|
40
41
|
* module replacements can be reported. ttsc's own package sources are omitted;
|
|
41
42
|
* this notification is not the complete compiler or project-rule input graph.
|
|
43
|
+
* The reporting load also hands over its record store, if it opened one, so the
|
|
44
|
+
* owner proves each root's state from the plugin cache's records instead of
|
|
45
|
+
* reading the sources the load is about to prove (#1728).
|
|
42
46
|
*
|
|
43
47
|
* @evidence contracts/common.md#principled-implementation Readonly roots describe the actual native-plugin sources whose later edits need observation.
|
|
44
48
|
* @evidence contracts/common.md#clear-and-simple-design A void notification leaves watcher construction and lifetime with the invocation owner.
|
|
@@ -49,7 +53,10 @@ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
|
|
|
49
53
|
* @evidenceExclude contracts/performance.md#efficient-algorithms A type declaration chooses no processing strategy.
|
|
50
54
|
* @evidenceExclude contracts/performance.md#reuse-equivalent-work A type declaration computes nothing, so there is no work to share.
|
|
51
55
|
*/
|
|
52
|
-
export type TtscPluginSourceInputsHandler = (
|
|
56
|
+
export type TtscPluginSourceInputsHandler = (
|
|
57
|
+
inputs: readonly string[],
|
|
58
|
+
identities?: PluginContentIdentities.Store,
|
|
59
|
+
) => void;
|
|
53
60
|
|
|
54
61
|
/**
|
|
55
62
|
* Receive the discovered project-rule filesystem topology for the watch owner.
|