Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 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
});
}
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",
"onyx.mjs",
],
},
{
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
Loading