@motiadev/core 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,15 @@
1
- import { LockedData } from './locked-data';
2
- import { Event, EventConfig, EventManager, InternalStateManager, Step } from './types';
3
- export declare const callStepFile: <TData>(step: Step<EventConfig>, lockedData: LockedData, event: Event<TData>, eventManager: EventManager, state: InternalStateManager) => Promise<void>;
1
+ import { EventManager, InternalStateManager, Step } from './types';
2
+ import { BaseLogger } from './logger';
3
+ import { Printer } from './printer';
4
+ type CallStepFileOptions = {
5
+ step: Step;
6
+ logger: BaseLogger;
7
+ eventManager: EventManager;
8
+ state: InternalStateManager;
9
+ traceId: string;
10
+ printer: Printer;
11
+ data?: any;
12
+ contextInFirstArg: boolean;
13
+ };
14
+ export declare const callStepFile: <TData>(options: CallStepFileOptions) => Promise<TData | undefined>;
15
+ export {};
@@ -8,67 +8,72 @@ const step_handler_rpc_processor_1 = require("./step-handler-rpc-processor");
8
8
  const child_process_1 = require("child_process");
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const utils_1 = require("./utils");
11
- const nodeRunner = path_1.default.join(__dirname, 'node', 'node-runner.js');
12
- const pythonRunner = path_1.default.join(__dirname, 'python', 'python-runner.py');
13
- const rubyRunner = path_1.default.join(__dirname, 'ruby', 'ruby_runner.rb');
14
11
  const getLanguageBasedRunner = (stepFilePath = '') => {
15
12
  const isPython = stepFilePath.endsWith('.py');
16
13
  const isRuby = stepFilePath.endsWith('.rb');
17
14
  const isNode = stepFilePath.endsWith('.js') || stepFilePath.endsWith('.ts');
18
15
  if (isPython) {
19
- return { runner: pythonRunner, command: 'python' };
16
+ const pythonRunner = path_1.default.join(__dirname, 'python', 'python-runner.py');
17
+ return { runner: pythonRunner, command: 'python', args: [] };
20
18
  }
21
19
  else if (isRuby) {
22
- return { runner: rubyRunner, command: 'ruby' };
20
+ const rubyRunner = path_1.default.join(__dirname, 'ruby', 'ruby-runner.rb');
21
+ return { runner: rubyRunner, command: 'ruby', args: [] };
23
22
  }
24
23
  else if (isNode) {
25
- return { runner: nodeRunner, command: 'node' };
24
+ if (process.env._MOTIA_TEST_MODE === 'true') {
25
+ const nodeRunner = path_1.default.join(__dirname, 'node', 'node-runner.ts');
26
+ return { runner: nodeRunner, command: 'node', args: ['-r', 'ts-node/register'] };
27
+ }
28
+ const nodeRunner = path_1.default.join(__dirname, 'node', 'node-runner.js');
29
+ return { runner: nodeRunner, command: 'node', args: [] };
26
30
  }
27
31
  throw Error(`Unsupported file extension ${stepFilePath}`);
28
32
  };
29
- const callStepFile = (step, lockedData, event, eventManager, state) => {
33
+ const callStepFile = (options) => {
34
+ const { step, printer, eventManager, state, traceId, data, contextInFirstArg } = options;
35
+ const logger = options.logger.child({ step: step.config.name });
36
+ const flows = step.config.flows;
30
37
  return new Promise((resolve, reject) => {
31
- const jsonData = JSON.stringify({ ...event });
32
- const { runner, command } = getLanguageBasedRunner(step.filePath);
33
- const child = (0, child_process_1.spawn)(command, [runner, step.filePath, jsonData], {
38
+ const jsonData = JSON.stringify({ data, flows, traceId, contextInFirstArg });
39
+ const { runner, command, args } = getLanguageBasedRunner(step.filePath);
40
+ let result;
41
+ const child = (0, child_process_1.spawn)(command, [...args, runner, step.filePath, jsonData], {
34
42
  stdio: [undefined, undefined, undefined, 'ipc'],
35
43
  });
36
44
  const rpcProcessor = new step_handler_rpc_processor_1.RpcProcessor(child);
37
45
  rpcProcessor.handler('close', async () => child.kill());
38
- rpcProcessor.handler('log', async (input) => event.logger.log(input));
46
+ rpcProcessor.handler('log', async (input) => logger.log(input));
39
47
  rpcProcessor.handler('state.get', (input) => state.get(input.traceId, input.key));
40
48
  rpcProcessor.handler('state.set', (input) => state.set(input.traceId, input.key, input.value));
41
49
  rpcProcessor.handler('state.delete', (input) => state.delete(input.traceId, input.key));
42
50
  rpcProcessor.handler('state.clear', (input) => state.clear(input.traceId));
51
+ rpcProcessor.handler('result', async (input) => {
52
+ result = input;
53
+ });
43
54
  rpcProcessor.handler('emit', async (input) => {
44
55
  if (!(0, utils_1.isAllowedToEmit)(step, input.type)) {
45
- lockedData.printer.printInvalidEmit(step, input.type);
46
- return;
56
+ return printer.printInvalidEmit(step, input.type);
47
57
  }
48
- return eventManager.emit({
49
- ...input,
50
- traceId: event.traceId,
51
- flows: event.flows,
52
- logger: event.logger,
53
- }, step.filePath);
58
+ return eventManager.emit({ ...input, traceId, flows: step.config.flows, logger }, step.filePath);
54
59
  });
55
60
  rpcProcessor.init();
56
61
  child.stdout?.on('data', (data) => {
57
62
  try {
58
63
  const message = JSON.parse(data.toString());
59
- event.logger.log(message);
64
+ logger.log(message);
60
65
  }
61
66
  catch {
62
- event.logger.info(Buffer.from(data).toString(), { step });
67
+ logger.info(Buffer.from(data).toString());
63
68
  }
64
69
  });
65
- child.stderr?.on('data', (data) => event.logger.error(Buffer.from(data).toString(), { step }));
70
+ child.stderr?.on('data', (data) => logger.error(Buffer.from(data).toString()));
66
71
  child.on('close', (code) => {
67
72
  if (code !== 0 && code !== null) {
68
73
  reject(new Error(`Process exited with code ${code}`));
69
74
  }
70
75
  else {
71
- resolve();
76
+ resolve(result);
72
77
  }
73
78
  });
74
79
  });
@@ -1,12 +1,13 @@
1
1
  import { Server } from 'socket.io';
2
2
  import { LockedData } from './locked-data';
3
- import { EventManager, Step, CronConfig } from './types';
3
+ import { StateAdapter } from './state/state-adapter';
4
+ import { CronConfig, EventManager, Step } from './types';
4
5
  export type CronManager = {
5
6
  createCronJob: (step: Step<CronConfig>) => void;
6
7
  removeCronJob: (step: Step<CronConfig>) => void;
7
8
  close: () => void;
8
9
  };
9
- export declare const setupCronHandlers: (lockedData: LockedData, eventManager: EventManager, socketServer: Server) => {
10
+ export declare const setupCronHandlers: (lockedData: LockedData, eventManager: EventManager, state: StateAdapter, socketServer: Server) => {
10
11
  createCronJob: (step: Step<CronConfig>) => void;
11
12
  removeCronJob: (step: Step<CronConfig>) => void;
12
13
  close: () => void;
@@ -34,45 +34,41 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.setupCronHandlers = void 0;
37
+ const crypto_1 = require("crypto");
37
38
  const cron = __importStar(require("node-cron"));
39
+ const call_step_file_1 = require("./call-step-file");
38
40
  const logger_1 = require("./logger");
39
- const get_module_export_1 = require("./node/get-module-export");
40
- const setupCronHandlers = (lockedData, eventManager, socketServer) => {
41
+ const setupCronHandlers = (lockedData, eventManager, state, socketServer) => {
41
42
  const cronJobs = new Map();
43
+ const printer = lockedData.printer;
42
44
  const createCronJob = (step) => {
43
45
  const { config, filePath } = step;
44
- const { cron: cronExpression } = config;
46
+ const { cron: cronExpression, name: stepName } = config;
45
47
  if (!cron.validate(cronExpression)) {
46
48
  logger_1.globalLogger.error('[cron handler] invalid cron expression', {
47
49
  expression: cronExpression,
48
- step: step.config.name,
50
+ step: stepName,
49
51
  });
50
52
  return;
51
53
  }
52
54
  logger_1.globalLogger.debug('[cron handler] setting up cron job', {
53
55
  filePath,
54
- step: step.config.name,
56
+ step: stepName,
55
57
  cron: cronExpression,
56
58
  });
57
59
  const task = cron.schedule(cronExpression, async () => {
58
- const traceId = Math.random().toString(36).substring(7);
59
- const logger = new logger_1.Logger(traceId, config.flows, step.config.name, socketServer);
60
- const flows = config.flows;
60
+ const traceId = (0, crypto_1.randomUUID)();
61
+ const logger = new logger_1.Logger(traceId, config.flows, stepName, socketServer);
61
62
  try {
62
- const handler = await (0, get_module_export_1.getModuleExport)(filePath, 'handler');
63
- const emit = async (event) => {
64
- await eventManager.emit({ ...event, traceId, flows, logger }, filePath);
65
- };
66
- if (handler) {
67
- await handler({ emit, logger, traceId });
68
- }
69
- else {
70
- const data = { timestamp: Date.now() };
71
- await Promise.all(config.emits.map((item) => {
72
- const type = typeof item === 'string' ? item : item.type;
73
- emit({ type, data });
74
- }));
75
- }
63
+ await (0, call_step_file_1.callStepFile)({
64
+ contextInFirstArg: true,
65
+ step,
66
+ eventManager,
67
+ printer,
68
+ state,
69
+ traceId,
70
+ logger,
71
+ });
76
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
73
  }
78
74
  catch (error) {
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateFlowsList = exports.flowsEndpoint = void 0;
7
7
  const crypto_1 = require("crypto");
8
8
  const fs_1 = __importDefault(require("fs"));
9
- const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
10
9
  const guards_1 = require("./guards");
11
10
  const get_step_language_1 = require("./get-step-language");
12
11
  const flowsEndpoint = (lockedData, app) => {
@@ -81,7 +80,7 @@ const createApiStepResponse = (step, id) => {
81
80
  subscribes: step.config.virtualSubscribes ?? undefined,
82
81
  action: 'webhook',
83
82
  webhookUrl: `${step.config.method} ${step.config.path}`,
84
- bodySchema: step.config.bodySchema ? (0, zod_to_json_schema_1.default)(step.config.bodySchema) : undefined,
83
+ bodySchema: step.config.bodySchema ?? undefined,
85
84
  };
86
85
  };
87
86
  const createEventStepResponse = (step, id) => {
@@ -1,2 +1,2 @@
1
1
  import { StepConfig } from './types';
2
- export declare const getStepConfig: (filePath: string) => Promise<StepConfig | null>;
2
+ export declare const getStepConfig: (file: string) => Promise<StepConfig | null>;
@@ -1,22 +1,58 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.getStepConfig = void 0;
4
- const get_config_1 = require("./node/get-config");
5
- const get_python_config_1 = require("./python/get-python-config");
6
- const get_ruby_config_1 = require("./ruby/get-ruby-config");
7
- const getStepConfig = async (filePath) => {
8
- const isRb = filePath.endsWith('.rb');
9
- const isPython = filePath.endsWith('.py');
10
- const isNode = filePath.endsWith('.js') || filePath.endsWith('.ts');
11
- if (isRb) {
12
- return (0, get_ruby_config_1.getRubyConfig)(filePath);
7
+ const child_process_1 = require("child_process");
8
+ const path_1 = __importDefault(require("path"));
9
+ const logger_1 = require("./logger");
10
+ const getLanguageBasedRunner = (stepFilePath = '') => {
11
+ const isPython = stepFilePath.endsWith('.py');
12
+ const isRuby = stepFilePath.endsWith('.rb');
13
+ const isNode = stepFilePath.endsWith('.js') || stepFilePath.endsWith('.ts');
14
+ if (isPython) {
15
+ const pythonRunner = path_1.default.join(__dirname, 'python', 'get-config.py');
16
+ return { runner: pythonRunner, command: 'python', args: [] };
13
17
  }
14
- else if (isPython) {
15
- return (0, get_python_config_1.getPythonConfig)(filePath);
18
+ else if (isRuby) {
19
+ const rubyRunner = path_1.default.join(__dirname, 'ruby', 'get-config.rb');
20
+ return { runner: rubyRunner, command: 'ruby', args: [] };
16
21
  }
17
22
  else if (isNode) {
18
- return (0, get_config_1.getNodeFileConfig)(filePath);
23
+ if (process.env._MOTIA_TEST_MODE === 'true') {
24
+ const nodeRunner = path_1.default.join(__dirname, 'node', 'get-config.ts');
25
+ return { runner: nodeRunner, command: 'node', args: ['-r', 'ts-node/register'] };
26
+ }
27
+ const nodeRunner = path_1.default.join(__dirname, 'node', 'get-config.js');
28
+ return { runner: nodeRunner, command: 'node', args: [] };
19
29
  }
20
- return null;
30
+ throw Error(`Unsupported file extension ${stepFilePath}`);
31
+ };
32
+ const getStepConfig = (file) => {
33
+ const { runner, command, args } = getLanguageBasedRunner(file);
34
+ return new Promise((resolve, reject) => {
35
+ let config = null;
36
+ const child = (0, child_process_1.spawn)(command, [...args, runner, file], {
37
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
38
+ });
39
+ child.on('message', (message) => {
40
+ logger_1.globalLogger.debug('[Config] Read config', { config: message });
41
+ config = message;
42
+ resolve(config);
43
+ child.kill(); // we can kill the child process since we already received the message
44
+ });
45
+ child.on('close', (code) => {
46
+ if (config) {
47
+ return; // Config was already resolved
48
+ }
49
+ else if (code !== 0) {
50
+ reject(new Error(`Process exited with code ${code}`));
51
+ }
52
+ else if (!config) {
53
+ reject(new Error(`No config found for file ${file}`));
54
+ }
55
+ });
56
+ });
21
57
  };
22
58
  exports.getStepConfig = getStepConfig;
@@ -1,18 +1,23 @@
1
1
  import { Server } from 'socket.io';
2
- declare class BaseLogger {
2
+ export declare class BaseLogger {
3
+ private readonly meta;
3
4
  private logger;
4
5
  constructor(meta?: Record<string, unknown>);
6
+ child(meta?: Record<string, unknown>): this;
5
7
  info(message: string, args?: unknown): void;
6
8
  error(message: string, args?: unknown): void;
7
9
  debug(message: string, args?: unknown): void;
8
10
  warn(message: string, args?: unknown): void;
11
+ log(args: unknown): void;
9
12
  }
10
13
  export declare class Logger extends BaseLogger {
11
14
  private readonly traceId;
12
15
  private readonly flows;
13
- private readonly file;
16
+ private readonly step;
17
+ private readonly socketServer?;
14
18
  private emitLog;
15
- constructor(traceId: string, flows: string[] | undefined, file: string, socketServer?: Server);
19
+ constructor(traceId: string, flows: string[] | undefined, step: string, socketServer?: Server | undefined);
20
+ child(meta?: Record<string, unknown>): this;
16
21
  log(message: any): void;
17
22
  info: (message: string, args?: unknown) => void;
18
23
  error: (message: string, args?: unknown) => void;
@@ -20,4 +25,3 @@ export declare class Logger extends BaseLogger {
20
25
  warn: (message: string, args?: unknown) => void;
21
26
  }
22
27
  export declare const globalLogger: BaseLogger;
23
- export {};
@@ -3,11 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.globalLogger = exports.Logger = void 0;
6
+ exports.globalLogger = exports.Logger = exports.BaseLogger = void 0;
7
7
  const pino_1 = __importDefault(require("pino"));
8
8
  const isDebugEnabled = () => process.env.LOG_LEVEL === 'debug';
9
9
  class BaseLogger {
10
10
  constructor(meta = {}) {
11
+ this.meta = meta;
11
12
  this.logger = (0, pino_1.default)({
12
13
  level: process.env.LOG_LEVEL || 'info',
13
14
  formatters: { level: (level) => ({ level }) },
@@ -15,6 +16,9 @@ class BaseLogger {
15
16
  mixin: () => meta,
16
17
  });
17
18
  }
19
+ child(meta = {}) {
20
+ return new BaseLogger({ ...this.meta, ...meta });
21
+ }
18
22
  info(message, args) {
19
23
  this.logger.info(args, message);
20
24
  }
@@ -27,13 +31,18 @@ class BaseLogger {
27
31
  warn(message, args) {
28
32
  this.logger.warn(args, message);
29
33
  }
34
+ log(args) {
35
+ this.logger.info(args);
36
+ }
30
37
  }
38
+ exports.BaseLogger = BaseLogger;
31
39
  class Logger extends BaseLogger {
32
- constructor(traceId, flows, file, socketServer) {
33
- super({ traceId, flows, file });
40
+ constructor(traceId, flows, step, socketServer) {
41
+ super({ traceId, flows, step });
34
42
  this.traceId = traceId;
35
43
  this.flows = flows;
36
- this.file = file;
44
+ this.step = step;
45
+ this.socketServer = socketServer;
37
46
  this.info = (message, args) => {
38
47
  super.info(message, args);
39
48
  this.emitLog('info', message, args);
@@ -53,11 +62,8 @@ class Logger extends BaseLogger {
53
62
  this.emitLog('warn', message, args);
54
63
  };
55
64
  this.emitLog = (level, msg, args) => {
56
- if (!socketServer) {
57
- return;
58
- }
59
- socketServer.emit('log', {
60
- file: this.file,
65
+ socketServer?.emit('log', {
66
+ step: this.step,
61
67
  ...(args ?? {}),
62
68
  level,
63
69
  time: Date.now(),
@@ -67,6 +73,9 @@ class Logger extends BaseLogger {
67
73
  });
68
74
  };
69
75
  }
76
+ child(meta = {}) {
77
+ return new Logger(this.traceId, this.flows, meta.step, this.socketServer);
78
+ }
70
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
80
  log(message) {
72
81
  console.log(JSON.stringify(message));
@@ -1,2 +1 @@
1
- import { StepConfig } from '../types';
2
- export declare const getNodeFileConfig: (filePath: string) => Promise<StepConfig>;
1
+ export {};
@@ -1,14 +1,45 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getNodeFileConfig = void 0;
4
- const get_module_export_1 = require("./get-module-export");
5
- const getNodeFileConfig = async (filePath) => {
6
+ const path_1 = __importDefault(require("path"));
7
+ const zod_1 = require("zod");
8
+ const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
9
+ // Add ts-node registration before dynamic imports
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ require('ts-node').register({
12
+ transpileOnly: true,
13
+ compilerOptions: { module: 'commonjs' },
14
+ });
15
+ async function getConfig(filePath) {
6
16
  try {
7
- return await (0, get_module_export_1.getModuleExport)(filePath, 'config');
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const module = require(path_1.default.resolve(filePath));
19
+ // Check if the specified function exists in the module
20
+ if (!module.config) {
21
+ throw new Error(`Config not found in module ${filePath}`);
22
+ }
23
+ if (module.config.input instanceof zod_1.ZodObject) {
24
+ module.config.input = (0, zod_to_json_schema_1.default)(module.config.input);
25
+ }
26
+ else if (module.config.bodySchema instanceof zod_1.ZodObject) {
27
+ module.config.bodySchema = (0, zod_to_json_schema_1.default)(module.config.bodySchema);
28
+ }
29
+ process.send?.(module.config);
30
+ process.exit(0);
8
31
  }
9
32
  catch (error) {
10
- console.error(`Failed to extract config from ${filePath}:`, error);
11
- throw new Error(`No config found in step ${filePath}`);
33
+ console.error('Error running TypeScript module:', error);
34
+ process.exit(1);
12
35
  }
13
- };
14
- exports.getNodeFileConfig = getNodeFileConfig;
36
+ }
37
+ const [, , filePath] = process.argv;
38
+ if (!filePath) {
39
+ console.error('Usage: node get-config.js <file-path>');
40
+ process.exit(1);
41
+ }
42
+ getConfig(filePath).catch((err) => {
43
+ console.error('Error:', err);
44
+ process.exit(1);
45
+ });
@@ -2,9 +2,8 @@ import { RpcSender } from './rpc';
2
2
  export declare class Logger {
3
3
  private readonly traceId;
4
4
  private readonly flows;
5
- private readonly fileName;
6
5
  private readonly sender;
7
- constructor(traceId: string, flows: string[], fileName: string, sender: RpcSender);
6
+ constructor(traceId: string, flows: string[], sender: RpcSender);
8
7
  private _log;
9
8
  info(message: string, args?: Record<string, unknown>): void;
10
9
  error(message: string, args?: Record<string, unknown>): void;
@@ -2,10 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Logger = void 0;
4
4
  class Logger {
5
- constructor(traceId, flows, fileName, sender) {
5
+ constructor(traceId, flows, sender) {
6
6
  this.traceId = traceId;
7
7
  this.flows = flows;
8
- this.fileName = fileName;
9
8
  this.sender = sender;
10
9
  }
11
10
  _log(level, message, args) {
@@ -15,7 +14,6 @@ class Logger {
15
14
  time: Date.now(),
16
15
  traceId: this.traceId,
17
16
  flows: this.flows,
18
- file: this.fileName,
19
17
  msg: message,
20
18
  };
21
19
  this.sender.sendNoWait('log', logEntry);
@@ -7,6 +7,8 @@ const path_1 = __importDefault(require("path"));
7
7
  const logger_1 = require("./logger");
8
8
  const rpc_state_manager_1 = require("./rpc-state-manager");
9
9
  const rpc_1 = require("./rpc");
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ require('dotenv').config();
10
12
  // Add ts-node registration before dynamic imports
11
13
  // eslint-disable-next-line @typescript-eslint/no-require-imports
12
14
  require('ts-node').register({
@@ -29,15 +31,16 @@ async function runTypescriptModule(filePath, event) {
29
31
  if (typeof module.handler !== 'function') {
30
32
  throw new Error(`Function handler not found in module ${filePath}`);
31
33
  }
32
- const { traceId, flows } = event;
34
+ const { traceId, flows, contextInFirstArg } = event;
33
35
  const sender = new rpc_1.RpcSender(process);
34
- const logger = new logger_1.Logger(traceId, flows, filePath, sender);
36
+ const logger = new logger_1.Logger(traceId, flows, sender);
35
37
  const state = new rpc_state_manager_1.RpcStateManager(sender);
36
38
  const emit = async (data) => sender.send('emit', data);
37
39
  const context = { traceId, flows, logger, state, emit };
38
40
  sender.init();
39
41
  // Call the function with provided arguments
40
- await module.handler(event.data, context);
42
+ const result = contextInFirstArg ? await module.handler(context) : await module.handler(event.data, context);
43
+ await sender.send('result', result);
41
44
  await sender.close();
42
45
  process.exit(0);
43
46
  }
@@ -3,10 +3,9 @@ from typing import Any, Dict, Optional
3
3
  from rpc import RpcSender
4
4
 
5
5
  class Logger:
6
- def __init__(self, trace_id: str, flows: list[str], file_path: str, rpc: RpcSender):
6
+ def __init__(self, trace_id: str, flows: list[str], rpc: RpcSender):
7
7
  self.trace_id = trace_id
8
8
  self.flows = flows
9
- self.file_name = file_path.split('/')[-1]
10
9
  self.rpc = rpc
11
10
 
12
11
  def _log(self, level: str, message: str, args: Optional[Dict[str, Any]] = None) -> None:
@@ -15,7 +14,6 @@ class Logger:
15
14
  "time": int(time.time() * 1000),
16
15
  "traceId": self.trace_id,
17
16
  "flows": self.flows,
18
- "file": self.file_name,
19
17
  "msg": message
20
18
  }
21
19
 
@@ -20,13 +20,13 @@ def parse_args(arg: str) -> Any:
20
20
  return arg
21
21
 
22
22
  class Context:
23
- def __init__(self, args: Any, rpc: RpcSender, file_name: str):
23
+ def __init__(self, args: Any, rpc: RpcSender):
24
24
  self.trace_id = args.traceId
25
+ self.traceId = args.traceId
25
26
  self.flows = args.flows
26
- self.file_name = file_name
27
27
  self.rpc = rpc
28
28
  self.state = RpcStateManager(rpc)
29
- self.logger = Logger(self.trace_id, self.flows, self.file_name, rpc)
29
+ self.logger = Logger(self.trace_id, self.flows, rpc)
30
30
 
31
31
  async def emit(self, event: Any):
32
32
  return await self.rpc.send('emit', event)
@@ -36,6 +36,7 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
36
36
  # Construct path relative to steps directory
37
37
  flows_dir = os.path.join(os.getcwd(), 'steps')
38
38
  module_path = os.path.join(flows_dir, file_path)
39
+ contextInFirstArg = args.contextInFirstArg
39
40
 
40
41
  # Load the module dynamically
41
42
  spec = importlib.util.spec_from_file_location("dynamic_module", module_path)
@@ -49,9 +50,15 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
49
50
  if not hasattr(module, 'handler'):
50
51
  raise AttributeError(f"Function 'handler' not found in module {module_path}")
51
52
 
52
- context = Context(args, rpc, file_path)
53
+ context = Context(args, rpc)
53
54
 
54
- await module.handler(args.data, context)
55
+ if contextInFirstArg:
56
+ result = await module.handler(context)
57
+ else:
58
+ result = await module.handler(args.data, context)
59
+
60
+ if (result):
61
+ await rpc.send('result', result)
55
62
 
56
63
  rpc.close()
57
64
 
@@ -10,8 +10,7 @@ def send_message(message)
10
10
  io.write(json_message)
11
11
  io.flush
12
12
  rescue Errno::EBADF => e
13
- warn "Error writing to IPC channel: #{e.message}"
14
- exit(1)
13
+ raise "Error writing to IPC channel: #{e.message}"
15
14
  ensure
16
15
  io.close if io && !io.closed?
17
16
  end
@@ -41,20 +40,17 @@ end
41
40
  # Main execution block
42
41
  begin
43
42
  if ARGV.empty?
44
- warn 'Error: No file path provided'
45
- exit(1)
43
+ raise 'Error: No file path provided'
46
44
  end
47
45
 
48
46
  file_path = ARGV[0]
49
47
 
50
48
  unless File.exist?(file_path)
51
- warn "Error: File not found: #{file_path}"
52
- exit(1)
49
+ raise "Error: File not found: #{file_path}"
53
50
  end
54
51
 
55
52
  unless File.readable?(file_path)
56
- warn "Error: File is not readable: #{file_path}"
57
- exit(1)
53
+ raise "Error: File is not readable: #{file_path}"
58
54
  end
59
55
 
60
56
  # Extract and send config
@@ -62,7 +58,4 @@ begin
62
58
  send_message(config)
63
59
 
64
60
  exit(0)
65
- rescue => e
66
- warn "Error: #{e.message}"
67
- exit(1)
68
61
  end
@@ -2,10 +2,9 @@ require 'json'
2
2
  require 'time'
3
3
 
4
4
  class CustomLogger
5
- def initialize(trace_id, flows, file_path, sender)
5
+ def initialize(trace_id, flows, sender)
6
6
  @trace_id = trace_id
7
7
  @flows = flows
8
- @file_name = file_path.split('/').last
9
8
  @sender = sender
10
9
  end
11
10
 
@@ -25,7 +24,6 @@ class CustomLogger
25
24
  time: (Time.now.to_f * 1000).to_i, # Milliseconds since epoch to match Node
26
25
  traceId: @trace_id,
27
26
  flows: @flows,
28
- file: @file_name,
29
27
  msg: message
30
28
  }
31
29
 
@@ -16,7 +16,7 @@ def parse_args(arg)
16
16
  end
17
17
 
18
18
  class Context
19
- attr_reader :trace_id, :flows, :file_name, :state, :logger
19
+ attr_reader :trace_id, :traceId, :flows, :file_name, :state, :logger
20
20
 
21
21
  def emit(event)
22
22
  # Add type field if not present to match Node.js/Python behavior
@@ -25,13 +25,13 @@ class Context
25
25
  promise # Return promise to maintain async pattern
26
26
  end
27
27
 
28
- def initialize(rpc, args, file_path)
28
+ def initialize(rpc, args)
29
29
  @rpc = rpc
30
30
  @trace_id = args.traceId
31
+ @traceId = args.traceId
31
32
  @flows = args.flows
32
- @file_name = file_path.split('/').last # Consistent with Python/Node
33
33
  @state = RpcStateManager.new(rpc)
34
- @logger = CustomLogger.new(@trace_id, @flows, @file_name, @rpc)
34
+ @logger = CustomLogger.new(@trace_id, @flows, @rpc)
35
35
  end
36
36
  end
37
37
 
@@ -50,15 +50,18 @@ def run_ruby_module(file_path, args)
50
50
  end
51
51
 
52
52
  rpc = RpcSender.new
53
- context = Context.new(rpc, args, file_path)
53
+ context = Context.new(rpc, args)
54
54
  rpc.init
55
55
 
56
56
  # Call handler and wait for any promises
57
- result = handler(args.data, context)
58
-
59
- # If handler returns a promise-like object, wait for it
60
- if result.respond_to?(:value)
61
- result.value
57
+ if args.contextInFirstArg
58
+ result = handler(context)
59
+ else
60
+ result = handler(args.data, context)
61
+ end
62
+
63
+ if result
64
+ rpc.send('result', result)
62
65
  end
63
66
 
64
67
  rescue => e
@@ -13,36 +13,43 @@ const socket_io_1 = require("socket.io");
13
13
  const flows_endpoint_1 = require("./flows-endpoint");
14
14
  const guards_1 = require("./guards");
15
15
  const logger_1 = require("./logger");
16
- const get_module_export_1 = require("./node/get-module-export");
17
16
  const steps_1 = require("./steps");
18
- const utils_1 = require("./utils");
17
+ const call_step_file_1 = require("./call-step-file");
19
18
  const createServer = async (lockedData, eventManager, state) => {
19
+ const printer = lockedData.printer;
20
20
  const app = (0, express_1.default)();
21
21
  const server = http_1.default.createServer(app);
22
22
  const io = new socket_io_1.Server(server);
23
23
  const allSteps = [...steps_1.systemSteps, ...lockedData.activeSteps];
24
- const cronManager = (0, cron_handler_1.setupCronHandlers)(lockedData, eventManager, io);
25
- const asyncHandler = (step, flows) => {
24
+ const cronManager = (0, cron_handler_1.setupCronHandlers)(lockedData, eventManager, state, io);
25
+ const asyncHandler = (step) => {
26
26
  return async (req, res) => {
27
27
  const traceId = (0, crypto_1.randomUUID)();
28
- const logger = new logger_1.Logger(traceId, flows, step.config.name, io);
29
- logger.debug('[API] Received request, processing step', { path: req.path, step });
30
- const handler = (await (0, get_module_export_1.getModuleExport)(step.filePath, 'handler'));
28
+ const { name, flows } = step.config;
29
+ const logger = new logger_1.Logger(traceId, flows, name, io);
30
+ logger.debug('[API] Received request, processing step', { path: req.path });
31
31
  const request = {
32
32
  body: req.body,
33
33
  headers: req.headers,
34
34
  pathParams: req.params,
35
35
  queryParams: req.query,
36
36
  };
37
- const emit = async ({ data, type }) => {
38
- if (!(0, utils_1.isAllowedToEmit)(step, type)) {
39
- lockedData.printer.printInvalidEmit(step, type);
37
+ try {
38
+ const data = request;
39
+ const result = await (0, call_step_file_1.callStepFile)({
40
+ contextInFirstArg: false,
41
+ data,
42
+ step,
43
+ printer,
44
+ logger,
45
+ eventManager,
46
+ state,
47
+ traceId,
48
+ });
49
+ if (!result) {
50
+ res.status(500).json({ error: 'Internal server error' });
40
51
  return;
41
52
  }
42
- await eventManager.emit({ data, type, traceId, flows, logger }, step.filePath);
43
- };
44
- try {
45
- const result = await handler(request, { emit, state, logger, traceId });
46
53
  if (result.headers) {
47
54
  Object.entries(result.headers).forEach(([key, value]) => res.setHeader(key, value));
48
55
  }
@@ -60,13 +67,13 @@ const createServer = async (lockedData, eventManager, state) => {
60
67
  app.use(body_parser_1.default.urlencoded({ extended: true }));
61
68
  const router = express_1.default.Router();
62
69
  const addRoute = (step) => {
63
- const { method, flows, path } = step.config;
70
+ const { method, path } = step.config;
64
71
  logger_1.globalLogger.debug('[API] Registering route', step.config);
65
72
  if (method === 'POST') {
66
- router.post(path, asyncHandler(step, flows));
73
+ router.post(path, asyncHandler(step));
67
74
  }
68
75
  else if (method === 'GET') {
69
- router.get(path, asyncHandler(step, flows));
76
+ router.get(path, asyncHandler(step));
70
77
  }
71
78
  else {
72
79
  throw new Error(`Unsupported method: ${method}`);
@@ -41,10 +41,10 @@ const fs_1 = __importDefault(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  class FileStateAdapter {
43
43
  constructor(config) {
44
- this.filePath = path.join(config.filePath, 'motia-state.json');
44
+ this.filePath = path.join(config.filePath, 'motia.state.json');
45
45
  }
46
46
  async init() {
47
- const dir = this.filePath.replace('motia-state.json', '');
47
+ const dir = this.filePath.replace('motia.state.json', '');
48
48
  try {
49
49
  fs_1.default.realpathSync(dir);
50
50
  }
@@ -5,10 +5,16 @@ const logger_1 = require("./logger");
5
5
  const call_step_file_1 = require("./call-step-file");
6
6
  const createStepHandlers = (lockedData, eventManager, state) => {
7
7
  const eventSteps = lockedData.eventSteps();
8
+ const printer = lockedData.printer;
8
9
  logger_1.globalLogger.debug(`[step handler] creating step handlers for ${eventSteps.length} steps`);
10
+ const removeLogger = (event) => {
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
+ const { logger, ...rest } = event;
13
+ return rest;
14
+ };
9
15
  const createHandler = (step) => {
10
16
  const { config, filePath } = step;
11
- const { subscribes } = config;
17
+ const { subscribes, name } = config;
12
18
  logger_1.globalLogger.debug('[step handler] establishing step subscriptions', { filePath, step: step.config.name });
13
19
  subscribes.forEach((subscribe) => {
14
20
  eventManager.subscribe({
@@ -16,18 +22,26 @@ const createStepHandlers = (lockedData, eventManager, state) => {
16
22
  event: subscribe,
17
23
  handlerName: step.config.name,
18
24
  handler: async (event) => {
19
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
- const { logger, ...rest } = event;
21
- logger_1.globalLogger.debug('[step handler] received event', { event: rest, step: step.config.name });
25
+ const { logger, data, traceId } = event;
26
+ logger_1.globalLogger.debug('[step handler] received event', { event: removeLogger(event), step: name });
22
27
  try {
23
- await (0, call_step_file_1.callStepFile)(step, lockedData, event, eventManager, state);
28
+ await (0, call_step_file_1.callStepFile)({
29
+ contextInFirstArg: false,
30
+ step,
31
+ printer,
32
+ eventManager,
33
+ state,
34
+ data,
35
+ traceId,
36
+ logger,
37
+ });
24
38
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
39
  }
26
40
  catch (error) {
27
41
  logger_1.globalLogger.error(`[step handler] error calling step`, {
28
42
  error: error.message,
29
43
  filePath,
30
- step: step.config.name,
44
+ step: name,
31
45
  });
32
46
  }
33
47
  },
@@ -2,6 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateStep = void 0;
4
4
  const zod_1 = require("zod");
5
+ const jsonSchema = zod_1.z.object({
6
+ type: zod_1.z.string(),
7
+ properties: zod_1.z.record(zod_1.z.any()),
8
+ required: zod_1.z.array(zod_1.z.string()).optional(),
9
+ additionalProperties: zod_1.z.boolean().optional(),
10
+ description: zod_1.z.string().optional(),
11
+ title: zod_1.z.string().optional(),
12
+ definitions: zod_1.z.record(zod_1.z.any()).optional(),
13
+ $ref: zod_1.z.string().optional(),
14
+ items: zod_1.z.any().optional(),
15
+ enum: zod_1.z.array(zod_1.z.any()).optional(),
16
+ allOf: zod_1.z.array(zod_1.z.any()).optional(),
17
+ anyOf: zod_1.z.array(zod_1.z.any()).optional(),
18
+ oneOf: zod_1.z.array(zod_1.z.any()).optional(),
19
+ not: zod_1.z.any().optional(),
20
+ format: zod_1.z.string().optional(),
21
+ default: zod_1.z.any().optional(),
22
+ examples: zod_1.z.array(zod_1.z.any()).optional(),
23
+ multipleOf: zod_1.z.number().optional(),
24
+ maximum: zod_1.z.number().optional(),
25
+ exclusiveMaximum: zod_1.z.union([zod_1.z.boolean(), zod_1.z.number()]).optional(),
26
+ minimum: zod_1.z.number().optional(),
27
+ exclusiveMinimum: zod_1.z.union([zod_1.z.boolean(), zod_1.z.number()]).optional(),
28
+ maxLength: zod_1.z.number().optional(),
29
+ minLength: zod_1.z.number().optional(),
30
+ pattern: zod_1.z.string().optional(),
31
+ maxItems: zod_1.z.number().optional(),
32
+ minItems: zod_1.z.number().optional(),
33
+ uniqueItems: zod_1.z.boolean().optional(),
34
+ maxProperties: zod_1.z.number().optional(),
35
+ minProperties: zod_1.z.number().optional(),
36
+ const: zod_1.z.any().optional(),
37
+ });
5
38
  const emits = zod_1.z.array(zod_1.z.union([
6
39
  zod_1.z.string(),
7
40
  zod_1.z
@@ -30,7 +63,7 @@ const eventSchema = zod_1.z
30
63
  subscribes: zod_1.z.array(zod_1.z.string()),
31
64
  emits: emits,
32
65
  virtualEmits: emits.optional(),
33
- input: zod_1.z.any(),
66
+ input: zod_1.z.union([jsonSchema, zod_1.z.null()]).optional(),
34
67
  flows: zod_1.z.array(zod_1.z.string()).optional(),
35
68
  })
36
69
  .strict();
@@ -45,7 +78,7 @@ const apiSchema = zod_1.z
45
78
  virtualEmits: emits.optional(),
46
79
  virtualSubscribes: zod_1.z.array(zod_1.z.string()).optional(),
47
80
  flows: zod_1.z.array(zod_1.z.string()).optional(),
48
- bodySchema: zod_1.z.instanceof(zod_1.z.ZodObject).optional(),
81
+ bodySchema: zod_1.z.union([jsonSchema, zod_1.z.null()]).optional(),
49
82
  })
50
83
  .strict();
51
84
  const cronSchema = zod_1.z
@@ -1,5 +1,5 @@
1
1
  import { z, ZodObject } from 'zod';
2
- import { Logger } from './logger';
2
+ import { BaseLogger, Logger } from './logger';
3
3
  export type InternalStateManager = {
4
4
  get<T>(traceId: string, key: string): Promise<T | null>;
5
5
  set<T>(traceId: string, key: string, value: T): Promise<void>;
@@ -75,13 +75,14 @@ export type CronConfig = {
75
75
  emits: Emit[];
76
76
  flows?: string[];
77
77
  };
78
- export type StepHandler<T> = T extends EventConfig<any> ? EventHandler<T['input']> : T extends ApiRouteConfig ? ApiRouteHandler : T extends CronConfig ? never : never;
78
+ export type CronHandler = (ctx: FlowContext) => Promise<void>;
79
+ export type StepHandler<T> = T extends EventConfig<any> ? EventHandler<T['input']> : T extends ApiRouteConfig ? ApiRouteHandler : T extends CronConfig ? CronHandler : never;
79
80
  export type Event<TData = unknown> = {
80
81
  type: string;
81
82
  data: TData;
82
83
  traceId: string;
83
84
  flows?: string[];
84
- logger: Logger;
85
+ logger: BaseLogger;
85
86
  };
86
87
  export type Handler<TData = unknown> = (event: Event<TData>) => Promise<void>;
87
88
  export type SubscribeConfig<TData> = {
package/dist/src/utils.js CHANGED
@@ -10,6 +10,9 @@ const isAllowedToEmit = (step, emit) => {
10
10
  if ((0, guards_1.isEventStep)(step)) {
11
11
  return step.config.emits.map(toType).includes(emit);
12
12
  }
13
+ if ((0, guards_1.isCronStep)(step)) {
14
+ return step.config.emits.map(toType).includes(emit);
15
+ }
13
16
  return false;
14
17
  };
15
18
  exports.isAllowedToEmit = isAllowedToEmit;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@motiadev/core",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "dependencies": {
6
6
  "body-parser": "^1.20.3",
7
7
  "colors": "^1.4.0",
@@ -20,13 +20,15 @@
20
20
  "@types/express": "^5.0.0",
21
21
  "@types/jest": "^29.5.14",
22
22
  "@types/node-cron": "^3.0.11",
23
+ "@types/supertest": "^6.0.2",
23
24
  "jest": "^29.7.0",
25
+ "supertest": "^7.0.0",
24
26
  "ts-jest": "^29.2.5",
25
27
  "typescript": "^5.7.2"
26
28
  },
27
29
  "scripts": {
28
- "move:python": "cp src/python/*.py dist/src/python",
29
- "move:rb": "cp src/ruby/*.rb dist/src/ruby",
30
+ "move:python": "mkdir -p dist/src/python && cp src/python/*.py dist/src/python",
31
+ "move:rb": "mkdir -p dist/src/ruby && cp src/ruby/*.rb dist/src/ruby",
30
32
  "move:steps": "cp src/steps/*.ts dist/src/steps",
31
33
  "build": "rm -rf dist && tsc && npm run move:python && npm run move:rb && npm run move:steps",
32
34
  "lint": "eslint --config ../../eslint.config.js",
@@ -1 +0,0 @@
1
- export declare const getModuleExport: (filePath: string, exportName: string) => Promise<any>;
@@ -1,68 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getModuleExport = void 0;
40
- const path_1 = __importDefault(require("path"));
41
- const tsconfigPaths = __importStar(require("tsconfig-paths"));
42
- const tsConfigPath = path_1.default.resolve(process.cwd(), 'tsconfig.json');
43
- const result = tsconfigPaths.loadConfig(tsConfigPath);
44
- if (result.resultType !== 'success') {
45
- throw Error('Failed to load tsconfig.json');
46
- }
47
- const { absoluteBaseUrl, paths } = result;
48
- // eslint-disable-next-line @typescript-eslint/no-require-imports
49
- require('ts-node').register({
50
- transpileOnly: true,
51
- compilerOptions: { module: 'commonjs', baseUrl: absoluteBaseUrl, paths },
52
- });
53
- const getModuleExport = async (filePath, exportName) => {
54
- try {
55
- const resolvedFilePath = require.resolve(filePath);
56
- if (resolvedFilePath in require.cache) {
57
- delete require.cache[resolvedFilePath];
58
- }
59
- // eslint-disable-next-line @typescript-eslint/no-require-imports
60
- const module = require(resolvedFilePath);
61
- return module[exportName];
62
- }
63
- catch (error) {
64
- console.error(`Failed to extract ${exportName} from`, error);
65
- return undefined;
66
- }
67
- };
68
- exports.getModuleExport = getModuleExport;
@@ -1,2 +0,0 @@
1
- import { StepConfig } from '../types';
2
- export declare const getPythonConfig: (file: string) => Promise<StepConfig>;
@@ -1,34 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getPythonConfig = void 0;
7
- const child_process_1 = require("child_process");
8
- const path_1 = __importDefault(require("path"));
9
- const logger_1 = require("../logger");
10
- const getPythonConfig = (file) => {
11
- const getConfig = path_1.default.join(__dirname, 'get-config.py');
12
- return new Promise((resolve, reject) => {
13
- let config = null;
14
- const child = (0, child_process_1.spawn)('python', [getConfig, file], {
15
- stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
16
- });
17
- child.on('message', (message) => {
18
- logger_1.globalLogger.debug('[Python Config] Read config', { config: message });
19
- config = message;
20
- });
21
- child.on('close', (code) => {
22
- if (code !== 0) {
23
- reject(new Error(`Process exited with code ${code}`));
24
- }
25
- else if (!config) {
26
- reject(new Error(`No config found for file ${file}`));
27
- }
28
- else {
29
- resolve(config);
30
- }
31
- });
32
- });
33
- };
34
- exports.getPythonConfig = getPythonConfig;