When creating format handlers, you’ll often need to use external libraries and tools. This guide explains how to properly add dependencies to the project.
Dependency Types
Convert to it! supports two main types of dependencies:
npm packages - JavaScript/TypeScript libraries from the npm registry
Git submodules - Git repositories included directly in the project
NPM Packages
For most JavaScript/TypeScript libraries, use npm packages.
Installing NPM Dependencies
Install the package
Use Bun to install the package: For development dependencies:
Install type definitions
If TypeScript types aren’t included, install them separately: bun add -d @types/package-name
Import in your handler
import { SomeFunction } from "package-name" ;
class myHandler implements FormatHandler {
// Use the imported package
}
Example: Using an NPM Package
Many handlers in Convert to it! use npm packages. For example, the FFmpeg handler:
import { FFmpeg } from "@ffmpeg/ffmpeg" ;
import type { LogEvent } from "@ffmpeg/ffmpeg" ;
import mime from "mime" ;
class FFmpegHandler implements FormatHandler {
#ffmpeg ?: FFmpeg ;
async init () {
this . #ffmpeg = new FFmpeg ();
await this . #ffmpeg . load ({
coreURL: "/convert/wasm/ffmpeg-core.js"
});
this . ready = true ;
}
}
The dependencies in package.json:
{
"dependencies" : {
"@ffmpeg/core" : "^0.12.10" ,
"@ffmpeg/ffmpeg" : "^0.12.15" ,
"@ffmpeg/util" : "^0.12.2" ,
"mime" : "^4.1.0"
}
}
Git Submodules
For projects that aren’t available as npm packages, or when you need the full source code, use Git submodules.
When to Use Submodules
Use Git submodules when:
The library isn’t published to npm
You need to modify the source code
The library is specifically designed for this project
You want to track a specific version of the repository
Adding a Git Submodule
Add the submodule
Add the repository as a submodule in src/handlers/: git submodule add https://github.com/username/repo-name src/handlers/repo-name
Initialize and update
If you’ve cloned the project, initialize submodules: git submodule update --init --recursive
Import from the submodule
import { SomeFunction } from "./submodule-name/src/index.ts" ;
class myHandler implements FormatHandler {
// Use the imported code
}
Current Submodules
Convert to it! uses several Git submodules:
[submodule "src/handlers/envelope"]
path = src/handlers/envelope
url = https://github.com/p2r3/envelope
[submodule "src/handlers/qoi-fu"]
path = src/handlers/qoi-fu
url = https://github.com/pfusik/qoi-fu
[submodule "src/handlers/sppd"]
path = src/handlers/sppd
url = https://github.com/p2r3/sppd
[submodule "src/handlers/qoa-fu"]
path = src/handlers/qoa-fu
url = https://github.com/pfusik/qoa-fu
[submodule "src/handlers/image-to-txt"]
path = src/handlers/image-to-txt
url = https://git.sr.ht/~thezipcreator/image-to-txt
[submodule "src/handlers/espeakng.js"]
path = src/handlers/espeakng.js
url = https://github.com/TheZipCreator/espeakng.js
Example: Using a Submodule
The canvas-to-blob handler uses the image-to-txt submodule:
src/handlers/canvasToBlob.ts
import { imageToText , rgbaToGrayscale } from "./image-to-txt/src/convert.ts" ;
class canvasToBlobHandler implements FormatHandler {
async doConvert ( inputFiles , inputFormat , outputFormat ) {
// Use the submodule function
const text = imageToText ( imageData );
}
}
WebAssembly Dependencies
Many conversion tools use WebAssembly (WASM) for performance. These require special handling.
Setting Up WebAssembly
Add the WASM file
Place the .wasm file in your handler directory or node_modules (if from npm): src/handlers/my-handler/my-library.wasm
Or install via npm:
Configure Vite
Add the WASM file to Vite’s static copy configuration in vite.config.js: export default defineConfig ({
plugins: [
viteStaticCopy ({
targets: [
{
src: "src/handlers/my-handler/my-library.wasm" ,
dest: "wasm"
},
// Or from node_modules
{
src: "node_modules/wasm-library/dist/library.wasm" ,
dest: "wasm"
}
]
})
]
}) ;
Load in handler
Load the WASM file from /convert/wasm/ in your handler: async init () {
await loadWasm ( "/convert/wasm/my-library.wasm" );
this . ready = true ;
}
Do not link to node_modules directly! Always copy WASM files to the /convert/wasm/ directory via Vite configuration. Direct linking to node_modules can cause issues in production builds.
Real Example: FFmpeg WebAssembly
The FFmpeg handler demonstrates WebAssembly setup:
class FFmpegHandler implements FormatHandler {
#ffmpeg ?: FFmpeg ;
async loadFFmpeg () {
if ( ! this . #ffmpeg ) return ;
return await this . #ffmpeg . load ({
coreURL: "/convert/wasm/ffmpeg-core.js"
});
}
async init () {
this . #ffmpeg = new FFmpeg ();
await this . loadFFmpeg ();
// ... rest of initialization
}
}
With the corresponding Vite configuration:
export default defineConfig ({
plugins: [
viteStaticCopy ({
targets: [
{
src: "node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.*" ,
dest: "wasm"
}
]
})
]
}) ;
Multiple WebAssembly Files
Some libraries require multiple files (WASM + JS glue code):
{
targets : [
{
src: "src/handlers/libopenmpt/libopenmpt.wasm" ,
dest: "wasm"
},
{
src: "src/handlers/libopenmpt/libopenmpt.js" ,
dest: "wasm"
}
]
}
Avoid CDNs
Please try to avoid CDNs (Content Delivery Networks) While CDNs are convenient, they have downsides:
Don’t work well with TypeScript
Each introduces instability
Can cause build issues
May have version inconsistencies
For a project that leans heavily on external dependencies, these issues can add up fast.
Alternatives to CDNs
NPM Package
Git Submodule
Vendored Code
# Instead of loading from CDN
bun add library-name
// Import directly
import Library from "library-name" ;
# Include the source directly
git submodule add https://github.com/user/library src/handlers/library
// Import from submodule
import Library from "./library/src/index.ts" ;
# Copy source files to your project
cp -r external-lib/ src/handlers/my-handler/vendor/
// Import from vendored code
import Library from "./vendor/external-lib/index.ts" ;
Common Dependencies in Convert to it!
Here are examples of well-integrated dependencies:
Image Processing
{
"dependencies" : {
"@imagemagick/magick-wasm" : "^0.0.37" ,
"imagetracer" : "^0.2.2"
}
}
Audio/Video Processing
{
"dependencies" : {
"@ffmpeg/ffmpeg" : "^0.12.15" ,
"@ffmpeg/core" : "^0.12.10" ,
"meyda" : "^5.6.3" ,
"wavefile" : "^11.0.0"
}
}
Document Processing
{
"dependencies" : {
"pdftoimg-js" : "^0.2.5" ,
"verovio" : "^6.0.1"
}
}
{
"dependencies" : {
"jszip" : "^3.10.1" ,
"pako" : "^2.1.0" ,
"bson" : "^7.2.0" ,
"nbtify" : "^2.2.0" ,
"papaparse" : "^5.5.3"
}
}
Dependency Checklist
Before adding a dependency, check:
Ensure the dependency’s license is compatible with GPL-2.0 (Convert to it!‘s license). Compatible licenses:
MIT
BSD
Apache 2.0
GPL-2.0 or later
LGPL
The dependency must work in browsers. Check for:
No Node.js-specific APIs (unless polyfilled)
WebAssembly support if needed
Reasonable bundle size
Prefer actively maintained dependencies:
Recent commits/releases
Responsive maintainers
Good documentation
Ensure TypeScript types are available:
Bundled with the package
Available via @types/ package
Or you’re willing to write them
Best Practices
Keep dependencies minimal
Only add dependencies that are truly necessary. Each dependency:
Increases bundle size
Adds maintenance burden
Can introduce security vulnerabilities
Lock versions appropriately
Use semantic versioning wisely: {
"@ffmpeg/ffmpeg" : "^0.12.15" , // Allow minor/patch updates
"critical-lib" : "1.2.3" // Lock exact version if needed
}
Document special setup
If your dependency requires special configuration, document it:
In your handler file comments
In the pull request description
In code comments near usage
Test thoroughly
Test your handler with the new dependency:
In development mode
In production build
Across different browsers
Troubleshooting
Module Not Found
If imports fail:
# Reinstall dependencies
bun install
# Clear cache and rebuild
rm -rf node_modules
bun install
WebAssembly Loading Fails
Check:
WASM file is in Vite static copy config
Path in handler uses /convert/wasm/ prefix
File extension is .wasm
WASM file was actually copied to dist
Type Errors
If TypeScript can’t find types:
# Install type definitions
bun add -d @types/package-name
# Or add to tsconfig.json
{
"compilerOptions" : {
"types" : [ "package-name" ]
}
}
Example: Complete Dependency Setup
Here’s a complete example of adding a new handler with dependencies:
Install npm package
bun add new-format-library
bun add -d @types/new-format-library
Add WASM to Vite config (if needed)
{
src : "node_modules/new-format-library/dist/library.wasm" ,
dest : "wasm"
}
Create handler
src/handlers/newFormat.ts
import { convert } from "new-format-library" ;
import type { FileData , FileFormat , FormatHandler } from "../FormatHandler.ts" ;
class newFormatHandler implements FormatHandler {
public name = "newFormat" ;
public ready = false ;
async init () {
// Load WASM if needed
await loadWasm ( "/convert/wasm/library.wasm" );
this . ready = true ;
}
async doConvert ( inputFiles , inputFormat , outputFormat ) {
const result = await convert ( inputFiles [ 0 ]. bytes , outputFormat . internal );
return [{ bytes: result , name: "output." + outputFormat . extension }];
}
}
export default newFormatHandler ;
Register handler
import newFormatHandler from "./newFormat.ts" ;
try { handlers . push ( new newFormatHandler ()) } catch ( _ ) { };