motia 0.6.1-beta.124 → 0.6.2-beta.126
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.
- package/README.md +34 -23
- package/dist/cjs/cli.js +59 -11
- package/dist/cjs/cloud/build/builders/node/index.js +4 -1
- package/dist/cjs/cloud/build/builders/python/index.d.ts +11 -3
- package/dist/cjs/cloud/build/builders/python/index.js +215 -78
- package/dist/cjs/cloud/build/builders/python/uv-packager.d.ts +13 -0
- package/dist/cjs/cloud/build/builders/python/uv-packager.js +77 -0
- package/dist/cjs/cloud/cli/deploy.js +8 -4
- package/dist/cjs/cloud/endpoints.d.ts +2 -0
- package/dist/cjs/cloud/endpoints.js +102 -0
- package/dist/cjs/cloud/new-deployment/build.js +12 -4
- package/dist/cjs/cloud/new-deployment/cloud-api/create-deployment.d.ts +3 -1
- package/dist/cjs/cloud/new-deployment/cloud-api/create-deployment.js +6 -1
- package/dist/cjs/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
- package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.js +278 -0
- package/dist/cjs/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
- package/dist/cjs/cloud/new-deployment/streams/deployment-stream.js +108 -0
- package/dist/cjs/create/index.js +0 -1
- package/dist/cjs/create/templates/python/motia-workbench.json +1 -1
- package/dist/cjs/create/templates/python/steps/api_step.py.txt +1 -1
- package/dist/cjs/create/templates/python/steps/state_audit_cron_step.py.txt +1 -1
- package/dist/cjs/create/templates/python/tutorial.tsx.txt +1 -1
- package/dist/cjs/create/templates/typescript/motia-workbench.json +1 -1
- package/dist/cjs/create/templates/typescript/steps/api.step.ts.txt +2 -2
- package/dist/cjs/create/templates/typescript/steps/state-audit-cron.step.ts.txt +1 -1
- package/dist/cjs/create/templates/typescript/tutorial.tsx.txt +1 -1
- package/dist/cjs/cursor-rules/index.d.ts +8 -0
- package/dist/cjs/cursor-rules/index.js +269 -0
- package/dist/cjs/dev.js +3 -1
- package/dist/cjs/generate-locked-data.js +13 -7
- package/dist/cjs/install.js +5 -0
- package/dist/cjs/utils/ensure-uv.d.ts +1 -0
- package/dist/cjs/utils/ensure-uv.js +81 -0
- package/dist/cjs/utils/errors/build.error.d.ts +9 -0
- package/dist/cjs/utils/errors/build.error.js +20 -0
- package/dist/cjs/utils/errors/compilation.error.d.ts +4 -0
- package/dist/cjs/utils/errors/compilation.error.js +11 -0
- package/dist/esm/cli.js +59 -11
- package/dist/esm/cloud/build/builders/node/index.js +4 -1
- package/dist/esm/cloud/build/builders/python/index.d.ts +11 -3
- package/dist/esm/cloud/build/builders/python/index.js +215 -78
- package/dist/esm/cloud/build/builders/python/uv-packager.d.ts +13 -0
- package/dist/esm/cloud/build/builders/python/uv-packager.js +70 -0
- package/dist/esm/cloud/cli/deploy.js +8 -4
- package/dist/esm/cloud/endpoints.d.ts +2 -0
- package/dist/esm/cloud/endpoints.js +98 -0
- package/dist/esm/cloud/new-deployment/build.js +12 -4
- package/dist/esm/cloud/new-deployment/cloud-api/create-deployment.d.ts +3 -1
- package/dist/esm/cloud/new-deployment/cloud-api/create-deployment.js +6 -1
- package/dist/esm/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
- package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.js +274 -0
- package/dist/esm/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
- package/dist/esm/cloud/new-deployment/streams/deployment-stream.js +103 -0
- package/dist/esm/create/index.js +0 -1
- package/dist/esm/create/templates/python/motia-workbench.json +1 -1
- package/dist/esm/create/templates/python/steps/api_step.py.txt +1 -1
- package/dist/esm/create/templates/python/steps/state_audit_cron_step.py.txt +1 -1
- package/dist/esm/create/templates/python/tutorial.tsx.txt +1 -1
- package/dist/esm/create/templates/typescript/motia-workbench.json +1 -1
- package/dist/esm/create/templates/typescript/steps/api.step.ts.txt +2 -2
- package/dist/esm/create/templates/typescript/steps/state-audit-cron.step.ts.txt +1 -1
- package/dist/esm/create/templates/typescript/tutorial.tsx.txt +1 -1
- package/dist/esm/cursor-rules/index.d.ts +8 -0
- package/dist/esm/cursor-rules/index.js +263 -0
- package/dist/esm/dev.js +3 -1
- package/dist/esm/generate-locked-data.js +13 -7
- package/dist/esm/install.js +5 -0
- package/dist/esm/utils/ensure-uv.d.ts +1 -0
- package/dist/esm/utils/ensure-uv.js +77 -0
- package/dist/esm/utils/errors/build.error.d.ts +9 -0
- package/dist/esm/utils/errors/build.error.js +16 -0
- package/dist/esm/utils/errors/compilation.error.d.ts +4 -0
- package/dist/esm/utils/errors/compilation.error.js +7 -0
- package/dist/types/cloud/build/builders/python/index.d.ts +11 -3
- package/dist/types/cloud/build/builders/python/uv-packager.d.ts +13 -0
- package/dist/types/cloud/endpoints.d.ts +2 -0
- package/dist/types/cloud/new-deployment/cloud-api/create-deployment.d.ts +3 -1
- package/dist/types/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/types/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +44 -0
- package/dist/types/cloud/new-deployment/streams/deployment-stream.d.ts +46 -0
- package/dist/types/cursor-rules/index.d.ts +8 -0
- package/dist/types/utils/ensure-uv.d.ts +1 -0
- package/dist/types/utils/errors/build.error.d.ts +9 -0
- package/dist/types/utils/errors/compilation.error.d.ts +4 -0
- package/package.json +4 -4
- package/dist/cjs/cloud/build/builders/python/add-package-to-archive.d.ts +0 -2
- package/dist/cjs/cloud/build/builders/python/add-package-to-archive.js +0 -56
- package/dist/cjs/cloud/build/builders/python/python-builder.py +0 -226
- package/dist/dot-files/.cursor/rules/api-steps.mdc +0 -169
- package/dist/dot-files/.cursor/rules/architecture.mdc +0 -189
- package/dist/dot-files/.cursor/rules/cron-steps.mdc +0 -257
- package/dist/dot-files/.cursor/rules/event-steps.mdc +0 -366
- package/dist/dot-files/.cursor/rules/instructions.mdc +0 -15
- package/dist/dot-files/.cursor/rules/noop-steps.mdc +0 -57
- package/dist/dot-files/.cursor/rules/state-management.mdc +0 -325
- package/dist/dot-files/.cursor/rules/steps.mdc +0 -317
- package/dist/dot-files/.cursor/rules/testing.mdc +0 -329
- package/dist/dot-files/.cursor/rules/typescript.mdc +0 -409
- package/dist/dot-files/.cursor/rules/ui-steps.mdc +0 -90
- package/dist/dot-files/CLAUDE.md +0 -827
- package/dist/dot-files/README.md +0 -58
- package/dist/esm/cloud/build/builders/python/add-package-to-archive.d.ts +0 -2
- package/dist/esm/cloud/build/builders/python/add-package-to-archive.js +0 -49
- package/dist/esm/cloud/build/builders/python/python-builder.py +0 -226
- 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**
|
|
102
|
-
|
|
103
|
-
| Multiple deployment targets | **Single unified deployment**
|
|
104
|
-
| Fragmented observability
|
|
105
|
-
| Language dependent
|
|
106
|
-
| Context-switching overhead
|
|
107
|
-
| Complex error handling
|
|
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
|
|
114
|
-
|
|
115
|
-
| **`api`**
|
|
116
|
-
| **`event`** | Emitted Topics
|
|
117
|
-
| **`cron`**
|
|
118
|
-
| **`noop`**
|
|
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
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
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
|
-
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
57
|
+
template: arg.template,
|
|
49
58
|
cursorEnabled: arg.cursor,
|
|
50
59
|
context,
|
|
51
|
-
skipTutorialTemplates:
|
|
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
|
|
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
|
-
|
|
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
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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}}',
|
|
90
|
-
.
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|