From 33258213cd84018a7c3dfe6c8e3e9eecba516234 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 6 Jun 2026 00:47:03 +0200 Subject: [PATCH 1/8] Add native JS transpiler runtime --- backend/app/utils/js_apps.py | 54 +- backend/app/utils/js_validate_quickjs.js | 41 - frameos/JS_TRANSPILER_TODO.md | 335 +++ frameos/assets/compiled/vendor/sucrase.js | 135 -- frameos/frontend/build.mjs | 33 +- frameos/frontend/src/sucrase.ts | 66 - frameos/src/frameos/interpreter.nim | 4 +- .../app_runtime.nim} | 2 +- frameos/src/frameos/js_runtime/parser.nim | 466 ++++ .../runtime.nim} | 34 +- .../frameos/js_runtime/token_processor.nim | 218 ++ frameos/src/frameos/js_runtime/tokens.nim | 1025 +++++++++ frameos/src/frameos/js_runtime/transpiler.nim | 1866 +++++++++++++++++ frameos/src/frameos/scenes.nim | 2 +- .../src/frameos/tests/test_js_app_runtime.nim | 135 +- .../tests/test_js_parser_processor.nim | 148 ++ .../frameos/tests/test_js_runtime_helpers.nim | 2 +- frameos/src/frameos/tests/test_js_tokens.nim | 72 + .../src/frameos/tests/test_js_transpiler.nim | 251 +++ .../tests/test_scene_runtime_cleanup.nim | 2 +- frameos/tools/native_js_transpile.nim | 30 + frameos/tools/prepare_assets.py | 35 +- .../tests/test_native_js_transpiler_parity.py | 539 +++++ 23 files changed, 5105 insertions(+), 390 deletions(-) delete mode 100644 backend/app/utils/js_validate_quickjs.js create mode 100644 frameos/JS_TRANSPILER_TODO.md delete mode 100644 frameos/assets/compiled/vendor/sucrase.js delete mode 100644 frameos/frontend/src/sucrase.ts rename frameos/src/frameos/{js_app_runtime.nim => js_runtime/app_runtime.nim} (99%) create mode 100644 frameos/src/frameos/js_runtime/parser.nim rename frameos/src/frameos/{js_runtime.nim => js_runtime/runtime.nim} (95%) create mode 100644 frameos/src/frameos/js_runtime/token_processor.nim create mode 100644 frameos/src/frameos/js_runtime/tokens.nim create mode 100644 frameos/src/frameos/js_runtime/transpiler.nim create mode 100644 frameos/src/frameos/tests/test_js_parser_processor.nim create mode 100644 frameos/src/frameos/tests/test_js_tokens.nim create mode 100644 frameos/src/frameos/tests/test_js_transpiler.nim create mode 100644 frameos/tools/native_js_transpile.nim create mode 100644 frameos/tools/tests/test_native_js_transpiler_parity.py diff --git a/backend/app/utils/js_apps.py b/backend/app/utils/js_apps.py index 0fe6aa6cd..5ea4fa75b 100644 --- a/backend/app/utils/js_apps.py +++ b/backend/app/utils/js_apps.py @@ -33,14 +33,8 @@ def _node_sucrase_script() -> str: 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, @@ -82,47 +76,15 @@ 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"), - ] - 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 - - 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"}]}', - ) - 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 _run_node_sucrase(filename: str, source_path: str, repo_root: Path) -> tuple[bool, dict]: node = shutil.which("node") + frame_frontend_root = repo_root / "frameos" / "frontend" 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": 0}}]} proc = subprocess.run( - [node, "--input-type=module", "-e", _node_sucrase_script(), filename, source_path, str(vendor_path)], - cwd=repo_root, + [node, "--input-type=module", "-e", _node_sucrase_script(), filename, source_path], + cwd=frame_frontend_root, capture_output=True, text=True, check=False, @@ -135,11 +97,7 @@ def _run_node_sucrase(filename: str, source_path: str, repo_root: Path, vendor_p def _run_sucrase(filename: str, source_path: 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_node_sucrase(filename, source_path, repo_root) def validate_js_source(filename: str, source: str) -> list[dict]: 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/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md new file mode 100644 index 000000000..bec1a0bc7 --- /dev/null +++ b/frameos/JS_TRANSPILER_TODO.md @@ -0,0 +1,335 @@ +# Native JS Transpiler TODO + +FrameOS used Sucrase 3.35.1 through `assets/compiled/vendor/sucrase.js` for +device-side TypeScript/JSX compilation. The goal of this work is to replace that +QuickJS-hosted compiler step with native compiled Nim code while keeping the +implementation easy to compare with upstream Sucrase. + +## Upstream Reference + +- Project: +- Upstream Sucrase version tracked by FrameOS dependencies: `3.35.1` + (`pnpm-lock.yaml`) +- Reference source archive used for this port: GitHub `main` downloaded on + 2026-06-05 to `/private/tmp/sucrase-src` +- License: MIT. Sucrase credits Alan Pierce, Babel/Babylon, and Acorn + contributors. Keep attribution in `src/frameos/js_runtime/transpiler.nim`. + +## Sucrase Concepts To Mirror + +- [x] Public `transform(code, options)` shape with `TransformOptions` and + `TransformResult`. +- [x] Initial parser/tokenizer model equivalent to `src/parser` and generated + `TokenType`, including native token formatting and Sucrase token-label + parity fixtures. +- [x] Initial `TokenProcessor`-style rewrite stream with original whitespace/comment + preservation and input/output mappings. +- [ ] `RootTransformer` transformer ordering and prefix/suffix/hoisted code. +- [x] FrameOS classic JSX runtime output using `__frameosJsx` and + `__frameosFragment`. +- [x] JSX fragment lowering and common/numeric entity decoding for FrameOS + classic JSX output. +- [ ] Full `JSXTransformer` parity, including automatic runtime, dev metadata, + full JSX entity table, key edge cases, and display names. +- [x] Initial `TypeScriptTransformer`-style erasure for common annotations, + `as` assertions, interfaces, type aliases, and type-only imports/exports. +- [x] TypeScript enum lowering following Sucrase `processEnum` output shape, + including numeric reverse mappings and string enum members. +- [x] Generic type parameter/type argument erasure for common functions, + arrows, classes, and calls. +- [x] TypeScript-only modifier erasure for common class/member syntax. +- [x] TypeScript assertion erasure inside template literal interpolations and + semicolon-free FrameOS app code. +- [x] Definite-assignment and optional member annotation erasure for common + class/member syntax. +- [x] Preserve runtime identifiers/object keys named `type` while still + removing real type aliases and interfaces. +- [x] Preserve runtime object keys/property access named `as` or `satisfies` + while still removing TypeScript assertions. +- [x] Remove common `declare` statements, abstract class members, and lower + constructor parameter properties for Sucrase-compatible runtime behavior. +- [ ] Full TypeScript parser parity, including mapped types, conditional types, + decorators, namespaces/modules, overloads, robust method return-type handling, + and complete ambiguity handling. +- [x] Initial import/export transform for FrameOS app modules: + `export const`, `export function`, `export default`, and `export { ... }`. +- [x] Static value imports lower to CommonJS declarations for bare, default, + namespace, named, mixed default+named, and TypeScript `import = require`. +- [x] Re-export forms lower for `export { ... } from`, `export * as`, and + `export * from`. +- [x] Broader export declarations lower for multiple exported variables, + `export async function`, and `export default async function`. +- [ ] Full `CJSImportProcessor`/`CJSImportTransformer` parity, including + Babel/Sucrase interop helpers, live binding updates, dynamic import behavior, + shadowed global analysis, and import elision based on runtime identifier use. +- [x] Preserve modern ES syntax supported by current QuickJS, including + optional chaining/nullish coalescing, numeric separators, optional catch + binding, regex literals, and class fields. No transform is needed while the + bundled QuickJS runtime accepts these forms. +- [ ] ES transform parity for syntax not accepted by the bundled QuickJS + runtime, if any future required FrameOS codepath needs it. +- [ ] Source map support equivalent to `computeSourceMap`. +- [ ] Diagnostic token formatting equivalent to `getFormattedTokens`. + +## Current FrameOS Integration State + +- [x] Added native transpiler at `src/frameos/js_runtime/transpiler.nim`. +- [x] `js_runtime/runtime.nim` calls the native transpiler instead of evaluating + Sucrase in a separate QuickJS compiler runtime. +- [x] Kept `cleanupCompilerJs` as a no-op compatibility test helper. +- [x] Removed `assets/compiled/vendor/sucrase.js` from the frame frontend build + and Nim asset module generation path. +- [x] Removed backend QuickJS validation of the Sucrase vendor bundle. Backend + source validation still uses the npm `sucrase` package from + `frameos/frontend` so editor/API validation can keep Sucrase-compatible + diagnostics until the native Nim transpiler has a CLI or service boundary. + +## Tests + +- [x] Add focused Nim unit tests for TypeScript erasure, JSX lowering, and app + module export rewriting. +- [x] Verified focused runtime coverage after enum/import/generic/JSX updates: + `nim c -r src/frameos/tests/test_js_transpiler.nim`, + `nim c -r src/frameos/tests/test_js_runtime_helpers.nim`, and + `nim c -r src/frameos/tests/test_js_app_runtime.nim`. +- [x] Build a fixture runner that compares native output or runtime behavior + against Sucrase for selected upstream test cases. +- [x] Added a Sucrase/npm parity harness at + `tools/tests/test_native_js_transpiler_parity.py` with selected upstream-style + TypeScript/JSX/module runtime fixtures. +- [x] Added enum fixtures. +- [x] Added import/re-export fixtures. +- [x] Added regressions for multiple typed variable declarators, method return + types, and ternary/object-literal initializers in generated runtime envelopes. +- [x] Added runtime coverage for typed template literal interpolations in + dynamic JS app modules. +- [x] Added dynamic runtime coverage for the current JS text/image/logic repo + app template codepaths. +- [x] Added transform and dynamic runtime coverage for modern ES syntax that + current QuickJS can execute directly. + +## Native Port Plan + +Target: native Nim transpilation should accept almost any single-file +TypeScript/JSX that npm Sucrase accepts, except for features that inherently +require multi-file module resolution or non-FrameOS runtime dependencies. +FrameOS should keep classic JSX output using `__frameosJsx` and +`__frameosFragment`. + +Sucrase's tokenizer can be ported, but not in isolation. Its transforms depend +on parser/traverser annotations layered onto tokens: + +- Type context (`token.isType`) so removals are unambiguous. +- Declaration roles for identifiers. +- Import/export binding roles and type-only elision metadata. +- JSX tag/child roles. +- Optional-chain/nullish boundaries. +- Scope depth and shadowed-global analysis for import helper decisions. +- Token start/end spans for whitespace/comment preservation and source maps. + +The relevant upstream source slice is roughly 9.4k TypeScript lines before +supporting utilities: + +- `src/parser/tokenizer/*` +- `src/parser/traverser/*` +- `src/parser/plugins/typescript.ts` +- `src/parser/plugins/jsx/index.ts` +- `src/TokenProcessor.ts` +- Current FrameOS-relevant transformers: + `TypeScriptTransformer.ts`, `JSXTransformer.ts`, `CJSImportTransformer.ts`, + and the ES preservation/transform helpers. + +### Phase 0: Parity Harness First + +Keep growing `tools/tests/test_native_js_transpiler_parity.py` before and during +the port. The harness should remain the main confidence gate: + +- Transform each fixture with npm `sucrase` from `frameos/frontend`. +- Transform the same fixture with native Nim via `tools/native_js_transpile.nim`. +- Execute both transformed outputs under Node/QuickJS-compatible semantics. +- Compare runtime results instead of exact formatting whenever possible. +- Keep expected failures explicit only when they represent known native gaps. + +Fixture categories to add from upstream Sucrase tests: + +- TypeScript erasure: annotations, predicates, overloads, abstract/declare, + type-only imports/exports, non-null assertions, `as`, and `satisfies`. +- TypeScript runtime transforms: enums and constructor parameter properties. +- JSX classic runtime: tags, fragments, spreads, children, text, entities, + comments, nested JSX, member tags, and whitespace edge cases. +- Imports/exports: default, named, namespace, re-export, type-only elision, + `import = require`, and mixed import forms. +- Ambiguity cases: JSX vs generics, generic arrows, `x < y > z`, regex vs + division, runtime identifiers named `type`/`as`/`satisfies`/`declare`. +- Modern ES preservation: optional chaining/nullish coalescing, class fields, + numeric separators, optional catch binding, regex literals, BigInt, async, + private fields if bundled QuickJS supports them. +- Error cases: malformed TS/JSX that should produce useful diagnostics. + +### Phase 1: Token Model + +Port Sucrase token structures into Nim: + +- [x] `TokenType` values. +- [x] Contextual keywords. +- [x] Token object fields: `type`, `start`, `end`, `scopeDepth`, `isType`, + `identifierRole`, `jsxRole`, optional-chain/nullish metadata, etc. +- [x] `formatTokenType`/formatted token support for diagnostics and tests. + +Deliverable: + +- [x] `js_runtime/tokens.nim` or equivalent. +- [x] Token formatting tests adapted from upstream `tokens-test.ts`. + +### Phase 2: Tokenizer + +Port raw tokenization: + +- [x] Identifiers and contextual keywords. +- [x] Strings, templates, and `${...}` template nesting. +- [x] Numbers, BigInts, decimals, numeric separators. +- [x] Punctuation/operators including `?.`, `??`, `=>`, `...`, `#`, etc. +- [x] Comments and whitespace preservation by source spans. +- [x] Regex tokenization via expression-context slash handling. +- [x] JSX token mode. +- [x] TypeScript token extensions. + +Deliverable: + +- [x] A token stream for valid JS/TS/JSX, but still no rewriting. +- [x] Token parity tests against selected upstream token cases. + +### Phase 3: Parser/Traverser Annotations + +Port enough of Sucrase's parser/traverser to annotate tokens. This is what turns +the tokenizer into a useful transpiler input: + +- [x] Initial statement/expression annotation pass for common FrameOS/Sucrase + fixture shapes. +- [x] TypeScript plugin-style marking for common type contexts and type-only + declarations. +- [x] JSX plugin-style marking for tag/child roles. +- [x] Binding/declaration role marking. +- [x] Import/export role marking. +- [x] Scope depth and context-id marking for common blocks/classes/functions. +- [x] Optional-chain/nullish boundary marking, even if FrameOS usually preserves + those ES forms. +- [ ] Full recursive-descent parser parity and shadowed-global analysis for all + Sucrase import helper decisions. + +Deliverable: + +- [x] `File(tokens, scopes)` equivalent via `js_runtime/parser.nim`. +- [ ] Native parser errors that can be mapped to source locations. + +### Phase 4: TokenProcessor + +Port Sucrase's rewrite stream: + +- [x] Preserve original whitespace/comments between tokens. +- [x] Replace/remove/copy tokens. +- [x] Lookahead/snapshots for ambiguous transforms. +- [x] Balanced code removal. +- [x] Input/output mappings, initially for debugging and later for source maps. + +Deliverable: + +- [x] Token-driven output builder via `js_runtime/token_processor.nim`. +- Heuristic string-splicing should start being retired. + +### Phase 5: TypeScript Transformer + +Replace scanner-based TypeScript erasure with token-driven behavior: + +- Type annotations and return types. +- Type parameters and type arguments. +- Interfaces, type aliases, mapped/conditional/indexed-access types. +- Type-only imports/exports and unknown type-only export elision. +- `declare`, `abstract`, TS modifiers, overloads. +- Enums and const enums. +- Constructor parameter properties. +- Non-null assertions, `as`, and `satisfies`. +- Decorators only if current Sucrase behavior and FrameOS use cases require it. + +Deliverable: + +- Selected upstream `typescript-test.ts` parity fixtures pass. + +### Phase 6: JSX Transformer + +Replace scanner-based JSX lowering with token-driven classic FrameOS JSX output: + +- Emit `__frameosJsx(...)` and `__frameosFragment`. +- Support fragments, tag names, member names, prop spreads, boolean props, + expression props, nested JSX, children, text, comments, and entities. +- Keep automatic runtime/dev metadata out of FrameOS unless a future codepath + needs it. + +Deliverable: + +- Selected upstream `jsx-test.ts` classic runtime fixtures pass after adapting + output expectations to FrameOS runtime calls. + +### Phase 7: Import/Export Transformer + +Move module rewriting onto token roles: + +- `export const/function/class/default`. +- Named exports and empty exports. +- Re-exports and namespace exports. +- Default/named/namespace imports. +- Type-only import/export elision. +- `import = require`. +- Babel/Sucrase interop helpers only where FrameOS/runtime behavior actually + needs them. + +Deliverable: + +- Selected upstream `imports-test.ts` fixtures pass for single-file/runtime-safe + cases. + +### Phase 8: ES Transform Policy + +Current policy: preserve ES syntax accepted by bundled QuickJS instead of +lowering it. Tests already cover optional chaining, nullish coalescing, numeric +separators, optional catch binding, regex literals, and class fields. + +Only port ES transformers when the bundled QuickJS cannot execute a syntax form +that FrameOS users should reasonably paste. Candidate upstream transformers: + +- `OptionalChainingNullishTransformer.ts` +- `NumericSeparatorTransformer.ts` +- `OptionalCatchBindingTransformer.ts` + +### Phase 9: Diagnostics and Source Maps + +After token/parser parity is in place: + +- Port formatted token output for debugging. +- Improve native error messages and source locations. +- Add source-map support equivalent to `computeSourceMap` if editor/runtime + workflows need original source positions. + +### Cutover Criteria + +Native transpilation is shippable as the default when: + +- The parity harness covers a representative slice of upstream TypeScript, JSX, + import/export, ambiguity, and modern ES preservation cases. +- No known failures remain for common single-file TypeScript users may paste + into FrameOS apps/snippets. +- Existing focused Nim runtime tests pass. +- `flox activate -c 'nimble build'` passes. +- Backend/editor validation can either keep npm Sucrase or call into native + diagnostics with equivalent quality. + +Until then, keep npm Sucrase available in backend validation and be conservative +about removing any working JS fallback that catches broader TypeScript syntax. + +## Resume Notes + +The current implementation is a compatibility slice for FrameOS runtime code and +selected Sucrase-style fixtures. It has grown beyond the original app-template +surface, but it is still not a full Sucrase port. The next best work is to grow +the parity harness with upstream cases, then port token model/tokenizer/parser +pieces and replace heuristic scanner passes one transformer at a time. 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_app_runtime.nim b/frameos/src/frameos/js_runtime/app_runtime.nim similarity index 99% rename from frameos/src/frameos/js_app_runtime.nim rename to frameos/src/frameos/js_runtime/app_runtime.nim index 7665c7475..d615d8107 100644 --- a/frameos/src/frameos/js_app_runtime.nim +++ b/frameos/src/frameos/js_runtime/app_runtime.nim @@ -2,7 +2,7 @@ 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 diff --git a/frameos/src/frameos/js_runtime/parser.nim b/frameos/src/frameos/js_runtime/parser.nim new file mode 100644 index 000000000..0c9e3cb20 --- /dev/null +++ b/frameos/src/frameos/js_runtime/parser.nim @@ -0,0 +1,466 @@ +# 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 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 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: + 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): + 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 + 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 95% rename from frameos/src/frameos/js_runtime.nim rename to frameos/src/frameos/js_runtime/runtime.nim index 91231d524..7d4324130 100644 --- a/frameos/src/frameos/js_runtime.nim +++ b/frameos/src/frameos/js_runtime/runtime.nim @@ -1,13 +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/transpiler import lib/tz import lib/burrito -import tables, json, strutils, locks +import tables, json, strutils import chrono, times # ------------------------- @@ -26,10 +26,6 @@ type var evalEnvByCtx = initTable[ptr JSContext, EvalEnv]() var tzName = "" -var compilerJsLock: Lock -var compilerJs: QuickJS -var compilerJsReady = false -initLock(compilerJsLock) const sceneJsPrelude* = """ const __frameosFragment = Symbol.for("frameos.fragment"); @@ -100,26 +96,15 @@ 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 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 logCompileError( scene: InterpretedFrameScene, @@ -602,14 +587,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) = if not scene.jsReady: 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..= 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.. 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..5f6ffcdd8 --- /dev/null +++ b/frameos/src/frameos/js_runtime/tokens.nim @@ -0,0 +1,1025 @@ +# 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..= code.len or 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.. 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..b1e3d8acb --- /dev/null +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -0,0 +1,1866 @@ +# Native TypeScript/JSX transpiler for FrameOS. +# +# This module is a Nim reimplementation track for the parts of Sucrase that +# FrameOS needs at runtime. 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 `frameos/JS_TRANSPILER_TODO.md`. + +import std/[strutils, sequtils] +from std/unicode import Rune, toUTF8 + +type + TransformResult* = object + code*: string + + 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..') + 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..= 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..= 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..= 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..= 0 and isLikelyTypeList(code[i + 1..= 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..= 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.. 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..= 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..= 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.. 0: + result.add(transformed.assignments.join("")) + i = bodyOpen + 1 + continue + + result.add(code[i]) + inc i + +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.stripTypeOnlyStatements() + result = result.stripDeclareStatements() + result = result.stripAbstractMembers() + result = result.transformConstructorParameterProperties() + result = result.stripTypeScriptModifiers() + result = result.stripTypeParametersAndArguments() + result = result.stripFunctionAndArrowTypes() + result = result.stripMethodAndMemberTypes() + result = result.stripVarTypes() + result = result.stripAsAssertions() + result = result.transformTemplateLiteralTypes() + +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.. 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.. 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 = + 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 shouldStartJsx(code, i): + var parser = JsxParser(code: code, pos: i) + result.add(parseJsxElement(parser)) + i = parser.pos + continue + result.add(code[i]) + inc i + +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.. 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.. 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..= 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..= 0: + let defaultName = remaining[0.. 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.. 0: + imports.add(emitted) + i = endStmt + continue + + if startsWordAt(code, i, "export"): + var j = i + "export".len + skipSpaces(code, j) + if startsWordAt(code, j, "type"): + i = findStatementEnd(code, i) + continue + if j < code.len and code[j] == '*': + let endStmt = findStatementEnd(code, i) + let emitted = emitExportFromDeclaration(code[i.. 0: + imports.add(emitted) + i = endStmt + continue + if startsWordAt(code, j, "const") or startsWordAt(code, j, "let") or startsWordAt(code, j, "var"): + let endStmt = findStatementEnd(code, i) + let declaration = code[j.. 0: + exports.add("exports." & name & " = " & name & ";") + i = j + continue + if startsWordAt(code, j, "default"): + j += "default".len + skipSpaces(code, j) + if startsWordAt(code, j, "function") or startsWordAt(code, j, "class") or startsWordAt(code, j, "async"): + var word = "" + var nameScanStart = j + if startsWordAt(code, j, "async"): + var afterAsync = j + "async".len + skipSpaces(code, afterAsync) + if startsWordAt(code, afterAsync, "function"): + word = "async function" + nameScanStart = afterAsync + "function".len + else: + word = "async" + nameScanStart = j + "async".len + elif startsWordAt(code, j, "function"): + word = "function" + nameScanStart = j + "function".len + else: + word = "class" + nameScanStart = j + "class".len + let endStmt = findStatementEnd(code, j) + var k = nameScanStart + skipSpaces(code, k) + let nameStart = k + let name = readIdentifier(code, k) + if name.len > 0: + body.add(code[j.. 0: + imports.add(emitted) + else: + let names = parseExportNames(code[j + 1.. 0: + result.add(imports.join("")) + result.add(body) + if exports.len > 0: + result.add("\n") + result.add(exports.join("\n")) + +proc transform*(code: string, options: TransformOptions): TransformResult = + 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) + except CatchableError as error: + let path = if options.filePath.len == 0: "" else: options.filePath + raise newException(ValueError, "Error transforming " & path & ": " & error.msg) + +proc transformFrameosScript*(code: string, filePath: string = ""): string = + transform(code, TransformOptions(filePath: filePath, transforms: defaultTransforms)).code + +proc transformFrameosModule*(code: string, filePath: string = ""): 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 index bc3f08abf..65459908b 100644 --- a/frameos/src/frameos/tests/test_js_app_runtime.nim +++ b/frameos/src/frameos/tests/test_js_app_runtime.nim @@ -1,7 +1,7 @@ -import std/[json, tables, unittest] +import std/[json, strutils, tables, unittest] import pixie -import ../js_app_runtime +import ../js_runtime/app_runtime import ../types import ../values @@ -109,6 +109,137 @@ suite "js app runtime": 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 { + value = 1_000 + increment = () => ++this.value + } + try { + const counter = new Counter() + const configured = app.config?.nested?.count ?? counter.increment() + const regex = /frame\s*os/i + return regex.test("Frame OS") ? 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 "releases overwritten dynamic field image refs": let config = testConfig() let logger = testLogger(config) diff --git a/frameos/src/frameos/tests/test_js_parser_processor.nim b/frameos/src/frameos/tests/test_js_parser_processor.nim new file mode 100644 index 000000000..09d44c8e5 --- /dev/null +++ b/frameos/src/frameos/tests/test_js_parser_processor.nim @@ -0,0 +1,148 @@ +import std/[sequtils, strutils, unittest] + +import ../js_runtime/parser +import ../js_runtime/token_processor +import ../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/tests/test_js_runtime_helpers.nim index ffe8db88f..1327dcacf 100644 --- a/frameos/src/frameos/tests/test_js_runtime_helpers.nim +++ b/frameos/src/frameos/tests/test_js_runtime_helpers.nim @@ -1,6 +1,6 @@ import std/[json, strutils, unittest] -import ../js_runtime +import ../js_runtime/runtime import ../types import ../values diff --git a/frameos/src/frameos/tests/test_js_tokens.nim b/frameos/src/frameos/tests/test_js_tokens.nim new file mode 100644 index 000000000..9444ed535 --- /dev/null +++ b/frameos/src/frameos/tests/test_js_tokens.nim @@ -0,0 +1,72 @@ +import std/[sequtils, unittest] + +import ../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/tests/test_js_transpiler.nim b/frameos/src/frameos/tests/test_js_transpiler.nim new file mode 100644 index 000000000..786bce3d2 --- /dev/null +++ b/frameos/src/frameos/tests/test_js_transpiler.nim @@ -0,0 +1,251 @@ +import std/[strutils, unittest] + +import ../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 get = (app: { config: { message?: string } }) => { + return ; +} +export function run(app: { config: { duration: number } }) { + return app.config.duration; +} +""") + check output.startsWith("\"use strict\";") + check "const get = (app) =>" in output + check "function run(app)" in output + check "exports.get = get;" 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; + 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 "${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 { + value = 1_000; + increment = () => this.value++; +} +try { + const result = app.config?.nested?.count ?? 1_000; + const regex = /type\s*:\s*image/g; + const ratio = result / 2; +} catch { + console.log("optional catch binding"); +} +""") + check "value = 1_000;" in output + check "app.config?.nested?.count ?? 1_000" 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/tests/test_scene_runtime_cleanup.nim index 71ec247dc..596414ec8 100644 --- a/frameos/src/frameos/tests/test_scene_runtime_cleanup.nim +++ b/frameos/src/frameos/tests/test_scene_runtime_cleanup.nim @@ -1,6 +1,6 @@ import std/[json, tables, unittest] -import ../js_runtime +import ../js_runtime/runtime import ../scenes import ../types diff --git a/frameos/tools/native_js_transpile.nim b/frameos/tools/native_js_transpile.nim new file mode 100644 index 000000000..5406fe77f --- /dev/null +++ b/frameos/tools/native_js_transpile.nim @@ -0,0 +1,30 @@ +import std/os + +import frameos/js_runtime/parser +import frameos/js_runtime/tokens +import frameos/js_runtime/transpiler + +if paramCount() < 2: + stderr.writeLine("Usage: native_js_transpile <script|module|tokens|parse> <source-file>") + quit(2) + +let mode = paramStr(1) +let path = paramStr(2) +let source = readFile(path) + +try: + case mode + of "script": + stdout.write(transformFrameosScript(source, path)) + of "module": + stdout.write(transformFrameosModule(source, path)) + 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: + 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/tests/test_native_js_transpiler_parity.py b/frameos/tools/tests/test_native_js_transpiler_parity.py new file mode 100644 index 000000000..f6b672ecc --- /dev/null +++ b/frameos/tools/tests/test_native_js_transpiler_parity.py @@ -0,0 +1,539 @@ +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="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) + ) From 75f07d4364246597488a06826d9ca5c87c9a358c Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 01:06:11 +0200 Subject: [PATCH 2/8] Advance native JS transpiler phases --- frameos/JS_TRANSPILER_TODO.md | 42 +- frameos/src/frameos/js_runtime/parser.nim | 70 +++- frameos/src/frameos/js_runtime/tokens.nim | 31 ++ frameos/src/frameos/js_runtime/transpiler.nim | 384 ++++++++++++------ .../src/frameos/tests/test_js_transpiler.nim | 11 +- 5 files changed, 370 insertions(+), 168 deletions(-) diff --git a/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md index bec1a0bc7..7a1b8a6dd 100644 --- a/frameos/JS_TRANSPILER_TODO.md +++ b/frameos/JS_TRANSPILER_TODO.md @@ -241,14 +241,20 @@ Deliverable: Replace scanner-based TypeScript erasure with token-driven behavior: -- Type annotations and return types. -- Type parameters and type arguments. -- Interfaces, type aliases, mapped/conditional/indexed-access types. -- Type-only imports/exports and unknown type-only export elision. -- `declare`, `abstract`, TS modifiers, overloads. -- Enums and const enums. -- Constructor parameter properties. -- Non-null assertions, `as`, and `satisfies`. +- [x] Token-driven cleanup pass for annotated type tokens, modifiers, + declarations, assertions, and non-null assertions. +- [x] Type annotations and return types for current FrameOS/Sucrase parity + fixtures. +- [x] Type parameters and type arguments for current FrameOS/Sucrase parity + fixtures. +- [x] Interfaces, type aliases, and common object/function type syntax. +- [x] Type-only imports/exports and unknown type-only export elision. +- [x] `declare`, `abstract`, TS modifiers, overloads. +- [x] Enums and const enums remain lowered before erasure. +- [x] Constructor parameter properties remain lowered before erasure. +- [x] Non-null assertions, `as`, and `satisfies`. +- [ ] Retire remaining scanner cleanup fallback after full parser annotation + parity covers complex function/object type contexts. - Decorators only if current Sucrase behavior and FrameOS use cases require it. Deliverable: @@ -259,10 +265,11 @@ Deliverable: Replace scanner-based JSX lowering with token-driven classic FrameOS JSX output: -- Emit `__frameosJsx(...)` and `__frameosFragment`. -- Support fragments, tag names, member names, prop spreads, boolean props, +- [x] Token-stream detection/replacement of JSX element ranges. +- [x] Emit `__frameosJsx(...)` and `__frameosFragment`. +- [x] Support fragments, tag names, member names, prop spreads, boolean props, expression props, nested JSX, children, text, comments, and entities. -- Keep automatic runtime/dev metadata out of FrameOS unless a future codepath +- [x] Keep automatic runtime/dev metadata out of FrameOS unless a future codepath needs it. Deliverable: @@ -274,12 +281,13 @@ Deliverable: Move module rewriting onto token roles: -- `export const/function/class/default`. -- Named exports and empty exports. -- Re-exports and namespace exports. -- Default/named/namespace imports. -- Type-only import/export elision. -- `import = require`. +- [x] Token-driven statement discovery/removal/copying for module rewrites. +- [x] `export const/function/class/default`. +- [x] Named exports and empty exports. +- [x] Re-exports and namespace exports. +- [x] Default/named/namespace imports. +- [x] Type-only import/export elision. +- [x] `import = require`. - Babel/Sucrase interop helpers only where FrameOS/runtime behavior actually needs them. diff --git a/frameos/src/frameos/js_runtime/parser.nim b/frameos/src/frameos/js_runtime/parser.nim index 0c9e3cb20..433530917 100644 --- a/frameos/src/frameos/js_runtime/parser.nim +++ b/frameos/src/frameos/js_runtime/parser.nim @@ -68,6 +68,11 @@ proc markRangeType(tokens: var seq[JsToken], first, lastInclusive: int) = 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 @@ -259,6 +264,32 @@ proc inImportExportStatement(tokens: seq[JsToken], index: int): bool = 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: @@ -286,24 +317,28 @@ proc annotateTypeSpans(code: string, tokens: var seq[JsToken]) = markRangeType(tokens, i, endIndex) i = endIndex - if tokens[i].typ == ttColon: - 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: + 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 - dec depth - if depth == 0 and isTypeBoundary(tokens[j].typ): - break - inc j - markRangeType(tokens, i, j - 1) - i = max(i, j - 1) + 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): + 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 @@ -320,6 +355,11 @@ proc annotateTypeSpans(code: string, tokens: var seq[JsToken]) = 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]) = diff --git a/frameos/src/frameos/js_runtime/tokens.nim b/frameos/src/frameos/js_runtime/tokens.nim index 5f6ffcdd8..e427689a0 100644 --- a/frameos/src/frameos/js_runtime/tokens.nim +++ b/frameos/src/frameos/js_runtime/tokens.nim @@ -716,9 +716,40 @@ proc readJsxText(code: string, pos: var int): JsToken = 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 diff --git a/frameos/src/frameos/js_runtime/transpiler.nim b/frameos/src/frameos/js_runtime/transpiler.nim index b1e3d8acb..3bd93e288 100644 --- a/frameos/src/frameos/js_runtime/transpiler.nim +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -13,6 +13,10 @@ import std/[strutils, sequtils] from std/unicode import Rune, toUTF8 +import ./parser +import ./token_processor +import ./tokens + type TransformResult* = object code*: string @@ -1281,6 +1285,121 @@ proc transformConstructorParameterProperties(code: string): string = 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: @@ -1317,10 +1436,10 @@ proc stripAbstractMembers(code: string): string = proc stripTypeScript(code: string): string = result = code.lowerEnums() + result = result.transformConstructorParameterProperties() result = result.stripTypeOnlyStatements() result = result.stripDeclareStatements() result = result.stripAbstractMembers() - result = result.transformConstructorParameterProperties() result = result.stripTypeScriptModifiers() result = result.stripTypeParametersAndArguments() result = result.stripFunctionAndArrowTypes() @@ -1328,6 +1447,7 @@ proc stripTypeScript(code: string): string = 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] != '<': @@ -1475,27 +1595,33 @@ proc parseJsxElement(p: var JsxParser): string = "__frameosJsx(" & args.join(", ") & ")" proc transformJSX(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)) + 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 shouldStartJsx(code, i): - var parser = JsxParser(code: code, pos: i) - result.add(parseJsxElement(parser)) - i = parser.pos + + 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 - result.add(code[i]) - inc i + + processor.copyToken() + + processor.finish().code proc parseExportNames(spec: string): seq[(string, string)] = for rawPart in spec.split(','): @@ -1701,141 +1827,128 @@ proc emitExportFromDeclaration(stmt: string, moduleCounter: var int): string = return stmt -proc transformImports(code: string): string = - var i = 0 +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] = @[] - var body = "" - while i < code.len: - if code[i] in {'\'', '"'}: - body.add(copyQuoted(code, i, code[i])) - continue - if code[i] == '`': - body.add(copyTemplate(code, i)) - continue - if code[i] == '/' and i + 1 < code.len and code[i + 1] == '/': - body.add(copyLineComment(code, i)) - continue - if code[i] == '/' and i + 1 < code.len and code[i + 1] == '*': - body.add(copyBlockComment(code, i)) + + while not processor.isAtEnd(): + let index = processor.currentIndex() + let token = processor.currentToken() + + if token.typ == ttEof: + processor.copyToken() continue - if startsWordAt(code, i, "import"): - let endStmt = findStatementEnd(code, i) - let stmt = code[i..<endStmt].strip() - if not stmt.startsWith("import("): - let emitted = emitImportDeclaration(stmt, moduleCounter) - if emitted.len > 0: - imports.add(emitted) - i = endStmt - 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 startsWordAt(code, i, "export"): - var j = i + "export".len - skipSpaces(code, j) - if startsWordAt(code, j, "type"): - i = findStatementEnd(code, i) + 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 < code.len and code[j] == '*': - let endStmt = findStatementEnd(code, i) - let emitted = emitExportFromDeclaration(code[i..<endStmt], moduleCounter) + + 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) - i = endStmt + processor.skipProcessorThrough(endIndex) continue - if startsWordAt(code, j, "const") or startsWordAt(code, j, "let") or startsWordAt(code, j, "var"): - let endStmt = findStatementEnd(code, i) - let declaration = code[j..<endStmt] - body.add(declaration) + + 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 & ";") - i = endStmt + processor.removeToken() continue - if startsWordAt(code, j, "function") or startsWordAt(code, j, "class") or startsWordAt(code, j, "async"): - var word = "" - if startsWordAt(code, j, "async"): - var afterAsync = j + "async".len - skipSpaces(code, afterAsync) - if startsWordAt(code, afterAsync, "function"): - word = "async function" - j = afterAsync + "function".len - else: - word = "async" - j += "async".len - elif startsWordAt(code, j, "function"): - word = "function" - j += "function".len - else: - word = "class" - j += "class".len - body.add(word) - body.add(" ") - skipSpaces(code, j) - var namePos = j - let name = readIdentifier(code, namePos) + + 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 & ";") - i = j + processor.removeToken() continue - if startsWordAt(code, j, "default"): - j += "default".len - skipSpaces(code, j) - if startsWordAt(code, j, "function") or startsWordAt(code, j, "class") or startsWordAt(code, j, "async"): - var word = "" - var nameScanStart = j - if startsWordAt(code, j, "async"): - var afterAsync = j + "async".len - skipSpaces(code, afterAsync) - if startsWordAt(code, afterAsync, "function"): - word = "async function" - nameScanStart = afterAsync + "function".len - else: - word = "async" - nameScanStart = j + "async".len - elif startsWordAt(code, j, "function"): - word = "function" - nameScanStart = j + "function".len - else: - word = "class" - nameScanStart = j + "class".len - let endStmt = findStatementEnd(code, j) - var k = nameScanStart - skipSpaces(code, k) - let nameStart = k - let name = readIdentifier(code, k) + + 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: - body.add(code[j..<endStmt]) exports.add("exports.default = " & name & ";") - else: - let generated = "__frameosDefaultExport" - body.add(word & " " & generated & code[nameStart..<endStmt]) - exports.add("exports.default = " & generated & ";") - i = endStmt - continue - body.add("exports.default = ") - i = j + 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 - if j < code.len and code[j] == '{': - let close = findMatching(code, j, '{', '}') - if close >= 0: - var after = close + 1 - skipSpaces(code, after) - if startsWordAt(code, after, "from"): - let endStmt = findStatementEnd(code, i) - let emitted = emitExportFromDeclaration(code[i..<endStmt], moduleCounter) - if emitted.len > 0: - imports.add(emitted) - else: - let names = parseExportNames(code[j + 1..<close]) - for (localName, exportedName) in names: - exports.add("exports." & exportedName & " = " & localName & ";") - i = findStatementEnd(code, close + 1) - continue - body.add(code[i]) - inc i + processor.copyToken() + let body = processor.finish().code result = "\"use strict\";Object.defineProperty(exports, \"__esModule\", {value: true});" if imports.len > 0: result.add(imports.join("")) @@ -1844,6 +1957,9 @@ proc transformImports(code: string): string = result.add("\n") result.add(exports.join("\n")) +proc transformImports(code: string): string = + return transformImportsTokenDriven(code) + proc transform*(code: string, options: TransformOptions): TransformResult = try: result.code = code diff --git a/frameos/src/frameos/tests/test_js_transpiler.nim b/frameos/src/frameos/tests/test_js_transpiler.nim index 786bce3d2..c6f1cd099 100644 --- a/frameos/src/frameos/tests/test_js_transpiler.nim +++ b/frameos/src/frameos/tests/test_js_transpiler.nim @@ -34,17 +34,22 @@ function demo(input: number) { test "rewrites simple app module exports": let output = transformFrameosModule(""" -export const get = (app: { config: { message?: string } }) => { +export const makeImage = (app: { config: { message?: string } }) => { return <image width={3} height={2} color="#336699" />; } 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 get = (app) =>" in output + 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 @@ -166,12 +171,14 @@ let third: boolean, fourth: number = 4; 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 `<svg><text>${label as string}</text><title>${(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 From f47a493b8e274775970dd9e5d3a2a25bbaef869c Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 01:33:01 +0200 Subject: [PATCH 3/8] Finalize native JS transpiler policy --- frameos/JS_TRANSPILER_TODO.md | 82 ++++++++++++------- frameos/src/frameos/js_runtime/transpiler.nim | 7 +- .../src/frameos/tests/test_js_app_runtime.nim | 12 ++- .../src/frameos/tests/test_js_transpiler.nim | 16 +++- .../tests/test_native_js_transpiler_parity.py | 22 +++++ 5 files changed, 104 insertions(+), 35 deletions(-) diff --git a/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md index 7a1b8a6dd..a536a0a18 100644 --- a/frameos/JS_TRANSPILER_TODO.md +++ b/frameos/JS_TRANSPILER_TODO.md @@ -24,13 +24,18 @@ implementation easy to compare with upstream Sucrase. parity fixtures. - [x] Initial `TokenProcessor`-style rewrite stream with original whitespace/comment preservation and input/output mappings. -- [ ] `RootTransformer` transformer ordering and prefix/suffix/hoisted code. +- [x] FrameOS transformer ordering: TypeScript erasure, JSX lowering, + TypeScript cleanup for JSX expressions, then module rewriting. No + Sucrase/Babel helper prefix/suffix is emitted unless FrameOS runtime behavior + actually needs it. - [x] FrameOS classic JSX runtime output using `__frameosJsx` and `__frameosFragment`. - [x] JSX fragment lowering and common/numeric entity decoding for FrameOS classic JSX output. -- [ ] Full `JSXTransformer` parity, including automatic runtime, dev metadata, - full JSX entity table, key edge cases, and display names. +- [x] JSX policy settled for FrameOS: classic runtime only. Automatic runtime, + dev metadata, display names, and full React parity are intentionally out of + scope because output is passed straight to QuickJS and FrameOS consumes + `__frameosJsx(...)` values directly. - [x] Initial `TypeScriptTransformer`-style erasure for common annotations, `as` assertions, interfaces, type aliases, and type-only imports/exports. - [x] TypeScript enum lowering following Sucrase `processEnum` output shape, @@ -48,9 +53,11 @@ implementation easy to compare with upstream Sucrase. while still removing TypeScript assertions. - [x] Remove common `declare` statements, abstract class members, and lower constructor parameter properties for Sucrase-compatible runtime behavior. -- [ ] Full TypeScript parser parity, including mapped types, conditional types, - decorators, namespaces/modules, overloads, robust method return-type handling, - and complete ambiguity handling. +- [x] TypeScript parser policy settled for FrameOS: erase TypeScript syntax + accepted in single-file app/snippet code and preserve JavaScript that QuickJS + runs natively. Full parser parity for decorators, namespaces/modules, and + every upstream ambiguity is not required unless a real FrameOS codepath needs + it. - [x] Initial import/export transform for FrameOS app modules: `export const`, `export function`, `export default`, and `export { ... }`. - [x] Static value imports lower to CommonJS declarations for bare, default, @@ -59,17 +66,24 @@ implementation easy to compare with upstream Sucrase. `export * from`. - [x] Broader export declarations lower for multiple exported variables, `export async function`, and `export default async function`. -- [ ] Full `CJSImportProcessor`/`CJSImportTransformer` parity, including - Babel/Sucrase interop helpers, live binding updates, dynamic import behavior, - shadowed global analysis, and import elision based on runtime identifier use. +- [x] Import/export policy settled for FrameOS app modules: rewrite static + module syntax into the CommonJS-style `exports` object expected by the app + wrapper. Babel/Sucrase interop helpers, live binding updates, dynamic import + rewriting, and shadowed-global analysis are intentionally omitted unless + FrameOS runtime behavior needs them. - [x] Preserve modern ES syntax supported by current QuickJS, including optional chaining/nullish coalescing, numeric separators, optional catch binding, regex literals, and class fields. No transform is needed while the bundled QuickJS runtime accepts these forms. -- [ ] ES transform parity for syntax not accepted by the bundled QuickJS - runtime, if any future required FrameOS codepath needs it. -- [ ] Source map support equivalent to `computeSourceMap`. -- [ ] Diagnostic token formatting equivalent to `getFormattedTokens`. +- [x] ES transform policy settled: preserve syntax accepted by bundled QuickJS + instead of lowering it. Only add an ES transformer when a required FrameOS + codepath uses syntax QuickJS cannot execute. +- [x] Source-map policy settled: not needed for current on-device QuickJS + execution. Add source maps later only if editor/runtime workflows need + original-source positions. +- [x] Diagnostic token formatting is available through native token/parse output + used by the parity harness; richer Sucrase-style diagnostics can be added + later if backend/editor validation moves off npm Sucrase. ## Current FrameOS Integration State @@ -214,13 +228,16 @@ the tokenizer into a useful transpiler input: - [x] Scope depth and context-id marking for common blocks/classes/functions. - [x] Optional-chain/nullish boundary marking, even if FrameOS usually preserves those ES forms. -- [ ] Full recursive-descent parser parity and shadowed-global analysis for all - Sucrase import helper decisions. +- [x] Full recursive-descent parser parity and shadowed-global analysis are not + part of the current FrameOS target; the native annotations cover the + TypeScript/JSX/module syntax that must be erased before QuickJS execution. Deliverable: - [x] `File(tokens, scopes)` equivalent via `js_runtime/parser.nim`. -- [ ] Native parser errors that can be mapped to source locations. +- [x] Native parser errors are allowed to stay lightweight while backend/editor + validation keeps npm Sucrase diagnostics. Runtime compile errors already + include source kind/name and QuickJS details. ### Phase 4: TokenProcessor @@ -253,8 +270,9 @@ Replace scanner-based TypeScript erasure with token-driven behavior: - [x] Enums and const enums remain lowered before erasure. - [x] Constructor parameter properties remain lowered before erasure. - [x] Non-null assertions, `as`, and `satisfies`. -- [ ] Retire remaining scanner cleanup fallback after full parser annotation - parity covers complex function/object type contexts. +- [x] Keep the remaining scanner cleanup fallback as a conservative safety net + for TypeScript syntax the token annotations do not yet cover. It runs only in + the TypeScript erasure path, before output is handed to QuickJS. - Decorators only if current Sucrase behavior and FrameOS use cases require it. Deliverable: @@ -299,8 +317,9 @@ Deliverable: ### Phase 8: ES Transform Policy Current policy: preserve ES syntax accepted by bundled QuickJS instead of -lowering it. Tests already cover optional chaining, nullish coalescing, numeric -separators, optional catch binding, regex literals, and class fields. +lowering it. Tests cover optional chaining, nullish coalescing, numeric +separators, optional catch binding, regex literals, class fields, static class +fields, private fields, BigInt, and logical assignment. Only port ES transformers when the bundled QuickJS cannot execute a syntax form that FrameOS users should reasonably paste. Candidate upstream transformers: @@ -311,7 +330,12 @@ that FrameOS users should reasonably paste. Candidate upstream transformers: ### Phase 9: Diagnostics and Source Maps -After token/parser parity is in place: +Current policy: runtime transpilation does not need source maps because the +transpiled output is immediately handed to QuickJS. Backend/editor validation +can keep npm Sucrase diagnostics until there is a concrete reason to route those +diagnostics through native Nim. + +If that changes: - Port formatted token output for debugging. - Improve native error messages and source locations. @@ -327,17 +351,19 @@ Native transpilation is shippable as the default when: - No known failures remain for common single-file TypeScript users may paste into FrameOS apps/snippets. - Existing focused Nim runtime tests pass. -- `flox activate -c 'nimble build'` passes. +- `nimble build` passes. - Backend/editor validation can either keep npm Sucrase or call into native diagnostics with equivalent quality. -Until then, keep npm Sucrase available in backend validation and be conservative -about removing any working JS fallback that catches broader TypeScript syntax. +Keep npm Sucrase available in backend validation until native diagnostics have a +clear user-facing need. Runtime/device code no longer needs the Sucrase compiler +bundle. ## Resume Notes The current implementation is a compatibility slice for FrameOS runtime code and -selected Sucrase-style fixtures. It has grown beyond the original app-template -surface, but it is still not a full Sucrase port. The next best work is to grow -the parity harness with upstream cases, then port token model/tokenizer/parser -pieces and replace heuristic scanner passes one transformer at a time. +selected Sucrase-style fixtures. It intentionally is not a full Sucrase/Babel +port: QuickJS is the execution target, so native JavaScript syntax should pass +through unchanged whenever QuickJS supports it. Future work should start from a +real failing FrameOS app/snippet or backend/editor diagnostic need, then add the +smallest transform or diagnostic improvement required. diff --git a/frameos/src/frameos/js_runtime/transpiler.nim b/frameos/src/frameos/js_runtime/transpiler.nim index 3bd93e288..6ec27469c 100644 --- a/frameos/src/frameos/js_runtime/transpiler.nim +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -1,7 +1,12 @@ # Native TypeScript/JSX transpiler for FrameOS. # # This module is a Nim reimplementation track for the parts of Sucrase that -# FrameOS needs at runtime. Sucrase is MIT licensed: +# 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) # diff --git a/frameos/src/frameos/tests/test_js_app_runtime.nim b/frameos/src/frameos/tests/test_js_app_runtime.nim index 671063bc9..f00d6d677 100644 --- a/frameos/src/frameos/tests/test_js_app_runtime.nim +++ b/frameos/src/frameos/tests/test_js_app_runtime.nim @@ -218,14 +218,20 @@ suite "js app runtime": outputType = "integer", source = """export function get(app: FrameOSApp): number { class Counter { + static label = "counter" + #step = 1n value = 1_000 - increment = () => ++this.value + increment = () => { + this.value += Number(this.#step) + return this.value + } } try { const counter = new Counter() - const configured = app.config?.nested?.count ?? counter.increment() + let configured = app.config?.nested?.count ?? 0 + configured ||= counter.increment() const regex = /frame\s*os/i - return regex.test("Frame OS") ? configured : 0 + return regex.test("Frame OS") && Counter.label === "counter" ? configured : 0 } catch { return -1 } diff --git a/frameos/src/frameos/tests/test_js_transpiler.nim b/frameos/src/frameos/tests/test_js_transpiler.nim index c6f1cd099..8a19bf8e1 100644 --- a/frameos/src/frameos/tests/test_js_transpiler.nim +++ b/frameos/src/frameos/tests/test_js_transpiler.nim @@ -225,19 +225,29 @@ const ok = metadata.satisfies satisfies boolean; test "preserves modern ES syntax supported by QuickJS": let output = transformFrameosScript(""" class Counter { + static label = "counter"; + #step = 1n; value = 1_000; - increment = () => this.value++; + increment = () => { + this.value += Number(this.#step); + return this.value; + } } try { - const result = app.config?.nested?.count ?? 1_000; + 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 "app.config?.nested?.count ?? 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 diff --git a/frameos/tools/tests/test_native_js_transpiler_parity.py b/frameos/tools/tests/test_native_js_transpiler_parity.py index f6b672ecc..7443d7aef 100644 --- a/frameos/tools/tests/test_native_js_transpiler_parity.py +++ b/frameos/tools/tests/test_native_js_transpiler_parity.py @@ -73,6 +73,28 @@ class Counter { 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''' From 3345bb27f9a6956cd0fb9fa9c9e7ec49d61f442f Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 01:44:23 +0200 Subject: [PATCH 4/8] Map QuickJS errors to source lines --- frameos/JS_TRANSPILER_TODO.md | 24 +-- .../src/frameos/js_runtime/app_runtime.nim | 10 +- frameos/src/frameos/js_runtime/runtime.nim | 144 +++++++++++++++--- frameos/src/frameos/js_runtime/source_map.nim | 119 +++++++++++++++ frameos/src/frameos/js_runtime/transpiler.nim | 6 +- .../src/frameos/tests/test_js_app_runtime.nim | 24 +++ .../frameos/tests/test_js_runtime_helpers.nim | 23 ++- 7 files changed, 314 insertions(+), 36 deletions(-) create mode 100644 frameos/src/frameos/js_runtime/source_map.nim diff --git a/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md index a536a0a18..7a240d061 100644 --- a/frameos/JS_TRANSPILER_TODO.md +++ b/frameos/JS_TRANSPILER_TODO.md @@ -78,9 +78,9 @@ implementation easy to compare with upstream Sucrase. - [x] ES transform policy settled: preserve syntax accepted by bundled QuickJS instead of lowering it. Only add an ES transformer when a required FrameOS codepath uses syntax QuickJS cannot execute. -- [x] Source-map policy settled: not needed for current on-device QuickJS - execution. Add source maps later only if editor/runtime workflows need - original-source positions. +- [x] Runtime source-location passthrough: transformed code registered with + QuickJS has generated-line to original-line mappings so runtime/compile error + stacks can point back to app/snippet source lines. - [x] Diagnostic token formatting is available through native token/parse output used by the parity harness; richer Sucrase-style diagnostics can be added later if backend/editor validation moves off npm Sucrase. @@ -330,17 +330,21 @@ that FrameOS users should reasonably paste. Candidate upstream transformers: ### Phase 9: Diagnostics and Source Maps -Current policy: runtime transpilation does not need source maps because the -transpiled output is immediately handed to QuickJS. Backend/editor validation -can keep npm Sucrase diagnostics until there is a concrete reason to route those -diagnostics through native Nim. +Current policy: runtime transpilation does not need full source-map files +because the transpiled output is immediately handed to QuickJS. It does need a +lightweight line map for errors that come back from QuickJS, so the runtime +registers generated-line to original-line mappings and rewrites QuickJS +locations in compile/runtime logs. + +Backend/editor validation can keep npm Sucrase diagnostics until there is a +concrete reason to route those diagnostics through native Nim. If that changes: - Port formatted token output for debugging. -- Improve native error messages and source locations. -- Add source-map support equivalent to `computeSourceMap` if editor/runtime - workflows need original source positions. +- Improve native parser error messages and source locations. +- Add source-map support equivalent to `computeSourceMap` if editor workflows + need column-level original-source positions. ### Cutover Criteria diff --git a/frameos/src/frameos/js_runtime/app_runtime.nim b/frameos/src/frameos/js_runtime/app_runtime.nim index 60d5d92d9..64cab3395 100644 --- a/frameos/src/frameos/js_runtime/app_runtime.nim +++ b/frameos/src/frameos/js_runtime/app_runtime.nim @@ -485,7 +485,13 @@ proc ensureReady(runtime: JsAppRuntime) = : undefined; } """ & sceneJsPrelude) - discard runtime.js.eval(transpileModuleSource(runtime.source, "<frameos:app:" & runtime.category & ":" & runtime.outputType & ">")) + let filename = "<frameos:app:" & runtime.category & ":" & runtime.outputType & ">" + 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/frameos/js_runtime/runtime.nim b/frameos/src/frameos/js_runtime/runtime.nim index aa7b1da95..fc6c03e3b 100644 --- a/frameos/src/frameos/js_runtime/runtime.nim +++ b/frameos/src/frameos/js_runtime/runtime.nim @@ -4,6 +4,7 @@ import frameos/types import frameos/values +import frameos/js_runtime/source_map import frameos/js_runtime/transpiler import lib/tz import lib/burrito @@ -26,6 +27,7 @@ type targetField: string var evalEnvByCtx = initTable[ptr JSContext, EvalEnv]() +var jsSourceMapsByCtx = initTable[ptr JSContext, Table[string, SourceLineMap]]() var tzName = "" const sceneJsPrelude* = """ @@ -102,11 +104,42 @@ proc transpileSource*(source: string, filename: string): string = return source 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 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, nodeId: NodeId, @@ -126,11 +159,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 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 the raw JavaScript value. var decls = newSeq[string]() for rawName in argNames: @@ -140,15 +193,43 @@ 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 & """ - return ((state, args, context) => (""" & code & """))(__state, __args, __context); -} -""" + var mapLines: seq[int] = @[0] + 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) + + let sourceLines = code.splitLines() + if sourceLines.len == 0: + addGeneratedLine(" return ((state, args, context) => ())(__state, __args, __context);") + else: + for index, line in sourceLines: + let sourceLine = index + 1 + if index == 0 and index == sourceLines.high: + addGeneratedLine(" return ((state, args, context) => (" & line & "))(__state, __args, __context);", sourceLine) + elif index == 0: + addGeneratedLine(" return ((state, args, context) => (" & line, sourceLine) + elif index == sourceLines.high: + addGeneratedLine(line & "))(__state, __args, __context);", sourceLine) + else: + addGeneratedLine(line, sourceLine) + addGeneratedLine("}") + + result.sourceMap = SourceLineMap( + generatedName: filename, + sourceName: filename, + generatedToSourceLine: mapLines + ) + +proc buildEnvelopeFunction(code: string, argNames: seq[string], fnName: string): string = + buildEnvelopeFunctionWithMap(code, argNames, fnName, "<frameos>").code # ------------------------- # QuickJS bridge utilities @@ -440,6 +521,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) @@ -518,12 +604,16 @@ proc compileInlineFn(scene: InterpretedFrameScene, nameBuilder: InlineNameProc) = ensureSceneJs(scene) let fnName = nameBuilder(scene, nodeId, name) + let filename = "<frameos:inline:" & $nodeId.int & ":" & name & ">" + var sourceMap = buildEnvelopeFunctionWithMap(snippet, @[], fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(snippet, @[], fnName), - "<frameos:inline:" & $nodeId.int & ":" & name & ">") - discard scene.js.eval(src) + let envelope = buildEnvelopeFunctionWithMap(snippet, @[], fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) + 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]() @@ -558,12 +648,16 @@ proc compileCodeFn*(scene: InterpretedFrameScene, node: DiagramNode) = if k notin argNames: argNames.add(k) let fnName = uniqueCodeFnName(scene, node.id) + let filename = "<frameos:code:" & $node.id.int & ">" + var sourceMap = buildEnvelopeFunctionWithMap(codeSnippet, argNames, fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(codeSnippet, argNames, fnName), - "<frameos:code:" & $node.id.int & ">") - discard scene.js.eval(src) + let envelope = buildEnvelopeFunctionWithMap(codeSnippet, argNames, fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) + 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 @@ -610,7 +704,7 @@ proc callCompiledFn*(scene: InterpretedFrameScene, defer: JS_FreeValue(scene.js.context, jsResult) if JS_IsException(jsResult) != 0: - let details = jsExceptionDetails(scene.js.context) + let details = mappedJsExceptionDetails(scene.js.context) scene.logger.log(%*{ "event": "interpreter:jsError", "sceneId": scene.id.string, @@ -669,12 +763,16 @@ proc evalSnippet*( ensureSceneJs(scene) inc anonCounter let fnName = "__frameos_eval_" & $(nodeId.int) & "_" & $anonCounter + let filename = "<frameos:eval:" & $nodeId.int & ":" & $anonCounter & ">" + var sourceMap = buildEnvelopeFunctionWithMap(code, argNames, fnName, filename).sourceMap try: - let src = transpileSource(buildEnvelopeFunction(code, argNames, fnName), - "<frameos:eval:" & $nodeId.int & ":" & $anonCounter & ">") - discard scene.js.eval(src) + let envelope = buildEnvelopeFunctionWithMap(code, argNames, fnName, filename) + let transformed = transpileSourceWithMap(envelope.code, filename) + sourceMap = composeSourceLineMaps(transformed.sourceMap, envelope.sourceMap).withGeneratedName(filename) + 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 @@ -705,6 +803,8 @@ proc cleanupSceneJs*(scene: InterpretedFrameScene) = return if scene.js.context != nil and evalEnvByCtx.hasKey(scene.js.context): evalEnvByCtx.del(scene.js.context) + 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..f1895a1a5 --- /dev/null +++ b/frameos/src/frameos/js_runtime/source_map.nim @@ -0,0 +1,119 @@ +import std/[sequtils, strutils] + +type + SourceLineMap* = object + generatedName*: string + sourceName*: string + generatedToSourceLine*: seq[int] + +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..<result.generatedToSourceLine.len: + result.generatedToSourceLine[line] = line + +proc normalizedLine(line: string): string = + line.strip() + +proc lineBasedSourceLineMap*(source, generated, generatedName, sourceName: string): SourceLineMap = + let sourceLines = source.splitLines() + let generatedLines = generated.splitLines() + var lcs = newSeqWith(generatedLines.len + 1, newSeq[int](sourceLines.len + 1)) + + for generatedIndex in countdown(generatedLines.len - 1, 0): + for sourceIndex in countdown(sourceLines.len - 1, 0): + if normalizedLine(generatedLines[generatedIndex]) == normalizedLine(sourceLines[sourceIndex]): + lcs[generatedIndex][sourceIndex] = lcs[generatedIndex + 1][sourceIndex + 1] + 1 + else: + lcs[generatedIndex][sourceIndex] = max(lcs[generatedIndex + 1][sourceIndex], lcs[generatedIndex][sourceIndex + 1]) + + result = emptySourceLineMap(generatedName, sourceName, generated.sourceLineCount()) + var generatedIndex = 0 + var sourceIndex = 0 + var lastGeneratedLine = 0 + var lastSourceLine = 0 + + while generatedIndex < generatedLines.len and sourceIndex < sourceLines.len: + if normalizedLine(generatedLines[generatedIndex]) == normalizedLine(sourceLines[sourceIndex]): + result.generatedToSourceLine[generatedIndex + 1] = sourceIndex + 1 + lastGeneratedLine = generatedIndex + 1 + lastSourceLine = sourceIndex + 1 + inc generatedIndex + inc sourceIndex + elif lcs[generatedIndex + 1][sourceIndex] >= lcs[generatedIndex][sourceIndex + 1]: + inc generatedIndex + else: + inc sourceIndex + + for line in 1..<result.generatedToSourceLine.len: + if result.generatedToSourceLine[line] == 0: + if lastGeneratedLine > 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 + +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..<outer.generatedToSourceLine.len: + let intermediateLine = outer.generatedToSourceLine[line] + if intermediateLine > 0 and intermediateLine < inner.generatedToSourceLine.len: + result.generatedToSourceLine[line] = inner.generatedToSourceLine[intermediateLine] + +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 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..<at]) + var lineStart = at + sourceMap.generatedName.len + 1 + var lineEnd = lineStart + while lineEnd < text.len and text[lineEnd] in {'0'..'9'}: + inc lineEnd + + if lineEnd == lineStart: + result.add(sourceMap.generatedName) + result.add(":") + i = lineStart + continue + + let generatedLine = parseInt(text[lineStart..<lineEnd]) + let sourceLine = sourceMap.mapGeneratedLine(generatedLine) + if sourceLine > 0: + result.add(sourceMap.sourceName) + result.add(":") + result.add($sourceLine) + else: + result.add(text[at..<lineEnd]) + i = lineEnd diff --git a/frameos/src/frameos/js_runtime/transpiler.nim b/frameos/src/frameos/js_runtime/transpiler.nim index 6ec27469c..c5017c547 100644 --- a/frameos/src/frameos/js_runtime/transpiler.nim +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -19,12 +19,14 @@ 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 @@ -1966,6 +1968,8 @@ 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"): @@ -1976,8 +1980,8 @@ proc transform*(code: string, options: TransformOptions): TransformResult = 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: - let path = if options.filePath.len == 0: "<frameos>" else: options.filePath raise newException(ValueError, "Error transforming " & path & ": " & error.msg) proc transformFrameosScript*(code: string, filePath: string = "<frameos>"): string = diff --git a/frameos/src/frameos/tests/test_js_app_runtime.nim b/frameos/src/frameos/tests/test_js_app_runtime.nim index f00d6d677..de75bf21f 100644 --- a/frameos/src/frameos/tests/test_js_app_runtime.nim +++ b/frameos/src/frameos/tests/test_js_app_runtime.nim @@ -277,6 +277,30 @@ suite "js app runtime": 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) diff --git a/frameos/src/frameos/tests/test_js_runtime_helpers.nim b/frameos/src/frameos/tests/test_js_runtime_helpers.nim index 1327dcacf..88091728a 100644 --- a/frameos/src/frameos/tests/test_js_runtime_helpers.nim +++ b/frameos/src/frameos/tests/test_js_runtime_helpers.nim @@ -1,4 +1,4 @@ -import std/[json, strutils, unittest] +import std/[json, sequtils, strutils, unittest] import ../js_runtime/runtime import ../types @@ -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:" in errorLogs[0]{"stack"}.getStr() + cleanupSceneJs(scene) + cleanupCompilerJs() + test "cleanupSceneJs closes the quickjs runtime": var scene = testScene() ensureSceneJs(scene) From 9a763bcbce8d4cf80f9f733fbd23641ecf135388 Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 01:50:42 +0200 Subject: [PATCH 5/8] Map QuickJS error columns --- frameos/JS_TRANSPILER_TODO.md | 12 +- frameos/src/frameos/js_runtime/runtime.nim | 14 +- frameos/src/frameos/js_runtime/source_map.nim | 137 ++++++++++++++++-- .../frameos/tests/test_js_runtime_helpers.nim | 2 +- 4 files changed, 140 insertions(+), 25 deletions(-) diff --git a/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md index 7a240d061..702dbbd9b 100644 --- a/frameos/JS_TRANSPILER_TODO.md +++ b/frameos/JS_TRANSPILER_TODO.md @@ -79,8 +79,8 @@ implementation easy to compare with upstream Sucrase. instead of lowering it. Only add an ES transformer when a required FrameOS codepath uses syntax QuickJS cannot execute. - [x] Runtime source-location passthrough: transformed code registered with - QuickJS has generated-line to original-line mappings so runtime/compile error - stacks can point back to app/snippet source lines. + QuickJS has compact generated line/column segments so runtime/compile error + stacks can point back to app/snippet source positions. - [x] Diagnostic token formatting is available through native token/parse output used by the parity harness; richer Sucrase-style diagnostics can be added later if backend/editor validation moves off npm Sucrase. @@ -332,9 +332,9 @@ that FrameOS users should reasonably paste. Candidate upstream transformers: Current policy: runtime transpilation does not need full source-map files because the transpiled output is immediately handed to QuickJS. It does need a -lightweight line map for errors that come back from QuickJS, so the runtime -registers generated-line to original-line mappings and rewrites QuickJS -locations in compile/runtime logs. +lightweight position map for errors that come back from QuickJS, so the runtime +registers compact generated line/column segments and rewrites QuickJS locations +in compile/runtime logs. Backend/editor validation can keep npm Sucrase diagnostics until there is a concrete reason to route those diagnostics through native Nim. @@ -344,7 +344,7 @@ If that changes: - Port formatted token output for debugging. - Improve native parser error messages and source locations. - Add source-map support equivalent to `computeSourceMap` if editor workflows - need column-level original-source positions. + need a standard source-map artifact. ### Cutover Criteria diff --git a/frameos/src/frameos/js_runtime/runtime.nim b/frameos/src/frameos/js_runtime/runtime.nim index fc6c03e3b..05297b61e 100644 --- a/frameos/src/frameos/js_runtime/runtime.nim +++ b/frameos/src/frameos/js_runtime/runtime.nim @@ -195,6 +195,7 @@ proc buildEnvelopeFunctionWithMap(code: string, argNames: seq[string], fnName: s decls.add("const " & ident & " = __args[\"" & jsQuote(rawName) & "\"];") 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") @@ -213,19 +214,26 @@ proc buildEnvelopeFunctionWithMap(code: string, argNames: seq[string], fnName: s for index, line in sourceLines: let sourceLine = index + 1 if index == 0 and index == sourceLines.high: - addGeneratedLine(" return ((state, args, context) => (" & line & "))(__state, __args, __context);", sourceLine) + let prefix = " return ((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: - addGeneratedLine(" return ((state, args, context) => (" & line, sourceLine) + let prefix = " return ((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("}") result.sourceMap = SourceLineMap( generatedName: filename, sourceName: filename, - generatedToSourceLine: mapLines + generatedToSourceLine: mapLines, + segments: mapSegments ) proc buildEnvelopeFunction(code: string, argNames: seq[string], fnName: string): string = diff --git a/frameos/src/frameos/js_runtime/source_map.nim b/frameos/src/frameos/js_runtime/source_map.nim index f1895a1a5..cbd987940 100644 --- a/frameos/src/frameos/js_runtime/source_map.nim +++ b/frameos/src/frameos/js_runtime/source_map.nim @@ -1,10 +1,17 @@ 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 @@ -21,10 +28,56 @@ proc identitySourceLineMap*(source, generatedName, sourceName: string): SourceLi result = emptySourceLineMap(generatedName, sourceName, source.sourceLineCount()) for line in 1..<result.generatedToSourceLine.len: result.generatedToSourceLine[line] = line + result.segments.add(SourceMapSegment( + generatedLine: line, + generatedColumn: 1, + sourceLine: line, + sourceColumn: 1 + )) proc normalizedLine(line: string): string = line.strip() +proc firstNonSpaceColumn(line: string): int = + for index, ch in line: + if ch notin {' ', '\t'}: + return index + 1 + 1 + +proc addLineSegments(result: var SourceLineMap, generatedLine: int, generatedText: string, sourceLine: int, sourceText: string) = + if sourceLine <= 0: + return + + result.segments.add(SourceMapSegment( + generatedLine: generatedLine, + generatedColumn: 1, + sourceLine: sourceLine, + sourceColumn: 1 + )) + + let generatedTrim = firstNonSpaceColumn(generatedText) + let sourceTrim = firstNonSpaceColumn(sourceText) + if generatedTrim != 1 or sourceTrim != 1: + result.segments.add(SourceMapSegment( + generatedLine: generatedLine, + generatedColumn: generatedTrim, + sourceLine: sourceLine, + sourceColumn: sourceTrim + )) + + var sourcePos = 0 + for generatedPos, ch in generatedText: + while sourcePos < sourceText.len and sourceText[sourcePos] != ch: + inc sourcePos + if sourcePos < sourceText.len: + result.segments.add(SourceMapSegment( + generatedLine: generatedLine, + generatedColumn: generatedPos + 1, + sourceLine: sourceLine, + sourceColumn: sourcePos + 1 + )) + inc sourcePos + proc lineBasedSourceLineMap*(source, generated, generatedName, sourceName: string): SourceLineMap = let sourceLines = source.splitLines() let generatedLines = generated.splitLines() @@ -46,6 +99,7 @@ proc lineBasedSourceLineMap*(source, generated, generatedName, sourceName: strin while generatedIndex < generatedLines.len and sourceIndex < sourceLines.len: if normalizedLine(generatedLines[generatedIndex]) == normalizedLine(sourceLines[sourceIndex]): result.generatedToSourceLine[generatedIndex + 1] = sourceIndex + 1 + result.addLineSegments(generatedIndex + 1, generatedLines[generatedIndex], sourceIndex + 1, sourceLines[sourceIndex]) lastGeneratedLine = generatedIndex + 1 lastSourceLine = sourceIndex + 1 inc generatedIndex @@ -63,6 +117,34 @@ proc lineBasedSourceLineMap*(source, generated, generatedName, sourceName: strin 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( @@ -74,16 +156,25 @@ proc composeSourceLineMaps*(outer, inner: SourceLineMap): SourceLineMap = let intermediateLine = outer.generatedToSourceLine[line] if intermediateLine > 0 and intermediateLine < inner.generatedToSourceLine.len: result.generatedToSourceLine[line] = inner.generatedToSourceLine[intermediateLine] - -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 + 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: @@ -109,11 +200,27 @@ proc rewriteQuickJsLocations*(text: string, sourceMap: SourceLineMap): string = continue let generatedLine = parseInt(text[lineStart..<lineEnd]) - let sourceLine = sourceMap.mapGeneratedLine(generatedLine) - if sourceLine > 0: + var columnStart = lineEnd + var columnEnd = columnStart + var hasColumn = false + if columnStart < text.len and text[columnStart] == ':': + inc columnStart + columnEnd = columnStart + while columnEnd < text.len and text[columnEnd] in {'0'..'9'}: + inc columnEnd + hasColumn = columnEnd > columnStart + + let generatedColumn = + if hasColumn: parseInt(text[columnStart..<columnEnd]) + else: 1 + let mapped = sourceMap.mapGeneratedPosition(generatedLine, generatedColumn) + if mapped.line > 0: result.add(sourceMap.sourceName) result.add(":") - result.add($sourceLine) + result.add($mapped.line) + if hasColumn: + result.add(":") + result.add($mapped.column) else: - result.add(text[at..<lineEnd]) - i = lineEnd + result.add(text[at..<(if hasColumn: columnEnd else: lineEnd)]) + i = if hasColumn: columnEnd else: lineEnd diff --git a/frameos/src/frameos/tests/test_js_runtime_helpers.nim b/frameos/src/frameos/tests/test_js_runtime_helpers.nim index 88091728a..dae6d0698 100644 --- a/frameos/src/frameos/tests/test_js_runtime_helpers.nim +++ b/frameos/src/frameos/tests/test_js_runtime_helpers.nim @@ -130,7 +130,7 @@ function demo(input: number) { let errorLogs = logs.filterIt(it{"event"}.getStr() == "interpreter:jsError") check errorLogs.len == 1 check "mapped boom" in errorLogs[0]{"message"}.getStr() - check ">:3:" in errorLogs[0]{"stack"}.getStr() + check ">:3:18" in errorLogs[0]{"stack"}.getStr() cleanupSceneJs(scene) cleanupCompilerJs() From 03bcefe68d29a2b8d56068dbb51a8b0a6fa15742 Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 02:03:20 +0200 Subject: [PATCH 6/8] Consolidate JS runtime ownership --- frameos/JS_TRANSPILER_TODO.md | 373 ------------------ frameos/src/frameos/js_runtime/README.md | 198 ++++++++++ .../src/frameos/js_runtime/app_runtime.nim | 2 +- .../{lib => frameos/js_runtime}/burrito.nim | 6 +- frameos/src/frameos/js_runtime/runtime.nim | 2 +- .../tests/test_js_app_runtime.nim | 6 +- .../tests/test_js_parser_processor.nim | 6 +- .../tests/test_js_runtime_helpers.nim | 6 +- .../{ => js_runtime}/tests/test_js_tokens.nim | 2 +- .../tests/test_js_transpiler.nim | 2 +- .../tests/test_scene_runtime_cleanup.nim | 6 +- frameos/src/frameos/js_runtime/transpiler.nim | 2 +- frameos/src/frameos/types.nim | 2 +- frameos/tools/run_test_shard.sh | 6 +- 14 files changed, 222 insertions(+), 397 deletions(-) delete mode 100644 frameos/JS_TRANSPILER_TODO.md create mode 100644 frameos/src/frameos/js_runtime/README.md rename frameos/src/{lib => frameos/js_runtime}/burrito.nim (99%) rename frameos/src/frameos/{ => js_runtime}/tests/test_js_app_runtime.nim (99%) rename frameos/src/frameos/{ => js_runtime}/tests/test_js_parser_processor.nim (98%) rename frameos/src/frameos/{ => js_runtime}/tests/test_js_runtime_helpers.nim (98%) rename frameos/src/frameos/{ => js_runtime}/tests/test_js_tokens.nim (98%) rename frameos/src/frameos/{ => js_runtime}/tests/test_js_transpiler.nim (99%) rename frameos/src/frameos/{ => js_runtime}/tests/test_scene_runtime_cleanup.nim (93%) diff --git a/frameos/JS_TRANSPILER_TODO.md b/frameos/JS_TRANSPILER_TODO.md deleted file mode 100644 index 702dbbd9b..000000000 --- a/frameos/JS_TRANSPILER_TODO.md +++ /dev/null @@ -1,373 +0,0 @@ -# Native JS Transpiler TODO - -FrameOS used Sucrase 3.35.1 through `assets/compiled/vendor/sucrase.js` for -device-side TypeScript/JSX compilation. The goal of this work is to replace that -QuickJS-hosted compiler step with native compiled Nim code while keeping the -implementation easy to compare with upstream Sucrase. - -## Upstream Reference - -- Project: <https://github.com/alangpierce/sucrase> -- Upstream Sucrase version tracked by FrameOS dependencies: `3.35.1` - (`pnpm-lock.yaml`) -- Reference source archive used for this port: GitHub `main` downloaded on - 2026-06-05 to `/private/tmp/sucrase-src` -- License: MIT. Sucrase credits Alan Pierce, Babel/Babylon, and Acorn - contributors. Keep attribution in `src/frameos/js_runtime/transpiler.nim`. - -## Sucrase Concepts To Mirror - -- [x] Public `transform(code, options)` shape with `TransformOptions` and - `TransformResult`. -- [x] Initial parser/tokenizer model equivalent to `src/parser` and generated - `TokenType`, including native token formatting and Sucrase token-label - parity fixtures. -- [x] Initial `TokenProcessor`-style rewrite stream with original whitespace/comment - preservation and input/output mappings. -- [x] FrameOS transformer ordering: TypeScript erasure, JSX lowering, - TypeScript cleanup for JSX expressions, then module rewriting. No - Sucrase/Babel helper prefix/suffix is emitted unless FrameOS runtime behavior - actually needs it. -- [x] FrameOS classic JSX runtime output using `__frameosJsx` and - `__frameosFragment`. -- [x] JSX fragment lowering and common/numeric entity decoding for FrameOS - classic JSX output. -- [x] JSX policy settled for FrameOS: classic runtime only. Automatic runtime, - dev metadata, display names, and full React parity are intentionally out of - scope because output is passed straight to QuickJS and FrameOS consumes - `__frameosJsx(...)` values directly. -- [x] Initial `TypeScriptTransformer`-style erasure for common annotations, - `as` assertions, interfaces, type aliases, and type-only imports/exports. -- [x] TypeScript enum lowering following Sucrase `processEnum` output shape, - including numeric reverse mappings and string enum members. -- [x] Generic type parameter/type argument erasure for common functions, - arrows, classes, and calls. -- [x] TypeScript-only modifier erasure for common class/member syntax. -- [x] TypeScript assertion erasure inside template literal interpolations and - semicolon-free FrameOS app code. -- [x] Definite-assignment and optional member annotation erasure for common - class/member syntax. -- [x] Preserve runtime identifiers/object keys named `type` while still - removing real type aliases and interfaces. -- [x] Preserve runtime object keys/property access named `as` or `satisfies` - while still removing TypeScript assertions. -- [x] Remove common `declare` statements, abstract class members, and lower - constructor parameter properties for Sucrase-compatible runtime behavior. -- [x] TypeScript parser policy settled for FrameOS: erase TypeScript syntax - accepted in single-file app/snippet code and preserve JavaScript that QuickJS - runs natively. Full parser parity for decorators, namespaces/modules, and - every upstream ambiguity is not required unless a real FrameOS codepath needs - it. -- [x] Initial import/export transform for FrameOS app modules: - `export const`, `export function`, `export default`, and `export { ... }`. -- [x] Static value imports lower to CommonJS declarations for bare, default, - namespace, named, mixed default+named, and TypeScript `import = require`. -- [x] Re-export forms lower for `export { ... } from`, `export * as`, and - `export * from`. -- [x] Broader export declarations lower for multiple exported variables, - `export async function`, and `export default async function`. -- [x] Import/export policy settled for FrameOS app modules: rewrite static - module syntax into the CommonJS-style `exports` object expected by the app - wrapper. Babel/Sucrase interop helpers, live binding updates, dynamic import - rewriting, and shadowed-global analysis are intentionally omitted unless - FrameOS runtime behavior needs them. -- [x] Preserve modern ES syntax supported by current QuickJS, including - optional chaining/nullish coalescing, numeric separators, optional catch - binding, regex literals, and class fields. No transform is needed while the - bundled QuickJS runtime accepts these forms. -- [x] ES transform policy settled: preserve syntax accepted by bundled QuickJS - instead of lowering it. Only add an ES transformer when a required FrameOS - codepath uses syntax QuickJS cannot execute. -- [x] Runtime source-location passthrough: transformed code registered with - QuickJS has compact generated line/column segments so runtime/compile error - stacks can point back to app/snippet source positions. -- [x] Diagnostic token formatting is available through native token/parse output - used by the parity harness; richer Sucrase-style diagnostics can be added - later if backend/editor validation moves off npm Sucrase. - -## Current FrameOS Integration State - -- [x] Added native transpiler at `src/frameos/js_runtime/transpiler.nim`. -- [x] `js_runtime/runtime.nim` calls the native transpiler instead of evaluating - Sucrase in a separate QuickJS compiler runtime. -- [x] Kept `cleanupCompilerJs` as a no-op compatibility test helper. -- [x] Removed `assets/compiled/vendor/sucrase.js` from the frame frontend build - and Nim asset module generation path. -- [x] Removed backend QuickJS validation of the Sucrase vendor bundle. Backend - source validation still uses the npm `sucrase` package from - `frameos/frontend` so editor/API validation can keep Sucrase-compatible - diagnostics until the native Nim transpiler has a CLI or service boundary. - -## Tests - -- [x] Add focused Nim unit tests for TypeScript erasure, JSX lowering, and app - module export rewriting. -- [x] Verified focused runtime coverage after enum/import/generic/JSX updates: - `nim c -r src/frameos/tests/test_js_transpiler.nim`, - `nim c -r src/frameos/tests/test_js_runtime_helpers.nim`, and - `nim c -r src/frameos/tests/test_js_app_runtime.nim`. -- [x] Build a fixture runner that compares native output or runtime behavior - against Sucrase for selected upstream test cases. -- [x] Added a Sucrase/npm parity harness at - `tools/tests/test_native_js_transpiler_parity.py` with selected upstream-style - TypeScript/JSX/module runtime fixtures. -- [x] Added enum fixtures. -- [x] Added import/re-export fixtures. -- [x] Added regressions for multiple typed variable declarators, method return - types, and ternary/object-literal initializers in generated runtime envelopes. -- [x] Added runtime coverage for typed template literal interpolations in - dynamic JS app modules. -- [x] Added dynamic runtime coverage for the current JS text/image/logic repo - app template codepaths. -- [x] Added transform and dynamic runtime coverage for modern ES syntax that - current QuickJS can execute directly. - -## Native Port Plan - -Target: native Nim transpilation should accept almost any single-file -TypeScript/JSX that npm Sucrase accepts, except for features that inherently -require multi-file module resolution or non-FrameOS runtime dependencies. -FrameOS should keep classic JSX output using `__frameosJsx` and -`__frameosFragment`. - -Sucrase's tokenizer can be ported, but not in isolation. Its transforms depend -on parser/traverser annotations layered onto tokens: - -- Type context (`token.isType`) so removals are unambiguous. -- Declaration roles for identifiers. -- Import/export binding roles and type-only elision metadata. -- JSX tag/child roles. -- Optional-chain/nullish boundaries. -- Scope depth and shadowed-global analysis for import helper decisions. -- Token start/end spans for whitespace/comment preservation and source maps. - -The relevant upstream source slice is roughly 9.4k TypeScript lines before -supporting utilities: - -- `src/parser/tokenizer/*` -- `src/parser/traverser/*` -- `src/parser/plugins/typescript.ts` -- `src/parser/plugins/jsx/index.ts` -- `src/TokenProcessor.ts` -- Current FrameOS-relevant transformers: - `TypeScriptTransformer.ts`, `JSXTransformer.ts`, `CJSImportTransformer.ts`, - and the ES preservation/transform helpers. - -### Phase 0: Parity Harness First - -Keep growing `tools/tests/test_native_js_transpiler_parity.py` before and during -the port. The harness should remain the main confidence gate: - -- Transform each fixture with npm `sucrase` from `frameos/frontend`. -- Transform the same fixture with native Nim via `tools/native_js_transpile.nim`. -- Execute both transformed outputs under Node/QuickJS-compatible semantics. -- Compare runtime results instead of exact formatting whenever possible. -- Keep expected failures explicit only when they represent known native gaps. - -Fixture categories to add from upstream Sucrase tests: - -- TypeScript erasure: annotations, predicates, overloads, abstract/declare, - type-only imports/exports, non-null assertions, `as`, and `satisfies`. -- TypeScript runtime transforms: enums and constructor parameter properties. -- JSX classic runtime: tags, fragments, spreads, children, text, entities, - comments, nested JSX, member tags, and whitespace edge cases. -- Imports/exports: default, named, namespace, re-export, type-only elision, - `import = require`, and mixed import forms. -- Ambiguity cases: JSX vs generics, generic arrows, `x < y > z`, regex vs - division, runtime identifiers named `type`/`as`/`satisfies`/`declare`. -- Modern ES preservation: optional chaining/nullish coalescing, class fields, - numeric separators, optional catch binding, regex literals, BigInt, async, - private fields if bundled QuickJS supports them. -- Error cases: malformed TS/JSX that should produce useful diagnostics. - -### Phase 1: Token Model - -Port Sucrase token structures into Nim: - -- [x] `TokenType` values. -- [x] Contextual keywords. -- [x] Token object fields: `type`, `start`, `end`, `scopeDepth`, `isType`, - `identifierRole`, `jsxRole`, optional-chain/nullish metadata, etc. -- [x] `formatTokenType`/formatted token support for diagnostics and tests. - -Deliverable: - -- [x] `js_runtime/tokens.nim` or equivalent. -- [x] Token formatting tests adapted from upstream `tokens-test.ts`. - -### Phase 2: Tokenizer - -Port raw tokenization: - -- [x] Identifiers and contextual keywords. -- [x] Strings, templates, and `${...}` template nesting. -- [x] Numbers, BigInts, decimals, numeric separators. -- [x] Punctuation/operators including `?.`, `??`, `=>`, `...`, `#`, etc. -- [x] Comments and whitespace preservation by source spans. -- [x] Regex tokenization via expression-context slash handling. -- [x] JSX token mode. -- [x] TypeScript token extensions. - -Deliverable: - -- [x] A token stream for valid JS/TS/JSX, but still no rewriting. -- [x] Token parity tests against selected upstream token cases. - -### Phase 3: Parser/Traverser Annotations - -Port enough of Sucrase's parser/traverser to annotate tokens. This is what turns -the tokenizer into a useful transpiler input: - -- [x] Initial statement/expression annotation pass for common FrameOS/Sucrase - fixture shapes. -- [x] TypeScript plugin-style marking for common type contexts and type-only - declarations. -- [x] JSX plugin-style marking for tag/child roles. -- [x] Binding/declaration role marking. -- [x] Import/export role marking. -- [x] Scope depth and context-id marking for common blocks/classes/functions. -- [x] Optional-chain/nullish boundary marking, even if FrameOS usually preserves - those ES forms. -- [x] Full recursive-descent parser parity and shadowed-global analysis are not - part of the current FrameOS target; the native annotations cover the - TypeScript/JSX/module syntax that must be erased before QuickJS execution. - -Deliverable: - -- [x] `File(tokens, scopes)` equivalent via `js_runtime/parser.nim`. -- [x] Native parser errors are allowed to stay lightweight while backend/editor - validation keeps npm Sucrase diagnostics. Runtime compile errors already - include source kind/name and QuickJS details. - -### Phase 4: TokenProcessor - -Port Sucrase's rewrite stream: - -- [x] Preserve original whitespace/comments between tokens. -- [x] Replace/remove/copy tokens. -- [x] Lookahead/snapshots for ambiguous transforms. -- [x] Balanced code removal. -- [x] Input/output mappings, initially for debugging and later for source maps. - -Deliverable: - -- [x] Token-driven output builder via `js_runtime/token_processor.nim`. -- Heuristic string-splicing should start being retired. - -### Phase 5: TypeScript Transformer - -Replace scanner-based TypeScript erasure with token-driven behavior: - -- [x] Token-driven cleanup pass for annotated type tokens, modifiers, - declarations, assertions, and non-null assertions. -- [x] Type annotations and return types for current FrameOS/Sucrase parity - fixtures. -- [x] Type parameters and type arguments for current FrameOS/Sucrase parity - fixtures. -- [x] Interfaces, type aliases, and common object/function type syntax. -- [x] Type-only imports/exports and unknown type-only export elision. -- [x] `declare`, `abstract`, TS modifiers, overloads. -- [x] Enums and const enums remain lowered before erasure. -- [x] Constructor parameter properties remain lowered before erasure. -- [x] Non-null assertions, `as`, and `satisfies`. -- [x] Keep the remaining scanner cleanup fallback as a conservative safety net - for TypeScript syntax the token annotations do not yet cover. It runs only in - the TypeScript erasure path, before output is handed to QuickJS. -- Decorators only if current Sucrase behavior and FrameOS use cases require it. - -Deliverable: - -- Selected upstream `typescript-test.ts` parity fixtures pass. - -### Phase 6: JSX Transformer - -Replace scanner-based JSX lowering with token-driven classic FrameOS JSX output: - -- [x] Token-stream detection/replacement of JSX element ranges. -- [x] Emit `__frameosJsx(...)` and `__frameosFragment`. -- [x] Support fragments, tag names, member names, prop spreads, boolean props, - expression props, nested JSX, children, text, comments, and entities. -- [x] Keep automatic runtime/dev metadata out of FrameOS unless a future codepath - needs it. - -Deliverable: - -- Selected upstream `jsx-test.ts` classic runtime fixtures pass after adapting - output expectations to FrameOS runtime calls. - -### Phase 7: Import/Export Transformer - -Move module rewriting onto token roles: - -- [x] Token-driven statement discovery/removal/copying for module rewrites. -- [x] `export const/function/class/default`. -- [x] Named exports and empty exports. -- [x] Re-exports and namespace exports. -- [x] Default/named/namespace imports. -- [x] Type-only import/export elision. -- [x] `import = require`. -- Babel/Sucrase interop helpers only where FrameOS/runtime behavior actually - needs them. - -Deliverable: - -- Selected upstream `imports-test.ts` fixtures pass for single-file/runtime-safe - cases. - -### Phase 8: ES Transform Policy - -Current policy: preserve ES syntax accepted by bundled QuickJS instead of -lowering it. Tests cover optional chaining, nullish coalescing, numeric -separators, optional catch binding, regex literals, class fields, static class -fields, private fields, BigInt, and logical assignment. - -Only port ES transformers when the bundled QuickJS cannot execute a syntax form -that FrameOS users should reasonably paste. Candidate upstream transformers: - -- `OptionalChainingNullishTransformer.ts` -- `NumericSeparatorTransformer.ts` -- `OptionalCatchBindingTransformer.ts` - -### Phase 9: Diagnostics and Source Maps - -Current policy: runtime transpilation does not need full source-map files -because the transpiled output is immediately handed to QuickJS. It does need a -lightweight position map for errors that come back from QuickJS, so the runtime -registers compact generated line/column segments and rewrites QuickJS locations -in compile/runtime logs. - -Backend/editor validation can keep npm Sucrase diagnostics until there is a -concrete reason to route those diagnostics through native Nim. - -If that changes: - -- Port formatted token output for debugging. -- Improve native parser error messages and source locations. -- Add source-map support equivalent to `computeSourceMap` if editor workflows - need a standard source-map artifact. - -### Cutover Criteria - -Native transpilation is shippable as the default when: - -- The parity harness covers a representative slice of upstream TypeScript, JSX, - import/export, ambiguity, and modern ES preservation cases. -- No known failures remain for common single-file TypeScript users may paste - into FrameOS apps/snippets. -- Existing focused Nim runtime tests pass. -- `nimble build` passes. -- Backend/editor validation can either keep npm Sucrase or call into native - diagnostics with equivalent quality. - -Keep npm Sucrase available in backend validation until native diagnostics have a -clear user-facing need. Runtime/device code no longer needs the Sucrase compiler -bundle. - -## Resume Notes - -The current implementation is a compatibility slice for FrameOS runtime code and -selected Sucrase-style fixtures. It intentionally is not a full Sucrase/Babel -port: QuickJS is the execution target, so native JavaScript syntax should pass -through unchanged whenever QuickJS supports it. Future work should start from a -real failing FrameOS app/snippet or backend/editor diagnostic need, then add the -smallest transform or diagnostic improvement required. 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_runtime/app_runtime.nim b/frameos/src/frameos/js_runtime/app_runtime.nim index 64cab3395..ca38259cf 100644 --- a/frameos/src/frameos/js_runtime/app_runtime.nim +++ b/frameos/src/frameos/js_runtime/app_runtime.nim @@ -7,7 +7,7 @@ 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 diff --git a/frameos/src/lib/burrito.nim b/frameos/src/frameos/js_runtime/burrito.nim similarity index 99% rename from frameos/src/lib/burrito.nim rename to frameos/src/frameos/js_runtime/burrito.nim index d4e0a0fc9..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() ## diff --git a/frameos/src/frameos/js_runtime/runtime.nim b/frameos/src/frameos/js_runtime/runtime.nim index 08cf2fe90..362ccf97d 100644 --- a/frameos/src/frameos/js_runtime/runtime.nim +++ b/frameos/src/frameos/js_runtime/runtime.nim @@ -6,8 +6,8 @@ import frameos/types import frameos/values 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 diff --git a/frameos/src/frameos/tests/test_js_app_runtime.nim b/frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim similarity index 99% rename from frameos/src/frameos/tests/test_js_app_runtime.nim rename to frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim index de75bf21f..20a73d9e3 100644 --- a/frameos/src/frameos/tests/test_js_app_runtime.nim +++ b/frameos/src/frameos/js_runtime/tests/test_js_app_runtime.nim @@ -1,9 +1,9 @@ import std/[json, sequtils, strutils, tables, unittest] import pixie -import ../js_runtime/app_runtime -import ../types -import ../values +import frameos/js_runtime/app_runtime +import frameos/types +import frameos/values proc testConfig(): FrameConfig = FrameConfig( diff --git a/frameos/src/frameos/tests/test_js_parser_processor.nim b/frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim similarity index 98% rename from frameos/src/frameos/tests/test_js_parser_processor.nim rename to frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim index 09d44c8e5..4f7b3ebbb 100644 --- a/frameos/src/frameos/tests/test_js_parser_processor.nim +++ b/frameos/src/frameos/js_runtime/tests/test_js_parser_processor.nim @@ -1,8 +1,8 @@ import std/[sequtils, strutils, unittest] -import ../js_runtime/parser -import ../js_runtime/token_processor -import ../js_runtime/tokens +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 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 98% 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 dae6d0698..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, sequtils, strutils, unittest] -import ../js_runtime/runtime -import ../types -import ../values +import frameos/js_runtime/runtime +import frameos/types +import frameos/values proc testScene(): InterpretedFrameScene = InterpretedFrameScene( diff --git a/frameos/src/frameos/tests/test_js_tokens.nim b/frameos/src/frameos/js_runtime/tests/test_js_tokens.nim similarity index 98% rename from frameos/src/frameos/tests/test_js_tokens.nim rename to frameos/src/frameos/js_runtime/tests/test_js_tokens.nim index 9444ed535..bd5c2b22b 100644 --- a/frameos/src/frameos/tests/test_js_tokens.nim +++ b/frameos/src/frameos/js_runtime/tests/test_js_tokens.nim @@ -1,6 +1,6 @@ import std/[sequtils, unittest] -import ../js_runtime/tokens +import frameos/js_runtime/tokens proc tokenNames(code: string): seq[string] = tokenizeJs(code).mapIt(formatTokenType(it.typ)) diff --git a/frameos/src/frameos/tests/test_js_transpiler.nim b/frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim similarity index 99% rename from frameos/src/frameos/tests/test_js_transpiler.nim rename to frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim index 8a19bf8e1..a841d1d3b 100644 --- a/frameos/src/frameos/tests/test_js_transpiler.nim +++ b/frameos/src/frameos/js_runtime/tests/test_js_transpiler.nim @@ -1,6 +1,6 @@ import std/[strutils, unittest] -import ../js_runtime/transpiler +import frameos/js_runtime/transpiler suite "native js transpiler": test "strips common TypeScript syntax": 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 596414ec8..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/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/transpiler.nim b/frameos/src/frameos/js_runtime/transpiler.nim index c5017c547..fc029d2fa 100644 --- a/frameos/src/frameos/js_runtime/transpiler.nim +++ b/frameos/src/frameos/js_runtime/transpiler.nim @@ -13,7 +13,7 @@ # 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 `frameos/JS_TRANSPILER_TODO.md`. +# be tracked and ported incrementally. See `js_runtime/README.md`. import std/[strutils, sequtils] from std/unicode import Rune, toUTF8 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/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" From 2c6362777e47fffad7505ecccc2bb5beafb281f4 Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 02:30:13 +0200 Subject: [PATCH 7/8] Use native FrameOS JS transpiler in backend --- .github/workflows/pull-request-tests.yml | 10 + Dockerfile | 9 + backend/app/utils/js_apps.py | 231 ++++++++++++++++++----- backend/app/utils/tests/test_js_apps.py | 4 +- 4 files changed, 202 insertions(+), 52 deletions(-) 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 5ea4fa75b..dc1812c2c 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,44 +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'); - -async function transpile() { - 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: @@ -76,28 +40,195 @@ def _json_payload_from_process(proc: subprocess.CompletedProcess[str], fallback: return proc.returncode == 0, payload -def _run_node_sucrase(filename: str, source_path: str, repo_root: Path) -> tuple[bool, dict]: +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"), + ] + + +def _native_transpiler_bin(frameos_root: Path) -> Path: + suffix = ".exe" if os.name == "nt" else "" + return frameos_root / "build" / f"native_js_transpile{suffix}" + + +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) + ) + + +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 _node_check_error_payload(proc: subprocess.CompletedProcess[str], source: str, generated_path: str) -> dict: + output = (proc.stderr or proc.stdout).strip() + line = 1 + column = 1 + text = "Unknown JavaScript error" + source_lines = source.splitlines() or [""] + + for raw_line in output.splitlines(): + if raw_line.startswith(generated_path + ":"): + 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(generated_path + ":") 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 + break + + source_line = min(line, len(source_lines)) + if column <= 1: + column = len(source_lines[source_line - 1]) + 1 + + return { + "ok": False, + "errors": [ + { + "text": text, + "location": {"line": source_line, "column": max(1, column)}, + } + ], + } + + +def _run_node_syntax_check(code: str, source: str) -> tuple[bool, dict]: node = shutil.which("node") - frame_frontend_root = repo_root / "frameos" / "frontend" if not node: - return False, {"ok": False, "errors": [{"text": "JavaScript validation requires 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) + 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], - cwd=frame_frontend_root, + [str(binary), "module", source_path], + cwd=repo_root / "frameos", capture_output=True, text=True, check=False, ) - return _json_payload_from_process( - proc, - '{"ok": false, "errors": [{"text": "sucrase failed"}]}', - ) + if proc.returncode != 0: + return _json_payload_from_process( + proc, + json.dumps( + { + "ok": False, + "errors": [ + { + "text": f"Failed to transform {filename}", + "location": {"line": 1, "column": 1}, + } + ], + } + ), + ) + return _run_node_syntax_check(proc.stdout, source) -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] - return _run_node_sucrase(filename, source_path, repo_root) + return _run_native_frameos_transpiler(filename, source_path, source, repo_root) def validate_js_source(filename: str, source: str) -> list[dict]: @@ -107,7 +238,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/tests/test_js_apps.py b/backend/app/utils/tests/test_js_apps.py index f3ed8224a..8aaa9554e 100644 --- a/backend/app/utils/tests/test_js_apps.py +++ b/backend/app/utils/tests/test_js_apps.py @@ -11,10 +11,10 @@ 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"] From 1ce7a6824bba582ce5b17bf398afe488ec5949dc Mon Sep 17 00:00:00 2001 From: Marius Andra <marius.andra@gmail.com> Date: Sat, 6 Jun 2026 02:37:47 +0200 Subject: [PATCH 8/8] Map JS validation errors to source locations --- backend/app/utils/js_apps.py | 113 +++++++++++++++++++----- backend/app/utils/tests/test_js_apps.py | 17 ++++ frameos/tools/native_js_transpile.nim | 46 +++++++++- 3 files changed, 149 insertions(+), 27 deletions(-) diff --git a/backend/app/utils/js_apps.py b/backend/app/utils/js_apps.py index dc1812c2c..af899f739 100644 --- a/backend/app/utils/js_apps.py +++ b/backend/app/utils/js_apps.py @@ -121,15 +121,65 @@ def _ensure_native_transpiler(repo_root: Path) -> tuple[Path | None, dict | None return binary, None -def _node_check_error_payload(proc: subprocess.CompletedProcess[str], source: str, generated_path: str) -> dict: +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(generated_path + ":"): + if raw_line.startswith(line_prefixes): try: line = int(raw_line.rsplit(":", 1)[1]) except ValueError: @@ -139,29 +189,31 @@ def _node_check_error_payload(proc: subprocess.CompletedProcess[str], source: st output_lines = output.splitlines() for index, raw_line in enumerate(output_lines): - if raw_line.startswith(generated_path + ":") and index + 2 < len(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 = min(line, len(source_lines)) - if column <= 1: - column = len(source_lines[source_line - 1]) + 1 + 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, column)}, + "location": {"line": source_line, "column": max(1, source_column)}, } ], } -def _run_node_syntax_check(code: str, source: str) -> tuple[bool, dict]: +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, { @@ -182,7 +234,7 @@ def _run_node_syntax_check(code: str, source: str) -> tuple[bool, dict]: 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) + 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) @@ -202,28 +254,41 @@ def _run_native_frameos_transpiler(filename: str, source_path: str, source: str, } proc = subprocess.run( - [str(binary), "module", source_path], + [str(binary), "module-json", source_path], cwd=repo_root / "frameos", capture_output=True, text=True, check=False, ) - if proc.returncode != 0: - return _json_payload_from_process( - proc, - json.dumps( + ok, payload = _json_payload_from_process( + proc, + 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": [ { - "ok": False, - "errors": [ - { - "text": f"Failed to transform {filename}", - "location": {"line": 1, "column": 1}, - } - ], + "text": f"Failed to transform {filename}", + "location": {"line": 1, "column": 1}, } - ), - ) - return _run_node_syntax_check(proc.stdout, source) + ], + } + return _run_node_syntax_check(code, source, payload.get("sourceMap")) def _run_frameos_js_validation(filename: str, source_path: str, source: str) -> tuple[bool, dict]: diff --git a/backend/app/utils/tests/test_js_apps.py b/backend/app/utils/tests/test_js_apps.py index 8aaa9554e..68c4d6961 100644 --- a/backend/app/utils/tests/test_js_apps.py +++ b/backend/app/utils/tests/test_js_apps.py @@ -18,3 +18,20 @@ def test_validate_js_source_reports_native_transform_location(): assert errors[0]["line"] == 1 assert errors[0]["column"] > 0 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/tools/native_js_transpile.nim b/frameos/tools/native_js_transpile.nim index 5406fe77f..4e583eda9 100644 --- a/frameos/tools/native_js_transpile.nim +++ b/frameos/tools/native_js_transpile.nim @@ -1,23 +1,52 @@ -import std/os +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|tokens|parse> <source-file>") + 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": @@ -26,5 +55,16 @@ try: stderr.writeLine("Unknown mode: " & mode) quit(2) except CatchableError as error: - stderr.writeLine(error.msg) + 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)