@scenar/cli 0.1.20 → 0.2.0
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/package.json +17 -5
- 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/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/index.d.ts.map +1 -1
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +4 -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/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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { resolve, join, basename } from "node:path";
|
|
2
|
+
import { stat, readFile } from "node:fs/promises";
|
|
3
|
+
import { PACK_MANIFEST_FILE } from "../pack/pack-manifest.js";
|
|
4
|
+
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
5
|
+
import { readBundleViewport } from "../bundle/read-viewport.js";
|
|
6
|
+
import type { Viewport } from "../pack/viewport.js";
|
|
7
|
+
import { createGithubPublisher } from "./github.js";
|
|
8
|
+
import {
|
|
9
|
+
runPublishFlow,
|
|
10
|
+
validateRepoName,
|
|
11
|
+
validatePublishPath,
|
|
12
|
+
normalizePath,
|
|
13
|
+
type PublishFlowDeps,
|
|
14
|
+
type PublishResult,
|
|
15
|
+
} from "./publish-flow.js";
|
|
16
|
+
|
|
17
|
+
/** The servable entry every packed bundle contains at its root. */
|
|
18
|
+
const INDEX_FILE = "index.html";
|
|
19
|
+
|
|
20
|
+
/** Repo-name rule for the default (same shape as scenario slugs). */
|
|
21
|
+
const REPO_FALLBACK_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default shared repo for embeds. Many tours live here, each under its own slug
|
|
25
|
+
* path, so previews never land in the user's source repo. Override with --repo.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_REPO = "scenar-embeds";
|
|
28
|
+
|
|
29
|
+
/** Options for {@link runPublish}. */
|
|
30
|
+
export interface RunPublishOptions {
|
|
31
|
+
readonly bundleDir: string;
|
|
32
|
+
/** Target repository name (default: {@link DEFAULT_REPO}). */
|
|
33
|
+
readonly repo?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Subdirectory within the repo (default: the packed scenario slug). Pass "/"
|
|
36
|
+
* or "" to publish at the repo root (a dedicated single-tour repo).
|
|
37
|
+
*/
|
|
38
|
+
readonly path?: string;
|
|
39
|
+
/** Target GitHub org (default: the authenticated user). */
|
|
40
|
+
readonly org?: string;
|
|
41
|
+
/** Create the repo private (GitHub Pages needs Pro for private repos). */
|
|
42
|
+
readonly private?: boolean;
|
|
43
|
+
/** Commit message for the published snapshot. */
|
|
44
|
+
readonly message?: string;
|
|
45
|
+
/** Progress sink for flow messages. */
|
|
46
|
+
readonly onLog?: (message: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Override the publish backend (the gh/git deps). Defaults to the real
|
|
49
|
+
* {@link createGithubPublisher}; tests inject a fake to avoid live calls.
|
|
50
|
+
*/
|
|
51
|
+
readonly deps?: Omit<PublishFlowDeps, "log">;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The published location plus the derived embed snippet. */
|
|
55
|
+
export interface PublishRunResult {
|
|
56
|
+
readonly result: PublishResult;
|
|
57
|
+
readonly viewport: Viewport;
|
|
58
|
+
readonly recordedViewport: boolean;
|
|
59
|
+
readonly snippet: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate a packed bundle and publish it to GitHub Pages — the orchestration
|
|
64
|
+
* behind `scenar publish`, with no process/exit coupling. Resolves the default
|
|
65
|
+
* repo name, runs the publish flow, and builds the embed snippet. Throws on an
|
|
66
|
+
* invalid bundle, an invalid repo name, or any flow failure.
|
|
67
|
+
*/
|
|
68
|
+
export async function runPublish(options: RunPublishOptions): Promise<PublishRunResult> {
|
|
69
|
+
const onLog = options.onLog ?? (() => {});
|
|
70
|
+
const resolvedDir = resolve(options.bundleDir);
|
|
71
|
+
|
|
72
|
+
const info = await stat(resolvedDir).catch(() => null);
|
|
73
|
+
if (!info || !info.isDirectory()) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${options.bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const indexInfo = await stat(join(resolvedDir, INDEX_FILE)).catch(() => null);
|
|
79
|
+
if (!indexInfo || !indexInfo.isFile()) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`no ${INDEX_FILE} in ${options.bundleDir}. Run \`scenar pack\` to produce a servable bundle first.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const slug = await scenarioSlug(resolvedDir);
|
|
86
|
+
|
|
87
|
+
const repo = options.repo ?? DEFAULT_REPO;
|
|
88
|
+
const repoError = validateRepoName(repo);
|
|
89
|
+
if (repoError) {
|
|
90
|
+
throw new Error(`${repoError}. Pass --repo <name> to override.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Path defaults to the scenario slug so tours coexist in one repo; an explicit
|
|
94
|
+
// "/" (or "") publishes at the repo root.
|
|
95
|
+
const path = normalizePath(options.path ?? slug);
|
|
96
|
+
const pathError = validatePublishPath(path);
|
|
97
|
+
if (pathError) {
|
|
98
|
+
throw new Error(`${pathError}. Pass --path <subpath> to override.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const deps: PublishFlowDeps = { ...(options.deps ?? createGithubPublisher()), log: onLog };
|
|
102
|
+
const result = await runPublishFlow(deps, {
|
|
103
|
+
bundleDir: resolvedDir,
|
|
104
|
+
repo,
|
|
105
|
+
path,
|
|
106
|
+
org: options.org,
|
|
107
|
+
private: Boolean(options.private),
|
|
108
|
+
message: options.message ?? "Publish Scenar embed",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const { viewport, recorded } = await readBundleViewport(resolvedDir);
|
|
112
|
+
const snippet = buildEmbedSnippet({ embedUrl: result.pagesUrl, viewport });
|
|
113
|
+
|
|
114
|
+
return { result, viewport, recordedViewport: recorded, snippet };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The packed scenario slug (from pack-manifest.json), falling back to the bundle
|
|
119
|
+
* directory's base name. Used as the default publish subpath. The downstream
|
|
120
|
+
* validatePublishPath guards the result.
|
|
121
|
+
*/
|
|
122
|
+
async function scenarioSlug(bundleDir: string): Promise<string> {
|
|
123
|
+
try {
|
|
124
|
+
const raw = await readFile(join(bundleDir, PACK_MANIFEST_FILE), "utf-8");
|
|
125
|
+
const id = (JSON.parse(raw) as { scenarioId?: unknown }).scenarioId;
|
|
126
|
+
if (typeof id === "string" && REPO_FALLBACK_PATTERN.test(id)) return id;
|
|
127
|
+
} catch {
|
|
128
|
+
// No/invalid manifest — fall back to the directory name.
|
|
129
|
+
}
|
|
130
|
+
return basename(bundleDir);
|
|
131
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Options for {@link runRender}. Paths may be relative; resolved here. */
|
|
2
|
+
export interface RunRenderOptions {
|
|
3
|
+
readonly scenarioDir: string;
|
|
4
|
+
readonly out?: string;
|
|
5
|
+
readonly fps?: number;
|
|
6
|
+
readonly width?: number;
|
|
7
|
+
readonly height?: number;
|
|
8
|
+
readonly compositionId?: string;
|
|
9
|
+
readonly entry?: string;
|
|
10
|
+
readonly webpackOverride?: string;
|
|
11
|
+
readonly onLog?: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** The outcome of a successful render. */
|
|
14
|
+
export interface RenderResult {
|
|
15
|
+
readonly scenarioId: string;
|
|
16
|
+
readonly outputPath: string;
|
|
17
|
+
readonly steps: number;
|
|
18
|
+
readonly hasNarration: boolean;
|
|
19
|
+
readonly entryAutoGenerated: boolean;
|
|
20
|
+
readonly fps: number;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render a scenario directory to an MP4 via Remotion — the pure orchestration
|
|
26
|
+
* behind `scenar render`, with no process/exit coupling. Throws on any failure
|
|
27
|
+
* (missing directory, missing Remotion peer deps, render error). The caller
|
|
28
|
+
* formats the result/error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runRender(options: RunRenderOptions): Promise<RenderResult>;
|
|
31
|
+
//# sourceMappingURL=run-render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-render.d.ts","sourceRoot":"","sources":["../../../src/render/run-render.ts"],"names":[],"mappings":"AAQA,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkFhF"}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
import { loadBundle } from "../util/load-bundle.js";
|
|
5
|
+
import { generateRemotionEntry } from "./generate-entry.js";
|
|
6
|
+
import { resolveProvidersPath } from "./resolve-providers.js";
|
|
7
|
+
import { detectRenderExport } from "./detect-render-export.js";
|
|
8
|
+
/**
|
|
9
|
+
* Render a scenario directory to an MP4 via Remotion — the pure orchestration
|
|
10
|
+
* behind `scenar render`, with no process/exit coupling. Throws on any failure
|
|
11
|
+
* (missing directory, missing Remotion peer deps, render error). The caller
|
|
12
|
+
* formats the result/error.
|
|
13
|
+
*/
|
|
14
|
+
export async function runRender(options) {
|
|
15
|
+
const onLog = options.onLog ?? (() => { });
|
|
16
|
+
const scenarioDir = resolve(options.scenarioDir);
|
|
17
|
+
const info = await stat(scenarioDir).catch(() => null);
|
|
18
|
+
if (!info) {
|
|
19
|
+
throw new Error(`${options.scenarioDir} does not exist.`);
|
|
20
|
+
}
|
|
21
|
+
if (!info.isDirectory()) {
|
|
22
|
+
throw new Error(`${options.scenarioDir} is not a directory. ` +
|
|
23
|
+
"The render command requires a scenario directory (with steps.ts).");
|
|
24
|
+
}
|
|
25
|
+
const fps = options.fps ?? 30;
|
|
26
|
+
const width = options.width ?? 1920;
|
|
27
|
+
const height = options.height ?? 1080;
|
|
28
|
+
let tempDir;
|
|
29
|
+
try {
|
|
30
|
+
const bundle = await loadBundle(scenarioDir);
|
|
31
|
+
const scenarioId = bundle.id;
|
|
32
|
+
const compositionId = options.compositionId ?? scenarioId;
|
|
33
|
+
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
34
|
+
const { entryPoint, generated } = await resolveEntryPoint({
|
|
35
|
+
entryOption: options.entry,
|
|
36
|
+
scenarioDir,
|
|
37
|
+
scenarioId,
|
|
38
|
+
compositionId,
|
|
39
|
+
hasNarration: !!bundle.narrationManifest,
|
|
40
|
+
fps,
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
});
|
|
44
|
+
tempDir = generated?.tempDir;
|
|
45
|
+
const webpackOverride = options.webpackOverride
|
|
46
|
+
? await loadWebpackOverride(options.webpackOverride)
|
|
47
|
+
: undefined;
|
|
48
|
+
onLog(`Scenario: ${scenarioId}`);
|
|
49
|
+
onLog(`Steps: ${bundle.steps.length}`);
|
|
50
|
+
onLog(`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}`);
|
|
51
|
+
onLog(`Entry: ${generated ? "(auto-generated)" : entryPoint}`);
|
|
52
|
+
if (generated?.providersPath) {
|
|
53
|
+
onLog(`Providers: ${generated.providersPath}`);
|
|
54
|
+
}
|
|
55
|
+
if (webpackOverride) {
|
|
56
|
+
onLog("Webpack: custom override loaded");
|
|
57
|
+
}
|
|
58
|
+
onLog(`Output: ${outputPath}`);
|
|
59
|
+
onLog(`Config: ${width}x${height} @ ${fps}fps`);
|
|
60
|
+
await renderScenario({
|
|
61
|
+
bundle,
|
|
62
|
+
entryPoint,
|
|
63
|
+
compositionId,
|
|
64
|
+
outputPath,
|
|
65
|
+
fps,
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
webpackOverride,
|
|
69
|
+
onLog,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
scenarioId,
|
|
73
|
+
outputPath,
|
|
74
|
+
steps: bundle.steps.length,
|
|
75
|
+
hasNarration: !!bundle.narrationManifest,
|
|
76
|
+
entryAutoGenerated: !!generated,
|
|
77
|
+
fps,
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (tempDir) {
|
|
84
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function resolveOutputPath(outOption, scenarioId) {
|
|
89
|
+
if (!outOption) {
|
|
90
|
+
return resolve(`./${scenarioId}.mp4`);
|
|
91
|
+
}
|
|
92
|
+
if (outOption.endsWith(".mp4")) {
|
|
93
|
+
return resolve(outOption);
|
|
94
|
+
}
|
|
95
|
+
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
96
|
+
}
|
|
97
|
+
async function resolveEntryPoint(input) {
|
|
98
|
+
// Explicit --entry: use as-is, no generation.
|
|
99
|
+
if (input.entryOption) {
|
|
100
|
+
const resolved = resolve(input.entryOption);
|
|
101
|
+
try {
|
|
102
|
+
await access(resolved);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Remotion entry point not found: ${input.entryOption}`);
|
|
106
|
+
}
|
|
107
|
+
return { entryPoint: resolved };
|
|
108
|
+
}
|
|
109
|
+
// Auto-generate: detect renderStep export, find providers, write temp entry.
|
|
110
|
+
const renderFilePath = await detectRenderExport(input.scenarioDir);
|
|
111
|
+
const providersPath = await resolveProvidersPath(input.scenarioDir);
|
|
112
|
+
const entrySource = generateRemotionEntry({
|
|
113
|
+
scenarioDir: input.scenarioDir,
|
|
114
|
+
renderFilePath,
|
|
115
|
+
scenarioId: input.scenarioId,
|
|
116
|
+
hasNarration: input.hasNarration,
|
|
117
|
+
providersPath,
|
|
118
|
+
fps: input.fps,
|
|
119
|
+
width: input.width,
|
|
120
|
+
height: input.height,
|
|
121
|
+
compositionId: input.compositionId,
|
|
122
|
+
});
|
|
123
|
+
// Write the entry inside the scenario directory so webpack's standard
|
|
124
|
+
// node_modules resolution walks up from here into the consumer project.
|
|
125
|
+
// A /tmp/ location would fail because no node_modules exist there.
|
|
126
|
+
const tempDir = join(input.scenarioDir, ".scenar-render");
|
|
127
|
+
await mkdir(tempDir, { recursive: true });
|
|
128
|
+
const entryPath = join(tempDir, "index.tsx");
|
|
129
|
+
await writeFile(entryPath, entrySource, "utf-8");
|
|
130
|
+
return { entryPoint: entryPath, generated: { tempDir, providersPath } };
|
|
131
|
+
}
|
|
132
|
+
async function loadWebpackOverride(overridePath) {
|
|
133
|
+
const resolved = resolve(overridePath);
|
|
134
|
+
try {
|
|
135
|
+
await access(resolved);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error(`Webpack override file not found: ${overridePath}`);
|
|
139
|
+
}
|
|
140
|
+
let mod;
|
|
141
|
+
try {
|
|
142
|
+
mod = (await import(pathToFileURL(resolved).href));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
146
|
+
throw new Error(`Failed to import webpack override from ${overridePath}:\n${detail}`);
|
|
147
|
+
}
|
|
148
|
+
const override = (mod.default ?? mod.webpackOverride);
|
|
149
|
+
if (typeof override !== "function") {
|
|
150
|
+
throw new Error(`${overridePath} does not export a webpack override function.\n\n` +
|
|
151
|
+
"Expected a default export or named 'webpackOverride' export:\n\n" +
|
|
152
|
+
' import type { WebpackOverrideFn } from "@remotion/bundler";\n\n' +
|
|
153
|
+
" const webpackOverride: WebpackOverrideFn = (config) => ({\n" +
|
|
154
|
+
" ...config,\n" +
|
|
155
|
+
" // your overrides\n" +
|
|
156
|
+
" });\n\n" +
|
|
157
|
+
" export default webpackOverride;\n");
|
|
158
|
+
}
|
|
159
|
+
return override;
|
|
160
|
+
}
|
|
161
|
+
async function renderScenario(config) {
|
|
162
|
+
const bundler = await import("@remotion/bundler").catch(() => {
|
|
163
|
+
throw new Error("Could not load @remotion/bundler.\nInstall it: pnpm add @remotion/bundler@4.0.448");
|
|
164
|
+
});
|
|
165
|
+
const renderer = await import("@remotion/renderer").catch(() => {
|
|
166
|
+
throw new Error("Could not load @remotion/renderer.\nInstall it: pnpm add @remotion/renderer@4.0.448");
|
|
167
|
+
});
|
|
168
|
+
config.onLog("Bundling Remotion project...");
|
|
169
|
+
const bundleOptions = { entryPoint: config.entryPoint };
|
|
170
|
+
if (config.webpackOverride) {
|
|
171
|
+
bundleOptions.webpackOverride = config.webpackOverride;
|
|
172
|
+
}
|
|
173
|
+
const serveUrl = await bundler.bundle(bundleOptions);
|
|
174
|
+
config.onLog(`Selecting composition: ${config.compositionId}`);
|
|
175
|
+
const composition = await renderer.selectComposition({
|
|
176
|
+
serveUrl,
|
|
177
|
+
id: config.compositionId,
|
|
178
|
+
});
|
|
179
|
+
config.onLog(`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
180
|
+
`${composition.durationInFrames} frames ` +
|
|
181
|
+
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)`);
|
|
182
|
+
const outDir = resolve(config.outputPath, "..");
|
|
183
|
+
await mkdir(outDir, { recursive: true });
|
|
184
|
+
config.onLog("Rendering...");
|
|
185
|
+
await renderer.renderMedia({
|
|
186
|
+
composition,
|
|
187
|
+
serveUrl,
|
|
188
|
+
codec: "h264",
|
|
189
|
+
outputLocation: config.outputPath,
|
|
190
|
+
onProgress: ({ progress }) => {
|
|
191
|
+
const pct = Math.round(progress * 100);
|
|
192
|
+
config.onLog(` Progress: ${pct}%`);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=run-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-render.js","sourceRoot":"","sources":["../../../src/render/run-render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
import { loadBundle, type CliBundle } from "../util/load-bundle.js";
|
|
5
|
+
import { generateRemotionEntry } from "./generate-entry.js";
|
|
6
|
+
import { resolveProvidersPath } from "./resolve-providers.js";
|
|
7
|
+
import { detectRenderExport } from "./detect-render-export.js";
|
|
8
|
+
|
|
9
|
+
/** Options for {@link runRender}. Paths may be relative; resolved here. */
|
|
10
|
+
export interface RunRenderOptions {
|
|
11
|
+
readonly scenarioDir: string;
|
|
12
|
+
readonly out?: string;
|
|
13
|
+
readonly fps?: number;
|
|
14
|
+
readonly width?: number;
|
|
15
|
+
readonly height?: number;
|
|
16
|
+
readonly compositionId?: string;
|
|
17
|
+
readonly entry?: string;
|
|
18
|
+
readonly webpackOverride?: string;
|
|
19
|
+
readonly onLog?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The outcome of a successful render. */
|
|
23
|
+
export interface RenderResult {
|
|
24
|
+
readonly scenarioId: string;
|
|
25
|
+
readonly outputPath: string;
|
|
26
|
+
readonly steps: number;
|
|
27
|
+
readonly hasNarration: boolean;
|
|
28
|
+
readonly entryAutoGenerated: boolean;
|
|
29
|
+
readonly fps: number;
|
|
30
|
+
readonly width: number;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render a scenario directory to an MP4 via Remotion — the pure orchestration
|
|
36
|
+
* behind `scenar render`, with no process/exit coupling. Throws on any failure
|
|
37
|
+
* (missing directory, missing Remotion peer deps, render error). The caller
|
|
38
|
+
* formats the result/error.
|
|
39
|
+
*/
|
|
40
|
+
export async function runRender(options: RunRenderOptions): Promise<RenderResult> {
|
|
41
|
+
const onLog = options.onLog ?? (() => {});
|
|
42
|
+
const scenarioDir = resolve(options.scenarioDir);
|
|
43
|
+
|
|
44
|
+
const info = await stat(scenarioDir).catch(() => null);
|
|
45
|
+
if (!info) {
|
|
46
|
+
throw new Error(`${options.scenarioDir} does not exist.`);
|
|
47
|
+
}
|
|
48
|
+
if (!info.isDirectory()) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${options.scenarioDir} is not a directory. ` +
|
|
51
|
+
"The render command requires a scenario directory (with steps.ts).",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fps = options.fps ?? 30;
|
|
56
|
+
const width = options.width ?? 1920;
|
|
57
|
+
const height = options.height ?? 1080;
|
|
58
|
+
|
|
59
|
+
let tempDir: string | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const bundle = await loadBundle(scenarioDir);
|
|
62
|
+
const scenarioId = bundle.id;
|
|
63
|
+
const compositionId = options.compositionId ?? scenarioId;
|
|
64
|
+
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
65
|
+
|
|
66
|
+
const { entryPoint, generated } = await resolveEntryPoint({
|
|
67
|
+
entryOption: options.entry,
|
|
68
|
+
scenarioDir,
|
|
69
|
+
scenarioId,
|
|
70
|
+
compositionId,
|
|
71
|
+
hasNarration: !!bundle.narrationManifest,
|
|
72
|
+
fps,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
});
|
|
76
|
+
tempDir = generated?.tempDir;
|
|
77
|
+
|
|
78
|
+
const webpackOverride = options.webpackOverride
|
|
79
|
+
? await loadWebpackOverride(options.webpackOverride)
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
onLog(`Scenario: ${scenarioId}`);
|
|
83
|
+
onLog(`Steps: ${bundle.steps.length}`);
|
|
84
|
+
onLog(`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}`);
|
|
85
|
+
onLog(`Entry: ${generated ? "(auto-generated)" : entryPoint}`);
|
|
86
|
+
if (generated?.providersPath) {
|
|
87
|
+
onLog(`Providers: ${generated.providersPath}`);
|
|
88
|
+
}
|
|
89
|
+
if (webpackOverride) {
|
|
90
|
+
onLog("Webpack: custom override loaded");
|
|
91
|
+
}
|
|
92
|
+
onLog(`Output: ${outputPath}`);
|
|
93
|
+
onLog(`Config: ${width}x${height} @ ${fps}fps`);
|
|
94
|
+
|
|
95
|
+
await renderScenario({
|
|
96
|
+
bundle,
|
|
97
|
+
entryPoint,
|
|
98
|
+
compositionId,
|
|
99
|
+
outputPath,
|
|
100
|
+
fps,
|
|
101
|
+
width,
|
|
102
|
+
height,
|
|
103
|
+
webpackOverride,
|
|
104
|
+
onLog,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
scenarioId,
|
|
109
|
+
outputPath,
|
|
110
|
+
steps: bundle.steps.length,
|
|
111
|
+
hasNarration: !!bundle.narrationManifest,
|
|
112
|
+
entryAutoGenerated: !!generated,
|
|
113
|
+
fps,
|
|
114
|
+
width,
|
|
115
|
+
height,
|
|
116
|
+
};
|
|
117
|
+
} finally {
|
|
118
|
+
if (tempDir) {
|
|
119
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveOutputPath(outOption: string | undefined, scenarioId: string): string {
|
|
125
|
+
if (!outOption) {
|
|
126
|
+
return resolve(`./${scenarioId}.mp4`);
|
|
127
|
+
}
|
|
128
|
+
if (outOption.endsWith(".mp4")) {
|
|
129
|
+
return resolve(outOption);
|
|
130
|
+
}
|
|
131
|
+
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface EntryResolutionInput {
|
|
135
|
+
entryOption: string | undefined;
|
|
136
|
+
scenarioDir: string;
|
|
137
|
+
scenarioId: string;
|
|
138
|
+
compositionId: string;
|
|
139
|
+
hasNarration: boolean;
|
|
140
|
+
fps: number;
|
|
141
|
+
width: number;
|
|
142
|
+
height: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface EntryResolutionResult {
|
|
146
|
+
entryPoint: string;
|
|
147
|
+
generated?: {
|
|
148
|
+
tempDir: string;
|
|
149
|
+
providersPath: string | null;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function resolveEntryPoint(input: EntryResolutionInput): Promise<EntryResolutionResult> {
|
|
154
|
+
// Explicit --entry: use as-is, no generation.
|
|
155
|
+
if (input.entryOption) {
|
|
156
|
+
const resolved = resolve(input.entryOption);
|
|
157
|
+
try {
|
|
158
|
+
await access(resolved);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`Remotion entry point not found: ${input.entryOption}`);
|
|
161
|
+
}
|
|
162
|
+
return { entryPoint: resolved };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Auto-generate: detect renderStep export, find providers, write temp entry.
|
|
166
|
+
const renderFilePath = await detectRenderExport(input.scenarioDir);
|
|
167
|
+
const providersPath = await resolveProvidersPath(input.scenarioDir);
|
|
168
|
+
|
|
169
|
+
const entrySource = generateRemotionEntry({
|
|
170
|
+
scenarioDir: input.scenarioDir,
|
|
171
|
+
renderFilePath,
|
|
172
|
+
scenarioId: input.scenarioId,
|
|
173
|
+
hasNarration: input.hasNarration,
|
|
174
|
+
providersPath,
|
|
175
|
+
fps: input.fps,
|
|
176
|
+
width: input.width,
|
|
177
|
+
height: input.height,
|
|
178
|
+
compositionId: input.compositionId,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Write the entry inside the scenario directory so webpack's standard
|
|
182
|
+
// node_modules resolution walks up from here into the consumer project.
|
|
183
|
+
// A /tmp/ location would fail because no node_modules exist there.
|
|
184
|
+
const tempDir = join(input.scenarioDir, ".scenar-render");
|
|
185
|
+
await mkdir(tempDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const entryPath = join(tempDir, "index.tsx");
|
|
188
|
+
await writeFile(entryPath, entrySource, "utf-8");
|
|
189
|
+
|
|
190
|
+
return { entryPoint: entryPath, generated: { tempDir, providersPath } };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function loadWebpackOverride(overridePath: string): Promise<(config: unknown) => unknown> {
|
|
194
|
+
const resolved = resolve(overridePath);
|
|
195
|
+
try {
|
|
196
|
+
await access(resolved);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(`Webpack override file not found: ${overridePath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let mod: Record<string, unknown>;
|
|
202
|
+
try {
|
|
203
|
+
mod = (await import(pathToFileURL(resolved).href)) as Record<string, unknown>;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
206
|
+
throw new Error(`Failed to import webpack override from ${overridePath}:\n${detail}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const override = (mod.default ?? mod.webpackOverride) as unknown;
|
|
210
|
+
if (typeof override !== "function") {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`${overridePath} does not export a webpack override function.\n\n` +
|
|
213
|
+
"Expected a default export or named 'webpackOverride' export:\n\n" +
|
|
214
|
+
' import type { WebpackOverrideFn } from "@remotion/bundler";\n\n' +
|
|
215
|
+
" const webpackOverride: WebpackOverrideFn = (config) => ({\n" +
|
|
216
|
+
" ...config,\n" +
|
|
217
|
+
" // your overrides\n" +
|
|
218
|
+
" });\n\n" +
|
|
219
|
+
" export default webpackOverride;\n",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return override as (config: unknown) => unknown;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface RenderConfig {
|
|
227
|
+
bundle: CliBundle;
|
|
228
|
+
entryPoint: string;
|
|
229
|
+
compositionId: string;
|
|
230
|
+
outputPath: string;
|
|
231
|
+
fps: number;
|
|
232
|
+
width: number;
|
|
233
|
+
height: number;
|
|
234
|
+
webpackOverride?: (config: unknown) => unknown;
|
|
235
|
+
onLog: (message: string) => void;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function renderScenario(config: RenderConfig): Promise<void> {
|
|
239
|
+
const bundler = await import("@remotion/bundler").catch(() => {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"Could not load @remotion/bundler.\nInstall it: pnpm add @remotion/bundler@4.0.448",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const renderer = await import("@remotion/renderer").catch(() => {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"Could not load @remotion/renderer.\nInstall it: pnpm add @remotion/renderer@4.0.448",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
config.onLog("Bundling Remotion project...");
|
|
252
|
+
|
|
253
|
+
const bundleOptions: Record<string, unknown> = { entryPoint: config.entryPoint };
|
|
254
|
+
if (config.webpackOverride) {
|
|
255
|
+
bundleOptions.webpackOverride = config.webpackOverride;
|
|
256
|
+
}
|
|
257
|
+
const serveUrl = await (bundler.bundle as (opts: Record<string, unknown>) => Promise<string>)(
|
|
258
|
+
bundleOptions,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
config.onLog(`Selecting composition: ${config.compositionId}`);
|
|
262
|
+
const composition = await renderer.selectComposition({
|
|
263
|
+
serveUrl,
|
|
264
|
+
id: config.compositionId,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
config.onLog(
|
|
268
|
+
`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
269
|
+
`${composition.durationInFrames} frames ` +
|
|
270
|
+
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const outDir = resolve(config.outputPath, "..");
|
|
274
|
+
await mkdir(outDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
config.onLog("Rendering...");
|
|
277
|
+
await renderer.renderMedia({
|
|
278
|
+
composition,
|
|
279
|
+
serveUrl,
|
|
280
|
+
codec: "h264" as const,
|
|
281
|
+
outputLocation: config.outputPath,
|
|
282
|
+
onProgress: ({ progress }: { progress: number }) => {
|
|
283
|
+
const pct = Math.round(progress * 100);
|
|
284
|
+
config.onLog(` Progress: ${pct}%`);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** File served for a directory request ("/" or any path ending in "/"). */
|
|
2
|
+
export declare const INDEX_FILE = "index.html";
|
|
3
|
+
/** Content type for an extension the bundle contract does not recognise. */
|
|
4
|
+
export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve an incoming request URL path to a clean, bundle-relative file path —
|
|
7
|
+
* or null if the request is malformed or attempts to escape the bundle root.
|
|
8
|
+
*
|
|
9
|
+
* Pure: no filesystem access. The caller maps the returned relative path onto
|
|
10
|
+
* disk (and 404s if the file is absent). This is the security-critical core of
|
|
11
|
+
* the local server, so it is isolated and exhaustively unit-tested:
|
|
12
|
+
* - percent-decoding (rejecting malformed encodings),
|
|
13
|
+
* - directory requests mapped to {@link INDEX_FILE},
|
|
14
|
+
* - rejection of "", ".", ".." segments and backslash/NUL injection.
|
|
15
|
+
*
|
|
16
|
+
* A defence-in-depth absolute-path containment check still runs in the server
|
|
17
|
+
* after this resolves, but this function alone must never yield a traversing
|
|
18
|
+
* path.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveStaticPath(urlPath: string): string | null;
|
|
21
|
+
/** Canonical content type for a bundle file path, keyed on its final extension. */
|
|
22
|
+
export declare function contentTypeFor(relativePath: string): string;
|
|
23
|
+
//# sourceMappingURL=request-path.d.ts.map
|