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.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/cloud/build/build-validation.d.ts +2 -7
  3. package/dist/cjs/cloud/build/build-validation.js +43 -5
  4. package/dist/cjs/cloud/build/builder.d.ts +7 -1
  5. package/dist/cjs/cloud/build/builder.js +16 -4
  6. package/dist/cjs/cloud/build/builders/archiver.d.ts +6 -1
  7. package/dist/cjs/cloud/build/builders/archiver.js +17 -1
  8. package/dist/cjs/cloud/build/builders/node/index.js +5 -4
  9. package/dist/cjs/cloud/build/builders/python/index.d.ts +3 -6
  10. package/dist/cjs/cloud/build/builders/python/index.js +73 -149
  11. package/dist/cjs/cloud/build/builders/python/package-copier.d.ts +13 -0
  12. package/dist/cjs/cloud/build/builders/python/package-copier.js +38 -0
  13. package/dist/cjs/cloud/build/builders/python/package-handler.d.ts +27 -0
  14. package/dist/cjs/cloud/build/builders/python/package-handler.js +209 -0
  15. package/dist/cjs/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
  16. package/dist/cjs/cloud/build/builders/python/python-dependency-analyzer.js +348 -0
  17. package/dist/cjs/cloud/build/builders/python/router_template.py +1 -0
  18. package/dist/cjs/cloud/build/builders/python/uv-packager.d.ts +1 -0
  19. package/dist/cjs/cloud/build/builders/python/uv-packager.js +44 -0
  20. package/dist/cjs/cloud/cli/build.js +6 -1
  21. package/dist/cjs/cloud/cli/deploy.js +3 -1
  22. package/dist/cjs/cloud/new-deployment/cloud-api/index.d.ts +3 -1
  23. package/dist/cjs/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
  24. package/dist/cjs/cloud/new-deployment/cloud-api/start-deployment.js +1 -1
  25. package/dist/cjs/cloud/new-deployment/deploy.d.ts +1 -0
  26. package/dist/cjs/cloud/new-deployment/deploy.js +30 -22
  27. package/dist/cjs/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
  28. package/dist/cjs/cloud/new-deployment/listeners/cli-listener.js +1 -2
  29. package/dist/cjs/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
  30. package/dist/cjs/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
  31. package/dist/esm/cloud/build/build-validation.d.ts +2 -7
  32. package/dist/esm/cloud/build/build-validation.js +43 -5
  33. package/dist/esm/cloud/build/builder.d.ts +7 -1
  34. package/dist/esm/cloud/build/builder.js +16 -4
  35. package/dist/esm/cloud/build/builders/archiver.d.ts +6 -1
  36. package/dist/esm/cloud/build/builders/archiver.js +17 -1
  37. package/dist/esm/cloud/build/builders/node/index.js +5 -4
  38. package/dist/esm/cloud/build/builders/python/index.d.ts +3 -6
  39. package/dist/esm/cloud/build/builders/python/index.js +73 -149
  40. package/dist/esm/cloud/build/builders/python/package-copier.d.ts +13 -0
  41. package/dist/esm/cloud/build/builders/python/package-copier.js +31 -0
  42. package/dist/esm/cloud/build/builders/python/package-handler.d.ts +27 -0
  43. package/dist/esm/cloud/build/builders/python/package-handler.js +202 -0
  44. package/dist/esm/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
  45. package/dist/esm/cloud/build/builders/python/python-dependency-analyzer.js +341 -0
  46. package/dist/esm/cloud/build/builders/python/router_template.py +1 -0
  47. package/dist/esm/cloud/build/builders/python/uv-packager.d.ts +1 -0
  48. package/dist/esm/cloud/build/builders/python/uv-packager.js +44 -0
  49. package/dist/esm/cloud/cli/build.js +6 -1
  50. package/dist/esm/cloud/cli/deploy.js +3 -1
  51. package/dist/esm/cloud/new-deployment/cloud-api/index.d.ts +3 -1
  52. package/dist/esm/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
  53. package/dist/esm/cloud/new-deployment/cloud-api/start-deployment.js +1 -1
  54. package/dist/esm/cloud/new-deployment/deploy.d.ts +1 -0
  55. package/dist/esm/cloud/new-deployment/deploy.js +30 -22
  56. package/dist/esm/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
  57. package/dist/esm/cloud/new-deployment/listeners/cli-listener.js +1 -2
  58. package/dist/esm/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
  59. package/dist/esm/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
  60. package/dist/types/cloud/build/build-validation.d.ts +2 -7
  61. package/dist/types/cloud/build/builder.d.ts +7 -1
  62. package/dist/types/cloud/build/builders/archiver.d.ts +6 -1
  63. package/dist/types/cloud/build/builders/python/index.d.ts +3 -6
  64. package/dist/types/cloud/build/builders/python/package-copier.d.ts +13 -0
  65. package/dist/types/cloud/build/builders/python/package-handler.d.ts +27 -0
  66. package/dist/types/cloud/build/builders/python/python-dependency-analyzer.d.ts +31 -0
  67. package/dist/types/cloud/build/builders/python/uv-packager.d.ts +1 -0
  68. package/dist/types/cloud/new-deployment/cloud-api/index.d.ts +3 -1
  69. package/dist/types/cloud/new-deployment/cloud-api/start-deployment.d.ts +4 -1
  70. package/dist/types/cloud/new-deployment/deploy.d.ts +1 -0
  71. package/dist/types/cloud/new-deployment/listeners/cli-listener.d.ts +1 -2
  72. package/dist/types/cloud/new-deployment/listeners/listener.types.d.ts +5 -1
  73. package/dist/types/cloud/new-deployment/listeners/streaming-deployment-listener.d.ts +1 -2
  74. package/package.json +6 -4
  75. package/dist/cjs/cloud/new-deployment/utils/validation.d.ts +0 -10
  76. package/dist/cjs/cloud/new-deployment/utils/validation.js +0 -107
  77. package/dist/esm/cloud/new-deployment/utils/validation.d.ts +0 -10
  78. package/dist/esm/cloud/new-deployment/utils/validation.js +0 -67
  79. 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
- ![Motia combines APIs, background queues, and AI agents into one system](https://github.com/MotiaDev/motia/raw/main/assets/Motia_Github_Repository_GIF.gif)
24
+ ![Motia combines APIs, background queues, and AI agents into one system](https://github.com/MotiaDev/motia/raw/main/assets/github-readme-banner.gif)
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 -i # runs the interactive terminal
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 -i` to use the create command in interactive mode
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, BuildStepConfig } from './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.validateCronExpression(step.config.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 > 30) {
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 30 characters.`,
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
- size: number;
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 { size, path } = await nodeBuilder.buildApiSteps(nodeSteps);
56
- this.listener.onApiRouterBuilt('node', size);
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 { size, path } = await pythonBuilder.buildApiSteps(pythonSteps);
62
- this.listener.onApiRouterBuilt('python', size);
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<number>;
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', () => resolve(this.archive.pointer()));
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 size = await archiver.finalize();
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 { size, path: zipName };
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 size = await archiver.finalize();
129
+ const { compressedSize, uncompressedSize } = await archiver.finalize();
130
130
  fs_1.default.unlinkSync(outputJsFile);
131
131
  fs_1.default.unlinkSync(outputMapFile);
132
- this.listener.onBuildEnd(step, size);
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 uvPackager;
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.uvPackager = new uv_packager_1.UvPackager(this.builder.projectDir);
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
- const tempSitePackages = path_1.default.join(constants_1.distDir, `temp-python-packages-${Date.now()}`);
24
+ let tempDirToCleanup;
25
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);
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 size = await archive.finalize();
37
- return { size, path: zipName };
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
- if (fs_1.default.existsSync(tempSitePackages)) {
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
- 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);
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]; // from X import Y ou import X
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 (error) {
201
- console.warn(`Could not analyze file: ${filePath}`);
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;