@editframe/cli 0.35.0-beta → 0.36.0-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.35.0-beta";
2
+ const VERSION = "0.36.0-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.35.0-beta\";\n"],"mappings":";AAAA,MAAa,UAAU"}
1
+ {"version":3,"file":"VERSION.js","names":[],"sources":["../src/VERSION.ts"],"sourcesContent":["export const VERSION = \"0.36.0-beta\";\n"],"mappings":";AAAA,MAAa,UAAU"}
@@ -31,16 +31,18 @@ const buildAssetId = async (srcDir, src, basename$1) => {
31
31
  };
32
32
  program.command("cloud-render [directory]").description("Render a directory's index.html file as a video in the editframe cloud").addOption(new Option("-s, --strategy <strategy>", "Render strategy").choices(["v1"]).default("v1")).action(async (directory, options) => {
33
33
  directory ??= ".";
34
- await syncAssetDirectory(join(process.cwd(), directory, "src", "assets", ".cache"));
35
- const srcDir = path.join(directory, "src");
36
- const distDir = path.join(directory, "dist");
34
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
35
+ const resolvedDirectory = path.resolve(baseCwd, directory);
36
+ await syncAssetDirectory(join(resolvedDirectory, "src", "assets", ".cache"));
37
+ const srcDir = path.join(resolvedDirectory, "src");
38
+ const distDir = path.join(resolvedDirectory, "dist");
37
39
  await withSpinner("Building\n", async () => {
38
40
  try {
39
41
  await withSpinner("Building\n", async () => {
40
42
  spawnSync("npx", [
41
43
  "vite",
42
44
  "build",
43
- directory,
45
+ resolvedDirectory,
44
46
  "--clearScreen",
45
47
  "false",
46
48
  "--logLevel",
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-render.js","names":["basename","parseHTML"],"sources":["../../src/commands/cloud-render.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path, { basename, join } from \"node:path\";\nimport { PassThrough } from \"node:stream\";\nimport { inspect } from \"node:util\";\nimport { createRender, uploadRender } from \"@editframe/api\";\nimport { md5Directory, md5FilePath } from \"@editframe/assets\";\nimport { getRenderInfo, RenderInfo } from \"@editframe/elements/node\";\nimport { Option, program } from \"commander\";\nimport debug from \"debug\";\nimport { parse as parseHTML } from \"node-html-parser\";\nimport * as tar from \"tar\";\nimport { processRenderInfo } from \"../operations/processRenderInfo.js\";\nimport { SyncStatus } from \"../operations/syncAssetsDirectory/SyncStatus.js\";\nimport { syncAssetDirectory } from \"../operations/syncAssetsDirectory.js\";\nimport { createReadableStreamFromReadable } from \"../utils/createReadableStreamFromReadable.js\";\nimport { getClient } from \"../utils/index.js\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\nimport { validateVideoResolution } from \"../utils/validateVideoResolution.js\";\nimport { withSpinner } from \"../utils/withSpinner.js\";\n\nconst log = debug(\"ef:cli:render\");\n\nexport const buildAssetId = async (\n srcDir: string,\n src: string,\n basename: string,\n) => {\n log(`Building image asset id for ${src}\\n`);\n const assetPath = path.join(srcDir, src);\n const assetMd5 = await md5FilePath(assetPath);\n const syncStatus = new SyncStatus(\n join(srcDir, \"assets\", \".cache\", assetMd5, basename),\n );\n const info = await syncStatus.readInfo();\n if (!info) {\n throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);\n }\n\n return info.id;\n};\n\nprogram\n .command(\"cloud-render [directory]\")\n .description(\n \"Render a directory's index.html file as a video in the editframe cloud\",\n )\n .addOption(\n new Option(\"-s, --strategy <strategy>\", \"Render strategy\")\n .choices([\"v1\"])\n .default(\"v1\"),\n )\n .action(async (directory, options) => {\n directory ??= \".\";\n\n await syncAssetDirectory(\n join(process.cwd(), directory, \"src\", \"assets\", \".cache\"),\n );\n\n const srcDir = path.join(directory, \"src\");\n const distDir = path.join(directory, \"dist\");\n await withSpinner(\"Building\\n\", async () => {\n try {\n await withSpinner(\"Building\\n\", async () => {\n spawnSync(\n \"npx\",\n // biome-ignore format: Grouping CLI arguments\n [\n \"vite\",\n \"build\",\n directory,\n \"--clearScreen\",\n \"false\",\n \"--logLevel\",\n \"debug\",\n ],\n {\n stdio: \"inherit\",\n },\n );\n });\n } catch (error) {\n console.error(\"Build failed:\", error);\n }\n });\n\n const previewServer = await PreviewServer.start(distDir);\n process.stderr.write(\"Preview server started at:\");\n process.stderr.write(previewServer.url);\n process.stderr.write(\"\\n\");\n await launchBrowserAndWaitForSDK(\n {\n url: previewServer.url,\n efInteractive: false,\n interactive: false,\n headless: true,\n },\n async (page) => {\n const renderInfo = RenderInfo.parse(await page.evaluate(getRenderInfo));\n\n validateVideoResolution({\n width: renderInfo.width,\n height: renderInfo.height,\n });\n\n await processRenderInfo(renderInfo);\n\n const doc = parseHTML(\n await readFile(path.join(distDir, \"index.html\"), \"utf-8\"),\n );\n\n log(\"Building asset IDs\");\n for (const element of doc.querySelectorAll(\n \"ef-image, ef-audio, ef-video\",\n )) {\n log(`Processing ${element.tagName}`);\n if (element.hasAttribute(\"asset-id\")) {\n log(\n `Asset ID for ${element.tagName} ${element.getAttribute(\"src\")} is ${element.getAttribute(\"asset-id\")}`,\n );\n continue;\n }\n const src = element.getAttribute(\"src\");\n if (!src) {\n log(`No src attribute for ${element.tagName}`);\n continue;\n }\n\n switch (element.tagName) {\n case \"EF-IMAGE\":\n element.setAttribute(\n \"asset-id\",\n await buildAssetId(srcDir, src, basename(src)),\n );\n break;\n case \"EF-AUDIO\":\n case \"EF-VIDEO\":\n element.setAttribute(\n \"asset-id\",\n await buildAssetId(srcDir, src, \"isobmff\"),\n );\n break;\n default:\n log(`Unknown element type: ${element.tagName}`);\n }\n }\n\n await writeFile(path.join(distDir, \"index.html\"), doc.toString());\n\n const md5 = await md5Directory(distDir);\n const render = await createRender(getClient(), {\n md5,\n width: renderInfo.width,\n height: renderInfo.height,\n fps: renderInfo.fps,\n duration_ms: renderInfo.durationMs,\n work_slice_ms: 4_000,\n strategy: options.strategy,\n });\n if (render?.status !== \"created\") {\n process.stderr.write(\n `Render is in '${render?.status}' status. It cannot be recreated while in this status.\\n`,\n );\n return;\n }\n /**\n * This tar stream is created with the dist directory as the root.\n * This is acheived by setting the cwd option to the dist directory.\n * And the files to be included in the tar stream are all files in the dist directory.\n *\n * The renderer expects to find the index.html file at the root of the tar stream.\n */\n const tarStream = tar.create(\n {\n gzip: true,\n cwd: distDir,\n },\n [\".\"],\n );\n const readable = new PassThrough();\n tarStream.pipe(readable);\n\n await uploadRender(\n getClient(),\n render.id,\n createReadableStreamFromReadable(readable),\n );\n process.stderr.write(\"Render assets uploaded\\n\");\n process.stderr.write(inspect(render));\n process.stderr.write(\"\\n\");\n },\n );\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"cloud-render.js","names":["basename","parseHTML"],"sources":["../../src/commands/cloud-render.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path, { basename, join } from \"node:path\";\nimport { PassThrough } from \"node:stream\";\nimport { inspect } from \"node:util\";\nimport { createRender, uploadRender } from \"@editframe/api\";\nimport { md5Directory, md5FilePath } from \"@editframe/assets\";\nimport { getRenderInfo, RenderInfo } from \"@editframe/elements/node\";\nimport { Option, program } from \"commander\";\nimport debug from \"debug\";\nimport { parse as parseHTML } from \"node-html-parser\";\nimport * as tar from \"tar\";\nimport { processRenderInfo } from \"../operations/processRenderInfo.js\";\nimport { SyncStatus } from \"../operations/syncAssetsDirectory/SyncStatus.js\";\nimport { syncAssetDirectory } from \"../operations/syncAssetsDirectory.js\";\nimport { createReadableStreamFromReadable } from \"../utils/createReadableStreamFromReadable.js\";\nimport { getClient } from \"../utils/index.js\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\nimport { validateVideoResolution } from \"../utils/validateVideoResolution.js\";\nimport { withSpinner } from \"../utils/withSpinner.js\";\n\nconst log = debug(\"ef:cli:render\");\n\nexport const buildAssetId = async (\n srcDir: string,\n src: string,\n basename: string,\n) => {\n log(`Building image asset id for ${src}\\n`);\n const assetPath = path.join(srcDir, src);\n const assetMd5 = await md5FilePath(assetPath);\n const syncStatus = new SyncStatus(\n join(srcDir, \"assets\", \".cache\", assetMd5, basename),\n );\n const info = await syncStatus.readInfo();\n if (!info) {\n throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);\n }\n\n return info.id;\n};\n\nprogram\n .command(\"cloud-render [directory]\")\n .description(\n \"Render a directory's index.html file as a video in the editframe cloud\",\n )\n .addOption(\n new Option(\"-s, --strategy <strategy>\", \"Render strategy\")\n .choices([\"v1\"])\n .default(\"v1\"),\n )\n .action(async (directory, options) => {\n directory ??= \".\";\n\n // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const resolvedDirectory = path.resolve(baseCwd, directory);\n\n await syncAssetDirectory(\n join(resolvedDirectory, \"src\", \"assets\", \".cache\"),\n );\n\n const srcDir = path.join(resolvedDirectory, \"src\");\n const distDir = path.join(resolvedDirectory, \"dist\");\n await withSpinner(\"Building\\n\", async () => {\n try {\n await withSpinner(\"Building\\n\", async () => {\n spawnSync(\n \"npx\",\n // biome-ignore format: Grouping CLI arguments\n [\n \"vite\",\n \"build\",\n resolvedDirectory,\n \"--clearScreen\",\n \"false\",\n \"--logLevel\",\n \"debug\",\n ],\n {\n stdio: \"inherit\",\n },\n );\n });\n } catch (error) {\n console.error(\"Build failed:\", error);\n }\n });\n\n const previewServer = await PreviewServer.start(distDir);\n process.stderr.write(\"Preview server started at:\");\n process.stderr.write(previewServer.url);\n process.stderr.write(\"\\n\");\n await launchBrowserAndWaitForSDK(\n {\n url: previewServer.url,\n efInteractive: false,\n interactive: false,\n headless: true,\n },\n async (page) => {\n const renderInfo = RenderInfo.parse(await page.evaluate(getRenderInfo));\n\n validateVideoResolution({\n width: renderInfo.width,\n height: renderInfo.height,\n });\n\n await processRenderInfo(renderInfo);\n\n const doc = parseHTML(\n await readFile(path.join(distDir, \"index.html\"), \"utf-8\"),\n );\n\n log(\"Building asset IDs\");\n for (const element of doc.querySelectorAll(\n \"ef-image, ef-audio, ef-video\",\n )) {\n log(`Processing ${element.tagName}`);\n if (element.hasAttribute(\"asset-id\")) {\n log(\n `Asset ID for ${element.tagName} ${element.getAttribute(\"src\")} is ${element.getAttribute(\"asset-id\")}`,\n );\n continue;\n }\n const src = element.getAttribute(\"src\");\n if (!src) {\n log(`No src attribute for ${element.tagName}`);\n continue;\n }\n\n switch (element.tagName) {\n case \"EF-IMAGE\":\n element.setAttribute(\n \"asset-id\",\n await buildAssetId(srcDir, src, basename(src)),\n );\n break;\n case \"EF-AUDIO\":\n case \"EF-VIDEO\":\n element.setAttribute(\n \"asset-id\",\n await buildAssetId(srcDir, src, \"isobmff\"),\n );\n break;\n default:\n log(`Unknown element type: ${element.tagName}`);\n }\n }\n\n await writeFile(path.join(distDir, \"index.html\"), doc.toString());\n\n const md5 = await md5Directory(distDir);\n const render = await createRender(getClient(), {\n md5,\n width: renderInfo.width,\n height: renderInfo.height,\n fps: renderInfo.fps,\n duration_ms: renderInfo.durationMs,\n work_slice_ms: 4_000,\n strategy: options.strategy,\n });\n if (render?.status !== \"created\") {\n process.stderr.write(\n `Render is in '${render?.status}' status. It cannot be recreated while in this status.\\n`,\n );\n return;\n }\n /**\n * This tar stream is created with the dist directory as the root.\n * This is acheived by setting the cwd option to the dist directory.\n * And the files to be included in the tar stream are all files in the dist directory.\n *\n * The renderer expects to find the index.html file at the root of the tar stream.\n */\n const tarStream = tar.create(\n {\n gzip: true,\n cwd: distDir,\n },\n [\".\"],\n );\n const readable = new PassThrough();\n tarStream.pipe(readable);\n\n await uploadRender(\n getClient(),\n render.id,\n createReadableStreamFromReadable(readable),\n );\n process.stderr.write(\"Render assets uploaded\\n\");\n process.stderr.write(inspect(render));\n process.stderr.write(\"\\n\");\n },\n );\n });\n"],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  import { program } from "commander";
2
+ import path from "node:path";
2
3
  import { spawn } from "node:child_process";
3
4
 
4
5
  //#region src/commands/preview.ts
5
6
  program.command("preview [directory]").description("Preview a directory's index.html file").action(async (projectDirectory = ".") => {
7
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
6
8
  spawn("npx", ["vite", "dev"], {
7
- cwd: projectDirectory,
9
+ cwd: path.resolve(baseCwd, projectDirectory),
8
10
  stdio: "inherit"
9
11
  });
10
12
  });
@@ -1 +1 @@
1
- {"version":3,"file":"preview.js","names":[],"sources":["../../src/commands/preview.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { program } from \"commander\";\n\nprogram\n .command(\"preview [directory]\")\n .description(\"Preview a directory's index.html file\")\n .action(async (projectDirectory = \".\") => {\n spawn(\"npx\", [\"vite\", \"dev\"], { cwd: projectDirectory, stdio: \"inherit\" });\n });\n"],"mappings":";;;;AAGA,QACG,QAAQ,sBAAsB,CAC9B,YAAY,wCAAwC,CACpD,OAAO,OAAO,mBAAmB,QAAQ;AACxC,OAAM,OAAO,CAAC,QAAQ,MAAM,EAAE;EAAE,KAAK;EAAkB,OAAO;EAAW,CAAC;EAC1E"}
1
+ {"version":3,"file":"preview.js","names":[],"sources":["../../src/commands/preview.ts"],"sourcesContent":["import path from \"node:path\";\nimport { spawn } from \"node:child_process\";\nimport { program } from \"commander\";\n\nprogram\n .command(\"preview [directory]\")\n .description(\"Preview a directory's index.html file\")\n .action(async (projectDirectory = \".\") => {\n // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n // Resolve projectDirectory relative to where the user actually ran the command\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const resolvedProjectDir = path.resolve(baseCwd, projectDirectory);\n \n spawn(\"npx\", [\"vite\", \"dev\"], { \n cwd: resolvedProjectDir, \n stdio: \"inherit\" \n });\n });\n"],"mappings":";;;;;AAIA,QACG,QAAQ,sBAAsB,CAC9B,YAAY,wCAAwC,CACpD,OAAO,OAAO,mBAAmB,QAAQ;CAGxC,MAAM,UAAU,QAAQ,IAAI,gBAAgB,QAAQ,KAAK;AAGzD,OAAM,OAAO,CAAC,QAAQ,MAAM,EAAE;EAC5B,KAHyB,KAAK,QAAQ,SAAS,iBAAiB;EAIhE,OAAO;EACR,CAAC;EACF"}
@@ -10,12 +10,14 @@ import { getRenderInfo } from "@editframe/elements/node";
10
10
  //#region src/commands/process.ts
11
11
  program.command("process [directory]").description("Process's a directory's index.html file, analyzing assets and processing them for rendering").action(async (directory) => {
12
12
  directory ??= ".";
13
- const distDir = path.join(directory, "dist");
13
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
14
+ const resolvedDirectory = path.resolve(baseCwd, directory);
15
+ const distDir = path.join(resolvedDirectory, "dist");
14
16
  await withSpinner("Building\n", async () => {
15
17
  spawnSync("npx", [
16
18
  "vite",
17
19
  "build",
18
- directory
20
+ resolvedDirectory
19
21
  ], { stdio: "inherit" });
20
22
  });
21
23
  const previewServer = await PreviewServer.start(distDir);
@@ -1 +1 @@
1
- {"version":3,"file":"process.js","names":[],"sources":["../../src/commands/process.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport { getRenderInfo } from \"@editframe/elements/node\";\nimport { program } from \"commander\";\nimport { processRenderInfo } from \"../operations/processRenderInfo.js\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\nimport { withSpinner } from \"../utils/withSpinner.js\";\n\nprogram\n .command(\"process [directory]\")\n .description(\n \"Process's a directory's index.html file, analyzing assets and processing them for rendering\",\n )\n .action(async (directory) => {\n directory ??= \".\";\n\n const distDir = path.join(directory, \"dist\");\n await withSpinner(\"Building\\n\", async () => {\n spawnSync(\"npx\", [\"vite\", \"build\", directory], {\n stdio: \"inherit\",\n });\n });\n\n const previewServer = await PreviewServer.start(distDir);\n process.stderr.write(\"Preview server started at \");\n process.stderr.write(previewServer.url);\n process.stderr.write(\"\\n\");\n await launchBrowserAndWaitForSDK(\n {\n url: previewServer.url,\n efInteractive: false,\n interactive: false,\n headless: true,\n },\n async (page) => {\n const renderInfo = await page.evaluate(getRenderInfo);\n await processRenderInfo(renderInfo);\n },\n );\n });\n"],"mappings":";;;;;;;;;;AASA,QACG,QAAQ,sBAAsB,CAC9B,YACC,8FACD,CACA,OAAO,OAAO,cAAc;AAC3B,eAAc;CAEd,MAAM,UAAU,KAAK,KAAK,WAAW,OAAO;AAC5C,OAAM,YAAY,cAAc,YAAY;AAC1C,YAAU,OAAO;GAAC;GAAQ;GAAS;GAAU,EAAE,EAC7C,OAAO,WACR,CAAC;GACF;CAEF,MAAM,gBAAgB,MAAM,cAAc,MAAM,QAAQ;AACxD,SAAQ,OAAO,MAAM,6BAA6B;AAClD,SAAQ,OAAO,MAAM,cAAc,IAAI;AACvC,SAAQ,OAAO,MAAM,KAAK;AAC1B,OAAM,2BACJ;EACE,KAAK,cAAc;EACnB,eAAe;EACf,aAAa;EACb,UAAU;EACX,EACD,OAAO,SAAS;AAEd,QAAM,kBADa,MAAM,KAAK,SAAS,cAAc,CAClB;GAEtC;EACD"}
1
+ {"version":3,"file":"process.js","names":[],"sources":["../../src/commands/process.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport { getRenderInfo } from \"@editframe/elements/node\";\nimport { program } from \"commander\";\nimport { processRenderInfo } from \"../operations/processRenderInfo.js\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { PreviewServer } from \"../utils/startPreviewServer.js\";\nimport { withSpinner } from \"../utils/withSpinner.js\";\n\nprogram\n .command(\"process [directory]\")\n .description(\n \"Process's a directory's index.html file, analyzing assets and processing them for rendering\",\n )\n .action(async (directory) => {\n directory ??= \".\";\n\n // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const resolvedDirectory = path.resolve(baseCwd, directory);\n \n const distDir = path.join(resolvedDirectory, \"dist\");\n await withSpinner(\"Building\\n\", async () => {\n spawnSync(\"npx\", [\"vite\", \"build\", resolvedDirectory], {\n stdio: \"inherit\",\n });\n });\n\n const previewServer = await PreviewServer.start(distDir);\n process.stderr.write(\"Preview server started at \");\n process.stderr.write(previewServer.url);\n process.stderr.write(\"\\n\");\n await launchBrowserAndWaitForSDK(\n {\n url: previewServer.url,\n efInteractive: false,\n interactive: false,\n headless: true,\n },\n async (page) => {\n const renderInfo = await page.evaluate(getRenderInfo);\n await processRenderInfo(renderInfo);\n },\n );\n });\n"],"mappings":";;;;;;;;;;AASA,QACG,QAAQ,sBAAsB,CAC9B,YACC,8FACD,CACA,OAAO,OAAO,cAAc;AAC3B,eAAc;CAGd,MAAM,UAAU,QAAQ,IAAI,gBAAgB,QAAQ,KAAK;CACzD,MAAM,oBAAoB,KAAK,QAAQ,SAAS,UAAU;CAE1D,MAAM,UAAU,KAAK,KAAK,mBAAmB,OAAO;AACpD,OAAM,YAAY,cAAc,YAAY;AAC1C,YAAU,OAAO;GAAC;GAAQ;GAAS;GAAkB,EAAE,EACrD,OAAO,WACR,CAAC;GACF;CAEF,MAAM,gBAAgB,MAAM,cAAc,MAAM,QAAQ;AACxD,SAAQ,OAAO,MAAM,6BAA6B;AAClD,SAAQ,OAAO,MAAM,cAAc,IAAI;AACvC,SAAQ,OAAO,MAAM,KAAK;AAC1B,OAAM,2BACJ;EACE,KAAK,cAAc;EACnB,eAAe;EACf,aAAa;EACb,UAAU;EACX,EACD,OAAO,SAAS;AAEd,QAAM,kBADa,MAAM,KAAK,SAAS,cAAc,CAClB;GAEtC;EACD"}
@@ -1,5 +1,5 @@
1
1
  import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.js";
2
- import { PreviewServer } from "../utils/startPreviewServer.js";
2
+ import { spawnViteServer } from "../utils/spawnViteServer.js";
3
3
  import { withProfiling } from "../utils/profileRender.js";
4
4
  import { program } from "commander";
5
5
  import debug from "debug";
@@ -22,7 +22,8 @@ function formatTime(ms) {
22
22
  return `${minutes}:${seconds.toString().padStart(2, "0")}`;
23
23
  }
24
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
- const outputPath = path.resolve(process.cwd(), options.output);
25
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
26
+ const outputPath = path.resolve(baseCwd, options.output);
26
27
  let renderData;
27
28
  if (options.dataFile) {
28
29
  const dataFileContent = await readFile(options.dataFile, "utf-8");
@@ -36,82 +37,108 @@ program.command("render [directory]").description("Render a video composition lo
36
37
  const scale = parseFloat(options.scale);
37
38
  const fromMs = options.fromMs ? parseInt(options.fromMs, 10) : void 0;
38
39
  const toMs = options.toMs ? parseInt(options.toMs, 10) : void 0;
40
+ const initSpinner = ora("Initializing...").start();
39
41
  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
- }
50
- await launchBrowserAndWaitForSDK({
51
- url: renderUrl,
52
- headless: true,
53
- interactive: false,
54
- efInteractive: false,
55
- nativeRender: options.experimentalNativeRender === true,
56
- profile: options.profile === true,
57
- profileOutput: options.profileOutput
58
- }, async (page) => {
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();
42
+ let viteServer = null;
43
+ try {
44
+ if (options.url) {
45
+ renderUrl = options.url;
46
+ log("Using provided URL:", renderUrl);
47
+ } else {
48
+ viteServer = await spawnViteServer(path.resolve(baseCwd, directory));
49
+ renderUrl = viteServer.url;
50
+ log("Vite server spawned at:", renderUrl);
51
+ }
52
+ await launchBrowserAndWaitForSDK({
53
+ url: renderUrl,
54
+ headless: true,
55
+ interactive: false,
56
+ efInteractive: false,
57
+ nativeRender: options.experimentalNativeRender === true,
58
+ profile: options.profile === true,
59
+ profileOutput: options.profileOutput,
60
+ silent: true
61
+ }, async (page) => {
62
+ initSpinner.succeed("Ready");
63
+ await withProfiling(page, {
64
+ enabled: options.profile === true,
65
+ outputPath: options.profileOutput
66
+ }, async () => {
67
+ const outputStream = createWriteStream(outputPath);
68
+ let chunkCount = 0;
69
+ let totalBytes = 0;
70
+ await page.exposeFunction("onRenderChunk", (chunk) => {
71
+ writeFile(outputPath, chunk.data, { flag: "a" });
72
+ chunkCount++;
73
+ totalBytes += chunk.data.length;
74
+ log(`Received chunk ${chunkCount}: ${chunk.data.length} bytes (total: ${totalBytes} bytes)`);
75
+ });
76
+ if (renderData) {
77
+ await page.evaluate((data) => {
78
+ window.EF_RENDER_DATA = data;
79
+ }, renderData);
80
+ log("Set EF_RENDER_DATA:", renderData);
81
+ }
82
+ await page.waitForFunction(() => typeof window.EF_RENDER !== "undefined", { timeout: 1e4 });
83
+ if (!await page.evaluate(() => window.EF_RENDER?.isReady())) throw new Error("Render API is not ready. No ef-timegroup found.");
84
+ const progressSpinner = ora("Rendering video...").start();
85
+ let lastProgress = null;
86
+ await page.exposeFunction("onRenderProgress", (progress) => {
87
+ const percent = (progress.progress * 100).toFixed(1);
88
+ const renderedTime = formatTime(progress.renderedMs);
89
+ const totalTime = formatTime(progress.totalDurationMs);
90
+ const remainingTime = formatTime(progress.estimatedRemainingMs);
91
+ const speed = progress.speedMultiplier.toFixed(2);
92
+ progressSpinner.text = `Rendering: ${progress.currentFrame}/${progress.totalFrames} frames (${percent}%) | ${renderedTime}/${totalTime} | ${remainingTime} remaining | ${speed}x speed`;
93
+ lastProgress = {
94
+ currentFrame: progress.currentFrame,
95
+ totalFrames: progress.totalFrames,
96
+ renderedMs: progress.renderedMs,
97
+ totalDurationMs: progress.totalDurationMs,
98
+ elapsedMs: progress.elapsedMs,
99
+ speedMultiplier: progress.speedMultiplier
100
+ };
101
+ });
102
+ try {
103
+ const renderOptions = {
104
+ fps,
105
+ scale,
106
+ includeAudio: options.includeAudio !== false
107
+ };
108
+ if (fromMs !== void 0) renderOptions.fromMs = fromMs;
109
+ if (toMs !== void 0) renderOptions.toMs = toMs;
110
+ await page.evaluate(async (opts) => {
111
+ await window.EF_RENDER.renderStreaming(opts);
112
+ }, renderOptions);
113
+ if (lastProgress) {
114
+ const renderedTime = formatTime(lastProgress.renderedMs);
115
+ const totalTime = formatTime(lastProgress.totalDurationMs);
116
+ const elapsedTime = formatTime(lastProgress.elapsedMs);
117
+ const speed = lastProgress.speedMultiplier.toFixed(2);
118
+ progressSpinner.succeed(`Render complete: ${lastProgress.currentFrame}/${lastProgress.totalFrames} frames | ${renderedTime}/${totalTime} | ${elapsedTime} elapsed | ${speed}x speed`);
119
+ } else progressSpinner.succeed("Render complete");
120
+ } catch (error) {
121
+ progressSpinner.fail("Render failed");
122
+ throw error;
123
+ }
124
+ outputStream.end();
125
+ await new Promise((resolve, reject) => {
126
+ outputStream.on("finish", () => {
127
+ log(`Render complete: ${chunkCount} chunks, ${totalBytes} bytes written to ${outputPath}`);
128
+ resolve();
129
+ });
130
+ outputStream.on("error", reject);
110
131
  });
111
- outputStream.on("error", reject);
112
132
  });
113
133
  });
114
- });
134
+ } catch (error) {
135
+ initSpinner.fail("Initialization failed");
136
+ throw error;
137
+ }
138
+ if (viteServer) {
139
+ viteServer.kill();
140
+ log("Vite server stopped");
141
+ }
115
142
  process.stderr.write(`\nRender complete: ${outputPath}\n`);
116
143
  });
117
144
 
@@ -1 +1 @@
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
+ {"version":3,"file":"render.js","names":["renderData: Record<string, unknown> | undefined","renderUrl: string","viteServer: SpawnedViteServer | null","lastProgress: {\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n speedMultiplier: number;\n } | 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 type { Page } from \"playwright\";\nimport { launchBrowserAndWaitForSDK } from \"../utils/launchBrowserAndWaitForSDK.js\";\nimport { spawnViteServer, type SpawnedViteServer } from \"../utils/spawnViteServer.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 // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const outputPath = path.resolve(baseCwd, 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 // Single initialization spinner for all setup steps\n const initSpinner = ora(\"Initializing...\").start();\n \n let renderUrl: string;\n let viteServer: SpawnedViteServer | null = null;\n\n try {\n // Determine URL to render\n if (options.url) {\n // Use provided URL directly\n renderUrl = options.url;\n log(\"Using provided URL:\", renderUrl);\n } else {\n // Spawn Vite dev server as subprocess\n // This allows Vite to run with full config resolution (including Tailwind)\n // while we maintain Playwright control for rendering\n const srcDir = path.resolve(baseCwd, directory);\n viteServer = await spawnViteServer(srcDir);\n renderUrl = viteServer.url;\n log(\"Vite server spawned at:\", renderUrl);\n }\n\n // Launch browser and load SDK (all within initialization)\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 silent: true, // Suppress individual spinners since we show unified \"Initializing...\"\n },\n async (page) => {\n initSpinner.succeed(\"Ready\");\n \n // Now handle the render\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 // Track last progress for completion message\n let lastProgress: {\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n speedMultiplier: number;\n } | null = null;\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 // Store last progress for completion message\n lastProgress = {\n currentFrame: progress.currentFrame,\n totalFrames: progress.totalFrames,\n renderedMs: progress.renderedMs,\n totalDurationMs: progress.totalDurationMs,\n elapsedMs: progress.elapsedMs,\n speedMultiplier: progress.speedMultiplier,\n };\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 // Build completion message with performance stats\n if (lastProgress) {\n const renderedTime = formatTime(lastProgress.renderedMs);\n const totalTime = formatTime(lastProgress.totalDurationMs);\n const elapsedTime = formatTime(lastProgress.elapsedMs);\n const speed = lastProgress.speedMultiplier.toFixed(2);\n progressSpinner.succeed(\n `Render complete: ${lastProgress.currentFrame}/${lastProgress.totalFrames} frames | ${renderedTime}/${totalTime} | ${elapsedTime} elapsed | ${speed}x speed`\n );\n } else {\n progressSpinner.succeed(\"Render complete\");\n }\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 } catch (error) {\n initSpinner.fail(\"Initialization failed\");\n throw error;\n }\n\n // Clean up spawned Vite process\n if (viteServer) {\n viteServer.kill();\n log(\"Vite server stopped\");\n }\n\n process.stderr.write(`\\nRender complete: ${outputPath}\\n`);\n });\n"],"mappings":"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"}
@@ -1,10 +1,11 @@
1
1
  import { syncAssetDirectory } from "../operations/syncAssetsDirectory.js";
2
2
  import { program } from "commander";
3
- import { join } from "node:path";
3
+ import path, { join } from "node:path";
4
4
 
5
5
  //#region src/commands/sync.ts
6
6
  program.command("sync").description("Sync assets to Editframe servers for rendering").argument("[directory]", "Path to project directory to sync.").action(async (directory = ".") => {
7
- await syncAssetDirectory(join(process.cwd(), directory, "src", "assets", ".cache"));
7
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
8
+ await syncAssetDirectory(join(path.resolve(baseCwd, directory), "src", "assets", ".cache"));
8
9
  });
9
10
 
10
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../../src/commands/sync.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { program } from \"commander\";\nimport { syncAssetDirectory } from \"../operations/syncAssetsDirectory.js\";\n\nprogram\n .command(\"sync\")\n .description(\"Sync assets to Editframe servers for rendering\")\n .argument(\"[directory]\", \"Path to project directory to sync.\")\n .action(async (directory = \".\") => {\n await syncAssetDirectory(\n join(process.cwd(), directory, \"src\", \"assets\", \".cache\"),\n );\n });\n"],"mappings":";;;;;AAIA,QACG,QAAQ,OAAO,CACf,YAAY,iDAAiD,CAC7D,SAAS,eAAe,qCAAqC,CAC7D,OAAO,OAAO,YAAY,QAAQ;AACjC,OAAM,mBACJ,KAAK,QAAQ,KAAK,EAAE,WAAW,OAAO,UAAU,SAAS,CAC1D;EACD"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../../src/commands/sync.ts"],"sourcesContent":["import path, { join } from \"node:path\";\nimport { program } from \"commander\";\nimport { syncAssetDirectory } from \"../operations/syncAssetsDirectory.js\";\n\nprogram\n .command(\"sync\")\n .description(\"Sync assets to Editframe servers for rendering\")\n .argument(\"[directory]\", \"Path to project directory to sync.\")\n .action(async (directory = \".\") => {\n // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const resolvedDirectory = path.resolve(baseCwd, directory);\n \n await syncAssetDirectory(\n join(resolvedDirectory, \"src\", \"assets\", \".cache\"),\n );\n });\n"],"mappings":";;;;;AAIA,QACG,QAAQ,OAAO,CACf,YAAY,iDAAiD,CAC7D,SAAS,eAAe,qCAAqC,CAC7D,OAAO,OAAO,YAAY,QAAQ;CAEjC,MAAM,UAAU,QAAQ,IAAI,gBAAgB,QAAQ,KAAK;AAGzD,OAAM,mBACJ,KAHwB,KAAK,QAAQ,SAAS,UAAU,EAGhC,OAAO,UAAU,SAAS,CACnD;EACD"}
@@ -8,7 +8,7 @@ import { chromium } from "playwright";
8
8
  const browserLog = debug("ef:cli::browser");
9
9
  async function launchBrowserAndWaitForSDK(options, fn) {
10
10
  if (options.interactive !== true && !options.chromePath) requireChrome();
11
- const browser = await withSpinner("Launching chrome", async () => {
11
+ const launchBrowser = async () => {
12
12
  const launchOptions = {
13
13
  channel: "chrome",
14
14
  headless: options.headless ?? true,
@@ -18,9 +18,23 @@ async function launchBrowserAndWaitForSDK(options, fn) {
18
18
  launchOptions.executablePath = options.chromePath;
19
19
  delete launchOptions.channel;
20
20
  }
21
+ const disableGpu = process.env.EF_DISABLE_GPU === "1";
22
+ if (options.headless && options.interactive !== true && !disableGpu) {
23
+ launchOptions.args = [
24
+ "--enable-gpu",
25
+ "--use-angle=default",
26
+ "--enable-accelerated-2d-canvas",
27
+ "--enable-webgl",
28
+ "--enable-features=VaapiVideoDecoder",
29
+ "--disable-software-rasterizer",
30
+ "--disable-dev-shm-usage"
31
+ ];
32
+ browserLog("Launching Chrome with GPU acceleration enabled");
33
+ } else if (disableGpu) browserLog("GPU acceleration disabled via EF_DISABLE_GPU");
21
34
  return chromium.launch(launchOptions);
22
- });
23
- await fn(await withSpinner("Loading Editframe SDK", async () => {
35
+ };
36
+ const browser = options.silent ? await launchBrowser() : await withSpinner("Launching chrome", launchBrowser);
37
+ const loadSDK = async () => {
24
38
  const pageOptions = {};
25
39
  if (options.interactive === true) pageOptions.viewport = null;
26
40
  const page = await browser.newPage(pageOptions);
@@ -31,15 +45,14 @@ async function launchBrowserAndWaitForSDK(options, fn) {
31
45
  if (!options.efInteractive) urlParams.set("EF_NONINTERACTIVE", "1");
32
46
  if (options.nativeRender) urlParams.set("EF_NATIVE_RENDER", "1");
33
47
  const url = options.url + (urlParams.toString() ? `?${urlParams.toString()}` : "");
34
- process.stderr.write("\nLoading url: ");
35
- process.stderr.write(url);
36
- process.stderr.write("\n");
48
+ browserLog("Loading url:", url);
37
49
  await page.goto(url);
38
50
  await page.waitForFunction(() => {
39
51
  return window.EF_REGISTERED;
40
52
  }, [], { timeout: 1e4 });
41
53
  return page;
42
- }));
54
+ };
55
+ await fn(options.silent ? await loadSDK() : await withSpinner("Loading Editframe SDK", loadSDK));
43
56
  if (options.interactive !== true) {
44
57
  await browser.close();
45
58
  process.exit(0);
@@ -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 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"}
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 silent?: boolean; // Suppress spinner output\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 launchBrowser = 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 // Enable GPU acceleration for headless rendering\n // This significantly improves canvas/WebGL performance for video rendering\n // Set EF_DISABLE_GPU=1 to disable GPU acceleration (for debugging/compatibility)\n const disableGpu = process.env.EF_DISABLE_GPU === \"1\";\n \n if (options.headless && options.interactive !== true && !disableGpu) {\n launchOptions.args = [\n // Core GPU acceleration flags\n \"--enable-gpu\", // Enable GPU hardware acceleration\n \"--use-angle=default\", // Use ANGLE for OpenGL (better compatibility)\n \"--enable-accelerated-2d-canvas\", // Hardware-accelerate canvas operations\n \"--enable-webgl\", // Enable WebGL\n \"--enable-features=VaapiVideoDecoder\", // Hardware video decoding (Linux)\n \n // Prevent fallback to software rendering\n \"--disable-software-rasterizer\",\n \n // System resource optimizations\n \"--disable-dev-shm-usage\", // Avoid /dev/shm issues (especially on Linux/Docker)\n \n // Optional: Uncomment if running in Docker or restricted environments\n // \"--no-sandbox\",\n // \"--disable-setuid-sandbox\",\n ];\n\n browserLog(\"Launching Chrome with GPU acceleration enabled\");\n } else if (disableGpu) {\n browserLog(\"GPU acceleration disabled via EF_DISABLE_GPU\");\n }\n\n return chromium.launch(launchOptions);\n };\n\n const browser = options.silent\n ? await launchBrowser()\n : await withSpinner(\"Launching chrome\", launchBrowser);\n\n const loadSDK = 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 browserLog(\"Loading url:\", url);\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\n const page = options.silent\n ? await loadSDK()\n : await withSpinner(\"Loading Editframe SDK\", loadSDK);\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;AAc3C,eAAsB,2BACpB,SACA,IACA;AAEA,KAAI,QAAQ,gBAAgB,QAAQ,CAAC,QAAQ,WAC3C,gBAAe;CAGjB,MAAM,gBAAgB,YAAY;EAChC,MAAMA,gBAAuD;GAC3D,SAAS;GACT,UAAU,QAAQ,YAAY;GAC9B,UAAU,QAAQ,gBAAgB;GACnC;AAGD,MAAI,QAAQ,YAAY;AACtB,iBAAc,iBAAiB,QAAQ;AAEvC,UAAO,cAAc;;EAMvB,MAAM,aAAa,QAAQ,IAAI,mBAAmB;AAElD,MAAI,QAAQ,YAAY,QAAQ,gBAAgB,QAAQ,CAAC,YAAY;AACnE,iBAAc,OAAO;IAEnB;IACA;IACA;IACA;IACA;IAGA;IAGA;IAKD;AAED,cAAW,iDAAiD;aACnD,WACT,YAAW,+CAA+C;AAG5D,SAAO,SAAS,OAAO,cAAc;;CAGvC,MAAM,UAAU,QAAQ,SACpB,MAAM,eAAe,GACrB,MAAM,YAAY,oBAAoB,cAAc;CAExD,MAAM,UAAU,YAAY;EAC1B,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,aAAW,gBAAgB,IAAI;AAC/B,QAAM,KAAK,KAAK,IAAI;AACpB,QAAM,KAAK,sBACH;AACJ,UAEE,OAAO;KAGX,EAAE,EACF,EAAE,SAAS,KAAQ,CACpB;AACD,SAAO;;AAOT,OAAM,GAJO,QAAQ,SACjB,MAAM,SAAS,GACf,MAAM,YAAY,yBAAyB,QAAQ,CAEzC;AACd,KAAI,QAAQ,gBAAgB,MAAM;AAChC,QAAM,QAAQ,OAAO;AACrB,UAAQ,KAAK,EAAE"}
@@ -0,0 +1,71 @@
1
+ import debug from "debug";
2
+ import { spawn } from "node:child_process";
3
+
4
+ //#region src/utils/spawnViteServer.ts
5
+ const log = debug("ef:cli::spawn-vite");
6
+ /**
7
+ * Spawn a Vite dev server as a subprocess and wait for it to be ready.
8
+ * This allows Vite to run in its own process with full config resolution,
9
+ * while the CLI maintains control via Playwright for rendering.
10
+ */
11
+ async function spawnViteServer(directory) {
12
+ return new Promise((resolve, reject) => {
13
+ log("Spawning vite dev server in", directory);
14
+ const viteProcess = spawn("npx", ["vite", "dev"], {
15
+ cwd: directory,
16
+ stdio: "pipe",
17
+ env: {
18
+ ...process.env,
19
+ BROWSER: "none"
20
+ }
21
+ });
22
+ let url = null;
23
+ let stderr = "";
24
+ let resolved = false;
25
+ viteProcess.stdout?.on("data", (data) => {
26
+ const output = data.toString();
27
+ log("vite stdout:", output);
28
+ const match = output.match(/Local:\s+(https?:\/\/[^\s]+)/);
29
+ if (match && !resolved) {
30
+ url = match[1].trim();
31
+ resolved = true;
32
+ log("Vite server ready at:", url);
33
+ resolve({
34
+ url,
35
+ process: viteProcess,
36
+ kill: () => {
37
+ log("Killing vite process");
38
+ viteProcess.kill();
39
+ }
40
+ });
41
+ }
42
+ });
43
+ viteProcess.stderr?.on("data", (data) => {
44
+ stderr += data.toString();
45
+ log("vite stderr:", data.toString());
46
+ });
47
+ viteProcess.on("error", (error) => {
48
+ if (!resolved) {
49
+ resolved = true;
50
+ reject(/* @__PURE__ */ new Error(`Failed to spawn vite: ${error.message}\n${stderr}`));
51
+ }
52
+ });
53
+ viteProcess.on("exit", (code, signal) => {
54
+ if (!resolved) {
55
+ resolved = true;
56
+ reject(/* @__PURE__ */ new Error(`Vite exited unexpectedly with code ${code} and signal ${signal}\n${stderr}`));
57
+ }
58
+ });
59
+ setTimeout(() => {
60
+ if (!resolved) {
61
+ resolved = true;
62
+ viteProcess.kill();
63
+ reject(/* @__PURE__ */ new Error(`Vite server did not start within 30 seconds\nStderr: ${stderr}`));
64
+ }
65
+ }, 3e4);
66
+ });
67
+ }
68
+
69
+ //#endregion
70
+ export { spawnViteServer };
71
+ //# sourceMappingURL=spawnViteServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawnViteServer.js","names":["url: string | null"],"sources":["../../src/utils/spawnViteServer.ts"],"sourcesContent":["import { spawn, type ChildProcess } from \"node:child_process\";\nimport debug from \"debug\";\n\nconst log = debug(\"ef:cli::spawn-vite\");\n\nexport interface SpawnedViteServer {\n url: string;\n process: ChildProcess;\n kill: () => void;\n}\n\n/**\n * Spawn a Vite dev server as a subprocess and wait for it to be ready.\n * This allows Vite to run in its own process with full config resolution,\n * while the CLI maintains control via Playwright for rendering.\n */\nexport async function spawnViteServer(\n directory: string,\n): Promise<SpawnedViteServer> {\n return new Promise((resolve, reject) => {\n log(\"Spawning vite dev server in\", directory);\n\n const viteProcess = spawn(\"npx\", [\"vite\", \"dev\"], {\n cwd: directory,\n stdio: \"pipe\", // Capture output to detect when ready\n env: {\n ...process.env,\n // Disable Vite's automatic browser opening\n BROWSER: \"none\",\n },\n });\n\n let url: string | null = null;\n let stderr = \"\";\n let resolved = false;\n\n // Parse stdout to detect when Vite is ready\n viteProcess.stdout?.on(\"data\", (data: Buffer) => {\n const output = data.toString();\n log(\"vite stdout:\", output);\n\n // Look for the Local URL in Vite's output\n // Format: \" ➜ Local: http://localhost:5173/\"\n const match = output.match(/Local:\\s+(https?:\\/\\/[^\\s]+)/);\n if (match && !resolved) {\n url = match[1].trim();\n resolved = true;\n log(\"Vite server ready at:\", url);\n resolve({\n url,\n process: viteProcess,\n kill: () => {\n log(\"Killing vite process\");\n viteProcess.kill();\n },\n });\n }\n });\n\n // Capture stderr for error reporting\n viteProcess.stderr?.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n log(\"vite stderr:\", data.toString());\n });\n\n // Handle process errors\n viteProcess.on(\"error\", (error) => {\n if (!resolved) {\n resolved = true;\n reject(\n new Error(`Failed to spawn vite: ${error.message}\\n${stderr}`),\n );\n }\n });\n\n // Handle unexpected process exit\n viteProcess.on(\"exit\", (code, signal) => {\n if (!resolved) {\n resolved = true;\n reject(\n new Error(\n `Vite exited unexpectedly with code ${code} and signal ${signal}\\n${stderr}`,\n ),\n );\n }\n });\n\n // Timeout after 30 seconds\n setTimeout(() => {\n if (!resolved) {\n resolved = true;\n viteProcess.kill();\n reject(\n new Error(\n `Vite server did not start within 30 seconds\\nStderr: ${stderr}`,\n ),\n );\n }\n }, 30000);\n });\n}\n"],"mappings":";;;;AAGA,MAAM,MAAM,MAAM,qBAAqB;;;;;;AAavC,eAAsB,gBACpB,WAC4B;AAC5B,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,+BAA+B,UAAU;EAE7C,MAAM,cAAc,MAAM,OAAO,CAAC,QAAQ,MAAM,EAAE;GAChD,KAAK;GACL,OAAO;GACP,KAAK;IACH,GAAG,QAAQ;IAEX,SAAS;IACV;GACF,CAAC;EAEF,IAAIA,MAAqB;EACzB,IAAI,SAAS;EACb,IAAI,WAAW;AAGf,cAAY,QAAQ,GAAG,SAAS,SAAiB;GAC/C,MAAM,SAAS,KAAK,UAAU;AAC9B,OAAI,gBAAgB,OAAO;GAI3B,MAAM,QAAQ,OAAO,MAAM,+BAA+B;AAC1D,OAAI,SAAS,CAAC,UAAU;AACtB,UAAM,MAAM,GAAG,MAAM;AACrB,eAAW;AACX,QAAI,yBAAyB,IAAI;AACjC,YAAQ;KACN;KACA,SAAS;KACT,YAAY;AACV,UAAI,uBAAuB;AAC3B,kBAAY,MAAM;;KAErB,CAAC;;IAEJ;AAGF,cAAY,QAAQ,GAAG,SAAS,SAAiB;AAC/C,aAAU,KAAK,UAAU;AACzB,OAAI,gBAAgB,KAAK,UAAU,CAAC;IACpC;AAGF,cAAY,GAAG,UAAU,UAAU;AACjC,OAAI,CAAC,UAAU;AACb,eAAW;AACX,2BACE,IAAI,MAAM,yBAAyB,MAAM,QAAQ,IAAI,SAAS,CAC/D;;IAEH;AAGF,cAAY,GAAG,SAAS,MAAM,WAAW;AACvC,OAAI,CAAC,UAAU;AACb,eAAW;AACX,2BACE,IAAI,MACF,sCAAsC,KAAK,cAAc,OAAO,IAAI,SACrE,CACF;;IAEH;AAGF,mBAAiB;AACf,OAAI,CAAC,UAAU;AACb,eAAW;AACX,gBAAY,MAAM;AAClB,2BACE,IAAI,MACF,wDAAwD,SACzD,CACF;;KAEF,IAAM;GACT"}
@@ -17,7 +17,8 @@ var PreviewServer = class PreviewServer {
17
17
  };
18
18
  const startPreviewServer = async (directory) => {
19
19
  return await withSpinner("Starting vite...", async () => {
20
- const resolvedDirectory = path.resolve(process.cwd(), directory);
20
+ const baseCwd = process.env.ORIGINAL_CWD || process.cwd();
21
+ const resolvedDirectory = path.resolve(baseCwd, directory);
21
22
  const devServer = await createServer({
22
23
  server: { watch: null },
23
24
  root: resolvedDirectory,
@@ -1 +1 @@
1
- {"version":3,"file":"startPreviewServer.js","names":["previewServer: ViteDevServer","editframe"],"sources":["../../src/utils/startPreviewServer.ts"],"sourcesContent":["import path from \"node:path\";\nimport { vitePluginEditframe as editframe } from \"@editframe/vite-plugin\";\nimport { createServer, type ViteDevServer } from \"vite\";\n\nimport { withSpinner } from \"./withSpinner.js\";\n\nexport class PreviewServer {\n static async start(directory: string) {\n return new PreviewServer(await startPreviewServer(directory));\n }\n\n constructor(private previewServer: ViteDevServer) {}\n\n get url() {\n return `http://localhost:${this.previewServer.config.server.port}`;\n }\n}\n\nconst startPreviewServer = async (directory: string) => {\n return await withSpinner(\"Starting vite...\", async () => {\n const resolvedDirectory = path.resolve(process.cwd(), directory);\n const cacheRoot = path.join(resolvedDirectory, \"assets\");\n const devServer = await createServer({\n server: {\n watch: null,\n },\n root: resolvedDirectory,\n plugins: [\n editframe({\n root: resolvedDirectory,\n cacheRoot,\n }),\n ],\n });\n await devServer.listen();\n return devServer;\n });\n};\n"],"mappings":";;;;;;AAMA,IAAa,gBAAb,MAAa,cAAc;CACzB,aAAa,MAAM,WAAmB;AACpC,SAAO,IAAI,cAAc,MAAM,mBAAmB,UAAU,CAAC;;CAG/D,YAAY,AAAQA,eAA8B;EAA9B;;CAEpB,IAAI,MAAM;AACR,SAAO,oBAAoB,KAAK,cAAc,OAAO,OAAO;;;AAIhE,MAAM,qBAAqB,OAAO,cAAsB;AACtD,QAAO,MAAM,YAAY,oBAAoB,YAAY;EACvD,MAAM,oBAAoB,KAAK,QAAQ,QAAQ,KAAK,EAAE,UAAU;EAEhE,MAAM,YAAY,MAAM,aAAa;GACnC,QAAQ,EACN,OAAO,MACR;GACD,MAAM;GACN,SAAS,CACPC,oBAAU;IACR,MAAM;IACN,WATY,KAAK,KAAK,mBAAmB,SAAS;IAUnD,CAAC,CACH;GACF,CAAC;AACF,QAAM,UAAU,QAAQ;AACxB,SAAO;GACP"}
1
+ {"version":3,"file":"startPreviewServer.js","names":["previewServer: ViteDevServer","editframe"],"sources":["../../src/utils/startPreviewServer.ts"],"sourcesContent":["import path from \"node:path\";\nimport { vitePluginEditframe as editframe } from \"@editframe/vite-plugin\";\nimport { createServer, type ViteDevServer } from \"vite\";\n\nimport { withSpinner } from \"./withSpinner.js\";\n\nexport class PreviewServer {\n static async start(directory: string) {\n return new PreviewServer(await startPreviewServer(directory));\n }\n\n constructor(private previewServer: ViteDevServer) {}\n\n get url() {\n return `http://localhost:${this.previewServer.config.server.port}`;\n }\n}\n\nconst startPreviewServer = async (directory: string) => {\n return await withSpinner(\"Starting vite...\", async () => {\n // If running from the dev script (via tsx), ORIGINAL_CWD contains the user's actual directory\n const baseCwd = process.env.ORIGINAL_CWD || process.cwd();\n const resolvedDirectory = path.resolve(baseCwd, directory);\n const cacheRoot = path.join(resolvedDirectory, \"assets\");\n \n const devServer = await createServer({\n server: {\n watch: null,\n },\n root: resolvedDirectory,\n plugins: [\n editframe({\n root: resolvedDirectory,\n cacheRoot,\n }),\n ],\n });\n await devServer.listen();\n return devServer;\n });\n};\n"],"mappings":";;;;;;AAMA,IAAa,gBAAb,MAAa,cAAc;CACzB,aAAa,MAAM,WAAmB;AACpC,SAAO,IAAI,cAAc,MAAM,mBAAmB,UAAU,CAAC;;CAG/D,YAAY,AAAQA,eAA8B;EAA9B;;CAEpB,IAAI,MAAM;AACR,SAAO,oBAAoB,KAAK,cAAc,OAAO,OAAO;;;AAIhE,MAAM,qBAAqB,OAAO,cAAsB;AACtD,QAAO,MAAM,YAAY,oBAAoB,YAAY;EAEvD,MAAM,UAAU,QAAQ,IAAI,gBAAgB,QAAQ,KAAK;EACzD,MAAM,oBAAoB,KAAK,QAAQ,SAAS,UAAU;EAG1D,MAAM,YAAY,MAAM,aAAa;GACnC,QAAQ,EACN,OAAO,MACR;GACD,MAAM;GACN,SAAS,CACPC,oBAAU;IACR,MAAM;IACN,WAVY,KAAK,KAAK,mBAAmB,SAAS;IAWnD,CAAC,CACH;GACF,CAAC;AACF,QAAM,UAAU,QAAQ;AACxB,SAAO;GACP"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/cli",
3
- "version": "0.35.0-beta",
3
+ "version": "0.36.0-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.35.0-beta",
26
- "@editframe/assets": "0.35.0-beta",
27
- "@editframe/elements": "0.35.0-beta",
28
- "@editframe/vite-plugin": "0.35.0-beta",
25
+ "@editframe/api": "0.36.0-beta",
26
+ "@editframe/assets": "0.36.0-beta",
27
+ "@editframe/elements": "0.36.0-beta",
28
+ "@editframe/vite-plugin": "0.36.0-beta",
29
29
  "@inquirer/prompts": "^5.3.8",
30
30
  "chalk": "^5.3.0",
31
31
  "commander": "^12.0.0",