import { promises as fsp } from 'node:fs'; import path from 'node:path'; import { cloneDeep } from 'lodash'; import * as esbuild from 'esbuild'; import { CollectionModuleConfig } from '../../domain/collection-modules/collection-module-config'; import { CollectionModuleCodeFileWithContent, CollectionModuleDefinitionRequest, } from '../../domain/collection-modules/collection-module-definition'; import { CollectionModuleTopDirectory, CollectionModuleFileName, CollectionModuleWorkflowsDirectory, } from '../../domain/collection-modules/collection-module-file-names'; import { existsAsync } from '../fs-helpers'; const readCodeFiles = async (params: { directory: string; existingFiles?: CollectionModuleCodeFileWithContent[]; pathSegments?: string[]; }) => { const { directory } = params; const existingFiles: CollectionModuleCodeFileWithContent[] = params.existingFiles || []; const entries = await fsp.readdir(directory, { withFileTypes: true }); const promises = entries.map(async (entry) => { if (entry.isFile() && !entry.name.endsWith('.ts') && !entry.name.endsWith('.js')) { return; } const pathSegments: string[] = cloneDeep(params.pathSegments || []); pathSegments.push(entry.name); const pathToThisEntry = path.join(directory, entry.name); if (entry.isDirectory()) { await readCodeFiles({ directory: pathToThisEntry, existingFiles, pathSegments }); } else if (entry.isFile()) { const content = await fsp.readFile(pathToThisEntry, 'utf8'); existingFiles.push({ pathSegments, content }); } }); await Promise.all(promises); return existingFiles; }; export const bundleCodeFiles = async (params: { directory: string; collectionModuleConfig: CollectionModuleConfig; }) => { const { directory, collectionModuleConfig } = params; const [mainTsExists, mainJsExists] = await Promise.all([ existsAsync(path.join(directory, CollectionModuleFileName.MainTs)), existsAsync(path.join(directory, CollectionModuleFileName.MainJs)), ]); if (!mainTsExists && !mainJsExists) { throw new Error(`Error bundling code files. No main.ts or main.js file found.`); } if (mainTsExists && mainJsExists) { throw new Error(`Error bundling code files. Both main.ts and main.js files found.`); } const entryPointFileName = mainTsExists ? CollectionModuleFileName.MainTs : CollectionModuleFileName.MainJs; const buildOptions: esbuild.BuildOptions = { entryPoints: [path.join(directory, entryPointFileName)], bundle: true, // Prevents renaming CM code functions minifyIdentifiers: false, minifySyntax: true, minifyWhitespace: true, write: false, platform: 'node', format: 'cjs', target: collectionModuleConfig.settings.legacyCodeExecution ? 'node12.10.0' : 'node20', packages: 'external', }; // Errors are logged automatically so we don't need to catch: https://esbuild.github.io/api/#log-level const { outputFiles } = await esbuild.build(buildOptions); if (!outputFiles) { throw new Error('Error bundling code files. No output files returned.'); } if (outputFiles.length !== 1) { throw new Error(`Error bundling code files. Unexpected number of output files: ${outputFiles.length}. Expected 1.`); } return outputFiles[0].text; }; const readManualTransactionWorkflows = async (params: { directory: string; manualTransactions: CollectionModuleConfig['manualTransactions']; }) => { const { directory, manualTransactions } = params; const manualTransactionsDirectory = path.join( directory, CollectionModuleTopDirectory.Workflows, CollectionModuleWorkflowsDirectory.ManualTransactions, ); const promises = manualTransactions.map(async (t) => { const { key } = t; const filePath = path.join(manualTransactionsDirectory, `${key}.json`); if (!(await existsAsync(filePath))) { throw new Error(`Manual transaction workflow schema not found at ${filePath}.`); } const schema = JSON.parse(await fsp.readFile(filePath, 'utf8')); return { key, schema }; }); return Promise.all(promises); }; /** * The config should be read first and then passed to this function. */ export const readCollectionModuleDefinition = async (params: { collectionModuleConfig: CollectionModuleConfig; directory: string; }): Promise => { const { collectionModuleConfig, directory } = params; const { manualTransactions } = collectionModuleConfig; const codeDirectory = path.join(directory, CollectionModuleTopDirectory.Code); const [codeFiles, bundledCode, manualTransactionWorkflows] = await Promise.all([ readCodeFiles({ directory: codeDirectory }), bundleCodeFiles({ directory: codeDirectory, collectionModuleConfig }), readManualTransactionWorkflows({ directory, manualTransactions }), ]); const { collectionModuleKey, collectionModuleName, host, organizationId, ...rest } = collectionModuleConfig; return { code: bundledCode, codeFiles, collectionModuleKey, rootConfig: rest, workflows: { manualTransactionWorkflows, }, }; };