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

test: use native ESM #2621

Merged
merged 16 commits into from
Dec 12, 2022
Merged
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/packages/schemas/tables @simeng-li @wangsijie
/packages/core/src/routes/session @simeng-li @wangsijie
/.changeset @gao-sun
8 changes: 5 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,22 @@ jobs:

- name: Setup Node and pnpm
uses: silverhand-io/actions-node-pnpm-run-steps@v2
with:
node-version: 18
xiaoyijun marked this conversation as resolved.
Show resolved Hide resolved

- name: Build
run: pnpm ci:build

main-lint:
# avoid out of memory issue since macOS has bigger memory
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
runs-on: ubuntu-latest-4-cores
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup Node and pnpm
uses: silverhand-io/actions-node-pnpm-run-steps@v2
with:
node-version: 18

- name: Prepack
run: pnpm prepack
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const config = {
coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'],
coverageReporters: ['text-summary', 'lcov'],
roots: ['./lib'],
moduleNameMapper: {
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
},
};

export default config;
17 changes: 0 additions & 17 deletions packages/cli/jest.config.ts

This file was deleted.

19 changes: 0 additions & 19 deletions packages/cli/jest.setup.ts

This file was deleted.

11 changes: 6 additions & 5 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
"precommit": "lint-staged",
"prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts",
"build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json",
"build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap",
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
"start": "node .",
"start:dev": "ts-node --files src/index.ts",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"test": "jest",
"test:ci": "jest",
"test": "pnpm build:test && NODE_OPTIONS=--experimental-vm-modules jest",
"test:ci": "pnpm run test",
"prepack": "pnpm build"
},
"engines": {
Expand All @@ -52,7 +53,7 @@
"hpagent": "^1.2.0",
"inquirer": "^8.2.2",
"nanoid": "^3.3.4",
"ora": "^5.0.0",
"ora": "^6.1.2",
"p-retry": "^4.6.1",
"roarr": "^7.11.0",
"semver": "^7.3.8",
Expand All @@ -65,21 +66,21 @@
},
"devDependencies": {
"@silverhand/eslint-config": "1.3.0",
"@silverhand/jest-config": "1.2.2",
"@silverhand/ts-config": "1.2.1",
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^8.2.1",
"@types/jest": "^29.1.2",
"@types/node": "^16.0.0",
"@types/semver": "^7.3.12",
"@types/sinon": "^10.0.13",
"@types/tar": "^6.1.2",
"@types/yargs": "^17.0.13",
"eslint": "^8.21.0",
"jest": "^29.3.1",
"lint-staged": "^13.0.0",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1",
"sinon": "^15.0.0",
"typescript": "^4.7.4"
},
"eslintConfig": {
Expand Down
60 changes: 35 additions & 25 deletions packages/cli/src/commands/database/alteration/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import { mockEsmWithActual } from '@logto/shared/esm';
import Sinon from 'sinon';
import { createMockPool } from 'slonik';

import * as queries from '../../../queries/logto-config.js';
import type { QueryType } from '../../../test-utilities.js';
import * as functions from './index.js';
import { chooseAlterationsByVersion } from './version.js';

const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
const { jest } = import.meta;

const pool = createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
query: jest.fn(),
});

describe('getUndeployedAlterations()', () => {
const files = Object.freeze([
{ filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' },
{ filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' },
{ filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' },
]);
const files = Object.freeze([
{ filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' },
{ filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' },
{ filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' },
]);

beforeEach(() => {
// `getAlterationFiles()` will ensure the order
jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]);
});
await mockEsmWithActual('./utils.js', () => ({
getAlterationFiles: async () => files,
}));

const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual(
'../../../queries/logto-config.js',
() => ({
getCurrentDatabaseAlterationTimestamp: jest.fn(),
})
);

const { getUndeployedAlterations } = await import('./index.js');

describe('getUndeployedAlterations()', () => {
it('returns all files if database timestamp is 0', async () => {
jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0);
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(0);
xiaoyijun marked this conversation as resolved.
Show resolved Hide resolved

await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files);
await expect(getUndeployedAlterations(pool)).resolves.toEqual(files);
});

it('returns files whose timestamp is greater then database timestamp', async () => {
jest
.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp')
.mockResolvedValueOnce(1_663_923_770);
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(1_663_923_770);

await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
await expect(getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
});
});

Expand All @@ -58,12 +61,19 @@ describe('chooseAlterationsByVersion()', () => {
'next1-1663923781-c.js',
].map((filename) => ({ filename, path: '/alterations/' + filename }))
);
const stub = Sinon.stub(global, 'process').value({ stdin: { isTTY: false } });

afterAll(() => {
stub.restore();
});

it('chooses nothing when input version is invalid', async () => {
await expect(chooseAlterationsByVersion(files, 'next1')).rejects.toThrow(
'Invalid Version: next1'
new TypeError('Invalid Version: next1')
);
await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow(
new TypeError('Invalid Version: ok')
);
await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow('Invalid Version: ok');
});

it('chooses correct alteration files', async () => {
Expand Down
55 changes: 2 additions & 53 deletions packages/cli/src/commands/database/alteration/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { fileURLToPath } from 'node:url';
import path from 'path';

import type { AlterationScript } from '@logto/schemas/lib/types/alteration.js';
import { findPackage } from '@logto/shared';
import { conditionalString } from '@silverhand/essentials';
import chalk from 'chalk';
import fsExtra from 'fs-extra';
import type { DatabasePool } from 'slonik';
import type { CommandModule } from 'yargs';

Expand All @@ -14,25 +9,11 @@ import {
getCurrentDatabaseAlterationTimestamp,
updateDatabaseTimestamp,
} from '../../../queries/logto-config.js';
import { getPathInModule, log } from '../../../utilities.js';
import { metaUrl } from './meta-url.js';
import { log } from '../../../utilities.js';
import type { AlterationFile } from './type.js';
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
import { chooseAlterationsByVersion } from './version.js';

const currentDirname = path.dirname(fileURLToPath(metaUrl));
const { copy, existsSync, remove, readdir } = fsExtra;
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;

const getTimestampFromFilename = (filename: string) => {
const match = alterationFilenameRegex.exec(filename);

if (!match?.[1]) {
throw new Error(`Can not get timestamp: ${filename}`);
}

return Number(match[1]);
};

const importAlterationScript = async (filePath: string): Promise<AlterationScript> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const module = await import(filePath);
Expand All @@ -41,38 +22,6 @@ const importAlterationScript = async (filePath: string): Promise<AlterationScrip
return module.default as AlterationScript;
};

export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js');

/**
* We copy all alteration scripts to the CLI package root directory,
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
* While the original `@logto/schemas` may remove them in production.
*/
const packageDirectory = await findPackage(currentDirname);

const localAlterationDirectory = path.resolve(
packageDirectory ?? currentDirname,
'alteration-scripts'
);

if (!existsSync(alterationDirectory)) {
return [];
}

// We need to copy alteration files to execute in the CLI context to make `slonik` available
await remove(localAlterationDirectory);
await copy(alterationDirectory, localAlterationDirectory);

const directory = await readdir(localAlterationDirectory);
const files = directory.filter((file) => alterationFilenameRegex.test(file));

return files
.slice()
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
};

export const getLatestAlterationTimestamp = async () => {
const files = await getAlterationFiles();
const lastFile = files[files.length - 1];
Expand Down
55 changes: 55 additions & 0 deletions packages/cli/src/commands/database/alteration/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { fileURLToPath } from 'node:url';
import path from 'path';

import { findPackage } from '@logto/shared';
import fsExtra from 'fs-extra';

import { getPathInModule } from '../../../utilities.js';
import { metaUrl } from './meta-url.js';
import type { AlterationFile } from './type.js';

const currentDirname = path.dirname(fileURLToPath(metaUrl));
const { copy, existsSync, remove, readdir } = fsExtra;
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;

export const getTimestampFromFilename = (filename: string) => {
const match = alterationFilenameRegex.exec(filename);

if (!match?.[1]) {
throw new Error(`Can not get timestamp: ${filename}`);
}

return Number(match[1]);
};

export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js');

/**
* We copy all alteration scripts to the CLI package root directory,
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
* While the original `@logto/schemas` may remove them in production.
*/
const packageDirectory = await findPackage(currentDirname);

const localAlterationDirectory = path.resolve(
packageDirectory ?? currentDirname,
'alteration-scripts'
);

if (!existsSync(alterationDirectory)) {
return [];
}

// We need to copy alteration files to execute in the CLI context to make `slonik` available
await remove(localAlterationDirectory);
await copy(alterationDirectory, localAlterationDirectory);

const directory = await readdir(localAlterationDirectory);
const files = directory.filter((file) => alterationFilenameRegex.test(file));

return files
.slice()
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
};
1 change: 1 addition & 0 deletions packages/cli/src/commands/database/alteration/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const chooseAlterationsByVersion = async (
.filter((version, index, self) => index === self.findIndex((another) => eq(version, another)))
.slice()
.sort((i, j) => compare(j, i));

const initialSemVersion = conditional(
initialVersion && initialVersion !== latestTag && new SemVer(initialVersion)
);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/queries/logto-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { QueryType } from '../test-utilities.js';
import { expectSqlAssert } from '../test-utilities.js';
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config.js';

const { jest } = import.meta;
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();

const pool = createMockPool({
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Progress } from 'got';
import { got } from 'got';
import { HttpsProxyAgent } from 'hpagent';
import inquirer from 'inquirer';
import type { Options } from 'ora';
import ora from 'ora';
import { z } from 'zod';

Expand Down Expand Up @@ -92,7 +93,7 @@ export const getPathInModule = (moduleName: string, relativePath = '/') =>

export const oraPromise = async <T>(
promise: PromiseLike<T>,
options?: ora.Options,
options?: Options,
exitOnError = false
) => {
const spinner = ora(options).start();
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"compilerOptions": {
"isolatedModules": false,
"allowJs": true
}
},
"include": ["src"]
}
Loading