diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index 43c1ffe..df25666 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -15,7 +15,7 @@ jobs: with: node-version-file: .nvmrc cache: npm - - run: npm ci && npm run build + - run: npm ci && npm run build:demo - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index d86a5f4..183561f 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -17,7 +17,7 @@ jobs: with: node-version-file: .nvmrc cache: npm - - run: npm ci && npm run build + - run: npm ci && npm run build:demo - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 73934c6..12084f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,5 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules -/.pnp -.pnp.js - -# testing -/coverage # production /build diff --git a/README.md b/README.md index 2a25c8f..377bb91 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,164 @@ -# GCode Preview 3.0 with React & TypeScript +# gcode-preview-react -This demo uses [GCode Preview](https://github.com/xyz-tools/gcode-preview) -`3.0.0-alpha.6` with a React 19 / TypeScript / Vite setup. +A React component for [GCode Preview](https://github.com/xyz-tools/gcode-preview): +a 3D preview of G-code files, rendered with three.js into a ``. It +creates and disposes the preview for you, loads a file from a URL or a string, +and keeps the preview in sync with its props. + +Works with React 18 and 19, and in the Next.js App Router (the component is +marked `'use client'`). + +## Install ```sh -npm install -npm start +npm install gcode-preview-react gcode-preview ``` -Open http://localhost:5173. `npm run build` typechecks and creates the -production build in `build/`, which `npm run preview` serves. `npm run lint` -checks the source and `npm test` runs the lifecycle regression tests (Vitest). +`gcode-preview` (`^3.0.0-alpha.6`) and `react` are peer dependencies. -## Component +With TypeScript, also install `@types/three` to get strict types for the +color props (the core's option types come from three.js). + +## Usage ```tsx -import GCodePreview from './components/GCodePreview'; - - +import { GCodePreview } from 'gcode-preview-react'; + +export function Viewer() { + return ( + console.warn(error.message)} + /> + ); +} ``` -The wrapper creates a `new GCodePreview(...)` after the canvas mounts and -passes a decoded fetch stream to `processGCodeStream`. Source or option changes -reload the preview. Layer ranges are applied through `preview.sceneManager` -after loading, when the complete layer count is known. Loading failures appear -below the canvas. Cleanup aborts fetch, removes the resize listener and calls -`preview.dispose()`, including during React Strict Mode remounts. +## Props + +### Loading + +| Prop | Type | Description | +| ------- | -------------------- | ---------------------------------------------------------- | +| `src` | `string \| URL` | URL of a G-code file. It is fetched and streamed in. | +| `gcode` | `string \| string[]` | G-code to process directly. If both are set, `gcode` wins. | + +Changing `src` or `gcode` clears the preview and starts a new load; a load +that is still running is aborted. Unsetting both clears the preview. Pass a +stable array for `gcode` (for example from state or `useMemo`), since a new +array starts a new load. + +### Preview options + +Every option of the core `GCodePreview` (except `canvas`) is a prop, with the +type from `gcode-preview`. See the +[core documentation](https://gcode-preview.web.app/docs) for what each one +does. An option you don't pass is not passed to the core either, so the core +default applies. + +**Live options** update the preview in place when the prop changes: + +`buildVolume`, `backgroundColor`, `extrusionColor`, `travelColor`, +`topLayerColor`, `lastSegmentColor`, `boundingBoxColor`, `startLayer`, +`endLayer`, `renderExtrusion`, `renderTravel`, `renderTubes`, `lineWidth`, +`lineHeight`, `extrusionWidth`, `disableGradient`, `orthographic`, `devMode` + +Only options whose value changed are applied. Objects and arrays such as +`buildVolume` and `extrusionColor` are compared one level deep, so passing +them inline is fine. `startLayer` and `endLayer` are applied again after each +load, because the core clamps them to the loaded layer count. + +Changing a live option back to `undefined` resets it for `startLayer`, +`endLayer`, `topLayerColor`, `lastSegmentColor`, `boundingBoxColor`, +`buildVolume`, `lineHeight`, `extrusionWidth` and `devMode`. The other live +options keep their last value, because their core setters don't accept +`undefined`. + +**Mount-only options** are read once, when the preview is created. Later +changes are ignored; remount the component (for example with a `key`) to +apply them: + +`initialCameraPosition`, `minLayerThreshold`, `droppable`, `keepLines`, +`liveRenderInterval`, `arcChordTolerance` + +### Canvas attributes + +Any other canvas attribute (`className`, `style`, `id`, `tabIndex`, `aria-*`, +event handlers, ...) is passed to the ``. It has +`aria-label="G-code preview"` unless you pass your own. + +## Callbacks + +| Prop | Called with | When | +| --------- | --------------------------- | ----------------------------------------------- | +| `onReady` | the `GCodePreview` instance | the preview has been created | +| `onLoad` | the `GCodePreview` instance | `src` or `gcode` has been loaded and rendered | +| `onError` | an `Error` | loading failed, e.g. `HTTP 404: /missing.gcode` | + +Without an `onError` prop, load errors are logged with `console.error`. A load +that was replaced or whose component unmounted calls nothing. The latest +callbacks are always used, so inline functions don't recreate the preview. + +## Instance access + +The `ref` gives you the core `GCodePreview` instance, for anything the props +don't cover. It is set after mount (use it in an effect, an event handler or +`onReady`), and `null` after unmount. + +```tsx +import type { GCodePreview as Preview } from 'gcode-preview'; +import { GCodePreview } from 'gcode-preview-react'; +import { useRef } from 'react'; + +function Viewer() { + const preview = useRef(null); + return ( + <> + + + + ); +} +``` + +## Sizing + +The component renders a bare `` without any size or stylesheet: size +the canvas with CSS (`className` or `style`). The preview follows the canvas +size through a `ResizeObserver`. + +## Development + +```sh +npm install +npm run dev +``` + +`npm run dev` starts the demo app (`src/App.tsx`) at http://localhost:5173. The +demo imports the component from source (`src/lib`). `npm run build:demo` +typechecks and builds the demo into `build/`, which is deployed to Firebase +Hosting. + +| Script | What it does | +| ---------------------- | ---------------------------------------------- | +| `npm run build` | builds the library (`src/lib`) into `dist/` | +| `npm test` | runs the component tests (Vitest, core mocked) | +| `npm run typecheck` | typechecks the demo, tests and library | +| `npm run lint` | ESLint + Prettier | +| `npm run lint:package` | checks the package setup with publint | + +The package is marked `"private": true` so it can't be published by accident. +Remove that flag when it is ready to be published; `prepack` builds the +library first. + +The square and triangle towers in `public/` are small synthetic samples, not +printer-ready G-code. + +## License -The local square and triangle towers are small synthetic preview samples, not -printer-ready G-code. Put your own files in `public/` and change `src` to use them. +MIT diff --git a/eslint.config.js b/eslint.config.js index 20eb782..22020a9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ['build', 'coverage'] }, + { ignores: ['build', 'dist', 'coverage'] }, js.configs.recommended, tseslint.configs.recommended, react.configs.flat.recommended, diff --git a/index.html b/index.html index 1f32332..ac2f040 100644 --- a/index.html +++ b/index.html @@ -2,19 +2,11 @@ - - - - - - gcode-preview-react-typescript-demo + + GCode Preview React demo -
diff --git a/package-lock.json b/package-lock.json index 89fbbf4..6dc52a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,13 @@ { - "name": "react-typescript", - "version": "1.0.0", + "name": "gcode-preview-react", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "react-typescript", - "version": "1.0.0", - "dependencies": { - "gcode-preview": "3.0.0-alpha.6", - "react": "19.3.0", - "react-dom": "19.3.0", - "three": "0.185.0" - }, + "name": "gcode-preview-react", + "version": "0.1.0", + "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.5", "@types/react": "^19.3.0", @@ -24,69 +19,79 @@ "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", - "globals": "^17.12.0", - "jsdom": "^29.1.1", - "prettier": "^3.9.6", - "typescript": "^5.9.3", - "typescript-eslint": "^8.70.0", - "vite": "^8.3.0", - "vitest": "^5.0.0" + "gcode-preview": "3.0.0-alpha.6", + "globals": "^17.13.0", + "jsdom": "^30.1.2", + "prettier": "^3.9.9", + "publint": "^0.3.25", + "react": "19.3.0", + "react-dom": "19.3.0", + "typescript": "~6.0", + "typescript-eslint": "^8.71.0", + "vite": "^8.3.2", + "vitest": "^5.0.3" }, "engines": { "node": ">=22.12.0" + }, + "peerDependencies": { + "gcode-preview": "^3.0.0-alpha.6", + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", + "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-calc": "^3.4.2", + "@csstools/css-color-parser": "^4.2.5", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.4.tgz", + "integrity": "sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", + "bidi-js": "^1.1.0", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "20 || >=22" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -264,9 +269,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { @@ -341,9 +346,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", - "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", "dev": true, "funding": [ { @@ -361,9 +366,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", "dev": true, "funding": [ { @@ -380,14 +385,14 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", - "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", "dev": true, "funding": [ { @@ -401,21 +406,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.3.0" + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", "dev": true, "funding": [ { @@ -432,13 +437,13 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.13.tgz", - "integrity": "sha512-i9ZylF5QNhmNfPA9l0vHAWK4kPrbIp6g9lKgaiIFsIBz2F/WNB7OLrzlNNcCOm+h42bkaSD2v1PG+IBPHhc3ZA==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", "dev": true, "funding": [ { @@ -461,9 +466,9 @@ } }, "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", "dev": true, "funding": [ { @@ -645,9 +650,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", "dev": true, "license": "MIT", "engines": { @@ -779,9 +784,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.149.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", - "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", "dev": true, "license": "MIT", "funding": { @@ -801,10 +806,26 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@publint/pack": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.7.tgz", + "integrity": "sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyexec": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", - "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", "cpu": [ "arm" ], @@ -819,9 +840,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", - "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", "cpu": [ "arm64" ], @@ -836,9 +857,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", - "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", "cpu": [ "arm64" ], @@ -853,9 +874,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", - "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", "cpu": [ "x64" ], @@ -870,9 +891,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", - "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", "cpu": [ "x64" ], @@ -887,9 +908,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", - "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", "cpu": [ "arm" ], @@ -904,13 +925,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", - "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -921,13 +945,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", - "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -938,13 +965,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", - "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -955,13 +985,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", - "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -972,13 +1005,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", - "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -989,13 +1025,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", - "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1006,9 +1045,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", - "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", "cpu": [ "arm64" ], @@ -1023,9 +1062,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", - "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", "cpu": [ "arm64" ], @@ -1040,9 +1079,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", - "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", "cpu": [ "x64" ], @@ -1152,17 +1191,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", - "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/type-utils": "8.70.0", - "@typescript-eslint/utils": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1175,15 +1214,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.70.0", + "@typescript-eslint/parser": "^8.71.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", - "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, "license": "MIT", "engines": { @@ -1191,16 +1230,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", - "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3" }, "engines": { @@ -1216,14 +1255,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", - "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.70.0", - "@typescript-eslint/types": "^8.70.0", + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", "debug": "^4.4.3" }, "engines": { @@ -1238,14 +1277,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", - "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0" + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1256,9 +1295,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", - "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", "dev": true, "license": "MIT", "engines": { @@ -1273,15 +1312,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", - "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1298,9 +1337,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", - "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", "dev": true, "license": "MIT", "engines": { @@ -1312,16 +1351,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", - "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.70.0", - "@typescript-eslint/tsconfig-utils": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/visitor-keys": "8.70.0", + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1350,9 +1389,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1392,16 +1431,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", - "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.70.0", - "@typescript-eslint/types": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0" + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1416,13 +1455,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", - "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/types": "8.71.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1477,14 +1516,14 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", - "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.0", + "@vitest/spy": "5.0.3", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, @@ -1505,9 +1544,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", - "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", "dev": true, "license": "MIT", "funding": { @@ -1759,9 +1798,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.22", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", - "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1782,9 +1821,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1793,9 +1832,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", - "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -1813,11 +1852,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.20", - "caniuse-lite": "^1.0.30001810", - "electron-to-chromium": "^1.5.420", - "node-releases": "^2.0.54", - "update-browserslist-db": "^1.3.2" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -1887,9 +1926,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -1908,9 +1947,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -2005,17 +2044,17 @@ "license": "MIT" }, "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" + "whatwg-url": "^17.1.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/data-view-buffer": { @@ -2179,9 +2218,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.426", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.426.tgz", - "integrity": "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "dev": true, "license": "ISC" }, @@ -2885,6 +2924,7 @@ "version": "3.0.0-alpha.6", "resolved": "https://registry.npmjs.org/gcode-preview/-/gcode-preview-3.0.0-alpha.6.tgz", "integrity": "sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ==", + "dev": true, "license": "MIT", "dependencies": { "lil-gui": "^0.20.0", @@ -2982,9 +3022,9 @@ } }, "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", "dev": true, "license": "MIT", "engines": { @@ -3136,16 +3176,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "@exodus/bytes": "^1.15.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, "node_modules/ignore": { @@ -3285,13 +3325,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.3" + "hasown": "^2.0.4" }, "engines": { "node": ">= 0.4" @@ -3668,39 +3708,38 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.2.tgz", + "integrity": "sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^9.2.4", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", "css-tree": "^3.2.1", - "data-urls": "^7.0.0", + "data-urls": "^8.0.0", "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", + "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.3", "parse5": "^8.0.1", "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.11.2", + "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.2.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -3709,9 +3748,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3948,6 +3987,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3969,6 +4011,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3990,6 +4035,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4011,6 +4059,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4070,6 +4121,7 @@ "version": "0.20.0", "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.20.0.tgz", "integrity": "sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -4119,9 +4171,9 @@ } }, "node_modules/magic-string": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", - "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, "license": "MIT", "dependencies": { @@ -4165,6 +4217,16 @@ "node": "*" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4218,9 +4280,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.55", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", - "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, "license": "MIT", "engines": { @@ -4418,6 +4480,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4541,9 +4610,9 @@ } }, "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", "dev": true, "license": "MIT", "bin": { @@ -4581,6 +4650,28 @@ "react-is": "^16.13.1" } }, + "node_modules/publint": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.25.tgz", + "integrity": "sha512-WMMbn/d7JqRlfXQL/JUI4n5skC8cJ5WVr8/wT5mmiYWkUdENkZr1I5rgs2+dQPVRgoJd+JRYtB46MTsuDylw9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.7", + "package-manager-detector": "^1.8.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4595,6 +4686,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4604,6 +4696,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.28.0" @@ -4708,13 +4801,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", - "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.149.0", + "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4724,21 +4817,34 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.8", - "@rolldown/binding-android-arm64": "1.2.8", - "@rolldown/binding-darwin-arm64": "1.2.8", - "@rolldown/binding-darwin-x64": "1.2.8", - "@rolldown/binding-freebsd-x64": "1.2.8", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", - "@rolldown/binding-linux-arm64-gnu": "1.2.8", - "@rolldown/binding-linux-arm64-musl": "1.2.8", - "@rolldown/binding-linux-ppc64-gnu": "1.2.8", - "@rolldown/binding-linux-s390x-gnu": "1.2.8", - "@rolldown/binding-linux-x64-gnu": "1.2.8", - "@rolldown/binding-linux-x64-musl": "1.2.8", - "@rolldown/binding-openharmony-arm64": "1.2.8", - "@rolldown/binding-win32-arm64-msvc": "1.2.8", - "@rolldown/binding-win32-x64-msvc": "1.2.8" + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/safe-array-concat": { @@ -4813,6 +4919,7 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -4973,34 +5080,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -5156,13 +5249,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/synckit": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", @@ -5180,15 +5266,16 @@ } }, "node_modules/three": { - "version": "0.185.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.185.0.tgz", - "integrity": "sha512-+yRrcRO2iZa8uzvNNl0d7cL4huhgKgBvVJ0njcTe8xFqZ6DMAFZdCKDP91SEAuj25bNAj7k1QQdf+srZywVK6w==", + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "dev": true, "license": "MIT" }, "node_modules/tinybench": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", - "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", "dev": true, "license": "MIT", "engines": { @@ -5196,9 +5283,9 @@ } }, "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -5223,22 +5310,22 @@ } }, "node_modules/tldts": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", - "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", + "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.12" + "tldts-core": "^7.4.16" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", - "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", + "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", "dev": true, "license": "MIT" }, @@ -5256,16 +5343,16 @@ } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/ts-api-utils": { @@ -5330,19 +5417,18 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", "gopd": "^1.2.0", - "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -5373,9 +5459,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5387,16 +5473,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.70.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", - "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.70.0", - "@typescript-eslint/parser": "8.70.0", - "@typescript-eslint/typescript-estree": "8.70.0", - "@typescript-eslint/utils": "8.70.0" + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5430,13 +5516,13 @@ } }, "node_modules/undici": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", - "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/update-browserslist-db": { @@ -5481,16 +5567,16 @@ } }, "node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", - "rolldown": "~1.2.6", + "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "bin": { @@ -5559,14 +5645,14 @@ } }, "node_modules/vitest": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", - "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.0", + "@vitest/mocker": "5.0.3", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -5574,10 +5660,10 @@ "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", - "tinybench": "6.1.4", - "tinyexec": "1.3.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", - "why-is-node-running": "^2.3.0" + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" @@ -5592,12 +5678,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.0", - "@vitest/browser-preview": "5.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.0", - "@vitest/coverage-v8": "5.0.0", - "@vitest/ui": "5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" @@ -5642,16 +5728,16 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/webidl-conversions": { @@ -5675,18 +5761,18 @@ } }, "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.2.0.tgz", + "integrity": "sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", "webidl-conversions": "^8.0.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/which": { @@ -5773,9 +5859,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5795,20 +5881,16 @@ } }, "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/word-wrap": { @@ -5859,9 +5941,9 @@ } }, "node_modules/zod": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.1.tgz", - "integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==", + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 82473db..eada837 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,59 @@ { - "name": "react-typescript", - "version": "1.0.0", - "description": "React and TypeScript example starter project", + "name": "gcode-preview-react", + "version": "0.1.0", + "private": true, + "description": "React component for gcode-preview: a 3D preview of G-code files in a canvas", "keywords": [ - "typescript", + "gcode", + "g-code", + "gcode-preview", + "3d-printing", + "preview", "react", - "starter" + "react-component", + "threejs" ], - "main": "src/index.tsx", - "dependencies": { - "gcode-preview": "3.0.0-alpha.6", - "react": "19.3.0", - "react-dom": "19.3.0", - "three": "0.185.0" + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/xyz-tools/gcode-preview-react.git" + }, + "homepage": "https://github.com/xyz-tools/gcode-preview-react#readme", + "bugs": { + "url": "https://github.com/xyz-tools/gcode-preview-react/issues" + }, + "type": "module", + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "npm run clean && tsc -p tsconfig.lib.json", + "build:demo": "tsc --noEmit && vite build", + "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", + "preview": "vite preview", + "prepack": "npm run build", + "typecheck": "tsc --noEmit && tsc -p tsconfig.lib.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "lint:package": "publint", + "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc.json" + }, + "peerDependencies": { + "gcode-preview": "^3.0.0-alpha.6", + "react": "^18.0.0 || ^19.0.0" }, "devDependencies": { "@eslint/js": "^9.39.5", @@ -25,26 +66,18 @@ "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", - "globals": "^17.12.0", - "jsdom": "^29.1.1", - "prettier": "^3.9.6", - "typescript": "^5.9.3", - "typescript-eslint": "^8.70.0", - "vite": "^8.3.0", - "vitest": "^5.0.0" - }, - "scripts": { - "start": "vite", - "dev": "vite", - "build": "tsc --noEmit && vite build", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc.json" + "gcode-preview": "3.0.0-alpha.6", + "globals": "^17.13.0", + "jsdom": "^30.1.2", + "prettier": "^3.9.9", + "publint": "^0.3.25", + "react": "19.3.0", + "react-dom": "19.3.0", + "typescript": "~6.0", + "typescript-eslint": "^8.71.0", + "vite": "^8.3.2", + "vitest": "^5.0.3" }, - "type": "module", "engines": { "node": ">=22.12.0" } diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 6c35195..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "GCode Preview", - "name": "GCode Preview React Demo", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/src/App.tsx b/src/App.tsx index f2e6484..21539d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,77 @@ -import type { JSX } from 'react'; +import { useState, type JSX } from 'react'; -import GCodePreview from './components/GCodePreview'; +import { GCodePreview } from './lib'; import './styles.css'; +const samples = [ + { + name: 'Square tower', + file: 'square-tower.gcode', + topLayerColor: 'lime', + lastSegmentColor: 'red', + startLayer: 20, + endLayer: 150 + }, + { + name: 'Triangle tower', + file: 'triangle-tower.gcode', + topLayerColor: 'purple', + lastSegmentColor: 'cyan' + } +]; + +type Status = + | { state: 'loading' } + | { state: 'loaded' } + | { state: 'error'; message: string }; + function App(): JSX.Element { + const [sample, setSample] = useState(samples[0]); + const [status, setStatus] = useState({ state: 'loading' }); + + function select(next: (typeof samples)[number]) { + setSample(next); + setStatus({ state: 'loading' }); + } + return (
-

GCode Preview 3.0 React & TypeScript Demo

- +

GCode Preview React demo

+

+ Drop a .gcode file onto the canvas to preview your own + print. +

+
+ {samples.map(s => ( + + ))} +
setStatus({ state: 'loaded' })} + onError={error => setStatus({ state: 'error', message: error.message })} /> + {status.state === 'loading' &&

Loading G-code…

} + {status.state === 'error' &&

{status.message}

} +

+ Layers {sample.startLayer ?? 'first'} to {sample.endLayer ?? 'last'} +

); } diff --git a/src/components/GCodePreview.test.jsx b/src/components/GCodePreview.test.jsx deleted file mode 100644 index 355f154..0000000 --- a/src/components/GCodePreview.test.jsx +++ /dev/null @@ -1,103 +0,0 @@ -import { StrictMode, act } from 'react'; -import { createRoot } from 'react-dom/client'; -import { GCodePreview } from 'gcode-preview'; -import Preview from './GCodePreview'; - -vi.mock('gcode-preview', () => ({ GCodePreview: vi.fn() })); - -let container; -let root; -let instances; -const originalFetch = globalThis.fetch; -const originalDecoder = globalThis.TextDecoderStream; - -beforeEach(() => { - globalThis.IS_REACT_ACT_ENVIRONMENT = true; - globalThis.TextDecoderStream = class {}; - instances = []; - vi.mocked(GCodePreview).mockImplementation(function () { - const instance = { - processGCodeStream: vi.fn().mockResolvedValue(undefined), - dispose: vi.fn(), - sceneManager: { resize: vi.fn(), render: vi.fn() } - }; - instances.push(instance); - return instance; - }); - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); -}); - -afterEach(async () => { - await act(async () => root.unmount()); - container.remove(); - globalThis.fetch = originalFetch; - globalThis.TextDecoderStream = originalDecoder; - vi.clearAllMocks(); -}); - -function response() { - return { ok: true, body: { pipeThrough: vi.fn(() => 'decoded stream') } }; -} - -test('streams decoded text, applies layers after loading, and cleans up', async () => { - globalThis.fetch = vi.fn().mockResolvedValue(response()); - await act(async () => { - root.render(); - }); - const preview = instances[0]; - expect(preview.processGCodeStream).toHaveBeenCalledWith('decoded stream'); - expect(preview.sceneManager.startLayer).toBe(20); - expect(preview.sceneManager.endLayer).toBe(150); - expect(container.querySelector('[role="status"]')).toBeNull(); - window.dispatchEvent(new Event('resize')); - expect(preview.sceneManager.resize).toHaveBeenCalledTimes(1); - await act(async () => root.render(null)); - expect(globalThis.fetch.mock.calls[0][1].signal.aborted).toBe(true); - expect(preview.dispose).toHaveBeenCalledTimes(1); - window.dispatchEvent(new Event('resize')); - expect(preview.sceneManager.resize).toHaveBeenCalledTimes(1); -}); - -test('ignores a late response from a replaced source', async () => { - let resolveOld; - globalThis.fetch = vi - .fn() - .mockImplementationOnce( - () => - new Promise(resolve => { - resolveOld = resolve; - }) - ) - .mockResolvedValue(response()); - await act(async () => root.render()); - await act(async () => root.render()); - await act(async () => resolveOld(response())); - expect(instances[0].dispose).toHaveBeenCalledTimes(1); - expect(instances[0].processGCodeStream).not.toHaveBeenCalled(); - expect(instances[1].processGCodeStream).toHaveBeenCalledTimes(1); -}); - -test('reports failed loads', async () => { - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404 }); - await act(async () => root.render()); - expect(container.querySelector('[role="alert"]').textContent).toContain( - 'HTTP 404' - ); -}); - -test('survives Strict Mode effect replay without loading a disposed preview', async () => { - globalThis.fetch = vi.fn().mockResolvedValue(response()); - await act(async () => - root.render( - - - - ) - ); - expect(instances).toHaveLength(2); - expect(instances[0].dispose).toHaveBeenCalledTimes(1); - expect(instances[0].processGCodeStream).not.toHaveBeenCalled(); - expect(instances[1].processGCodeStream).toHaveBeenCalledTimes(1); -}); diff --git a/src/components/GCodePreview.tsx b/src/components/GCodePreview.tsx deleted file mode 100644 index adff4e8..0000000 --- a/src/components/GCodePreview.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { GCodePreview } from 'gcode-preview'; -import { useEffect, useRef, useState, type JSX } from 'react'; - -interface GCodePreviewProps { - src: string; - topLayerColor?: string; - lastSegmentColor?: string; - startLayer?: number; - endLayer?: number; - lineWidth?: number; -} - -export default function GCodePreviewUI({ - src, - topLayerColor = 'lime', - lastSegmentColor = 'red', - startLayer, - endLayer, - lineWidth = 1 -}: GCodePreviewProps): JSX.Element { - const canvasRef = useRef(null); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (!canvasRef.current) return; - - const controller = new AbortController(); - const preview = new GCodePreview({ - canvas: canvasRef.current, - lineWidth, - topLayerColor, - lastSegmentColor, - buildVolume: { x: 250, y: 220, z: 150, smallGrid: true }, - initialCameraPosition: [0, 400, 450], - droppable: false - }); - const resize = () => preview.sceneManager.resize(); - window.addEventListener('resize', resize); - setError(''); - setLoading(true); - - async function load() { - try { - const response = await fetch(src, { signal: controller.signal }); - if (controller.signal.aborted) return; - if (!response.ok) throw new Error(`HTTP ${response.status}: ${src}`); - if (!response.body) - throw new Error('The response has no G-code stream.'); - await preview.processGCodeStream( - response.body.pipeThrough(new TextDecoderStream()) - ); - if (controller.signal.aborted) return; - // Apply the range after loading so it is clamped against the full job. - preview.sceneManager.startLayer = startLayer; - preview.sceneManager.endLayer = endLayer; - preview.sceneManager.render(); - } catch (cause) { - if (!controller.signal.aborted) { - setError(cause instanceof Error ? cause.message : String(cause)); - } - } finally { - if (!controller.signal.aborted) setLoading(false); - } - } - void load(); - - return () => { - controller.abort(); - window.removeEventListener('resize', resize); - preview.dispose(); - }; - }, [src, topLayerColor, lastSegmentColor, startLayer, endLayer, lineWidth]); - - return ( -
- - {loading &&

Loading G-code…

} - {error &&

{error}

} -
topLayerColor: {topLayerColor}
-
lastSegmentColor: {lastSegmentColor}
-
startLayer: {startLayer ?? 'first'}
-
endLayer: {endLayer ?? 'last'}
-
lineWidth: {lineWidth}
-
- ); -} diff --git a/src/lib/GCodePreview.test.tsx b/src/lib/GCodePreview.test.tsx new file mode 100644 index 0000000..904c5c2 --- /dev/null +++ b/src/lib/GCodePreview.test.tsx @@ -0,0 +1,371 @@ +import { GCodePreview as CorePreview } from 'gcode-preview'; +import { StrictMode, act, createRef, type ReactNode } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import type { Mock } from 'vitest'; +import { GCodePreview } from './index'; + +vi.mock('gcode-preview', () => ({ GCodePreview: vi.fn() })); + +interface MockPreview { + options: Record; + processGCode: Mock; + processGCodeStream: Mock; + clear: Mock; + dispose: Mock; + devMode?: unknown; + sceneManager: Record & { resize: Mock }; +} + +let container: HTMLDivElement; +let root: Root; +let instances: MockPreview[]; +// Every setter call and core method call, in order. +let log: string[]; +let observers: MockResizeObserver[]; +const originalFetch = globalThis.fetch; + +class MockResizeObserver { + observe = vi.fn(); + disconnect = vi.fn(); + constructor(public callback: () => void) { + observers.push(this); + } +} + +class MockTextDecoderStream {} + +function recording(target: T, prefix: string): T { + return new Proxy(target, { + set(obj, key, value) { + log.push( + `${prefix}${String(key)}=${JSON.stringify(value) ?? 'undefined'}` + ); + return Reflect.set(obj, key, value); + } + }); +} + +beforeEach(() => { + ( + globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + vi.stubGlobal('ResizeObserver', MockResizeObserver); + vi.stubGlobal('TextDecoderStream', MockTextDecoderStream); + instances = []; + log = []; + observers = []; + vi.mocked(CorePreview).mockImplementation(function ( + options: Record + ) { + const instance: MockPreview = { + options, + processGCode: vi.fn(async () => void log.push('processGCode')), + processGCodeStream: vi.fn( + async () => void log.push('processGCodeStream') + ), + clear: vi.fn(() => void log.push('clear')), + dispose: vi.fn(), + sceneManager: recording({ resize: vi.fn() }, '') + }; + const preview = recording(instance, 'preview.'); + instances.push(preview); + return preview; + } as never); + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + vi.clearAllMocks(); +}); + +async function render(element: ReactNode) { + await act(async () => root.render(element)); +} + +function response() { + return { ok: true, body: { pipeThrough: vi.fn(() => 'decoded stream') } }; +} + +function mockFetch(...responses: unknown[]) { + const fetch = vi.fn(); + for (const r of responses) fetch.mockImplementationOnce(() => r); + fetch.mockImplementation(() => Promise.resolve(response())); + globalThis.fetch = fetch; + return fetch; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise(r => (resolve = r)); + return { promise, resolve }; +} + +test('creates the preview with the canvas and the given options only', async () => { + mockFetch(); + const onReady = vi.fn(); + await render( + {}} + onError={() => {}} + className="viewer" + id="preview" + /> + ); + const canvas = container.querySelector('canvas')!; + expect(instances).toHaveLength(1); + expect(instances[0].options).toEqual({ + canvas, + lineWidth: 2, + topLayerColor: 'lime', + buildVolume: { x: 200, y: 200, z: 180, smallGrid: false }, + initialCameraPosition: [0, 400, 450], + renderTravel: false + }); + expect('renderExtrusion' in instances[0].options).toBe(false); + expect(onReady).toHaveBeenCalledWith(instances[0]); + expect(canvas.className).toBe('viewer'); + expect(canvas.id).toBe('preview'); + expect(canvas.getAttribute('aria-label')).toBe('G-code preview'); + expect(canvas.hasAttribute('src')).toBe(false); + expect(container.children).toHaveLength(1); + + await render(); + expect(canvas.getAttribute('aria-label')).toBe('Benchy'); +}); + +test('streams a decoded src, then applies the layer range and calls onLoad', async () => { + const res = response(); + const fetch = mockFetch(Promise.resolve(res)); + const onLoad = vi.fn(); + await render( + + ); + const preview = instances[0]; + expect(fetch).toHaveBeenCalledWith('https://example.com/a.gcode', { + signal: expect.any(AbortSignal) + }); + expect(res.body.pipeThrough).toHaveBeenCalledWith( + expect.any(MockTextDecoderStream) + ); + expect(preview.processGCodeStream).toHaveBeenCalledWith('decoded stream'); + expect(log).toEqual([ + 'clear', + 'processGCodeStream', + 'startLayer=20', + 'endLayer=150' + ]); + expect(onLoad).toHaveBeenCalledExactlyOnceWith(preview); +}); + +test('processes gcode directly, and gcode wins over src', async () => { + const fetch = mockFetch(); + const onLoad = vi.fn(); + await render( + + ); + const preview = instances[0]; + expect(fetch).not.toHaveBeenCalled(); + expect(preview.processGCode).toHaveBeenCalledWith('G1 X1'); + expect(log).toEqual([ + 'clear', + 'processGCode', + 'startLayer=undefined', + 'endLayer=3' + ]); + expect(onLoad).toHaveBeenCalledWith(preview); + + // Unsetting both clears the preview. + log.length = 0; + await render(); + expect(log).toEqual(['clear']); +}); + +test('does not load or clear without src or gcode', async () => { + const fetch = mockFetch(); + await render(); + expect(fetch).not.toHaveBeenCalled(); + expect(instances[0].clear).not.toHaveBeenCalled(); + expect(instances[0].processGCode).not.toHaveBeenCalled(); +}); + +test('replacing src aborts the old fetch and ignores its late response', async () => { + const old = deferred>(); + const oldResponse = response(); + const fetch = mockFetch(old.promise); + const onLoad = vi.fn(); + await render(); + await render(); + const oldSignal: AbortSignal = fetch.mock.calls[0][1].signal; + expect(oldSignal.aborted).toBe(true); + expect(fetch.mock.calls[1][1].signal.aborted).toBe(false); + + await act(async () => old.resolve(oldResponse)); + expect(instances).toHaveLength(1); + expect(oldResponse.body.pipeThrough).not.toHaveBeenCalled(); + expect(instances[0].processGCodeStream).toHaveBeenCalledTimes(1); + expect(instances[0].clear).toHaveBeenCalledTimes(2); + expect(onLoad).toHaveBeenCalledTimes(1); +}); + +test('applies only changed live options and ignores mount-only options', async () => { + const onReady = vi.fn(); + const props = { + buildVolume: { x: 200, y: 200, z: 180, smallGrid: false }, + extrusionColor: ['red', 'blue'], + topLayerColor: 'lime', + renderTravel: true, + initialCameraPosition: [0, 1, 2], + droppable: false + }; + await render(); + const preview = instances[0]; + expect(log).toEqual([]); + + // Equal inline objects/arrays and a new callback identity change nothing. + await render( + {}} + /> + ); + expect(log).toEqual([]); + + props.topLayerColor = 'purple'; + await render(); + expect(log).toEqual(['topLayerColor="purple"']); + + log.length = 0; + await render(); + expect(log).toEqual(['preview.devMode=true']); + + log.length = 0; + await render( + + ); + expect(log).toEqual([]); + + // Setters that accept undefined get it; the others keep their value. + log.length = 0; + await render( + + ); + expect(log).toEqual(['topLayerColor=undefined']); + + expect(instances).toHaveLength(1); + expect(preview.dispose).not.toHaveBeenCalled(); + expect(onReady).toHaveBeenCalledTimes(1); +}); + +test('reports a failed fetch to onError', async () => { + mockFetch(Promise.resolve({ ok: false, status: 404 })); + const onError = vi.fn(); + const onLoad = vi.fn(); + await render( + + ); + expect(onError).toHaveBeenCalledExactlyOnceWith(expect.any(Error)); + expect(onError.mock.calls[0][0].message).toBe('HTTP 404: /missing.gcode'); + expect(onLoad).not.toHaveBeenCalled(); +}); + +test('logs a failed load without an error handler', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + mockFetch(Promise.resolve({ ok: false, status: 404 })); + await render(); + expect(consoleError).toHaveBeenCalledExactlyOnceWith(expect.any(Error)); + expect(consoleError.mock.calls[0][0].message).toBe( + 'HTTP 404: /missing.gcode' + ); +}); + +test('resizes on ResizeObserver and cleans up on unmount', async () => { + const fetch = mockFetch(new Promise(() => {})); + const onLoad = vi.fn(); + await render(); + const preview = instances[0]; + const canvas = container.querySelector('canvas'); + expect(observers).toHaveLength(1); + expect(observers[0].observe).toHaveBeenCalledWith(canvas); + observers[0].callback(); + expect(preview.sceneManager.resize).toHaveBeenCalledTimes(1); + + await render(null); + expect(fetch.mock.calls[0][1].signal.aborted).toBe(true); + expect(observers[0].disconnect).toHaveBeenCalledTimes(1); + expect(preview.dispose).toHaveBeenCalledTimes(1); + expect(onLoad).not.toHaveBeenCalled(); +}); + +test('ignores a load that finishes after unmount', async () => { + const done = deferred(); + mockFetch(); + const onLoad = vi.fn(); + await render(); + instances[0].processGCode.mockReturnValueOnce(done.promise); + await render(); + onLoad.mockClear(); + log.length = 0; + await render(null); + await act(async () => done.resolve()); + expect(onLoad).not.toHaveBeenCalled(); + expect(log).toEqual([]); +}); + +test('exposes the preview instance through ref', async () => { + const ref = createRef(); + await render(); + expect(ref.current).toBe(instances[0]); + await render(null); + expect(ref.current).toBeNull(); +}); + +test('points the ref at the live preview under StrictMode', async () => { + mockFetch(); + const ref = createRef(); + const callbackRef = vi.fn(); + await render( + + + + + ); + const live = instances.filter(i => !i.dispose.mock.calls.length); + expect(live).toHaveLength(2); + expect(ref.current).toBe(live[0]); + expect(callbackRef).toHaveBeenLastCalledWith(live[1]); + for (const instance of instances) { + const loads = instance.processGCodeStream.mock.calls.length; + expect(loads).toBe(instance === live[0] ? 1 : 0); + } +}); diff --git a/src/lib/GCodePreview.tsx b/src/lib/GCodePreview.tsx new file mode 100644 index 0000000..c6d8f6b --- /dev/null +++ b/src/lib/GCodePreview.tsx @@ -0,0 +1,285 @@ +'use client'; + +import { + GCodePreview as CorePreview, + type GCodePreviewOptions +} from 'gcode-preview'; +import { + forwardRef, + useEffect, + useRef, + type ComponentPropsWithoutRef, + type ForwardedRef +} from 'react'; + +type Options = Omit; + +// Options with a live setter: a changed prop updates the preview in place. +const LIVE_OPTIONS = [ + 'buildVolume', + 'backgroundColor', + 'extrusionColor', + 'travelColor', + 'topLayerColor', + 'lastSegmentColor', + 'boundingBoxColor', + 'startLayer', + 'endLayer', + 'renderExtrusion', + 'renderTravel', + 'renderTubes', + 'lineWidth', + 'lineHeight', + 'extrusionWidth', + 'disableGradient', + 'orthographic', + 'devMode' +] as const satisfies readonly (keyof Options)[]; + +// Options the core only reads in its constructor. +const MOUNT_OPTIONS = [ + 'initialCameraPosition', + 'minLayerThreshold', + 'droppable', + 'keepLines', + 'liveRenderInterval', + 'arcChordTolerance' +] as const satisfies readonly (keyof Options)[]; + +type Unlisted = Exclude< + keyof Options, + (typeof LIVE_OPTIONS)[number] | (typeof MOUNT_OPTIONS)[number] +>; +// Fails the typecheck when the core gains an option that is in neither list. +const allListed: [Unlisted] extends [never] ? true : Unlisted = true; +void allListed; + +type LiveOption = (typeof LIVE_OPTIONS)[number]; + +const OPTION_KEYS: ReadonlySet = new Set([ + ...LIVE_OPTIONS, + ...MOUNT_OPTIONS +]); + +// Everything else is passed through to the canvas. +const OWN_PROPS: ReadonlySet = new Set([ + ...OPTION_KEYS, + 'src', + 'gcode', + 'onReady', + 'onLoad', + 'onError' +]); + +// The other setters do not accept undefined, so an unset prop keeps the last value. +const ACCEPTS_UNDEFINED: ReadonlySet = new Set([ + 'startLayer', + 'endLayer', + 'topLayerColor', + 'lastSegmentColor', + 'boundingBoxColor', + 'buildVolume', + 'lineHeight', + 'extrusionWidth', + 'devMode' +]); + +type CanvasProps = Omit< + ComponentPropsWithoutRef<'canvas'>, + keyof Options | 'children' | 'onLoad' | 'onError' +>; + +export interface GCodePreviewProps extends Options, CanvasProps { + /** URL of a G-code file to fetch and stream in. Ignored when `gcode` is set. */ + src?: string | URL; + /** G-code to process directly. Wins over `src`. */ + gcode?: string | string[]; + /** Called once the preview has been created. */ + onReady?: (preview: CorePreview) => void; + /** Called after `src` or `gcode` has been loaded and rendered. */ + onLoad?: (preview: CorePreview) => void; + /** Called when loading fails. Without it, the error is logged. */ + onError?: (error: Error) => void; +} + +// Inline objects and arrays (buildVolume, extrusionColor, ...) are new on +// every render; compare them one level deep so their setters, some of which +// rebuild geometry, only run on a real change. +function sameValue(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true; + if (Array.isArray(a) && Array.isArray(b)) { + return a.length === b.length && a.every((v, i) => Object.is(v, b[i])); + } + if (isPlainObject(a) && isPlainObject(b)) { + const keys = Object.keys(a); + return ( + keys.length === Object.keys(b).length && + keys.every(key => Object.is(a[key], b[key])) + ); + } + return false; +} + +function isPlainObject(value: unknown): value is Record { + return ( + typeof value === 'object' && + value !== null && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function applyOption(preview: CorePreview, key: LiveOption, value: unknown) { + if (key === 'devMode') { + preview.devMode = value as Options['devMode']; + return; + } + // Each key has its own setter type; the value comes from the matching prop. + (preview.sceneManager as unknown as Record)[key] = value; +} + +function assignRef(ref: ForwardedRef, value: T | null) { + if (typeof ref === 'function') ref(value); + else if (ref) ref.current = value; +} + +export const GCodePreview = forwardRef( + function GCodePreview(props, ref) { + const canvasRef = useRef(null); + const previewRef = useRef(null); + const loadRef = useRef(null); + const appliedRef = useRef>>({}); + // Whether the current preview holds content that an unset source clears. + const loadedRef = useRef(false); + // Callbacks and options are read from here at call time, so a new + // callback identity never recreates the preview. + const latest = useRef(props); + + useEffect(() => { + latest.current = props; + }); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const options: GCodePreviewOptions = { canvas }; + const applied: Partial> = {}; + for (const key of OPTION_KEYS) { + const value = latest.current[key as keyof Options]; + if (value !== undefined) { + (options as Record)[key] = value; + } + } + for (const key of LIVE_OPTIONS) applied[key] = latest.current[key]; + + const preview = new CorePreview(options); + previewRef.current = preview; + appliedRef.current = applied; + + const observer = new ResizeObserver(() => preview.sceneManager.resize()); + observer.observe(canvas); + + latest.current.onReady?.(preview); + + return () => { + loadRef.current?.abort(); + loadRef.current = null; + loadedRef.current = false; + observer.disconnect(); + previewRef.current = null; + preview.dispose(); + }; + }, []); + + // Separate from the effect above so a new ref does not recreate the + // preview; declared after it so the instance exists (also after a + // StrictMode remount). + useEffect(() => { + assignRef(ref, previewRef.current); + return () => assignRef(ref, null); + }, [ref]); + + useEffect(() => { + const preview = previewRef.current; + if (!preview) return; + for (const key of LIVE_OPTIONS) { + const value = props[key]; + if (sameValue(value, appliedRef.current[key])) continue; + appliedRef.current[key] = value; + if (value === undefined && !ACCEPTS_UNDEFINED.has(key)) continue; + applyOption(preview, key, value); + } + }); + + // A URL object is new on every render; compare its string form instead. + const src = props.src === undefined ? undefined : String(props.src); + const { gcode } = props; + const hasContent = gcode !== undefined || src !== undefined; + + useEffect(() => { + const preview = previewRef.current; + if (!preview) return; + if (!hasContent) { + if (loadedRef.current) preview.clear(); + loadedRef.current = false; + return; + } + + // The controller doubles as the load token: it is aborted when this + // load is superseded or the component unmounts. + loadRef.current?.abort(); + const controller = new AbortController(); + loadRef.current = controller; + // Also cancels a stream that is still being read. + preview.clear(); + loadedRef.current = true; + + async function load(preview: CorePreview) { + try { + if (gcode !== undefined) { + await preview.processGCode(gcode); + } else if (src !== undefined) { + const response = await fetch(src, { signal: controller.signal }); + if (controller.signal.aborted) return; + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${src}`); + } + if (!response.body) throw new Error(`No response body: ${src}`); + await preview.processGCodeStream( + response.body.pipeThrough(new TextDecoderStream()) + ); + } + if (controller.signal.aborted) return; + // The layer setters clamp against the loaded job, so apply the + // range again now that the job is complete. + const { startLayer, endLayer, onLoad } = latest.current; + preview.sceneManager.startLayer = startLayer; + preview.sceneManager.endLayer = endLayer; + appliedRef.current.startLayer = startLayer; + appliedRef.current.endLayer = endLayer; + onLoad?.(preview); + } catch (cause) { + if (controller.signal.aborted) return; + const error = + cause instanceof Error ? cause : new Error(String(cause)); + const { onError } = latest.current; + if (onError) onError(error); + else console.error(error); + } + } + void load(preview); + + return () => controller.abort(); + }, [hasContent, src, gcode]); + + const canvasProps: Record = {}; + for (const key in props) { + if (!OWN_PROPS.has(key)) { + canvasProps[key] = props[key as keyof GCodePreviewProps]; + } + } + + return ( + + ); + } +); diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..0cb11e4 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,2 @@ +export { GCodePreview } from './GCodePreview.js'; +export type { GCodePreviewProps } from './GCodePreview.js'; diff --git a/src/styles.css b/src/styles.css index 4df3e8f..58f823e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -7,13 +7,22 @@ body { margin-top: 60px; } -.gcode-preview { - width: 50vw; - margin: 0 auto 30px; +.samples { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 16px; +} + +button[aria-pressed='true'] { + font-weight: bold; } -canvas { +/* The component renders a bare canvas; its size comes from here. */ +.gcode-preview { + display: block; outline: none; - width: 100%; + width: 50vw; height: 320px; + margin: 0 auto; } diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 0000000..185db5c --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src/lib/**/*"], + "exclude": ["./src/lib/**/*.test.*"], + "compilerOptions": { + "noEmit": false, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src/lib", + "allowJs": false, + "types": [] + } +}