motia 0.8.2-beta.140-428722 → 0.8.3-beta.140

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.
Files changed (111) hide show
  1. package/dist/cjs/cloud/build/builders/archiver.d.ts +2 -1
  2. package/dist/cjs/cloud/build/builders/archiver.js +25 -14
  3. package/dist/cjs/cloud/build/builders/include-static-files.js +2 -1
  4. package/dist/cjs/cloud/build/builders/node/index.js +4 -4
  5. package/dist/cjs/cloud/build/builders/python/index.js +1 -1
  6. package/dist/cjs/create/index.js +33 -32
  7. package/dist/cjs/create/interactive.js +1 -0
  8. package/dist/cjs/create/templates/generate.d.ts +1 -0
  9. package/dist/cjs/create/templates/generate.js +89 -18
  10. package/dist/cjs/create/templates/generate.ts +93 -19
  11. package/dist/cjs/create/templates/index.js +1 -0
  12. package/dist/cjs/create/templates/index.ts +2 -1
  13. package/dist/cjs/create/templates/nodejs/tutorial.tsx.txt +1 -1
  14. package/dist/cjs/create/templates/plugin/.gitignore.txt +3 -0
  15. package/dist/cjs/create/templates/plugin/README.md.txt +64 -0
  16. package/dist/cjs/create/templates/plugin/package.json.txt +47 -0
  17. package/dist/cjs/create/templates/plugin/postcss.config.js.txt +6 -0
  18. package/dist/cjs/create/templates/plugin/src/components/example-page.tsx.txt +64 -0
  19. package/dist/cjs/create/templates/plugin/src/index.ts.txt +4 -0
  20. package/dist/cjs/create/templates/plugin/src/plugin.ts.txt +17 -0
  21. package/dist/cjs/create/templates/plugin/src/styles.css.txt +3 -0
  22. package/dist/cjs/create/templates/plugin/tsconfig.json.txt +27 -0
  23. package/dist/cjs/create/templates/plugin/vite.config.ts.txt +30 -0
  24. package/dist/cjs/create/templates/python/tutorial.tsx.txt +1 -1
  25. package/dist/cjs/dev.js +10 -18
  26. package/dist/cjs/generate-locked-data.d.ts +3 -2
  27. package/dist/cjs/generate-locked-data.js +2 -2
  28. package/dist/cjs/plugins/collect-plugin-steps.d.ts +5 -0
  29. package/dist/cjs/plugins/collect-plugin-steps.js +44 -0
  30. package/dist/cjs/plugins/create-plugin-context.d.ts +2 -0
  31. package/dist/cjs/plugins/create-plugin-context.js +33 -0
  32. package/dist/cjs/plugins/index.d.ts +1 -0
  33. package/dist/cjs/plugins/index.js +5 -0
  34. package/dist/cjs/plugins/install-plugin-dependencies.d.ts +2 -0
  35. package/dist/cjs/plugins/install-plugin-dependencies.js +56 -0
  36. package/dist/cjs/plugins/load-config.d.ts +2 -0
  37. package/dist/cjs/{load-motia-config.js → plugins/load-config.js} +17 -12
  38. package/dist/cjs/plugins/plugin-dependencies.d.ts +1 -0
  39. package/dist/cjs/plugins/plugin-dependencies.js +10 -0
  40. package/dist/cjs/{generate-plugins.d.ts → plugins/process-plugins.d.ts} +1 -1
  41. package/dist/cjs/plugins/process-plugins.js +20 -0
  42. package/dist/cjs/plugins/process-steps.d.ts +2 -0
  43. package/dist/cjs/plugins/process-steps.js +40 -0
  44. package/dist/cjs/start.js +7 -15
  45. package/dist/cjs/utils/get-package-manager.d.ts +1 -0
  46. package/dist/cjs/utils/get-package-manager.js +19 -0
  47. package/dist/esm/cloud/build/builders/archiver.d.ts +2 -1
  48. package/dist/esm/cloud/build/builders/archiver.js +25 -14
  49. package/dist/esm/cloud/build/builders/include-static-files.js +2 -1
  50. package/dist/esm/cloud/build/builders/node/index.js +4 -4
  51. package/dist/esm/cloud/build/builders/python/index.js +1 -1
  52. package/dist/esm/create/index.js +32 -31
  53. package/dist/esm/create/interactive.js +1 -0
  54. package/dist/esm/create/templates/generate.d.ts +1 -0
  55. package/dist/esm/create/templates/generate.js +87 -17
  56. package/dist/esm/create/templates/generate.ts +93 -19
  57. package/dist/esm/create/templates/index.js +2 -1
  58. package/dist/esm/create/templates/index.ts +2 -1
  59. package/dist/esm/create/templates/nodejs/tutorial.tsx.txt +1 -1
  60. package/dist/esm/create/templates/plugin/.gitignore.txt +3 -0
  61. package/dist/esm/create/templates/plugin/README.md.txt +64 -0
  62. package/dist/esm/create/templates/plugin/package.json.txt +47 -0
  63. package/dist/esm/create/templates/plugin/postcss.config.js.txt +6 -0
  64. package/dist/esm/create/templates/plugin/src/components/example-page.tsx.txt +64 -0
  65. package/dist/esm/create/templates/plugin/src/index.ts.txt +4 -0
  66. package/dist/esm/create/templates/plugin/src/plugin.ts.txt +17 -0
  67. package/dist/esm/create/templates/plugin/src/styles.css.txt +3 -0
  68. package/dist/esm/create/templates/plugin/tsconfig.json.txt +27 -0
  69. package/dist/esm/create/templates/plugin/vite.config.ts.txt +30 -0
  70. package/dist/esm/create/templates/python/tutorial.tsx.txt +1 -1
  71. package/dist/esm/dev.js +10 -18
  72. package/dist/esm/generate-locked-data.d.ts +3 -2
  73. package/dist/esm/generate-locked-data.js +2 -2
  74. package/dist/esm/plugins/collect-plugin-steps.d.ts +5 -0
  75. package/dist/esm/plugins/collect-plugin-steps.js +37 -0
  76. package/dist/esm/plugins/create-plugin-context.d.ts +2 -0
  77. package/dist/esm/plugins/create-plugin-context.js +29 -0
  78. package/dist/esm/plugins/index.d.ts +1 -0
  79. package/dist/esm/plugins/index.js +1 -0
  80. package/dist/esm/plugins/install-plugin-dependencies.d.ts +2 -0
  81. package/dist/esm/plugins/install-plugin-dependencies.js +49 -0
  82. package/dist/esm/plugins/load-config.d.ts +2 -0
  83. package/dist/esm/plugins/load-config.js +17 -0
  84. package/dist/esm/plugins/plugin-dependencies.d.ts +1 -0
  85. package/dist/esm/plugins/plugin-dependencies.js +7 -0
  86. package/dist/esm/{generate-plugins.d.ts → plugins/process-plugins.d.ts} +1 -1
  87. package/dist/esm/plugins/process-plugins.js +16 -0
  88. package/dist/esm/plugins/process-steps.d.ts +2 -0
  89. package/dist/esm/plugins/process-steps.js +36 -0
  90. package/dist/esm/start.js +7 -15
  91. package/dist/esm/utils/get-package-manager.d.ts +1 -0
  92. package/dist/esm/utils/get-package-manager.js +15 -0
  93. package/dist/types/cloud/build/builders/archiver.d.ts +2 -1
  94. package/dist/types/create/templates/generate.d.ts +1 -0
  95. package/dist/types/generate-locked-data.d.ts +3 -2
  96. package/dist/types/plugins/collect-plugin-steps.d.ts +5 -0
  97. package/dist/types/plugins/create-plugin-context.d.ts +2 -0
  98. package/dist/types/plugins/index.d.ts +1 -0
  99. package/dist/types/plugins/install-plugin-dependencies.d.ts +2 -0
  100. package/dist/types/plugins/load-config.d.ts +2 -0
  101. package/dist/types/plugins/plugin-dependencies.d.ts +1 -0
  102. package/dist/types/{generate-plugins.d.ts → plugins/process-plugins.d.ts} +1 -1
  103. package/dist/types/plugins/process-steps.d.ts +2 -0
  104. package/dist/types/utils/get-package-manager.d.ts +1 -0
  105. package/package.json +4 -4
  106. package/dist/cjs/generate-plugins.js +0 -163
  107. package/dist/cjs/load-motia-config.d.ts +0 -2
  108. package/dist/esm/generate-plugins.js +0 -123
  109. package/dist/esm/load-motia-config.d.ts +0 -2
  110. package/dist/esm/load-motia-config.js +0 -15
  111. package/dist/types/load-motia-config.d.ts +0 -2
@@ -9,6 +9,7 @@ export declare class Archiver {
9
9
  private uncompressedSize;
10
10
  constructor(filePath: string);
11
11
  appendDirectory(sourcePath: string, targetPath: string): void;
12
- append(stream: fs.ReadStream | string, filePath: string): void;
12
+ private calculateDirectorySize;
13
+ append(stream: fs.ReadStream | string | Buffer, filePath: string): void;
13
14
  finalize(): Promise<ArchiveResult>;
14
15
  }
@@ -16,39 +16,50 @@ class Archiver {
16
16
  }
17
17
  appendDirectory(sourcePath, targetPath) {
18
18
  try {
19
- const items = fs_1.default.readdirSync(sourcePath);
19
+ const stat = fs_1.default.statSync(sourcePath);
20
+ if (!stat.isDirectory()) {
21
+ return;
22
+ }
23
+ this.uncompressedSize += this.calculateDirectorySize(sourcePath);
24
+ this.archive.directory(sourcePath, targetPath === '/' ? false : targetPath);
25
+ }
26
+ catch (_error) { }
27
+ }
28
+ calculateDirectorySize(dirPath) {
29
+ let totalSize = 0;
30
+ try {
31
+ const items = fs_1.default.readdirSync(dirPath);
20
32
  for (const item of items) {
21
- const fullPath = path_1.default.join(sourcePath, item);
33
+ const fullPath = path_1.default.join(dirPath, item);
22
34
  try {
23
35
  const stat = fs_1.default.statSync(fullPath);
24
36
  if (stat.isDirectory()) {
25
- this.appendDirectory(fullPath, path_1.default.join(targetPath, item));
37
+ totalSize += this.calculateDirectorySize(fullPath);
26
38
  }
27
39
  else {
28
- this.append(fs_1.default.createReadStream(fullPath), targetPath ? path_1.default.join(targetPath, item) : item);
40
+ totalSize += stat.size;
29
41
  }
30
42
  }
31
- catch (_error) {
32
- // Ignore individual file errors
33
- }
43
+ catch (_error) { }
34
44
  }
35
45
  }
36
- catch (_error) {
37
- // Ignore directory read errors
38
- }
46
+ catch (_error) { }
47
+ return totalSize;
39
48
  }
40
49
  append(stream, filePath) {
41
- // Track uncompressed size
42
50
  if (typeof stream === 'string') {
43
- // String content
44
51
  this.uncompressedSize += Buffer.byteLength(stream, 'utf8');
52
+ this.archive.append(stream, { name: filePath });
53
+ }
54
+ else if (Buffer.isBuffer(stream)) {
55
+ this.uncompressedSize += stream.length;
56
+ this.archive.append(stream, { name: filePath });
45
57
  }
46
58
  else {
47
- // ReadStream - get file stats
48
59
  const stats = fs_1.default.statSync(stream.path);
49
60
  this.uncompressedSize += stats.size;
61
+ this.archive.append(stream, { name: filePath });
50
62
  }
51
- this.archive.append(stream, { name: filePath });
52
63
  }
53
64
  async finalize() {
54
65
  return new Promise((resolve, reject) => {
@@ -18,7 +18,8 @@ const includeStaticFiles = (steps, builder, archive) => {
18
18
  const matches = (0, glob_1.globSync)(file, { cwd: path_1.default.dirname(step.filePath), absolute: true });
19
19
  matches.forEach((filePath) => {
20
20
  const relativeFilePath = path_1.default.relative(builder.projectDir, filePath);
21
- archive.append(fs_1.default.createReadStream(filePath), relativeFilePath);
21
+ const content = fs_1.default.readFileSync(filePath);
22
+ archive.append(content, relativeFilePath);
22
23
  });
23
24
  });
24
25
  }
@@ -94,8 +94,8 @@ class NodeBuilder {
94
94
  const archiver = new archiver_1.Archiver(path_1.default.join(constants_1.distDir, zipName));
95
95
  const routerJs = path_1.default.join(constants_1.distDir, 'router.js');
96
96
  const routerMap = path_1.default.join(constants_1.distDir, 'router.js.map');
97
- archiver.append(fs_1.default.createReadStream(routerJs), 'router.js');
98
- archiver.append(fs_1.default.createReadStream(routerMap), 'router.js.map');
97
+ archiver.append(fs_1.default.readFileSync(routerJs), 'router.js');
98
+ archiver.append(fs_1.default.readFileSync(routerMap), 'router.js.map');
99
99
  (0, include_static_files_1.includeStaticFiles)(steps, this.builder, archiver);
100
100
  const { compressedSize, uncompressedSize } = await archiver.finalize();
101
101
  fs_1.default.unlinkSync(tsRouter);
@@ -123,8 +123,8 @@ class NodeBuilder {
123
123
  };
124
124
  await esbuild.build(userConfig ? { ...defaultConfig, ...userConfig } : defaultConfig);
125
125
  const archiver = new archiver_1.Archiver(path_1.default.join(constants_1.distDir, bundlePath));
126
- archiver.append(fs_1.default.createReadStream(outputJsFile), entrypointPath);
127
- archiver.append(fs_1.default.createReadStream(outputMapFile), entrypointMapPath);
126
+ archiver.append(fs_1.default.readFileSync(outputJsFile), entrypointPath);
127
+ archiver.append(fs_1.default.readFileSync(outputMapFile), entrypointMapPath);
128
128
  (0, include_static_files_1.includeStaticFiles)([step], this.builder, archiver);
129
129
  const { compressedSize, uncompressedSize } = await archiver.finalize();
130
130
  fs_1.default.unlinkSync(outputJsFile);
@@ -123,7 +123,7 @@ class PythonBuilder {
123
123
  }
124
124
  getModuleName(step) {
125
125
  // return step path
126
- return step.filePath.replace(this.builder.projectDir, '').substring(1).replace(/\.py$/, '').replace(/\//g, '.');
126
+ return step.filePath.replace(this.builder.projectDir, '').substring(1).replace(/\.py$/, '').replace(/[\\/]/g, '.');
127
127
  }
128
128
  }
129
129
  exports.PythonBuilder = PythonBuilder;
@@ -8,7 +8,9 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const generate_types_1 = require("../generate-types");
10
10
  const install_1 = require("../install");
11
+ const plugin_dependencies_1 = require("../plugins/plugin-dependencies");
11
12
  const execute_command_1 = require("../utils/execute-command");
13
+ const get_package_manager_1 = require("../utils/get-package-manager");
12
14
  const version_1 = require("../version");
13
15
  const pull_rules_1 = require("./pull-rules");
14
16
  const setup_template_1 = require("./setup-template");
@@ -17,20 +19,6 @@ require('ts-node').register({
17
19
  transpileOnly: true,
18
20
  compilerOptions: { module: 'commonjs' },
19
21
  });
20
- const getPackageManager = (dir) => {
21
- if ((0, utils_1.checkIfFileExists)(dir, 'yarn.lock')) {
22
- return 'yarn';
23
- }
24
- else if ((0, utils_1.checkIfFileExists)(dir, 'pnpm-lock.yaml')) {
25
- return 'pnpm';
26
- }
27
- else if ((0, utils_1.checkIfFileExists)(dir, 'package-lock.json')) {
28
- return 'npm';
29
- }
30
- else {
31
- return 'unknown';
32
- }
33
- };
34
22
  const installRequiredDependencies = async (packageManager, rootDir, context) => {
35
23
  context.log('installing-dependencies', (message) => message.tag('info').append('Installing dependencies...'));
36
24
  const installCommand = {
@@ -47,11 +35,7 @@ const installRequiredDependencies = async (packageManager, rootDir, context) =>
47
35
  '@types/jest@^29.5.14',
48
36
  'jest@^29.7.0',
49
37
  'ts-jest@^29.2.5',
50
- `@motiadev/core@${version_1.version}`,
51
- `@motiadev/plugin-logs@${version_1.version}`,
52
- `@motiadev/plugin-states@${version_1.version}`,
53
- `@motiadev/plugin-endpoint@${version_1.version}`,
54
- `@motiadev/plugin-observability@${version_1.version}`,
38
+ ...plugin_dependencies_1.pluginDependencies.map((dep) => `${dep}@${version_1.version}`),
55
39
  ].join(' ');
56
40
  try {
57
41
  await (0, execute_command_1.executeCommand)(`${installCommand} ${dependencies}`, rootDir);
@@ -64,7 +48,7 @@ const installRequiredDependencies = async (packageManager, rootDir, context) =>
64
48
  };
65
49
  const preparePackageManager = async (rootDir, context) => {
66
50
  let packageManager = 'npm';
67
- const detectedPackageManager = getPackageManager(rootDir);
51
+ const detectedPackageManager = (0, get_package_manager_1.getPackageManager)(rootDir);
68
52
  if (detectedPackageManager !== 'unknown') {
69
53
  context.log('package-manager-detected', (message) => message.tag('info').append('Detected package manager').append(detectedPackageManager, 'gray'));
70
54
  packageManager = detectedPackageManager;
@@ -82,9 +66,14 @@ const installNodeDependencies = async (rootDir, context) => {
82
66
  });
83
67
  return packageManager;
84
68
  };
85
- const wrapUp = async (context, packageManager) => {
69
+ const wrapUp = async (context, packageManager, isPlugin = false) => {
86
70
  context.log('project-setup-completed', (message) => message.tag('success').append('Project setup completed, happy coding!'));
87
- context.log('package-manager-used', (message) => message.tag('info').append('To start the development server, run').append(`${packageManager} run dev`, 'gray'));
71
+ if (isPlugin) {
72
+ context.log('package-manager-used', (message) => message.tag('info').append('To build the plugin, run').append(`${packageManager} run build`, 'gray'));
73
+ }
74
+ else {
75
+ context.log('package-manager-used', (message) => message.tag('info').append('To start the development server, run').append(`${packageManager} run dev`, 'gray'));
76
+ }
88
77
  };
89
78
  const create = async ({ projectName, template, cursorEnabled, context }) => {
90
79
  console.log('\n\n' +
@@ -100,6 +89,7 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
100
89
  '\n\n');
101
90
  const isCurrentDir = projectName === '.' || projectName === './' || projectName === '.\\';
102
91
  const rootDir = isCurrentDir ? process.cwd() : path_1.default.join(process.cwd(), projectName);
92
+ const isPluginTemplate = template === 'plugin';
103
93
  if (!isCurrentDir && !(0, utils_1.checkIfDirectoryExists)(rootDir)) {
104
94
  fs_1.default.mkdirSync(path_1.default.join(rootDir));
105
95
  context.log('directory-created', (message) => message.tag('success').append('Directory created ').append(projectName, 'gray'));
@@ -107,7 +97,8 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
107
97
  else {
108
98
  context.log('directory-using', (message) => message.tag('info').append('Using current directory'));
109
99
  }
110
- if (!(0, utils_1.checkIfFileExists)(rootDir, 'package.json')) {
100
+ // Plugin template handles package.json differently (via template)
101
+ if (!isPluginTemplate && !(0, utils_1.checkIfFileExists)(rootDir, 'package.json')) {
111
102
  const finalProjectName = !projectName || projectName === '.' || projectName === './' || projectName === '.\\'
112
103
  ? path_1.default.basename(process.cwd())
113
104
  : projectName.trim();
@@ -127,7 +118,7 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
127
118
  fs_1.default.writeFileSync(path_1.default.join(rootDir, 'package.json'), JSON.stringify(packageJsonContent, null, 2));
128
119
  context.log('package-json-created', (message) => message.tag('success').append('File').append('package.json', 'cyan').append('has been created.'));
129
120
  }
130
- else {
121
+ else if (!isPluginTemplate) {
131
122
  const packageJsonPath = path_1.default.join(rootDir, 'package.json');
132
123
  const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
133
124
  if (!packageJson.scripts) {
@@ -149,7 +140,8 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
149
140
  .append('command to')
150
141
  .append('package.json', 'gray'));
151
142
  }
152
- if (!(0, utils_1.checkIfFileExists)(rootDir, 'tsconfig.json')) {
143
+ // Plugin template handles tsconfig.json via template
144
+ if (!isPluginTemplate && !(0, utils_1.checkIfFileExists)(rootDir, 'tsconfig.json')) {
153
145
  const tsconfigContent = {
154
146
  compilerOptions: {
155
147
  target: 'ES2020',
@@ -172,7 +164,8 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
172
164
  fs_1.default.writeFileSync(path_1.default.join(rootDir, 'tsconfig.json'), JSON.stringify(tsconfigContent, null, 2));
173
165
  context.log('tsconfig-json-created', (message) => message.tag('success').append('File').append('tsconfig.json', 'cyan').append('has been created.'));
174
166
  }
175
- if (!(0, utils_1.checkIfFileExists)(rootDir, '.gitignore')) {
167
+ // Plugin template handles .gitignore via template
168
+ if (!isPluginTemplate && !(0, utils_1.checkIfFileExists)(rootDir, '.gitignore')) {
176
169
  const gitignoreContent = [
177
170
  'node_modules',
178
171
  'python_modules',
@@ -186,18 +179,26 @@ const create = async ({ projectName, template, cursorEnabled, context }) => {
186
179
  fs_1.default.writeFileSync(path_1.default.join(rootDir, '.gitignore'), gitignoreContent);
187
180
  context.log('gitignore-created', (message) => message.tag('success').append('File').append('.gitignore', 'cyan').append('has been created.'));
188
181
  }
189
- if (cursorEnabled) {
182
+ // Skip cursor rules for plugin template
183
+ if (!isPluginTemplate && cursorEnabled) {
190
184
  await (0, pull_rules_1.pullRules)({ force: true, rootDir }, context);
191
185
  }
192
186
  if (template) {
193
187
  await (0, setup_template_1.setupTemplate)(template, rootDir, context);
194
188
  }
195
- const packageManager = await installNodeDependencies(rootDir, context);
196
- if (template === 'python') {
197
- await (0, install_1.pythonInstall)({ baseDir: rootDir });
189
+ let packageManager;
190
+ if (!isPluginTemplate) {
191
+ packageManager = await installNodeDependencies(rootDir, context);
192
+ if (template === 'python') {
193
+ await (0, install_1.pythonInstall)({ baseDir: rootDir });
194
+ }
195
+ await (0, generate_types_1.generateTypes)(rootDir);
196
+ }
197
+ else {
198
+ // For plugin template, just detect the package manager
199
+ packageManager = await preparePackageManager(rootDir, context);
198
200
  }
199
- await (0, generate_types_1.generateTypes)(rootDir);
200
- await wrapUp(context, packageManager);
201
+ await wrapUp(context, packageManager, isPluginTemplate);
201
202
  return;
202
203
  };
203
204
  exports.create = create;
@@ -10,6 +10,7 @@ const index_1 = require("./index");
10
10
  const choices = {
11
11
  nodejs: 'Base (TypeScript)',
12
12
  python: 'Base (Python)',
13
+ plugin: 'Plugin (TypeScript)',
13
14
  };
14
15
  const createInteractive = async (args, context) => {
15
16
  context.log('welcome', (message) => message.append('\n🚀 ' + colors_1.default.bold('Welcome to Motia Project Creator!')));
@@ -1,3 +1,4 @@
1
1
  import type { CliContext } from '../../cloud/config-utils';
2
2
  export type Generator = (rootDir: string, context: CliContext) => Promise<void>;
3
3
  export declare const generateTemplateSteps: (templateFolder: string) => Generator;
4
+ export declare const generatePluginTemplate: (templateFolder: string) => Generator;
@@ -33,10 +33,28 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.generateTemplateSteps = void 0;
36
+ exports.generatePluginTemplate = exports.generateTemplateSteps = void 0;
37
37
  const fs_1 = require("fs");
38
38
  const glob_1 = require("glob");
39
39
  const path = __importStar(require("path"));
40
+ const replaceTemplateVariables = (content, projectName) => {
41
+ const replacements = {
42
+ '{{PROJECT_NAME}}': projectName,
43
+ '{{PLUGIN_NAME}}': toPascalCase(projectName),
44
+ '{{CSS_FILE_NAME}}': projectName.replace(/^@[^/]+\//, ''),
45
+ };
46
+ return Object.entries(replacements).reduce((result, [key, value]) => {
47
+ return result.replace(new RegExp(key, 'g'), value);
48
+ }, content);
49
+ };
50
+ const toPascalCase = (str) => {
51
+ // Remove @ and scope if present
52
+ const name = str.replace(/^@[^/]+\//, '');
53
+ return name
54
+ .split(/[-_]/)
55
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
56
+ .join('');
57
+ };
40
58
  const generateTemplateSteps = (templateFolder) => {
41
59
  return async (rootDir, context) => {
42
60
  const templatePath = path.join(__dirname, templateFolder);
@@ -46,13 +64,7 @@ const generateTemplateSteps = (templateFolder) => {
46
64
  const filePath = path.join(templatePath, fileName);
47
65
  const targetFilePath = path.join(rootDir, fileName);
48
66
  const targetDir = path.dirname(targetFilePath);
49
- try {
50
- // Check if it's a directory in the template
51
- (0, fs_1.statSync)(targetDir);
52
- }
53
- catch {
54
- (0, fs_1.mkdirSync)(targetDir, { recursive: true });
55
- }
67
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
56
68
  if ((0, fs_1.statSync)(filePath).isDirectory()) {
57
69
  const folderPath = filePath.replace(templatePath, '');
58
70
  (0, fs_1.mkdirSync)(path.join(rootDir, folderPath), { recursive: true });
@@ -62,19 +74,37 @@ const generateTemplateSteps = (templateFolder) => {
62
74
  const isWorkbenchConfig = fileName.match('motia-workbench.json');
63
75
  const generateFilePath = path.join(rootDir, sanitizedFileName);
64
76
  let content = await fs_1.promises.readFile(filePath, 'utf8');
65
- // Make sure statSync doesn't break the execution if the file doesn't exist
66
- try {
67
- if (isWorkbenchConfig && (0, fs_1.statSync)(generateFilePath).isFile()) {
68
- const existingWorkbenchConfig = await fs_1.promises.readFile(generateFilePath, 'utf8');
69
- const workbenchContent = JSON.parse(content);
70
- content = JSON.stringify([...JSON.parse(existingWorkbenchConfig), ...workbenchContent], null, 2);
71
- context.log('workbench-config-updated', (message) => message.tag('success').append('Workbench config').append('has been updated.'));
77
+ if (isWorkbenchConfig) {
78
+ try {
79
+ // Use file descriptor to avoid TOCTOU vulnerability
80
+ let fd = null;
81
+ try {
82
+ // Try to open existing file for reading
83
+ fd = await fs_1.promises.open(generateFilePath, fs_1.constants.O_RDONLY);
84
+ const existingWorkbenchConfig = await fd.readFile('utf8');
85
+ const workbenchContent = JSON.parse(content);
86
+ content = JSON.stringify([...JSON.parse(existingWorkbenchConfig), ...workbenchContent], null, 2);
87
+ context.log('workbench-config-updated', (message) => message.tag('success').append('Workbench config').append('has been updated.'));
88
+ }
89
+ finally {
90
+ if (fd)
91
+ await fd.close();
92
+ }
93
+ }
94
+ catch {
95
+ void 0;
72
96
  }
73
97
  }
74
- catch {
75
- void 0;
98
+ // Use file descriptor for atomic write operation
99
+ let fd = null;
100
+ try {
101
+ fd = await fs_1.promises.open(generateFilePath, fs_1.constants.O_CREAT | fs_1.constants.O_WRONLY | fs_1.constants.O_TRUNC, 0o644);
102
+ await fd.writeFile(content, 'utf8');
103
+ }
104
+ finally {
105
+ if (fd)
106
+ await fd.close();
76
107
  }
77
- await fs_1.promises.writeFile(generateFilePath, content, 'utf8');
78
108
  context.log(sanitizedFileName, (message) => {
79
109
  message.tag('success').append('File').append(sanitizedFileName, 'cyan').append('has been created.');
80
110
  });
@@ -86,3 +116,44 @@ const generateTemplateSteps = (templateFolder) => {
86
116
  };
87
117
  };
88
118
  exports.generateTemplateSteps = generateTemplateSteps;
119
+ const generatePluginTemplate = (templateFolder) => {
120
+ return async (rootDir, context) => {
121
+ const templatePath = path.join(__dirname, templateFolder);
122
+ const files = (0, glob_1.globSync)('**/*', { absolute: false, cwd: templatePath, dot: true });
123
+ const projectName = path.basename(rootDir);
124
+ try {
125
+ for (const fileName of files) {
126
+ const filePath = path.join(templatePath, fileName);
127
+ const targetFilePath = path.join(rootDir, fileName);
128
+ const targetDir = path.dirname(targetFilePath);
129
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
130
+ if ((0, fs_1.statSync)(filePath).isDirectory()) {
131
+ const folderPath = filePath.replace(templatePath, '');
132
+ (0, fs_1.mkdirSync)(path.join(rootDir, folderPath), { recursive: true });
133
+ continue;
134
+ }
135
+ const sanitizedFileName = fileName.replace('.txt', '');
136
+ const generateFilePath = path.join(rootDir, sanitizedFileName);
137
+ let content = await fs_1.promises.readFile(filePath, 'utf8');
138
+ content = replaceTemplateVariables(content, projectName);
139
+ // Use file descriptor for atomic write operation
140
+ let fd = null;
141
+ try {
142
+ fd = await fs_1.promises.open(generateFilePath, fs_1.constants.O_CREAT | fs_1.constants.O_WRONLY | fs_1.constants.O_TRUNC, 0o644);
143
+ await fd.writeFile(content, 'utf8');
144
+ }
145
+ finally {
146
+ if (fd)
147
+ await fd.close();
148
+ }
149
+ context.log(sanitizedFileName, (message) => {
150
+ message.tag('success').append('File').append(sanitizedFileName, 'cyan').append('has been created.');
151
+ });
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.error('Error generating template files:', error);
156
+ }
157
+ };
158
+ };
159
+ exports.generatePluginTemplate = generatePluginTemplate;
@@ -1,10 +1,31 @@
1
- import { promises as fs, mkdirSync, statSync } from 'fs'
1
+ import { constants, promises as fs, mkdirSync, statSync } from 'fs'
2
2
  import { globSync } from 'glob'
3
3
  import * as path from 'path'
4
4
  import type { CliContext } from '../../cloud/config-utils'
5
5
 
6
6
  export type Generator = (rootDir: string, context: CliContext) => Promise<void>
7
7
 
8
+ const replaceTemplateVariables = (content: string, projectName: string): string => {
9
+ const replacements: Record<string, string> = {
10
+ '{{PROJECT_NAME}}': projectName,
11
+ '{{PLUGIN_NAME}}': toPascalCase(projectName),
12
+ '{{CSS_FILE_NAME}}': projectName.replace(/^@[^/]+\//, ''),
13
+ }
14
+
15
+ return Object.entries(replacements).reduce((result, [key, value]) => {
16
+ return result.replace(new RegExp(key, 'g'), value)
17
+ }, content)
18
+ }
19
+
20
+ const toPascalCase = (str: string): string => {
21
+ // Remove @ and scope if present
22
+ const name = str.replace(/^@[^/]+\//, '')
23
+ return name
24
+ .split(/[-_]/)
25
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
26
+ .join('')
27
+ }
28
+
8
29
  export const generateTemplateSteps = (templateFolder: string): Generator => {
9
30
  return async (rootDir: string, context: CliContext): Promise<void> => {
10
31
  const templatePath = path.join(__dirname, templateFolder)
@@ -16,12 +37,7 @@ export const generateTemplateSteps = (templateFolder: string): Generator => {
16
37
  const targetFilePath = path.join(rootDir, fileName)
17
38
  const targetDir = path.dirname(targetFilePath)
18
39
 
19
- try {
20
- // Check if it's a directory in the template
21
- statSync(targetDir)
22
- } catch {
23
- mkdirSync(targetDir, { recursive: true })
24
- }
40
+ mkdirSync(targetDir, { recursive: true })
25
41
 
26
42
  if (statSync(filePath).isDirectory()) {
27
43
  const folderPath = filePath.replace(templatePath, '')
@@ -34,23 +50,81 @@ export const generateTemplateSteps = (templateFolder: string): Generator => {
34
50
  const generateFilePath = path.join(rootDir, sanitizedFileName)
35
51
  let content = await fs.readFile(filePath, 'utf8')
36
52
 
37
- // Make sure statSync doesn't break the execution if the file doesn't exist
38
- try {
39
- if (isWorkbenchConfig && statSync(generateFilePath).isFile()) {
40
- const existingWorkbenchConfig = await fs.readFile(generateFilePath, 'utf8')
41
- const workbenchContent = JSON.parse(content)
53
+ if (isWorkbenchConfig) {
54
+ try {
55
+ // Use file descriptor to avoid TOCTOU vulnerability
56
+ let fd: fs.FileHandle | null = null
57
+ try {
58
+ // Try to open existing file for reading
59
+ fd = await fs.open(generateFilePath, constants.O_RDONLY)
60
+ const existingWorkbenchConfig = await fd.readFile('utf8')
61
+ const workbenchContent = JSON.parse(content)
42
62
 
43
- content = JSON.stringify([...JSON.parse(existingWorkbenchConfig), ...workbenchContent], null, 2)
63
+ content = JSON.stringify([...JSON.parse(existingWorkbenchConfig), ...workbenchContent], null, 2)
44
64
 
45
- context.log('workbench-config-updated', (message) =>
46
- message.tag('success').append('Workbench config').append('has been updated.'),
47
- )
65
+ context.log('workbench-config-updated', (message) =>
66
+ message.tag('success').append('Workbench config').append('has been updated.'),
67
+ )
68
+ } finally {
69
+ if (fd) await fd.close()
70
+ }
71
+ } catch {
72
+ void 0
48
73
  }
49
- } catch {
50
- void 0
51
74
  }
52
75
 
53
- await fs.writeFile(generateFilePath, content, 'utf8')
76
+ // Use file descriptor for atomic write operation
77
+ let fd: fs.FileHandle | null = null
78
+ try {
79
+ fd = await fs.open(generateFilePath, constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC, 0o644)
80
+ await fd.writeFile(content, 'utf8')
81
+ } finally {
82
+ if (fd) await fd.close()
83
+ }
84
+ context.log(sanitizedFileName, (message) => {
85
+ message.tag('success').append('File').append(sanitizedFileName, 'cyan').append('has been created.')
86
+ })
87
+ }
88
+ } catch (error) {
89
+ console.error('Error generating template files:', error)
90
+ }
91
+ }
92
+ }
93
+
94
+ export const generatePluginTemplate = (templateFolder: string): Generator => {
95
+ return async (rootDir: string, context: CliContext): Promise<void> => {
96
+ const templatePath = path.join(__dirname, templateFolder)
97
+ const files = globSync('**/*', { absolute: false, cwd: templatePath, dot: true })
98
+ const projectName = path.basename(rootDir)
99
+
100
+ try {
101
+ for (const fileName of files) {
102
+ const filePath = path.join(templatePath, fileName)
103
+ const targetFilePath = path.join(rootDir, fileName)
104
+ const targetDir = path.dirname(targetFilePath)
105
+
106
+ mkdirSync(targetDir, { recursive: true })
107
+
108
+ if (statSync(filePath).isDirectory()) {
109
+ const folderPath = filePath.replace(templatePath, '')
110
+ mkdirSync(path.join(rootDir, folderPath), { recursive: true })
111
+ continue
112
+ }
113
+
114
+ const sanitizedFileName = fileName.replace('.txt', '')
115
+ const generateFilePath = path.join(rootDir, sanitizedFileName)
116
+ let content = await fs.readFile(filePath, 'utf8')
117
+
118
+ content = replaceTemplateVariables(content, projectName)
119
+
120
+ // Use file descriptor for atomic write operation
121
+ let fd: fs.FileHandle | null = null
122
+ try {
123
+ fd = await fs.open(generateFilePath, constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC, 0o644)
124
+ await fd.writeFile(content, 'utf8')
125
+ } finally {
126
+ if (fd) await fd.close()
127
+ }
54
128
  context.log(sanitizedFileName, (message) => {
55
129
  message.tag('success').append('File').append(sanitizedFileName, 'cyan').append('has been created.')
56
130
  })
@@ -5,4 +5,5 @@ const generate_1 = require("./generate");
5
5
  exports.templates = {
6
6
  nodejs: (0, generate_1.generateTemplateSteps)('nodejs'),
7
7
  python: (0, generate_1.generateTemplateSteps)('python'),
8
+ plugin: (0, generate_1.generatePluginTemplate)('plugin'),
8
9
  };
@@ -1,6 +1,7 @@
1
- import { type Generator, generateTemplateSteps } from './generate'
1
+ import { type Generator, generatePluginTemplate, generateTemplateSteps } from './generate'
2
2
 
3
3
  export const templates: Record<string, Generator> = {
4
4
  nodejs: generateTemplateSteps('nodejs'),
5
5
  python: generateTemplateSteps('python'),
6
+ plugin: generatePluginTemplate('plugin'),
6
7
  }
@@ -653,7 +653,7 @@ export const steps: TutorialStep[] = [
653
653
  <br />
654
654
  <br />
655
655
  We recommend you give our{' '}
656
- <a href="https://www.motia.dev/docs/concepts" target="_blank">
656
+ <a href="https://www.motia.dev/docs/concepts/overview" target="_blank">
657
657
  core concepts
658
658
  </a>{' '}
659
659
  a read if you wish to learn further about Motia's fundamentals.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+
@@ -0,0 +1,64 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A minimal plugin demonstrating the Motia plugin system.
4
+
5
+ ## Overview
6
+
7
+ This plugin serves as a reference implementation showing how to create custom workbench plugins for Motia. It demonstrates:
8
+
9
+ - Basic plugin structure and configuration
10
+ - Creating custom workbench tabs
11
+ - Using Motia's UI component library
12
+ - Building with Vite and TypeScript
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm install
18
+ ```
19
+
20
+ ## Development
21
+
22
+ ```bash
23
+ # Build the plugin
24
+ pnpm run build
25
+
26
+ # Watch mode for development
27
+ pnpm run dev
28
+
29
+ # Clean build artifacts
30
+ pnpm run clean
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ To use this plugin in your Motia project, import it in your `motia.config.ts`:
36
+
37
+ ```typescript
38
+ import examplePlugin from '{{PROJECT_NAME}}/plugin'
39
+
40
+ export default {
41
+ plugins: [examplePlugin],
42
+ }
43
+ ```
44
+
45
+ ## Structure
46
+
47
+ ```
48
+ {{PROJECT_NAME}}/
49
+ ├── src/
50
+ │ ├── components/
51
+ │ │ └── example-page.tsx # Main UI component
52
+ │ ├── index.ts # Package entry point
53
+ │ ├── plugin.ts # Plugin definition
54
+ │ └── styles.css # Tailwind styles
55
+ ├── package.json
56
+ ├── tsconfig.json
57
+ ├── vite.config.ts
58
+ └── README.md
59
+ ```
60
+
61
+ ## Learn More
62
+
63
+ For detailed documentation on creating plugins, see the [Motia Plugins Guide](https://motia.dev/docs/development-guide/plugins).
64
+