Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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");

Check failure on line 1 in build.js

View workflow job for this annotation

GitHub Actions / build-lint-test

A `require()` style import is forbidden
const path = require("path");

Check failure on line 2 in build.js

View workflow job for this annotation

GitHub Actions / build-lint-test

A `require()` style import is forbidden
const fs = require("fs");

Check failure on line 3 in build.js

View workflow job for this annotation

GitHub Actions / build-lint-test

A `require()` style import is forbidden

// 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
});
}
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"
}
}
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,
};
5 changes: 4 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
console.log("Core package");
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.
43 changes: 43 additions & 0 deletions packages/core/src/matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { deepEqual } from "./utils/deepEqual";
import { extendMatchers } from "./expect";

export interface MatcherMap {
[name: string]: MatcherFn<unknown>;
}

export const coreMatchers: MatcherMap = {};

export const matcherRegistry: MatcherMap = { ...coreMatchers };

extendMatchers({
toBe<T>(this: MatcherContext, received: T, expected: T) {
const pass = Object.is(received, expected);
if (this.isNot ? pass : !pass) {
throw new Error(
`Expected ${received} ${this.isNot ? "not " : ""}to be ${expected}`,
);
}
},

toEqual<T>(this: MatcherContext, received: T, expected: T) {
const pass = JSON.stringify(received) === JSON.stringify(expected);
Comment thread
ElijahKotyluk marked this conversation as resolved.

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.

JSON.stringify does not handle undefined values, functions, symbols, Maps, Sets, or circular references correctly. This will produce incorrect equality results for these types. Consider using deepEqual here or documenting that toEqual only supports JSON-serializable values.

Suggested change
const pass = JSON.stringify(received) === JSON.stringify(expected);
const pass = deepEqual(received, expected);

Copilot uses AI. Check for mistakes.
if (this.isNot ? pass : !pass) {
throw new Error(`Expected:\n${this.diff(received, expected)}`);
}
},
Comment thread
ElijahKotyluk marked this conversation as resolved.

toDeepEqual<T>(this: MatcherContext, received: T, expected: T) {
deepEqual(received, expected);
Comment thread
ElijahKotyluk marked this conversation as resolved.
Outdated
},
});

export interface MatcherContext {
isNot: boolean;
diff(a: unknown, b: unknown): string;
}

export type MatcherFn<T, A extends unknown[] = unknown[]> = (
this: MatcherContext,
received: T,
...args: A
) => void | Promise<void>;
Loading
Loading