diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.ts b/packages/cli/mocks/configs/code-pushup.config.ts similarity index 91% rename from testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.ts rename to packages/cli/mocks/configs/code-pushup.config.ts index aad20f9b6..7aa6f327e 100644 --- a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.ts +++ b/packages/cli/mocks/configs/code-pushup.config.ts @@ -1,5 +1,3 @@ -import { type CoreConfig } from '@code-pushup/models'; - export default { upload: { organization: 'code-pushup', @@ -42,4 +40,4 @@ export default { icon: 'javascript', }, ], -} satisfies CoreConfig; +}; diff --git a/packages/cli/mocks/configs/code-pushup.needs-tsconfig.config.ts b/packages/cli/mocks/configs/code-pushup.needs-tsconfig.config.ts new file mode 100644 index 000000000..d5d13931a --- /dev/null +++ b/packages/cli/mocks/configs/code-pushup.needs-tsconfig.config.ts @@ -0,0 +1,12 @@ +// the point is to test runtime import which requires tsconfig for path aliases + +/* eslint-disable import/no-unresolved */ +// @ts-expect-error - test tsconfig paths missing in config +import customPlugin from '@example/custom-plugin'; + +/* eslint-enable import/no-unresolved */ +const config = { + plugins: [customPlugin], +}; + +export default config; diff --git a/testing/test-fixtures/src/lib/fixtures/configs/custom-plugin.ts b/packages/cli/mocks/configs/custom-plugin.ts similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/custom-plugin.ts rename to packages/cli/mocks/configs/custom-plugin.ts diff --git a/packages/cli/mocks/configs/tsconfig.json b/packages/cli/mocks/configs/tsconfig.json new file mode 100644 index 000000000..d0072f442 --- /dev/null +++ b/packages/cli/mocks/configs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "allowImportingTsExtensions": true, + "moduleResolution": "node", + "paths": { + "@example/custom-plugin": ["./custom-plugin.ts"] + } + }, + "include": ["*.ts"] +} diff --git a/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts b/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts index 4aeba1175..f29d2cd53 100644 --- a/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts +++ b/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts @@ -8,12 +8,8 @@ const configDirPath = path.join( '..', '..', '..', - '..', - 'testing', - 'test-fixtures', - 'src', - 'lib', - 'fixtures', + 'cli', + 'mocks', 'configs', ); @@ -24,22 +20,13 @@ describe('coreConfigMiddleware', () => { skipPlugins: [], }; - it.each(['ts', 'mjs', 'js'])( - 'should load a valid .%s config', - async extension => { - const config = await coreConfigMiddleware({ - config: path.join(configDirPath, `code-pushup.config.${extension}`), - ...CLI_DEFAULTS, - }); - expect(config.config).toContain(`code-pushup.config.${extension}`); - expect(config.upload?.project).toContain(extension); - }, - ); - - it('should throw with invalid config path', async () => { - await expect( - coreConfigMiddleware({ config: 'wrong/path/to/config', ...CLI_DEFAULTS }), - ).rejects.toThrow(/File '.*' does not exist/); + it('should load a valid .%s config', async () => { + const config = await coreConfigMiddleware({ + config: path.join(configDirPath, `code-pushup.config.ts`), + ...CLI_DEFAULTS, + }); + expect(config.config).toContain(`code-pushup.config.ts`); + expect(config.upload?.project).toContain('ts'); }); it('should load config which relies on provided --tsconfig', async () => { @@ -54,16 +41,4 @@ describe('coreConfigMiddleware', () => { }), ).resolves.toBeTruthy(); }); - - it('should throw if --tsconfig is missing but needed to resolve import', async () => { - await expect( - coreConfigMiddleware({ - config: path.join( - configDirPath, - 'code-pushup.needs-tsconfig.config.ts', - ), - ...CLI_DEFAULTS, - }), - ).rejects.toThrow("Cannot find package '@example/custom-plugin'"); - }); }); diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.js b/packages/core/mocks/fixtures/configs/code-pushup.config.js similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.js rename to packages/core/mocks/fixtures/configs/code-pushup.config.js diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.empty.config.js b/packages/core/mocks/fixtures/configs/code-pushup.empty.config.js similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/code-pushup.empty.config.js rename to packages/core/mocks/fixtures/configs/code-pushup.empty.config.js diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.invalid.config.ts b/packages/core/mocks/fixtures/configs/code-pushup.invalid.config.ts similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/code-pushup.invalid.config.ts rename to packages/core/mocks/fixtures/configs/code-pushup.invalid.config.ts diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.needs-tsconfig.config.ts b/packages/core/mocks/fixtures/configs/code-pushup.needs-tsconfig.config.ts similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/code-pushup.needs-tsconfig.config.ts rename to packages/core/mocks/fixtures/configs/code-pushup.needs-tsconfig.config.ts diff --git a/packages/core/mocks/fixtures/configs/custom-plugin.ts b/packages/core/mocks/fixtures/configs/custom-plugin.ts new file mode 100644 index 000000000..6afe6bc80 --- /dev/null +++ b/packages/core/mocks/fixtures/configs/custom-plugin.ts @@ -0,0 +1,24 @@ +const customPluginConfig = { + slug: 'good-feels', + title: 'Good feels', + icon: 'javascript', + audits: [ + { + slug: 'always-perfect', + title: 'Always perfect', + }, + ], + runner: () => [ + { + slug: 'always-perfect', + score: 1, + value: 100, + displayValue: '✅ Perfect! 👌', + }, + ], +}; + +export function customPlugin() { + return customPluginConfig; +} +export default customPluginConfig; diff --git a/testing/test-fixtures/src/lib/fixtures/configs/tsconfig.json b/packages/core/mocks/fixtures/configs/tsconfig.json similarity index 100% rename from testing/test-fixtures/src/lib/fixtures/configs/tsconfig.json rename to packages/core/mocks/fixtures/configs/tsconfig.json diff --git a/packages/core/src/lib/implementation/read-rc-file.int.test.ts b/packages/core/src/lib/implementation/read-rc-file.int.test.ts index 59a520175..ac01c8987 100644 --- a/packages/core/src/lib/implementation/read-rc-file.int.test.ts +++ b/packages/core/src/lib/implementation/read-rc-file.int.test.ts @@ -8,12 +8,7 @@ describe('readRcByPath', () => { '..', '..', '..', - '..', - '..', - 'testing', - 'test-fixtures', - 'src', - 'lib', + 'mocks', 'fixtures', 'configs', ); diff --git a/packages/core/src/lib/implementation/read-rc-file.ts b/packages/core/src/lib/implementation/read-rc-file.ts index 090ad2c0e..fb27a0733 100644 --- a/packages/core/src/lib/implementation/read-rc-file.ts +++ b/packages/core/src/lib/implementation/read-rc-file.ts @@ -27,7 +27,6 @@ export async function readRcByPath( const result = await importModule({ filepath: filePath, tsconfig, - format: 'esm', }); return { result, message: `Imported config from ${formattedTarget}` }; }, diff --git a/packages/core/src/lib/implementation/read-rc-file.unit.test.ts b/packages/core/src/lib/implementation/read-rc-file.unit.test.ts index 9f72ca34b..c80e005fd 100644 --- a/packages/core/src/lib/implementation/read-rc-file.unit.test.ts +++ b/packages/core/src/lib/implementation/read-rc-file.unit.test.ts @@ -3,32 +3,33 @@ import { CONFIG_FILE_NAME, type CoreConfig } from '@code-pushup/models'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { autoloadRc } from './read-rc-file.js'; -// mock bundleRequire inside importEsmModule used for fetching config -vi.mock('bundle-require', async () => { +// mock importModule from @code-pushup/utils used for fetching config +vi.mock('@code-pushup/utils', async () => { + const utils = + await vi.importActual( + '@code-pushup/utils', + ); const { CORE_CONFIG_MOCK }: Record = await vi.importActual('@code-pushup/test-fixtures'); return { - bundleRequire: vi + ...utils, + importModule: vi .fn() - .mockImplementation((options: { filepath: string }) => { + .mockImplementation(async (options: { filepath: string }) => { const extension = options.filepath.split('.').at(-1); return { - mod: { - default: { - ...CORE_CONFIG_MOCK, - upload: { - ...CORE_CONFIG_MOCK?.upload, - project: extension, // returns loaded file extension to check format precedence - }, - }, + ...CORE_CONFIG_MOCK, + upload: { + ...CORE_CONFIG_MOCK?.upload, + project: extension, // returns loaded file extension to check format precedence }, }; }), }; }); -// Note: memfs files are only listed to satisfy a system check, value is used from bundle-require mock +// Note: memfs files are only listed to satisfy a system check, value is used from importModule mock describe('autoloadRc', () => { it('prioritise a .ts configuration file', async () => { vol.fromJSON( diff --git a/packages/plugin-coverage/src/lib/nx/coverage-paths.ts b/packages/plugin-coverage/src/lib/nx/coverage-paths.ts index 1890e5509..b95234f52 100644 --- a/packages/plugin-coverage/src/lib/nx/coverage-paths.ts +++ b/packages/plugin-coverage/src/lib/nx/coverage-paths.ts @@ -147,7 +147,6 @@ export async function getCoveragePathForVitest( const vitestConfig = await importModule({ filepath: config, - format: 'esm', }); const reportsDirectory = diff --git a/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts b/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts index 5bcaaa422..08e70014f 100644 --- a/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts +++ b/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts @@ -13,80 +13,86 @@ import { getCoveragePathsForTarget, } from './coverage-paths.js'; -vi.mock('bundle-require', () => ({ - bundleRequire: vi.fn().mockImplementation((options: { filepath: string }) => { - const VITEST_VALID: VitestCoverageConfig = { - test: { - coverage: { - reporter: ['lcov'], - reportsDirectory: path.join('coverage', 'cli'), - }, - }, - }; +vi.mock('@code-pushup/utils', async () => { + const utils = + await vi.importActual( + '@code-pushup/utils', + ); - const VITEST_NO_DIR: VitestCoverageConfig = { - test: { coverage: { reporter: ['lcov'] } }, - }; + return { + ...utils, + importModule: vi + .fn() + .mockImplementation(async (options: { filepath: string }) => { + const VITEST_VALID: VitestCoverageConfig = { + test: { + coverage: { + reporter: ['lcov'], + reportsDirectory: path.join('coverage', 'cli'), + }, + }, + }; - const VITEST_NO_LCOV: VitestCoverageConfig = { - test: { - coverage: { - reporter: ['json'], - reportsDirectory: 'coverage', - }, - }, - }; + const VITEST_NO_DIR: VitestCoverageConfig = { + test: { coverage: { reporter: ['lcov'] } }, + }; - const JEST_VALID: JestCoverageConfig = { - coverageReporters: ['lcov'], - coverageDirectory: path.join('coverage', 'core'), - }; + const VITEST_NO_LCOV: VitestCoverageConfig = { + test: { + coverage: { + reporter: ['json'], + reportsDirectory: 'coverage', + }, + }, + }; - const JEST_NO_DIR: JestCoverageConfig = { - coverageReporters: ['lcov'], - }; + const JEST_VALID: JestCoverageConfig = { + coverageReporters: ['lcov'], + coverageDirectory: path.join('coverage', 'core'), + }; - const JEST_NO_LCOV: JestCoverageConfig = { - coverageReporters: ['json'], - coverageDirectory: 'coverage', - }; + const JEST_NO_DIR: JestCoverageConfig = { + coverageReporters: ['lcov'], + }; - const JEST_PRESET: JestCoverageConfig & { preset?: string } = { - preset: '../../jest.preset.ts', - coverageDirectory: 'coverage', - }; + const JEST_NO_LCOV: JestCoverageConfig = { + coverageReporters: ['json'], + coverageDirectory: 'coverage', + }; - const wrapReturnValue = ( - value: VitestCoverageConfig | JestCoverageConfig, - ) => ({ mod: { default: value } }); + const JEST_PRESET: JestCoverageConfig & { preset?: string } = { + preset: '../../jest.preset.ts', + coverageDirectory: 'coverage', + }; - const config = options.filepath.split('.')[0]; - switch (config) { - case 'vitest-valid': - return wrapReturnValue(VITEST_VALID); - case 'vitest-no-lcov': - return wrapReturnValue(VITEST_NO_LCOV); - case 'vitest-no-dir': - return wrapReturnValue(VITEST_NO_DIR); - case 'jest-valid': - return wrapReturnValue(JEST_VALID); - case 'jest-no-lcov': - return wrapReturnValue(JEST_NO_LCOV); - case 'jest-no-dir': - return wrapReturnValue(JEST_NO_DIR); - case 'jest-preset': - return wrapReturnValue(JEST_PRESET); - default: - return wrapReturnValue({}); - } - }), -})); + const config = options.filepath.split('.')[0]; + switch (config) { + case 'vitest-valid': + return VITEST_VALID; + case 'vitest-no-lcov': + return VITEST_NO_LCOV; + case 'vitest-no-dir': + return VITEST_NO_DIR; + case 'jest-valid': + return JEST_VALID; + case 'jest-no-lcov': + return JEST_NO_LCOV; + case 'jest-no-dir': + return JEST_NO_DIR; + case 'jest-preset': + return JEST_PRESET; + default: + return {}; + } + }), + }; +}); describe('getCoveragePathForTarget', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'vitest-valid.config.ts': '', 'jest-valid.config.ts': '', }, @@ -162,7 +168,7 @@ describe('getCoveragePathForVitest', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'vitest-valid.config.unit.ts': '', 'vitest-no-dir.config.integration.ts': '', 'vitest-no-lcov.config.integration.ts': '', @@ -260,7 +266,7 @@ describe('getCoveragePathForJest', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'jest-preset.config.ts': '', 'jest-valid.config.unit.ts': '', 'jest-valid.config.integration.ts': '', diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.ts b/packages/plugin-lighthouse/src/lib/runner/utils.ts index a68ad368e..347c16bb4 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.ts @@ -144,7 +144,6 @@ export async function getConfig( message, result: await importModule({ filepath: configPath, - format: 'esm', }), }; } diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts index a5dfee309..6cf6db40a 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts @@ -25,25 +25,26 @@ import { withLocalTmpDir, } from './utils.js'; -// mock bundleRequire inside importEsmModule used for fetching config -vi.mock('bundle-require', async () => { +// mock importModule from @code-pushup/utils used for fetching config +vi.mock('@code-pushup/utils', async () => { + const utils = + await vi.importActual( + '@code-pushup/utils', + ); const { CORE_CONFIG_MOCK }: Record = - await vi.importActual('@code-pushup/test-utils'); + await vi.importActual('@code-pushup/test-fixtures'); return { - bundleRequire: vi + ...utils, + importModule: vi .fn() - .mockImplementation((options: { filepath: string }) => { + .mockImplementation(async (options: { filepath: string }) => { const project = options.filepath.split('.').at(-2); return { - mod: { - default: { - ...CORE_CONFIG_MOCK, - upload: { - ...CORE_CONFIG_MOCK?.upload, - project, // returns loaded file extension to check in test - }, - }, + ...CORE_CONFIG_MOCK, + upload: { + ...CORE_CONFIG_MOCK?.upload, + project, // returns loaded file extension to check in test }, }; }), diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ce3d64ed2..c9a1f0b30 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -41,7 +41,6 @@ export { filePathToCliArg, findLineNumberInText, findNearestFile, - importModule, pluginWorkDir, projectToFilename, readJsonFile, @@ -184,3 +183,4 @@ export type { Prettify, WithRequired, } from './lib/types.js'; +export * from './lib/import-module.js'; diff --git a/packages/utils/src/lib/file-system.ts b/packages/utils/src/lib/file-system.ts index 5956dbbff..cbe72f547 100644 --- a/packages/utils/src/lib/file-system.ts +++ b/packages/utils/src/lib/file-system.ts @@ -1,9 +1,7 @@ -import { type Options, bundleRequire } from 'bundle-require'; import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises'; import path from 'node:path'; import type { Format, PersistConfig } from '@code-pushup/models'; import { logger } from './logger.js'; -import { settlePromise } from './promises.js'; export async function readTextFile(filePath: string): Promise { const buffer = await readFile(filePath); @@ -52,23 +50,6 @@ export async function removeDirectoryIfExists(dir: string) { } } -export async function importModule(options: Options): Promise { - const resolvedStats = await settlePromise(stat(options.filepath)); - if (resolvedStats.status === 'rejected') { - throw new Error(`File '${options.filepath}' does not exist`); - } - if (!resolvedStats.value.isFile()) { - throw new Error(`Expected '${options.filepath}' to be a file`); - } - - const { mod } = await bundleRequire(options); - - if (typeof mod === 'object' && 'default' in mod) { - return mod.default as T; - } - return mod as T; -} - export function createReportPath({ outputDir, filename, diff --git a/packages/utils/src/lib/file-system.int.test.ts b/packages/utils/src/lib/import-module.int.test.ts similarity index 97% rename from packages/utils/src/lib/file-system.int.test.ts rename to packages/utils/src/lib/import-module.int.test.ts index 77d16eeff..181c85ce0 100644 --- a/packages/utils/src/lib/file-system.int.test.ts +++ b/packages/utils/src/lib/import-module.int.test.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { importModule } from './file-system.js'; +import { importModule } from './import-module.js'; describe('importModule', () => { const mockDir = path.join( diff --git a/packages/utils/src/lib/import-module.ts b/packages/utils/src/lib/import-module.ts new file mode 100644 index 000000000..6f43faaab --- /dev/null +++ b/packages/utils/src/lib/import-module.ts @@ -0,0 +1,23 @@ +import { type Options, bundleRequire } from 'bundle-require'; +import { stat } from 'node:fs/promises'; +import { settlePromise } from './promises.js'; + +export async function importModule(options: Options): Promise { + const resolvedStats = await settlePromise(stat(options.filepath)); + if (resolvedStats.status === 'rejected') { + throw new Error(`File '${options.filepath}' does not exist`); + } + if (!resolvedStats.value.isFile()) { + throw new Error(`Expected '${options.filepath}' to be a file`); + } + + const { mod } = await bundleRequire({ + format: 'esm', + ...options, + }); + + if (typeof mod === 'object' && 'default' in mod) { + return mod.default as T; + } + return mod as T; +} diff --git a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.mjs b/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.mjs deleted file mode 100644 index d7f531533..000000000 --- a/testing/test-fixtures/src/lib/fixtures/configs/code-pushup.config.mjs +++ /dev/null @@ -1,43 +0,0 @@ -export default { - upload: { - organization: 'code-pushup', - project: 'cli-mjs', - apiKey: 'e2e-api-key', - server: 'https://e2e.com/api', - }, - categories: [ - { - slug: 'category-1', - title: 'Category 1', - refs: [ - { - type: 'audit', - plugin: 'node', - slug: 'node-version', - weight: 1, - }, - ], - }, - ], - plugins: [ - { - audits: [ - { - slug: 'node-version', - title: 'Node version', - description: 'prints node version to file', - docsUrl: 'https://nodejs.org/', - }, - ], - runner: { - command: 'node', - args: ['-v'], - outputFile: 'output.json', - }, - groups: [], - slug: 'node', - title: 'Node.js', - icon: 'javascript', - }, - ], -};