@depact/cli 0.0.1
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/AGENTS.md +16 -0
- package/README.md +3 -0
- package/fixtures/barrel/src/entry.ts +2 -0
- package/fixtures/barrel/src/ui/Button.ts +3 -0
- package/fixtures/barrel/src/ui/Card.ts +2 -0
- package/fixtures/barrel/src/ui/Input.ts +2 -0
- package/fixtures/barrel/src/ui/Modal.ts +2 -0
- package/fixtures/barrel/src/ui/Tooltip.ts +2 -0
- package/fixtures/barrel/src/ui/helpers/a.ts +1 -0
- package/fixtures/barrel/src/ui/helpers/b.ts +1 -0
- package/fixtures/barrel/src/ui/helpers/c.ts +1 -0
- package/fixtures/barrel/src/ui/helpers/d.ts +1 -0
- package/fixtures/barrel/src/ui/helpers/e.ts +1 -0
- package/fixtures/barrel/src/ui/helpers/f.ts +1 -0
- package/fixtures/barrel/src/ui/index.ts +5 -0
- package/fixtures/barrel/tsconfig.json +11 -0
- package/fixtures/externals/node_modules/acme/helper.js +1 -0
- package/fixtures/externals/node_modules/acme/index.d.ts +1 -0
- package/fixtures/externals/node_modules/acme/index.js +2 -0
- package/fixtures/externals/node_modules/acme/package.json +6 -0
- package/fixtures/externals/node_modules/leftpad/index.d.ts +1 -0
- package/fixtures/externals/node_modules/leftpad/index.js +1 -0
- package/fixtures/externals/node_modules/leftpad/package.json +6 -0
- package/fixtures/externals/src/entry.ts +5 -0
- package/fixtures/externals/src/local.ts +2 -0
- package/fixtures/externals/tsconfig.json +12 -0
- package/fixtures/linear/src/b.ts +2 -0
- package/fixtures/linear/src/c.ts +2 -0
- package/fixtures/linear/src/d.ts +1 -0
- package/fixtures/linear/src/entry.ts +2 -0
- package/fixtures/linear/tsconfig.json +11 -0
- package/fixtures/paths/src/app/page.tsx +7 -0
- package/fixtures/paths/src/lib/hook.ts +1 -0
- package/fixtures/paths/src/lib/icon.tsx +3 -0
- package/fixtures/paths/tsconfig.json +16 -0
- package/go.mod +3 -0
- package/main.go +9 -0
- package/package.json +5 -0
- package/parser/module.go +45 -0
- package/parser/parser.go +7 -0
- package/parser/scanner.go +215 -0
- package/parser/scanner_test.go +62 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# depact
|
|
2
|
+
|
|
3
|
+
Dependency impact analyzer for large TypeScript codebases
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`depact` builds the exact transitive import graph from an entry file and surfaces actionable metrics: exclusive cost per contributor, barrel amplification, shortest import chain to a target (`why`), affected test selection from changed files.
|
|
8
|
+
|
|
9
|
+
## Key architecture decisions
|
|
10
|
+
|
|
11
|
+
Custom import scanner and resolver written in Go for speed and parallelism.
|
|
12
|
+
|
|
13
|
+
## Commands (target CLI)
|
|
14
|
+
|
|
15
|
+
`analyze`, `check`, `why`, `diff`.
|
|
16
|
+
All commands support `--json`, `--type`, `--follow-externals`, `--project`.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperA = () => 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperB = () => 2;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperC = () => 3;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperD = () => 4;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperE = () => 5;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helperF = () => 6;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helper = () => 2;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const acme: () => number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const leftpad: (s: string) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const leftpad = (s) => " " + s;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const d = () => 42;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const useThing = (): string => "thing";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"paths": {
|
|
12
|
+
"@lib/*": ["src/lib/*"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|
package/go.mod
ADDED
package/main.go
ADDED
package/package.json
ADDED
package/parser/module.go
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package parser
|
|
2
|
+
|
|
3
|
+
type Symbol struct {
|
|
4
|
+
Name string
|
|
5
|
+
TypeOnly bool
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type EdgeType int
|
|
9
|
+
|
|
10
|
+
const (
|
|
11
|
+
DefaultEdge EdgeType = iota
|
|
12
|
+
NamedEdge
|
|
13
|
+
NamespaceEdge
|
|
14
|
+
SideEffectEdge
|
|
15
|
+
DynamicEdge
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type Import struct {
|
|
19
|
+
Kind EdgeType
|
|
20
|
+
From string
|
|
21
|
+
Symbols []Symbol
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Export struct {
|
|
25
|
+
Kind EdgeType
|
|
26
|
+
Symbols []Symbol
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Module struct {
|
|
30
|
+
Path string
|
|
31
|
+
Imports []Import
|
|
32
|
+
Exports []Export
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func (i Import) TypeOnly() bool {
|
|
36
|
+
if len(i.Symbols) == 0 {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
for _, s := range i.Symbols {
|
|
40
|
+
if !s.TypeOnly {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
package/parser/parser.go
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
package parser
|
|
2
|
+
|
|
3
|
+
type state int
|
|
4
|
+
|
|
5
|
+
const (
|
|
6
|
+
code state = iota
|
|
7
|
+
lineComment // //
|
|
8
|
+
blockComment // /* */
|
|
9
|
+
stringDouble // "..."
|
|
10
|
+
stringSingle // '...'
|
|
11
|
+
template // `...`
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type scanner struct {
|
|
15
|
+
src []byte
|
|
16
|
+
state state
|
|
17
|
+
i int
|
|
18
|
+
imports []Import
|
|
19
|
+
exports []Export
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func (s *scanner) scan() ([]Import, []Export) {
|
|
23
|
+
for s.i < len(s.src) {
|
|
24
|
+
char := s.src[s.i]
|
|
25
|
+
|
|
26
|
+
switch s.state {
|
|
27
|
+
case code:
|
|
28
|
+
switch {
|
|
29
|
+
case char == '/' && s.i+1 < len(s.src) && s.src[s.i+1] == '/':
|
|
30
|
+
s.state = lineComment
|
|
31
|
+
s.i++
|
|
32
|
+
|
|
33
|
+
case char == '/' && s.i+1 < len(s.src) && s.src[s.i+1] == '*':
|
|
34
|
+
s.state = blockComment
|
|
35
|
+
s.i++
|
|
36
|
+
|
|
37
|
+
case char == '"':
|
|
38
|
+
s.state = stringDouble
|
|
39
|
+
|
|
40
|
+
case char == '\'':
|
|
41
|
+
s.state = stringSingle
|
|
42
|
+
|
|
43
|
+
case char == '`':
|
|
44
|
+
s.state = template
|
|
45
|
+
|
|
46
|
+
default: // TODO: case == 'i' || 'e'
|
|
47
|
+
s.parseCode()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case lineComment:
|
|
51
|
+
if char == '\n' {
|
|
52
|
+
s.state = code
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case blockComment:
|
|
56
|
+
if char == '*' && s.i+1 < len(s.src) && s.src[s.i+1] == '/' {
|
|
57
|
+
s.state = code
|
|
58
|
+
s.i++
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case stringDouble: // TODO: handle \\"
|
|
62
|
+
if char == '"' && s.i > 0 && s.src[s.i-1] != '\\' {
|
|
63
|
+
s.state = code
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case stringSingle: // TODO: handle \\'
|
|
67
|
+
if char == '\'' && s.i > 0 && s.src[s.i-1] != '\\' {
|
|
68
|
+
s.state = code
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case template: // TODO: handle `${}`
|
|
72
|
+
if char == '`' {
|
|
73
|
+
s.state = code
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
s.i++
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return s.imports, s.exports
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// TODO: add a s.peek() method
|
|
84
|
+
|
|
85
|
+
func (s *scanner) parseCode() {
|
|
86
|
+
if !s.isWord([]byte("import")) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
imp := Import{}
|
|
91
|
+
exp := Export{}
|
|
92
|
+
|
|
93
|
+
s.skipSpace()
|
|
94
|
+
|
|
95
|
+
onlyTypes := false
|
|
96
|
+
if s.src[s.i] == 't' && s.nextWord() == "type" {
|
|
97
|
+
onlyTypes = true
|
|
98
|
+
s.skipSpace()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
switch s.src[s.i] {
|
|
102
|
+
case '"', '\'':
|
|
103
|
+
imp.Kind = SideEffectEdge
|
|
104
|
+
imp.From = s.readString()
|
|
105
|
+
|
|
106
|
+
case '{':
|
|
107
|
+
imp.Kind = NamedEdge
|
|
108
|
+
s.i++
|
|
109
|
+
for {
|
|
110
|
+
s.skipSpace()
|
|
111
|
+
if s.src[s.i] == '}' {
|
|
112
|
+
s.i++
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
if s.src[s.i] == ',' {
|
|
116
|
+
s.i++
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
imp.Symbols = append(imp.Symbols, s.symbolFromNextWord(onlyTypes))
|
|
120
|
+
}
|
|
121
|
+
s.skipSpace()
|
|
122
|
+
s.nextWord() // from
|
|
123
|
+
s.skipSpace()
|
|
124
|
+
imp.From = s.readString()
|
|
125
|
+
|
|
126
|
+
case '(':
|
|
127
|
+
imp.Kind = DynamicEdge
|
|
128
|
+
s.i++
|
|
129
|
+
imp.From = s.readString()
|
|
130
|
+
s.i++
|
|
131
|
+
|
|
132
|
+
case '*':
|
|
133
|
+
imp.Kind = NamespaceEdge
|
|
134
|
+
s.i++
|
|
135
|
+
s.skipSpace()
|
|
136
|
+
s.nextWord() // as
|
|
137
|
+
s.skipSpace()
|
|
138
|
+
imp.Symbols = append(imp.Symbols, s.symbolFromNextWord(onlyTypes))
|
|
139
|
+
s.skipSpace()
|
|
140
|
+
s.nextWord() // from
|
|
141
|
+
s.skipSpace()
|
|
142
|
+
imp.From = s.readString()
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
imp.Kind = DefaultEdge
|
|
146
|
+
imp.Symbols = append(imp.Symbols, s.symbolFromNextWord(onlyTypes))
|
|
147
|
+
s.skipSpace()
|
|
148
|
+
s.nextWord() // from
|
|
149
|
+
s.skipSpace()
|
|
150
|
+
imp.From = s.readString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
s.imports = append(s.imports, imp)
|
|
154
|
+
s.exports = append(s.exports, exp)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func (s *scanner) isWord(word []byte) bool {
|
|
158
|
+
if s.i+len(word) > len(s.src) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
for i := range len(word) {
|
|
162
|
+
c := word[i]
|
|
163
|
+
if c != s.src[s.i+i] {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
s.i += len(word)
|
|
168
|
+
return true // TODO: check byte before and after are not letters
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func (s *scanner) nextWord() string {
|
|
172
|
+
pos := s.i
|
|
173
|
+
for isLetter(s.src[s.i]) {
|
|
174
|
+
s.i++
|
|
175
|
+
}
|
|
176
|
+
return string(s.src[pos:s.i])
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func (s *scanner) skipSpace() {
|
|
180
|
+
for s.i < len(s.src) {
|
|
181
|
+
char := s.src[s.i]
|
|
182
|
+
if char == ' ' || char == '\t' || char == '\n' || char == '\r' {
|
|
183
|
+
s.i++
|
|
184
|
+
} else {
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func (s *scanner) readString() string {
|
|
191
|
+
quote := s.src[s.i]
|
|
192
|
+
s.i++
|
|
193
|
+
start := s.i
|
|
194
|
+
for s.src[s.i] != quote {
|
|
195
|
+
s.i++
|
|
196
|
+
}
|
|
197
|
+
s.i++
|
|
198
|
+
return string(s.src[start : s.i-1])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func isLetter(char byte) bool {
|
|
202
|
+
return 'a' <= char && char <= 'z' || 'A' <= char && char <= 'Z' || char == '_'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (s *scanner) symbolFromNextWord(onlyTypes bool) Symbol {
|
|
206
|
+
symbol := Symbol{Name: s.nextWord()}
|
|
207
|
+
if symbol.Name == "type" {
|
|
208
|
+
symbol.TypeOnly = true
|
|
209
|
+
s.skipSpace()
|
|
210
|
+
symbol.Name = s.nextWord()
|
|
211
|
+
} else if onlyTypes {
|
|
212
|
+
symbol.TypeOnly = true
|
|
213
|
+
}
|
|
214
|
+
return symbol
|
|
215
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package parser
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"reflect"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestScannerEveryImport(t *testing.T) {
|
|
9
|
+
tests := []struct {
|
|
10
|
+
name string
|
|
11
|
+
input string
|
|
12
|
+
expected Import
|
|
13
|
+
}{
|
|
14
|
+
{"side-effect single quote", "import 'mod'", Import{Kind: SideEffectEdge, From: "mod"}},
|
|
15
|
+
{"side-effect double quote", "import \"mod\"", Import{Kind: SideEffectEdge, From: "mod"}},
|
|
16
|
+
{"default import", "import foo from 'mod'", Import{Kind: DefaultEdge, From: "mod", Symbols: []Symbol{{Name: "foo"}}}},
|
|
17
|
+
{"default type import", "import type foo from 'mod'", Import{Kind: DefaultEdge, From: "mod", Symbols: []Symbol{{Name: "foo", TypeOnly: true}}}},
|
|
18
|
+
{"named imports", "import { foo, bar } from 'mod'", Import{Kind: NamedEdge, From: "mod", Symbols: []Symbol{{Name: "foo"}, {Name: "bar"}}}},
|
|
19
|
+
// {"renamed imports", "import { foo as bar } from 'mod'", Import{Kind: NamedEdge, From: "mod", Symbols: []Symbol{{Name: "foo"}}}},
|
|
20
|
+
{"named type imports", "import { type foo, bar } from 'mod'", Import{Kind: NamedEdge, From: "mod", Symbols: []Symbol{{Name: "foo", TypeOnly: true}, {Name: "bar"}}}},
|
|
21
|
+
{"named only type imports", "import type { foo, bar } from 'mod'", Import{Kind: NamedEdge, From: "mod", Symbols: []Symbol{{Name: "foo", TypeOnly: true}, {Name: "bar", TypeOnly: true}}}},
|
|
22
|
+
{"namepace import", "import * as foo from 'mod'", Import{Kind: NamespaceEdge, From: "mod", Symbols: []Symbol{{Name: "foo"}}}},
|
|
23
|
+
// {"", "import type foo, { bar } from 'mod'", Import{}},
|
|
24
|
+
{"namepace type import", "import type * as foo from 'mod'", Import{Kind: NamespaceEdge, From: "mod", Symbols: []Symbol{{Name: "foo", TypeOnly: true}}}},
|
|
25
|
+
{"dynamic import", "import('mod')", Import{Kind: DynamicEdge, From: "mod"}},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// TODO: Tightly packed forms
|
|
29
|
+
|
|
30
|
+
for _, tt := range tests {
|
|
31
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
32
|
+
scanner := &scanner{src: []byte(tt.input)}
|
|
33
|
+
imports, _ := scanner.scan()
|
|
34
|
+
|
|
35
|
+
if len(imports) != 1 {
|
|
36
|
+
t.Fatalf("expected 1 edge, got %d", len(imports))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if !reflect.DeepEqual(imports[0], tt.expected) {
|
|
40
|
+
t.Errorf("expected %+v, got %+v", tt.expected, imports[0])
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// TODO: add tests for non imports
|
|
47
|
+
|
|
48
|
+
// import x from "mod"
|
|
49
|
+
// import { a, b } from "mod"
|
|
50
|
+
// import * as ns from "mod"
|
|
51
|
+
// import "mod"
|
|
52
|
+
// import type { T } from "mod"
|
|
53
|
+
// const x = require("mod")
|
|
54
|
+
// const x = await import("mod")
|
|
55
|
+
|
|
56
|
+
// export const x = ...
|
|
57
|
+
// export function f() {}
|
|
58
|
+
// export default ...
|
|
59
|
+
// export { a, b as c }
|
|
60
|
+
// export { x } from "mod"
|
|
61
|
+
// export * from "mod"
|
|
62
|
+
// export * as ns from "mod"
|