Skip to content

Commit 795bf94

Browse files
committed
add importSource sourceType field
1 parent bb5e3f3 commit 795bf94

File tree

11 files changed

+133
-87
lines changed

11 files changed

+133
-87
lines changed

docs/source/examples/victory_chart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
victory = idom.install("victory", fallback="loading...")
55
bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
6-
idom.run(idom.component(lambda: victory.VictoryBar({"style": bar_style})), port=8000)
6+
idom.run(idom.component(lambda: victory.VictoryBar({"style": bar_style})))

scripts/one_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def main():
2929
return
3030

3131
idom_run = idom.run
32-
idom.run = lambda component: idom_run(component, port=5000)
32+
idom.run = lambda component: idom_run(component)
3333

3434
with example_file.open() as f:
3535
exec(

src/idom/client/_private.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX = "/_snowpack/pkg"
1616

1717

18-
def get_use_packages_file(app_dir: Path) -> Path:
18+
def get_user_packages_file(app_dir: Path) -> Path:
1919
return app_dir / "packages" / "idom-app-react" / "src" / "user-packages.js"
2020

2121

src/idom/client/app/packages/idom-app-react/src/index.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { mountLayoutWithWebSocket } from "idom-client-react";
22
import { unmountComponentAtNode } from "react-dom";
33

4+
// imported so static analysis knows to pick up files linked by user-packages.js
5+
import("./user-packages.js").then((module) => {
6+
for (const pkgName in module.default) {
7+
module.default[pkgName].then((pkg) => {
8+
console.log(`Loaded module '${pkgName}'`);
9+
});
10+
}
11+
});
12+
413
export function mount(mountPoint) {
514
mountLayoutWithWebSocket(
615
mountPoint,
@@ -26,8 +35,8 @@ function getWebSocketEndpoint() {
2635
return protocol + "//" + url.join("/") + "?" + queryParams.user.toString();
2736
}
2837

29-
function loadImportSource(source) {
30-
return import("./user-packages.js").then((module) => module.default[source]);
38+
function loadImportSource(source, sourceType) {
39+
return import(sourceType == "NAME" ? `./${source}.js` : source);
3140
}
3241

3342
function shouldReconnect() {

src/idom/client/app/packages/idom-client-react/src/component.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ function ImportedElement({ model }) {
4141
const mountPoint = react.useRef(null);
4242

4343
react.useEffect(() => {
44-
config.loadImportSource(model.importSource.source).then((module) => {
45-
mountImportSource(mountPoint.current, module, model, config);
46-
});
44+
config
45+
.loadImportSource(
46+
model.importSource.source,
47+
model.importSource.sourceType
48+
)
49+
.then((module) => {
50+
mountImportSource(mountPoint.current, module, model, config);
51+
});
4752
});
4853

4954
return html`<div ref=${mountPoint} />`;

src/idom/client/app/packages/idom-client-react/src/mount.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function mountLayoutWithWebSocket(
1010
element,
1111
endpoint,
1212
loadImportSource,
13-
maxReconnectTimeout = 0
13+
maxReconnectTimeout
1414
) {
1515
mountLayoutWithReconnectingWebSocket(
1616
element,
@@ -28,6 +28,7 @@ function mountLayoutWithReconnectingWebSocket(
2828
mountState = {
2929
everMounted: false,
3030
reconnectAttempts: 0,
31+
reconnectTimeoutRange: 0,
3132
}
3233
) {
3334
const socket = new WebSocket(endpoint);
@@ -36,15 +37,17 @@ function mountLayoutWithReconnectingWebSocket(
3637

3738
socket.onopen = (event) => {
3839
console.log(`Connected.`);
40+
3941
if (mountState.everMounted) {
4042
unmountComponentAtNode(element);
4143
}
44+
_resetOpenMountState(mountState);
45+
4246
mountLayout(element, {
4347
loadImportSource,
4448
saveUpdateHook: updateHookPromise.resolve,
4549
sendEvent: (event) => socket.send(JSON.stringify(event)),
4650
});
47-
_setOpenMountState(mountState);
4851
};
4952

5053
socket.onmessage = (event) => {
@@ -53,26 +56,35 @@ function mountLayoutWithReconnectingWebSocket(
5356
};
5457

5558
socket.onclose = (event) => {
56-
if (maxReconnectTimeout != 0) {
59+
if (!maxReconnectTimeout) {
5760
console.log(`Connection lost.`);
5861
return;
5962
}
63+
6064
const reconnectTimeout = _nextReconnectTimeout(
6165
maxReconnectTimeout,
6266
mountState
6367
);
68+
6469
console.log(`Connection lost, reconnecting in ${reconnectTimeout} seconds`);
70+
6571
setTimeout(function () {
6672
mountState.reconnectAttempts++;
67-
mountLayoutWithWebSocket(element, endpoint, importSourceURL, mountState);
73+
mountLayoutWithReconnectingWebSocket(
74+
element,
75+
endpoint,
76+
loadImportSource,
77+
maxReconnectTimeout,
78+
mountState
79+
);
6880
}, reconnectTimeout * 1000);
6981
};
7082
}
7183

72-
function _setOpenMountState(mountState) {
84+
function _resetOpenMountState(mountState) {
7385
mountState.everMounted = true;
7486
mountState.reconnectAttempts = 0;
75-
mountState.reconnectTimeoutRange = 1;
87+
mountState.reconnectTimeoutRange = 0;
7688
}
7789

7890
function _nextReconnectTimeout(maxReconnectTimeout, mountState) {

src/idom/client/manage.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,6 @@ def web_module_exports(package_name: str) -> Set[str]:
3434
)
3535

3636

37-
def web_module_url(package_name: str) -> str:
38-
"""Get the URL the where the web module should reside
39-
40-
If this URL is relative, then the base URL is determined by the client
41-
"""
42-
return package_name
43-
44-
4537
def web_module_exists(package_name: str) -> bool:
4638
"""Whether a web module with a given name exists"""
4739
return web_module_path(package_name).exists()
@@ -62,15 +54,19 @@ def web_module_names() -> Set[str]:
6254
return set(names)
6355

6456

65-
def add_web_module(package_name: str, source: Union[Path, str]) -> str:
57+
def add_web_module(
58+
package_name: str,
59+
source: Union[Path, str],
60+
) -> None:
6661
"""Add a web module from source"""
62+
if web_module_exists(package_name):
63+
raise ValueError(f"Web module {package_name!r} already exists")
6764
source = Path(source)
6865
if not source.exists():
6966
raise FileNotFoundError(f"Package source file does not exist: {str(source)!r}")
7067
target = web_module_path(package_name)
7168
target.parent.mkdir(parents=True, exist_ok=True)
7269
target.symlink_to(source.absolute())
73-
return web_module_url(package_name)
7470

7571

7672
def restore() -> None:
@@ -173,7 +169,7 @@ def _run_subprocess(args: List[str], cwd: Path) -> None:
173169

174170

175171
def _write_user_packages_file(app_dir: Path, packages: Iterable[str]) -> None:
176-
_private.get_use_packages_file(app_dir).write_text(
172+
_private.get_user_packages_file(app_dir).write_text(
177173
_USER_PACKAGES_FILE_TEMPLATE.format(
178174
imports=",".join(f'"{pkg}":import({pkg!r})' for pkg in packages)
179175
)

src/idom/client/module.py

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,28 @@ def install(
5959
]
6060

6161

62+
NAME_SOURCE = "NAME"
63+
"""A named souce - usually a Javascript package name"""
64+
65+
URL_SOURCE = "URL"
66+
"""A source loaded from a URL, usually from a CDN"""
67+
68+
SOURCE_TYPES = {NAME_SOURCE, URL_SOURCE}
69+
"""The possible source types for a :class:`Module`"""
70+
71+
6272
class Module:
6373
"""A Javascript module
6474
6575
Parameters:
66-
url_or_name:
76+
source:
6777
The URL to an ECMAScript module which exports React components
6878
(*with* a ``.js`` file extension) or name of a module installed in the
6979
built-in client application (*without* a ``.js`` file extension).
70-
source_file:
80+
source_type:
81+
The type of the given ``source``. See :const:`SOURCE_TYPES` for the set of
82+
possible values.
83+
file:
7184
Only applicable if running on a client app which supports this feature.
7285
Dynamically install the code in the give file as a single-file module. The
7386
built-in client will inject this module adjacent to other installed modules
@@ -87,50 +100,48 @@ class Module:
87100
The URL this module will be imported from.
88101
"""
89102

90-
__slots__ = (
91-
"url",
92-
"fallback",
93-
"exports",
94-
"exports_mount",
95-
"check_exports",
96-
"_export_names",
97-
)
103+
__slots__ = "source", "source_type", "fallback", "exports", "exports_mount"
98104

99105
def __init__(
100106
self,
101-
url_or_name: str,
107+
source: str,
108+
source_type: Optional[str] = None,
102109
source_file: Optional[Union[str, Path]] = None,
103110
fallback: Optional[str] = None,
104111
exports_mount: bool = False,
105-
check_exports: bool = True,
112+
check_exports: Optional[bool] = None,
106113
) -> None:
114+
self.source = source
107115
self.fallback = fallback
108116
self.exports_mount = exports_mount
109-
self.check_exports = check_exports
117+
self.exports: Optional[Set[str]] = None
110118

111-
self.exports: Set[str] = set()
112-
if source_file is not None:
113-
self.url = (
114-
manage.web_module_url(url_or_name)
115-
if manage.web_module_exists(url_or_name)
116-
else manage.add_web_module(url_or_name, source_file)
117-
)
118-
if check_exports:
119-
self.exports = manage.web_module_exports(url_or_name)
120-
elif _is_url(url_or_name):
121-
self.url = url_or_name
122-
self.check_exports = check_exports = False
123-
elif manage.web_module_exists(url_or_name):
124-
self.url = manage.web_module_url(url_or_name)
125-
if check_exports:
126-
self.exports = manage.web_module_exports(url_or_name)
119+
if source_type is None:
120+
self.source_type = URL_SOURCE if _is_url(source) else NAME_SOURCE
121+
elif source_type in SOURCE_TYPES:
122+
self.source_type = source_type
127123
else:
128-
raise ValueError(f"{url_or_name!r} is not installed or is not a URL")
124+
raise ValueError(f"Invalid source type {source_type!r}")
125+
126+
if self.source_type == URL_SOURCE:
127+
if check_exports is True:
128+
raise ValueError("Cannot check exports for source type {source_type!r}")
129+
elif source_file is not None:
130+
raise ValueError(f"File given, but source type is {source_type!r}")
131+
else:
132+
return None
133+
elif check_exports is None:
134+
check_exports = True
129135

130-
if check_exports and exports_mount and "mount" not in self.exports:
131-
raise ValueError(
132-
f"Module {url_or_name!r} does not export 'mount' but exports_mount=True"
133-
)
136+
if source_file is not None:
137+
manage.add_web_module(source, source_file)
138+
elif not manage.web_module_exists(source):
139+
raise ValueError(f"Module {source!r} does not exist")
140+
141+
if check_exports:
142+
self.exports = manage.web_module_exports(source)
143+
if exports_mount and "mount" not in self.exports:
144+
raise ValueError(f"Module {source!r} does not export 'mount'")
134145

135146
def declare(
136147
self,
@@ -149,24 +160,35 @@ def declare(
149160
Where ``name`` is the given name, and ``module`` is the :attr:`Module.url` of
150161
this :class:`Module` instance.
151162
"""
152-
if self.check_exports and name not in self.exports:
163+
if self.exports is not None and name not in self.exports:
153164
raise ValueError(
154165
f"{self} does not export {name!r}, available options are {list(self.exports)}"
155166
)
156167

157168
return Import(
158-
self.url,
159169
name,
160-
has_children=has_children,
161-
exports_mount=self.exports_mount,
162-
fallback=fallback or self.fallback,
170+
self.source,
171+
self.source_type,
172+
has_children,
173+
self.exports_mount,
174+
fallback or self.fallback,
163175
)
164176

165177
def __getattr__(self, name: str) -> Import:
178+
if name[0].lower() == name[0]:
179+
# component names should be capitalized
180+
raise AttributeError(f"{self} has no attribute {name!r}")
166181
return self.declare(name)
167182

183+
def __eq__(self, other: Any) -> bool:
184+
return (
185+
isinstance(other, Module)
186+
and self.source == other.source
187+
and self.source_type == other.source_type
188+
)
189+
168190
def __repr__(self) -> str:
169-
return f"{type(self).__name__}({self.url})"
191+
return f"{type(self).__name__}({self.source})"
170192

171193

172194
class Import:
@@ -188,8 +210,9 @@ class Import:
188210

189211
def __init__(
190212
self,
191-
module: str,
192213
name: str,
214+
source: str,
215+
source_type: str,
193216
has_children: bool = True,
194217
exports_mount: bool = False,
195218
fallback: Optional[str] = None,
@@ -199,12 +222,15 @@ def __init__(
199222
# set after Import instances have been constructed. A more comprehensive
200223
# check can be introduced if that is shown to be an issue in practice.
201224
raise RuntimeError(
202-
f"{IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT} is set and {module} has no mount"
225+
f"{IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT} is set and {source} has no mount"
203226
)
204227
self._name = name
205228
self._constructor = make_vdom_constructor(name, has_children)
206229
self._import_source = ImportSourceDict(
207-
source=module, fallback=fallback, exportsMount=exports_mount
230+
source=source,
231+
sourceType=source_type,
232+
fallback=fallback,
233+
exportsMount=exports_mount,
208234
)
209235

210236
def __call__(

src/idom/core/vdom.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"type": "object",
6060
"properties": {
6161
"source": {"type": "string"},
62+
"sourceType": {"enum": ["URL", "NAME"]},
6263
"fallback": {
6364
"type": ["object", "string", "null"],
6465
"if": {"not": {"type": "null"}},
@@ -90,6 +91,7 @@ def validate_vdom(value: Any) -> None:
9091
class ImportSourceDict(TypedDict):
9192
source: str
9293
fallback: Any
94+
sourceType: str
9395
exportsMount: bool # noqa
9496

9597

0 commit comments

Comments
 (0)