@scenar/cli 0.1.20 → 0.2.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/example-bundle/assets/index-BLQ14SEo.js +147 -0
- package/example-bundle/assets/logo-f-5LjVqJ.png +0 -0
- package/example-bundle/assets/style-Dnzx_jUO.css +1 -0
- package/example-bundle/index.html +13 -0
- package/example-bundle/pack-manifest.json +36 -0
- package/example-bundle/scenario.json +9 -0
- package/package.json +17 -5
- package/src/__tests__/pack-generate-embed-entry.test.ts +59 -6
- package/src/__tests__/publish-command.test.ts +46 -0
- package/src/__tests__/publish-flow.test.ts +220 -0
- package/src/__tests__/read-viewport.test.ts +45 -0
- package/src/__tests__/run-publish.test.ts +92 -0
- package/src/__tests__/serve-command.test.ts +44 -0
- package/src/__tests__/serve-request-path.test.ts +67 -0
- package/src/__tests__/serve-static-server.test.ts +88 -0
- package/src/__tests__/try-command.test.ts +21 -0
- package/src/api.d.ts +31 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +28 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +55 -0
- package/src/bundle/read-viewport.d.ts +23 -0
- package/src/bundle/read-viewport.d.ts.map +1 -0
- package/src/bundle/read-viewport.js +25 -0
- package/src/bundle/read-viewport.js.map +1 -0
- package/src/bundle/read-viewport.ts +36 -0
- package/src/commands/deploy.d.ts.map +1 -1
- package/src/commands/deploy.js +8 -24
- package/src/commands/deploy.js.map +1 -1
- package/src/commands/deploy.ts +10 -28
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +39 -170
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +48 -289
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +12 -112
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +12 -128
- package/src/commands/publish.d.ts +3 -0
- package/src/commands/publish.d.ts.map +1 -0
- package/src/commands/publish.js +64 -0
- package/src/commands/publish.js.map +1 -0
- package/src/commands/publish.ts +86 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +12 -194
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +19 -286
- package/src/commands/serve.d.ts +3 -0
- package/src/commands/serve.d.ts.map +1 -0
- package/src/commands/serve.js +91 -0
- package/src/commands/serve.js.map +1 -0
- package/src/commands/serve.ts +112 -0
- package/src/commands/try.d.ts +3 -0
- package/src/commands/try.d.ts.map +1 -0
- package/src/commands/try.js +99 -0
- package/src/commands/try.js.map +1 -0
- package/src/commands/try.ts +122 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +6 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +6 -0
- package/src/narrate/run-narrate.d.ts +43 -0
- package/src/narrate/run-narrate.d.ts.map +1 -0
- package/src/narrate/run-narrate.js +174 -0
- package/src/narrate/run-narrate.js.map +1 -0
- package/src/narrate/run-narrate.ts +298 -0
- package/src/pack/generate-embed-entry.d.ts +11 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +81 -10
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +81 -10
- package/src/pack/run-pack.d.ts +34 -0
- package/src/pack/run-pack.d.ts.map +1 -0
- package/src/pack/run-pack.js +117 -0
- package/src/pack/run-pack.js.map +1 -0
- package/src/pack/run-pack.ts +163 -0
- package/src/publish/github.d.ts +11 -0
- package/src/publish/github.d.ts.map +1 -0
- package/src/publish/github.js +197 -0
- package/src/publish/github.js.map +1 -0
- package/src/publish/github.ts +216 -0
- package/src/publish/publish-flow.d.ts +110 -0
- package/src/publish/publish-flow.d.ts.map +1 -0
- package/src/publish/publish-flow.js +117 -0
- package/src/publish/publish-flow.js.map +1 -0
- package/src/publish/publish-flow.ts +199 -0
- package/src/publish/run-publish.d.ts +41 -0
- package/src/publish/run-publish.d.ts.map +1 -0
- package/src/publish/run-publish.js +77 -0
- package/src/publish/run-publish.js.map +1 -0
- package/src/publish/run-publish.ts +131 -0
- package/src/render/run-render.d.ts +31 -0
- package/src/render/run-render.d.ts.map +1 -0
- package/src/render/run-render.js +196 -0
- package/src/render/run-render.js.map +1 -0
- package/src/render/run-render.ts +287 -0
- package/src/serve/request-path.d.ts +23 -0
- package/src/serve/request-path.d.ts.map +1 -0
- package/src/serve/request-path.js +52 -0
- package/src/serve/request-path.js.map +1 -0
- package/src/serve/request-path.ts +55 -0
- package/src/serve/run-serve.d.ts +31 -0
- package/src/serve/run-serve.d.ts.map +1 -0
- package/src/serve/run-serve.js +35 -0
- package/src/serve/run-serve.js.map +1 -0
- package/src/serve/run-serve.ts +68 -0
- package/src/serve/static-server.d.ts +38 -0
- package/src/serve/static-server.d.ts.map +1 -0
- package/src/serve/static-server.js +91 -0
- package/src/serve/static-server.js.map +1 -0
- package/src/serve/static-server.ts +118 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/render.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { stat, mkdir, access, writeFile, rm } from "node:fs/promises";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { loadBundle } from "../util/load-bundle.js";
|
|
5
|
-
import { generateRemotionEntry } from "../render/generate-entry.js";
|
|
6
|
-
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
7
|
-
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
1
|
+
import { runRender } from "../render/run-render.js";
|
|
8
2
|
export function registerRenderCommand(program) {
|
|
9
3
|
program
|
|
10
4
|
.command("render")
|
|
@@ -25,201 +19,25 @@ export function registerRenderCommand(program) {
|
|
|
25
19
|
.option("--entry <path>", "path to a custom Remotion entry file")
|
|
26
20
|
.option("--webpack-override <path>", "path to a module that default-exports a Remotion WebpackOverrideFn")
|
|
27
21
|
.action(async (dir, options) => {
|
|
28
|
-
const resolved = resolve(dir);
|
|
29
|
-
let info;
|
|
30
22
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
process.exitCode = 1;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const fps = Number(options.fps) || 30;
|
|
45
|
-
const width = Number(options.width) || 1920;
|
|
46
|
-
const height = Number(options.height) || 1080;
|
|
47
|
-
let tempDir;
|
|
48
|
-
try {
|
|
49
|
-
const bundle = await loadBundle(resolved);
|
|
50
|
-
const scenarioId = bundle.id;
|
|
51
|
-
const compositionId = options.compositionId ?? scenarioId;
|
|
52
|
-
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
53
|
-
const { entryPoint, generated } = await resolveEntryPoint({
|
|
54
|
-
entryOption: options.entry,
|
|
55
|
-
scenarioDir: resolved,
|
|
56
|
-
scenarioId,
|
|
57
|
-
compositionId,
|
|
58
|
-
hasNarration: !!bundle.narrationManifest,
|
|
59
|
-
fps,
|
|
60
|
-
width,
|
|
61
|
-
height,
|
|
62
|
-
});
|
|
63
|
-
tempDir = generated?.tempDir;
|
|
64
|
-
const webpackOverride = options.webpackOverride
|
|
65
|
-
? await loadWebpackOverride(options.webpackOverride)
|
|
66
|
-
: undefined;
|
|
67
|
-
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
68
|
-
process.stderr.write(`Steps: ${bundle.steps.length}\n`);
|
|
69
|
-
process.stderr.write(`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}\n`);
|
|
70
|
-
process.stderr.write(`Entry: ${generated ? "(auto-generated)" : entryPoint}\n`);
|
|
71
|
-
if (generated?.providersPath) {
|
|
72
|
-
process.stderr.write(`Providers: ${generated.providersPath}\n`);
|
|
73
|
-
}
|
|
74
|
-
if (webpackOverride) {
|
|
75
|
-
process.stderr.write(`Webpack: custom override loaded\n`);
|
|
76
|
-
}
|
|
77
|
-
process.stderr.write(`Output: ${outputPath}\n`);
|
|
78
|
-
process.stderr.write(`Config: ${width}x${height} @ ${fps}fps\n\n`);
|
|
79
|
-
await renderScenario({
|
|
80
|
-
bundle,
|
|
81
|
-
entryPoint,
|
|
82
|
-
compositionId,
|
|
83
|
-
outputPath,
|
|
84
|
-
fps,
|
|
85
|
-
width,
|
|
86
|
-
height,
|
|
87
|
-
webpackOverride,
|
|
23
|
+
const result = await runRender({
|
|
24
|
+
scenarioDir: dir,
|
|
25
|
+
out: options.out,
|
|
26
|
+
fps: Number(options.fps) || 30,
|
|
27
|
+
width: Number(options.width) || 1920,
|
|
28
|
+
height: Number(options.height) || 1080,
|
|
29
|
+
compositionId: options.compositionId,
|
|
30
|
+
entry: options.entry,
|
|
31
|
+
webpackOverride: options.webpackOverride,
|
|
32
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
88
33
|
});
|
|
89
|
-
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${outputPath}\n`);
|
|
34
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${result.outputPath}\n`);
|
|
90
35
|
}
|
|
91
36
|
catch (error) {
|
|
92
37
|
const msg = error instanceof Error ? error.message : String(error);
|
|
93
38
|
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
94
39
|
process.exitCode = 1;
|
|
95
40
|
}
|
|
96
|
-
finally {
|
|
97
|
-
if (tempDir) {
|
|
98
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Output path resolution
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
function resolveOutputPath(outOption, scenarioId) {
|
|
107
|
-
if (!outOption) {
|
|
108
|
-
return resolve(`./${scenarioId}.mp4`);
|
|
109
|
-
}
|
|
110
|
-
if (outOption.endsWith(".mp4")) {
|
|
111
|
-
return resolve(outOption);
|
|
112
|
-
}
|
|
113
|
-
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
114
|
-
}
|
|
115
|
-
async function resolveEntryPoint(input) {
|
|
116
|
-
// Explicit --entry: use as-is, no generation.
|
|
117
|
-
if (input.entryOption) {
|
|
118
|
-
const resolved = resolve(input.entryOption);
|
|
119
|
-
try {
|
|
120
|
-
await access(resolved);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
throw new Error(`Remotion entry point not found: ${input.entryOption}`);
|
|
124
|
-
}
|
|
125
|
-
return { entryPoint: resolved };
|
|
126
|
-
}
|
|
127
|
-
// Auto-generate: detect renderStep export, find providers, write temp entry.
|
|
128
|
-
const renderFilePath = await detectRenderExport(input.scenarioDir);
|
|
129
|
-
const providersPath = await resolveProvidersPath(input.scenarioDir);
|
|
130
|
-
const entrySource = generateRemotionEntry({
|
|
131
|
-
scenarioDir: input.scenarioDir,
|
|
132
|
-
renderFilePath,
|
|
133
|
-
scenarioId: input.scenarioId,
|
|
134
|
-
hasNarration: input.hasNarration,
|
|
135
|
-
providersPath,
|
|
136
|
-
fps: input.fps,
|
|
137
|
-
width: input.width,
|
|
138
|
-
height: input.height,
|
|
139
|
-
compositionId: input.compositionId,
|
|
140
|
-
});
|
|
141
|
-
// Write the entry inside the scenario directory so webpack's standard
|
|
142
|
-
// node_modules resolution walks up from here into the consumer project.
|
|
143
|
-
// A /tmp/ location would fail because no node_modules exist there.
|
|
144
|
-
const tempDir = join(input.scenarioDir, ".scenar-render");
|
|
145
|
-
await mkdir(tempDir, { recursive: true });
|
|
146
|
-
const entryPath = join(tempDir, "index.tsx");
|
|
147
|
-
await writeFile(entryPath, entrySource, "utf-8");
|
|
148
|
-
return {
|
|
149
|
-
entryPoint: entryPath,
|
|
150
|
-
generated: { tempDir, providersPath },
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
// Webpack override loading
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
async function loadWebpackOverride(overridePath) {
|
|
157
|
-
const resolved = resolve(overridePath);
|
|
158
|
-
try {
|
|
159
|
-
await access(resolved);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
throw new Error(`Webpack override file not found: ${overridePath}`);
|
|
163
|
-
}
|
|
164
|
-
let mod;
|
|
165
|
-
try {
|
|
166
|
-
mod = await import(pathToFileURL(resolved).href);
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
170
|
-
throw new Error(`Failed to import webpack override from ${overridePath}:\n${detail}`);
|
|
171
|
-
}
|
|
172
|
-
const override = (mod.default ?? mod.webpackOverride);
|
|
173
|
-
if (typeof override !== "function") {
|
|
174
|
-
throw new Error(`${overridePath} does not export a webpack override function.\n\n` +
|
|
175
|
-
"Expected a default export or named 'webpackOverride' export:\n\n" +
|
|
176
|
-
' import type { WebpackOverrideFn } from "@remotion/bundler";\n\n' +
|
|
177
|
-
" const webpackOverride: WebpackOverrideFn = (config) => ({\n" +
|
|
178
|
-
" ...config,\n" +
|
|
179
|
-
" // your overrides\n" +
|
|
180
|
-
" });\n\n" +
|
|
181
|
-
" export default webpackOverride;\n");
|
|
182
|
-
}
|
|
183
|
-
return override;
|
|
184
|
-
}
|
|
185
|
-
async function renderScenario(config) {
|
|
186
|
-
const bundler = await import("@remotion/bundler").catch(() => {
|
|
187
|
-
throw new Error("Could not load @remotion/bundler.\n" +
|
|
188
|
-
"Install it: pnpm add @remotion/bundler@4.0.448");
|
|
189
|
-
});
|
|
190
|
-
const renderer = await import("@remotion/renderer").catch(() => {
|
|
191
|
-
throw new Error("Could not load @remotion/renderer.\n" +
|
|
192
|
-
"Install it: pnpm add @remotion/renderer@4.0.448");
|
|
193
|
-
});
|
|
194
|
-
process.stderr.write("Bundling Remotion project...\n");
|
|
195
|
-
const bundleOptions = {
|
|
196
|
-
entryPoint: config.entryPoint,
|
|
197
|
-
};
|
|
198
|
-
if (config.webpackOverride) {
|
|
199
|
-
bundleOptions.webpackOverride = config.webpackOverride;
|
|
200
|
-
}
|
|
201
|
-
const serveUrl = await bundler.bundle(bundleOptions);
|
|
202
|
-
process.stderr.write(`Selecting composition: ${config.compositionId}\n`);
|
|
203
|
-
const composition = await renderer.selectComposition({
|
|
204
|
-
serveUrl,
|
|
205
|
-
id: config.compositionId,
|
|
206
|
-
});
|
|
207
|
-
process.stderr.write(`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
208
|
-
`${composition.durationInFrames} frames ` +
|
|
209
|
-
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)\n`);
|
|
210
|
-
const outDir = resolve(config.outputPath, "..");
|
|
211
|
-
await mkdir(outDir, { recursive: true });
|
|
212
|
-
process.stderr.write("Rendering...\n");
|
|
213
|
-
await renderer.renderMedia({
|
|
214
|
-
composition,
|
|
215
|
-
serveUrl,
|
|
216
|
-
codec: "h264",
|
|
217
|
-
outputLocation: config.outputPath,
|
|
218
|
-
onProgress: ({ progress }) => {
|
|
219
|
-
const pct = Math.round(progress * 100);
|
|
220
|
-
process.stderr.write(`\r Progress: ${pct}%`);
|
|
221
|
-
},
|
|
222
41
|
});
|
|
223
|
-
process.stderr.write("\n");
|
|
224
42
|
}
|
|
225
43
|
//# sourceMappingURL=render.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/commands/render.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/commands/render.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAYpD,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CACV,uDAAuD;QACrD,oEAAoE;QACpE,4DAA4D;QAC5D,oEAAoE;QACpE,kEAAkE;QAClE,yDAAyD;QACzD,iEAAiE;QACjE,oDAAoD,CACvD;SACA,QAAQ,CAAC,OAAO,EAAE,sDAAsD,CAAC;SACzE,MAAM,CAAC,cAAc,EAAE,2CAA2C,CAAC;SACnE,MAAM,CAAC,gBAAgB,EAAE,iCAAiC,EAAE,IAAI,CAAC;SACjE,MAAM,CAAC,kBAAkB,EAAE,uCAAuC,EAAE,MAAM,CAAC;SAC3E,MAAM,CAAC,mBAAmB,EAAE,wCAAwC,EAAE,MAAM,CAAC;SAC7E,MAAM,CAAC,uBAAuB,EAAE,mCAAmC,CAAC;SACpE,MAAM,CAAC,gBAAgB,EAAE,sCAAsC,CAAC;SAChE,MAAM,CACL,2BAA2B,EAC3B,oEAAoE,CACrE;SACA,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,OAAsB,EAAE,EAAE;QACpD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC;gBAC7B,WAAW,EAAE,GAAG;gBAChB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE;gBAC9B,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI;gBACpC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI;gBACtC,aAAa,EAAE,OAAO,CAAC,aAAa;gBACpC,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC;aACzD,CAAC,CAAC;YAEH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/src/commands/render.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { resolve, join } from "node:path";
|
|
2
|
-
import { stat, mkdir, access, writeFile, rm } from "node:fs/promises";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
1
|
import { Command } from "commander";
|
|
5
|
-
import {
|
|
6
|
-
import { generateRemotionEntry } from "../render/generate-entry.js";
|
|
7
|
-
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
8
|
-
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
2
|
+
import { runRender } from "../render/run-render.js";
|
|
9
3
|
|
|
10
4
|
interface RenderOptions {
|
|
11
5
|
out?: string;
|
|
@@ -22,13 +16,13 @@ export function registerRenderCommand(program: Command): void {
|
|
|
22
16
|
.command("render")
|
|
23
17
|
.description(
|
|
24
18
|
"Render a scenario as an MP4 video using Remotion.\n\n" +
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
"Accepts a scenario directory containing steps.ts and an optional\n" +
|
|
20
|
+
"narration/ subfolder with manifest.json + audio clips.\n\n" +
|
|
21
|
+
"When the scenario directory contains an index.tsx that exports a\n" +
|
|
22
|
+
"renderStep function, the CLI auto-generates the Remotion entry\n" +
|
|
23
|
+
"point — no remotion/ directory or bundle.ts needed.\n\n" +
|
|
24
|
+
"Output defaults to ./<scenario-id>.mp4 in the current working\n" +
|
|
25
|
+
"directory. Use --out to write to a different path.",
|
|
32
26
|
)
|
|
33
27
|
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
34
28
|
.option("--out <path>", "output file path or directory for the MP4")
|
|
@@ -42,285 +36,24 @@ export function registerRenderCommand(program: Command): void {
|
|
|
42
36
|
"path to a module that default-exports a Remotion WebpackOverrideFn",
|
|
43
37
|
)
|
|
44
38
|
.action(async (dir: string, options: RenderOptions) => {
|
|
45
|
-
const resolved = resolve(dir);
|
|
46
|
-
|
|
47
|
-
let info;
|
|
48
39
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"The render command requires a scenario directory (with steps.ts).\n",
|
|
60
|
-
);
|
|
61
|
-
process.exitCode = 1;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const fps = Number(options.fps) || 30;
|
|
66
|
-
const width = Number(options.width) || 1920;
|
|
67
|
-
const height = Number(options.height) || 1080;
|
|
68
|
-
|
|
69
|
-
let tempDir: string | undefined;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const bundle = await loadBundle(resolved);
|
|
73
|
-
const scenarioId = bundle.id;
|
|
74
|
-
const compositionId = options.compositionId ?? scenarioId;
|
|
75
|
-
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
76
|
-
|
|
77
|
-
const { entryPoint, generated } = await resolveEntryPoint({
|
|
78
|
-
entryOption: options.entry,
|
|
79
|
-
scenarioDir: resolved,
|
|
80
|
-
scenarioId,
|
|
81
|
-
compositionId,
|
|
82
|
-
hasNarration: !!bundle.narrationManifest,
|
|
83
|
-
fps,
|
|
84
|
-
width,
|
|
85
|
-
height,
|
|
40
|
+
const result = await runRender({
|
|
41
|
+
scenarioDir: dir,
|
|
42
|
+
out: options.out,
|
|
43
|
+
fps: Number(options.fps) || 30,
|
|
44
|
+
width: Number(options.width) || 1920,
|
|
45
|
+
height: Number(options.height) || 1080,
|
|
46
|
+
compositionId: options.compositionId,
|
|
47
|
+
entry: options.entry,
|
|
48
|
+
webpackOverride: options.webpackOverride,
|
|
49
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
86
50
|
});
|
|
87
|
-
tempDir = generated?.tempDir;
|
|
88
|
-
|
|
89
|
-
const webpackOverride = options.webpackOverride
|
|
90
|
-
? await loadWebpackOverride(options.webpackOverride)
|
|
91
|
-
: undefined;
|
|
92
|
-
|
|
93
|
-
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
94
|
-
process.stderr.write(`Steps: ${bundle.steps.length}\n`);
|
|
95
|
-
process.stderr.write(
|
|
96
|
-
`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}\n`,
|
|
97
|
-
);
|
|
98
|
-
process.stderr.write(`Entry: ${generated ? "(auto-generated)" : entryPoint}\n`);
|
|
99
|
-
if (generated?.providersPath) {
|
|
100
|
-
process.stderr.write(`Providers: ${generated.providersPath}\n`);
|
|
101
|
-
}
|
|
102
|
-
if (webpackOverride) {
|
|
103
|
-
process.stderr.write(`Webpack: custom override loaded\n`);
|
|
104
|
-
}
|
|
105
|
-
process.stderr.write(`Output: ${outputPath}\n`);
|
|
106
|
-
process.stderr.write(`Config: ${width}x${height} @ ${fps}fps\n\n`);
|
|
107
51
|
|
|
108
|
-
|
|
109
|
-
bundle,
|
|
110
|
-
entryPoint,
|
|
111
|
-
compositionId,
|
|
112
|
-
outputPath,
|
|
113
|
-
fps,
|
|
114
|
-
width,
|
|
115
|
-
height,
|
|
116
|
-
webpackOverride,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${outputPath}\n`);
|
|
52
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${result.outputPath}\n`);
|
|
120
53
|
} catch (error) {
|
|
121
54
|
const msg = error instanceof Error ? error.message : String(error);
|
|
122
55
|
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
123
56
|
process.exitCode = 1;
|
|
124
|
-
} finally {
|
|
125
|
-
if (tempDir) {
|
|
126
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
127
|
-
}
|
|
128
57
|
}
|
|
129
58
|
});
|
|
130
59
|
}
|
|
131
|
-
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
// Output path resolution
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
|
|
136
|
-
function resolveOutputPath(outOption: string | undefined, scenarioId: string): string {
|
|
137
|
-
if (!outOption) {
|
|
138
|
-
return resolve(`./${scenarioId}.mp4`);
|
|
139
|
-
}
|
|
140
|
-
if (outOption.endsWith(".mp4")) {
|
|
141
|
-
return resolve(outOption);
|
|
142
|
-
}
|
|
143
|
-
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Entry point resolution
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
interface EntryResolutionInput {
|
|
151
|
-
entryOption: string | undefined;
|
|
152
|
-
scenarioDir: string;
|
|
153
|
-
scenarioId: string;
|
|
154
|
-
compositionId: string;
|
|
155
|
-
hasNarration: boolean;
|
|
156
|
-
fps: number;
|
|
157
|
-
width: number;
|
|
158
|
-
height: number;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface EntryResolutionResult {
|
|
162
|
-
entryPoint: string;
|
|
163
|
-
generated?: {
|
|
164
|
-
tempDir: string;
|
|
165
|
-
providersPath: string | null;
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function resolveEntryPoint(
|
|
170
|
-
input: EntryResolutionInput,
|
|
171
|
-
): Promise<EntryResolutionResult> {
|
|
172
|
-
// Explicit --entry: use as-is, no generation.
|
|
173
|
-
if (input.entryOption) {
|
|
174
|
-
const resolved = resolve(input.entryOption);
|
|
175
|
-
try {
|
|
176
|
-
await access(resolved);
|
|
177
|
-
} catch {
|
|
178
|
-
throw new Error(`Remotion entry point not found: ${input.entryOption}`);
|
|
179
|
-
}
|
|
180
|
-
return { entryPoint: resolved };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Auto-generate: detect renderStep export, find providers, write temp entry.
|
|
184
|
-
const renderFilePath = await detectRenderExport(input.scenarioDir);
|
|
185
|
-
const providersPath = await resolveProvidersPath(input.scenarioDir);
|
|
186
|
-
|
|
187
|
-
const entrySource = generateRemotionEntry({
|
|
188
|
-
scenarioDir: input.scenarioDir,
|
|
189
|
-
renderFilePath,
|
|
190
|
-
scenarioId: input.scenarioId,
|
|
191
|
-
hasNarration: input.hasNarration,
|
|
192
|
-
providersPath,
|
|
193
|
-
fps: input.fps,
|
|
194
|
-
width: input.width,
|
|
195
|
-
height: input.height,
|
|
196
|
-
compositionId: input.compositionId,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Write the entry inside the scenario directory so webpack's standard
|
|
200
|
-
// node_modules resolution walks up from here into the consumer project.
|
|
201
|
-
// A /tmp/ location would fail because no node_modules exist there.
|
|
202
|
-
const tempDir = join(input.scenarioDir, ".scenar-render");
|
|
203
|
-
await mkdir(tempDir, { recursive: true });
|
|
204
|
-
|
|
205
|
-
const entryPath = join(tempDir, "index.tsx");
|
|
206
|
-
await writeFile(entryPath, entrySource, "utf-8");
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
entryPoint: entryPath,
|
|
210
|
-
generated: { tempDir, providersPath },
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// Webpack override loading
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
async function loadWebpackOverride(
|
|
219
|
-
overridePath: string,
|
|
220
|
-
): Promise<(config: unknown) => unknown> {
|
|
221
|
-
const resolved = resolve(overridePath);
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
await access(resolved);
|
|
225
|
-
} catch {
|
|
226
|
-
throw new Error(`Webpack override file not found: ${overridePath}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let mod: Record<string, unknown>;
|
|
230
|
-
try {
|
|
231
|
-
mod = await import(pathToFileURL(resolved).href) as Record<string, unknown>;
|
|
232
|
-
} catch (err) {
|
|
233
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Failed to import webpack override from ${overridePath}:\n${detail}`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const override = (mod.default ?? mod.webpackOverride) as unknown;
|
|
240
|
-
if (typeof override !== "function") {
|
|
241
|
-
throw new Error(
|
|
242
|
-
`${overridePath} does not export a webpack override function.\n\n` +
|
|
243
|
-
"Expected a default export or named 'webpackOverride' export:\n\n" +
|
|
244
|
-
' import type { WebpackOverrideFn } from "@remotion/bundler";\n\n' +
|
|
245
|
-
" const webpackOverride: WebpackOverrideFn = (config) => ({\n" +
|
|
246
|
-
" ...config,\n" +
|
|
247
|
-
" // your overrides\n" +
|
|
248
|
-
" });\n\n" +
|
|
249
|
-
" export default webpackOverride;\n",
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return override as (config: unknown) => unknown;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// Render orchestration
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
interface RenderConfig {
|
|
261
|
-
bundle: CliBundle;
|
|
262
|
-
entryPoint: string;
|
|
263
|
-
compositionId: string;
|
|
264
|
-
outputPath: string;
|
|
265
|
-
fps: number;
|
|
266
|
-
width: number;
|
|
267
|
-
height: number;
|
|
268
|
-
webpackOverride?: (config: unknown) => unknown;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function renderScenario(config: RenderConfig): Promise<void> {
|
|
272
|
-
const bundler = await import("@remotion/bundler").catch(() => {
|
|
273
|
-
throw new Error(
|
|
274
|
-
"Could not load @remotion/bundler.\n" +
|
|
275
|
-
"Install it: pnpm add @remotion/bundler@4.0.448",
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const renderer = await import("@remotion/renderer").catch(() => {
|
|
280
|
-
throw new Error(
|
|
281
|
-
"Could not load @remotion/renderer.\n" +
|
|
282
|
-
"Install it: pnpm add @remotion/renderer@4.0.448",
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
process.stderr.write("Bundling Remotion project...\n");
|
|
287
|
-
|
|
288
|
-
const bundleOptions: Record<string, unknown> = {
|
|
289
|
-
entryPoint: config.entryPoint,
|
|
290
|
-
};
|
|
291
|
-
if (config.webpackOverride) {
|
|
292
|
-
bundleOptions.webpackOverride = config.webpackOverride;
|
|
293
|
-
}
|
|
294
|
-
const serveUrl = await (bundler.bundle as (opts: Record<string, unknown>) => Promise<string>)(
|
|
295
|
-
bundleOptions,
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
process.stderr.write(`Selecting composition: ${config.compositionId}\n`);
|
|
299
|
-
const composition = await renderer.selectComposition({
|
|
300
|
-
serveUrl,
|
|
301
|
-
id: config.compositionId,
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
process.stderr.write(
|
|
305
|
-
`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
306
|
-
`${composition.durationInFrames} frames ` +
|
|
307
|
-
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)\n`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const outDir = resolve(config.outputPath, "..");
|
|
311
|
-
await mkdir(outDir, { recursive: true });
|
|
312
|
-
|
|
313
|
-
process.stderr.write("Rendering...\n");
|
|
314
|
-
await renderer.renderMedia({
|
|
315
|
-
composition,
|
|
316
|
-
serveUrl,
|
|
317
|
-
codec: "h264" as const,
|
|
318
|
-
outputLocation: config.outputPath,
|
|
319
|
-
onProgress: ({ progress }) => {
|
|
320
|
-
const pct = Math.round(progress * 100);
|
|
321
|
-
process.stderr.write(`\r Progress: ${pct}%`);
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
process.stderr.write("\n");
|
|
326
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4D3D"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:process";
|
|
3
|
+
import { runServe } from "../serve/run-serve.js";
|
|
4
|
+
/** Vite's preview port — the conventional "serve a built site" port. */
|
|
5
|
+
const DEFAULT_PORT = 4173;
|
|
6
|
+
/** Bind to loopback by default; a preview is a single-developer tool. */
|
|
7
|
+
const DEFAULT_HOST = "localhost";
|
|
8
|
+
export function registerServeCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("serve")
|
|
11
|
+
.description("Serve a packed bundle locally and print its embed URL.\n\n" +
|
|
12
|
+
"Takes a bundle directory produced by `scenar pack` and serves it as a\n" +
|
|
13
|
+
"static site on http://localhost:4173 (override with --port/--host).\n" +
|
|
14
|
+
"Prints the URL plus a ready-to-paste responsive <iframe> snippet, then\n" +
|
|
15
|
+
"stays running until you press Ctrl+C.\n\n" +
|
|
16
|
+
"This is the zero-infrastructure way to view an embed: no backend, no\n" +
|
|
17
|
+
"account. For a permanent public URL, use `scenar publish` (GitHub\n" +
|
|
18
|
+
"Pages) or `scenar deploy` (Scenar Cloud).")
|
|
19
|
+
.argument("<bundleDir>", "path to a packed bundle directory (from scenar pack)")
|
|
20
|
+
.option("--port <number>", `port to listen on (default: ${DEFAULT_PORT})`, String(DEFAULT_PORT))
|
|
21
|
+
.option("--host <host>", `host/interface to bind (default: ${DEFAULT_HOST})`, DEFAULT_HOST)
|
|
22
|
+
.option("--open", "open the embed in your default browser")
|
|
23
|
+
.action(async (bundleDir, options) => {
|
|
24
|
+
try {
|
|
25
|
+
const port = Number(options.port);
|
|
26
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
27
|
+
throw new Error(`invalid --port "${options.port}"; expected an integer between 0 and 65535.`);
|
|
28
|
+
}
|
|
29
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
30
|
+
let result;
|
|
31
|
+
try {
|
|
32
|
+
result = await runServe({ bundleDir, port, host });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (isAddrInUse(error)) {
|
|
36
|
+
throw new Error(`port ${port} is already in use. Stop the other process or pass --port <number>.`);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Serving at ${result.url}\n`);
|
|
41
|
+
if (!result.recordedViewport) {
|
|
42
|
+
process.stderr.write(" Note: no recorded viewport; snippet uses the default " +
|
|
43
|
+
`${result.viewport.width}x${result.viewport.height}. Re-pack with the current CLI ` +
|
|
44
|
+
"to embed at the exact aspect ratio.\n");
|
|
45
|
+
}
|
|
46
|
+
process.stderr.write("\n Embed snippet (paste into any page):\n\n");
|
|
47
|
+
process.stderr.write(`${indent(result.snippet)}\n`);
|
|
48
|
+
process.stderr.write("\n Press Ctrl+C to stop.\n");
|
|
49
|
+
if (options.open) {
|
|
50
|
+
openInBrowser(result.url);
|
|
51
|
+
}
|
|
52
|
+
await runUntilInterrupted(result.handle);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Block until SIGINT/SIGTERM, then close the server cleanly. */
|
|
62
|
+
function runUntilInterrupted(handle) {
|
|
63
|
+
return new Promise((resolvePromise) => {
|
|
64
|
+
const shutdown = () => {
|
|
65
|
+
process.stderr.write("\nStopping...\n");
|
|
66
|
+
void handle.close().finally(() => resolvePromise());
|
|
67
|
+
};
|
|
68
|
+
process.once("SIGINT", shutdown);
|
|
69
|
+
process.once("SIGTERM", shutdown);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Best-effort: launch the OS default browser. Failures are silent (non-fatal). */
|
|
73
|
+
function openInBrowser(url) {
|
|
74
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
75
|
+
try {
|
|
76
|
+
const child = spawn(command, [url], { stdio: "ignore", detached: true, shell: platform === "win32" });
|
|
77
|
+
child.on("error", () => { });
|
|
78
|
+
child.unref();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Opening a browser is a convenience; never fail the command over it.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isAddrInUse(error) {
|
|
85
|
+
return typeof error === "object" && error !== null && error.code === "EADDRINUSE";
|
|
86
|
+
}
|
|
87
|
+
/** Indent every line by two spaces (for nesting a block under a heading). */
|
|
88
|
+
function indent(block) {
|
|
89
|
+
return block.replace(/^/gm, " ");
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=serve.js.map
|