a simpler package manager for no-build websites
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) 2026 Jake Lazaroff https://github.com/jakelazaroff/unpm
6
7package cfg
8
9import (
10 "encoding/json"
11 "fmt"
12 "os"
13 "path/filepath"
14 "slices"
15 "strings"
16)
17
18type Config struct {
19 Imports map[string]string `json:"imports"`
20 Unpm Options `json:"$unpm"`
21}
22
23type Options struct {
24 Out string `json:"out,omitempty"`
25 Root string `json:"root,omitempty"`
26 Pin []string `json:"pin,omitempty"`
27}
28
29func ReadConfig(configPath string) (*Config, error) {
30 data, err := os.ReadFile(configPath)
31 if err != nil {
32 return nil, fmt.Errorf("reading config: %w", err)
33 }
34
35 var cfg Config
36 if err := json.Unmarshal(data, &cfg); err != nil {
37 return nil, fmt.Errorf("parsing config: %w", err)
38 }
39
40 if len(cfg.Imports) == 0 {
41 return nil, fmt.Errorf("no imports found in %s", configPath)
42 }
43
44 for key, url := range cfg.Imports {
45 if strings.HasSuffix(key, "/") {
46 return nil, fmt.Errorf("trailing-slash prefix entries are not supported: %q (list each subpath explicitly)", key)
47 }
48 if !strings.HasPrefix(url, "https://") {
49 return nil, fmt.Errorf("URL for %q must be an https:// URL, got: %s", key, url)
50 }
51 }
52
53 // Apply defaults, resolved relative to the config file's directory.
54 dir := filepath.Dir(configPath)
55 if cfg.Unpm.Out == "" {
56 cfg.Unpm.Out = filepath.Join(dir, "vendor")
57 } else {
58 cfg.Unpm.Out = filepath.Join(dir, cfg.Unpm.Out)
59 }
60 if cfg.Unpm.Root == "" {
61 cfg.Unpm.Root = dir
62 } else {
63 cfg.Unpm.Root = filepath.Join(dir, cfg.Unpm.Root)
64 }
65
66 return &cfg, nil
67}
68
69// IsPinned reports whether the given import key is pinned.
70func (c *Config) IsPinned(key string) bool {
71 return slices.Contains(c.Unpm.Pin, key)
72}