diff --git a/.changeset/nervous-tips-unite.md b/.changeset/nervous-tips-unite.md new file mode 100644 index 00000000..fb008780 --- /dev/null +++ b/.changeset/nervous-tips-unite.md @@ -0,0 +1,5 @@ +--- +'nxjs-runtime': patch +--- + +Add source map tracing in error stack traces diff --git a/apps/2048/.gitignore b/apps/2048/.gitignore index c8113175..0bd936ba 100644 --- a/apps/2048/.gitignore +++ b/apps/2048/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/2048/package.json b/apps/2048/package.json index 192ccff3..276382e2 100644 --- a/apps/2048/package.json +++ b/apps/2048/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js port of the 2048 game", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.js --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.js --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/ansi/.gitignore b/apps/ansi/.gitignore index c8113175..0bd936ba 100644 --- a/apps/ansi/.gitignore +++ b/apps/ansi/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/ansi/package.json b/apps/ansi/package.json index fe757745..41ba9922 100644 --- a/apps/ansi/package.json +++ b/apps/ansi/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app with colors and cursor movement using ANSI escapes", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/canvas/.gitignore b/apps/canvas/.gitignore index c8113175..0bd936ba 100644 --- a/apps/canvas/.gitignore +++ b/apps/canvas/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/canvas/package.json b/apps/canvas/package.json index 4ba14589..7f6d8371 100644 --- a/apps/canvas/package.json +++ b/apps/canvas/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app using the web `Canvas` API", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/fonts/.gitignore b/apps/fonts/.gitignore index c8113175..0bd936ba 100644 --- a/apps/fonts/.gitignore +++ b/apps/fonts/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/fonts/package.json b/apps/fonts/package.json index 8a1a5866..d914197b 100644 --- a/apps/fonts/package.json +++ b/apps/fonts/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app using customs fonts", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/hello-world/.gitignore b/apps/hello-world/.gitignore index c8113175..8cccdcb2 100644 --- a/apps/hello-world/.gitignore +++ b/apps/hello-world/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules -/romfs/main.js +/romfs/main.js* +/romfs/main.js.map diff --git a/apps/hello-world/package.json b/apps/hello-world/package.json index 400c3ee7..10852f64 100644 --- a/apps/hello-world/package.json +++ b/apps/hello-world/package.json @@ -4,7 +4,7 @@ "private": true, "description": "Simple 'Hello World' nx.js app", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 --format=esm src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/repl/.gitignore b/apps/repl/.gitignore index c8113175..0bd936ba 100644 --- a/apps/repl/.gitignore +++ b/apps/repl/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/repl/package.json b/apps/repl/package.json index 30e91172..15f60696 100644 --- a/apps/repl/package.json +++ b/apps/repl/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js REPL (read-eval-print-loop) app", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/snake/.gitignore b/apps/snake/.gitignore index c8113175..0bd936ba 100644 --- a/apps/snake/.gitignore +++ b/apps/snake/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/snake/package.json b/apps/snake/package.json index 8573e6e3..524c3652 100644 --- a/apps/snake/package.json +++ b/apps/snake/package.json @@ -4,7 +4,7 @@ "private": true, "description": "Classic 'snake' game using nx.js", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/starwars/.gitignore b/apps/starwars/.gitignore index c8113175..0bd936ba 100644 --- a/apps/starwars/.gitignore +++ b/apps/starwars/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/starwars/package.json b/apps/starwars/package.json index 3f8283f9..311bcd66 100644 --- a/apps/starwars/package.json +++ b/apps/starwars/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app that renders the Star Wars ASCII text", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/svg/.gitignore b/apps/svg/.gitignore index c8113175..0bd936ba 100644 --- a/apps/svg/.gitignore +++ b/apps/svg/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/svg/package.json b/apps/svg/package.json index 7ecc16eb..95e140a6 100644 --- a/apps/svg/package.json +++ b/apps/svg/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app that renders an SVG image to the screen", "scripts": { - "build": "esbuild --bundle --banner:js='globalThis.global = globalThis;' --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --banner:js='globalThis.global = globalThis;' --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/tests/.gitignore b/apps/tests/.gitignore index c8113175..0bd936ba 100644 --- a/apps/tests/.gitignore +++ b/apps/tests/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/tests/package.json b/apps/tests/package.json index f89afe09..f38c57e1 100644 --- a/apps/tests/package.json +++ b/apps/tests/package.json @@ -5,7 +5,7 @@ "type": "module", "description": "nx.js API tests", "scripts": { - "build": "esbuild --bundle --target=es2020 --format=esm src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --target=es2020 --sourcemap --sources-content=false --format=esm src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/touchscreen/.gitignore b/apps/touchscreen/.gitignore index c8113175..0bd936ba 100644 --- a/apps/touchscreen/.gitignore +++ b/apps/touchscreen/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/touchscreen/package.json b/apps/touchscreen/package.json index 90328f26..6b0a1c96 100644 --- a/apps/touchscreen/package.json +++ b/apps/touchscreen/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app using the touchscreen", "scripts": { - "build": "esbuild --bundle --main-fields=module,main --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --main-fields=module,main --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/vibrate/.gitignore b/apps/vibrate/.gitignore index c8113175..0bd936ba 100644 --- a/apps/vibrate/.gitignore +++ b/apps/vibrate/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/vibrate/package.json b/apps/vibrate/package.json index f03506d8..bffbd508 100644 --- a/apps/vibrate/package.json +++ b/apps/vibrate/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app demonstrating the `Switch.vibrate()` API", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/apps/wasm/.gitignore b/apps/wasm/.gitignore index c8113175..0bd936ba 100644 --- a/apps/wasm/.gitignore +++ b/apps/wasm/.gitignore @@ -1,3 +1,4 @@ /*.nro /node_modules /romfs/main.js +/romfs/main.js.map diff --git a/apps/wasm/package.json b/apps/wasm/package.json index c7073058..00b68fa6 100644 --- a/apps/wasm/package.json +++ b/apps/wasm/package.json @@ -4,7 +4,7 @@ "private": true, "description": "nx.js app that loads a WASM binary", "scripts": { - "build": "esbuild --bundle --target=es2020 src/main.ts --outfile=romfs/main.js", + "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2020 src/main.ts --outfile=romfs/main.js", "nro": "nxjs-pack" }, "license": "MIT", diff --git a/build.sh b/build.sh index 4674c461..c2c0d668 100755 --- a/build.sh +++ b/build.sh @@ -4,7 +4,7 @@ set -euo pipefail # Build JS runtime pnpm bundle mkdir -p romfs -cp -v ./packages/runtime/runtime.js ./romfs/runtime.js +cp -v ./packages/runtime/runtime.* ./romfs/ # Build packages and example apps pnpm build diff --git a/packages/runtime/.gitignore b/packages/runtime/.gitignore index b5ddc8d1..58fb5070 100644 --- a/packages/runtime/.gitignore +++ b/packages/runtime/.gitignore @@ -1,2 +1,2 @@ -/runtime.js +/runtime.js* /public diff --git a/packages/runtime/bundle.mjs b/packages/runtime/bundle.mjs index 8e450a51..103caa63 100644 --- a/packages/runtime/bundle.mjs +++ b/packages/runtime/bundle.mjs @@ -4,7 +4,10 @@ await build({ bundle: true, minify: process.env.MINIFY === '1', mainFields: ['module', 'main'], + //conditions: ['nxjs', 'import', 'browser', 'require', 'default'], target: 'es2020', entryPoints: ['src/index.ts'], + sourcemap: true, + sourcesContent: false, outfile: 'runtime.js', }); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b2179a18..c24538ea 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -15,6 +15,7 @@ "author": "Nathan Rajlich ", "license": "MIT", "devDependencies": { + "@jridgewell/trace-mapping": "^0.3.19", "@types/color-rgba": "^2.1.0", "@types/to-px": "^1.1.2", "@ungap/event-target": "^0.2.4", diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index d8fef0dc..a977c384 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -79,6 +79,8 @@ import './navigator'; import * as WebAssembly from './wasm'; def('WebAssembly', WebAssembly); +import './source-map'; + /** * The `import.meta` meta-property exposes context-specific metadata to a JavaScript module. * It contains information about the module, such as the module's URL. diff --git a/packages/runtime/src/source-map.ts b/packages/runtime/src/source-map.ts new file mode 100644 index 00000000..7bdd6aec --- /dev/null +++ b/packages/runtime/src/source-map.ts @@ -0,0 +1,75 @@ +import { dataUriToBuffer } from 'data-uri-to-buffer'; +import { + TraceMap, + originalPositionFor, + type EncodedSourceMap, +} from '@jridgewell/trace-mapping'; +import type { SwitchClass } from './switch'; + +declare const Switch: SwitchClass; + +const SOURCE_MAPPING_URL_PREFIX = '//# sourceMappingURL='; +const sourceMapCache = new Map(); + +function filenameToTracer(filename: string) { + let tracer = sourceMapCache.get(filename); + if (typeof tracer !== 'undefined') return tracer; + + // `null` means the source map could not be retrieved for this file + tracer = null; + + const contentsBuffer = Switch.readFileSync(filename); + const contents = new TextDecoder().decode(contentsBuffer).trimEnd(); + const lastNewline = contents.lastIndexOf('\n'); + const lastLine = contents.slice(lastNewline + 1); + if (lastLine.startsWith(SOURCE_MAPPING_URL_PREFIX)) { + const sourceMappingURL = lastLine.slice( + SOURCE_MAPPING_URL_PREFIX.length + ); + let sourceMapBuffer: ArrayBuffer; + if (sourceMappingURL.startsWith('data:')) { + sourceMapBuffer = dataUriToBuffer(sourceMappingURL).buffer; + } else { + sourceMapBuffer = Switch.readFileSync( + new URL(sourceMappingURL, filename) + ); + } + const sourceMap: EncodedSourceMap = JSON.parse( + new TextDecoder().decode(sourceMapBuffer) + ); + tracer = new TraceMap(sourceMap); + } + sourceMapCache.set(filename, tracer); + return tracer; +} + +(Error as any).prepareStackTrace = (_: Error, stack: string) => { + return stack + .split('\n') + .map((line) => { + try { + const m = line.match(/(\s+at )(.*) \((.*)\:(\d+)\)$/); + if (!m) return line; + + const [_, at, name, filename, lineNo] = m; + const tracer = filenameToTracer(filename); + if (!tracer) return line; + + const traced = originalPositionFor(tracer, { + line: +lineNo, + // QuickJS doesn't provide column number. + // Unfortunately that means that minification + // doesn't work well with source maps :( + column: 0, + }); + if (!traced.source || !traced.line) return line; + + const proto = filename === 'romfs:/runtime.js' ? 'nxjs' : 'app'; + return `${at}${traced.name || name} (${proto}:${ + traced.source + }:${traced.line})`; + } catch (_) {} + return line; + }) + .join('\n'); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f36ee673..7aefb512 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -308,6 +308,9 @@ importers: packages/runtime: devDependencies: + '@jridgewell/trace-mapping': + specifier: ^0.3.19 + version: 0.3.19 '@types/color-rgba': specifier: ^2.1.0 version: 2.1.0 @@ -787,6 +790,22 @@ packages: dev: true optional: true + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: diff --git a/upload.sh b/upload.sh index 4457abea..ba617493 100755 --- a/upload.sh +++ b/upload.sh @@ -1,7 +1,16 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail APP="${1-hello-world}" SWITCH_HOST="${2-192.168.86.115:5000}" -curl --netrc-optional \ - --upload-file nxjs.nro "ftp://${SWITCH_HOST}/switch/nxjs.nro" \ + +ARGS=( + --upload-file nxjs.nro "ftp://${SWITCH_HOST}/switch/nxjs.nro" --upload-file "./apps/${APP}/romfs/main.js" "ftp://${SWITCH_HOST}/switch/nxjs.js" +) + +# Upload source map file, if it exists +if [ -f "./apps/${APP}/romfs/main.js.map" ]; then + ARGS+=(--upload-file "./apps/${APP}/romfs/main.js.map" "ftp://${SWITCH_HOST}/switch/main.js.map") +fi + +curl --netrc-optional "${ARGS[@]}"