Plain is headed towards 1.0! Subscribe for development updates →

  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()