Skip to content

Commit

Permalink
feat: introduce startScript and startService (#91)
Browse files Browse the repository at this point in the history
* docs: improve documentation and descriptions

* ci: refactor tests to BDD

* docs(types): improve inline documentation

* feat: introduce `startScript` and `startService`

* ci: resolve port conflict on GitHub Actions

* ci: debug port conflict on GitHub Actions

* ci: debug port conflict on GitHub Actions

* ci: debug port conflict on GitHub Actions

* ci: forcing process exit on GitHub Actions
  • Loading branch information
wellwelwel authored Mar 10, 2024
1 parent 7581a4f commit bc414bb
Show file tree
Hide file tree
Showing 81 changed files with 1,205 additions and 1,595 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
jobs:
coverage:
runs-on: ubuntu-latest
timeout-minutes: 1
name: Coverage
steps:
- name: ➕ Actions - Checkout
Expand All @@ -30,7 +31,7 @@ jobs:
run: npm ci

- name: 🧪 Checking for Coverage
run: npm run test:ci:c8
run: npm run test:c8

- name: ☔️ Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Enjoying **Poku**? Consider giving him a star ⭐️

## Why Poku?

By creating **Poku**, my aim is to show that testing can be simpler 🌱
**Poku** can show you how simple testing can be 🌱

- No configurations
- Supports **ESM** and **CJS**
Expand Down Expand Up @@ -127,6 +127,8 @@ assert(true, 'Poku will describe it 🐷');
</tr>
</table>

> Note that these examples use [**ESM**](https://poku.io/docs/examples/cjs-esm), but you can use [**CJS**](https://poku.io/docs/examples/cjs-esm) as well.
### Run it 🚀

<table>
Expand Down Expand Up @@ -171,8 +173,10 @@ deno run npm:poku

### Essentials

- `poku` (_test runner_)
- `assert` and `assertPromise` (_test assertion_)
- [**poku**](https://poku.io/docs/category/poku) (_test runner_)
- [**assert**](https://poku.io/docs/documentation/assert) (_test assertion_)
- [**startScript**](https://poku.io/docs/documentation/startScript) (_run `package.json` scripts in a background process_)
- [**startService**](https://poku.io/docs/documentation/startService) (_run files in a background process_)

### Helpers

Expand Down
30 changes: 19 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@
"description": "🐷 Poku makes testing easy for Node.js, Bun & Deno at the same time.",
"main": "./lib/index.js",
"scripts": {
"test": "tsx --tsconfig ./tsconfig.test.json ./test/run.test.ts",
"test": "tsx ./test/run.test.ts",
"pretest:c8": "npm run build",
"test:c8": "docker compose -f test/docker/docker-compose.c8.yml up",
"test:ci:c8": "c8 --include 'src/**' --exclude 'src/@types/**' --reporter=text --reporter=lcov tsx test/run.test.ts",
"test:c8": "c8 --include 'src/**' --exclude 'src/@types/**' --reporter=text --reporter=lcov npx poku --parallel --debug test/unit,test/integration,test/e2e",
"test:ci": "tsx ./test/ci.test.ts",
"test:node": "FILTER='node-' npm run test:ci",
"test:deno": "FILTER='deno-' npm run test:ci",
"test:bun": "FILTER='bun-' npm run test:ci",
"predocker:deno": "docker compose -f ./test/docker/playground/deno/docker-compose.yml down",
"docker:deno": "docker compose -f ./test/docker/playground/deno/docker-compose.yml up --build",
"prebuild": "rm -rf ./lib ./ci",
"clear": "rm -rf ./lib ./ci",
"build": "tsc && tsc -p tsconfig.test.json",
"postbuild": "tsx ./tools/compatibility/node.ts && chmod +x lib/bin/index.js && npm audit",
"postbuild": "tsx ./tools/compatibility/node.ts && chmod +x lib/bin/index.js",
"eslint:checker": "eslint . --ext .js,.ts",
"eslint:fix": "eslint . --fix --config ./.eslintrc.json",
"lint:checker": "npm run eslint:checker && npm run prettier:checker",
Expand All @@ -41,9 +40,9 @@
"assert",
"assertion",
"testing",
"run",
"isolate",
"isolation",
"bun",
"deno",
"cli",
"concurrent",
"concurrency",
"parallelism",
Expand All @@ -52,22 +51,31 @@
"unit",
"integration",
"typescript",
"isolate",
"isolation",
"run",
"queue",
"queuing",
"nodejs",
"node",
"bun",
"deno",
"cli",
"cli-app",
"expect",
"mocha",
"chai",
"jest",
"ava",
"tap",
"tape",
"karma",
"urun",
"supertest",
"e2e",
"end-to-end",
"tdd",
"bdd",
"framework",
"tool",
"tools",
"filter",
"exclude",
"list",
Expand Down
42 changes: 42 additions & 0 deletions src/@types/background-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Runner } from './runner.js';
import { Configs } from './poku.js';

type BackgroundProcessOptions = {
/**
* - By default, it will resolve in the first console output
* - By setting a string: it will wait for a specifc string on console output to resolve
* - By setting a number: it will wait for time in milliseconds to resolve
*
* @default undefined
*/
startAfter?: string | number;
/**
* Stops the service after:
* @default 60000
*/
timeout?: number;
/**
* Shows the output from service
*/
verbose?: boolean;
/**
* Specify a target path to start the process
*
* @default "./"
*/
cwd?: string | undefined;
};

export type StartScriptOptions = {
/**
* By default, Poku will use `npm`. Change it as you want.
*/
readonly runner?: Runner;
} & BackgroundProcessOptions;

export type StartServiceOptions = {
/**
* By default, Poku will try to identify the actual platform, but you can specify it manually
*/
readonly platform?: Configs['platform'];
} & BackgroundProcessOptions;
1 change: 1 addition & 0 deletions src/@types/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Runner = 'npm' | 'bun' | 'deno' | 'yarn' | 'pnpm';
30 changes: 29 additions & 1 deletion src/helpers/runner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import process from 'node:process';
import path from 'node:path';
import { getRuntime } from './get-runtime.js';
import { Configs } from '../@types/poku.js';
import { Runner } from '../@types/runner.js';

const isWindows = process.platform === 'win32';

export const runner = (filename: string, configs?: Configs): string[] => {
const runtime = getRuntime(configs);

// Bun
if (runtime === 'bun') return ['bun'];

// Deno
if (runtime === 'deno')
return ['deno', 'run', '--allow-read', '--allow-env', '--allow-run'];
return path.extname(filename) === '.ts' ? ['tsx'] : ['node'];

// Node.js
return path.extname(filename) === '.ts'
? [isWindows ? 'npx.cmd' : 'npx', 'tsx']
: ['node'];
};

export const scriptRunner = (runner: Runner): string[] => {
// Bun
if (runner === 'bun') return ['bun'];

// Deno
if (runner === 'deno') return ['deno', 'task'];

// Yarn
if (runner === 'yarn') return ['yarn'];

// PNPM
if (runner === 'pnpm') return ['pnpm', 'run'];

// Node.js
return ['npm', 'run'];
};
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export { assertPromise } from './modules/assert-promise.js';
export { beforeEach, afterEach } from './modules/each.js';
export { publicListFiles as listFiles } from './modules/list-files.js';
export { test } from './modules/test.js';
export type { Code } from './@types/code.ts';
export type { Configs } from './@types/poku.ts';
export type { Configs as ListFilesConfigs } from './@types/list-files.ts';
export { startService, startScript } from './modules/create-service.js';
export type { Code } from './@types/code.js';
export type { Configs } from './@types/poku.js';
export type { Configs as ListFilesConfigs } from './@types/list-files.js';
export type {
StartServiceOptions,
StartScriptOptions,
} from './@types/background-process.js';
2 changes: 1 addition & 1 deletion src/modules/assert-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ const match = async (
message?: ParseAssertionOptions['message']
): Promise<void> => {
if (typeof version === 'number' && version < 12) {
throw new Error('doesNotMatch is available from Node.js 12 or higher');
throw new Error('match is available from Node.js 12 or higher');
}

await parseAssertion(() => nodeAssert?.match(value, regExp), {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const match = (
message?: ParseAssertionOptions['message']
): void => {
if (typeof version === 'number' && version < 12) {
throw new Error('doesNotMatch is available from Node.js 12 or higher');
throw new Error('match is available from Node.js 12 or higher');
}

parseAssertion(() => nodeAssert?.match(value, regExp), {
Expand Down
122 changes: 122 additions & 0 deletions src/modules/create-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import process from 'node:process';
import { spawn } from 'node:child_process';
import { runner, scriptRunner } from '../helpers/runner.js';
import path from 'node:path';
import {
StartScriptOptions,
StartServiceOptions,
} from '../@types/background-process.js';
import { sanitizePath } from './list-files.js';

const backgroundProcess = (
runtime: string,
args: string[],
file: string,
options?: StartServiceOptions
): Promise<{ end: () => boolean }> =>
new Promise((resolve, reject) => {
let isResolved = false;

const service = spawn(runtime, args, {
stdio: ['inherit', 'pipe', 'pipe'],
shell: false,
cwd: options?.cwd ? sanitizePath(path.normalize(options.cwd)) : undefined,
env: process.env,
detached: true,
});

const end = () => {
process.kill(-service.pid!, 'SIGKILL');
return true;
};

service.stdout.on('data', (data: Buffer) => {
if (!isResolved && typeof options?.startAfter !== 'number') {
const stringData = String(data);

if (
typeof options?.startAfter === 'undefined' ||
(typeof options?.startAfter === 'string' &&
stringData.includes(options.startAfter))
) {
resolve({ end });
clearTimeout(timeout);

isResolved = true;
}
}

options?.verbose && console.log(String(data));
});

service.stderr.on('data', (data: Buffer) => {
reject(new Error(`Service failed to start: ${data}`));
});

service.on('error', (err) => {
reject(new Error(`Service failed to start: ${err}`));
});

service.on('close', (code) => {
if (code !== 0) reject(new Error(`Service exited with code ${code}`));
});

const timeout = setTimeout(() => {
if (!isResolved) {
service.kill();
reject(`createService: Timeout\nFile: ${file}`);
}
}, options?.timeout || 10000);

if (typeof options?.startAfter === 'number') {
setTimeout(() => {
if (!isResolved) {
resolve({ end });
clearTimeout(timeout);

isResolved = true;
}
}, options.startAfter);
}
});

/**
*
* Starts a file in a background process
*
* Useful for servers, APIs, etc.
*/
export const startService = async (
file: string,
options?: StartServiceOptions
): Promise<{ end: () => boolean }> => {
const runtimeOptions = runner(file, { platform: options?.platform });
const runtime = runtimeOptions.shift()!;
const runtimeArgs = [...runtimeOptions, file];

return await backgroundProcess(
runtime,
runtimeArgs,
path.normalize(sanitizePath(file)),
options
);
};

/**
*
* Starts a script (package.json) or task (deno.json) in a background process
*
* By default, it uses **npm**, but you can costumize it using the `runner` option.
*
* Useful for servers, APIs, etc.
*/
export const startScript = async (
script: string,
options?: StartScriptOptions
): Promise<{ end: () => boolean }> => {
const runtimeOptions = scriptRunner(options?.runner || 'npm');
const runtime = runtimeOptions.shift()!;
const runtimeArgs = [...runtimeOptions, script];

return await backgroundProcess(runtime, runtimeArgs, script, options);
};
2 changes: 1 addition & 1 deletion src/modules/describe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type DescribeOptions = {
* By default **Poku** only shows outputs generated from itself.
* This helper allows you to use an alternative to `console.log` with **Poku**.
*
* Need to debug? Just use the [`debug`](https://poku.io/docs/documentation/poku/configs/debug) option from `poku`.
* Need to debug? Just use the [`debug`](https://poku.io/docs/documentation/poku/options/debug) option from `poku`.
*/
export const log = (message: string) => console.log(`\x1b[0m${message}\x1b[0m`);

Expand Down
Loading

0 comments on commit bc414bb

Please sign in to comment.