├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── preload.ts ├── res └── logo.gif ├── src ├── cli.ts ├── fetchgit.ts ├── index.ts ├── loader.ts ├── loaders │ ├── library.ts │ ├── rs.ts │ └── zig.ts ├── types.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bun.lockb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aritra Karak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](res/logo.gif) 2 | 3 | # hyperimport 4 | 5 | ⚡ TypeScript imports on steroids. Import C, Rust, Zig etc. files in your TypeScript code and more. 6 | 7 | A powerful plugin for the [Bun](https://bun.sh/) runtime that pushes the limits of Plugin and FFI APIs together, lets you easily import functions from other languages. It works with languages that support the C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, etc). If the loader of your language isn't there already, go ahead write your own custom loader with it's super flexible API and extend hyperimport to support your favorite language or even customize the built-in loaders to work in the way you want. Not just loaders but any plugin can be imported in hyperimport through it's own package management from [hyperimport registry](https://github.com/tr1ckydev/hyperimport_registry) (our own community registry for bun plugins). [See how](https://github.com/tr1ckydev/hyperimport/wiki/Importing-a-package). 8 | 9 | [Read the dev.to article for behind the scenes of this project.](https://dev.to/tr1ckydev/hyperimport-import-c-rust-zig-etc-files-in-typescript-1ia5) 10 | 11 | In simple ways, you can do this, 12 | 13 | *index.ts* 14 | 15 | ```ts 16 | import { add } from "./add.rs"; 17 | console.log(add(5, 5)); // 10 18 | ``` 19 | 20 | *add.rs* 21 | 22 | ```rust 23 | #[no_mangle] 24 | pub extern "C" fn add(a: isize, b: isize) -> isize { 25 | a + b 26 | } 27 | ``` 28 | 29 | and, more... 30 | 31 | - Write a TypeScript program using native C functions through libc. [See how](https://github.com/tr1ckydev/hyperimport/wiki/Importing-libc-in-typescript). 32 | - Import native system functions in typescript through system shared libraries. 33 | - Import any kind of bun plugin package from the [hyperimport registry](https://github.com/tr1ckydev/hyperimport_registry). 34 | - Your imagination is now your limit... 35 | 36 | 37 | 38 | ## Showcases 39 | 40 | - Featured at official Bun 1.0 launch - [Watch video](https://youtu.be/BsnCpESUEqM?t=221) 41 | - Importing a Rust function in typescript (@jarredsumner) - [Watch video](https://twitter.com/jarredsumner/status/1681608754067046400) 42 | - Importing a Zig function in typescript (@jarredsumner) - [Watch video](https://twitter.com/jarredsumner/status/1681610300699869184) 43 | - Walkthrough guide by the community - [Watch video](https://www.youtube.com/watch?v=boD1m5Ex80c) 44 | 45 | 46 | 47 | ## Documentation 48 | 49 | *—"I wanna learn more about this! How do I get started?"* 50 | 51 | Check out the [Wiki](https://github.com/tr1ckydev/hyperimport/wiki) page of this repository to read the entire documentation for this project. 52 | 53 | If you have any questions, feel free to join the discord server [here](https://discord.com/invite/tfBA2z8mbq). 54 | 55 | 56 | 57 | ## License 58 | 59 | This repository uses MIT license. See [LICENSE](https://github.com/tr1ckydev/hyperimport/blob/main/LICENSE) for full license text. 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperimport", 3 | "version": "0.2.0", 4 | "description": "⚡ Import c, rust, zig etc. files in your TypeScript code and more.", 5 | "main": "src/index.ts", 6 | "files": [ 7 | "preload.ts", 8 | "src" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tr1ckydev/hyperimport.git" 13 | }, 14 | "keywords": [ 15 | "hyperimport", 16 | "plugin", 17 | "ffi", 18 | "import", 19 | "bun", 20 | "rust", 21 | "zig" 22 | ], 23 | "author": "tr1ckydev", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/tr1ckydev/hyperimport/issues" 27 | }, 28 | "homepage": "https://github.com/tr1ckydev/hyperimport#readme", 29 | "bin": "./src/cli.ts", 30 | "devDependencies": { 31 | "bun-types": "latest" 32 | } 33 | } -------------------------------------------------------------------------------- /preload.ts: -------------------------------------------------------------------------------- 1 | import { HyperImportConfig } from "./src/types"; 2 | import { debugLog } from "./src/utils"; 3 | 4 | if (Bun.env.DISABLE_PRELOAD !== "1") { 5 | 6 | const cwd = process.cwd(); 7 | const config: HyperImportConfig = (await import(`${cwd}/bunfig.toml`)).default.hyperimport; 8 | 9 | debugLog(config.debug, 3, "registering loaders..."); 10 | 11 | for (const loader of config.loaders ?? []) { 12 | await importPlugin(`./src/loaders/${loader}.ts`) 13 | .then(name => debugLog(config.debug, 2, name, "has been registered")) 14 | .catch(() => debugLog(config.debug, 1, "loader not found:", loader)); 15 | } 16 | 17 | for (const loader of config.custom ?? []) { 18 | await importPlugin(`${cwd}/${loader}`) 19 | .then(name => debugLog(config.debug, 2, "[CUSTOM]", name, "has been registered")) 20 | .catch(() => debugLog(config.debug, 1, "[CUSTOM] loader not found:", loader)); 21 | } 22 | 23 | for (const pkg of config.packages ?? []) { 24 | await importPlugin(`${cwd}/node_modules/.hyperimport/${pkg}/index.ts`) 25 | .then(() => debugLog(config.debug, 2, "[PACKAGE]", pkg, "has been registered")) 26 | .catch(async () => { 27 | debugLog(config.debug, 1, "[PACKAGE] not installed:", pkg); 28 | debugLog(config.debug, 3, "executing install command..."); 29 | Bun.spawnSync(["bun", "x", "hyperimport", "install"], { env: { PATH: process.env.PATH, DISABLE_PRELOAD: "1" }, stderr: "inherit" }); 30 | await importPlugin(`${cwd}/node_modules/.hyperimport/${pkg}/index.ts`) 31 | .then(() => debugLog(config.debug, 2, "[PACKAGE]", pkg, "has been registered")) 32 | .catch(() => debugLog(config.debug, 1, "[PACKAGE] unable to import:", pkg)); 33 | }); 34 | } 35 | 36 | async function importPlugin(path: string) { 37 | const l = await import(path); 38 | const plugin = await new l.default(config.debug, cwd).toPlugin(); 39 | Bun.plugin(plugin); 40 | return plugin.name; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /res/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr1ckydev/hyperimport/03cd02fe444705a22d50474f4a00606ab802e441/res/logo.gif -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | 3 | import { HyperImportConfig, debugLog } from "."; 4 | import { fetchDirectory } from "./fetchgit"; 5 | 6 | const cwd = process.cwd(); 7 | const PKG_INSTALL_DIR = `${cwd}/node_modules/.hyperimport`; 8 | 9 | function installPackage(name: string, debug: boolean) { 10 | debugLog(debug, 3, "installing package:", name); 11 | fetchDirectory(name, { 12 | path: `packages/${name}`, 13 | destination: `${PKG_INSTALL_DIR}/${name}` 14 | }).then(() => Bun.spawnSync(["bun", "i", "--production"], { cwd: `${PKG_INSTALL_DIR}/${name}`, stderr: "ignore" })); 15 | } 16 | 17 | switch (process.argv[2]) { 18 | case "i": 19 | case "install": 20 | const config: HyperImportConfig = (await import(`${cwd}/bunfig.toml`)).default.hyperimport; 21 | if (config.packages) { 22 | config.packages.forEach(pkg => installPackage(pkg, config.debug)); 23 | } else { 24 | throw "no packages found in the config"; 25 | } 26 | break; 27 | default: 28 | throw "no arguments provided"; 29 | } -------------------------------------------------------------------------------- /src/fetchgit.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/tr1ckydev/fetchgit 2 | 3 | import { mkdirSync } from "fs"; 4 | 5 | interface DirectoryConfig { 6 | path: string, 7 | destination: string, 8 | } 9 | 10 | interface GitHubContents { 11 | name: string, 12 | path: string, 13 | sha: string, 14 | size: number, 15 | url: string, 16 | html_url: string, 17 | git_url: string, 18 | download_url: string, 19 | type: "file" | "dir", 20 | _links: { 21 | self: string, 22 | git: string, 23 | html: string, 24 | }, 25 | } 26 | 27 | interface ContentNotFound { 28 | message: "Not Found", 29 | documentation_url: string; 30 | } 31 | 32 | export async function fetchDirectory(pkg: string, config: DirectoryConfig) { 33 | const contents = await (await fetch(`https://api.github.com/repos/tr1ckydev/hyperimport_registry/contents/${config.path}`)).json(); 34 | if ((contents as ContentNotFound).message === "Not Found") { 35 | throw `not found in registry: ${pkg}`; 36 | } 37 | mkdirSync(`${config.destination}/${config.path.replace(`packages/${pkg}`, "")}`, { recursive: true }); 38 | for (const content of (contents as GitHubContents[])) { 39 | const fetch_path = content.path.replace(`packages/${pkg}/`, ""); 40 | switch (content.type) { 41 | case "file": 42 | Bun.write(`${config.destination}/${fetch_path}`, await fetch(content.download_url)); 43 | break; 44 | case "dir": 45 | fetchDirectory(pkg, { 46 | path: content.path, 47 | destination: config.destination 48 | }); 49 | break; 50 | } 51 | } 52 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./utils"; 3 | export { default as Loader } from "./loader"; 4 | export { default as LibraryLoader } from "./loaders/library"; 5 | export { default as RustLoader } from "./loaders/rs"; 6 | export { default as ZigLoader } from "./loaders/zig"; -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { BunPlugin } from "bun"; 2 | import { FFIFunction, Narrow, dlopen, suffix } from "bun:ffi"; 3 | import { mkdirSync, readFileSync } from "fs"; 4 | import { basename, parse } from "path"; 5 | import { LoaderConfig } from "./types"; 6 | import { lastModified, nm } from "./utils"; 7 | 8 | export default class { 9 | /**The name of the loader. */ 10 | name: string; 11 | protected cwd: string; 12 | protected _config: LoaderConfig.Builder; 13 | // @ts-expect-error 14 | protected config: LoaderConfig.Internal = {}; 15 | 16 | constructor(name: string, config: LoaderConfig.Builder) { 17 | this.name = name; 18 | this.cwd = process.cwd(); 19 | this._config = config; 20 | } 21 | 22 | /** 23 | * To build the source file into a shared library file. 24 | */ 25 | async build() { 26 | Bun.spawnSync(this.config.buildCommand); 27 | } 28 | 29 | /** 30 | * Runs at the beginning of `initConfig()`. 31 | * By default asks for the build command and output directory from the user on importing the source file for the first time. 32 | */ 33 | async initConfigPre() { 34 | console.log(`\x1b[33m[HYPERIMPORT]\x1b[39m: ${this.name}\nNo configuration was found for "${this.config.importPath}"\nEnter the build command and output directory to configure it.\nPress enter to use the default values.\n`); 35 | this.config.buildCommand = prompt("build command: (default)")?.split(" ") ?? this.config.buildCommand; 36 | this.config.outDir = prompt(`output directory: (${this.config.outDir})`) ?? this.config.outDir; 37 | mkdirSync(this.config.outDir, { recursive: true }); 38 | } 39 | 40 | /** 41 | * Generates `config.ts` and `types.d.ts` to add type completions for the source file. 42 | */ 43 | async initConfigTypes() { 44 | const filename = basename(this.config.importPath); 45 | mkdirSync(`${this.cwd}/@types/${filename}`, { recursive: true }); 46 | Bun.write(`${this.cwd}/@types/${filename}/lastModified`, lastModified(this.config.importPath)); 47 | const configWriter = Bun.file(`${this.cwd}/@types/${filename}/config.ts`).writer(); 48 | configWriter.write(`import { LoaderConfig, T } from "hyperimport";\nexport default {\n\tbuildCommand: ${JSON.stringify(this.config.buildCommand)},\n\toutDir: "${this.config.outDir}",\n\tsymbols: {`); 49 | for (const symbol of nm(this.config.libPath)) { 50 | configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [],\n\t\t\treturns: T.void\n\t\t},`); 51 | } 52 | configWriter.write(`\n\t}\n} satisfies LoaderConfig.Main;`); 53 | configWriter.end(); 54 | Bun.write( 55 | `${this.cwd}/@types/${filename}/types.d.ts`, 56 | `declare module "*/${filename}" {\n\tconst symbols: import("bun:ffi").ConvertFns;\n\texport = symbols;\n}` 57 | ); 58 | console.log(`\n\x1b[32mConfig file has been generated at "${this.cwd}/@types/${filename}/config.ts"\x1b[39m\nEdit the config.ts and set the argument and return types, then rerun the script.`); 59 | } 60 | 61 | /** 62 | * When the source file isn't configured yet, this executes to configure it. 63 | */ 64 | async initConfig() { 65 | await this.initConfigPre(); 66 | console.log("\nBuilding the source file..."); 67 | await this.build(); 68 | console.log("The source file has been built."); 69 | await this.initConfigTypes(); 70 | } 71 | 72 | /** 73 | * Checks if the source file was modified, if it is, then `build()` is executed to rebuild the changed source file. 74 | */ 75 | async ifSourceModify() { 76 | const lm = lastModified(this.config.importPath); 77 | const lmfile = `${this.cwd}/@types/${basename(this.config.importPath)}/lastModified`; 78 | if (lm !== readFileSync(lmfile).toString()) { 79 | await this.build(); 80 | Bun.write(lmfile, lm); 81 | } 82 | } 83 | 84 | /** 85 | * Imports the symbols defined in `config.ts` to be used when opening the shared library. 86 | * If `config.ts` isn't found, the source file isn't configured yet, hence executes `initConfig()` and exits the process. 87 | * @returns An object containing the symbols. 88 | */ 89 | async getSymbols(): Promise>> { 90 | try { 91 | await this.ifSourceModify(); 92 | return (await import(`${this.cwd}/@types/${basename(this.config.importPath)}/config.ts`)).default.symbols; 93 | } catch { 94 | await this.initConfig(); 95 | process.exit(); 96 | } 97 | } 98 | 99 | /** 100 | * Runs just before opening/loading the shared library. 101 | */ 102 | async preload() { 103 | this.config.outDir = this._config.outDir!(this.config.importPath); 104 | this.config.buildCommand = this._config.buildCommand!(this.config.importPath, this.config.outDir); 105 | this.config.libPath = `${this.config.outDir}/lib${parse(this.config.importPath).name}.${suffix}`; 106 | } 107 | 108 | /** 109 | * Creates the plugin instance to be consumed by `Bun.plugin()` to register it. 110 | * @returns A `BunPlugin` instance. 111 | */ 112 | async toPlugin(): Promise { 113 | const parentThis = this; 114 | return { 115 | name: parentThis.name, 116 | setup(build) { 117 | build.onLoad({ filter: new RegExp(`\.(${parentThis._config.extension})$`) }, async args => { 118 | parentThis.config.importPath = args.path; 119 | await parentThis.preload(); 120 | return { 121 | exports: dlopen(parentThis.config.libPath, await parentThis.getSymbols()).symbols, 122 | loader: "object" 123 | }; 124 | }); 125 | } 126 | }; 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /src/loaders/library.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync } from "fs"; 2 | import { basename } from "path"; 3 | import Loader from "../loader"; 4 | import { lastModified, nm } from "../utils"; 5 | 6 | export default class extends Loader { 7 | constructor() { 8 | super("Library Loader", 9 | { 10 | extension: "so|dylib", 11 | } 12 | ); 13 | } 14 | async preload() { 15 | this.config.libPath = this.config.importPath; 16 | } 17 | async initConfigTypes() { 18 | const filename = basename(this.config.importPath); 19 | mkdirSync(`${this.cwd}/@types/${filename}`, { recursive: true }); 20 | Bun.write(`${this.cwd}/@types/${filename}/lastModified`, lastModified(this.config.importPath)); 21 | const configWriter = Bun.file(`${this.cwd}/@types/${filename}/config.ts`).writer(); 22 | configWriter.write(`import { LoaderConfig, T } from "hyperimport";\nexport default {\n\tsymbols: {`); 23 | for (const symbol of nm(this.config.libPath)) { 24 | configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [],\n\t\t\treturns: T.void\n\t\t},`); 25 | } 26 | configWriter.write(`\n\t}\n} satisfies LoaderConfig.Main;`); 27 | configWriter.end(); 28 | Bun.write( 29 | `${this.cwd}/@types/${filename}/types.d.ts`, 30 | `declare module "*/${filename}" {\n\tconst symbols: import("bun:ffi").ConvertFns;\n\texport = symbols;\n}` 31 | ); 32 | console.log(`\x1b[32mConfig file has been generated at "${this.cwd}/@types/${filename}/config.ts"\x1b[39m\nEdit the config.ts and set the argument and return types, then rerun the script.`); 33 | } 34 | async initConfig() { 35 | this.initConfigPre(); 36 | this.initConfigTypes(); 37 | } 38 | async initConfigPre() { 39 | console.log(`\x1b[33m[HYPERIMPORT]\x1b[39m: ${this.name}\nNo configuration was found for "${this.config.importPath}"\n`); 40 | } 41 | async ifSourceModify() { } 42 | } -------------------------------------------------------------------------------- /src/loaders/rs.ts: -------------------------------------------------------------------------------- 1 | import { basename } from "path"; 2 | import Loader from "../loader"; 3 | 4 | export default class extends Loader { 5 | constructor() { 6 | super("Rust Loader", 7 | { 8 | extension: "rs", 9 | buildCommand: (importPath, outDir) => [ 10 | "rustc", 11 | "--crate-type", 12 | "cdylib", 13 | importPath, 14 | "--out-dir", 15 | outDir 16 | ], 17 | outDir: importPath => `build/${basename(importPath)}` 18 | } 19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /src/loaders/zig.ts: -------------------------------------------------------------------------------- 1 | import { suffix } from "bun:ffi"; 2 | import { basename, parse } from "path"; 3 | import Loader from "../loader"; 4 | 5 | export default class extends Loader { 6 | constructor() { 7 | super("Zig Loader", 8 | { 9 | extension: "zig", 10 | buildCommand: (importPath, outDir) => [ 11 | "zig", 12 | "build-lib", 13 | importPath, 14 | "-dynamic", 15 | "-OReleaseFast", 16 | `-femit-bin=${outDir}/lib${parse(importPath).name}.${suffix}` 17 | ], 18 | outDir: importPath => `build/${basename(importPath)}` 19 | } 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { FFIFunction, Narrow } from "bun:ffi"; 2 | 3 | export { FFIType as T } from "bun:ffi"; 4 | 5 | export interface HyperImportConfig { 6 | loaders?: string[], 7 | custom?: string[], 8 | packages?: string[], 9 | debug: boolean, 10 | } 11 | 12 | export namespace LoaderConfig { 13 | 14 | export interface Main { 15 | buildCommand?: string[], 16 | outDir?: string, 17 | symbols: Record>, 18 | } 19 | 20 | export interface Builder { 21 | extension: string, 22 | buildCommand?: (importPath: string, outDir: string) => string[], 23 | outDir?: (importPath: string) => string, 24 | } 25 | 26 | export interface Internal { 27 | importPath: string, 28 | libPath: string, 29 | buildCommand: string[], 30 | outDir: string, 31 | symbols: Record>, 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints a debug message to the console. 3 | * @param isDebug The boolean debug flag. 4 | * @param mode The color of the message (1: red, 2: green, 3: yellow). 5 | * @param args The message to print. 6 | */ 7 | export function debugLog(isDebug: boolean, mode: 1 | 2 | 3, ...args: string[]) { 8 | console.assert(!isDebug, `\x1b[3${mode}m\x1b[1m[HYPERIMPORT]\x1b[22m\x1b[39m`, ...args); 9 | } 10 | 11 | /** 12 | * Returns the last modified time of the file. 13 | * @param path The path to the file. 14 | */ 15 | export function lastModified(path: string) { 16 | return `${Bun.file(path).lastModified}`; 17 | } 18 | 19 | /** 20 | * Returns the list of exported symbols in the library using the `nm` command. 21 | * (Removes the leading underscore if any.) 22 | * @param path The path to the library to be loaded. 23 | */ 24 | export function nm(path: string) { 25 | return [...Bun.spawnSync(["nm", path]).stdout.toString().matchAll(/T (.*)$/gm)].map(x => x[1][0] === "_" ? x[1].slice(1) : x[1]); 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | --------------------------------------------------------------------------------