···
11
11
12
12
"tangled.org/jakelazaroff.com/unpm/internal/cli"
13
13
"tangled.org/jakelazaroff.com/unpm/internal/inspect"
14
14
-
"tangled.org/jakelazaroff.com/unpm/internal/unpm"
14
14
+
"tangled.org/jakelazaroff.com/unpm/internal/vendor"
15
15
)
16
16
17
17
func main() {
18
18
app := cli.App{
19
19
-
Vendor: unpm.Vendor,
19
19
+
Vendor: vendor.Vendor,
20
20
Check: inspect.Check,
21
21
Why: inspect.Why,
22
22
}
···
39
39
return nil, fmt.Errorf("parsing config: %w", err)
40
40
}
41
41
42
42
-
if len(cfg.Imports) == 0 {
43
43
-
return nil, fmt.Errorf("no imports found in %s", configPath)
44
44
-
}
45
45
-
46
42
for key, url := range cfg.Imports {
47
43
if strings.HasSuffix(key, "/") {
48
44
return nil, fmt.Errorf("trailing-slash prefix entries are not supported: %q (list each subpath explicitly)", key)
···
30
30
}
31
31
})
32
32
33
33
-
t.Run("no imports", func(t *testing.T) {
34
34
-
dir := t.TempDir()
35
35
-
p := filepath.Join(dir, "unpm.json")
36
36
-
os.WriteFile(p, []byte(`{}`), 0o644)
37
37
-
38
38
-
_, err := cfg.ReadConfig(p)
39
39
-
if err == nil || !strings.Contains(err.Error(), "no imports") {
40
40
-
t.Fatalf("expected 'no imports' error, got: %v", err)
41
41
-
}
42
42
-
})
43
43
-
44
33
t.Run("trailing slash", func(t *testing.T) {
45
34
dir := t.TempDir()
46
35
p := filepath.Join(dir, "unpm.json")
···
4
4
//
5
5
// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/unpm
6
6
7
7
-
// Package imports scans JavaScript and TypeScript source for the references
8
8
-
// unpm cares about — import/export specifiers, type-only imports, and source
9
9
-
// map comments — and rewrites them. It is the single home for "what counts as
10
10
-
// an import," shared by the vendoring engine and the check/why inspector.
7
7
+
// Package imports scans for and rewrites imports in JavaScript and TypeScript
11
8
package imports
12
9
13
10
import (
···
17
14
"strings"
18
15
)
19
16
20
20
-
// staticImportRe matches a static import/export statement whose specifier is a
21
21
-
// string literal: prefix(1) quote(2) spec(3) quote(4).
17
17
+
// staticImportRe matches a static import/export statement whose specifier is a string literal
18
18
+
// prefix(1) quote(2) spec(3) quote(4)
22
19
var staticImportRe = regexp.MustCompile(`(\b(?:import|export)\s*(?:[^"']*\bfrom\s*|))(["'])([^"']+)(["'])`)
23
20
24
24
-
// dynamicImportRe matches a dynamic import() expression whose specifier is a
25
25
-
// string literal: prefix(1) quote(2) spec(3) quote(4).
21
21
+
// dynamicImportRe matches a dynamic import() expression whose specifier is a string literal
22
22
+
// prefix(1) quote(2) spec(3) quote(4).
26
23
var dynamicImportRe = regexp.MustCompile(`(\bimport\s*\(\s*)(["'])([^"']+)(["'][^)]*\))`)
27
24
28
28
-
// sourceMappingRe matches //# sourceMappingURL=... comments: prefix(1) url(2).
25
25
+
// sourceMappingRe matches //# sourceMappingURL=... comments:
26
26
+
// prefix(1) url(2)
29
27
var sourceMappingRe = regexp.MustCompile(`(//[#@]\s*sourceMappingURL\s*=\s*)(\S+)`)
30
28
31
29
// Scan returns every import/export/dynamic-import specifier in src, in source
32
30
// order, including duplicates and bare specifiers.
33
31
func Scan(src string) []string {
34
32
type entry struct {
35
35
-
pos int
36
36
-
spec string
33
33
+
pos int
34
34
+
specifier string
37
35
}
36
36
+
38
37
var entries []entry
39
38
for _, loc := range staticImportRe.FindAllStringSubmatchIndex(src, -1) {
40
39
entries = append(entries, entry{loc[0], src[loc[6]:loc[7]]})
41
40
}
41
41
+
42
42
for _, loc := range dynamicImportRe.FindAllStringSubmatchIndex(src, -1) {
43
43
entries = append(entries, entry{loc[0], src[loc[6]:loc[7]]})
44
44
}
45
45
+
45
46
sort.Slice(entries, func(i, j int) bool { return entries[i].pos < entries[j].pos })
46
47
specs := make([]string, len(entries))
47
48
for i, e := range entries {
48
48
-
specs[i] = e.spec
49
49
+
specs[i] = e.specifier
49
50
}
51
51
+
50
52
return specs
51
53
}
52
54
···
4
4
//
5
5
// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/unpm
6
6
7
7
-
package unpm
7
7
+
package vendor
8
8
9
9
import (
10
10
"encoding/json"
···
4
4
//
5
5
// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/unpm
6
6
7
7
-
package unpm_test
7
7
+
package vendor_test
8
8
9
9
import (
10
10
"encoding/json"
···
16
16
"testing"
17
17
18
18
"tangled.org/jakelazaroff.com/unpm/internal/cfg"
19
19
-
"tangled.org/jakelazaroff.com/unpm/internal/unpm"
19
19
+
"tangled.org/jakelazaroff.com/unpm/internal/vendor"
20
20
)
21
21
22
22
// testFile defines a file served by the test server.
···
52
52
Imports: map[string]string{"mylib": srv.URL + "/mylib.js"},
53
53
Unpm: cfg.Options{Out: outDir, Root: "/"},
54
54
}
55
55
-
if _, err := unpm.Vendor(c); err != nil {
55
55
+
if _, err := vendor.Vendor(c); err != nil {
56
56
t.Fatal(err)
57
57
}
58
58
···
105
105
Imports: map[string]string{"a": srv.URL + "/a.js"},
106
106
Unpm: cfg.Options{Out: outDir, Root: "/"},
107
107
}
108
108
-
if _, err := unpm.Vendor(c); err != nil {
108
108
+
if _, err := vendor.Vendor(c); err != nil {
109
109
t.Fatal(err)
110
110
}
111
111
···
138
138
Imports: map[string]string{"entry": srv.URL + "/entry.js"},
139
139
Unpm: cfg.Options{Out: outDir, Root: "/"},
140
140
}
141
141
-
if _, err := unpm.Vendor(c); err != nil {
141
141
+
if _, err := vendor.Vendor(c); err != nil {
142
142
t.Fatal(err)
143
143
}
144
144
···
184
184
Imports: map[string]string{"lib": srv.URL + "/lib.d.ts"},
185
185
Unpm: cfg.Options{Out: outDir, Root: "/"},
186
186
}
187
187
-
if _, err := unpm.Vendor(c); err != nil {
187
187
+
if _, err := vendor.Vendor(c); err != nil {
188
188
t.Fatal(err)
189
189
}
190
190
···
222
222
Imports: map[string]string{"preact": srv.URL + "/preact"},
223
223
Unpm: cfg.Options{Out: outDir, Root: "/"},
224
224
}
225
225
-
if _, err := unpm.Vendor(c); err != nil {
225
225
+
if _, err := vendor.Vendor(c); err != nil {
226
226
t.Fatal(err)
227
227
}
228
228
···
262
262
Imports: map[string]string{"mylib": srv.URL + "/mylib.js"},
263
263
Unpm: cfg.Options{Out: outDir, Root: "/"},
264
264
}
265
265
-
if _, err := unpm.Vendor(c); err != nil {
265
265
+
if _, err := vendor.Vendor(c); err != nil {
266
266
t.Fatal(err)
267
267
}
268
268
···
307
307
Imports: map[string]string{"preact": srv.URL + "/preact@10"},
308
308
Unpm: cfg.Options{Out: outDir, Root: "/"},
309
309
}
310
310
-
if _, err := unpm.Vendor(c); err != nil {
310
310
+
if _, err := vendor.Vendor(c); err != nil {
311
311
t.Fatal(err)
312
312
}
313
313
···
342
342
Imports: map[string]string{"mylib": srv.URL + "/mylib.js"},
343
343
Unpm: cfg.Options{Out: outDir, Root: "/assets/vendor"},
344
344
}
345
345
-
if _, err := unpm.Vendor(c); err != nil {
345
345
+
if _, err := vendor.Vendor(c); err != nil {
346
346
t.Fatal(err)
347
347
}
348
348
···
376
376
Imports: map[string]string{"app": srv.URL + "/app.js"},
377
377
Unpm: cfg.Options{Out: outDir, Root: "/"},
378
378
}
379
379
-
if _, err := unpm.Vendor(c); err != nil {
379
379
+
if _, err := vendor.Vendor(c); err != nil {
380
380
t.Fatal(err)
381
381
}
382
382
···
408
408
Imports: map[string]string{"entry": srv.URL + "/entry.js"},
409
409
Unpm: cfg.Options{Out: outDir, Root: "/"},
410
410
}
411
411
-
if _, err := unpm.Vendor(c); err != nil {
411
411
+
if _, err := vendor.Vendor(c); err != nil {
412
412
t.Fatal(err)
413
413
}
414
414
···
444
444
Imports: map[string]string{"entry": srv.URL + "/entry.js"},
445
445
Unpm: cfg.Options{Out: outDir, Root: "/"},
446
446
}
447
447
-
if _, err := unpm.Vendor(c); err != nil {
447
447
+
if _, err := vendor.Vendor(c); err != nil {
448
448
t.Fatal(err)
449
449
}
450
450
···
486
486
},
487
487
Unpm: cfg.Options{Out: outDir, Root: "/", Pin: []string{host + "/b.js"}},
488
488
}
489
489
-
if _, err := unpm.Vendor(c); err != nil {
489
489
+
if _, err := vendor.Vendor(c); err != nil {
490
490
t.Fatal(err)
491
491
}
492
492
···
525
525
},
526
526
Unpm: cfg.Options{Out: outDir, Root: "/", Pin: []string{host + "/**"}},
527
527
}
528
528
-
if _, err := unpm.Vendor(c); err != nil {
528
528
+
if _, err := vendor.Vendor(c); err != nil {
529
529
t.Fatal(err)
530
530
}
531
531
···
556
556
Imports: map[string]string{"htm": srv.URL + "/src/index.mjs"},
557
557
Unpm: cfg.Options{Out: outDir, Root: "/"},
558
558
}
559
559
-
if _, err := unpm.Vendor(c); err != nil {
559
559
+
if _, err := vendor.Vendor(c); err != nil {
560
560
t.Fatal(err)
561
561
}
562
562
···
598
598
Imports: map[string]string{"lib": srv.URL + "/entry.js"},
599
599
Unpm: cfg.Options{Out: outDir, Root: "/"},
600
600
}
601
601
-
if _, err := unpm.Vendor(c); err != nil {
601
601
+
if _, err := vendor.Vendor(c); err != nil {
602
602
t.Fatal(err)
603
603
}
604
604
···
635
635
Imports: map[string]string{"lib": srv.URL + "/index.ts"},
636
636
Unpm: cfg.Options{Out: outDir, Root: "/"},
637
637
}
638
638
-
if _, err := unpm.Vendor(c); err != nil {
638
638
+
if _, err := vendor.Vendor(c); err != nil {
639
639
t.Fatal(err)
640
640
}
641
641
···
698
698
},
699
699
Unpm: cfg.Options{Out: outDir, Root: "/"},
700
700
}
701
701
-
warnings, err := unpm.Vendor(c)
701
701
+
warnings, err := vendor.Vendor(c)
702
702
if err != nil {
703
703
t.Fatalf("vendor should not fail when one entry returns HTML: %v", err)
704
704
}
···
743
743
Imports: map[string]string{"entry": srv.URL + "/entry.ts"},
744
744
Unpm: cfg.Options{Out: outDir, Root: "/"},
745
745
}
746
746
-
if _, err := unpm.Vendor(c); err != nil {
746
746
+
if _, err := vendor.Vendor(c); err != nil {
747
747
t.Fatal(err)
748
748
}
749
749
···
780
780
Imports: map[string]string{"mylib": srv.URL + "/mylib.js"},
781
781
Unpm: cfg.Options{Out: outDir, Root: "/"},
782
782
}
783
783
-
if _, err := unpm.Vendor(c); err != nil {
783
783
+
if _, err := vendor.Vendor(c); err != nil {
784
784
t.Fatal(err)
785
785
}
786
786
···
811
811
Imports: map[string]string{"mylib": srv.URL + "/mylib.js"},
812
812
Unpm: cfg.Options{Out: outDir, Root: "/"},
813
813
}
814
814
-
if _, err := unpm.Vendor(c); err != nil {
814
814
+
if _, err := vendor.Vendor(c); err != nil {
815
815
t.Fatalf("vendor should succeed even when sidecar is absent: %v", err)
816
816
}
817
817
···
840
840
Types: map[string]string{"mylib": srv.URL + "/explicit-types.d.ts"},
841
841
Unpm: cfg.Options{Out: outDir, Root: "/"},
842
842
}
843
843
-
if _, err := unpm.Vendor(c); err != nil {
843
843
+
if _, err := vendor.Vendor(c); err != nil {
844
844
t.Fatal(err)
845
845
}
846
846
···
878
878
Types: map[string]string{"mylib": srv.URL + "/types.d.ts"},
879
879
Unpm: cfg.Options{Out: outDir, Root: "/"},
880
880
}
881
881
-
if _, err := unpm.Vendor(c); err != nil {
881
881
+
if _, err := vendor.Vendor(c); err != nil {
882
882
t.Fatal(err)
883
883
}
884
884
···
917
917
Imports: map[string]string{"a": srv.URL + "/a.js"},
918
918
Unpm: cfg.Options{Out: outDir, Root: "/"},
919
919
}
920
920
-
if _, err := unpm.Vendor(c); err != nil {
920
920
+
if _, err := vendor.Vendor(c); err != nil {
921
921
t.Fatal(err)
922
922
}
923
923