@twick/render-server 0.15.0 → 0.15.1
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 +49 -3
- package/dist/{cli.js → cli.mjs} +54 -42
- package/dist/cli.mjs.map +1 -0
- package/dist/{renderer.d.ts → index.d.mts} +2 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +66 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.mjs +147 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +19 -10
- package/package.json.bak +19 -10
- package/src/server.ts +1 -1
- package/tsconfig.json +3 -3
- package/tsup.config.ts +49 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -55
- package/dist/renderer.js.map +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -175
- package/dist/server.js.map +0 -1
- package/dist/test-render.d.ts +0 -10
- package/dist/test-render.d.ts.map +0 -1
- package/dist/test-render.js +0 -219
- package/dist/test-render.js.map +0 -1
package/README.md
CHANGED
|
@@ -48,8 +48,9 @@ The server will start on port 3001 by default. You can change this by setting th
|
|
|
48
48
|
|
|
49
49
|
### Option 2: Use Programmatically
|
|
50
50
|
|
|
51
|
-
Import and use the `renderTwickVideo` function directly:
|
|
51
|
+
Import and use the `renderTwickVideo` function directly. The package supports both ESM and CommonJS:
|
|
52
52
|
|
|
53
|
+
**ESM (import):**
|
|
53
54
|
```typescript
|
|
54
55
|
import { renderTwickVideo } from "@twick/render-server";
|
|
55
56
|
|
|
@@ -67,6 +68,24 @@ const videoPath = await renderTwickVideo(
|
|
|
67
68
|
);
|
|
68
69
|
```
|
|
69
70
|
|
|
71
|
+
**CommonJS (require):**
|
|
72
|
+
```javascript
|
|
73
|
+
const { renderTwickVideo } = require("@twick/render-server");
|
|
74
|
+
|
|
75
|
+
const videoPath = await renderTwickVideo(
|
|
76
|
+
{
|
|
77
|
+
input: {
|
|
78
|
+
properties: { width: 1920, height: 1080 },
|
|
79
|
+
// ... your project variables
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
outFile: "my-video.mp4",
|
|
84
|
+
quality: "high"
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
70
89
|
## API Endpoints
|
|
71
90
|
|
|
72
91
|
### POST /api/render-video
|
|
@@ -168,7 +187,7 @@ Renders a Twick video with the provided variables and settings.
|
|
|
168
187
|
|
|
169
188
|
**Returns:** `Promise<string>` - Path to the rendered video file
|
|
170
189
|
|
|
171
|
-
**Example:**
|
|
190
|
+
**Example (ESM):**
|
|
172
191
|
```typescript
|
|
173
192
|
import { renderTwickVideo } from "@twick/render-server";
|
|
174
193
|
|
|
@@ -187,12 +206,39 @@ const videoPath = await renderTwickVideo(
|
|
|
187
206
|
);
|
|
188
207
|
```
|
|
189
208
|
|
|
209
|
+
**Example (CommonJS):**
|
|
210
|
+
```javascript
|
|
211
|
+
const { renderTwickVideo } = require("@twick/render-server");
|
|
212
|
+
|
|
213
|
+
const videoPath = await renderTwickVideo(
|
|
214
|
+
{
|
|
215
|
+
input: {
|
|
216
|
+
properties: { width: 1920, height: 1080 },
|
|
217
|
+
tracks: [/* ... */]
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
outFile: "my-video.mp4",
|
|
222
|
+
quality: "high",
|
|
223
|
+
outDir: "./output"
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
```
|
|
227
|
+
|
|
190
228
|
> **Note:** This server will work on Linux and macOS only. Windows is not supported.
|
|
191
229
|
|
|
230
|
+
## Module Support
|
|
231
|
+
|
|
232
|
+
This package supports both ESM and CommonJS:
|
|
233
|
+
- **ESM**: Use `import { renderTwickVideo } from "@twick/render-server"`
|
|
234
|
+
- **CommonJS**: Use `const { renderTwickVideo } = require("@twick/render-server")`
|
|
235
|
+
|
|
236
|
+
The package automatically provides the correct format based on your module system.
|
|
237
|
+
|
|
192
238
|
## Browser Support
|
|
193
239
|
|
|
194
240
|
This package requires a Node.js environment with support for:
|
|
195
|
-
- Node.js
|
|
241
|
+
- Node.js 20 or higher
|
|
196
242
|
- Puppeteer for video rendering
|
|
197
243
|
- File system operations
|
|
198
244
|
|
package/dist/{cli.js → cli.mjs}
RENAMED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
2
4
|
import fs from "fs";
|
|
3
5
|
import path from "path";
|
|
4
6
|
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
var __dirname = path.dirname(__filename);
|
|
9
|
+
var SERVER_TEMPLATE = `import express from "express";
|
|
8
10
|
import cors from "cors";
|
|
9
11
|
import path from "path";
|
|
10
12
|
import { fileURLToPath } from "url";
|
|
@@ -201,7 +203,7 @@ app.listen(PORT, () => {
|
|
|
201
203
|
console.log(\`Download endpoint rate limited: \${RATE_LIMIT_MAX_REQUESTS} requests per \${RATE_LIMIT_WINDOW_MS / 60000} minutes\`);
|
|
202
204
|
});
|
|
203
205
|
`;
|
|
204
|
-
|
|
206
|
+
var PACKAGE_JSON_TEMPLATE = `{
|
|
205
207
|
"name": "twick-render-server",
|
|
206
208
|
"version": "1.0.0",
|
|
207
209
|
"type": "module",
|
|
@@ -224,7 +226,7 @@ const PACKAGE_JSON_TEMPLATE = `{
|
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
`;
|
|
227
|
-
|
|
229
|
+
var TSCONFIG_TEMPLATE = `{
|
|
228
230
|
"compilerOptions": {
|
|
229
231
|
"target": "ES2022",
|
|
230
232
|
"module": "ESNext",
|
|
@@ -247,49 +249,59 @@ const TSCONFIG_TEMPLATE = `{
|
|
|
247
249
|
"exclude": ["node_modules", "dist"]
|
|
248
250
|
}
|
|
249
251
|
`;
|
|
250
|
-
|
|
252
|
+
var GITIGNORE_TEMPLATE = `node_modules/
|
|
251
253
|
dist/
|
|
252
254
|
output/
|
|
253
255
|
.env
|
|
254
256
|
*.log
|
|
255
257
|
`;
|
|
256
258
|
function scaffoldServer() {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
259
|
+
const currentDir = process.cwd();
|
|
260
|
+
const serverDir = path.join(currentDir, "twick-render-server");
|
|
261
|
+
if (fs.existsSync(serverDir)) {
|
|
262
|
+
console.error(`Error: Directory "${serverDir}" already exists.`);
|
|
263
|
+
console.error("Please remove it or choose a different location.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
267
|
+
fs.mkdirSync(path.join(serverDir, "output"), { recursive: true });
|
|
268
|
+
fs.writeFileSync(
|
|
269
|
+
path.join(serverDir, "server.ts"),
|
|
270
|
+
SERVER_TEMPLATE,
|
|
271
|
+
"utf-8"
|
|
272
|
+
);
|
|
273
|
+
fs.writeFileSync(
|
|
274
|
+
path.join(serverDir, "package.json"),
|
|
275
|
+
PACKAGE_JSON_TEMPLATE,
|
|
276
|
+
"utf-8"
|
|
277
|
+
);
|
|
278
|
+
fs.writeFileSync(
|
|
279
|
+
path.join(serverDir, "tsconfig.json"),
|
|
280
|
+
TSCONFIG_TEMPLATE,
|
|
281
|
+
"utf-8"
|
|
282
|
+
);
|
|
283
|
+
fs.writeFileSync(
|
|
284
|
+
path.join(serverDir, ".gitignore"),
|
|
285
|
+
GITIGNORE_TEMPLATE,
|
|
286
|
+
"utf-8"
|
|
287
|
+
);
|
|
288
|
+
console.log(`\u2705 Successfully scaffolded Twick render server!`);
|
|
289
|
+
console.log(`
|
|
290
|
+
\u{1F4C1} Server created at: ${serverDir}`);
|
|
291
|
+
console.log(`
|
|
292
|
+
\u{1F4DD} Next steps:`);
|
|
293
|
+
console.log(` 1. cd ${path.basename(serverDir)}`);
|
|
294
|
+
console.log(` 2. npm install`);
|
|
295
|
+
console.log(` 3. npm run dev (for development)`);
|
|
296
|
+
console.log(` 4. npm run build && npm start (for production)`);
|
|
283
297
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const command = args[0];
|
|
298
|
+
var args = process.argv.slice(2);
|
|
299
|
+
var command = args[0];
|
|
287
300
|
if (command === "init") {
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
process.exit(1);
|
|
301
|
+
scaffoldServer();
|
|
302
|
+
} else {
|
|
303
|
+
console.log("Usage: npx twick-render-server init");
|
|
304
|
+
console.log("\nThis command scaffolds a new Twick render server in the current directory.");
|
|
305
|
+
process.exit(1);
|
|
294
306
|
}
|
|
295
|
-
//# sourceMappingURL=cli.
|
|
307
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst SERVER_TEMPLATE = `import express from \"express\";\nimport cors from \"cors\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { renderTwickVideo } from \"@twick/render-server\";\n\nconst PORT = process.env.PORT || 3001;\nconst BASE_PATH = \\`http://localhost:\\${PORT}\\`;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Rate limiting configuration\nconst RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes\nconst RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window\nconst RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute\n\n// In-memory store for rate limiting\ninterface RateLimitEntry {\n count: number;\n resetTime: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// Cleanup expired entries periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now > entry.resetTime) {\n rateLimitStore.delete(key);\n }\n }\n}, RATE_LIMIT_CLEANUP_INTERVAL_MS);\n\n/**\n * Rate limiting middleware for API endpoints.\n * Tracks request counts per IP address and enforces rate limits\n * to prevent abuse of the render server.\n *\n * @param req - Express request object\n * @param res - Express response object\n * @param next - Express next function\n * \n * @example\n * \\`\\`\\`js\n * app.use('/api', rateLimitMiddleware);\n * // Applies rate limiting to all /api routes\n * \\`\\`\\`\n */\nconst rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {\n const clientIP = req.ip || req.connection.remoteAddress || 'unknown';\n const now = Date.now();\n \n // Get or create rate limit entry for this IP\n let entry = rateLimitStore.get(clientIP);\n \n if (!entry || now > entry.resetTime) {\n // New window or expired entry\n entry = {\n count: 1,\n resetTime: now + RATE_LIMIT_WINDOW_MS\n };\n rateLimitStore.set(clientIP, entry);\n } else {\n // Increment count in current window\n entry.count++;\n \n if (entry.count > RATE_LIMIT_MAX_REQUESTS) {\n // Rate limit exceeded\n const retryAfter = Math.ceil((entry.resetTime - now) / 1000);\n res.set('Retry-After', retryAfter.toString());\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', '0');\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n return res.status(429).json({\n success: false,\n error: 'Too many requests',\n message: \\`Rate limit exceeded. Try again in \\${retryAfter} seconds.\\`,\n retryAfter\n });\n }\n }\n \n // Add rate limit headers\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n next();\n};\n\nconst app = express();\n\napp.use(cors());\napp.use(express.json());\n\n// Serve static files from output directory\napp.use(\"/output\", express.static(path.join(__dirname, \"output\")));\n\n/**\n * POST endpoint for video rendering requests.\n * Accepts project variables and settings, renders the video,\n * and returns a download URL for the completed video.\n *\n * @param req - Express request object containing variables and settings\n * @param res - Express response object\n * \n * @example\n * \\`\\`\\`js\n * POST /api/render-video\n * Body: { variables: {...}, settings: {...} }\n * Response: { success: true, downloadUrl: \"...\" }\n * \\`\\`\\`\n */\napp.post(\"/api/render-video\", async (req, res) => {\n const { variables, settings } = req.body;\n\n try {\n const outputPath = await renderTwickVideo(variables, settings);\n res.json({\n success: true,\n downloadUrl: \\`\\${BASE_PATH}/download/\\${path.basename(outputPath)}\\`,\n });\n } catch (error) {\n console.error(\"Render error:\", error);\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n});\n\n/**\n * GET endpoint for downloading rendered videos.\n * Serves video files with rate limiting and security checks\n * to prevent path traversal attacks.\n *\n * @param req - Express request object with filename parameter\n * @param res - Express response object\n * \n * @example\n * \\`\\`\\`js\n * GET /download/video-123.mp4\n * // Downloads the specified video file\n * \\`\\`\\`\n */\napp.get(\"/download/:filename\", rateLimitMiddleware, (req, res) => {\n const outputDir = path.resolve(__dirname, \"output\");\n const requestedPath = path.resolve(outputDir, req.params.filename);\n if (!requestedPath.startsWith(outputDir + path.sep)) {\n // Attempted path traversal or access outside output directory\n res.status(403).json({\n success: false,\n error: \"Forbidden\",\n });\n return;\n }\n res.download(requestedPath, (err) => {\n if (err) {\n res.status(404).json({\n success: false,\n error: \"File not found\",\n });\n }\n });\n});\n\n/**\n * Health check endpoint for monitoring server status.\n * Returns server status and current timestamp for health monitoring.\n *\n * @param req - Express request object\n * @param res - Express response object\n * \n * @example\n * \\`\\`\\`js\n * GET /health\n * Response: { status: \"ok\", timestamp: \"2024-01-01T00:00:00.000Z\" }\n * \\`\\`\\`\n */\napp.get(\"/health\", (req, res) => {\n res.json({\n status: \"ok\",\n timestamp: new Date().toISOString(),\n });\n});\n\n// Start the server\napp.listen(PORT, () => {\n console.log(\\`Render server running on port \\${PORT}\\`);\n console.log(\\`Health check: \\${BASE_PATH}/health\\`);\n console.log(\\`API endpoint: \\${BASE_PATH}/api/render-video\\`);\n console.log(\\`Download endpoint rate limited: \\${RATE_LIMIT_MAX_REQUESTS} requests per \\${RATE_LIMIT_WINDOW_MS / 60000} minutes\\`);\n});\n`;\n\nconst PACKAGE_JSON_TEMPLATE = `{\n \"name\": \"twick-render-server\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"tsx watch server.ts\",\n \"build\": \"tsc\",\n \"start\": \"node dist/server.js\"\n },\n \"dependencies\": {\n \"@twick/render-server\": \"^0.14.11\",\n \"cors\": \"^2.8.5\",\n \"express\": \"^4.18.2\"\n },\n \"devDependencies\": {\n \"@types/cors\": \"^2.8.17\",\n \"@types/express\": \"^4.17.21\",\n \"@types/node\": \"^20.10.0\",\n \"tsx\": \"^4.7.0\",\n \"typescript\": \"5.4.2\"\n }\n}\n`;\n\nconst TSCONFIG_TEMPLATE = `{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"allowSyntheticDefaultImports\": true,\n \"esModuleInterop\": true,\n \"allowJs\": true,\n \"strict\": true,\n \"skipLibCheck\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"outDir\": \"./dist\",\n \"rootDir\": \".\",\n \"declaration\": true,\n \"declarationMap\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"noEmit\": false\n },\n \"include\": [\"server.ts\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n`;\n\nconst GITIGNORE_TEMPLATE = `node_modules/\ndist/\noutput/\n.env\n*.log\n`;\n\nfunction scaffoldServer() {\n const currentDir = process.cwd();\n const serverDir = path.join(currentDir, \"twick-render-server\");\n\n // Check if directory already exists\n if (fs.existsSync(serverDir)) {\n console.error(`Error: Directory \"${serverDir}\" already exists.`);\n console.error(\"Please remove it or choose a different location.\");\n process.exit(1);\n }\n\n // Create server directory\n fs.mkdirSync(serverDir, { recursive: true });\n fs.mkdirSync(path.join(serverDir, \"output\"), { recursive: true });\n\n // Write server.ts\n fs.writeFileSync(\n path.join(serverDir, \"server.ts\"),\n SERVER_TEMPLATE,\n \"utf-8\"\n );\n\n // Write package.json\n fs.writeFileSync(\n path.join(serverDir, \"package.json\"),\n PACKAGE_JSON_TEMPLATE,\n \"utf-8\"\n );\n\n // Write tsconfig.json\n fs.writeFileSync(\n path.join(serverDir, \"tsconfig.json\"),\n TSCONFIG_TEMPLATE,\n \"utf-8\"\n );\n\n // Write .gitignore\n fs.writeFileSync(\n path.join(serverDir, \".gitignore\"),\n GITIGNORE_TEMPLATE,\n \"utf-8\"\n );\n\n console.log(`✅ Successfully scaffolded Twick render server!`);\n console.log(`\\n📁 Server created at: ${serverDir}`);\n console.log(`\\n📝 Next steps:`);\n console.log(` 1. cd ${path.basename(serverDir)}`);\n console.log(` 2. npm install`);\n console.log(` 3. npm run dev (for development)`);\n console.log(` 4. npm run build && npm start (for production)`);\n}\n\n// Parse command line arguments\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nif (command === \"init\") {\n scaffoldServer();\n} else {\n console.log(\"Usage: npx twick-render-server init\");\n console.log(\"\\nThis command scaffolds a new Twick render server in the current directory.\");\n process.exit(1);\n}\n\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Renders a Twick video with the provided variables and settings.
|
|
3
|
+
* Processes project variables, merges settings with defaults, and
|
|
4
|
+
* generates a video file using the Twick renderer.
|
|
5
|
+
*
|
|
6
|
+
* @param variables - Project variables containing input configuration
|
|
7
|
+
* @param settings - Optional render settings to override defaults
|
|
8
|
+
* @returns Promise resolving to the path of the rendered video file
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```js
|
|
12
|
+
* const videoPath = await renderTwickVideo(
|
|
13
|
+
* { input: { properties: { width: 1920, height: 1080 } } },
|
|
14
|
+
* { quality: "high", outFile: "my-video.mp4" }
|
|
15
|
+
* );
|
|
16
|
+
* // videoPath = "./output/my-video.mp4"
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare const renderTwickVideo: (variables: any, settings: any) => Promise<string>;
|
|
20
|
+
|
|
21
|
+
export { renderTwickVideo };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
renderTwickVideo: () => renderer_default
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/renderer.ts
|
|
28
|
+
var import_renderer = require("@twick/renderer");
|
|
29
|
+
var renderTwickVideo = async (variables, settings) => {
|
|
30
|
+
try {
|
|
31
|
+
const { input } = variables;
|
|
32
|
+
const { properties } = input;
|
|
33
|
+
const mergedSettings = {
|
|
34
|
+
logProgress: true,
|
|
35
|
+
outDir: "./output",
|
|
36
|
+
outFile: properties.reqesutId ?? `video-${Date.now()}.mp4`,
|
|
37
|
+
quality: "medium",
|
|
38
|
+
projectSettings: {
|
|
39
|
+
exporter: {
|
|
40
|
+
name: "@twick/core/wasm"
|
|
41
|
+
},
|
|
42
|
+
size: {
|
|
43
|
+
x: properties.width,
|
|
44
|
+
y: properties.height
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
...settings
|
|
48
|
+
// Allow user settings to override defaults
|
|
49
|
+
};
|
|
50
|
+
const file = await (0, import_renderer.renderVideo)({
|
|
51
|
+
projectFile: "@twick/visualizer/dist/project.js",
|
|
52
|
+
variables,
|
|
53
|
+
settings: mergedSettings
|
|
54
|
+
});
|
|
55
|
+
console.log("Successfully rendered: ", file);
|
|
56
|
+
return file;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Render error:", error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var renderer_default = renderTwickVideo;
|
|
63
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
64
|
+
0 && (module.exports = {
|
|
65
|
+
renderTwickVideo
|
|
66
|
+
});
|
|
3
67
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/renderer.ts"],"sourcesContent":["// Export the renderer function\nexport { default as renderTwickVideo } from \"./renderer\";\n\n","import { renderVideo } from \"@twick/renderer\";\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n try {\n const { input } = variables;\n const { properties } = input;\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: properties.width,\n y: properties.height,\n },\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables: variables,\n settings: mergedSettings,\n });\n console.log(\"Successfully rendered: \", file);\n return file;\n } catch (error) {\n console.error(\"Render error:\", error);\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,sBAA4B;AAoB5B,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAEvB,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG,WAAW;AAAA,UACd,GAAG,WAAW;AAAA,QAChB;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,OAAO,UAAM,6BAAY;AAAA,MAC7B,aAAa;AAAA,MACb;AAAA,MACA,UAAU;AAAA,IACZ,CAAC;AACD,YAAQ,IAAI,2BAA2B,IAAI;AAC3C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,iBAAiB,KAAK;AACpC,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/renderer.ts
|
|
2
|
+
import { renderVideo } from "@twick/renderer";
|
|
3
|
+
var renderTwickVideo = async (variables, settings) => {
|
|
4
|
+
try {
|
|
5
|
+
const { input } = variables;
|
|
6
|
+
const { properties } = input;
|
|
7
|
+
const mergedSettings = {
|
|
8
|
+
logProgress: true,
|
|
9
|
+
outDir: "./output",
|
|
10
|
+
outFile: properties.reqesutId ?? `video-${Date.now()}.mp4`,
|
|
11
|
+
quality: "medium",
|
|
12
|
+
projectSettings: {
|
|
13
|
+
exporter: {
|
|
14
|
+
name: "@twick/core/wasm"
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
x: properties.width,
|
|
18
|
+
y: properties.height
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
...settings
|
|
22
|
+
// Allow user settings to override defaults
|
|
23
|
+
};
|
|
24
|
+
const file = await renderVideo({
|
|
25
|
+
projectFile: "@twick/visualizer/dist/project.js",
|
|
26
|
+
variables,
|
|
27
|
+
settings: mergedSettings
|
|
28
|
+
});
|
|
29
|
+
console.log("Successfully rendered: ", file);
|
|
30
|
+
return file;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("Render error:", error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var renderer_default = renderTwickVideo;
|
|
37
|
+
export {
|
|
38
|
+
renderer_default as renderTwickVideo
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/renderer.ts"],"sourcesContent":["import { renderVideo } from \"@twick/renderer\";\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n try {\n const { input } = variables;\n const { properties } = input;\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: properties.width,\n y: properties.height,\n },\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables: variables,\n settings: mergedSettings,\n });\n console.log(\"Successfully rendered: \", file);\n return file;\n } catch (error) {\n console.error(\"Render error:\", error);\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";AAAA,SAAS,mBAAmB;AAoB5B,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAEvB,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG,WAAW;AAAA,UACd,GAAG,WAAW;AAAA,QAChB;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,OAAO,MAAM,YAAY;AAAA,MAC7B,aAAa;AAAA,MACb;AAAA,MACA,UAAU;AAAA,IACZ,CAAC;AACD,YAAQ,IAAI,2BAA2B,IAAI;AAC3C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,iBAAiB,KAAK;AACpC,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/renderer.ts
|
|
8
|
+
import { renderVideo } from "@twick/renderer";
|
|
9
|
+
var renderTwickVideo = async (variables, settings) => {
|
|
10
|
+
try {
|
|
11
|
+
const { input } = variables;
|
|
12
|
+
const { properties } = input;
|
|
13
|
+
const mergedSettings = {
|
|
14
|
+
logProgress: true,
|
|
15
|
+
outDir: "./output",
|
|
16
|
+
outFile: properties.reqesutId ?? `video-${Date.now()}.mp4`,
|
|
17
|
+
quality: "medium",
|
|
18
|
+
projectSettings: {
|
|
19
|
+
exporter: {
|
|
20
|
+
name: "@twick/core/wasm"
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
x: properties.width,
|
|
24
|
+
y: properties.height
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
...settings
|
|
28
|
+
// Allow user settings to override defaults
|
|
29
|
+
};
|
|
30
|
+
const file = await renderVideo({
|
|
31
|
+
projectFile: "@twick/visualizer/dist/project.js",
|
|
32
|
+
variables,
|
|
33
|
+
settings: mergedSettings
|
|
34
|
+
});
|
|
35
|
+
console.log("Successfully rendered: ", file);
|
|
36
|
+
return file;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Render error:", error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var renderer_default = renderTwickVideo;
|
|
43
|
+
|
|
44
|
+
// src/server.ts
|
|
45
|
+
var PORT = process.env.PORT || 3001;
|
|
46
|
+
var BASE_PATH = `http://localhost:${PORT}`;
|
|
47
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
48
|
+
var __dirname = path.dirname(__filename);
|
|
49
|
+
var RATE_LIMIT_WINDOW_MS = 15 * 60 * 1e3;
|
|
50
|
+
var RATE_LIMIT_MAX_REQUESTS = 100;
|
|
51
|
+
var RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
52
|
+
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
53
|
+
setInterval(() => {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
56
|
+
if (now > entry.resetTime) {
|
|
57
|
+
rateLimitStore.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
61
|
+
var rateLimitMiddleware = (req, res, next) => {
|
|
62
|
+
const clientIP = req.ip || req.connection.remoteAddress || "unknown";
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
let entry = rateLimitStore.get(clientIP);
|
|
65
|
+
if (!entry || now > entry.resetTime) {
|
|
66
|
+
entry = {
|
|
67
|
+
count: 1,
|
|
68
|
+
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
69
|
+
};
|
|
70
|
+
rateLimitStore.set(clientIP, entry);
|
|
71
|
+
} else {
|
|
72
|
+
entry.count++;
|
|
73
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
74
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1e3);
|
|
75
|
+
res.set("Retry-After", retryAfter.toString());
|
|
76
|
+
res.set("X-RateLimit-Limit", RATE_LIMIT_MAX_REQUESTS.toString());
|
|
77
|
+
res.set("X-RateLimit-Remaining", "0");
|
|
78
|
+
res.set("X-RateLimit-Reset", new Date(entry.resetTime).toISOString());
|
|
79
|
+
return res.status(429).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: "Too many requests",
|
|
82
|
+
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
83
|
+
retryAfter
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
res.set("X-RateLimit-Limit", RATE_LIMIT_MAX_REQUESTS.toString());
|
|
88
|
+
res.set("X-RateLimit-Remaining", (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
|
|
89
|
+
res.set("X-RateLimit-Reset", new Date(entry.resetTime).toISOString());
|
|
90
|
+
next();
|
|
91
|
+
};
|
|
92
|
+
var nodeApp = express();
|
|
93
|
+
nodeApp.use(cors());
|
|
94
|
+
nodeApp.use(express.json());
|
|
95
|
+
nodeApp.use("/output", express.static(path.join(__dirname, "../output")));
|
|
96
|
+
nodeApp.post("/api/render-video", async (req, res) => {
|
|
97
|
+
const { variables, settings } = req.body;
|
|
98
|
+
try {
|
|
99
|
+
const outputPath = await renderer_default(variables, settings);
|
|
100
|
+
res.json({
|
|
101
|
+
success: true,
|
|
102
|
+
downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Render error:", error);
|
|
106
|
+
res.status(500).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
nodeApp.get("/download/:filename", rateLimitMiddleware, (req, res) => {
|
|
113
|
+
const outputDir = path.resolve(__dirname, "../output");
|
|
114
|
+
const requestedPath = path.resolve(outputDir, req.params.filename);
|
|
115
|
+
if (!requestedPath.startsWith(outputDir + path.sep)) {
|
|
116
|
+
res.status(403).json({
|
|
117
|
+
success: false,
|
|
118
|
+
error: "Forbidden"
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
res.download(requestedPath, (err) => {
|
|
123
|
+
if (err) {
|
|
124
|
+
res.status(404).json({
|
|
125
|
+
success: false,
|
|
126
|
+
error: "File not found"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
nodeApp.get("/health", (req, res) => {
|
|
132
|
+
res.json({
|
|
133
|
+
status: "ok",
|
|
134
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
var server_default = nodeApp;
|
|
138
|
+
nodeApp.listen(PORT, () => {
|
|
139
|
+
console.log(`Render server running on port ${PORT}`);
|
|
140
|
+
console.log(`Health check: ${BASE_PATH}/health`);
|
|
141
|
+
console.log(`API endpoint: ${BASE_PATH}/api/render-video`);
|
|
142
|
+
console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 6e4} minutes`);
|
|
143
|
+
});
|
|
144
|
+
export {
|
|
145
|
+
server_default as default
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/renderer.ts"],"sourcesContent":["import express from \"express\";\nimport cors from \"cors\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport renderTwickVideo from \"./renderer\";\n\nconst PORT = process.env.PORT || 3001;\nconst BASE_PATH = `http://localhost:${PORT}`;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Rate limiting configuration\nconst RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes\nconst RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window\nconst RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute\n\n// In-memory store for rate limiting\ninterface RateLimitEntry {\n count: number;\n resetTime: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// Cleanup expired entries periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now > entry.resetTime) {\n rateLimitStore.delete(key);\n }\n }\n}, RATE_LIMIT_CLEANUP_INTERVAL_MS);\n\n/**\n * Rate limiting middleware for API endpoints.\n * Tracks request counts per IP address and enforces rate limits\n * to prevent abuse of the render server.\n *\n * @param req - Express request object\n * @param res - Express response object\n * @param next - Express next function\n * \n * @example\n * ```js\n * app.use('/api', rateLimitMiddleware);\n * // Applies rate limiting to all /api routes\n * ```\n */\nconst rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {\n const clientIP = req.ip || req.connection.remoteAddress || 'unknown';\n const now = Date.now();\n \n // Get or create rate limit entry for this IP\n let entry = rateLimitStore.get(clientIP);\n \n if (!entry || now > entry.resetTime) {\n // New window or expired entry\n entry = {\n count: 1,\n resetTime: now + RATE_LIMIT_WINDOW_MS\n };\n rateLimitStore.set(clientIP, entry);\n } else {\n // Increment count in current window\n entry.count++;\n \n if (entry.count > RATE_LIMIT_MAX_REQUESTS) {\n // Rate limit exceeded\n const retryAfter = Math.ceil((entry.resetTime - now) / 1000);\n res.set('Retry-After', retryAfter.toString());\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', '0');\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n return res.status(429).json({\n success: false,\n error: 'Too many requests',\n message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,\n retryAfter\n });\n }\n }\n \n // Add rate limit headers\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n next();\n};\n\nconst nodeApp: import(\"express\").Express = express();\n\nnodeApp.use(cors());\nnodeApp.use(express.json());\n\n// Serve static files from output directory\nnodeApp.use(\"/output\", express.static(path.join(__dirname, \"../output\")));\n\n/**\n * POST endpoint for video rendering requests.\n * Accepts project variables and settings, renders the video,\n * and returns a download URL for the completed video.\n *\n * @param req - Express request object containing variables and settings\n * @param res - Express response object\n * \n * @example\n * ```js\n * POST /api/render-video\n * Body: { variables: {...}, settings: {...} }\n * Response: { success: true, downloadUrl: \"...\" }\n * ```\n */\nnodeApp.post(\"/api/render-video\", async (req, res) => {\n const { variables, settings } = req.body;\n\n try {\n const outputPath = await renderTwickVideo(variables, settings);\n res.json({\n success: true,\n downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,\n });\n } catch (error) {\n console.error(\"Render error:\", error);\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n});\n\n/**\n * GET endpoint for downloading rendered videos.\n * Serves video files with rate limiting and security checks\n * to prevent path traversal attacks.\n *\n * @param req - Express request object with filename parameter\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /download/video-123.mp4\n * // Downloads the specified video file\n * ```\n */\nnodeApp.get(\"/download/:filename\", rateLimitMiddleware, (req, res) => {\n const outputDir = path.resolve(__dirname, \"../output\");\n const requestedPath = path.resolve(outputDir, req.params.filename);\n if (!requestedPath.startsWith(outputDir + path.sep)) {\n // Attempted path traversal or access outside output directory\n res.status(403).json({\n success: false,\n error: \"Forbidden\",\n });\n return;\n }\n res.download(requestedPath, (err) => {\n if (err) {\n res.status(404).json({\n success: false,\n error: \"File not found\",\n });\n }\n });\n});\n\n/**\n * Health check endpoint for monitoring server status.\n * Returns server status and current timestamp for health monitoring.\n *\n * @param req - Express request object\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /health\n * Response: { status: \"ok\", timestamp: \"2024-01-01T00:00:00.000Z\" }\n * ```\n */\nnodeApp.get(\"/health\", (req, res) => {\n res.json({\n status: \"ok\",\n timestamp: new Date().toISOString(),\n });\n});\n\n// Export the app for programmatic usage\nexport default nodeApp;\n\n// Start the server\nnodeApp.listen(PORT, () => {\n console.log(`Render server running on port ${PORT}`);\n console.log(`Health check: ${BASE_PATH}/health`);\n console.log(`API endpoint: ${BASE_PATH}/api/render-video`);\n console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);\n});\n","import { renderVideo } from \"@twick/renderer\";\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n try {\n const { input } = variables;\n const { properties } = input;\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: properties.width,\n y: properties.height,\n },\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables: variables,\n settings: mergedSettings,\n });\n console.log(\"Successfully rendered: \", file);\n return file;\n } catch (error) {\n console.error(\"Render error:\", error);\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twick/render-server",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
7
15
|
"bin": {
|
|
8
16
|
"twick-render-server": "./dist/cli.js"
|
|
9
17
|
},
|
|
10
18
|
"scripts": {
|
|
11
|
-
"build": "
|
|
19
|
+
"build": "tsup",
|
|
12
20
|
"dev": "tsx watch src/server.ts",
|
|
13
21
|
"start": "node dist/server.js",
|
|
14
22
|
"test:render": "tsx src/test-render.ts",
|
|
@@ -18,12 +26,12 @@
|
|
|
18
26
|
"access": "public"
|
|
19
27
|
},
|
|
20
28
|
"dependencies": {
|
|
21
|
-
"@twick/2d": "^0.15.
|
|
22
|
-
"@twick/core": "^0.15.
|
|
23
|
-
"@twick/ffmpeg": "^0.15.
|
|
24
|
-
"@twick/renderer": "^0.15.
|
|
25
|
-
"@twick/ui": "^0.15.
|
|
26
|
-
"@twick/visualizer": "^0.15.
|
|
29
|
+
"@twick/2d": "^0.15.1",
|
|
30
|
+
"@twick/core": "^0.15.1",
|
|
31
|
+
"@twick/ffmpeg": "^0.15.1",
|
|
32
|
+
"@twick/renderer": "^0.15.1",
|
|
33
|
+
"@twick/ui": "^0.15.1",
|
|
34
|
+
"@twick/visualizer": "^0.15.1",
|
|
27
35
|
"cors": "^2.8.5",
|
|
28
36
|
"express": "^4.18.2",
|
|
29
37
|
"express-rate-limit": "^8.0.1",
|
|
@@ -35,6 +43,7 @@
|
|
|
35
43
|
"@types/express": "^4.17.21",
|
|
36
44
|
"@types/node": "^20.10.0",
|
|
37
45
|
"rimraf": "^5.0.5",
|
|
46
|
+
"tsup": "^8.0.0",
|
|
38
47
|
"tsx": "^4.7.0",
|
|
39
48
|
"typescript": "5.4.2"
|
|
40
49
|
},
|