Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ name: CI/CD Pipeline
on:
push:
branches:
- main
- master
- develop
pull_request:
branches:
- main
- master
- develop

Expand Down Expand Up @@ -34,8 +36,8 @@ jobs:
- name: Run Linter
run: pnpm lint

- name: Run Tests
run: pnpm test:all
# - name: Run Tests
# run: pnpm test:all

- name: Run Coverage
run: pnpm coverage:all
Comment thread
ElijahKotyluk marked this conversation as resolved.
Outdated
Expand Down
1 change: 0 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pnpm test
pnpm lint
pnpm typecheck
34 changes: 34 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const esbuild = require("esbuild");
const path = require("path");
const fs = require("fs");

// List your packages here
const packages = [
"core",
// Add more package names as needed
];

for (const pkg of packages) {
const srcDir = path.join(__dirname, `packages/${pkg}/src`);
const distDir = path.join(__dirname, `packages/${pkg}/dist`);
// Find all .ts entry points (excluding test files)
const entryPoints = fs
.readdirSync(srcDir)
.filter((f) => f.endsWith(".ts") && !f.endsWith(".spec.ts"))
.map((f) => path.join(srcDir, f));

if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });

esbuild.buildSync({
entryPoints,
outdir: distDir,
// outfile: path.join(distDir, 'index.js'),
bundle: true,
format: "esm",
platform: "node",
sourcemap: true,
target: ["node24"],
tsconfig: path.join(__dirname, `packages/${pkg}/tsconfig.build.json`),
external: ["tsconfig.json"], // Add external dependencies if needed
});
}
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default tsEslint.config(
"**/coverage",
"**/out",
"**/lib",
"build.js",
],
},
{
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"workspaces": [
"packages/*"
],
"scripts": {
"scripts": {
"build:esbuild": "node build.js",
"clean": "pnpm run -r clean",
"clean:all": "npx lerna run clean",
"build": "pnpm run -r build",
Expand All @@ -27,7 +28,8 @@
},
"devDependencies": {
"@eslint/js": "~9.38.0",
"@vitest/coverage-v8": "^4.0.9",
"@types/node": "^24.10.1",
"esbuild": "^0.27.0",
"eslint": "~9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "~5.5.0",
Expand All @@ -36,8 +38,6 @@
"lerna": "^9.0.1",
"prettier": "~3.6.0",
"typescript": "^5.9.3",
"typescript-eslint": "~8.46.0",
"vitest": "^4.0.9"
},
"dependencies": {}
"typescript-eslint": "~8.46.0"
}
}
36 changes: 36 additions & 0 deletions packages/core/bin/onyx.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
"use strict";

import { existsSync } from "fs";
import { dirname, join, resolve } from "path";

const rootDir = resolve(process.cwd());

const CONFIG_BASENAME = "onyx.config";
const CONFIG_EXTS = [".js", ".ts", ".json", ".mjs", ".cjs"];

function findOnyxConfig(startDir = rootDir) {
let dir = startDir;

while (true) {
for (const ext of CONFIG_EXTS) {
const configPath = join(dir, CONFIG_BASENAME + ext);
if (existsSync(configPath)) return configPath;
}

const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}

return null;
}

const configPath = findOnyxConfig();

if (configPath) {
const config = await import(configPath);
console.log("Config contents:", config.default || config);
}

await import("../dist/cli.js");
9 changes: 9 additions & 0 deletions packages/core/onyx.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "@onyxjs/core";

Copilot AI Nov 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import path '@onyxjs/core' doesn't match the package name '@onyx.js/core' defined in package.json. Update to use '@onyx.js/core' for consistency.

Suggested change
import { defineConfig } from "@onyxjs/core";
import { defineConfig } from "@onyx.js/core";

Copilot uses AI. Check for mistakes.

export default defineConfig({
testDir: "test",
timeoutMs: 5000,
bail: false,
includes: ["**/*.spec.ts"],
excludes: ["**/node_modules/**", "**/dist/**"],
});
28 changes: 23 additions & 5 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,42 @@
"name": "@onyxjs/core",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"files": [
"dist"
"dist",
"bin"
],
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./*": "./*"
},
"bin": {
"onyx": "./bin/onyx.mjs"
},
"scripts": {
"build": "pnpm run clean && pnpm run compile",
"build:tsc": "tsc -p ./tsconfig.build.json && tsc-esm-fix dist --tsconfig ./tsconfig.build.json",
"build:esbuild": "pnpm clean && pnpm build:tsc && pnpm --filter @onyxjs/core exec node ../../build.js",
"clean": "rm -rf ./dist",
"compile": "tsc -p tsconfig.build.json",
"compile": "tsc -p tsconfig.build.json && tsc-esm-fix dist --tsconfig ./tsconfig.build.json",
"prepublishOnly": "pnpm run build",
"typecheck": "tsc -p tsconfig.build.json --noEmit",
"test": "vitest run"
"onyx": "node bin/onyx.mjs"
},
"keywords": [],
"author": "elijah kotyluk <elijah@elijahkotyluk.com>",
"license": "ISC",
"packageManager": "pnpm@10.20.0",
"devDependencies": {
"chalk": "^5.6.2",
"ts-node": "^10.9.2",
"tsc-esm-fix": "^3.1.2",
"typescript": "^5.9.3"
}
}
11 changes: 11 additions & 0 deletions packages/core/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node

import { runTestsCLI } from "./runner";

const args = process.argv.slice(2);
const patternArg = args[0] ? new RegExp(args[0]) : undefined;
Comment thread
ElijahKotyluk marked this conversation as resolved.

runTestsCLI({ pattern: patternArg }).catch((err) => {
console.error(err);
process.exit(1);
});
10 changes: 10 additions & 0 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Suite } from "./suite";

interface OnyxGlobalContext {
currentSuite: Suite | null;
}
const onyxGlobalContext: OnyxGlobalContext = {
currentSuite: null,
};

export { onyxGlobalContext, type OnyxGlobalContext };
74 changes: 74 additions & 0 deletions packages/core/src/expect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
MatcherContext,
MatcherFn,
MatcherMap,
matcherRegistry,
} from "./matchers";

export interface ExpectInterface {
<T>(value: T): Expectation<T>;
extend<M extends MatcherMap>(m: M): void;
}

type Expectation<T> = {
not: Expectation<T>;
} & {
[K in keyof typeof matcherRegistry]: (typeof matcherRegistry)[K] extends MatcherFn<
T,
infer A
>
? (...args: A) => ReturnType<(typeof matcherRegistry)[K]>
: never;
};

export function extendMatchers(newMatchers: MatcherMap) {
for (const key in newMatchers) {
matcherRegistry[key] = newMatchers[key];
}
}

export const expect: ExpectInterface = (function () {
function expectFn<T>(received: T): Expectation<T> {
function makeExpectation(isNot: boolean): Expectation<T> {
const ctx: MatcherContext = {
isNot,
diff(a, b) {
return (
JSON.stringify(a, null, 2) + "\nvs\n" + JSON.stringify(b, null, 2)
);
},
};

const handler: Record<string, unknown> = {};

for (const name in matcherRegistry) {
const fn = matcherRegistry[name] as MatcherFn<T>;

handler[name] = (...args: unknown[]) =>
fn.call(ctx, received, ...(args as []));
}

return new Proxy(handler as Expectation<T>, {
get(target, prop) {
if (prop === "not") {
return makeExpectation(!isNot);
}
return target[prop as keyof typeof target];
},
});
}

return makeExpectation(false);
}

(expectFn as ExpectInterface).extend = extendMatchers;

return expectFn as ExpectInterface;
})();

expect.extend = extendMatchers;
Object.defineProperty(expect, "matchers", {
get() {
return matcherRegistry;
},
});
40 changes: 40 additions & 0 deletions packages/core/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getCurrentSuite } from "./suite";
import { PromisableFn } from "./types";

type HookFn = PromisableFn<void>;

type Hook = Array<HookFn>;

interface Hooks {
beforeAll: Hook;
afterAll: Hook;
beforeEach: Hook;
afterEach: Hook;
}

type HookName = keyof Hooks;

function beforeAll(...hooks: HookFn[]): void {
getCurrentSuite().beforeAllHooks.push(...hooks);
}
function afterAll(...hooks: HookFn[]): void {
getCurrentSuite().afterAllHooks.push(...hooks);
}
function beforeEach(...hooks: HookFn[]): void {
getCurrentSuite().beforeEachHooks.push(...hooks);
}

function afterEach(...hooks: HookFn[]): void {
getCurrentSuite().afterEachHooks.push(...hooks);
}

export {
beforeAll,
afterAll,
beforeEach,
afterEach,
type Hooks,
type HookName,
type Hook,
type HookFn,
};
7 changes: 6 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
console.log("Core package");
export { defineConfig } from "./utils/defineConfig";

export { expect } from "./expect";
export { describe, it } from "./interface";

export { beforeEach, afterEach, beforeAll, afterAll } from "./hooks";
33 changes: 33 additions & 0 deletions packages/core/src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { onyxGlobalContext } from "./context";
import { getCurrentSuite, Suite } from "./suite";
import { Test } from "./test";

import type { PromisableFn } from "./types";

function _describe(description: string, fn: PromisableFn<void>) {

Copilot AI Nov 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fn parameter is typed as PromisableFn (which can return a Promise), but it's called synchronously on line 14 without await. This could cause issues if fn returns a Promise. Either change the type to exclude Promise or handle async execution properly.

Copilot uses AI. Check for mistakes.
const parent = getCurrentSuite();
const suite = new Suite(description, parent);
parent.addSuite(suite);

onyxGlobalContext.currentSuite = suite;
try {
fn();
} finally {
onyxGlobalContext.currentSuite = parent;
}
}
Comment thread
ElijahKotyluk marked this conversation as resolved.

function _it(description: string, fn: PromisableFn<void>) {
const test = new Test(description, fn);
getCurrentSuite().addTest(test);
}

const describe = (description: string, fn: PromisableFn<void>) => {
return _describe(description, fn);
};

const it = (description: string, fn: PromisableFn<void>) => {
return _it(description, fn);
};

export { describe, it };
Comment on lines +25 to +33

Copilot AI Nov 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] These wrapper functions are unnecessary - they simply forward to the internal functions without adding any value. Consider exporting _describe and _it directly as describe and it.

Suggested change
const describe = (description: string, fn: PromisableFn<void>) => {
return _describe(description, fn);
};
const it = (description: string, fn: PromisableFn<void>) => {
return _it(description, fn);
};
export { describe, it };
export { _describe as describe, _it as it };

Copilot uses AI. Check for mistakes.
Loading