diff --git a/.github/workflows/pull-request-tests.yml b/.github/workflows/pull-request-tests.yml index eb34679b8..4c2d551a0 100644 --- a/.github/workflows/pull-request-tests.yml +++ b/.github/workflows/pull-request-tests.yml @@ -400,6 +400,16 @@ jobs: with: python-version: '3.12' + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - uses: jiro4989/setup-nim-action@v1 + with: + nim-version: 2.2.4 + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install redis run: sudo apt-get -y update && sudo apt-get install -y redis-server diff --git a/Dockerfile b/Dockerfile index 2e109cec6..3a8faa50a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -136,6 +136,14 @@ RUN find /app/frameos -path '*/tests' -type d -prune -exec rm -rf {} + \ /app/frameos/agent/build \ /app/frameos/agent/tmp +WORKDIR /app/frameos +RUN nim c \ + --nimCache:/tmp/frameos-native-js-transpile-nimcache \ + --out:/app/frameos/build/native_js_transpile \ + tools/native_js_transpile.nim \ + && test -x /app/frameos/build/native_js_transpile \ + && rm -rf /tmp/frameos-native-js-transpile-nimcache + FROM ${PYTHON_IMAGE} AS python-deps ENV DEBIAN_FRONTEND=noninteractive @@ -163,6 +171,7 @@ ENV DEBIAN_FRONTEND=noninteractive ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV VIRTUAL_ENV=/app/backend/.venv +ENV FRAMEOS_NATIVE_JS_TRANSPILE=/app/frameos/build/native_js_transpile ENV PATH="/opt/nim/bin:${VIRTUAL_ENV}/bin:${PATH}" WORKDIR /app diff --git a/backend/app/utils/js_apps.py b/backend/app/utils/js_apps.py index 0fe6aa6cd..af899f739 100644 --- a/backend/app/utils/js_apps.py +++ b/backend/app/utils/js_apps.py @@ -5,9 +5,11 @@ import shutil import subprocess import tempfile +import threading from pathlib import Path JS_APP_SOURCE_FILES = ("app.ts", "app.js", "app.tsx", "app.jsx") +_NATIVE_TRANSPILER_LOCK = threading.Lock() def find_js_app_source_key(sources: dict | None) -> str | None: @@ -27,50 +29,6 @@ def find_js_app_source_filename(app_dir: str) -> str | None: return None -def _node_sucrase_script() -> str: - return """ -import fs from 'node:fs'; - -const filename = process.argv[1]; -const source = fs.readFileSync(process.argv[2], 'utf8'); -const vendorPath = process.argv[3]; - -async function transpile() { - if (vendorPath && fs.existsSync(vendorPath)) { - globalThis.eval(fs.readFileSync(vendorPath, 'utf8')); - return globalThis.__frameosTranspile(source, { filePath: filename }); - } - - const { transform } = await import('sucrase'); - return transform(source, { - filePath: filename, - transforms: ['typescript', 'jsx'], - jsxRuntime: 'classic', - jsxPragma: '__frameosJsx', - jsxFragmentPragma: '__frameosFragment', - production: true, - }).code; -} - -try { - await transpile(); - process.stdout.write(JSON.stringify({ ok: true })); -} catch (error) { - process.stderr.write(JSON.stringify({ - ok: false, - errors: [{ - text: String(error?.message || error || 'Unknown JavaScript error'), - location: { - line: Number(error?.loc?.line || 1), - column: Number(error?.loc?.column || 1), - }, - }], - })); - process.exit(1); -} -""" - - def _json_payload_from_process(proc: subprocess.CompletedProcess[str], fallback: str) -> tuple[bool, dict]: output = proc.stdout.strip() or proc.stderr.strip() if not output: @@ -82,64 +40,260 @@ def _json_payload_from_process(proc: subprocess.CompletedProcess[str], fallback: return proc.returncode == 0, payload -def _quickjs_binary(repo_root: Path) -> str | None: - candidates = [ - repo_root / "frameos" / "quickjs" / "qjs", - Path("/app/frameos/quickjs/qjs"), +def _native_transpiler_sources(frameos_root: Path) -> list[Path]: + return [ + frameos_root / "tools" / "native_js_transpile.nim", + *(frameos_root / "src" / "frameos" / "js_runtime").glob("*.nim"), ] - for candidate in candidates: - if candidate.exists() and os.access(candidate, os.X_OK): - return str(candidate) - return shutil.which("qjs") -def _run_quickjs_sucrase(filename: str, source_path: str, repo_root: Path, vendor_path: Path) -> tuple[bool, dict] | None: - qjs = _quickjs_binary(repo_root) - if not qjs or not vendor_path.exists(): - return None +def _native_transpiler_bin(frameos_root: Path) -> Path: + suffix = ".exe" if os.name == "nt" else "" + return frameos_root / "build" / f"native_js_transpile{suffix}" - script_path = Path(__file__).resolve().with_name("js_validate_quickjs.js") - proc = subprocess.run( - [qjs, "--std", str(script_path), filename, source_path, str(vendor_path)], - cwd=repo_root, - capture_output=True, - text=True, - check=False, - ) - ok, payload = _json_payload_from_process( - proc, - '{"ok": false, "errors": [{"text": "quickjs sucrase validation failed"}]}', + +def _native_transpiler_is_current(binary: Path, frameos_root: Path) -> bool: + if not binary.exists(): + return False + binary_mtime = binary.stat().st_mtime + return all( + path.exists() and path.stat().st_mtime <= binary_mtime + for path in _native_transpiler_sources(frameos_root) ) - if ok or payload.get("errors"): - return ok, payload - return None -def _run_node_sucrase(filename: str, source_path: str, repo_root: Path, vendor_path: Path) -> tuple[bool, dict]: +def _ensure_native_transpiler(repo_root: Path) -> tuple[Path | None, dict | None]: + override = os.environ.get("FRAMEOS_NATIVE_JS_TRANSPILE") + if override: + binary = Path(override) + if binary.exists(): + return binary, None + return None, { + "ok": False, + "errors": [ + { + "text": f"FRAMEOS_NATIVE_JS_TRANSPILE does not exist: {override}", + "location": {"line": 1, "column": 1}, + } + ], + } + + frameos_root = repo_root / "frameos" + binary = _native_transpiler_bin(frameos_root) + if _native_transpiler_is_current(binary, frameos_root): + return binary, None + + nim = shutil.which("nim") + if not nim: + return None, { + "ok": False, + "errors": [ + { + "text": "JavaScript validation requires Nim to build the FrameOS native transpiler", + "location": {"line": 1, "column": 1}, + } + ], + } + + with _NATIVE_TRANSPILER_LOCK: + if _native_transpiler_is_current(binary, frameos_root): + return binary, None + binary.parent.mkdir(parents=True, exist_ok=True) + proc = subprocess.run( + [ + nim, + "c", + "--nimCache:build/nimcache/native_js_transpile", + f"--out:build/{binary.name}", + "tools/native_js_transpile.nim", + ], + cwd=frameos_root, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + output = (proc.stderr or proc.stdout).strip() or "Failed to build FrameOS native JavaScript transpiler" + return None, { + "ok": False, + "errors": [{"text": output, "location": {"line": 1, "column": 1}}], + } + return binary, None + + +def _source_map_generated_position(source_map: dict | None, line: int, column: int) -> tuple[int, int]: + if not source_map: + return line, column + + mapped_line = 0 + generated_to_source_line = source_map.get("generatedToSourceLine") or [] + if 0 < line < len(generated_to_source_line): + try: + mapped_line = int(generated_to_source_line[line]) + except (TypeError, ValueError): + mapped_line = 0 + + mapped_column = max(1, column) + best_segment: dict | None = None + for segment in source_map.get("segments") or []: + try: + segment_line = int(segment.get("generatedLine", 0)) + segment_column = int(segment.get("generatedColumn", 0)) + except (TypeError, ValueError): + continue + if segment_line == line and segment_column <= column: + if best_segment is None or segment_column > int(best_segment.get("generatedColumn", 0)): + best_segment = segment + + if best_segment is not None: + try: + mapped_line = int(best_segment.get("sourceLine", mapped_line)) + source_column = int(best_segment.get("sourceColumn", 1)) + generated_column = int(best_segment.get("generatedColumn", 1)) + except (TypeError, ValueError): + return mapped_line or line, mapped_column + mapped_column = max(1, source_column + (column - generated_column)) + + return mapped_line or line, mapped_column + + +def _path_line_prefixes(path: str) -> tuple[str, ...]: + paths = [path, os.path.realpath(path)] + if path.startswith("/var/"): + paths.append("/private" + path) + return tuple(dict.fromkeys(path + ":" for path in paths)) + + +def _node_check_error_payload( + proc: subprocess.CompletedProcess[str], + source: str, + generated_path: str, + source_map: dict | None = None, +) -> dict: + output = (proc.stderr or proc.stdout).strip() + line = 1 + column = 1 + found_column = False + text = "Unknown JavaScript error" + source_lines = source.splitlines() or [""] + line_prefixes = _path_line_prefixes(generated_path) + + for raw_line in output.splitlines(): + if raw_line.startswith(line_prefixes): + try: + line = int(raw_line.rsplit(":", 1)[1]) + except ValueError: + line = 1 + elif raw_line.startswith("SyntaxError:"): + text = raw_line.removeprefix("SyntaxError:").strip() or raw_line + + output_lines = output.splitlines() + for index, raw_line in enumerate(output_lines): + if raw_line.startswith(line_prefixes) and index + 2 < len(output_lines): + caret_line = output_lines[index + 2] + caret_index = caret_line.find("^") + if caret_index >= 0: + column = caret_index + 1 + found_column = True + break + + source_line, source_column = _source_map_generated_position(source_map, line, column) + source_line = min(max(1, source_line), len(source_lines)) + if not found_column: + source_column = len(source_lines[source_line - 1]) + 1 + + return { + "ok": False, + "errors": [ + { + "text": text, + "location": {"line": source_line, "column": max(1, source_column)}, + } + ], + } + + +def _run_node_syntax_check(code: str, source: str, source_map: dict | None = None) -> tuple[bool, dict]: node = shutil.which("node") if not node: - return False, {"ok": False, "errors": [{"text": "JavaScript validation requires QuickJS or Node", "location": {"line": 1, "column": 0}}]} + return False, { + "ok": False, + "errors": [ + { + "text": "JavaScript validation requires Node", + "location": {"line": 1, "column": 1}, + } + ], + } + + tmp_path = "" + try: + with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as tmp: + tmp.write(code) + tmp_path = str(tmp.name) + proc = subprocess.run([node, "--check", tmp_path], capture_output=True, text=True, check=False) + if proc.returncode == 0: + return True, {"ok": True} + return False, _node_check_error_payload(proc, source, tmp_path, source_map) + finally: + if tmp_path and os.path.exists(tmp_path): + os.remove(tmp_path) + + +def _run_native_frameos_transpiler(filename: str, source_path: str, source: str, repo_root: Path) -> tuple[bool, dict]: + binary, error_payload = _ensure_native_transpiler(repo_root) + if error_payload is not None or binary is None: + return False, error_payload or { + "ok": False, + "errors": [ + { + "text": "FrameOS native JavaScript transpiler is unavailable", + "location": {"line": 1, "column": 1}, + } + ], + } proc = subprocess.run( - [node, "--input-type=module", "-e", _node_sucrase_script(), filename, source_path, str(vendor_path)], - cwd=repo_root, + [str(binary), "module-json", source_path], + cwd=repo_root / "frameos", capture_output=True, text=True, check=False, ) - return _json_payload_from_process( + ok, payload = _json_payload_from_process( proc, - '{"ok": false, "errors": [{"text": "sucrase failed"}]}', + json.dumps( + { + "ok": False, + "errors": [ + { + "text": f"Failed to transform {filename}", + "location": {"line": 1, "column": 1}, + } + ], + } + ), ) + if not ok or not payload.get("ok", ok): + return False, payload + + code = payload.get("code") + if not isinstance(code, str): + return False, { + "ok": False, + "errors": [ + { + "text": f"Failed to transform {filename}", + "location": {"line": 1, "column": 1}, + } + ], + } + return _run_node_syntax_check(code, source, payload.get("sourceMap")) -def _run_sucrase(filename: str, source_path: str) -> tuple[bool, dict]: +def _run_frameos_js_validation(filename: str, source_path: str, source: str) -> tuple[bool, dict]: repo_root = Path(__file__).resolve().parents[3] - vendor_path = repo_root / "frameos" / "assets" / "compiled" / "vendor" / "sucrase.js" - quickjs_result = _run_quickjs_sucrase(filename, source_path, repo_root, vendor_path) - if quickjs_result is not None: - return quickjs_result - return _run_node_sucrase(filename, source_path, repo_root, vendor_path) + return _run_native_frameos_transpiler(filename, source_path, source, repo_root) def validate_js_source(filename: str, source: str) -> list[dict]: @@ -149,7 +303,7 @@ def validate_js_source(filename: str, source: str) -> list[dict]: tmp.write(source) tmp_path = str(tmp.name) - ok, payload = _run_sucrase(filename, tmp_path) + ok, payload = _run_frameos_js_validation(filename, tmp_path, source) finally: if tmp_path and os.path.exists(tmp_path): os.remove(tmp_path) diff --git a/backend/app/utils/js_validate_quickjs.js b/backend/app/utils/js_validate_quickjs.js deleted file mode 100644 index 2f6a86bca..000000000 --- a/backend/app/utils/js_validate_quickjs.js +++ /dev/null @@ -1,41 +0,0 @@ -const args = scriptArgs.length >= 4 ? scriptArgs.slice(1) : scriptArgs -const filename = args[0] -const sourcePath = args[1] -const vendorPath = args[2] - -function writePayload(payload, exitCode) { - std.out.puts(JSON.stringify(payload)) - std.exit(exitCode) -} - -try { - const source = std.loadFile(sourcePath) - const vendor = std.loadFile(vendorPath) - - if (source === null) { - throw new Error(`Unable to read JavaScript source: ${sourcePath}`) - } - if (vendor === null) { - throw new Error(`Unable to read Sucrase vendor bundle: ${vendorPath}`) - } - - globalThis.eval(vendor) - globalThis.__frameosTranspile(source, { filePath: filename }) - writePayload({ ok: true }, 0) -} catch (error) { - writePayload( - { - ok: false, - errors: [ - { - text: String((error && error.message) || error || 'Unknown JavaScript error'), - location: { - line: Number((error && error.loc && error.loc.line) || 1), - column: Number((error && error.loc && error.loc.column) || 1), - }, - }, - ], - }, - 1 - ) -} diff --git a/backend/app/utils/tests/test_js_apps.py b/backend/app/utils/tests/test_js_apps.py index f3ed8224a..68c4d6961 100644 --- a/backend/app/utils/tests/test_js_apps.py +++ b/backend/app/utils/tests/test_js_apps.py @@ -11,10 +11,27 @@ def test_validate_js_source_accepts_typescript_jsx(): ) -def test_validate_js_source_reports_sucrase_location(): +def test_validate_js_source_reports_native_transform_location(): errors = validate_js_source("app.ts", "export function get(app: any) { return ") assert errors assert errors[0]["line"] == 1 assert errors[0]["column"] > 0 - assert "Unexpected token" in errors[0]["error"] + assert "Unexpected" in errors[0]["error"] + + +def test_validate_js_source_reports_multiline_node_check_location(): + source = """export function run(app: FrameOSApp, context: FrameOSContext): void { + const stateKey = app.config.stateKey || 'jsLogicResult' + + app.log('JS logic app ran', { event: context.eve +nt, stateKey }) +} +""" + + errors = validate_js_source("app.ts", source) + + assert errors + assert errors[0]["line"] == 5 + assert errors[0]["column"] == 1 + assert "Unexpected identifier 'nt'" in errors[0]["error"] diff --git a/frameos/assets/compiled/vendor/sucrase.js b/frameos/assets/compiled/vendor/sucrase.js deleted file mode 100644 index 8435d283d..000000000 --- a/frameos/assets/compiled/vendor/sucrase.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict";var __frameosSucraseBundle=(()=>{var na=Object.create;var po=Object.defineProperty;var sa=Object.getOwnPropertyDescriptor;var ra=Object.getOwnPropertyNames;var oa=Object.getPrototypeOf,ia=Object.prototype.hasOwnProperty;var kt=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports);var aa=(e,n,s,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of ra(n))!ia.call(e,i)&&i!==s&&po(e,i,{get:()=>n[i],enumerable:!(o=sa(n,i))||o.enumerable});return e};var en=(e,n,s)=>(s=e!=null?na(oa(e)):{},aa(n||!e||!e.__esModule?po(s,"default",{value:e,enumerable:!0}):s,e));var No=kt((fr,hr)=>{(function(e,n){typeof fr=="object"&&typeof hr<"u"?hr.exports=n():typeof define=="function"&&define.amd?define(n):(e=typeof globalThis<"u"?globalThis:e||self,e.resolveURI=n())})(fr,function(){"use strict";let e=/^[\w+.-]+:\/\//,n=/^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/,s=/^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;function o(b){return e.test(b)}function i(b){return b.startsWith("//")}function c(b){return b.startsWith("/")}function u(b){return b.startsWith("file:")}function d(b){return/^[.?#]/.test(b)}function x(b){let R=n.exec(b);return _(R[1],R[2]||"",R[3],R[4]||"",R[5]||"/",R[6]||"",R[7]||"")}function g(b){let R=s.exec(b),L=R[2];return _("file:","",R[1]||"","",c(L)?L:"/"+L,R[3]||"",R[4]||"")}function _(b,R,L,z,V,W,te){return{scheme:b,user:R,host:L,port:z,path:V,query:W,hash:te,type:7}}function w(b){if(i(b)){let L=x("http:"+b);return L.scheme="",L.type=6,L}if(c(b)){let L=x("http://foo.com"+b);return L.scheme="",L.host="",L.type=5,L}if(u(b))return g(b);if(o(b))return x(b);let R=x("http://foo.com/"+b);return R.scheme="",R.host="",R.type=b?b.startsWith("?")?3:b.startsWith("#")?2:4:1,R}function S(b){if(b.endsWith("/.."))return b;let R=b.lastIndexOf("/");return b.slice(0,R+1)}function v(b,R){j(R,R.type),b.path==="/"?b.path=R.path:b.path=S(R.path)+b.path}function j(b,R){let L=R<=4,z=b.path.split("/"),V=1,W=0,te=!1;for(let oe=1;oez&&(z=te)}j(L,z);let V=L.query+L.hash;switch(z){case 2:case 3:return V;case 4:{let W=L.path.slice(1);return W?d(R||b)&&!d(W)?"./"+W+V:W+V:V||"."}case 5:return L.path+V;default:return L.scheme+"//"+L.user+L.host+L.port+L.path+V}}return U})});var fn=kt(Te=>{"use strict";var Qa=Te&&Te.__extends||function(){var e=function(n,s){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,i){o.__proto__=i}||function(o,i){for(var c in i)i.hasOwnProperty(c)&&(o[c]=i[c])},e(n,s)};return function(n,s){e(n,s);function o(){this.constructor=n}n.prototype=s===null?Object.create(s):(o.prototype=s.prototype,new o)}}();Object.defineProperty(Te,"__esModule",{value:!0});Te.DetailContext=Te.NoopContext=Te.VError=void 0;var Mo=function(e){Qa(n,e);function n(s,o){var i=e.call(this,o)||this;return i.path=s,Object.setPrototypeOf(i,n.prototype),i}return n}(Error);Te.VError=Mo;var Za=function(){function e(){}return e.prototype.fail=function(n,s,o){return!1},e.prototype.unionResolver=function(){return this},e.prototype.createContext=function(){return this},e.prototype.resolveUnion=function(n){},e}();Te.NoopContext=Za;var Bo=function(){function e(){this._propNames=[""],this._messages=[null],this._score=0}return e.prototype.fail=function(n,s,o){return this._propNames.push(n),this._messages.push(s),this._score+=o,!1},e.prototype.unionResolver=function(){return new ec},e.prototype.resolveUnion=function(n){for(var s,o,i=n,c=null,u=0,d=i.contexts;u=c._score)&&(c=x)}c&&c._score>0&&((s=this._propNames).push.apply(s,c._propNames),(o=this._messages).push.apply(o,c._messages))},e.prototype.getError=function(n){for(var s=[],o=this._propNames.length-1;o>=0;o--){var i=this._propNames[o];n+=typeof i=="number"?"["+i+"]":i?"."+i:"";var c=this._messages[o];c&&s.push(n+" "+c)}return new Mo(n,s.join("; "))},e.prototype.getErrorDetail=function(n){for(var s=[],o=this._propNames.length-1;o>=0;o--){var i=this._propNames[o];n+=typeof i=="number"?"["+i+"]":i?"."+i:"";var c=this._messages[o];c&&s.push({path:n,message:c})}for(var u=null,o=s.length-1;o>=0;o--)u&&(s[o].nested=[u]),u=s[o];return u},e}();Te.DetailContext=Bo;var ec=function(){function e(){this.contexts=[]}return e.prototype.createContext=function(){var n=new Bo;return this.contexts.push(n),n},e}()});var wr=kt(T=>{"use strict";var ue=T&&T.__extends||function(){var e=function(n,s){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,i){o.__proto__=i}||function(o,i){for(var c in i)i.hasOwnProperty(c)&&(o[c]=i[c])},e(n,s)};return function(n,s){e(n,s);function o(){this.constructor=n}n.prototype=s===null?Object.create(s):(o.prototype=s.prototype,new o)}}();Object.defineProperty(T,"__esModule",{value:!0});T.basicTypes=T.BasicType=T.TParamList=T.TParam=T.param=T.TFunc=T.func=T.TProp=T.TOptional=T.opt=T.TIface=T.iface=T.TEnumLiteral=T.enumlit=T.TEnumType=T.enumtype=T.TIntersection=T.intersection=T.TUnion=T.union=T.TTuple=T.tuple=T.TArray=T.array=T.TLiteral=T.lit=T.TName=T.name=T.TType=void 0;var Uo=fn(),ae=function(){function e(){}return e}();T.TType=ae;function Re(e){return typeof e=="string"?Ho(e):e}function _r(e,n){var s=e[n];if(!s)throw new Error("Unknown type "+n);return s}function Ho(e){return new yr(e)}T.name=Ho;var yr=function(e){ue(n,e);function n(s){var o=e.call(this)||this;return o.name=s,o._failMsg="is not a "+s,o}return n.prototype.getChecker=function(s,o,i){var c=this,u=_r(s,this.name),d=u.getChecker(s,o,i);return u instanceof ne||u instanceof n?d:function(x,g){return d(x,g)?!0:g.fail(null,c._failMsg,0)}},n}(ae);T.TName=yr;function tc(e){return new Ir(e)}T.lit=tc;var Ir=function(e){ue(n,e);function n(s){var o=e.call(this)||this;return o.value=s,o.name=JSON.stringify(s),o._failMsg="is not "+o.name,o}return n.prototype.getChecker=function(s,o){var i=this;return function(c,u){return c===i.value?!0:u.fail(null,i._failMsg,-1)}},n}(ae);T.TLiteral=Ir;function nc(e){return new Vo(Re(e))}T.array=nc;var Vo=function(e){ue(n,e);function n(s){var o=e.call(this)||this;return o.ttype=s,o}return n.prototype.getChecker=function(s,o){var i=this.ttype.getChecker(s,o);return function(c,u){if(!Array.isArray(c))return u.fail(null,"is not an array",0);for(var d=0;d0&&i.push(c+" more"),o._failMsg="is none of "+i.join(", ")):o._failMsg="is none of "+c+" types",o}return n.prototype.getChecker=function(s,o){var i=this,c=this.ttypes.map(function(u){return u.getChecker(s,o)});return function(u,d){for(var x=d.unionResolver(),g=0;g{"use strict";var kc=F&&F.__spreadArrays||function(){for(var e=0,n=0,s=arguments.length;n{"use strict";Zt.__esModule=!0;Zt.LinesAndColumns=void 0;var Mn=` -`,Oi="\r",Fi=function(){function e(n){this.string=n;for(var s=[0],o=0;othis.string.length)return null;for(var s=0,o=this.offsets;o[s+1]<=n;)s++;var i=n-o[s];return{line:s,column:i}},e.prototype.indexForLocation=function(n){var s=n.line,o=n.column;return s<0||s>=this.offsets.length||o<0||o>this.lengthOfLine(s)?null:this.offsets[s]+o},e.prototype.lengthOfLine=function(n){var s=this.offsets[n],o=n===this.offsets.length-1?this.string.length:this.offsets[n+1];return o-s},e}();Zt.LinesAndColumns=Fi;Zt.default=Fi});var l;(function(e){e[e.NONE=0]="NONE";let s=1;e[e._abstract=s]="_abstract";let o=s+1;e[e._accessor=o]="_accessor";let i=o+1;e[e._as=i]="_as";let c=i+1;e[e._assert=c]="_assert";let u=c+1;e[e._asserts=u]="_asserts";let d=u+1;e[e._async=d]="_async";let x=d+1;e[e._await=x]="_await";let g=x+1;e[e._checks=g]="_checks";let _=g+1;e[e._constructor=_]="_constructor";let w=_+1;e[e._declare=w]="_declare";let S=w+1;e[e._enum=S]="_enum";let v=S+1;e[e._exports=v]="_exports";let j=v+1;e[e._from=j]="_from";let U=j+1;e[e._get=U]="_get";let b=U+1;e[e._global=b]="_global";let R=b+1;e[e._implements=R]="_implements";let L=R+1;e[e._infer=L]="_infer";let z=L+1;e[e._interface=z]="_interface";let V=z+1;e[e._is=V]="_is";let W=V+1;e[e._keyof=W]="_keyof";let te=W+1;e[e._mixins=te]="_mixins";let fe=te+1;e[e._module=fe]="_module";let oe=fe+1;e[e._namespace=oe]="_namespace";let he=oe+1;e[e._of=he]="_of";let Ue=he+1;e[e._opaque=Ue]="_opaque";let He=Ue+1;e[e._out=He]="_out";let Ve=He+1;e[e._override=Ve]="_override";let We=Ve+1;e[e._private=We]="_private";let Xe=We+1;e[e._protected=Xe]="_protected";let Ge=Xe+1;e[e._proto=Ge]="_proto";let Je=Ge+1;e[e._public=Je]="_public";let ze=Je+1;e[e._readonly=ze]="_readonly";let Ke=ze+1;e[e._require=Ke]="_require";let Ye=Ke+1;e[e._satisfies=Ye]="_satisfies";let Qe=Ye+1;e[e._set=Qe]="_set";let Ze=Qe+1;e[e._static=Ze]="_static";let et=Ze+1;e[e._symbol=et]="_symbol";let tt=et+1;e[e._type=tt]="_type";let nt=tt+1;e[e._unique=nt]="_unique";let dt=nt+1;e[e._using=dt]="_using"})(l||(l={}));var t;(function(e){e[e.PRECEDENCE_MASK=15]="PRECEDENCE_MASK";let s=16;e[e.IS_KEYWORD=s]="IS_KEYWORD";let o=32;e[e.IS_ASSIGN=o]="IS_ASSIGN";let i=64;e[e.IS_RIGHT_ASSOCIATIVE=i]="IS_RIGHT_ASSOCIATIVE";let c=128;e[e.IS_PREFIX=c]="IS_PREFIX";let u=256;e[e.IS_POSTFIX=u]="IS_POSTFIX";let d=512;e[e.IS_EXPRESSION_START=d]="IS_EXPRESSION_START";let x=512;e[e.num=x]="num";let g=1536;e[e.bigint=g]="bigint";let _=2560;e[e.decimal=_]="decimal";let w=3584;e[e.regexp=w]="regexp";let S=4608;e[e.string=S]="string";let v=5632;e[e.name=v]="name";let j=6144;e[e.eof=j]="eof";let U=7680;e[e.bracketL=U]="bracketL";let b=8192;e[e.bracketR=b]="bracketR";let R=9728;e[e.braceL=R]="braceL";let L=10752;e[e.braceBarL=L]="braceBarL";let z=11264;e[e.braceR=z]="braceR";let V=12288;e[e.braceBarR=V]="braceBarR";let W=13824;e[e.parenL=W]="parenL";let te=14336;e[e.parenR=te]="parenR";let fe=15360;e[e.comma=fe]="comma";let oe=16384;e[e.semi=oe]="semi";let he=17408;e[e.colon=he]="colon";let Ue=18432;e[e.doubleColon=Ue]="doubleColon";let He=19456;e[e.dot=He]="dot";let Ve=20480;e[e.question=Ve]="question";let We=21504;e[e.questionDot=We]="questionDot";let Xe=22528;e[e.arrow=Xe]="arrow";let Ge=23552;e[e.template=Ge]="template";let Je=24576;e[e.ellipsis=Je]="ellipsis";let ze=25600;e[e.backQuote=ze]="backQuote";let Ke=27136;e[e.dollarBraceL=Ke]="dollarBraceL";let Ye=27648;e[e.at=Ye]="at";let Qe=29184;e[e.hash=Qe]="hash";let Ze=29728;e[e.eq=Ze]="eq";let et=30752;e[e.assign=et]="assign";let tt=32640;e[e.preIncDec=tt]="preIncDec";let nt=33664;e[e.postIncDec=nt]="postIncDec";let dt=34432;e[e.bang=dt]="bang";let Bn=35456;e[e.tilde=Bn]="tilde";let qn=35841;e[e.pipeline=qn]="pipeline";let $n=36866;e[e.nullishCoalescing=$n]="nullishCoalescing";let Un=37890;e[e.logicalOR=Un]="logicalOR";let Hn=38915;e[e.logicalAND=Hn]="logicalAND";let Vn=39940;e[e.bitwiseOR=Vn]="bitwiseOR";let Wn=40965;e[e.bitwiseXOR=Wn]="bitwiseXOR";let Xn=41990;e[e.bitwiseAND=Xn]="bitwiseAND";let Gn=43015;e[e.equality=Gn]="equality";let Jn=44040;e[e.lessThan=Jn]="lessThan";let zn=45064;e[e.greaterThan=zn]="greaterThan";let Kn=46088;e[e.relationalOrEqual=Kn]="relationalOrEqual";let Yn=47113;e[e.bitShiftL=Yn]="bitShiftL";let Qn=48137;e[e.bitShiftR=Qn]="bitShiftR";let Zn=49802;e[e.plus=Zn]="plus";let es=50826;e[e.minus=es]="minus";let ts=51723;e[e.modulo=ts]="modulo";let ns=52235;e[e.star=ns]="star";let ss=53259;e[e.slash=ss]="slash";let rs=54348;e[e.exponent=rs]="exponent";let os=55296;e[e.jsxName=os]="jsxName";let is=56320;e[e.jsxText=is]="jsxText";let as=57344;e[e.jsxEmptyText=as]="jsxEmptyText";let cs=58880;e[e.jsxTagStart=cs]="jsxTagStart";let ls=59392;e[e.jsxTagEnd=ls]="jsxTagEnd";let us=60928;e[e.typeParameterStart=us]="typeParameterStart";let ps=61440;e[e.nonNullAssertion=ps]="nonNullAssertion";let fs=62480;e[e._break=fs]="_break";let hs=63504;e[e._case=hs]="_case";let ms=64528;e[e._catch=ms]="_catch";let ds=65552;e[e._continue=ds]="_continue";let ks=66576;e[e._debugger=ks]="_debugger";let xs=67600;e[e._default=xs]="_default";let gs=68624;e[e._do=gs]="_do";let _s=69648;e[e._else=_s]="_else";let ys=70672;e[e._finally=ys]="_finally";let Is=71696;e[e._for=Is]="_for";let Ts=73232;e[e._function=Ts]="_function";let bs=73744;e[e._if=bs]="_if";let ws=74768;e[e._return=ws]="_return";let Ss=75792;e[e._switch=Ss]="_switch";let Es=77456;e[e._throw=Es]="_throw";let As=77840;e[e._try=As]="_try";let vs=78864;e[e._var=vs]="_var";let Cs=79888;e[e._let=Cs]="_let";let Ps=80912;e[e._const=Ps]="_const";let Ns=81936;e[e._while=Ns]="_while";let Rs=82960;e[e._with=Rs]="_with";let Ls=84496;e[e._new=Ls]="_new";let Ds=85520;e[e._this=Ds]="_this";let Os=86544;e[e._super=Os]="_super";let Fs=87568;e[e._class=Fs]="_class";let js=88080;e[e._extends=js]="_extends";let Ms=89104;e[e._export=Ms]="_export";let Bs=90640;e[e._import=Bs]="_import";let qs=91664;e[e._yield=qs]="_yield";let $s=92688;e[e._null=$s]="_null";let Us=93712;e[e._true=Us]="_true";let Hs=94736;e[e._false=Hs]="_false";let Vs=95256;e[e._in=Vs]="_in";let Ws=96280;e[e._instanceof=Ws]="_instanceof";let Xs=97936;e[e._typeof=Xs]="_typeof";let Gs=98960;e[e._void=Gs]="_void";let qi=99984;e[e._delete=qi]="_delete";let $i=100880;e[e._async=$i]="_async";let Ui=101904;e[e._get=Ui]="_get";let Hi=102928;e[e._set=Hi]="_set";let Vi=103952;e[e._declare=Vi]="_declare";let Wi=104976;e[e._readonly=Wi]="_readonly";let Xi=106e3;e[e._abstract=Xi]="_abstract";let Gi=107024;e[e._static=Gi]="_static";let Ji=107536;e[e._public=Ji]="_public";let zi=108560;e[e._private=zi]="_private";let Ki=109584;e[e._protected=Ki]="_protected";let Yi=110608;e[e._override=Yi]="_override";let Qi=112144;e[e._as=Qi]="_as";let Zi=113168;e[e._enum=Zi]="_enum";let ea=114192;e[e._type=ea]="_type";let ta=115216;e[e._implements=ta]="_implements"})(t||(t={}));function Js(e){switch(e){case t.num:return"num";case t.bigint:return"bigint";case t.decimal:return"decimal";case t.regexp:return"regexp";case t.string:return"string";case t.name:return"name";case t.eof:return"eof";case t.bracketL:return"[";case t.bracketR:return"]";case t.braceL:return"{";case t.braceBarL:return"{|";case t.braceR:return"}";case t.braceBarR:return"|}";case t.parenL:return"(";case t.parenR:return")";case t.comma:return",";case t.semi:return";";case t.colon:return":";case t.doubleColon:return"::";case t.dot:return".";case t.question:return"?";case t.questionDot:return"?.";case t.arrow:return"=>";case t.template:return"template";case t.ellipsis:return"...";case t.backQuote:return"`";case t.dollarBraceL:return"${";case t.at:return"@";case t.hash:return"#";case t.eq:return"=";case t.assign:return"_=";case t.preIncDec:return"++/--";case t.postIncDec:return"++/--";case t.bang:return"!";case t.tilde:return"~";case t.pipeline:return"|>";case t.nullishCoalescing:return"??";case t.logicalOR:return"||";case t.logicalAND:return"&&";case t.bitwiseOR:return"|";case t.bitwiseXOR:return"^";case t.bitwiseAND:return"&";case t.equality:return"==/!=";case t.lessThan:return"<";case t.greaterThan:return">";case t.relationalOrEqual:return"<=/>=";case t.bitShiftL:return"<<";case t.bitShiftR:return">>/>>>";case t.plus:return"+";case t.minus:return"-";case t.modulo:return"%";case t.star:return"*";case t.slash:return"/";case t.exponent:return"**";case t.jsxName:return"jsxName";case t.jsxText:return"jsxText";case t.jsxEmptyText:return"jsxEmptyText";case t.jsxTagStart:return"jsxTagStart";case t.jsxTagEnd:return"jsxTagEnd";case t.typeParameterStart:return"typeParameterStart";case t.nonNullAssertion:return"nonNullAssertion";case t._break:return"break";case t._case:return"case";case t._catch:return"catch";case t._continue:return"continue";case t._debugger:return"debugger";case t._default:return"default";case t._do:return"do";case t._else:return"else";case t._finally:return"finally";case t._for:return"for";case t._function:return"function";case t._if:return"if";case t._return:return"return";case t._switch:return"switch";case t._throw:return"throw";case t._try:return"try";case t._var:return"var";case t._let:return"let";case t._const:return"const";case t._while:return"while";case t._with:return"with";case t._new:return"new";case t._this:return"this";case t._super:return"super";case t._class:return"class";case t._extends:return"extends";case t._export:return"export";case t._import:return"import";case t._yield:return"yield";case t._null:return"null";case t._true:return"true";case t._false:return"false";case t._in:return"in";case t._instanceof:return"instanceof";case t._typeof:return"typeof";case t._void:return"void";case t._delete:return"delete";case t._async:return"async";case t._get:return"get";case t._set:return"set";case t._declare:return"declare";case t._readonly:return"readonly";case t._abstract:return"abstract";case t._static:return"static";case t._public:return"public";case t._private:return"private";case t._protected:return"protected";case t._override:return"override";case t._as:return"as";case t._enum:return"enum";case t._type:return"type";case t._implements:return"implements";default:return""}}var ie=class{constructor(n,s,o){this.startTokenIndex=n,this.endTokenIndex=s,this.isFunctionScope=o}},zs=class{constructor(n,s,o,i,c,u,d,x,g,_,w,S,v){this.potentialArrowAt=n,this.noAnonFunctionType=s,this.inDisallowConditionalTypesContext=o,this.tokensLength=i,this.scopesLength=c,this.pos=u,this.type=d,this.contextualKeyword=x,this.start=g,this.end=_,this.isType=w,this.scopeDepth=S,this.error=v}},xt=class e{constructor(){e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),e.prototype.__init6.call(this),e.prototype.__init7.call(this),e.prototype.__init8.call(this),e.prototype.__init9.call(this),e.prototype.__init10.call(this),e.prototype.__init11.call(this),e.prototype.__init12.call(this),e.prototype.__init13.call(this)}__init(){this.potentialArrowAt=-1}__init2(){this.noAnonFunctionType=!1}__init3(){this.inDisallowConditionalTypesContext=!1}__init4(){this.tokens=[]}__init5(){this.scopes=[]}__init6(){this.pos=0}__init7(){this.type=t.eof}__init8(){this.contextualKeyword=l.NONE}__init9(){this.start=0}__init10(){this.end=0}__init11(){this.isType=!1}__init12(){this.scopeDepth=0}__init13(){this.error=null}snapshot(){return new zs(this.potentialArrowAt,this.noAnonFunctionType,this.inDisallowConditionalTypesContext,this.tokens.length,this.scopes.length,this.pos,this.type,this.contextualKeyword,this.start,this.end,this.isType,this.scopeDepth,this.error)}restoreFromSnapshot(n){this.potentialArrowAt=n.potentialArrowAt,this.noAnonFunctionType=n.noAnonFunctionType,this.inDisallowConditionalTypesContext=n.inDisallowConditionalTypesContext,this.tokens.length=n.tokensLength,this.scopes.length=n.scopesLength,this.pos=n.pos,this.type=n.type,this.contextualKeyword=n.contextualKeyword,this.start=n.start,this.end=n.end,this.isType=n.isType,this.scopeDepth=n.scopeDepth,this.error=n.error}};var p;(function(e){e[e.backSpace=8]="backSpace";let s=10;e[e.lineFeed=s]="lineFeed";let o=9;e[e.tab=o]="tab";let i=13;e[e.carriageReturn=i]="carriageReturn";let c=14;e[e.shiftOut=c]="shiftOut";let u=32;e[e.space=u]="space";let d=33;e[e.exclamationMark=d]="exclamationMark";let x=34;e[e.quotationMark=x]="quotationMark";let g=35;e[e.numberSign=g]="numberSign";let _=36;e[e.dollarSign=_]="dollarSign";let w=37;e[e.percentSign=w]="percentSign";let S=38;e[e.ampersand=S]="ampersand";let v=39;e[e.apostrophe=v]="apostrophe";let j=40;e[e.leftParenthesis=j]="leftParenthesis";let U=41;e[e.rightParenthesis=U]="rightParenthesis";let b=42;e[e.asterisk=b]="asterisk";let R=43;e[e.plusSign=R]="plusSign";let L=44;e[e.comma=L]="comma";let z=45;e[e.dash=z]="dash";let V=46;e[e.dot=V]="dot";let W=47;e[e.slash=W]="slash";let te=48;e[e.digit0=te]="digit0";let fe=49;e[e.digit1=fe]="digit1";let oe=50;e[e.digit2=oe]="digit2";let he=51;e[e.digit3=he]="digit3";let Ue=52;e[e.digit4=Ue]="digit4";let He=53;e[e.digit5=He]="digit5";let Ve=54;e[e.digit6=Ve]="digit6";let We=55;e[e.digit7=We]="digit7";let Xe=56;e[e.digit8=Xe]="digit8";let Ge=57;e[e.digit9=Ge]="digit9";let Je=58;e[e.colon=Je]="colon";let ze=59;e[e.semicolon=ze]="semicolon";let Ke=60;e[e.lessThan=Ke]="lessThan";let Ye=61;e[e.equalsTo=Ye]="equalsTo";let Qe=62;e[e.greaterThan=Qe]="greaterThan";let Ze=63;e[e.questionMark=Ze]="questionMark";let et=64;e[e.atSign=et]="atSign";let tt=65;e[e.uppercaseA=tt]="uppercaseA";let nt=66;e[e.uppercaseB=nt]="uppercaseB";let dt=67;e[e.uppercaseC=dt]="uppercaseC";let Bn=68;e[e.uppercaseD=Bn]="uppercaseD";let qn=69;e[e.uppercaseE=qn]="uppercaseE";let $n=70;e[e.uppercaseF=$n]="uppercaseF";let Un=71;e[e.uppercaseG=Un]="uppercaseG";let Hn=72;e[e.uppercaseH=Hn]="uppercaseH";let Vn=73;e[e.uppercaseI=Vn]="uppercaseI";let Wn=74;e[e.uppercaseJ=Wn]="uppercaseJ";let Xn=75;e[e.uppercaseK=Xn]="uppercaseK";let Gn=76;e[e.uppercaseL=Gn]="uppercaseL";let Jn=77;e[e.uppercaseM=Jn]="uppercaseM";let zn=78;e[e.uppercaseN=zn]="uppercaseN";let Kn=79;e[e.uppercaseO=Kn]="uppercaseO";let Yn=80;e[e.uppercaseP=Yn]="uppercaseP";let Qn=81;e[e.uppercaseQ=Qn]="uppercaseQ";let Zn=82;e[e.uppercaseR=Zn]="uppercaseR";let es=83;e[e.uppercaseS=es]="uppercaseS";let ts=84;e[e.uppercaseT=ts]="uppercaseT";let ns=85;e[e.uppercaseU=ns]="uppercaseU";let ss=86;e[e.uppercaseV=ss]="uppercaseV";let rs=87;e[e.uppercaseW=rs]="uppercaseW";let os=88;e[e.uppercaseX=os]="uppercaseX";let is=89;e[e.uppercaseY=is]="uppercaseY";let as=90;e[e.uppercaseZ=as]="uppercaseZ";let cs=91;e[e.leftSquareBracket=cs]="leftSquareBracket";let ls=92;e[e.backslash=ls]="backslash";let us=93;e[e.rightSquareBracket=us]="rightSquareBracket";let ps=94;e[e.caret=ps]="caret";let fs=95;e[e.underscore=fs]="underscore";let hs=96;e[e.graveAccent=hs]="graveAccent";let ms=97;e[e.lowercaseA=ms]="lowercaseA";let ds=98;e[e.lowercaseB=ds]="lowercaseB";let ks=99;e[e.lowercaseC=ks]="lowercaseC";let xs=100;e[e.lowercaseD=xs]="lowercaseD";let gs=101;e[e.lowercaseE=gs]="lowercaseE";let _s=102;e[e.lowercaseF=_s]="lowercaseF";let ys=103;e[e.lowercaseG=ys]="lowercaseG";let Is=104;e[e.lowercaseH=Is]="lowercaseH";let Ts=105;e[e.lowercaseI=Ts]="lowercaseI";let bs=106;e[e.lowercaseJ=bs]="lowercaseJ";let ws=107;e[e.lowercaseK=ws]="lowercaseK";let Ss=108;e[e.lowercaseL=Ss]="lowercaseL";let Es=109;e[e.lowercaseM=Es]="lowercaseM";let As=110;e[e.lowercaseN=As]="lowercaseN";let vs=111;e[e.lowercaseO=vs]="lowercaseO";let Cs=112;e[e.lowercaseP=Cs]="lowercaseP";let Ps=113;e[e.lowercaseQ=Ps]="lowercaseQ";let Ns=114;e[e.lowercaseR=Ns]="lowercaseR";let Rs=115;e[e.lowercaseS=Rs]="lowercaseS";let Ls=116;e[e.lowercaseT=Ls]="lowercaseT";let Ds=117;e[e.lowercaseU=Ds]="lowercaseU";let Os=118;e[e.lowercaseV=Os]="lowercaseV";let Fs=119;e[e.lowercaseW=Fs]="lowercaseW";let js=120;e[e.lowercaseX=js]="lowercaseX";let Ms=121;e[e.lowercaseY=Ms]="lowercaseY";let Bs=122;e[e.lowercaseZ=Bs]="lowercaseZ";let qs=123;e[e.leftCurlyBrace=qs]="leftCurlyBrace";let $s=124;e[e.verticalBar=$s]="verticalBar";let Us=125;e[e.rightCurlyBrace=Us]="rightCurlyBrace";let Hs=126;e[e.tilde=Hs]="tilde";let Vs=160;e[e.nonBreakingSpace=Vs]="nonBreakingSpace";let Ws=5760;e[e.oghamSpaceMark=Ws]="oghamSpaceMark";let Xs=8232;e[e.lineSeparator=Xs]="lineSeparator";let Gs=8233;e[e.paragraphSeparator=Gs]="paragraphSeparator"})(p||(p={}));var st,D,O,r,k,fo;function Fe(){return fo++}function ho(e){if("pos"in e){let n=ca(e.pos);e.message+=` (${n.line}:${n.column})`,e.loc=n}return e}var Ks=class{constructor(n,s){this.line=n,this.column=s}};function ca(e){let n=1,s=1;for(let o=0;op.lowercaseZ));){let i=er[e+(n-p.lowercaseA)+1];if(i===-1)break;e=i,s++}let o=er[e];if(o>-1&&!se[n]){r.pos=s,o&1?C(o>>>1):C(t.name,o>>>1);return}for(;s=k.length){let e=r.tokens;e.length>=2&&e[e.length-1].start>=k.length&&e[e.length-2].start>=k.length&&A("Unexpectedly reached the end of input."),C(t.eof);return}ua(k.charCodeAt(r.pos))}function ua(e){Se[e]||e===p.backslash||e===p.atSign&&k.charCodeAt(r.pos+1)===p.atSign?tr():lr(e)}function pa(){for(;k.charCodeAt(r.pos)!==p.asterisk||k.charCodeAt(r.pos+1)!==p.slash;)if(r.pos++,r.pos>k.length){A("Unterminated comment",r.pos-2);return}r.pos+=2}function ar(e){let n=k.charCodeAt(r.pos+=e);if(r.pos=p.digit0&&e<=p.digit9){To(!0);return}e===p.dot&&k.charCodeAt(r.pos+2)===p.dot?(r.pos+=3,C(t.ellipsis)):(++r.pos,C(t.dot))}function ha(){k.charCodeAt(r.pos+1)===p.equalsTo?M(t.assign,2):M(t.slash,1)}function ma(e){let n=e===p.asterisk?t.star:t.modulo,s=1,o=k.charCodeAt(r.pos+1);e===p.asterisk&&o===p.asterisk&&(s++,o=k.charCodeAt(r.pos+2),n=t.exponent),o===p.equalsTo&&k.charCodeAt(r.pos+2)!==p.greaterThan&&(s++,n=t.assign),M(n,s)}function da(e){let n=k.charCodeAt(r.pos+1);if(n===e){k.charCodeAt(r.pos+2)===p.equalsTo?M(t.assign,3):M(e===p.verticalBar?t.logicalOR:t.logicalAND,2);return}if(e===p.verticalBar){if(n===p.greaterThan){M(t.pipeline,2);return}else if(n===p.rightCurlyBrace&&O){M(t.braceBarR,2);return}}if(n===p.equalsTo){M(t.assign,2);return}M(e===p.verticalBar?t.bitwiseOR:t.bitwiseAND,1)}function ka(){k.charCodeAt(r.pos+1)===p.equalsTo?M(t.assign,2):M(t.bitwiseXOR,1)}function xa(e){let n=k.charCodeAt(r.pos+1);if(n===e){M(t.preIncDec,2);return}n===p.equalsTo?M(t.assign,2):e===p.plusSign?M(t.plus,1):M(t.minus,1)}function ga(){let e=k.charCodeAt(r.pos+1);if(e===p.lessThan){if(k.charCodeAt(r.pos+2)===p.equalsTo){M(t.assign,3);return}r.isType?M(t.lessThan,1):M(t.bitShiftL,2);return}e===p.equalsTo?M(t.relationalOrEqual,2):M(t.lessThan,1)}function Io(){if(r.isType){M(t.greaterThan,1);return}let e=k.charCodeAt(r.pos+1);if(e===p.greaterThan){let n=k.charCodeAt(r.pos+2)===p.greaterThan?3:2;if(k.charCodeAt(r.pos+n)===p.equalsTo){M(t.assign,n+1);return}M(t.bitShiftR,n);return}e===p.equalsTo?M(t.relationalOrEqual,2):M(t.greaterThan,1)}function on(){r.type===t.greaterThan&&(r.pos-=1,Io())}function _a(e){let n=k.charCodeAt(r.pos+1);if(n===p.equalsTo){M(t.equality,k.charCodeAt(r.pos+2)===p.equalsTo?3:2);return}if(e===p.equalsTo&&n===p.greaterThan){r.pos+=2,C(t.arrow);return}M(e===p.equalsTo?t.eq:t.bang,1)}function ya(){let e=k.charCodeAt(r.pos+1),n=k.charCodeAt(r.pos+2);e===p.questionMark&&!(O&&r.isType)?n===p.equalsTo?M(t.assign,3):M(t.nullishCoalescing,2):e===p.dot&&!(n>=p.digit0&&n<=p.digit9)?(r.pos+=2,C(t.questionDot)):(++r.pos,C(t.question))}function lr(e){switch(e){case p.numberSign:++r.pos,C(t.hash);return;case p.dot:fa();return;case p.leftParenthesis:++r.pos,C(t.parenL);return;case p.rightParenthesis:++r.pos,C(t.parenR);return;case p.semicolon:++r.pos,C(t.semi);return;case p.comma:++r.pos,C(t.comma);return;case p.leftSquareBracket:++r.pos,C(t.bracketL);return;case p.rightSquareBracket:++r.pos,C(t.bracketR);return;case p.leftCurlyBrace:O&&k.charCodeAt(r.pos+1)===p.verticalBar?M(t.braceBarL,2):(++r.pos,C(t.braceL));return;case p.rightCurlyBrace:++r.pos,C(t.braceR);return;case p.colon:k.charCodeAt(r.pos+1)===p.colon?M(t.doubleColon,2):(++r.pos,C(t.colon));return;case p.questionMark:ya();return;case p.atSign:++r.pos,C(t.at);return;case p.graveAccent:++r.pos,C(t.backQuote);return;case p.digit0:{let n=k.charCodeAt(r.pos+1);if(n===p.lowercaseX||n===p.uppercaseX||n===p.lowercaseO||n===p.uppercaseO||n===p.lowercaseB||n===p.uppercaseB){Ta();return}}case p.digit1:case p.digit2:case p.digit3:case p.digit4:case p.digit5:case p.digit6:case p.digit7:case p.digit8:case p.digit9:To(!1);return;case p.quotationMark:case p.apostrophe:ba(e);return;case p.slash:ha();return;case p.percentSign:case p.asterisk:ma(e);return;case p.verticalBar:case p.ampersand:da(e);return;case p.caret:ka();return;case p.plusSign:case p.dash:xa(e);return;case p.lessThan:ga();return;case p.greaterThan:Io();return;case p.equalsTo:case p.exclamationMark:_a(e);return;case p.tilde:M(t.tilde,1);return;default:break}A(`Unexpected character '${String.fromCharCode(e)}'`,r.pos)}function M(e,n){r.pos+=n,C(e)}function Ia(){let e=r.pos,n=!1,s=!1;for(;;){if(r.pos>=k.length){A("Unterminated regular expression",e);return}let o=k.charCodeAt(r.pos);if(n)n=!1;else{if(o===p.leftSquareBracket)s=!0;else if(o===p.rightSquareBracket&&s)s=!1;else if(o===p.slash&&!s)break;n=o===p.backslash}++r.pos}++r.pos,Sa(),C(t.regexp)}function nr(){for(;;){let e=k.charCodeAt(r.pos);if(e>=p.digit0&&e<=p.digit9||e===p.underscore)r.pos++;else break}}function Ta(){for(r.pos+=2;;){let n=k.charCodeAt(r.pos);if(n>=p.digit0&&n<=p.digit9||n>=p.lowercaseA&&n<=p.lowercaseF||n>=p.uppercaseA&&n<=p.uppercaseF||n===p.underscore)r.pos++;else break}k.charCodeAt(r.pos)===p.lowercaseN?(++r.pos,C(t.bigint)):C(t.num)}function To(e){let n=!1,s=!1;e||nr();let o=k.charCodeAt(r.pos);if(o===p.dot&&(++r.pos,nr(),o=k.charCodeAt(r.pos)),(o===p.uppercaseE||o===p.lowercaseE)&&(o=k.charCodeAt(++r.pos),(o===p.plusSign||o===p.dash)&&++r.pos,nr(),o=k.charCodeAt(r.pos)),o===p.lowercaseN?(++r.pos,n=!0):o===p.lowercaseM&&(++r.pos,s=!0),n){C(t.bigint);return}if(s){C(t.decimal);return}C(t.num)}function ba(e){for(r.pos++;;){if(r.pos>=k.length){A("Unterminated string constant");return}let n=k.charCodeAt(r.pos);if(n===p.backslash)r.pos++;else if(n===e)break;r.pos++}r.pos++,C(t.string)}function wa(){for(;;){if(r.pos>=k.length){A("Unterminated template");return}let e=k.charCodeAt(r.pos);if(e===p.graveAccent||e===p.dollarSign&&k.charCodeAt(r.pos+1)===p.leftCurlyBrace){if(r.pos===r.start&&a(t.template))if(e===p.dollarSign){r.pos+=2,C(t.dollarBraceL);return}else{++r.pos,C(t.backQuote);return}C(t.template);return}e===p.backslash&&r.pos++,r.pos++}}function Sa(){for(;r.pos"],["nbsp","\xA0"],["iexcl","\xA1"],["cent","\xA2"],["pound","\xA3"],["curren","\xA4"],["yen","\xA5"],["brvbar","\xA6"],["sect","\xA7"],["uml","\xA8"],["copy","\xA9"],["ordf","\xAA"],["laquo","\xAB"],["not","\xAC"],["shy","\xAD"],["reg","\xAE"],["macr","\xAF"],["deg","\xB0"],["plusmn","\xB1"],["sup2","\xB2"],["sup3","\xB3"],["acute","\xB4"],["micro","\xB5"],["para","\xB6"],["middot","\xB7"],["cedil","\xB8"],["sup1","\xB9"],["ordm","\xBA"],["raquo","\xBB"],["frac14","\xBC"],["frac12","\xBD"],["frac34","\xBE"],["iquest","\xBF"],["Agrave","\xC0"],["Aacute","\xC1"],["Acirc","\xC2"],["Atilde","\xC3"],["Auml","\xC4"],["Aring","\xC5"],["AElig","\xC6"],["Ccedil","\xC7"],["Egrave","\xC8"],["Eacute","\xC9"],["Ecirc","\xCA"],["Euml","\xCB"],["Igrave","\xCC"],["Iacute","\xCD"],["Icirc","\xCE"],["Iuml","\xCF"],["ETH","\xD0"],["Ntilde","\xD1"],["Ograve","\xD2"],["Oacute","\xD3"],["Ocirc","\xD4"],["Otilde","\xD5"],["Ouml","\xD6"],["times","\xD7"],["Oslash","\xD8"],["Ugrave","\xD9"],["Uacute","\xDA"],["Ucirc","\xDB"],["Uuml","\xDC"],["Yacute","\xDD"],["THORN","\xDE"],["szlig","\xDF"],["agrave","\xE0"],["aacute","\xE1"],["acirc","\xE2"],["atilde","\xE3"],["auml","\xE4"],["aring","\xE5"],["aelig","\xE6"],["ccedil","\xE7"],["egrave","\xE8"],["eacute","\xE9"],["ecirc","\xEA"],["euml","\xEB"],["igrave","\xEC"],["iacute","\xED"],["icirc","\xEE"],["iuml","\xEF"],["eth","\xF0"],["ntilde","\xF1"],["ograve","\xF2"],["oacute","\xF3"],["ocirc","\xF4"],["otilde","\xF5"],["ouml","\xF6"],["divide","\xF7"],["oslash","\xF8"],["ugrave","\xF9"],["uacute","\xFA"],["ucirc","\xFB"],["uuml","\xFC"],["yacute","\xFD"],["thorn","\xFE"],["yuml","\xFF"],["OElig","\u0152"],["oelig","\u0153"],["Scaron","\u0160"],["scaron","\u0161"],["Yuml","\u0178"],["fnof","\u0192"],["circ","\u02C6"],["tilde","\u02DC"],["Alpha","\u0391"],["Beta","\u0392"],["Gamma","\u0393"],["Delta","\u0394"],["Epsilon","\u0395"],["Zeta","\u0396"],["Eta","\u0397"],["Theta","\u0398"],["Iota","\u0399"],["Kappa","\u039A"],["Lambda","\u039B"],["Mu","\u039C"],["Nu","\u039D"],["Xi","\u039E"],["Omicron","\u039F"],["Pi","\u03A0"],["Rho","\u03A1"],["Sigma","\u03A3"],["Tau","\u03A4"],["Upsilon","\u03A5"],["Phi","\u03A6"],["Chi","\u03A7"],["Psi","\u03A8"],["Omega","\u03A9"],["alpha","\u03B1"],["beta","\u03B2"],["gamma","\u03B3"],["delta","\u03B4"],["epsilon","\u03B5"],["zeta","\u03B6"],["eta","\u03B7"],["theta","\u03B8"],["iota","\u03B9"],["kappa","\u03BA"],["lambda","\u03BB"],["mu","\u03BC"],["nu","\u03BD"],["xi","\u03BE"],["omicron","\u03BF"],["pi","\u03C0"],["rho","\u03C1"],["sigmaf","\u03C2"],["sigma","\u03C3"],["tau","\u03C4"],["upsilon","\u03C5"],["phi","\u03C6"],["chi","\u03C7"],["psi","\u03C8"],["omega","\u03C9"],["thetasym","\u03D1"],["upsih","\u03D2"],["piv","\u03D6"],["ensp","\u2002"],["emsp","\u2003"],["thinsp","\u2009"],["zwnj","\u200C"],["zwj","\u200D"],["lrm","\u200E"],["rlm","\u200F"],["ndash","\u2013"],["mdash","\u2014"],["lsquo","\u2018"],["rsquo","\u2019"],["sbquo","\u201A"],["ldquo","\u201C"],["rdquo","\u201D"],["bdquo","\u201E"],["dagger","\u2020"],["Dagger","\u2021"],["bull","\u2022"],["hellip","\u2026"],["permil","\u2030"],["prime","\u2032"],["Prime","\u2033"],["lsaquo","\u2039"],["rsaquo","\u203A"],["oline","\u203E"],["frasl","\u2044"],["euro","\u20AC"],["image","\u2111"],["weierp","\u2118"],["real","\u211C"],["trade","\u2122"],["alefsym","\u2135"],["larr","\u2190"],["uarr","\u2191"],["rarr","\u2192"],["darr","\u2193"],["harr","\u2194"],["crarr","\u21B5"],["lArr","\u21D0"],["uArr","\u21D1"],["rArr","\u21D2"],["dArr","\u21D3"],["hArr","\u21D4"],["forall","\u2200"],["part","\u2202"],["exist","\u2203"],["empty","\u2205"],["nabla","\u2207"],["isin","\u2208"],["notin","\u2209"],["ni","\u220B"],["prod","\u220F"],["sum","\u2211"],["minus","\u2212"],["lowast","\u2217"],["radic","\u221A"],["prop","\u221D"],["infin","\u221E"],["ang","\u2220"],["and","\u2227"],["or","\u2228"],["cap","\u2229"],["cup","\u222A"],["int","\u222B"],["there4","\u2234"],["sim","\u223C"],["cong","\u2245"],["asymp","\u2248"],["ne","\u2260"],["equiv","\u2261"],["le","\u2264"],["ge","\u2265"],["sub","\u2282"],["sup","\u2283"],["nsub","\u2284"],["sube","\u2286"],["supe","\u2287"],["oplus","\u2295"],["otimes","\u2297"],["perp","\u22A5"],["sdot","\u22C5"],["lceil","\u2308"],["rceil","\u2309"],["lfloor","\u230A"],["rfloor","\u230B"],["lang","\u2329"],["rang","\u232A"],["loz","\u25CA"],["spades","\u2660"],["clubs","\u2663"],["hearts","\u2665"],["diams","\u2666"]]);function _t(e){let[n,s]=wo(e.jsxPragma||"React.createElement"),[o,i]=wo(e.jsxFragmentPragma||"React.Fragment");return{base:n,suffix:s,fragmentBase:o,fragmentSuffix:i}}function wo(e){let n=e.indexOf(".");return n===-1&&(n=e.length),[e.slice(0,n),e.slice(n)]}var G=class{getPrefixCode(){return""}getHoistedCode(){return""}getSuffixCode(){return""}};var yt=class e extends G{__init(){this.lastLineNumber=1}__init2(){this.lastIndex=0}__init3(){this.filenameVarName=null}__init4(){this.esmAutomaticImportNameResolutions={}}__init5(){this.cjsAutomaticModuleNameResolutions={}}constructor(n,s,o,i,c){super(),this.rootTransformer=n,this.tokens=s,this.importProcessor=o,this.nameManager=i,this.options=c,e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),this.jsxPragmaInfo=_t(c),this.isAutomaticRuntime=c.jsxRuntime==="automatic",this.jsxImportSource=c.jsxImportSource||"react"}process(){return this.tokens.matches1(t.jsxTagStart)?(this.processJSXTag(),!0):!1}getPrefixCode(){let n="";if(this.filenameVarName&&(n+=`const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath||"")};`),this.isAutomaticRuntime)if(this.importProcessor)for(let[s,o]of Object.entries(this.cjsAutomaticModuleNameResolutions))n+=`var ${o} = require("${s}");`;else{let{createElement:s,...o}=this.esmAutomaticImportNameResolutions;s&&(n+=`import {createElement as ${s}} from "${this.jsxImportSource}";`);let i=Object.entries(o).map(([c,u])=>`${c} as ${u}`).join(", ");if(i){let c=this.jsxImportSource+(this.options.production?"/jsx-runtime":"/jsx-dev-runtime");n+=`import {${i}} from "${c}";`}}return n}processJSXTag(){let{jsxRole:n,start:s}=this.tokens.currentToken(),o=this.options.production?null:this.getElementLocationCode(s);this.isAutomaticRuntime&&n!==le.KeyAfterPropSpread?this.transformTagToJSXFunc(o,n):this.transformTagToCreateElement(o)}getElementLocationCode(n){return`lineNumber: ${this.getLineNumberForIndex(n)}`}getLineNumberForIndex(n){let s=this.tokens.code;for(;this.lastIndex or > at the end of the tag.");i&&this.tokens.appendCode(`, ${i}`)}for(this.options.production||(i===null&&this.tokens.appendCode(", void 0"),this.tokens.appendCode(`, ${o}, ${this.getDevSource(n)}, this`)),this.tokens.removeInitialToken();!this.tokens.matches1(t.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}transformTagToCreateElement(n){if(this.tokens.replaceToken(this.getCreateElementInvocationCode()),this.tokens.matches1(t.jsxTagEnd))this.tokens.replaceToken(`${this.getFragmentCode()}, null`),this.processChildren(!0);else if(this.processTagIntro(),this.processPropsObjectWithDevInfo(n),!this.tokens.matches2(t.slash,t.jsxTagEnd))if(this.tokens.matches1(t.jsxTagEnd))this.tokens.removeToken(),this.processChildren(!0);else throw new Error("Expected either /> or > at the end of the tag.");for(this.tokens.removeInitialToken();!this.tokens.matches1(t.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}getJSXFuncInvocationCode(n){return this.options.production?n?this.claimAutoImportedFuncInvocation("jsxs","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsx","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsxDEV","/jsx-dev-runtime")}getCreateElementInvocationCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedFuncInvocation("createElement","");{let{jsxPragmaInfo:n}=this;return`${this.importProcessor&&this.importProcessor.getIdentifierReplacement(n.base)||n.base}${n.suffix}(`}}getFragmentCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedName("Fragment",this.options.production?"/jsx-runtime":"/jsx-dev-runtime");{let{jsxPragmaInfo:n}=this;return(this.importProcessor&&this.importProcessor.getIdentifierReplacement(n.fragmentBase)||n.fragmentBase)+n.fragmentSuffix}}claimAutoImportedFuncInvocation(n,s){let o=this.claimAutoImportedName(n,s);return this.importProcessor?`${o}.call(void 0, `:`${o}(`}claimAutoImportedName(n,s){if(this.importProcessor){let o=this.jsxImportSource+s;return this.cjsAutomaticModuleNameResolutions[o]||(this.cjsAutomaticModuleNameResolutions[o]=this.importProcessor.getFreeIdentifierForPath(o)),`${this.cjsAutomaticModuleNameResolutions[o]}.${n}`}else return this.esmAutomaticImportNameResolutions[n]||(this.esmAutomaticImportNameResolutions[n]=this.nameManager.claimFreeName(`_${n}`)),this.esmAutomaticImportNameResolutions[n]}processTagIntro(){let n=this.tokens.currentIndex()+1;for(;this.tokens.tokens[n].isType||!this.tokens.matches2AtIndex(n-1,t.jsxName,t.jsxName)&&!this.tokens.matches2AtIndex(n-1,t.greaterThan,t.jsxName)&&!this.tokens.matches1AtIndex(n,t.braceL)&&!this.tokens.matches1AtIndex(n,t.jsxTagEnd)&&!this.tokens.matches2AtIndex(n,t.slash,t.jsxTagEnd);)n++;if(n===this.tokens.currentIndex()+1){let s=this.tokens.identifierName();ur(s)&&this.tokens.replaceToken(`'${s}'`)}for(;this.tokens.currentIndex()=p.lowercaseA&&n<=p.lowercaseZ}function Ea(e){let n="",s="",o=!1,i=!1;for(let c=0;c=p.digit0&&e<=p.digit9}function Ca(e){return e>=p.digit0&&e<=p.digit9||e>=p.lowercaseA&&e<=p.lowercaseF||e>=p.uppercaseA&&e<=p.uppercaseF}function cn(e,n){let s=_t(n),o=new Set;for(let i=0;i0||s.namedExports.length>0)continue;[...s.defaultNames,...s.wildcardNames,...s.namedImports.map(({localName:i})=>i)].every(i=>this.shouldAutomaticallyElideImportedName(i))&&this.importsToReplace.set(n,"")}}shouldAutomaticallyElideImportedName(n){return this.isTypeScriptTransformEnabled&&!this.keepUnusedImports&&!this.nonTypeIdentifiers.has(n)}generateImportReplacements(){for(let[n,s]of this.importInfoByPath.entries()){let{defaultNames:o,wildcardNames:i,namedImports:c,namedExports:u,exportStarNames:d,hasStarExport:x}=s;if(o.length===0&&i.length===0&&c.length===0&&u.length===0&&d.length===0&&!x){this.importsToReplace.set(n,`require('${n}');`);continue}let g=this.getFreeIdentifierForPath(n),_;this.enableLegacyTypeScriptModuleInterop?_=g:_=i.length>0?i[0]:this.getFreeIdentifierForPath(n);let w=`var ${g} = require('${n}');`;if(i.length>0)for(let S of i){let v=this.enableLegacyTypeScriptModuleInterop?g:`${this.helperManager.getHelperName("interopRequireWildcard")}(${g})`;w+=` var ${S} = ${v};`}else d.length>0&&_!==g?w+=` var ${_} = ${this.helperManager.getHelperName("interopRequireWildcard")}(${g});`:o.length>0&&_!==g&&(w+=` var ${_} = ${this.helperManager.getHelperName("interopRequireDefault")}(${g});`);for(let{importedName:S,localName:v}of u)w+=` ${this.helperManager.getHelperName("createNamedExportFrom")}(${g}, '${v}', '${S}');`;for(let S of d)w+=` exports.${S} = ${_};`;x&&(w+=` ${this.helperManager.getHelperName("createStarExport")}(${g});`),this.importsToReplace.set(n,w);for(let S of o)this.identifierReplacements.set(S,`${_}.default`);for(let{importedName:S,localName:v}of c)this.identifierReplacements.set(v,`${g}.${S}`)}}getFreeIdentifierForPath(n){let s=n.split("/"),i=s[s.length-1].replace(/\W/g,"");return this.nameManager.claimFreeName(`_${i}`)}preprocessImportAtIndex(n){let s=[],o=[],i=[];if(n++,(this.tokens.matchesContextualAtIndex(n,l._type)||this.tokens.matches1AtIndex(n,t._typeof))&&!this.tokens.matches1AtIndex(n+1,t.comma)&&!this.tokens.matchesContextualAtIndex(n+1,l._from)||this.tokens.matches1AtIndex(n,t.parenL))return;if(this.tokens.matches1AtIndex(n,t.name)&&(s.push(this.tokens.identifierNameAtIndex(n)),n++,this.tokens.matches1AtIndex(n,t.comma)&&n++),this.tokens.matches1AtIndex(n,t.star)&&(n+=2,o.push(this.tokens.identifierNameAtIndex(n)),n++),this.tokens.matches1AtIndex(n,t.braceL)){let d=this.getNamedImports(n+1);n=d.newIndex;for(let x of d.namedImports)x.importedName==="default"?s.push(x.localName):i.push(x)}if(this.tokens.matchesContextualAtIndex(n,l._from)&&n++,!this.tokens.matches1AtIndex(n,t.string))throw new Error("Expected string token at the end of import statement.");let c=this.tokens.stringValueAtIndex(n),u=this.getImportInfo(c);u.defaultNames.push(...s),u.wildcardNames.push(...o),u.namedImports.push(...i),s.length===0&&o.length===0&&i.length===0&&(u.hasBareImport=!0)}preprocessExportAtIndex(n){if(this.tokens.matches2AtIndex(n,t._export,t._var)||this.tokens.matches2AtIndex(n,t._export,t._let)||this.tokens.matches2AtIndex(n,t._export,t._const))this.preprocessVarExportAtIndex(n);else if(this.tokens.matches2AtIndex(n,t._export,t._function)||this.tokens.matches2AtIndex(n,t._export,t._class)){let s=this.tokens.identifierNameAtIndex(n+2);this.addExportBinding(s,s)}else if(this.tokens.matches3AtIndex(n,t._export,t.name,t._function)){let s=this.tokens.identifierNameAtIndex(n+3);this.addExportBinding(s,s)}else this.tokens.matches2AtIndex(n,t._export,t.braceL)?this.preprocessNamedExportAtIndex(n):this.tokens.matches2AtIndex(n,t._export,t.star)&&this.preprocessExportStarAtIndex(n)}preprocessVarExportAtIndex(n){let s=0;for(let o=n+2;;o++)if(this.tokens.matches1AtIndex(o,t.braceL)||this.tokens.matches1AtIndex(o,t.dollarBraceL)||this.tokens.matches1AtIndex(o,t.bracketL))s++;else if(this.tokens.matches1AtIndex(o,t.braceR)||this.tokens.matches1AtIndex(o,t.bracketR))s--;else{if(s===0&&!this.tokens.matches1AtIndex(o,t.name))break;if(this.tokens.matches1AtIndex(1,t.eq)){let i=this.tokens.currentToken().rhsEndIndex;if(i==null)throw new Error("Expected = token with an end index.");o=i-1}else{let i=this.tokens.tokens[o];if(nn(i)){let c=this.tokens.identifierNameAtIndex(o);this.identifierReplacements.set(c,`exports.${c}`)}}}}preprocessNamedExportAtIndex(n){n+=2;let{newIndex:s,namedImports:o}=this.getNamedImports(n);if(n=s,this.tokens.matchesContextualAtIndex(n,l._from))n++;else{for(let{importedName:u,localName:d}of o)this.addExportBinding(u,d);return}if(!this.tokens.matches1AtIndex(n,t.string))throw new Error("Expected string token at the end of import statement.");let i=this.tokens.stringValueAtIndex(n);this.getImportInfo(i).namedExports.push(...o)}preprocessExportStarAtIndex(n){let s=null;if(this.tokens.matches3AtIndex(n,t._export,t.star,t._as)?(n+=3,s=this.tokens.identifierNameAtIndex(n),n+=2):n+=3,!this.tokens.matches1AtIndex(n,t.string))throw new Error("Expected string token at the end of star export statement.");let o=this.tokens.stringValueAtIndex(n),i=this.getImportInfo(o);s!==null?i.exportStarNames.push(s):i.hasStarExport=!0}getNamedImports(n){let s=[];for(;;){if(this.tokens.matches1AtIndex(n,t.braceR)){n++;break}let o=Ie(this.tokens,n);if(n=o.endIndex,o.isType||s.push({importedName:o.leftName,localName:o.rightName}),this.tokens.matches2AtIndex(n,t.comma,t.braceR)){n+=2;break}else if(this.tokens.matches1AtIndex(n,t.braceR)){n++;break}else if(this.tokens.matches1AtIndex(n,t.comma))n++;else throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[n])}`)}return{newIndex:n,namedImports:s}}getImportInfo(n){let s=this.importInfoByPath.get(n);if(s)return s;let o={defaultNames:[],wildcardNames:[],namedImports:[],namedExports:[],hasBareImport:!1,exportStarNames:[],hasStarExport:!1};return this.importInfoByPath.set(n,o),o}addExportBinding(n,s){this.exportBindingsByLocalName.has(n)||this.exportBindingsByLocalName.set(n,[]),this.exportBindingsByLocalName.get(n).push(s)}claimImportCode(n){let s=this.importsToReplace.get(n);return this.importsToReplace.set(n,""),s||""}getIdentifierReplacement(n){return this.identifierReplacements.get(n)||null}resolveExportBinding(n){let s=this.exportBindingsByLocalName.get(n);return!s||s.length===0?null:s.map(o=>`exports.${o}`).join(" = ")}getGlobalNames(){return new Set([...this.identifierReplacements.keys(),...this.exportBindingsByLocalName.keys()])}};var Pa=44,Na=59,Ao="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Po=new Uint8Array(64),Ra=new Uint8Array(128);for(let e=0;e>>=5,o>0&&(i|=32),e.write(Po[i])}while(o>0);return n}var vo=1024*16,Co=typeof TextDecoder<"u"?new TextDecoder:typeof Buffer<"u"?{decode(e){return Buffer.from(e.buffer,e.byteOffset,e.byteLength).toString()}}:{decode(e){let n="";for(let s=0;s0?n+Co.decode(e.subarray(0,s)):n}};function pr(e){let n=new La,s=0,o=0,i=0,c=0;for(let u=0;u0&&n.write(Na),d.length===0)continue;let x=0;for(let g=0;g0&&n.write(Pa),x=Tt(n,_[0],x),_.length!==1&&(s=Tt(n,_[1],s),o=Tt(n,_[2],o),i=Tt(n,_[3],i),_.length!==4&&(c=Tt(n,_[4],c)))}}return n.flush()}var Da=en(No(),1);var mr=class{constructor(){this._indexes={__proto__:null},this.array=[]}};function Oa(e,n){return e._indexes[n]}function Ro(e,n){let s=Oa(e,n);if(s!==void 0)return s;let{array:o,_indexes:i}=e,c=o.push(n);return i[n]=c-1}var Fa=0,ja=1,Ma=2,Ba=3,qa=4,Do=-1,Oo=class{constructor({file:e,sourceRoot:n}={}){this._names=new mr,this._sources=new mr,this._sourcesContent=[],this._mappings=[],this.file=e,this.sourceRoot=n,this._ignoreList=new mr}};var ln=(e,n,s,o,i,c,u,d)=>Ua(!0,e,n,s,o,i,c,u,d);function $a(e){let{_mappings:n,_sources:s,_sourcesContent:o,_names:i,_ignoreList:c}=e;return Wa(n),{version:3,file:e.file||void 0,names:i.array,sourceRoot:e.sourceRoot||void 0,sources:s.array,sourcesContent:o,mappings:n,ignoreList:c.array}}function Fo(e){let n=$a(e);return Object.assign({},n,{mappings:pr(n.mappings)})}function Ua(e,n,s,o,i,c,u,d,x){let{_mappings:g,_sources:_,_sourcesContent:w,_names:S}=n,v=Ha(g,s),j=Va(v,o);if(!i)return e&&Xa(v,j)?void 0:Lo(v,j,[o]);let U=Ro(_,i),b=d?Ro(S,d):Do;if(U===w.length&&(w[U]=x??null),!(e&&Ga(v,j,U,c,u,b)))return Lo(v,j,d?[o,U,c,u,b]:[o,U,c,u])}function Ha(e,n){for(let s=e.length;s<=n;s++)e[s]=[];return e[n]}function Va(e,n){let s=e.length;for(let o=s-1;o>=0;s=o--){let i=e[o];if(n>=i[Fa])break}return s}function Lo(e,n,s){for(let o=e.length;o>n;o--)e[o]=e[o-1];e[n]=s}function Wa(e){let{length:n}=e,s=n;for(let o=s-1;o>=0&&!(e[o].length>0);s=o,o--);s obj[importedName]}); - } - `,createStarExport:` - function createStarExport(obj) { - Object.keys(obj) - .filter((key) => key !== "default" && key !== "__esModule") - .forEach((key) => { - if (exports.hasOwnProperty(key)) { - return; - } - Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); - }); - } - `,nullishCoalesce:` - function nullishCoalesce(lhs, rhsFn) { - if (lhs != null) { - return lhs; - } else { - return rhsFn(); - } - } - `,asyncNullishCoalesce:` - async function asyncNullishCoalesce(lhs, rhsFn) { - if (lhs != null) { - return lhs; - } else { - return await rhsFn(); - } - } - `,optionalChain:` - function optionalChain(ops) { - let lastAccessLHS = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i]; - const fn = ops[i + 1]; - i += 2; - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - return undefined; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = fn((...args) => value.call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; - } - `,asyncOptionalChain:` - async function asyncOptionalChain(ops) { - let lastAccessLHS = undefined; - let value = ops[0]; - let i = 1; - while (i < ops.length) { - const op = ops[i]; - const fn = ops[i + 1]; - i += 2; - if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { - return undefined; - } - if (op === 'access' || op === 'optionalAccess') { - lastAccessLHS = value; - value = await fn(value); - } else if (op === 'call' || op === 'optionalCall') { - value = await fn((...args) => value.call(lastAccessLHS, ...args)); - lastAccessLHS = undefined; - } - } - return value; - } - `,optionalChainDelete:` - function optionalChainDelete(ops) { - const result = OPTIONAL_CHAIN_NAME(ops); - return result == null ? true : result; - } - `,asyncOptionalChainDelete:` - async function asyncOptionalChainDelete(ops) { - const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops); - return result == null ? true : result; - } - `},un=class e{__init(){this.helperNames={}}__init2(){this.createRequireName=null}constructor(n){this.nameManager=n,e.prototype.__init.call(this),e.prototype.__init2.call(this)}getHelperName(n){let s=this.helperNames[n];return s||(s=this.nameManager.claimFreeName(`_${n}`),this.helperNames[n]=s,s)}emitHelpers(){let n="";this.helperNames.optionalChainDelete&&this.getHelperName("optionalChain"),this.helperNames.asyncOptionalChainDelete&&this.getHelperName("asyncOptionalChain");for(let[s,o]of Object.entries(za)){let i=this.helperNames[s],c=o;s==="optionalChainDelete"?c=c.replace("OPTIONAL_CHAIN_NAME",this.helperNames.optionalChain):s==="asyncOptionalChainDelete"?c=c.replace("ASYNC_OPTIONAL_CHAIN_NAME",this.helperNames.asyncOptionalChain):s==="require"&&(this.createRequireName===null&&(this.createRequireName=this.nameManager.claimFreeName("_createRequire")),c=c.replace(/CREATE_REQUIRE_NAME/g,this.createRequireName)),i&&(n+=" ",n+=c.replace(s,i).replace(/\s+/g," ").trim())}return n}};function pn(e,n,s){Ka(e,s)&&Ya(e,n,s)}function Ka(e,n){for(let s of e.tokens)if(s.type===t.name&&!s.isType&&ko(s)&&n.has(e.identifierNameForToken(s)))return!0;return!1}function Ya(e,n,s){let o=[],i=n.length-1;for(let c=e.tokens.length-1;;c--){for(;o.length>0&&o[o.length-1].startTokenIndex===c+1;)o.pop();for(;i>=0&&n[i].endTokenIndex===c+1;)o.push(n[i]),i--;if(c<0)break;let u=e.tokens[c],d=e.identifierNameForToken(u);if(o.length>1&&!u.isType&&u.type===t.name&&s.has(d)){if(xo(u))jo(o[o.length-1],e,d);else if(go(u)){let x=o.length-1;for(;x>0&&!o[x].isFunctionScope;)x--;if(x<0)throw new Error("Did not find parent function scope.");jo(o[x],e,d)}}}if(o.length>0)throw new Error("Expected empty scope stack after processing file.")}function jo(e,n,s){for(let o=e.startTokenIndex;o0&&!r.error;)a(t.braceL)||a(t.bracketL)?e++:(a(t.braceR)||a(t.bracketR))&&e--,m();return!0}return!1}function zc(){let e=r.snapshot(),n=Kc();return r.restoreFromSnapshot(e),n}function Kc(){return m(),!!(a(t.parenR)||a(t.ellipsis)||Jc()&&(a(t.colon)||a(t.comma)||a(t.question)||a(t.eq)||a(t.parenR)&&(m(),a(t.arrow))))}function Pt(e){let n=N(0);h(e),Zc()||J(),P(n)}function Yc(){a(t.colon)&&Pt(t.colon)}function Me(){a(t.colon)&&ct()}function Qc(){f(t.colon)&&J()}function Zc(){let e=r.snapshot();return y(l._asserts)?(m(),K(l._is)?(J(),!0):Cr()||a(t._this)?(m(),K(l._is)&&J(),!0):(r.restoreFromSnapshot(e),!1)):Cr()||a(t._this)?(m(),y(l._is)&&!Z()?(m(),J(),!0):(r.restoreFromSnapshot(e),!1)):!1}function ct(){let e=N(0);h(t.colon),J(),P(e)}function J(){if(l1(),r.inDisallowConditionalTypesContext||Z()||!f(t._extends))return;let e=r.inDisallowConditionalTypesContext;r.inDisallowConditionalTypesContext=!0,l1(),r.inDisallowConditionalTypesContext=e,h(t.question),J(),h(t.colon),J()}function el(){return y(l._abstract)&&$()===t._new}function l1(){if(Gc()){vr(Le.TSFunctionType);return}if(a(t._new)){vr(Le.TSConstructorType);return}else if(el()){vr(Le.TSAbstractConstructorType);return}Xc()}function k1(){let e=N(1);J(),h(t.greaterThan),P(e),ut()}function x1(){if(f(t.jsxTagStart)){r.tokens[r.tokens.length-1].type=t.typeParameterStart;let e=N(1);for(;!a(t.greaterThan)&&!r.error;)J(),f(t.comma);pe(),P(e)}}function g1(){for(;!a(t.braceL)&&!r.error;)tl(),f(t.comma)}function tl(){Nt(),a(t.lessThan)&<()}function nl(){ge(!1),Oe(),f(t._extends)&&g1(),d1()}function sl(){ge(!1),Oe(),h(t.eq),J(),q()}function rl(){if(a(t.string)?De():E(),f(t.eq)){let e=r.tokens.length-1;Y(),r.tokens[e].rhsEndIndex=r.tokens.length}}function Lr(){for(ge(!1),h(t.braceL);!f(t.braceR)&&!r.error;)rl(),f(t.comma)}function Dr(){h(t.braceL),pt(t.braceR)}function Nr(){ge(!1),f(t.dot)?Nr():Dr()}function _1(){y(l._global)?E():a(t.string)?ke():A(),a(t.braceL)?Dr():q()}function xn(){it(),h(t.eq),il(),q()}function ol(){return y(l._require)&&$()===t.parenL}function il(){ol()?al():Nt()}function al(){X(l._require),h(t.parenL),a(t.string)||A(),De(),h(t.parenR)}function cl(){if(me())return!1;switch(r.type){case t._function:{let e=N(1);m();let n=r.start;return Ee(n,!0),P(e),!0}case t._class:{let e=N(1);return ve(!0,!1),P(e),!0}case t._const:if(a(t._const)&&rt(l._enum)){let e=N(1);return h(t._const),X(l._enum),r.tokens[r.tokens.length-1].type=t._enum,Lr(),P(e),!0}case t._var:case t._let:{let e=N(1);return Lt(r.type!==t._var),P(e),!0}case t.name:{let e=N(1),n=r.contextualKeyword,s=!1;return n===l._global?(_1(),s=!0):s=gn(n,!0),P(e),s}default:return!1}}function u1(){return gn(r.contextualKeyword,!0)}function ll(e){switch(e){case l._declare:{let n=r.tokens.length-1;if(cl())return r.tokens[n].type=t._declare,!0;break}case l._global:if(a(t.braceL))return Dr(),!0;break;default:return gn(e,!1)}return!1}function gn(e,n){switch(e){case l._abstract:if(at(n)&&a(t._class))return r.tokens[r.tokens.length-1].type=t._abstract,ve(!0,!1),!0;break;case l._enum:if(at(n)&&a(t.name))return r.tokens[r.tokens.length-1].type=t._enum,Lr(),!0;break;case l._interface:if(at(n)&&a(t.name)){let s=N(n?2:1);return nl(),P(s),!0}break;case l._module:if(at(n)){if(a(t.string)){let s=N(n?2:1);return _1(),P(s),!0}else if(a(t.name)){let s=N(n?2:1);return Nr(),P(s),!0}}break;case l._namespace:if(at(n)&&a(t.name)){let s=N(n?2:1);return Nr(),P(s),!0}break;case l._type:if(at(n)&&a(t.name)){let s=N(n?2:1);return sl(),P(s),!0}break;default:break}return!1}function at(e){return e?(m(),!0):!me()}function ul(){let e=r.snapshot();return kn(),Ae(),Yc(),h(t.arrow),r.error?(r.restoreFromSnapshot(e),!1):(qe(!0),!0)}function Or(){r.type===t.bitShiftL&&(r.pos-=1,C(t.lessThan)),lt()}function lt(){let e=N(0);for(h(t.lessThan);!a(t.greaterThan)&&!r.error;)J(),f(t.comma);e?(h(t.greaterThan),P(e)):(P(e),on(),h(t.greaterThan),r.tokens[r.tokens.length-1].isType=!0)}function Fr(){if(a(t.name))switch(r.contextualKeyword){case l._abstract:case l._declare:case l._enum:case l._interface:case l._module:case l._namespace:case l._type:return!0;default:break}return!1}function y1(e,n){if(a(t.colon)&&Pt(t.colon),!a(t.braceL)&&me()){let s=r.tokens.length-1;for(;s>=0&&(r.tokens[s].start>=e||r.tokens[s].type===t._default||r.tokens[s].type===t._export);)r.tokens[s].isType=!0,s--;return}qe(!1,n)}function I1(e,n,s){if(!Z()&&f(t.bang)){r.tokens[r.tokens.length-1].type=t.nonNullAssertion;return}if(a(t.lessThan)||a(t.bitShiftL)){let o=r.snapshot();if(!n&&jr()&&ul())return;if(Or(),!n&&f(t.parenL)?(r.tokens[r.tokens.length-1].subscriptStartIndex=e,_e()):a(t.backQuote)?_n():(r.type===t.greaterThan||r.type!==t.parenL&&r.type&t.IS_EXPRESSION_START&&!Z())&&A(),r.error)r.restoreFromSnapshot(o);else return}else!n&&a(t.questionDot)&&$()===t.lessThan&&(m(),r.tokens[e].isOptionalChainStart=!0,r.tokens[r.tokens.length-1].subscriptStartIndex=e,lt(),h(t.parenL),_e());Rt(e,n,s)}function T1(){if(f(t._import))return y(l._type)&&$()!==t.eq&&X(l._type),xn(),!0;if(f(t.eq))return Q(),q(),!0;if(K(l._as))return X(l._namespace),E(),q(),!0;if(y(l._type)){let e=$();(e===t.braceL||e===t.star)&&m()}return!1}function b1(){if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-1].identifierRole=I.ImportDeclaration;return}if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-1].identifierRole=I.ImportDeclaration,r.tokens[r.tokens.length-2].isType=!0,r.tokens[r.tokens.length-1].isType=!0;return}if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-3].identifierRole=I.ImportAccess,r.tokens[r.tokens.length-1].identifierRole=I.ImportDeclaration;return}E(),r.tokens[r.tokens.length-3].identifierRole=I.ImportAccess,r.tokens[r.tokens.length-1].identifierRole=I.ImportDeclaration,r.tokens[r.tokens.length-4].isType=!0,r.tokens[r.tokens.length-3].isType=!0,r.tokens[r.tokens.length-2].isType=!0,r.tokens[r.tokens.length-1].isType=!0}function w1(){if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-1].identifierRole=I.ExportAccess;return}if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-1].identifierRole=I.ExportAccess,r.tokens[r.tokens.length-2].isType=!0,r.tokens[r.tokens.length-1].isType=!0;return}if(E(),a(t.comma)||a(t.braceR)){r.tokens[r.tokens.length-3].identifierRole=I.ExportAccess;return}E(),r.tokens[r.tokens.length-3].identifierRole=I.ExportAccess,r.tokens[r.tokens.length-4].isType=!0,r.tokens[r.tokens.length-3].isType=!0,r.tokens[r.tokens.length-2].isType=!0,r.tokens[r.tokens.length-1].isType=!0}function S1(){if(y(l._abstract)&&$()===t._class)return r.type=t._abstract,m(),ve(!0,!0),!0;if(y(l._interface)){let e=N(2);return gn(l._interface,!0),P(e),!0}return!1}function E1(){if(r.type===t._const){let e=we();if(e.type===t.name&&e.contextualKeyword===l._enum)return h(t._const),X(l._enum),r.tokens[r.tokens.length-1].type=t._enum,Lr(),!0}return!1}function A1(e){let n=r.tokens.length;vt([l._abstract,l._readonly,l._declare,l._static,l._override]);let s=r.tokens.length;if(m1()){let i=e?n-1:n;for(let c=i;c=k.length){A("Unterminated JSX contents");return}let s=k.charCodeAt(r.pos);if(s===p.lessThan||s===p.leftCurlyBrace){if(r.pos===r.start){if(s===p.lessThan){r.pos++,C(t.jsxTagStart);return}lr(s);return}e&&!n?C(t.jsxEmptyText):C(t.jsxText);return}s===p.lineFeed?e=!0:s!==p.space&&s!==p.carriageReturn&&s!==p.tab&&(n=!0),r.pos++}}function ml(e){for(r.pos++;;){if(r.pos>=k.length){A("Unterminated string constant");return}if(k.charCodeAt(r.pos)===e){r.pos++;break}r.pos++}C(t.string)}function dl(){let e;do{if(r.pos>k.length){A("Unexpectedly reached the end of input.");return}e=k.charCodeAt(++r.pos)}while(se[e]||e===p.dash);C(t.jsxName)}function Br(){pe()}function M1(e){if(Br(),!f(t.colon)){r.tokens[r.tokens.length-1].identifierRole=e;return}Br()}function B1(){let e=r.tokens.length;M1(I.Access);let n=!1;for(;a(t.dot);)n=!0,pe(),Br();if(!n){let s=r.tokens[e],o=k.charCodeAt(s.start);o>=p.lowercaseA&&o<=p.lowercaseZ&&(s.identifierRole=null)}}function kl(){switch(r.type){case t.braceL:m(),Q(),pe();return;case t.jsxTagStart:qr(),pe();return;case t.string:pe();return;default:A("JSX value should be either an expression or a quoted JSX text")}}function xl(){h(t.ellipsis),Q()}function gl(e){if(a(t.jsxTagEnd))return!1;B1(),D&&x1();let n=!1;for(;!a(t.slash)&&!a(t.jsxTagEnd)&&!r.error;){if(f(t.braceL)){n=!0,h(t.ellipsis),Y(),pe();continue}n&&r.end-r.start===3&&k.charCodeAt(r.start)===p.lowercaseK&&k.charCodeAt(r.start+1)===p.lowercaseE&&k.charCodeAt(r.start+2)===p.lowercaseY&&(r.tokens[e].jsxRole=le.KeyAfterPropSpread),M1(I.ObjectKey),a(t.eq)&&(pe(),kl())}let s=a(t.slash);return s&&pe(),s}function _l(){a(t.jsxTagEnd)||B1()}function q1(){let e=r.tokens.length-1;r.tokens[e].jsxRole=le.NoChildren;let n=0;if(!gl(e))for(ft();;)switch(r.type){case t.jsxTagStart:if(pe(),a(t.slash)){pe(),_l(),r.tokens[e].jsxRole!==le.KeyAfterPropSpread&&(n===1?r.tokens[e].jsxRole=le.OneChild:n>1&&(r.tokens[e].jsxRole=le.StaticChildren));return}n++,q1(),ft();break;case t.jsxText:n++,ft();break;case t.jsxEmptyText:ft();break;case t.braceL:m(),a(t.ellipsis)?(xl(),ft(),n+=2):(a(t.braceR)||(n++,Q()),ft());break;default:A();return}}function qr(){pe(),q1()}function pe(){r.tokens.push(new je),cr(),r.start=r.pos;let e=k.charCodeAt(r.pos);if(Se[e])dl();else if(e===p.quotationMark||e===p.apostrophe)ml(e);else switch(++r.pos,e){case p.greaterThan:C(t.jsxTagEnd);break;case p.lessThan:C(t.jsxTagStart);break;case p.slash:C(t.slash);break;case p.equalsTo:C(t.eq);break;case p.leftCurlyBrace:C(t.braceL);break;case p.dot:C(t.dot);break;case p.colon:C(t.colon);break;default:A()}}function ft(){r.tokens.push(new je),r.start=r.pos,hl()}function $1(e){if(a(t.question)){let n=$();if(n===t.colon||n===t.comma||n===t.parenR)return}$r(e)}function U1(){rn(t.question),a(t.colon)&&(D?ct():O&&Ce())}var Ur=class{constructor(n){this.stop=n}};function Q(e=!1){if(Y(e),a(t.comma))for(;f(t.comma);)Y(e)}function Y(e=!1,n=!1){return D?O1(e,n):O?Y1(e,n):de(e,n)}function de(e,n){if(a(t._yield))return Ol(),!1;(a(t.parenL)||a(t.name)||a(t._yield))&&(r.potentialArrowAt=r.start);let s=yl(e);return n&&Gr(),r.type&t.IS_ASSIGN?(m(),Y(e),!1):s}function yl(e){return Tl(e)?!0:(Il(e),!1)}function Il(e){D||O?$1(e):$r(e)}function $r(e){f(t.question)&&(Y(),h(t.colon),Y(e))}function Tl(e){let n=r.tokens.length;return ut()?!0:(yn(n,-1,e),!1)}function yn(e,n,s){if(D&&(t._in&t.PRECEDENCE_MASK)>n&&!Z()&&(K(l._as)||K(l._satisfies))){let i=N(1);J(),P(i),on(),yn(e,n,s);return}let o=r.type&t.PRECEDENCE_MASK;if(o>0&&(!s||!a(t._in))&&o>n){let i=r.type;m(),i===t.nullishCoalescing&&(r.tokens[r.tokens.length-1].nullishStartIndex=e);let c=r.tokens.length;ut(),yn(c,i&t.IS_RIGHT_ASSOCIATIVE?o-1:o,s),i===t.nullishCoalescing&&(r.tokens[e].numNullishCoalesceStarts++,r.tokens[r.tokens.length-1].numNullishCoalesceEnds++),yn(e,n,s)}}function ut(){if(D&&!st&&f(t.lessThan))return k1(),!1;if(y(l._module)&&or()===p.leftCurlyBrace&&!tn())return Fl(),!1;if(r.type&t.IS_PREFIX)return m(),ut(),!1;if(Hr())return!0;for(;r.type&t.IS_POSTFIX&&!ee();)r.type===t.preIncDec&&(r.type=t.postIncDec),m();return!1}function Hr(){let e=r.tokens.length;return ke()?!0:(Vr(e),r.tokens.length>e&&r.tokens[e].isOptionalChainStart&&(r.tokens[r.tokens.length-1].isOptionalChainEnd=!0),!1)}function Vr(e,n=!1){O?Z1(e,n):Wr(e,n)}function Wr(e,n=!1){let s=new Ur(!1);do bl(e,n,s);while(!s.stop&&!r.error)}function bl(e,n,s){D?I1(e,n,s):O?G1(e,n,s):Rt(e,n,s)}function Rt(e,n,s){if(!n&&f(t.doubleColon))Xr(),s.stop=!0,Vr(e,n);else if(a(t.questionDot)){if(r.tokens[e].isOptionalChainStart=!0,n&&$()===t.parenL){s.stop=!0;return}m(),r.tokens[r.tokens.length-1].subscriptStartIndex=e,f(t.bracketL)?(Q(),h(t.bracketR)):f(t.parenL)?_e():In()}else if(f(t.dot))r.tokens[r.tokens.length-1].subscriptStartIndex=e,In();else if(f(t.bracketL))r.tokens[r.tokens.length-1].subscriptStartIndex=e,Q(),h(t.bracketR);else if(!n&&a(t.parenL))if(jr()){let o=r.snapshot(),i=r.tokens.length;m(),r.tokens[r.tokens.length-1].subscriptStartIndex=e;let c=Fe();r.tokens[r.tokens.length-1].contextId=c,_e(),r.tokens[r.tokens.length-1].contextId=c,wl()&&(r.restoreFromSnapshot(o),s.stop=!0,r.scopeDepth++,Ae(),Sl(i))}else{m(),r.tokens[r.tokens.length-1].subscriptStartIndex=e;let o=Fe();r.tokens[r.tokens.length-1].contextId=o,_e(),r.tokens[r.tokens.length-1].contextId=o}else a(t.backQuote)?_n():s.stop=!0}function jr(){return r.tokens[r.tokens.length-1].contextualKeyword===l._async&&!ee()}function _e(){let e=!0;for(;!f(t.parenR)&&!r.error;){if(e)e=!1;else if(h(t.comma),f(t.parenR))break;W1(!1)}}function wl(){return a(t.colon)||a(t.arrow)}function Sl(e){D?D1():O&&K1(),h(t.arrow),ht(e)}function Xr(){let e=r.tokens.length;ke(),Vr(e,!0)}function ke(){if(f(t.modulo))return E(),!1;if(a(t.jsxText)||a(t.jsxEmptyText))return De(),!1;if(a(t.lessThan)&&st)return r.type=t.jsxTagStart,qr(),m(),!1;let e=r.potentialArrowAt===r.start;switch(r.type){case t.slash:case t.assign:yo();case t._super:case t._this:case t.regexp:case t.num:case t.bigint:case t.decimal:case t.string:case t._null:case t._true:case t._false:return m(),!1;case t._import:return m(),a(t.dot)&&(r.tokens[r.tokens.length-1].type=t.name,m(),E()),!1;case t.name:{let n=r.tokens.length,s=r.start,o=r.contextualKeyword;return E(),o===l._await?(Dl(),!1):o===l._async&&a(t._function)&&!ee()?(m(),Ee(s,!1),!1):e&&o===l._async&&!ee()&&a(t.name)?(r.scopeDepth++,ge(!1),h(t.arrow),ht(n),!0):a(t._do)&&!ee()?(m(),Pe(),!1):e&&!ee()&&a(t.arrow)?(r.scopeDepth++,mn(!1),h(t.arrow),ht(n),!0):(r.tokens[r.tokens.length-1].identifierRole=I.Access,!1)}case t._do:return m(),Pe(),!1;case t.parenL:return H1(e);case t.bracketL:return m(),V1(t.bracketR,!0),!1;case t.braceL:return Ct(!1,!1),!1;case t._function:return El(),!1;case t.at:Sn();case t._class:return ve(!1),!1;case t._new:return vl(),!1;case t.backQuote:return _n(),!1;case t.doubleColon:return m(),Xr(),!1;case t.hash:{let n=or();return Se[n]||n===p.backslash?In():m(),!1}default:return A(),!1}}function In(){f(t.hash),E()}function El(){let e=r.start;E(),f(t.dot)&&E(),Ee(e,!1)}function De(){m()}function Dt(){h(t.parenL),Q(),h(t.parenR)}function H1(e){let n=r.snapshot(),s=r.tokens.length;h(t.parenL);let o=!0;for(;!a(t.parenR)&&!r.error;){if(o)o=!1;else if(h(t.comma),a(t.parenR))break;if(a(t.ellipsis)){Ar(!1),Gr();break}else Y(!1,!0)}return h(t.parenR),e&&Al()&&Tn()?(r.restoreFromSnapshot(n),r.scopeDepth++,Ae(),Tn(),ht(s),r.error?(r.restoreFromSnapshot(n),H1(!1),!1):!0):!1}function Al(){return a(t.colon)||!ee()}function Tn(){return D?F1():O?Q1():f(t.arrow)}function Gr(){(D||O)&&U1()}function vl(){if(h(t._new),f(t.dot)){E();return}Cl(),O&&J1(),f(t.parenL)&&V1(t.parenR)}function Cl(){Xr(),f(t.questionDot)}function _n(){for(ye(),ye();!a(t.backQuote)&&!r.error;)h(t.dollarBraceL),Q(),ye(),ye();m()}function Ct(e,n){let s=Fe(),o=!0;for(m(),r.tokens[r.tokens.length-1].contextId=s;!f(t.braceR)&&!r.error;){if(o)o=!1;else if(h(t.comma),f(t.braceR))break;let i=!1;if(a(t.ellipsis)){let c=r.tokens.length;if(Er(),e&&(r.tokens.length===c+2&&mn(n),f(t.braceR)))break;continue}e||(i=f(t.star)),!e&&y(l._async)?(i&&A(),E(),a(t.colon)||a(t.parenL)||a(t.braceR)||a(t.eq)||a(t.comma)||(a(t.star)&&(m(),i=!0),Be(s))):Be(s),Ll(e,n,s)}r.tokens[r.tokens.length-1].contextId=s}function Pl(e){return!e&&(a(t.string)||a(t.num)||a(t.bracketL)||a(t.name)||!!(r.type&t.IS_KEYWORD))}function Nl(e,n){let s=r.start;return a(t.parenL)?(e&&A(),bn(s,!1),!0):Pl(e)?(Be(n),bn(s,!1),!0):!1}function Rl(e,n){if(f(t.colon)){e?St(n):Y(!1);return}let s;e?r.scopeDepth===0?s=I.ObjectShorthandTopLevelDeclaration:n?s=I.ObjectShorthandBlockScopedDeclaration:s=I.ObjectShorthandFunctionScopedDeclaration:s=I.ObjectShorthand,r.tokens[r.tokens.length-1].identifierRole=s,St(n,!0)}function Ll(e,n,s){D?N1():O&&z1(),Nl(e,s)||Rl(e,n)}function Be(e){O&&wn(),f(t.bracketL)?(r.tokens[r.tokens.length-1].contextId=e,Y(),h(t.bracketR),r.tokens[r.tokens.length-1].contextId=e):(a(t.num)||a(t.string)||a(t.bigint)||a(t.decimal)?ke():In(),r.tokens[r.tokens.length-1].identifierRole=I.ObjectKey,r.tokens[r.tokens.length-1].contextId=e)}function bn(e,n){let s=Fe();r.scopeDepth++;let o=r.tokens.length;Ae(n,s),Jr(e,s);let c=r.tokens.length;r.scopes.push(new ie(o,c,!0)),r.scopeDepth--}function ht(e){qe(!0);let n=r.tokens.length;r.scopes.push(new ie(e,n,!0)),r.scopeDepth--}function Jr(e,n=0){D?y1(e,n):O?X1(n):qe(!1,n)}function qe(e,n=0){e&&!a(t.braceL)?Y():Pe(!0,n)}function V1(e,n=!1){let s=!0;for(;!f(e)&&!r.error;){if(s)s=!1;else if(h(t.comma),f(e))break;W1(n)}}function W1(e){e&&a(t.comma)||(a(t.ellipsis)?(Er(),Gr()):a(t.question)?m():Y(!1,!0))}function E(){m(),r.tokens[r.tokens.length-1].type=t.name}function Dl(){ut()}function Ol(){m(),!a(t.semi)&&!ee()&&(f(t.star),Y())}function Fl(){X(l._module),h(t.braceL),pt(t.braceR)}function jl(e){return(e.type===t.name||!!(e.type&t.IS_KEYWORD))&&e.contextualKeyword!==l._from}function be(e){let n=N(0);h(e||t.colon),ce(),P(n)}function ei(){h(t.modulo),X(l._checks),f(t.parenL)&&(Q(),h(t.parenR))}function Yr(){let e=N(0);h(t.colon),a(t.modulo)?ei():(ce(),a(t.modulo)&&ei()),P(e)}function Ml(){m(),Qr(!0)}function Bl(){m(),E(),a(t.lessThan)&&xe(),h(t.parenL),Kr(),h(t.parenR),Yr(),q()}function zr(){a(t._class)?Ml():a(t._function)?Bl():a(t._var)?ql():K(l._module)?f(t.dot)?Hl():$l():y(l._type)?Vl():y(l._opaque)?Wl():y(l._interface)?Xl():a(t._export)?Ul():A()}function ql(){m(),ii(),q()}function $l(){for(a(t.string)?ke():E(),h(t.braceL);!a(t.braceR)&&!r.error;)a(t._import)?(m(),oo()):A();h(t.braceR)}function Ul(){h(t._export),f(t._default)?a(t._function)||a(t._class)?zr():(ce(),q()):a(t._var)||a(t._function)||a(t._class)||y(l._opaque)?zr():a(t.star)||a(t.braceL)||y(l._interface)||y(l._type)||y(l._opaque)?ro():A()}function Hl(){X(l._exports),Ce(),q()}function Vl(){m(),eo()}function Wl(){m(),to(!0)}function Xl(){m(),Qr()}function Qr(e=!1){if(Pn(),a(t.lessThan)&&xe(),f(t._extends))do En();while(!e&&f(t.comma));if(y(l._mixins)){m();do En();while(f(t.comma))}if(y(l._implements)){m();do En();while(f(t.comma))}An(e,!1,e)}function En(){si(!1),a(t.lessThan)&&$e()}function Zr(){Qr()}function Pn(){E()}function eo(){Pn(),a(t.lessThan)&&xe(),be(t.eq),q()}function to(e){X(l._type),Pn(),a(t.lessThan)&&xe(),a(t.colon)&&be(t.colon),e||be(t.eq),q()}function Gl(){wn(),ii(),f(t.eq)&&ce()}function xe(){let e=N(0);a(t.lessThan)||a(t.typeParameterStart)?m():A();do Gl(),a(t.greaterThan)||h(t.comma);while(!a(t.greaterThan)&&!r.error);h(t.greaterThan),P(e)}function $e(){let e=N(0);for(h(t.lessThan);!a(t.greaterThan)&&!r.error;)ce(),a(t.greaterThan)||h(t.comma);h(t.greaterThan),P(e)}function Jl(){if(X(l._interface),f(t._extends))do En();while(f(t.comma));An(!1,!1,!1)}function no(){a(t.num)||a(t.string)?ke():E()}function zl(){$()===t.colon?(no(),be()):ce(),h(t.bracketR),be()}function Kl(){no(),h(t.bracketR),h(t.bracketR),a(t.lessThan)||a(t.parenL)?so():(f(t.question),be())}function so(){for(a(t.lessThan)&&xe(),h(t.parenL);!a(t.parenR)&&!a(t.ellipsis)&&!r.error;)vn(),a(t.parenR)||h(t.comma);f(t.ellipsis)&&vn(),h(t.parenR),be()}function Yl(){so()}function An(e,n,s){let o;for(n&&a(t.braceBarL)?(h(t.braceBarL),o=t.braceBarR):(h(t.braceL),o=t.braceR);!a(o)&&!r.error;){if(s&&y(l._proto)){let i=$();i!==t.colon&&i!==t.question&&(m(),e=!1)}if(e&&y(l._static)){let i=$();i!==t.colon&&i!==t.question&&m()}if(wn(),f(t.bracketL))f(t.bracketL)?Kl():zl();else if(a(t.parenL)||a(t.lessThan))Yl();else{if(y(l._get)||y(l._set)){let i=$();(i===t.name||i===t.string||i===t.num)&&m()}Ql()}Zl()}h(o)}function Ql(){if(a(t.ellipsis)){if(h(t.ellipsis),f(t.comma)||f(t.semi),a(t.braceR))return;ce()}else no(),a(t.lessThan)||a(t.parenL)?so():(f(t.question),be())}function Zl(){!f(t.semi)&&!f(t.comma)&&!a(t.braceR)&&!a(t.braceBarR)&&A()}function si(e){for(e||E();f(t.dot);)E()}function eu(){si(!0),a(t.lessThan)&&$e()}function tu(){h(t._typeof),ri()}function nu(){for(h(t.bracketL);r.pos0&&n0?this.tokens[this.tokenIndex-1].end:0,this.tokenIndex0&&this.tokenAtRelativeIndex(-1).type===t._delete?n.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChainDelete"):this.resultCode+=this.helperManager.getHelperName("optionalChainDelete"):n.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChain"):this.resultCode+=this.helperManager.getHelperName("optionalChain"),this.resultCode+="([")}}appendTokenSuffix(){let n=this.currentToken();if(n.isOptionalChainEnd&&!this.disableESTransforms&&(this.resultCode+="])"),n.numNullishCoalesceEnds&&!this.disableESTransforms)for(let s=0;s ${s}require`);let o=this.tokens.currentToken().contextId;if(o==null)throw new Error("Expected context ID on dynamic import invocation.");for(this.tokens.copyToken();!this.tokens.matchesContextIdAndLabel(t.parenR,o);)this.rootTransformer.processToken();this.tokens.replaceToken(s?")))":"))");return}if(this.removeImportAndDetectIfShouldElide())this.tokens.removeToken();else{let s=this.tokens.stringValue();this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(s)),this.tokens.appendCode(this.importProcessor.claimImportCode(s))}Ne(this.tokens),this.tokens.matches1(t.semi)&&this.tokens.removeToken()}removeImportAndDetectIfShouldElide(){if(this.tokens.removeInitialToken(),this.tokens.matchesContextual(l._type)&&!this.tokens.matches1AtIndex(this.tokens.currentIndex()+1,t.comma)&&!this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+1,l._from))return this.removeRemainingImport(),!0;if(this.tokens.matches1(t.name)||this.tokens.matches1(t.star))return this.removeRemainingImport(),!1;if(this.tokens.matches1(t.string))return!1;let n=!1,s=!1;for(;!this.tokens.matches1(t.string);)(!n&&this.tokens.matches1(t.braceL)||this.tokens.matches1(t.comma))&&(this.tokens.removeToken(),this.tokens.matches1(t.braceR)||(s=!0),(this.tokens.matches2(t.name,t.comma)||this.tokens.matches2(t.name,t.braceR)||this.tokens.matches4(t.name,t.name,t.name,t.comma)||this.tokens.matches4(t.name,t.name,t.name,t.braceR))&&(n=!0)),this.tokens.removeToken();return this.keepUnusedImports?!1:this.isTypeScriptTransformEnabled?!n:this.isFlowTransformEnabled?s&&!n:!1}removeRemainingImport(){for(;!this.tokens.matches1(t.string);)this.tokens.removeToken()}processIdentifier(){let n=this.tokens.currentToken();if(n.shadowsGlobal)return!1;if(n.identifierRole===I.ObjectShorthand)return this.processObjectShorthand();if(n.identifierRole!==I.Access)return!1;let s=this.importProcessor.getIdentifierReplacement(this.tokens.identifierNameForToken(n));if(!s)return!1;let o=this.tokens.currentIndex()+1;for(;o=2&&this.tokens.matches1AtIndex(n-2,t.dot)||n>=2&&[t._var,t._let,t._const].includes(this.tokens.tokens[n-2].type))return!1;let o=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(s));return o?(this.tokens.copyToken(),this.tokens.appendCode(` ${o} =`),!0):!1}processComplexAssignment(){let n=this.tokens.currentIndex(),s=this.tokens.tokens[n-1];if(s.type!==t.name||s.shadowsGlobal||n>=2&&this.tokens.matches1AtIndex(n-2,t.dot))return!1;let o=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(s));return o?(this.tokens.appendCode(` = ${o}`),this.tokens.copyToken(),!0):!1}processPreIncDec(){let n=this.tokens.currentIndex(),s=this.tokens.tokens[n+1];if(s.type!==t.name||s.shadowsGlobal||n+2=1&&this.tokens.matches1AtIndex(n-1,t.dot))return!1;let i=this.tokens.identifierNameForToken(s),c=this.importProcessor.resolveExportBinding(i);if(!c)return!1;let u=this.tokens.rawCodeForToken(o),d=this.importProcessor.getIdentifierReplacement(i)||i;if(u==="++")this.tokens.replaceToken(`(${d} = ${c} = ${d} + 1, ${d} - 1)`);else if(u==="--")this.tokens.replaceToken(`(${d} = ${c} = ${d} - 1, ${d} + 1)`);else throw new Error(`Unexpected operator: ${u}`);return this.tokens.removeToken(),!0}processExportDefault(){let n=!0;if(this.tokens.matches4(t._export,t._default,t._function,t.name)||this.tokens.matches5(t._export,t._default,t.name,t._function,t.name)&&this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+2,l._async)){this.tokens.removeInitialToken(),this.tokens.removeToken();let s=this.processNamedFunction();this.tokens.appendCode(` exports.default = ${s};`)}else if(this.tokens.matches4(t._export,t._default,t._class,t.name)||this.tokens.matches5(t._export,t._default,t._abstract,t._class,t.name)||this.tokens.matches3(t._export,t._default,t.at)){this.tokens.removeInitialToken(),this.tokens.removeToken(),this.copyDecorators(),this.tokens.matches1(t._abstract)&&this.tokens.removeToken();let s=this.rootTransformer.processNamedClass();this.tokens.appendCode(` exports.default = ${s};`)}else if($t(this.isTypeScriptTransformEnabled,this.keepUnusedImports,this.tokens,this.declarationInfo))n=!1,this.tokens.removeInitialToken(),this.tokens.removeToken(),this.tokens.removeToken();else if(this.reactHotLoaderTransformer){let s=this.nameManager.claimFreeName("_default");this.tokens.replaceToken(`let ${s}; exports.`),this.tokens.copyToken(),this.tokens.appendCode(` = ${s} =`),this.reactHotLoaderTransformer.setExtractedDefaultExportName(s)}else this.tokens.replaceToken("exports."),this.tokens.copyToken(),this.tokens.appendCode(" =");n&&(this.hadDefaultExport=!0)}copyDecorators(){for(;this.tokens.matches1(t.at);)if(this.tokens.copyToken(),this.tokens.matches1(t.parenL))this.tokens.copyExpectedToken(t.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(t.parenR);else{for(this.tokens.copyExpectedToken(t.name);this.tokens.matches1(t.dot);)this.tokens.copyExpectedToken(t.dot),this.tokens.copyExpectedToken(t.name);this.tokens.matches1(t.parenL)&&(this.tokens.copyExpectedToken(t.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(t.parenR))}}processExportVar(){this.isSimpleExportVar()?this.processSimpleExportVar():this.processComplexExportVar()}isSimpleExportVar(){let n=this.tokens.currentIndex();if(n++,n++,!this.tokens.matches1AtIndex(n,t.name))return!1;for(n++;ns.call(n,...u)),n=void 0)}return s}var Fn="jest",Ku=["mock","unmock","enableAutomock","disableAutomock"],Wt=class e extends G{__init(){this.hoistedFunctionNames=[]}constructor(n,s,o,i){super(),this.rootTransformer=n,this.tokens=s,this.nameManager=o,this.importProcessor=i,e.prototype.__init.call(this)}process(){return this.tokens.currentToken().scopeDepth===0&&this.tokens.matches4(t.name,t.dot,t.name,t.parenL)&&this.tokens.identifierName()===Fn?zu([this,"access",n=>n.importProcessor,"optionalAccess",n=>n.getGlobalNames,"call",n=>n(),"optionalAccess",n=>n.has,"call",n=>n(Fn)])?!1:this.extractHoistedCalls():!1}getHoistedCode(){return this.hoistedFunctionNames.length>0?this.hoistedFunctionNames.map(n=>`${n}();`).join(""):""}extractHoistedCalls(){this.tokens.removeToken();let n=!1;for(;this.tokens.matches3(t.dot,t.name,t.parenL);){let s=this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+1);if(Ku.includes(s)){let i=this.nameManager.claimFreeName("__jestHoist");this.hoistedFunctionNames.push(i),this.tokens.replaceToken(`function ${i}(){${Fn}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(t.parenR),this.tokens.appendCode(";}"),n=!1}else n?this.tokens.copyToken():this.tokens.replaceToken(`${Fn}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(t.parenR),n=!0}return!0}};var Xt=class extends G{constructor(n){super(),this.tokens=n}process(){if(this.tokens.matches1(t.num)){let n=this.tokens.currentTokenCode();if(n.includes("_"))return this.tokens.replaceToken(n.replace(/_/g,"")),!0}return!1}};var Gt=class extends G{constructor(n,s){super(),this.tokens=n,this.nameManager=s}process(){return this.tokens.matches2(t._catch,t.braceL)?(this.tokens.copyToken(),this.tokens.appendCode(` (${this.nameManager.claimFreeName("e")})`),!0):!1}};var Jt=class extends G{constructor(n,s){super(),this.tokens=n,this.nameManager=s}process(){if(this.tokens.matches1(t.nullishCoalescing)){let o=this.tokens.currentToken();return this.tokens.tokens[o.nullishStartIndex].isAsyncOperation?this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => ("):this.tokens.replaceTokenTrimmingLeftWhitespace(", () => ("),!0}if(this.tokens.matches1(t._delete)&&this.tokens.tokenAtRelativeIndex(1).isOptionalChainStart)return this.tokens.removeInitialToken(),!0;let s=this.tokens.currentToken().subscriptStartIndex;if(s!=null&&this.tokens.tokens[s].isOptionalChainStart&&this.tokens.tokenAtRelativeIndex(-1).type!==t._super){let o=this.nameManager.claimFreeName("_"),i;if(s>0&&this.tokens.matches1AtIndex(s-1,t._delete)&&this.isLastSubscriptInChain()?i=`${o} => delete ${o}`:i=`${o} => ${o}`,this.tokens.tokens[s].isAsyncOperation&&(i=`async ${i}`),this.tokens.matches2(t.questionDot,t.parenL)||this.tokens.matches2(t.questionDot,t.lessThan))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${i}`);else if(this.tokens.matches2(t.questionDot,t.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${i}`);else if(this.tokens.matches1(t.questionDot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${i}.`);else if(this.tokens.matches1(t.dot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${i}.`);else if(this.tokens.matches1(t.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${i}[`);else if(this.tokens.matches1(t.parenL))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${i}(`);else throw new Error("Unexpected subscript operator in optional chain.");return!0}return!1}isLastSubscriptInChain(){let n=0;for(let s=this.tokens.currentIndex()+1;;s++){if(s>=this.tokens.tokens.length)throw new Error("Reached the end of the code while finding the end of the access chain.");if(this.tokens.tokens[s].isOptionalChainStart?n++:this.tokens.tokens[s].isOptionalChainEnd&&n--,n<0)return!0;if(n===0&&this.tokens.tokens[s].subscriptStartIndex!=null)return!1}}justSkippedSuper(){let n=0,s=this.tokens.currentIndex()-1;for(;;){if(s<0)throw new Error("Reached the start of the code while finding the start of the access chain.");if(this.tokens.tokens[s].isOptionalChainStart?n--:this.tokens.tokens[s].isOptionalChainEnd&&n++,n<0)return!1;if(n===0&&this.tokens.tokens[s].subscriptStartIndex!=null)return this.tokens.tokens[s-1].type===t._super;s--}}};var zt=class extends G{constructor(n,s,o,i){super(),this.rootTransformer=n,this.tokens=s,this.importProcessor=o,this.options=i}process(){let n=this.tokens.currentIndex();if(this.tokens.identifierName()==="createReactClass"){let s=this.importProcessor&&this.importProcessor.getIdentifierReplacement("createReactClass");return s?this.tokens.replaceToken(`(0, ${s})`):this.tokens.copyToken(),this.tryProcessCreateClassCall(n),!0}if(this.tokens.matches3(t.name,t.dot,t.name)&&this.tokens.identifierName()==="React"&&this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+2)==="createClass"){let s=this.importProcessor&&this.importProcessor.getIdentifierReplacement("React")||"React";return s?(this.tokens.replaceToken(s),this.tokens.copyToken(),this.tokens.copyToken()):(this.tokens.copyToken(),this.tokens.copyToken(),this.tokens.copyToken()),this.tryProcessCreateClassCall(n),!0}return!1}tryProcessCreateClassCall(n){let s=this.findDisplayName(n);s&&this.classNeedsDisplayName()&&(this.tokens.copyExpectedToken(t.parenL),this.tokens.copyExpectedToken(t.braceL),this.tokens.appendCode(`displayName: '${s}',`),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(t.braceR),this.tokens.copyExpectedToken(t.parenR))}findDisplayName(n){return n<2?null:this.tokens.matches2AtIndex(n-2,t.name,t.eq)?this.tokens.identifierNameAtIndex(n-2):n>=2&&this.tokens.tokens[n-2].identifierRole===I.ObjectKey?this.tokens.identifierNameAtIndex(n-2):this.tokens.matches2AtIndex(n-2,t._export,t._default)?this.getDisplayNameFromFilename():null}getDisplayNameFromFilename(){let s=(this.options.filePath||"unknown").split("/"),o=s[s.length-1],i=o.lastIndexOf("."),c=i===-1?o:o.slice(0,i);return c==="index"&&s[s.length-2]?s[s.length-2]:c}classNeedsDisplayName(){let n=this.tokens.currentIndex();if(!this.tokens.matches2(t.parenL,t.braceL))return!1;let s=n+1,o=this.tokens.tokens[s].contextId;if(o==null)throw new Error("Expected non-null context ID on object open-brace.");for(;n({variableName:o,uniqueLocalName:o}));return this.extractedDefaultExportName&&s.push({variableName:this.extractedDefaultExportName,uniqueLocalName:"default"}),` -;(function () { - var reactHotLoader = require('react-hot-loader').default; - var leaveModule = require('react-hot-loader').leaveModule; - if (!reactHotLoader) { - return; - } -${s.map(({variableName:o,uniqueLocalName:i})=>` reactHotLoader.register(${o}, "${i}", ${JSON.stringify(this.filePath||"")});`).join(` -`)} - leaveModule(module); -})();`}process(){return!1}};var Yu=new Set(["break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","function","if","import","in","instanceof","new","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","enum","implements","interface","let","package","private","protected","public","static","await","false","null","true"]);function jn(e){if(e.length===0||!Se[e.charCodeAt(0)])return!1;for(let n=1;n` var ${u};`).join("");for(let u of this.transformers)s+=u.getHoistedCode();let o="";for(let u of this.transformers)o+=u.getSuffixCode();let i=this.tokens.finish(),{code:c}=i;if(c.startsWith("#!")){let u=c.indexOf(` -`);return u===-1&&(u=c.length,c+=` -`),{code:c.slice(0,u+1)+s+c.slice(u+1)+o,mappings:this.shiftMappings(i.mappings,s.length)}}else return{code:s+c+o,mappings:this.shiftMappings(i.mappings,s.length)}}processBalancedCode(){let n=0,s=0;for(;!this.tokens.isAtEnd();){if(this.tokens.matches1(t.braceL)||this.tokens.matches1(t.dollarBraceL))n++;else if(this.tokens.matches1(t.braceR)){if(n===0)return;n--}if(this.tokens.matches1(t.parenL))s++;else if(this.tokens.matches1(t.parenR)){if(s===0)return;s--}this.processToken()}}processToken(){if(this.tokens.matches1(t._class)){this.processClass();return}for(let n of this.transformers)if(n.process())return;this.tokens.copyToken()}processNamedClass(){if(!this.tokens.matches2(t._class,t.name))throw new Error("Expected identifier for exported class name.");let n=this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+1);return this.processClass(),n}processClass(){let n=lo(this,this.tokens,this.nameManager,this.disableESTransforms),s=(n.headerInfo.isExpression||!n.headerInfo.className)&&n.staticInitializerNames.length+n.instanceInitializerNames.length>0,o=n.headerInfo.className;s&&(o=this.nameManager.claimFreeName("_class"),this.generatedVariables.push(o),this.tokens.appendCode(` (${o} =`));let c=this.tokens.currentToken().contextId;if(c==null)throw new Error("Expected class to have a context ID.");for(this.tokens.copyExpectedToken(t._class);!this.tokens.matchesContextIdAndLabel(t.braceL,c);)this.processToken();this.processClassBody(n,o);let u=n.staticInitializerNames.map(d=>`${o}.${d}()`);s?this.tokens.appendCode(`, ${u.map(d=>`${d}, `).join("")}${o})`):n.staticInitializerNames.length>0&&this.tokens.appendCode(` ${u.map(d=>`${d};`).join(" ")}`)}processClassBody(n,s){let{headerInfo:o,constructorInsertPos:i,constructorInitializerStatements:c,fields:u,instanceInitializerNames:d,rangesToRemove:x}=n,g=0,_=0,w=this.tokens.currentToken().contextId;if(w==null)throw new Error("Expected non-null context ID on class.");this.tokens.copyExpectedToken(t.braceL),this.isReactHotLoaderTransformEnabled&&this.tokens.appendCode("__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}");let S=c.length+d.length>0;if(i===null&&S){let v=this.makeConstructorInitCode(c,d,s);if(o.hasSuperclass){let j=this.nameManager.claimFreeName("args");this.tokens.appendCode(`constructor(...${j}) { super(...${j}); ${v}; }`)}else this.tokens.appendCode(`constructor() { ${v}; }`)}for(;!this.tokens.matchesContextIdAndLabel(t.braceR,w);)if(g=x[_].start){for(this.tokens.currentIndex()`${o}.prototype.${i}.call(this)`)].join(";")}processPossibleArrowParamEnd(){if(this.tokens.matches2(t.parenR,t.colon)&&this.tokens.tokenAtRelativeIndex(1).isType){let n=this.tokens.currentIndex()+1;for(;this.tokens.tokens[n].isType;)n++;if(this.tokens.matches1AtIndex(n,t.arrow)){for(this.tokens.removeInitialToken();this.tokens.currentIndex()"),!0}}return!1}processPossibleAsyncArrowWithTypeParams(){if(!this.tokens.matchesContextual(l._async)&&!this.tokens.matches1(t._async))return!1;let n=this.tokens.tokenAtRelativeIndex(1);if(n.type!==t.lessThan||!n.isType)return!1;let s=this.tokens.currentIndex()+1;for(;this.tokens.tokens[s].isType;)s++;if(this.tokens.matches1AtIndex(s,t.parenL)){for(this.tokens.replaceToken("async ("),this.tokens.removeInitialToken();this.tokens.currentIndex(){if(e.length!==0)return e.length===1?e[0]:e};globalThis.__frameosFragment=Bi;globalThis.__frameosJsx=(e,n,...s)=>{let o=n?{...n}:{},i=sp(s),c=Object.prototype.hasOwnProperty.call(o,"children")?o.children:void 0;Object.prototype.hasOwnProperty.call(o,"children")&&delete o.children;let u=i??c;return e===Bi?u??null:(u!==void 0&&(o.children=u),{type:e,props:o})};globalThis.__frameosTranspile=(e,n={})=>Mi(e,{filePath:n.filePath??"",transforms:n.transforms??np,jsxRuntime:"classic",jsxPragma:"__frameosJsx",jsxFragmentPragma:"__frameosFragment",production:!0}).code;})(); diff --git a/frameos/frontend/build.mjs b/frameos/frontend/build.mjs index 4802adf08..0e9f67b38 100644 --- a/frameos/frontend/build.mjs +++ b/frameos/frontend/build.mjs @@ -17,14 +17,11 @@ const isWatch = process.argv.includes('--watch') const outputDir = path.resolve(__dirname, '../assets/compiled/frame_web') const staticDir = path.join(outputDir, 'static') -const vendorOutputDir = path.resolve(__dirname, '../assets/compiled/vendor') await import('../../frontend/scripts/generateRepoApps.mjs') await fs.rm(outputDir, { recursive: true, force: true }) await fs.mkdir(staticDir, { recursive: true }) -await fs.rm(vendorOutputDir, { recursive: true, force: true }) -await fs.mkdir(vendorOutputDir, { recursive: true }) await fs.copyFile(path.resolve(__dirname, 'src/index.html'), path.join(outputDir, 'index.html')) const postcssPlugins = [ @@ -134,35 +131,9 @@ const buildOptions = { } if (isWatch) { - const vendorBuildContext = await context({ - absWorkingDir: __dirname, - entryPoints: ['src/sucrase.ts'], - bundle: true, - format: 'iife', - globalName: '__frameosSucraseBundle', - platform: 'browser', - target: ['es2020'], - minify: true, - write: true, - outfile: path.join(vendorOutputDir, 'sucrase.js'), - }) const buildContext = await context(buildOptions) - await Promise.all([vendorBuildContext.watch(), buildContext.watch()]) + await buildContext.watch() console.log(`👀 Watching ${staticDir}`) } else { - await Promise.all([ - build({ - absWorkingDir: __dirname, - entryPoints: ['src/sucrase.ts'], - bundle: true, - format: 'iife', - globalName: '__frameosSucraseBundle', - platform: 'browser', - target: ['es2020'], - minify: true, - write: true, - outfile: path.join(vendorOutputDir, 'sucrase.js'), - }), - build(buildOptions), - ]) + await build(buildOptions) } diff --git a/frameos/frontend/src/sucrase.ts b/frameos/frontend/src/sucrase.ts deleted file mode 100644 index bb6c8469b..000000000 --- a/frameos/frontend/src/sucrase.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { transform } from 'sucrase' - -type TransformName = 'jsx' | 'typescript' | 'imports' - -interface TranspileOptions { - filePath?: string - transforms?: TransformName[] -} - -const defaultTransforms: TransformName[] = ['typescript', 'jsx'] - -const fragmentMarker = Symbol.for('frameos.fragment') - -const normalizeChildren = (children: unknown[]): unknown => { - if (children.length === 0) { - return undefined - } - if (children.length === 1) { - return children[0] - } - return children -} - -;(globalThis as typeof globalThis & Record).__frameosFragment = fragmentMarker -;(globalThis as typeof globalThis & Record).__frameosJsx = ( - type: unknown, - props: Record | null, - ...children: unknown[] -): unknown => { - const nextProps = props ? { ...props } : {} - const explicitChildren = normalizeChildren(children) - const propChildren = Object.prototype.hasOwnProperty.call(nextProps, 'children') ? nextProps.children : undefined - - if (Object.prototype.hasOwnProperty.call(nextProps, 'children')) { - delete nextProps.children - } - - const normalizedChildren = explicitChildren ?? propChildren - if (type === fragmentMarker) { - return normalizedChildren ?? null - } - - if (normalizedChildren !== undefined) { - nextProps.children = normalizedChildren - } - - return { - type, - props: nextProps, - } -} - -;(globalThis as typeof globalThis & Record).__frameosTranspile = ( - code: string, - options: TranspileOptions = {} -): string => { - const result = transform(code, { - filePath: options.filePath ?? '', - transforms: options.transforms ?? defaultTransforms, - jsxRuntime: 'classic', - jsxPragma: '__frameosJsx', - jsxFragmentPragma: '__frameosFragment', - production: true, - }) - return result.code -} diff --git a/frameos/src/frameos/interpreter.nim b/frameos/src/frameos/interpreter.nim index 4eabd9125..da9d12fe6 100644 --- a/frameos/src/frameos/interpreter.nim +++ b/frameos/src/frameos/interpreter.nim @@ -1,7 +1,7 @@ import frameos/types import frameos/values -import frameos/js_runtime -import frameos/js_app_runtime +import frameos/js_runtime/app_runtime +import frameos/js_runtime/runtime import frameos/channels import frameos/runtime_diagnostics import tables, json, os, zippy, chroma, pixie, jsony, sequtils, options, strutils, times diff --git a/frameos/src/frameos/js_runtime/README.md b/frameos/src/frameos/js_runtime/README.md new file mode 100644 index 000000000..d5e1356dc --- /dev/null +++ b/frameos/src/frameos/js_runtime/README.md @@ -0,0 +1,198 @@ +# FrameOS JavaScript Runtime + +This directory contains the JavaScript support used by the on-device FrameOS +runtime. It covers two related paths: + +- Scene snippets and code nodes, compiled by `runtime.nim`. +- Repository JavaScript apps, compiled and hosted by `app_runtime.nim`. + +Both paths end by passing JavaScript directly to the bundled QuickJS engine. The +native transpiler therefore only removes or rewrites syntax that QuickJS cannot +run in the form FrameOS receives it. Modern JavaScript that the bundled QuickJS +accepts should pass through unchanged. + +## File Origins and Licenses + +FrameOS is licensed under the repository license, AGPL-3.0. Most files in this +directory are FrameOS source files. `burrito.nim` is the exception: it is copied +from Burrito under MIT and modified locally. The file-level lineage is below. + +### FrameOS Runtime Files + +- `runtime.nim` is FrameOS code extracted from the older interpreter runtime. + It owns the scene-snippet bridge to QuickJS: context setup, global helpers, + state/args/context proxies, console logging, JSX runtime helpers, runtime + value conversion, source-location registration, and cleanup. +- `app_runtime.nim` is FrameOS code for repo-provided JavaScript apps. It wraps + an app module, exposes the `frameos` app API to QuickJS, manages app lifecycle + calls, handles image references, and converts JS return values back to + FrameOS values. +- `source_map.nim` is FrameOS code. It is not a standard source-map generator. + It stores a compact generated line/column table that is enough to rewrite + QuickJS compile/runtime error locations back to the original app or snippet + source. + +### Native Sucrase-Compatible Port + +- `tokens.nim`, `parser.nim`, `token_processor.nim`, and `transpiler.nim` are + FrameOS code written as a native Nim reimplementation of the subset of + Sucrase needed by FrameOS. +- The public shape intentionally mirrors Sucrase concepts such as + `TransformOptions`, `TransformResult`, token labels, parser annotations, and + `TokenProcessor`-style rewriting so behavior can be compared against + upstream Sucrase. +- The implementation is not a vendored copy of Sucrase. It is a compatibility + slice designed for single-file FrameOS apps/snippets and for the QuickJS + execution target. + +Sucrase attribution: + +- Upstream project: https://github.com/alangpierce/sucrase +- Version used for parity and dependency reference: `3.35.1` from + `frameos/frontend/package.json` and `pnpm-lock.yaml`. +- License: MIT. +- Copyright notice used by Sucrase: `Copyright (c) 2012-2018 various + contributors (see AUTHORS)`. +- Primary author/project maintainer attribution: Alan Pierce and Sucrase + contributors. +- Sucrase also credits Babel/Babylon and Acorn ancestry. Babel/Babylon and + Acorn contributors should be preserved in attribution when copying concepts + from those parser layers through Sucrase. + +The native port started from the runtime need to remove the QuickJS-hosted +Sucrase compiler bundle from devices. During development, upstream-style +fixtures were checked against npm Sucrase through +`tools/tests/test_native_js_transpiler_parity.py`, while the native CLI in +`tools/native_js_transpile.nim` exposed `script`, `module`, `tokens`, and +`parse` modes for parity tests and diagnostics. + +### QuickJS and Burrito + +The JS engine and Nim binding are outside this directory but are part of this +runtime stack: + +- `burrito.nim` is copied from + https://github.com/tapsterbot/burrito/blob/main/src/burrito/qjs.nim and then + adjusted for FrameOS build/runtime needs. Burrito is MIT licensed with + copyright attribution to Tapster Robotics, Inc. +- QuickJS is downloaded/built by `frameos.nimble` as `quickjs-2026-06-04`. + QuickJS is MIT licensed with copyright attribution to Fabrice Bellard and + Charlie Gordon. + +FrameOS uses Burrito as the thin Nim/QuickJS FFI layer. The code in this +directory deliberately keeps most app/snippet semantics in FrameOS code and +uses QuickJS only to execute the resulting JavaScript. + +## Runtime Responsibilities + +`runtime.nim` handles interpreted scene JavaScript: + +- Creates one QuickJS context per interpreted scene. +- Registers Nim bridge functions exposed to JS: `getState`, `getArg`, + `getContext`, `jsLog`, `parseTs`, `format`, and `now`. +- Installs global JS proxies for `state`, `args`, and `context`. +- Installs FrameOS classic JSX helpers: + `__frameosJsx(...)` and `__frameosFragment`. +- Compiles code nodes, inline code snippets, and one-shot eval snippets into + named JS functions. +- Wraps snippets in a JSON envelope so ordinary values return as strings rather + than crossing the Nim/QuickJS boundary as arbitrary `JSValue`s. +- Coerces returned envelope JSON to FrameOS `Value` instances using expected + output types where available. +- Logs JS compile/runtime errors through the scene logger. +- Registers compact source-location data and rewrites QuickJS error stacks back + to app/snippet source lines and columns. +- Serializes scene JS access behind `sceneJsLock`; QuickJS contexts are not + treated as thread-safe. + +`app_runtime.nim` handles repo JavaScript apps: + +- Builds a CommonJS-style module wrapper around app source. +- Runs the native module transform before loading the wrapper into QuickJS. +- Exposes a `frameos` API object to JS apps, including logging, state updates, + image operations, sleep scheduling, HTTP helpers, and context access. +- Calls exported app lifecycle functions such as `init` and `get`. +- Tracks persistent and transient image references so overwritten dynamic image + fields can be released. +- Maps app runtime errors through the same compact source-location mechanism. + +## Native Transpiler Policy + +The native transpiler is intentionally smaller than Sucrase. It should support +the TypeScript/JSX/module syntax that FrameOS users paste into single-file apps +or snippets, then preserve the rest for QuickJS. + +The current transform set is: + +- TypeScript erasure for common annotations, type-only declarations/imports, + interfaces, type aliases, assertions, `satisfies`, non-null assertions, + modifiers, overloads, `declare`, abstract members, generics, constructor + parameter properties, and enums. +- JSX lowering to the FrameOS classic runtime: + `__frameosJsx(type, props, ...children)` and `__frameosFragment`. +- Module rewriting for app modules into the CommonJS-style `exports` object + expected by the app wrapper. +- Modern ES preservation for syntax accepted by bundled QuickJS, including + optional chaining, nullish coalescing, numeric separators, optional catch + binding, regex literals, class fields, private fields, BigInt, and logical + assignment. + +The current non-goals are: + +- Full Babel/Sucrase parser parity. +- React automatic JSX runtime or development metadata. +- Babel/Sucrase interop helpers unless FrameOS runtime behavior needs them. +- Lowering JavaScript that QuickJS already runs natively. +- Standard `.map` source-map file generation. Runtime diagnostics only need the + compact line/column table in `source_map.nim`. + +Backend/editor validation still uses npm Sucrase from `frameos/frontend` for +user-facing diagnostics. The device runtime no longer needs a vendored Sucrase +compiler bundle. + +## Source Locations + +Transpiled code is passed directly to QuickJS, so there is no consumer for a +separate source-map file during normal execution. Instead, transform functions +return a `SourceLineMap` alongside generated code. + +The map records: + +- Generated filename and original source filename. +- Generated line to original source line. +- Sparse generated line/column segments to original source line/column. + +Runtime wrappers compose their wrapper map with the transpiler map and register +the result per QuickJS context. When QuickJS returns an error stack containing +`filename:line:column`, `rewriteQuickJsLocations` rewrites it to the original +source location before it is logged. + +This is deliberately compact: it gives better compile/runtime error positions +without carrying a full source-map artifact through the runtime. + +## Test Coverage + +Focused tests for this directory live in: + +- `src/frameos/js_runtime/tests/test_js_tokens.nim` +- `src/frameos/js_runtime/tests/test_js_parser_processor.nim` +- `src/frameos/js_runtime/tests/test_js_transpiler.nim` +- `src/frameos/js_runtime/tests/test_js_runtime_helpers.nim` +- `src/frameos/js_runtime/tests/test_js_app_runtime.nim` +- `src/frameos/js_runtime/tests/test_scene_runtime_cleanup.nim` +- `tools/tests/test_native_js_transpiler_parity.py` + +The parity harness compares selected native output or runtime behavior against +npm Sucrase. Prefer adding a focused fixture there when changing tokenizer, +parser, TypeScript, JSX, or module behavior. + +## Maintenance Notes + +- Add transforms only for syntax QuickJS cannot execute or for TypeScript/JSX + syntax that must be erased before QuickJS sees it. +- Keep runtime errors mapped back to original app/snippet source. If a transform + moves user code across lines or columns, update the compact source map. +- Keep source attribution in this README if code is copied or closely ported + from an upstream project. +- Keep npm Sucrase available for backend/editor diagnostics unless native + diagnostics become good enough for that user-facing path. diff --git a/frameos/src/frameos/js_app_runtime.nim b/frameos/src/frameos/js_runtime/app_runtime.nim similarity index 97% rename from frameos/src/frameos/js_app_runtime.nim rename to frameos/src/frameos/js_runtime/app_runtime.nim index d96da0531..ca38259cf 100644 --- a/frameos/src/frameos/js_app_runtime.nim +++ b/frameos/src/frameos/js_runtime/app_runtime.nim @@ -2,12 +2,12 @@ import std/[base64, json, options, strformat, strutils, tables] import pixie import frameos/apps as frameos_apps -import frameos/js_runtime +import frameos/js_runtime/runtime import frameos/types import frameos/values import frameos/utils/http_client import frameos/utils/image -import lib/burrito +import frameos/js_runtime/burrito type JsAppRuntime* = ref object @@ -485,7 +485,13 @@ proc ensureReady(runtime: JsAppRuntime) = : undefined; } """ & sceneJsPrelude) - discard runtime.js.eval(transpileModuleSource(runtime.source, "")) + let filename = "" + let transformed = transpileModuleSourceWithMap(runtime.source, filename) + try: + discard runtime.js.eval(transformed.code, filename) + except CatchableError as error: + raise newException(JSException, error.msg.mapJsErrorText(transformed.sourceMap)) + registerJsSourceMap(runtime.js.context, transformed.sourceMap) runtime.ready = true proc defaultImageWidth(owner: AppRoot, context: ExecutionContext, spec: JsonNode): int = @@ -652,7 +658,7 @@ proc invoke(runtime: JsAppRuntime, owner: AppRoot, configJson: JsonNode, context jsAppEnvByCtx.del(ctx) if JS_IsException(result) != 0: - let details = jsExceptionDetails(ctx) + let details = mappedJsExceptionDetails(ctx) frameos_apps.logError(owner, &"JS app {fnName} failed: " & details.message) if details.stack.len > 0: frameos_apps.log(owner, %*{ diff --git a/frameos/src/lib/burrito.nim b/frameos/src/frameos/js_runtime/burrito.nim similarity index 98% rename from frameos/src/lib/burrito.nim rename to frameos/src/frameos/js_runtime/burrito.nim index 2824f7d1e..528e85a5c 100644 --- a/frameos/src/lib/burrito.nim +++ b/frameos/src/frameos/js_runtime/burrito.nim @@ -12,7 +12,7 @@ ## ## **Example: Basic Usage** ## ```nim -## import burrito +## import frameos/js_runtime/burrito ## ## # Create a QuickJS instance ## var js = newQuickJS() @@ -33,7 +33,7 @@ ## ## **Example: Embedded REPL** ## ```nim -## import burrito +## import frameos/js_runtime/burrito ## ## # Create QuickJS with full standard library support ## var js = newQuickJS(configWithBothLibs()) @@ -54,7 +54,7 @@ ## ## **Example: Bytecode Compilation** ## ```nim -## import burrito +## import frameos/js_runtime/burrito ## ## var js = newQuickJS() ## @@ -132,7 +132,10 @@ proc js_std_init_handlers*(rt: ptr JSRuntime) proc js_std_free_handlers*(rt: ptr JSRuntime) proc js_std_await*(ctx: ptr JSContext, val: JSValue): JSValue proc js_std_loop*(ctx: ptr JSContext) -proc js_module_loader*(ctx: ptr JSContext, module_name: cstring, opaque: pointer): ptr JSModuleDef {.cdecl.} +proc js_module_loader*(ctx: ptr JSContext, module_name: cstring, opaque: pointer, + attributes: JSValueConst): ptr JSModuleDef {.cdecl.} +proc js_module_check_attributes*(ctx: ptr JSContext, opaque: pointer, + attributes: JSValueConst): cint {.cdecl.} {.pop.} @@ -224,6 +227,10 @@ proc JS_AtomToString*(ctx: ptr JSContext, atom: JSAtom): JSValue # Module loading proc JS_SetModuleLoaderFunc*(rt: ptr JSRuntime, module_normalize: pointer, module_loader: proc(ctx: ptr JSContext, moduleName: cstring, opaque: pointer): ptr JSModuleDef {.cdecl.}, opaque: pointer) +proc JS_SetModuleLoaderFunc2*(rt: ptr JSRuntime, module_normalize: pointer, module_loader: proc(ctx: ptr JSContext, + moduleName: cstring, opaque: pointer, attributes: JSValueConst): ptr JSModuleDef {.cdecl.}, + module_check_attrs: proc(ctx: ptr JSContext, opaque: pointer, attributes: JSValueConst): cint {.cdecl.}, + opaque: pointer) # Promise-related functions proc JS_PromiseState*(ctx: ptr JSContext, promise: JSValueConst): cint proc JS_PromiseResult*(ctx: ptr JSContext, promise: JSValueConst): JSValue diff --git a/frameos/src/frameos/js_runtime/parser.nim b/frameos/src/frameos/js_runtime/parser.nim new file mode 100644 index 000000000..433530917 --- /dev/null +++ b/frameos/src/frameos/js_runtime/parser.nim @@ -0,0 +1,506 @@ +# Lightweight Sucrase-style parser/traverser annotations for the native +# FrameOS JS transpiler. This module consumes the raw tokenizer stream and +# annotates token roles used by token-driven transformers. + +import std/strutils + +import ./tokens + +type + ParsedFile* = object + tokens*: seq[JsToken] + scopes*: seq[Scope] + +proc raw(code: string, token: JsToken): string = + if token.start >= 0 and token.`end` <= code.len and token.start <= token.`end`: + code[token.start.. 0: dec parenDepth + of ttBraceL: inc braceDepth + of ttBraceR: + if braceDepth == 0 and parenDepth == 0 and bracketDepth == 0: + return i + if braceDepth > 0: dec braceDepth + of ttBracketL: inc bracketDepth + of ttBracketR: + if bracketDepth > 0: dec bracketDepth + of ttSemi: + if parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return i + of ttEof: + return i + else: + discard + max(0, tokens.len - 1) + +proc isTypeBoundary(typ: TokenType): bool = + typ in {ttComma, ttSemi, ttEq, ttBraceR, ttParenR, ttBracketR, ttArrow, ttEof} + +proc markRangeType(tokens: var seq[JsToken], first, lastInclusive: int) = + if first < 0 or lastInclusive < first: + return + for i in first..min(lastInclusive, tokens.len - 1): + tokens[i].isType = true + +proc prevTokenIndex(tokens: seq[JsToken], index: int): int = + result = index - 1 + while result >= 0 and tokens[result].typ == ttEof: + dec result + +proc roleForDeclaration(scopeDepth: int, functionScoped: bool): IdentifierRole = + if scopeDepth == 0: + irTopLevelDeclaration + elif functionScoped: + irFunctionScopedDeclaration + else: + irBlockScopedDeclaration + +proc annotateScopes(tokens: var seq[JsToken]): seq[Scope] = + var scopeDepth = 0 + var stack: seq[tuple[index: int, isFunction: bool]] = @[] + var nextContextId = 1 + var pendingFunctionBrace = false + + for i in 0.. 0: + dec scopeDepth + tokens[i].scopeDepth = scopeDepth + if stack.len > 0: + let opened = stack.pop() + tokens[i].contextId = tokens[opened.index].contextId + result.add(Scope( + startTokenIndex: opened.index, + endTokenIndex: i, + isFunctionScope: opened.isFunction, + )) + else: + discard + + result.add(Scope(startTokenIndex: 0, endTokenIndex: max(0, tokens.len - 1), isFunctionScope: true)) + +proc markBindingList(tokens: var seq[JsToken], start, stop: int, role: IdentifierRole) = + var i = start + var expectingBinding = true + var depth = 0 + while i <= stop and i < tokens.len: + case tokens[i].typ + of ttBraceL, ttBracketL, ttParenL: + inc depth + of ttBraceR, ttBracketR, ttParenR: + if depth > 0: dec depth + of ttComma: + if depth == 0: + expectingBinding = true + of ttName: + if expectingBinding: + tokens[i].identifierRole = role + expectingBinding = false + of ttEq: + expectingBinding = false + else: + discard + inc i + +proc annotateVarDeclarations(tokens: var seq[JsToken]) = + var i = 0 + while i < tokens.len: + if tokens[i].typ in {ttConst, ttLet, ttVar}: + let role = roleForDeclaration(tokens[i].scopeDepth, tokens[i].typ == ttVar) + var j = i + 1 + var expectingBinding = true + var depth = 0 + while j < tokens.len: + case tokens[j].typ + of ttSemi, ttEof: + break + of ttBraceL, ttBracketL, ttParenL: + inc depth + of ttBraceR, ttBracketR, ttParenR: + if depth == 0 and tokens[j].typ == ttParenR: + break + if depth > 0: dec depth + of ttComma: + if depth == 0: + expectingBinding = true + of ttEq: + expectingBinding = false + of ttName: + if expectingBinding: + tokens[j].identifierRole = role + expectingBinding = false + else: + discard + inc j + i = j + inc i + +proc annotateFunctionAndClassDeclarations(tokens: var seq[JsToken]) = + for i in 0..= 0: + markBindingList(tokens, paren + 1, close - 1, irFunctionScopedDeclaration) + + if tokens[i].typ == ttClass: + let nameIndex = i + 1 + if nameIndex < tokens.len and tokens[nameIndex].typ == ttName: + tokens[nameIndex].identifierRole = roleForDeclaration(tokens[i].scopeDepth, false) + +proc annotateImportsExports(code: string, tokens: var seq[JsToken]) = + var i = 0 + while i < tokens.len: + if tokens[i].typ == ttImport: + let stmtEnd = findStatementEnd(tokens, i) + var j = i + 1 + if j < tokens.len and tokens[j].typ == ttType: + markRangeType(tokens, i, stmtEnd) + elif j < tokens.len and tokens[j].typ != ttParenL and tokens[j].typ != ttDot and tokens[j].typ != ttString: + if tokens[j].typ == ttName: + tokens[j].identifierRole = irImportDeclaration + inc j + if j < tokens.len and tokens[j].typ == ttComma: + inc j + if j < tokens.len and tokens[j].typ == ttStar: + if j + 2 < tokens.len and tokens[j + 1].typ == ttAs and tokens[j + 2].typ == ttName: + tokens[j + 2].identifierRole = irImportDeclaration + elif j < tokens.len and tokens[j].typ == ttBraceL: + let close = findMatching(tokens, j, ttBraceL, ttBraceR) + var k = j + 1 + while k >= 0 and close >= 0 and k < close: + if tokens[k].typ == ttName or tokens[k].typ == ttType: + if k + 1 < close and tokens[k + 1].typ == ttAs: + tokens[k].identifierRole = irImportAccess + if k + 2 < close and tokens[k + 2].typ == ttName: + tokens[k + 2].identifierRole = irImportDeclaration + k += 2 + elif k > j + 1 and tokens[k - 1].typ == ttAs: + tokens[k].identifierRole = irImportDeclaration + else: + tokens[k].identifierRole = irImportDeclaration + inc k + i = stmtEnd + + elif tokens[i].typ == ttExport: + let stmtEnd = findStatementEnd(tokens, i) + tokens[i].rhsEndIndex = stmtEnd + var j = i + 1 + if j < tokens.len and tokens[j].typ == ttType: + markRangeType(tokens, i, stmtEnd) + elif j < tokens.len and tokens[j].typ == ttBraceL: + let close = findMatching(tokens, j, ttBraceL, ttBraceR) + var k = j + 1 + while k >= 0 and close >= 0 and k < close: + if tokens[k].typ == ttName or tokens[k].typ == ttType: + if k == j + 1 or tokens[k - 1].typ in {ttComma, ttBraceL}: + tokens[k].identifierRole = irExportAccess + inc k + elif j < tokens.len and tokens[j].typ in {ttConst, ttLet, ttVar, ttFunction, ttClass, ttEnum}: + discard + i = stmtEnd + inc i + +proc annotateObjectKeys(code: string, tokens: var seq[JsToken]) = + for i in 0..= 0 and tokens[i].typ notin {ttSemi, ttEof}: + if tokens[i].typ in {ttImport, ttExport}: + return true + dec i + false + +proc isPropertyAccessName(tokens: seq[JsToken], index: int): bool = + let prev = prevTokenIndex(tokens, index) + prev >= 0 and tokens[prev].typ in {ttDot, ttQuestionDot} + +proc isLikelyTernaryColon(tokens: seq[JsToken], index: int): bool = + var depth = 0 + var i = index - 1 + while i >= 0: + case tokens[i].typ + of ttParenR, ttBraceR, ttBracketR: + inc depth + of ttParenL, ttBraceL, ttBracketL, ttDollarBraceL: + if depth == 0: + return false + dec depth + of ttQuestion: + if depth == 0: + return i != index - 1 + of ttComma, ttSemi: + if depth == 0: + return false + else: + discard + dec i + false + +proc annotateTypeSpans(code: string, tokens: var seq[JsToken]) = + var i = 0 + while i < tokens.len: + if tokens[i].typ == ttType: + var j = i + 1 + if j < tokens.len and tokens[j].typ == ttName: + while j < tokens.len and tokens[j].typ != ttEq and tokens[j].typ != ttEof: + inc j + if j < tokens.len and tokens[j].typ == ttEq: + let endIndex = findStatementEnd(tokens, i) + markRangeType(tokens, i, endIndex) + i = endIndex + + if tokens[i].typ == ttName and tokens[i].contextualKeyword == ckInterface: + let endIndex = + block: + var brace = i + while brace < tokens.len and tokens[brace].typ != ttBraceL and tokens[brace].typ != ttEof: + inc brace + if brace < tokens.len and tokens[brace].typ == ttBraceL: + let close = findMatching(tokens, brace, ttBraceL, ttBraceR) + if close >= 0: close else: findStatementEnd(tokens, i) + else: + findStatementEnd(tokens, i) + markRangeType(tokens, i, endIndex) + i = endIndex + + if tokens[i].typ == ttColon and not isLikelyTernaryColon(tokens, i): + let prev = prevTokenIndex(tokens, i) + if prev >= 0 and tokens[prev].identifierRole != irObjectKey: + var j = i + 1 + var depth = 0 + while j < tokens.len: + if tokens[j].typ in {ttLessThan, ttBraceL, ttBracketL, ttParenL}: + inc depth + elif tokens[j].typ in {ttGreaterThan, ttBraceR, ttBracketR, ttParenR}: + if depth == 0: + break + dec depth + if depth == 0 and isTypeBoundary(tokens[j].typ): + break + inc j + markRangeType(tokens, i, j - 1) + i = max(i, j - 1) + + if (tokens[i].typ == ttAs or (tokens[i].typ == ttName and tokens[i].contextualKeyword == ckSatisfies)) and + not inImportExportStatement(tokens, i) and + not isPropertyAccessName(tokens, i) and + tokens[i].identifierRole != irObjectKey: + var j = i + 1 + while j < tokens.len and not isTypeBoundary(tokens[j].typ): + inc j + markRangeType(tokens, i, j - 1) + i = max(i, j - 1) + + if tokens[i].typ == ttLessThan: + var prev = i - 1 + while prev >= 0 and tokens[prev].typ == ttEof: + dec prev + let close = findMatching(tokens, i, ttLessThan, ttGreaterThan) + if close > i and prev >= 0 and tokens[prev].typ in {ttName, ttFunction, ttClass, ttParenR}: + var after = close + 1 + if after < tokens.len and tokens[after].typ in {ttParenL, ttBraceL, ttExtends, ttImplements}: + markRangeType(tokens, i, close) + i = close + elif close > i and close + 1 < tokens.len and tokens[close + 1].typ == ttParenL: + let parenClose = findMatching(tokens, close + 1, ttParenL, ttParenR) + if parenClose > close and parenClose + 1 < tokens.len and tokens[parenClose + 1].typ == ttArrow: + markRangeType(tokens, i, close) + i = close + inc i + +proc annotateJsxRoles(code: string, tokens: var seq[JsToken]) = + var stack: seq[tuple[start: int, explicitChildren: int, hasSpread: bool, propSpreadSeen: bool]] = @[] + var i = 0 + while i < tokens.len: + if tokens[i].typ == ttJsxTagStart: + let isClosing = i + 1 < tokens.len and tokens[i + 1].typ == ttSlash + if isClosing: + if stack.len > 0: + let item = stack.pop() + if tokens[item.start].jsxRole != jsxKeyAfterPropSpread: + tokens[item.start].jsxRole = + if item.explicitChildren == 0: jsxNoChildren + elif item.explicitChildren == 1 and not item.hasSpread: jsxOneChild + else: jsxStaticChildren + if stack.len > 0: + stack[^1].explicitChildren += 1 + inc i + continue + + var selfClosing = false + var propSpreadSeen = false + var keyAfterSpread = false + var seenTagName = false + var j = i + 1 + while j < tokens.len and tokens[j].typ != ttJsxTagEnd: + if tokens[j].typ == ttBraceL and j + 1 < tokens.len and tokens[j + 1].typ == ttEllipsis: + propSpreadSeen = true + if tokens[j].typ == ttJsxName: + if not seenTagName: + seenTagName = true + elif j + 1 < tokens.len and tokens[j + 1].typ in {ttEq, ttJsxTagEnd, ttSlash}: + tokens[j].identifierRole = irObjectKey + if propSpreadSeen and tokens[j].typ == ttJsxName and raw(code, tokens[j]) == "key": + keyAfterSpread = true + if tokens[j].typ == ttSlash: + selfClosing = true + inc j + + if keyAfterSpread: + tokens[i].jsxRole = jsxKeyAfterPropSpread + elif selfClosing: + tokens[i].jsxRole = jsxNoChildren + if stack.len > 0: + stack[^1].explicitChildren += 1 + else: + tokens[i].jsxRole = jsxNoChildren + stack.add((i, 0, false, propSpreadSeen)) + i = j + elif stack.len > 0: + if tokens[i].typ == ttJsxText: + stack[^1].explicitChildren += 1 + elif tokens[i].typ == ttBraceL: + let next = i + 1 + if next < tokens.len and tokens[next].typ == ttEllipsis: + stack[^1].hasSpread = true + stack[^1].explicitChildren += 1 + else: + let close = findMatching(tokens, i, ttBraceL, ttBraceR) + if close < 0 or close == i + 1: + discard + else: + stack[^1].explicitChildren += 1 + inc i + +proc annotateOptionalAndNullish(tokens: var seq[JsToken]) = + for i in 0.. 0 and tokens[start].typ notin {ttComma, ttSemi, ttParenL, ttBraceL, ttBracketL, ttEq}: + dec start + if start < i and tokens[start].typ in {ttComma, ttSemi, ttParenL, ttBraceL, ttBracketL, ttEq}: + inc start + tokens[start].numNullishCoalesceStarts += 1 + var finish = i + 1 + while finish < tokens.len and tokens[finish].typ notin {ttComma, ttSemi, ttParenR, ttBraceR, ttBracketR, ttEof}: + inc finish + if finish > i + 1: + tokens[finish - 1].numNullishCoalesceEnds += 1 + + if tokens[i].typ == ttQuestionDot: + var start = i - 1 + while start > 0 and tokens[start].typ notin {ttComma, ttSemi, ttParenL, ttBraceL, ttBracketL, ttEq}: + dec start + if start < i and tokens[start].typ in {ttComma, ttSemi, ttParenL, ttBraceL, ttBracketL, ttEq}: + inc start + tokens[start].isOptionalChainStart = true + var finish = i + 1 + while finish < tokens.len and tokens[finish].typ notin {ttComma, ttSemi, ttParenR, ttBraceR, ttBracketR, ttEof}: + inc finish + if finish > i + 1: + tokens[finish - 1].isOptionalChainEnd = true + tokens[i].subscriptStartIndex = start + +proc annotateAccessIdentifiers(tokens: var seq[JsToken]) = + for i in 0.. 0: + result.add(" [" & fields.join(",") & "]") + +proc formatAnnotatedTokens*(code: string, file: ParsedFile): string = + for token in file.tokens: + if result.len > 0: + result.add("\n") + result.add(formatAnnotatedToken(code, token)) diff --git a/frameos/src/frameos/js_runtime.nim b/frameos/src/frameos/js_runtime/runtime.nim similarity index 76% rename from frameos/src/frameos/js_runtime.nim rename to frameos/src/frameos/js_runtime/runtime.nim index d6dfde6cb..362ccf97d 100644 --- a/frameos/src/frameos/js_runtime.nim +++ b/frameos/src/frameos/js_runtime/runtime.nim @@ -1,12 +1,13 @@ -# frameos/src/frameos/js_runtime.nim +# frameos/src/frameos/js_runtime/runtime.nim # Centralized JavaScript runtime helpers for interpreted scenes. # Extracted from interpreter.nim so the JS bridge can be reused anywhere. import frameos/types import frameos/values -import assets/vendor as vendorAssets +import frameos/js_runtime/source_map +import frameos/js_runtime/transpiler +import frameos/js_runtime/burrito import lib/tz -import lib/burrito import tables, json, strutils, locks import chrono, times import pixie @@ -25,14 +26,11 @@ type outputTypes: Table[string, string] targetField: string +var jsSourceMapsByCtx = initTable[ptr JSContext, Table[string, SourceLineMap]]() var currentEvalCtx: ptr JSContext var currentEvalEnv: EvalEnv var tzName = "" -var compilerJsLock: Lock var sceneJsLock: Lock -var compilerJs: QuickJS -var compilerJsReady = false -initLock(compilerJsLock) initLock(sceneJsLock) const sceneJsPrelude* = """ @@ -104,26 +102,46 @@ proc getCodeSnippet(node: DiagramNode): string = return node.data["code"].getStr() "" -proc ensureCompilerJsLocked() = - if compilerJsReady: - return - compilerJs = newQuickJS() - discard compilerJs.eval(vendorAssets.getAsset("assets/compiled/vendor/sucrase.js")) - compilerJsReady = true - proc transpileSource*(source: string, filename: string): string = if source.len == 0: return source - withLock compilerJsLock: - ensureCompilerJsLocked() - result = compilerJs.eval("__frameosTranspile(\"" & jsQuote(source) & "\", { filePath: \"" & jsQuote(filename) & "\" })") + transformFrameosScript(source, filename) + +proc transpileSourceWithMap*(source: string, filename: string): TransformResult = + if source.len == 0: + return TransformResult(code: source, sourceMap: identitySourceLineMap(source, filename, filename)) + transform(source, TransformOptions(filePath: filename, transforms: @["typescript", "jsx"])) proc transpileModuleSource*(source: string, filename: string): string = if source.len == 0: return source - withLock compilerJsLock: - ensureCompilerJsLocked() - result = compilerJs.eval("__frameosTranspile(\"" & jsQuote(source) & "\", { filePath: \"" & jsQuote(filename) & "\", transforms: [\"typescript\", \"jsx\", \"imports\"] })") + transformFrameosModule(source, filename) + +proc transpileModuleSourceWithMap*(source: string, filename: string): TransformResult = + if source.len == 0: + return TransformResult(code: source, sourceMap: identitySourceLineMap(source, filename, filename)) + transform(source, TransformOptions(filePath: filename, transforms: @["typescript", "jsx", "imports"])) + +proc registerJsSourceMap*(ctx: ptr JSContext, sourceMap: SourceLineMap) = + if ctx == nil or sourceMap.generatedName.len == 0: + return + if not jsSourceMapsByCtx.hasKey(ctx): + jsSourceMapsByCtx[ctx] = initTable[string, SourceLineMap]() + jsSourceMapsByCtx[ctx][sourceMap.generatedName] = sourceMap + +proc clearJsSourceMaps*(ctx: ptr JSContext) = + if ctx != nil and jsSourceMapsByCtx.hasKey(ctx): + jsSourceMapsByCtx.del(ctx) + +proc mapJsErrorText*(ctx: ptr JSContext, text: string): string = + result = text + if ctx == nil or not jsSourceMapsByCtx.hasKey(ctx): + return + for _, sourceMap in jsSourceMapsByCtx[ctx]: + result = result.rewriteQuickJsLocations(sourceMap) + +proc mapJsErrorText*(text: string, sourceMap: SourceLineMap): string = + text.rewriteQuickJsLocations(sourceMap) proc logCompileError( scene: InterpretedFrameScene, @@ -144,11 +162,31 @@ proc logCompileError( "snippet": snippet }) +proc logCompileError( + scene: InterpretedFrameScene, + nodeId: NodeId, + sourceKind: string, + sourceName: string, + snippet: string, + error: ref CatchableError, + sourceMap: SourceLineMap +) = + scene.logger.log(%*{ + "event": "interpreter:jsCompileError", + "sceneId": scene.id.string, + "nodeId": nodeId.int, + "sourceKind": sourceKind, + "sourceName": sourceName, + "error": error.msg.mapJsErrorText(sourceMap), + "stacktrace": error.getStackTrace().mapJsErrorText(sourceMap), + "snippet": snippet + }) + # ------------------------- # Build JS envelope function # ------------------------- -proc buildEnvelopeFunction(code: string, argNames: seq[string], fnName: string): string = +proc buildEnvelopeFunctionWithMap(code: string, argNames: seq[string], fnName: string, filename: string): tuple[code: string, sourceMap: SourceLineMap] = ## Create a named function returning a BigInt-safe JSON envelope. var decls = newSeq[string]() for rawName in argNames: @@ -158,25 +196,61 @@ proc buildEnvelopeFunction(code: string, argNames: seq[string], fnName: string): continue let ident = toJsIdent(rawName) decls.add("const " & ident & " = __args[\"" & jsQuote(rawName) & "\"];") - let declBlock = decls.join("\n") - - result = """ -function """ & fnName & """() { - "use strict"; - """ & declBlock & """ - try { - const __v = ((state, args, context) => (""" & code & """))(__state, __args, __context); - const __k = (__v === null) ? "null" : (Array.isArray(__v) ? "array" : typeof __v); - const json = (typeof __v === 'undefined') - ? JSON.stringify({ k: __k }) - : JSON.stringify({ k: __k, v: __v }, __jsReplacer); - return json; - } catch (e) { - const msg = (e && e.stack) ? e.stack : String(e); - return JSON.stringify({ k: "error", v: { message: String(e && e.message || e), stack: msg } }); - } -} -""" + + var mapLines: seq[int] = @[0] + var mapSegments: seq[SourceMapSegment] = @[] + template addGeneratedLine(line: string, sourceLine: int = 0) = + if result.code.len > 0: + result.code.add("\n") + result.code.add(line) + mapLines.add(sourceLine) + + addGeneratedLine("function " & fnName & "() {") + addGeneratedLine(" \"use strict\";") + for decl in decls: + addGeneratedLine(" " & decl) + addGeneratedLine(" try {") + + let sourceLines = code.splitLines() + if sourceLines.len == 0: + addGeneratedLine(" const __v = ((state, args, context) => ())(__state, __args, __context);") + else: + for index, line in sourceLines: + let sourceLine = index + 1 + if index == 0 and index == sourceLines.high: + let prefix = " const __v = ((state, args, context) => (" + addGeneratedLine(prefix & line & "))(__state, __args, __context);", sourceLine) + mapSegments.add(SourceMapSegment(generatedLine: mapLines.len - 1, generatedColumn: prefix.len + 1, sourceLine: sourceLine, sourceColumn: 1)) + elif index == 0: + let prefix = " const __v = ((state, args, context) => (" + addGeneratedLine(prefix & line, sourceLine) + mapSegments.add(SourceMapSegment(generatedLine: mapLines.len - 1, generatedColumn: prefix.len + 1, sourceLine: sourceLine, sourceColumn: 1)) + elif index == sourceLines.high: + addGeneratedLine(line & "))(__state, __args, __context);", sourceLine) + mapSegments.add(SourceMapSegment(generatedLine: mapLines.len - 1, generatedColumn: 1, sourceLine: sourceLine, sourceColumn: 1)) + else: + addGeneratedLine(line, sourceLine) + mapSegments.add(SourceMapSegment(generatedLine: mapLines.len - 1, generatedColumn: 1, sourceLine: sourceLine, sourceColumn: 1)) + addGeneratedLine(" const __k = (__v === null) ? \"null\" : (Array.isArray(__v) ? \"array\" : typeof __v);") + addGeneratedLine(" const json = (typeof __v === 'undefined')") + addGeneratedLine(" ? JSON.stringify({ k: __k })") + addGeneratedLine(" : JSON.stringify({ k: __k, v: __v }, __jsReplacer);") + addGeneratedLine(" return json;") + addGeneratedLine(" } catch (e) {") + addGeneratedLine(" const msg = (e && e.stack) ? e.stack : String(e);") + addGeneratedLine(" return JSON.stringify({ k: \"error\", v: { message: String(e && e.message || e), stack: msg } });") + addGeneratedLine(" }") + addGeneratedLine("}") + + result.sourceMap = SourceLineMap( + generatedName: filename, + sourceName: filename, + generatedToSourceLine: mapLines, + segments: mapSegments + ) + +proc buildEnvelopeFunction(code: string, argNames: seq[string], fnName: string): string = + buildEnvelopeFunctionWithMap(code, argNames, fnName, "").code # ------------------------- # QuickJS bridge utilities @@ -488,6 +562,11 @@ proc jsExceptionDetails*(ctx: ptr JSContext): tuple[message: string, stack: stri if result.message.len == 0: result.message = "JavaScript error" +proc mappedJsExceptionDetails*(ctx: ptr JSContext): tuple[message: string, stack: string] = + result = jsExceptionDetails(ctx) + result.message = mapJsErrorText(ctx, result.message) + result.stack = mapJsErrorText(ctx, result.stack) + proc callGlobalFunction*(ctx: ptr JSContext, fnName: string, args: openArray[JSValueConst] = []): JSValue = let globalObj = JS_GetGlobalObject(ctx) defer: JS_FreeValue(ctx, globalObj) @@ -567,13 +646,17 @@ proc compileInlineFn(scene: InterpretedFrameScene, nameBuilder: InlineNameProc) = ensureSceneJs(scene) let fnName = nameBuilder(scene, nodeId, name) + let filename = "" + var sourceMap = buildEnvelopeFunctionWithMap(snippet, @[], fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(snippet, @[], fnName), - "") + let envelope = buildEnvelopeFunctionWithMap(snippet, @[], fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) withLock sceneJsLock: - discard scene.js.eval(src) + discard scene.js.eval(transformed.code, filename) + registerJsSourceMap(scene.js.context, sourceMap) except CatchableError as e: - logCompileError(scene, nodeId, "inline", name, snippet, e) + logCompileError(scene, nodeId, "inline", name, snippet, e, sourceMap) raise if not mappingRef.hasKey(nodeId): mappingRef[nodeId] = initTable[string, string]() @@ -608,13 +691,17 @@ proc compileCodeFn*(scene: InterpretedFrameScene, node: DiagramNode) = if k notin argNames: argNames.add(k) let fnName = uniqueCodeFnName(scene, node.id) + let filename = "" + var sourceMap = buildEnvelopeFunctionWithMap(codeSnippet, argNames, fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(codeSnippet, argNames, fnName), - "") + let envelope = buildEnvelopeFunctionWithMap(codeSnippet, argNames, fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) withLock sceneJsLock: - discard scene.js.eval(src) + discard scene.js.eval(transformed.code, filename) + registerJsSourceMap(scene.js.context, sourceMap) except CatchableError as e: - logCompileError(scene, node.id, "code", "codeJS", codeSnippet, e) + logCompileError(scene, node.id, "code", "codeJS", codeSnippet, e, sourceMap) raise scene.jsFuncNameByNode[node.id] = fnName @@ -670,12 +757,14 @@ proc callCompiledFn*(scene: InterpretedFrameScene, let kind = parsed{"k"}.getStr() if kind == "error": + let message = mapJsErrorText(scene.js.context, parsed{"v"}{"message"}.getStr()) + let stack = mapJsErrorText(scene.js.context, parsed{"v"}{"stack"}.getStr()) scene.logger.log(%*{ "event": "interpreter:jsError", "sceneId": scene.id.string, "nodeId": nodeId.int, - "message": parsed{"v"}{"message"}.getStr(), - "stack": parsed{"v"}{"stack"}.getStr() + "message": message, + "stack": stack }) if expectedType.len > 0: if expectedType == "string": return Value(kind: fkString, s: "") @@ -728,13 +817,17 @@ proc evalSnippet*( ensureSceneJs(scene) inc anonCounter let fnName = "__frameos_eval_" & $(nodeId.int) & "_" & $anonCounter + let filename = "" + var sourceMap = buildEnvelopeFunctionWithMap(code, argNames, fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(code, argNames, fnName), - "") + let envelope = buildEnvelopeFunctionWithMap(code, argNames, fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) withLock sceneJsLock: - discard scene.js.eval(src) + discard scene.js.eval(transformed.code, filename) + registerJsSourceMap(scene.js.context, sourceMap) except CatchableError as e: - logCompileError(scene, nodeId, "eval", fnName, code, e) + logCompileError(scene, nodeId, "eval", fnName, code, e, sourceMap) raise var outs = outputTypes @@ -758,14 +851,7 @@ proc transpileSourceForTest*(source: string, filename: string = ""): strin transpileSource(source, filename) proc cleanupCompilerJs*() = - withLock compilerJsLock: - if not compilerJsReady: - return - if compilerJs.runtime != nil: - compilerJs.runPendingJobs() - JS_RunGC(compilerJs.runtime) - compilerJs.close() - compilerJsReady = false + discard proc cleanupSceneJs*(scene: InterpretedFrameScene) = withLock sceneJsLock: @@ -774,6 +860,8 @@ proc cleanupSceneJs*(scene: InterpretedFrameScene) = if scene.js.context != nil and currentEvalCtx == scene.js.context: currentEvalCtx = nil currentEvalEnv = nil + if scene.js.context != nil: + clearJsSourceMaps(scene.js.context) if scene.js.runtime != nil: scene.js.runPendingJobs() JS_RunGC(scene.js.runtime) diff --git a/frameos/src/frameos/js_runtime/source_map.nim b/frameos/src/frameos/js_runtime/source_map.nim new file mode 100644 index 000000000..cbd987940 --- /dev/null +++ b/frameos/src/frameos/js_runtime/source_map.nim @@ -0,0 +1,226 @@ +import std/[sequtils, strutils] + +type + SourceMapSegment* = object + generatedLine*: int + generatedColumn*: int + sourceLine*: int + sourceColumn*: int + + SourceLineMap* = object + generatedName*: string + sourceName*: string + generatedToSourceLine*: seq[int] + segments*: seq[SourceMapSegment] + +proc sourceLineCount*(source: string): int = + result = 1 + for ch in source: + if ch == '\n': + inc result + +proc emptySourceLineMap*(generatedName, sourceName: string, generatedLineCount = 1): SourceLineMap = + result.generatedName = generatedName + result.sourceName = sourceName + result.generatedToSourceLine = newSeq[int](max(1, generatedLineCount) + 1) + +proc identitySourceLineMap*(source, generatedName, sourceName: string): SourceLineMap = + result = emptySourceLineMap(generatedName, sourceName, source.sourceLineCount()) + for line in 1..= lcs[generatedIndex][sourceIndex + 1]: + inc generatedIndex + else: + inc sourceIndex + + for line in 1.. 0: + let estimated = lastSourceLine + (line - lastGeneratedLine) + if estimated >= 1 and estimated <= max(1, sourceLines.len): + result.generatedToSourceLine[line] = estimated + elif line <= sourceLines.len: + result.generatedToSourceLine[line] = line + if result.generatedToSourceLine[line] > 0 and line <= generatedLines.len and result.generatedToSourceLine[line] <= sourceLines.len: + result.addLineSegments(line, generatedLines[line - 1], result.generatedToSourceLine[line], sourceLines[result.generatedToSourceLine[line] - 1]) + +proc withGeneratedName*(sourceMap: SourceLineMap, generatedName: string): SourceLineMap = + result = sourceMap + result.generatedName = generatedName + +proc mapGeneratedLine*(sourceMap: SourceLineMap, generatedLine: int): int = + if generatedLine > 0 and generatedLine < sourceMap.generatedToSourceLine.len: + sourceMap.generatedToSourceLine[generatedLine] + else: + 0 + +proc mapGeneratedPosition*(sourceMap: SourceLineMap, generatedLine, generatedColumn: int): tuple[line: int, column: int] = + result.line = sourceMap.mapGeneratedLine(generatedLine) + result.column = if generatedColumn > 0: generatedColumn else: 1 + + var best: SourceMapSegment + var hasBest = false + for segment in sourceMap.segments: + if segment.generatedLine == generatedLine and segment.generatedColumn <= result.column: + if not hasBest or segment.generatedColumn > best.generatedColumn: + best = segment + hasBest = true + + if hasBest: + result.line = best.sourceLine + result.column = max(1, best.sourceColumn + (result.column - best.generatedColumn)) + +proc composeSourceLineMaps*(outer, inner: SourceLineMap): SourceLineMap = + result = emptySourceLineMap( + outer.generatedName, + if inner.sourceName.len > 0: inner.sourceName else: outer.sourceName, + max(0, outer.generatedToSourceLine.len - 1) + ) + for line in 1.. 0 and intermediateLine < inner.generatedToSourceLine.len: + result.generatedToSourceLine[line] = inner.generatedToSourceLine[intermediateLine] + for segment in outer.segments: + let mapped = inner.mapGeneratedPosition(segment.sourceLine, segment.sourceColumn) + if mapped.line > 0: + result.segments.add(SourceMapSegment( + generatedLine: segment.generatedLine, + generatedColumn: segment.generatedColumn, + sourceLine: mapped.line, + sourceColumn: mapped.column + )) + +proc addSourceSegment*(sourceMap: var SourceLineMap, generatedLine, generatedColumn, sourceLine, sourceColumn: int) = + if generatedLine <= 0 or generatedColumn <= 0 or sourceLine <= 0 or sourceColumn <= 0: + return + sourceMap.segments.add(SourceMapSegment( + generatedLine: generatedLine, + generatedColumn: generatedColumn, + sourceLine: sourceLine, + sourceColumn: sourceColumn + )) + +proc rewriteQuickJsLocations*(text: string, sourceMap: SourceLineMap): string = + if text.len == 0 or sourceMap.generatedName.len == 0: + return text + + var i = 0 + while i < text.len: + let at = text.find(sourceMap.generatedName & ":", i) + if at < 0: + result.add(text[i..^1]) + break + + result.add(text[i.. columnStart + + let generatedColumn = + if hasColumn: parseInt(text[columnStart.. 0: + result.add(sourceMap.sourceName) + result.add(":") + result.add($mapped.line) + if hasColumn: + result.add(":") + result.add($mapped.column) + else: + result.add(text[at..<(if hasColumn: columnEnd else: lineEnd)]) + i = if hasColumn: columnEnd else: lineEnd diff --git a/frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim b/frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim new file mode 100644 index 000000000..20a73d9e3 --- /dev/null +++ b/frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim @@ -0,0 +1,331 @@ +import std/[json, sequtils, strutils, tables, unittest] +import pixie + +import frameos/js_runtime/app_runtime +import frameos/types +import frameos/values + +proc testConfig(): FrameConfig = + FrameConfig( + width: 6, + height: 4, + rotate: 0, + scalingMode: "cover", + debug: true, + saveAssets: %*false, + assetsPath: "/tmp" + ) + +proc testLogger(config: FrameConfig): Logger = + var logger = Logger(frameConfig: config, enabled: true) + logger.log = proc(payload: JsonNode) = + discard payload + logger.enable = proc() = + logger.enabled = true + logger.disable = proc() = + logger.enabled = false + logger + +suite "js app runtime": + test "returns string, node, and image values": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 7.NodeId, nodeName: "jsText", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "text", + source = """export const get = (app: { config: { mode: string; message?: string; targetNode?: number } }, context: { event: string }) => { + if (app.config.mode === "image") { + return + } + if (app.config.mode === "node") { + return frameos.node(app.config.targetNode) + } + return `${app.config.message}:${context.event}` + }""" + ) + + let textValue = runtime.get(owner, %*{"message": "hello", "mode": "text"}, context) + check textValue.kind == fkString + check textValue.asString() == "hello:render" + + let nodeValue = runtime.get(owner, %*{"mode": "node", "targetNode": 9}, context) + check nodeValue.kind == fkNode + check nodeValue.asNode() == 9.NodeId + + let imageValue = runtime.get(owner, %*{"mode": "image"}, context) + check imageValue.kind == fkImage + check imageValue.asImage().width == 3 + check imageValue.asImage().height == 2 + + test "run can set next sleep, state, and draw a render image": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-run".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 8.NodeId, nodeName: "jsLogic", scene: scene, frameConfig: config) + var image = newImage(4, 3) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: true, image: image, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "render", + outputType = "image", + source = """export function run(app: { config: { duration: number } }) { + frameos.setNextSleep(app.config.duration) + frameos.setState("lastDuration", app.config.duration) + return + }""" + ) + + runtime.run(owner, %*{"duration": 12.5}, context) + check abs(context.nextSleep - 12.5) < 0.0001 + check scene.state["lastDuration"].getFloat() == 12.5 + let pixel = context.image.data[context.image.dataIndex(0, 0)] + check pixel.r > 0 + check runtime.images.len == 0 + + test "clears transient context image refs after JS calls": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-image-refs".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 9.NodeId, nodeName: "jsImageRefs", scene: scene, frameConfig: config) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "image", + source = """export function get(app, context) { + return context.image + }""" + ) + + for i in 0..<3: + let image = newImage(4 + i, 3) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: true, image: image, loopIndex: i, loopKey: ".", nextSleep: -1) + let value = runtime.get(owner, %*{}, context) + check value.kind == fkImage + check value.asImage().width == 4 + i + check value.asImage().height == 3 + check runtime.images.len == 0 + + test "runs typed template literal interpolations": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-template".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 11.NodeId, nodeName: "jsTemplate", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "text", + source = """export function get(app: FrameOSApp): string { + const label = app.config.label as string + return `${label as string}` + }""" + ) + + let value = runtime.get(owner, %*{"label": "FrameOS"}, context) + check value.kind == fkString + check value.asString() == "FrameOS" + + test "runs text app template init and get functions": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-text-template".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 12.NodeId, nodeName: "jsText", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "text", + source = """export function init(app: FrameOSApp): void { + app.initialized = true + } + + export function get(app: FrameOSApp, context: FrameOSContext): string { + const eventLabel = context.event ? ` (${context.event})` : '' + return `${app.config.prefix}: ${app.config.message}${app.initialized ? eventLabel : ''}` + }""" + ) + + let value = runtime.get(owner, %*{"prefix": "FrameOS", "message": "Hello"}, context) + check value.kind == fkString + check value.asString() == "FrameOS: Hello (render)" + + test "runs image app template frameos.image output": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-image-template".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 13.NodeId, nodeName: "jsImage", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "image", + source = """export function get(app: FrameOSApp): FrameOSImageSpec { + return frameos.image({ + width: app.config.width, + height: app.config.height, + color: app.config.color, + opacity: app.config.opacity, + }) + }""" + ) + + let value = runtime.get(owner, %*{"width": 5, "height": 3, "color": "#00ff00", "opacity": 0.5}, context) + check value.kind == fkImage + check value.asImage().width == 5 + check value.asImage().height == 3 + let pixel = value.asImage().data[value.asImage().dataIndex(0, 0)] + check pixel.g > 0 + check pixel.a > 0 + + test "runs logic app template logging path": + let config = testConfig() + var logged: seq[JsonNode] = @[] + var logger = testLogger(config) + logger.log = proc(payload: JsonNode) = + logged.add(payload) + let scene = FrameScene(id: "tests/js-app-logic-template".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 14.NodeId, nodeName: "jsLogic", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "logic", + outputType = "", + source = """export function run(app: FrameOSApp, context: FrameOSContext): void { + const stateKey = app.config.stateKey || 'jsLogicResult' + app.log('JS logic app ran', { event: context.event, stateKey }) + }""" + ) + + runtime.run(owner, %*{"stateKey": "customState"}, context) + check logged.len > 0 + check logged[^1]["event"].getStr() == "log:14:jsLogic" + check "JS logic app ran" in logged[^1]["message"].getStr() + check "customState" in logged[^1]["message"].getStr() + + test "runs modern ES syntax supported by QuickJS": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-modern-es".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 15.NodeId, nodeName: "jsModernEs", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "integer", + source = """export function get(app: FrameOSApp): number { + class Counter { + static label = "counter" + #step = 1n + value = 1_000 + increment = () => { + this.value += Number(this.#step) + return this.value + } + } + try { + const counter = new Counter() + let configured = app.config?.nested?.count ?? 0 + configured ||= counter.increment() + const regex = /frame\s*os/i + return regex.test("Frame OS") && Counter.label === "counter" ? configured : 0 + } catch { + return -1 + } + }""" + ) + + let fallbackValue = runtime.get(owner, %*{}, context) + check fallbackValue.kind == fkInteger + check fallbackValue.asInt() == 1001 + + let configuredValue = runtime.get(owner, %*{"nested": {"count": 42}}, context) + check configuredValue.kind == fkInteger + check configuredValue.asInt() == 42 + + test "lazy app proxies support keys and spread": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-proxy-keys".SceneId, frameConfig: config, state: %*{"seen": true}, logger: logger) + let owner = AppRoot(nodeId: 11.NodeId, nodeName: "jsProxyKeys", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "json", + source = """export function get(app, context) { + return { + configKeys: Object.keys(app.config).sort(), + stateKeys: Object.keys(app.state).sort(), + frameKeys: Object.keys(app.frame).sort(), + contextKeys: Object.keys(context).sort(), + spreadConfig: { ...app.config }, + } + }""" + ) + + let value = runtime.get(owner, %*{"message": "hello", "mode": "text"}, context) + check value.kind == fkJson + let payload = value.asJson() + check payload["configKeys"][0].getStr() == "message" + check payload["configKeys"][1].getStr() == "mode" + check payload["stateKeys"][0].getStr() == "seen" + check "width" in payload["frameKeys"].mapIt(it.getStr()) + check "event" in payload["contextKeys"].mapIt(it.getStr()) + check payload["spreadConfig"]["message"].getStr() == "hello" + + test "maps JS app runtime errors to original source lines": + let config = testConfig() + var logged: seq[JsonNode] = @[] + var logger = testLogger(config) + logger.log = proc(payload: JsonNode) = + logged.add(payload) + let scene = FrameScene(id: "tests/js-app-error-map".SceneId, frameConfig: config, state: %*{}, logger: logger) + let owner = AppRoot(nodeId: 16.NodeId, nodeName: "jsErrorMap", scene: scene, frameConfig: config) + let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) + + let runtime = newJsAppRuntime( + category = "data", + outputType = "text", + source = """export function get(app: FrameOSApp): string { + const value: number = 1 + throw new Error("app mapped boom") + }""" + ) + + discard runtime.get(owner, %*{}, context) + let stackLogs = logged.filterIt("jsApp:error" in it{"event"}.getStr()) + check stackLogs.len == 1 + check ">:3:" in stackLogs[0]{"stack"}.getStr() + + test "releases overwritten dynamic field image refs": + let config = testConfig() + let logger = testLogger(config) + let scene = FrameScene(id: "tests/js-app-field-refs".SceneId, frameConfig: config, state: %*{}, logger: logger) + let runtime = newJsAppRuntime(category = "data", outputType = "image", source = "export const get = () => null") + let app = DynamicJsApp( + nodeId: 10.NodeId, + nodeName: "jsFieldRefs", + scene: scene, + frameConfig: config, + configJson: %*{}, + runtime: runtime + ) + + setDynamicJsAppField(app, "inputImage", VImage(newImage(4, 3))) + check runtime.images.len == 1 + let firstId = app.configJson["inputImage"]["id"].getInt() + check runtime.images.hasKey(firstId) + + setDynamicJsAppField(app, "inputImage", VImage(newImage(5, 3))) + check runtime.images.len == 1 + check not runtime.images.hasKey(firstId) + let secondId = app.configJson["inputImage"]["id"].getInt() + check secondId != firstId + check runtime.images.hasKey(secondId) + + setDynamicJsAppField(app, "inputImage", VString("not an image")) + check runtime.images.len == 0 diff --git a/frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim b/frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim new file mode 100644 index 000000000..4f7b3ebbb --- /dev/null +++ b/frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim @@ -0,0 +1,148 @@ +import std/[sequtils, strutils, unittest] + +import frameos/js_runtime/parser +import frameos/js_runtime/token_processor +import frameos/js_runtime/tokens + +proc tokensOf(code: string): seq[JsToken] = + parseJs(code).tokens + +proc tokenText(code: string, token: JsToken): string = + if token.start >= 0 and token.`end` <= code.len and token.start <= token.`end`: + code[token.start.. 0) + + test "marks import/export binding roles": + let code = """ +import DefaultThing, { value as renamed, other } from "pkg"; +export { renamed as publicName }; +export const answer = 42; +""" + check firstToken(code, "DefaultThing").identifierRole == irImportDeclaration + check firstToken(code, "value").identifierRole == irImportAccess + check firstToken(code, "renamed").identifierRole == irImportDeclaration + check firstToken(code, "other").identifierRole == irImportDeclaration + + let exportedRenamed = tokensOf(code).filterIt(tokenText(code, it) == "renamed" and it.identifierRole == irExportAccess) + check exportedRenamed.len == 1 + check firstToken(code, "answer").identifierRole == irTopLevelDeclaration + + test "marks TypeScript type spans": + let code = """ +type Alias = { value: T }; +interface Input { value?: string } +const answer: number = 42; +const label = answer as number satisfies number; +""" + let tokens = tokensOf(code) + for text in ["Alias", "Input"]: + check tokens.anyIt(tokenText(code, it) == text and it.isType) + check tokens.anyIt(tokenText(code, it) == ":" and it.isType) + check tokens.anyIt(tokenText(code, it) == "number" and it.isType) + check tokens.anyIt(tokenText(code, it) == "as" and it.isType) + check tokens.anyIt(tokenText(code, it) == "satisfies" and it.isType) + + test "marks JSX roles": + let code = """ +const empty =
; +const one =
{child}
; +const many =
{child}
; +const keyed =
; +""" + let allTokens = tokensOf(code) + var jsxStarts: seq[JsToken] = @[] + for index, token in allTokens: + if token.typ == ttJsxTagStart and (index + 1 >= allTokens.len or allTokens[index + 1].typ != ttSlash): + jsxStarts.add(token) + check jsxStarts[0].jsxRole == jsxNoChildren + check jsxStarts[1].jsxRole == jsxOneChild + check jsxStarts[2].jsxRole == jsxStaticChildren + check jsxStarts[^1].jsxRole == jsxKeyAfterPropSpread + + test "marks optional chain and nullish boundaries": + let code = "const result = app.config?.nested?.count ?? 1;" + let tokens = tokensOf(code) + check tokens.anyIt(it.isOptionalChainStart) + check tokens.anyIt(it.isOptionalChainEnd) + check tokens.anyIt(it.numNullishCoalesceStarts > 0) + check tokens.anyIt(it.numNullishCoalesceEnds > 0) + +suite "native js token processor": + test "copies all tokens while preserving source": + let code = "const value = 1;\n// trailing\n" + var processor = initTokenProcessor(code, tokenizeJs(code)) + check processor.copyAll().code == code + + test "removes annotated type tokens while preserving runtime whitespace": + let code = "const value: number = 1;\n" + let parsed = parseJs(code) + var processor = initTokenProcessor(code, parsed.tokens) + while not processor.isAtEnd(): + if processor.currentToken().isType: + processor.removeToken() + else: + processor.copyToken() + let output = processor.finish().code + check "value: number" notin output + check "const value = 1;" in output + + test "replaces tokens and records mappings": + let code = "const value = 1;" + var processor = initTokenProcessor(code, tokenizeJs(code)) + while not processor.isAtEnd(): + if processor.currentTokenCode() == "value": + processor.replaceToken("renamed") + else: + processor.copyToken() + let result = processor.finish() + check result.code == "const renamed = 1;" + var valueIndex = -1 + for index, token in tokenizeJs(code): + if tokenText(code, token) == "value": + valueIndex = index + break + check result.mappings[valueIndex] == "const ".len + + test "supports snapshots for lookahead-style rewrites": + let code = "const value = 1;" + var processor = initTokenProcessor(code, tokenizeJs(code)) + let snapshot = processor.snapshot() + processor.copyToken() + processor.copyToken() + let removed = processor.dangerouslyGetAndRemoveCodeSinceSnapshot(snapshot) + check removed == "const value" + processor.restoreToSnapshot(snapshot) + processor.replaceToken("let") + while not processor.isAtEnd(): + processor.copyToken() + check processor.finish().code == "let value = 1;" diff --git a/frameos/src/frameos/tests/test_js_runtime_helpers.nim b/frameos/src/frameos/js_runtime/tests/test_js_runtime_helpers.nim similarity index 83% rename from frameos/src/frameos/tests/test_js_runtime_helpers.nim rename to frameos/src/frameos/js_runtime/tests/test_js_runtime_helpers.nim index ffe8db88f..11aca8028 100644 --- a/frameos/src/frameos/tests/test_js_runtime_helpers.nim +++ b/frameos/src/frameos/js_runtime/tests/test_js_runtime_helpers.nim @@ -1,8 +1,8 @@ -import std/[json, strutils, unittest] +import std/[json, sequtils, strutils, unittest] -import ../js_runtime -import ../types -import ../values +import frameos/js_runtime/runtime +import frameos/types +import frameos/values proc testScene(): InterpretedFrameScene = InterpretedFrameScene( @@ -113,6 +113,27 @@ function demo(input: number) { cleanupSceneJs(scene) cleanupCompilerJs() + test "evalSnippet maps runtime errors to original source lines": + var logs: seq[JsonNode] = @[] + var scene = testScene() + scene.logger.log = proc(payload: JsonNode) = + logs.add(payload) + + let value = evalSnippet( + scene, + testContext(scene), + 2.NodeId, + "(() => {\n const count: number = 1\n throw new Error(\"mapped boom\")\n})()" + ) + + check value.kind == fkNone + let errorLogs = logs.filterIt(it{"event"}.getStr() == "interpreter:jsError") + check errorLogs.len == 1 + check "mapped boom" in errorLogs[0]{"message"}.getStr() + check ">:3:18" in errorLogs[0]{"stack"}.getStr() + cleanupSceneJs(scene) + cleanupCompilerJs() + test "cleanupSceneJs closes the quickjs runtime": var scene = testScene() ensureSceneJs(scene) diff --git a/frameos/src/frameos/js_runtime/tests/test_js_tokens.nim b/frameos/src/frameos/js_runtime/tests/test_js_tokens.nim new file mode 100644 index 000000000..bd5c2b22b --- /dev/null +++ b/frameos/src/frameos/js_runtime/tests/test_js_tokens.nim @@ -0,0 +1,72 @@ +import std/[sequtils, unittest] + +import frameos/js_runtime/tokens + +proc tokenNames(code: string): seq[string] = + tokenizeJs(code).mapIt(formatTokenType(it.typ)) + +suite "native js tokenizer": + test "recognizes plain expressions, division, regex, and modern operators": + check tokenNames("5/3/1") == @["num", "/", "num", "/", "num", "eof"] + check tokenNames("5 + /3/") == @["num", "+", "regexp", "eof"] + check tokenNames("a?.b ?? c && d || e") == @["name", "?.", "name", "??", "name", "&&", "name", "||", "name", "eof"] + check tokenNames("value ||= 1; count &&= 2; n ??= 3") == @["name", "_=", "num", ";", "name", "_=", "num", ";", "name", "_=", "num", "eof"] + + test "distinguishes JSX from relational operators": + check tokenNames("x2") == @["name", "<", "name", ">", "num", "eof"] + check tokenNames("x + < Hello / >") == @["name", "+", "jsxTagStart", "jsxName", "/", "jsxTagEnd", "eof"] + + test "recognizes nested JSX content": + let names = tokenNames(""" +
+ Hello, world! + +
+""") + check names == @[ + "jsxTagStart", "jsxName", "jsxName", "=", "string", "jsxTagEnd", + "jsxText", "jsxTagStart", "jsxName", "jsxName", "=", "{", "name", + "}", "/", "jsxTagEnd", "jsxEmptyText", "jsxTagStart", "/", "jsxName", + "jsxTagEnd", "eof", + ] + + test "recognizes template string boundaries and expressions": + check tokenNames("`Hello, ${name} ${surname}`") == @[ + "`", "template", "${", "name", "}", "template", "${", "name", "}", + "template", "`", "eof", + ] + + test "distinguishes pre-increment and post-increment": + check tokenNames(""" +a = b +++c +d++ +e = f++ +g = ++h +""") == @[ + "name", "=", "name", "++/--", "name", "name", "++/--", "name", "=", + "name", "++/--", "name", "=", "++/--", "name", "eof", + ] + + test "tracks contextual keywords": + let tokens = tokenizeJs("""import foo from "./foo.json" with {type: "json"};""") + check tokens.mapIt(formatTokenType(it.typ)) == @[ + "import", "name", "name", "string", "with", "{", "name", ":", "string", + "}", ";", "eof", + ] + check tokens[2].contextualKeyword == ckFrom + check tokens[6].contextualKeyword == ckType + + test "recognizes private-property punctuation": + check tokenNames(""" +class { + #x = 3 +} +this.#x = 3 +delete this?.#x +if (#x in obj) { } +""") == @[ + "class", "{", "#", "name", "=", "num", "}", "this", ".", "#", "name", + "=", "num", "delete", "this", "?.", "#", "name", "if", "(", "#", + "name", "in", "name", ")", "{", "}", "eof", + ] diff --git a/frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim b/frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim new file mode 100644 index 000000000..a841d1d3b --- /dev/null +++ b/frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim @@ -0,0 +1,268 @@ +import std/[strutils, unittest] + +import frameos/js_runtime/transpiler + +suite "native js transpiler": + test "strips common TypeScript syntax": + let output = transformFrameosScript(""" +interface Removed { value: number } +type AlsoRemoved = { value: string }; +const answer: number = 42; +function demo(input: number): number { + return input as number; +} +const fn = ({count}: {count: number}) => count satisfies number; +""") + check "interface Removed" notin output + check "type AlsoRemoved" notin output + check "answer: number" notin output + check "input: number" notin output + check "as number" notin output + check "satisfies number" notin output + check "const answer = 42" in output + + test "lowers classic JSX to FrameOS runtime calls": + let output = transformFrameosScript(""" +function demo(input: number) { + return 0}>{input as number}; +} +""") + check "__frameosJsx(\"card\"" in output + check "\"active\": input > 0" in output + check "input: number" notin output + check "as number" notin output + + test "rewrites simple app module exports": + let output = transformFrameosModule(""" +export const makeImage = (app: { config: { message?: string } }) => { + return ; +} +export function run(app: { config: { duration: number } }) { + return app.config.duration; +} +export function get(app, context) { + return context.image; +} +""") + check output.startsWith("\"use strict\";") + check "const makeImage = (app) =>" in output + check "function run(app)" in output + check "function get(app, context)" in output + check "exports.get = get;" in output + check "exports.makeImage = makeImage;" in output + check "exports.run = run;" in output + check "__frameosJsx(\"image\"" in output + + test "rewrites broader export declarations": + let output = transformFrameosModule(""" +export const first = 1, second = 2; +export async function load(): Promise { + return first + second; +} +export default async function namedDefault(): Promise { + return load(); +} +""") + check "const first = 1, second = 2;" in output + check "exports.first = first;" in output + check "exports.second = second;" in output + check "async function load()" in output + check "exports.load = load;" in output + check "async function namedDefault()" in output + check "exports.default = namedDefault;" in output + + test "lowers TypeScript enums": + let output = transformFrameosModule(""" +export enum Mode { + First, + Second = First + 2, + Label = "label", + "spaced key" = 9, +} +""") + check "var Mode; (function (Mode)" in output + check "const First = 0;" in output + check "Mode[Mode[\"First\"] = First] = \"First\";" in output + check "const Second = First + 2;" in output + check "const Label = \"label\";" in output + check "Mode[\"spaced key\"] = 9" in output + check "exports.Mode = Mode;" in output + + test "rewrites static imports to CommonJS declarations": + let output = transformFrameosModule(""" +import "./setup"; +import DefaultThing, { value as renamed, other } from "pkg"; +import * as tools from "./tools"; +export { renamed as publicName }; +export { other as remoteOther } from "pkg2"; +export * as everything from "pkg3"; +export * from "pkg4"; +""") + check "require(\"./setup\");" in output + check "require(\"pkg\")" in output + check "var DefaultThing =" in output + check "var renamed =" in output + check ".value;" in output + check "var other =" in output + check ".other;" in output + check "var tools =" in output + check "exports.publicName = renamed;" in output + check "exports.remoteOther =" in output + check "exports.everything = require(\"pkg3\");" in output + check "Object.keys(" in output + + test "rewrites TypeScript import equals": + let output = transformFrameosModule(""" +import tool = require("toolkit"); +export const value = tool.value; +""") + check "const tool = require(\"toolkit\");" in output + check "exports.value = value;" in output + + test "lowers fragments and decodes JSX entities": + let output = transformFrameosScript(""" +const value = <>A < B !; +""") + check "__frameosJsx(__frameosFragment, null" in output + check "\"Tom & Jerry\"" in output + check "\"A < B !\"" in output + + test "strips generics and TypeScript-only modifiers": + let output = transformFrameosScript(""" +abstract class Box { + public readonly value: T; + getValue(): T { + return this.value; + } + constructor(value: T) { + this.value = identity(value); + } +} +function identity(value: T): T { + return value; +} +const arrow = (value: T): T => value; +""") + check "abstract" notin output + check "public" notin output + check "readonly" notin output + check "Box" notin output + check "identity" notin output + check "(value" notin output + check "getValue()" in output + check "getValue(): T" notin output + check "function identity(value)" in output + check "const arrow = (value)" in output + check "=> value" in output + + test "strips multiple variable declarator types without touching initializers": + let output = transformFrameosScript(""" +const first: number = 1, second: string = "two", obj = { label: "ok", nested: { count: 1 } }; +let third: boolean, fourth: number = 4; +""") + check "first: number" notin output + check "second: string" notin output + check "third: boolean" notin output + check "fourth: number" notin output + check "const first = 1, second = \"two\"" in output + check "obj = { label: \"ok\", nested: { count: 1 } }" in output + + test "strips types inside template literal interpolations": + let output = transformFrameosModule(""" +export function get(app: FrameOSApp): string { + const label = app.config.label as string; + const eventLabel = app.context.event ? ` (${app.context.event})` : ''; + return `${label as string}${(app.config.title satisfies string)}`; +} +""") + check "app: FrameOSApp" notin output + check "as string" notin output + check "satisfies string" notin output + check "app.context.event ? ` (${app.context.event})` : ''" in output + check "${label" in output + check "${(app.config.title" in output + + test "strips definite assignment member annotations": + let output = transformFrameosScript(""" +class AppState { + value!: string; + optional?: number; +} +""") + check "value!: string" notin output + check "optional?: number" notin output + check "value;" in output + check "optional;" in output + + test "preserves runtime type identifiers and object keys": + let output = transformFrameosScript(""" +type Alias = { label: string }; +interface Removed { label: string } +const type = "image"; +const spec = { type: "image", props: { type: "text" } }; +function get(type: string) { + return { type }; +} +""") + check "type Alias" notin output + check "interface Removed" notin output + check "const type = \"image\";" in output + check "{ type: \"image\", props: { type: \"text\" } }" in output + check "function get(type)" in output + + test "preserves runtime as and satisfies object keys": + let output = transformFrameosScript(""" +const metadata = { as: "alias", satisfies: true }; +const alias = metadata.as as string; +const ok = metadata.satisfies satisfies boolean; +""") + check "{ as: \"alias\", satisfies: true }" in output + check "metadata.as as string" notin output + check "metadata.satisfies satisfies boolean" notin output + check "const alias = metadata.as" in output + check "const ok = metadata.satisfies" in output + + test "preserves modern ES syntax supported by QuickJS": + let output = transformFrameosScript(""" +class Counter { + static label = "counter"; + #step = 1n; + value = 1_000; + increment = () => { + this.value += Number(this.#step); + return this.value; + } +} +try { + let result = app.config?.nested?.count ?? 0; + result ||= new Counter().increment(); + const regex = /type\s*:\s*image/g; + const ratio = result / 2; +} catch { + console.log("optional catch binding"); +} +""") + check "static label = \"counter\";" in output + check "#step = 1n;" in output + check "value = 1_000;" in output + check "this.value += Number(this.#step)" in output + check "app.config?.nested?.count ?? 0" in output + check "result ||= new Counter().increment()" in output + check "/type\\s*:\\s*image/g" in output + check "result / 2" in output + check "} catch {" in output + + test "lowers constructor parameter properties": + let output = transformFrameosModule(""" +class Box { + constructor(public value: string, private readonly count: number = 1) {} +} +export function get() { + return new Box("ok").value; +} +""") + check "constructor(value, count" in output + check "this.value = value;" in output + check "this.count = count;" in output + check "public value" notin output + check "private readonly" notin output diff --git a/frameos/src/frameos/tests/test_scene_runtime_cleanup.nim b/frameos/src/frameos/js_runtime/tests/test_scene_runtime_cleanup.nim similarity index 93% rename from frameos/src/frameos/tests/test_scene_runtime_cleanup.nim rename to frameos/src/frameos/js_runtime/tests/test_scene_runtime_cleanup.nim index 71ec247dc..84fb20582 100644 --- a/frameos/src/frameos/tests/test_scene_runtime_cleanup.nim +++ b/frameos/src/frameos/js_runtime/tests/test_scene_runtime_cleanup.nim @@ -1,8 +1,8 @@ import std/[json, tables, unittest] -import ../js_runtime -import ../scenes -import ../types +import frameos/js_runtime/runtime +import frameos/scenes +import frameos/types proc testLogger(): Logger = Logger( diff --git a/frameos/src/frameos/js_runtime/token_processor.nim b/frameos/src/frameos/js_runtime/token_processor.nim new file mode 100644 index 000000000..43c3c59da --- /dev/null +++ b/frameos/src/frameos/js_runtime/token_processor.nim @@ -0,0 +1,218 @@ +# TokenProcessor-style rewrite stream for the native FrameOS JS transpiler. +# It preserves original whitespace/comments between tokens and records +# token-index to output-position mappings for future diagnostics/source maps. + +import std/sequtils + +import ./tokens + +type + TokenProcessorSnapshot* = object + resultCode*: string + tokenIndex*: int + + TokenProcessorResult* = object + code*: string + mappings*: seq[int] + + TokenProcessor* = object + code*: string + tokens*: seq[JsToken] + resultCode*: string + resultMappings*: seq[int] + tokenIndex*: int + +proc initTokenProcessor*(code: string, tokens: seq[JsToken]): TokenProcessor = + TokenProcessor( + code: code, + tokens: tokens, + resultMappings: newSeqWith(tokens.len, -1), + ) + +proc isAtEnd*(processor: TokenProcessor): bool = + processor.tokenIndex >= processor.tokens.len + +proc currentIndex*(processor: TokenProcessor): int = + processor.tokenIndex + +proc currentToken*(processor: TokenProcessor): JsToken = + if processor.isAtEnd(): + raise newException(ValueError, "Unexpectedly reached end of input.") + processor.tokens[processor.tokenIndex] + +proc tokenAtRelativeIndex*(processor: TokenProcessor, relativeIndex: int): JsToken = + let index = processor.tokenIndex + relativeIndex + if index < 0 or index >= processor.tokens.len: + raise newException(ValueError, "Token lookaround out of bounds.") + processor.tokens[index] + +proc rawCodeForToken*(processor: TokenProcessor, token: JsToken): string = + if token.start >= 0 and token.`end` <= processor.code.len and token.start <= token.`end`: + processor.code[token.start..<token.`end`] + else: + "" + +proc currentTokenCode*(processor: TokenProcessor): string = + processor.rawCodeForToken(processor.currentToken()) + +proc identifierNameForToken*(processor: TokenProcessor, token: JsToken): string = + processor.rawCodeForToken(token) + +proc identifierName*(processor: TokenProcessor): string = + processor.identifierNameForToken(processor.currentToken()) + +proc identifierNameAtIndex*(processor: TokenProcessor, index: int): string = + processor.identifierNameForToken(processor.tokens[index]) + +proc stringValueForToken*(processor: TokenProcessor, token: JsToken): string = + let raw = processor.rawCodeForToken(token) + if raw.len >= 2: + raw[1..^2] + else: + "" + +proc stringValue*(processor: TokenProcessor): string = + processor.stringValueForToken(processor.currentToken()) + +proc matches1AtIndex*(processor: TokenProcessor, index: int, t1: TokenType): bool = + index >= 0 and index < processor.tokens.len and processor.tokens[index].typ == t1 + +proc matches2AtIndex*(processor: TokenProcessor, index: int, t1, t2: TokenType): bool = + processor.matches1AtIndex(index, t1) and processor.matches1AtIndex(index + 1, t2) + +proc matches3AtIndex*(processor: TokenProcessor, index: int, t1, t2, t3: TokenType): bool = + processor.matches2AtIndex(index, t1, t2) and processor.matches1AtIndex(index + 2, t3) + +proc matches1*(processor: TokenProcessor, t1: TokenType): bool = + processor.matches1AtIndex(processor.tokenIndex, t1) + +proc matches2*(processor: TokenProcessor, t1, t2: TokenType): bool = + processor.matches2AtIndex(processor.tokenIndex, t1, t2) + +proc matches3*(processor: TokenProcessor, t1, t2, t3: TokenType): bool = + processor.matches3AtIndex(processor.tokenIndex, t1, t2, t3) + +proc matchesContextualAtIndex*(processor: TokenProcessor, index: int, keyword: ContextualKeyword): bool = + processor.matches1AtIndex(index, ttName) and processor.tokens[index].contextualKeyword == keyword + +proc matchesContextual*(processor: TokenProcessor, keyword: ContextualKeyword): bool = + processor.matchesContextualAtIndex(processor.tokenIndex, keyword) + +proc matchesContextIdAndLabel*(processor: TokenProcessor, typ: TokenType, contextId: int): bool = + processor.matches1(typ) and processor.currentToken().contextId == contextId + +proc previousWhitespaceAndComments*(processor: TokenProcessor): string = + let start = + if processor.tokenIndex > 0: processor.tokens[processor.tokenIndex - 1].`end` + else: 0 + let finish = + if processor.tokenIndex < processor.tokens.len: processor.tokens[processor.tokenIndex].start + else: processor.code.len + if start >= 0 and finish >= start and finish <= processor.code.len: + processor.code[start..<finish] + else: + "" + +proc snapshot*(processor: TokenProcessor): TokenProcessorSnapshot = + TokenProcessorSnapshot(resultCode: processor.resultCode, tokenIndex: processor.tokenIndex) + +proc restoreToSnapshot*(processor: var TokenProcessor, snapshot: TokenProcessorSnapshot) = + processor.resultCode = snapshot.resultCode + processor.tokenIndex = snapshot.tokenIndex + +proc dangerouslyGetAndRemoveCodeSinceSnapshot*(processor: var TokenProcessor, snapshot: TokenProcessorSnapshot): string = + result = processor.resultCode[snapshot.resultCode.len..^1] + processor.resultCode = snapshot.resultCode + +proc appendTokenPrefix(processor: var TokenProcessor) = + discard + +proc appendTokenSuffix(processor: var TokenProcessor) = + discard + +proc replaceToken*(processor: var TokenProcessor, newCode: string) = + if processor.isAtEnd(): + raise newException(ValueError, "Cannot replace token at end of input.") + processor.resultCode.add(processor.previousWhitespaceAndComments()) + processor.appendTokenPrefix() + processor.resultMappings[processor.tokenIndex] = processor.resultCode.len + processor.resultCode.add(newCode) + processor.appendTokenSuffix() + inc processor.tokenIndex + +proc replaceTokenTrimmingLeftWhitespace*(processor: var TokenProcessor, newCode: string) = + let whitespace = processor.previousWhitespaceAndComments() + for ch in whitespace: + if ch in {'\n', '\r'}: + processor.resultCode.add(ch) + processor.appendTokenPrefix() + processor.resultMappings[processor.tokenIndex] = processor.resultCode.len + processor.resultCode.add(newCode) + processor.appendTokenSuffix() + inc processor.tokenIndex + +proc removeInitialToken*(processor: var TokenProcessor) = + processor.replaceToken("") + +proc removeToken*(processor: var TokenProcessor) = + processor.replaceTokenTrimmingLeftWhitespace("") + +proc copyToken*(processor: var TokenProcessor) = + if processor.isAtEnd(): + raise newException(ValueError, "Cannot copy token at end of input.") + processor.resultCode.add(processor.previousWhitespaceAndComments()) + processor.appendTokenPrefix() + processor.resultMappings[processor.tokenIndex] = processor.resultCode.len + processor.resultCode.add(processor.rawCodeForToken(processor.currentToken())) + processor.appendTokenSuffix() + inc processor.tokenIndex + +proc copyTokenWithPrefix*(processor: var TokenProcessor, prefix: string) = + if processor.isAtEnd(): + raise newException(ValueError, "Cannot copy token at end of input.") + processor.resultCode.add(processor.previousWhitespaceAndComments()) + processor.appendTokenPrefix() + processor.resultCode.add(prefix) + processor.resultMappings[processor.tokenIndex] = processor.resultCode.len + processor.resultCode.add(processor.rawCodeForToken(processor.currentToken())) + processor.appendTokenSuffix() + inc processor.tokenIndex + +proc copyExpectedToken*(processor: var TokenProcessor, tokenType: TokenType) = + if not processor.matches1(tokenType): + raise newException(ValueError, "Expected token " & formatTokenType(tokenType)) + processor.copyToken() + +proc appendCode*(processor: var TokenProcessor, code: string) = + processor.resultCode.add(code) + +proc nextToken*(processor: var TokenProcessor) = + if processor.isAtEnd(): + raise newException(ValueError, "Unexpectedly reached end of input.") + inc processor.tokenIndex + +proc previousToken*(processor: var TokenProcessor) = + if processor.tokenIndex > 0: + dec processor.tokenIndex + +proc removeBalancedCode*(processor: var TokenProcessor) = + var braceDepth = 0 + while not processor.isAtEnd(): + if processor.matches1(ttBraceL): + inc braceDepth + elif processor.matches1(ttBraceR): + if braceDepth == 0: + return + dec braceDepth + processor.removeToken() + +proc finish*(processor: var TokenProcessor): TokenProcessorResult = + if processor.tokenIndex != processor.tokens.len: + raise newException(ValueError, "Tried to finish processing tokens before reaching the end.") + processor.resultCode.add(processor.previousWhitespaceAndComments()) + TokenProcessorResult(code: processor.resultCode, mappings: processor.resultMappings) + +proc copyAll*(processor: var TokenProcessor): TokenProcessorResult = + while not processor.isAtEnd(): + processor.copyToken() + processor.finish() diff --git a/frameos/src/frameos/js_runtime/tokens.nim b/frameos/src/frameos/js_runtime/tokens.nim new file mode 100644 index 000000000..e427689a0 --- /dev/null +++ b/frameos/src/frameos/js_runtime/tokens.nim @@ -0,0 +1,1056 @@ +# Sucrase-compatible JavaScript/TypeScript/JSX token model for FrameOS. +# +# This is intentionally shaped after Sucrase 3.35.1's parser/tokenizer layer so +# the native transpiler can move from string-scanner passes to token-driven +# transforms incrementally. Sucrase is MIT licensed; see transpiler.nim for +# attribution context. + +import std/[strutils] + +type + TokenType* = enum + ttNum, + ttBigint, + ttDecimal, + ttRegexp, + ttString, + ttName, + ttEof, + ttBracketL, + ttBracketR, + ttBraceL, + ttBraceBarL, + ttBraceR, + ttBraceBarR, + ttParenL, + ttParenR, + ttComma, + ttSemi, + ttColon, + ttDoubleColon, + ttDot, + ttQuestion, + ttQuestionDot, + ttArrow, + ttTemplate, + ttEllipsis, + ttBackQuote, + ttDollarBraceL, + ttAt, + ttHash, + ttEq, + ttAssign, + ttPreIncDec, + ttPostIncDec, + ttBang, + ttTilde, + ttPipeline, + ttNullishCoalescing, + ttLogicalOR, + ttLogicalAND, + ttBitwiseOR, + ttBitwiseXOR, + ttBitwiseAND, + ttEquality, + ttLessThan, + ttGreaterThan, + ttRelationalOrEqual, + ttBitShiftL, + ttBitShiftR, + ttPlus, + ttMinus, + ttModulo, + ttStar, + ttSlash, + ttExponent, + ttJsxName, + ttJsxText, + ttJsxEmptyText, + ttJsxTagStart, + ttJsxTagEnd, + ttTypeParameterStart, + ttNonNullAssertion, + ttBreak, + ttCase, + ttCatch, + ttContinue, + ttDebugger, + ttDefault, + ttDo, + ttElse, + ttFinally, + ttFor, + ttFunction, + ttIf, + ttReturn, + ttSwitch, + ttThrow, + ttTry, + ttVar, + ttLet, + ttConst, + ttWhile, + ttWith, + ttNew, + ttThis, + ttSuper, + ttClass, + ttExtends, + ttExport, + ttImport, + ttYield, + ttNull, + ttTrue, + ttFalse, + ttIn, + ttInstanceof, + ttTypeof, + ttVoid, + ttDelete, + ttAsync, + ttGet, + ttSet, + ttDeclare, + ttReadonly, + ttAbstract, + ttStatic, + ttPublic, + ttPrivate, + ttProtected, + ttOverride, + ttAs, + ttEnum, + ttType, + ttImplements + + ContextualKeyword* = enum + ckNone, + ckAbstract, + ckAccessor, + ckAs, + ckAssert, + ckAsserts, + ckAsync, + ckAwait, + ckChecks, + ckConstructor, + ckDeclare, + ckEnum, + ckExports, + ckFrom, + ckGet, + ckGlobal, + ckImplements, + ckInfer, + ckInterface, + ckIs, + ckKeyof, + ckMixins, + ckModule, + ckNamespace, + ckOf, + ckOpaque, + ckOut, + ckOverride, + ckPrivate, + ckProtected, + ckProto, + ckPublic, + ckReadonly, + ckRequire, + ckSatisfies, + ckSet, + ckStatic, + ckSymbol, + ckType, + ckUnique, + ckUsing + + IdentifierRole* = enum + irNone, + irAccess, + irExportAccess, + irTopLevelDeclaration, + irFunctionScopedDeclaration, + irBlockScopedDeclaration, + irObjectShorthandTopLevelDeclaration, + irObjectShorthandFunctionScopedDeclaration, + irObjectShorthandBlockScopedDeclaration, + irObjectShorthand, + irImportDeclaration, + irObjectKey, + irImportAccess + + JSXRole* = enum + jsxRoleNone, + jsxNoChildren, + jsxOneChild, + jsxStaticChildren, + jsxKeyAfterPropSpread + + Scope* = object + startTokenIndex*: int + endTokenIndex*: int + isFunctionScope*: bool + + JsToken* = object + typ*: TokenType + contextualKeyword*: ContextualKeyword + start*: int + `end`*: int + scopeDepth*: int + isType*: bool + identifierRole*: IdentifierRole + jsxRole*: JSXRole + shadowsGlobal*: bool + isAsyncOperation*: bool + contextId*: int + rhsEndIndex*: int + isExpression*: bool + numNullishCoalesceStarts*: int + numNullishCoalesceEnds*: int + isOptionalChainStart*: bool + isOptionalChainEnd*: bool + subscriptStartIndex*: int + nullishStartIndex*: int + + TokenizeOptions* = object + jsx*: bool + typescript*: bool + + Mode = enum + modeNormal, + modeJsxTag, + modeJsxText, + modeTemplate + +const + keywordTypes = { + "break": ttBreak, + "case": ttCase, + "catch": ttCatch, + "continue": ttContinue, + "debugger": ttDebugger, + "default": ttDefault, + "do": ttDo, + "else": ttElse, + "finally": ttFinally, + "for": ttFor, + "function": ttFunction, + "if": ttIf, + "return": ttReturn, + "switch": ttSwitch, + "throw": ttThrow, + "try": ttTry, + "var": ttVar, + "let": ttLet, + "const": ttConst, + "while": ttWhile, + "with": ttWith, + "new": ttNew, + "this": ttThis, + "super": ttSuper, + "class": ttClass, + "extends": ttExtends, + "export": ttExport, + "import": ttImport, + "yield": ttYield, + "null": ttNull, + "true": ttTrue, + "false": ttFalse, + "in": ttIn, + "instanceof": ttInstanceof, + "typeof": ttTypeof, + "void": ttVoid, + "delete": ttDelete, + "async": ttAsync, + "get": ttGet, + "set": ttSet, + "declare": ttDeclare, + "readonly": ttReadonly, + "abstract": ttAbstract, + "static": ttStatic, + "public": ttPublic, + "private": ttPrivate, + "protected": ttProtected, + "override": ttOverride, + "as": ttAs, + "enum": ttEnum, + "type": ttType, + "implements": ttImplements, + } + contextualKeywords = { + "abstract": ckAbstract, + "accessor": ckAccessor, + "as": ckAs, + "assert": ckAssert, + "asserts": ckAsserts, + "async": ckAsync, + "await": ckAwait, + "checks": ckChecks, + "constructor": ckConstructor, + "declare": ckDeclare, + "enum": ckEnum, + "exports": ckExports, + "from": ckFrom, + "get": ckGet, + "global": ckGlobal, + "implements": ckImplements, + "infer": ckInfer, + "interface": ckInterface, + "is": ckIs, + "keyof": ckKeyof, + "mixins": ckMixins, + "module": ckModule, + "namespace": ckNamespace, + "of": ckOf, + "opaque": ckOpaque, + "out": ckOut, + "override": ckOverride, + "private": ckPrivate, + "protected": ckProtected, + "proto": ckProto, + "public": ckPublic, + "readonly": ckReadonly, + "require": ckRequire, + "satisfies": ckSatisfies, + "set": ckSet, + "static": ckStatic, + "symbol": ckSymbol, + "type": ckType, + "unique": ckUnique, + "using": ckUsing, + } + +proc defaultTokenizeOptions*(): TokenizeOptions = + TokenizeOptions(jsx: true, typescript: true) + +proc formatTokenType*(typ: TokenType): string = + case typ + of ttNum: "num" + of ttBigint: "bigint" + of ttDecimal: "decimal" + of ttRegexp: "regexp" + of ttString: "string" + of ttName: "name" + of ttEof: "eof" + of ttBracketL: "[" + of ttBracketR: "]" + of ttBraceL: "{" + of ttBraceBarL: "{|" + of ttBraceR: "}" + of ttBraceBarR: "|}" + of ttParenL: "(" + of ttParenR: ")" + of ttComma: "," + of ttSemi: ";" + of ttColon: ":" + of ttDoubleColon: "::" + of ttDot: "." + of ttQuestion: "?" + of ttQuestionDot: "?." + of ttArrow: "=>" + of ttTemplate: "template" + of ttEllipsis: "..." + of ttBackQuote: "`" + of ttDollarBraceL: "${" + of ttAt: "@" + of ttHash: "#" + of ttEq: "=" + of ttAssign: "_=" + of ttPreIncDec, ttPostIncDec: "++/--" + of ttBang: "!" + of ttTilde: "~" + of ttPipeline: "|>" + of ttNullishCoalescing: "??" + of ttLogicalOR: "||" + of ttLogicalAND: "&&" + of ttBitwiseOR: "|" + of ttBitwiseXOR: "^" + of ttBitwiseAND: "&" + of ttEquality: "==/!=" + of ttLessThan: "<" + of ttGreaterThan: ">" + of ttRelationalOrEqual: "<=/>=" + of ttBitShiftL: "<<" + of ttBitShiftR: ">>/>>>" + of ttPlus: "+" + of ttMinus: "-" + of ttModulo: "%" + of ttStar: "*" + of ttSlash: "/" + of ttExponent: "**" + of ttJsxName: "jsxName" + of ttJsxText: "jsxText" + of ttJsxEmptyText: "jsxEmptyText" + of ttJsxTagStart: "jsxTagStart" + of ttJsxTagEnd: "jsxTagEnd" + of ttTypeParameterStart: "typeParameterStart" + of ttNonNullAssertion: "nonNullAssertion" + of ttBreak: "break" + of ttCase: "case" + of ttCatch: "catch" + of ttContinue: "continue" + of ttDebugger: "debugger" + of ttDefault: "default" + of ttDo: "do" + of ttElse: "else" + of ttFinally: "finally" + of ttFor: "for" + of ttFunction: "function" + of ttIf: "if" + of ttReturn: "return" + of ttSwitch: "switch" + of ttThrow: "throw" + of ttTry: "try" + of ttVar: "var" + of ttLet: "let" + of ttConst: "const" + of ttWhile: "while" + of ttWith: "with" + of ttNew: "new" + of ttThis: "this" + of ttSuper: "super" + of ttClass: "class" + of ttExtends: "extends" + of ttExport: "export" + of ttImport: "import" + of ttYield: "yield" + of ttNull: "null" + of ttTrue: "true" + of ttFalse: "false" + of ttIn: "in" + of ttInstanceof: "instanceof" + of ttTypeof: "typeof" + of ttVoid: "void" + of ttDelete: "delete" + of ttAsync: "async" + of ttGet: "get" + of ttSet: "set" + of ttDeclare: "declare" + of ttReadonly: "readonly" + of ttAbstract: "abstract" + of ttStatic: "static" + of ttPublic: "public" + of ttPrivate: "private" + of ttProtected: "protected" + of ttOverride: "override" + of ttAs: "as" + of ttEnum: "enum" + of ttType: "type" + of ttImplements: "implements" + +proc formatContextualKeyword*(keyword: ContextualKeyword): string = + case keyword + of ckNone: "NONE" + of ckAbstract: "abstract" + of ckAccessor: "accessor" + of ckAs: "as" + of ckAssert: "assert" + of ckAsserts: "asserts" + of ckAsync: "async" + of ckAwait: "await" + of ckChecks: "checks" + of ckConstructor: "constructor" + of ckDeclare: "declare" + of ckEnum: "enum" + of ckExports: "exports" + of ckFrom: "from" + of ckGet: "get" + of ckGlobal: "global" + of ckImplements: "implements" + of ckInfer: "infer" + of ckInterface: "interface" + of ckIs: "is" + of ckKeyof: "keyof" + of ckMixins: "mixins" + of ckModule: "module" + of ckNamespace: "namespace" + of ckOf: "of" + of ckOpaque: "opaque" + of ckOut: "out" + of ckOverride: "override" + of ckPrivate: "private" + of ckProtected: "protected" + of ckProto: "proto" + of ckPublic: "public" + of ckReadonly: "readonly" + of ckRequire: "require" + of ckSatisfies: "satisfies" + of ckSet: "set" + of ckStatic: "static" + of ckSymbol: "symbol" + of ckType: "type" + of ckUnique: "unique" + of ckUsing: "using" + +proc formatIdentifierRole*(role: IdentifierRole): string = + case role + of irNone: "none" + of irAccess: "access" + of irExportAccess: "exportAccess" + of irTopLevelDeclaration: "topLevelDeclaration" + of irFunctionScopedDeclaration: "functionScopedDeclaration" + of irBlockScopedDeclaration: "blockScopedDeclaration" + of irObjectShorthandTopLevelDeclaration: "objectShorthandTopLevelDeclaration" + of irObjectShorthandFunctionScopedDeclaration: "objectShorthandFunctionScopedDeclaration" + of irObjectShorthandBlockScopedDeclaration: "objectShorthandBlockScopedDeclaration" + of irObjectShorthand: "objectShorthand" + of irImportDeclaration: "importDeclaration" + of irObjectKey: "objectKey" + of irImportAccess: "importAccess" + +proc formatJSXRole*(role: JSXRole): string = + case role + of jsxRoleNone: "none" + of jsxNoChildren: "noChildren" + of jsxOneChild: "oneChild" + of jsxStaticChildren: "staticChildren" + of jsxKeyAfterPropSpread: "keyAfterPropSpread" + +proc isIdentStart(c: char): bool = + c in {'a'..'z', 'A'..'Z', '_', '$'} or ord(c) >= 128 + +proc isIdentPart(c: char): bool = + isIdentStart(c) or c in {'0'..'9'} + +proc isWhitespace(c: char): bool = + c in {' ', '\t', '\n', '\r', '\v', '\f'} + +proc tokenCanEndExpression(typ: TokenType): bool = + typ in { + ttNum, ttBigint, ttDecimal, ttRegexp, ttString, ttName, ttBracketR, + ttBraceR, ttParenR, ttTemplate, ttBackQuote, ttPostIncDec, ttJsxTagEnd, + ttNull, ttTrue, ttFalse, ttThis, ttSuper + } + +proc shouldReadRegex(prev: TokenType): bool = + prev == ttEof or not tokenCanEndExpression(prev) or prev in { + ttReturn, ttThrow, ttCase, ttDelete, ttTypeof, ttVoid, ttNew, ttIn, + ttInstanceof + } + +proc skipSpace(code: string, pos: var int): bool = + while pos < code.len: + case code[pos] + of ' ', '\t', '\v', '\f': + inc pos + of '\n': + result = true + inc pos + of '\r': + result = true + inc pos + if pos < code.len and code[pos] == '\n': + inc pos + of '/': + if pos + 1 < code.len and code[pos + 1] == '/': + pos += 2 + while pos < code.len and code[pos] notin {'\n', '\r'}: + inc pos + elif pos + 1 < code.len and code[pos + 1] == '*': + pos += 2 + while pos + 1 < code.len and not (code[pos] == '*' and code[pos + 1] == '/'): + if code[pos] in {'\n', '\r'}: + result = true + inc pos + if pos + 1 >= code.len: + raise newException(ValueError, "Unterminated comment") + pos += 2 + else: + break + else: + if isWhitespace(code[pos]): + inc pos + else: + break + +proc makeToken(typ: TokenType, start, finish: int, contextualKeyword = ckNone): JsToken = + JsToken( + typ: typ, + contextualKeyword: contextualKeyword, + start: start, + `end`: finish, + contextId: -1, + rhsEndIndex: -1, + subscriptStartIndex: -1, + nullishStartIndex: -1, + ) + +proc readWordToken(code: string, pos: var int, jsxName = false): JsToken = + let start = pos + while pos < code.len: + if isIdentPart(code[pos]) or code[pos] == '-': + inc pos + elif code[pos] == '\\': + pos += 2 + if pos < code.len and code[pos] == '{': + while pos < code.len and code[pos] != '}': + inc pos + if pos < code.len: + inc pos + else: + break + let word = code[start..<pos] + if jsxName: + return makeToken(ttJsxName, start, pos) + var after = pos + while after < code.len and code[after] in {' ', '\t', '\n', '\r'}: + inc after + for pair in contextualKeywords: + if pair[0] == word and after < code.len and code[after] in {':', '?', '!'}: + return makeToken(ttName, start, pos, pair[1]) + if word == "import" and after < code.len and code[after] == '.': + return makeToken(ttName, start, pos) + for pair in keywordTypes: + if pair[0] == word: + return makeToken(pair[1], start, pos) + for pair in contextualKeywords: + if pair[0] == word: + return makeToken(ttName, start, pos, pair[1]) + makeToken(ttName, start, pos) + +proc readNumberToken(code: string, pos: var int, startsWithDot: bool): JsToken = + let start = pos + var isBigInt = false + var isDecimal = false + + template readInt() = + while pos < code.len and (code[pos] in {'0'..'9'} or code[pos] == '_'): + inc pos + + if startsWithDot: + inc pos + readInt() + elif pos + 1 < code.len and code[pos] == '0' and code[pos + 1] in {'x', 'X', 'o', 'O', 'b', 'B'}: + pos += 2 + while pos < code.len and (code[pos] in {'0'..'9', 'a'..'f', 'A'..'F'} or code[pos] == '_'): + inc pos + else: + readInt() + if pos < code.len and code[pos] == '.': + inc pos + readInt() + if pos < code.len and code[pos] in {'e', 'E'}: + inc pos + if pos < code.len and code[pos] in {'+', '-'}: + inc pos + readInt() + + if pos < code.len and code[pos] == 'n': + isBigInt = true + inc pos + elif pos < code.len and code[pos] == 'm': + isDecimal = true + inc pos + + makeToken(if isBigInt: ttBigint elif isDecimal: ttDecimal else: ttNum, start, pos) + +proc readStringToken(code: string, pos: var int): JsToken = + let start = pos + let quote = code[pos] + inc pos + while pos < code.len: + if code[pos] == '\\': + pos += min(2, code.len - pos) + elif code[pos] == quote: + inc pos + return makeToken(ttString, start, pos) + else: + inc pos + raise newException(ValueError, "Unterminated string constant") + +proc readRegexToken(code: string, pos: var int): JsToken = + let start = pos + var escaped = false + var inClass = false + inc pos + while pos < code.len: + let ch = code[pos] + if escaped: + escaped = false + else: + if ch == '[': + inClass = true + elif ch == ']' and inClass: + inClass = false + elif ch == '/' and not inClass: + inc pos + while pos < code.len and isIdentPart(code[pos]): + inc pos + return makeToken(ttRegexp, start, pos) + escaped = ch == '\\' + inc pos + raise newException(ValueError, "Unterminated regular expression") + +proc readTemplatePart(code: string, pos: var int, prev: TokenType): JsToken = + let start = pos + while pos < code.len: + if code[pos] == '\\': + pos += min(2, code.len - pos) + continue + if code[pos] == '`': + if pos == start and prev != ttTemplate: + return makeToken(ttTemplate, start, pos) + if pos == start: + inc pos + return makeToken(ttBackQuote, start, pos) + return makeToken(ttTemplate, start, pos) + if code[pos] == '$' and pos + 1 < code.len and code[pos + 1] == '{': + if pos == start and prev != ttTemplate: + return makeToken(ttTemplate, start, pos) + if pos == start: + pos += 2 + return makeToken(ttDollarBraceL, start, pos) + return makeToken(ttTemplate, start, pos) + inc pos + raise newException(ValueError, "Unterminated template") + +proc readJsxText(code: string, pos: var int): JsToken = + let start = pos + while pos < code.len and code[pos] notin {'<', '{'}: + inc pos + if pos == start: + return makeToken(ttJsxEmptyText, start, pos) + if code[start..<pos].strip().len == 0: + return makeToken(ttJsxEmptyText, start, pos) + makeToken(ttJsxText, start, pos) + +proc looksLikeGenericArrowStart(code: string, pos: int): bool = + var i = pos + 1 + while i < code.len and code[i] in {' ', '\t'}: + inc i + if i >= code.len or not isIdentStart(code[i]): + return false + while i < code.len and (isIdentPart(code[i]) or code[i] in {' ', '\t', ',', '?'}) : + inc i + if i >= code.len or code[i] != '>': + return false + inc i + while i < code.len and code[i] in {' ', '\t', '\n', '\r'}: + inc i + if i >= code.len or code[i] != '(': + return false + var depth = 0 + while i < code.len: + if code[i] == '(': + inc depth + elif code[i] == ')': + dec depth + if depth == 0: + inc i + break + inc i + while i < code.len and code[i] in {' ', '\t', '\n', '\r'}: + inc i + i + 1 < code.len and code[i] == '=' and code[i + 1] == '>' + +proc looksLikeJsxStart(code: string, pos: int, prev: TokenType): bool = + if pos >= code.len or code[pos] != '<': + return false + if looksLikeGenericArrowStart(code, pos): + return false + if prev != ttEof and tokenCanEndExpression(prev): + return false + var next = pos + 1 + while next < code.len and code[next] in {' ', '\t'}: + inc next + next < code.len and (isIdentStart(code[next]) or code[next] in {'/', '>'}) + +proc punctToken(code: string, pos: var int, prev: TokenType, hadNewline: bool): JsToken = + let start = pos + template finish(kind: TokenType, width: int): JsToken = + pos += width + makeToken(kind, start, pos) + + case code[pos] + of '#': finish(ttHash, 1) + of '.': + if pos + 1 < code.len and code[pos + 1] in {'0'..'9'}: + readNumberToken(code, pos, true) + elif pos + 2 < code.len and code[pos + 1] == '.' and code[pos + 2] == '.': + finish(ttEllipsis, 3) + else: + finish(ttDot, 1) + of '(': + finish(ttParenL, 1) + of ')': + finish(ttParenR, 1) + of ';': + finish(ttSemi, 1) + of ',': + finish(ttComma, 1) + of '[': + finish(ttBracketL, 1) + of ']': + finish(ttBracketR, 1) + of '{': + finish(ttBraceL, 1) + of '}': + finish(ttBraceR, 1) + of ':': + if pos + 1 < code.len and code[pos + 1] == ':': finish(ttDoubleColon, 2) + else: finish(ttColon, 1) + of '?': + if pos + 2 < code.len and code[pos + 1] == '?' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '?': + finish(ttNullishCoalescing, 2) + elif pos + 1 < code.len and code[pos + 1] == '.' and not (pos + 2 < code.len and code[pos + 2] in {'0'..'9'}): + finish(ttQuestionDot, 2) + else: + finish(ttQuestion, 1) + of '@': + finish(ttAt, 1) + of '`': + finish(ttBackQuote, 1) + of '/': + if shouldReadRegex(prev): + readRegexToken(code, pos) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttSlash, 1) + of '%': + if pos + 1 < code.len and code[pos + 1] == '=': finish(ttAssign, 2) + else: finish(ttModulo, 1) + of '*': + if pos + 2 < code.len and code[pos + 1] == '*' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '*': + finish(ttExponent, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttStar, 1) + of '|': + if pos + 2 < code.len and code[pos + 1] == '|' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '|': + finish(ttLogicalOR, 2) + elif pos + 1 < code.len and code[pos + 1] == '>': + finish(ttPipeline, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttBitwiseOR, 1) + of '&': + if pos + 2 < code.len and code[pos + 1] == '&' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '&': + finish(ttLogicalAND, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttBitwiseAND, 1) + of '^': + if pos + 1 < code.len and code[pos + 1] == '=': finish(ttAssign, 2) + else: finish(ttBitwiseXOR, 1) + of '+': + if pos + 1 < code.len and code[pos + 1] == '+': + finish(if tokenCanEndExpression(prev) and not hadNewline: ttPostIncDec else: ttPreIncDec, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttPlus, 1) + of '-': + if pos + 1 < code.len and code[pos + 1] == '-': + finish(if tokenCanEndExpression(prev) and not hadNewline: ttPostIncDec else: ttPreIncDec, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttAssign, 2) + else: + finish(ttMinus, 1) + of '<': + if pos + 2 < code.len and code[pos + 1] == '<' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '<': + finish(ttBitShiftL, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttRelationalOrEqual, 2) + else: + finish(ttLessThan, 1) + of '>': + if pos + 3 < code.len and code[pos + 1] == '>' and code[pos + 2] == '>' and code[pos + 3] == '=': + finish(ttAssign, 4) + elif pos + 2 < code.len and code[pos + 1] == '>' and code[pos + 2] == '=': + finish(ttAssign, 3) + elif pos + 1 < code.len and code[pos + 1] == '>': + finish(if pos + 2 < code.len and code[pos + 2] == '>': ttBitShiftR else: ttBitShiftR, if pos + 2 < code.len and code[pos + 2] == '>': 3 else: 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttRelationalOrEqual, 2) + else: + finish(ttGreaterThan, 1) + of '=': + if pos + 1 < code.len and code[pos + 1] == '>': + finish(ttArrow, 2) + elif pos + 1 < code.len and code[pos + 1] == '=': + finish(ttEquality, if pos + 2 < code.len and code[pos + 2] == '=': 3 else: 2) + else: + finish(ttEq, 1) + of '!': + if pos + 1 < code.len and code[pos + 1] == '=': + finish(ttEquality, if pos + 2 < code.len and code[pos + 2] == '=': 3 else: 2) + else: + finish(ttBang, 1) + of '~': + finish(ttTilde, 1) + else: + raise newException(ValueError, "Unexpected character '" & $code[pos] & "'") + +proc tokenizeJs*(code: string, options = defaultTokenizeOptions()): seq[JsToken] = + var pos = 0 + var prev = ttEof + var mode = modeNormal + var modeStack: seq[Mode] = @[] + var braceModeStack: seq[Mode] = @[] + var jsxDepth = 0 + var jsxTagClosing = false + var jsxSelfClosing = false + var tokens: seq[JsToken] = @[] + + proc pushToken(token: JsToken) = + tokens.add(token) + prev = token.typ + + while true: + if mode == modeTemplate: + let token = readTemplatePart(code, pos, prev) + pushToken(token) + if token.typ == ttDollarBraceL: + braceModeStack.add(modeTemplate) + mode = modeNormal + elif token.typ == ttBackQuote: + if modeStack.len > 0: + mode = modeStack.pop() + else: + mode = modeNormal + continue + + if mode == modeJsxText: + if pos >= code.len: + pushToken(makeToken(ttEof, pos, pos)) + break + if code[pos] == '<': + let start = pos + inc pos + pushToken(makeToken(ttJsxTagStart, start, pos)) + mode = modeJsxTag + var look = pos + while look < code.len and code[look] in {' ', '\t'}: + inc look + jsxTagClosing = look < code.len and code[look] == '/' + jsxSelfClosing = false + continue + if code[pos] == '{': + let start = pos + inc pos + pushToken(makeToken(ttBraceL, start, pos)) + braceModeStack.add(modeJsxText) + mode = modeNormal + continue + let token = readJsxText(code, pos) + pushToken(token) + continue + + let hadNewline = skipSpace(code, pos) + if pos >= code.len: + pushToken(makeToken(ttEof, pos, pos)) + break + + if mode == modeJsxTag: + let start = pos + case code[pos] + of '>': + inc pos + pushToken(makeToken(ttJsxTagEnd, start, pos)) + if jsxSelfClosing: + if jsxDepth == 0: + mode = modeNormal + else: + mode = modeJsxText + elif jsxTagClosing: + if jsxDepth > 0: + dec jsxDepth + mode = if jsxDepth == 0: modeNormal else: modeJsxText + else: + inc jsxDepth + mode = modeJsxText + continue + of '/': + inc pos + if not jsxTagClosing: + jsxSelfClosing = true + pushToken(makeToken(ttSlash, start, pos)) + continue + of '{': + inc pos + pushToken(makeToken(ttBraceL, start, pos)) + braceModeStack.add(modeJsxTag) + mode = modeNormal + continue + of '=', ':', '.', '-': + pushToken(punctToken(code, pos, prev, hadNewline)) + continue + of '\'', '"': + pushToken(readStringToken(code, pos)) + continue + else: + if isIdentStart(code[pos]): + pushToken(readWordToken(code, pos, jsxName = true)) + continue + pushToken(punctToken(code, pos, prev, hadNewline)) + continue + + if options.jsx and looksLikeJsxStart(code, pos, prev): + let start = pos + inc pos + pushToken(makeToken(ttJsxTagStart, start, pos)) + mode = modeJsxTag + var look = pos + while look < code.len and code[look] in {' ', '\t'}: + inc look + jsxTagClosing = look < code.len and code[look] == '/' + jsxSelfClosing = false + continue + + if code[pos] == '`': + let start = pos + inc pos + pushToken(makeToken(ttBackQuote, start, pos)) + modeStack.add(mode) + mode = modeTemplate + continue + + if code[pos] in {'\'', '"'}: + pushToken(readStringToken(code, pos)) + continue + + if code[pos] in {'0'..'9'}: + pushToken(readNumberToken(code, pos, startsWithDot = false)) + continue + + if isIdentStart(code[pos]) or code[pos] == '\\': + pushToken(readWordToken(code, pos)) + continue + + let token = punctToken(code, pos, prev, hadNewline) + pushToken(token) + if token.typ == ttBraceR and braceModeStack.len > 0: + mode = braceModeStack.pop() + + tokens + +proc formatToken*(code: string, token: JsToken): string = + let raw = if token.start >= 0 and token.`end` <= code.len and token.start <= token.`end`: code[token.start..<token.`end`] else: "" + var parts = @[formatTokenType(token.typ) & "(" & $token.start & "," & $token.`end` & ")"] + if token.contextualKeyword != ckNone: + parts.add("contextual=" & formatContextualKeyword(token.contextualKeyword)) + if raw.len > 0: + parts.add(raw.multiReplace(("\n", "\\n"), ("\r", "\\r"), ("\t", "\\t"))) + parts.join(" ") + +proc formatTokens*(code: string, tokens: seq[JsToken]): string = + for token in tokens: + if result.len > 0: + result.add("\n") + result.add(formatToken(code, token)) diff --git a/frameos/src/frameos/js_runtime/transpiler.nim b/frameos/src/frameos/js_runtime/transpiler.nim new file mode 100644 index 000000000..fc029d2fa --- /dev/null +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -0,0 +1,1991 @@ +# Native TypeScript/JSX transpiler for FrameOS. +# +# This module is a Nim reimplementation track for the parts of Sucrase that +# FrameOS needs at runtime. The output is always evaluated by the bundled +# QuickJS runtime, so this intentionally erases TypeScript, lowers JSX to the +# FrameOS classic runtime, rewrites modules for the app wrapper, and preserves +# modern JavaScript syntax that QuickJS already supports. +# +# Sucrase is MIT licensed: +# +# Copyright (c) 2012-2018 various contributors (see AUTHORS) +# +# Sucrase itself includes a modified fork of Babylon, which was forked from +# Acorn. This file intentionally keeps public naming close to Sucrase concepts +# (`TransformOptions`, `TransformResult`, `transform`) so upstream changes can +# be tracked and ported incrementally. See `js_runtime/README.md`. + +import std/[strutils, sequtils] +from std/unicode import Rune, toUTF8 + +import ./parser +import ./source_map +import ./token_processor +import ./tokens + +type + TransformResult* = object + code*: string + sourceMap*: SourceLineMap + + TransformOptions* = object + filePath*: string + transforms*: seq[string] + + JsxParser = object + code: string + pos: int + +const + defaultTransforms = @["typescript", "jsx"] + moduleTransforms = @["typescript", "jsx", "imports"] + reservedWords = [ + "break", "case", "catch", "class", "const", "continue", "debugger", + "default", "delete", "do", "else", "export", "extends", "finally", + "for", "function", "if", "import", "in", "instanceof", "new", "return", + "super", "switch", "this", "throw", "try", "typeof", "var", "void", + "while", "with", "yield", "enum", "implements", "interface", "let", + "package", "private", "protected", "public", "static", "await", "false", + "null", "true" + ] + +proc hasTransform(options: TransformOptions, name: string): bool = + let transforms = if options.transforms.len == 0: defaultTransforms else: options.transforms + name in transforms + +proc isIdentStart(c: char): bool = + c in {'a'..'z', 'A'..'Z', '_', '$'} + +proc isIdentPart(c: char): bool = + isIdentStart(c) or c in {'0'..'9'} + +proc isIdentifierName(name: string): bool = + if name.len == 0 or not isIdentStart(name[0]): + return false + for ch in name: + if not isIdentPart(ch): + return false + name notin reservedWords + +proc skipSpaces(code: string, i: var int) = + while i < code.len and code[i] in {' ', '\t', '\n', '\r'}: + inc i + +proc jsonQuote(s: string): string = + result = "\"" + for ch in s: + case ch + of '\\': result.add("\\\\") + of '"': result.add("\\\"") + of '\n': result.add("\\n") + of '\r': result.add("\\r") + of '\t': result.add("\\t") + else: result.add(ch) + result.add('"') + +proc decodeJsxEntities(s: string): string = + var i = 0 + while i < s.len: + if s[i] != '&': + result.add(s[i]) + inc i + continue + let semi = s.find(';', i + 1) + if semi < 0: + result.add(s[i]) + inc i + continue + let entity = s[i + 1..<semi] + case entity + of "amp": result.add('&') + of "lt": result.add('<') + of "gt": result.add('>') + of "quot": result.add('"') + of "apos": result.add('\'') + of "nbsp": result.add(" ") + else: + if entity.startsWith("#x") or entity.startsWith("#X"): + try: + result.add(Rune(parseHexInt(entity[2..^1])).toUTF8()) + except CatchableError: + result.add("&" & entity & ";") + elif entity.startsWith("#"): + try: + result.add(Rune(parseInt(entity[1..^1])).toUTF8()) + except CatchableError: + result.add("&" & entity & ";") + else: + result.add("&" & entity & ";") + i = semi + 1 + +proc startsWordAt(code: string, i: int, word: string): bool = + if i < 0 or i + word.len > code.len: + return false + if code.substr(i, i + word.len - 1) != word: + return false + if i > 0 and isIdentPart(code[i - 1]): + return false + let after = i + word.len + after >= code.len or not isIdentPart(code[after]) + +proc readIdentifier(code: string, i: var int): string = + let start = i + if i < code.len and isIdentStart(code[i]): + inc i + while i < code.len and isIdentPart(code[i]): + inc i + code[start..<i] + +proc copyQuoted(code: string, i: var int, quote: char): string = + let start = i + inc i + while i < code.len: + if code[i] == '\\': + i += min(2, code.len - i) + elif code[i] == quote: + inc i + break + else: + inc i + code[start..<i] + +proc skipQuoted(code: string, i: var int, quote: char) = + discard copyQuoted(code, i, quote) + +proc copyLineComment(code: string, i: var int): string = + let start = i + i += 2 + while i < code.len and code[i] notin {'\n', '\r'}: + inc i + code[start..<i] + +proc skipLineComment(code: string, i: var int) = + discard copyLineComment(code, i) + +proc copyBlockComment(code: string, i: var int): string = + let start = i + i += 2 + while i + 1 < code.len: + if code[i] == '*' and code[i + 1] == '/': + i += 2 + break + inc i + code[start..<i] + +proc skipBlockComment(code: string, i: var int) = + discard copyBlockComment(code, i) + +proc copyTemplate(code: string, i: var int): string = + let start = i + inc i + while i < code.len: + if code[i] == '\\': + i += min(2, code.len - i) + elif code[i] == '`': + inc i + break + else: + inc i + code[start..<i] + +proc skipTemplate(code: string, i: var int) = + discard copyTemplate(code, i) + +proc findMatching(code: string, openIndex: int, openCh: char, closeCh: char): int = + var i = openIndex + var depth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '/': + if i + 1 < code.len and code[i + 1] == '/': + skipLineComment(code, i) + continue + if i + 1 < code.len and code[i + 1] == '*': + skipBlockComment(code, i) + continue + else: + discard + if code[i] == openCh: + inc depth + elif code[i] == closeCh: + dec depth + if depth == 0: + return i + inc i + -1 + +proc findMatchingReverse(code: string, closeIndex: int, openCh: char, closeCh: char): int = + var i = closeIndex + var depth = 0 + while i >= 0: + if code[i] == closeCh: + inc depth + elif code[i] == openCh: + dec depth + if depth == 0: + return i + dec i + -1 + +proc findMatchingAngle(code: string, openIndex: int): int = + var i = openIndex + var depth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '/': + if i + 1 < code.len and code[i + 1] == '/': + skipLineComment(code, i) + continue + if i + 1 < code.len and code[i + 1] == '*': + skipBlockComment(code, i) + continue + of '<': + inc depth + of '>': + dec depth + if depth == 0: + return i + else: + discard + inc i + -1 + +proc findStatementEnd(code: string, start: int): int = + var i = start + var parenDepth = 0 + var braceDepth = 0 + var bracketDepth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '/': + if i + 1 < code.len and code[i + 1] == '/': + skipLineComment(code, i) + continue + if i + 1 < code.len and code[i + 1] == '*': + skipBlockComment(code, i) + continue + of '(': + inc parenDepth + of ')': + if parenDepth > 0: dec parenDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth > 0: dec bracketDepth + of '{': + inc braceDepth + of '}': + if braceDepth > 0: + dec braceDepth + elif parenDepth == 0 and bracketDepth == 0: + return i + of ';': + if parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return i + 1 + of '\n': + if parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return i + else: + discard + inc i + code.len + +proc findTopLevelCommaOrBrace(code: string, start: int, closeCh: char): int = + var i = start + var parenDepth = 0 + var braceDepth = 0 + var bracketDepth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '/': + if i + 1 < code.len and code[i + 1] == '/': + skipLineComment(code, i) + continue + if i + 1 < code.len and code[i + 1] == '*': + skipBlockComment(code, i) + continue + of '(': + inc parenDepth + of ')': + if parenDepth > 0: dec parenDepth + of '{': + inc braceDepth + of '}': + if closeCh == '}' and parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return i + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth > 0: dec bracketDepth + of ',': + if parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return i + else: + discard + inc i + code.len + +proc readPropertyName(code: string, i: var int): tuple[nameStringCode: string, variableName: string] = + skipSpaces(code, i) + if i < code.len and code[i] in {'\'', '"'}: + let raw = copyQuoted(code, i, code[i]) + let value = + if raw.len >= 2: raw[1..^2] + else: "" + return (raw, if isIdentifierName(value): value else: "") + let name = readIdentifier(code, i) + if name.len == 0: + raise newException(ValueError, "Expected name or string at beginning of enum element.") + (jsonQuote(name), if isIdentifierName(name): name else: "") + +proc isStringLiteralCode(code: string): bool = + let trimmed = code.strip() + trimmed.len >= 2 and trimmed[0] in {'\'', '"'} and trimmed[^1] == trimmed[0] + +proc lowerEnumMember(enumName: string, nameStringCode: string, variableName: string, valueCode: string, hasValue: bool, previousValueCode: string): tuple[code: string, previous: string] = + if hasValue and isStringLiteralCode(valueCode): + if variableName.len > 0: + result.code = "const " & variableName & " = " & valueCode.strip() & "; " & enumName & "[" & nameStringCode & "] = " & variableName & ";" + result.previous = variableName + else: + result.code = enumName & "[" & nameStringCode & "] = " & valueCode.strip() & ";" + result.previous = enumName & "[" & nameStringCode & "]" + return + + let resolvedValue = + if hasValue: + valueCode.strip() + elif previousValueCode.len > 0: + previousValueCode & " + 1" + else: + "0" + if variableName.len > 0: + result.code = "const " & variableName & " = " & resolvedValue & "; " & enumName & "[" & enumName & "[" & nameStringCode & "] = " & variableName & "] = " & nameStringCode & ";" + result.previous = variableName + else: + result.code = enumName & "[" & enumName & "[" & nameStringCode & "] = " & resolvedValue & "] = " & nameStringCode & ";" + result.previous = enumName & "[" & nameStringCode & "]" + +proc lowerEnumDeclaration(code: string, start: int): tuple[code: string, next: int] = + var i = start + var isExport = false + if startsWordAt(code, i, "export"): + isExport = true + i += "export".len + skipSpaces(code, i) + if startsWordAt(code, i, "const"): + i += "const".len + skipSpaces(code, i) + if not startsWordAt(code, i, "enum"): + raise newException(ValueError, "Expected enum declaration.") + i += "enum".len + skipSpaces(code, i) + let enumName = readIdentifier(code, i) + if enumName.len == 0: + raise newException(ValueError, "Expected enum name.") + skipSpaces(code, i) + if i >= code.len or code[i] != '{': + raise newException(ValueError, "Expected enum body.") + let close = findMatching(code, i, '{', '}') + if close < 0: + raise newException(ValueError, "Unterminated enum body.") + + var body = "" + var memberPos = i + 1 + var previousValueCode = "" + while memberPos < close: + skipSpaces(code, memberPos) + if memberPos >= close: + break + if code[memberPos] == ',': + inc memberPos + continue + let keyInfo = readPropertyName(code, memberPos) + skipSpaces(code, memberPos) + var valueCode = "" + var hasValue = false + if memberPos < close and code[memberPos] == '=': + hasValue = true + inc memberPos + let valueStart = memberPos + let valueEnd = findTopLevelCommaOrBrace(code, memberPos, '}') + valueCode = code[valueStart..<min(valueEnd, close)] + memberPos = min(valueEnd, close) + let lowered = lowerEnumMember(enumName, keyInfo.nameStringCode, keyInfo.variableName, valueCode, hasValue, previousValueCode) + body.add(lowered.code) + previousValueCode = lowered.previous + skipSpaces(code, memberPos) + if memberPos < close and code[memberPos] == ',': + inc memberPos + + result.code = (if isExport: "export " else: "") & "var " & enumName & "; (function (" & enumName & ") {" & body & "})(" & enumName & " || (" & enumName & " = {}));" + result.next = close + 1 + +proc lowerEnums(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "enum") or + startsWordAt(code, i, "const") and (block: + var j = i + "const".len + skipSpaces(code, j) + startsWordAt(code, j, "enum") + ) or + startsWordAt(code, i, "export") and (block: + var j = i + "export".len + skipSpaces(code, j) + if startsWordAt(code, j, "const"): + j += "const".len + skipSpaces(code, j) + startsWordAt(code, j, "enum") + ): + let lowered = lowerEnumDeclaration(code, i) + result.add(lowered.code) + i = lowered.next + continue + + result.add(code[i]) + inc i + +proc removeTypeDeclaration(code: string, start: int): int = + var i = start + if startsWordAt(code, i, "export"): + i += "export".len + skipSpaces(code, i) + if startsWordAt(code, i, "interface"): + let brace = code.find('{', i) + if brace >= 0: + let close = findMatching(code, brace, '{', '}') + if close >= 0: + return close + 1 + findStatementEnd(code, i) + +proc skipType(code: string, start: int): int = + var i = start + var angleDepth = 0 + var parenDepth = 0 + var braceDepth = 0 + var bracketDepth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '<': + inc angleDepth + of '>': + if angleDepth > 0: + dec angleDepth + else: + break + of '(': + inc parenDepth + of ')': + if parenDepth == 0 and angleDepth == 0 and braceDepth == 0 and bracketDepth == 0: + break + if parenDepth > 0: dec parenDepth + of '{': + inc braceDepth + of '}': + if braceDepth == 0 and angleDepth == 0 and parenDepth == 0 and bracketDepth == 0: + break + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth == 0 and angleDepth == 0 and parenDepth == 0 and braceDepth == 0: + break + if bracketDepth > 0: dec bracketDepth + of ',', ';', '=': + if angleDepth == 0 and parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + break + else: + discard + if i + 1 < code.len and code[i] == '=' and code[i + 1] == '>': + break + inc i + i + +proc skipAssertionType(code: string, start: int): int = + var i = start + var angleDepth = 0 + var parenDepth = 0 + var braceDepth = 0 + var bracketDepth = 0 + while i < code.len: + case code[i] + of '\'', '"': + skipQuoted(code, i, code[i]) + continue + of '`': + skipTemplate(code, i) + continue + of '<': + inc angleDepth + of '>': + if angleDepth > 0: + dec angleDepth + else: + break + of '(': + inc parenDepth + of ')': + if parenDepth == 0 and angleDepth == 0 and braceDepth == 0 and bracketDepth == 0: + break + if parenDepth > 0: dec parenDepth + of '{': + inc braceDepth + of '}': + if braceDepth == 0 and angleDepth == 0 and parenDepth == 0 and bracketDepth == 0: + break + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth == 0 and angleDepth == 0 and parenDepth == 0 and braceDepth == 0: + break + if bracketDepth > 0: dec bracketDepth + of ',', ';', '\n', '\r': + if angleDepth == 0 and parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + break + else: + discard + if angleDepth == 0 and parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + if i + 1 < code.len and ((code[i] == '=' and code[i + 1] == '>') or + (code[i] == '|' and code[i + 1] == '|') or + (code[i] == '&' and code[i + 1] == '&') or + (code[i] == '?' and code[i + 1] == '?')): + break + inc i + i + +proc stripParamTypes(params: string): string + +proc stripReturnTypeAfterParen(code: string, i: var int) = + var j = i + skipSpaces(code, j) + if j < code.len and code[j] == ':': + inc j + skipSpaces(code, j) + if j < code.len and code[j] == '{': + let close = findMatching(code, j, '{', '}') + if close >= 0: + j = close + 1 + else: + j = skipType(code, j) + else: + while j < code.len: + if code[j] in {'{', ';'}: + break + if j + 1 < code.len and code[j] == '=' and code[j + 1] == '>': + break + if code[j] in {'\'', '"'}: + skipQuoted(code, j, code[j]) + continue + if code[j] == '`': + skipTemplate(code, j) + continue + inc j + i = j + +proc stripParamTypes(params: string): string = + var i = 0 + var braceDepth = 0 + var bracketDepth = 0 + while i < params.len: + case params[i] + of '\'', '"': + result.add(copyQuoted(params, i, params[i])) + continue + of '`': + result.add(copyTemplate(params, i)) + continue + of '/': + if i + 1 < params.len and params[i + 1] == '/': + result.add(copyLineComment(params, i)) + continue + if i + 1 < params.len and params[i + 1] == '*': + result.add(copyBlockComment(params, i)) + continue + of '{': + inc braceDepth + of '}': + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth > 0: dec bracketDepth + of '?': + var j = i + 1 + skipSpaces(params, j) + if j < params.len and params[j] == ':' and braceDepth == 0 and bracketDepth == 0: + i = j + continue + of ':': + if braceDepth == 0 and bracketDepth == 0: + inc i + i = skipType(params, i) + continue + else: + discard + result.add(params[i]) + inc i + +proc stripFunctionAndArrowTypes(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "function"): + result.add("function") + i += "function".len + while i < code.len and code[i] != '(': + result.add(code[i]) + inc i + if i < code.len: + let close = findMatching(code, i, '(', ')') + if close >= 0: + result.add('(') + result.add(stripParamTypes(code[i + 1..<close])) + result.add(')') + i = close + 1 + stripReturnTypeAfterParen(code, i) + continue + + if code[i] == '(': + let close = findMatching(code, i, '(', ')') + if close >= 0: + var after = close + 1 + stripReturnTypeAfterParen(code, after) + var arrowCheck = after + skipSpaces(code, arrowCheck) + if arrowCheck + 1 < code.len and code[arrowCheck] == '=' and code[arrowCheck + 1] == '>': + result.add('(') + result.add(stripParamTypes(code[i + 1..<close])) + result.add(')') + i = after + continue + + result.add(code[i]) + inc i + +proc stripTypeParametersAndArguments(code: string): string = + proc isLikelyTypeList(raw: string): bool = + let content = raw.strip() + if content.len == 0: + return false + for ch in content: + if ch in {'{', '}', '"', '\'', '`'}: + return false + if "=" in content and not ("extends" in content): + return false + true + + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if code[i] == '<': + let close = findMatchingAngle(code, i) + if close >= 0 and isLikelyTypeList(code[i + 1..<close]): + var after = close + 1 + skipSpaces(code, after) + if after < code.len and code[after] in {'{', '('}: + i = close + 1 + continue + if startsWordAt(code, after, "extends") or startsWordAt(code, after, "implements"): + i = close + 1 + continue + if after < code.len and code[after] == '(': + let parenClose = findMatching(code, after, '(', ')') + var arrowCheck = if parenClose >= 0: parenClose + 1 else: after + skipSpaces(code, arrowCheck) + let prev = i - 1 + let isAfterIdentifier = prev >= 0 and (isIdentPart(code[prev]) or code[prev] == ')') + let isGenericArrow = arrowCheck + 1 < code.len and code[arrowCheck] == '=' and code[arrowCheck + 1] == '>' + if isAfterIdentifier or isGenericArrow: + i = close + 1 + continue + + result.add(code[i]) + inc i + +proc stripTypeScriptModifiers(code: string): string = + let modifiers = ["public", "private", "protected", "abstract", "readonly", "override", "declare"] + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + var removed = false + for modifier in modifiers: + if startsWordAt(code, i, modifier): + var after = i + modifier.len + skipSpaces(code, after) + if after < code.len and (isIdentStart(code[after]) or code[after] in {'{', '(', '[', '*'}): + i = after + removed = true + break + if removed: + continue + result.add(code[i]) + inc i + +proc stripMethodAndMemberTypes(code: string): string = + proc isLikelyMemberTypeColon(colonIndex: int): bool = + var lineStart = colonIndex - 1 + while lineStart >= 0 and code[lineStart] notin {'\n', '\r', '{', ';'}: + dec lineStart + let prefix = code[lineStart + 1..<colonIndex] + if "?" in prefix or "=" in prefix or "(" in prefix or ")" in prefix or "," in prefix: + return false + var prev = colonIndex - 1 + while prev >= 0 and code[prev] in {' ', '\t'}: + dec prev + if prev >= 0 and code[prev] == '!': + dec prev + while prev >= 0 and code[prev] in {' ', '\t'}: + dec prev + prev >= 0 and (isIdentPart(code[prev]) or code[prev] in {']', '?'}) + + proc isMethodContextBeforeName(nameStart: int): bool = + var before = nameStart - 1 + while before >= 0 and code[before] in {' ', '\t'}: + dec before + if before < 0: + return true + if code[before] in {'{', '}', ';', ',', '\n', '\r'}: + return true + if isIdentPart(code[before]): + var wordStart = before + while wordStart >= 0 and isIdentPart(code[wordStart]): + dec wordStart + let word = code[wordStart + 1..before] + if word in ["async", "static", "get", "set"]: + return isMethodContextBeforeName(wordStart + 1) + false + + proc isLikelyMethodParen(openIndex: int): bool = + var prev = openIndex - 1 + while prev >= 0 and code[prev] in {' ', '\t'}: + dec prev + if prev < 0: + return false + if code[prev] == ']': + let openBracket = findMatchingReverse(code, prev, '[', ']') + return openBracket >= 0 and isMethodContextBeforeName(openBracket) + if not isIdentPart(code[prev]): + return false + var nameStart = prev + while nameStart >= 0 and isIdentPart(code[nameStart]): + dec nameStart + inc nameStart + isMethodContextBeforeName(nameStart) + + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if code[i] == '(' and isLikelyMethodParen(i): + let close = findMatching(code, i, '(', ')') + if close >= 0: + var after = close + 1 + stripReturnTypeAfterParen(code, after) + var braceCheck = after + skipSpaces(code, braceCheck) + if braceCheck < code.len and code[braceCheck] == '{': + result.add('(') + result.add(stripParamTypes(code[i + 1..<close])) + result.add(')') + i = after + continue + + if code[i] == '?': + var j = i + 1 + skipSpaces(code, j) + if j < code.len and code[j] == ':': + i = j + continue + + if code[i] == ':': + if isLikelyMemberTypeColon(i): + let typeStart = i + 1 + let typeEnd = skipType(code, typeStart) + var after = typeEnd + skipSpaces(code, after) + if after < code.len and code[after] in {';', '='}: + if code[after] == '=': + result.add(' ') + i = typeEnd + continue + + result.add(code[i]) + inc i + +proc stripVarTypes(code: string): string = + var i = 0 + var inVarDecl = false + var inInitializer = false + var braceDepth = 0 + var bracketDepth = 0 + var parenDepth = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "const") or startsWordAt(code, i, "let") or startsWordAt(code, i, "var"): + let word = + if startsWordAt(code, i, "const"): "const" + elif startsWordAt(code, i, "let"): "let" + else: "var" + result.add(word) + i += word.len + inVarDecl = true + inInitializer = false + braceDepth = 0 + bracketDepth = 0 + parenDepth = 0 + continue + + if inVarDecl: + case code[i] + of '{': + inc braceDepth + of '}': + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth > 0: dec bracketDepth + of '(': + inc parenDepth + of ')': + if parenDepth > 0: dec parenDepth + else: + discard + + let atVarTopLevel = inVarDecl and braceDepth == 0 and bracketDepth == 0 and parenDepth == 0 + + if atVarTopLevel and not inInitializer and code[i] == ':': + inc i + i = skipType(code, i) + if i < code.len and code[i] == '=': + result.add(' ') + continue + + if atVarTopLevel: + if code[i] == '=': + inInitializer = true + elif code[i] == ',': + inInitializer = false + elif code[i] in {';', '\n'}: + inVarDecl = false + inInitializer = false + + result.add(code[i]) + inc i + +proc stripAsAssertions(code: string): string = + proc isLikelyAssertionTypeStart(code: string, i: int): bool = + i < code.len and (isIdentStart(code[i]) or code[i] in {'{', '[', '(', '\'', '"'}) + + proc isPropertyAccessName(code: string, i: int): bool = + var prev = i - 1 + while prev >= 0 and code[prev] in {' ', '\t'}: + dec prev + prev >= 0 and code[prev] == '.' + + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + if startsWordAt(code, i, "import"): + let endStmt = findStatementEnd(code, i) + result.add(code[i..<endStmt]) + i = endStmt + continue + if startsWordAt(code, i, "export"): + var j = i + "export".len + skipSpaces(code, j) + if j < code.len and code[j] in {'{', '*'}: + let endStmt = findStatementEnd(code, i) + result.add(code[i..<endStmt]) + i = endStmt + continue + if startsWordAt(code, i, "as") and not isPropertyAccessName(code, i): + var j = i + 2 + skipSpaces(code, j) + if not isLikelyAssertionTypeStart(code, j): + result.add(code[i]) + inc i + continue + let endType = skipAssertionType(code, j) + i = endType + continue + if startsWordAt(code, i, "satisfies") and not isPropertyAccessName(code, i): + var j = i + "satisfies".len + skipSpaces(code, j) + if not isLikelyAssertionTypeStart(code, j): + result.add(code[i]) + inc i + continue + i = skipAssertionType(code, j) + continue + if code[i] == '!' and i + 1 < code.len and code[i + 1] notin {'=', '!'}: + var j = i + 1 + skipSpaces(code, j) + if j >= code.len or code[j] in {'.', ',', ')', ']', '}', ';'}: + inc i + continue + result.add(code[i]) + inc i + +proc stripTypeScript(code: string): string + +proc copyTemplateWithTransformedExpressions(code: string, i: var int): string = + result.add('`') + inc i + while i < code.len: + if code[i] == '\\': + let count = min(2, code.len - i) + result.add(code[i..<i + count]) + i += count + continue + if code[i] == '`': + result.add('`') + inc i + break + if code[i] == '$' and i + 1 < code.len and code[i + 1] == '{': + let close = findMatching(code, i + 1, '{', '}') + if close < 0: + raise newException(ValueError, "Unterminated template literal expression.") + result.add("${") + result.add(stripTypeScript(code[i + 2..<close])) + result.add('}') + i = close + 1 + continue + result.add(code[i]) + inc i + +proc transformTemplateLiteralTypes(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplateWithTransformedExpressions(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + result.add(code[i]) + inc i + +proc splitTopLevelCommaList(spec: string): seq[string] + +proc skipTypeParametersAt(code: string, i: var int) = + skipSpaces(code, i) + if i < code.len and code[i] == '<': + let close = findMatchingAngle(code, i) + if close >= 0: + i = close + 1 + +proc looksLikeTypeAliasAt(code: string, i: int): bool = + if not startsWordAt(code, i, "type"): + return false + var j = i + "type".len + skipSpaces(code, j) + if j >= code.len or not isIdentStart(code[j]): + return false + discard readIdentifier(code, j) + skipTypeParametersAt(code, j) + skipSpaces(code, j) + j < code.len and code[j] == '=' + +proc looksLikeInterfaceDeclarationAt(code: string, i: int): bool = + if not startsWordAt(code, i, "interface"): + return false + var j = i + "interface".len + skipSpaces(code, j) + if j >= code.len or not isIdentStart(code[j]): + return false + discard readIdentifier(code, j) + skipTypeParametersAt(code, j) + skipSpaces(code, j) + if startsWordAt(code, j, "extends"): + j += "extends".len + while j < code.len and code[j] != '{': + if code[j] in {';', '\n', '\r'}: + return false + if code[j] in {'\'', '"'}: + skipQuoted(code, j, code[j]) + continue + if code[j] == '`': + skipTemplate(code, j) + continue + inc j + skipSpaces(code, j) + j < code.len and code[j] == '{' + +proc stripTypeOnlyStatements(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplate(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if looksLikeInterfaceDeclarationAt(code, i) or + looksLikeTypeAliasAt(code, i) or + (startsWordAt(code, i, "export") and (block: + var j = i + "export".len + skipSpaces(code, j) + looksLikeInterfaceDeclarationAt(code, j) or looksLikeTypeAliasAt(code, j) or + (startsWordAt(code, j, "type") and (block: + j += "type".len + skipSpaces(code, j) + j < code.len and code[j] == '{' + )) + )): + i = removeTypeDeclaration(code, i) + continue + + if startsWordAt(code, i, "import"): + var j = i + "import".len + skipSpaces(code, j) + if startsWordAt(code, j, "type"): + i = findStatementEnd(code, i) + continue + + result.add(code[i]) + inc i + +proc stripDeclareStatements(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplateWithTransformedExpressions(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "declare"): + var j = i + "declare".len + skipSpaces(code, j) + if startsWordAt(code, j, "var") or startsWordAt(code, j, "let") or + startsWordAt(code, j, "const") or startsWordAt(code, j, "function") or + startsWordAt(code, j, "class") or startsWordAt(code, j, "enum") or + startsWordAt(code, j, "module") or startsWordAt(code, j, "global"): + i = findStatementEnd(code, i) + continue + + if startsWordAt(code, i, "export"): + var j = i + "export".len + skipSpaces(code, j) + if startsWordAt(code, j, "declare"): + var k = j + "declare".len + skipSpaces(code, k) + if startsWordAt(code, k, "var") or startsWordAt(code, k, "let") or + startsWordAt(code, k, "const") or startsWordAt(code, k, "function") or + startsWordAt(code, k, "class") or startsWordAt(code, k, "enum") or + startsWordAt(code, k, "module") or startsWordAt(code, k, "global"): + i = findStatementEnd(code, i) + continue + + result.add(code[i]) + inc i + +proc transformConstructorParameterProperties(code: string): string = + proc transformParams(params: string): tuple[code: string, assignments: seq[string]] = + let parts = splitTopLevelCommaList(params) + for index, part in parts: + if index > 0: + result.code.add(", ") + var i = 0 + var leading = "" + while i < part.len and part[i] in {' ', '\t', '\n', '\r'}: + leading.add(part[i]) + inc i + + var scan = i + var foundModifier = false + while true: + var beforeModifier = scan + skipSpaces(part, scan) + let modifier = + if startsWordAt(part, scan, "public"): "public" + elif startsWordAt(part, scan, "private"): "private" + elif startsWordAt(part, scan, "protected"): "protected" + elif startsWordAt(part, scan, "readonly"): "readonly" + else: "" + if modifier.len == 0: + scan = beforeModifier + break + foundModifier = true + scan += modifier.len + skipSpaces(part, scan) + + if foundModifier and scan < part.len and isIdentStart(part[scan]): + var namePos = scan + let name = readIdentifier(part, namePos) + if name.len > 0: + result.assignments.add("this." & name & " = " & name & ";") + result.code.add(leading & part[scan..^1]) + continue + + result.code.add(part) + + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplateWithTransformedExpressions(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "constructor"): + var open = i + "constructor".len + skipSpaces(code, open) + if open < code.len and code[open] == '(': + let close = findMatching(code, open, '(', ')') + if close >= 0: + var bodyOpen = close + 1 + skipSpaces(code, bodyOpen) + if bodyOpen < code.len and code[bodyOpen] == '{': + let transformed = transformParams(code[open + 1..<close]) + result.add(code[i..open]) + result.add(transformed.code) + result.add(code[close..<bodyOpen + 1]) + if transformed.assignments.len > 0: + result.add(transformed.assignments.join("")) + i = bodyOpen + 1 + continue + + result.add(code[i]) + inc i + +proc tokenRaw(code: string, token: JsToken): string = + if token.start >= 0 and token.`end` <= code.len and token.start <= token.`end`: + code[token.start..<token.`end`] + else: + "" + +proc tokenStatementEnd(tokens: seq[JsToken], start: int): int = + var parenDepth = 0 + var braceDepth = 0 + var bracketDepth = 0 + for index in start..<tokens.len: + case tokens[index].typ + of ttParenL: + inc parenDepth + of ttParenR: + if parenDepth > 0: dec parenDepth + of ttBraceL: + inc braceDepth + of ttBraceR: + if braceDepth == 0 and parenDepth == 0 and bracketDepth == 0: + return index + if braceDepth > 0: dec braceDepth + of ttBracketL: + inc bracketDepth + of ttBracketR: + if bracketDepth > 0: dec bracketDepth + of ttSemi: + if parenDepth == 0 and braceDepth == 0 and bracketDepth == 0: + return index + of ttEof: + return index + else: + discard + max(0, tokens.len - 1) + +proc tokenMatching(tokens: seq[JsToken], openIndex: int, openType, closeType: TokenType): int = + var depth = 0 + for index in openIndex..<tokens.len: + if tokens[index].typ == openType: + inc depth + elif tokens[index].typ == closeType: + dec depth + if depth == 0: + return index + -1 + +proc isTsModifierToken(token: JsToken): bool = + token.typ in {ttPublic, ttPrivate, ttProtected, ttReadonly, ttOverride, ttDeclare, ttAbstract} + +proc shouldRemoveDeclareStatement(tokens: seq[JsToken], index: int): bool = + if tokens[index].typ != ttDeclare: + return false + let next = index + 1 + next < tokens.len and tokens[next].typ in {ttVar, ttLet, ttConst, ttFunction, ttClass, ttEnum, ttName} + +proc shouldRemoveAbstractMember(tokens: seq[JsToken], index: int): bool = + if tokens[index].typ != ttAbstract: + return false + let next = index + 1 + if next < tokens.len and tokens[next].typ == ttClass: + return false + true + +proc tokenStripTypeScriptErasure(code: string): string = + let parsed = parseJs(code) + let tokens = parsed.tokens + var processor = initTokenProcessor(code, tokens) + var removeUntil = -1 + + while not processor.isAtEnd(): + let index = processor.currentIndex() + let token = processor.currentToken() + + if index <= removeUntil: + processor.removeToken() + continue + + if token.typ == ttEof: + processor.copyToken() + continue + + if shouldRemoveDeclareStatement(tokens, index): + removeUntil = tokenStatementEnd(tokens, index) + processor.removeToken() + continue + + if shouldRemoveAbstractMember(tokens, index): + removeUntil = tokenStatementEnd(tokens, index) + processor.removeToken() + continue + + if token.isType: + processor.removeToken() + continue + + if isTsModifierToken(token): + processor.removeToken() + continue + + if token.typ == ttBang: + let next = index + 1 + if next >= tokens.len or tokens[next].typ in {ttDot, ttComma, ttParenR, ttBracketR, ttBraceR, ttSemi, ttEof, ttColon}: + processor.removeToken() + continue + + if token.typ == ttQuestion: + let next = index + 1 + if next < tokens.len and tokens[next].typ == ttColon: + processor.removeToken() + continue + + processor.copyToken() + + processor.finish().code + +proc stripAbstractMembers(code: string): string = + var i = 0 + while i < code.len: + if code[i] in {'\'', '"'}: + result.add(copyQuoted(code, i, code[i])) + continue + if code[i] == '`': + result.add(copyTemplateWithTransformedExpressions(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': + result.add(copyLineComment(code, i)) + continue + if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': + result.add(copyBlockComment(code, i)) + continue + + if startsWordAt(code, i, "abstract"): + var j = i + "abstract".len + skipSpaces(code, j) + if startsWordAt(code, j, "class"): + result.add(code[i]) + inc i + continue + let endStmt = findStatementEnd(code, i) + var k = endStmt + while k < code.len and code[k] in {' ', '\t'}: + inc k + if k >= code.len or code[k] in {'\n', '\r', ';', '}'}: + i = endStmt + continue + + result.add(code[i]) + inc i + +proc stripTypeScript(code: string): string = + result = code.lowerEnums() + result = result.transformConstructorParameterProperties() + result = result.stripTypeOnlyStatements() + result = result.stripDeclareStatements() + result = result.stripAbstractMembers() + result = result.stripTypeScriptModifiers() + result = result.stripTypeParametersAndArguments() + result = result.stripFunctionAndArrowTypes() + result = result.stripMethodAndMemberTypes() + result = result.stripVarTypes() + result = result.stripAsAssertions() + result = result.transformTemplateLiteralTypes() + result = result.tokenStripTypeScriptErasure() + +proc shouldStartJsx(code: string, i: int): bool = + if i + 1 >= code.len or code[i] != '<': + return false + if not (code[i + 1] == '>' or isIdentStart(code[i + 1])): + return false + var prev = i - 1 + while prev >= 0 and code[prev] in {' ', '\t', '\n', '\r'}: + dec prev + if prev < 0: + return true + if code[prev] in {'(', '[', '{', '=', ':', ',', ';', '!', '?', '>'}: + return true + if isIdentPart(code[prev]): + var start = prev + while start >= 0 and isIdentPart(code[start]): + dec start + let word = code[start + 1..prev] + return word in ["return", "yield", "case", "throw"] + false + +proc readBalancedBrace(p: var JsxParser): string = + if p.pos >= p.code.len or p.code[p.pos] != '{': + return "" + let start = p.pos + 1 + let close = findMatching(p.code, p.pos, '{', '}') + if close < 0: + raise newException(ValueError, "Unterminated JSX expression.") + p.pos = close + 1 + p.code[start..<close] + +proc transformJSX(code: string): string + +proc transformExpression(code: string): string = + transformJSX(stripTypeScript(code)) + +proc readJsxName(p: var JsxParser): string = + let start = p.pos + while p.pos < p.code.len and (isIdentPart(p.code[p.pos]) or p.code[p.pos] in {'-', ':', '.'}): + inc p.pos + p.code[start..<p.pos] + +proc jsxTagCode(name: string): string = + if name.len == 0: + return "\"\"" + if name[0] in {'a'..'z'} or '-' in name or ':' in name: + jsonQuote(name) + else: + name + +proc normalizedJsxText(raw: string): string = + let collapsed = raw.replace("\r", "\n").splitLines().mapIt(it.strip()).filterIt(it.len > 0).join(" ") + decodeJsxEntities(collapsed) + +proc parseJsxElement(p: var JsxParser): string + +proc parseJsxChildren(p: var JsxParser, closingName: string): seq[string] = + while p.pos < p.code.len: + if p.code[p.pos] == '<' and p.pos + 1 < p.code.len and p.code[p.pos + 1] == '/': + p.pos += 2 + if closingName.len > 0: + let found = readJsxName(p) + if found != closingName: + raise newException(ValueError, "Mismatched JSX closing tag: " & found) + skipSpaces(p.code, p.pos) + if p.pos < p.code.len and p.code[p.pos] == '>': + inc p.pos + return + if shouldStartJsx(p.code, p.pos): + result.add(parseJsxElement(p)) + continue + if p.code[p.pos] == '{': + let expression = readBalancedBrace(p).strip() + if expression.len > 0 and expression != "...": + result.add(transformExpression(expression)) + continue + let start = p.pos + while p.pos < p.code.len and not (p.code[p.pos] == '{' or p.code[p.pos] == '<'): + inc p.pos + let text = normalizedJsxText(p.code[start..<p.pos]) + if text.len > 0: + result.add(jsonQuote(text)) + +proc parseJsxProps(p: var JsxParser): string = + var props: seq[string] = @[] + while p.pos < p.code.len: + skipSpaces(p.code, p.pos) + if p.pos >= p.code.len or p.code[p.pos] == '>' or + (p.code[p.pos] == '/' and p.pos + 1 < p.code.len and p.code[p.pos + 1] == '>'): + break + if p.code[p.pos] == '{': + let expression = readBalancedBrace(p).strip() + if expression.startsWith("..."): + props.add("..." & transformExpression(expression[3..^1].strip())) + continue + let name = readJsxName(p) + if name.len == 0: + inc p.pos + continue + skipSpaces(p.code, p.pos) + if p.pos >= p.code.len or p.code[p.pos] != '=': + props.add(jsonQuote(name) & ": true") + continue + inc p.pos + skipSpaces(p.code, p.pos) + var value = "true" + if p.pos < p.code.len and p.code[p.pos] in {'\'', '"'}: + let raw = copyQuoted(p.code, p.pos, p.code[p.pos]) + value = + if raw.len >= 2: + jsonQuote(decodeJsxEntities(raw[1..^2])) + else: + raw + elif p.pos < p.code.len and p.code[p.pos] == '{': + value = transformExpression(readBalancedBrace(p)) + elif shouldStartJsx(p.code, p.pos): + value = parseJsxElement(p) + props.add(jsonQuote(name) & ": " & value) + if props.len == 0: + "null" + else: + "{" & props.join(", ") & "}" + +proc parseJsxElement(p: var JsxParser): string = + if p.pos >= p.code.len or p.code[p.pos] != '<': + raise newException(ValueError, "Expected JSX tag.") + p.pos += 1 + if p.pos < p.code.len and p.code[p.pos] == '>': + inc p.pos + let children = parseJsxChildren(p, "") + var args = @["__frameosFragment", "null"] + args.add(children) + return "__frameosJsx(" & args.join(", ") & ")" + + let name = readJsxName(p) + let props = parseJsxProps(p) + if p.pos + 1 < p.code.len and p.code[p.pos] == '/' and p.code[p.pos + 1] == '>': + p.pos += 2 + return "__frameosJsx(" & jsxTagCode(name) & ", " & props & ")" + if p.pos < p.code.len and p.code[p.pos] == '>': + inc p.pos + let children = parseJsxChildren(p, name) + var args = @[jsxTagCode(name), props] + args.add(children) + "__frameosJsx(" & args.join(", ") & ")" + +proc transformJSX(code: string): string = + let parsed = parseJs(code) + let tokens = parsed.tokens + var processor = initTokenProcessor(code, tokens) + + while not processor.isAtEnd(): + let token = processor.currentToken() + if token.typ == ttEof: + processor.copyToken() + continue + + if token.typ == ttJsxTagStart: + let next = processor.currentIndex() + 1 + if next < tokens.len and tokens[next].typ == ttSlash: + processor.copyToken() + continue + + var parser = JsxParser(code: code, pos: token.start) + let lowered = parseJsxElement(parser) + processor.replaceToken(lowered) + while not processor.isAtEnd() and processor.currentToken().typ != ttEof and + processor.currentToken().start < parser.pos: + inc processor.tokenIndex + continue + + processor.copyToken() + + processor.finish().code + +proc parseExportNames(spec: string): seq[(string, string)] = + for rawPart in spec.split(','): + let part = rawPart.strip() + if part.len == 0: + continue + let pieces = part.splitWhitespace() + if pieces.len == 1: + result.add((pieces[0], pieces[0])) + elif pieces.len == 3 and pieces[1] == "as": + result.add((pieces[0], pieces[2])) + +proc sanitizeModuleIdentifier(path: string): string = + result = "_" + for ch in path: + if ch.isAlphaNumeric: + result.add(ch) + else: + result.add('_') + if result.len == 1: + result.add("module") + +proc uniqueModuleIdentifier(path: string, counter: var int): string = + inc counter + sanitizeModuleIdentifier(path) & "_" & $counter + +proc unquoteModulePath(raw: string): string = + let value = raw.strip() + if value.len >= 2 and value[0] in {'\'', '"'} and value[^1] == value[0]: + value[1..^2] + else: + value + +proc splitTopLevelCommaList(spec: string): seq[string] = + var i = 0 + var partStart = 0 + var braceDepth = 0 + var bracketDepth = 0 + var parenDepth = 0 + while i < spec.len: + case spec[i] + of '\'', '"': + skipQuoted(spec, i, spec[i]) + continue + of '`': + skipTemplate(spec, i) + continue + of '{': + inc braceDepth + of '}': + if braceDepth > 0: dec braceDepth + of '[': + inc bracketDepth + of ']': + if bracketDepth > 0: dec bracketDepth + of '(': + inc parenDepth + of ')': + if parenDepth > 0: dec parenDepth + of ',': + if braceDepth == 0 and bracketDepth == 0 and parenDepth == 0: + let part = spec[partStart..<i].strip() + if part.len > 0: + result.add(part) + partStart = i + 1 + else: + discard + inc i + let lastPart = spec[partStart..^1].strip() + if lastPart.len > 0: + result.add(lastPart) + +proc parseImportExportSpecifiers(spec: string): seq[(string, string)] = + for rawPart in splitTopLevelCommaList(spec): + var part = rawPart.strip() + if part.startsWith("type "): + continue + let pieces = part.splitWhitespace() + if pieces.len == 1: + result.add((pieces[0], pieces[0])) + elif pieces.len == 3 and pieces[1] == "as": + result.add((pieces[0], pieces[2])) + +proc collectVarDeclarationNames(declaration: string): seq[string] = + let trimmed = declaration.strip() + var i = 0 + if startsWordAt(trimmed, i, "const"): + i += "const".len + elif startsWordAt(trimmed, i, "let"): + i += "let".len + elif startsWordAt(trimmed, i, "var"): + i += "var".len + else: + return + let declarators = trimmed[i..^1].strip().strip(chars = {';'}) + for part in splitTopLevelCommaList(declarators): + var namePos = 0 + let name = readIdentifier(part, namePos) + if name.len > 0: + result.add(name) + +proc emitImportDeclaration(stmt: string, moduleCounter: var int): string = + let stripped = stmt.strip().strip(chars = {';'}) + if stripped.startsWith("import type"): + return "" + if stripped.startsWith("import("): + return stmt + if not stripped.startsWith("import"): + return stmt + + var rest = stripped["import".len..^1].strip() + if rest.len == 0: + return "" + + let requireEquals = rest.find("= require") + if requireEquals > 0: + let localName = rest[0..<requireEquals].strip() + let requireCall = rest[requireEquals + 1..^1].strip() + if localName.len > 0: + return "const " & localName & " = " & requireCall & ";" + + if rest[0] in {'\'', '"'}: + return "require(" & rest & ");" + + let fromIndex = rest.rfind(" from ") + if fromIndex < 0: + return "throw new Error(\"Unsupported import declaration\");" + let bindings = rest[0..<fromIndex].strip() + let pathCode = rest[fromIndex + " from ".len..^1].strip() + let path = unquoteModulePath(pathCode) + let moduleName = uniqueModuleIdentifier(path, moduleCounter) + result = "var " & moduleName & " = require(" & pathCode & ");" + + var remaining = bindings + if remaining.len == 0: + return + + if remaining.startsWith("*"): + let pieces = remaining.splitWhitespace() + if pieces.len >= 3 and pieces[1] == "as": + result.add(" var " & pieces[2] & " = " & moduleName & ";") + return + + if remaining.startsWith("{"): + let close = remaining.rfind("}") + if close >= 0: + for (importedName, localName) in parseImportExportSpecifiers(remaining[1..<close]): + result.add(" var " & localName & " = " & moduleName & "." & importedName & ";") + return + + let commaIndex = remaining.find(',') + if commaIndex >= 0: + let defaultName = remaining[0..<commaIndex].strip() + if defaultName.len > 0: + result.add(" var " & defaultName & " = " & moduleName & ".default;") + remaining = remaining[commaIndex + 1..^1].strip() + if remaining.startsWith("*"): + let pieces = remaining.splitWhitespace() + if pieces.len >= 3 and pieces[1] == "as": + result.add(" var " & pieces[2] & " = " & moduleName & ";") + elif remaining.startsWith("{"): + let close = remaining.rfind("}") + if close >= 0: + for (importedName, localName) in parseImportExportSpecifiers(remaining[1..<close]): + result.add(" var " & localName & " = " & moduleName & "." & importedName & ";") + else: + result.add(" var " & remaining & " = " & moduleName & ".default;") + +proc emitExportFromDeclaration(stmt: string, moduleCounter: var int): string = + let stripped = stmt.strip().strip(chars = {';'}) + if not stripped.startsWith("export"): + return stmt + var rest = stripped["export".len..^1].strip() + if rest.startsWith("type "): + return "" + if rest.startsWith("*"): + let fromIndex = rest.rfind(" from ") + if fromIndex < 0: + return "throw new Error(\"Unsupported export star declaration\");" + let pathCode = rest[fromIndex + " from ".len..^1].strip() + let path = unquoteModulePath(pathCode) + let moduleName = uniqueModuleIdentifier(path, moduleCounter) + if rest.startsWith("* as "): + let exportedName = rest["* as ".len..<fromIndex].strip() + return "exports." & exportedName & " = require(" & pathCode & ");" + return "var " & moduleName & " = require(" & pathCode & "); Object.keys(" & moduleName & ").forEach(function (key) { if (key !== \"default\" && key !== \"__esModule\") exports[key] = " & moduleName & "[key]; });" + if rest.startsWith("{"): + let close = rest.find('}') + if close < 0: + return "throw new Error(\"Unsupported export declaration\");" + var after = close + 1 + skipSpaces(rest, after) + if not startsWordAt(rest, after, "from"): + return "" + after += "from".len + skipSpaces(rest, after) + let pathCode = rest[after..^1].strip() + let path = unquoteModulePath(pathCode) + let moduleName = uniqueModuleIdentifier(path, moduleCounter) + result = "var " & moduleName & " = require(" & pathCode & ");" + for (importedName, exportedName) in parseImportExportSpecifiers(rest[1..<close]): + result.add(" exports." & exportedName & " = " & moduleName & "." & importedName & ";") + return + stmt + +proc tokenStatementSlice(code: string, tokens: seq[JsToken], startIndex, endIndex: int): string = + if startIndex < 0 or startIndex >= tokens.len: + return "" + let startPos = tokens[startIndex].start + let endPos = + if endIndex >= 0 and endIndex < tokens.len and tokens[endIndex].typ != ttEof: + tokens[endIndex].`end` + elif endIndex > startIndex and endIndex - 1 < tokens.len: + tokens[endIndex - 1].`end` + else: + tokens[startIndex].`end` + if startPos >= 0 and endPos >= startPos and endPos <= code.len: + code[startPos..<endPos] + else: + "" + +proc skipProcessorThrough(processor: var TokenProcessor, endIndex: int) = + while not processor.isAtEnd() and processor.currentIndex() <= endIndex and processor.currentToken().typ != ttEof: + processor.removeToken() + +proc exportDeclarationName(code: string, tokens: seq[JsToken], startIndex: int): string = + var i = startIndex + if i < tokens.len and tokens[i].typ == ttAsync: + inc i + if i < tokens.len and tokens[i].typ in {ttFunction, ttClass}: + inc i + if i < tokens.len and tokens[i].typ == ttStar: + inc i + if i < tokens.len and tokens[i].typ in {ttName, ttGet, ttSet}: + tokenRaw(code, tokens[i]) + else: + "" + +proc transformImportsTokenDriven(code: string): string = + let parsed = parseJs(code) + let tokens = parsed.tokens + var processor = initTokenProcessor(code, tokens) + var moduleCounter = 0 + var imports: seq[string] = @[] + var exports: seq[string] = @[] + + while not processor.isAtEnd(): + let index = processor.currentIndex() + let token = processor.currentToken() + + if token.typ == ttEof: + processor.copyToken() + continue + + if token.typ == ttImport: + let next = index + 1 + if next < tokens.len and tokens[next].typ notin {ttParenL, ttDot}: + let endIndex = tokenStatementEnd(tokens, index) + let stmt = tokenStatementSlice(code, tokens, index, endIndex).strip() + if not stmt.startsWith("import("): + let emitted = emitImportDeclaration(stmt, moduleCounter) + if emitted.len > 0: + imports.add(emitted) + processor.skipProcessorThrough(endIndex) + continue + + if token.typ == ttExport: + let endIndex = tokenStatementEnd(tokens, index) + var j = index + 1 + if j < tokens.len and tokens[j].typ == ttType: + processor.skipProcessorThrough(endIndex) + continue + + if j < tokens.len and tokens[j].typ == ttStar: + let emitted = emitExportFromDeclaration(tokenStatementSlice(code, tokens, index, endIndex), moduleCounter) + if emitted.len > 0: + imports.add(emitted) + processor.skipProcessorThrough(endIndex) + continue + + if j < tokens.len and tokens[j].typ == ttBraceL: + let close = tokenMatching(tokens, j, ttBraceL, ttBraceR) + var after = close + 1 + if close >= 0 and after < tokens.len and tokens[after].typ == ttName and tokenRaw(code, tokens[after]) == "from": + let emitted = emitExportFromDeclaration(tokenStatementSlice(code, tokens, index, endIndex), moduleCounter) + if emitted.len > 0: + imports.add(emitted) + processor.skipProcessorThrough(endIndex) + continue + if close >= 0: + for (localName, exportedName) in parseExportNames(code[tokens[j].`end`..<tokens[close].start]): + exports.add("exports." & exportedName & " = " & localName & ";") + processor.skipProcessorThrough(endIndex) + continue + + if j < tokens.len and tokens[j].typ in {ttConst, ttLet, ttVar}: + let declaration = tokenStatementSlice(code, tokens, j, endIndex) + for name in collectVarDeclarationNames(declaration): + exports.add("exports." & name & " = " & name & ";") + processor.removeToken() + continue + + if j < tokens.len and tokens[j].typ in {ttFunction, ttClass, ttAsync}: + let name = exportDeclarationName(code, tokens, j) + if name.len > 0: + exports.add("exports." & name & " = " & name & ";") + processor.removeToken() + continue + + if j < tokens.len and tokens[j].typ == ttDefault: + var declarationStart = j + 1 + if declarationStart < tokens.len and tokens[declarationStart].typ in {ttFunction, ttClass, ttAsync}: + let name = exportDeclarationName(code, tokens, declarationStart) + if name.len > 0: + exports.add("exports.default = " & name & ";") + processor.removeToken() + if not processor.isAtEnd() and processor.currentIndex() == j: + processor.removeToken() + continue + processor.replaceToken("exports.default =") + if not processor.isAtEnd() and processor.currentIndex() == j: + processor.removeToken() + continue + + processor.copyToken() + + let body = processor.finish().code + result = "\"use strict\";Object.defineProperty(exports, \"__esModule\", {value: true});" + if imports.len > 0: + result.add(imports.join("")) + result.add(body) + if exports.len > 0: + result.add("\n") + result.add(exports.join("\n")) + +proc transformImports(code: string): string = + return transformImportsTokenDriven(code) + +proc transform*(code: string, options: TransformOptions): TransformResult = + let originalCode = code + let path = if options.filePath.len == 0: "<frameos>" else: options.filePath + try: + result.code = code + if options.hasTransform("typescript"): + result.code = stripTypeScript(result.code) + if options.hasTransform("jsx"): + result.code = transformJSX(result.code) + if options.hasTransform("typescript"): + result.code = stripTypeScript(result.code) + if options.hasTransform("imports"): + result.code = transformImports(result.code) + result.sourceMap = lineBasedSourceLineMap(originalCode, result.code, path, path) + except CatchableError as error: + raise newException(ValueError, "Error transforming " & path & ": " & error.msg) + +proc transformFrameosScript*(code: string, filePath: string = "<frameos>"): string = + transform(code, TransformOptions(filePath: filePath, transforms: defaultTransforms)).code + +proc transformFrameosModule*(code: string, filePath: string = "<frameos>"): string = + transform(code, TransformOptions(filePath: filePath, transforms: moduleTransforms)).code diff --git a/frameos/src/frameos/scenes.nim b/frameos/src/frameos/scenes.nim index 7c639c112..b043125bf 100644 --- a/frameos/src/frameos/scenes.nim +++ b/frameos/src/frameos/scenes.nim @@ -4,7 +4,7 @@ import scenes/scenes import system/scenes as systemScenesRegistry import frameos/types import frameos/interpreter -import frameos/js_runtime +import frameos/js_runtime/runtime # Where to store the persisted states const SCENE_STATE_JSON_FOLDER = "./state" diff --git a/frameos/src/frameos/tests/test_js_app_runtime.nim b/frameos/src/frameos/tests/test_js_app_runtime.nim deleted file mode 100644 index cc39ceb3e..000000000 --- a/frameos/src/frameos/tests/test_js_app_runtime.nim +++ /dev/null @@ -1,170 +0,0 @@ -import std/[json, sequtils, tables, unittest] -import pixie - -import ../js_app_runtime -import ../types -import ../values - -proc testConfig(): FrameConfig = - FrameConfig( - width: 6, - height: 4, - rotate: 0, - scalingMode: "cover", - debug: true, - saveAssets: %*false, - assetsPath: "/tmp" - ) - -proc testLogger(config: FrameConfig): Logger = - var logger = Logger(frameConfig: config, enabled: true) - logger.log = proc(payload: JsonNode) = - discard payload - logger.enable = proc() = - logger.enabled = true - logger.disable = proc() = - logger.enabled = false - logger - -suite "js app runtime": - test "returns string, node, and image values": - let config = testConfig() - let logger = testLogger(config) - let scene = FrameScene(id: "tests/js-app".SceneId, frameConfig: config, state: %*{}, logger: logger) - let owner = AppRoot(nodeId: 7.NodeId, nodeName: "jsText", scene: scene, frameConfig: config) - let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) - - let runtime = newJsAppRuntime( - category = "data", - outputType = "text", - source = """export const get = (app: { config: { mode: string; message?: string; targetNode?: number } }, context: { event: string }) => { - if (app.config.mode === "image") { - return <image width={3} height={2} color="#336699" /> - } - if (app.config.mode === "node") { - return frameos.node(app.config.targetNode) - } - return `${app.config.message}:${context.event}` - }""" - ) - - let textValue = runtime.get(owner, %*{"message": "hello", "mode": "text"}, context) - check textValue.kind == fkString - check textValue.asString() == "hello:render" - - let nodeValue = runtime.get(owner, %*{"mode": "node", "targetNode": 9}, context) - check nodeValue.kind == fkNode - check nodeValue.asNode() == 9.NodeId - - let imageValue = runtime.get(owner, %*{"mode": "image"}, context) - check imageValue.kind == fkImage - check imageValue.asImage().width == 3 - check imageValue.asImage().height == 2 - - test "run can set next sleep, state, and draw a render image": - let config = testConfig() - let logger = testLogger(config) - let scene = FrameScene(id: "tests/js-app-run".SceneId, frameConfig: config, state: %*{}, logger: logger) - let owner = AppRoot(nodeId: 8.NodeId, nodeName: "jsLogic", scene: scene, frameConfig: config) - var image = newImage(4, 3) - let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: true, image: image, loopIndex: 0, loopKey: ".", nextSleep: -1) - - let runtime = newJsAppRuntime( - category = "render", - outputType = "image", - source = """export function run(app: { config: { duration: number } }) { - frameos.setNextSleep(app.config.duration) - frameos.setState("lastDuration", app.config.duration) - return <image width={4} height={3} color="#ff0000" /> - }""" - ) - - runtime.run(owner, %*{"duration": 12.5}, context) - check abs(context.nextSleep - 12.5) < 0.0001 - check scene.state["lastDuration"].getFloat() == 12.5 - let pixel = context.image.data[context.image.dataIndex(0, 0)] - check pixel.r > 0 - check runtime.images.len == 0 - - test "clears transient context image refs after JS calls": - let config = testConfig() - let logger = testLogger(config) - let scene = FrameScene(id: "tests/js-app-image-refs".SceneId, frameConfig: config, state: %*{}, logger: logger) - let owner = AppRoot(nodeId: 9.NodeId, nodeName: "jsImageRefs", scene: scene, frameConfig: config) - - let runtime = newJsAppRuntime( - category = "data", - outputType = "image", - source = """export function get(app, context) { - return context.image - }""" - ) - - for i in 0..<3: - let image = newImage(4 + i, 3) - let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: true, image: image, loopIndex: i, loopKey: ".", nextSleep: -1) - let value = runtime.get(owner, %*{}, context) - check value.kind == fkImage - check value.asImage().width == 4 + i - check value.asImage().height == 3 - check runtime.images.len == 0 - - test "lazy app proxies support keys and spread": - let config = testConfig() - let logger = testLogger(config) - let scene = FrameScene(id: "tests/js-app-proxy-keys".SceneId, frameConfig: config, state: %*{"seen": true}, logger: logger) - let owner = AppRoot(nodeId: 11.NodeId, nodeName: "jsProxyKeys", scene: scene, frameConfig: config) - let context = ExecutionContext(scene: scene, event: "render", payload: %*{}, hasImage: false, loopIndex: 0, loopKey: ".", nextSleep: -1) - - let runtime = newJsAppRuntime( - category = "data", - outputType = "json", - source = """export function get(app, context) { - return { - configKeys: Object.keys(app.config).sort(), - stateKeys: Object.keys(app.state).sort(), - frameKeys: Object.keys(app.frame).sort(), - contextKeys: Object.keys(context).sort(), - spreadConfig: { ...app.config }, - } - }""" - ) - - let value = runtime.get(owner, %*{"message": "hello", "mode": "text"}, context) - check value.kind == fkJson - let payload = value.asJson() - check payload["configKeys"][0].getStr() == "message" - check payload["configKeys"][1].getStr() == "mode" - check payload["stateKeys"][0].getStr() == "seen" - check "width" in payload["frameKeys"].mapIt(it.getStr()) - check "event" in payload["contextKeys"].mapIt(it.getStr()) - check payload["spreadConfig"]["message"].getStr() == "hello" - - test "releases overwritten dynamic field image refs": - let config = testConfig() - let logger = testLogger(config) - let scene = FrameScene(id: "tests/js-app-field-refs".SceneId, frameConfig: config, state: %*{}, logger: logger) - let runtime = newJsAppRuntime(category = "data", outputType = "image", source = "export const get = () => null") - let app = DynamicJsApp( - nodeId: 10.NodeId, - nodeName: "jsFieldRefs", - scene: scene, - frameConfig: config, - configJson: %*{}, - runtime: runtime - ) - - setDynamicJsAppField(app, "inputImage", VImage(newImage(4, 3))) - check runtime.images.len == 1 - let firstId = app.configJson["inputImage"]["id"].getInt() - check runtime.images.hasKey(firstId) - - setDynamicJsAppField(app, "inputImage", VImage(newImage(5, 3))) - check runtime.images.len == 1 - check not runtime.images.hasKey(firstId) - let secondId = app.configJson["inputImage"]["id"].getInt() - check secondId != firstId - check runtime.images.hasKey(secondId) - - setDynamicJsAppField(app, "inputImage", VString("not an image")) - check runtime.images.len == 0 diff --git a/frameos/src/frameos/types.nim b/frameos/src/frameos/types.nim index 8408340d4..78d3c5b06 100644 --- a/frameos/src/frameos/types.nim +++ b/frameos/src/frameos/types.nim @@ -1,7 +1,7 @@ import json, pixie, locks, tables, options, asyncdispatch, mummy import frameos/ids export ids -import lib/burrito +import frameos/js_runtime/burrito const DefaultMaxHttpResponseBytes* = 64 * 1024 * 1024 diff --git a/frameos/tools/native_js_transpile.nim b/frameos/tools/native_js_transpile.nim new file mode 100644 index 000000000..4e583eda9 --- /dev/null +++ b/frameos/tools/native_js_transpile.nim @@ -0,0 +1,70 @@ +import std/[json, os] + +import frameos/js_runtime/parser +import frameos/js_runtime/source_map +import frameos/js_runtime/tokens +import frameos/js_runtime/transpiler + +if paramCount() < 2: + stderr.writeLine("Usage: native_js_transpile <script|module|script-json|module-json|tokens|parse> <source-file>") + quit(2) + +let mode = paramStr(1) +let path = paramStr(2) +let source = readFile(path) + +proc sourceMapToJson(sourceMap: SourceLineMap): JsonNode = + let segments = newJArray() + for segment in sourceMap.segments: + segments.add(%*{ + "generatedLine": segment.generatedLine, + "generatedColumn": segment.generatedColumn, + "sourceLine": segment.sourceLine, + "sourceColumn": segment.sourceColumn, + }) + + %*{ + "generatedName": sourceMap.generatedName, + "sourceName": sourceMap.sourceName, + "generatedToSourceLine": sourceMap.generatedToSourceLine, + "segments": segments, + } + +proc writeTransformJson(transformed: TransformResult) = + stdout.write($(%*{ + "ok": true, + "code": transformed.code, + "sourceMap": transformed.sourceMap.sourceMapToJson(), + })) + +try: + case mode + of "script": + stdout.write(transformFrameosScript(source, path)) + of "module": + stdout.write(transformFrameosModule(source, path)) + of "script-json": + writeTransformJson(transform(source, TransformOptions(filePath: path, transforms: @["typescript", "jsx"]))) + of "module-json": + writeTransformJson(transform(source, TransformOptions(filePath: path, transforms: @["typescript", "jsx", "imports"]))) + of "tokens": + stdout.write(formatTokens(source, tokenizeJs(source))) + of "parse": + stdout.write(formatAnnotatedTokens(source, parseJs(source))) + else: + stderr.writeLine("Unknown mode: " & mode) + quit(2) +except CatchableError as error: + if mode in ["script-json", "module-json"]: + stdout.write($(%*{ + "ok": false, + "errors": [ + { + "text": error.msg, + "location": {"line": 1, "column": 1}, + } + ], + })) + else: + stderr.writeLine(error.msg) + quit(1) diff --git a/frameos/tools/prepare_assets.py b/frameos/tools/prepare_assets.py index 6755d949e..d22c11306 100644 --- a/frameos/tools/prepare_assets.py +++ b/frameos/tools/prepare_assets.py @@ -20,10 +20,6 @@ Path("assets/compiled/frame_web/static/main.css"), ) -OPTIONAL_FRONTEND_OUTPUTS = ( - Path("assets/compiled/vendor/sucrase.js"), -) - MODULE_OUTPUTS = ( Path("src/assets/apps.nim"), Path("src/assets/web.nim"), @@ -31,10 +27,6 @@ Path("src/assets/fonts.nim"), ) -OPTIONAL_MODULE_OUTPUTS = ( - (Path("assets/compiled/vendor"), Path("src/assets/vendor.nim")), -) - MODULE_SOURCE_FILES = ( Path("assets/compiled/web/control.html"), Path("assets/compiled/fonts/Ubuntu-Regular.ttf"), @@ -195,30 +187,17 @@ def hash_module_inputs(project_root: Path) -> str: entries = [ *MODULE_SOURCE_FILES, Path("assets/compiled/frame_web"), - Path("assets/compiled/vendor"), *iter_app_config_entries(project_root), ] return hash_inputs(project_root, entries) def frontend_outputs_exist(project_root: Path) -> bool: - if not all((project_root / output).is_file() for output in FRONTEND_OUTPUTS): - return False - for output in OPTIONAL_FRONTEND_OUTPUTS: - parent = project_root / output.parent - if parent.exists() and any(parent.rglob("*")) and not (project_root / output).is_file(): - return False - return True + return all((project_root / output).is_file() for output in FRONTEND_OUTPUTS) def module_outputs_exist(project_root: Path) -> bool: - if not all((project_root / output).is_file() for output in MODULE_OUTPUTS): - return False - for source_dir, output in OPTIONAL_MODULE_OUTPUTS: - source_root = project_root / source_dir - if source_root.exists() and any(source_root.rglob("*")) and not (project_root / output).is_file(): - return False - return True + return all((project_root / output).is_file() for output in MODULE_OUTPUTS) def load_manifest(project_root: Path) -> AssetsManifest | None: @@ -373,16 +352,6 @@ def generate_asset_modules(project_root: Path) -> None: "--output", "src/assets/fonts.nim", ], - [ - python, - "tools/generate_compressed_asset_nim.py", - "--source-dir", - ".", - "--dir", - "assets/compiled/vendor", - "--output", - "src/assets/vendor.nim", - ], ) for command in commands: diff --git a/frameos/tools/run_test_shard.sh b/frameos/tools/run_test_shard.sh index de24a2914..9f1afece5 100644 --- a/frameos/tools/run_test_shard.sh +++ b/frameos/tools/run_test_shard.sh @@ -83,7 +83,7 @@ declare -a shard_1_tests=( "src/apps/data/weather/tests/test_app.nim" "src/apps/data/rotateImage/tests/test_app.nim" "src/apps/data/prettyJson/tests/test_app.nim" - "src/frameos/tests/test_scene_runtime_cleanup.nim" + "src/frameos/js_runtime/tests/test_scene_runtime_cleanup.nim" "src/frameos/tests/test_scenes_persistence.nim" "src/apps/render/svg/tests/test_app.nim" "src/apps/data/icalJson/tests/test_ical.nim" @@ -147,7 +147,7 @@ declare -a shard_4_tests=( "src/frameos/server/tests/test_web_routes_behavior.nim" "src/apps/data/unsplash/tests/test_app.nim" "src/frameos/tests/test_logger.nim" - "src/frameos/tests/test_js_runtime_helpers.nim" + "src/frameos/js_runtime/tests/test_js_runtime_helpers.nim" "src/frameos/utils/tests/test_url.nim" "src/frameos/utils/tests/test_dither.nim" ) @@ -157,7 +157,7 @@ declare -a shard_5_tests=( "src/frameos/server/tests/test_auth.nim" "src/frameos/tests/test_apps_helpers.nim" "src/apps/data/clock/tests/test_app.nim" - "src/frameos/tests/test_js_app_runtime.nim" + "src/frameos/js_runtime/tests/test_js_app_runtime.nim" "src/frameos/tests/test_scenes_registry_state_cleanup.nim" "src/apps/data/frameOSGallery/tests/test_app.nim" "src/apps/data/beRecycle/tests/test_app.nim" diff --git a/frameos/tools/tests/test_native_js_transpiler_parity.py b/frameos/tools/tests/test_native_js_transpiler_parity.py new file mode 100644 index 000000000..7443d7aef --- /dev/null +++ b/frameos/tools/tests/test_native_js_transpiler_parity.py @@ -0,0 +1,561 @@ +from __future__ import annotations + +import json +import re +import shutil +import subprocess +import tempfile +from dataclasses import dataclass +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[3] +FRAMEOS_ROOT = ROOT / "frameos" +FRAME_FRONTEND_ROOT = FRAMEOS_ROOT / "frontend" +NATIVE_CLI = FRAMEOS_ROOT / "tools" / "native_js_transpile.nim" + + +JSX_PRELUDE = r""" +const __frameosFragment = Symbol.for("frameos.fragment"); +const __frameosNormalizeChildren = (children) => { + if (children.length === 0) return undefined; + if (children.length === 1) return children[0]; + return children; +}; +const __frameosJsx = (type, props, ...children) => { + const nextProps = props ? { ...props } : {}; + const explicitChildren = __frameosNormalizeChildren(children); + const propChildren = Object.prototype.hasOwnProperty.call(nextProps, "children") + ? nextProps.children + : undefined; + if (Object.prototype.hasOwnProperty.call(nextProps, "children")) { + delete nextProps.children; + } + const normalizedChildren = explicitChildren ?? propChildren; + if (type === __frameosFragment) { + return normalizedChildren ?? null; + } + if (normalizedChildren !== undefined) { + nextProps.children = normalizedChildren; + } + return { type, props: nextProps }; +}; +""" + + +@dataclass(frozen=True) +class Fixture: + name: str + source: str + app: dict + context: dict + xfail_native: str | None = None + + +FIXTURES = [ + Fixture( + name="typed_jsx_and_modern_es", + source=r''' + export function get(app: { config?: { nested?: { count?: number }, label?: string } }, context: { event: string }) { + class Counter { + value = 1_000 + increment = () => ++this.value + } + const metadata = { type: "image", as: "alias", satisfies: true } + const count = app.config?.nested?.count ?? new Counter().increment() + const label = (app.config?.label ?? "FrameOS") as string + const ok = /frame\s*os/i.test("Frame OS") + return <image width={count} label={`${label}:${context.event}`} metadata={metadata} ok={ok} /> + } + ''', + app={"config": {"label": "Native", "nested": {"count": 42}}}, + context={"event": "render"}, + ), + Fixture( + name="quickjs_native_es_passthrough", + source=r''' + export function get(app: { config?: { nested?: { count?: number } } }) { + class Counter { + static label = "counter" + #step = 1n + value = 1_000 + increment = () => { + this.value += Number(this.#step) + return this.value + } + } + const counter = new Counter() + let configured = app.config?.nested?.count ?? 0 + configured ||= counter.increment() + return Counter.label === "counter" ? configured : 0 + } + ''', + app={"config": {}}, + context={}, + ), + Fixture( + name="enum_runtime_values", + source=r''' + export enum Mode { + First, + Second = First + 2, + Label = "label", + } + export function get() { + return [Mode.First, Mode[0], Mode.Second, Mode.Label] + } + ''', + app={}, + context={}, + ), + Fixture( + name="complex_type_alias_and_predicate", + source=r''' + type Wrapped<T> = T extends string ? { [K in keyof T]: T[K] } : never + interface Input<T> extends Record<string, unknown> { + value?: T + } + function isString(value: unknown): value is string { + return typeof value === "string" + } + export function get(app: { config: Input<string> }) { + return isString(app.config.value) ? app.config.value : "missing" + } + ''', + app={"config": {"value": "ok"}}, + context={}, + ), + Fixture( + name="constructor_parameter_property", + source=r''' + class Box { + constructor(public value: string) {} + } + export function get() { + return new Box("ok").value + } + ''', + app={}, + context={}, + ), + Fixture( + name="function_overload_declarations", + source=r''' + function pick(value: string): string + function pick(value: number): number + function pick(value: string | number): string | number { + return value + } + export function get() { + return [pick("ok"), pick(3)] + } + ''', + app={}, + context={}, + ), + Fixture( + name="const_enum_runtime_values", + source=r''' + const enum Mode { + A, + B = A + 2, + } + export function get() { + return [Mode.A, Mode.B] + } + ''', + app={}, + context={}, + ), + Fixture( + name="type_only_export_elision", + source=r''' + type Foo = string + export type { Foo } + export function get() { + return "ok" + } + ''', + app={}, + context={}, + ), + Fixture( + name="declare_const_elision", + source=r''' + declare const injected: any + export function get() { + return typeof injected + } + ''', + app={}, + context={}, + ), + Fixture( + name="abstract_class_members", + source=r''' + abstract class Base { + abstract value(): string + concrete(): string { + return "ok" + } + } + class Child extends Base { + value(): string { + return "child" + } + } + export function get() { + return new Child().concrete() + } + ''', + app={}, + context={}, + ), +] + +TOKEN_FIXTURES = [ + pytest.param("5/3/1", id="division_sequence"), + pytest.param("5 + /3/", id="regex_after_operator"), + pytest.param("x<Hello>2", id="relational_not_jsx"), + pytest.param("x + < Hello / >", id="jsx_after_operator"), + pytest.param( + ''' + <div className="foo"> + Hello, world! + <span className={bar} /> + </div> + ''', + id="nested_jsx", + ), + pytest.param("`Hello, ${name} ${surname}`", id="template_expressions"), + pytest.param( + ''' + a = b + ++c + d++ + e = f++ + g = ++h + ''', + id="pre_post_increment", + ), + pytest.param( + ''' + import foo from "./foo.json" with {type: "json"}; + export {val} from './foo.js' with {type: "javascript"}; + ''', + id="import_attributes", + ), + pytest.param( + ''' + class { + #x = 3 + } + this.#x = 3 + delete this?.#x + if (#x in obj) { } + ''', + id="private_properties", + ), +] + +ANNOTATION_FIXTURES = [ + pytest.param( + ''' + function outer(a: number) { + const x: string = "x"; + var y = 1; + class Inner {} + } + ''', + id="declaration_roles_and_types", + ), + pytest.param( + ''' + import DefaultThing, { value as renamed, other } from "pkg"; + export { renamed as publicName }; + export const answer = 42; + ''', + id="import_export_roles", + ), + pytest.param( + ''' + const empty = <div />; + const one = <div>{child}</div>; + const many = <div><span />{child}</div>; + const keyed = <div {...props} key={1} />; + ''', + id="jsx_roles", + ), +] + + +def require_tool(name: str) -> str: + path = shutil.which(name) + if not path: + pytest.skip(f"{name} is required for native/Sucrase parity tests") + return path + + +@pytest.fixture(scope="session") +def native_cli(tmp_path_factory: pytest.TempPathFactory) -> Path: + require_tool("nim") + out = tmp_path_factory.mktemp("native-js-transpile") / "native_js_transpile" + proc = subprocess.run( + [ + "nim", + "c", + "--hints:off", + "--verbosity:0", + f"--nimcache:{out.parent / 'nimcache'}", + f"--out:{out}", + str(NATIVE_CLI), + ], + cwd=FRAMEOS_ROOT, + text=True, + capture_output=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + proc.stdout + return out + + +def transform_native(native_cli: Path, source: str) -> str: + with tempfile.NamedTemporaryFile("w", suffix=".tsx", encoding="utf-8", delete=False) as tmp: + tmp.write(source) + tmp_path = tmp.name + try: + proc = subprocess.run( + [str(native_cli), "module", tmp_path], + cwd=FRAMEOS_ROOT, + text=True, + capture_output=True, + check=False, + ) + finally: + Path(tmp_path).unlink(missing_ok=True) + assert proc.returncode == 0, proc.stderr + proc.stdout + return proc.stdout + + +def tokenize_native(native_cli: Path, source: str) -> list[str]: + with tempfile.NamedTemporaryFile("w", suffix=".tsx", encoding="utf-8", delete=False) as tmp: + tmp.write(source) + tmp_path = tmp.name + try: + proc = subprocess.run( + [str(native_cli), "tokens", tmp_path], + cwd=FRAMEOS_ROOT, + text=True, + capture_output=True, + check=False, + ) + finally: + Path(tmp_path).unlink(missing_ok=True) + assert proc.returncode == 0, proc.stderr + proc.stdout + labels = [] + for line in proc.stdout.splitlines(): + if not line: + continue + match = re.match(r"^(.*?)\(\d+,\d+\)", line) + assert match, line + labels.append(match.group(1)) + return labels + + +def annotations_from_native(native_cli: Path, source: str) -> list[dict]: + with tempfile.NamedTemporaryFile("w", suffix=".tsx", encoding="utf-8", delete=False) as tmp: + tmp.write(source) + tmp_path = tmp.name + try: + proc = subprocess.run( + [str(native_cli), "parse", tmp_path], + cwd=FRAMEOS_ROOT, + text=True, + capture_output=True, + check=False, + ) + finally: + Path(tmp_path).unlink(missing_ok=True) + assert proc.returncode == 0, proc.stderr + proc.stdout + result = [] + for line in proc.stdout.splitlines(): + if not line: + continue + match = re.match(r"^(.*?)\((\d+),(\d+)\)(?:.*?\[(.*)\])?", line) + assert match, line + start = int(match.group(2)) + end = int(match.group(3)) + fields = {} + if match.group(4): + for field in match.group(4).split(","): + if "=" in field: + key, value = field.split("=", 1) + fields[key] = value + else: + fields[field] = True + result.append( + { + "label": match.group(1), + "text": source[start:end], + "type": bool(fields.get("type")), + "role": fields.get("role"), + "jsx": fields.get("jsx"), + } + ) + return result + + +def transform_sucrase(source: str) -> str: + require_tool("node") + script = r''' + import { transform } from "sucrase"; + const chunks = []; + process.stdin.setEncoding("utf8"); + for await (const chunk of process.stdin) chunks.push(chunk); + const source = chunks.join(""); + const result = transform(source, { + transforms: ["typescript", "jsx", "imports"], + jsxRuntime: "classic", + jsxPragma: "__frameosJsx", + jsxFragmentPragma: "__frameosFragment", + production: true, + }); + process.stdout.write(result.code); + ''' + proc = subprocess.run( + ["node", "--input-type=module", "-e", script], + cwd=FRAME_FRONTEND_ROOT, + input=source, + text=True, + capture_output=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + proc.stdout + return proc.stdout + + +def tokenize_sucrase(source: str) -> list[str]: + require_tool("node") + script = r''' + const {parse} = require("sucrase/dist/parser"); + const {formatTokenType} = require("sucrase/dist/parser/tokenizer/types"); + const chunks = []; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => chunks.push(chunk)); + process.stdin.on("end", () => { + const file = parse(chunks.join(""), true, true, false); + process.stdout.write(JSON.stringify(file.tokens.map((token) => formatTokenType(token.type)))); + }); + ''' + proc = subprocess.run( + ["node", "-e", script], + cwd=FRAME_FRONTEND_ROOT, + input=source, + text=True, + capture_output=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + proc.stdout + return json.loads(proc.stdout) + + +def annotations_from_sucrase(source: str) -> list[dict]: + require_tool("node") + script = r''' + const {parse} = require("sucrase/dist/parser"); + const {formatTokenType} = require("sucrase/dist/parser/tokenizer/types"); + const {IdentifierRole, JSXRole} = require("sucrase/dist/parser/tokenizer"); + const chunks = []; + const lowerFirst = (value) => value ? value[0].toLowerCase() + value.slice(1) : null; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => chunks.push(chunk)); + process.stdin.on("end", () => { + const source = chunks.join(""); + const file = parse(source, true, true, false); + process.stdout.write(JSON.stringify(file.tokens.map((token) => ({ + label: formatTokenType(token.type), + text: source.slice(token.start, token.end), + type: Boolean(token.isType), + role: token.identifierRole == null ? null : lowerFirst(IdentifierRole[token.identifierRole]), + jsx: token.jsxRole == null ? null : lowerFirst(JSXRole[token.jsxRole]), + })))); + }); + ''' + proc = subprocess.run( + ["node", "-e", script], + cwd=FRAME_FRONTEND_ROOT, + input=source, + text=True, + capture_output=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + proc.stdout + return json.loads(proc.stdout) + + +def interesting_annotations(tokens: list[dict]) -> list[dict]: + return [ + token + for token in tokens + if token["type"] or token["role"] is not None or token["jsx"] is not None + ] + + +def run_transformed(code: str, app: dict, context: dict): + require_tool("node") + runner = ( + JSX_PRELUDE + + "\n" + + "const exports = {};\n" + + code + + "\n" + + "const value = exports.get(" + + json.dumps(app) + + ", " + + json.dumps(context) + + ");\n" + + "process.stdout.write(JSON.stringify(value));\n" + ) + proc = subprocess.run( + ["node", "--input-type=module", "-e", runner], + cwd=FRAME_FRONTEND_ROOT, + text=True, + capture_output=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + proc.stdout + "\nCode:\n" + code + return json.loads(proc.stdout) + + +@pytest.mark.parametrize("fixture", FIXTURES, ids=lambda fixture: fixture.name) +def test_native_transpiler_matches_sucrase_runtime(native_cli: Path, fixture: Fixture): + sucrase_code = transform_sucrase(fixture.source) + sucrase_output = run_transformed(sucrase_code, fixture.app, fixture.context) + + try: + native_code = transform_native(native_cli, fixture.source) + native_output = run_transformed(native_code, fixture.app, fixture.context) + except AssertionError as error: + if fixture.xfail_native: + pytest.xfail(fixture.xfail_native + f": {error}") + raise + + if fixture.xfail_native and native_output == sucrase_output: + pytest.fail(f"Fixture marked xfail now matches Sucrase: {fixture.xfail_native}") + if fixture.xfail_native: + pytest.xfail(fixture.xfail_native) + assert native_output == sucrase_output + + +@pytest.mark.parametrize("source", TOKEN_FIXTURES) +def test_native_tokenizer_matches_sucrase_tokens(native_cli: Path, source: str): + assert tokenize_native(native_cli, source) == tokenize_sucrase(source) + + +@pytest.mark.parametrize("source", ANNOTATION_FIXTURES) +def test_native_parser_annotations_match_sucrase_subset(native_cli: Path, source: str): + assert interesting_annotations(annotations_from_native(native_cli, source)) == interesting_annotations( + annotations_from_sucrase(source) + )