diff --git a/examples/test262/index.js b/examples/test262/index.js index 1cfd98fb..66eb4810 100755 --- a/examples/test262/index.js +++ b/examples/test262/index.js @@ -1,5 +1,6 @@ import fs from 'fs'; import { join } from 'path'; +import { spawnSync } from 'child_process'; const basePath = import.meta.dirname; const indexPath = join(basePath, 'ant.json'); @@ -53,6 +54,9 @@ let state = { index: 0, filter: '', filtered: entries, + memoryOffset: 0, + memoryStatus: '', + memoryStatusAt: 0, searchMode: false, searchQuery: '' }; @@ -96,6 +100,114 @@ function truncate(str, len) { return str.slice(0, len - 1) + '…'; } +const byteStatSuffixes = new Set([ + 'used', + 'capacity', + 'totalUsed', + 'totalCapacity', + 'objects', + 'overflow', + 'extraSlots', + 'promises', + 'proxies', + 'exotic', + 'arrays', + 'shapes', + 'closures', + 'upvalues', + 'propRefs', + 'total', + 'buffers', + 'code', + 'bytes', + 'cstack', + 'rss', + 'virtualSize' +]); + +function formatStatValue(path, value, styled = true) { + if (typeof value === 'number') { + const key = path.split('.').pop(); + if (byteStatSuffixes.has(key)) { + return styled ? `${fmt(value)} ${c.dim}(${value})${c.reset}` : `${fmt(value)} (${value})`; + } + } + return String(value); +} + +function flattenStats(value, prefix = '', out = [], styled = true) { + if (value && typeof value === 'object') { + for (const [key, child] of Object.entries(value)) { + flattenStats(child, prefix ? `${prefix}.${key}` : key, out, styled); + } + return out; + } + + if (styled) { + out.push(`${c.gray}${prefix}${c.reset}: ${c.bold}${formatStatValue(prefix, value, true)}${c.reset}`); + } else { + out.push(`${prefix}: ${formatStatValue(prefix, value, false)}`); + } + return out; +} + +function buildMemoryLines(styled = true) { + const mem = Ant.stats(); + const lines = styled + ? [`${c.cyan}Ant.stats()${c.reset}`, `${c.dim}Raw runtime stats dump${c.reset}`, ''] + : ['Ant.stats()', 'Raw runtime stats dump', '']; + lines.push(...flattenStats(mem, '', [], styled)); + + if (Ant.raw && typeof Ant.raw.gcMarkProfile === 'function') { + lines.push(''); + lines.push(styled ? `${c.cyan}Ant.raw.gcMarkProfile()${c.reset}` : 'Ant.raw.gcMarkProfile()'); + lines.push(styled ? `${c.dim}Extra GC mark profiler stats${c.reset}` : 'Extra GC mark profiler stats'); + lines.push(''); + lines.push(...flattenStats(Ant.raw.gcMarkProfile(), 'gcMarkProfile', [], styled)); + } + + return lines; +} + +function memoryStatusText() { + if (!state.memoryStatus) return ''; + if (Date.now() - state.memoryStatusAt > 4000) { + state.memoryStatus = ''; + return ''; + } + return state.memoryStatus; +} + +function setMemoryStatus(message) { + state.memoryStatus = message; + state.memoryStatusAt = Date.now(); +} + +function copyMemoryStats() { + const text = buildMemoryLines(false).join('\n') + '\n'; + const commands = process.platform === 'win32' + ? [{ command: 'clip', args: [] }] + : [ + { command: 'pbcopy', args: [] }, + { command: 'wl-copy', args: [] }, + { command: 'xclip', args: ['-selection', 'clipboard'] }, + { command: 'xsel', args: ['--clipboard', '--input'] } + ]; + + for (const { command, args } of commands) { + try { + const result = spawnSync(command, args, { input: text }); + if (result.status === 0 || result.exitCode === 0) { + setMemoryStatus(`${c.green}Copied stats to clipboard with ${command}${c.reset}`); + return true; + } + } catch {} + } + + setMemoryStatus(`${c.red}Clipboard copy failed${c.reset}`); + return false; +} + function buildScreen() { const rows = getRows(); const cols = getCols(); @@ -187,69 +299,27 @@ function buildScreen() { lines.push(''); lines.push(`${c.dim}s browse · q quit${c.reset}`); } else if (state.mode === 'memory') { - const mem = Ant.stats(); - - lines.push(`${c.cyan}Pools${c.reset}`); - lines.push(` Rope: ${c.bold}${fmt(mem.pools.rope.used)}${c.reset} / ${fmt(mem.pools.rope.capacity)} (${mem.pools.rope.blocks} blocks)`); - lines.push( - ` String: ${c.bold}${fmt(mem.pools.string.used)}${c.reset} / ${fmt(mem.pools.string.capacity)} (${mem.pools.string.blocks} blocks)` - ); - lines.push( - ` Symbol: ${c.bold}${fmt(mem.pools.symbol.used)}${c.reset} / ${fmt(mem.pools.symbol.capacity)} (${mem.pools.symbol.blocks} blocks)` - ); - lines.push( - ` Bigint: ${c.bold}${fmt(mem.pools.bigint.used)}${c.reset} / ${fmt(mem.pools.bigint.capacity)} (${mem.pools.bigint.blocks} blocks)` - ); - lines.push(` Total: ${c.bold}${fmt(mem.pools.totalUsed)} / ${fmt(mem.pools.totalCapacity)}${c.reset}`); - lines.push(''); - - lines.push(`${c.cyan}Alloc${c.reset}`); - lines.push(` Objects: ${c.bold}${fmt(mem.alloc.objects)}${c.reset} (${mem.alloc.objectCount} objects)`); - lines.push(` Overflow: ${c.bold}${fmt(mem.alloc.overflow)}${c.reset}`); - lines.push(` ExtraSlots: ${c.bold}${fmt(mem.alloc.extraSlots)}${c.reset}`); - lines.push(` Promises: ${c.bold}${fmt(mem.alloc.promises)}${c.reset}`); - lines.push(` Proxies: ${c.bold}${fmt(mem.alloc.proxies)}${c.reset}`); - lines.push(` Exotic: ${c.bold}${fmt(mem.alloc.exotic)}${c.reset}`); - lines.push(` Arrays: ${c.bold}${fmt(mem.alloc.arrays)}${c.reset}`); - lines.push(` Shapes: ${c.bold}${fmt(mem.alloc.shapes)}${c.reset}`); - lines.push(` Closures: ${c.bold}${fmt(mem.alloc.closures)}${c.reset}`); - lines.push(` Upvalues: ${c.bold}${fmt(mem.alloc.upvalues)}${c.reset}`); - lines.push(` PropRefs: ${c.bold}${fmt(mem.alloc.propRefs)}${c.reset}`); - lines.push(` Total: ${c.bold}${fmt(mem.alloc.total)}${c.reset}`); - lines.push(''); - - lines.push(`${c.cyan}External${c.reset}`); - lines.push(` Buffers: ${c.bold}${fmt(mem.external.buffers)}${c.reset}`); - lines.push(` Code: ${c.bold}${fmt(mem.external.code)}${c.reset}`); - lines.push(` Total: ${c.bold}${fmt(mem.external.total)}${c.reset}`); - lines.push(''); - - lines.push(`${c.cyan}Intern Table${c.reset}`); - lines.push(` Strings: ${c.bold}${mem.intern.count}${c.reset}`); - lines.push(` Bytes: ${c.bold}${fmt(mem.intern.bytes)}${c.reset}`); - lines.push(''); + const footerLines = 2; + const memoryLines = buildMemoryLines(); + const viewHeight = Math.max(1, rows - footerLines); + const maxOffset = Math.max(0, memoryLines.length - viewHeight); - if (mem.vm) { - lines.push(`${c.cyan}VM${c.reset}`); - lines.push(` Stack: ${c.bold}${mem.vm.stackUsed} / ${mem.vm.stackSize}${c.reset}`); - lines.push(` Frames: ${c.bold}${mem.vm.framesUsed} / ${mem.vm.maxFrames}${c.reset}`); - lines.push(''); + if (state.memoryOffset > maxOffset) { + state.memoryOffset = maxOffset; } - lines.push(`${c.cyan}Process${c.reset}`); - lines.push(` RSS: ${c.bold}${fmt(mem.rss)}${c.reset}`); - if (mem.virtualSize) { - lines.push(` Virtual: ${c.bold}${fmt(mem.virtualSize)}${c.reset}`); - } - lines.push(` C Stack: ${c.bold}${fmt(mem.cstack)}${c.reset}`); - lines.push(''); + const end = Math.min(memoryLines.length, state.memoryOffset + viewHeight); + lines.push(...memoryLines.slice(state.memoryOffset, end)); - while (lines.length < rows - 2) { + while (lines.length < rows - footerLines) { lines.push(''); } lines.push(''); - lines.push(`${c.dim}m browse · q quit${c.reset} ${c.cyan}${fps.current} fps${c.reset}`); + const status = memoryStatusText(); + lines.push( + `${c.dim}↑↓ scroll · PgUp/PgDn · g/G top/bottom · c copy · m browse · q quit${c.reset} ${c.dim}[${state.memoryOffset + 1}-${end}/${memoryLines.length}]${c.reset} ${status}${status ? ' ' : ''}${c.cyan}${fps.current} fps${c.reset}` + ); } return lines.map(l => pad(l, cols)).join('\n'); @@ -313,28 +383,52 @@ function handleKey(key) { break; case '\x1b[A': case 'k': - state.index = Math.max(0, state.index - 1); + if (state.mode === 'memory') { + state.memoryOffset = Math.max(0, state.memoryOffset - 1); + } else { + state.index = Math.max(0, state.index - 1); + } needsRender = true; break; case '\x1b[B': case 'j': - state.index = Math.min(state.filtered.length - 1, state.index + 1); + if (state.mode === 'memory') { + state.memoryOffset++; + } else { + state.index = Math.min(state.filtered.length - 1, state.index + 1); + } needsRender = true; break; case '\x1b[5~': - state.index = Math.max(0, state.index - 10); + if (state.mode === 'memory') { + state.memoryOffset = Math.max(0, state.memoryOffset - Math.max(1, getRows() - 4)); + } else { + state.index = Math.max(0, state.index - 10); + } needsRender = true; break; case '\x1b[6~': - state.index = Math.min(state.filtered.length - 1, state.index + 10); + if (state.mode === 'memory') { + state.memoryOffset += Math.max(1, getRows() - 4); + } else { + state.index = Math.min(state.filtered.length - 1, state.index + 10); + } needsRender = true; break; case 'g': - state.index = 0; + if (state.mode === 'memory') { + state.memoryOffset = 0; + } else { + state.index = 0; + } needsRender = true; break; case 'G': - state.index = state.filtered.length - 1; + if (state.mode === 'memory') { + state.memoryOffset = Number.MAX_SAFE_INTEGER; + } else { + state.index = state.filtered.length - 1; + } needsRender = true; break; case 'p': @@ -363,6 +457,12 @@ function handleKey(key) { state.mode = state.mode === 'memory' ? 'browse' : 'memory'; needsRender = true; break; + case 'c': + if (state.mode === 'memory') { + copyMemoryStats(); + needsRender = true; + } + break; case '/': if (state.mode === 'browse') { state.searchMode = true; diff --git a/include/oxc.h b/include/oxc.h index 4c1931b9..48acd98c 100644 --- a/include/oxc.h +++ b/include/oxc.h @@ -12,6 +12,7 @@ char *OXC_strip_types_owned( const char *input, const char *filename, + int is_module, size_t *out_len, int *out_error, char *error_output, diff --git a/include/utils.h b/include/utils.h index d4325076..ec9e2bb1 100644 --- a/include/utils.h +++ b/include/utils.h @@ -28,6 +28,7 @@ int strip_typescript_inplace( char **buffer, size_t len, const char *filename, + int is_module, size_t *out_len, const char **error_detail ); diff --git a/src/esm/loader.c b/src/esm/loader.c index f1308e55..90974a3a 100644 --- a/src/esm/loader.c +++ b/src/esm/loader.c @@ -1147,7 +1147,9 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { if (!mod->embedded_code) { int strip_result = strip_typescript_inplace( - &content, size, mod->resolved_path, &js_len, &strip_detail + &content, size, mod->resolved_path, + mod->format != MODULE_EVAL_FORMAT_CJS, + &js_len, &strip_detail ); if (strip_result < 0) { diff --git a/src/strip/src/ffi.rs b/src/strip/src/ffi.rs index 882d9a14..3a13defa 100644 --- a/src/strip/src/ffi.rs +++ b/src/strip/src/ffi.rs @@ -36,7 +36,7 @@ unsafe fn write_error(output: *mut c_char, output_len: usize, msg: &str) { #[unsafe(no_mangle)] pub unsafe extern "C" fn OXC_strip_types_owned( - input: *const c_char, filename: *const c_char, out_len: *mut usize, out_error: *mut c_int, error_output: *mut c_char, error_output_len: usize, + input: *const c_char, filename: *const c_char, is_module: c_int, out_len: *mut usize, out_error: *mut c_int, error_output: *mut c_char, error_output_len: usize, ) -> *mut c_char { if !out_error.is_null() { unsafe { *out_error = OXC_ERR_NULL_INPUT }; @@ -73,7 +73,7 @@ pub unsafe extern "C" fn OXC_strip_types_owned( } }; - match strip_types_internal(input_str, filename_str) { + match strip_types_internal(input_str, filename_str, is_module != 0) { Ok(result) => { let bytes = result.as_bytes(); let alloc_len = bytes.len() + 1; diff --git a/src/strip/src/strip.rs b/src/strip/src/strip.rs index ec108bdd..a0df17e9 100644 --- a/src/strip/src/strip.rs +++ b/src/strip/src/strip.rs @@ -7,9 +7,9 @@ use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; use oxc_transformer::{TransformOptions, Transformer, TypeScriptOptions}; -pub fn strip_types_internal(source: &str, filename: &str) -> Result { +pub fn strip_types_internal(source: &str, filename: &str, is_module: bool) -> Result { let allocator = Allocator::default(); - let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts()); + let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts()).with_module(is_module); let parser_ret = Parser::new(&allocator, source, source_type).parse(); if !parser_ret.errors.is_empty() { diff --git a/src/types/ant.d.ts b/src/types/ant.d.ts index 53ef321f..80d436f7 100644 --- a/src/types/ant.d.ts +++ b/src/types/ant.d.ts @@ -43,11 +43,18 @@ interface AntPoolInfo { blocks: number; } +interface AntStringPoolInfo extends AntPoolInfo { + pooled: AntPoolInfo; + largeLive: AntPoolInfo; + largeReusable: AntPoolInfo; + largeQuarantine: AntPoolInfo; +} + interface AntPoolStats { rope: AntPoolInfo; symbol: AntPoolInfo; bigint: AntPoolInfo; - string: AntPoolInfo; + string: AntStringPoolInfo; totalUsed: number; totalCapacity: number; } @@ -78,6 +85,16 @@ interface AntStatsResult { pools: AntPoolStats; alloc: AntAllocStats; external: AntExternalMemory; + intern: { + count: number; + bytes: number; + }; + vm?: { + stackSize: number; + stackUsed: number; + maxFrames: number; + framesUsed: number; + }; cstack: number; rss?: number; virtualSize?: number; diff --git a/src/utils.c b/src/utils.c index 12b8113a..ba78ea20 100644 --- a/src/utils.c +++ b/src/utils.c @@ -129,6 +129,7 @@ int strip_typescript_inplace( char **buffer, size_t len, const char *filename, + int is_module, size_t *out_len, const char **error_detail ) { @@ -147,7 +148,7 @@ int strip_typescript_inplace( int strip_error = OXC_ERR_TRANSFORM_FAILED; char *stripped = OXC_strip_types_owned( - input, filename, + input, filename, is_module, &stripped_len, &strip_error, error_buf, sizeof(error_buf) ); diff --git a/tests/test_promise_chained_catch_unhandled.cjs b/tests/test_promise_chained_catch_unhandled.cjs new file mode 100644 index 00000000..1161d7e6 --- /dev/null +++ b/tests/test_promise_chained_catch_unhandled.cjs @@ -0,0 +1,21 @@ +const assert = require("node:assert"); +const { spawnSync } = require("child_process"); + +const source = [ + "async function explode(){ throw new Error('boom') }", + "explode().then(() => {}).catch(() => console.log('chained-caught'))", +].join(" "); + +const result = spawnSync(process.execPath, ["-e", source], { + encoding: "utf8", +}); + +if (result.error) { + throw result.error; +} + +assert.strictEqual(result.status, 0, result.stderr || result.stdout); +assert.match(result.stdout, /chained-caught/); +assert.doesNotMatch(result.stderr, /Uncaught \\(in promise\\)/); + +console.log("promise chains with downstream catch are not reported as unhandled"); diff --git a/tests/test_typescript_entry_tla.cjs b/tests/test_typescript_entry_tla.cjs new file mode 100644 index 00000000..90ded31c --- /dev/null +++ b/tests/test_typescript_entry_tla.cjs @@ -0,0 +1,41 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ant-ts-entry-tla-')); +const scriptPath = path.join(tmpRoot, 'entry.ts'); + +fs.writeFileSync( + scriptPath, + [ + 'const message: string = await Promise.resolve("ts tla ok");', + 'console.log(message);', + '', + ].join('\n') +); + +const result = spawnSync(process.execPath, [scriptPath], { + encoding: 'utf8', +}); + +if (result.error) throw result.error; + +assert( + result.status === 0, + `expected direct .ts entry with top-level await to exit 0, got ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` +); +assert( + result.stdout === 'ts tla ok\n', + `expected stdout to be ts tla ok, got ${JSON.stringify(result.stdout)}` +); +assert( + !/strip failed|await is only allowed within async functions/i.test(result.stderr), + `expected no TypeScript strip/TLA parse error, got stderr:\n${result.stderr}` +); + +console.log('direct TypeScript entrypoint top-level await works');