66from __future__ import annotations
77
88from pathlib import Path
9- from typing import Any , Dict , List , Optional , Tuple , Union , overload
9+ from typing import Any , Dict , List , Optional , Set , Tuple , Union , overload
1010from urllib .parse import urlparse
1111
12+ from idom .config import IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT
1213from idom .core .vdom import ImportSourceDict , VdomDict , make_vdom_constructor
1314
1415from . import _private , manage
@@ -36,6 +37,8 @@ def install(
3637 packages : Union [str , List [str ], Tuple [str ]],
3738 ignore_installed : bool = False ,
3839 fallback : Optional [str ] = None ,
40+ # dynamically installed modules probably won't have a mount so we default to False
41+ has_mount : bool = False ,
3942) -> Union [Module , List [Module ]]:
4043 return_one = False
4144 if isinstance (packages , str ):
@@ -48,9 +51,11 @@ def install(
4851 manage .build (packages , clean_build = False )
4952
5053 if return_one :
51- return Module (pkg_names [0 ], fallback = fallback )
54+ return Module (pkg_names [0 ], fallback = fallback , has_mount = has_mount )
5255 else :
53- return [Module (pkg , fallback = fallback ) for pkg in pkg_names ]
56+ return [
57+ Module (pkg , fallback = fallback , has_mount = has_mount ) for pkg in pkg_names
58+ ]
5459
5560
5661class Module :
@@ -67,6 +72,12 @@ class Module:
6772 built-in client will inject this module adjacent to other installed modules
6873 which means they can be imported via a relative path like
6974 ``./some-other-installed-module.js``.
75+ fallack:
76+ What to display while the modules is being loaded.
77+ has_mount:
78+ Whether the module exports a ``mount`` function that allows components to
79+ be mounted directly to the DOM. Such a mount function enables greater
80+ flexibility in how custom components can be implemented.
7081
7182 Attributes:
7283 installed:
@@ -75,41 +86,52 @@ class Module:
7586 The URL this module will be imported from.
7687 """
7788
78- __slots__ = "url" , "fallback" , "exports" , "_export_names"
89+ __slots__ = (
90+ "url" ,
91+ "fallback" ,
92+ "exports" ,
93+ "has_mount" ,
94+ "check_exports" ,
95+ "_export_names" ,
96+ )
7997
8098 def __init__ (
8199 self ,
82100 url_or_name : str ,
83101 source_file : Optional [Union [str , Path ]] = None ,
84102 fallback : Optional [str ] = None ,
103+ has_mount : bool = False ,
85104 check_exports : bool = True ,
86105 ) -> None :
87106 self .fallback = fallback
88- self ._export_names : Optional [List [str ]] = None
107+ self .has_mount = has_mount
108+ self .check_exports = check_exports
109+
110+ self .exports : Set [str ] = set ()
89111 if source_file is not None :
90112 self .url = (
91113 manage .web_module_url (url_or_name )
92114 if manage .web_module_exists (url_or_name )
93115 else manage .add_web_module (url_or_name , source_file )
94116 )
95117 if check_exports :
96- self ._export_names = manage .web_module_exports (url_or_name )
118+ self .exports = manage .web_module_exports (url_or_name )
97119 elif _is_url (url_or_name ):
98120 self .url = url_or_name
121+ self .check_exports = False
99122 elif manage .web_module_exists (url_or_name ):
100123 self .url = manage .web_module_url (url_or_name )
101124 if check_exports :
102- self ._export_names = manage .web_module_exports (url_or_name )
125+ self .exports = manage .web_module_exports (url_or_name )
103126 else :
104127 raise ValueError (f"{ url_or_name !r} is not installed or is not a URL" )
105- self .exports = {name : self .declare (name ) for name in (self ._export_names or [])}
106128
107129 def declare (
108130 self ,
109131 name : str ,
110132 has_children : bool = True ,
111133 fallback : Optional [str ] = None ,
112- ) -> " Import" :
134+ ) -> Import :
113135 """Return an :class:`Import` for the given :class:`Module` and ``name``
114136
115137 This roughly translates to the javascript statement
@@ -121,19 +143,20 @@ def declare(
121143 Where ``name`` is the given name, and ``module`` is the :attr:`Module.url` of
122144 this :class:`Module` instance.
123145 """
124- if (
125- self ._export_names is not None
126- # if 'default' is exported there's not much we can infer
127- and "default" not in self ._export_names
128- ):
129- if name not in self ._export_names :
130- raise ValueError (
131- f"{ self } does not export { name !r} , available options are { self ._export_names } "
132- )
133- return Import (self .url , name , has_children , fallback = fallback or self .fallback )
134-
135- def __getattr__ (self , name : str ) -> "Import" :
136- return self .exports .get (name ) or self .declare (name )
146+ if self .check_exports and name not in self .exports :
147+ raise ValueError (
148+ f"{ self } does not export { name !r} , available options are { list (self .exports )} "
149+ )
150+ return Import (
151+ self .url ,
152+ name ,
153+ has_children ,
154+ has_mount = self .has_mount ,
155+ fallback = fallback or self .fallback ,
156+ )
157+
158+ def __getattr__ (self , name : str ) -> Import :
159+ return self .declare (name )
137160
138161 def __repr__ (self ) -> str :
139162 return f"{ type (self ).__name__ } ({ self .url } )"
@@ -161,11 +184,19 @@ def __init__(
161184 module : str ,
162185 name : str ,
163186 has_children : bool = True ,
187+ has_mount : bool = False ,
164188 fallback : Optional [str ] = None ,
165189 ) -> None :
190+ if IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT .current and not has_mount :
191+ raise RuntimeError (
192+ f"{ IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT } is set and { module } has no mount"
193+ )
194+
166195 self ._name = name
167196 self ._constructor = make_vdom_constructor (name , has_children )
168- self ._import_source = ImportSourceDict (source = module , fallback = fallback )
197+ self ._import_source = ImportSourceDict (
198+ source = module , fallback = fallback , hasMount = has_mount
199+ )
169200
170201 def __call__ (
171202 self ,
0 commit comments