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
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./storage": {
"types": "./dist/storage.d.ts",
"import": "./dist/storage.js"
}
},
Expand All @@ -19,12 +20,10 @@
"logs-mcp": "./dist/mcp/index.js",
"logs-serve": "./dist/server/index.js"
},
"files": [
"dist",
"dashboard/dist"
],
"files": ["dist", "dashboard/dist"],
"scripts": {
"build": "rm -rf dist && bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts src/index.ts src/storage.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse",
"build": "rm -rf dist && bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts src/index.ts src/storage.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse && bun scripts/build-types.ts",
"build:types": "bun scripts/build-types.ts",
"build:dashboard": "cd dashboard && bun run build",
"build:all": "bun run build:dashboard && bun run build",
"dev": "bun run src/server/index.ts",
Expand All @@ -38,7 +37,11 @@
"validate:stress": "bun scripts/high-volume-ingest-stress.ts",
"validate:structured-logs": "bun scripts/structured-log-validation-lab.ts",
"validate:streams": "bun scripts/stream-load-validation.ts",
"validate:pack": "bun scripts/validate-package.ts",
"validate:package": "bun run validate:pack",
"lint": "biome check src/",
"prepack": "bun run build",
"prepublishOnly": "bun run validate:package",
"postinstall": "mkdir -p $HOME/.hasna/logs 2>/dev/null || true"
},
"repository": {
Expand Down
88 changes: 88 additions & 0 deletions scripts/build-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const distDir = join(repoRoot, "dist");

function run(command: string[], cwd = repoRoot): void {
const result = Bun.spawnSync(command, {
cwd,
stderr: "inherit",
stdout: "inherit",
});

if (result.exitCode !== 0) {
throw new Error(
`Command failed (${result.exitCode}): ${command.join(" ")}`,
);
}
}

function rewriteRelativeTsImports(directory: string): void {
for (const entry of readdirSync(directory)) {
const path = join(directory, entry);
const stats = statSync(path);

if (stats.isDirectory()) {
rewriteRelativeTsImports(path);
continue;
}

if (!path.endsWith(".d.ts")) {
continue;
}

const source = readFileSync(path, "utf8");
const rewritten = source.replace(
/(["'])(\.\.?\/[^"']+)\1/g,
(match, quote: string, specifier: string) => {
if (!specifier.endsWith(".ts") || specifier.endsWith(".d.ts")) {
return match;
}

return `${quote}${specifier.slice(0, -3)}.js${quote}`;
},
);

if (rewritten !== source) {
writeFileSync(path, rewritten);
}
}
}

run(["bun", "run", "build"], join(repoRoot, "sdk"));

mkdirSync(distDir, { recursive: true });
copyFileSync(
join(repoRoot, "sdk", "dist", "index.d.ts"),
join(distDir, "index.d.ts"),
);
copyFileSync(
join(repoRoot, "sdk", "dist", "types.d.ts"),
join(distDir, "types.d.ts"),
);

run([
join(repoRoot, "node_modules", ".bin", "tsc"),
"-p",
"tsconfig.types.json",
]);

if (!existsSync(join(distDir, "index.d.ts"))) {
throw new Error("Missing dist/index.d.ts after declaration build");
}

if (!existsSync(join(distDir, "storage.d.ts"))) {
throw new Error("Missing dist/storage.d.ts after declaration build");
}

rewriteRelativeTsImports(distDir);
228 changes: 228 additions & 0 deletions scripts/validate-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, posix, resolve } from "node:path";
import { fileURLToPath } from "node:url";

interface PackageJson {
bin?: Record<string, string> | string;
exports?: unknown;
main?: string;
types?: string;
typings?: string;
}

interface PackFile {
path: string;
}

interface PackResult {
files: PackFile[];
}

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const packageJson = JSON.parse(
readFileSync(join(repoRoot, "package.json"), "utf8"),
) as PackageJson;

function normalizePackagePath(path: string): string {
return path.replace(/\\/g, "/").replace(/^\.\//, "");
}

function isLocalPackagePath(value: string): boolean {
return value.startsWith("./");
}

function addRequiredPath(requiredPaths: Set<string>, value: unknown): void {
if (typeof value === "string" && isLocalPackagePath(value)) {
requiredPaths.add(normalizePackagePath(value));
}
}

function collectExportPaths(requiredPaths: Set<string>, value: unknown): void {
if (typeof value === "string") {
addRequiredPath(requiredPaths, value);
return;
}

if (!value || typeof value !== "object") {
return;
}

for (const [condition, target] of Object.entries(value)) {
if (
condition === "types" ||
condition === "import" ||
condition === "require" ||
condition === "default"
) {
addRequiredPath(requiredPaths, target);
}

collectExportPaths(requiredPaths, target);
}
}

function collectRequiredPackagePaths(): Set<string> {
const requiredPaths = new Set<string>();

addRequiredPath(requiredPaths, packageJson.main);
addRequiredPath(requiredPaths, packageJson.types);
addRequiredPath(requiredPaths, packageJson.typings);

if (typeof packageJson.bin === "string") {
addRequiredPath(requiredPaths, packageJson.bin);
} else if (packageJson.bin) {
for (const target of Object.values(packageJson.bin)) {
addRequiredPath(requiredPaths, target);
}
}

collectExportPaths(requiredPaths, packageJson.exports);

return requiredPaths;
}

function runPackDryRun(): PackResult {
const result = Bun.spawnSync(["npm", "pack", "--dry-run", "--json"], {
cwd: repoRoot,
stderr: "pipe",
stdout: "pipe",
});

if (result.exitCode !== 0) {
const stderr = new TextDecoder().decode(result.stderr);
throw new Error(`npm pack --dry-run failed:\n${stderr}`);
}

const stdout = new TextDecoder().decode(result.stdout);
const jsonStart = stdout.indexOf("[");
const jsonEnd = stdout.lastIndexOf("]");

if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
throw new Error(`npm pack --dry-run returned no JSON array:\n${stdout}`);
}

const packResults = JSON.parse(
stdout.slice(jsonStart, jsonEnd + 1),
) as PackResult[];
const packResult = packResults[0];

if (!packResult) {
throw new Error("npm pack --dry-run returned no package result");
}

return packResult;
}

function extractDeclarationSpecifiers(source: string): string[] {
const specifiers: string[] = [];
const pattern =
/(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']|import\(["']([^"']+)["']\)/g;

for (const match of source.matchAll(pattern)) {
const specifier = match[1] ?? match[2];
if (specifier) {
specifiers.push(specifier);
}
}

return specifiers;
}

function declarationTargetForSpecifier(
fromPath: string,
specifier: string,
): string | null {
if (!specifier.startsWith(".")) {
return null;
}

if (specifier.endsWith(".ts") && !specifier.endsWith(".d.ts")) {
throw new Error(
`${fromPath} imports ${specifier}; declaration files must reference emitted .js specifiers`,
);
}

const basePath = posix.normalize(
posix.join(posix.dirname(fromPath), specifier),
);

if (basePath.endsWith(".d.ts")) {
return basePath;
}

if (
basePath.endsWith(".js") ||
basePath.endsWith(".mjs") ||
basePath.endsWith(".cjs")
) {
return `${basePath.slice(0, basePath.lastIndexOf("."))}.d.ts`;
}

return `${basePath}.d.ts`;
}

function validateDeclarationImports(packFiles: Set<string>): string[] {
const errors: string[] = [];

for (const file of packFiles) {
if (!file.endsWith(".d.ts")) {
continue;
}

const absolutePath = join(repoRoot, file);
if (!existsSync(absolutePath)) {
errors.push(`${file} is listed by pack but does not exist locally`);
continue;
}

const source = readFileSync(absolutePath, "utf8");
for (const specifier of extractDeclarationSpecifiers(source)) {
try {
const target = declarationTargetForSpecifier(file, specifier);
if (target && !packFiles.has(target)) {
errors.push(
`${file} imports ${specifier}, but ${target} is not packed`,
);
}
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
}
}
}

return errors;
}

const packResult = runPackDryRun();
const packFiles = new Set(
packResult.files.map((file) => normalizePackagePath(file.path)),
);
const requiredPaths = collectRequiredPackagePaths();
const missingRequiredPaths = [...requiredPaths].filter(
(path) => !packFiles.has(path),
);
const declarationErrors = validateDeclarationImports(packFiles);

if (missingRequiredPaths.length > 0 || declarationErrors.length > 0) {
console.error("Package validation failed.");

if (missingRequiredPaths.length > 0) {
console.error("Missing package metadata targets:");
for (const path of missingRequiredPaths) {
console.error(` - ${path}`);
}
}

if (declarationErrors.length > 0) {
console.error("Invalid declaration imports:");
for (const error of declarationErrors) {
console.error(` - ${error}`);
}
}

process.exit(1);
}

console.log(
`Package validation passed: ${requiredPaths.size} metadata target(s), ${packFiles.size} packed file(s).`,
);
12 changes: 12 additions & 0 deletions tsconfig.types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/storage.ts"]
}