@depact/cli 0.0.1 → 0.0.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.
Files changed (43) hide show
  1. package/bin/depact.js +32 -0
  2. package/package.json +31 -2
  3. package/AGENTS.md +0 -16
  4. package/README.md +0 -3
  5. package/fixtures/barrel/src/entry.ts +0 -2
  6. package/fixtures/barrel/src/ui/Button.ts +0 -3
  7. package/fixtures/barrel/src/ui/Card.ts +0 -2
  8. package/fixtures/barrel/src/ui/Input.ts +0 -2
  9. package/fixtures/barrel/src/ui/Modal.ts +0 -2
  10. package/fixtures/barrel/src/ui/Tooltip.ts +0 -2
  11. package/fixtures/barrel/src/ui/helpers/a.ts +0 -1
  12. package/fixtures/barrel/src/ui/helpers/b.ts +0 -1
  13. package/fixtures/barrel/src/ui/helpers/c.ts +0 -1
  14. package/fixtures/barrel/src/ui/helpers/d.ts +0 -1
  15. package/fixtures/barrel/src/ui/helpers/e.ts +0 -1
  16. package/fixtures/barrel/src/ui/helpers/f.ts +0 -1
  17. package/fixtures/barrel/src/ui/index.ts +0 -5
  18. package/fixtures/barrel/tsconfig.json +0 -11
  19. package/fixtures/externals/node_modules/acme/helper.js +0 -1
  20. package/fixtures/externals/node_modules/acme/index.d.ts +0 -1
  21. package/fixtures/externals/node_modules/acme/index.js +0 -2
  22. package/fixtures/externals/node_modules/acme/package.json +0 -6
  23. package/fixtures/externals/node_modules/leftpad/index.d.ts +0 -1
  24. package/fixtures/externals/node_modules/leftpad/index.js +0 -1
  25. package/fixtures/externals/node_modules/leftpad/package.json +0 -6
  26. package/fixtures/externals/src/entry.ts +0 -5
  27. package/fixtures/externals/src/local.ts +0 -2
  28. package/fixtures/externals/tsconfig.json +0 -12
  29. package/fixtures/linear/src/b.ts +0 -2
  30. package/fixtures/linear/src/c.ts +0 -2
  31. package/fixtures/linear/src/d.ts +0 -1
  32. package/fixtures/linear/src/entry.ts +0 -2
  33. package/fixtures/linear/tsconfig.json +0 -11
  34. package/fixtures/paths/src/app/page.tsx +0 -7
  35. package/fixtures/paths/src/lib/hook.ts +0 -1
  36. package/fixtures/paths/src/lib/icon.tsx +0 -3
  37. package/fixtures/paths/tsconfig.json +0 -16
  38. package/go.mod +0 -3
  39. package/main.go +0 -9
  40. package/parser/module.go +0 -45
  41. package/parser/parser.go +0 -7
  42. package/parser/scanner.go +0 -215
  43. package/parser/scanner_test.go +0 -62
package/bin/depact.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawnSync } = require("child_process");
5
+
6
+ // npm publishes win32 packages as "windows" to avoid spam-detection false
7
+ // positives; process.platform still reports "win32" at runtime, so map it.
8
+ const OS = { win32: "windows" }[process.platform] ?? process.platform;
9
+ const EXT = process.platform === "win32" ? ".exe" : "";
10
+ const PKG = `@depact/cli-${OS}-${process.arch}`;
11
+
12
+ function binaryPath() {
13
+ try {
14
+ return require.resolve(`${PKG}/bin/depact${EXT}`);
15
+ } catch {
16
+ throw new Error(
17
+ `depact: no prebuilt binary for ${process.platform}-${process.arch}.\n` +
18
+ `The optional dependency ${PKG} is not installed. This usually means ` +
19
+ `your platform is unsupported, or optional dependencies were skipped ` +
20
+ `(e.g. --no-optional / --ignore-optional).`
21
+ );
22
+ }
23
+ }
24
+
25
+ const result = spawnSync(binaryPath(), process.argv.slice(2), {
26
+ stdio: "inherit",
27
+ });
28
+
29
+ if (result.error) {
30
+ throw result.error;
31
+ }
32
+ process.exit(result.status ?? 1);
package/package.json CHANGED
@@ -1,5 +1,34 @@
1
1
  {
2
2
  "name": "@depact/cli",
3
- "version": "0.0.1",
4
- "scripts": {}
3
+ "version": "0.0.3",
4
+ "description": "Dependency impact analyzer for large TypeScript codebases",
5
+ "keywords": [
6
+ "typescript",
7
+ "analyzer",
8
+ "import",
9
+ "devtool",
10
+ "dependency"
11
+ ],
12
+ "homepage": "https://github.com/colinlienard/depact",
13
+ "bugs": {
14
+ "url": "https://github.com/colinlienard/depact/issues"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/colinlienard/depact.git"
20
+ },
21
+ "bin": {
22
+ "depact": "bin/depact.js"
23
+ },
24
+ "files": [
25
+ "bin/depact.js"
26
+ ],
27
+ "optionalDependencies": {
28
+ "@depact/cli-darwin-arm64": "0.0.3",
29
+ "@depact/cli-darwin-x64": "0.0.3",
30
+ "@depact/cli-linux-x64": "0.0.3",
31
+ "@depact/cli-linux-arm64": "0.0.3",
32
+ "@depact/cli-windows-x64": "0.0.3"
33
+ }
5
34
  }
package/AGENTS.md DELETED
@@ -1,16 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- # depact
2
-
3
- Dependency impact analyzer for large TypeScript codebases
@@ -1,2 +0,0 @@
1
- import { Button } from "./ui/index.ts";
2
- export const page = () => Button();
@@ -1,3 +0,0 @@
1
- import { helperA } from "./helpers/a.ts";
2
- import { helperB } from "./helpers/b.ts";
3
- export const Button = () => helperA() + helperB();
@@ -1,2 +0,0 @@
1
- import { helperF } from "./helpers/f.ts";
2
- export const Card = () => helperF();
@@ -1,2 +0,0 @@
1
- import { helperC } from "./helpers/c.ts";
2
- export const Input = () => helperC();
@@ -1,2 +0,0 @@
1
- import { helperD } from "./helpers/d.ts";
2
- export const Modal = () => helperD();
@@ -1,2 +0,0 @@
1
- import { helperE } from "./helpers/e.ts";
2
- export const Tooltip = () => helperE();
@@ -1 +0,0 @@
1
- export const helperA = () => 1;
@@ -1 +0,0 @@
1
- export const helperB = () => 2;
@@ -1 +0,0 @@
1
- export const helperC = () => 3;
@@ -1 +0,0 @@
1
- export const helperD = () => 4;
@@ -1 +0,0 @@
1
- export const helperE = () => 5;
@@ -1 +0,0 @@
1
- export const helperF = () => 6;
@@ -1,5 +0,0 @@
1
- export * from "./Button.ts";
2
- export * from "./Input.ts";
3
- export * from "./Modal.ts";
4
- export * from "./Tooltip.ts";
5
- export * from "./Card.ts";
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "noEmit": true
9
- },
10
- "include": ["src/**/*"]
11
- }
@@ -1 +0,0 @@
1
- export const helper = () => 2;
@@ -1 +0,0 @@
1
- export const acme: () => number;
@@ -1,2 +0,0 @@
1
- import { helper } from "./helper.js";
2
- export const acme = () => helper() + 1;
@@ -1,6 +0,0 @@
1
- {
2
- "name": "acme",
3
- "version": "1.0.0",
4
- "main": "./index.js",
5
- "types": "./index.d.ts"
6
- }
@@ -1 +0,0 @@
1
- export const leftpad: (s: string) => string;
@@ -1 +0,0 @@
1
- export const leftpad = (s) => " " + s;
@@ -1,6 +0,0 @@
1
- {
2
- "name": "leftpad",
3
- "version": "1.0.0",
4
- "main": "./index.js",
5
- "types": "./index.d.ts"
6
- }
@@ -1,5 +0,0 @@
1
- import { acme } from "acme";
2
- import { leftpad } from "leftpad";
3
- import { local } from "./local.ts";
4
-
5
- export const x = () => acme() + leftpad("1") + local();
@@ -1,2 +0,0 @@
1
- import { leftpad } from "leftpad";
2
- export const local = () => leftpad("ok");
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Node10",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "allowJs": true,
9
- "noEmit": true
10
- },
11
- "include": ["src/**/*"]
12
- }
@@ -1,2 +0,0 @@
1
- import { c } from "./c.ts";
2
- export const b = () => c() + 1;
@@ -1,2 +0,0 @@
1
- import { d } from "./d.ts";
2
- export const c = () => d() + 1;
@@ -1 +0,0 @@
1
- export const d = () => 42;
@@ -1,2 +0,0 @@
1
- import { b } from "./b.ts";
2
- export const entry = () => b() + 1;
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "noEmit": true
9
- },
10
- "include": ["src/**/*"]
11
- }
@@ -1,7 +0,0 @@
1
- import { useThing } from "@lib/hook";
2
- import { Icon } from "@lib/icon";
3
-
4
- export function Page() {
5
- const thing = useThing();
6
- return <Icon label={thing} />;
7
- }
@@ -1 +0,0 @@
1
- export const useThing = (): string => "thing";
@@ -1,3 +0,0 @@
1
- export function Icon({ label }: { label: string }) {
2
- return <span>{label}</span>;
3
- }
@@ -1,16 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- module depact
2
-
3
- go 1.25.0
package/main.go DELETED
@@ -1,9 +0,0 @@
1
- package main
2
-
3
- import (
4
- "depact/parser"
5
- )
6
-
7
- func main() {
8
- _ = parser.Parse([]byte("import x from 'mod'"))
9
- }
package/parser/module.go DELETED
@@ -1,45 +0,0 @@
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 DELETED
@@ -1,7 +0,0 @@
1
- package parser
2
-
3
- func Parse(src []byte) *Module {
4
- scanner := &scanner{src: src}
5
- imports, exports := scanner.scan() // TODO: handle errors
6
- return &Module{Imports: imports, Exports: exports}
7
- }
package/parser/scanner.go DELETED
@@ -1,215 +0,0 @@
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
- }
@@ -1,62 +0,0 @@
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"