motia 0.6.0-beta.123 → 0.6.2-beta.125

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 (95) hide show
  1. package/README.md +34 -23
  2. package/dist/cjs/cli.js +59 -11
  3. package/dist/cjs/cloud/build/builders/node/index.js +4 -1
  4. package/dist/cjs/cloud/build/builders/python/index.d.ts +11 -3
  5. package/dist/cjs/cloud/build/builders/python/index.js +215 -78
  6. package/dist/cjs/cloud/build/builders/python/uv-packager.d.ts +13 -0
  7. package/dist/cjs/cloud/build/builders/python/uv-packager.js +77 -0
  8. package/dist/cjs/cloud/endpoints.d.ts +2 -0
  9. package/dist/cjs/cloud/endpoints.js +102 -0
  10. package/dist/cjs/cloud/new-deployment/build.js +12 -4
  11. package/dist/cjs/cloud/new-deployment/cloud-api/create-deployment.js +6 -1
  12. package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
  13. package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.js +278 -0
  14. package/dist/cjs/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
  15. package/dist/cjs/cloud/new-deployment/streams/deployment-stream.js +108 -0
  16. package/dist/cjs/create/index.js +0 -1
  17. package/dist/cjs/create/templates/python/motia-workbench.json +1 -1
  18. package/dist/cjs/create/templates/python/steps/api_step.py.txt +1 -1
  19. package/dist/cjs/create/templates/python/tutorial.tsx.txt +1 -1
  20. package/dist/cjs/create/templates/typescript/motia-workbench.json +1 -1
  21. package/dist/cjs/create/templates/typescript/steps/api.step.ts.txt +2 -2
  22. package/dist/cjs/create/templates/typescript/tutorial.tsx.txt +1 -1
  23. package/dist/cjs/cursor-rules/index.d.ts +8 -0
  24. package/dist/cjs/cursor-rules/index.js +269 -0
  25. package/dist/cjs/dev.js +3 -1
  26. package/dist/cjs/generate-locked-data.js +13 -7
  27. package/dist/cjs/install.js +5 -0
  28. package/dist/cjs/utils/ensure-uv.d.ts +1 -0
  29. package/dist/cjs/utils/ensure-uv.js +81 -0
  30. package/dist/cjs/utils/errors/build.error.d.ts +9 -0
  31. package/dist/cjs/utils/errors/build.error.js +20 -0
  32. package/dist/cjs/utils/errors/compilation.error.d.ts +4 -0
  33. package/dist/cjs/utils/errors/compilation.error.js +11 -0
  34. package/dist/esm/cli.js +59 -11
  35. package/dist/esm/cloud/build/builders/node/index.js +4 -1
  36. package/dist/esm/cloud/build/builders/python/index.d.ts +11 -3
  37. package/dist/esm/cloud/build/builders/python/index.js +215 -78
  38. package/dist/esm/cloud/build/builders/python/uv-packager.d.ts +13 -0
  39. package/dist/esm/cloud/build/builders/python/uv-packager.js +70 -0
  40. package/dist/esm/cloud/endpoints.d.ts +2 -0
  41. package/dist/esm/cloud/endpoints.js +98 -0
  42. package/dist/esm/cloud/new-deployment/build.js +12 -4
  43. package/dist/esm/cloud/new-deployment/cloud-api/create-deployment.js +6 -1
  44. package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
  45. package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.js +274 -0
  46. package/dist/esm/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
  47. package/dist/esm/cloud/new-deployment/streams/deployment-stream.js +103 -0
  48. package/dist/esm/create/index.js +0 -1
  49. package/dist/esm/create/templates/python/motia-workbench.json +1 -1
  50. package/dist/esm/create/templates/python/steps/api_step.py.txt +1 -1
  51. package/dist/esm/create/templates/python/tutorial.tsx.txt +1 -1
  52. package/dist/esm/create/templates/typescript/motia-workbench.json +1 -1
  53. package/dist/esm/create/templates/typescript/steps/api.step.ts.txt +2 -2
  54. package/dist/esm/create/templates/typescript/tutorial.tsx.txt +1 -1
  55. package/dist/esm/cursor-rules/index.d.ts +8 -0
  56. package/dist/esm/cursor-rules/index.js +263 -0
  57. package/dist/esm/dev.js +3 -1
  58. package/dist/esm/generate-locked-data.js +13 -7
  59. package/dist/esm/install.js +5 -0
  60. package/dist/esm/utils/ensure-uv.d.ts +1 -0
  61. package/dist/esm/utils/ensure-uv.js +77 -0
  62. package/dist/esm/utils/errors/build.error.d.ts +9 -0
  63. package/dist/esm/utils/errors/build.error.js +16 -0
  64. package/dist/esm/utils/errors/compilation.error.d.ts +4 -0
  65. package/dist/esm/utils/errors/compilation.error.js +7 -0
  66. package/dist/types/cloud/build/builders/python/index.d.ts +11 -3
  67. package/dist/types/cloud/build/builders/python/uv-packager.d.ts +13 -0
  68. package/dist/types/cloud/endpoints.d.ts +2 -0
  69. package/dist/types/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
  70. package/dist/types/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
  71. package/dist/types/cursor-rules/index.d.ts +8 -0
  72. package/dist/types/utils/ensure-uv.d.ts +1 -0
  73. package/dist/types/utils/errors/build.error.d.ts +9 -0
  74. package/dist/types/utils/errors/compilation.error.d.ts +4 -0
  75. package/package.json +4 -4
  76. package/dist/cjs/cloud/build/builders/python/add-package-to-archive.d.ts +0 -2
  77. package/dist/cjs/cloud/build/builders/python/add-package-to-archive.js +0 -56
  78. package/dist/cjs/cloud/build/builders/python/python-builder.py +0 -226
  79. package/dist/dot-files/.cursor/rules/api-steps.mdc +0 -169
  80. package/dist/dot-files/.cursor/rules/architecture.mdc +0 -189
  81. package/dist/dot-files/.cursor/rules/cron-steps.mdc +0 -257
  82. package/dist/dot-files/.cursor/rules/event-steps.mdc +0 -366
  83. package/dist/dot-files/.cursor/rules/instructions.mdc +0 -15
  84. package/dist/dot-files/.cursor/rules/noop-steps.mdc +0 -57
  85. package/dist/dot-files/.cursor/rules/state-management.mdc +0 -325
  86. package/dist/dot-files/.cursor/rules/steps.mdc +0 -317
  87. package/dist/dot-files/.cursor/rules/testing.mdc +0 -329
  88. package/dist/dot-files/.cursor/rules/typescript.mdc +0 -409
  89. package/dist/dot-files/.cursor/rules/ui-steps.mdc +0 -90
  90. package/dist/dot-files/CLAUDE.md +0 -827
  91. package/dist/dot-files/README.md +0 -58
  92. package/dist/esm/cloud/build/builders/python/add-package-to-archive.d.ts +0 -2
  93. package/dist/esm/cloud/build/builders/python/add-package-to-archive.js +0 -49
  94. package/dist/esm/cloud/build/builders/python/python-builder.py +0 -226
  95. package/dist/types/cloud/build/builders/python/add-package-to-archive.d.ts +0 -2
package/README.md CHANGED
@@ -98,24 +98,24 @@ Backend teams juggle **fragmented runtimes** across APIs, background queues, and
98
98
 
99
99
  Motia unifies your entire backend into a **unified state**. APIs, background jobs, and AI agents become interconnected Steps with shared state and integrated observability.
100
100
 
101
- | **Before** | **After (Motia)** |
102
- |---|---|
103
- | Multiple deployment targets | **Single unified deployment** |
104
- | Fragmented observability | **End-to-end tracing** |
105
- | Language dependent | **JavaScript, TypeScript, Python, etc** |
106
- | Context-switching overhead | **Single intuitive model** |
107
- | Complex error handling | **Automatic retries & fault tolerance** |
101
+ | **Before** | **After (Motia)** |
102
+ | --------------------------- | --------------------------------------- |
103
+ | Multiple deployment targets | **Single unified deployment** |
104
+ | Fragmented observability | **End-to-end tracing** |
105
+ | Language dependent | **JavaScript, TypeScript, Python, etc** |
106
+ | Context-switching overhead | **Single intuitive model** |
107
+ | Complex error handling | **Automatic retries & fault tolerance** |
108
108
 
109
109
  ---
110
110
 
111
111
  ## 🔧 Supported Step Types
112
112
 
113
- | Type | Trigger | Use Case |
114
- |---|---|---|
115
- | **`api`** | HTTP Request | Expose REST endpoints |
116
- | **`event`** | Emitted Topics | React to internal or external events |
117
- | **`cron`** | Scheduled Time (cron) | Automate recurring jobs |
118
- | **`noop`** | None | Placeholder for manual/external tasks |
113
+ | Type | Trigger | Use Case |
114
+ | ----------- | --------------------- | ------------------------------------- |
115
+ | **`api`** | HTTP Request | Expose REST endpoints |
116
+ | **`event`** | Emitted Topics | React to internal or external events |
117
+ | **`cron`** | Scheduled Time (cron) | Automate recurring jobs |
118
+ | **`noop`** | None | Placeholder for manual/external tasks |
119
119
 
120
120
  ---
121
121
 
@@ -123,9 +123,9 @@ Motia unifies your entire backend into a **unified state**. APIs, background job
123
123
 
124
124
  Motia's architecture is built around a single, powerful primitive: the **Step**. A Step is not just a trigger; it's a powerful container for your business logic. You can write anything from a simple database query to a complex AI agent interaction inside a single step. Instead of managing separate services for APIs, background workers, and scheduled tasks, you simply define how your steps are triggered.
125
125
 
126
- - **Need a public API?** Create an `api` step. This defines a route and handler for HTTP requests. You can build a complete REST or GraphQL API just with these steps.
127
- - **Need a background job or queue?** Have your `api` step `emit` an event. An `event` step subscribed to that event's topic will pick up the job and process it asynchronously. This is how you handle anything that shouldn't block the main request thread, from sending emails to complex data processing.
128
- - **Need to run a task on a schedule?** Use a `cron` step. It will trigger automatically based on the schedule you define.
126
+ - **Need a public API?** Create an `api` step. This defines a route and handler for HTTP requests. You can build a complete REST or GraphQL API just with these steps.
127
+ - **Need a background job or queue?** Have your `api` step `emit` an event. An `event` step subscribed to that event's topic will pick up the job and process it asynchronously. This is how you handle anything that shouldn't block the main request thread, from sending emails to complex data processing.
128
+ - **Need to run a task on a schedule?** Use a `cron` step. It will trigger automatically based on the schedule you define.
129
129
 
130
130
  This model means you no longer need to glue together separate frameworks and tools. A single Motia application can replace a stack that might otherwise include **Nest.js** (for APIs), **Temporal** (for workflows), and **Celery/BullMQ** (for background jobs). It's all just steps and events.
131
131
 
@@ -173,9 +173,9 @@ Motia unifies your entire backend into a **unified state**. APIs, background job
173
173
 
174
174
  Motia's architecture is built around a single, powerful primitive: the **Step**. A Step is not just a trigger; it's a powerful container for your business logic. You can write anything from a simple database query to a complex AI agent interaction inside a single step. Instead of managing separate services for APIs, background workers, and scheduled tasks, you simply define how your steps are triggered.
175
175
 
176
- - **Need a public API?** Create an `api` step. This defines a route and handler for HTTP requests. You can build a complete REST or GraphQL API just with these steps.
177
- - **Need a background job or queue?** Have your `api` step `emit` an event. An `event` step subscribed to that event's topic will pick up the job and process it asynchronously. This is how you handle anything that shouldn't block the main request thread, from sending emails to complex data processing.
178
- - **Need to run a task on a schedule?** Use a `cron` step. It will trigger automatically based on the schedule you define.
176
+ - **Need a public API?** Create an `api` step. This defines a route and handler for HTTP requests. You can build a complete REST or GraphQL API just with these steps.
177
+ - **Need a background job or queue?** Have your `api` step `emit` an event. An `event` step subscribed to that event's topic will pick up the job and process it asynchronously. This is how you handle anything that shouldn't block the main request thread, from sending emails to complex data processing.
178
+ - **Need to run a task on a schedule?** Use a `cron` step. It will trigger automatically based on the schedule you define.
179
179
 
180
180
  This model means you no longer need to glue together separate frameworks and tools. A single Motia application can replace a stack that might otherwise include **Nest.js** (for APIs), **Temporal** (for workflows), and **Celery/BullMQ** (for background jobs). It's all just steps and events.
181
181
 
@@ -184,24 +184,31 @@ This model means you no longer need to glue together separate frameworks and too
184
184
  The **Step** is Motia's core primitive. The following concepts are deeply integrated with Steps to help you build powerful, complex, and scalable backends:
185
185
 
186
186
  ### 🔑 Steps & Step Types
187
+
187
188
  Understand the three ways Steps are triggered:
189
+
188
190
  - **HTTP (`api`)** – Build REST/GraphQL endpoints with zero boilerplate.
189
191
  - **Events (`event`)** – React to internal or external events emitted by other steps.
190
192
  - **Cron (`cron`)** – Schedule recurring jobs with a familiar cron syntax.
191
193
 
192
194
  ### 📣 Emit & Subscribe (Event-Driven Workflows)
195
+
193
196
  Steps talk to each other by **emitting** and **subscribing** to topics. This decouples producers from consumers and lets you compose complex workflows with simple, declarative code.
194
197
 
195
198
  ### 🏪 State Management
199
+
196
200
  All steps share a unified key-value state store. Every `get`, `set`, and `delete` is automatically traced so you always know when and where your data changed.
197
201
 
198
202
  ### 📊 Structured Logging
203
+
199
204
  Motia provides structured, JSON logs correlated with trace IDs and step names. Search and filter your logs without regex gymnastics.
200
205
 
201
206
  ### 📡 Streams: Real-time Messaging
207
+
202
208
  Push live updates from long-running or asynchronous workflows to clients without polling. Perfect for dashboards, progress indicators, and interactive AI agents.
203
209
 
204
210
  ### 👁️ End-to-End Observability with Traces
211
+
205
212
  Every execution generates a full trace, capturing step timelines, state operations, emits, stream calls, and logs. Visualise everything in the Workbench's Traces UI and debug faster.
206
213
 
207
214
  ---
@@ -211,7 +218,9 @@ Every execution generates a full trace, capturing step timelines, state operatio
211
218
  Motia comes with a range of [powerful CLI commands](https://www.motia.dev/docs/concepts/cli) to help you manage your projects:
212
219
 
213
220
  ### `npx motia create [options]`
221
+
214
222
  Create a new Motia project in a fresh directory or the current one.
223
+
215
224
  ```sh
216
225
  npx motia create [options]
217
226
 
@@ -238,12 +247,12 @@ bun run dev [options]
238
247
  # options:
239
248
  # -p, --port <port> The port to run the server on (default: 3000)
240
249
  # -H, --host [host] The host address for the server (default: localhost)
241
- # -v, --verbose Enable verbose logging
242
250
  # -d, --debug Enable debug logging
243
251
  # -m, --mermaid Enable mermaid diagram generation
244
252
  ```
245
253
 
246
254
  ### `npx motia build`
255
+
247
256
  Compiles all your steps (Node.js, Python and more) and builds a lock file based on your current project setup, which is then used by the Motia ecosystem.
248
257
 
249
258
  ```bash
@@ -256,12 +265,12 @@ motia build
256
265
 
257
266
  Write steps in your preferred language:
258
267
 
259
- | Language | Status | Example |
260
- | -------------- | ------------- | ----------------- |
268
+ | Language | Status | Example |
269
+ | -------------- | -------------- | ----------------- |
261
270
  | **JavaScript** | ✅ Stable | `handler.step.js` |
262
271
  | **TypeScript** | ✅ Stable | `handler.step.ts` |
263
272
  | **Python** | ✅ Stable | `handler.step.py` |
264
- | **Ruby** | 🚧 Beta | `handler.step.rb` |
273
+ | **Ruby** | 🚧 Beta | `handler.step.rb` |
265
274
  | **Go** | 🔄 Coming Soon | `handler.step.go` |
266
275
  | **Rust** | 🔄 Coming Soon | `handler.step.rs` |
267
276
 
@@ -276,6 +285,7 @@ motia <command> --help
276
285
  ```
277
286
 
278
287
  ### 💬 **Get Help**
288
+
279
289
  - **📋 Questions**: Use our [Discord community](https://discord.gg/motia)
280
290
  - **🐛 Bug Reports**: [GitHub Issues](https://github.com/MotiaDev/motia/issues)
281
291
  - **📖 Documentation**: [Official Docs](https://motia.dev/docs)
@@ -284,6 +294,7 @@ motia <command> --help
284
294
  ### 🤝 **Contributing**
285
295
 
286
296
  We welcome contributions! Whether it's:
297
+
287
298
  - 🐛 Bug fixes and improvements
288
299
  - ✨ New features and step types
289
300
  - 📚 Documentation and examples
package/dist/cjs/cli.js CHANGED
@@ -28,27 +28,36 @@ commander_1.program
28
28
  .command('create')
29
29
  .description('Create a new motia project')
30
30
  .option('-n, --name <project name>', 'The name for your project, used to create a directory, use ./ or . to create it under the existing directory')
31
- .option('-t, --template <template name>', 'The motia template name to use for your project')
31
+ .option('-t, --template <template name>', 'The motia template name to use for your project', 'typescript')
32
32
  .option('-c, --cursor', 'Copy .cursor folder from template')
33
33
  .option('-i, --interactive', 'Use interactive prompts to create project')
34
34
  .option('-y, --skip-confirmation', 'Skip confirmation prompt')
35
- .option('-d, --skip-tutorial', 'Skip the motia tutorial', false)
35
+ .option('-d, --skip-tutorial [value]', 'Skip the motia tutorial (true/false)')
36
36
  .action((0, config_utils_1.handler)(async (arg, context) => {
37
37
  if (arg.name || arg.template || arg.cursor) {
38
38
  const { create } = require('./create');
39
- const disableTutorial = await inquirer_1.default.prompt({
40
- type: 'confirm',
41
- name: 'disableTutorial',
42
- message: 'Do you wish to disable the motia tutorial?',
43
- default: arg.skipTutorial,
44
- when: () => arg.skipTutorial === false,
45
- });
39
+ // Handle skip-tutorial option: 'true', 'false', or prompt user
40
+ const skipTutorialValue = await (async () => {
41
+ const skipValue = String(arg.skipTutorial).toLowerCase();
42
+ if (skipValue === 'true')
43
+ return true;
44
+ if (skipValue === 'false')
45
+ return false;
46
+ // Prompt user when not explicitly set
47
+ const { disableTutorial } = await inquirer_1.default.prompt({
48
+ type: 'confirm',
49
+ name: 'disableTutorial',
50
+ message: 'Do you wish to disable the motia tutorial?',
51
+ default: false,
52
+ });
53
+ return disableTutorial;
54
+ })();
46
55
  await create({
47
56
  projectName: arg.name ?? '.',
48
- template: arg.template ?? 'default',
57
+ template: arg.template,
49
58
  cursorEnabled: arg.cursor,
50
59
  context,
51
- skipTutorialTemplates: disableTutorial.disableTutorial,
60
+ skipTutorialTemplates: skipTutorialValue,
52
61
  });
53
62
  }
54
63
  else {
@@ -185,5 +194,44 @@ docker
185
194
  await build(arg.projectName);
186
195
  process.exit(0);
187
196
  });
197
+ const rules = commander_1.program
198
+ .command('rules')
199
+ .description('Manage Motia AI development guides (AGENTS.md, CLAUDE.md) and IDE-specific rules');
200
+ rules
201
+ .command('pull')
202
+ .description('Install essential AI development guides (AGENTS.md, CLAUDE.md) and optional Cursor IDE rules')
203
+ .option('-f, --force', 'Overwrite existing files')
204
+ .action(async (options) => {
205
+ const { handleAIGuides } = require('./cursor-rules');
206
+ await handleAIGuides({ force: options.force });
207
+ });
208
+ rules
209
+ .command('list')
210
+ .description('List available AI development guides and IDE rules')
211
+ .action(async () => {
212
+ const { handleAIGuides } = require('./cursor-rules');
213
+ await handleAIGuides({ list: true });
214
+ });
215
+ rules
216
+ .command('show <rule-name>')
217
+ .description('Show content of a specific AI guide or IDE rule')
218
+ .action(async (ruleName) => {
219
+ const { handleAIGuides } = require('./cursor-rules');
220
+ await handleAIGuides({ show: ruleName });
221
+ });
222
+ rules
223
+ .command('remove')
224
+ .description('Remove AI development guides and IDE rules from your project')
225
+ .action(async () => {
226
+ const { handleAIGuides } = require('./cursor-rules');
227
+ await handleAIGuides({ remove: true });
228
+ });
229
+ rules
230
+ .command('version')
231
+ .description('Show AI development guides version')
232
+ .action(async () => {
233
+ const { handleAIGuides } = require('./cursor-rules');
234
+ await handleAIGuides({ version: true });
235
+ });
188
236
  commander_1.program.version(version_1.version, '-V, --version', 'Output the current version');
189
237
  commander_1.program.parse(process.argv);
@@ -68,7 +68,10 @@ class NodeBuilder {
68
68
  async buildApiSteps(steps) {
69
69
  const relativePath = path_1.default.relative(constants_1.distDir, this.builder.projectDir);
70
70
  const getStepPath = (step) => {
71
- return step.filePath.replace(this.builder.projectDir, relativePath).replace(/(.*)\.(ts|js)$/, '$1.js');
71
+ return step.filePath
72
+ .replace(this.builder.projectDir, relativePath)
73
+ .replace(/(.*)\.(ts|js)$/, '$1.js')
74
+ .replace(/\\/g, '/');
72
75
  };
73
76
  const file = fs_1.default
74
77
  .readFileSync(path_1.default.join(__dirname, 'router-template.ts'), 'utf-8')
@@ -4,9 +4,17 @@ import { BuildListener } from '../../../new-deployment/listeners/listener.types'
4
4
  export declare class PythonBuilder implements StepBuilder {
5
5
  private readonly builder;
6
6
  private readonly listener;
7
+ private uvPackager;
7
8
  constructor(builder: Builder, listener: BuildListener);
8
- private buildStep;
9
- build(step: Step): Promise<void>;
10
9
  buildApiSteps(steps: Step<ApiRouteConfig>[]): Promise<RouterBuildResult>;
11
- private getPythonBuilderData;
10
+ build(step: Step): Promise<void>;
11
+ private addStepToArchive;
12
+ private addPackagesToArchive;
13
+ private shouldIgnoreFile;
14
+ private normalizeStepPath;
15
+ private createRouterTemplate;
16
+ private findInternalFiles;
17
+ private resolveModulePaths;
18
+ private getModuleName;
19
+ private waitForDirectoryReady;
12
20
  }
@@ -4,115 +4,252 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PythonBuilder = void 0;
7
- const child_process_1 = require("child_process");
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
10
- const activate_python_env_1 = require("../../../../utils/activate-python-env");
11
9
  const archiver_1 = require("../archiver");
12
10
  const include_static_files_1 = require("../include-static-files");
13
- const add_package_to_archive_1 = require("./add-package-to-archive");
14
11
  const constants_1 = require("../../../new-deployment/constants");
12
+ const uv_packager_1 = require("./uv-packager");
13
+ const activate_python_env_1 = require("../../../../utils/activate-python-env");
15
14
  class PythonBuilder {
16
15
  constructor(builder, listener) {
17
16
  this.builder = builder;
18
17
  this.listener = listener;
19
18
  (0, activate_python_env_1.activatePythonVenv)({ baseDir: this.builder.projectDir });
19
+ this.uvPackager = new uv_packager_1.UvPackager(this.builder.projectDir);
20
20
  }
21
- async buildStep(step, archive) {
22
- const entrypointPath = step.filePath.replace(this.builder.projectDir, '');
23
- const normalizedEntrypointPath = entrypointPath.replace(/[.]step.py$/, '_step.py');
24
- const sitePackagesDir = `${process.env.PYTHON_SITE_PACKAGES}-lambda`;
25
- // Get Python builder response
26
- const { packages } = await this.getPythonBuilderData(step);
27
- // Add main file to archive
28
- if (!fs_1.default.existsSync(step.filePath)) {
29
- throw new Error(`Source file not found: ${step.filePath}`);
21
+ async buildApiSteps(steps) {
22
+ const zipName = 'router-python.zip';
23
+ const archive = new archiver_1.Archiver(path_1.default.join(constants_1.distDir, zipName));
24
+ const tempSitePackages = path_1.default.join(constants_1.distDir, `temp-python-packages-${Date.now()}`);
25
+ try {
26
+ await this.uvPackager.packageDependencies(tempSitePackages);
27
+ // Wait for directory to be ready with proper access checks
28
+ await this.waitForDirectoryReady(tempSitePackages);
29
+ await this.addPackagesToArchive(archive, tempSitePackages);
30
+ for (const step of steps) {
31
+ await this.addStepToArchive(step, archive);
32
+ }
33
+ const routerTemplate = this.createRouterTemplate(steps);
34
+ archive.append(routerTemplate, 'router.py');
35
+ (0, include_static_files_1.includeStaticFiles)(steps, this.builder, archive);
36
+ const size = await archive.finalize();
37
+ return { size, path: zipName };
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Failed to build Python API router: ${error}`);
41
+ }
42
+ finally {
43
+ if (fs_1.default.existsSync(tempSitePackages)) {
44
+ fs_1.default.rmSync(tempSitePackages, { recursive: true, force: true });
45
+ }
30
46
  }
31
- archive.append(fs_1.default.createReadStream(step.filePath), path_1.default.relative(this.builder.projectDir, normalizedEntrypointPath));
32
- await Promise.all(packages.map(async (packageName) => (0, add_package_to_archive_1.addPackageToArchive)(archive, sitePackagesDir, packageName)));
33
- return normalizedEntrypointPath;
34
47
  }
35
48
  async build(step) {
36
49
  const entrypointPath = step.filePath.replace(this.builder.projectDir, '');
37
50
  const bundlePath = path_1.default.join('python', entrypointPath.replace(/(.*)\.py$/, '$1.zip'));
38
- const normalizedEntrypointPath = entrypointPath.replace(/[.]step.py$/, '_step.py');
39
51
  const outfile = path_1.default.join(constants_1.distDir, bundlePath);
52
+ this.builder.registerStep({ entrypointPath, bundlePath, step, type: 'python' });
53
+ this.listener.onBuildStart(step);
40
54
  try {
41
- // Create output directory
42
55
  fs_1.default.mkdirSync(path_1.default.dirname(outfile), { recursive: true });
43
- this.listener.onBuildStart(step);
44
- // Get Python builder response
45
- const { packages } = await this.getPythonBuilderData(step);
46
- const stepArchiver = new archiver_1.Archiver(outfile);
47
- const stepPath = await this.buildStep(step, stepArchiver);
48
- // Add main file to archive
49
- if (!fs_1.default.existsSync(step.filePath)) {
50
- throw new Error(`Source file not found: ${step.filePath}`);
56
+ const archive = new archiver_1.Archiver(outfile);
57
+ const tempSitePackages = path_1.default.join(constants_1.distDir, `temp-python-packages-${Date.now()}`);
58
+ try {
59
+ await this.uvPackager.packageDependencies(tempSitePackages);
60
+ await this.waitForDirectoryReady(tempSitePackages);
61
+ await this.addStepToArchive(step, archive);
62
+ await this.addPackagesToArchive(archive, tempSitePackages);
63
+ (0, include_static_files_1.includeStaticFiles)([step], this.builder, archive);
64
+ const size = await archive.finalize();
65
+ this.listener.onBuildEnd(step, size);
51
66
  }
52
- stepArchiver.append(fs_1.default.createReadStream(step.filePath), path_1.default.relative(this.builder.projectDir, normalizedEntrypointPath));
53
- // Add all imported files to archive
54
- this.listener.onBuildProgress(step, 'Adding imported files to archive...');
55
- const sitePackagesDir = `${process.env.PYTHON_SITE_PACKAGES}-lambda`;
56
- (0, include_static_files_1.includeStaticFiles)([step], this.builder, stepArchiver);
57
- if (packages.length > 0) {
58
- await Promise.all(packages.map(async (packageName) => (0, add_package_to_archive_1.addPackageToArchive)(stepArchiver, sitePackagesDir, packageName)));
59
- this.listener.onBuildProgress(step, `Added ${packages.length} packages to archive`);
67
+ finally {
68
+ if (fs_1.default.existsSync(tempSitePackages)) {
69
+ fs_1.default.rmSync(tempSitePackages, { recursive: true, force: true });
70
+ }
60
71
  }
61
- // Finalize the archive and wait for completion
62
- const size = await stepArchiver.finalize();
63
- this.builder.registerStep({ entrypointPath: stepPath, bundlePath, step, type: 'python' });
64
- this.listener.onBuildEnd(step, size);
65
72
  }
66
73
  catch (err) {
67
74
  this.listener.onBuildError(step, err);
68
75
  throw err;
69
76
  }
70
77
  }
71
- async buildApiSteps(steps) {
72
- const getStepPath = (step) => {
73
- const normalizedEntrypointPath = step.filePath.replace(/[.]step.py$/, '_step.py');
74
- return normalizedEntrypointPath
75
- .replace(`${this.builder.projectDir}/`, '')
76
- .replace(/(.*)\.py$/, '$1')
77
- .replace(/\//g, '.');
78
- };
79
- const zipName = 'router-python.zip';
80
- const archive = new archiver_1.Archiver(path_1.default.join(constants_1.distDir, zipName));
81
- const dependencies = ['uvicorn', 'pydantic', 'pydantic_core', 'uvloop', 'starlette', 'typing_inspection'];
82
- const lambdaSitePackages = `${process.env.PYTHON_SITE_PACKAGES}-lambda`;
83
- await Promise.all(dependencies.map(async (packageName) => (0, add_package_to_archive_1.addPackageToArchive)(archive, lambdaSitePackages, packageName)));
84
- for (const step of steps) {
85
- await this.buildStep(step, archive);
78
+ async addStepToArchive(step, archive) {
79
+ const normalizedPath = this.normalizeStepPath(step, false);
80
+ archive.append(fs_1.default.createReadStream(step.filePath), normalizedPath);
81
+ const internalFiles = await this.findInternalFiles(step.filePath);
82
+ for (const file of internalFiles) {
83
+ const fullPath = path_1.default.join(this.builder.projectDir, file);
84
+ if (fs_1.default.existsSync(fullPath) && fullPath !== step.filePath) {
85
+ const archivePath = file.replace(/\.step\.py$/, '_step.py');
86
+ archive.append(fs_1.default.createReadStream(fullPath), archivePath);
87
+ }
88
+ }
89
+ }
90
+ async addPackagesToArchive(archive, sitePackagesDir) {
91
+ if (!fs_1.default.existsSync(sitePackagesDir)) {
92
+ console.warn(`Warning: Site packages directory not found: ${sitePackagesDir}`);
93
+ return;
94
+ }
95
+ try {
96
+ fs_1.default.accessSync(sitePackagesDir, fs_1.default.constants.R_OK);
86
97
  }
87
- const file = fs_1.default
98
+ catch (error) {
99
+ console.warn(`Warning: Cannot access site packages directory: ${sitePackagesDir}`);
100
+ return;
101
+ }
102
+ const addDirectory = (dirPath, basePath = sitePackagesDir) => {
103
+ try {
104
+ const items = fs_1.default.readdirSync(dirPath);
105
+ for (const item of items) {
106
+ const fullPath = path_1.default.join(dirPath, item);
107
+ const relativePath = path_1.default.relative(basePath, fullPath);
108
+ if (this.shouldIgnoreFile(relativePath)) {
109
+ continue;
110
+ }
111
+ try {
112
+ const stat = fs_1.default.statSync(fullPath);
113
+ if (stat.isDirectory()) {
114
+ addDirectory(fullPath, basePath);
115
+ }
116
+ else {
117
+ archive.append(fs_1.default.createReadStream(fullPath), relativePath);
118
+ }
119
+ }
120
+ catch (error) {
121
+ console.warn(`Warning: Could not process file ${fullPath}: ${error}`);
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ console.warn(`Warning: Could not read directory ${dirPath}: ${error}`);
127
+ }
128
+ };
129
+ addDirectory(sitePackagesDir);
130
+ }
131
+ shouldIgnoreFile(filePath) {
132
+ const ignorePatterns = [
133
+ /\.pyc$/,
134
+ /\.pyo$/,
135
+ /\.egg$/,
136
+ /\.egg-info$/,
137
+ /__pycache__/,
138
+ /\.dist-info$/,
139
+ /^tests?\//,
140
+ /^docs?\//,
141
+ /^examples?\//,
142
+ /\.pytest_cache/,
143
+ ];
144
+ return ignorePatterns.some((pattern) => pattern.test(filePath));
145
+ }
146
+ normalizeStepPath(step, normalizePythonModulePath) {
147
+ let normalizedStepPath = step.filePath
148
+ .replace(/[.]step.py$/, '_step.py') // Replace .step.py with _step.py
149
+ .replace(`${this.builder.projectDir}/`, ''); // Remove the project directory from the path
150
+ const pathParts = normalizedStepPath.split(path_1.default.sep).map((part) => part
151
+ .replace(/^[0-9]+/g, '') // Remove numeric prefixes
152
+ .replace(/[^a-zA-Z0-9._]/g, '_') // Replace any non-alphanumeric characters (except dots) with underscores
153
+ .replace(/^_/, '')); // Remove leading underscore
154
+ normalizedStepPath = normalizePythonModulePath
155
+ ? pathParts.join('.') // Convert path delimiter to dot (python module separator)
156
+ : '/' + pathParts.join(path_1.default.sep);
157
+ return normalizedStepPath;
158
+ }
159
+ createRouterTemplate(steps) {
160
+ const imports = steps
161
+ .map((step, index) => {
162
+ const moduleName = this.getModuleName(step);
163
+ return `from ${moduleName} import handler as route${index}_handler, config as route${index}_config`;
164
+ })
165
+ .join('\n');
166
+ const routerPaths = steps
167
+ .map((step, index) => {
168
+ const method = step.config.method.toUpperCase();
169
+ const path = step.config.path;
170
+ return ` '${method} ${path}': RouterPath('${step.config.name}', '${step.config.method.toLowerCase()}', route${index}_handler, route${index}_config)`;
171
+ })
172
+ .join(',\n');
173
+ return fs_1.default
88
174
  .readFileSync(path_1.default.join(__dirname, 'router_template.py'), 'utf-8')
89
- .replace('# {{imports}}', steps
90
- .map((step, index) => `from ${getStepPath(step)} import handler as route${index}_handler, config as route${index}_config`)
91
- .join('\n'))
92
- .replace('# {{router paths}}', steps
93
- .map((step, index) => `'${step.config.method} ${step.config.path}': RouterPath('${step.config.name}', '${step.config.method.toLowerCase()}', route${index}_handler, route${index}_config)`)
94
- .join(',\n '));
95
- archive.append(file, 'router.py');
96
- (0, include_static_files_1.includeStaticFiles)(steps, this.builder, archive);
97
- // Finalize the archive and wait for completion
98
- const size = await archive.finalize();
99
- return { size, path: zipName };
175
+ .replace('# {{imports}}', imports)
176
+ .replace('# {{router paths}}', routerPaths);
100
177
  }
101
- async getPythonBuilderData(step) {
102
- return new Promise((resolve, reject) => {
103
- const child = (0, child_process_1.spawn)('python', [path_1.default.join(__dirname, 'python-builder.py'), step.filePath], {
104
- cwd: this.builder.projectDir,
105
- stdio: [undefined, undefined, 'pipe', 'ipc'],
106
- });
107
- const err = [];
108
- child.on('stderr', (data) => err.push(data.toString()));
109
- child.on('message', resolve);
110
- child.on('close', (code) => {
111
- if (code !== 0) {
112
- reject(new Error(err.join('')));
178
+ async findInternalFiles(entryFile) {
179
+ const files = [];
180
+ const visited = new Set();
181
+ const analyzeFile = (filePath) => {
182
+ if (visited.has(filePath) || !fs_1.default.existsSync(filePath)) {
183
+ return;
184
+ }
185
+ visited.add(filePath);
186
+ files.push(path_1.default.relative(this.builder.projectDir, filePath));
187
+ try {
188
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
189
+ const importRegex = /^(?:from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import|import\s+([a-zA-Z_][a-zA-Z0-9_.]*))/gm;
190
+ let match;
191
+ while ((match = importRegex.exec(content)) !== null) {
192
+ const moduleName = match[1] || match[2]; // from X import Y ou import X
193
+ this.resolveModulePaths(moduleName, path_1.default.dirname(filePath)).forEach((possiblePath) => {
194
+ if (fs_1.default.existsSync(possiblePath)) {
195
+ analyzeFile(possiblePath);
196
+ }
197
+ });
113
198
  }
114
- });
115
- });
199
+ }
200
+ catch (error) {
201
+ console.warn(`Could not analyze file: ${filePath}`);
202
+ }
203
+ };
204
+ analyzeFile(entryFile);
205
+ return files;
206
+ }
207
+ resolveModulePaths(moduleName, currentDir) {
208
+ const parts = moduleName.split('.');
209
+ const baseName = parts[0];
210
+ const subPath = parts.length > 1 ? path_1.default.join(...parts) : baseName;
211
+ return [
212
+ path_1.default.join(currentDir, `${baseName}.py`),
213
+ path_1.default.join(currentDir, baseName, '__init__.py'),
214
+ path_1.default.join(currentDir, `${subPath}.py`),
215
+ path_1.default.join(this.builder.projectDir, `${baseName}.py`),
216
+ path_1.default.join(this.builder.projectDir, baseName, '__init__.py'),
217
+ path_1.default.join(this.builder.projectDir, `${subPath}.py`),
218
+ path_1.default.join(this.builder.projectDir, subPath + '.py'),
219
+ path_1.default.join(this.builder.projectDir, subPath, '__init__.py'),
220
+ ];
221
+ }
222
+ getModuleName(step) {
223
+ return this.normalizeStepPath(step, true).replace(/\.py$/, '').replace(/\//g, '.');
224
+ }
225
+ async waitForDirectoryReady(dirPath, maxRetries = 10, initialDelayMs = 10) {
226
+ let lastError = null;
227
+ for (let i = 0; i < maxRetries; i++) {
228
+ try {
229
+ const exists = await fs_1.default.promises
230
+ .access(dirPath, fs_1.default.constants.F_OK)
231
+ .then(() => true)
232
+ .catch(() => false);
233
+ if (!exists) {
234
+ // Directory doesn't exist yet, wait
235
+ lastError = new Error(`Directory ${dirPath} does not exist yet`);
236
+ }
237
+ else {
238
+ await fs_1.default.promises.access(dirPath, fs_1.default.constants.R_OK);
239
+ return;
240
+ }
241
+ }
242
+ catch (error) {
243
+ lastError = error;
244
+ }
245
+ if (i === maxRetries - 1) {
246
+ throw new Error(`Directory ${dirPath} is not accessible after ${maxRetries} attempts. ` +
247
+ `Last error: ${lastError?.message || 'Unknown error'}`);
248
+ }
249
+ // Exponential backoff: 10ms, 20ms, 40ms, 80ms, etc.
250
+ const delay = initialDelayMs * Math.pow(2, i);
251
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delay, 1000))); // Cap at 1 second
252
+ }
116
253
  }
117
254
  }
118
255
  exports.PythonBuilder = PythonBuilder;
@@ -0,0 +1,13 @@
1
+ export interface UvPackageConfig {
2
+ pythonVersion?: string;
3
+ platform?: string;
4
+ onlyBinary?: boolean;
5
+ }
6
+ export declare const defaultUvConfig: UvPackageConfig;
7
+ export declare class UvPackager {
8
+ private readonly projectDir;
9
+ private readonly config;
10
+ constructor(projectDir: string, config?: UvPackageConfig);
11
+ private runCommand;
12
+ packageDependencies(targetDir: string): Promise<void>;
13
+ }