Compare commits
	
		
			128 Commits
		
	
	
		
			v0.14.0
			...
			fb297f9831
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					fb297f9831 | ||
| 
						 | 
					aee9677029 | ||
| 
						 | 
					da982dc831 | ||
| 
						 | 
					a64eaa3fc0 | ||
| 
						 | 
					a63651f715 | ||
| 
						 | 
					bb67b708e7 | ||
| 
						 | 
					dc0d37c71e | ||
| 
						 | 
					0287c4d458 | ||
| 
						 | 
					47be1061b7 | ||
| 
						 | 
					1c79de2f37 | ||
| 
						 | 
					9696cc7188 | ||
| 
						 | 
					082dd8c1f2 | ||
| 
						 | 
					43524dcdb1 | ||
| 
						 | 
					45a0540edf | ||
| 
						 | 
					2b784d1edc | ||
| 
						 | 
					8650cf9a63 | ||
| 
						 | 
					76c840dbaa | ||
| 
						 | 
					e78de6f6de | ||
| 
						 | 
					554edf5a27 | ||
| 
						 | 
					6fca398a12 | ||
| 
						 | 
					5bf3fbc10e | ||
| 
						 | 
					d994c38219 | ||
| 
						 | 
					3dccbfc797 | ||
| 
						 | 
					c3d461f102 | ||
| 
						 | 
					c0105889ab | ||
| 
						 | 
					c6006b58d2 | ||
| 
						 | 
					178f009458 | ||
| 
						 | 
					9c24cf4aba | ||
| 
						 | 
					af68498494 | ||
| 
						 | 
					eac22d53d3 | ||
| 
						 | 
					e5ac60c187 | ||
| 
						 | 
					4b42a5fbda | ||
| 
						 | 
					08a833f1cf | ||
| 
						 | 
					c0f0dc5192 | ||
| 
						 | 
					6452d0b357 | ||
| 
						 | 
					9f6b815197 | ||
| 
						 | 
					d8cbc0aaee | ||
| 
						 | 
					b1f70ec36c | ||
| 
						 | 
					311d2516ce | ||
| 
						 | 
					5957873534 | ||
| 
						 | 
					7524f304fe | ||
| 
						 | 
					f1730ede97 | ||
| 
						 | 
					7f9c8868fd | ||
| 
						 | 
					72b484a480 | ||
| 
						 | 
					c47c1dd4f4 | ||
| 
						 | 
					3975c70de7 | ||
| 
						 | 
					fd4e73e76c | ||
| 
						 | 
					301fab5c17 | ||
| 
						 | 
					2c90454244 | ||
| 
						 | 
					2db99edeaf | ||
| 
						 | 
					4fa471263f | ||
| 
						 | 
					435f654cbe | ||
| 
						 | 
					15b03d7561 | ||
| 
						 | 
					219e6a29e4 | ||
| 
						 | 
					a85edb6cee | ||
| 
						 | 
					43081c5179 | ||
| 
						 | 
					d390dce843 | ||
| 
						 | 
					e5939aaa5d | ||
| 
						 | 
					1db0c0f531 | ||
| 
						 | 
					eefd33ac88 | ||
| 
						 | 
					de10436c1a | ||
| 
						 | 
					81f109f830 | ||
| 
						 | 
					858ee28ef2 | ||
| 
						 | 
					b89afe1b0c | ||
| 
						 | 
					b4fedc1c1d | ||
| 
						 | 
					d24e8b4027 | ||
| 
						 | 
					93fbdbe0f3 | ||
| 
						 | 
					068d9b8716 | ||
| 
						 | 
					2295f23725 | ||
| 
						 | 
					fed587b4a4 | ||
| 
						 | 
					8f73f9c365 | ||
| 
						 | 
					363efc2e7f | ||
| 
						 | 
					cf93fed64b | ||
| 
						 | 
					99c689657f | ||
| 
						 | 
					394c98c65a | ||
| 
						 | 
					8f93ac29dd | ||
| 
						 | 
					5ffb7f4a01 | ||
| 
						 | 
					f5f718a84a | ||
| 
						 | 
					4c36a950a7 | ||
| 
						 | 
					a9bc9d7e8d | ||
| 
						 | 
					18fed70ddf | ||
| 
						 | 
					dd9d117ab8 | ||
| 
						 | 
					0e94fe354f | ||
| 
						 | 
					3b99c79495 | ||
| 
						 | 
					20e914c85b | ||
| 
						 | 
					efc4b3f84c | ||
| 
						 | 
					2bc6b52e99 | ||
| 
						 | 
					feb59e560b | ||
| 
						 | 
					17be8f3601 | ||
| 
						 | 
					3b053e8222 | ||
| 
						 | 
					9d5050d3ee | ||
| 
						 | 
					1bd56e1d0e | ||
| 
						 | 
					3f46abf261 | ||
| 
						 | 
					6a248e17be | ||
| 
						 | 
					8273a2a6a0 | ||
| 
						 | 
					78f52c769d | ||
| 
						 | 
					fe22b2f8fb | ||
| 
						 | 
					482421f10e | ||
| 
						 | 
					dcb15aee0e | ||
| 
						 | 
					827f22e2fc | ||
| 
						 | 
					bd36314f00 | ||
| 
						 | 
					4ad7892eab | ||
| 
						 | 
					31b7e62983 | ||
| 
						 | 
					0f5ef2f49c | ||
| 
						 | 
					2ce3fee70b | ||
| 
						 | 
					33e5bee9fb | ||
| 
						 | 
					b32f7dba5d | ||
| 
						 | 
					2661acbadb | ||
| 
						 | 
					d28c079a57 | ||
| 
						 | 
					29a159c094 | ||
| 
						 | 
					68dad51948 | ||
| 
						 | 
					3bf82b5b86 | ||
| 
						 | 
					e52e8c12cf | ||
| 
						 | 
					8f7a7faa91 | ||
| 
						 | 
					ff0edec652 | ||
| 
						 | 
					bbcecf274f | ||
| 
						 | 
					cc41be6856 | ||
| 
						 | 
					761f56b869 | ||
| 
						 | 
					d4e8eaadd7 | ||
| 
						 | 
					9f2bdadde7 | ||
| 
						 | 
					f789d9dfe3 | ||
| 
						 | 
					ce41ee2387 | ||
| 
						 | 
					01c8fad012 | ||
| 
						 | 
					908e91cb91 | ||
| 
						 | 
					f1c5cd9f6b | ||
| 
						 | 
					6ea3058e66 | ||
| 
						 | 
					a4e741cc0a | ||
| 
						 | 
					33388cf209 | 
							
								
								
									
										1
									
								
								.bun-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
1.2.2
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,10 +1,9 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve
 | 
			
		||||
title: ''
 | 
			
		||||
title: ""
 | 
			
		||||
labels: bug
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
@@ -12,10 +11,12 @@ A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Checklist:**
 | 
			
		||||
 | 
			
		||||
- [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true`
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/converter_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
---
 | 
			
		||||
name: Converter request
 | 
			
		||||
about: Suggest a converter for this project
 | 
			
		||||
title: "[Converter Request]"
 | 
			
		||||
labels: "converter request"
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**What file formats are missing?**
 | 
			
		||||
 | 
			
		||||
<!-- Provide an example of what you would like to convert -->
 | 
			
		||||
 | 
			
		||||
**What converter should be added**
 | 
			
		||||
 | 
			
		||||
<!-- It has to be free and preferably open source -->
 | 
			
		||||
 | 
			
		||||
**Are you willing to add it?**
 | 
			
		||||
 | 
			
		||||
<!-- Adding a converter is very easy just copy one of the existing and modify it -->
 | 
			
		||||
 | 
			
		||||
- [ ] Yes
 | 
			
		||||
- [ ] No
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
 | 
			
		||||
<!-- Add any other context or screenshots about the feature request here. -->
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -3,8 +3,7 @@ name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: "[Feature Request]"
 | 
			
		||||
labels: enhancement
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/check-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Lint
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: Run linting checks
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run lint
 | 
			
		||||
        run: bun run lint
 | 
			
		||||
							
								
								
									
										39
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -10,7 +10,6 @@ on:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
env:
 | 
			
		||||
  GHCR_IMAGE: ghcr.io/c4illin/convertx
 | 
			
		||||
  IMAGE_NAME: ${{ github.repository }}
 | 
			
		||||
  DOCKERHUB_USERNAME: c4illin
 | 
			
		||||
 | 
			
		||||
@@ -32,8 +31,7 @@ jobs:
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      checks: write
 | 
			
		||||
      actions: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
 | 
			
		||||
 | 
			
		||||
@@ -51,22 +49,26 @@ jobs:
 | 
			
		||||
          echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Docker meta default
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: ${{ env.GHCR_IMAGE }}
 | 
			
		||||
          images: ghcr.io/${{ env.REPO }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
      - name: Login to GitHub Container Registry
 | 
			
		||||
        # here we only login to ghcr.io since the this only pushes internal images
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -74,7 +76,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Build and push by digest
 | 
			
		||||
        id: build
 | 
			
		||||
        uses: docker/build-push-action@v6.18.0
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_BUILDKIT: 1
 | 
			
		||||
        with:
 | 
			
		||||
@@ -82,7 +84,8 @@ jobs:
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
          outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
 | 
			
		||||
          outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true
 | 
			
		||||
          push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
 | 
			
		||||
          cache-from: type=gha,scope=${{ matrix.platform }}
 | 
			
		||||
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
@@ -101,30 +104,36 @@ jobs:
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  merge:
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository
 | 
			
		||||
    name: Merge Docker manifests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    permissions:
 | 
			
		||||
      attestations: write
 | 
			
		||||
      contents: read
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    needs:
 | 
			
		||||
      - build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v4
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Extract Docker metadata
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            ${{ env.GHCR_IMAGE }}
 | 
			
		||||
            ghcr.io/${{ env.REPO }}
 | 
			
		||||
            ${{ env.IMAGE_NAME }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
@@ -157,8 +166,8 @@ jobs:
 | 
			
		||||
            --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
 | 
			
		||||
            $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
 | 
			
		||||
            $(printf 'ghcr.io/${{ env.REPO }}@sha256:%s ' *)
 | 
			
		||||
 | 
			
		||||
      - name: Inspect image
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'
 | 
			
		||||
          docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/dockerhub-description.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -15,7 +15,7 @@ jobs:
 | 
			
		||||
  dockerHubDescription:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Docker Hub Description
 | 
			
		||||
        uses: peter-evans/dockerhub-description@v4
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/run-bun-test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Tests
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Run tests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: bun test
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -46,6 +46,7 @@ package-lock.json
 | 
			
		||||
/output
 | 
			
		||||
/db
 | 
			
		||||
/data
 | 
			
		||||
/dist
 | 
			
		||||
/Bruno
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/public/generated.css
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,24 +1,43 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
 | 
			
		||||
## [0.15.0](https://github.com/C4illin/ConvertX/compare/v0.14.1...v0.15.0) (2025-08-25)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b))
 | 
			
		||||
* add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3))
 | 
			
		||||
* enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced))
 | 
			
		||||
* improve job details interaction and accessibility ([2a3b084](https://github.com/C4illin/ConvertX/commit/2a3b08487ec4bf215e1e80059dbdc1dcccea68c8))
 | 
			
		||||
* improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611))
 | 
			
		||||
* show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
 | 
			
		||||
* vtracer implemented and added docker file binaries install ([76c840d](https://github.com/C4illin/ConvertX/commit/76c840dbaa4a26d0623422b61581bb761ad6a6bc))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49))
 | 
			
		||||
* progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
 | 
			
		||||
* register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1))
 | 
			
		||||
* switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde))
 | 
			
		||||
* add language env ([f789d9d](https://github.com/C4illin/ConvertX/commit/f789d9dfe381780dcc715b70bcf304d570a73e3f))
 | 
			
		||||
* add lmodern ([761f56b](https://github.com/C4illin/ConvertX/commit/761f56b869d3a4faa7550d90b3da2d853baf8a1d)), closes [#320](https://github.com/C4illin/ConvertX/issues/320)
 | 
			
		||||
* move color variables to seperate directory ([3bf82b5](https://github.com/C4illin/ConvertX/commit/3bf82b5b86177f95531293cab1dfee1e12c898a1)), closes [#53](https://github.com/C4illin/ConvertX/issues/53)
 | 
			
		||||
* run qtwebengine without sandbox ([9f2bdad](https://github.com/C4illin/ConvertX/commit/9f2bdadde779d88973296e81af103ed0016f5411))
 | 
			
		||||
* update favicon ([827f22e](https://github.com/C4illin/ConvertX/commit/827f22e2fc33bf32a02befb3c5bd519511826b38)), closes [#158](https://github.com/C4illin/ConvertX/issues/158)
 | 
			
		||||
 | 
			
		||||
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
 | 
			
		||||
 | 
			
		||||
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b))
 | 
			
		||||
- add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)), closes [#295](https://github.com/C4illin/ConvertX/issues/295), closes [#269](https://github.com/C4illin/ConvertX/issues/269)
 | 
			
		||||
- enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)), closes [#251](https://github.com/C4illin/ConvertX/issues/251)
 | 
			
		||||
- improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611))
 | 
			
		||||
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)), closes [#287](https://github.com/C4illin/ConvertX/issues/287), closes [#293](https://github.com/C4illin/ConvertX/issues/293)
 | 
			
		||||
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
 | 
			
		||||
- register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1))
 | 
			
		||||
- switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)), closes [#234](https://github.com/C4illin/ConvertX/issues/234), closes [#199](https://github.com/C4illin/ConvertX/issues/199)
 | 
			
		||||
 | 
			
		||||
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -3,14 +3,22 @@ LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# install bun
 | 
			
		||||
ENV BUN_INSTALL=/etc/.bun
 | 
			
		||||
ENV PATH=$BUN_INSTALL/bin:$PATH
 | 
			
		||||
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.2"
 | 
			
		||||
 | 
			
		||||
# if architecture is arm64, use the arm64 version of bun
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
 | 
			
		||||
  else \
 | 
			
		||||
    curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
# install dependencies into temp directory
 | 
			
		||||
# this will cache them and speed up future builds
 | 
			
		||||
@@ -46,10 +54,14 @@ RUN apt-get update && apt-get install -y \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  imagemagick-7.q16 \
 | 
			
		||||
  inkscape \
 | 
			
		||||
  latexmk \
 | 
			
		||||
  libheif-examples \
 | 
			
		||||
  libjxl-tools \
 | 
			
		||||
  libreoffice \
 | 
			
		||||
  libva2 \
 | 
			
		||||
  libvips-tools \
 | 
			
		||||
  libemail-outlook-message-perl \
 | 
			
		||||
  lmodern \
 | 
			
		||||
  mupdf-tools \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  poppler-utils \
 | 
			
		||||
@@ -57,15 +69,35 @@ RUN apt-get update && apt-get install -y \
 | 
			
		||||
  python3-numpy \
 | 
			
		||||
  resvg \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-fonts-recommended \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  texlive-latex-recommended \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  --no-install-recommends \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Install VTracer binary
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  else \
 | 
			
		||||
    VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  fi && \
 | 
			
		||||
  curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
 | 
			
		||||
  tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
 | 
			
		||||
  mv /tmp/vtracer /usr/local/bin/vtracer && \
 | 
			
		||||
  chmod +x /usr/local/bin/vtracer && \
 | 
			
		||||
  rm /tmp/vtracer.tar.gz
 | 
			
		||||
 | 
			
		||||
COPY --from=install /temp/prod/node_modules node_modules
 | 
			
		||||
COPY --from=prerelease /app/public/generated.css /app/public/
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=prerelease /app/dist /app/dist
 | 
			
		||||
 | 
			
		||||
# COPY . .
 | 
			
		||||
RUN mkdir data
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
# used for calibre
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
 | 
			
		||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -26,7 +26,7 @@ A self-hosted online file converter. Supports over a thousand different formats.
 | 
			
		||||
## Converters supported
 | 
			
		||||
 | 
			
		||||
| Converter                                          | Use case         | Converts from | Converts to |
 | 
			
		||||
| ------------------------------------------------ | ---------------- | ------------- | ----------- |
 | 
			
		||||
| -------------------------------------------------- | ---------------- | ------------- | ----------- |
 | 
			
		||||
| [libjxl](https://github.com/libjxl/libjxl)         | JPEG XL          | 11            | 11          |
 | 
			
		||||
| [resvg](https://github.com/RazrFalcon/resvg)       | SVG              | 1             | 1           |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)         | Images           | 45            | 23          |
 | 
			
		||||
@@ -41,6 +41,7 @@ A self-hosted online file converter. Supports over a thousand different formats.
 | 
			
		||||
| [Assimp](https://github.com/assimp/assimp)         | 3D Assets        | 77            | 23          |
 | 
			
		||||
| [FFmpeg](https://ffmpeg.org/)                      | Video            | ~472          | ~199        |
 | 
			
		||||
| [Potrace](https://potrace.sourceforge.net/)        | Raster to vector | 4             | 11          |
 | 
			
		||||
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8             | 1           |
 | 
			
		||||
 | 
			
		||||
<!-- many ffmpeg fileformats are duplicates -->
 | 
			
		||||
 | 
			
		||||
@@ -62,6 +63,7 @@ services:
 | 
			
		||||
      - "3000:3000"
 | 
			
		||||
    environment:
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
 | 
			
		||||
      # - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
```
 | 
			
		||||
@@ -81,7 +83,7 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
 | 
			
		||||
All are optional, JWT_SECRET is recommended to be set.
 | 
			
		||||
 | 
			
		||||
| Name                         | Default                                            | Description                                                                                                               |
 | 
			
		||||
| ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| JWT_SECRET                   | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token                                                                  |
 | 
			
		||||
| ACCOUNT_REGISTRATION         | false                                              | Allow users to register accounts                                                                                          |
 | 
			
		||||
| HTTP_ALLOWED                 | false                                              | Allow HTTP connections, only set this to true locally                                                                     |
 | 
			
		||||
@@ -90,6 +92,8 @@ All are optional, JWT_SECRET is recommended to be set.
 | 
			
		||||
| WEBROOT                      |                                                    | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/"                  |
 | 
			
		||||
| FFMPEG_ARGS                  |                                                    | Arguments to pass to ffmpeg, e.g. `-preset veryfast`                                                                      |
 | 
			
		||||
| HIDE_HISTORY                 | false                                              | Hide the history page                                                                                                     |
 | 
			
		||||
| LANGUAGE                     | en                                                 | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
 | 
			
		||||
| UNAUTHENTICATED_USER_SHARING | false                                              | Shares conversion history between all unauthenticated users                                                               |
 | 
			
		||||
 | 
			
		||||
### Docker images
 | 
			
		||||
 | 
			
		||||
@@ -129,19 +133,10 @@ Tutorial in chinese: <https://xzllll.com/24092901/>
 | 
			
		||||
2. `bun install`
 | 
			
		||||
3. `bun run dev`
 | 
			
		||||
 | 
			
		||||
Pull requests are welcome! See below and open issues for the list of todos.
 | 
			
		||||
Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome!
 | 
			
		||||
 | 
			
		||||
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
 | 
			
		||||
 | 
			
		||||
## Todo
 | 
			
		||||
 | 
			
		||||
- [ ] Add options for converters
 | 
			
		||||
- [ ] Add tests
 | 
			
		||||
- [ ] Make errors logs visible from the web ui
 | 
			
		||||
- [ ] Add more converters:
 | 
			
		||||
  - [ ] [deark](https://github.com/jsummers/deark)
 | 
			
		||||
  - [ ] LibreOffice
 | 
			
		||||
 | 
			
		||||
## Contributors
 | 
			
		||||
 | 
			
		||||
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										277
									
								
								bun.lock
									
									
									
									
									
								
							
							
						
						@@ -4,35 +4,36 @@
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "convertx-frontend",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@elysiajs/html": "^1.3.0",
 | 
			
		||||
        "@elysiajs/jwt": "^1.3.0",
 | 
			
		||||
        "@elysiajs/html": "^1.3.1",
 | 
			
		||||
        "@elysiajs/jwt": "^1.3.2",
 | 
			
		||||
        "@elysiajs/static": "^1.3.0",
 | 
			
		||||
        "@kitajs/html": "^4.2.9",
 | 
			
		||||
        "elysia": "^1.3.1",
 | 
			
		||||
        "elysia": "^1.3.8",
 | 
			
		||||
        "sanitize-filename": "^1.6.3",
 | 
			
		||||
        "tar": "^7.4.3",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.27.0",
 | 
			
		||||
        "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.1",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.7",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.7",
 | 
			
		||||
        "@eslint/js": "^9.33.0",
 | 
			
		||||
        "@ianvs/prettier-plugin-sort-imports": "^4.6.2",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.2",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.11",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.11",
 | 
			
		||||
        "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
        "@types/bun": "^1.2.14",
 | 
			
		||||
        "@types/node": "^22.15.21",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.33.1",
 | 
			
		||||
        "eslint": "^9.27.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.0.0",
 | 
			
		||||
        "@types/bun": "latest",
 | 
			
		||||
        "@types/node": "^24.2.1",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.39.1",
 | 
			
		||||
        "eslint": "^9.33.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.7.4",
 | 
			
		||||
        "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
        "globals": "^16.1.0",
 | 
			
		||||
        "knip": "^5.57.2",
 | 
			
		||||
        "npm-run-all2": "^8.0.3",
 | 
			
		||||
        "postcss": "^8.5.3",
 | 
			
		||||
        "prettier": "^3.5.3",
 | 
			
		||||
        "globals": "^16.3.0",
 | 
			
		||||
        "knip": "^5.62.0",
 | 
			
		||||
        "npm-run-all2": "^8.0.4",
 | 
			
		||||
        "postcss": "^8.5.6",
 | 
			
		||||
        "prettier": "^3.6.2",
 | 
			
		||||
        "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
        "tailwindcss": "^4.1.7",
 | 
			
		||||
        "typescript": "^5.8.3",
 | 
			
		||||
        "typescript-eslint": "^8.32.1",
 | 
			
		||||
        "tailwindcss": "^4.1.11",
 | 
			
		||||
        "typescript": "^5.9.2",
 | 
			
		||||
        "typescript-eslint": "^8.39.1",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
@@ -61,35 +62,31 @@
 | 
			
		||||
 | 
			
		||||
    "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.3.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-NpujllWwiEXdsX8GJhbBppOv7+aJr+OU7Gn3K8fVXpwieutwau0/B/M6vzjYXsh9OaoGByUTpL8U9rA/tVSn7w=="],
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.3.1", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-jOWUfvL9vZ2Gs3uCx2w4Po+jxOwRD/sXW3JgvOAD3rEjX0NuygwcvixtbONSzAH8lFhaDBbHAtmCfpue46X9IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.3.1", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-BVLAp0ER4839bR82ElgTsI7OoPxvFWP5u02KMgqpNoAM6xirJBYTKqANzY9ghuMfQoVEuD4B1/8lZwdPKAVg9Q=="],
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.3.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-1Ysb+THWmwy/AKqn9Q1SaBeYK6f499VEVV0E+YifKQjadJT5W+0qKhncOdfqrb4NufUtd65BxULdPQGKJwYo1Q=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/css-tree": ["@eslint/css-tree@3.6.3", "", { "dependencies": { "mdn-data": "2.21.0", "source-map-js": "^1.0.1" } }, "sha512-M9iq4Brt/MG+5/B4Jrla5XZqaCgaHjfZyMSUJM3KNpBU61u8gMYg4TTaNTP/mUGR/rnRrVV7RXmh5qI4pIk0Yw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.33.0", "", {}, "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +96,7 @@
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
 | 
			
		||||
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.4.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@vue/compiler-sfc": "2.7.x || 3.x", "prettier": "2 || 3 || ^4.0.0-0" }, "optionalPeers": ["@vue/compiler-sfc"] }, "sha512-KkVFy3TLh0OFzimbZglMmORi+vL/i2OFhEs5M07R9w0IwWAGpsNNyE4CY/2u0YoMF5bawKC2+8/fUH60nnNtjw=="],
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.6.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@prettier/plugin-oxc": "^0.0.4", "@vue/compiler-sfc": "2.7.x || 3.x", "content-tag": "^4.0.0", "prettier": "2 || 3 || ^4.0.0-0", "prettier-plugin-ember-template-tag": "^2.1.0" }, "optionalPeers": ["@prettier/plugin-oxc", "@vue/compiler-sfc", "content-tag", "prettier-plugin-ember-template-tag"] }, "sha512-kHiL1IghIodo43clNQaJJU2rPqXEioPG+Ink4/T5za46A0ggSNvIx4NM3hGgciQ2VpDaR/X8cTJIZDKRurWjPw=="],
 | 
			
		||||
 | 
			
		||||
    "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
 | 
			
		||||
 | 
			
		||||
@@ -115,9 +112,9 @@
 | 
			
		||||
 | 
			
		||||
    "@kitajs/html": ["@kitajs/html@4.2.9", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.1", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-wmjyV8hmJmDOnUM/ZyPkc0UBYgUYmf32/93rkW8wr8h+HiHVMU0tEKFnmRdBjTcy9jwoC9Bnt2NuzS9l67lq5g=="],
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.2", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-XE9iIe93TELBdQSvNC3xxXOPDhkcK7on4Oi2HUKhln3jAc5hzn1o33uzjHCYhLeW36r/LXCT70beoXRCFcuTxQ=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="],
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 | 
			
		||||
 | 
			
		||||
@@ -125,31 +122,31 @@
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@9.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA=="],
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@9.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A=="],
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0zhgNUm5bYezdSFOg3FYhtVP83bAq7FTV/3suGQDl/43MixfQG7+bl+hlrP4mz6WlD2SUb2u9BomnJWl1uey9w=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@9.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w=="],
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SHOxfCcZV1axeIGfyeD1BkdLvfQgjmPy18tO0OUXGElcdScxD6MqU5rj/AVtiuBT+51GtFfOKlwl1+BdVwhD1A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mgEkYrJ+N90sgEDqEZ07zH+4I1D28WjqAhdzfW3aS2x2vynVpoY9jWfHuH8S62vZt3uATJrTKTRa8CjPWEsrdw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@9.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.2.0", "", { "os": "linux", "cpu": "none" }, "sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@9.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g=="],
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@9.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA=="],
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.2.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@9.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw=="],
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-AI4KIpS8Zf6vwfOPk0uQPSC0pQ1m5HU4hCbtrgL21JgJSlnJaeEu3/aoOBB45AXKiExBU9R+CDR7aSnW7uhc5A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@9.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg=="],
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-r19cQc7HaEJ76HFsMsbiKMTIV2YqFGSof8H5hB7e5Jkb/23Y8Isv1YrSzkDaGhcw02I/COsrPo+eEmjy35eFuA=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
 | 
			
		||||
 | 
			
		||||
@@ -179,41 +176,41 @@
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
 | 
			
		||||
 | 
			
		||||
    "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="],
 | 
			
		||||
    "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
 | 
			
		||||
 | 
			
		||||
    "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.8", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.8" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA=="],
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.11", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.11" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="],
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="],
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="],
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="],
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="],
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="],
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="],
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="],
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.8", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "postcss": "^8.4.41", "tailwindcss": "4.1.8" } }, "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw=="],
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
 | 
			
		||||
 | 
			
		||||
    "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
 | 
			
		||||
 | 
			
		||||
@@ -221,39 +218,39 @@
 | 
			
		||||
 | 
			
		||||
    "@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
 | 
			
		||||
 | 
			
		||||
    "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
 | 
			
		||||
    "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
 | 
			
		||||
    "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="],
 | 
			
		||||
    "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="],
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/type-utils": "8.39.1", "@typescript-eslint/utils": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="],
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1" } }, "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA=="],
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.1", "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="],
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1" } }, "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="],
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="],
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.1", "@typescript-eslint/tsconfig-utils": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA=="],
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.39.1", "", {}, "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="],
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.1", "@typescript-eslint/tsconfig-utils": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="],
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -271,7 +268,7 @@
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
 | 
			
		||||
    "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -303,27 +300,27 @@
 | 
			
		||||
 | 
			
		||||
    "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
 | 
			
		||||
 | 
			
		||||
    "elysia": ["elysia@1.3.4", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="],
 | 
			
		||||
    "elysia": ["elysia@1.3.8", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.3", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kxYFhegJbUEf5otzmisEvGt3R7d/dPBNVERO2nHo0kFqKBHyj5slArc90mSRKLfi1vamMtPcz67rL6Zeg5F2yg=="],
 | 
			
		||||
 | 
			
		||||
    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 | 
			
		||||
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
 | 
			
		||||
 | 
			
		||||
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 | 
			
		||||
 | 
			
		||||
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="],
 | 
			
		||||
    "eslint": ["eslint@9.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.33.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.1.0", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "postcss": "^8.5.4", "postcss-import": "^16.1.0", "synckit": "0.9.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.0.0" } }, "sha512-qaOnCBBvkxq5O1CPwzD8NWTNbBLY5RtjfpbOXiv0MtjX5GHnraj/cKBvjrfAPGH7FWrDeycR+kQ52aYqNWpujw=="],
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.7.4", "", { "dependencies": { "@eslint/css-tree": "^3.6.3", "enhanced-resolve": "^5.18.2", "jiti": "^2.5.1", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "synckit": "^0.11.11", "tailwind-csstree": "^0.1.2", "tsconfig-paths-webpack-plugin": "^4.2.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.1.6" } }, "sha512-jlLHLoqrNbcqqROVjFojGDv1m2LGiJwFqynbARbyeRj9rc1Hmh46EeQhmYVQihhD4j+DSxG/bcwoA9PABIpLmw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
 | 
			
		||||
 | 
			
		||||
    "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
 | 
			
		||||
    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
 | 
			
		||||
 | 
			
		||||
    "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
 | 
			
		||||
 | 
			
		||||
@@ -333,7 +330,7 @@
 | 
			
		||||
 | 
			
		||||
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
 | 
			
		||||
 | 
			
		||||
    "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
 | 
			
		||||
    "exact-mirror": ["exact-mirror@0.1.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-yI62LpSby0ItzPJF05C4DRycVAoknRiCIDOLOCCs9zaEKylOXQtOFM3flX54S44swpRz584vk3P70yWQodsLlg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
 | 
			
		||||
 | 
			
		||||
@@ -347,7 +344,7 @@
 | 
			
		||||
 | 
			
		||||
    "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
 | 
			
		||||
 | 
			
		||||
    "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="],
 | 
			
		||||
    "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
 | 
			
		||||
 | 
			
		||||
    "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
 | 
			
		||||
 | 
			
		||||
@@ -363,7 +360,7 @@
 | 
			
		||||
 | 
			
		||||
    "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
 | 
			
		||||
 | 
			
		||||
    "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="],
 | 
			
		||||
    "formatly": ["formatly@0.2.4", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA=="],
 | 
			
		||||
 | 
			
		||||
    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 | 
			
		||||
 | 
			
		||||
@@ -371,7 +368,7 @@
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
 | 
			
		||||
    "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
 | 
			
		||||
 | 
			
		||||
    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -401,7 +398,7 @@
 | 
			
		||||
 | 
			
		||||
    "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
 | 
			
		||||
 | 
			
		||||
    "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
 | 
			
		||||
    "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
 | 
			
		||||
 | 
			
		||||
    "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
 | 
			
		||||
 | 
			
		||||
@@ -419,9 +416,11 @@
 | 
			
		||||
 | 
			
		||||
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 | 
			
		||||
 | 
			
		||||
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 | 
			
		||||
 | 
			
		||||
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 | 
			
		||||
 | 
			
		||||
    "knip": ["knip@5.59.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^9.0.2", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-pOMBw6sLQhi/RfnpI6TwBY6NrAtKXDO5wkmMm+pCsSK5eWbVfDnDtPXbLDGNCoZPXiuAojb27y4XOpp4JPNxlA=="],
 | 
			
		||||
    "knip": ["knip@5.62.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.4", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.1.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.3.4", "strip-json-comments": "5.0.2", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A=="],
 | 
			
		||||
 | 
			
		||||
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -453,6 +452,8 @@
 | 
			
		||||
 | 
			
		||||
    "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
 | 
			
		||||
 | 
			
		||||
    "mdn-data": ["mdn-data@2.21.0", "", {}, "sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ=="],
 | 
			
		||||
 | 
			
		||||
    "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
 | 
			
		||||
 | 
			
		||||
    "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
 | 
			
		||||
@@ -489,7 +490,7 @@
 | 
			
		||||
 | 
			
		||||
    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
 | 
			
		||||
 | 
			
		||||
    "oxc-resolver": ["oxc-resolver@9.0.2", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "9.0.2", "@oxc-resolver/binding-darwin-x64": "9.0.2", "@oxc-resolver/binding-freebsd-x64": "9.0.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "9.0.2", "@oxc-resolver/binding-linux-arm64-gnu": "9.0.2", "@oxc-resolver/binding-linux-arm64-musl": "9.0.2", "@oxc-resolver/binding-linux-riscv64-gnu": "9.0.2", "@oxc-resolver/binding-linux-s390x-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-musl": "9.0.2", "@oxc-resolver/binding-wasm32-wasi": "9.0.2", "@oxc-resolver/binding-win32-arm64-msvc": "9.0.2", "@oxc-resolver/binding-win32-x64-msvc": "9.0.2" } }, "sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA=="],
 | 
			
		||||
    "oxc-resolver": ["oxc-resolver@11.2.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "11.2.0", "@oxc-resolver/binding-darwin-x64": "11.2.0", "@oxc-resolver/binding-freebsd-x64": "11.2.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.2.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.2.0", "@oxc-resolver/binding-linux-arm64-musl": "11.2.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.2.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.2.0", "@oxc-resolver/binding-linux-x64-gnu": "11.2.0", "@oxc-resolver/binding-linux-x64-musl": "11.2.0", "@oxc-resolver/binding-wasm32-wasi": "11.2.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.2.0", "@oxc-resolver/binding-win32-x64-msvc": "11.2.0" } }, "sha512-3iJYyIdDZMDoj0ZSVBrI1gUvPBMkDC4gxonBG+7uqUyK5EslG0mCwnf6qhxK8oEU7jLHjbRBNyzflPSd3uvH7Q=="],
 | 
			
		||||
 | 
			
		||||
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -513,15 +514,15 @@
 | 
			
		||||
 | 
			
		||||
    "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
 | 
			
		||||
 | 
			
		||||
    "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
 | 
			
		||||
    "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-import": ["postcss-import@16.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg=="],
 | 
			
		||||
    "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
 | 
			
		||||
 | 
			
		||||
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 | 
			
		||||
 | 
			
		||||
    "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
 | 
			
		||||
    "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
 | 
			
		||||
 | 
			
		||||
    "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
 | 
			
		||||
 | 
			
		||||
@@ -555,7 +556,7 @@
 | 
			
		||||
 | 
			
		||||
    "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
 | 
			
		||||
 | 
			
		||||
    "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
 | 
			
		||||
    "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
@@ -563,7 +564,9 @@
 | 
			
		||||
 | 
			
		||||
    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="],
 | 
			
		||||
    "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
 | 
			
		||||
 | 
			
		||||
    "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
 | 
			
		||||
 | 
			
		||||
@@ -571,11 +574,13 @@
 | 
			
		||||
 | 
			
		||||
    "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
 | 
			
		||||
 | 
			
		||||
    "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
 | 
			
		||||
    "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-csstree": ["tailwind-csstree@0.1.3", "", {}, "sha512-LfOT807005OVfyxAjHpOajlIgoEaE894jqjkrhONC/HqBLS8OAhhNifnNs3Y5wD26eIdf0vk1zu9gja2oI3/1Q=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
 | 
			
		||||
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="],
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
 | 
			
		||||
 | 
			
		||||
    "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -589,23 +594,27 @@
 | 
			
		||||
 | 
			
		||||
    "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="],
 | 
			
		||||
 | 
			
		||||
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 | 
			
		||||
 | 
			
		||||
    "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
 | 
			
		||||
    "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
 | 
			
		||||
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="],
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.39.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/parser": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg=="],
 | 
			
		||||
 | 
			
		||||
    "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
 | 
			
		||||
    "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
 | 
			
		||||
 | 
			
		||||
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 | 
			
		||||
 | 
			
		||||
    "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
 | 
			
		||||
 | 
			
		||||
    "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="],
 | 
			
		||||
    "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
 | 
			
		||||
 | 
			
		||||
    "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
 | 
			
		||||
 | 
			
		||||
@@ -631,12 +640,20 @@
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
@@ -645,7 +662,7 @@
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
@@ -667,8 +684,50 @@
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,5 +15,6 @@ services:
 | 
			
		||||
      # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
 | 
			
		||||
      # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
 | 
			
		||||
      - TZ=Europe/Stockholm # set your timezone, defaults to UTC
 | 
			
		||||
      # - UNAUTHENTICATED_USER_SHARING=true # for use with ALLOW_UNAUTHENTICATED=true to share history with all unauthenticated users / devices
 | 
			
		||||
    ports:
 | 
			
		||||
      - 3000:3000
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,19 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import eslintParserTypeScript from "@typescript-eslint/parser";
 | 
			
		||||
import type { Linter } from "eslint";
 | 
			
		||||
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
 | 
			
		||||
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  js.configs.recommended,
 | 
			
		||||
  ...tseslint.configs.recommended,
 | 
			
		||||
  // ...tailwind.configs["flat/recommended"],
 | 
			
		||||
  tseslint.configs.recommended,
 | 
			
		||||
  {
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "simple-import-sort": simpleImportSortPlugin,
 | 
			
		||||
      "better-tailwindcss": eslintPluginBetterTailwindcss,
 | 
			
		||||
    },
 | 
			
		||||
    ignores: ["**/node_modules/**"],
 | 
			
		||||
    ignores: ["**/node_modules/**", "eslint.config.ts"],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      parser: eslintParserTypeScript,
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
@@ -26,10 +24,9 @@ export default [
 | 
			
		||||
      },
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.node,
 | 
			
		||||
        ...globals.browser,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
 | 
			
		||||
    files: ["**/*.{tsx,ts}"],
 | 
			
		||||
    settings: {
 | 
			
		||||
      "better-tailwindcss": {
 | 
			
		||||
        entryPoint: "src/main.css",
 | 
			
		||||
@@ -39,7 +36,7 @@ export default [
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules,
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules,
 | 
			
		||||
      // "tailwindcss/classnames-order": "off",
 | 
			
		||||
      "better-tailwindcss/multiline": [
 | 
			
		||||
      "better-tailwindcss/enforce-consistent-line-wrapping": [
 | 
			
		||||
        "warn",
 | 
			
		||||
        {
 | 
			
		||||
          group: "newLine",
 | 
			
		||||
@@ -63,4 +60,13 @@ export default [
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
] as Linter.Config[];
 | 
			
		||||
  {
 | 
			
		||||
    files: ["**/*.{js,cjs,mjs,jsx}"],
 | 
			
		||||
    extends: [tseslint.configs.disableTypeChecked],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.browser,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://unpkg.com/knip@5/schema.json",
 | 
			
		||||
  "entry": ["src/index.tsx"],
 | 
			
		||||
  "project": ["src/**/*.ts", "src/**/*.tsx", "src/main.css"],
 | 
			
		||||
  "entry": ["tests/**/*.test.ts"],
 | 
			
		||||
  "project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "entry": ["src/main.css"]
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,13 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "convertx-frontend",
 | 
			
		||||
  "version": "0.14.0",
 | 
			
		||||
  "version": "0.15.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "bun run --watch src/index.tsx",
 | 
			
		||||
    "hot": "bun run --hot src/index.tsx",
 | 
			
		||||
    "format": "run-p 'format:*'",
 | 
			
		||||
    "format:eslint": "eslint --fix .",
 | 
			
		||||
    "format:prettier": "prettier --write .",
 | 
			
		||||
    "build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css",
 | 
			
		||||
    "build:js": "tsc",
 | 
			
		||||
    "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js",
 | 
			
		||||
    "lint": "run-p 'lint:*'",
 | 
			
		||||
    "lint:tsc": "tsc --noEmit",
 | 
			
		||||
    "lint:knip": "knip",
 | 
			
		||||
@@ -15,12 +16,13 @@
 | 
			
		||||
    "lint:prettier": "prettier --check ."
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@elysiajs/html": "^1.3.0",
 | 
			
		||||
    "@elysiajs/jwt": "^1.3.1",
 | 
			
		||||
    "@elysiajs/html": "^1.3.1",
 | 
			
		||||
    "@elysiajs/jwt": "^1.3.2",
 | 
			
		||||
    "@elysiajs/static": "^1.3.0",
 | 
			
		||||
    "@kitajs/html": "^4.2.9",
 | 
			
		||||
    "elysia": "^1.3.4",
 | 
			
		||||
    "sanitize-filename": "^1.6.3"
 | 
			
		||||
    "elysia": "^1.3.8",
 | 
			
		||||
    "sanitize-filename": "^1.6.3",
 | 
			
		||||
    "tar": "^7.4.3"
 | 
			
		||||
  },
 | 
			
		||||
  "module": "src/index.tsx",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
@@ -28,27 +30,27 @@
 | 
			
		||||
    "start": "bun run src/index.tsx"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.28.0",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.1",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.8",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.8",
 | 
			
		||||
    "@eslint/js": "^9.33.0",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.6.2",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.2",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.11",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.11",
 | 
			
		||||
    "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
    "@types/bun": "^1.2.15",
 | 
			
		||||
    "@types/node": "^22.15.29",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.33.1",
 | 
			
		||||
    "eslint": "^9.28.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.1.0",
 | 
			
		||||
    "@types/bun": "latest",
 | 
			
		||||
    "@types/node": "^24.2.1",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.39.1",
 | 
			
		||||
    "eslint": "^9.33.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.7.4",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
    "globals": "^16.2.0",
 | 
			
		||||
    "knip": "^5.59.1",
 | 
			
		||||
    "globals": "^16.3.0",
 | 
			
		||||
    "knip": "^5.62.0",
 | 
			
		||||
    "npm-run-all2": "^8.0.4",
 | 
			
		||||
    "postcss": "^8.5.4",
 | 
			
		||||
    "prettier": "^3.5.3",
 | 
			
		||||
    "postcss": "^8.5.6",
 | 
			
		||||
    "prettier": "^3.6.2",
 | 
			
		||||
    "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
    "tailwindcss": "^4.1.8",
 | 
			
		||||
    "typescript": "^5.8.3",
 | 
			
		||||
    "typescript-eslint": "^8.33.1"
 | 
			
		||||
    "tailwindcss": "^4.1.11",
 | 
			
		||||
    "typescript": "^5.9.2",
 | 
			
		||||
    "typescript-eslint": "^8.39.1"
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 7.7 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB  | 
| 
		 Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.6 KiB  | 
| 
		 Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 405 B  | 
| 
		 Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 831 B  | 
| 
		 Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="512" height="512" style="cursor:default" viewBox="0 0 96.983 132.292"><g transform="translate(-56.568 -82.29)"><path d="M124.878 83.82h-60.91a5.86 5.86 0 0 0-5.87 5.87v117.496a5.855 5.855 0 0 0 5.87 5.866h82.182a5.855 5.855 0 0 0 5.87-5.866v-92.55z" style="display:inline;fill:#111827;stroke:#aeb9d0;stroke-width:3.06006;stroke-dasharray:none;stroke-opacity:1"/><circle cx="84.331" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="128.904" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="148.438" r="6.653" style="display:inline;fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="167.971" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><path d="M119.124 161.326h13.287v13.287h-13.287z" style="fill:#84cc16;fill-opacity:1;stroke:none;stroke-width:3.04496;stroke-opacity:1"/></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
@@ -1,18 +1,4 @@
 | 
			
		||||
const webroot = document.querySelector("meta[name='webroot']").content;
 | 
			
		||||
 | 
			
		||||
window.downloadAll = function () {
 | 
			
		||||
  // Get all download links
 | 
			
		||||
  const downloadLinks = document.querySelectorAll("a[download]");
 | 
			
		||||
 | 
			
		||||
  // Trigger download for each link
 | 
			
		||||
  downloadLinks.forEach((link, index) => {
 | 
			
		||||
    // We add a delay for each download to prevent them from starting at the same time
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const event = new MouseEvent("click");
 | 
			
		||||
      link.dispatchEvent(event);
 | 
			
		||||
    }, index * 100);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
const jobId = window.location.pathname.split("/").pop();
 | 
			
		||||
const main = document.querySelector("main");
 | 
			
		||||
let progressElem = document.querySelector("progress");
 | 
			
		||||
 
 | 
			
		||||
@@ -4,5 +4,6 @@
 | 
			
		||||
  "lockFileMaintenance": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "automerge": true
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  "ignoreDeps": ["bun-types", "@types/bun"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ export const BaseHtml = ({
 | 
			
		||||
      <link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
 | 
			
		||||
      <link rel="manifest" href={`${webroot}/site.webmanifest`} />
 | 
			
		||||
    </head>
 | 
			
		||||
    <body class="flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200">
 | 
			
		||||
    <body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
 | 
			
		||||
      {children}
 | 
			
		||||
      <footer class="w-full">
 | 
			
		||||
        <div class="p-4 text-center text-sm text-neutral-500">
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ export const Header = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header class="w-full p-4">
 | 
			
		||||
      <nav class="mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4">
 | 
			
		||||
      <nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}>
 | 
			
		||||
        <ul>
 | 
			
		||||
          <li>
 | 
			
		||||
            <strong>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -116,8 +117,8 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -39,6 +40,7 @@ export const properties = {
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "html",
 | 
			
		||||
      "htmlz",
 | 
			
		||||
      "kepub.epub",
 | 
			
		||||
      "lit",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "mobi",
 | 
			
		||||
@@ -61,8 +63,8 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const inputArgs: string[] = [];
 | 
			
		||||
  if (fileType === "eps") {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -691,8 +692,8 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let extraArgs: string[] = [];
 | 
			
		||||
  let message = "Done";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -313,8 +314,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -445,8 +446,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let outputArgs: string[] = [];
 | 
			
		||||
  let inputArgs: string[] = [];
 | 
			
		||||
@@ -460,6 +461,13 @@ export function convert(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle EMF files specifically to avoid LibreOffice delegate issues
 | 
			
		||||
  if (fileType === "emf") {
 | 
			
		||||
    // Use direct conversion without delegates for EMF files
 | 
			
		||||
    inputArgs.push("-define", "emf:delegate=false", "-density", "300");
 | 
			
		||||
    outputArgs.push("-background", "white", "-alpha", "remove");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile(
 | 
			
		||||
      "magick",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -32,8 +33,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -17,8 +18,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let tool = "";
 | 
			
		||||
  if (fileType === "jxl") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								src/converters/libreoffice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,177 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "602",
 | 
			
		||||
      "abw",
 | 
			
		||||
      "csv",
 | 
			
		||||
      "cwk",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "hwp",
 | 
			
		||||
      "mcw",
 | 
			
		||||
      "mw",
 | 
			
		||||
      "mwd",
 | 
			
		||||
      "lwp",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pages",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "psw",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "sdw",
 | 
			
		||||
      "stw",
 | 
			
		||||
      "sxw",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wn",
 | 
			
		||||
      "wpd",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "wri",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "zabw",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "csv",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type FileCategories = "text" | "calc";
 | 
			
		||||
 | 
			
		||||
const filters: Record<FileCategories, Record<string, string>> = {
 | 
			
		||||
  text: {
 | 
			
		||||
    "602": "T602Document",
 | 
			
		||||
    abw: "AbiWord",
 | 
			
		||||
    csv: "Text",
 | 
			
		||||
    doc: "MS Word 97",
 | 
			
		||||
    docm: "MS Word 2007 XML VBA",
 | 
			
		||||
    docx: "MS Word 2007 XML",
 | 
			
		||||
    dot: "MS Word 97 Vorlage",
 | 
			
		||||
    dotx: "MS Word 2007 XML Template",
 | 
			
		||||
    dotm: "MS Word 2007 XML Template",
 | 
			
		||||
    epub: "EPUB",
 | 
			
		||||
    fb2: "Fictionbook 2",
 | 
			
		||||
    fodt: "OpenDocument Text Flat XML",
 | 
			
		||||
    htm: "HTML (StarWriter)",
 | 
			
		||||
    html: "HTML (StarWriter)",
 | 
			
		||||
    hwp: "writer_MIZI_Hwp_97",
 | 
			
		||||
    mcw: "MacWrite",
 | 
			
		||||
    mw: "MacWrite",
 | 
			
		||||
    mwd: "Mariner_Write",
 | 
			
		||||
    lwp: "LotusWordPro",
 | 
			
		||||
    lrf: "BroadBand eBook",
 | 
			
		||||
    odt: "writer8",
 | 
			
		||||
    ott: "writer8_template",
 | 
			
		||||
    pages: "Apple Pages",
 | 
			
		||||
    // pdf: "writer_pdf_import",
 | 
			
		||||
    psw: "PocketWord File",
 | 
			
		||||
    rtf: "Rich Text Format",
 | 
			
		||||
    sdw: "StarOffice_Writer",
 | 
			
		||||
    stw: "writer_StarOffice_XML_Writer_Template",
 | 
			
		||||
    sxw: "StarOffice XML (Writer)",
 | 
			
		||||
    tab: "Text",
 | 
			
		||||
    tsv: "Text",
 | 
			
		||||
    txt: "Text",
 | 
			
		||||
    wn: "WriteNow",
 | 
			
		||||
    wpd: "WordPerfect",
 | 
			
		||||
    wps: "MS Word 97",
 | 
			
		||||
    wpt: "MS Word 97 Vorlage",
 | 
			
		||||
    wri: "MS_Write",
 | 
			
		||||
    xhtml: "HTML (StarWriter)",
 | 
			
		||||
    xml: "OpenDocument Text Flat XML",
 | 
			
		||||
    zabw: "AbiWord",
 | 
			
		||||
  },
 | 
			
		||||
  calc: {},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFilters = (fileType: string, converto: string) => {
 | 
			
		||||
  if (fileType in filters.text && converto in filters.text) {
 | 
			
		||||
    return [filters.text[fileType], filters.text[converto]];
 | 
			
		||||
  } else if (fileType in filters.calc && converto in filters.calc) {
 | 
			
		||||
    return [filters.calc[fileType], filters.calc[converto]];
 | 
			
		||||
  }
 | 
			
		||||
  return [null, null];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
 | 
			
		||||
 | 
			
		||||
  // Build arguments array
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
  args.push("--headless");
 | 
			
		||||
  const [inFilter, outFilter] = getFilters(fileType, convertTo);
 | 
			
		||||
 | 
			
		||||
  if (inFilter) {
 | 
			
		||||
    args.push(`--infilter="${inFilter}"`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (outFilter) {
 | 
			
		||||
    args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
 | 
			
		||||
  } else {
 | 
			
		||||
    args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("soffice", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,7 @@
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { Cookie } from "elysia";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { MAX_CONVERT_PROCESS } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
 | 
			
		||||
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
 | 
			
		||||
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
 | 
			
		||||
@@ -11,10 +14,13 @@ import { convert as convertImagemagick, properties as propertiesImagemagick } fr
 | 
			
		||||
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
 | 
			
		||||
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
 | 
			
		||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
 | 
			
		||||
import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
 | 
			
		||||
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
 | 
			
		||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
 | 
			
		||||
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
 | 
			
		||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
 | 
			
		||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
 | 
			
		||||
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
 | 
			
		||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
 | 
			
		||||
 | 
			
		||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
 | 
			
		||||
@@ -47,6 +53,11 @@ const properties: Record<
 | 
			
		||||
    ) => unknown;
 | 
			
		||||
  }
 | 
			
		||||
> = {
 | 
			
		||||
  // Prioritize Inkscape for EMF files as it handles them better than ImageMagick
 | 
			
		||||
  inkscape: {
 | 
			
		||||
    properties: propertiesInkscape,
 | 
			
		||||
    converter: convertInkscape,
 | 
			
		||||
  },
 | 
			
		||||
  libjxl: {
 | 
			
		||||
    properties: propertiesLibjxl,
 | 
			
		||||
    converter: convertLibjxl,
 | 
			
		||||
@@ -71,10 +82,18 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesCalibre,
 | 
			
		||||
    converter: convertCalibre,
 | 
			
		||||
  },
 | 
			
		||||
  libreoffice: {
 | 
			
		||||
    properties: propertiesLibreOffice,
 | 
			
		||||
    converter: convertLibreOffice,
 | 
			
		||||
  },
 | 
			
		||||
  pandoc: {
 | 
			
		||||
    properties: propertiesPandoc,
 | 
			
		||||
    converter: convertPandoc,
 | 
			
		||||
  },
 | 
			
		||||
  msgconvert: {
 | 
			
		||||
    properties: propertiesMsgconvert,
 | 
			
		||||
    converter: convertMsgconvert,
 | 
			
		||||
  },
 | 
			
		||||
  dvisvgm: {
 | 
			
		||||
    properties: propertiesDvisvgm,
 | 
			
		||||
    converter: convertDvisvgm,
 | 
			
		||||
@@ -87,10 +106,6 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesGraphicsmagick,
 | 
			
		||||
    converter: convertGraphicsmagick,
 | 
			
		||||
  },
 | 
			
		||||
  inkscape: {
 | 
			
		||||
    properties: propertiesInkscape,
 | 
			
		||||
    converter: convertInkscape,
 | 
			
		||||
  },
 | 
			
		||||
  assimp: {
 | 
			
		||||
    properties: propertiesassimp,
 | 
			
		||||
    converter: convertassimp,
 | 
			
		||||
@@ -103,9 +118,63 @@ const properties: Record<
 | 
			
		||||
    properties: propertiesPotrace,
 | 
			
		||||
    converter: convertPotrace,
 | 
			
		||||
  },
 | 
			
		||||
  vtracer: {
 | 
			
		||||
    properties: propertiesVtracer,
 | 
			
		||||
    converter: convertVtracer,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function mainConverter(
 | 
			
		||||
function chunks<T>(arr: T[], size: number): T[][] {
 | 
			
		||||
  if (size <= 0) {
 | 
			
		||||
    return [arr];
 | 
			
		||||
  }
 | 
			
		||||
  return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
 | 
			
		||||
    arr.slice(i * size, i * size + size),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleConvert(
 | 
			
		||||
  fileNames: string[],
 | 
			
		||||
  userUploadsDir: string,
 | 
			
		||||
  userOutputDir: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  converterName: string,
 | 
			
		||||
  jobId: Cookie<string | undefined>,
 | 
			
		||||
) {
 | 
			
		||||
  const query = db.query(
 | 
			
		||||
    "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
 | 
			
		||||
    const toProcess: Promise<string>[] = [];
 | 
			
		||||
    for (const fileName of chunk) {
 | 
			
		||||
      const filePath = `${userUploadsDir}${fileName}`;
 | 
			
		||||
      const fileTypeOrig = fileName.split(".").pop() ?? "";
 | 
			
		||||
      const fileType = normalizeFiletype(fileTypeOrig);
 | 
			
		||||
      const newFileExt = normalizeOutputFiletype(convertTo);
 | 
			
		||||
      const newFileName = fileName.replace(
 | 
			
		||||
        new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
 | 
			
		||||
        newFileExt,
 | 
			
		||||
      );
 | 
			
		||||
      const targetPath = `${userOutputDir}${newFileName}`;
 | 
			
		||||
      toProcess.push(
 | 
			
		||||
        new Promise((resolve, reject) => {
 | 
			
		||||
          mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
 | 
			
		||||
            .then((r) => {
 | 
			
		||||
              if (jobId.value) {
 | 
			
		||||
                query.run(jobId.value, fileName, newFileName, r);
 | 
			
		||||
              }
 | 
			
		||||
              resolve(r);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((c) => reject(c));
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(toProcess);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mainConverter(
 | 
			
		||||
  inputFilePath: string,
 | 
			
		||||
  fileTypeOriginal: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/converters/msgconvert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    email: ["msg"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    email: ["eml"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    if (fileType === "msg" && convertTo === "eml") {
 | 
			
		||||
      // Convert MSG to EML using msgconvert
 | 
			
		||||
      // msgconvert will output to the same directory as the input file with .eml extension
 | 
			
		||||
      // We need to use --outfile to specify the target path
 | 
			
		||||
      const args = ["--outfile", targetPath, filePath];
 | 
			
		||||
 | 
			
		||||
      execFile("msgconvert", args, (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(new Error(`msgconvert failed: ${error.message}`));
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          // Log sanitized stderr to avoid exposing sensitive paths
 | 
			
		||||
          const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resolve(targetPath);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      reject(
 | 
			
		||||
        new Error(
 | 
			
		||||
          `Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -124,8 +125,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // set xelatex here
 | 
			
		||||
  const xelatex = ["pdf", "latex"];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -26,8 +27,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/converters/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
export type ExecFileFn = (
 | 
			
		||||
  cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  options?: import("child_process").ExecFileOptions,
 | 
			
		||||
) => void;
 | 
			
		||||
 | 
			
		||||
export type ConvertFnWithExecFile = (
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options: unknown,
 | 
			
		||||
  execFileOverride?: ExecFileFn,
 | 
			
		||||
) => Promise<string>;
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -94,8 +95,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // if (fileType === "svg") {
 | 
			
		||||
  //   const scale = options.scale || 1;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/converters/vtracer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif", "webp"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["svg"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface VTracerOptions {
 | 
			
		||||
  colormode?: string;
 | 
			
		||||
  hierarchical?: string;
 | 
			
		||||
  mode?: string;
 | 
			
		||||
  filter_speckle?: string | number;
 | 
			
		||||
  color_precision?: string | number;
 | 
			
		||||
  layer_difference?: string | number;
 | 
			
		||||
  corner_threshold?: string | number;
 | 
			
		||||
  length_threshold?: string | number;
 | 
			
		||||
  max_iterations?: string | number;
 | 
			
		||||
  splice_threshold?: string | number;
 | 
			
		||||
  path_precision?: string | number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // Build vtracer arguments
 | 
			
		||||
    const args = ["--input", filePath, "--output", targetPath];
 | 
			
		||||
 | 
			
		||||
    // Add optional parameter if provided
 | 
			
		||||
    if (options && typeof options === "object") {
 | 
			
		||||
      const opts = options as VTracerOptions;
 | 
			
		||||
      const validOptions: Array<keyof VTracerOptions> = [
 | 
			
		||||
        "colormode",
 | 
			
		||||
        "hierarchical",
 | 
			
		||||
        "mode",
 | 
			
		||||
        "filter_speckle",
 | 
			
		||||
        "color_precision",
 | 
			
		||||
        "layer_difference",
 | 
			
		||||
        "corner_threshold",
 | 
			
		||||
        "length_threshold",
 | 
			
		||||
        "max_iterations",
 | 
			
		||||
        "splice_threshold",
 | 
			
		||||
        "path_precision",
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      for (const option of validOptions) {
 | 
			
		||||
        if (opts[option] !== undefined) {
 | 
			
		||||
          args.push(`--${option}`, String(opts[option]));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execFile("vtracer", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}${stderr ? `\nstderr: ${stderr}` : ""}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.log(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { execFile } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,8 +15,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
 | 
			
		||||
 
 | 
			
		||||
@@ -13,3 +13,13 @@ export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
 | 
			
		||||
export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const WEBROOT = process.env.WEBROOT ?? "";
 | 
			
		||||
 | 
			
		||||
export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en";
 | 
			
		||||
 | 
			
		||||
export const MAX_CONVERT_PROCESS =
 | 
			
		||||
  process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0
 | 
			
		||||
    ? Number(process.env.MAX_CONVERT_PROCESS)
 | 
			
		||||
    : 0;
 | 
			
		||||
 | 
			
		||||
export const UNAUTHENTICATED_USER_SHARING =
 | 
			
		||||
  process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;
 | 
			
		||||
 
 | 
			
		||||
@@ -144,6 +144,26 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("soffice --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("libreoffice is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("msgconvert --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("msgconvert (libemail-outlook-message-perl) is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("bun -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Bun is not installed. wait what");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								src/main.css
									
									
									
									
									
								
							
							
						
						@@ -1,21 +1,22 @@
 | 
			
		||||
@import "./theme/theme.css";
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
 | 
			
		||||
@plugin 'tailwind-scrollbar';
 | 
			
		||||
@plugin "tailwind-scrollbar";
 | 
			
		||||
 | 
			
		||||
@theme {
 | 
			
		||||
  --color-contrast: rgba(var(--contrast));
 | 
			
		||||
  --color-neutral-900: rgba(var(--neutral-900));
 | 
			
		||||
  --color-neutral-800: rgba(var(--neutral-800));
 | 
			
		||||
  --color-neutral-700: rgba(var(--neutral-700));
 | 
			
		||||
  --color-neutral-600: rgba(var(--neutral-600));
 | 
			
		||||
  --color-neutral-500: rgba(var(--neutral-500));
 | 
			
		||||
  --color-neutral-400: rgba(var(--neutral-400));
 | 
			
		||||
  --color-neutral-300: rgba(var(--neutral-300));
 | 
			
		||||
  --color-neutral-200: rgba(var(--neutral-200));
 | 
			
		||||
  --color-neutral-100: rgba(var(--neutral-100));
 | 
			
		||||
  --color-accent-600: rgba(var(--accent-600));
 | 
			
		||||
  --color-accent-500: rgba(var(--accent-500));
 | 
			
		||||
  --color-accent-400: rgba(var(--accent-400));
 | 
			
		||||
  --color-contrast: var(--contrast);
 | 
			
		||||
  --color-neutral-900: var(--neutral-900);
 | 
			
		||||
  --color-neutral-800: var(--neutral-800);
 | 
			
		||||
  --color-neutral-700: var(--neutral-700);
 | 
			
		||||
  --color-neutral-600: var(--neutral-600);
 | 
			
		||||
  --color-neutral-500: var(--neutral-500);
 | 
			
		||||
  --color-neutral-400: var(--neutral-400);
 | 
			
		||||
  --color-neutral-300: var(--neutral-300);
 | 
			
		||||
  --color-neutral-200: var(--neutral-200);
 | 
			
		||||
  --color-neutral-100: var(--neutral-100);
 | 
			
		||||
  --color-accent-600: var(--accent-600);
 | 
			
		||||
  --color-accent-500: var(--accent-500);
 | 
			
		||||
  --color-accent-400: var(--accent-400);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@utility article {
 | 
			
		||||
@@ -29,36 +30,3 @@
 | 
			
		||||
@utility btn-secondary {
 | 
			
		||||
  @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --contrast: 255, 255, 255;
 | 
			
		||||
  --neutral-900: 243, 244, 246;
 | 
			
		||||
  --neutral-800: 229, 231, 235;
 | 
			
		||||
  --neutral-700: 209, 213, 219;
 | 
			
		||||
  --neutral-600: 156, 163, 175;
 | 
			
		||||
  --neutral-500: 180, 180, 180;
 | 
			
		||||
  --neutral-400: 75, 85, 99;
 | 
			
		||||
  --neutral-300: 55, 65, 81;
 | 
			
		||||
  --neutral-200: 31, 41, 55;
 | 
			
		||||
  --neutral-100: 17, 24, 39;
 | 
			
		||||
  --accent-400: 132, 204, 22;
 | 
			
		||||
  --accent-500: 101, 163, 13;
 | 
			
		||||
  --accent-600: 77, 124, 15;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    --contrast: 0, 0, 0;
 | 
			
		||||
    --neutral-900: 17, 24, 39;
 | 
			
		||||
    --neutral-800: 31, 41, 55;
 | 
			
		||||
    --neutral-700: 55, 65, 81;
 | 
			
		||||
    --neutral-600: 75, 85, 99;
 | 
			
		||||
    --neutral-500: 107, 114, 128;
 | 
			
		||||
    --neutral-300: 209, 213, 219;
 | 
			
		||||
    --neutral-400: 156, 163, 175;
 | 
			
		||||
    --neutral-200: 229, 231, 235;
 | 
			
		||||
    --accent-600: 101, 163, 13;
 | 
			
		||||
    --accent-500: 132, 204, 22;
 | 
			
		||||
    --accent-400: 163, 230, 53;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ export const chooseConverter = new Elysia().use(userService).post(
 | 
			
		||||
        >
 | 
			
		||||
          {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
 | 
			
		||||
            <article
 | 
			
		||||
              class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
 | 
			
		||||
              class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
 | 
			
		||||
              data-converter={converter}
 | 
			
		||||
            >
 | 
			
		||||
              <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import { mkdir } from "node:fs/promises";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import { outputDir, uploadsDir } from "..";
 | 
			
		||||
import { mainConverter } from "../converters/main";
 | 
			
		||||
import { handleConvert } from "../converters/main";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Jobs } from "../db/types";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const convert = new Elysia().use(userService).post(
 | 
			
		||||
@@ -46,6 +46,11 @@ export const convert = new Elysia().use(userService).post(
 | 
			
		||||
 | 
			
		||||
    const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
 | 
			
		||||
    const converterName = body.convert_to.split(",")[1];
 | 
			
		||||
 | 
			
		||||
    if (!converterName) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fileNames = JSON.parse(body.file_names) as string[];
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < fileNames.length; i++) {
 | 
			
		||||
@@ -61,36 +66,8 @@ export const convert = new Elysia().use(userService).post(
 | 
			
		||||
      jobId.value,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const query = db.query(
 | 
			
		||||
      "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Start the conversion process in the background
 | 
			
		||||
    Promise.all(
 | 
			
		||||
      fileNames.map(async (fileName) => {
 | 
			
		||||
        const filePath = `${userUploadsDir}${fileName}`;
 | 
			
		||||
        const fileTypeOrig = fileName.split(".").pop() ?? "";
 | 
			
		||||
        const fileType = normalizeFiletype(fileTypeOrig);
 | 
			
		||||
        const newFileExt = normalizeOutputFiletype(convertTo);
 | 
			
		||||
        const newFileName = fileName.replace(
 | 
			
		||||
          new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
 | 
			
		||||
          newFileExt,
 | 
			
		||||
        );
 | 
			
		||||
        const targetPath = `${userOutputDir}${newFileName}`;
 | 
			
		||||
 | 
			
		||||
        const result = await mainConverter(
 | 
			
		||||
          filePath,
 | 
			
		||||
          fileType,
 | 
			
		||||
          convertTo,
 | 
			
		||||
          targetPath,
 | 
			
		||||
          {},
 | 
			
		||||
          converterName,
 | 
			
		||||
        );
 | 
			
		||||
        if (jobId.value) {
 | 
			
		||||
          query.run(jobId.value, fileName, newFileName, result);
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    )
 | 
			
		||||
    handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        // All conversions are done, update the job status to 'completed'
 | 
			
		||||
        if (jobId.value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import * as tar from "tar";
 | 
			
		||||
import { outputDir } from "..";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
@@ -35,8 +37,7 @@ export const download = new Elysia()
 | 
			
		||||
      return Bun.file(filePath);
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    // TODO: Implement zip download
 | 
			
		||||
  .get("/archive/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
@@ -54,9 +55,20 @@ export const download = new Elysia()
 | 
			
		||||
      return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // const userId = decodeURIComponent(params.userId);
 | 
			
		||||
    // const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
    // const outputPath = `${outputDir}${userId}/`{jobId}/);
 | 
			
		||||
    const userId = decodeURIComponent(params.userId);
 | 
			
		||||
    const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
    const outputPath = `${outputDir}${userId}/${jobId}`;
 | 
			
		||||
    const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
 | 
			
		||||
 | 
			
		||||
    // return Bun.zip(outputPath);
 | 
			
		||||
    await tar.create(
 | 
			
		||||
      {
 | 
			
		||||
        file: outputTar,
 | 
			
		||||
        cwd: outputPath,
 | 
			
		||||
        filter: (path) => {
 | 
			
		||||
          return !path.match(".*\\.tar");
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      ["."],
 | 
			
		||||
    );
 | 
			
		||||
    return Bun.file(outputTar);
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Filename, Jobs } from "../db/types";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const history = new Elysia()
 | 
			
		||||
@@ -133,7 +133,7 @@ export const history = new Elysia()
 | 
			
		||||
                            />
 | 
			
		||||
                          </svg>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td safe>{new Date(job.date_created).toLocaleTimeString()}</td>
 | 
			
		||||
                        <td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
 | 
			
		||||
                        <td>{job.num_files}</td>
 | 
			
		||||
                        <td class="max-sm:hidden">{job.finished_files}</td>
 | 
			
		||||
                        <td safe>{job.status}</td>
 | 
			
		||||
@@ -162,7 +162,7 @@ export const history = new Elysia()
 | 
			
		||||
                                  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                                  viewBox="0 0 20 20"
 | 
			
		||||
                                  fill="currentColor"
 | 
			
		||||
                                  class="mx-2 inline-block h-4 w-4 text-neutral-500"
 | 
			
		||||
                                  class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
 | 
			
		||||
                                >
 | 
			
		||||
                                  <path
 | 
			
		||||
                                    fill-rule="evenodd"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
@@ -8,10 +9,14 @@ import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
function ResultsArticle({
 | 
			
		||||
  user,
 | 
			
		||||
  job,
 | 
			
		||||
  files,
 | 
			
		||||
  outputPath,
 | 
			
		||||
}: {
 | 
			
		||||
  user: {
 | 
			
		||||
    id: string;
 | 
			
		||||
  } & JWTPayloadSpec;
 | 
			
		||||
  job: Jobs;
 | 
			
		||||
  files: Filename[];
 | 
			
		||||
  outputPath: string;
 | 
			
		||||
@@ -21,14 +26,19 @@ function ResultsArticle({
 | 
			
		||||
      <div class="mb-4 flex items-center justify-between">
 | 
			
		||||
        <h1 class="text-xl">Results</h1>
 | 
			
		||||
        <div>
 | 
			
		||||
          <a
 | 
			
		||||
            style={files.length !== job.num_files ? "pointer-events: none;" : ""}
 | 
			
		||||
            href={`${WEBROOT}/archive/${user.id}/${job.id}`}
 | 
			
		||||
            download={`converted_files_${job.id}.tar`}
 | 
			
		||||
          >
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="float-right w-40 btn-primary"
 | 
			
		||||
            onclick="downloadAll()"
 | 
			
		||||
              {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
            >
 | 
			
		||||
              {files.length === job.num_files ? "Download All" : "Converting..."}
 | 
			
		||||
            </button>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <progress
 | 
			
		||||
@@ -170,7 +180,7 @@ export const results = new Elysia()
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <ResultsArticle job={job} files={files} outputPath={outputPath} />
 | 
			
		||||
            <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
 | 
			
		||||
          </main>
 | 
			
		||||
          <script src={`${WEBROOT}/results.js`} defer />
 | 
			
		||||
        </>
 | 
			
		||||
@@ -211,5 +221,5 @@ export const results = new Elysia()
 | 
			
		||||
      .as(Filename)
 | 
			
		||||
      .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    return <ResultsArticle job={job} files={files} outputPath={outputPath} />;
 | 
			
		||||
    return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import {
 | 
			
		||||
  ALLOW_UNAUTHENTICATED,
 | 
			
		||||
  HIDE_HISTORY,
 | 
			
		||||
  HTTP_ALLOWED,
 | 
			
		||||
  UNAUTHENTICATED_USER_SHARING,
 | 
			
		||||
  WEBROOT,
 | 
			
		||||
} from "../helpers/env";
 | 
			
		||||
import { FIRST_RUN, userService } from "./user";
 | 
			
		||||
@@ -33,7 +34,9 @@ export const root = new Elysia()
 | 
			
		||||
    let user: ({ id: string } & JWTPayloadSpec) | false = false;
 | 
			
		||||
    if (ALLOW_UNAUTHENTICATED) {
 | 
			
		||||
      const newUserId = String(
 | 
			
		||||
        randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
 | 
			
		||||
        UNAUTHENTICATED_USER_SHARING
 | 
			
		||||
          ? 0
 | 
			
		||||
          : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
 | 
			
		||||
      );
 | 
			
		||||
      const accessToken = await jwt.sign({
 | 
			
		||||
        id: newUserId,
 | 
			
		||||
@@ -182,7 +185,7 @@ export const root = new Elysia()
 | 
			
		||||
                        <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
                          {converter}
 | 
			
		||||
                        </header>
 | 
			
		||||
                        <ul class="convert_to_target flex flex-row flex-wrap gap-1">
 | 
			
		||||
                        <ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
 | 
			
		||||
                          {targets.map((target) => (
 | 
			
		||||
                            <button
 | 
			
		||||
                              // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								src/theme/theme.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
:root {
 | 
			
		||||
  /* Light mode */
 | 
			
		||||
  --contrast: oklch(100% 0 0);
 | 
			
		||||
  /* Neutral colors - Gray */
 | 
			
		||||
  --neutral-950: oklch(98.5% 0.002 247.839);
 | 
			
		||||
  --neutral-900: oklch(96.7% 0.003 264.542);
 | 
			
		||||
  --neutral-800: oklch(92.8% 0.006 264.531);
 | 
			
		||||
  --neutral-700: oklch(87.2% 0.01 258.338);
 | 
			
		||||
  --neutral-600: oklch(70.7% 0.022 261.325);
 | 
			
		||||
  --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
  --neutral-400: oklch(44.6% 0.03 256.802);
 | 
			
		||||
  --neutral-300: oklch(37.3% 0.034 259.733);
 | 
			
		||||
  --neutral-200: oklch(26.9% 0 0);
 | 
			
		||||
  --neutral-100: oklch(21% 0.034 264.665);
 | 
			
		||||
  --neutral-50: oklch(13% 0.028 261.692);
 | 
			
		||||
  /* lime-700 */
 | 
			
		||||
  --accent-600: oklch(53.2% 0.157 131.589);
 | 
			
		||||
  /* lime-600 */
 | 
			
		||||
  --accent-500: oklch(64.8% 0.2 131.684);
 | 
			
		||||
  /* lime-500 */
 | 
			
		||||
  --accent-400: oklch(76.8% 0.233 130.85);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  /* Dark mode */
 | 
			
		||||
  :root {
 | 
			
		||||
    --contrast: oklch(0% 0 0);
 | 
			
		||||
    /* Neutral colors - Gray */
 | 
			
		||||
    --neutral-950: oklch(13% 0.028 261.692);
 | 
			
		||||
    --neutral-900: oklch(21% 0.034 264.665);
 | 
			
		||||
    --neutral-800: oklch(27.8% 0.033 256.848);
 | 
			
		||||
    --neutral-700: oklch(37.3% 0.034 259.733);
 | 
			
		||||
    --neutral-600: oklch(44.6% 0.03 256.802);
 | 
			
		||||
    --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
    --neutral-400: oklch(70.7% 0.022 261.325);
 | 
			
		||||
    --neutral-300: oklch(87.2% 0.01 258.338);
 | 
			
		||||
    --neutral-200: oklch(92.8% 0.006 264.531);
 | 
			
		||||
    --neutral-100: oklch(96.7% 0.003 264.542);
 | 
			
		||||
    --neutral-50: oklch(98.5% 0.002 247.839);
 | 
			
		||||
    /* lime-600 */
 | 
			
		||||
    --accent-600: oklch(64.8% 0.2 131.684);
 | 
			
		||||
    /* lime-500 */
 | 
			
		||||
    --accent-500: oklch(76.8% 0.233 130.85);
 | 
			
		||||
    /* lime-400 */
 | 
			
		||||
    --accent-400: oklch(84.1% 0.238 128.85);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/assimp.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/assimp";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/calibre.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/calibre";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										91
									
								
								tests/converters/dvisvgm.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/dvisvgm";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert respects eps filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.eps", "eps", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--eps", "input.eps", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects pdf filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.pdf", "pdf", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--pdf", "input.pdf", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects svgz conversion target type", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.obj", "eps", "svgz", "output.svgz", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-z", "input.obj", "output.svgz"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										181
									
								
								tests/converters/ffmpeg.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,181 @@
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/ffmpeg";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
function mockExecFile(
 | 
			
		||||
  _cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
) {
 | 
			
		||||
  calls.push(args);
 | 
			
		||||
  if (args.includes("fail.mov")) {
 | 
			
		||||
    callback(new Error("mock failure"), "", "Fake stderr: fail");
 | 
			
		||||
  } else {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
  delete process.env.FFMPEG_ARGS;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("converts a normal file", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("adds resize for ico output", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done: resized to 256x256");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libaom-av1 for av1.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx264 for h264.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx265 for h265.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx266 for h266.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("respects FFMPEG_ARGS", async () => {
 | 
			
		||||
  process.env.FFMPEG_ARGS = "-hide_banner -y";
 | 
			
		||||
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]);
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("fails on exec error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow(
 | 
			
		||||
    "mock failure",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr: fail");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("logs stderr when execFile returns only stderr and no error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Mock execFile to call back with no error, no stdout, but with stderr
 | 
			
		||||
  const mockExecFileStderrOnly = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Only stderr output");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFileStderrOnly);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Only stderr output");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/graphicsmagick.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/graphicsmagick";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										26
									
								
								tests/converters/helpers/commonTests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile } from "../../../src/converters/types";
 | 
			
		||||
import {
 | 
			
		||||
  runConvertFailTest,
 | 
			
		||||
  runConvertLogsStderror,
 | 
			
		||||
  runConvertLogsStderrorAndStdout,
 | 
			
		||||
  runConvertSuccessTest,
 | 
			
		||||
} from "./converters";
 | 
			
		||||
 | 
			
		||||
export function runCommonTests(convert: ConvertFnWithExecFile) {
 | 
			
		||||
  test("convert resolves when execFile succeeds", async () => {
 | 
			
		||||
    await runConvertSuccessTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert rejects when execFile fails", async () => {
 | 
			
		||||
    await runConvertFailTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs stderr when present", async () => {
 | 
			
		||||
    await runConvertLogsStderror(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs both stderr and stdout when present", async () => {
 | 
			
		||||
    await runConvertLogsStderrorAndStdout(convert);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								tests/converters/helpers/converters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { expect } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
 | 
			
		||||
 | 
			
		||||
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(new Error("Test error"), "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
 | 
			
		||||
  ).rejects.toMatch(/error: Error: Test error/);
 | 
			
		||||
 | 
			
		||||
  // Test with error object lacking 'message' property
 | 
			
		||||
  const mockExecFileNoMessage: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    // Simulate a non-standard error object
 | 
			
		||||
    callback({ notMessage: true } as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
 | 
			
		||||
  // Test with a non-object error (e.g., a string)
 | 
			
		||||
  const mockExecFileStringError: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback("string error" as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedError = "";
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedError = msg;
 | 
			
		||||
  };
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(loggedError).toBe("stderr: Fake stderr");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								tests/converters/imagemagick.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,165 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/imagemagick";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.obj", "eps", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "input.obj",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type with svg as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.svg", "svg", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "512",
 | 
			
		||||
      "input.svg",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects ico conversion target type with emf as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.emf", "emf", "ico", "output.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "icon:auto-resize=256,128,64,48,32,16",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "none",
 | 
			
		||||
      "emf:delegate=false",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "300",
 | 
			
		||||
      "white",
 | 
			
		||||
      "-alpha",
 | 
			
		||||
      "remove",
 | 
			
		||||
      "input.emf",
 | 
			
		||||
      "output.ico",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects emf as input filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.emf", "emf", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining([
 | 
			
		||||
      "-define",
 | 
			
		||||
      "emf:delegate=false",
 | 
			
		||||
      "-density",
 | 
			
		||||
      "300",
 | 
			
		||||
      "-background",
 | 
			
		||||
      "white",
 | 
			
		||||
      "-alpha",
 | 
			
		||||
      "remove",
 | 
			
		||||
      "input.emf",
 | 
			
		||||
      "output.obj",
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/inkscape.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/inkscape";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/libheif.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/libheif";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										91
									
								
								tests/converters/libjxl.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/libjxl";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let command: string = "";
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  command = "";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert uses djxl with input filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.jxl", "jxl", "png", "output.png", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("djxl");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses cjxl with output filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.png", "png", "jxl", "output.jxl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("cjxl");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses empty string as command with neither input nor output filetype being jxl", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    command = _cmd;
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.png", "png", "jpg", "output.jpg", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(command).toEqual("");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										61
									
								
								tests/converters/msgconvert.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/msgconvert";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
 | 
			
		||||
test("convert rejects conversion if input filetype is not msg and output type is not eml", async () => {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expectedError = new Error(
 | 
			
		||||
    "Unsupported conversion from obj to stl. Only MSG to EML conversion is currently supported.",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  expect(convert("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile)).rejects.toEqual(
 | 
			
		||||
    expectedError,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert rejects conversion on error", async () => {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(new Error("Test error"), "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expectedError = new Error("msgconvert failed: Test error");
 | 
			
		||||
 | 
			
		||||
  expect(convert("input.msg", "msg", "eml", "output.eml", undefined, mockExecFile)).rejects.toEqual(
 | 
			
		||||
    expectedError,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert logs stderr as warning", async () => {
 | 
			
		||||
  const originalConsoleWarn = console.warn;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.warn = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("file.msg", "msg", "eml", "out.eml", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleWarn;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("msgconvert stderr: Fake stderr");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/potrace.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/potrace";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/resvg.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/resvg";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										65
									
								
								tests/converters/vips.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,65 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { convert } from "../../src/converters/vips";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert uses action pdfload with filetype being pdf", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.pdf", "pdf", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["pdfload"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert uses action copy with filetype being anything but pdf", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.jpg", "jpg", "obj", "output.obj", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["copy"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/xelatex.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/xelatex";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
@@ -5,8 +5,9 @@
 | 
			
		||||
    "target": "ES2021",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "moduleDetection": "force",
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    // "allowImportingTsExtensions": true,
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "noEmit": false,
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "downlevelIteration": true,
 | 
			
		||||
@@ -24,7 +25,10 @@
 | 
			
		||||
    // "noUnusedParameters": true,
 | 
			
		||||
    "exactOptionalPropertyTypes": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noImplicitOverride": true
 | 
			
		||||
    "noImplicitOverride": true,
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "esModuleInterop": true
 | 
			
		||||
    // "noImplicitReturns": true
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src", "tests", "package.json", "reset.d.ts"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||