Something went wrong. Try again.
a simpler package manager for no-build websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.//// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/unpm
// Package modgraph reads the graph of already-vendored modules from disk. It is// the shared foundation for the `check` and `why` commands, which both need to// walk imports starting from the import-map entries.//// Vendored files are identified by their layout.RelativePath. Because that path// encodes the original host and path, references can be resolved with plain// path arithmetic, without reconstructing URLs. modgraph never fetches anything.package modgraph
import ( "io/fs" "net/url" "os" "path" "path/filepath" "strings"
"tangled.org/jakelazaroff.com/unpm/internal/imports" "tangled.org/jakelazaroff.com/unpm/internal/layout")
// Import is a single reference found inside a vendored file.type Import struct { // Spec is the specifier exactly as it appears in the source. Spec string // Bare is true for bare module specifiers, which are resolved by the // import map rather than by location. Bare bool // SourceMap is true for a //# sourceMappingURL= reference. SourceMap bool // Target is the resolved location of the referenced file (empty when Bare, // or when the reference is an external URL that can't be placed on disk). Target layout.RelativePath}
// Graph reads the vendored module graph rooted at a layout.type Graph struct { layout layout.Layout}
// New returns a Graph that reads files under l.func New(l layout.Layout) Graph { return Graph{layout: l}}
// Imports reads the vendored file at rel and returns its references. Files that// are not JavaScript/TypeScript have no references.func (g Graph) Imports(rel layout.RelativePath) ([]Import, error) { if !isJavaScript(rel) { return nil, nil } data, err := os.ReadFile(g.layout.DiskPath(rel)) if err != nil { return nil, err } src := string(data)
var imps []Import for _, spec := range imports.Scan(src) { if isBare(spec) { imps = append(imps, Import{Spec: spec, Bare: true}) } else { imps = append(imps, Import{Spec: spec, Target: resolve(rel, spec)}) } } for _, spec := range imports.ScanSourceMaps(src) { imps = append(imps, Import{Spec: spec, SourceMap: true, Target: resolve(rel, spec)}) } return imps, nil}
// Exists reports whether the vendored file at rel is present on disk.func (g Graph) Exists(rel layout.RelativePath) bool { info, err := os.Stat(g.layout.DiskPath(rel)) return err == nil && !info.IsDir()}
// Walk calls fn for every vendored file under the output directory, identified// by its RelativePath. It is a no-op when the output directory is absent.func (g Graph) Walk(fn func(rel layout.RelativePath) error) error { out := g.layout.Out if _, err := os.Stat(out); os.IsNotExist(err) { return nil } return filepath.WalkDir(out, func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } rel, err := filepath.Rel(out, p) if err != nil { return err } return fn(layout.RelativePath(filepath.ToSlash(rel))) })}
// isBare reports whether spec is a bare module specifier — one resolved by the// import map rather than by location (not "./", "../", "/", or a full URL).func isBare(spec string) bool { return spec != "" && !strings.HasPrefix(spec, ".") && !strings.HasPrefix(spec, "/") && !strings.Contains(spec, "://")}
// resolve turns a non-bare specifier found in the file at rel into the location// it points to: a full URL maps to its own host/path; a root-relative specifier// maps under the file's host; a relative specifier is joined to the file's// directory. An empty result means the reference can't be placed on disk.func resolve(rel layout.RelativePath, spec string) layout.RelativePath { switch { case strings.Contains(spec, "://"): u, err := url.Parse(spec) if err != nil || u.Host == "" { return "" } return layout.RelativePath(path.Clean(u.Host + u.Path)) case strings.HasPrefix(spec, "/"): host := string(rel) if i := strings.IndexByte(host, '/'); i >= 0 { host = host[:i] } return layout.RelativePath(path.Clean(host + spec)) default: return rel.Dir().Join(spec) }}
var jsExts = map[string]bool{ ".js": true, ".mjs": true, ".cjs": true, ".jsx": true, ".ts": true, ".tsx": true, ".mts": true, ".cts": true,}
// isJavaScript reports whether rel should be scanned for module references.// Extensionless paths (such as esm.sh entry points) are treated as JavaScript.func isJavaScript(rel layout.RelativePath) bool { ext := path.Ext(string(rel)) return ext == "" || jsExts[ext]}