diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ec8a6633f..a5c10d30bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: run: pnpm run lint - name: Check formatting run: pnpm exec prettier . --check + # Ahead of the build because these are pure unit tests and take a second. + - name: Test + run: pnpm test # The docs pages under src/routes/docs/_compiled-docs are generated by the # build and are not committed, so type-checking before it would silently # skip every page compiled from markdown. diff --git a/package.json b/package.json index 4b193be89b..6811651935 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "lint": "markdownlint \"docs/**/*.md\" && cspell \"**/*.{md,ts,marko}\"", "prepare": "husky", "preview": "marko-run preview", + "test": "vitest run", "type-check": "marko-type-check" }, "dependencies": { @@ -82,6 +83,7 @@ "terser": "^5.49.0", "util": "./shim/util", "vite": "^8.1.5", + "vitest": "^4.1.10", "writable-dom": "^1.0.6" }, "packageManager": "pnpm@11.15.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc8b650495..12b26a8f53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,9 @@ importers: vite: specifier: ^8.1.5 version: 8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.1.2)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)) writable-dom: specifier: ^1.0.6 version: 1.0.6 @@ -1033,8 +1036,8 @@ packages: resolution: {integrity: sha512-BKSrjZ2vxtLWJgKeVYGw+75K2VsvRkKNz52hSojQnve21F65mPmDQxnME8H00niGLYJC40iNnNVYy5T6ptDTpA==} hasBin: true - '@marko/vite@6.1.8': - resolution: {integrity: sha512-ZWyI4EPRD8osWi4Gl0eXl5U4ZuiAs7qkYtAlih1zGnQfyIG9oUFaeNiiHZ8TP5q5qYvjyssvb4tExZB8yGiV0w==} + '@marko/vite@6.1.9': + resolution: {integrity: sha512-ozJjVNe6988rWuNwYJf5+aWFO9iPkz5I6nIz7iK8fCrJq8KEHd7InMdovGaeKFf+mb0VZBWEH9WjbNqUoBQZlg==} peerDependencies: '@marko/compiler': ^5 vite: ^8 @@ -1518,9 +1521,15 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -1555,6 +1564,35 @@ packages: '@ungap/structured-clone@1.3.3': resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -1589,6 +1627,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1641,6 +1683,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk-template@1.1.2: resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==} engines: {node: '>=14.16'} @@ -1721,6 +1767,9 @@ packages: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + crelt@1.0.7: resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} @@ -1929,6 +1978,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + esbuild-plugin-browserslist@2.0.0: resolution: {integrity: sha512-gm8EITyyfS3h5I+f/+6C+TFXI23PWi80vHtoccIA17GIoZDSSjHYZw+MINXrlQZ7DZ57myL0JAuDsKbUZlWQgw==} engines: {node: ^20.19.0 || ^22.12.0 || >23.0.0} @@ -1957,6 +2009,9 @@ packages: engines: {node: '>=4'} hasBin: true + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -1965,6 +2020,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fast-equals@6.0.2: resolution: {integrity: sha512-sAjhj9ZhOxYCGiNMnZLaucOqf5ZeFnHNoKoAZiD9thhJ0N8RP85qJK759/97C/3L7NzzmGVB5uiX9AUpySZmUQ==} engines: {node: '>=6.0.0'} @@ -2499,6 +2558,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2562,6 +2625,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3015,6 +3081,9 @@ packages: resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} engines: {node: '>=20'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sirv@1.0.19: resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==} engines: {node: '>= 10'} @@ -3049,6 +3118,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -3056,6 +3128,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3117,10 +3192,21 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3245,6 +3331,47 @@ packages: yaml: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} @@ -3257,6 +3384,11 @@ packages: warp10@2.1.0: resolution: {integrity: sha512-krhkqzJdUxAZv2Cx0Gz6dN1r7TTrG9RDewkDHBbJQIqbNTCdB5ZUHVh7VkA4DgrKW4ZXPPUQKCwmI/3btDse9A==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + writable-dom@1.0.6: resolution: {integrity: sha512-sky15kmZw2kQXP9n4GOCeO8orSI61A9z9l6NGrbHRiWiUJnUVjHcSTrVKa9Av3lF97eYy87efFCmTnIg1dgRUg==} @@ -4087,7 +4219,7 @@ snapshots: '@marko/run@0.11.6(@marko/compiler@5.41.11)(@types/node@26.1.2)(esbuild@0.27.7)(marko@6.3.28)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)': dependencies: '@marko/run-explorer': 2.0.4(@marko/run@0.11.6(@marko/compiler@5.41.11)(@types/node@26.1.2)(esbuild@0.27.7)(marko@6.3.28)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)) - '@marko/vite': 6.1.8(@marko/compiler@5.41.11)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)) + '@marko/vite': 6.1.9(@marko/compiler@5.41.11)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)) '@remix-run/form-data-parser': 0.17.4 '@standard-schema/spec': 1.1.0 browserslist: 4.28.7 @@ -4131,7 +4263,7 @@ snapshots: kleur: 4.1.5 typescript: 6.0.3 - '@marko/vite@6.1.8(@marko/compiler@5.41.11)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0))': + '@marko/vite@6.1.9(@marko/compiler@5.41.11)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0))': dependencies: '@chialab/estransform': 0.19.1 '@marko/compiler': 5.41.11 @@ -4483,10 +4615,17 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.9': {} '@types/flexsearch@0.7.42': @@ -4517,6 +4656,47 @@ snapshots: '@ungap/structured-clone@1.3.3': {} + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + acorn@8.17.0: {} ansi-regex@5.0.1: {} @@ -4538,6 +4718,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + async@3.2.6: {} autoprefixer@10.5.4(postcss@8.5.23): @@ -4581,6 +4763,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk-template@1.1.2: dependencies: chalk: 5.6.2 @@ -4661,6 +4845,8 @@ snapshots: transitivePeerDependencies: - supports-color + convert-source-map@2.0.0: {} + crelt@1.0.7: {} cspell-config-lib@10.0.1: @@ -4877,6 +5063,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} + esbuild-plugin-browserslist@2.0.0(browserslist@4.28.7)(esbuild@0.27.7)(supports-color@10.2.2): dependencies: browserslist: 4.28.7 @@ -4923,10 +5111,16 @@ snapshots: esprima@4.0.1: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + etag@1.8.1: {} events@3.3.0: {} + expect-type@1.4.0: {} + fast-equals@6.0.2: {} fast-glob@3.3.3: @@ -5525,6 +5719,8 @@ snapshots: normalize-path@3.0.0: {} + obug@2.1.4: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5596,6 +5792,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6121,6 +6319,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + siginfo@2.0.0: {} + sirv@1.0.19: dependencies: '@polka/url': 1.0.0-next.29 @@ -6154,10 +6354,14 @@ snapshots: space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} statuses@2.0.2: {} + std-env@4.2.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6215,11 +6419,17 @@ snapshots: tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyrainbow@3.1.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6312,6 +6522,33 @@ snapshots: terser: 5.49.0 yaml: 2.9.0 + vitest@4.1.10(@types/node@26.1.2)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.27.7)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.49.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.1.2 + transitivePeerDependencies: + - msw + vscode-languageserver-textdocument@1.0.12: {} vscode-uri@3.1.0: {} @@ -6320,6 +6557,11 @@ snapshots: warp10@2.1.0: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + writable-dom@1.0.6: {} xdg-basedir@5.1.0: {} diff --git a/src/routes/playground/tags/playground/tags/result/result.marko b/src/routes/playground/tags/playground/tags/result/result.marko index 0749cc2985..d6b45565b1 100644 --- a/src/routes/playground/tags/playground/tags/result/result.marko +++ b/src/routes/playground/tags/playground/tags/result/result.marko @@ -142,7 +142,7 @@ div class=styles.result div class=styles.output if=ws.buildErrors - error-output errors=ws.buildErrors kind="build" + error-output errors=ws.buildErrors files=(ws.fs?.files) kind="build" else if=outputType !== "preview" div ,id=styles.compiled @@ -158,7 +158,7 @@ div class=styles.result ) } else if=ws.runtimeErrors - error-output errors=ws.runtimeErrors kind="runtime" + error-output errors=ws.runtimeErrors files=(ws.fs?.files) kind="runtime" iframe/$frame ,title="code output" diff --git a/src/routes/playground/tags/playground/tags/result/tags/error-output/error-output.marko b/src/routes/playground/tags/playground/tags/result/tags/error-output/error-output.marko index f0775a6b84..867b756071 100644 --- a/src/routes/playground/tags/playground/tags/result/tags/error-output/error-output.marko +++ b/src/routes/playground/tags/playground/tags/result/tags/error-output/error-output.marko @@ -2,7 +2,7 @@ client import highlighter from "app/util/shiki"; import { styleToClass } from "app/util/style-to-class"; import { langForFile, - parseError, + normalizeErrors, parseInline, type CodeFrame, type Inline, @@ -10,6 +10,8 @@ import { import styles from "./error-output.style.module.scss"; export interface Input { errors: readonly unknown[]; + /** Workspace sources, so a frame can be cut from the file the error names. */ + files?: Record; kind: "build" | "runtime"; } @@ -75,8 +77,7 @@ define/Frame|{ frame }: { frame: CodeFrame }| } div class=styles.errors role="alert" - for|err| of=input.errors - const/parsed=parseError(err) + for|parsed| of=normalizeErrors(input.errors, input.files) section class=styles.error header class=styles.head span class=styles.badge -- diff --git a/src/util/compile-error.test.ts b/src/util/compile-error.test.ts new file mode 100644 index 0000000000..8b05aa4b0e --- /dev/null +++ b/src/util/compile-error.test.ts @@ -0,0 +1,121 @@ +import { expect, test } from "vitest"; + +import { attachErrorFile, normalizeErrors, parseError } from "./compile-error"; + +/** Shaped like the `CompileError` the Marko compiler raises. */ +function compileError( + label: string, + loc: { + start: { line: number; column: number }; + end?: { line: number; column: number }; + }, + message = "", +) { + return Object.assign(new Error(message), { + name: "CompileError", + label, + loc, + }); +} + +const FILE = "/app/tags/index.marko"; + +test("reads position and message off the error rather than its frame text", () => { + const files = { [FILE]: "
\n\n" }; + const err = attachErrorFile( + compileError('Missing ending "span" tag', { + start: { line: 2, column: 0 }, + end: { line: 2, column: 6 }, + }), + FILE, + ); + + const [parsed] = normalizeErrors([err], files); + expect(parsed.message).toBe('Missing ending "span" tag'); + expect(parsed.frame?.file).toBe("app/tags/index.marko"); + expect(parsed.frame?.line).toBe(2); + expect(parsed.frame?.col).toBe(1); + + const errorRow = parsed.frame?.rows.find((row) => row.isError); + expect(errorRow?.code).toBe(""); + expect(errorRow?.marker).toEqual({ start: 0, width: 6 }); +}); + +test("keeps the diagnostic when the source is indented with tabs", () => { + // The frame text Babel emits preserves tabs in the caret row, which the + // string parser could not match -- message and caret were both dropped. + const files = { [FILE]: "\t\t\n" }; + const err = attachErrorFile( + compileError("Line has extra indentation at the beginning", { + start: { line: 1, column: 2 }, + end: { line: 1, column: 2 }, + }), + FILE, + ); + + const [parsed] = normalizeErrors([err], files); + expect(parsed.message).toBe("Line has extra indentation at the beginning"); + + const errorRow = parsed.frame?.rows.find((row) => row.isError); + // Tabs are expanded, so the caret offset indexes the same grid as the code. + expect(errorRow?.code).toBe(" "); + expect(errorRow?.marker?.start).toBe(4); + expect(errorRow?.code[errorRow.marker!.start]).toBe("<"); +}); + +test("gives each error in an aggregate its own entry", () => { + const files = { [FILE]: "\n