myjs 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
myjs/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """myjs -- a small JavaScript interpreter for Python.
2
+
3
+ It runs a practical subset of ECMAScript (via the ``domonic_libs.acorn`` port and
4
+ its tree-walking evaluator) against domonic's server-side DOM, and hands scripts
5
+ an ``ffi`` global for calling native C libraries through ``ctypes``.
6
+
7
+ import myjs
8
+
9
+ myjs.eval("window.console.log('hi from myjs')")
10
+ myjs.run("script.js")
11
+
12
+ s = myjs.Session()
13
+ s.eval("const x = 21;")
14
+ s.eval("x * 2") # -> 42
15
+
16
+ Command line: ``myjs`` starts a REPL, ``myjs file.js`` executes a file.
17
+ """
18
+
19
+ def _read_version() -> str:
20
+ from pathlib import Path
21
+ f = Path(__file__).resolve().parents[2] / "VERSION" # repo-root source checkout
22
+ if f.is_file():
23
+ return f.read_text().strip()
24
+ from importlib.metadata import PackageNotFoundError, version
25
+ try:
26
+ return version("myjs")
27
+ except PackageNotFoundError:
28
+ return "0+unknown"
29
+
30
+
31
+ __version__ = _read_version()
32
+
33
+ from ._engine import JSError, Session
34
+
35
+ __all__ = ["Session", "JSError", "eval", "run", "repl", "__version__"]
36
+
37
+ _shared: Session | None = None
38
+
39
+
40
+ def _session() -> Session:
41
+ global _shared
42
+ if _shared is None:
43
+ _shared = Session()
44
+ return _shared
45
+
46
+
47
+ def eval(src: str, *, scope: dict | None = None):
48
+ """Evaluate a string of JavaScript and return its completion value.
49
+
50
+ With ``scope`` a throwaway :class:`Session` is used; otherwise a process-wide
51
+ session persists definitions between calls.
52
+ """
53
+ return (Session(scope=scope) if scope else _session()).eval(src)
54
+
55
+
56
+ def run(path: str, *, scope: dict | None = None):
57
+ """Execute a ``.js`` file. Returns its completion value."""
58
+ return Session(scope=scope).run_file(path)
59
+
60
+
61
+ def repl(**kwargs) -> int:
62
+ """Start the interactive REPL. Returns a process exit code."""
63
+ from .repl import repl as _repl
64
+
65
+ return _repl(**kwargs)
myjs/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
myjs/_engine.py ADDED
@@ -0,0 +1,125 @@
1
+ """The myjs session: a persistent JS interpreter over domonic's DOM + ``ffi``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from domonic_libs.acorn.interpret import (
8
+ UNDEFINED,
9
+ JSThrow,
10
+ _Console,
11
+ _stringify,
12
+ make_interpreter,
13
+ )
14
+
15
+ from .ffi import scope as _ffi_scope
16
+ from .host import async_fetch as _async_fetch
17
+ from .host import scope as _host_scope
18
+ from .host import websocket_ctor as _websocket_ctor
19
+
20
+
21
+ class PrintConsole(_Console):
22
+ """Captures like the base console, and echoes each line to stdout."""
23
+
24
+ def log(self, *a):
25
+ super().log(*a)
26
+ print(self.lines[-1])
27
+
28
+ warn = error = info = debug = trace = log
29
+
30
+
31
+ class JSError(Exception):
32
+ """A JavaScript exception (or syntax error) that escaped the script."""
33
+
34
+ def __init__(self, message, *, name="Error", line=None, trace=None):
35
+ super().__init__(message)
36
+ self.js_name = name
37
+ self.js_line = line
38
+ self.js_trace = trace
39
+
40
+ def __str__(self):
41
+ base = super().__str__()
42
+ where = f" (line {self.js_line})" if self.js_line else ""
43
+ stack = f"\n at {self.js_trace}" if self.js_trace else ""
44
+ return f"{self.js_name}: {base}{where}{stack}"
45
+
46
+
47
+ def _reraise(exc):
48
+ val = getattr(exc, "value", None)
49
+ if isinstance(val, dict):
50
+ raise JSError(
51
+ val.get("message", ""),
52
+ name=val.get("name", "Error"),
53
+ line=getattr(exc, "js_line", None),
54
+ trace=getattr(exc, "js_trace", None),
55
+ ) from None
56
+ raise JSError(
57
+ str(exc),
58
+ name=type(exc).__name__,
59
+ line=getattr(exc, "js_line", None),
60
+ trace=getattr(exc, "js_trace", None),
61
+ ) from None
62
+
63
+
64
+ class Session:
65
+ """One JS global scope. Feed it source with :meth:`eval`; state persists."""
66
+
67
+ def __init__(self, scope=None, console=None):
68
+ from . import __version__
69
+
70
+ extra = {**_ffi_scope(), **_host_scope(__version__)}
71
+ if scope:
72
+ extra.update(scope)
73
+ self.interp, self.console, self.document, self.window = make_interpreter(
74
+ extra_globals=extra, console=console
75
+ )
76
+ for name, value in (
77
+ ("fetch", _async_fetch(self.interp.loop)),
78
+ ("WebSocket", _websocket_ctor(self.interp.loop)),
79
+ ):
80
+ self.interp.global_env.declare(name, value)
81
+ self.window._own[name] = value
82
+
83
+ def eval(self, src, ecma_version=2022):
84
+ """Run ``src`` and return its completion value (``None`` for undefined)."""
85
+ try:
86
+ result = self.interp.run(src, ecma_version=ecma_version)
87
+ except (JSThrow, SyntaxError) as exc:
88
+ _reraise(exc)
89
+ return None if result is UNDEFINED else result
90
+
91
+ def run_file(self, path, ecma_version=2022):
92
+ p = Path(path).resolve()
93
+ self.interp.module_base = str(p.parent)
94
+ self.interp._cur_module["dir"] = str(p.parent)
95
+ return self.eval(p.read_text(), ecma_version=ecma_version)
96
+
97
+ @property
98
+ def console_lines(self):
99
+ return list(getattr(self.console, "lines", []))
100
+
101
+ def render_html(self):
102
+ """Serialise the DOM the script built into a standalone HTML document."""
103
+ try:
104
+ title = self.document.title
105
+ except Exception:
106
+ title = None
107
+ try:
108
+ root = str(self.document.documentElement)
109
+ except Exception:
110
+ root = f"<html><head></head><body>{self.document.body}</body></html>"
111
+ if title and isinstance(title, str) and "<title>" not in root:
112
+ root = root.replace("<head>", f"<head><title>{title}</title>", 1)
113
+ return "<!doctype html>\n" + root
114
+
115
+ @staticmethod
116
+ def display(value):
117
+ """Human-readable form of a completion value, for a REPL."""
118
+ if value is None or value is UNDEFINED:
119
+ return "undefined"
120
+ if isinstance(value, str):
121
+ return repr(value)
122
+ try:
123
+ return _stringify(value)
124
+ except Exception:
125
+ return repr(value)
myjs/cli.py ADDED
@@ -0,0 +1,87 @@
1
+ """``myjs`` command line: run a script, evaluate a snippet, or start a REPL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from . import __version__
9
+ from ._engine import JSError, PrintConsole, Session
10
+
11
+
12
+ def build_parser() -> argparse.ArgumentParser:
13
+ p = argparse.ArgumentParser(
14
+ prog="myjs",
15
+ description="A small JavaScript interpreter on domonic's DOM, with an ffi bridge to native C.",
16
+ )
17
+ p.add_argument("script", nargs="?", help="a .js file to execute (omit for a REPL)")
18
+ p.add_argument("args", nargs="*", help="arguments passed to the script as globalThis.argv")
19
+ p.add_argument("-e", "--eval", dest="code", metavar="JS", help="evaluate a string and print the result")
20
+ p.add_argument("-i", "--interactive", action="store_true", help="enter the REPL after running")
21
+ p.add_argument("--gui", action="store_true",
22
+ help="after running, show the DOM the script built in a native window")
23
+ p.add_argument("--version", action="version", version=f"myjs {__version__}")
24
+ return p
25
+
26
+
27
+ def _open_gui(session: Session) -> int:
28
+ try:
29
+ import webview
30
+ except ImportError:
31
+ print("myjs --gui needs pywebview: pip install 'domonic-libs[app]'", file=sys.stderr)
32
+ return 1
33
+ try:
34
+ title = session.eval("document.title") or "myjs"
35
+ except JSError:
36
+ title = "myjs"
37
+ webview.create_window(str(title) or "myjs", html=session.render_html(),
38
+ width=960, height=720)
39
+ webview.start()
40
+ return 0
41
+
42
+
43
+ def _report(exc: JSError) -> None:
44
+ print(exc, file=sys.stderr)
45
+
46
+
47
+ def main(argv=None) -> int:
48
+ args = build_parser().parse_args(argv)
49
+
50
+ if args.code is not None:
51
+ session = Session(console=PrintConsole())
52
+ try:
53
+ result = session.eval(args.code)
54
+ except JSError as exc:
55
+ _report(exc)
56
+ return 1
57
+ if result is not None:
58
+ print(result if isinstance(result, str) else session.display(result))
59
+ if args.interactive:
60
+ from .repl import repl
61
+ return repl(session)
62
+ return 0
63
+
64
+ if args.script:
65
+ session = Session(console=PrintConsole())
66
+ session.interp.global_env.declare("argv", list(args.args))
67
+ try:
68
+ session.run_file(args.script)
69
+ except FileNotFoundError:
70
+ print(f"myjs: cannot open {args.script}", file=sys.stderr)
71
+ return 1
72
+ except JSError as exc:
73
+ _report(exc)
74
+ return 1
75
+ if args.gui:
76
+ return _open_gui(session)
77
+ if args.interactive:
78
+ from .repl import repl
79
+ return repl(session)
80
+ return 0
81
+
82
+ from .repl import repl
83
+ return repl()
84
+
85
+
86
+ if __name__ == "__main__":
87
+ raise SystemExit(main())
myjs/ffi.py ADDED
@@ -0,0 +1,238 @@
1
+ """``ffi`` -- call native C libraries from JavaScript, via Python's ``ctypes``.
2
+
3
+ Exposed to scripts as the global ``ffi`` (plus a small ``os`` namespace). The
4
+ interpreter reaches arbitrary Python objects through ``getattr`` / ``setattr`` /
5
+ ``__call__``, so the wrappers here are thin: a :class:`Library` yields
6
+ :class:`CFunc` proxies that marshal JS values to and from C.
7
+
8
+ const libc = ffi.loadLibrary("c");
9
+ libc.abs.argtypes = [ffi.types.int];
10
+ libc.abs.restype = ffi.types.int;
11
+ console.log(libc.abs(-42)); // 42
12
+
13
+ Strings are encoded to UTF-8 ``bytes`` on the way in and decoded on the way out;
14
+ a :class:`Buffer` (from ``ffi.createStringBuffer``) passes its underlying storage
15
+ straight through. C -> JS callbacks are built with ``ffi.callback``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ctypes
21
+ import ctypes.util
22
+ import os
23
+ import sys
24
+
25
+ # --- C type table ---------------------------------------------------------
26
+
27
+ TYPES = {
28
+ "void": None,
29
+ "bool": ctypes.c_bool,
30
+ "char": ctypes.c_char,
31
+ "schar": ctypes.c_char,
32
+ "uchar": ctypes.c_ubyte,
33
+ "byte": ctypes.c_ubyte,
34
+ "short": ctypes.c_short,
35
+ "ushort": ctypes.c_ushort,
36
+ "int": ctypes.c_int,
37
+ "uint": ctypes.c_uint,
38
+ "long": ctypes.c_long,
39
+ "ulong": ctypes.c_ulong,
40
+ "longlong": ctypes.c_longlong,
41
+ "ulonglong": ctypes.c_ulonglong,
42
+ "int8": ctypes.c_int8,
43
+ "uint8": ctypes.c_uint8,
44
+ "int16": ctypes.c_int16,
45
+ "uint16": ctypes.c_uint16,
46
+ "int32": ctypes.c_int32,
47
+ "uint32": ctypes.c_uint32,
48
+ "int64": ctypes.c_int64,
49
+ "uint64": ctypes.c_uint64,
50
+ "size_t": ctypes.c_size_t,
51
+ "ssize_t": ctypes.c_ssize_t,
52
+ "float": ctypes.c_float,
53
+ "double": ctypes.c_double,
54
+ "string": ctypes.c_char_p,
55
+ "wstring": ctypes.c_wchar_p,
56
+ "pointer": ctypes.c_void_p,
57
+ }
58
+
59
+
60
+ # --- marshalling ---------------------------------------------------------
61
+
62
+ def _to_c(value):
63
+ """JS value -> something ctypes accepts as an argument."""
64
+ if isinstance(value, Buffer):
65
+ return value._buf
66
+ if isinstance(value, CFunc):
67
+ return value._ptr
68
+ if isinstance(value, str):
69
+ return value.encode("utf-8")
70
+ if isinstance(value, bool):
71
+ return value
72
+ if isinstance(value, float) and value.is_integer():
73
+ return int(value)
74
+ return value
75
+
76
+
77
+ def _from_c(value):
78
+ """ctypes return value -> JS-friendly value."""
79
+ if isinstance(value, bytes):
80
+ return value.decode("utf-8", "replace")
81
+ return value
82
+
83
+
84
+ # --- library / function proxies ---------------------------------------------
85
+
86
+ class CFunc:
87
+ """A single C function. ``argtypes`` / ``restype`` accept ``ffi.types`` and
88
+ are forwarded to the underlying ctypes pointer."""
89
+
90
+ def __init__(self, ptr, name):
91
+ object.__setattr__(self, "_ptr", ptr)
92
+ object.__setattr__(self, "_name", name)
93
+
94
+ def __call__(self, *args):
95
+ try:
96
+ return _from_c(self._ptr(*[_to_c(a) for a in args]))
97
+ except Exception as exc: # surfaced to JS as a thrown error
98
+ raise RuntimeError(f"ffi call {self._name}(): {exc}") from exc
99
+
100
+ def __setattr__(self, key, value):
101
+ if key == "argtypes":
102
+ self._ptr.argtypes = [t for t in value if t is not None] or None
103
+ elif key == "restype":
104
+ self._ptr.restype = value
105
+ elif key == "errcheck":
106
+ self._ptr.errcheck = value
107
+ else:
108
+ object.__setattr__(self, key, value)
109
+
110
+ def __getattr__(self, key):
111
+ if key.startswith("__") and key.endswith("__"):
112
+ raise AttributeError(key)
113
+ return getattr(object.__getattribute__(self, "_ptr"), key)
114
+
115
+ def __repr__(self):
116
+ return f"<ffi function {self._name}>"
117
+
118
+
119
+ class Library:
120
+ """A loaded shared library. ``lib.<name>`` resolves a :class:`CFunc`."""
121
+
122
+ def __init__(self, cdll, name):
123
+ object.__setattr__(self, "_cdll", cdll)
124
+ object.__setattr__(self, "_name", name)
125
+ object.__setattr__(self, "_cache", {})
126
+
127
+ def __getattr__(self, key):
128
+ if key.startswith("__") and key.endswith("__"):
129
+ raise AttributeError(key)
130
+ cache = object.__getattribute__(self, "_cache")
131
+ if key not in cache:
132
+ try:
133
+ cache[key] = CFunc(getattr(self._cdll, key), key)
134
+ except AttributeError:
135
+ raise AttributeError(f"{self._name!r} has no symbol {key!r}")
136
+ return cache[key]
137
+
138
+ def __repr__(self):
139
+ return f"<ffi library {self._name!r}>"
140
+
141
+
142
+ class Buffer:
143
+ """A mutable C string / byte buffer (``ffi.createStringBuffer``)."""
144
+
145
+ def __init__(self, buf):
146
+ object.__setattr__(self, "_buf", buf)
147
+
148
+ @property
149
+ def value(self):
150
+ return self._buf.value.decode("utf-8", "replace")
151
+
152
+ @value.setter
153
+ def value(self, v):
154
+ self._buf.value = v.encode("utf-8") if isinstance(v, str) else v
155
+
156
+ @property
157
+ def raw(self):
158
+ return self._buf.raw
159
+
160
+ @property
161
+ def length(self):
162
+ return len(self._buf)
163
+
164
+ def __len__(self):
165
+ return len(self._buf)
166
+
167
+ def __repr__(self):
168
+ return f"<ffi buffer {self.value!r}>"
169
+
170
+
171
+ # --- loading -----------------------------------------------------------------
172
+
173
+ def load_library(name):
174
+ """Load a shared library by short name (``"c"``, ``"m"``), by path
175
+ (``"./libfoo.so"``), or -- on macOS -- by system-framework name
176
+ (``"CoreFoundation"``)."""
177
+ if os.sep in str(name) or str(name).endswith((".so", ".dylib", ".dll")):
178
+ return Library(ctypes.CDLL(name), name)
179
+ found = ctypes.util.find_library(name)
180
+ if found:
181
+ return Library(ctypes.CDLL(found), name)
182
+ if sys.platform == "darwin":
183
+ fw = f"/System/Library/Frameworks/{name}.framework/{name}"
184
+ if os.path.exists(fw):
185
+ return Library(ctypes.CDLL(fw), name)
186
+ return Library(ctypes.CDLL(name), name) # let ctypes raise a clear OSError
187
+
188
+
189
+ def create_string_buffer(init):
190
+ if isinstance(init, str):
191
+ return Buffer(ctypes.create_string_buffer(init.encode("utf-8")))
192
+ if isinstance(init, (int, float)):
193
+ return Buffer(ctypes.create_string_buffer(int(init)))
194
+ if isinstance(init, (bytes, bytearray)):
195
+ return Buffer(ctypes.create_string_buffer(bytes(init)))
196
+ raise TypeError("createStringBuffer expects a string or a size")
197
+
198
+
199
+ def make_callback(restype, argtypes, fn):
200
+ """Wrap a JS function as a C callback pointer (``CFUNCTYPE``)."""
201
+ proto = ctypes.CFUNCTYPE(restype, *[t for t in (argtypes or []) if t is not None])
202
+
203
+ def thunk(*c_args):
204
+ return _to_c(fn(*[_from_c(a) for a in c_args]))
205
+
206
+ holder = proto(thunk)
207
+ _CALLBACK_KEEPALIVE.append(holder) # ctypes callbacks must outlive the call
208
+ return holder
209
+
210
+
211
+ _CALLBACK_KEEPALIVE = []
212
+
213
+
214
+ def c_string(ptr):
215
+ """Read a NUL-terminated C string at ``ptr`` (an int address or c_char_p)."""
216
+ return ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8", "replace")
217
+
218
+
219
+ # --- the scope handed to the interpreter -----------------------------------
220
+
221
+ FFI = {
222
+ "loadLibrary": load_library,
223
+ "createStringBuffer": create_string_buffer,
224
+ "callback": make_callback,
225
+ "string": c_string,
226
+ "sizeof": lambda t: ctypes.sizeof(t),
227
+ "addressof": lambda b: ctypes.addressof(b._buf if isinstance(b, Buffer) else b),
228
+ "cast": lambda v, t: ctypes.cast(_to_c(v), t),
229
+ "pointer": ctypes.pointer,
230
+ "byref": ctypes.byref,
231
+ "NULL": None,
232
+ "errno": lambda: ctypes.get_errno(),
233
+ "types": dict(TYPES),
234
+ }
235
+
236
+ def scope():
237
+ """Fresh copies so one session cannot mutate another's ``ffi.types``."""
238
+ return {"ffi": {**FFI, "types": dict(TYPES)}}
myjs/host.py ADDED
@@ -0,0 +1,568 @@
1
+ """Host bindings for myjs -- the things that make "it's just JavaScript" land:
2
+ a filesystem (``fs``), shell (``sh``), synchronous HTTP (``http`` / ``fetch``),
3
+ a ``process`` object, and ``py`` for reaching into the whole Python ecosystem.
4
+
5
+ Everything here is synchronous on purpose -- there is no event loop yet, and a
6
+ one-liner that blocks is what reads as magic in a REPL. All of it is handed to
7
+ scripts as globals (see :func:`scope`).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64 as _base64
13
+ import hashlib as _hashlib
14
+ import importlib
15
+ import json as _json
16
+ import os
17
+ import platform
18
+ import shutil
19
+ import socket as _socket
20
+ import ssl as _ssl
21
+ import subprocess
22
+ import sys
23
+ import threading
24
+ import time as _time
25
+ import urllib.error
26
+ import urllib.parse
27
+ import urllib.request
28
+ from pathlib import Path
29
+
30
+ from domonic_libs.acorn.interpret import JSArray, JSObject
31
+
32
+
33
+ # --- fs --------------------------------------------------------------------
34
+
35
+ def _read_file(path, encoding="utf-8"):
36
+ data = Path(path).read_bytes()
37
+ return data if encoding in (None, "buffer", "binary") else data.decode(encoding)
38
+
39
+
40
+ def _write_file(path, data, encoding="utf-8"):
41
+ p = Path(path)
42
+ if isinstance(data, (bytes, bytearray)):
43
+ p.write_bytes(bytes(data))
44
+ else:
45
+ p.write_text(str(data), encoding=encoding)
46
+ return len(data)
47
+
48
+
49
+ def _append_file(path, data, encoding="utf-8"):
50
+ with open(path, "a", encoding=encoding) as fh:
51
+ fh.write(str(data))
52
+
53
+
54
+ def _stat(path):
55
+ st = os.stat(path)
56
+ return JSObject({
57
+ "size": st.st_size,
58
+ "mtimeMs": st.st_mtime * 1000,
59
+ "isFile": os.path.isfile(path),
60
+ "isDirectory": os.path.isdir(path),
61
+ "mode": st.st_mode,
62
+ })
63
+
64
+
65
+ def _mkdir(path, opts=None):
66
+ recursive = bool(opts and opts.get("recursive"))
67
+ Path(path).mkdir(parents=recursive, exist_ok=recursive)
68
+
69
+
70
+ def _rm(path, opts=None):
71
+ opts = opts or {}
72
+ p = Path(path)
73
+ if p.is_dir() and opts.get("recursive"):
74
+ shutil.rmtree(p, ignore_errors=bool(opts.get("force")))
75
+ elif p.exists() or not opts.get("force"):
76
+ p.unlink()
77
+
78
+
79
+ FS = {
80
+ "readFileSync": _read_file,
81
+ "writeFileSync": _write_file,
82
+ "appendFileSync": _append_file,
83
+ "existsSync": lambda p: Path(p).exists(),
84
+ "readdirSync": lambda p: JSArray(sorted(os.listdir(p))),
85
+ "mkdirSync": _mkdir,
86
+ "rmSync": _rm,
87
+ "renameSync": lambda a, b: os.replace(a, b),
88
+ "copyFileSync": lambda a, b: shutil.copyfile(a, b),
89
+ "statSync": _stat,
90
+ "realpathSync": lambda p: str(Path(p).resolve()),
91
+ "cwd": os.getcwd,
92
+ }
93
+
94
+ PATH = {
95
+ "join": lambda *parts: os.path.join(*[str(p) for p in parts]),
96
+ "dirname": lambda p: os.path.dirname(p),
97
+ "basename": lambda p, ext="": os.path.basename(p)[: -len(ext)] if ext and os.path.basename(p).endswith(ext) else os.path.basename(p),
98
+ "extname": lambda p: os.path.splitext(p)[1],
99
+ "resolve": lambda *parts: str(Path(*[str(p) for p in parts]).resolve()) if parts else os.getcwd(),
100
+ "sep": os.sep,
101
+ }
102
+
103
+
104
+ # --- sh -------------------------------------------------------------------
105
+
106
+ class ShellResult(JSObject):
107
+ def __init__(self, proc):
108
+ super().__init__({
109
+ "stdout": proc.stdout,
110
+ "stderr": proc.stderr,
111
+ "code": proc.returncode,
112
+ "ok": proc.returncode == 0,
113
+ })
114
+
115
+ def __str__(self):
116
+ return self["stdout"]
117
+
118
+ def trim(self):
119
+ return self["stdout"].strip()
120
+
121
+ def json(self):
122
+ return _to_js(_json.loads(self["stdout"]))
123
+
124
+ def lines(self):
125
+ return JSArray(self["stdout"].splitlines())
126
+
127
+
128
+ def _sh(cmd, opts=None):
129
+ opts = opts or {}
130
+ proc = subprocess.run(
131
+ cmd, shell=True, capture_output=True, text=True,
132
+ cwd=opts.get("cwd"), timeout=opts.get("timeout"),
133
+ )
134
+ return ShellResult(proc)
135
+
136
+
137
+ def _say(text, *_):
138
+ if sys.platform == "darwin":
139
+ subprocess.run(["say", str(text)], check=False)
140
+ elif sys.platform.startswith("linux") and shutil.which("espeak"):
141
+ subprocess.run(["espeak", str(text)], check=False)
142
+ else:
143
+ print(f"\a(say) {text}")
144
+
145
+
146
+ def _osa(script):
147
+ subprocess.run(["osascript", "-e", script], check=False, capture_output=True)
148
+
149
+
150
+ def _notify(text, title="myjs"):
151
+ if sys.platform == "darwin":
152
+ _osa(f'display notification {_json.dumps(str(text))} with title {_json.dumps(str(title))}')
153
+ elif shutil.which("notify-send"):
154
+ subprocess.run(["notify-send", str(title), str(text)], check=False)
155
+ else:
156
+ print(f"(notify) {title}: {text}")
157
+
158
+
159
+ def _alert(text, title="myjs"):
160
+ if sys.platform == "darwin":
161
+ _osa(f'display dialog {_json.dumps(str(text))} with title {_json.dumps(str(title))} '
162
+ f'buttons {{"OK"}} default button "OK"')
163
+ else:
164
+ print(f"(alert) {title}: {text}")
165
+
166
+
167
+ def _open(target, *_):
168
+ opener = {"darwin": "open", "win32": "start"}.get(sys.platform, "xdg-open")
169
+ subprocess.run([opener, str(target)], check=False, shell=(opener == "start"))
170
+
171
+
172
+ # --- http / fetch --------------------------------------------------------
173
+
174
+ class Response(JSObject):
175
+ def __init__(self, url, status, headers, body):
176
+ super().__init__({
177
+ "url": url,
178
+ "status": status,
179
+ "ok": 200 <= status < 300,
180
+ "headers": JSObject({k.lower(): v for k, v in headers}),
181
+ })
182
+ self._body = body
183
+
184
+ @property
185
+ def text(self):
186
+ return self._body.decode("utf-8", "replace")
187
+
188
+ def json(self):
189
+ return _to_js(_json.loads(self._body.decode("utf-8", "replace")))
190
+
191
+ def bytes(self):
192
+ return self._body
193
+
194
+
195
+ def _request(url, opts=None):
196
+ opts = opts or {}
197
+ data = opts.get("body")
198
+ if data is not None and not isinstance(data, (bytes, bytearray)):
199
+ data = str(data).encode("utf-8")
200
+ headers = {k: str(v) for k, v in (opts.get("headers") or {}).items()}
201
+ req = urllib.request.Request(url, data=data, headers=headers, method=opts.get("method"))
202
+ try:
203
+ with urllib.request.urlopen(req, timeout=opts.get("timeout", 30)) as resp:
204
+ return Response(resp.geturl(), resp.status, resp.getheaders(), resp.read())
205
+ except urllib.error.HTTPError as e:
206
+ return Response(url, e.code, list(e.headers.items()), e.read())
207
+
208
+
209
+ HTTP = {
210
+ "get": lambda url, opts=None: _request(url, {**(opts or {}), "method": "GET"}),
211
+ "post": lambda url, body=None, opts=None: _request(url, {**(opts or {}), "method": "POST", "body": body}),
212
+ "request": _request,
213
+ "requestSync": _request,
214
+ "serve": None, # set in scope() -- needs the calling session's interpreter
215
+ }
216
+
217
+
218
+ def async_fetch(loop):
219
+ """A real asynchronous ``fetch``: the request runs on a background thread and
220
+ settles a promise through ``loop``, so ``await Promise.all([...])`` genuinely
221
+ runs requests concurrently."""
222
+ import threading
223
+
224
+ from domonic_libs.acorn.interpret import _make_error, _Promise
225
+
226
+ def fetch(url, opts=None):
227
+ p = _Promise(loop)
228
+ loop.io_start()
229
+
230
+ def worker():
231
+ try:
232
+ resp = _request(url, opts)
233
+ loop.io_finish(lambda: p._resolve(resp))
234
+ except Exception as exc: # noqa: BLE001
235
+ loop.io_finish(lambda exc=exc: p._reject(_make_error("TypeError", f"fetch failed: {exc}")))
236
+
237
+ threading.Thread(target=worker, daemon=True).start()
238
+ return p
239
+
240
+ return fetch
241
+
242
+
243
+ def _make_serve():
244
+ from http.server import BaseHTTPRequestHandler, HTTPServer
245
+
246
+ def serve(port, handler):
247
+ class H(BaseHTTPRequestHandler):
248
+ def _run(self):
249
+ length = int(self.headers.get("content-length", 0) or 0)
250
+ body = self.rfile.read(length).decode("utf-8", "replace") if length else ""
251
+ req = JSObject({
252
+ "method": self.command,
253
+ "path": self.path,
254
+ "headers": JSObject(dict(self.headers)),
255
+ "body": body,
256
+ })
257
+ out = handler(req)
258
+ if isinstance(out, dict):
259
+ status = int(out.get("status", 200))
260
+ text = str(out.get("body", ""))
261
+ extra = out.get("headers") or {}
262
+ else:
263
+ status, text, extra = 200, str(out), {}
264
+ payload = text.encode("utf-8")
265
+ self.send_response(status)
266
+ self.send_header("content-type", extra.get("content-type", "text/html; charset=utf-8"))
267
+ self.send_header("content-length", str(len(payload)))
268
+ for k, v in extra.items():
269
+ if k != "content-type":
270
+ self.send_header(k, str(v))
271
+ self.end_headers()
272
+ self.wfile.write(payload)
273
+
274
+ do_GET = do_POST = do_PUT = do_DELETE = _run
275
+
276
+ def log_message(self, *a): # quiet
277
+ pass
278
+
279
+ srv = HTTPServer(("127.0.0.1", int(port)), H)
280
+ print(f"myjs http.serve listening on http://127.0.0.1:{int(port)} (Ctrl-C to stop)")
281
+ try:
282
+ srv.serve_forever()
283
+ except KeyboardInterrupt:
284
+ print("\nstopped")
285
+ finally:
286
+ srv.server_close()
287
+
288
+ return serve
289
+
290
+
291
+ # --- WebSocket (RFC 6455 client, dependency-free) --------------------------
292
+
293
+ def _fire(handler, event):
294
+ if callable(handler):
295
+ handler(event)
296
+
297
+
298
+ class WebSocketClient:
299
+ CONNECTING, OPEN, CLOSING, CLOSED = 0, 1, 2, 3
300
+
301
+ def __init__(self, url, loop, protocols=None):
302
+ self.url = url
303
+ self.protocol = ""
304
+ self.readyState = self.CONNECTING
305
+ self.onopen = self.onmessage = self.onclose = self.onerror = None
306
+ self._loop = loop
307
+ self._sock = None
308
+ self._buf = b""
309
+ loop.io_start()
310
+ threading.Thread(target=self._run, daemon=True).start()
311
+
312
+ # -- lifecycle
313
+ def _run(self):
314
+ try:
315
+ self._connect()
316
+ self.readyState = self.OPEN
317
+ self._loop.post(lambda: _fire(self.onopen, JSObject({"type": "open"})))
318
+ self._read_loop()
319
+ except Exception as exc: # noqa: BLE001
320
+ self._loop.post(lambda exc=exc: _fire(self.onerror, JSObject({"type": "error", "message": str(exc)})))
321
+ finally:
322
+ self.readyState = self.CLOSED
323
+ try:
324
+ self._sock.close()
325
+ except Exception:
326
+ pass
327
+ self._loop.io_finish(lambda: _fire(self.onclose, JSObject({"type": "close"})))
328
+
329
+ def _connect(self):
330
+ u = urllib.parse.urlparse(self.url)
331
+ port = u.port or (443 if u.scheme == "wss" else 80)
332
+ raw = _socket.create_connection((u.hostname, port), timeout=10)
333
+ if u.scheme == "wss":
334
+ raw = _ssl.create_default_context().wrap_socket(raw, server_hostname=u.hostname)
335
+ key = _base64.b64encode(os.urandom(16)).decode()
336
+ path = (u.path or "/") + (f"?{u.query}" if u.query else "")
337
+ raw.sendall((
338
+ f"GET {path} HTTP/1.1\r\nHost: {u.hostname}\r\nUpgrade: websocket\r\n"
339
+ f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
340
+ ).encode())
341
+ resp = b""
342
+ while b"\r\n\r\n" not in resp:
343
+ chunk = raw.recv(4096)
344
+ if not chunk:
345
+ raise ConnectionError("connection closed during handshake")
346
+ resp += chunk
347
+ if b" 101 " not in resp.split(b"\r\n", 1)[0]:
348
+ raise RuntimeError(f"handshake rejected: {resp.splitlines()[0].decode('latin1')}")
349
+ self._sock = raw
350
+ self._buf = resp.split(b"\r\n\r\n", 1)[1]
351
+
352
+ # -- framing
353
+ def _recv(self, n):
354
+ while len(self._buf) < n:
355
+ chunk = self._sock.recv(65536)
356
+ if not chunk:
357
+ raise ConnectionError("closed")
358
+ self._buf += chunk
359
+ out, self._buf = self._buf[:n], self._buf[n:]
360
+ return out
361
+
362
+ def _read_loop(self):
363
+ while self.readyState == self.OPEN:
364
+ b1, b2 = self._recv(2)
365
+ opcode = b1 & 0x0F
366
+ length = b2 & 0x7F
367
+ if length == 126:
368
+ length = int.from_bytes(self._recv(2), "big")
369
+ elif length == 127:
370
+ length = int.from_bytes(self._recv(8), "big")
371
+ payload = self._recv(length)
372
+ if b2 & 0x80:
373
+ mask = payload[:4]
374
+ payload = bytes(c ^ mask[i % 4] for i, c in enumerate(payload[4:]))
375
+ if opcode == 0x8:
376
+ return
377
+ if opcode == 0x9:
378
+ self._frame(0xA, payload)
379
+ continue
380
+ if opcode in (0x1, 0x2):
381
+ data = payload.decode("utf-8", "replace") if opcode == 0x1 else payload
382
+ self._loop.post(lambda d=data: _fire(self.onmessage, JSObject({"type": "message", "data": d})))
383
+
384
+ def _frame(self, opcode, data):
385
+ if isinstance(data, str):
386
+ data = data.encode("utf-8")
387
+ mask = os.urandom(4)
388
+ body = bytes(c ^ mask[i % 4] for i, c in enumerate(data))
389
+ header = bytes([0x80 | opcode])
390
+ n = len(data)
391
+ if n < 126:
392
+ header += bytes([0x80 | n])
393
+ elif n < 65536:
394
+ header += bytes([0x80 | 126]) + n.to_bytes(2, "big")
395
+ else:
396
+ header += bytes([0x80 | 127]) + n.to_bytes(8, "big")
397
+ self._sock.sendall(header + mask + body)
398
+
399
+ # -- JS surface
400
+ def send(self, data):
401
+ if self.readyState != self.OPEN:
402
+ raise RuntimeError("WebSocket is not open")
403
+ self._frame(0x1 if isinstance(data, str) else 0x2, data)
404
+
405
+ def close(self, code=1000, reason=""):
406
+ if self.readyState == self.OPEN:
407
+ self.readyState = self.CLOSING
408
+ try:
409
+ self._frame(0x8, int(code).to_bytes(2, "big"))
410
+ except Exception:
411
+ pass
412
+
413
+ def addEventListener(self, event, fn):
414
+ setattr(self, "on" + event, fn)
415
+
416
+ def removeEventListener(self, event, fn=None):
417
+ setattr(self, "on" + event, None)
418
+
419
+
420
+ def websocket_ctor(loop):
421
+ def WebSocket(url, protocols=None, *_):
422
+ return WebSocketClient(url, loop, protocols)
423
+ WebSocket.CONNECTING = 0
424
+ WebSocket.OPEN = 1
425
+ WebSocket.CLOSING = 2
426
+ WebSocket.CLOSED = 3
427
+ return WebSocket
428
+
429
+
430
+ # --- py: the whole Python ecosystem ------------------------------------------
431
+
432
+ def _to_js(v):
433
+ if isinstance(v, dict):
434
+ return JSObject({k: _to_js(val) for k, val in v.items()})
435
+ if isinstance(v, (list, tuple)):
436
+ return JSArray(_to_js(x) for x in v)
437
+ return v
438
+
439
+
440
+ PY = {
441
+ "import": importlib.import_module,
442
+ "eval": lambda expr: eval(expr, {}), # noqa: S307 - explicit host feature
443
+ "list": lambda it: JSArray(it),
444
+ "dict": lambda o: JSObject(dict(o)),
445
+ "tuple": lambda it: tuple(it),
446
+ "repr": repr,
447
+ "str": str,
448
+ "int": int,
449
+ "float": float,
450
+ "len": len,
451
+ "dir": lambda o: JSArray(dir(o)),
452
+ "type": lambda o: type(o).__name__,
453
+ "toJS": _to_js,
454
+ }
455
+
456
+
457
+ # --- os (Node-flavoured) -------------------------------------------------
458
+
459
+ def _totalmem():
460
+ try:
461
+ return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
462
+ except (ValueError, AttributeError, OSError):
463
+ return None
464
+
465
+
466
+ def _cpus():
467
+ n = os.cpu_count() or 1
468
+ model = platform.processor() or platform.machine()
469
+ if sys.platform == "darwin":
470
+ try:
471
+ model = subprocess.run(
472
+ ["sysctl", "-n", "machdep.cpu.brand_string"],
473
+ capture_output=True, text=True, check=True,
474
+ ).stdout.strip() or model
475
+ except Exception:
476
+ pass
477
+ return JSArray(JSObject({"model": model}) for _ in range(n))
478
+
479
+
480
+ OS = {
481
+ "platform": lambda: sys.platform if sys.platform != "win32" else "win32",
482
+ "arch": lambda: {"AMD64": "x64", "x86_64": "x64"}.get(platform.machine(), platform.machine()),
483
+ "type": platform.system,
484
+ "release": platform.release,
485
+ "version": lambda: platform.version(),
486
+ "hostname": platform.node,
487
+ "homedir": lambda: os.path.expanduser("~"),
488
+ "tmpdir": lambda: __import__("tempfile").gettempdir(),
489
+ "cpus": _cpus,
490
+ "totalmem": _totalmem,
491
+ "uptime": lambda: _time.clock_gettime(_time.CLOCK_MONOTONIC) if hasattr(_time, "CLOCK_MONOTONIC") else None,
492
+ "userInfo": lambda *_: JSObject({
493
+ "username": __import__("getpass").getuser(),
494
+ "homedir": os.path.expanduser("~"),
495
+ "shell": os.environ.get("SHELL"),
496
+ }),
497
+ "EOL": os.linesep,
498
+ "python": platform.python_version(),
499
+ # convenience shorthands (not Node, but handy in a REPL)
500
+ "system": platform.system,
501
+ "name": platform.system(),
502
+ }
503
+
504
+
505
+ # --- process --------------------------------------------------------------
506
+
507
+ def _process(myjs_version):
508
+ return JSObject({
509
+ "argv": JSArray(sys.argv[1:]),
510
+ "env": JSObject(dict(os.environ)),
511
+ "platform": sys.platform,
512
+ "arch": platform.machine(),
513
+ "pid": os.getpid(),
514
+ "version": f"myjs/{myjs_version}",
515
+ "cwd": os.getcwd,
516
+ "chdir": os.chdir,
517
+ "exit": lambda code=0: sys.exit(int(code)),
518
+ "hrtime": lambda *_: _time.perf_counter_ns(),
519
+ })
520
+
521
+
522
+ # --- assembly ------------------------------------------------------------
523
+
524
+ def scope(myjs_version="0.0.1"):
525
+ fs = dict(FS)
526
+ http = dict(HTTP)
527
+ http["serve"] = _make_serve()
528
+ os_ns = dict(OS)
529
+ require_table = {
530
+ "fs": fs, "path": dict(PATH), "http": http, "https": http, "os": os_ns,
531
+ "child_process": {"exec": _sh, "execSync": lambda c, o=None: _sh(c, o)["stdout"]},
532
+ "crypto": {
533
+ "md5": lambda s: _hashlib.md5(str(s).encode()).hexdigest(),
534
+ "sha256": lambda s: _hashlib.sha256(str(s).encode()).hexdigest(),
535
+ "randomUUID": lambda: __import__("uuid").uuid4().hex,
536
+ },
537
+ }
538
+
539
+ def require(name):
540
+ if name.startswith("node:"):
541
+ name = name[5:]
542
+ if name in require_table and require_table[name] is not None:
543
+ return require_table[name]
544
+ return importlib.import_module(name) # fall through to any Python module
545
+
546
+ return {
547
+ "fs": fs,
548
+ "path": dict(PATH),
549
+ "http": http,
550
+ "os": os_ns,
551
+ "sh": _sh,
552
+ "say": _say,
553
+ "notify": _notify,
554
+ "alert": _alert,
555
+ "open": _open,
556
+ "fetchSync": _request, # `fetch` (async) is installed per-session by the engine
557
+ "process": _process(myjs_version),
558
+ "py": dict(PY),
559
+ "require": require,
560
+ "atob": lambda s: _base64.b64decode(s).decode("utf-8", "replace"),
561
+ "btoa": lambda s: _base64.b64encode(str(s).encode("utf-8")).decode("ascii"),
562
+ "sleep": _time.sleep,
563
+ "now": lambda: _time.time() * 1000,
564
+ "hash": {
565
+ "md5": lambda s: _hashlib.md5(str(s).encode()).hexdigest(),
566
+ "sha256": lambda s: _hashlib.sha256(str(s).encode()).hexdigest(),
567
+ },
568
+ }
myjs/repl.py ADDED
@@ -0,0 +1,90 @@
1
+ """The ``myjs`` interactive REPL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from . import __version__
8
+ from ._engine import JSError, PrintConsole, Session
9
+
10
+
11
+ def _incomplete(src: str) -> bool:
12
+ """True if ``src`` has unclosed brackets / strings / block comments -- a
13
+ cheap heuristic so the REPL keeps reading multi-line input."""
14
+ depth = 0
15
+ i, n = 0, len(src)
16
+ quote = None
17
+ while i < n:
18
+ c = src[i]
19
+ if quote:
20
+ if c == "\\":
21
+ i += 2
22
+ continue
23
+ if c == quote:
24
+ quote = None
25
+ elif c in "\"'`":
26
+ quote = c
27
+ elif c == "/" and i + 1 < n and src[i + 1] == "/":
28
+ nl = src.find("\n", i)
29
+ if nl == -1:
30
+ return False
31
+ i = nl
32
+ elif c == "/" and i + 1 < n and src[i + 1] == "*":
33
+ end = src.find("*/", i + 2)
34
+ if end == -1:
35
+ return True
36
+ i = end + 1
37
+ elif c in "([{":
38
+ depth += 1
39
+ elif c in ")]}":
40
+ depth -= 1
41
+ i += 1
42
+ return depth > 0 or quote in ('`',)
43
+
44
+
45
+ def repl(session: Session | None = None) -> int:
46
+ session = session or Session(console=PrintConsole())
47
+ print(f"myjs {__version__} - JavaScript on domonic. Ctrl-D or .exit to quit, .help for help.")
48
+ buffer = ""
49
+ while True:
50
+ prompt = "... " if buffer else "js> "
51
+ try:
52
+ line = input(prompt)
53
+ except EOFError:
54
+ print()
55
+ return 0
56
+ except KeyboardInterrupt:
57
+ print("^C")
58
+ buffer = ""
59
+ continue
60
+
61
+ if not buffer and line.strip() in (".exit", ".quit"):
62
+ return 0
63
+ if not buffer and line.strip() == ".help":
64
+ print(" .exit quit\n .clear reset the session\n .help this message\n"
65
+ " _ the last result\n ffi, os native bindings; window/document the DOM")
66
+ continue
67
+ if not buffer and line.strip() == ".clear":
68
+ session = Session(console=PrintConsole())
69
+ print("(session cleared)")
70
+ continue
71
+
72
+ buffer = f"{buffer}\n{line}" if buffer else line
73
+ if _incomplete(buffer):
74
+ continue
75
+
76
+ src, buffer = buffer, ""
77
+ if not src.strip():
78
+ continue
79
+ try:
80
+ result = session.eval(src)
81
+ except JSError as exc:
82
+ print(exc, file=sys.stderr)
83
+ continue
84
+ except RecursionError:
85
+ print("RangeError: Maximum call stack size exceeded", file=sys.stderr)
86
+ continue
87
+ if result is not None:
88
+ session.interp.global_env.declare("_", result)
89
+ print(session.display(result))
90
+ return 0
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: myjs
3
+ Version: 0.0.2
4
+ Summary: A JavaScript interpreter for Python -- run .js against a DOM, call native C via ffi, reach the Python ecosystem
5
+ Author-email: byteface <byteface@googlemail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/byteface/domonic-libs/tree/master/src/myjs
8
+ Project-URL: Source, https://github.com/byteface/domonic-libs
9
+ Project-URL: Documentation, https://github.com/byteface/domonic-libs/blob/master/docs/myjs.md
10
+ Project-URL: Tracker, https://github.com/byteface/domonic-libs/issues
11
+ Keywords: javascript,interpreter,ecmascript,acorn,ffi,ctypes,dom,repl,domonic
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: JavaScript
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Interpreters
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: domonic-libs>=0.0.2
28
+ Provides-Extra: gui
29
+ Requires-Dist: pywebview>=5.0; extra == "gui"
30
+ Dynamic: license-file
31
+
32
+ # myjs
33
+
34
+ A JavaScript interpreter for Python. It runs a practical subset of ECMAScript against a server-side DOM, hands scripts an `ffi` bridge to native C libraries, and lets JavaScript reach the entire Python ecosystem.
35
+
36
+ ```bash
37
+ pip install myjs
38
+ ```
39
+
40
+ ```bash
41
+ myjs # start a REPL
42
+ myjs script.js # execute a file
43
+ myjs -e "6 * 7" # evaluate a snippet
44
+ myjs --gui app.js # run, then show the DOM the script built in a native window
45
+ ```
46
+
47
+ ```python
48
+ import myjs
49
+
50
+ myjs.eval("1 + 2 * 3") # -> 7
51
+ myjs.run("script.js")
52
+
53
+ s = myjs.Session() # an isolated global scope
54
+ s.eval("const x = 21;")
55
+ s.eval("x * 2") # -> 42
56
+ ```
57
+
58
+ ## What a script can reach
59
+
60
+ - **The DOM** — `document`, `window`, and ~190 constructors (`URL`, `Headers`, `Event`, `DOMRect`, …). `document.createElement(...).appendChild(...)` builds a real object tree.
61
+ - **An event loop** — `Promise` (with `all` / `allSettled` / `race` / `any`), `async` / `await`, `setTimeout` / `setInterval`, `queueMicrotask`. Microtasks run before timers.
62
+ - **ES modules** — `import` / `export` from `.js` files; a bare specifier resolves to a Python module.
63
+ - **`ffi`** — call native C libraries through `ctypes`, no compiler:
64
+
65
+ ```javascript
66
+ const libc = ffi.loadLibrary("c");
67
+ libc.abs.argtypes = [ffi.types.int];
68
+ libc.abs.restype = ffi.types.int;
69
+ libc.abs(-42); // 42
70
+ ```
71
+
72
+ - **Host bindings** — `fs`, `path`, `sh` (shell), `http` + an async `fetch`, `os` (Node-flavoured), `process`, `WebSocket` (a real RFC 6455 client), `say` / `notify` / `open`, `hash`, `atob` / `btoa`.
73
+ - **`py`** — `py.import("numpy")`, `py.eval(...)`, `py.list(iter)` → the whole Python package index from JavaScript.
74
+
75
+ The language layer passes a curated test262-style battery: closures, classes (extends / super / fields / getters-setters / private / static), destructuring, generators, labelled break, `async` / `await`, and the `Array` / `String` / `Object` / `Number` / `Math` / `JSON` / `RegExp` built-ins. Not covered: real prototype chains, `Proxy` / `Symbol`, `with`.
76
+
77
+ ## Language embedding
78
+
79
+ ```python
80
+ s = myjs.Session(scope={"answer": 42})
81
+ s.eval("answer * 2") # -> 84
82
+ ```
83
+
84
+ A JavaScript exception that escapes becomes `myjs.JSError` with `js_name`, `js_line`, and `js_trace` (the call stack).
85
+
86
+ ## How it is built
87
+
88
+ `myjs` is the runtime layer on top of the [`domonic-libs`](https://pypi.org/project/domonic-libs/) `acorn` port — a faithful Python port of the [acorn](https://github.com/acornjs/acorn) parser plus a tree-walking evaluator — which in turn runs on [`domonic`](https://pypi.org/project/domonic/)'s DOM. Full reference: [docs/myjs.md](https://github.com/byteface/domonic-libs/blob/master/docs/myjs.md).
89
+
90
+ MIT licensed.
@@ -0,0 +1,13 @@
1
+ myjs/__init__.py,sha256=nie5sW_zre7MxT0V3_V4h8ajGsqKQN_abdWlPzjFSfY,1866
2
+ myjs/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ myjs/_engine.py,sha256=HejwkF_qXZFI7TEHr6LxNY9-zTeO-1Ks0y4XDzKEL6A,4032
4
+ myjs/cli.py,sha256=4w9aDliQC9Q2ggBIY9mrEcfMf8RYV9UTzKcF-bpj1V8,2798
5
+ myjs/ffi.py,sha256=k1aO7Ffxci1PSqutWw_tlqC4Xig7QtwOhzUVkjHHX64,7613
6
+ myjs/host.py,sha256=1SnluwQjIzzqMtpRdmcDGD_vyuKckL67eZlN6M2RbQo,18915
7
+ myjs/repl.py,sha256=XELA1ilpPTov4E2pyXgevZvCCCjt_FhQdtpLmbAgMsc,2761
8
+ myjs-0.0.2.dist-info/licenses/LICENSE,sha256=QAxrfKvz2-qZ3wES7ocFTmPJDj74dyL-5uxRjiDhZn4,1065
9
+ myjs-0.0.2.dist-info/METADATA,sha256=r3MvNDFNk3EIqgyd7F7jMv0iCftdn5AA-XxT_CbWm90,4210
10
+ myjs-0.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ myjs-0.0.2.dist-info/entry_points.txt,sha256=erJBPK-zEJBVlNW2u_tmAiQJCJHpXVhBDERgnIO4pAM,39
12
+ myjs-0.0.2.dist-info/top_level.txt,sha256=bfUZ6eSxZ2MUr9ybXAUbnPAWL0IGnZkujU5ToVClU3U,5
13
+ myjs-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ myjs = myjs.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 byteface
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ myjs