1import hashlib
2import json
3import logging
4import os
5
6import tomli
7from marshmallow import Schema, fields
8
9from .generator import ImportmapGenerator
10
11logger = logging.getLogger(__name__)
12
13
14DEFAULT_CONFIG_FILENAME = "importmap.toml"
15DEFAULT_LOCK_FILENAME = "importmap.lock"
16
17
18class PackageSchema(Schema):
19 name = fields.String(required=True)
20 source = fields.String(required=True)
21 # preload
22 # vendor, or vendor all is one option?
23
24
25class ConfigSchema(Schema):
26 packages = fields.List(fields.Nested(PackageSchema), required=True)
27
28
29class LockfileSchema(Schema):
30 config_hash = fields.String(required=True)
31 importmap = fields.Dict(required=True)
32 importmap_dev = fields.Dict(required=True)
33
34
35def hash_for_data(data):
36 return hashlib.md5(json.dumps(data, sort_keys=True).encode("utf-8")).hexdigest()
37
38
39class Importmap:
40 def __init__(
41 self,
42 config_filename=DEFAULT_CONFIG_FILENAME,
43 lock_filename=DEFAULT_LOCK_FILENAME,
44 ):
45 self.config_filename = config_filename
46 self.lock_filename = lock_filename
47 self.config = {}
48 self.map = {}
49 self.map_dev = {}
50
51 def load(self):
52 # TODO preflight check to compare lock and config hash
53
54 self.config = self.load_config()
55
56 if not self.config:
57 # No config = no map and no lockfile
58 self.map = {}
59 self.map_dev = {}
60 self.delete_lockfile()
61 return
62
63 config_hash = hash_for_data(self.config)
64 lockfile = self.load_lockfile()
65
66 if not lockfile or lockfile["config_hash"] != config_hash:
67 # Generate both maps now, tag will choose which to use at runtime
68 self.map = self.generate_map()
69 self.map_dev = self.generate_map(development=True)
70
71 lockfile["config_hash"] = config_hash
72 lockfile["importmap"] = self.map
73 lockfile["importmap_dev"] = self.map_dev
74 self.save_lockfile(lockfile)
75
76 elif lockfile:
77 # Use map from up-to-date lockfile
78 self.map = lockfile["importmap"]
79 self.map_dev = lockfile["importmap_dev"]
80
81 def load_config(self):
82 # TODO raise custom exceptions
83
84 if not os.path.exists(self.config_filename):
85 logger.warning(f"{self.config_filename} not found")
86 return {}
87
88 with open(self.config_filename) as f:
89 # why doesn't tomli.load(f) work?
90 toml_data = tomli.loads(f.read())
91
92 return ConfigSchema().load(toml_data)
93
94 def load_lockfile(self):
95 if not os.path.exists(self.lock_filename):
96 return {}
97
98 with open(self.lock_filename) as f:
99 json_data = json.load(f)
100
101 return LockfileSchema().load(json_data)
102
103 def save_lockfile(self, lockfile):
104 with open(self.lock_filename, "w+") as f:
105 json.dump(lockfile, f, indent=2, sort_keys=True)
106
107 def delete_lockfile(self):
108 if os.path.exists(self.lock_filename):
109 os.remove(self.lock_filename)
110
111 def generate_map(self, *args, **kwargs):
112 return ImportmapGenerator.from_config(self.config, *args, **kwargs).generate()