@ttsc/evidence 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/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
package/native/health.go
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
// recordPopulationFailure preserves a loader failure that belongs to a whole
|
|
4
|
+
// population rather than to one file.
|
|
5
|
+
//
|
|
6
|
+
// A synthetic inventory keeps the existing loader boundary intact: callers
|
|
7
|
+
// still receive the inventories and diagnostics they asked for, while graph
|
|
8
|
+
// materialization can tell "matched nothing" from "could not inspect enough to
|
|
9
|
+
// know what matched". Its NUL-prefixed key cannot be produced by a normalized
|
|
10
|
+
// artifact address and matchingInventoryPaths rejects it through relativeOf.
|
|
11
|
+
func recordPopulationFailure(
|
|
12
|
+
inventories map[string]*artifactInventory,
|
|
13
|
+
kind artifactKind,
|
|
14
|
+
base populationBase,
|
|
15
|
+
) {
|
|
16
|
+
if inventories == nil || base.Absolute == "" {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
key := "\x00load-failure:" + string(kind) + ":" + base.Absolute
|
|
20
|
+
inventories[key] = &artifactInventory{
|
|
21
|
+
Type: kind,
|
|
22
|
+
LoadFailed: true,
|
|
23
|
+
FailureBase: base.Absolute,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func populationIsHealthy(
|
|
28
|
+
inventories map[string]*artifactInventory,
|
|
29
|
+
base populationBase,
|
|
30
|
+
paths []string,
|
|
31
|
+
) bool {
|
|
32
|
+
for _, inventory := range inventories {
|
|
33
|
+
if inventory != nil &&
|
|
34
|
+
inventory.LoadFailed &&
|
|
35
|
+
inventory.FailureBase == base.Absolute {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for _, path := range paths {
|
|
40
|
+
if inventory := inventories[path]; inventory != nil && inventory.LoadFailed {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
package/native/hints.go
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sort"
|
|
5
|
+
"sync"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// graphCorpus is what a passing Check publishes for Hints to project.
|
|
11
|
+
//
|
|
12
|
+
// It carries the inventories Check already built rather than the paths to
|
|
13
|
+
// rebuild them from. The graph loads Markdown and Swagger to do its own job, so
|
|
14
|
+
// handing that result forward costs a build nothing; reloading inside Hints
|
|
15
|
+
// would read every source a second time to answer a question the rule had just
|
|
16
|
+
// finished answering.
|
|
17
|
+
//
|
|
18
|
+
// What the editor path does pay is Check itself. A hints request runs a fresh
|
|
19
|
+
// project cycle (`linthost/hints.go:74`), so every source the graph reads is
|
|
20
|
+
// read again there — including an HTTP(S) Swagger reference, which is fetched.
|
|
21
|
+
// That cost is inherent to answering from a current index rather than a stale
|
|
22
|
+
// one, and it is bounded the same way a build is: a source that fails to load
|
|
23
|
+
// is reported by Check, which fails the rule, which withdraws the corpus.
|
|
24
|
+
type graphCorpus struct {
|
|
25
|
+
Config graphConfig
|
|
26
|
+
Markdown map[string]*artifactInventory
|
|
27
|
+
Prisma map[string]*artifactInventory
|
|
28
|
+
Swagger map[string]*artifactInventory
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// graphCycleState is the immutable graph corpus plus the one piece of mutable
|
|
32
|
+
// coordination file rules need during this Program cycle.
|
|
33
|
+
//
|
|
34
|
+
// The host exposes project state, not a lifecycle identifier, to contributor
|
|
35
|
+
// file rules. Keeping the diagnostic gate here gives every parallel file walk
|
|
36
|
+
// one cycle-scoped rendezvous and lets the host discard it on the next watch
|
|
37
|
+
// rebuild. The mutex owns only a set membership check; graph data remains
|
|
38
|
+
// immutable.
|
|
39
|
+
type graphCycleState struct {
|
|
40
|
+
Corpus graphCorpus
|
|
41
|
+
Diagnostics programDiagnosticGate
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type programDiagnosticGate struct {
|
|
45
|
+
mutex sync.Mutex
|
|
46
|
+
reported map[string]bool
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (gate *programDiagnosticGate) first(key string) bool {
|
|
50
|
+
if gate == nil {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
gate.mutex.Lock()
|
|
54
|
+
defer gate.mutex.Unlock()
|
|
55
|
+
if gate.reported == nil {
|
|
56
|
+
gate.reported = map[string]bool{}
|
|
57
|
+
}
|
|
58
|
+
if gate.reported[key] {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
gate.reported[key] = true
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Hints projects the configured evidence population into editor completions.
|
|
66
|
+
//
|
|
67
|
+
// A target has to be reproduced exactly, and the anchor an author cannot recall
|
|
68
|
+
// is the one this offers. The corpus is a value: the host serializes it and the
|
|
69
|
+
// LSP proxy answers from cache long after the lint process exited, so nothing
|
|
70
|
+
// here can be computed per keystroke.
|
|
71
|
+
//
|
|
72
|
+
// The corpus is available only while the graph passes, and that is a host gate
|
|
73
|
+
// rather than a choice made here. `linthost/hints.go:147-149` skips a rule whose
|
|
74
|
+
// snapshot is not `ProjectRulePassed` or whose state is nil, and
|
|
75
|
+
// `projectReporter.Report` sets `failed = true` unconditionally
|
|
76
|
+
// (`linthost/project_engine.go:68-77`). So the cycle that reports an unmet
|
|
77
|
+
// obligation is the cycle that withdraws the completions — which is the cycle an
|
|
78
|
+
// author is most likely to be writing a citation in. Nothing in this package can
|
|
79
|
+
// widen that; it widens upstream, by letting a project rule publish state it
|
|
80
|
+
// reported against.
|
|
81
|
+
func (graphRule) Hints(ctx *rule.HintContext) []rule.Hint {
|
|
82
|
+
if ctx == nil {
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
cycle, published := ctx.State.(*graphCycleState)
|
|
86
|
+
if !published || cycle == nil {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
corpus := cycle.Corpus
|
|
90
|
+
hints := []rule.Hint{}
|
|
91
|
+
for _, trigger := range evidenceHintTriggers {
|
|
92
|
+
exclusion := trigger.After == "@evidenceExclude "
|
|
93
|
+
units := selectedCompletionUnits(
|
|
94
|
+
corpus.Config,
|
|
95
|
+
corpus.Markdown,
|
|
96
|
+
corpus.Prisma,
|
|
97
|
+
corpus.Swagger,
|
|
98
|
+
exclusion,
|
|
99
|
+
)
|
|
100
|
+
routes := selectsTypeScriptReference(corpus.Config, exclusion)
|
|
101
|
+
if routes {
|
|
102
|
+
hints = append(hints, typeScriptRouteHint(trigger))
|
|
103
|
+
}
|
|
104
|
+
for _, unit := range units {
|
|
105
|
+
hints = append(hints, rule.Hint{
|
|
106
|
+
Insert: unit.Target,
|
|
107
|
+
Detail: unit.Readable,
|
|
108
|
+
Trigger: trigger,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return hints
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// evidenceHintTriggers names both tag positions a target can be written in.
|
|
116
|
+
//
|
|
117
|
+
// Each `After` ends where the target begins, which is what makes the text the
|
|
118
|
+
// author has typed so far the editor's filter. The trailing space also keeps
|
|
119
|
+
// the two apart: `"@evidence "` cannot occur inside `"@evidenceExclude "`,
|
|
120
|
+
// because the character following `@evidence` there is `E`.
|
|
121
|
+
var evidenceHintTriggers = []rule.HintTrigger{
|
|
122
|
+
{Scope: rule.HintScopeJSDoc, After: "@evidence "},
|
|
123
|
+
{Scope: rule.HintScopeJSDoc, After: "@evidenceExclude "},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// typeScriptRouteHint routes the author into TypeScript's own completion.
|
|
127
|
+
//
|
|
128
|
+
// The host merges a corpus into the upstream response and cannot remove from
|
|
129
|
+
// it, so narrowing TypeScript's symbol list is not available to us. Putting the
|
|
130
|
+
// author inside that list is: once the brace opens, the language service
|
|
131
|
+
// answers every keystroke after it, against the import scope at the cursor —
|
|
132
|
+
// which a corpus built once per Program could never know.
|
|
133
|
+
//
|
|
134
|
+
// The inserted text is deliberately unclosed. `Insert` is verbatim with no
|
|
135
|
+
// snippet expansion, so the cursor lands where the text ends; `{@link }` would
|
|
136
|
+
// park it one character past the only position where completion fires. A
|
|
137
|
+
// missing brace is visible and one keystroke, and the target grammar catches it
|
|
138
|
+
// if forgotten. A cursor in the wrong place is silent.
|
|
139
|
+
func typeScriptRouteHint(trigger rule.HintTrigger) rule.Hint {
|
|
140
|
+
return rule.Hint{
|
|
141
|
+
Insert: "{@link ",
|
|
142
|
+
Label: "{@link",
|
|
143
|
+
Detail: "TypeScript symbol, resolved through this file's imports",
|
|
144
|
+
Trigger: trigger,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// selectsTypeScriptReference reports whether any claim cites TypeScript.
|
|
149
|
+
//
|
|
150
|
+
// The entry is withheld otherwise, because a graph citing only Markdown cannot
|
|
151
|
+
// resolve an inline-link target — offering the grammar there would hand the
|
|
152
|
+
// author an unresolved-target diagnostic for taking a suggestion.
|
|
153
|
+
//
|
|
154
|
+
// The condition is necessarily global. A corpus answers a keystroke with no
|
|
155
|
+
// file context, so it cannot tell which claim owns the file the cursor is in;
|
|
156
|
+
// a repository mixing a TypeScript-citing claim with a Markdown-only one offers
|
|
157
|
+
// the entry in both. Narrowing that needs a per-file corpus, which the contract
|
|
158
|
+
// deliberately does not have.
|
|
159
|
+
func selectsTypeScriptReference(config graphConfig, exclusion bool) bool {
|
|
160
|
+
for _, claim := range config.Claims {
|
|
161
|
+
for _, reference := range claim.References {
|
|
162
|
+
if reference.Type == artifactTypeScript &&
|
|
163
|
+
(!exclusion || !reference.Policy.NoExclude) {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// selectedCompletionUnits collects the units some configured reference selects,
|
|
172
|
+
// in the order they should be offered.
|
|
173
|
+
//
|
|
174
|
+
// Slice order is the corpus's only ranking channel, so it answers what an
|
|
175
|
+
// author cannot supply from memory. A heading's generated anchor is neither
|
|
176
|
+
// visible in the project tree nor guessable from its text, so selected headings
|
|
177
|
+
// come first; a Markdown file path is typed as easily as it is read, so file
|
|
178
|
+
// targets follow; Swagger operations come last, being both few and mechanical.
|
|
179
|
+
//
|
|
180
|
+
// Order therefore serves the author who is still browsing, and the filter
|
|
181
|
+
// serves the one who is not. No hint sets Label, so the editor lists and
|
|
182
|
+
// filters on the target itself: typing `docs/pricing` narrows to that
|
|
183
|
+
// document's anchors, which is the flow this ranking is for — an author writes
|
|
184
|
+
// a citation because they are implementing a document they have already chosen.
|
|
185
|
+
//
|
|
186
|
+
// Labelling a heading by its text would invert that. `sale` would match and
|
|
187
|
+
// `docs/pricing` would match nothing, two documents sharing a heading would
|
|
188
|
+
// become indistinguishable entries, and the listed text would stop being the
|
|
189
|
+
// inserted text — three costs to serve recall-by-title, which Detail already
|
|
190
|
+
// serves by being visible. Leaving Label empty keeps what is shown and what is
|
|
191
|
+
// written identical by construction.
|
|
192
|
+
//
|
|
193
|
+
// TypeScript units are absent on purpose. TypeScript's own language service
|
|
194
|
+
// completes symbols inside `{@link}` against the scope at the cursor, which a
|
|
195
|
+
// corpus built once per Program cannot know — offering one here would duplicate
|
|
196
|
+
// a correct list with a worse one.
|
|
197
|
+
func selectedCompletionUnits(
|
|
198
|
+
config graphConfig,
|
|
199
|
+
markdown map[string]*artifactInventory,
|
|
200
|
+
prisma map[string]*artifactInventory,
|
|
201
|
+
swagger map[string]*artifactInventory,
|
|
202
|
+
exclusion bool,
|
|
203
|
+
) []*evidenceUnit {
|
|
204
|
+
ranked := map[string][]*evidenceUnit{}
|
|
205
|
+
seen := map[string]bool{}
|
|
206
|
+
for _, claim := range config.Claims {
|
|
207
|
+
for _, reference := range claim.References {
|
|
208
|
+
if exclusion && reference.Policy.NoExclude {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
inventories := inventoriesOf(
|
|
212
|
+
reference.Type,
|
|
213
|
+
markdown,
|
|
214
|
+
prisma,
|
|
215
|
+
swagger,
|
|
216
|
+
map[string]*artifactInventory{},
|
|
217
|
+
)
|
|
218
|
+
for _, path := range matchingReferencePaths(inventories, reference) {
|
|
219
|
+
inventory := inventories[path]
|
|
220
|
+
if inventory == nil {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
for _, unit := range inventory.Units {
|
|
224
|
+
if unit.Hidden != "" ||
|
|
225
|
+
!reference.Symbols.contains(unit.Symbol) ||
|
|
226
|
+
seen[unit.ID] {
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
seen[unit.ID] = true
|
|
230
|
+
group := completionRank(unit)
|
|
231
|
+
ranked[group] = append(ranked[group], unit)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
units := []*evidenceUnit{}
|
|
237
|
+
for _, group := range []string{"heading", "file", "operation", "model"} {
|
|
238
|
+
tier := ranked[group]
|
|
239
|
+
sort.SliceStable(tier, func(left int, right int) bool {
|
|
240
|
+
if tier[left].Path != tier[right].Path {
|
|
241
|
+
return tier[left].Path < tier[right].Path
|
|
242
|
+
}
|
|
243
|
+
return tier[left].Line < tier[right].Line
|
|
244
|
+
})
|
|
245
|
+
units = append(units, tier...)
|
|
246
|
+
}
|
|
247
|
+
return units
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func completionRank(unit *evidenceUnit) string {
|
|
251
|
+
switch {
|
|
252
|
+
case unit.Type == artifactSwagger:
|
|
253
|
+
return "operation"
|
|
254
|
+
case unit.Type == artifactPrisma:
|
|
255
|
+
return "model"
|
|
256
|
+
case unit.Symbol == "file":
|
|
257
|
+
return "file"
|
|
258
|
+
default:
|
|
259
|
+
return "heading"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// importBinding is one name a module brings into scope, and where it came from.
|
|
13
|
+
//
|
|
14
|
+
// Imported is empty for a namespace import, which contributes no segment of its
|
|
15
|
+
// own: `import * as api` makes `api.functional` mean `functional` inside the
|
|
16
|
+
// resolved module, while `import { ISale }` makes `ISale` mean `ISale` there.
|
|
17
|
+
type importBinding struct {
|
|
18
|
+
Local string
|
|
19
|
+
Specifier string
|
|
20
|
+
Imported string
|
|
21
|
+
Namespace bool
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// collectImportBindings indexes a file's imports by the local name each binds.
|
|
25
|
+
//
|
|
26
|
+
// Type-only imports are included deliberately. A citation-only import should be
|
|
27
|
+
// `import type`, because it is erased at emit and creates no runtime dependency
|
|
28
|
+
// or cycle — so the form the rule recommends must be the form it can resolve.
|
|
29
|
+
func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
|
|
30
|
+
bindings := map[string]importBinding{}
|
|
31
|
+
if file == nil || file.Statements == nil {
|
|
32
|
+
return bindings
|
|
33
|
+
}
|
|
34
|
+
for _, statement := range file.Statements.Nodes {
|
|
35
|
+
if statement == nil || statement.Kind != shimast.KindImportDeclaration {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
declaration := statement.AsImportDeclaration()
|
|
39
|
+
if declaration == nil || declaration.ImportClause == nil {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
specifier := stringLiteralText(declaration.ModuleSpecifier)
|
|
43
|
+
if specifier == "" {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
clause := declaration.ImportClause.AsImportClause()
|
|
47
|
+
if clause == nil {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if name := declarationName(declaration.ImportClause.Name()); name != "" {
|
|
51
|
+
bindings[name] = importBinding{
|
|
52
|
+
Local: name,
|
|
53
|
+
Specifier: specifier,
|
|
54
|
+
Imported: "default",
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if clause.NamedBindings == nil {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
switch clause.NamedBindings.Kind {
|
|
61
|
+
case shimast.KindNamespaceImport:
|
|
62
|
+
if name := declarationName(clause.NamedBindings.Name()); name != "" {
|
|
63
|
+
bindings[name] = importBinding{
|
|
64
|
+
Local: name,
|
|
65
|
+
Specifier: specifier,
|
|
66
|
+
Namespace: true,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
case shimast.KindNamedImports:
|
|
70
|
+
named := clause.NamedBindings.AsNamedImports()
|
|
71
|
+
if named == nil || named.Elements == nil {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
for _, element := range named.Elements.Nodes {
|
|
75
|
+
if element == nil || element.Kind != shimast.KindImportSpecifier {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
specifierNode := element.AsImportSpecifier()
|
|
79
|
+
if specifierNode == nil {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
local := declarationName(element.Name())
|
|
83
|
+
imported := local
|
|
84
|
+
if specifierNode.PropertyName != nil {
|
|
85
|
+
imported = declarationName(specifierNode.PropertyName)
|
|
86
|
+
}
|
|
87
|
+
if local == "" || imported == "" {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
bindings[local] = importBinding{
|
|
91
|
+
Local: local,
|
|
92
|
+
Specifier: specifier,
|
|
93
|
+
Imported: imported,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return bindings
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func stringLiteralText(node *shimast.Node) string {
|
|
102
|
+
if node == nil || node.Kind != shimast.KindStringLiteral {
|
|
103
|
+
return ""
|
|
104
|
+
}
|
|
105
|
+
return node.Text()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// typeScriptModuleExtensions are tried in the order TypeScript itself prefers.
|
|
109
|
+
var typeScriptModuleExtensions = []string{
|
|
110
|
+
".ts",
|
|
111
|
+
".tsx",
|
|
112
|
+
".mts",
|
|
113
|
+
".cts",
|
|
114
|
+
".d.ts",
|
|
115
|
+
".d.mts",
|
|
116
|
+
".d.cts",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// splitPackageSpecifier separates a package name from a deep-import subpath,
|
|
120
|
+
// keeping the leading segment of a scoped name attached to its scope.
|
|
121
|
+
// moduleCandidates lists the files a specifier may denote, most specific first.
|
|
122
|
+
//
|
|
123
|
+
// The `.js` rewrite is not a convenience: under `nodenext` a TypeScript source
|
|
124
|
+
// must spell its sibling as `./x.js`, so refusing to map it back would make the
|
|
125
|
+
// correct import form unresolvable.
|
|
126
|
+
func moduleCandidates(base string) []string {
|
|
127
|
+
candidates := []string{base}
|
|
128
|
+
stripped := base
|
|
129
|
+
for _, emitted := range []string{".js", ".mjs", ".cjs"} {
|
|
130
|
+
if strings.HasSuffix(base, emitted) {
|
|
131
|
+
stripped = strings.TrimSuffix(base, emitted)
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for _, extension := range typeScriptModuleExtensions {
|
|
136
|
+
candidates = append(candidates, stripped+extension)
|
|
137
|
+
}
|
|
138
|
+
for _, extension := range typeScriptModuleExtensions {
|
|
139
|
+
candidates = append(candidates, path.Join(stripped, "index"+extension))
|
|
140
|
+
}
|
|
141
|
+
return candidates
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func splitPackageSpecifier(specifier string) (string, string) {
|
|
145
|
+
segments := strings.Split(specifier, "/")
|
|
146
|
+
if len(segments) == 0 || segments[0] == "" {
|
|
147
|
+
return "", ""
|
|
148
|
+
}
|
|
149
|
+
count := 1
|
|
150
|
+
if strings.HasPrefix(segments[0], "@") {
|
|
151
|
+
if len(segments) < 2 {
|
|
152
|
+
return "", ""
|
|
153
|
+
}
|
|
154
|
+
count = 2
|
|
155
|
+
}
|
|
156
|
+
name := strings.Join(segments[:count], "/")
|
|
157
|
+
subpath := strings.Join(segments[count:], "/")
|
|
158
|
+
if subpath != "" {
|
|
159
|
+
subpath = "./" + subpath
|
|
160
|
+
}
|
|
161
|
+
return name, subpath
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func readPackageManifest(root string, relative string) map[string]json.RawMessage {
|
|
165
|
+
content, err := os.ReadFile(path.Join(strings.ReplaceAll(root, "\\", "/"), relative))
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
manifest := map[string]json.RawMessage{}
|
|
170
|
+
if err := json.Unmarshal(content, &manifest); err != nil {
|
|
171
|
+
return nil
|
|
172
|
+
}
|
|
173
|
+
return manifest
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func packageTypeEntry(
|
|
177
|
+
manifest map[string]json.RawMessage,
|
|
178
|
+
subpath string,
|
|
179
|
+
) string {
|
|
180
|
+
if manifest == nil {
|
|
181
|
+
return ""
|
|
182
|
+
}
|
|
183
|
+
key := "."
|
|
184
|
+
if subpath != "" {
|
|
185
|
+
key = subpath
|
|
186
|
+
}
|
|
187
|
+
if entry := exportsTypeEntry(manifest["exports"], key); entry != "" {
|
|
188
|
+
return entry
|
|
189
|
+
}
|
|
190
|
+
if entry := typesVersionsEntry(manifest["typesVersions"], key); entry != "" {
|
|
191
|
+
return entry
|
|
192
|
+
}
|
|
193
|
+
if subpath == "" {
|
|
194
|
+
for _, field := range []string{"types", "typings"} {
|
|
195
|
+
var value string
|
|
196
|
+
if err := json.Unmarshal(manifest[field], &value); err == nil && value != "" {
|
|
197
|
+
return value
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Every declaration channel above has been asked first, because a package
|
|
202
|
+
// that names one has said where its declarations are and its runtime entry
|
|
203
|
+
// is not it. Only when none answers is the runtime entry considered, and
|
|
204
|
+
// only when it names TypeScript: a source-first workspace package has no
|
|
205
|
+
// other way to say where its entry is.
|
|
206
|
+
if entry := exportsRuntimeEntry(manifest["exports"], key); entry != "" {
|
|
207
|
+
return entry
|
|
208
|
+
}
|
|
209
|
+
if subpath != "" {
|
|
210
|
+
return ""
|
|
211
|
+
}
|
|
212
|
+
var main string
|
|
213
|
+
if err := json.Unmarshal(manifest["main"], &main); err == nil {
|
|
214
|
+
return typeScriptEntryTarget(main)
|
|
215
|
+
}
|
|
216
|
+
return ""
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// typeScriptEntryTarget returns target when it names TypeScript itself.
|
|
220
|
+
//
|
|
221
|
+
// A published package points its runtime entry at emitted JavaScript and keeps
|
|
222
|
+
// its declarations behind a `types` condition, so following that entry would
|
|
223
|
+
// address the wrong file. A source-first workspace package has no emit to point
|
|
224
|
+
// at: pnpm links `packages/api` into `node_modules`, and its entry names
|
|
225
|
+
// `./src/index.ts`, which is both what a consumer imports and where the
|
|
226
|
+
// declarations are. Refusing it there leaves a linked package with no
|
|
227
|
+
// resolvable entry, and a reference that cannot find its entry publishes its
|
|
228
|
+
// units under the module that matched instead — which is how
|
|
229
|
+
// `functional.health.get` collapses to `get`.
|
|
230
|
+
func typeScriptEntryTarget(target string) string {
|
|
231
|
+
for _, extension := range typeScriptModuleExtensions {
|
|
232
|
+
if strings.HasSuffix(target, extension) {
|
|
233
|
+
return target
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return ""
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// exportsTypeEntry reads the `types` condition of an exports map.
|
|
240
|
+
//
|
|
241
|
+
// The map may be a bare string, a condition object, or a subpath object whose
|
|
242
|
+
// values are either. Only the `types` condition is followed, because a
|
|
243
|
+
// citation addresses declarations rather than the runtime entry `import` and
|
|
244
|
+
// `require` name. [exportsRuntimeEntry] answers for the package that has no
|
|
245
|
+
// declaration channel at all.
|
|
246
|
+
func exportsTypeEntry(raw json.RawMessage, key string) string {
|
|
247
|
+
if len(raw) == 0 {
|
|
248
|
+
return ""
|
|
249
|
+
}
|
|
250
|
+
// A bare-string exports map names the runtime entry only, and a citation
|
|
251
|
+
// addresses declarations, so there is nothing here to follow.
|
|
252
|
+
var direct string
|
|
253
|
+
if err := json.Unmarshal(raw, &direct); err == nil {
|
|
254
|
+
return ""
|
|
255
|
+
}
|
|
256
|
+
object := map[string]json.RawMessage{}
|
|
257
|
+
if err := json.Unmarshal(raw, &object); err != nil {
|
|
258
|
+
return ""
|
|
259
|
+
}
|
|
260
|
+
if _, subpaths := object["."]; subpaths || strings.HasPrefix(key, "./") {
|
|
261
|
+
entry, exists := object[key]
|
|
262
|
+
if !exists {
|
|
263
|
+
return ""
|
|
264
|
+
}
|
|
265
|
+
return exportsConditionEntry(entry)
|
|
266
|
+
}
|
|
267
|
+
if key != "." {
|
|
268
|
+
return ""
|
|
269
|
+
}
|
|
270
|
+
return exportsConditionEntry(raw)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func exportsConditionEntry(raw json.RawMessage) string {
|
|
274
|
+
if len(raw) == 0 {
|
|
275
|
+
return ""
|
|
276
|
+
}
|
|
277
|
+
var direct string
|
|
278
|
+
if err := json.Unmarshal(raw, &direct); err == nil {
|
|
279
|
+
return ""
|
|
280
|
+
}
|
|
281
|
+
object := map[string]json.RawMessage{}
|
|
282
|
+
if err := json.Unmarshal(raw, &object); err != nil {
|
|
283
|
+
return ""
|
|
284
|
+
}
|
|
285
|
+
if types, exists := object["types"]; exists {
|
|
286
|
+
var value string
|
|
287
|
+
if err := json.Unmarshal(types, &value); err == nil {
|
|
288
|
+
return value
|
|
289
|
+
}
|
|
290
|
+
return exportsConditionEntry(types)
|
|
291
|
+
}
|
|
292
|
+
for _, condition := range []string{"import", "default"} {
|
|
293
|
+
if nested, exists := object[condition]; exists {
|
|
294
|
+
if entry := exportsConditionEntry(nested); entry != "" {
|
|
295
|
+
return entry
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return ""
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// exportsRuntimeEntry reads the runtime entry of an exports map, and returns
|
|
303
|
+
// it only when it names TypeScript.
|
|
304
|
+
//
|
|
305
|
+
// It mirrors [exportsTypeEntry] over the same three shapes, following the
|
|
306
|
+
// target a consumer actually imports rather than the `types` condition. That
|
|
307
|
+
// target is the declarations exactly when it is TypeScript, which is the
|
|
308
|
+
// source-first workspace package [typeScriptEntryTarget] describes.
|
|
309
|
+
func exportsRuntimeEntry(raw json.RawMessage, key string) string {
|
|
310
|
+
if len(raw) == 0 {
|
|
311
|
+
return ""
|
|
312
|
+
}
|
|
313
|
+
var direct string
|
|
314
|
+
if err := json.Unmarshal(raw, &direct); err == nil {
|
|
315
|
+
if key != "." {
|
|
316
|
+
return ""
|
|
317
|
+
}
|
|
318
|
+
return typeScriptEntryTarget(direct)
|
|
319
|
+
}
|
|
320
|
+
object := map[string]json.RawMessage{}
|
|
321
|
+
if err := json.Unmarshal(raw, &object); err != nil {
|
|
322
|
+
return ""
|
|
323
|
+
}
|
|
324
|
+
if _, subpaths := object["."]; subpaths || strings.HasPrefix(key, "./") {
|
|
325
|
+
entry, exists := object[key]
|
|
326
|
+
if !exists {
|
|
327
|
+
return ""
|
|
328
|
+
}
|
|
329
|
+
return runtimeConditionEntry(entry)
|
|
330
|
+
}
|
|
331
|
+
if key != "." {
|
|
332
|
+
return ""
|
|
333
|
+
}
|
|
334
|
+
return runtimeConditionEntry(raw)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
func runtimeConditionEntry(raw json.RawMessage) string {
|
|
338
|
+
if len(raw) == 0 {
|
|
339
|
+
return ""
|
|
340
|
+
}
|
|
341
|
+
var direct string
|
|
342
|
+
if err := json.Unmarshal(raw, &direct); err == nil {
|
|
343
|
+
return typeScriptEntryTarget(direct)
|
|
344
|
+
}
|
|
345
|
+
object := map[string]json.RawMessage{}
|
|
346
|
+
if err := json.Unmarshal(raw, &object); err != nil {
|
|
347
|
+
return ""
|
|
348
|
+
}
|
|
349
|
+
for _, condition := range []string{"import", "default", "require"} {
|
|
350
|
+
if nested, exists := object[condition]; exists {
|
|
351
|
+
if entry := runtimeConditionEntry(nested); entry != "" {
|
|
352
|
+
return entry
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return ""
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
func typesVersionsEntry(raw json.RawMessage, key string) string {
|
|
360
|
+
if len(raw) == 0 || key != "." {
|
|
361
|
+
return ""
|
|
362
|
+
}
|
|
363
|
+
versions := map[string]map[string][]string{}
|
|
364
|
+
if err := json.Unmarshal(raw, &versions); err != nil {
|
|
365
|
+
return ""
|
|
366
|
+
}
|
|
367
|
+
for _, mapping := range versions {
|
|
368
|
+
if entries, exists := mapping["*"]; exists && len(entries) != 0 {
|
|
369
|
+
return entries[0]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return ""
|
|
373
|
+
}
|