From 3b0e559bdf8517ebef4b89e325447f9dbbb2ec35 Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Tue, 9 Jul 2024 22:42:27 +0800 Subject: [PATCH 1/3] feat: setup rsbuild --- .gitignore | 13 ++ package.json | 23 +++ pnpm-lock.yaml | 360 ++++++++++++++++++++++++++++++++++++++++++++++ rsbuild.config.ts | 7 + src/env.d.ts | 1 + src/utils.ts | 50 +++++++ tsconfig.json | 17 +++ 7 files changed, 471 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 rsbuild.config.ts create mode 100644 src/env.d.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38d7344 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/package.json b/package.json new file mode 100644 index 0000000..b8eadee --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "source-map-visualization", + "private": true, + "version": "1.0.0", + "scripts": { + "dev": "rsbuild dev --open", + "build": "rsbuild build", + "preview": "rsbuild preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "source-map": "^0.7.4", + "type-fest": "^4.21.0" + }, + "devDependencies": { + "@rsbuild/core": "1.0.0-alpha.5", + "@rsbuild/plugin-react": "1.0.0-alpha.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.5.3" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3637918 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,360 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + source-map: + specifier: ^0.7.4 + version: 0.7.4 + type-fest: + specifier: ^4.21.0 + version: 4.21.0 + +devDependencies: + '@rsbuild/core': + specifier: 1.0.0-alpha.5 + version: 1.0.0-alpha.5 + '@rsbuild/plugin-react': + specifier: 1.0.0-alpha.5 + version: 1.0.0-alpha.5(@rsbuild/core@1.0.0-alpha.5) + '@types/react': + specifier: ^18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + +packages: + + /@module-federation/runtime-tools@0.2.3: + resolution: {integrity: sha512-capN8CVTCEqNAjnl102girrkevczoQfnQYyiYC4WuyKsg7+LUqfirIe1Eiyv6VSE2UgvOTZDnqvervA6rBOlmg==} + dependencies: + '@module-federation/runtime': 0.2.3 + '@module-federation/webpack-bundler-runtime': 0.2.3 + dev: true + + /@module-federation/runtime@0.2.3: + resolution: {integrity: sha512-N+ZxBUb1mkmfO9XT1BwgYQgShtUTlijHbukqQ4afFka5lRAT+ayC7RKfHJLz0HbuexKPCmPBDfdmCnErR5WyTQ==} + dependencies: + '@module-federation/sdk': 0.2.3 + dev: true + + /@module-federation/sdk@0.2.3: + resolution: {integrity: sha512-W9zrPchLocyCBc/B8CW21akcfJXLl++9xBe1L1EtgxZGfj/xwHt0GcBWE/y+QGvYTL2a1iZjwscbftbUhxgxXg==} + dev: true + + /@module-federation/webpack-bundler-runtime@0.2.3: + resolution: {integrity: sha512-L/jt2uJ+8dwYiyn9GxryzDR6tr/Wk8rpgvelM2EBeLIhu7YxCHSmSjQYhw3BTux9zZIr47d1K9fGjBFsVRd/SQ==} + dependencies: + '@module-federation/runtime': 0.2.3 + '@module-federation/sdk': 0.2.3 + dev: true + + /@rsbuild/core@1.0.0-alpha.5: + resolution: {integrity: sha512-zcEwgzeuDHUjSpBBRMfk6+seYLZKLTFa89wdQuSSgylCfvAahfz/Pea5PoblzfSd1PP8Z/5h+Z0Er2WYS+GMWg==} + engines: {node: '>=16.7.0'} + hasBin: true + dependencies: + '@rspack/core': 1.0.0-alpha.1(@swc/helpers@0.5.11) + '@swc/helpers': 0.5.11 + caniuse-lite: 1.0.30001640 + core-js: 3.37.1 + html-rspack-plugin: 5.8.0(@rspack/core@1.0.0-alpha.1) + postcss: 8.4.39 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /@rsbuild/plugin-react@1.0.0-alpha.5(@rsbuild/core@1.0.0-alpha.5): + resolution: {integrity: sha512-zdt6MzFgTknsybQohSFWq+274O6mik0tCO9rKDJw8/tme3jNpOLxZXBoBMdoGKd+vm1z/SoWvHEtB37+U6MpVQ==} + peerDependencies: + '@rsbuild/core': ^1.0.0-alpha.5 + dependencies: + '@rsbuild/core': 1.0.0-alpha.5 + '@rspack/plugin-react-refresh': 1.0.0-alpha.1(react-refresh@0.14.2) + react-refresh: 0.14.2 + dev: true + + /@rspack/binding-darwin-arm64@1.0.0-alpha.1: + resolution: {integrity: sha512-6ZbxlS5+29bvYqsEoPMvCnn6NxZPq8CucuDDSwuP/w0UH81DD2yf9Mtn7IYDBhjiS7wv9pF4NYr7oDpGfOksHA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-darwin-x64@1.0.0-alpha.1: + resolution: {integrity: sha512-4Y9xfwaTHJBTrq5uav/tOWvCMLcBG72SEi+XgVjsZOC2ZS2WNqg4z0QCRzmt+agdmlWuXTOMB2LpYxRLxBj9sg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-linux-arm64-gnu@1.0.0-alpha.1: + resolution: {integrity: sha512-cew/WxOILZkTN1I+oWrt1BSnm3iK4/8DqX/6XxiUwCAmgfStZXB5NQS+SDIDBzFJ4I+NibLRMiJy5T9uCtOWzQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-linux-arm64-musl@1.0.0-alpha.1: + resolution: {integrity: sha512-+yLjjl8nkWRseQwwovaaLMshTKTep/5PSFN3nHtXPo/TsL1itDgUtM9XntTdeTdaIEgVyNGcb1dRARDoAq/vKw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-linux-x64-gnu@1.0.0-alpha.1: + resolution: {integrity: sha512-QQfGCTrn76d6fVoWnn6tm2eYTSJe78wc3X4H92Js+ZhjcHVn5XSTeyqyb0Oq4+TRKmwAi39/wUgPsbSxQj9g5A==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-linux-x64-musl@1.0.0-alpha.1: + resolution: {integrity: sha512-ZpVTXPjG5SLwKUYiwLR6XIlo/G4ZQHA6TBKFbPG3p9kZ0DYurGjt8bcjeiu9xP050XmTA0Hj5vS6zEdYtgE6bA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-win32-arm64-msvc@1.0.0-alpha.1: + resolution: {integrity: sha512-8wTNt1MgJgUMRJ2ySAToUZoeBJabOV/lqH2o1ypHdmirZ0aMno+C05XOmbrLhgZL2qKBROVt4VVa7+IlmvJ7yQ==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-win32-ia32-msvc@1.0.0-alpha.1: + resolution: {integrity: sha512-hfujEyp1+0yfAzShQJZUezdiHQ7KvTwC0T817/dQi4CdwoXWOuPnnYrb5JG5TAcagNttQNX2NFhVbeYQgj5c8Q==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding-win32-x64-msvc@1.0.0-alpha.1: + resolution: {integrity: sha512-KMIosN4wdXVFb3RIBX7fEXBv5jfinK0PkYSv8aQcIAdyhm2mSI95aCpAFE8xhgGCeSwaZ2PCO0zBgO0ZldAc7Q==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rspack/binding@1.0.0-alpha.1: + resolution: {integrity: sha512-3q3cN5kZZdaAnIrjVhkW2f2RbLpxgSp8ATs4P6fzUoKvuunU1v+KXbhPir/tKaKBXLgYH2h3i13tPDcuL3kA+A==} + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.0.0-alpha.1 + '@rspack/binding-darwin-x64': 1.0.0-alpha.1 + '@rspack/binding-linux-arm64-gnu': 1.0.0-alpha.1 + '@rspack/binding-linux-arm64-musl': 1.0.0-alpha.1 + '@rspack/binding-linux-x64-gnu': 1.0.0-alpha.1 + '@rspack/binding-linux-x64-musl': 1.0.0-alpha.1 + '@rspack/binding-win32-arm64-msvc': 1.0.0-alpha.1 + '@rspack/binding-win32-ia32-msvc': 1.0.0-alpha.1 + '@rspack/binding-win32-x64-msvc': 1.0.0-alpha.1 + dev: true + + /@rspack/core@1.0.0-alpha.1(@swc/helpers@0.5.11): + resolution: {integrity: sha512-UN6oAWnDJpouldf6UDuZZIc1GSgEgSAeeIQlpCwob9v+uuZ/NjJHvG7HCjxeHtkh1g9Oly8clOZA7gKOWtE4CA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@module-federation/runtime-tools': 0.2.3 + '@rspack/binding': 1.0.0-alpha.1 + '@rspack/lite-tapable': 1.0.0-alpha.1 + '@swc/helpers': 0.5.11 + caniuse-lite: 1.0.30001640 + dev: true + + /@rspack/lite-tapable@1.0.0-alpha.1: + resolution: {integrity: sha512-vgY3jauZk+Pd6u6I5d4Rs9Q7hUtl5mClKG2Hn/FucFL4WK+1m6kssu/576WEY6HP5ptBPWlqcBbamzodai1q1g==} + engines: {node: '>=16.0.0'} + dev: true + + /@rspack/plugin-react-refresh@1.0.0-alpha.1(react-refresh@0.14.2): + resolution: {integrity: sha512-lIQBT3xbaf8wStCt8gRI2LUoYTYix3Z4GYqQGjVGX66ypGliIR5N7yfs318nbjzD7mr5KJmv6H4XG+AtXrElYQ==} + peerDependencies: + react-refresh: '>=0.10.0 <1.0.0' + peerDependenciesMeta: + react-refresh: + optional: true + dependencies: + error-stack-parser: 2.1.4 + html-entities: 2.5.2 + react-refresh: 0.14.2 + dev: true + + /@swc/helpers@0.5.11: + resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} + dependencies: + tslib: 2.6.3 + dev: true + + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + dev: true + + /@types/react-dom@18.3.0: + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + dependencies: + '@types/react': 18.3.3 + dev: true + + /@types/react@18.3.3: + resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + dev: true + + /caniuse-lite@1.0.30001640: + resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==} + dev: true + + /core-js@3.37.1: + resolution: {integrity: sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==} + requiresBuild: true + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: true + + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + dev: true + + /html-rspack-plugin@5.8.0(@rspack/core@1.0.0-alpha.1): + resolution: {integrity: sha512-ilfK60cxmBzglkHw91SlHDwbTd8uS7+poG12ueuwn012XPdFq8jU0pFuGEqoryJ+1l/uQuVffJ2jlpJDlhJBsg==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + peerDependenciesMeta: + '@rspack/core': + optional: true + dependencies: + '@rspack/core': 1.0.0-alpha.1(@swc/helpers@0.5.11) + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + + /postcss@8.4.39: + resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + + /react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + dev: true + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: false + + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: true + + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + dev: true + + /type-fest@4.21.0: + resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==} + engines: {node: '>=16'} + dev: false + + /typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true diff --git a/rsbuild.config.ts b/rsbuild.config.ts new file mode 100644 index 0000000..04c3f82 --- /dev/null +++ b/rsbuild.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from '@rsbuild/core'; + +export default defineConfig({ + html: { + template: './index.html', + }, +}); diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..b0ac762 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..96f5dab --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,50 @@ +export function assert(condition: any, message?: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +export function $assert(condition: T, message?: string): NonNullable { + assert(condition, message); + return condition; +} + +export function assertInstance( + value: any, + constructor: new (...args: any[]) => T, + message?: string, +): asserts value is T { + if (!(value instanceof constructor)) { + throw new Error(message); + } +} + +export function $assertInstance( + value: any, + constructor: new (...args: any[]) => T, + message?: string, +): T { + assertInstance(value, constructor, message); + return value; +} + +const SELECTOR_MARKS = ['.', '#'] as const; +type SelectorMark = (typeof SELECTOR_MARKS)[number]; + +/** + * Query elements by selector and infer the return type by the template string of the selector. + * @example querySelector('div') // => HTMLDivElement + * @example querySelector('span.foo') // => HTMLSpanElement + * @example querySelector('canvas#bar') // => HTMLCanvasElement + */ +export function querySelector( + selector: S, +): S extends `${infer Name extends keyof HTMLElementTagNameMap}${Mark}${string}` + ? HTMLElementTagNameMap[Name] + : HTMLElement { + const el = document.querySelector(selector); + assert(el instanceof HTMLElement, `Element not found: ${selector}`); + return el as any; +} + +export { querySelector as $ }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6fe0027 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["DOM", "ES2020"], + "module": "ESNext", + "jsx": "react-jsx", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "useDefineForClassFields": true, + "allowImportingTsExtensions": true + }, + "include": ["src"] +} From b35321a5a0c4d4713e364354d204f3c3075e250c Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Tue, 9 Jul 2024 22:48:24 +0800 Subject: [PATCH 2/3] feat: move source files into src directory --- index.html | 156 +++++++++++++++++++++---------------- code.js => src/index.ts | 0 style.css => src/style.css | 26 +++---- 3 files changed, 103 insertions(+), 79 deletions(-) rename code.js => src/index.ts (100%) rename style.css => src/style.css (85%) diff --git a/index.html b/index.html index fd8d2ac..0c782fa 100644 --- a/index.html +++ b/index.html @@ -1,70 +1,94 @@ + + + Source Map Visualization + - - - Source Map Visualization - - - - - -
-
-
-

Source Map Visualization

-

This is a visualization of JavaScript/CSS source map data, which is useful for debugging problems with - generated source maps. It's designed to be high-performance so it doesn't fall over with huge source maps.

-

Drag and drop some files here or to get started. You can either - drop a single JavaScript/CSS file with an inline source map comment, or a JavaScript/CSS file and a separate - source map file together.

-

Or you can to play around with the visualization.

-
- -
-
-
-
-

Original code

-
-
-
-

Generated code

-
-
-
-
-
-
-
-
-
- -
-
-
- - - - - - - - -
-
-
-
- - - + + +
+
+
+

Source Map Visualization

+

+ This is a visualization of JavaScript/CSS source map data, + which is useful for debugging problems with generated source maps. + It's designed to be high-performance so it doesn't fall over with huge + source maps. +

+

+ Drag and drop some files here or + to get started. You can + either drop a single JavaScript/CSS file with an inline source + map comment, or a JavaScript/CSS file and a separate source map + file together. +

+

+ Or you can to play + around with the visualization. +

+
+ +
+
+
+
+

Original code

+
+
+
+

Generated code

+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + +
+
+
+
+ diff --git a/code.js b/src/index.ts similarity index 100% rename from code.js rename to src/index.ts diff --git a/style.css b/src/style.css similarity index 85% rename from style.css rename to src/style.css index a750f56..eddb976 100644 --- a/style.css +++ b/src/style.css @@ -44,7 +44,7 @@ canvas { padding-left: 8px; } -#toolbar section>* { +#toolbar section > * { vertical-align: middle; margin-right: 20px; } @@ -88,7 +88,7 @@ canvas { padding-left: 8px; } -#statusBar section>:first-child { +#statusBar section > :first-child { vertical-align: middle; margin-right: 20px; opacity: 0.7; @@ -195,61 +195,61 @@ noscript:before { /* ---------- Light colors ---------- */ -body:not([data-theme=dark]) { +body:not([data-theme='dark']) { color: #222; fill: #222; stroke: #222; background: white; } -body:not([data-theme=dark]) #theme-dark { +body:not([data-theme='dark']) #theme-dark { display: none; } -body:not([data-theme=dark]) #progressBar .progress { +body:not([data-theme='dark']) #progressBar .progress { background: #222; } /* ---------- Dark colors ---------- */ -body[data-theme=dark] { +body[data-theme='dark'] { color: #eee; fill: #eee; stroke: #eee; background: #333; } -body[data-theme=dark] #theme-light { +body[data-theme='dark'] #theme-light { display: none; } -body[data-theme=dark] #theme-dark { +body[data-theme='dark'] #theme-dark { display: block; } -body[data-theme=dark] #progressBar .progress { +body[data-theme='dark'] #progressBar .progress { background: #eee; } /* ---------- Dark colors without JavaScript ---------- */ @media (prefers-color-scheme: dark) { - body:not([data-theme=light]) { + body:not([data-theme='light']) { color: #eee; fill: #eee; stroke: #eee; background: #333; } - body:not([data-theme=light]) #theme-light { + body:not([data-theme='light']) #theme-light { display: none; } - body:not([data-theme=light]) #theme-dark { + body:not([data-theme='light']) #theme-dark { display: block; } - body:not([data-theme=light]) #progressBar .progress { + body:not([data-theme='light']) #progressBar .progress { background: #eee; } } From 112fa903c5687a6ac3f895f172a88b960eb285a9 Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Tue, 9 Jul 2024 22:48:36 +0800 Subject: [PATCH 3/3] refactor: into typescript module --- src/index.ts | 4340 +++++++++++++++++++++++++++++--------------------- 1 file changed, 2505 insertions(+), 1835 deletions(-) diff --git a/src/index.ts b/src/index.ts index db48310..6becb9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2079 +1,2749 @@ -(() => { - //////////////////////////////////////////////////////////////////////////////// - // Dragging - - const dragTarget = document.getElementById('dragTarget'); - const uploadFiles = document.getElementById('uploadFiles'); - const loadExample = document.getElementById('loadExample'); - let dragging = 0; - let filesInput; - - function isFilesDragEvent(e) { - return e.dataTransfer && e.dataTransfer.types && Array.prototype.indexOf.call(e.dataTransfer.types, 'Files') !== -1; - } +import './style.css'; +import { Promisable } from 'type-fest'; +import { $, $assert, assert } from './utils'; +import { RawIndexMap, RawSourceMap } from 'source-map'; + +//////////////////////////////////////////////////////////////////////////////// +// Dragging + +const dragTarget = $('#dragTarget'); +const uploadFiles = $('#uploadFiles'); +const loadExample = $('#loadExample'); + +let dragging = 0; +let filesInput: HTMLInputElement; + +function isFilesDragEvent(e: DragEvent) { + return ( + e.dataTransfer && + e.dataTransfer.types && + Array.prototype.indexOf.call(e.dataTransfer.types, 'Files') !== -1 + ); +} - document.ondragover = e => { - e.preventDefault(); - }; +document.ondragover = e => { + e.preventDefault(); +}; - document.ondragenter = e => { - e.preventDefault(); - if (!isFilesDragEvent(e)) return; - dragTarget.style.display = 'block'; - dragging++; - }; +document.ondragenter = e => { + e.preventDefault(); + if (!isFilesDragEvent(e)) return; + dragTarget.style.display = 'block'; + dragging++; +}; - document.ondragleave = e => { - e.preventDefault(); - if (!isFilesDragEvent(e)) return; - if (--dragging === 0) dragTarget.style.display = 'none'; - }; +document.ondragleave = e => { + e.preventDefault(); + if (!isFilesDragEvent(e)) return; + if (--dragging === 0) dragTarget.style.display = 'none'; +}; - document.ondrop = e => { - e.preventDefault(); - dragTarget.style.display = 'none'; - dragging = 0; - if (e.dataTransfer && e.dataTransfer.files) startLoading(e.dataTransfer.files); - }; +document.ondrop = e => { + e.preventDefault(); + dragTarget.style.display = 'none'; + dragging = 0; + if (e.dataTransfer && e.dataTransfer.files) + startLoading(e.dataTransfer.files); +}; - uploadFiles.onclick = () => { - if (filesInput) document.body.removeChild(filesInput); - filesInput = document.createElement('input'); - filesInput.type = 'file'; - filesInput.multiple = true; - filesInput.style.display = 'none'; - document.body.appendChild(filesInput); - filesInput.click(); - filesInput.onchange = () => startLoading(filesInput.files); - }; +uploadFiles.onclick = () => { + if (filesInput) document.body.removeChild(filesInput); + filesInput = document.createElement('input'); + filesInput.type = 'file'; + filesInput.multiple = true; + filesInput.style.display = 'none'; + document.body.appendChild(filesInput); + filesInput.click(); + filesInput.onchange = () => + filesInput.files && startLoading(filesInput.files); +}; - loadExample.onclick = () => { - finishLoading(exampleJS, exampleMap); - }; +loadExample.onclick = () => { + finishLoading(exampleJS, exampleMap); +}; - //////////////////////////////////////////////////////////////////////////////// - // Loading +//////////////////////////////////////////////////////////////////////////////// +// Loading - const utf8ToUTF16 = x => decodeURIComponent(escape(x)); - const utf16ToUTF8 = x => unescape(encodeURIComponent(x)); +const utf8ToUTF16 = (x: string) => decodeURIComponent(escape(x)); +const utf16ToUTF8 = (x: string) => unescape(encodeURIComponent(x)); - const promptText = document.getElementById('promptText'); - const errorText = document.getElementById('errorText'); - const toolbar = document.getElementById('toolbar'); - const statusBar = document.getElementById('statusBar'); - const progressBarOverlay = document.getElementById('progressBar'); - const progressBar = document.querySelector('#progressBar .progress'); - const originalStatus = document.getElementById('originalStatus'); - const generatedStatus = document.getElementById('generatedStatus'); +const promptText = $('#promptText'); +const errorText = $('#errorText'); +const toolbar = $('#toolbar'); +const statusBar = $('#statusBar'); +const progressBarOverlay = $('#progressBar'); +const progressBar = $('#progressBar .progress'); +const originalStatus = $('#originalStatus'); +const generatedStatus = $('#generatedStatus'); +const fileList = $('select#fileList'); - function isProbablySourceMap(file) { - return file.name.endsWith('.map') || file.name.endsWith('.json'); - } +function isProbablySourceMap(file: File) { + return file.name.endsWith('.map') || file.name.endsWith('.json'); +} - function loadFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onloadend = () => resolve(reader.result); - reader.readAsText(file); - }); - } +function loadFile(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = () => resolve(reader.result && reader.result.toString()); + reader.readAsText(file); + }); +} - function resetLoadingState() { - promptText.style.display = 'block'; - toolbar.style.display = 'none'; - statusBar.style.display = 'none'; - canvas.style.display = 'none'; - } +function resetLoadingState() { + promptText.style.display = 'block'; + toolbar.style.display = 'none'; + statusBar.style.display = 'none'; + canvas.style.display = 'none'; +} - function showLoadingError(text) { - resetLoadingState(); - errorText.style.display = 'block'; - errorText.textContent = text; +function showLoadingError(text: string) { + resetLoadingState(); + errorText.style.display = 'block'; + errorText.textContent = text; - // Push an empty hash since the state has been cleared - if (location.hash !== '') { - try { - history.pushState({}, '', location.pathname); - } catch (e) { - } - } + // Push an empty hash since the state has been cleared + if (location.hash !== '') { + try { + history.pushState({}, '', location.pathname); + } catch (e) { } } +} - async function finishLoadingCodeWithEmbeddedSourceMap(code, file) { - let url, match; - - // Check for both "//" and "/*" comments. This is mostly done manually - // instead of doing it all with a regular expression because Firefox's - // regular expression engine crashes with an internal error when the - // match is too big. - for (let regex = /\/([*/])[#@] *sourceMappingURL=/g; match = regex.exec(code);) { - const start = match.index + match[0].length; - const n = code.length; - let end = start; - while (end < n && code.charCodeAt(end) > 32) { - end++; - } - if (end > start && (match[1] === '/' || code.slice(end).indexOf('*/') > 0)) { - url = code.slice(start, end); - break; - } - } - - // Check for a non-empty data URL payload - if (url) { - let map; - try { - // Use "new URL" to ensure that the URL has a protocol (e.g. "data:" or "https:") - map = await fetch(new URL(url)).then(r => r.text()); - } catch (e) { - showLoadingError(`Failed to parse the URL in the "/${match[1]}# sourceMappingURL=" comment: ${e && e.message || e}`); - return; - } - finishLoading(code, map); - } - - else if (file && isProbablySourceMap(file)) { - // Allow loading a source map without a generated file because why not - finishLoading('', code); +async function finishLoadingCodeWithEmbeddedSourceMap( + code: string, + file: File | null, +) { + let url = ''; + let match: RegExpExecArray | null; + + // Check for both "//" and "/*" comments. This is mostly done manually + // instead of doing it all with a regular expression because Firefox's + // regular expression engine crashes with an internal error when the + // match is too big. + for ( + let regex = /\/([*/])[#@] *sourceMappingURL=/g; + (match = regex.exec(code)); + + ) { + const start = match.index + match[0].length; + const n = code.length; + let end = start; + while (end < n && code.charCodeAt(end) > 32) { + end++; } - - else { - const c = file && file.name.endsWith('ss') ? '*' : '/'; - showLoadingError(`Failed to find an embedded "/${c}# sourceMappingURL=" comment in the ${file ? 'imported file' : 'pasted text'}.`); + if ( + end > start && + (match[1] === '/' || code.slice(end).indexOf('*/') > 0) + ) { + url = code.slice(start, end); + break; } } - async function startLoading(files) { - if (files.length === 1) { - const file0 = files[0]; - const code = await loadFile(file0); - finishLoadingCodeWithEmbeddedSourceMap(code, file0); - } - - else if (files.length === 2) { - const file0 = files[0]; - const file1 = files[1]; - - if (isProbablySourceMap(file0)) { - const codePromise = loadFile(file1); - const mapPromise = loadFile(file0); - const code = await codePromise; - const map = await mapPromise; - finishLoading(code, map); - } - - else if (isProbablySourceMap(file1)) { - const codePromise = loadFile(file0); - const mapPromise = loadFile(file1); - const code = await codePromise; - const map = await mapPromise; - finishLoading(code, map); - } - - else { - showLoadingError(`The source map file must end in either ".map" or ".json" to be detected.`); - } + // Check for a non-empty data URL payload + if (url) { + let map; + try { + // Use "new URL" to ensure that the URL has a protocol (e.g. "data:" or "https:") + map = await fetch(new URL(url)).then(r => r.text()); + } catch (e) { + assert(e instanceof Error); + assert(match); + showLoadingError( + `Failed to parse the URL in the "/${match[1] + }# sourceMappingURL=" comment: ${(e && e.message) || e}`, + ); + return; } + finishLoading(code, map); + } else if (file && isProbablySourceMap(file)) { + // Allow loading a source map without a generated file because why not + finishLoading('', code); + } else { + const c = file && file.name.endsWith('ss') ? '*' : '/'; + showLoadingError( + `Failed to find an embedded "/${c}# sourceMappingURL=" comment in the ${file ? 'imported file' : 'pasted text' + }.`, + ); + } +} - else { - showLoadingError(`Please import either 1 or 2 files.`); +async function startLoading(files: FileList) { + if (files.length === 1) { + const file0 = files[0]; + const code = await loadFile(file0); + assert(code); + finishLoadingCodeWithEmbeddedSourceMap(code, file0); + } else if (files.length === 2) { + const file0 = files[0]; + const file1 = files[1]; + + if (isProbablySourceMap(file0)) { + const codePromise = loadFile(file1); + const mapPromise = loadFile(file0); + const code = await codePromise; + const map = await mapPromise; + assert(code && map); + finishLoading(code, map); + } else if (isProbablySourceMap(file1)) { + const codePromise = loadFile(file0); + const mapPromise = loadFile(file1); + const code = await codePromise; + const map = await mapPromise; + assert(code && map); + finishLoading(code, map); + } else { + showLoadingError( + `The source map file must end in either ".map" or ".json" to be detected.`, + ); } + } else { + showLoadingError(`Please import either 1 or 2 files.`); } +} - document.body.addEventListener('paste', e => { - e.preventDefault(); - const code = e.clipboardData.getData('text/plain'); - finishLoadingCodeWithEmbeddedSourceMap(code, null); - }); - - // Accelerate VLQ decoding with a lookup table - const vlqTable = new Uint8Array(128); - const vlqChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - for (let i = 0; i < vlqTable.length; i++) vlqTable[i] = 0xFF; - for (let i = 0; i < vlqChars.length; i++) vlqTable[vlqChars.charCodeAt(i)] = i; - - function decodeMappings(mappings, sourcesCount, namesCount) { - const n = mappings.length; - let data = new Int32Array(1024); - let dataLength = 0; - let generatedLine = 0; - let generatedLineStart = 0; - let generatedColumn = 0; - let originalSource = 0; - let originalLine = 0; - let originalColumn = 0; - let originalName = 0; - let needToSortGeneratedColumns = false; - let i = 0; +document.body.addEventListener('paste', e => { + e.preventDefault(); + const { clipboardData } = e; + assert(clipboardData); + const code = clipboardData.getData('text/plain'); + finishLoadingCodeWithEmbeddedSourceMap(code, null); +}); + +// Accelerate VLQ decoding with a lookup table +const vlqTable = new Uint8Array(128); +const vlqChars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +for (let i = 0; i < vlqTable.length; i++) vlqTable[i] = 0xff; +for (let i = 0; i < vlqChars.length; i++) vlqTable[vlqChars.charCodeAt(i)] = i; + +function decodeMappings( + mappings: string, + sourcesCount: number, + namesCount: number, +) { + const n = mappings.length; + let data = new Int32Array(1024); + let dataLength = 0; + let generatedLine = 0; + let generatedLineStart = 0; + let generatedColumn = 0; + let originalSource = 0; + let originalLine = 0; + let originalColumn = 0; + let originalName = 0; + let needToSortGeneratedColumns = false; + let i = 0; + + function decodeError(text: string) { + const error = `Invalid VLQ data at index ${i}: ${text}`; + showLoadingError( + `The "mappings" field of the imported source map contains invalid data. ${error}.`, + ); + throw new Error(error); + } - function decodeError(text) { - const error = `Invalid VLQ data at index ${i}: ${text}`; - showLoadingError(`The "mappings" field of the imported source map contains invalid data. ${error}.`); - throw new Error(error); + function decodeVLQ() { + let shift = 0; + let vlq = 0; + + // Scan over the input + while (true) { + // Read a byte + if (i >= mappings.length) + decodeError('Unexpected early end of mapping data'); + const c = mappings.charCodeAt(i); + if ((c & 0x7f) !== c) + decodeError( + `Invalid mapping character: ${JSON.stringify( + String.fromCharCode(c), + )}`, + ); + const index = vlqTable[c & 0x7f]; + if (index === 0xff) + decodeError( + `Invalid mapping character: ${JSON.stringify( + String.fromCharCode(c), + )}`, + ); + i++; + + // Decode the byte + vlq |= (index & 31) << shift; + shift += 5; + + // Stop if there's no continuation bit + if ((index & 32) === 0) break; } - function decodeVLQ() { - let shift = 0; - let vlq = 0; - - // Scan over the input - while (true) { - // Read a byte - if (i >= mappings.length) decodeError('Unexpected early end of mapping data'); - const c = mappings.charCodeAt(i); - if ((c & 0x7F) !== c) decodeError(`Invalid mapping character: ${JSON.stringify(String.fromCharCode(c))}`); - const index = vlqTable[c & 0x7F]; - if (index === 0xFF) decodeError(`Invalid mapping character: ${JSON.stringify(String.fromCharCode(c))}`); - i++; - - // Decode the byte - vlq |= (index & 31) << shift; - shift += 5; + // Recover the signed value + return vlq & 1 ? -(vlq >> 1) : vlq >> 1; + } - // Stop if there's no continuation bit - if ((index & 32) === 0) break; + while (i < n) { + let c = mappings.charCodeAt(i); + + // Handle a line break + if (c === 59 /* ; */) { + // The generated columns are very rarely out of order. In that case, + // sort them with insertion since they are very likely almost ordered. + if (needToSortGeneratedColumns) { + for (let j = generatedLineStart + 6; j < dataLength; j += 6) { + const genL = data[j]; + const genC = data[j + 1]; + const origS = data[j + 2]; + const origL = data[j + 3]; + const origC = data[j + 4]; + const origN = data[j + 5]; + let k = j - 6; + for (; k >= generatedLineStart && data[k + 1] > genC; k -= 6) { + data[k + 6] = data[k]; + data[k + 7] = data[k + 1]; + data[k + 8] = data[k + 2]; + data[k + 9] = data[k + 3]; + data[k + 10] = data[k + 4]; + data[k + 11] = data[k + 5]; + } + data[k + 6] = genL; + data[k + 7] = genC; + data[k + 8] = origS; + data[k + 9] = origL; + data[k + 10] = origC; + data[k + 11] = origN; + } } - // Recover the signed value - return vlq & 1 ? -(vlq >> 1) : vlq >> 1; + generatedLine++; + generatedColumn = 0; + generatedLineStart = dataLength; + needToSortGeneratedColumns = false; + i++; + continue; } - while (i < n) { - let c = mappings.charCodeAt(i); - - // Handle a line break - if (c === 59 /* ; */) { - // The generated columns are very rarely out of order. In that case, - // sort them with insertion since they are very likely almost ordered. - if (needToSortGeneratedColumns) { - for (let j = generatedLineStart + 6; j < dataLength; j += 6) { - const genL = data[j]; - const genC = data[j + 1]; - const origS = data[j + 2]; - const origL = data[j + 3]; - const origC = data[j + 4]; - const origN = data[j + 5]; - let k = j - 6; - for (; k >= generatedLineStart && data[k + 1] > genC; k -= 6) { - data[k + 6] = data[k]; - data[k + 7] = data[k + 1]; - data[k + 8] = data[k + 2]; - data[k + 9] = data[k + 3]; - data[k + 10] = data[k + 4]; - data[k + 11] = data[k + 5]; - } - data[k + 6] = genL; - data[k + 7] = genC; - data[k + 8] = origS; - data[k + 9] = origL; - data[k + 10] = origC; - data[k + 11] = origN; - } - } - - generatedLine++; - generatedColumn = 0; - generatedLineStart = dataLength; - needToSortGeneratedColumns = false; - i++; - continue; - } + // Ignore stray commas + if (c === 44 /* , */) { + i++; + continue; + } - // Ignore stray commas + // Read the generated column + const generatedColumnDelta = decodeVLQ(); + if (generatedColumnDelta < 0) needToSortGeneratedColumns = true; + generatedColumn += generatedColumnDelta; + if (generatedColumn < 0) + decodeError(`Invalid generated column: ${generatedColumn}`); + + // It's valid for a mapping to have 1, 4, or 5 variable-length fields + let isOriginalSourceMissing = true; + let isOriginalNameMissing = true; + if (i < n) { + c = mappings.charCodeAt(i); if (c === 44 /* , */) { i++; - continue; - } - - // Read the generated column - const generatedColumnDelta = decodeVLQ(); - if (generatedColumnDelta < 0) needToSortGeneratedColumns = true; - generatedColumn += generatedColumnDelta; - if (generatedColumn < 0) decodeError(`Invalid generated column: ${generatedColumn}`); - - // It's valid for a mapping to have 1, 4, or 5 variable-length fields - let isOriginalSourceMissing = true; - let isOriginalNameMissing = true; - if (i < n) { - c = mappings.charCodeAt(i); - if (c === 44 /* , */) { - i++; - } else if (c !== 59 /* ; */) { - isOriginalSourceMissing = false; - - // Read the original source - const originalSourceDelta = decodeVLQ(); - originalSource += originalSourceDelta; - if (originalSource < 0 || originalSource >= sourcesCount) decodeError(`Original source index ${originalSource} is invalid (there are ${sourcesCount} sources)`); - - // Read the original line - const originalLineDelta = decodeVLQ(); - originalLine += originalLineDelta; - if (originalLine < 0) decodeError(`Invalid original line: ${originalLine}`); - - // Read the original column - const originalColumnDelta = decodeVLQ(); - originalColumn += originalColumnDelta; - if (originalColumn < 0) decodeError(`Invalid original column: ${originalColumn}`); - - // Check for the optional name index - if (i < n) { - c = mappings.charCodeAt(i); - if (c === 44 /* , */) { - i++; - } else if (c !== 59 /* ; */) { - isOriginalNameMissing = false; - - // Read the optional name index - const originalNameDelta = decodeVLQ(); - originalName += originalNameDelta; - if (originalName < 0 || originalName >= namesCount) decodeError(`Original name index ${originalName} is invalid (there are ${namesCount} names)`); - - // Handle the next character - if (i < n) { - c = mappings.charCodeAt(i); - if (c === 44 /* , */) { - i++; - } else if (c !== 59 /* ; */) { - decodeError(`Invalid character after mapping: ${JSON.stringify(String.fromCharCode(c))}`); - } + } else if (c !== 59 /* ; */) { + isOriginalSourceMissing = false; + + // Read the original source + const originalSourceDelta = decodeVLQ(); + originalSource += originalSourceDelta; + if (originalSource < 0 || originalSource >= sourcesCount) + decodeError( + `Original source index ${originalSource} is invalid (there are ${sourcesCount} sources)`, + ); + + // Read the original line + const originalLineDelta = decodeVLQ(); + originalLine += originalLineDelta; + if (originalLine < 0) + decodeError(`Invalid original line: ${originalLine}`); + + // Read the original column + const originalColumnDelta = decodeVLQ(); + originalColumn += originalColumnDelta; + if (originalColumn < 0) + decodeError(`Invalid original column: ${originalColumn}`); + + // Check for the optional name index + if (i < n) { + c = mappings.charCodeAt(i); + if (c === 44 /* , */) { + i++; + } else if (c !== 59 /* ; */) { + isOriginalNameMissing = false; + + // Read the optional name index + const originalNameDelta = decodeVLQ(); + originalName += originalNameDelta; + if (originalName < 0 || originalName >= namesCount) + decodeError( + `Original name index ${originalName} is invalid (there are ${namesCount} names)`, + ); + + // Handle the next character + if (i < n) { + c = mappings.charCodeAt(i); + if (c === 44 /* , */) { + i++; + } else if (c !== 59 /* ; */) { + decodeError( + `Invalid character after mapping: ${JSON.stringify( + String.fromCharCode(c), + )}`, + ); } } } } } - - // Append the mapping to the typed array - if (dataLength + 6 > data.length) { - const newData = new Int32Array(data.length << 1); - newData.set(data); - data = newData; - } - data[dataLength] = generatedLine; - data[dataLength + 1] = generatedColumn; - if (isOriginalSourceMissing) { - data[dataLength + 2] = -1; - data[dataLength + 3] = -1; - data[dataLength + 4] = -1; - } else { - data[dataLength + 2] = originalSource; - data[dataLength + 3] = originalLine; - data[dataLength + 4] = originalColumn; - } - data[dataLength + 5] = isOriginalNameMissing ? -1 : originalName; - dataLength += 6; } - return data.subarray(0, dataLength); + // Append the mapping to the typed array + if (dataLength + 6 > data.length) { + const newData = new Int32Array(data.length << 1); + newData.set(data); + data = newData; + } + data[dataLength] = generatedLine; + data[dataLength + 1] = generatedColumn; + if (isOriginalSourceMissing) { + data[dataLength + 2] = -1; + data[dataLength + 3] = -1; + data[dataLength + 4] = -1; + } else { + data[dataLength + 2] = originalSource; + data[dataLength + 3] = originalLine; + data[dataLength + 4] = originalColumn; + } + data[dataLength + 5] = isOriginalNameMissing ? -1 : originalName; + dataLength += 6; } - function generateInverseMappings(sources, data) { - let longestDataLength = 0; - - // Scatter the mappings to the individual sources - for (let i = 0, n = data.length; i < n; i += 6) { - const originalSource = data[i + 2]; - if (originalSource === -1) continue; + return data.subarray(0, dataLength); +} - const source = sources[originalSource]; - let inverseData = source.data; - let j = source.dataLength; +interface Source { + name: string; + content: string; + data: Int32Array; + dataLength: number; +} - // Append the mapping to the typed array - if (j + 6 > inverseData.length) { - const newLength = inverseData.length << 1; - const newData = new Int32Array(newLength > 1024 ? newLength : 1024); - newData.set(inverseData); - source.data = inverseData = newData; - } - inverseData[j] = data[i]; - inverseData[j + 1] = data[i + 1]; - inverseData[j + 2] = originalSource; - inverseData[j + 3] = data[i + 3]; - inverseData[j + 4] = data[i + 4]; - inverseData[j + 5] = data[i + 5]; - j += 6; - source.dataLength = j; - if (j > longestDataLength) longestDataLength = j; - } +function generateInverseMappings(sources: Source[], data: Int32Array) { + let longestDataLength = 0; - // Sort the mappings for each individual source - const temp = new Int32Array(longestDataLength); - for (const source of sources) { - const data = source.data.subarray(0, source.dataLength); - - // Sort lazily for performance - let isSorted = false; - Object.defineProperty(source, 'data', { - get() { - if (!isSorted) { - temp.set(data); - topDownSplitMerge(temp, 0, data.length, data); - isSorted = true; - } - return data; - }, - }) - } + // Scatter the mappings to the individual sources + for (let i = 0, n = data.length; i < n; i += 6) { + const originalSource = data[i + 2]; + if (originalSource === -1) continue; - // From: https://en.wikipedia.org/wiki/Merge_sort - function topDownSplitMerge(B, iBegin, iEnd, A) { - if (iEnd - iBegin <= 6) return; - - // Optimization: Don't do merge sort if it's already sorted - let isAlreadySorted = true; - for (let i = iBegin + 3, j = i + 6; j < iEnd; i = j, j += 6) { - // Compare mappings first by original line (index 3) and then by original column (index 4) - if (A[i] < A[j] || (A[i] === A[j] && A[i + 1] <= A[j + 1])) continue; - isAlreadySorted = false; - break; - } - if (isAlreadySorted) { - return; - } + const source = sources[originalSource]; + let inverseData = source.data; + let j = source.dataLength; - const iMiddle = ((iEnd / 6 + iBegin / 6) >> 1) * 6; - topDownSplitMerge(A, iBegin, iMiddle, B); - topDownSplitMerge(A, iMiddle, iEnd, B); - topDownMerge(B, iBegin, iMiddle, iEnd, A); + // Append the mapping to the typed array + if (j + 6 > inverseData.length) { + const newLength = inverseData.length << 1; + const newData = new Int32Array(newLength > 1024 ? newLength : 1024); + newData.set(inverseData); + source.data = inverseData = newData; } + inverseData[j] = data[i]; + inverseData[j + 1] = data[i + 1]; + inverseData[j + 2] = originalSource; + inverseData[j + 3] = data[i + 3]; + inverseData[j + 4] = data[i + 4]; + inverseData[j + 5] = data[i + 5]; + j += 6; + source.dataLength = j; + if (j > longestDataLength) longestDataLength = j; + } - // From: https://en.wikipedia.org/wiki/Merge_sort - function topDownMerge(A, iBegin, iMiddle, iEnd, B) { - let i = iBegin, j = iMiddle; - for (let k = iBegin; k < iEnd; k += 6) { - if (i < iMiddle && (j >= iEnd || - // Compare mappings first by original line (index 3) and then by original column (index 4) - A[i + 3] < A[j + 3] || - (A[i + 3] === A[j + 3] && A[i + 4] <= A[j + 4]) - )) { - B[k] = A[i]; - B[k + 1] = A[i + 1]; - B[k + 2] = A[i + 2]; - B[k + 3] = A[i + 3]; - B[k + 4] = A[i + 4]; - B[k + 5] = A[i + 5]; - i += 6; - } else { - B[k] = A[j]; - B[k + 1] = A[j + 1]; - B[k + 2] = A[j + 2]; - B[k + 3] = A[j + 3]; - B[k + 4] = A[j + 4]; - B[k + 5] = A[j + 5]; - j += 6; + // Sort the mappings for each individual source + const temp = new Int32Array(longestDataLength); + for (const source of sources) { + const data = source.data.subarray(0, source.dataLength); + + // Sort lazily for performance + let isSorted = false; + Object.defineProperty(source, 'data', { + get() { + if (!isSorted) { + temp.set(data); + topDownSplitMerge(temp, 0, data.length, data); + isSorted = true; } - } - } + return data; + }, + }); } - function parseSourceMap(json) { - try { - json = JSON.parse(json); - } catch (e) { - showLoadingError(`The imported source map contains invalid JSON data: ${e && e.message || e}`); - throw e; + // From: https://en.wikipedia.org/wiki/Merge_sort + function topDownSplitMerge( + B: Int32Array, + iBegin: number, + iEnd: number, + A: Int32Array, + ) { + if (iEnd - iBegin <= 6) return; + + // Optimization: Don't do merge sort if it's already sorted + let isAlreadySorted = true; + for (let i = iBegin + 3, j = i + 6; j < iEnd; i = j, j += 6) { + // Compare mappings first by original line (index 3) and then by original column (index 4) + if (A[i] < A[j] || (A[i] === A[j] && A[i + 1] <= A[j + 1])) continue; + isAlreadySorted = false; + break; } - - if (json.version !== 3) { - showLoadingError(`The imported source map is invalid. Expected the "version" field to contain the number 3.`); - throw new Error('Invalid source map'); + if (isAlreadySorted) { + return; } - if (json.sections instanceof Array) { - const sections = json.sections; - const decodedSections = []; - let totalDataLength = 0; + const iMiddle = ((iEnd / 6 + iBegin / 6) >> 1) * 6; + topDownSplitMerge(A, iBegin, iMiddle, B); + topDownSplitMerge(A, iMiddle, iEnd, B); + topDownMerge(B, iBegin, iMiddle, iEnd, A); + } - for (let i = 0; i < sections.length; i++) { - const { offset: { line, column }, map } = sections[i]; - if (typeof line !== 'number' || typeof column !== 'number') { - showLoadingError(`The imported source map is invalid. Expected the "offset" field for section ${i} to have a line and column.`); - throw new Error('Invalid source map'); - } + // From: https://en.wikipedia.org/wiki/Merge_sort + function topDownMerge( + A: Int32Array, + iBegin: number, + iMiddle: number, + iEnd: number, + B: Int32Array, + ) { + let i = iBegin, + j = iMiddle; + for (let k = iBegin; k < iEnd; k += 6) { + if ( + i < iMiddle && + (j >= iEnd || + // Compare mappings first by original line (index 3) and then by original column (index 4) + A[i + 3] < A[j + 3] || + (A[i + 3] === A[j + 3] && A[i + 4] <= A[j + 4])) + ) { + B[k] = A[i]; + B[k + 1] = A[i + 1]; + B[k + 2] = A[i + 2]; + B[k + 3] = A[i + 3]; + B[k + 4] = A[i + 4]; + B[k + 5] = A[i + 5]; + i += 6; + } else { + B[k] = A[j]; + B[k + 1] = A[j + 1]; + B[k + 2] = A[j + 2]; + B[k + 3] = A[j + 3]; + B[k + 4] = A[j + 4]; + B[k + 5] = A[j + 5]; + j += 6; + } + } + } +} - if (!map) { - showLoadingError(`The imported source map is unsupported. Section ${i} does not contain a "map" field.`); - throw new Error('Invalid source map'); - } +function parseSourceMap(raw: string) { + let json: RawIndexMap | RawSourceMap; + try { + json = JSON.parse(raw); + assert(json instanceof Object); + assert(!(json instanceof Array)); + } catch (e) { + assert(e instanceof Error); + showLoadingError( + `The imported source map contains invalid JSON data: ${(e && e.message) || e + }`, + ); + throw e; + } - if (map.version !== 3) { - showLoadingError(`The imported source map is invalid. Expected the "version" field for section ${i} to contain the number 3.`); - throw new Error('Invalid source map'); - } + if (json.version !== 3) { + showLoadingError( + `The imported source map is invalid. Expected the "version" field to contain the number 3.`, + ); + throw new Error('Invalid source map'); + } - if (!(map.sources instanceof Array) || map.sources.some(x => typeof x !== 'string')) { - showLoadingError(`The imported source map is invalid. Expected the "sources" field for section ${i} to be an array of strings.`); - throw new Error('Invalid source map'); - } + if ('sections' in json && json.sections instanceof Array) { + const sections = json.sections; + const decodedSections = []; + let totalDataLength = 0; + + for (let i = 0; i < sections.length; i++) { + const { + offset: { line, column }, + map, + } = sections[i]; + if (typeof line !== 'number' || typeof column !== 'number') { + showLoadingError( + `The imported source map is invalid. Expected the "offset" field for section ${i} to have a line and column.`, + ); + throw new Error('Invalid source map'); + } - if (typeof map.mappings !== 'string') { - showLoadingError(`The imported source map is invalid. Expected the "mappings" field for section ${i} to be a string.`); - throw new Error('Invalid source map'); - } + if (!map) { + showLoadingError( + `The imported source map is unsupported. Section ${i} does not contain a "map" field.`, + ); + throw new Error('Invalid source map'); + } - const { sources, sourcesContent, names, mappings } = map; - const emptyData = new Int32Array(0); - for (let i = 0; i < sources.length; i++) { - sources[i] = { - name: sources[i], - content: sourcesContent && sourcesContent[i] || '', - data: emptyData, - dataLength: 0, - }; - } + if (map.version !== 3) { + showLoadingError( + `The imported source map is invalid. Expected the "version" field for section ${i} to contain the number 3.`, + ); + throw new Error('Invalid source map'); + } - const data = decodeMappings(mappings, sources.length, names ? names.length : 0); - decodedSections.push({ offset: { line, column }, sources, names, data }); - totalDataLength += data.length; + if ( + !(map.sources instanceof Array) || + map.sources.some(x => typeof x !== 'string') + ) { + showLoadingError( + `The imported source map is invalid. Expected the "sources" field for section ${i} to be an array of strings.`, + ); + throw new Error('Invalid source map'); } - decodedSections.sort((a, b) => { - if (a.offset.line < b.offset.line) return -1; - if (a.offset.line > b.offset.line) return 1; - if (a.offset.column < b.offset.column) return -1; - if (a.offset.column > b.offset.column) return 1; - return 0; - }); + if (typeof map.mappings !== 'string') { + showLoadingError( + `The imported source map is invalid. Expected the "mappings" field for section ${i} to be a string.`, + ); + throw new Error('Invalid source map'); + } - const mergedData = new Int32Array(totalDataLength); - const mergedSources = []; - const mergedNames = []; - let dataOffset = 0; + const { sourcesContent, names, mappings } = map; + let sources: Source[] = []; + const emptyData = new Int32Array(0); + for (let i = 0; i < map.sources.length; i++) { + sources[i] = { + name: map.sources[i], + content: (sourcesContent && sourcesContent[i]) || '', + data: emptyData, + dataLength: 0, + }; + } - for (const { offset: { line, column }, sources, names, data } of decodedSections) { - const sourcesOffset = mergedSources.length; - const nameOffset = mergedNames.length; + const data = decodeMappings( + mappings, + sources.length, + names ? names.length : 0, + ); + decodedSections.push({ offset: { line, column }, sources, names, data }); + totalDataLength += data.length; + } - for (let i = 0, n = data.length; i < n; i += 6) { - if (data[i] === 0) data[i + 1] += column; - data[i] += line; - if (data[i + 2] !== -1) data[i + 2] += sourcesOffset; - if (data[i + 5] !== -1) data[i + 5] += nameOffset; - } + decodedSections.sort((a, b) => { + if (a.offset.line < b.offset.line) return -1; + if (a.offset.line > b.offset.line) return 1; + if (a.offset.column < b.offset.column) return -1; + if (a.offset.column > b.offset.column) return 1; + return 0; + }); - mergedData.set(data, dataOffset); - for (const source of sources) mergedSources.push(source); - if (names) for (const name of names) mergedNames.push(name); - dataOffset += data.length; + const mergedData = new Int32Array(totalDataLength); + const mergedSources = []; + const mergedNames = []; + let dataOffset = 0; + + for (const { + offset: { line, column }, + sources, + names, + data, + } of decodedSections) { + const sourcesOffset = mergedSources.length; + const nameOffset = mergedNames.length; + + for (let i = 0, n = data.length; i < n; i += 6) { + if (data[i] === 0) data[i + 1] += column; + data[i] += line; + if (data[i + 2] !== -1) data[i + 2] += sourcesOffset; + if (data[i + 5] !== -1) data[i + 5] += nameOffset; } - generateInverseMappings(mergedSources, mergedData); - return { - sources: mergedSources, - names: mergedNames, - data: mergedData, - }; + mergedData.set(data, dataOffset); + for (const source of sources) mergedSources.push(source); + if (names) for (const name of names) mergedNames.push(name); + dataOffset += data.length; } - if (!(json.sources instanceof Array) || json.sources.some(x => typeof x !== 'string')) { - showLoadingError(`The imported source map is invalid. Expected the "sources" field to be an array of strings.`); - throw new Error('Invalid source map'); - } + generateInverseMappings(mergedSources, mergedData); + return { + sources: mergedSources, + names: mergedNames, + data: mergedData, + }; + } - if (typeof json.mappings !== 'string') { - showLoadingError(`The imported source map is invalid. Expected the "mappings" field to be a string.`); - throw new Error('Invalid source map'); - } + if ( + !('sources' in json) || + !(json.sources instanceof Array) || + json.sources.some(x => typeof x !== 'string') + ) { + showLoadingError( + `The imported source map is invalid. Expected the "sources" field to be an array of strings.`, + ); + throw new Error('Invalid source map'); + } - const { sources, sourcesContent, names, mappings } = json; - const emptyData = new Int32Array(0); - for (let i = 0; i < sources.length; i++) { - sources[i] = { - name: sources[i], - content: sourcesContent && sourcesContent[i] || '', - data: emptyData, - dataLength: 0, - }; - } + if (typeof json.mappings !== 'string') { + showLoadingError( + `The imported source map is invalid. Expected the "mappings" field to be a string.`, + ); + throw new Error('Invalid source map'); + } - const data = decodeMappings(mappings, sources.length, names ? names.length : 0); - generateInverseMappings(sources, data); - return { sources, names, data }; + const { sourcesContent, names, mappings } = json; + let sources: Source[] = []; + const emptyData = new Int32Array(0); + for (let i = 0; i < json.sources.length; i++) { + sources[i] = { + name: json.sources[i], + content: (sourcesContent && sourcesContent[i]) || '', + data: emptyData, + dataLength: 0, + }; } - const toolbarHeight = 32; - const statusBarHeight = 32; + const data = decodeMappings( + mappings, + sources.length, + names ? names.length : 0, + ); + generateInverseMappings(sources, data); + return { sources, names, data }; +} - function waitForDOM() { - return new Promise(r => setTimeout(r, 1)); - } +const toolbarHeight = 32; +const statusBarHeight = 32; - async function finishLoading(code, map) { - const startTime = Date.now(); - promptText.style.display = 'none'; - toolbar.style.display = 'flex'; - statusBar.style.display = 'flex'; - canvas.style.display = 'block'; - originalStatus.textContent = generatedStatus.textContent = ''; - fileList.innerHTML = ''; - const option = document.createElement('option'); - option.textContent = `Loading...`; - fileList.appendChild(option); - fileList.disabled = true; - fileList.selectedIndex = 0; - originalTextArea = generatedTextArea = hover = null; - isInvalid = true; - updateHash(code, map); - - // Let the browser update before parsing the source map, which may be slow - await waitForDOM(); - const sm = parseSourceMap(map); - - // Show a progress bar if this is is going to take a while - let charsSoFar = 0; - let progressCalls = 0; - let isProgressVisible = false; - const progressStart = Date.now(); - const totalChars = code.length + (sm.sources.length > 0 ? sm.sources[0].content.length : 0); - const progress = chars => { - charsSoFar += chars; - if (!isProgressVisible && progressCalls++ > 2 && charsSoFar) { - const estimatedTimeLeftMS = (Date.now() - progressStart) / charsSoFar * (totalChars - charsSoFar); - if (estimatedTimeLeftMS > 250) { - progressBarOverlay.style.display = 'block'; - isProgressVisible = true; - } - } - if (isProgressVisible) { - progressBar.style.transform = `scaleX(${charsSoFar / totalChars})`; - return waitForDOM(); +function waitForDOM() { + return new Promise(r => setTimeout(r, 1)); +} + +async function finishLoading(code: string, map: string) { + const startTime = Date.now(); + promptText.style.display = 'none'; + toolbar.style.display = 'flex'; + statusBar.style.display = 'flex'; + canvas.style.display = 'block'; + originalStatus.textContent = generatedStatus.textContent = ''; + fileList.innerHTML = ''; + const option = document.createElement('option'); + option.textContent = `Loading...`; + fileList.appendChild(option); + fileList.disabled = true; + fileList.selectedIndex = 0; + originalTextArea = generatedTextArea = hover = null; + isInvalid = true; + updateHash(code, map); + + // Let the browser update before parsing the source map, which may be slow + await waitForDOM(); + const sm = parseSourceMap(map); + + // Show a progress bar if this is is going to take a while + let charsSoFar = 0; + let progressCalls = 0; + let isProgressVisible = false; + const progressStart = Date.now(); + const totalChars = + code.length + (sm.sources.length > 0 ? sm.sources[0].content.length : 0); + const progress = (chars: number) => { + charsSoFar += chars; + if (!isProgressVisible && progressCalls++ > 2 && charsSoFar) { + const estimatedTimeLeftMS = + ((Date.now() - progressStart) / charsSoFar) * (totalChars - charsSoFar); + if (estimatedTimeLeftMS > 250) { + progressBarOverlay.style.display = 'block'; + isProgressVisible = true; } - }; - progressBar.style.transform = `scaleX(0)`; - - // Update the original text area when the source changes - const otherSource = index => index === -1 ? null : sm.sources[index].name; - const originalName = index => sm.names[index]; - let finalOriginalTextArea = null; - if (sm.sources.length > 0) { - const updateOriginalSource = (sourceIndex, progress) => { - const source = sm.sources[sourceIndex]; - return createTextArea({ - sourceIndex, - text: source.content, - progress, - mappings: source.data, - mappingsOffset: 3, - otherSource, - originalName, - bounds() { - return { - x: 0, - y: toolbarHeight, - width: (innerWidth >>> 1) - (splitterWidth >> 1), - height: innerHeight - toolbarHeight - statusBarHeight, - }; - }, - }); - }; - fileList.onchange = async () => { - originalTextArea = await updateOriginalSource(fileList.selectedIndex); - isInvalid = true; - }; - finalOriginalTextArea = await updateOriginalSource(0, progress); } + if (isProgressVisible) { + progressBar.style.transform = `scaleX(${charsSoFar / totalChars})`; + return waitForDOM(); + } + }; + progressBar.style.transform = `scaleX(0)`; + + // Update the original text area when the source changes + const otherSource = (index: number) => + index === -1 ? null : sm.sources[index].name; + const originalName = (index: number) => sm.names[index]; + let finalOriginalTextArea = null; + if (sm.sources.length > 0) { + const updateOriginalSource = ( + sourceIndex: number, + progress?: ProgressCallback, + ) => { + const source = sm.sources[sourceIndex]; + return createTextArea({ + sourceIndex, + text: source.content, + progress, + mappings: source.data, + mappingsOffset: 3, + otherSource, + originalName, + bounds() { + return { + x: 0, + y: toolbarHeight, + width: (innerWidth >>> 1) - (splitterWidth >> 1), + height: innerHeight - toolbarHeight - statusBarHeight, + }; + }, + }); + }; + fileList.onchange = async () => { + originalTextArea = await updateOriginalSource(fileList.selectedIndex); + isInvalid = true; + }; + finalOriginalTextArea = await updateOriginalSource(0, progress); + } - generatedTextArea = await createTextArea({ - sourceIndex: null, - text: code, - progress, - mappings: sm.data, - mappingsOffset: 0, - otherSource, - originalName, - bounds() { - const x = (innerWidth >> 1) + ((splitterWidth + 1) >> 1); - return { - x, - y: toolbarHeight, - width: innerWidth - x, - height: innerHeight - toolbarHeight - statusBarHeight, - }; - }, - }); + generatedTextArea = await createTextArea({ + sourceIndex: null, + text: code, + progress, + mappings: sm.data, + mappingsOffset: 0, + otherSource, + originalName, + bounds() { + const x = (innerWidth >> 1) + ((splitterWidth + 1) >> 1); + return { + x, + y: toolbarHeight, + width: innerWidth - x, + height: innerHeight - toolbarHeight - statusBarHeight, + }; + }, + }); - // Only render the original text area once the generated text area is ready - originalTextArea = finalOriginalTextArea; - isInvalid = true; + // Only render the original text area once the generated text area is ready + originalTextArea = finalOriginalTextArea; + isInvalid = true; - // Populate the file picker once there will be no more await points - fileList.innerHTML = ''; - if (sm.sources.length > 0) { - for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) { - const option = document.createElement('option'); - option.textContent = `${i}: ${sources[i].name}`; - fileList.appendChild(option); - } - fileList.disabled = false; - } else { + // Populate the file picker once there will be no more await points + fileList.innerHTML = ''; + if (sm.sources.length > 0) { + for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) { const option = document.createElement('option'); - option.textContent = `(no original code)`; + option.textContent = `${i}: ${sources[i].name}`; fileList.appendChild(option); } - fileList.selectedIndex = 0; - - if (isProgressVisible) progressBarOverlay.style.display = 'none'; - const endTime = Date.now(); - console.log(`Finished loading in ${endTime - startTime}ms`); + fileList.disabled = false; + } else { + const option = document.createElement('option'); + option.textContent = `(no original code)`; + fileList.appendChild(option); } + fileList.selectedIndex = 0; - //////////////////////////////////////////////////////////////////////////////// - // Drawing - - const originalLineColors = [ - 'rgba(25, 133, 255, 0.3)', // Blue - 'rgba(174, 97, 174, 0.3)', // Purple - 'rgba(255, 97, 106, 0.3)', // Red - 'rgba(250, 192, 61, 0.3)', // Yellow - 'rgba(115, 192, 88, 0.3)', // Green - ]; - - // Use a striped pattern for bad mappings (good mappings are solid) - const patternContours = [ - [0, 24, 24, 0, 12, 0, 0, 12, 0, 24], - [0, 28, 28, 0, 40, 0, 0, 40, 0, 28], - [0, 44, 44, 0, 56, 0, 0, 56, 0, 44], - [12, 64, 24, 64, 64, 24, 64, 12, 12, 64], - [0, 60, 0, 64, 8, 64, 64, 8, 64, 0, 60, 0, 0, 60], - [28, 64, 40, 64, 64, 40, 64, 28, 28, 64], - [0, 8, 8, 0, 0, 0, 0, 8], - [44, 64, 56, 64, 64, 56, 64, 44, 44, 64], - [64, 64, 64, 60, 60, 64, 64, 64], - ]; - const badMappingPatterns = originalLineColors.map(color => { - let patternCanvas = document.createElement('canvas'); - let patternContext = patternCanvas.getContext('2d'); - let ratio, scale, pattern; - return (dx, dy) => { - if (devicePixelRatio !== ratio) { - ratio = devicePixelRatio; - scale = Math.round(64 * ratio) / 64; - patternCanvas.width = patternCanvas.height = Math.round(64 * scale); - patternContext.scale(scale, scale); - patternContext.beginPath(); - for (const contour of patternContours) { - for (let i = 0; i < contour.length; i += 2) { - if (i === 0) patternContext.moveTo(contour[i], contour[i + 1]); - else patternContext.lineTo(contour[i], contour[i + 1]); - } + if (isProgressVisible) progressBarOverlay.style.display = 'none'; + const endTime = Date.now(); + console.log(`Finished loading in ${endTime - startTime}ms`); +} + +//////////////////////////////////////////////////////////////////////////////// +// Drawing +interface Mapping { + generatedLine: number; + generatedColumn: number; + originalSource: number; + originalLine: number; + originalColumn: number; + originalName: number; +} +interface Hover { + sourceIndex: number | null; + lineIndex: number; + row: number; + column: number; + index: number; + mapping?: Mapping; +} +interface Bounds { + x: number; + y: number; + width: number; + height: number; +} +type ProgressCallback = (chars: number) => Promisable; +interface TextArea { + sourceIndex: number | null; + updateAfterWrapChange(): void; + getHoverRect(): [x: number, y: number, w: number, h: number] | null; + bounds(): Bounds; + draw(bodyStyle: CSSStyleDeclaration): void; + scrollTo(column: number, line: number): void; + onmousemove(e: MouseEvent): void; + onmousedown(e: MouseEvent): void; + onwheel(e: WheelEvent): void; +} + +const originalLineColors = [ + 'rgba(25, 133, 255, 0.3)', // Blue + 'rgba(174, 97, 174, 0.3)', // Purple + 'rgba(255, 97, 106, 0.3)', // Red + 'rgba(250, 192, 61, 0.3)', // Yellow + 'rgba(115, 192, 88, 0.3)', // Green +]; + +// Use a striped pattern for bad mappings (good mappings are solid) +const patternContours = [ + [0, 24, 24, 0, 12, 0, 0, 12, 0, 24], + [0, 28, 28, 0, 40, 0, 0, 40, 0, 28], + [0, 44, 44, 0, 56, 0, 0, 56, 0, 44], + [12, 64, 24, 64, 64, 24, 64, 12, 12, 64], + [0, 60, 0, 64, 8, 64, 64, 8, 64, 0, 60, 0, 0, 60], + [28, 64, 40, 64, 64, 40, 64, 28, 28, 64], + [0, 8, 8, 0, 0, 0, 0, 8], + [44, 64, 56, 64, 64, 56, 64, 44, 44, 64], + [64, 64, 64, 60, 60, 64, 64, 64], +]; +const badMappingPatterns = originalLineColors.map(color => { + let patternCanvas = document.createElement('canvas'); + let patternContext = patternCanvas.getContext('2d'); + assert(patternContext); + let ratio: number, scale: number, pattern: CanvasPattern; + return (dx: number, dy: number) => { + if (devicePixelRatio !== ratio) { + ratio = devicePixelRatio; + scale = Math.round(64 * ratio) / 64; + patternCanvas.width = patternCanvas.height = Math.round(64 * scale); + patternContext.scale(scale, scale); + patternContext.beginPath(); + for (const contour of patternContours) { + for (let i = 0; i < contour.length; i += 2) { + if (i === 0) patternContext.moveTo(contour[i], contour[i + 1]); + else patternContext.lineTo(contour[i], contour[i + 1]); } - patternContext.fillStyle = color.replace(' 0.3)', ' 0.2)'); - patternContext.fill(); - pattern = c.createPattern(patternCanvas, 'repeat'); } - pattern.setTransform(new DOMMatrix([1 / scale, 0, 0, 1 / scale, dx, dy])); - return pattern; - }; - }); + patternContext.fillStyle = color.replace(' 0.3)', ' 0.2)'); + patternContext.fill(); + pattern = $assert(c.createPattern(patternCanvas, 'repeat')); + } + pattern.setTransform(new DOMMatrix([1 / scale, 0, 0, 1 / scale, dx, dy])); + return pattern; + }; +}); + +const canvas = document.createElement('canvas'); +const c = $assert(canvas.getContext('2d')); +const monospaceFont = '14px monospace'; +const rowHeight = 21; +const splitterWidth = 6; +const margin = 64; +let isInvalid = true; +let originalTextArea: TextArea | null = null; +let generatedTextArea: TextArea | null = null; +let hover: Hover | null = null; +let highlighted: Record< + 'sourceIndex' | 'startIndex' | 'startColumn' | 'endIndex' | 'endColumn', + number +> | null = { + sourceIndex: 0, + startIndex: 5, + startColumn: 5, + endIndex: 7, + endColumn: 11, +}; - const canvas = document.createElement('canvas'); - const c = canvas.getContext('2d'); - const monospaceFont = '14px monospace'; - const rowHeight = 21; - const splitterWidth = 6; - const margin = 64; - let isInvalid = true; - let originalTextArea; - let generatedTextArea; - let hover = null; - - const wrapCheckbox = document.getElementById('wrap'); - let wrap = true; +const wrapCheckbox = $('input#wrap'); +let wrap = true; +try { + wrap = localStorage.getItem('wrap') !== 'false'; +} catch (e) { } +wrapCheckbox.checked = wrap; +wrapCheckbox.onchange = () => { + wrap = wrapCheckbox.checked; try { - wrap = localStorage.getItem('wrap') !== 'false'; - } catch (e) { - } - wrapCheckbox.checked = wrap; - wrapCheckbox.onchange = () => { - wrap = wrapCheckbox.checked; - try { - localStorage.setItem('wrap', wrap); - } catch (e) { + localStorage.setItem('wrap', String(wrap)); + } catch (e) { } + if (originalTextArea) originalTextArea.updateAfterWrapChange(); + if (generatedTextArea) generatedTextArea.updateAfterWrapChange(); + isInvalid = true; +}; + +interface Line { + raw: string; + runBase: number; + runCount: number; + runText: Record; + endIndex: number; + endColumn: number; +} + +async function splitTextIntoLinesAndRuns( + text: string, + progress?: ProgressCallback, +) { + c.font = monospaceFont; + const spaceWidth = c.measureText(' ').width; + const spacesPerTab = 2; + const parts = text.split(/(\r\n|\r|\n)/g); + const unicodeWidthCache = new Map(); + const lines: Line[] = []; + const progressChunkSize = 1 << 20; + let longestColumnForLine = new Int32Array(1024); + let runData = new Int32Array(1024); + let runDataLength = 0; + let prevProgressPoint = 0; + let longestLineInColumns = 0; + let lineStartOffset = 0; + + for (let part = 0; part < parts.length; part++) { + let raw = parts[part]; + if (part & 1) { + // Accumulate the length of the newline (CRLF uses two code units) + lineStartOffset += raw.length; + continue; } - if (originalTextArea) originalTextArea.updateAfterWrapChange(); - if (generatedTextArea) generatedTextArea.updateAfterWrapChange(); - isInvalid = true; - }; - async function splitTextIntoLinesAndRuns(text, progress) { - c.font = monospaceFont; - const spaceWidth = c.measureText(' ').width; - const spacesPerTab = 2; - const parts = text.split(/(\r\n|\r|\n)/g); - const unicodeWidthCache = new Map(); - const lines = []; - const progressChunkSize = 1 << 20; - let longestColumnForLine = new Int32Array(1024); - let runData = new Int32Array(1024); - let runDataLength = 0; - let prevProgressPoint = 0; - let longestLineInColumns = 0; - let lineStartOffset = 0; - - for (let part = 0; part < parts.length; part++) { - let raw = parts[part]; - if (part & 1) { - // Accumulate the length of the newline (CRLF uses two code units) - lineStartOffset += raw.length; - continue; - } + const runBase = runDataLength; + const n = raw.length + 1; // Add 1 for the extra character at the end + let nextProgressPoint = progress + ? prevProgressPoint + progressChunkSize - lineStartOffset + : Infinity; + let i = 0; + let column = 0; - const runBase = runDataLength; - const n = raw.length + 1; // Add 1 for the extra character at the end - let nextProgressPoint = progress ? prevProgressPoint + progressChunkSize - lineStartOffset : Infinity; - let i = 0; - let column = 0; + while (i < n) { + let startIndex = i; + let startColumn = column; + let whitespace = 0; + let isSingleChunk = false; + + // Update the progress bar occasionally + if (i > nextProgressPoint) { + assert(progress); + await progress(lineStartOffset + i - prevProgressPoint); + prevProgressPoint = lineStartOffset + i; + nextProgressPoint = i + progressChunkSize; + } while (i < n) { - let startIndex = i; - let startColumn = column; - let whitespace = 0; - let isSingleChunk = false; - - // Update the progress bar occasionally - if (i > nextProgressPoint) { - await progress(lineStartOffset + i - prevProgressPoint); - prevProgressPoint = lineStartOffset + i; - nextProgressPoint = i + progressChunkSize; + let c1 = raw.charCodeAt(i); + let c2; + + // Draw each tab into its own run + if (c1 === 0x09 /* tab */) { + if (i > startIndex) break; + isSingleChunk = true; + column += spacesPerTab; + column -= column % spacesPerTab; + i++; + whitespace = c1; + break; } - while (i < n) { - let c1 = raw.charCodeAt(i); - let c2; + // Draw each newline into its own run + if (c1 !== c1 /* end of line */) { + if (i > startIndex) break; + isSingleChunk = true; + column++; + i++; + whitespace = 0x0a /* newline */; + break; + } - // Draw each tab into its own run - if (c1 === 0x09 /* tab */) { - if (i > startIndex) break; - isSingleChunk = true; - column += spacesPerTab; - column -= column % spacesPerTab; - i++; - whitespace = c1; - break; - } + // Draw each non-ASCII character into its own run (e.g. emoji) + if (c1 < 0x20 || c1 > 0x7e) { + if (i > startIndex) break; + isSingleChunk = true; + i++; - // Draw each newline into its own run - if (c1 !== c1 /* end of line */) { - if (i > startIndex) break; - isSingleChunk = true; - column++; + // Consume another code unit if this code unit is a high surrogate + // and the next code point is a low surrogate. This handles code + // points that span two UTF-16 code units. + if ( + i < n && + c1 >= 0xd800 && + c1 <= 0xdbff && + (c2 = raw.charCodeAt(i)) >= 0xdc00 && + c2 <= 0xdfff + ) { i++; - whitespace = 0x0A /* newline */; - break; } - // Draw each non-ASCII character into its own run (e.g. emoji) - if (c1 < 0x20 || c1 > 0x7E) { - if (i > startIndex) break; - isSingleChunk = true; - i++; + // This contains some logic to handle more complex emoji such as "👯‍♂️" + // which is [U+1F46F, U+200D, U+2642, U+FE0F]. + while (i < n) { + c1 = raw.charCodeAt(i); - // Consume another code unit if this code unit is a high surrogate - // and the next code point is a low surrogate. This handles code - // points that span two UTF-16 code units. - if (i < n && c1 >= 0xD800 && c1 <= 0xDBFF && (c2 = raw.charCodeAt(i)) >= 0xDC00 && c2 <= 0xDFFF) { + // Consume another code unit if the next code point is a variation selector + if ((c1 & ~0xf) === 0xfe00) { i++; } - // This contains some logic to handle more complex emoji such as "👯‍♂️" - // which is [U+1F46F, U+200D, U+2642, U+FE0F]. - while (i < n) { - c1 = raw.charCodeAt(i); - - // Consume another code unit if the next code point is a variation selector - if ((c1 & ~0xF) === 0xFE00) { - i++; - } + // Consume another code unit if the next code point is a skin tone modifier + else if ( + c1 === 0xd83c && + i + 1 < n && + (c2 = raw.charCodeAt(i + 1)) >= 0xdffb && + c2 <= 0xdfff + ) { + i += 2; + } - // Consume another code unit if the next code point is a skin tone modifier - else if (c1 === 0xD83C && i + 1 < n && (c2 = raw.charCodeAt(i + 1)) >= 0xDFFB && c2 <= 0xDFFF) { - i += 2; - } + // Consume another code unit and stop if the next code point is a zero-width non-joiner + else if (c1 === 0x200c) { + i++; + break; + } - // Consume another code unit and stop if the next code point is a zero-width non-joiner - else if (c1 === 0x200C) { - i++; - break; - } + // Consume another code unit if the next code point is a zero-width joiner + else if (c1 === 0x200d) { + i++; - // Consume another code unit if the next code point is a zero-width joiner - else if (c1 === 0x200D) { + // Consume the next code point that is "joined" to this one + if (i < n) { + c1 = raw.charCodeAt(i); i++; - - // Consume the next code point that is "joined" to this one - if (i < n) { - c1 = raw.charCodeAt(i); + if ( + c1 >= 0xd800 && + c1 <= 0xdbff && + i < n && + (c2 = raw.charCodeAt(i)) >= 0xdc00 && + c2 <= 0xdfff + ) { i++; - if (c1 >= 0xD800 && c1 <= 0xDBFF && i < n && (c2 = raw.charCodeAt(i)) >= 0xDC00 && c2 <= 0xDFFF) { - i++; - } } } - - else { - break; - } - } - - const key = raw.slice(startIndex, i); - let width = unicodeWidthCache.get(key); - if (width === void 0) { - width = Math.round(c.measureText(key).width / spaceWidth); - if (width < 1) width = 1; - unicodeWidthCache.set(key, width); + } else { + break; } - column += width; - break; } - // Draw runs of spaces in their own run - if (c1 === 0x20 /* space */) { - if (i === startIndex) whitespace = c1; - else if (!whitespace) break; - } else { - if (whitespace) break; + const key = raw.slice(startIndex, i); + let width = unicodeWidthCache.get(key); + if (width === void 0) { + width = Math.round(c.measureText(key).width / spaceWidth); + if (width < 1) width = 1; + unicodeWidthCache.set(key, width); } + column += width; + break; + } - column++; - i++; + // Draw runs of spaces in their own run + if (c1 === 0x20 /* space */) { + if (i === startIndex) whitespace = c1; + else if (!whitespace) break; + } else { + if (whitespace) break; } - // Append the run to the typed array - if (runDataLength + 5 > runData.length) { - const newData = new Int32Array(runData.length << 1); - newData.set(runData); - runData = newData; - } - runData[runDataLength] = whitespace | (isSingleChunk ? 0x100 /* isSingleChunk */ : 0); - runData[runDataLength + 1] = startIndex; - runData[runDataLength + 2] = i; - runData[runDataLength + 3] = startColumn; - runData[runDataLength + 4] = column; - runDataLength += 5; + column++; + i++; } - const lineIndex = lines.length; - if (lineIndex >= longestColumnForLine.length) { - const newData = new Int32Array(longestColumnForLine.length << 1); - newData.set(longestColumnForLine); - longestColumnForLine = newData; + // Append the run to the typed array + if (runDataLength + 5 > runData.length) { + const newData = new Int32Array(runData.length << 1); + newData.set(runData); + runData = newData; } - longestColumnForLine[lineIndex] = column; - - const runCount = (runDataLength - runBase) / 5; - lines.push({ raw, runBase, runCount, runText: {}, endIndex: i, endColumn: column }); - longestLineInColumns = Math.max(longestLineInColumns, column); - lineStartOffset += raw.length; + runData[runDataLength] = + whitespace | (isSingleChunk ? 0x100 /* isSingleChunk */ : 0); + runData[runDataLength + 1] = startIndex; + runData[runDataLength + 2] = i; + runData[runDataLength + 3] = startColumn; + runData[runDataLength + 4] = column; + runDataLength += 5; } - if (prevProgressPoint < text.length && progress) { - await progress(text.length - prevProgressPoint); + const lineIndex = lines.length; + if (lineIndex >= longestColumnForLine.length) { + const newData = new Int32Array(longestColumnForLine.length << 1); + newData.set(longestColumnForLine); + longestColumnForLine = newData; } - - return { lines, longestColumnForLine, longestLineInColumns, runData: runData.subarray(0, runDataLength) }; + longestColumnForLine[lineIndex] = column; + + const runCount = (runDataLength - runBase) / 5; + lines.push({ + raw, + runBase, + runCount, + runText: {}, + endIndex: i, + endColumn: column, + }); + longestLineInColumns = Math.max(longestLineInColumns, column); + lineStartOffset += raw.length; } - async function createTextArea({ sourceIndex, text, progress, mappings, mappingsOffset, otherSource, originalName, bounds }) { - const shadowWidth = 16; - const textPaddingX = 5; - const textPaddingY = 1; - const scrollbarThickness = 16; - const hoverBoxLineThickness = 2; - - // Runs are stored in a flat typed array to improve loading time - const run_whitespace = index => runData[index] & 0xFF; - const run_isSingleChunk = index => runData[index] & 0x100; - const run_startIndex = index => runData[index + 1]; - const run_endIndex = index => runData[index + 2]; - const run_startColumn = index => runData[index + 3]; - const run_endColumn = index => runData[index + 4]; - - let { lines, longestColumnForLine, longestLineInColumns, runData } = await splitTextIntoLinesAndRuns(text, progress); - let animate = null; - let lastLineIndex = lines.length - 1; - let scrollX = 0; - let scrollY = 0; - - // Source mappings may lie outside of the source code. This happens both - // when the source code is missing or when the source mappings are buggy. - // In these cases, we should extend the scroll area to allow the user to - // view these out-of-bounds source mappings. - for (let i = 0, n = mappings.length; i < n; i += 6) { - let line = mappings[i + mappingsOffset]; - let column = mappings[i + mappingsOffset + 1]; - if (line < lines.length) { - const { endIndex, endColumn } = lines[line]; - - // Take into account tabs tops and surrogate pairs - if (endColumn > column) { - column = endColumn; - } else if (column > endColumn) { - column = column - endIndex + endColumn; - } - } else if (line > lastLineIndex) { - lastLineIndex = line; - } - if (column > longestLineInColumns) { - longestLineInColumns = column; - } - if (line >= longestColumnForLine.length) { - const newData = new Int32Array(longestColumnForLine.length << 1); - newData.set(longestColumnForLine); - longestColumnForLine = newData; - } - longestColumnForLine[line] = column; - } + if (prevProgressPoint < text.length && progress) { + await progress(text.length - prevProgressPoint); + } - const wrappedRowsCache = new Map; + return { + lines, + longestColumnForLine, + longestLineInColumns, + runData: runData.subarray(0, runDataLength), + }; +} - function computeColumnsAcross(width, columnWidth) { - if (!wrap) return Infinity; - return Math.max(1, Math.floor((width - margin - textPaddingX - scrollbarThickness) / columnWidth)); - } +interface CreateTextAreaParams { + sourceIndex: number | null; + text: string; + progress?: ProgressCallback; + mappings: Int32Array; + mappingsOffset: number; + otherSource: (index: number) => string | null; + originalName: (index: number) => string; + bounds: () => Bounds; +} - function wrappedRowsForColumns(columnsAcross) { - let result = wrappedRowsCache.get(columnsAcross); - if (!result) { - result = new Int32Array(lastLineIndex + 2); - let rows = 0, n = lastLineIndex + 1; - if (columnsAcross === Infinity) { - for (let i = 0; i <= n; i++) { - result[i] = i; - } - } else { - for (let i = 0; i < n; i++) { - result[i] = rows; - rows += Math.ceil(longestColumnForLine[i] / columnsAcross) || 1; - } - result[n] = rows; - } - wrappedRowsCache.set(columnsAcross, result); +async function createTextArea({ + sourceIndex, + text, + progress, + mappings, + mappingsOffset, + otherSource, + originalName, + bounds, +}: CreateTextAreaParams): Promise