@editframe/cli 0.33.0-beta → 0.34.5-beta

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/dist/VERSION.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/VERSION.ts
2
- const VERSION = "0.33.0-beta";
2
+ const VERSION = "0.34.5-beta";
3
3
 
4
4
  //#endregion
5
5
  export { VERSION };
@@ -1 +1 @@
1
- {"version":3,"file":"VERSION.js","names":[],"sources":["../src/VERSION.ts"],"sourcesContent":["export const VERSION = \"0.33.0-beta\";\n"],"mappings":";AAAA,MAAa,UAAU"}
1
+ {"version":3,"file":"VERSION.js","names":[],"sources":["../src/VERSION.ts"],"sourcesContent":["export const VERSION = \"0.34.5-beta\";\n"],"mappings":";AAAA,MAAa,UAAU"}
@@ -1,10 +1,11 @@
1
1
  import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.js";
2
2
  import { PreviewServer } from "../utils/startPreviewServer.js";
3
+ import { withProfiling } from "../utils/profileRender.js";
3
4
  import { program } from "commander";
4
5
  import debug from "debug";
5
6
  import ora from "ora";
6
7
  import path from "node:path";
7
- import { readFile } from "node:fs/promises";
8
+ import { readFile, writeFile } from "node:fs/promises";
8
9
  import { createWriteStream } from "node:fs";
9
10
 
10
11
  //#region src/commands/render.ts
@@ -20,8 +21,7 @@ function formatTime(ms) {
20
21
  if (hours > 0) return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
21
22
  return `${minutes}:${seconds.toString().padStart(2, "0")}`;
22
23
  }
23
- program.command("render [directory]").description("Render a video composition locally").option("-o, --output <path>", "Output file path", "output.mp4").option("-d, --data <json>", "Custom render data (JSON string)").option("--data-file <path>", "Custom render data from JSON file").option("--fps <number>", "Frame rate", "30").option("--scale <number>", "Resolution scale (0-1)", "1").option("--include-audio", "Include audio track", true).option("--no-include-audio", "Exclude audio track").option("--from-ms <number>", "Start time in milliseconds").option("--to-ms <number>", "End time in milliseconds").option("--experimental-native-render", "Use experimental canvas capture API (faster)").action(async (directory = ".", options) => {
24
- const srcDir = path.resolve(process.cwd(), directory);
24
+ program.command("render [directory]").description("Render a video composition locally").option("-o, --output <path>", "Output file path", "output.mp4").option("--url <url>", "URL to render (bypasses directory/server startup)").option("-d, --data <json>", "Custom render data (JSON string)").option("--data-file <path>", "Custom render data from JSON file").option("--fps <number>", "Frame rate", "30").option("--scale <number>", "Resolution scale (0-1)", "1").option("--include-audio", "Include audio track", true).option("--no-include-audio", "Exclude audio track").option("--from-ms <number>", "Start time in milliseconds").option("--to-ms <number>", "End time in milliseconds").option("--experimental-native-render", "Use experimental canvas capture API (faster)").option("--profile", "Enable CPU profiling").option("--profile-output <path>", "Profile output path", "./render-profile.cpuprofile").action(async (directory = ".", options) => {
25
25
  const outputPath = path.resolve(process.cwd(), options.output);
26
26
  let renderData;
27
27
  if (options.dataFile) {
@@ -36,65 +36,80 @@ program.command("render [directory]").description("Render a video composition lo
36
36
  const scale = parseFloat(options.scale);
37
37
  const fromMs = options.fromMs ? parseInt(options.fromMs, 10) : void 0;
38
38
  const toMs = options.toMs ? parseInt(options.toMs, 10) : void 0;
39
- const previewServer = await PreviewServer.start(srcDir);
40
- log("Preview server started at:", previewServer.url);
39
+ let renderUrl;
40
+ let previewServer = null;
41
+ if (options.url) {
42
+ renderUrl = options.url;
43
+ log("Using provided URL:", renderUrl);
44
+ } else {
45
+ const srcDir = path.resolve(process.cwd(), directory);
46
+ previewServer = await PreviewServer.start(srcDir);
47
+ renderUrl = previewServer.url;
48
+ log("Preview server started at:", renderUrl);
49
+ }
41
50
  await launchBrowserAndWaitForSDK({
42
- url: previewServer.url,
51
+ url: renderUrl,
43
52
  headless: true,
44
53
  interactive: false,
45
54
  efInteractive: false,
46
- nativeRender: options.experimentalNativeRender === true
55
+ nativeRender: options.experimentalNativeRender === true,
56
+ profile: options.profile === true,
57
+ profileOutput: options.profileOutput
47
58
  }, async (page) => {
48
- const outputStream = createWriteStream(outputPath);
49
- let chunkCount = 0;
50
- let totalBytes = 0;
51
- await page.exposeFunction("onRenderChunk", (chunkArray) => {
52
- const chunk = Buffer.from(chunkArray);
53
- outputStream.write(chunk);
54
- chunkCount++;
55
- totalBytes += chunk.length;
56
- log(`Received chunk ${chunkCount}: ${chunk.length} bytes (total: ${totalBytes} bytes)`);
57
- });
58
- if (renderData) {
59
- await page.evaluate((data) => {
60
- window.EF_RENDER_DATA = data;
61
- }, renderData);
62
- log("Set EF_RENDER_DATA:", renderData);
63
- }
64
- await page.waitForFunction(() => typeof window.EF_RENDER !== "undefined", { timeout: 1e4 });
65
- if (!await page.evaluate(() => window.EF_RENDER?.isReady())) throw new Error("Render API is not ready. No ef-timegroup found.");
66
- const progressSpinner = ora("Rendering video...").start();
67
- await page.exposeFunction("onRenderProgress", (progress) => {
68
- const percent = (progress.progress * 100).toFixed(1);
69
- const renderedTime = formatTime(progress.renderedMs);
70
- const totalTime = formatTime(progress.totalDurationMs);
71
- const remainingTime = formatTime(progress.estimatedRemainingMs);
72
- const speed = progress.speedMultiplier.toFixed(2);
73
- progressSpinner.text = `Rendering: ${progress.currentFrame}/${progress.totalFrames} frames (${percent}%) | ${renderedTime}/${totalTime} | ${remainingTime} remaining | ${speed}x speed`;
74
- });
75
- try {
76
- const renderOptions = {
77
- fps,
78
- scale,
79
- includeAudio: options.includeAudio !== false
80
- };
81
- if (fromMs !== void 0) renderOptions.fromMs = fromMs;
82
- if (toMs !== void 0) renderOptions.toMs = toMs;
83
- await page.evaluate(async (opts) => {
84
- await window.EF_RENDER.renderStreaming(opts);
85
- }, renderOptions);
86
- progressSpinner.succeed("Render complete");
87
- } catch (error) {
88
- progressSpinner.fail("Render failed");
89
- throw error;
90
- }
91
- outputStream.end();
92
- await new Promise((resolve, reject) => {
93
- outputStream.on("finish", () => {
94
- log(`Render complete: ${chunkCount} chunks, ${totalBytes} bytes written to ${outputPath}`);
95
- resolve();
59
+ await withProfiling(page, {
60
+ enabled: options.profile === true,
61
+ outputPath: options.profileOutput
62
+ }, async () => {
63
+ const outputStream = createWriteStream(outputPath);
64
+ let chunkCount = 0;
65
+ let totalBytes = 0;
66
+ await page.exposeFunction("onRenderChunk", (chunk) => {
67
+ writeFile(outputPath, chunk.data, { flag: "a" });
68
+ chunkCount++;
69
+ totalBytes += chunk.data.length;
70
+ log(`Received chunk ${chunkCount}: ${chunk.data.length} bytes (total: ${totalBytes} bytes)`);
71
+ });
72
+ if (renderData) {
73
+ await page.evaluate((data) => {
74
+ window.EF_RENDER_DATA = data;
75
+ }, renderData);
76
+ log("Set EF_RENDER_DATA:", renderData);
77
+ }
78
+ await page.waitForFunction(() => typeof window.EF_RENDER !== "undefined", { timeout: 1e4 });
79
+ if (!await page.evaluate(() => window.EF_RENDER?.isReady())) throw new Error("Render API is not ready. No ef-timegroup found.");
80
+ const progressSpinner = ora("Rendering video...").start();
81
+ await page.exposeFunction("onRenderProgress", (progress) => {
82
+ const percent = (progress.progress * 100).toFixed(1);
83
+ const renderedTime = formatTime(progress.renderedMs);
84
+ const totalTime = formatTime(progress.totalDurationMs);
85
+ const remainingTime = formatTime(progress.estimatedRemainingMs);
86
+ const speed = progress.speedMultiplier.toFixed(2);
87
+ progressSpinner.text = `Rendering: ${progress.currentFrame}/${progress.totalFrames} frames (${percent}%) | ${renderedTime}/${totalTime} | ${remainingTime} remaining | ${speed}x speed`;
88
+ });
89
+ try {
90
+ const renderOptions = {
91
+ fps,
92
+ scale,
93
+ includeAudio: options.includeAudio !== false
94
+ };
95
+ if (fromMs !== void 0) renderOptions.fromMs = fromMs;
96
+ if (toMs !== void 0) renderOptions.toMs = toMs;
97
+ await page.evaluate(async (opts) => {
98
+ await window.EF_RENDER.renderStreaming(opts);
99
+ }, renderOptions);
100
+ progressSpinner.succeed("Render complete");
101
+ } catch (error) {
102
+ progressSpinner.fail("Render failed");
103
+ throw error;
104
+ }
105
+ outputStream.end();
106
+ await new Promise((resolve, reject) => {
107
+ outputStream.on("finish", () => {
108
+ log(`Render complete: ${chunkCount} chunks, ${totalBytes} bytes written to ${outputPath}`);
109
+ resolve();
110
+ });
111
+ outputStream.on("error", reject);
96
112
  });
97
- outputStream.on("error", reject);
98
113
  });
99
114
  });
100
115
  process.stderr.write(`\nRender complete: ${outputPath}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","names":["renderData: Record<string, unknown> | undefined","renderOptions: any"],"sources":["../../src/commands/render.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { createWriteStream } from \"node:fs\";\nimport path from \"node:path\";\nimport { program } from \"commander\";\nimport debug from \"debug\";\nimport ora from \"ora\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\n\nconst log = debug(\"ef:cli:render\");\n\n/**\n * Format milliseconds as MM:SS or HH:MM:SS\n */\nfunction formatTime(ms: number): string {\n const totalSeconds = Math.floor(ms / 1000);\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n\n if (hours > 0) {\n return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`;\n }\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n}\n\nprogram\n .command(\"render [directory]\")\n .description(\"Render a video composition locally\")\n .option(\"-o, --output <path>\", \"Output file path\", \"output.mp4\")\n .option(\"-d, --data <json>\", \"Custom render data (JSON string)\")\n .option(\"--data-file <path>\", \"Custom render data from JSON file\")\n .option(\"--fps <number>\", \"Frame rate\", \"30\")\n .option(\"--scale <number>\", \"Resolution scale (0-1)\", \"1\")\n .option(\"--include-audio\", \"Include audio track\", true)\n .option(\"--no-include-audio\", \"Exclude audio track\")\n .option(\"--from-ms <number>\", \"Start time in milliseconds\")\n .option(\"--to-ms <number>\", \"End time in milliseconds\")\n .option(\"--experimental-native-render\", \"Use experimental canvas capture API (faster)\")\n .action(async (directory = \".\", options) => {\n const srcDir = path.resolve(process.cwd(), directory);\n const outputPath = path.resolve(process.cwd(), options.output);\n\n // Parse custom data if provided\n let renderData: Record<string, unknown> | undefined;\n if (options.dataFile) {\n const dataFileContent = await readFile(options.dataFile, \"utf-8\");\n renderData = JSON.parse(dataFileContent);\n log(\"Loaded render data from file:\", options.dataFile);\n } else if (options.data) {\n renderData = JSON.parse(options.data);\n log(\"Using render data from --data option\");\n }\n\n // Parse numeric options\n const fps = parseInt(options.fps, 10);\n const scale = parseFloat(options.scale);\n const fromMs = options.fromMs ? parseInt(options.fromMs, 10) : undefined;\n const toMs = options.toMs ? parseInt(options.toMs, 10) : undefined;\n\n // Start preview server\n const previewServer = await PreviewServer.start(srcDir);\n log(\"Preview server started at:\", previewServer.url);\n\n // Launch browser and render\n await launchBrowserAndWaitForSDK(\n {\n url: previewServer.url,\n headless: true,\n interactive: false,\n efInteractive: false,\n nativeRender: options.experimentalNativeRender === true,\n },\n async (page) => {\n // Open output file for streaming writes\n const outputStream = createWriteStream(outputPath);\n let chunkCount = 0;\n let totalBytes = 0;\n\n // Expose chunk handler - writes directly to file\n await page.exposeFunction(\"onRenderChunk\", (chunkArray: number[]) => {\n const chunk = Buffer.from(chunkArray);\n outputStream.write(chunk);\n chunkCount++;\n totalBytes += chunk.length;\n log(`Received chunk ${chunkCount}: ${chunk.length} bytes (total: ${totalBytes} bytes)`);\n });\n\n // Set custom render data if provided\n if (renderData) {\n await page.evaluate((data) => {\n window.EF_RENDER_DATA = data;\n }, renderData);\n log(\"Set EF_RENDER_DATA:\", renderData);\n }\n\n // Wait for EF_RENDER API to be available\n await page.waitForFunction(\n () => typeof window.EF_RENDER !== \"undefined\",\n { timeout: 10_000 },\n );\n\n // Check if ready\n const isReady = await page.evaluate(() => window.EF_RENDER?.isReady());\n if (!isReady) {\n throw new Error(\"Render API is not ready. No ef-timegroup found.\");\n }\n\n // Create progress spinner\n const progressSpinner = ora(\"Rendering video...\").start();\n\n // Expose progress callback\n await page.exposeFunction(\"onRenderProgress\", (progress: {\n progress: number;\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n estimatedRemainingMs: number;\n speedMultiplier: number;\n }) => {\n const percent = (progress.progress * 100).toFixed(1);\n const renderedTime = formatTime(progress.renderedMs);\n const totalTime = formatTime(progress.totalDurationMs);\n const remainingTime = formatTime(progress.estimatedRemainingMs);\n const speed = progress.speedMultiplier.toFixed(2);\n \n progressSpinner.text = `Rendering: ${progress.currentFrame}/${progress.totalFrames} frames (${percent}%) | ${renderedTime}/${totalTime} | ${remainingTime} remaining | ${speed}x speed`;\n });\n\n // Render with streaming\n try {\n const renderOptions: any = {\n fps,\n scale,\n includeAudio: options.includeAudio !== false,\n };\n\n if (fromMs !== undefined) {\n renderOptions.fromMs = fromMs;\n }\n if (toMs !== undefined) {\n renderOptions.toMs = toMs;\n }\n\n await page.evaluate(async (opts) => {\n await window.EF_RENDER!.renderStreaming(opts);\n }, renderOptions);\n\n progressSpinner.succeed(\"Render complete\");\n } catch (error) {\n progressSpinner.fail(\"Render failed\");\n throw error;\n }\n\n // Close the output stream\n outputStream.end();\n\n // Wait for stream to finish\n await new Promise<void>((resolve, reject) => {\n outputStream.on(\"finish\", () => {\n log(`Render complete: ${chunkCount} chunks, ${totalBytes} bytes written to ${outputPath}`);\n resolve();\n });\n outputStream.on(\"error\", reject);\n });\n },\n );\n\n process.stderr.write(`\\nRender complete: ${outputPath}\\n`);\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"render.js","names":["renderData: Record<string, unknown> | undefined","renderUrl: string","previewServer: PreviewServer | null","renderOptions: any"],"sources":["../../src/commands/render.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { createWriteStream } from \"node:fs\";\nimport path from \"node:path\";\nimport { program } from \"commander\";\nimport debug from \"debug\";\nimport ora from \"ora\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\nimport { StreamTargetChunk } from \"mediabunny\";\nimport { withProfiling } from \"../utils/profileRender.js\";\n\nconst log = debug(\"ef:cli:render\");\n\n/**\n * Format milliseconds as MM:SS or HH:MM:SS\n */\nfunction formatTime(ms: number): string {\n const totalSeconds = Math.floor(ms / 1000);\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n\n if (hours > 0) {\n return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`;\n }\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n}\n\nprogram\n .command(\"render [directory]\")\n .description(\"Render a video composition locally\")\n .option(\"-o, --output <path>\", \"Output file path\", \"output.mp4\")\n .option(\"--url <url>\", \"URL to render (bypasses directory/server startup)\")\n .option(\"-d, --data <json>\", \"Custom render data (JSON string)\")\n .option(\"--data-file <path>\", \"Custom render data from JSON file\")\n .option(\"--fps <number>\", \"Frame rate\", \"30\")\n .option(\"--scale <number>\", \"Resolution scale (0-1)\", \"1\")\n .option(\"--include-audio\", \"Include audio track\", true)\n .option(\"--no-include-audio\", \"Exclude audio track\")\n .option(\"--from-ms <number>\", \"Start time in milliseconds\")\n .option(\"--to-ms <number>\", \"End time in milliseconds\")\n .option(\"--experimental-native-render\", \"Use experimental canvas capture API (faster)\")\n .option(\"--profile\", \"Enable CPU profiling\")\n .option(\"--profile-output <path>\", \"Profile output path\", \"./render-profile.cpuprofile\")\n .action(async (directory = \".\", options) => {\n const outputPath = path.resolve(process.cwd(), options.output);\n\n // Parse custom data if provided\n let renderData: Record<string, unknown> | undefined;\n if (options.dataFile) {\n const dataFileContent = await readFile(options.dataFile, \"utf-8\");\n renderData = JSON.parse(dataFileContent);\n log(\"Loaded render data from file:\", options.dataFile);\n } else if (options.data) {\n renderData = JSON.parse(options.data);\n log(\"Using render data from --data option\");\n }\n\n // Parse numeric options\n const fps = parseInt(options.fps, 10);\n const scale = parseFloat(options.scale);\n const fromMs = options.fromMs ? parseInt(options.fromMs, 10) : undefined;\n const toMs = options.toMs ? parseInt(options.toMs, 10) : undefined;\n\n // Determine URL to render\n let renderUrl: string;\n let previewServer: PreviewServer | null = null;\n\n if (options.url) {\n // Use provided URL directly\n renderUrl = options.url;\n log(\"Using provided URL:\", renderUrl);\n } else {\n // Start preview server for directory\n const srcDir = path.resolve(process.cwd(), directory);\n previewServer = await PreviewServer.start(srcDir);\n renderUrl = previewServer.url;\n log(\"Preview server started at:\", renderUrl);\n }\n\n // Launch browser and render\n await launchBrowserAndWaitForSDK(\n {\n url: renderUrl,\n headless: true,\n interactive: false,\n efInteractive: false,\n nativeRender: options.experimentalNativeRender === true,\n profile: options.profile === true,\n profileOutput: options.profileOutput,\n },\n async (page) => {\n await withProfiling(\n page,\n {\n enabled: options.profile === true,\n outputPath: options.profileOutput,\n },\n async () => {\n // Open output file for streaming writes\n const outputStream = createWriteStream(outputPath);\n let chunkCount = 0;\n let totalBytes = 0;\n\n // Expose chunk handler - writes directly to file\n await page.exposeFunction(\"onRenderChunk\", (chunk: StreamTargetChunk) => {\n writeFile(outputPath, chunk.data, { flag: \"a\" });\n chunkCount++;\n totalBytes += chunk.data.length;\n log(`Received chunk ${chunkCount}: ${chunk.data.length} bytes (total: ${totalBytes} bytes)`);\n });\n\n // Set custom render data if provided\n if (renderData) {\n await page.evaluate((data) => {\n window.EF_RENDER_DATA = data;\n }, renderData);\n log(\"Set EF_RENDER_DATA:\", renderData);\n }\n\n // Wait for EF_RENDER API to be available\n await page.waitForFunction(\n () => typeof window.EF_RENDER !== \"undefined\",\n { timeout: 10_000 },\n );\n\n // Check if ready\n const isReady = await page.evaluate(() => window.EF_RENDER?.isReady());\n if (!isReady) {\n throw new Error(\"Render API is not ready. No ef-timegroup found.\");\n }\n\n // Create progress spinner\n const progressSpinner = ora(\"Rendering video...\").start();\n\n // Expose progress callback\n await page.exposeFunction(\"onRenderProgress\", (progress: {\n progress: number;\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n estimatedRemainingMs: number;\n speedMultiplier: number;\n }) => {\n const percent = (progress.progress * 100).toFixed(1);\n const renderedTime = formatTime(progress.renderedMs);\n const totalTime = formatTime(progress.totalDurationMs);\n const remainingTime = formatTime(progress.estimatedRemainingMs);\n const speed = progress.speedMultiplier.toFixed(2);\n \n progressSpinner.text = `Rendering: ${progress.currentFrame}/${progress.totalFrames} frames (${percent}%) | ${renderedTime}/${totalTime} | ${remainingTime} remaining | ${speed}x speed`;\n });\n\n // Render with streaming\n try {\n const renderOptions: any = {\n fps,\n scale,\n includeAudio: options.includeAudio !== false,\n };\n\n if (fromMs !== undefined) {\n renderOptions.fromMs = fromMs;\n }\n if (toMs !== undefined) {\n renderOptions.toMs = toMs;\n }\n\n await page.evaluate(async (opts) => {\n await window.EF_RENDER!.renderStreaming(opts);\n }, renderOptions);\n\n progressSpinner.succeed(\"Render complete\");\n } catch (error) {\n progressSpinner.fail(\"Render failed\");\n throw error;\n }\n\n // Close the output stream\n outputStream.end();\n\n // Wait for stream to finish\n await new Promise<void>((resolve, reject) => {\n outputStream.on(\"finish\", () => {\n log(`Render complete: ${chunkCount} chunks, ${totalBytes} bytes written to ${outputPath}`);\n resolve();\n });\n outputStream.on(\"error\", reject);\n });\n },\n );\n },\n );\n\n process.stderr.write(`\\nRender complete: ${outputPath}\\n`);\n });\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"launchBrowserAndWaitForSDK.js","names":["launchOptions: Parameters<typeof chromium.launch>[0]","pageOptions: Parameters<Browser[\"newPage\"]>[0]"],"sources":["../../src/utils/launchBrowserAndWaitForSDK.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport debug from \"debug\";\nimport { type Browser, chromium, type Page } from \"playwright\";\n\nimport { requireChrome } from \"./detectChrome.js\";\nimport { withSpinner } from \"./withSpinner.js\";\n\nconst browserLog = debug(\"ef:cli::browser\");\n\ninterface LaunchOptions {\n url: string;\n headless?: boolean;\n interactive?: boolean;\n efInteractive?: boolean;\n nativeRender?: boolean;\n chromePath?: string;\n}\n\nexport async function launchBrowserAndWaitForSDK(\n options: LaunchOptions,\n fn: (page: Page) => Promise<void>,\n) {\n // Detect Chrome before launching (only for non-interactive renders)\n if (options.interactive !== true && !options.chromePath) {\n requireChrome();\n }\n\n const browser = await withSpinner(\"Launching chrome\", async () => {\n const launchOptions: Parameters<typeof chromium.launch>[0] = {\n channel: \"chrome\",\n headless: options.headless ?? true,\n devtools: options.interactive === true,\n };\n\n // Use custom Chrome path if provided\n if (options.chromePath) {\n launchOptions.executablePath = options.chromePath;\n // Don't use channel when providing explicit path\n delete launchOptions.channel;\n }\n\n return chromium.launch(launchOptions);\n });\n\n const page = await withSpinner(\"Loading Editframe SDK\", async () => {\n const pageOptions: Parameters<Browser[\"newPage\"]>[0] = {};\n if (options.interactive === true) {\n // By default, playwright uses its own viewport, so resizing the browser window\n // doesn't actually change the viewport. And the gui doesn't scale to fit.\n // This is not desirable for interactive mode, so we disable the viewport feature.\n pageOptions.viewport = null;\n }\n const page = await browser.newPage(pageOptions);\n page.on(\"console\", (msg) => {\n browserLog(chalk.blue(`browser (${msg.type()}) |`), msg.text());\n });\n\n // Build URL with query parameters\n const urlParams = new URLSearchParams();\n if (!options.efInteractive) {\n urlParams.set(\"EF_NONINTERACTIVE\", \"1\");\n }\n if (options.nativeRender) {\n urlParams.set(\"EF_NATIVE_RENDER\", \"1\");\n }\n const url = options.url + (urlParams.toString() ? `?${urlParams.toString()}` : \"\");\n\n process.stderr.write(\"\\nLoading url: \");\n process.stderr.write(url);\n process.stderr.write(\"\\n\");\n await page.goto(url);\n await page.waitForFunction(\n () => {\n return (\n // @ts-expect-error\n window.EF_REGISTERED\n );\n },\n [],\n { timeout: 10_000 },\n );\n return page;\n });\n await fn(page);\n if (options.interactive !== true) {\n await browser.close();\n process.exit(0);\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAM,aAAa,MAAM,kBAAkB;AAW3C,eAAsB,2BACpB,SACA,IACA;AAEA,KAAI,QAAQ,gBAAgB,QAAQ,CAAC,QAAQ,WAC3C,gBAAe;CAGjB,MAAM,UAAU,MAAM,YAAY,oBAAoB,YAAY;EAChE,MAAMA,gBAAuD;GAC3D,SAAS;GACT,UAAU,QAAQ,YAAY;GAC9B,UAAU,QAAQ,gBAAgB;GACnC;AAGD,MAAI,QAAQ,YAAY;AACtB,iBAAc,iBAAiB,QAAQ;AAEvC,UAAO,cAAc;;AAGvB,SAAO,SAAS,OAAO,cAAc;GACrC;AAyCF,OAAM,GAvCO,MAAM,YAAY,yBAAyB,YAAY;EAClE,MAAMC,cAAiD,EAAE;AACzD,MAAI,QAAQ,gBAAgB,KAI1B,aAAY,WAAW;EAEzB,MAAM,OAAO,MAAM,QAAQ,QAAQ,YAAY;AAC/C,OAAK,GAAG,YAAY,QAAQ;AAC1B,cAAW,MAAM,KAAK,YAAY,IAAI,MAAM,CAAC,KAAK,EAAE,IAAI,MAAM,CAAC;IAC/D;EAGF,MAAM,YAAY,IAAI,iBAAiB;AACvC,MAAI,CAAC,QAAQ,cACX,WAAU,IAAI,qBAAqB,IAAI;AAEzC,MAAI,QAAQ,aACV,WAAU,IAAI,oBAAoB,IAAI;EAExC,MAAM,MAAM,QAAQ,OAAO,UAAU,UAAU,GAAG,IAAI,UAAU,UAAU,KAAK;AAE/E,UAAQ,OAAO,MAAM,kBAAkB;AACvC,UAAQ,OAAO,MAAM,IAAI;AACzB,UAAQ,OAAO,MAAM,KAAK;AAC1B,QAAM,KAAK,KAAK,IAAI;AACpB,QAAM,KAAK,sBACH;AACJ,UAEE,OAAO;KAGX,EAAE,EACF,EAAE,SAAS,KAAQ,CACpB;AACD,SAAO;GACP,CACY;AACd,KAAI,QAAQ,gBAAgB,MAAM;AAChC,QAAM,QAAQ,OAAO;AACrB,UAAQ,KAAK,EAAE"}
1
+ {"version":3,"file":"launchBrowserAndWaitForSDK.js","names":["launchOptions: Parameters<typeof chromium.launch>[0]","pageOptions: Parameters<Browser[\"newPage\"]>[0]"],"sources":["../../src/utils/launchBrowserAndWaitForSDK.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport debug from \"debug\";\nimport { type Browser, chromium, type Page } from \"playwright\";\n\nimport { requireChrome } from \"./detectChrome.js\";\nimport { withSpinner } from \"./withSpinner.js\";\n\nconst browserLog = debug(\"ef:cli::browser\");\n\ninterface LaunchOptions {\n url: string;\n headless?: boolean;\n interactive?: boolean;\n efInteractive?: boolean;\n nativeRender?: boolean;\n chromePath?: string;\n profile?: boolean;\n profileOutput?: string;\n}\n\nexport async function launchBrowserAndWaitForSDK(\n options: LaunchOptions,\n fn: (page: Page) => Promise<void>,\n) {\n // Detect Chrome before launching (only for non-interactive renders)\n if (options.interactive !== true && !options.chromePath) {\n requireChrome();\n }\n\n const browser = await withSpinner(\"Launching chrome\", async () => {\n const launchOptions: Parameters<typeof chromium.launch>[0] = {\n channel: \"chrome\",\n headless: options.headless ?? true,\n devtools: options.interactive === true,\n };\n\n // Use custom Chrome path if provided\n if (options.chromePath) {\n launchOptions.executablePath = options.chromePath;\n // Don't use channel when providing explicit path\n delete launchOptions.channel;\n }\n\n return chromium.launch(launchOptions);\n });\n\n const page = await withSpinner(\"Loading Editframe SDK\", async () => {\n const pageOptions: Parameters<Browser[\"newPage\"]>[0] = {};\n if (options.interactive === true) {\n // By default, playwright uses its own viewport, so resizing the browser window\n // doesn't actually change the viewport. And the gui doesn't scale to fit.\n // This is not desirable for interactive mode, so we disable the viewport feature.\n pageOptions.viewport = null;\n }\n const page = await browser.newPage(pageOptions);\n page.on(\"console\", (msg) => {\n browserLog(chalk.blue(`browser (${msg.type()}) |`), msg.text());\n });\n\n // Build URL with query parameters\n const urlParams = new URLSearchParams();\n if (!options.efInteractive) {\n urlParams.set(\"EF_NONINTERACTIVE\", \"1\");\n }\n if (options.nativeRender) {\n urlParams.set(\"EF_NATIVE_RENDER\", \"1\");\n }\n const url = options.url + (urlParams.toString() ? `?${urlParams.toString()}` : \"\");\n\n process.stderr.write(\"\\nLoading url: \");\n process.stderr.write(url);\n process.stderr.write(\"\\n\");\n await page.goto(url);\n await page.waitForFunction(\n () => {\n return (\n // @ts-expect-error\n window.EF_REGISTERED\n );\n },\n [],\n { timeout: 10_000 },\n );\n return page;\n });\n await fn(page);\n if (options.interactive !== true) {\n await browser.close();\n process.exit(0);\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAM,aAAa,MAAM,kBAAkB;AAa3C,eAAsB,2BACpB,SACA,IACA;AAEA,KAAI,QAAQ,gBAAgB,QAAQ,CAAC,QAAQ,WAC3C,gBAAe;CAGjB,MAAM,UAAU,MAAM,YAAY,oBAAoB,YAAY;EAChE,MAAMA,gBAAuD;GAC3D,SAAS;GACT,UAAU,QAAQ,YAAY;GAC9B,UAAU,QAAQ,gBAAgB;GACnC;AAGD,MAAI,QAAQ,YAAY;AACtB,iBAAc,iBAAiB,QAAQ;AAEvC,UAAO,cAAc;;AAGvB,SAAO,SAAS,OAAO,cAAc;GACrC;AAyCF,OAAM,GAvCO,MAAM,YAAY,yBAAyB,YAAY;EAClE,MAAMC,cAAiD,EAAE;AACzD,MAAI,QAAQ,gBAAgB,KAI1B,aAAY,WAAW;EAEzB,MAAM,OAAO,MAAM,QAAQ,QAAQ,YAAY;AAC/C,OAAK,GAAG,YAAY,QAAQ;AAC1B,cAAW,MAAM,KAAK,YAAY,IAAI,MAAM,CAAC,KAAK,EAAE,IAAI,MAAM,CAAC;IAC/D;EAGF,MAAM,YAAY,IAAI,iBAAiB;AACvC,MAAI,CAAC,QAAQ,cACX,WAAU,IAAI,qBAAqB,IAAI;AAEzC,MAAI,QAAQ,aACV,WAAU,IAAI,oBAAoB,IAAI;EAExC,MAAM,MAAM,QAAQ,OAAO,UAAU,UAAU,GAAG,IAAI,UAAU,UAAU,KAAK;AAE/E,UAAQ,OAAO,MAAM,kBAAkB;AACvC,UAAQ,OAAO,MAAM,IAAI;AACzB,UAAQ,OAAO,MAAM,KAAK;AAC1B,QAAM,KAAK,KAAK,IAAI;AACpB,QAAM,KAAK,sBACH;AACJ,UAEE,OAAO;KAGX,EAAE,EACF,EAAE,SAAS,KAAQ,CACpB;AACD,SAAO;GACP,CACY;AACd,KAAI,QAAQ,gBAAgB,MAAM;AAChC,QAAM,QAAQ,OAAO;AACrB,UAAQ,KAAK,EAAE"}
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+ import { writeFile } from "node:fs/promises";
3
+
4
+ //#region src/utils/profileRender.ts
5
+ async function withProfiling(page, options, fn) {
6
+ if (!options.enabled) return await fn();
7
+ const client = await page.context().newCDPSession(page);
8
+ await client.send("Profiler.enable");
9
+ await client.send("Profiler.setSamplingInterval", { interval: 100 });
10
+ await client.send("Profiler.start");
11
+ console.error("🔎 CPU profiling started...");
12
+ const startTime = Date.now();
13
+ const result = await fn();
14
+ const { profile } = await client.send("Profiler.stop");
15
+ await client.send("Profiler.disable");
16
+ const duration = Date.now() - startTime;
17
+ console.error(`✅ CPU profiling complete (${(duration / 1e3).toFixed(1)}s)`);
18
+ const outputPath = options.outputPath || "./render-profile.cpuprofile";
19
+ const absolutePath = path.resolve(process.cwd(), outputPath);
20
+ await writeFile(absolutePath, JSON.stringify(profile, null, 2));
21
+ console.error(`ðŸ’ū Profile saved to: ${absolutePath}`);
22
+ const samples = profile.samples?.length || 0;
23
+ console.error(`📊 Captured ${samples.toLocaleString()} samples`);
24
+ const hitCounts = /* @__PURE__ */ new Map();
25
+ if (profile.samples) for (const sample of profile.samples) hitCounts.set(sample, (hitCounts.get(sample) || 0) + 1);
26
+ const hotspots = profile.nodes.map((node) => ({
27
+ name: node.callFrame.functionName || "(anonymous)",
28
+ url: node.callFrame.url,
29
+ line: node.callFrame.lineNumber,
30
+ hits: hitCounts.get(node.id) || 0
31
+ })).filter((h) => h.hits > 0).sort((a, b) => b.hits - a.hits).slice(0, 20);
32
+ if (hotspots.length > 0) {
33
+ console.error("\n📊 Top 20 Hotspots:");
34
+ for (const h of hotspots) {
35
+ const file = h.url.split("/").pop() || h.url;
36
+ console.error(` ${h.hits.toString().padStart(6)} samples | ${h.name} @ ${file}:${h.line}`);
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ //#endregion
43
+ export { withProfiling };
44
+ //# sourceMappingURL=profileRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileRender.js","names":[],"sources":["../../src/utils/profileRender.ts"],"sourcesContent":["import { Page } from \"playwright\";\nimport { writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface ProfileOptions {\n enabled: boolean;\n outputPath?: string;\n}\n\nexport async function withProfiling<T>(\n page: Page,\n options: ProfileOptions,\n fn: () => Promise<T>\n): Promise<T> {\n if (!options.enabled) {\n return await fn();\n }\n\n // Get CDP session\n const client = await page.context().newCDPSession(page);\n \n // Start profiling\n await client.send('Profiler.enable');\n await client.send('Profiler.setSamplingInterval', { interval: 100 }); // 100Ξs\n await client.send('Profiler.start');\n \n console.error('🔎 CPU profiling started...');\n const startTime = Date.now();\n \n // Run the render\n const result = await fn();\n \n // Stop profiling\n const { profile } = await client.send('Profiler.stop');\n await client.send('Profiler.disable');\n \n const duration = Date.now() - startTime;\n console.error(`✅ CPU profiling complete (${(duration/1000).toFixed(1)}s)`);\n \n // Save profile\n const outputPath = options.outputPath || './render-profile.cpuprofile';\n const absolutePath = path.resolve(process.cwd(), outputPath);\n await writeFile(absolutePath, JSON.stringify(profile, null, 2));\n console.error(`ðŸ’ū Profile saved to: ${absolutePath}`);\n \n // Basic analysis\n const samples = profile.samples?.length || 0;\n console.error(`📊 Captured ${samples.toLocaleString()} samples`);\n \n // Calculate hotspots\n const hitCounts = new Map<number, number>();\n if (profile.samples) {\n for (const sample of profile.samples) {\n hitCounts.set(sample, (hitCounts.get(sample) || 0) + 1);\n }\n }\n \n // Find top functions\n const hotspots = profile.nodes\n .map((node: any) => ({\n name: node.callFrame.functionName || '(anonymous)',\n url: node.callFrame.url,\n line: node.callFrame.lineNumber,\n hits: hitCounts.get(node.id) || 0,\n }))\n .filter(h => h.hits > 0)\n .sort((a, b) => b.hits - a.hits)\n .slice(0, 20);\n \n if (hotspots.length > 0) {\n console.error('\\n📊 Top 20 Hotspots:');\n for (const h of hotspots) {\n const file = h.url.split('/').pop() || h.url;\n console.error(` ${h.hits.toString().padStart(6)} samples | ${h.name} @ ${file}:${h.line}`);\n }\n }\n \n return result;\n}\n"],"mappings":";;;;AASA,eAAsB,cACpB,MACA,SACA,IACY;AACZ,KAAI,CAAC,QAAQ,QACX,QAAO,MAAM,IAAI;CAInB,MAAM,SAAS,MAAM,KAAK,SAAS,CAAC,cAAc,KAAK;AAGvD,OAAM,OAAO,KAAK,kBAAkB;AACpC,OAAM,OAAO,KAAK,gCAAgC,EAAE,UAAU,KAAK,CAAC;AACpE,OAAM,OAAO,KAAK,iBAAiB;AAEnC,SAAQ,MAAM,8BAA8B;CAC5C,MAAM,YAAY,KAAK,KAAK;CAG5B,MAAM,SAAS,MAAM,IAAI;CAGzB,MAAM,EAAE,YAAY,MAAM,OAAO,KAAK,gBAAgB;AACtD,OAAM,OAAO,KAAK,mBAAmB;CAErC,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,SAAQ,MAAM,8BAA8B,WAAS,KAAM,QAAQ,EAAE,CAAC,IAAI;CAG1E,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,eAAe,KAAK,QAAQ,QAAQ,KAAK,EAAE,WAAW;AAC5D,OAAM,UAAU,cAAc,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC;AAC/D,SAAQ,MAAM,wBAAwB,eAAe;CAGrD,MAAM,UAAU,QAAQ,SAAS,UAAU;AAC3C,SAAQ,MAAM,eAAe,QAAQ,gBAAgB,CAAC,UAAU;CAGhE,MAAM,4BAAY,IAAI,KAAqB;AAC3C,KAAI,QAAQ,QACV,MAAK,MAAM,UAAU,QAAQ,QAC3B,WAAU,IAAI,SAAS,UAAU,IAAI,OAAO,IAAI,KAAK,EAAE;CAK3D,MAAM,WAAW,QAAQ,MACtB,KAAK,UAAe;EACnB,MAAM,KAAK,UAAU,gBAAgB;EACrC,KAAK,KAAK,UAAU;EACpB,MAAM,KAAK,UAAU;EACrB,MAAM,UAAU,IAAI,KAAK,GAAG,IAAI;EACjC,EAAE,CACF,QAAO,MAAK,EAAE,OAAO,EAAE,CACvB,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,CAC/B,MAAM,GAAG,GAAG;AAEf,KAAI,SAAS,SAAS,GAAG;AACvB,UAAQ,MAAM,wBAAwB;AACtC,OAAK,MAAM,KAAK,UAAU;GACxB,MAAM,OAAO,EAAE,IAAI,MAAM,IAAI,CAAC,KAAK,IAAI,EAAE;AACzC,WAAQ,MAAM,KAAK,EAAE,KAAK,UAAU,CAAC,SAAS,EAAE,CAAC,aAAa,EAAE,KAAK,KAAK,KAAK,GAAG,EAAE,OAAO;;;AAI/F,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/cli",
3
- "version": "0.33.0-beta",
3
+ "version": "0.34.5-beta",
4
4
  "description": "Command line interface for EditFrame",
5
5
  "bin": {
6
6
  "editframe": "./dist/index.js"
@@ -22,10 +22,10 @@
22
22
  "typescript": "^5.5.4"
23
23
  },
24
24
  "dependencies": {
25
- "@editframe/api": "0.33.0-beta",
26
- "@editframe/assets": "0.33.0-beta",
27
- "@editframe/elements": "0.33.0-beta",
28
- "@editframe/vite-plugin": "0.33.0-beta",
25
+ "@editframe/api": "0.34.5-beta",
26
+ "@editframe/assets": "0.34.5-beta",
27
+ "@editframe/elements": "0.34.5-beta",
28
+ "@editframe/vite-plugin": "0.34.5-beta",
29
29
  "@inquirer/prompts": "^5.3.8",
30
30
  "chalk": "^5.3.0",
31
31
  "commander": "^12.0.0",