@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/inputs.go
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ProjectInputs declares the files this rule reads from outside the Program.
|
|
10
|
+
//
|
|
11
|
+
// Markdown, Prisma, and Swagger evidence never enters the TypeScript Program,
|
|
12
|
+
// so until `@ttsc/lint@0.22.0` shipped this contract the host had no way to
|
|
13
|
+
// learn that the graph depended on them. The resulting asymmetry was invisible
|
|
14
|
+
// rather than merely inconvenient: a developer editing code saw fresh
|
|
15
|
+
// diagnostics, because the TypeScript event drove a cycle that reloaded the
|
|
16
|
+
// documents along with it, while a developer editing only a spec section or a
|
|
17
|
+
// schema saw the citation that had just gone stale keep reporting green.
|
|
18
|
+
//
|
|
19
|
+
// The host calls this after resolving options and project identity but before
|
|
20
|
+
// loading a Program, so nothing here may read `ctx.Sources`, and nothing here
|
|
21
|
+
// may touch the filesystem — this declares configured topology rather than the
|
|
22
|
+
// files a successful load happened to reach.
|
|
23
|
+
func (graphRule) ProjectInputs(ctx *rule.ProjectInputContext) []rule.ProjectInput {
|
|
24
|
+
if ctx == nil {
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
// Configuration problems belong to Check, which has a reporter and a source
|
|
28
|
+
// position to name. Declaring the inputs of whichever claims did decode is
|
|
29
|
+
// strictly better than declaring none: a graph the author is midway through
|
|
30
|
+
// repairing is exactly when the editor must keep watching the sources.
|
|
31
|
+
config, _ := decodeGraphConfig(ctx.Options)
|
|
32
|
+
return graphProjectInputs(config)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// graphProjectInputs collects the external dependency of every configured
|
|
36
|
+
// obligation.
|
|
37
|
+
//
|
|
38
|
+
// TypeScript claims and references are absent by design. Their inventories are
|
|
39
|
+
// materialized from `ctx.Sources`, which is the Program the host already
|
|
40
|
+
// watches, so declaring them again would ask for a second watcher on a file
|
|
41
|
+
// that already has one.
|
|
42
|
+
func graphProjectInputs(config graphConfig) []rule.ProjectInput {
|
|
43
|
+
config = enabledGraphConfig(config)
|
|
44
|
+
inputs := []rule.ProjectInput{}
|
|
45
|
+
for _, claim := range config.Claims {
|
|
46
|
+
switch claim.Type {
|
|
47
|
+
case artifactMarkdown, artifactPrisma:
|
|
48
|
+
inputs = append(inputs, globInputs(claim.Root, claim.Files)...)
|
|
49
|
+
}
|
|
50
|
+
for _, reference := range claim.References {
|
|
51
|
+
switch reference.Type {
|
|
52
|
+
case artifactMarkdown, artifactPrisma:
|
|
53
|
+
inputs = append(inputs, globInputs(reference.Root, reference.Files)...)
|
|
54
|
+
case artifactSwagger:
|
|
55
|
+
inputs = append(inputs, localSwaggerInputs(reference.Source)...)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return inputs
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// globInputs publishes the positive half of one glob set, anchored on the root
|
|
63
|
+
// its population resolves against.
|
|
64
|
+
//
|
|
65
|
+
// Exclusions are dropped rather than translated, because the host's dependency
|
|
66
|
+
// model has no negation — declaring `!docs/private/**` as a glob would watch
|
|
67
|
+
// precisely the documents this graph refuses to read. What is left over-selects
|
|
68
|
+
// slightly, and that is the safe direction: a spurious rebuild costs one cycle,
|
|
69
|
+
// while a missing one leaves a stale citation reporting green, which is the
|
|
70
|
+
// whole failure this contract exists to remove.
|
|
71
|
+
//
|
|
72
|
+
// The declared pattern is the compiled segment form rather than `Raw`, so the
|
|
73
|
+
// host receives a pattern with the exclusion marker already stripped, `\`
|
|
74
|
+
// already normalized to `/`, and a leading `./` already gone.
|
|
75
|
+
//
|
|
76
|
+
// The root is prefixed as the author wrote it rather than resolved here. The
|
|
77
|
+
// host anchors a relative pattern against the same physical project root this
|
|
78
|
+
// rule uses (`linthost/project_inputs.go:151-171`), and it accepts one that
|
|
79
|
+
// ascends or is absolute — so `../../docs/**/*.md` and `C:/shared/docs/**/*.md`
|
|
80
|
+
// both arrive at the directory `Check` will read. Resolving it here would
|
|
81
|
+
// duplicate that arithmetic and make this contract depend on a project identity
|
|
82
|
+
// it is supposed to be able to answer without.
|
|
83
|
+
func globInputs(root string, globs globSet) []rule.ProjectInput {
|
|
84
|
+
inputs := make([]rule.ProjectInput, 0, len(globs.Patterns))
|
|
85
|
+
for _, pattern := range globs.Patterns {
|
|
86
|
+
if pattern.Exclude {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
joined := strings.Join(pattern.Segments, "/")
|
|
90
|
+
if root != "" {
|
|
91
|
+
joined = strings.TrimSuffix(root, "/") + "/" + joined
|
|
92
|
+
}
|
|
93
|
+
inputs = append(inputs, rule.ProjectInput{
|
|
94
|
+
Kind: rule.ProjectInputGlob,
|
|
95
|
+
Pattern: joined,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return inputs
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// localSwaggerInputs publishes a Swagger reference only when it names a file.
|
|
102
|
+
//
|
|
103
|
+
// Withholding the URL form is not a shortcut. The host rejects a remote pattern
|
|
104
|
+
// outright (`linthost/project_inputs.go:159-161`), and one rejected input
|
|
105
|
+
// discards the whole snapshot for every project rule in the run
|
|
106
|
+
// (`linthost/project_inputs.go:113-135`) — so declaring a URL here would take
|
|
107
|
+
// this graph's Markdown globs down with it and restore the exact blindness the
|
|
108
|
+
// contract removes.
|
|
109
|
+
//
|
|
110
|
+
// The boundary is also real rather than imposed: a URL has no filesystem event
|
|
111
|
+
// to observe, so its freshness is per-evaluation and belongs to the loader.
|
|
112
|
+
//
|
|
113
|
+
// A local path is published exactly as it normalized, including one that
|
|
114
|
+
// ascends out of the project or names an absolute location. The host resolves
|
|
115
|
+
// both against the same root this rule reads them from, so a generated document
|
|
116
|
+
// in a sibling package is watched on the same terms as one inside the project.
|
|
117
|
+
func localSwaggerInputs(source string) []rule.ProjectInput {
|
|
118
|
+
if source == "" || isRemoteSwaggerSource(source) {
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
return []rule.ProjectInput{{
|
|
122
|
+
Kind: rule.ProjectInputFile,
|
|
123
|
+
Pattern: source,
|
|
124
|
+
}}
|
|
125
|
+
}
|
package/native/loader.go
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"io/fs"
|
|
6
|
+
"os"
|
|
7
|
+
"path"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"sort"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
14
|
+
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// typeScriptLoader materializes TypeScript inventories for a reference
|
|
18
|
+
// population, from the Program where possible and from disk where necessary.
|
|
19
|
+
//
|
|
20
|
+
// Disk parsing is not an optimization detail, it is the requirement. A package
|
|
21
|
+
// symbol that nothing imports is absent from the Program by definition, and
|
|
22
|
+
// that symbol is precisely the one an obligation needs to name — an evidence
|
|
23
|
+
// graph exists to report the operation the frontend never called.
|
|
24
|
+
type typeScriptLoader struct {
|
|
25
|
+
root string
|
|
26
|
+
program map[string]*artifactInventory
|
|
27
|
+
parsed map[string]*artifactInventory
|
|
28
|
+
resolved map[string]string
|
|
29
|
+
failures map[string]string
|
|
30
|
+
installs map[string]installedPackageLocation
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func newTypeScriptLoader(
|
|
34
|
+
root string,
|
|
35
|
+
program map[string]*artifactInventory,
|
|
36
|
+
) *typeScriptLoader {
|
|
37
|
+
loader := &typeScriptLoader{
|
|
38
|
+
root: strings.ReplaceAll(root, "\\", "/"),
|
|
39
|
+
program: map[string]*artifactInventory{},
|
|
40
|
+
parsed: map[string]*artifactInventory{},
|
|
41
|
+
resolved: map[string]string{},
|
|
42
|
+
failures: map[string]string{},
|
|
43
|
+
installs: map[string]installedPackageLocation{},
|
|
44
|
+
}
|
|
45
|
+
for _, inventory := range program {
|
|
46
|
+
if inventory == nil || inventory.Path == "" {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
location := loader.projectPath(inventory.Path)
|
|
50
|
+
current := loader.program[location]
|
|
51
|
+
if current == nil ||
|
|
52
|
+
current.Address != current.Path && inventory.Address == inventory.Path {
|
|
53
|
+
loader.program[location] = inventory
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return loader
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// inventory returns the scanned form of a project-relative TypeScript file.
|
|
60
|
+
//
|
|
61
|
+
// The Program's copy wins when it exists so that a file under edit is read as
|
|
62
|
+
// the editor has it, not as the disk last saw it.
|
|
63
|
+
func (loader *typeScriptLoader) inventory(relative string) *artifactInventory {
|
|
64
|
+
if relative == "" {
|
|
65
|
+
return nil
|
|
66
|
+
}
|
|
67
|
+
relative = loader.projectPath(relative)
|
|
68
|
+
if inventory := loader.program[relative]; inventory != nil {
|
|
69
|
+
return inventory
|
|
70
|
+
}
|
|
71
|
+
if inventory, cached := loader.parsed[relative]; cached {
|
|
72
|
+
return inventory
|
|
73
|
+
}
|
|
74
|
+
loader.parsed[relative] = loader.parse(relative)
|
|
75
|
+
return loader.parsed[relative]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
|
|
79
|
+
relative = loader.projectPath(relative)
|
|
80
|
+
content, err := os.ReadFile(path.Join(loader.root, relative))
|
|
81
|
+
if err != nil {
|
|
82
|
+
loader.failures[relative] = err.Error()
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
kind := shimcore.ScriptKindTS
|
|
86
|
+
if strings.HasSuffix(strings.ToLower(relative), ".tsx") {
|
|
87
|
+
kind = shimcore.ScriptKindTSX
|
|
88
|
+
}
|
|
89
|
+
file := shimparser.ParseSourceFile(
|
|
90
|
+
shimast.SourceFileParseOptions{
|
|
91
|
+
FileName: path.Join(loader.root, relative),
|
|
92
|
+
},
|
|
93
|
+
string(content),
|
|
94
|
+
kind,
|
|
95
|
+
)
|
|
96
|
+
if file == nil {
|
|
97
|
+
loader.failures[relative] = "the TypeScript parser returned no source file"
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
return scanTypeScriptInventory(relative, file)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func (loader *typeScriptLoader) failure(relative string) string {
|
|
104
|
+
if loader == nil {
|
|
105
|
+
return ""
|
|
106
|
+
}
|
|
107
|
+
return loader.failures[relative]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// exists reports whether a project-relative TypeScript file can be read at all,
|
|
111
|
+
// without paying to scan it.
|
|
112
|
+
func (loader *typeScriptLoader) exists(relative string) bool {
|
|
113
|
+
relative = loader.projectPath(relative)
|
|
114
|
+
if loader.program[relative] != nil {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
return loader.existsOnDisk(relative)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// existsOnDisk probes the filesystem for an already-normalized path.
|
|
121
|
+
//
|
|
122
|
+
// The probe is the expensive half of module resolution — one syscall per
|
|
123
|
+
// candidate, and a watch cycle resolves every re-export in the population — so
|
|
124
|
+
// callers that can answer from the Program should do that first.
|
|
125
|
+
func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
|
|
126
|
+
info, err := os.Stat(path.Join(loader.root, relative))
|
|
127
|
+
return err == nil && !info.IsDir()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// resolve maps a module specifier written in one file to a project-relative
|
|
131
|
+
// path, trying the same candidates TypeScript would.
|
|
132
|
+
func (loader *typeScriptLoader) resolve(from string, specifier string) string {
|
|
133
|
+
key := from + "\x00" + specifier
|
|
134
|
+
if cached, exists := loader.resolved[key]; exists {
|
|
135
|
+
return cached
|
|
136
|
+
}
|
|
137
|
+
loader.resolved[key] = loader.resolveUncached(from, specifier)
|
|
138
|
+
return loader.resolved[key]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func (loader *typeScriptLoader) resolveUncached(
|
|
142
|
+
from string,
|
|
143
|
+
specifier string,
|
|
144
|
+
) string {
|
|
145
|
+
if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
|
|
146
|
+
base := path.Clean(path.Join(path.Dir(from), specifier))
|
|
147
|
+
candidates := moduleCandidates(base)
|
|
148
|
+
normalized := make([]string, 0, len(candidates))
|
|
149
|
+
for _, candidate := range candidates {
|
|
150
|
+
normalized = append(normalized, loader.projectPath(candidate))
|
|
151
|
+
}
|
|
152
|
+
// The Program answers first, and not only because it answers without a
|
|
153
|
+
// syscall. A project that emits beside its sources has both `x.js` and
|
|
154
|
+
// `x.ts` on disk, and `x.js` is written earlier in the candidate order;
|
|
155
|
+
// resolving an import to emitted JavaScript would read a module whose
|
|
156
|
+
// declarations the graph cannot address.
|
|
157
|
+
for _, candidate := range normalized {
|
|
158
|
+
if loader.program[candidate] != nil {
|
|
159
|
+
return candidate
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for _, candidate := range normalized {
|
|
163
|
+
if loader.existsOnDisk(candidate) {
|
|
164
|
+
return candidate
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return ""
|
|
168
|
+
}
|
|
169
|
+
if strings.HasPrefix(specifier, "/") {
|
|
170
|
+
return ""
|
|
171
|
+
}
|
|
172
|
+
return loader.resolvePackage(specifier)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// projectPath gives every Program source and module candidate one identity
|
|
176
|
+
// relative to the ttsc project. A rooted claim may name the same physical file
|
|
177
|
+
// through `../api`, while an import can arrive there through a different
|
|
178
|
+
// sequence of sibling segments; resolving both through the project root keeps
|
|
179
|
+
// Windows and POSIX separators from creating distinct module identities.
|
|
180
|
+
func (loader *typeScriptLoader) projectPath(relative string) string {
|
|
181
|
+
if loader == nil || relative == "" {
|
|
182
|
+
return relative
|
|
183
|
+
}
|
|
184
|
+
// A path that is already a clean, forward-slashed, project-relative name is
|
|
185
|
+
// its own identity. Saying so here matters: module resolution normalizes
|
|
186
|
+
// every candidate of every specifier, and the general form below walks the
|
|
187
|
+
// path twice through the filesystem package to learn nothing.
|
|
188
|
+
if isCleanProjectRelativePath(relative) {
|
|
189
|
+
return relative
|
|
190
|
+
}
|
|
191
|
+
local := filepath.FromSlash(relative)
|
|
192
|
+
absolute := local
|
|
193
|
+
if !filepath.IsAbs(local) {
|
|
194
|
+
absolute = filepath.Join(filepath.FromSlash(loader.root), local)
|
|
195
|
+
}
|
|
196
|
+
projectRelative, err := filepath.Rel(
|
|
197
|
+
filepath.FromSlash(loader.root),
|
|
198
|
+
filepath.Clean(absolute),
|
|
199
|
+
)
|
|
200
|
+
if err != nil {
|
|
201
|
+
return filepath.ToSlash(filepath.Clean(absolute))
|
|
202
|
+
}
|
|
203
|
+
return strings.TrimPrefix(filepath.ToSlash(projectRelative), "./")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// isCleanProjectRelativePath reports whether a path is already the identity
|
|
207
|
+
// `projectPath` would produce: forward slashes, no drive or leading separator,
|
|
208
|
+
// and no `.` or `..` segment to collapse.
|
|
209
|
+
func isCleanProjectRelativePath(value string) bool {
|
|
210
|
+
if value == "" || strings.ContainsRune(value, '\\') {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
if strings.HasPrefix(value, "/") || filepath.IsAbs(value) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
for segment := range strings.SplitSeq(value, "/") {
|
|
217
|
+
if segment == "" || segment == "." || segment == ".." {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// resolvePackage finds the declaration entry of an installed package.
|
|
225
|
+
//
|
|
226
|
+
// The entry comes from the `types` condition of an `exports` map, then
|
|
227
|
+
// `typesVersions`, then `types` or `typings`. A package that emits JavaScript
|
|
228
|
+
// stops there, because its runtime entry names the emit rather than the
|
|
229
|
+
// declarations a citation addresses.
|
|
230
|
+
//
|
|
231
|
+
// A source-first workspace package is followed one step further: when its
|
|
232
|
+
// `exports` target or `main` names TypeScript itself, that file is both the
|
|
233
|
+
// entry a consumer imports and the declarations, so it is the entry. This is
|
|
234
|
+
// the ordinary shape in a pnpm TypeScript monorepo, where the dependency is a
|
|
235
|
+
// link to a package that has no emit to point at.
|
|
236
|
+
func (loader *typeScriptLoader) resolvePackage(specifier string) string {
|
|
237
|
+
name, subpath := splitPackageSpecifier(specifier)
|
|
238
|
+
if name == "" {
|
|
239
|
+
return ""
|
|
240
|
+
}
|
|
241
|
+
directory, manifest := loader.installedPackage(name)
|
|
242
|
+
entry := packageTypeEntry(manifest, subpath)
|
|
243
|
+
if entry == "" {
|
|
244
|
+
if subpath == "" {
|
|
245
|
+
return ""
|
|
246
|
+
}
|
|
247
|
+
entry = subpath
|
|
248
|
+
}
|
|
249
|
+
for _, candidate := range moduleCandidates(path.Join(directory, entry)) {
|
|
250
|
+
if loader.exists(candidate) {
|
|
251
|
+
return candidate
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return ""
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// packageEntryModule resolves the declaration entry a package reference starts
|
|
258
|
+
// its traversal from.
|
|
259
|
+
func (loader *typeScriptLoader) packageEntryModule(name string) string {
|
|
260
|
+
return loader.resolvePackage(name)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// installedPackage finds where a package is installed, and its manifest.
|
|
264
|
+
//
|
|
265
|
+
// Resolution walks up from the project root the way Node's own does, because a
|
|
266
|
+
// nested Program does not have its own install. `packages/backend/test` is its
|
|
267
|
+
// own ttsc project — `test/tsconfig.json` compiles the tests together with the
|
|
268
|
+
// backend source — but the package manager installed into
|
|
269
|
+
// `packages/backend/node_modules`, one level above. Looking only beside the
|
|
270
|
+
// project root leaves that Program unable to see a dependency it imports, and a
|
|
271
|
+
// package reference that cannot read a manifest resolves no entry, which
|
|
272
|
+
// publishes its units under the module that matched instead of under the
|
|
273
|
+
// specifier a citation can spell.
|
|
274
|
+
//
|
|
275
|
+
// The walk stops at the filesystem root. A directory that holds no
|
|
276
|
+
// `node_modules` for this name simply is not the install, so the loop asks the
|
|
277
|
+
// parent rather than concluding the package is absent.
|
|
278
|
+
func (loader *typeScriptLoader) installedPackage(
|
|
279
|
+
name string,
|
|
280
|
+
) (string, map[string]json.RawMessage) {
|
|
281
|
+
if cached, exists := loader.installs[name]; exists {
|
|
282
|
+
return cached.Directory, cached.Manifest
|
|
283
|
+
}
|
|
284
|
+
directory, manifest := loader.locateInstalledPackage(name)
|
|
285
|
+
loader.installs[name] = installedPackageLocation{
|
|
286
|
+
Directory: directory,
|
|
287
|
+
Manifest: manifest,
|
|
288
|
+
}
|
|
289
|
+
return directory, manifest
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// installedPackageLocation caches one upward search. The walk costs a read per
|
|
293
|
+
// level, and both the glob base and the entry ask for the same package on every
|
|
294
|
+
// rebuild.
|
|
295
|
+
type installedPackageLocation struct {
|
|
296
|
+
Directory string
|
|
297
|
+
Manifest map[string]json.RawMessage
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func (loader *typeScriptLoader) locateInstalledPackage(
|
|
301
|
+
name string,
|
|
302
|
+
) (string, map[string]json.RawMessage) {
|
|
303
|
+
prefix := ""
|
|
304
|
+
for range 32 {
|
|
305
|
+
directory := path.Join(prefix, "node_modules", name)
|
|
306
|
+
manifest := readPackageManifest(
|
|
307
|
+
loader.root,
|
|
308
|
+
path.Join(directory, "package.json"),
|
|
309
|
+
)
|
|
310
|
+
if manifest != nil {
|
|
311
|
+
return directory, manifest
|
|
312
|
+
}
|
|
313
|
+
prefix = path.Join(prefix, "..")
|
|
314
|
+
}
|
|
315
|
+
return path.Join("node_modules", name), nil
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// resolveLinkedDirectory returns the directory a path ultimately names.
|
|
319
|
+
//
|
|
320
|
+
// `filepath.EvalSymlinks` answers this for a POSIX symlink but not for a
|
|
321
|
+
// Windows junction, which is exactly what pnpm creates for a workspace
|
|
322
|
+
// dependency there: it returns the junction unchanged, so a caller that trusts
|
|
323
|
+
// it walks the link and finds nothing. `os.Readlink` does report a junction's
|
|
324
|
+
// target, and `os.Stat` sees through both, so ask those instead.
|
|
325
|
+
func resolveLinkedDirectory(directory string) string {
|
|
326
|
+
current := directory
|
|
327
|
+
for range 32 {
|
|
328
|
+
info, err := os.Lstat(current)
|
|
329
|
+
if err != nil || info.IsDir() {
|
|
330
|
+
return current
|
|
331
|
+
}
|
|
332
|
+
if target, err := os.Stat(current); err != nil || !target.IsDir() {
|
|
333
|
+
return current
|
|
334
|
+
}
|
|
335
|
+
linked, err := os.Readlink(current)
|
|
336
|
+
if err != nil {
|
|
337
|
+
return current
|
|
338
|
+
}
|
|
339
|
+
if !filepath.IsAbs(linked) {
|
|
340
|
+
linked = filepath.Join(filepath.Dir(current), linked)
|
|
341
|
+
}
|
|
342
|
+
current = filepath.ToSlash(linked)
|
|
343
|
+
}
|
|
344
|
+
return current
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// walk lists the project-relative TypeScript files below a directory.
|
|
348
|
+
//
|
|
349
|
+
// A package's files are enumerated from disk for the same reason its entry is
|
|
350
|
+
// parsed from disk: the ones an obligation most needs to name are precisely the
|
|
351
|
+
// ones nothing imported, so the Program cannot be the source of truth.
|
|
352
|
+
func (loader *typeScriptLoader) walk(base string) ([]string, string) {
|
|
353
|
+
root := path.Join(loader.root, base)
|
|
354
|
+
// A workspace dependency is a link, not a directory: pnpm installs one that
|
|
355
|
+
// way on every platform, and npm and Yarn do the same for a linked package.
|
|
356
|
+
// `filepath.WalkDir` reports a link as a plain entry and descends into
|
|
357
|
+
// nothing, so walking the spelled path finds no files and the reference
|
|
358
|
+
// looks empty rather than unresolvable. Walk what the link points at, and
|
|
359
|
+
// report the files under the spelled path so addresses stay stable.
|
|
360
|
+
walked := resolveLinkedDirectory(root)
|
|
361
|
+
found := []string{}
|
|
362
|
+
problem := ""
|
|
363
|
+
err := filepath.WalkDir(walked, func(current string, entry fs.DirEntry, err error) error {
|
|
364
|
+
if walked != root {
|
|
365
|
+
if inside, ok := containedProjectPath(walked, filepath.ToSlash(current)); ok {
|
|
366
|
+
current = path.Join(root, inside)
|
|
367
|
+
} else if filepath.ToSlash(current) == walked {
|
|
368
|
+
current = root
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if err != nil {
|
|
372
|
+
if problem == "" {
|
|
373
|
+
problem = err.Error()
|
|
374
|
+
}
|
|
375
|
+
if entry != nil && entry.IsDir() {
|
|
376
|
+
return filepath.SkipDir
|
|
377
|
+
}
|
|
378
|
+
return nil
|
|
379
|
+
}
|
|
380
|
+
if entry.IsDir() {
|
|
381
|
+
if entry.Name() == "node_modules" && filepath.ToSlash(current) != root {
|
|
382
|
+
return filepath.SkipDir
|
|
383
|
+
}
|
|
384
|
+
return nil
|
|
385
|
+
}
|
|
386
|
+
relative, ok := relativeProjectPath(loader.root, filepath.ToSlash(current))
|
|
387
|
+
if !ok {
|
|
388
|
+
// An install can sit above the project: a nested Program resolves its
|
|
389
|
+
// dependencies out of an ancestor's `node_modules`. Name such a file
|
|
390
|
+
// through the project the same way a rooted population above the
|
|
391
|
+
// project is named, rather than dropping it and reporting the package
|
|
392
|
+
// as empty.
|
|
393
|
+
relative = loader.projectPath(filepath.ToSlash(current))
|
|
394
|
+
}
|
|
395
|
+
if relative == "" || !isTypeScriptPath(relative) {
|
|
396
|
+
return nil
|
|
397
|
+
}
|
|
398
|
+
found = append(found, relative)
|
|
399
|
+
return nil
|
|
400
|
+
})
|
|
401
|
+
if err != nil && problem == "" {
|
|
402
|
+
problem = err.Error()
|
|
403
|
+
}
|
|
404
|
+
sort.Strings(found)
|
|
405
|
+
return found, problem
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// referenceBase gives the directory a package reference enumerates.
|
|
409
|
+
//
|
|
410
|
+
// It asks the loader rather than assuming `node_modules` sits beside the
|
|
411
|
+
// project root, so the walk and the entry agree on where the package is. A
|
|
412
|
+
// nested Program is installed into an ancestor's `node_modules`, and a walk
|
|
413
|
+
// that looked only beside its own root would enumerate nothing while the entry
|
|
414
|
+
// resolved fine.
|
|
415
|
+
func referenceBase(loader *typeScriptLoader, reference referenceSpec) string {
|
|
416
|
+
if reference.Package == "" {
|
|
417
|
+
return ""
|
|
418
|
+
}
|
|
419
|
+
if loader == nil {
|
|
420
|
+
return path.Join("node_modules", reference.Package)
|
|
421
|
+
}
|
|
422
|
+
directory, _ := loader.installedPackage(reference.Package)
|
|
423
|
+
return directory
|
|
424
|
+
}
|