motia 0.0.19 → 0.0.21

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.
@@ -0,0 +1,61 @@
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.createDevWatchers = void 0;
7
+ const core_1 = require("@motiadev/core");
8
+ const watcher_1 = require("./watcher");
9
+ const path_1 = __importDefault(require("path"));
10
+ const createDevWatchers = (lockedData, server, eventHandler, cronManager) => {
11
+ const stepDir = path_1.default.join(process.cwd(), 'steps');
12
+ const watcher = new watcher_1.Watcher(stepDir, lockedData);
13
+ lockedData.on('flow-created', (flowName) => {
14
+ server.socketServer.emit('flow-created', flowName);
15
+ });
16
+ lockedData.on('flow-removed', (flowName) => {
17
+ server.socketServer.emit('flow-removed', flowName);
18
+ });
19
+ lockedData.on('flow-updated', (flowName) => {
20
+ server.socketServer.emit('flow-updated', flowName);
21
+ });
22
+ watcher.onStepChange((oldStep, newStep) => {
23
+ if ((0, core_1.isApiStep)(oldStep))
24
+ server.removeRoute(oldStep);
25
+ if ((0, core_1.isCronStep)(oldStep))
26
+ cronManager.removeCronJob(oldStep);
27
+ if ((0, core_1.isEventStep)(oldStep))
28
+ eventHandler.removeHandler(oldStep);
29
+ const isUpdated = lockedData.updateStep(oldStep, newStep);
30
+ if (isUpdated) {
31
+ if ((0, core_1.isCronStep)(newStep))
32
+ cronManager.createCronJob(newStep);
33
+ if ((0, core_1.isEventStep)(newStep))
34
+ eventHandler.createHandler(newStep);
35
+ if ((0, core_1.isApiStep)(newStep))
36
+ server.addRoute(newStep);
37
+ }
38
+ });
39
+ watcher.onStepCreate((step) => {
40
+ const isCreated = lockedData.createStep(step);
41
+ if (isCreated) {
42
+ if ((0, core_1.isApiStep)(step))
43
+ server.addRoute(step);
44
+ if ((0, core_1.isEventStep)(step))
45
+ eventHandler.createHandler(step);
46
+ if ((0, core_1.isCronStep)(step))
47
+ cronManager.createCronJob(step);
48
+ }
49
+ });
50
+ watcher.onStepDelete((step) => {
51
+ if ((0, core_1.isApiStep)(step))
52
+ server.removeRoute(step);
53
+ if ((0, core_1.isEventStep)(step))
54
+ eventHandler.removeHandler(step);
55
+ if ((0, core_1.isCronStep)(step))
56
+ cronManager.removeCronJob(step);
57
+ lockedData.deleteStep(step);
58
+ });
59
+ return watcher;
60
+ };
61
+ exports.createDevWatchers = createDevWatchers;
package/dist/src/dev.js CHANGED
@@ -7,6 +7,7 @@ exports.dev = void 0;
7
7
  const core_1 = require("@motiadev/core");
8
8
  const generate_locked_data_1 = require("./generate-locked-data");
9
9
  const path_1 = __importDefault(require("path"));
10
+ const dev_watchers_1 = require("./dev-watchers");
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  require('ts-node').register({
12
13
  transpileOnly: true,
@@ -14,25 +15,27 @@ require('ts-node').register({
14
15
  });
15
16
  const dev = async (port) => {
16
17
  const lockedData = await (0, generate_locked_data_1.generateLockedData)(process.cwd());
17
- const steps = [...lockedData.steps.active, ...lockedData.steps.dev];
18
18
  const eventManager = (0, core_1.createEventManager)();
19
19
  const state = (0, core_1.createStateAdapter)({
20
20
  adapter: 'default',
21
21
  filePath: path_1.default.join(process.cwd(), '.motia'),
22
22
  });
23
23
  await state.init();
24
- const { app, server } = await (0, core_1.createServer)({ steps, state, flows: lockedData.flows, eventManager });
25
- (0, core_1.createStepHandlers)(steps, eventManager, state);
26
- server.listen(port);
24
+ const motiaServer = await (0, core_1.createServer)(lockedData, eventManager, state);
25
+ const motiaEventManager = (0, core_1.createStepHandlers)(lockedData, eventManager, state);
26
+ const watcher = (0, dev_watchers_1.createDevWatchers)(lockedData, motiaServer, motiaEventManager, motiaServer.cronManager);
27
+ watcher.init();
28
+ motiaServer.server.listen(port);
27
29
  console.log('🚀 Server ready and listening on port', port);
28
30
  console.log(`🔗 Open http://localhost:${port}/ to open workbench 🛠️`);
29
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
30
32
  const { applyMiddleware } = require('@motiadev/workbench/dist/middleware');
31
- await applyMiddleware(app);
33
+ await applyMiddleware(motiaServer.app);
32
34
  // 6) Gracefully shut down on SIGTERM
33
35
  process.on('SIGTERM', async () => {
34
36
  core_1.globalLogger.info('🛑 Shutting down...');
35
- server.close();
37
+ motiaServer.server.close();
38
+ await watcher.stop();
36
39
  process.exit(0);
37
40
  });
38
41
  };
@@ -3,12 +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.generateLockedData = void 0;
6
+ exports.generateLockedData = exports.readConfig = void 0;
7
+ const core_1 = require("@motiadev/core");
7
8
  const crypto_1 = require("crypto");
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
10
11
  const yaml_1 = __importDefault(require("yaml"));
11
- const core_1 = require("@motiadev/core");
12
12
  const version = `${(0, crypto_1.randomUUID)()}:${Math.floor(Date.now() / 1000)}`;
13
13
  const baseFlowRegex = new RegExp(/flows"?\s?.*\s*\[([^\]]+)\]/);
14
14
  // Helper function to read config.yml
@@ -21,99 +21,40 @@ const readConfig = (configPath) => {
21
21
  const configContent = fs_1.default.readFileSync(configPath, 'utf-8');
22
22
  return yaml_1.default.parse(configContent);
23
23
  };
24
- const extractStepConfig = (filePath) => {
25
- const isRb = filePath.endsWith('.rb');
26
- const isPython = filePath.endsWith('.py');
27
- const isNode = filePath.endsWith('.js') || filePath.endsWith('.ts');
28
- if (isRb) {
29
- return (0, core_1.getRubyConfig)(filePath);
30
- }
31
- if (isPython) {
32
- return (0, core_1.getPythonConfig)(filePath);
33
- }
34
- if (isNode) {
35
- return (0, core_1.getNodeFileConfig)(filePath);
36
- }
37
- };
24
+ exports.readConfig = readConfig;
38
25
  // Helper function to recursively collect flow data
39
26
  const collectFlows = async (baseDir) => {
40
27
  const folderItems = fs_1.default.readdirSync(baseDir, { withFileTypes: true });
41
28
  let steps = [];
42
29
  for (const item of folderItems) {
43
- const itemPath = path_1.default.join(baseDir, item.name);
30
+ const filePath = path_1.default.join(baseDir, item.name);
44
31
  if (item.isDirectory()) {
45
- steps = steps.concat(await collectFlows(itemPath));
32
+ steps = steps.concat(await collectFlows(filePath));
46
33
  }
47
34
  else if (item.name.match(/\.step\.(ts|js|py|rb)$/)) {
48
- const fileContent = fs_1.default.readFileSync(itemPath, 'utf-8');
35
+ const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
49
36
  const flowMatch = fileContent.match(baseFlowRegex);
50
- const config = await extractStepConfig(itemPath);
37
+ const config = await (0, core_1.getStepConfig)(filePath);
51
38
  if (!config) {
52
- console.warn(`No config found in step ${itemPath}, step skipped`);
39
+ console.warn(`No config found in step ${filePath}, step skipped`);
53
40
  continue;
54
41
  }
55
42
  if (flowMatch) {
56
- steps.push({
57
- // TODO: when the lock file comes back, this needs to be updated to use a relative path
58
- filePath: itemPath,
59
- version,
60
- config,
61
- });
43
+ steps.push({ filePath, version, config });
62
44
  }
63
45
  }
64
46
  }
65
47
  return steps;
66
48
  };
67
49
  const generateLockedData = async (projectDir) => {
68
- const configPath = path_1.default.join(projectDir, 'config.yml');
69
- // NOTE: right now for performance and simplicity let's enforce a folder, but we might want to remove this and scan the entire current directory
70
- const stepsPath = path_1.default.join(projectDir, 'steps');
71
50
  try {
72
- const config = readConfig(configPath);
73
- // Collect steps data from steps folder
74
- const sourceSteps = await collectFlows(stepsPath);
75
- const { flows, steps } = sourceSteps.reduce((acc, step) => {
76
- const nextSteps = { ...acc.steps };
77
- // NOTE: identifies a noop step
78
- if (step.config.virtualEmits) {
79
- nextSteps.dev.push(step);
80
- }
81
- else {
82
- nextSteps.active.push(step);
83
- }
84
- const nextFlows = { ...acc.flows };
85
- step.config.flows.forEach((flowName) => {
86
- if (!nextFlows[flowName]) {
87
- nextFlows[flowName] = {
88
- name: flowName,
89
- // TODO: how are we going to extract descriptions?
90
- description: '',
91
- steps: [],
92
- };
93
- }
94
- nextFlows[flowName].steps.push(step);
95
- });
96
- return {
97
- ...acc,
98
- steps: nextSteps,
99
- flows: nextFlows,
100
- };
101
- }, {
102
- flows: {},
103
- steps: {
104
- active: [],
105
- dev: [],
106
- },
107
- });
108
- // Prepare the lock file data
109
- const lockedData = {
110
- baseDir: projectDir,
111
- state: config?.state || {},
112
- steps,
113
- flows,
114
- };
115
- console.log('Project config loaded');
116
- // TODO: for now this will return the locked data as an object, in the future we will write it to a lock file
51
+ /*
52
+ * NOTE: right now for performance and simplicity let's enforce a folder,
53
+ * but we might want to remove this and scan the entire current directory
54
+ */
55
+ const sourceSteps = await collectFlows(path_1.default.join(projectDir, 'steps'));
56
+ const lockedData = new core_1.LockedData(projectDir);
57
+ sourceSteps.forEach((step) => lockedData.createStep(step));
117
58
  return lockedData;
118
59
  }
119
60
  catch (error) {
@@ -0,0 +1,94 @@
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.Watcher = void 0;
7
+ const chokidar_1 = __importDefault(require("chokidar"));
8
+ const crypto_1 = require("crypto");
9
+ const core_1 = require("@motiadev/core");
10
+ class Watcher {
11
+ constructor(dir, lockedData) {
12
+ this.dir = dir;
13
+ this.lockedData = lockedData;
14
+ }
15
+ onStepChange(handler) {
16
+ this.stepChangeHandler = handler;
17
+ }
18
+ onStepCreate(handler) {
19
+ this.stepCreateHandler = handler;
20
+ }
21
+ onStepDelete(handler) {
22
+ this.stepDeleteHandler = handler;
23
+ }
24
+ findStep(path) {
25
+ return (this.lockedData.activeSteps.find((step) => step.filePath === path) ||
26
+ this.lockedData.devSteps.find((step) => step.filePath === path));
27
+ }
28
+ async onFileAdd(path) {
29
+ if (!this.stepCreateHandler) {
30
+ console.warn(`No step create handler, step skipped`);
31
+ return;
32
+ }
33
+ const config = await (0, core_1.getStepConfig)(path);
34
+ if (!config) {
35
+ console.warn(`No config found in step ${path}, step skipped`);
36
+ return;
37
+ }
38
+ const version = `${(0, crypto_1.randomUUID)()}:${Math.floor(Date.now() / 1000)}`;
39
+ const step = { filePath: path, version, config };
40
+ this.stepCreateHandler?.(step);
41
+ }
42
+ async onFileChange(path) {
43
+ if (!this.stepChangeHandler) {
44
+ console.warn(`No step change handler, step skipped`);
45
+ return;
46
+ }
47
+ const config = await (0, core_1.getStepConfig)(path);
48
+ const step = this.findStep(path);
49
+ if (!step && !config) {
50
+ console.warn(`Step ${path} not found, step skipped`);
51
+ return;
52
+ }
53
+ // didn't have a step, but now we have a config
54
+ if (!step && config) {
55
+ const version = `${(0, crypto_1.randomUUID)()}:${Math.floor(Date.now() / 1000)}`;
56
+ const step = { filePath: path, version, config };
57
+ this.stepCreateHandler?.(step);
58
+ }
59
+ // had a step, and now we have a config
60
+ if (step && config) {
61
+ const newStep = { ...step, config };
62
+ this.stepChangeHandler?.(step, newStep);
63
+ }
64
+ // had a step, but no config
65
+ if (step && !config) {
66
+ this.stepDeleteHandler?.(step);
67
+ }
68
+ }
69
+ async onFileDelete(path) {
70
+ if (!this.stepDeleteHandler) {
71
+ console.warn(`No step delete handler, step skipped`);
72
+ return;
73
+ }
74
+ const step = this.findStep(path);
75
+ if (!step) {
76
+ console.warn(`Step ${path} not found, step skipped`);
77
+ return;
78
+ }
79
+ this.stepDeleteHandler?.(step);
80
+ }
81
+ init() {
82
+ this.watcher = chokidar_1.default
83
+ .watch(this.dir, { persistent: true, ignoreInitial: true })
84
+ .on('add', (path) => this.onFileAdd(path))
85
+ .on('change', (path) => this.onFileChange(path))
86
+ .on('unlink', (path) => this.onFileDelete(path));
87
+ }
88
+ async stop() {
89
+ if (this.watcher) {
90
+ await this.watcher.close();
91
+ }
92
+ }
93
+ }
94
+ exports.Watcher = Watcher;
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "motia",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "motia": "dist/src/cli.js"
7
7
  },
8
8
  "dependencies": {
9
+ "chokidar": "^4.0.3",
9
10
  "commander": "^13.0.0",
10
11
  "dotenv": "^16.4.7",
11
12
  "figlet": "^1.8.0",
12
13
  "ts-node": "^10.9.2",
13
14
  "yaml": "^2.7.0",
14
- "@motiadev/core": "0.0.19",
15
- "@motiadev/workbench": "0.0.19"
15
+ "@motiadev/core": "0.0.21",
16
+ "@motiadev/workbench": "0.0.21"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/figlet": "^1.7.0",