Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
node_modules/
bun.lockb
bun.lockb
bun.lock

# Generated files
@types/
build/
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ and, more...



## Platform Support

hyperimport works on **Linux, macOS, and Windows**.

### Windows Requirements

On Windows, you need to install [LLVM](https://llvm.org/) to use hyperimport:

```bash
# Using scoop (recommended)
scoop install llvm

# Or using chocolatey
choco install llvm

# Or download from https://releases.llvm.org/
```

The `llvm-nm` tool is used to extract symbols from compiled libraries on Windows.



## Documentation

*—"I wanna learn more about this! How do I get started?"*
Expand Down
7 changes: 7 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[hyperimport]
loaders = ["rs", "zig"]

[test]
preload = ["./preload.ts"]
root = "./test"

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.2.0",
"description": "⚡ Import c, rust, zig etc. files in your TypeScript code and more.",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"files": [
"preload.ts",
"src"
Expand All @@ -27,6 +30,9 @@
},
"homepage": "https://github.com/tr1ckydev/hyperimport#readme",
"bin": "./src/cli.ts",
"dependencies": {
"web-tree-sitter": "0.25.10"
},
"devDependencies": {
"bun-types": "latest"
}
Expand Down
4 changes: 3 additions & 1 deletion preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ if (Bun.env.DISABLE_PRELOAD !== "1") {
debugLog(config.debug, 3, "registering loaders...");

for (const loader of config.loaders ?? []) {
// Try both .ts file and directory with index.ts
await importPlugin(`./src/loaders/${loader}.ts`)
.catch(() => importPlugin(`./src/loaders/${loader}/index.ts`))
.then(name => debugLog(config.debug, 2, name, "has been registered"))
.catch(() => debugLog(config.debug, 1, "loader not found:", loader));
}
Expand All @@ -35,7 +37,7 @@ if (Bun.env.DISABLE_PRELOAD !== "1") {

async function importPlugin(path: string) {
const l = await import(path);
const plugin = await new l.default(config.debug, cwd).toPlugin();
const plugin = await new l.default().toPlugin();
Bun.plugin(plugin);
return plugin.name;
}
Expand Down
55 changes: 40 additions & 15 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BunPlugin } from "bun";
import { FFIFunction, Narrow, dlopen, suffix } from "bun:ffi";
import { FFIFunction, dlopen, suffix } from "bun:ffi";
import { mkdirSync, readFileSync } from "fs";
import { basename, parse } from "path";
import { LoaderConfig } from "./types";
import { lastModified, nm } from "./utils";
import { lastModified } from "./utils";

export default class {
/**The name of the loader. */
Expand Down Expand Up @@ -31,6 +31,13 @@ export default class {
* By default asks for the build command and output directory from the user on importing the source file for the first time.
*/
async initConfigPre() {
// In test mode, automatically use defaults without prompting
if (process.env.NODE_ENV === "test") {
console.log(`\x1b[33m[HYPERIMPORT]\x1b[39m: ${this.name}\nNo configuration was found for "${this.config.importPath}"\nUsing default configuration (test mode)...\n`);
mkdirSync(this.config.outDir, { recursive: true });
return;
}

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`);
this.config.buildCommand = prompt("build command: (default)")?.split(" ") ?? this.config.buildCommand;
this.config.outDir = prompt(`output directory: (${this.config.outDir})`) ?? this.config.outDir;
Expand All @@ -42,20 +49,35 @@ export default class {
*/
async initConfigTypes() {
const filename = basename(this.config.importPath);
mkdirSync(`${this.cwd}/@types/${filename}`, { recursive: true });
Bun.write(`${this.cwd}/@types/${filename}/lastModified`, lastModified(this.config.importPath));
const configWriter = Bun.file(`${this.cwd}/@types/${filename}/config.ts`).writer();
configWriter.write(`import { LoaderConfig, T } from "hyperimport";\nexport default {\n\tbuildCommand: ${JSON.stringify(this.config.buildCommand)},\n\toutDir: "${this.config.outDir}",\n\tsymbols: {`);
for (const symbol of nm(this.config.libPath)) {
configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [],\n\t\t\treturns: T.void\n\t\t},`);
const configDir = `${this.cwd}/@types/${filename}`;
mkdirSync(configDir, { recursive: true });
Bun.write(`${configDir}/lastModified`, lastModified(this.config.importPath));

const configWriter = Bun.file(`${configDir}/config.ts`).writer();
configWriter.write(`import type { LoaderConfig } from "hyperimport";\nimport { T } from "hyperimport";\nexport default {\n\tbuildCommand: ${JSON.stringify(this.config.buildCommand)},\n\toutDir: "${this.config.outDir}",\n\tsymbols: {`);

const types = this._config.parseTypes ? await this._config.parseTypes(this.config.importPath) : undefined;

if (types && Object.keys(types).length > 0) {
for (const [symbol, type] of Object.entries(types)) {
const args = type.args.join(", ");
if (type.line) {
// Convert Windows backslashes to forward slashes for proper file:// URL
const filePath = this.config.importPath.replace(/\\/g, '/');
configWriter.write(`\n\t\t/** Source: {@link file:///${filePath}#L${type.line}} */`);
}
configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [${args}],\n\t\t\treturns: ${type.returns}\n\t\t},`);
}
}
configWriter.write(`\n\t}\n} satisfies LoaderConfig.Main;`);
configWriter.end();
Bun.write(
await configWriter.end();

// Generate types.d.ts - simplified since JSDoc is in config.ts
await Bun.write(
`${this.cwd}/@types/${filename}/types.d.ts`,
`declare module "*/${filename}" {\n\tconst symbols: import("bun:ffi").ConvertFns<typeof import("./config.ts").default.symbols>;\n\texport = symbols;\n}`
);
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.`);
console.log(`\n\x1b[32mConfig file has been generated at "${this.cwd}/@types/${filename}/config.ts"\x1b[39m\nTypes have been automatically generated!`);
}

/**
Expand All @@ -77,22 +99,24 @@ export default class {
const lmfile = `${this.cwd}/@types/${basename(this.config.importPath)}/lastModified`;
if (lm !== readFileSync(lmfile).toString()) {
await this.build();
await this.initConfigTypes();
Bun.write(lmfile, lm);
}
}

/**
* Imports the symbols defined in `config.ts` to be used when opening the shared library.
* If `config.ts` isn't found, the source file isn't configured yet, hence executes `initConfig()` and exits the process.
* If `config.ts` doesn't exist, generates it automatically with type inference.
* @returns An object containing the symbols.
*/
async getSymbols(): Promise<Record<string, Narrow<FFIFunction>>> {
async getSymbols(): Promise<Record<string, FFIFunction>> {
try {
await this.ifSourceModify();
return (await import(`${this.cwd}/@types/${basename(this.config.importPath)}/config.ts`)).default.symbols;
} catch {
await this.initConfig();
process.exit();
// Config generated, now import and return it
return (await import(`${this.cwd}/@types/${basename(this.config.importPath)}/config.ts`)).default.symbols;
}
}

Expand All @@ -102,7 +126,8 @@ export default class {
async preload() {
this.config.outDir = this._config.outDir!(this.config.importPath);
this.config.buildCommand = this._config.buildCommand!(this.config.importPath, this.config.outDir);
this.config.libPath = `${this.config.outDir}/lib${parse(this.config.importPath).name}.${suffix}`;
const libPrefix = process.platform === "win32" ? "" : "lib";
this.config.libPath = `${this.config.outDir}/${libPrefix}${parse(this.config.importPath).name}.${suffix}`;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/loaders/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { basename } from "path";
import Loader from "../loader";
import { lastModified, nm } from "../utils";

export default class extends Loader {
export default class LibraryLoader extends Loader {
constructor() {
super("Library Loader",
{
extension: "so|dylib",
extension: "so|dylib|dll",
}
);
}
Expand Down
13 changes: 9 additions & 4 deletions src/loaders/rs.ts → src/loaders/rs/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { basename } from "path";
import Loader from "../loader";
import Loader from "../../loader";
import { parseRustTypes } from "./parse-types";

export default class extends Loader {
export default class RustLoader extends Loader {
constructor() {
super("Rust Loader",
{
Expand All @@ -14,8 +15,12 @@ export default class extends Loader {
"--out-dir",
outDir
],
outDir: importPath => `build/${basename(importPath)}`
outDir: importPath => `build/${basename(importPath)}`,
parseTypes: async (importPath) => {
const sourceCode = await Bun.file(importPath).text();
return parseRustTypes(sourceCode);
}
}
);
}
}
}
Loading