diff --git a/packages/io-utils/src/config/index.ts b/packages/io-utils/src/config/index.ts new file mode 100644 index 000000000..0ac86beb1 --- /dev/null +++ b/packages/io-utils/src/config/index.ts @@ -0,0 +1 @@ +export * from './loading' diff --git a/packages/io-utils/src/config/loading.ts b/packages/io-utils/src/config/loading.ts new file mode 100644 index 000000000..6c7fd7155 --- /dev/null +++ b/packages/io-utils/src/config/loading.ts @@ -0,0 +1,49 @@ +import { importDefault, isFile, isReadable } from '@/filesystem/filesystem' +import { createModuleLogger } from '@/stdio/logger' +import { printZodErrors } from '@/stdio/printer' +import { z } from 'zod' + +export const createConfigLoader = + (schema: z.ZodSchema, logger = createModuleLogger('config loader')) => + async (path: string): Promise => { + // First we check that the config file is indeed there and we can read it + logger.verbose(`Checking config file '${path}' for existence & readability`) + const isConfigReadable = isFile(path) && isReadable(path) + if (!isConfigReadable) { + throw new Error( + `Unable to read config file '${path}'. Check that the file exists and is readable to your terminal user` + ) + } + + // Keep talking to the user + logger.verbose(`Config file '${path}' exists & is readable`) + + // Now let's see if we can load the config file + let rawConfig: unknown + try { + logger.verbose(`Loading config file '${path}'`) + + rawConfig = await importDefault(path) + } catch (error) { + throw new Error(`Unable to read config file '${path}': ${error}`) + } + + logger.verbose(`Loaded config file '${path}'`) + + // It's time to make sure that the config is not malformed + // + // At this stage we are only interested in the shape of the data, + // we are not checking whether the information makes sense (e.g. + // whether there are no missing nodes etc) + logger.verbose(`Validating the structure of config file '${path}'`) + const configParseResult = schema.safeParse(rawConfig) + if (configParseResult.success === false) { + const userFriendlyErrors = printZodErrors(configParseResult.error) + + throw new Error( + `Config from file '${path}' is malformed. Please fix the following errors:\n\n${userFriendlyErrors}` + ) + } + + return configParseResult.data + } diff --git a/packages/io-utils/src/index.ts b/packages/io-utils/src/index.ts index fbc046e1c..69145f4f8 100644 --- a/packages/io-utils/src/index.ts +++ b/packages/io-utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './config' export * from './filesystem' export * from './language' export * from './stdio' diff --git a/packages/io-utils/test/config/__snapshots__/loading.test.ts.snap b/packages/io-utils/test/config/__snapshots__/loading.test.ts.snap new file mode 100644 index 000000000..06141da5d --- /dev/null +++ b/packages/io-utils/test/config/__snapshots__/loading.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config/loading createConfigLoader should reject if the file cannot be imported 1`] = `[Error: Unable to read config file './myconfig.ts': No way]`; + +exports[`config/loading createConfigLoader should reject if the file contents do not match the schema 1`] = ` +[Error: Config from file './myconfig.ts' is malformed. Please fix the following errors: + +Property 'good': Required] +`; + +exports[`config/loading createConfigLoader should reject if the path is not a file 1`] = `[Error: Unable to read config file './myconfig.ts'. Check that the file exists and is readable to your terminal user]`; + +exports[`config/loading createConfigLoader should reject if the path is not readable 1`] = `[Error: Unable to read config file './myconfig.ts'. Check that the file exists and is readable to your terminal user]`; + +exports[`config/loading createConfigLoader should resolve if the file contents match the schema 1`] = ` +{ + "good": "config", +} +`; diff --git a/packages/io-utils/test/config/loading.test.ts b/packages/io-utils/test/config/loading.test.ts new file mode 100644 index 000000000..3775d2052 --- /dev/null +++ b/packages/io-utils/test/config/loading.test.ts @@ -0,0 +1,72 @@ +import { createConfigLoader } from '@/config' +import { importDefault, isFile, isReadable } from '@/filesystem/filesystem' +import { z } from 'zod' + +jest.mock('@/filesystem/filesystem', () => ({ + isFile: jest.fn().mockReturnValue(false), + isReadable: jest.fn().mockReturnValue(false), + importDefault: jest.fn().mockRejectedValue('Not mocked'), +})) + +const isFileMock = isFile as jest.Mock +const importDefaultMock = importDefault as jest.Mock +const isReadableMock = isReadable as jest.Mock + +describe('config/loading', () => { + beforeEach(() => { + isFileMock.mockReset() + importDefaultMock.mockReset() + isReadableMock.mockReset() + }) + + describe('createConfigLoader', () => { + it('should reject if the path is not a file', async () => { + isFileMock.mockReturnValue(false) + + const configLoader = createConfigLoader(z.unknown()) + + await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot() + }) + + it('should reject if the path is not readable', async () => { + isFileMock.mockReturnValue(true) + isReadableMock.mockReturnValue(false) + + const configLoader = createConfigLoader(z.unknown()) + + await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot() + }) + + it('should reject if the file cannot be imported', async () => { + isFileMock.mockReturnValue(true) + isReadableMock.mockReturnValue(true) + importDefaultMock.mockRejectedValue('No way') + + const configLoader = createConfigLoader(z.unknown()) + + await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot() + }) + + it('should reject if the file contents do not match the schema', async () => { + isFileMock.mockReturnValue(true) + isReadableMock.mockReturnValue(true) + importDefaultMock.mockResolvedValue({ bad: 'config' }) + + const schema = z.object({ good: z.string() }) + const configLoader = createConfigLoader(schema) + + await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot() + }) + + it('should resolve if the file contents match the schema', async () => { + isFileMock.mockReturnValue(true) + isReadableMock.mockReturnValue(true) + importDefaultMock.mockResolvedValue({ good: 'config' }) + + const schema = z.object({ good: z.string() }) + const configLoader = createConfigLoader(schema) + + await expect(configLoader('./myconfig.ts')).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap index f54f59fb3..1761a1318 100644 --- a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap +++ b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`task/oapp/wire with invalid configs should fail if the config file does not exist 1`] = `[Error: Unable to read config file './does-not-exist.js'. Check that the file exists and is readable to your terminal user]`; +exports[`task/oapp/wire with invalid configs should fail if the config file does not exist 1`] = `[Error: Unable to read config file '/app/packages/ua-utils-evm-hardhat-test/does-not-exist.js'. Check that the file exists and is readable to your terminal user]`; exports[`task/oapp/wire with invalid configs should fail if the config file is not a file 1`] = `[Error: Unable to read config file '/app/packages/ua-utils-evm-hardhat-test/test/task/oapp'. Check that the file exists and is readable to your terminal user]`; diff --git a/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts b/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts index 2d67408f4..e5e02d496 100644 --- a/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts +++ b/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts @@ -2,15 +2,12 @@ import { task, types } from 'hardhat/config' import type { ActionType } from 'hardhat/types' import { TASK_LZ_WIRE_OAPP } from '@/constants/tasks' import { - isFile, - isReadable, createLogger, + createConfigLoader, setDefaultLogLevel, promptToContinue, printJson, - printZodErrors, pluralizeNoun, - importDefault, } from '@layerzerolabs/io-utils' import { OAppOmniGraphHardhat, OAppOmniGraphHardhatSchema } from '@/oapp' import { OAppOmniGraph, configureOApp } from '@layerzerolabs/ua-utils' @@ -29,49 +26,14 @@ const action: ActionType = async ({ oappConfig: oappConfigPath, logLev // We'll set the global logging level to get as much info as needed setDefaultLogLevel(logLevel) + // And we'll create a logger for ourselves const logger = createLogger() - // First we check that the config file is indeed there and we can read it - logger.verbose(`Checking config file '${oappConfigPath}' for existence & readability`) - const isConfigReadable = isFile(oappConfigPath) && isReadable(oappConfigPath) - if (!isConfigReadable) { - throw new Error( - `Unable to read config file '${oappConfigPath}'. Check that the file exists and is readable to your terminal user` - ) - } - - // Keep talking to the user - logger.verbose(`Config file '${oappConfigPath}' exists & is readable`) - - // Now let's see if we can load the config file - let rawConfig: unknown - try { - logger.verbose(`Loading config file '${oappConfigPath}'`) - - rawConfig = await importDefault(resolve(oappConfigPath)) - } catch (error) { - throw new Error(`Unable to read config file '${oappConfigPath}': ${error}`) - } - - logger.verbose(`Loaded config file '${oappConfigPath}'`) - - // It's time to make sure that the config is not malformed - // - // At this stage we are only interested in the shape of the data, - // we are not checking whether the information makes sense (e.g. - // whether there are no missing nodes etc) - logger.verbose(`Validating the structure of config file '${oappConfigPath}'`) - const configParseResult = OAppOmniGraphHardhatSchema.safeParse(rawConfig) - if (configParseResult.success === false) { - const userFriendlyErrors = printZodErrors(configParseResult.error) - - throw new Error( - `Config from file '${oappConfigPath}' is malformed. Please fix the following errors:\n\n${userFriendlyErrors}` - ) - } + // Now we create our config loader + const configLoader = createConfigLoader(OAppOmniGraphHardhatSchema) // At this point we have a correctly typed config in the hardhat format - const hardhatGraph: OAppOmniGraphHardhat = configParseResult.data + const hardhatGraph: OAppOmniGraphHardhat = await configLoader(resolve(oappConfigPath)) // We'll also print out the whole config for verbose loggers logger.verbose(`Config file '${oappConfigPath}' has correct structure`)