Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CI github workflow #2

Merged
merged 6 commits into from
Jan 29, 2025
Merged
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
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on: ['push', 'pull_request']

jobs:
lint:
name: Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x

- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: yarn install
if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here!
run: yarn install --frozen-lockfile --ignore-scripts

- name: yarn lint
run: yarn lint --max-warnings=0

env:
CI: true

test:
name: Test
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: yarn install
if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here!
run: yarn install --frozen-lockfile --ignore-scripts

- name: yarn test
run: yarn test --ci --reporters="default" --reporters="github-actions"

env:
CI: true
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors"
"yoavbls.pretty-ts-errors",
"streetsidesoftware.code-spell-checker"
]
}
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"eslint.format.enable": true,
"eslint.useFlatConfig": true,
"cSpell.words": [
"autoconfig"
"autoconfig",
"proxymate"
]
}
}
96 changes: 94 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import globals from 'globals';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';

export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
eslintPluginUnicorn.configs['flat/recommended'],
eslintConfigPrettier,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: globals.node,
Expand All @@ -19,8 +19,100 @@ export default tseslint.config(
},
},
},
{
files: ['src/**/*.ts', 'tests/**/*.ts'],
rules: {
'no-console': 'warn',
'no-debugger': 'warn',
'no-empty-pattern': 'warn',
'no-empty': 'warn',
'no-param-reassign': 'warn',
'no-unreachable': 'warn',
'no-var': 'warn',
'no-warning-comments': 'warn',
'prefer-const': 'warn',
eqeqeq: 'warn',

'prettier/prettier': 'warn',

'unicorn/prevent-abbreviations': 'off',
'unicorn/prefer-optional-catch-binding': 'warn',
'unicorn/no-empty-file': 'warn',
'unicorn/prefer-set-has': 'warn',
/**
* There are situations where you might prefer to use `null` because
* `undefined` doesn't fit. For example, when merging one object into another
* with lodash's merge function, it completely ignores properties with
* `undefined` values and you may want to keep those properties.
*/
'unicorn/no-null': 'off',

'@typescript-eslint/no-magic-numbers': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-floating-promises': ['warn', { ignoreIIFE: true }],
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: false,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: false,
allowDirectConstAssertionInArrowFunctions: true,
allowConciseArrowFunctionExpressionsStartingWithVoid: false,
allowFunctionsWithoutTypeParameters: false,
allowedNames: [],
allowIIFEs: false,
},
],
'@typescript-eslint/explicit-module-boundary-types': 'warn',
'@typescript-eslint/typedef': [
'warn',
{
arrayDestructuring: true,
objectDestructuring: true,
arrowParameter: true,
memberVariableDeclaration: true,
parameter: true,
propertyDeclaration: true,
variableDeclaration: true,
variableDeclarationIgnoreFunction: true,
},
],
'@typescript-eslint/strict-boolean-expressions': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-for-in-array': 'warn',
'@typescript-eslint/consistent-type-exports': 'warn',
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-shadow': 'warn',
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
/**
* It's better to explicitly define the type. The implementation may change in the future,
* and you can forget to add the type if it's not there. Also, you may need to create a variable
* that could have several types and in this case it's better to explicitly define what are those types.
*/
'@typescript-eslint/no-inferrable-types': 'off',
// /**
// * The nullish coalescing operator only coalesces when the checked value is null or
// * undefined, but what if you want to coalesce on any falsy value like an empty string?
// * In that case you would need "||". This requires {"strictNullChecks": true}
// */
// '@typescript-eslint/prefer-nullish-coalescing': 'off',
},
},
{
files: ['**/*.js'],
// TS rules are applied by default so we have to disable the
// type-checking rules for JS files.
extends: [tseslint.configs.disableTypeChecked],
},
{
files: ['tests/**/*.ts'],
rules: {
'@typescript-eslint/no-magic-numbers': 'off',
},
},
);
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export default {
transform: {
'^.+.ts$': ['ts-jest', {}],
},
clearMocks: true,
};
10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
},
"scripts": {
"check-types": "yarn tsc --noEmit --pretty",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"lint": "eslint '{src,tests}/**/*.ts'",
"lint:fix": "eslint '{src,tests}/**/*.ts' --fix",
"dev": "tsx watch src/server.ts",
"test": "jest",
"test:watch": "jest --watch",
Expand All @@ -34,16 +34,14 @@
"@types/node": "^22.10.10",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"prettier": "3.4.2",
"ts-jest": "^29.2.5",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0"
},
"engines": {
"node": ">=22"
}
}
7 changes: 5 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ const envSchema = z.object({
.transform((val: string) => Number.parseInt(val, 10))
.default('2000'),
});
let env: z.infer<typeof envSchema>;

type Env = z.infer<typeof envSchema>;

let env: Env;

try {
env = envSchema.parse({
Expand All @@ -93,4 +96,4 @@ try {
}

export { config, env, LogLevel, NodeEnv };
export type { Config };
export type { Config, Env };
4 changes: 1 addition & 3 deletions src/utils/get-proxy-uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ export function getProxyUri(fastify: FastifyInstance): string {
throw new Error('Unable to determine server address');
}

fastify.log.info(address);

return `127.0.0.1:${String(address.port)}`;
return `${address.address}:${String(address.port)}`;
}
27 changes: 27 additions & 0 deletions tests/__helpers__/fastify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type {
FastifyInstance,
FastifyPluginAsync,
FastifyPluginCallback,
} from 'fastify';
import Fastify from 'fastify';

/**
* Builds a Fastify instance for testing.
*
* @param plugins - The plugins to register on the Fastify instance
* @returns The Fastify instance
*/
export function buildFastifyInstance(
plugins: (FastifyPluginAsync | FastifyPluginCallback)[] = [],
): FastifyInstance {
const fastify: FastifyInstance = Fastify();

for (const plugin of plugins) {
fastify.register(plugin);
}

beforeAll(() => fastify.ready());
afterAll(() => fastify.close());

return fastify;
}
17 changes: 17 additions & 0 deletions tests/plugins/add-app-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FastifyInstance, LightMyRequestResponse } from 'fastify';
import pkg from '../../package.json';
import { buildFastifyInstance } from '../__helpers__/fastify';
import addAppHeaders from '../../src/plugins/add-app-headers';

describe('Add App Headers Plugin', () => {
const fastify: FastifyInstance = buildFastifyInstance([addAppHeaders]);

it('returns ProxyMate header', async () => {
const res: LightMyRequestResponse = await fastify.inject({
method: 'GET',
url: '/',
});

expect(res.headers['x-proxymate']).toEqual(pkg.version);
});
});
54 changes: 54 additions & 0 deletions tests/plugins/print-registered-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { FastifyInstance } from 'fastify';
import type { Config, Env } from '../../src/config';
import { config, env } from '../../src/config';
import printRegisteredRoutes from '../../src/plugins/print-registered-routes';
import { buildFastifyInstance } from '../__helpers__/fastify';

jest.mock('../../src/config', () => ({
config: {
tld: undefined,
},
env: {
PORT: undefined,
},
}));

describe('Print Registered Routes Plugin', () => {
const fastify: FastifyInstance = buildFastifyInstance([
printRegisteredRoutes,
]);

it('logs routes on server start', async () => {
const mockedLogInfo: jest.SpyInstance<
void,
[msg: string, ...args: unknown[]],
unknown
> = jest.spyOn(fastify.log, 'info');
const mockedConfig: jest.MockedObjectDeep<Config> = jest.mocked(config);
const mockedEnv: jest.MockedObjectDeep<Env> = jest.mocked(env);

mockedConfig.tld = '.test';
mockedConfig.routes = {
'auth.app': 'http://localhost:3001',
'api.app': 'http://localhost:3002',
};
mockedEnv.PORT = 3000;

await fastify.listen({
port: mockedEnv.PORT,
});

const routesHostnames: string[] = Object.keys(mockedConfig.routes);

// Verify the logs
expect(mockedLogInfo).toHaveBeenCalledWith(
`Proxy listening on port ${String(mockedEnv.PORT)} for domains:`,
);
expect(mockedLogInfo).toHaveBeenCalledWith(
`- ${String(routesHostnames[0])}${String(mockedConfig.tld)}`,
);
expect(mockedLogInfo).toHaveBeenCalledWith(
`- ${String(routesHostnames[1])}${String(mockedConfig.tld)}`,
);
});
});
Loading