motia 0.6.4-beta.130 → 0.6.4-beta.131-595093
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 +3 -3
- package/dist/cjs/cloud/build/build-validation.d.ts +2 -7
- package/dist/cjs/cloud/build/build-validation.js +43 -5
- package/dist/cjs/cloud/build/builder.d.ts +7 -1
- package/dist/cjs/cloud/build/builder.js +16 -4
- package/dist/cjs/cloud/build/builders/archiver.d.ts +6 -1
- package/dist/cjs/cloud/build/builders/archiver.js +17 -1
- package/dist/cjs/cloud/build/builders/node/index.js +5 -4
- package/dist/cjs/cloud/build/builders/python/index.d.ts +3 -6
- package/dist/cjs/cloud/build/builders/python/index.js +73 -149
- package/dist/cjs/cloud/build/builders/python/package-copier.d.ts +13 -0
- package/dist/cjs/cloud/build/builders/python/package-copier.js +38 -0
- package/dist/cjs/cloud/build/builders/python/package-handler.d.ts +27 -0
- package/dist/cjs/cloud/build/builders/python/package-handler.js +209 -0
- package/dist/cjs/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
- package/dist/cjs/cloud/build/builders/python/python-dependency-analyzer.js +348 -0
- package/dist/cjs/cloud/build/builders/python/router_template.py +1 -0
- package/dist/cjs/cloud/build/builders/python/uv-packager.d.ts +1 -0
- package/dist/cjs/cloud/build/builders/python/uv-packager.js +44 -0
- package/dist/cjs/cloud/cli/build.js +6 -1
- package/dist/cjs/cloud/cli/deploy.js +3 -1
- package/dist/cjs/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/cjs/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
- package/dist/cjs/cloud/new-deployment/cloud-api/start-deployment.js +1 -1
- package/dist/cjs/cloud/new-deployment/deploy.d.ts +1 -0
- package/dist/cjs/cloud/new-deployment/deploy.js +30 -22
- package/dist/cjs/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
- package/dist/cjs/cloud/new-deployment/listeners/cli-listener.js +1 -2
- package/dist/cjs/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
- package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
- package/dist/esm/cloud/build/build-validation.d.ts +2 -7
- package/dist/esm/cloud/build/build-validation.js +43 -5
- package/dist/esm/cloud/build/builder.d.ts +7 -1
- package/dist/esm/cloud/build/builder.js +16 -4
- package/dist/esm/cloud/build/builders/archiver.d.ts +6 -1
- package/dist/esm/cloud/build/builders/archiver.js +17 -1
- package/dist/esm/cloud/build/builders/node/index.js +5 -4
- package/dist/esm/cloud/build/builders/python/index.d.ts +3 -6
- package/dist/esm/cloud/build/builders/python/index.js +73 -149
- package/dist/esm/cloud/build/builders/python/package-copier.d.ts +13 -0
- package/dist/esm/cloud/build/builders/python/package-copier.js +31 -0
- package/dist/esm/cloud/build/builders/python/package-handler.d.ts +27 -0
- package/dist/esm/cloud/build/builders/python/package-handler.js +202 -0
- package/dist/esm/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
- package/dist/esm/cloud/build/builders/python/python-dependency-analyzer.js +341 -0
- package/dist/esm/cloud/build/builders/python/router_template.py +1 -0
- package/dist/esm/cloud/build/builders/python/uv-packager.d.ts +1 -0
- package/dist/esm/cloud/build/builders/python/uv-packager.js +44 -0
- package/dist/esm/cloud/cli/build.js +6 -1
- package/dist/esm/cloud/cli/deploy.js +3 -1
- package/dist/esm/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/esm/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
- package/dist/esm/cloud/new-deployment/cloud-api/start-deployment.js +1 -1
- package/dist/esm/cloud/new-deployment/deploy.d.ts +1 -0
- package/dist/esm/cloud/new-deployment/deploy.js +30 -22
- package/dist/esm/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
- package/dist/esm/cloud/new-deployment/listeners/cli-listener.js +1 -2
- package/dist/esm/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
- package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
- package/dist/types/cloud/build/build-validation.d.ts +2 -7
- package/dist/types/cloud/build/builder.d.ts +7 -1
- package/dist/types/cloud/build/builders/archiver.d.ts +6 -1
- package/dist/types/cloud/build/builders/python/index.d.ts +3 -6
- package/dist/types/cloud/build/builders/python/package-copier.d.ts +13 -0
- package/dist/types/cloud/build/builders/python/package-handler.d.ts +27 -0
- package/dist/types/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
- package/dist/types/cloud/build/builders/python/uv-packager.d.ts +1 -0
- package/dist/types/cloud/new-deployment/cloud-api/index.d.ts +3 -1
- package/dist/types/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
- package/dist/types/cloud/new-deployment/deploy.d.ts +1 -0
- package/dist/types/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
- package/dist/types/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
- package/dist/types/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
- package/package.json +6 -4
- package/dist/cjs/cloud/new-deployment/utils/validation.d.ts +0 -10
- package/dist/cjs/cloud/new-deployment/utils/validation.js +0 -107
- package/dist/esm/cloud/new-deployment/utils/validation.d.ts +0 -10
- package/dist/esm/cloud/new-deployment/utils/validation.js +0 -67
- package/dist/types/cloud/new-deployment/utils/validation.d.ts +0 -10
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Motia is a **modern backend framework** that unifies APIs, background jobs, work
|
|
|
21
21
|
|
|
22
22
|
Motia brings cohesion to the fragmented backend world with our core primitive: the **Step**.
|
|
23
23
|
|
|
24
|
-

|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ Get Motia project up and running in **under 60 seconds**:
|
|
|
32
32
|
### 1. Bootstrap a New Motia Project
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx motia@latest create
|
|
35
|
+
npx motia@latest create # runs the interactive terminal
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Follow the prompts to pick a template, project name, and language.
|
|
@@ -228,7 +228,7 @@ npx motia create [options]
|
|
|
228
228
|
# -n, --name <project name>: Project name; use . or ./ to use current directory
|
|
229
229
|
# -t, --template <template name>: Template to use; run npx motia templates to view available ones
|
|
230
230
|
# -c, --cursor: Adds .cursor config for Cursor IDE
|
|
231
|
-
# Alternatively, you can use `npx motia create
|
|
231
|
+
# Alternatively, you can use `npx motia create` to use the create command in interactive mode
|
|
232
232
|
```
|
|
233
233
|
|
|
234
234
|
### `npx motia dev`
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { BuildListener } from '../new-deployment/listeners/listener.types';
|
|
2
|
-
import { Builder
|
|
1
|
+
import { BuildListener, ValidationError } from '../new-deployment/listeners/listener.types';
|
|
2
|
+
import { Builder } from './builder';
|
|
3
3
|
export declare const buildValidation: (builder: Builder, listener: BuildListener) => boolean;
|
|
4
|
-
export type ValidationError = {
|
|
5
|
-
relativePath: string;
|
|
6
|
-
message: string;
|
|
7
|
-
step: BuildStepConfig;
|
|
8
|
-
};
|
|
9
4
|
export declare const validateStepsConfig: (builder: Builder) => {
|
|
10
5
|
errors: ValidationError[];
|
|
11
6
|
warnings: ValidationError[];
|
|
@@ -38,7 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.validateStepsConfig = exports.buildValidation = void 0;
|
|
40
40
|
const colors_1 = __importDefault(require("colors"));
|
|
41
|
-
const cron = __importStar(require("cron"));
|
|
41
|
+
const cron = __importStar(require("node-cron"));
|
|
42
42
|
const path_1 = __importDefault(require("path"));
|
|
43
43
|
const buildValidation = (builder, listener) => {
|
|
44
44
|
const { errors, warnings } = (0, exports.validateStepsConfig)(builder);
|
|
@@ -70,10 +70,29 @@ const validateStepsConfig = (builder) => {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
for (const step of Object.values(builder.stepsConfig)) {
|
|
73
|
-
// TODO: check bundle size
|
|
74
73
|
const relativePath = path_1.default.relative(builder.projectDir, step.filePath);
|
|
74
|
+
// Check individual step bundle size (150MB limit - uncompressed)
|
|
75
|
+
const stepUncompressedSize = builder.stepUncompressedSizes.get(step.filePath);
|
|
76
|
+
if (stepUncompressedSize !== undefined) {
|
|
77
|
+
const maxSize = 250 * 1024 * 1024; // 250MB in bytes
|
|
78
|
+
if (stepUncompressedSize > maxSize) {
|
|
79
|
+
const sizeMB = (stepUncompressedSize / (1024 * 1024)).toFixed(2);
|
|
80
|
+
const compressedSize = builder.stepCompressedSizes.get(step.filePath);
|
|
81
|
+
const compressedSizeMB = compressedSize ? (compressedSize / (1024 * 1024)).toFixed(2) : 'unknown';
|
|
82
|
+
errors.push({
|
|
83
|
+
relativePath,
|
|
84
|
+
message: [
|
|
85
|
+
'Step bundle size exceeds 250MB limit (uncompressed).',
|
|
86
|
+
` ${colors_1.default.red('➜')} Uncompressed size: ${colors_1.default.magenta(sizeMB + 'MB')}`,
|
|
87
|
+
` ${colors_1.default.red('➜')} Compressed size: ${colors_1.default.cyan(compressedSizeMB + 'MB')}`,
|
|
88
|
+
` ${colors_1.default.red('➜')} Maximum allowed: ${colors_1.default.blue('250MB')}`,
|
|
89
|
+
].join('\n'),
|
|
90
|
+
step,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
75
94
|
if (step.config.type === 'cron') {
|
|
76
|
-
if (!cron.
|
|
95
|
+
if (!cron.validate(step.config.cron)) {
|
|
77
96
|
errors.push({
|
|
78
97
|
relativePath,
|
|
79
98
|
message: [
|
|
@@ -103,17 +122,36 @@ const validateStepsConfig = (builder) => {
|
|
|
103
122
|
endpoints.set(endpoint, entrypoint);
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
|
-
if (step.config.name.length >
|
|
125
|
+
if (step.config.name.length > 40) {
|
|
107
126
|
errors.push({
|
|
108
127
|
relativePath,
|
|
109
128
|
message: [
|
|
110
|
-
`Step name is too long. Maximum is
|
|
129
|
+
`Step name is too long. Maximum is 40 characters.`,
|
|
111
130
|
` ${colors_1.default.red('➜')} ${colors_1.default.magenta(step.config.name)}`,
|
|
112
131
|
].join('\n'),
|
|
113
132
|
step,
|
|
114
133
|
});
|
|
115
134
|
}
|
|
116
135
|
}
|
|
136
|
+
// Check API router bundle sizes (150MB limit - uncompressed)
|
|
137
|
+
const maxRouterSize = 150 * 1024 * 1024; // 150MB in bytes
|
|
138
|
+
for (const [routerType, uncompressedSize] of builder.routerUncompressedSizes.entries()) {
|
|
139
|
+
if (uncompressedSize > maxRouterSize) {
|
|
140
|
+
const uncompressedSizeMB = (uncompressedSize / (1024 * 1024)).toFixed(2);
|
|
141
|
+
const compressedSize = builder.routerCompressedSizes.get(routerType);
|
|
142
|
+
const compressedSizeMB = compressedSize ? (compressedSize / (1024 * 1024)).toFixed(2) : 'unknown';
|
|
143
|
+
errors.push({
|
|
144
|
+
relativePath: `${routerType} API router`,
|
|
145
|
+
message: [
|
|
146
|
+
`${routerType.charAt(0).toUpperCase() + routerType.slice(1)} API router bundle size exceeds 150MB limit (uncompressed).`,
|
|
147
|
+
` ${colors_1.default.red('➜')} Uncompressed size: ${colors_1.default.magenta(uncompressedSizeMB + 'MB')}`,
|
|
148
|
+
` ${colors_1.default.red('➜')} Compressed size: ${colors_1.default.cyan(compressedSizeMB + 'MB')}`,
|
|
149
|
+
` ${colors_1.default.red('➜')} Maximum allowed: ${colors_1.default.blue('150MB')}`,
|
|
150
|
+
].join('\n'),
|
|
151
|
+
step: Object.values(builder.stepsConfig)[0], // Use first step as reference
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
117
155
|
return { errors, warnings };
|
|
118
156
|
};
|
|
119
157
|
exports.validateStepsConfig = validateStepsConfig;
|
|
@@ -21,7 +21,8 @@ export type StepsConfigFile = {
|
|
|
21
21
|
routers: BuildRoutersConfig;
|
|
22
22
|
};
|
|
23
23
|
export interface RouterBuildResult {
|
|
24
|
-
|
|
24
|
+
compressedSize: number;
|
|
25
|
+
uncompressedSize: number;
|
|
25
26
|
path: string;
|
|
26
27
|
}
|
|
27
28
|
export interface StepBuilder {
|
|
@@ -34,6 +35,10 @@ export declare class Builder {
|
|
|
34
35
|
readonly stepsConfig: BuildStepsConfig;
|
|
35
36
|
readonly streamsConfig: BuildStreamsConfig;
|
|
36
37
|
routersConfig: BuildRoutersConfig;
|
|
38
|
+
readonly stepCompressedSizes: Map<string, number>;
|
|
39
|
+
readonly stepUncompressedSizes: Map<string, number>;
|
|
40
|
+
readonly routerCompressedSizes: Map<string, number>;
|
|
41
|
+
readonly routerUncompressedSizes: Map<string, number>;
|
|
37
42
|
modulegraphInstalled: boolean;
|
|
38
43
|
private readonly builders;
|
|
39
44
|
constructor(projectDir: string, listener: BuildListener);
|
|
@@ -45,6 +50,7 @@ export declare class Builder {
|
|
|
45
50
|
step: Step;
|
|
46
51
|
type: StepType;
|
|
47
52
|
}): void;
|
|
53
|
+
recordStepSize(step: Step, compressedSize: number, uncompressedSize: number): void;
|
|
48
54
|
buildStep(step: Step): Promise<void>;
|
|
49
55
|
buildApiSteps(steps: Step<ApiRouteConfig>[]): Promise<void>;
|
|
50
56
|
private determineStepType;
|
|
@@ -5,6 +5,10 @@ class Builder {
|
|
|
5
5
|
constructor(projectDir, listener) {
|
|
6
6
|
this.projectDir = projectDir;
|
|
7
7
|
this.listener = listener;
|
|
8
|
+
this.stepCompressedSizes = new Map();
|
|
9
|
+
this.stepUncompressedSizes = new Map();
|
|
10
|
+
this.routerCompressedSizes = new Map();
|
|
11
|
+
this.routerUncompressedSizes = new Map();
|
|
8
12
|
this.modulegraphInstalled = false;
|
|
9
13
|
this.builders = new Map();
|
|
10
14
|
this.stepsConfig = {};
|
|
@@ -29,6 +33,10 @@ class Builder {
|
|
|
29
33
|
filePath: args.step.filePath,
|
|
30
34
|
};
|
|
31
35
|
}
|
|
36
|
+
recordStepSize(step, compressedSize, uncompressedSize) {
|
|
37
|
+
this.stepCompressedSizes.set(step.filePath, compressedSize);
|
|
38
|
+
this.stepUncompressedSizes.set(step.filePath, uncompressedSize);
|
|
39
|
+
}
|
|
32
40
|
async buildStep(step) {
|
|
33
41
|
const type = this.determineStepType(step);
|
|
34
42
|
const builder = this.builders.get(type);
|
|
@@ -52,15 +60,19 @@ class Builder {
|
|
|
52
60
|
this.routersConfig = {};
|
|
53
61
|
if (nodeSteps.length > 0 && nodeBuilder) {
|
|
54
62
|
this.listener.onApiRouterBuilding('node');
|
|
55
|
-
const {
|
|
56
|
-
this.listener.onApiRouterBuilt('node',
|
|
63
|
+
const { compressedSize, uncompressedSize, path } = await nodeBuilder.buildApiSteps(nodeSteps);
|
|
64
|
+
this.listener.onApiRouterBuilt('node', compressedSize);
|
|
57
65
|
this.routersConfig.node = path;
|
|
66
|
+
this.routerCompressedSizes.set('node', compressedSize);
|
|
67
|
+
this.routerUncompressedSizes.set('node', uncompressedSize);
|
|
58
68
|
}
|
|
59
69
|
if (pythonSteps.length > 0 && pythonBuilder) {
|
|
60
70
|
this.listener.onApiRouterBuilding('python');
|
|
61
|
-
const {
|
|
62
|
-
this.listener.onApiRouterBuilt('python',
|
|
71
|
+
const { compressedSize, uncompressedSize, path } = await pythonBuilder.buildApiSteps(pythonSteps);
|
|
72
|
+
this.listener.onApiRouterBuilt('python', compressedSize);
|
|
63
73
|
this.routersConfig.python = path;
|
|
74
|
+
this.routerCompressedSizes.set('python', compressedSize);
|
|
75
|
+
this.routerUncompressedSizes.set('python', uncompressedSize);
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
78
|
determineStepType(step) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
export interface ArchiveResult {
|
|
3
|
+
compressedSize: number;
|
|
4
|
+
uncompressedSize: number;
|
|
5
|
+
}
|
|
2
6
|
export declare class Archiver {
|
|
3
7
|
private readonly archive;
|
|
4
8
|
private readonly outputStream;
|
|
9
|
+
private uncompressedSize;
|
|
5
10
|
constructor(filePath: string);
|
|
6
11
|
append(stream: fs.ReadStream | string, filePath: string): void;
|
|
7
|
-
finalize(): Promise<
|
|
12
|
+
finalize(): Promise<ArchiveResult>;
|
|
8
13
|
}
|
|
@@ -8,16 +8,32 @@ const archiver_1 = __importDefault(require("archiver"));
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
class Archiver {
|
|
10
10
|
constructor(filePath) {
|
|
11
|
+
this.uncompressedSize = 0;
|
|
11
12
|
this.archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
|
|
12
13
|
this.outputStream = fs_1.default.createWriteStream(filePath);
|
|
13
14
|
this.archive.pipe(this.outputStream);
|
|
14
15
|
}
|
|
15
16
|
append(stream, filePath) {
|
|
17
|
+
// Track uncompressed size
|
|
18
|
+
if (typeof stream === 'string') {
|
|
19
|
+
// String content
|
|
20
|
+
this.uncompressedSize += Buffer.byteLength(stream, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// ReadStream - get file stats
|
|
24
|
+
const stats = fs_1.default.statSync(stream.path);
|
|
25
|
+
this.uncompressedSize += stats.size;
|
|
26
|
+
}
|
|
16
27
|
this.archive.append(stream, { name: filePath });
|
|
17
28
|
}
|
|
18
29
|
async finalize() {
|
|
19
30
|
return new Promise((resolve, reject) => {
|
|
20
|
-
this.outputStream.on('close', () =>
|
|
31
|
+
this.outputStream.on('close', () => {
|
|
32
|
+
resolve({
|
|
33
|
+
compressedSize: this.archive.pointer(),
|
|
34
|
+
uncompressedSize: this.uncompressedSize,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
21
37
|
this.outputStream.on('error', reject);
|
|
22
38
|
this.archive.finalize();
|
|
23
39
|
});
|
|
@@ -97,11 +97,11 @@ class NodeBuilder {
|
|
|
97
97
|
archiver.append(fs_1.default.createReadStream(routerJs), 'router.js');
|
|
98
98
|
archiver.append(fs_1.default.createReadStream(routerMap), 'router.js.map');
|
|
99
99
|
(0, include_static_files_1.includeStaticFiles)(steps, this.builder, archiver);
|
|
100
|
-
const
|
|
100
|
+
const { compressedSize, uncompressedSize } = await archiver.finalize();
|
|
101
101
|
fs_1.default.unlinkSync(tsRouter);
|
|
102
102
|
fs_1.default.unlinkSync(routerJs);
|
|
103
103
|
fs_1.default.unlinkSync(routerMap);
|
|
104
|
-
return {
|
|
104
|
+
return { compressedSize, uncompressedSize, path: zipName };
|
|
105
105
|
}
|
|
106
106
|
async build(step) {
|
|
107
107
|
const relativeFilePath = step.filePath.replace(this.builder.projectDir, '');
|
|
@@ -126,10 +126,11 @@ class NodeBuilder {
|
|
|
126
126
|
archiver.append(fs_1.default.createReadStream(outputJsFile), entrypointPath);
|
|
127
127
|
archiver.append(fs_1.default.createReadStream(outputMapFile), entrypointMapPath);
|
|
128
128
|
(0, include_static_files_1.includeStaticFiles)([step], this.builder, archiver);
|
|
129
|
-
const
|
|
129
|
+
const { compressedSize, uncompressedSize } = await archiver.finalize();
|
|
130
130
|
fs_1.default.unlinkSync(outputJsFile);
|
|
131
131
|
fs_1.default.unlinkSync(outputMapFile);
|
|
132
|
-
this.
|
|
132
|
+
this.builder.recordStepSize(step, compressedSize, uncompressedSize);
|
|
133
|
+
this.listener.onBuildEnd(step, compressedSize);
|
|
133
134
|
}
|
|
134
135
|
catch (err) {
|
|
135
136
|
this.listener.onBuildError(step, err);
|
|
@@ -4,17 +4,14 @@ 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
|
|
7
|
+
private packageHandler;
|
|
8
8
|
constructor(builder: Builder, listener: BuildListener);
|
|
9
9
|
buildApiSteps(steps: Step<ApiRouteConfig>[]): Promise<RouterBuildResult>;
|
|
10
10
|
build(step: Step): Promise<void>;
|
|
11
11
|
private addStepToArchive;
|
|
12
|
-
private addPackagesToArchive;
|
|
13
|
-
private shouldIgnoreFile;
|
|
14
|
-
private normalizeStepPath;
|
|
15
|
-
private createRouterTemplate;
|
|
16
12
|
private findInternalFiles;
|
|
17
13
|
private resolveModulePaths;
|
|
14
|
+
private normalizeStepPath;
|
|
15
|
+
private createRouterTemplate;
|
|
18
16
|
private getModuleName;
|
|
19
|
-
private waitForDirectoryReady;
|
|
20
17
|
}
|
|
@@ -9,71 +9,86 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const archiver_1 = require("../archiver");
|
|
10
10
|
const include_static_files_1 = require("../include-static-files");
|
|
11
11
|
const constants_1 = require("../../../new-deployment/constants");
|
|
12
|
-
const uv_packager_1 = require("./uv-packager");
|
|
13
12
|
const activate_python_env_1 = require("../../../../utils/activate-python-env");
|
|
13
|
+
const package_handler_1 = require("./package-handler");
|
|
14
14
|
class PythonBuilder {
|
|
15
15
|
constructor(builder, listener) {
|
|
16
16
|
this.builder = builder;
|
|
17
17
|
this.listener = listener;
|
|
18
18
|
(0, activate_python_env_1.activatePythonVenv)({ baseDir: this.builder.projectDir });
|
|
19
|
-
this.
|
|
19
|
+
this.packageHandler = new package_handler_1.PackageHandler(this.builder.projectDir);
|
|
20
20
|
}
|
|
21
21
|
async buildApiSteps(steps) {
|
|
22
22
|
const zipName = 'router-python.zip';
|
|
23
23
|
const archive = new archiver_1.Archiver(path_1.default.join(constants_1.distDir, zipName));
|
|
24
|
-
|
|
24
|
+
let tempDirToCleanup;
|
|
25
25
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await this.
|
|
26
|
+
// Collect all Python files for analysis
|
|
27
|
+
const pythonFiles = steps.map((step) => step.filePath);
|
|
28
|
+
// Build packages using the unified handler
|
|
29
|
+
const buildResult = await this.packageHandler.buildPackages({
|
|
30
|
+
pythonFiles,
|
|
31
|
+
archive,
|
|
32
|
+
projectDir: this.builder.projectDir,
|
|
33
|
+
});
|
|
34
|
+
tempDirToCleanup = buildResult.tempDirCreated;
|
|
35
|
+
// Add all step files to archive
|
|
30
36
|
for (const step of steps) {
|
|
31
37
|
await this.addStepToArchive(step, archive);
|
|
32
38
|
}
|
|
39
|
+
// Add router template
|
|
33
40
|
const routerTemplate = this.createRouterTemplate(steps);
|
|
34
41
|
archive.append(routerTemplate, 'router.py');
|
|
42
|
+
// Include static files
|
|
35
43
|
(0, include_static_files_1.includeStaticFiles)(steps, this.builder, archive);
|
|
36
|
-
const
|
|
37
|
-
return {
|
|
44
|
+
const { compressedSize, uncompressedSize } = await archive.finalize();
|
|
45
|
+
return { compressedSize, uncompressedSize, path: zipName };
|
|
38
46
|
}
|
|
39
47
|
catch (error) {
|
|
40
48
|
throw new Error(`Failed to build Python API router: ${error}`);
|
|
41
49
|
}
|
|
42
50
|
finally {
|
|
43
|
-
|
|
44
|
-
fs_1.default.rmSync(tempSitePackages, { recursive: true, force: true });
|
|
45
|
-
}
|
|
51
|
+
this.packageHandler.cleanup(tempDirToCleanup);
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
async build(step) {
|
|
49
|
-
const entrypointPath = step.filePath.replace(this.builder.projectDir, '');
|
|
55
|
+
const entrypointPath = step.filePath.replace(this.builder.projectDir, '').replace(/\.step\.py$/, '_step.py');
|
|
50
56
|
const bundlePath = path_1.default.join('python', entrypointPath.replace(/(.*)\.py$/, '$1.zip'));
|
|
51
57
|
const outfile = path_1.default.join(constants_1.distDir, bundlePath);
|
|
52
58
|
this.builder.registerStep({ entrypointPath, bundlePath, step, type: 'python' });
|
|
53
59
|
this.listener.onBuildStart(step);
|
|
60
|
+
let tempDirToCleanup;
|
|
54
61
|
try {
|
|
55
62
|
fs_1.default.mkdirSync(path_1.default.dirname(outfile), { recursive: true });
|
|
56
63
|
const archive = new archiver_1.Archiver(outfile);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
if (fs_1.default.existsSync(tempSitePackages)) {
|
|
69
|
-
fs_1.default.rmSync(tempSitePackages, { recursive: true, force: true });
|
|
70
|
-
}
|
|
64
|
+
// Collect Python files for analysis (including internal files)
|
|
65
|
+
const pythonFiles = [step.filePath];
|
|
66
|
+
const internalFiles = await this.findInternalFiles(step.filePath);
|
|
67
|
+
for (const file of internalFiles) {
|
|
68
|
+
pythonFiles.push(path_1.default.join(this.builder.projectDir, file));
|
|
71
69
|
}
|
|
70
|
+
// Build packages using the unified handler
|
|
71
|
+
const buildResult = await this.packageHandler.buildPackages({
|
|
72
|
+
pythonFiles,
|
|
73
|
+
archive,
|
|
74
|
+
projectDir: this.builder.projectDir,
|
|
75
|
+
});
|
|
76
|
+
tempDirToCleanup = buildResult.tempDirCreated;
|
|
77
|
+
// Add step file to archive
|
|
78
|
+
await this.addStepToArchive(step, archive);
|
|
79
|
+
// Include static files
|
|
80
|
+
(0, include_static_files_1.includeStaticFiles)([step], this.builder, archive);
|
|
81
|
+
const { compressedSize, uncompressedSize } = await archive.finalize();
|
|
82
|
+
this.builder.recordStepSize(step, compressedSize, uncompressedSize);
|
|
83
|
+
this.listener.onBuildEnd(step, compressedSize);
|
|
72
84
|
}
|
|
73
85
|
catch (err) {
|
|
74
86
|
this.listener.onBuildError(step, err);
|
|
75
87
|
throw err;
|
|
76
88
|
}
|
|
89
|
+
finally {
|
|
90
|
+
this.packageHandler.cleanup(tempDirToCleanup);
|
|
91
|
+
}
|
|
77
92
|
}
|
|
78
93
|
async addStepToArchive(step, archive) {
|
|
79
94
|
const normalizedPath = this.normalizeStepPath(step, false);
|
|
@@ -87,101 +102,12 @@ class PythonBuilder {
|
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
104
|
}
|
|
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);
|
|
97
|
-
}
|
|
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
|
|
174
|
-
.readFileSync(path_1.default.join(__dirname, 'router_template.py'), 'utf-8')
|
|
175
|
-
.replace('# {{imports}}', imports)
|
|
176
|
-
.replace('# {{router paths}}', routerPaths);
|
|
177
|
-
}
|
|
178
105
|
async findInternalFiles(entryFile) {
|
|
179
106
|
const files = [];
|
|
180
107
|
const visited = new Set();
|
|
181
108
|
const analyzeFile = (filePath) => {
|
|
182
|
-
if (visited.has(filePath) || !fs_1.default.existsSync(filePath))
|
|
109
|
+
if (visited.has(filePath) || !fs_1.default.existsSync(filePath))
|
|
183
110
|
return;
|
|
184
|
-
}
|
|
185
111
|
visited.add(filePath);
|
|
186
112
|
files.push(path_1.default.relative(this.builder.projectDir, filePath));
|
|
187
113
|
try {
|
|
@@ -189,7 +115,7 @@ class PythonBuilder {
|
|
|
189
115
|
const importRegex = /^(?:from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import|import\s+([a-zA-Z_][a-zA-Z0-9_.]*))/gm;
|
|
190
116
|
let match;
|
|
191
117
|
while ((match = importRegex.exec(content)) !== null) {
|
|
192
|
-
const moduleName = match[1] || match[2];
|
|
118
|
+
const moduleName = match[1] || match[2];
|
|
193
119
|
this.resolveModulePaths(moduleName, path_1.default.dirname(filePath)).forEach((possiblePath) => {
|
|
194
120
|
if (fs_1.default.existsSync(possiblePath)) {
|
|
195
121
|
analyzeFile(possiblePath);
|
|
@@ -197,8 +123,8 @@ class PythonBuilder {
|
|
|
197
123
|
});
|
|
198
124
|
}
|
|
199
125
|
}
|
|
200
|
-
catch (
|
|
201
|
-
|
|
126
|
+
catch (_error) {
|
|
127
|
+
// Ignore file read/parse errors
|
|
202
128
|
}
|
|
203
129
|
};
|
|
204
130
|
analyzeFile(entryFile);
|
|
@@ -219,37 +145,35 @@ class PythonBuilder {
|
|
|
219
145
|
path_1.default.join(this.builder.projectDir, subPath, '__init__.py'),
|
|
220
146
|
];
|
|
221
147
|
}
|
|
148
|
+
normalizeStepPath(step, normalizePythonModulePath) {
|
|
149
|
+
let normalizedStepPath = step.filePath.replace(/[.]step.py$/, '_step.py').replace(`${this.builder.projectDir}/`, '');
|
|
150
|
+
const pathParts = normalizedStepPath.split(path_1.default.sep).map((part) => part
|
|
151
|
+
.replace(/[^a-zA-Z0-9._]/g, '_') // Replace any non-alphanumeric characters (except dots) with underscores
|
|
152
|
+
.replace(/^_/, ''));
|
|
153
|
+
normalizedStepPath = normalizePythonModulePath ? pathParts.join('.') : '/' + pathParts.join(path_1.default.sep);
|
|
154
|
+
return normalizedStepPath;
|
|
155
|
+
}
|
|
156
|
+
createRouterTemplate(steps) {
|
|
157
|
+
const imports = steps
|
|
158
|
+
.map((step, index) => {
|
|
159
|
+
const moduleName = this.getModuleName(step);
|
|
160
|
+
return `route${index}_module = importlib.import_module('${moduleName}')`;
|
|
161
|
+
})
|
|
162
|
+
.join('\n');
|
|
163
|
+
const routerPaths = steps
|
|
164
|
+
.map((step, index) => {
|
|
165
|
+
const method = step.config.method.toUpperCase();
|
|
166
|
+
const path = step.config.path;
|
|
167
|
+
return ` '${method} ${path}': RouterPath('${step.config.name}', '${step.config.method.toLowerCase()}', route${index}_module.handler, route${index}_module.config)`;
|
|
168
|
+
})
|
|
169
|
+
.join(',\n');
|
|
170
|
+
return fs_1.default
|
|
171
|
+
.readFileSync(path_1.default.join(__dirname, 'router_template.py'), 'utf-8')
|
|
172
|
+
.replace('# {{imports}}', imports)
|
|
173
|
+
.replace('# {{router paths}}', routerPaths);
|
|
174
|
+
}
|
|
222
175
|
getModuleName(step) {
|
|
223
176
|
return this.normalizeStepPath(step, true).replace(/\.py$/, '').replace(/\//g, '.');
|
|
224
177
|
}
|
|
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
|
-
}
|
|
253
|
-
}
|
|
254
178
|
}
|
|
255
179
|
exports.PythonBuilder = PythonBuilder;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PackageCopyResult {
|
|
2
|
+
copiedPackages: string[];
|
|
3
|
+
skippedPackages: string[];
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class PackageCopier {
|
|
7
|
+
private readonly projectDir;
|
|
8
|
+
private readonly pythonVersion;
|
|
9
|
+
private lambdaSitePackagesPath;
|
|
10
|
+
constructor(projectDir: string, pythonVersion?: string);
|
|
11
|
+
private getLambdaSitePackagesPath;
|
|
12
|
+
isLambdaSitePackagesAvailable(): boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PackageCopier = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const python_version_utils_1 = require("../../../../utils/python-version-utils");
|
|
10
|
+
class PackageCopier {
|
|
11
|
+
constructor(projectDir, pythonVersion = '3.13') {
|
|
12
|
+
this.projectDir = projectDir;
|
|
13
|
+
this.pythonVersion = pythonVersion;
|
|
14
|
+
this.lambdaSitePackagesPath = null;
|
|
15
|
+
}
|
|
16
|
+
getLambdaSitePackagesPath() {
|
|
17
|
+
if (this.lambdaSitePackagesPath) {
|
|
18
|
+
return this.lambdaSitePackagesPath;
|
|
19
|
+
}
|
|
20
|
+
const venvPath = path_1.default.join(this.projectDir, 'python_modules');
|
|
21
|
+
const libPath = path_1.default.join(venvPath, 'lib');
|
|
22
|
+
try {
|
|
23
|
+
const actualPythonVersionPath = (0, python_version_utils_1.findPythonSitePackagesDir)(libPath, this.pythonVersion);
|
|
24
|
+
this.lambdaSitePackagesPath = path_1.default.join(venvPath, 'lib', actualPythonVersionPath, 'site-packages-lambda');
|
|
25
|
+
if (!fs_1.default.existsSync(this.lambdaSitePackagesPath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return this.lambdaSitePackagesPath;
|
|
29
|
+
}
|
|
30
|
+
catch (_error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
isLambdaSitePackagesAvailable() {
|
|
35
|
+
return this.getLambdaSitePackagesPath() !== null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.PackageCopier = PackageCopier;
|