Skip to content
47 changes: 42 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,55 @@
"helixent": "dist/bin/helixent"
},
"files": [
"dist/bin/helixent"
"dist/bin/helixent",
"dist/js",
"dist/types"
],
"module": "index.ts",
"module": "./dist/js/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/js/index.js",
"default": "./dist/js/index.js"
},
"./foundation": {
"types": "./dist/types/foundation/index.d.ts",
"import": "./dist/js/foundation/index.js",
"default": "./dist/js/foundation/index.js"
},
"./agent": {
"types": "./dist/types/agent/index.d.ts",
"import": "./dist/js/agent/index.js",
"default": "./dist/js/agent/index.js"
},
"./coding": {
"types": "./dist/types/coding/index.d.ts",
"import": "./dist/js/coding/index.js",
"default": "./dist/js/coding/index.js"
},
"./community/openai": {
"types": "./dist/types/community/openai/index.d.ts",
"import": "./dist/js/community/openai/index.js",
"default": "./dist/js/community/openai/index.js"
},
"./community/anthropic": {
"types": "./dist/types/community/anthropic/index.d.ts",
"import": "./dist/js/community/anthropic/index.js",
"default": "./dist/js/community/anthropic/index.js"
}
},
"type": "module",
"scripts": {
"prepublishOnly": "bun run build:bin",
"prepublishOnly": "bun run check",
"release:patch": "npm version patch && npm publish",
"release:minor": "npm version minor && npm publish",
"hooks:install": "git config core.hooksPath .githooks",
"build:bin": "rm -rf dist/bin && bun build index.ts --compile --outfile dist/bin/helixent",
"build:js": "rm -rf dist/js && bun build ./index.ts --outdir ./dist/js --splitting --target bun",
"check": "tsc --noEmit && eslint . --ext .ts && bun test",
"build:js": "rm -rf dist/js && bun build ./src/index.ts ./src/foundation/index.ts ./src/agent/index.ts ./src/coding/index.ts ./src/community/openai/index.ts ./src/community/anthropic/index.ts --outdir ./dist/js --root ./src --splitting --target bun --packages external",
"build:types": "rm -rf dist/types && tsc -p tsconfig.build.json && bun run scripts/rewrite-dts-aliases.ts",
"check": "tsc --noEmit && eslint . --ext .ts && bun test && bun run check:package",
"check:package": "bun run build:js && bun run build:types && bun run build:bin && bun run scripts/check-package.ts",
"check:types": "tsc --noEmit",
"dev": "bun run index.ts",
"lint": "eslint . --ext .ts",
Expand Down
125 changes: 125 additions & 0 deletions scripts/check-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const packageJsonPath = resolve(root, "package.json");

type PackageExport = {
types?: string;
import?: string;
default?: string;
};

type PackageJson = {
files?: string[];
module?: string;
types?: string;
exports?: Record<string, PackageExport>;
};

const requiredExports: Record<string, { runtime: string; types: string }> = {
".": { runtime: "./dist/js/index.js", types: "./dist/types/index.d.ts" },
"./foundation": { runtime: "./dist/js/foundation/index.js", types: "./dist/types/foundation/index.d.ts" },
"./agent": { runtime: "./dist/js/agent/index.js", types: "./dist/types/agent/index.d.ts" },
"./coding": { runtime: "./dist/js/coding/index.js", types: "./dist/types/coding/index.d.ts" },
"./community/openai": {
runtime: "./dist/js/community/openai/index.js",
types: "./dist/types/community/openai/index.d.ts",
},
"./community/anthropic": {
runtime: "./dist/js/community/anthropic/index.js",
types: "./dist/types/community/anthropic/index.d.ts",
},
};

const pkg = (await Bun.file(packageJsonPath).json()) as PackageJson;
const errors: string[] = [];

if (pkg.module !== requiredExports["."]!.runtime) {
errors.push(`package.json module must point to ${requiredExports["."]!.runtime}`);
}
if (pkg.types !== requiredExports["."]!.types) {
errors.push(`package.json types must point to ${requiredExports["."]!.types}`);
}

for (const requiredFile of ["dist/bin/helixent", "dist/js", "dist/types"]) {
if (!pkg.files?.includes(requiredFile)) {
errors.push(`package.json files must include ${requiredFile}`);
}
}

for (const [subpath, targets] of Object.entries(requiredExports)) {
const exported = pkg.exports?.[subpath];
if (exported?.types !== targets.types) {
errors.push(`package.json export ${subpath} types must point to ${targets.types}`);
}
if (exported?.import !== targets.runtime || exported?.default !== targets.runtime) {
errors.push(`package.json export ${subpath} runtime must point to ${targets.runtime}`);
}

for (const target of [targets.runtime, targets.types]) {
const absoluteTarget = resolve(root, target.replace(/^\.\//, ""));
if (!(await Bun.file(absoluteTarget).exists())) {
errors.push(`Built export target is missing: ${target}`);
}
}
}

const typeGlob = new Bun.Glob("**/*.d.ts");
const relativeModuleSpecifier = /\b(?:from|import)\s*\(?\s*["'](\.\.?\/[^"']+)["']/g;
const explicitRuntimeExtension = /\.(?:[cm]?js|json|node)$/;
for await (const typeFile of typeGlob.scan({ cwd: resolve(root, "dist/types"), absolute: true })) {
const content = await Bun.file(typeFile).text();
if (content.includes('"@/') || content.includes("'@/")) {
errors.push(`Published declaration contains an unresolved internal alias: ${typeFile}`);
}

relativeModuleSpecifier.lastIndex = 0;
for (const match of content.matchAll(relativeModuleSpecifier)) {
const specifier = match[1];
if (specifier && !explicitRuntimeExtension.test(specifier)) {
errors.push(`Published declaration contains a NodeNext-unsafe relative specifier ${specifier}: ${typeFile}`);
}
}
}

const binaryPath = resolve(root, "dist/bin/helixent");
if (!(await Bun.file(binaryPath).exists())) {
errors.push("Built CLI binary is missing: dist/bin/helixent");
}

const pack = Bun.spawn(["bun", "pm", "pack", "--dry-run", "--ignore-scripts"], {
cwd: root,
stdout: "pipe",
stderr: "pipe",
});
const [packOutput, packError, packExitCode] = await Promise.all([
new Response(pack.stdout).text(),
new Response(pack.stderr).text(),
pack.exited,
]);

if (packExitCode !== 0) {
errors.push(`bun pm pack --dry-run failed: ${packError.trim()}`);
} else {
const expectedPublishedPaths = [
"dist/bin/helixent",
...Object.values(requiredExports).flatMap(({ runtime, types }) => [runtime, types]).map((target) => target.replace(/^\.\//, "")),
];

for (const path of expectedPublishedPaths) {
if (!packOutput.includes(path)) {
errors.push(`Package dry-run does not include required path: ${path}`);
}
}
}

if (errors.length > 0) {
console.error("Package smoke check failed:");
for (const error of errors) {
console.error(`- ${error}`);
}
process.exit(1);
}

console.info("Package smoke check passed.");
60 changes: 60 additions & 0 deletions scripts/rewrite-dts-aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { existsSync } from "node:fs";
import { dirname, relative, resolve, sep } from "node:path";

const typesRoot = resolve(import.meta.dir, "../dist/types");
const declarations = new Bun.Glob("**/*.d.ts");
const explicitRuntimeExtension = /\.(?:[cm]?js|json|node)$/;

function toPortableRelative(fromDir: string, target: string): string {
let specifier = relative(fromDir, target).split(sep).join("/");
if (!specifier.startsWith(".")) {
specifier = `./${specifier}`;
}
return specifier;
}

function resolveDeclarationRuntimeTarget(specifier: string, declarationPath: string): string | null {
if (specifier.startsWith("@/")) {
const target = resolve(typesRoot, specifier.slice(2));
if (existsSync(`${target}.d.ts`)) return `${target}.js`;
if (existsSync(resolve(target, "index.d.ts"))) return resolve(target, "index.js");
return null;
}

if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
return null;
}
if (explicitRuntimeExtension.test(specifier)) {
return null;
}

const target = resolve(dirname(declarationPath), specifier);
if (existsSync(`${target}.d.ts`)) return `${target}.js`;
if (existsSync(resolve(target, "index.d.ts"))) return resolve(target, "index.js");
return null;
}

function rewriteModuleSpecifiers(content: string, declarationPath: string): string {
const moduleSpecifier = /(\b(?:from|import)\s*\(?\s*)(["'])(@\/[^"']+|\.\.?\/[^"']+)\2/g;

return content.replace(moduleSpecifier, (match, prefix: string, quote: string, specifier: string) => {
const target = resolveDeclarationRuntimeTarget(specifier, declarationPath);
if (!target) return match;
const rewritten = toPortableRelative(dirname(declarationPath), target);
return `${prefix}${quote}${rewritten}${quote}`;
});
}

let rewrittenFiles = 0;
for await (const filePath of declarations.scan({ cwd: typesRoot, absolute: true })) {
const file = Bun.file(filePath);
const original = await file.text();
const rewritten = rewriteModuleSpecifiers(original, filePath);

if (rewritten !== original) {
await Bun.write(filePath, rewritten);
rewrittenFiles += 1;
}
}

console.info(`Rewrote module specifiers in ${rewrittenFiles} declaration file(s).`);
21 changes: 21 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"noEmit": false,
"declaration": true,
"declarationMap": false,
"emitDeclarationOnly": true,
"rootDir": "./src",
"outDir": "./dist/types"
},
"include": [
"src/index.ts",
"src/foundation/**/*.ts",
"src/agent/**/*.ts",
"src/coding/**/*.ts",
"src/community/openai/**/*.ts",
"src/community/anthropic/**/*.ts"
],
"exclude": ["**/__tests__/**"]
}