@kitschpatrol/tldraw-cli 4.3.2 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -17247,81 +17247,13 @@ var TldrawController = class {
17247
17247
  async close() {
17248
17248
  if (!this.browser)
17249
17249
  throw new Error("Controller not started");
17250
+ const pages = await this.browser.pages();
17251
+ await Promise.all(pages.map(async (page) => page.close()));
17250
17252
  await this.browser.close();
17251
17253
  log_default.info("Stopped controller");
17252
17254
  }
17253
- // Public method doesn't expose pageFrame
17255
+ // eslint-disable-next-line complexity
17254
17256
  async download(options) {
17255
- return this._download(void 0, options);
17256
- }
17257
- async downloadFrame(frameNameOrId, options) {
17258
- return this.downloadFrames([frameNameOrId], options);
17259
- }
17260
- async downloadFrames(frameNamesOrIds, options) {
17261
- const validPageFrames = [];
17262
- for (const frame of frameNamesOrIds) {
17263
- const pageFrame = await this.getPageFrameWithNameOrId(frame);
17264
- if (pageFrame === void 0) {
17265
- log_default.warn(`Frame "${frame}" not found, skipping`);
17266
- } else {
17267
- validPageFrames.push(pageFrame);
17268
- }
17269
- }
17270
- if (validPageFrames.length === 0) {
17271
- throw new Error("No valid frames found");
17272
- }
17273
- const validFrameNames = validPageFrames.map((frame) => slugify(frame.name));
17274
- const isFrameNameCollision = validFrameNames.length !== new Set(validFrameNames).size;
17275
- if (isFrameNameCollision) {
17276
- log_default.warn(
17277
- "Frame names are not unique, including frame IDs in the output filenames to avoid collisions"
17278
- );
17279
- }
17280
- const outputAccumulator = [];
17281
- for (const frame of validPageFrames) {
17282
- const frameSuffix = isFrameNameCollision ? `-${frame.id.replace("shape:", "")}` : "";
17283
- outputAccumulator.push(
17284
- ...await this._download(frame, {
17285
- ...options,
17286
- name: `${options.name}${frameSuffix}`
17287
- })
17288
- );
17289
- }
17290
- return outputAccumulator;
17291
- }
17292
- async downloadAllFrames(options) {
17293
- const pageFrames = await this.getPageFrames();
17294
- const frameNamesOrIds = pageFrames.map((f) => f.id);
17295
- return this.downloadFrames(frameNamesOrIds, options);
17296
- }
17297
- async loadFile(filePath) {
17298
- if (!this.page)
17299
- throw new Error("Controller not started");
17300
- if (this.isLocal)
17301
- throw new Error(
17302
- "File loading is only supported for remote tldraw.com instances. See tldraw-open.ts for local file loading approach."
17303
- );
17304
- await this.closeMenus();
17305
- await this.page.evaluate(`userPreferences.showFileOpenWarning.set(false);`);
17306
- log_default.info(`Uploading local file to tldraw.com: ${filePath}`);
17307
- const tldrFile = await fs.readFile(filePath, "utf8");
17308
- await this.page.evaluate(set_tldr_iife_default);
17309
- await this.page.evaluate(`window.setTldr(${tldrFile})`);
17310
- }
17311
- async getShareUrl() {
17312
- if (!this.page)
17313
- throw new Error("Controller not started");
17314
- if (this.isLocal)
17315
- throw new Error("Share URLs are only supported for remote tldraw.com instances.");
17316
- await this.closeMenus();
17317
- await this.clickButtonTitles(["Menu", "File", "Share this project"]);
17318
- await this.page.waitForNavigation({ waitUntil: "networkidle0" });
17319
- const shareUrl = new URL(this.page.url());
17320
- shareUrl.search = "";
17321
- return shareUrl.href;
17322
- }
17323
- // eslint-disable-next-line @typescript-eslint/naming-convention
17324
- async _download(pageFrame, options) {
17325
17257
  if (!this.page)
17326
17258
  throw new Error("Controller not started");
17327
17259
  if (this.isEmpty)
@@ -17329,10 +17261,14 @@ var TldrawController = class {
17329
17261
  if (options.stripStyle && options.format !== "svg")
17330
17262
  log_default.warn("--strip-style is only supported for SVG output");
17331
17263
  if (options.format === "tldr") {
17332
- if (pageFrame !== void 0)
17264
+ if (options.frames !== void 0)
17333
17265
  log_default.warn(
17334
17266
  '--frames is not supported when exporting to "tldr", ignoring flag and exporting entire sketch file'
17335
17267
  );
17268
+ if (options.pages !== void 0)
17269
+ log_default.warn(
17270
+ '--pages is not supported when exporting to "tldr", ignoring flag and exporting entire sketch file'
17271
+ );
17336
17272
  if (options.dark !== false) {
17337
17273
  log_default.warn(
17338
17274
  '--dark is not supported when exporting to "tldr", ignoring flag and exporting sketch file'
@@ -17365,22 +17301,37 @@ var TldrawController = class {
17365
17301
  stripStyle = false,
17366
17302
  transparent = false
17367
17303
  } = this.stripUndefined(options);
17368
- let frameSuffix = "";
17369
- let base64String = "";
17370
17304
  if (format3 === "tldr") {
17371
17305
  await this.page.evaluate(get_tldr_iife_default);
17372
- base64String = await this.page.evaluate(async () => window.getTldr());
17373
- } else {
17374
- if (pageFrame === void 0) {
17306
+ const base64String = await this.page.evaluate(async () => window.getTldr());
17307
+ if (print) {
17308
+ const plainString = base64ToString(base64String).replaceAll("\n", "");
17309
+ return [plainString];
17310
+ }
17311
+ const outputPath = path2.resolve(untildify(path2.join(output, `${filename}.${format3}`)));
17312
+ await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17313
+ return [outputPath];
17314
+ }
17315
+ await this.page.evaluate(get_image_iife_default);
17316
+ const sketchStructure = await this.getSketchStructure();
17317
+ const downloadPlan = this.getDownloadPlans(sketchStructure, options.pages, options.frames);
17318
+ const outputAccumulator = [];
17319
+ const initialPageId = await this.getCurrentPage();
17320
+ let pageChanged = false;
17321
+ for (const download of downloadPlan) {
17322
+ if (download.pageId !== initialPageId) {
17323
+ log_default.info(`Selecting sketch page "${download.pageId}"`);
17324
+ await this.setCurrentPage(download.pageId);
17325
+ pageChanged = true;
17326
+ }
17327
+ if (download.frameId === void 0) {
17375
17328
  await this.page.evaluate("editor.selectAll()");
17376
17329
  } else {
17377
- log_default.info(`Selecting sketch frame "${pageFrame.name}" with ID "${pageFrame.id}"`);
17378
- frameSuffix = `-${slugify(pageFrame.name)}`;
17330
+ log_default.info(`Selecting sketch frame "${download.frameId}"`);
17379
17331
  await this.page.evaluate("editor.selectNone()");
17380
- await this.page.evaluate(`editor.select('${pageFrame.id}')`);
17332
+ await this.page.evaluate(`editor.select('${download.frameId}')`);
17381
17333
  }
17382
- await this.page.evaluate(get_image_iife_default);
17383
- base64String = await this.page.evaluate(async (options2) => window.getImage(options2), {
17334
+ let base64String = await this.page.evaluate(async (options2) => window.getImage(options2), {
17384
17335
  background: transparent === void 0 ? void 0 : !transparent,
17385
17336
  darkMode: dark,
17386
17337
  format: format3,
@@ -17390,19 +17341,51 @@ var TldrawController = class {
17390
17341
  if (stripStyle && format3 === "svg") {
17391
17342
  base64String = stringToBase64(this.stripStyleElement(base64ToString(base64String)));
17392
17343
  }
17393
- }
17394
- const outputPath = path2.resolve(
17395
- untildify(path2.join(output, `${filename}${frameSuffix}.${format3}`))
17396
- );
17397
- if (print) {
17398
- if (format3 === "png") {
17399
- return [base64String];
17344
+ if (print) {
17345
+ if (format3 === "png") {
17346
+ outputAccumulator.push(base64String);
17347
+ } else {
17348
+ const plainString = base64ToString(base64String).replaceAll("\n", "");
17349
+ outputAccumulator.push(plainString);
17350
+ }
17351
+ } else {
17352
+ const outputPath = path2.resolve(
17353
+ untildify(path2.join(output, `${filename}${download.fileSuffix}.${format3}`))
17354
+ );
17355
+ await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17356
+ outputAccumulator.push(outputPath);
17400
17357
  }
17401
- const plainString = base64ToString(base64String).replaceAll("\n", "");
17402
- return [plainString];
17403
17358
  }
17404
- await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17405
- return [outputPath];
17359
+ if (pageChanged) {
17360
+ await this.setCurrentPage(initialPageId);
17361
+ }
17362
+ return outputAccumulator;
17363
+ }
17364
+ async loadFile(filePath) {
17365
+ if (!this.page)
17366
+ throw new Error("Controller not started");
17367
+ if (this.isLocal)
17368
+ throw new Error(
17369
+ "File loading is only supported for remote tldraw.com instances. See tldraw-open.ts for local file loading approach."
17370
+ );
17371
+ await this.closeMenus();
17372
+ await this.page.evaluate(`userPreferences.showFileOpenWarning.set(false);`);
17373
+ log_default.info(`Uploading local file to tldraw.com: ${filePath}`);
17374
+ const tldrFile = await fs.readFile(filePath, "utf8");
17375
+ await this.page.evaluate(set_tldr_iife_default);
17376
+ await this.page.evaluate(`window.setTldr(${tldrFile})`);
17377
+ }
17378
+ async getShareUrl() {
17379
+ if (!this.page)
17380
+ throw new Error("Controller not started");
17381
+ if (this.isLocal)
17382
+ throw new Error("Share URLs are only supported for remote tldraw.com instances.");
17383
+ await this.closeMenus();
17384
+ await this.clickButtonTitles(["Menu", "File", "Share this project"]);
17385
+ await this.page.waitForNavigation({ waitUntil: "networkidle0" });
17386
+ const shareUrl = new URL(this.page.url());
17387
+ shareUrl.search = "";
17388
+ return shareUrl.href;
17406
17389
  }
17407
17390
  async closeMenus() {
17408
17391
  if (!this.page)
@@ -17419,28 +17402,163 @@ var TldrawController = class {
17419
17402
  await this.page.click(`button[title="${title}"]`);
17420
17403
  }
17421
17404
  }
17422
- // TODO memoize...
17423
- async getPageFrameWithNameOrId(nameOrId) {
17424
- const pageFrames = await this.getPageFrames();
17425
- if (pageFrames.length === 0) {
17426
- throw new Error("No frames found");
17405
+ // Deduplicates, verifies existence, and normalizes to ids of the style 'page:xxxx'
17406
+ validatePages(sketch, pages) {
17407
+ if (Array.isArray(pages)) {
17408
+ const validPages = [];
17409
+ for (const p of pages) {
17410
+ const matchingPage = typeof p === "number" ? sketch[p] : sketch.find(
17411
+ (page) => slugify(p) === slugify(page.name) || `page:${p.replace(/^page:/, "")}` === page.id
17412
+ );
17413
+ if (matchingPage) {
17414
+ if (!validPages.includes(matchingPage.id)) {
17415
+ validPages.push(matchingPage.id);
17416
+ }
17417
+ } else {
17418
+ log_default.warn(`Page "${p}" not found in sketch`);
17419
+ }
17420
+ }
17421
+ if (validPages.length === 0) {
17422
+ log_default.warn("None of the requested pages were found in sketch, ignoring pages option");
17423
+ return void 0;
17424
+ }
17425
+ return validPages;
17426
+ }
17427
+ return pages;
17428
+ }
17429
+ validateFrames(sketch, pages, frames) {
17430
+ if (Array.isArray(frames)) {
17431
+ const validSketch = sketch.filter(
17432
+ (page, index2) => pages === void 0 || typeof pages === "boolean" && !pages && index2 === 0 || typeof pages === "boolean" && pages || Array.isArray(pages) && pages.some(
17433
+ (p, index3) => typeof p === "number" ? index3 === p : slugify(p) === slugify(page.name) || p.replace(/^page:/, "") === page.id.replace(/^page:/, "")
17434
+ )
17435
+ );
17436
+ const validFrames = [];
17437
+ for (const f of frames) {
17438
+ const matchingFrames = [];
17439
+ for (const page of validSketch) {
17440
+ const match = page.frames.find(
17441
+ (frame) => slugify(f) === slugify(frame.name) || `shape:${f.replace(/^shape:/, "")}` === frame.id
17442
+ );
17443
+ if (match && !matchingFrames.includes(match.id)) {
17444
+ matchingFrames.push(match.id);
17445
+ }
17446
+ }
17447
+ if (matchingFrames.length === 0) {
17448
+ log_default.warn(`Frame "${f}" not found in sketch`);
17449
+ } else {
17450
+ validFrames.push(...matchingFrames);
17451
+ }
17452
+ if (validFrames.length === 0) {
17453
+ log_default.warn("None of the requested frames were found in sketch, ignoring frames option");
17454
+ return void 0;
17455
+ }
17456
+ }
17457
+ return validFrames;
17427
17458
  }
17428
- return pageFrames.find((f) => f.name === nameOrId || slugify(f.name) === nameOrId) ?? pageFrames.find(
17429
- (f) => f.id === (nameOrId.startsWith("shape:") ? nameOrId : `shape:${nameOrId}`)
17459
+ return frames;
17460
+ }
17461
+ // eslint-disable-next-line complexity
17462
+ getDownloadPlans(sketch, pages, frames) {
17463
+ const validPages = this.validatePages(sketch, pages);
17464
+ const validFrames = this.validateFrames(sketch, validPages, frames);
17465
+ const filteredSketch = validPages === void 0 ? [sketch[0]] : typeof validPages === "boolean" && !validPages ? [sketch[0]] : sketch.filter(
17466
+ (sketchPage) => typeof validPages === "boolean" && validPages ? true : validPages.some(
17467
+ (p) => slugify(p) === slugify(sketchPage.name) || p.replace("page:", "") === sketchPage.id.replace("page:", "")
17468
+ )
17430
17469
  );
17470
+ for (const page of filteredSketch) {
17471
+ page.frames = validFrames === void 0 || typeof validFrames === "boolean" && !validFrames ? [] : page.frames.filter(
17472
+ (frame) => typeof validFrames === "boolean" && validFrames ? true : validFrames.some(
17473
+ (f) => slugify(f) === slugify(frame.name) || f.replace("shape:", "") === frame.id.replace("shape:", "")
17474
+ )
17475
+ );
17476
+ }
17477
+ const downloadPlans = [];
17478
+ const isPageNameCollision = new Set(filteredSketch.map((page) => slugify(page.name))).size !== filteredSketch.length;
17479
+ if (isPageNameCollision) {
17480
+ log_default.warn(
17481
+ "Page names are not unique, including page IDs in the output filenames to avoid collisions"
17482
+ );
17483
+ }
17484
+ for (const page of filteredSketch) {
17485
+ const pageSuffix = validPages === void 0 || typeof validPages === "boolean" && !validPages ? void 0 : isPageNameCollision ? `${slugify(page.name)}-${page.id.replace("page:", "")}` : slugify(page.name);
17486
+ if (page.frames.length === 0) {
17487
+ downloadPlans.push({
17488
+ fileSuffix: pageSuffix ?? "",
17489
+ frameId: void 0,
17490
+ pageId: page.id
17491
+ });
17492
+ } else {
17493
+ const isFrameNameCollision = new Set(page.frames.map((frame) => slugify(frame.name))).size !== page.frames.length;
17494
+ if (isFrameNameCollision) {
17495
+ log_default.warn(
17496
+ "Frame names are not unique, including frame IDs in the output filenames to avoid collisions"
17497
+ );
17498
+ }
17499
+ for (const frame of page.frames) {
17500
+ const frameSuffix = isFrameNameCollision ? [slugify(frame.name), frame.id.replace("shape:", "")].filter((value) => value !== "").join("-") : slugify(frame.name);
17501
+ downloadPlans.push({
17502
+ fileSuffix: [pageSuffix, frameSuffix].filter((value) => value !== void 0).join("-"),
17503
+ frameId: frame.id,
17504
+ pageId: page.id
17505
+ });
17506
+ }
17507
+ }
17508
+ }
17509
+ for (const plan of downloadPlans) {
17510
+ if (plan.fileSuffix !== "") {
17511
+ plan.fileSuffix = `-${plan.fileSuffix}`;
17512
+ }
17513
+ }
17514
+ return downloadPlans;
17431
17515
  }
17432
- // TODO memoize
17433
- async getPageFrames() {
17516
+ // Structure
17517
+ async getSketchStructure() {
17518
+ if (!this.page)
17519
+ throw new Error("Controller not started");
17520
+ const document = await this.getPages();
17521
+ for (const page of document) {
17522
+ page.frames = await this.getPageFrames(page.id);
17523
+ }
17524
+ return document;
17525
+ }
17526
+ async getPages() {
17434
17527
  if (!this.page)
17435
17528
  throw new Error("Controller not started");
17436
17529
  return await this.page.evaluate(
17530
+ `editor.getPages().map((page) => ({ id: page.id, name: page.name, frames: []}))`
17531
+ );
17532
+ }
17533
+ async getPageFrames(pageId) {
17534
+ if (!this.page)
17535
+ throw new Error("Controller not started");
17536
+ const initialPageId = await this.getCurrentPage();
17537
+ if (pageId !== initialPageId) {
17538
+ await this.setCurrentPage(pageId);
17539
+ }
17540
+ const frames = await this.page.evaluate(
17437
17541
  `editor.getCurrentPageShapes().reduce((accumulator, shape) => {
17438
17542
  if (shape.type === 'frame') {
17439
- accumulator.push({ id: shape.id, name: shape.props.name })
17543
+ accumulator.push({ id: shape.id, name: shape.props.name === '' ? 'Frame' : shape.props.name })
17440
17544
  }
17441
17545
  return accumulator
17442
17546
  }, [])`
17443
17547
  );
17548
+ if (pageId !== initialPageId) {
17549
+ await this.setCurrentPage(initialPageId);
17550
+ }
17551
+ return frames;
17552
+ }
17553
+ async getCurrentPage() {
17554
+ if (!this.page)
17555
+ throw new Error("Controller not started");
17556
+ return await this.page.evaluate(`editor.getCurrentPageId()`);
17557
+ }
17558
+ async setCurrentPage(pageId) {
17559
+ if (!this.page)
17560
+ throw new Error("Controller not started");
17561
+ await this.page.evaluate(`editor.setCurrentPage("${pageId}")`);
17444
17562
  }
17445
17563
  // Helpers
17446
17564
  stripStyleElement(svg) {
@@ -17676,14 +17794,7 @@ async function tldrawToImage(tldrPathOrUrl, options = {}) {
17676
17794
  const tldrawUrl = isLocal ? tldrawServer.href : validatedPathOrUrl.href;
17677
17795
  const tldrawController = new TldrawController(tldrawUrl);
17678
17796
  await tldrawController.start();
17679
- let exportReport;
17680
- if (options.frames && typeof options.frames === "boolean") {
17681
- exportReport = await tldrawController.downloadAllFrames(options);
17682
- } else if (Array.isArray(options.frames) && options.frames.length > 0) {
17683
- exportReport = await tldrawController.downloadFrames(options.frames, options);
17684
- } else {
17685
- exportReport = await tldrawController.download(options);
17686
- }
17797
+ const exportReport = await tldrawController.download(options);
17687
17798
  await tldrawController.close();
17688
17799
  if (isLocal)
17689
17800
  tldrawServer.close();
@@ -23183,7 +23294,17 @@ await yargsInstance.scriptName("tldraw-cli").command("$0 <command>", "CLI tools
23183
23294
  // value to true, and still be able to extract meaning from the
23184
23295
  // absence of the option
23185
23296
  defaultDescription: "false",
23186
- describe: 'Export each sketch "frame" as a separate image. Pass one or more frame names or IDs to export specific frames, or skip the arguments to export all frames.',
23297
+ describe: 'Export each sketch "frame" as a separate image. Pass one or more frame names or IDs to export specific frames, or pass the flag without the arguments to export all frames. By default, the entire first page is exported with all frames.',
23298
+ type: "array"
23299
+ }).option("pages", {
23300
+ coerce(args) {
23301
+ return args.length === 0 ? true : args;
23302
+ },
23303
+ // Do not set a default value, so we can coerce --pages without a
23304
+ // value to true, and still be able to extract meaning from the
23305
+ // absence of the option
23306
+ defaultDescription: "false",
23307
+ describe: 'Export each sketch "page" as a separate image. Pass one or more page names or IDs to export specific page, or pass one or more page index numbers (from 0), or pass the flag without the arguments to export all pages. By default, only the first page is exported.',
23187
23308
  type: "array"
23188
23309
  }).option("transparent", {
23189
23310
  alias: "t",
@@ -23233,6 +23354,7 @@ await yargsInstance.scriptName("tldraw-cli").command("$0 <command>", "CLI tools
23233
23354
  name,
23234
23355
  output,
23235
23356
  padding,
23357
+ pages,
23236
23358
  print,
23237
23359
  scale,
23238
23360
  stripStyle,
@@ -23254,6 +23376,7 @@ await yargsInstance.scriptName("tldraw-cli").command("$0 <command>", "CLI tools
23254
23376
  name: resolvedName,
23255
23377
  output,
23256
23378
  padding,
23379
+ pages,
23257
23380
  print,
23258
23381
  scale,
23259
23382
  stripStyle,
@@ -23342,7 +23465,7 @@ await yargsInstance.scriptName("tldraw-cli").command("$0 <command>", "CLI tools
23342
23465
  process.exit(1);
23343
23466
  }
23344
23467
  }
23345
- ).demandCommand(1).alias("h", "help").version("version").alias("v", "version").help().wrap(process.stdout.isTTY ? Math.min(120, yargsInstance.terminalWidth()) : 0).parse();
23468
+ ).demandCommand(1).alias("h", "help").version().alias("v", "version").help().wrap(process.stdout.isTTY ? Math.min(120, yargsInstance.terminalWidth()) : 0).parse();
23346
23469
  /*! Bundled license information:
23347
23470
 
23348
23471
  yargs-parser/build/lib/string-utils.js:
package/dist/.DS_Store CHANGED
Binary file
package/dist/lib/index.js CHANGED
@@ -17049,81 +17049,13 @@ var TldrawController = class {
17049
17049
  async close() {
17050
17050
  if (!this.browser)
17051
17051
  throw new Error("Controller not started");
17052
+ const pages = await this.browser.pages();
17053
+ await Promise.all(pages.map(async (page) => page.close()));
17052
17054
  await this.browser.close();
17053
17055
  log_default.info("Stopped controller");
17054
17056
  }
17055
- // Public method doesn't expose pageFrame
17057
+ // eslint-disable-next-line complexity
17056
17058
  async download(options) {
17057
- return this._download(void 0, options);
17058
- }
17059
- async downloadFrame(frameNameOrId, options) {
17060
- return this.downloadFrames([frameNameOrId], options);
17061
- }
17062
- async downloadFrames(frameNamesOrIds, options) {
17063
- const validPageFrames = [];
17064
- for (const frame of frameNamesOrIds) {
17065
- const pageFrame = await this.getPageFrameWithNameOrId(frame);
17066
- if (pageFrame === void 0) {
17067
- log_default.warn(`Frame "${frame}" not found, skipping`);
17068
- } else {
17069
- validPageFrames.push(pageFrame);
17070
- }
17071
- }
17072
- if (validPageFrames.length === 0) {
17073
- throw new Error("No valid frames found");
17074
- }
17075
- const validFrameNames = validPageFrames.map((frame) => slugify(frame.name));
17076
- const isFrameNameCollision = validFrameNames.length !== new Set(validFrameNames).size;
17077
- if (isFrameNameCollision) {
17078
- log_default.warn(
17079
- "Frame names are not unique, including frame IDs in the output filenames to avoid collisions"
17080
- );
17081
- }
17082
- const outputAccumulator = [];
17083
- for (const frame of validPageFrames) {
17084
- const frameSuffix = isFrameNameCollision ? `-${frame.id.replace("shape:", "")}` : "";
17085
- outputAccumulator.push(
17086
- ...await this._download(frame, {
17087
- ...options,
17088
- name: `${options.name}${frameSuffix}`
17089
- })
17090
- );
17091
- }
17092
- return outputAccumulator;
17093
- }
17094
- async downloadAllFrames(options) {
17095
- const pageFrames = await this.getPageFrames();
17096
- const frameNamesOrIds = pageFrames.map((f) => f.id);
17097
- return this.downloadFrames(frameNamesOrIds, options);
17098
- }
17099
- async loadFile(filePath) {
17100
- if (!this.page)
17101
- throw new Error("Controller not started");
17102
- if (this.isLocal)
17103
- throw new Error(
17104
- "File loading is only supported for remote tldraw.com instances. See tldraw-open.ts for local file loading approach."
17105
- );
17106
- await this.closeMenus();
17107
- await this.page.evaluate(`userPreferences.showFileOpenWarning.set(false);`);
17108
- log_default.info(`Uploading local file to tldraw.com: ${filePath}`);
17109
- const tldrFile = await fs.readFile(filePath, "utf8");
17110
- await this.page.evaluate(set_tldr_iife_default);
17111
- await this.page.evaluate(`window.setTldr(${tldrFile})`);
17112
- }
17113
- async getShareUrl() {
17114
- if (!this.page)
17115
- throw new Error("Controller not started");
17116
- if (this.isLocal)
17117
- throw new Error("Share URLs are only supported for remote tldraw.com instances.");
17118
- await this.closeMenus();
17119
- await this.clickButtonTitles(["Menu", "File", "Share this project"]);
17120
- await this.page.waitForNavigation({ waitUntil: "networkidle0" });
17121
- const shareUrl = new URL(this.page.url());
17122
- shareUrl.search = "";
17123
- return shareUrl.href;
17124
- }
17125
- // eslint-disable-next-line @typescript-eslint/naming-convention
17126
- async _download(pageFrame, options) {
17127
17059
  if (!this.page)
17128
17060
  throw new Error("Controller not started");
17129
17061
  if (this.isEmpty)
@@ -17131,10 +17063,14 @@ var TldrawController = class {
17131
17063
  if (options.stripStyle && options.format !== "svg")
17132
17064
  log_default.warn("--strip-style is only supported for SVG output");
17133
17065
  if (options.format === "tldr") {
17134
- if (pageFrame !== void 0)
17066
+ if (options.frames !== void 0)
17135
17067
  log_default.warn(
17136
17068
  '--frames is not supported when exporting to "tldr", ignoring flag and exporting entire sketch file'
17137
17069
  );
17070
+ if (options.pages !== void 0)
17071
+ log_default.warn(
17072
+ '--pages is not supported when exporting to "tldr", ignoring flag and exporting entire sketch file'
17073
+ );
17138
17074
  if (options.dark !== false) {
17139
17075
  log_default.warn(
17140
17076
  '--dark is not supported when exporting to "tldr", ignoring flag and exporting sketch file'
@@ -17167,22 +17103,37 @@ var TldrawController = class {
17167
17103
  stripStyle = false,
17168
17104
  transparent = false
17169
17105
  } = this.stripUndefined(options);
17170
- let frameSuffix = "";
17171
- let base64String = "";
17172
17106
  if (format === "tldr") {
17173
17107
  await this.page.evaluate(get_tldr_iife_default);
17174
- base64String = await this.page.evaluate(async () => window.getTldr());
17175
- } else {
17176
- if (pageFrame === void 0) {
17108
+ const base64String = await this.page.evaluate(async () => window.getTldr());
17109
+ if (print) {
17110
+ const plainString = base64ToString(base64String).replaceAll("\n", "");
17111
+ return [plainString];
17112
+ }
17113
+ const outputPath = path2.resolve(untildify(path2.join(output, `${filename}.${format}`)));
17114
+ await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17115
+ return [outputPath];
17116
+ }
17117
+ await this.page.evaluate(get_image_iife_default);
17118
+ const sketchStructure = await this.getSketchStructure();
17119
+ const downloadPlan = this.getDownloadPlans(sketchStructure, options.pages, options.frames);
17120
+ const outputAccumulator = [];
17121
+ const initialPageId = await this.getCurrentPage();
17122
+ let pageChanged = false;
17123
+ for (const download of downloadPlan) {
17124
+ if (download.pageId !== initialPageId) {
17125
+ log_default.info(`Selecting sketch page "${download.pageId}"`);
17126
+ await this.setCurrentPage(download.pageId);
17127
+ pageChanged = true;
17128
+ }
17129
+ if (download.frameId === void 0) {
17177
17130
  await this.page.evaluate("editor.selectAll()");
17178
17131
  } else {
17179
- log_default.info(`Selecting sketch frame "${pageFrame.name}" with ID "${pageFrame.id}"`);
17180
- frameSuffix = `-${slugify(pageFrame.name)}`;
17132
+ log_default.info(`Selecting sketch frame "${download.frameId}"`);
17181
17133
  await this.page.evaluate("editor.selectNone()");
17182
- await this.page.evaluate(`editor.select('${pageFrame.id}')`);
17134
+ await this.page.evaluate(`editor.select('${download.frameId}')`);
17183
17135
  }
17184
- await this.page.evaluate(get_image_iife_default);
17185
- base64String = await this.page.evaluate(async (options2) => window.getImage(options2), {
17136
+ let base64String = await this.page.evaluate(async (options2) => window.getImage(options2), {
17186
17137
  background: transparent === void 0 ? void 0 : !transparent,
17187
17138
  darkMode: dark,
17188
17139
  format,
@@ -17192,19 +17143,51 @@ var TldrawController = class {
17192
17143
  if (stripStyle && format === "svg") {
17193
17144
  base64String = stringToBase64(this.stripStyleElement(base64ToString(base64String)));
17194
17145
  }
17195
- }
17196
- const outputPath = path2.resolve(
17197
- untildify(path2.join(output, `${filename}${frameSuffix}.${format}`))
17198
- );
17199
- if (print) {
17200
- if (format === "png") {
17201
- return [base64String];
17146
+ if (print) {
17147
+ if (format === "png") {
17148
+ outputAccumulator.push(base64String);
17149
+ } else {
17150
+ const plainString = base64ToString(base64String).replaceAll("\n", "");
17151
+ outputAccumulator.push(plainString);
17152
+ }
17153
+ } else {
17154
+ const outputPath = path2.resolve(
17155
+ untildify(path2.join(output, `${filename}${download.fileSuffix}.${format}`))
17156
+ );
17157
+ await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17158
+ outputAccumulator.push(outputPath);
17202
17159
  }
17203
- const plainString = base64ToString(base64String).replaceAll("\n", "");
17204
- return [plainString];
17205
17160
  }
17206
- await fs.writeFile(outputPath, base64ToUint8Array(base64String));
17207
- return [outputPath];
17161
+ if (pageChanged) {
17162
+ await this.setCurrentPage(initialPageId);
17163
+ }
17164
+ return outputAccumulator;
17165
+ }
17166
+ async loadFile(filePath) {
17167
+ if (!this.page)
17168
+ throw new Error("Controller not started");
17169
+ if (this.isLocal)
17170
+ throw new Error(
17171
+ "File loading is only supported for remote tldraw.com instances. See tldraw-open.ts for local file loading approach."
17172
+ );
17173
+ await this.closeMenus();
17174
+ await this.page.evaluate(`userPreferences.showFileOpenWarning.set(false);`);
17175
+ log_default.info(`Uploading local file to tldraw.com: ${filePath}`);
17176
+ const tldrFile = await fs.readFile(filePath, "utf8");
17177
+ await this.page.evaluate(set_tldr_iife_default);
17178
+ await this.page.evaluate(`window.setTldr(${tldrFile})`);
17179
+ }
17180
+ async getShareUrl() {
17181
+ if (!this.page)
17182
+ throw new Error("Controller not started");
17183
+ if (this.isLocal)
17184
+ throw new Error("Share URLs are only supported for remote tldraw.com instances.");
17185
+ await this.closeMenus();
17186
+ await this.clickButtonTitles(["Menu", "File", "Share this project"]);
17187
+ await this.page.waitForNavigation({ waitUntil: "networkidle0" });
17188
+ const shareUrl = new URL(this.page.url());
17189
+ shareUrl.search = "";
17190
+ return shareUrl.href;
17208
17191
  }
17209
17192
  async closeMenus() {
17210
17193
  if (!this.page)
@@ -17221,28 +17204,163 @@ var TldrawController = class {
17221
17204
  await this.page.click(`button[title="${title}"]`);
17222
17205
  }
17223
17206
  }
17224
- // TODO memoize...
17225
- async getPageFrameWithNameOrId(nameOrId) {
17226
- const pageFrames = await this.getPageFrames();
17227
- if (pageFrames.length === 0) {
17228
- throw new Error("No frames found");
17207
+ // Deduplicates, verifies existence, and normalizes to ids of the style 'page:xxxx'
17208
+ validatePages(sketch, pages) {
17209
+ if (Array.isArray(pages)) {
17210
+ const validPages = [];
17211
+ for (const p of pages) {
17212
+ const matchingPage = typeof p === "number" ? sketch[p] : sketch.find(
17213
+ (page) => slugify(p) === slugify(page.name) || `page:${p.replace(/^page:/, "")}` === page.id
17214
+ );
17215
+ if (matchingPage) {
17216
+ if (!validPages.includes(matchingPage.id)) {
17217
+ validPages.push(matchingPage.id);
17218
+ }
17219
+ } else {
17220
+ log_default.warn(`Page "${p}" not found in sketch`);
17221
+ }
17222
+ }
17223
+ if (validPages.length === 0) {
17224
+ log_default.warn("None of the requested pages were found in sketch, ignoring pages option");
17225
+ return void 0;
17226
+ }
17227
+ return validPages;
17228
+ }
17229
+ return pages;
17230
+ }
17231
+ validateFrames(sketch, pages, frames) {
17232
+ if (Array.isArray(frames)) {
17233
+ const validSketch = sketch.filter(
17234
+ (page, index2) => pages === void 0 || typeof pages === "boolean" && !pages && index2 === 0 || typeof pages === "boolean" && pages || Array.isArray(pages) && pages.some(
17235
+ (p, index3) => typeof p === "number" ? index3 === p : slugify(p) === slugify(page.name) || p.replace(/^page:/, "") === page.id.replace(/^page:/, "")
17236
+ )
17237
+ );
17238
+ const validFrames = [];
17239
+ for (const f of frames) {
17240
+ const matchingFrames = [];
17241
+ for (const page of validSketch) {
17242
+ const match = page.frames.find(
17243
+ (frame) => slugify(f) === slugify(frame.name) || `shape:${f.replace(/^shape:/, "")}` === frame.id
17244
+ );
17245
+ if (match && !matchingFrames.includes(match.id)) {
17246
+ matchingFrames.push(match.id);
17247
+ }
17248
+ }
17249
+ if (matchingFrames.length === 0) {
17250
+ log_default.warn(`Frame "${f}" not found in sketch`);
17251
+ } else {
17252
+ validFrames.push(...matchingFrames);
17253
+ }
17254
+ if (validFrames.length === 0) {
17255
+ log_default.warn("None of the requested frames were found in sketch, ignoring frames option");
17256
+ return void 0;
17257
+ }
17258
+ }
17259
+ return validFrames;
17229
17260
  }
17230
- return pageFrames.find((f) => f.name === nameOrId || slugify(f.name) === nameOrId) ?? pageFrames.find(
17231
- (f) => f.id === (nameOrId.startsWith("shape:") ? nameOrId : `shape:${nameOrId}`)
17261
+ return frames;
17262
+ }
17263
+ // eslint-disable-next-line complexity
17264
+ getDownloadPlans(sketch, pages, frames) {
17265
+ const validPages = this.validatePages(sketch, pages);
17266
+ const validFrames = this.validateFrames(sketch, validPages, frames);
17267
+ const filteredSketch = validPages === void 0 ? [sketch[0]] : typeof validPages === "boolean" && !validPages ? [sketch[0]] : sketch.filter(
17268
+ (sketchPage) => typeof validPages === "boolean" && validPages ? true : validPages.some(
17269
+ (p) => slugify(p) === slugify(sketchPage.name) || p.replace("page:", "") === sketchPage.id.replace("page:", "")
17270
+ )
17232
17271
  );
17272
+ for (const page of filteredSketch) {
17273
+ page.frames = validFrames === void 0 || typeof validFrames === "boolean" && !validFrames ? [] : page.frames.filter(
17274
+ (frame) => typeof validFrames === "boolean" && validFrames ? true : validFrames.some(
17275
+ (f) => slugify(f) === slugify(frame.name) || f.replace("shape:", "") === frame.id.replace("shape:", "")
17276
+ )
17277
+ );
17278
+ }
17279
+ const downloadPlans = [];
17280
+ const isPageNameCollision = new Set(filteredSketch.map((page) => slugify(page.name))).size !== filteredSketch.length;
17281
+ if (isPageNameCollision) {
17282
+ log_default.warn(
17283
+ "Page names are not unique, including page IDs in the output filenames to avoid collisions"
17284
+ );
17285
+ }
17286
+ for (const page of filteredSketch) {
17287
+ const pageSuffix = validPages === void 0 || typeof validPages === "boolean" && !validPages ? void 0 : isPageNameCollision ? `${slugify(page.name)}-${page.id.replace("page:", "")}` : slugify(page.name);
17288
+ if (page.frames.length === 0) {
17289
+ downloadPlans.push({
17290
+ fileSuffix: pageSuffix ?? "",
17291
+ frameId: void 0,
17292
+ pageId: page.id
17293
+ });
17294
+ } else {
17295
+ const isFrameNameCollision = new Set(page.frames.map((frame) => slugify(frame.name))).size !== page.frames.length;
17296
+ if (isFrameNameCollision) {
17297
+ log_default.warn(
17298
+ "Frame names are not unique, including frame IDs in the output filenames to avoid collisions"
17299
+ );
17300
+ }
17301
+ for (const frame of page.frames) {
17302
+ const frameSuffix = isFrameNameCollision ? [slugify(frame.name), frame.id.replace("shape:", "")].filter((value) => value !== "").join("-") : slugify(frame.name);
17303
+ downloadPlans.push({
17304
+ fileSuffix: [pageSuffix, frameSuffix].filter((value) => value !== void 0).join("-"),
17305
+ frameId: frame.id,
17306
+ pageId: page.id
17307
+ });
17308
+ }
17309
+ }
17310
+ }
17311
+ for (const plan of downloadPlans) {
17312
+ if (plan.fileSuffix !== "") {
17313
+ plan.fileSuffix = `-${plan.fileSuffix}`;
17314
+ }
17315
+ }
17316
+ return downloadPlans;
17233
17317
  }
17234
- // TODO memoize
17235
- async getPageFrames() {
17318
+ // Structure
17319
+ async getSketchStructure() {
17320
+ if (!this.page)
17321
+ throw new Error("Controller not started");
17322
+ const document = await this.getPages();
17323
+ for (const page of document) {
17324
+ page.frames = await this.getPageFrames(page.id);
17325
+ }
17326
+ return document;
17327
+ }
17328
+ async getPages() {
17236
17329
  if (!this.page)
17237
17330
  throw new Error("Controller not started");
17238
17331
  return await this.page.evaluate(
17332
+ `editor.getPages().map((page) => ({ id: page.id, name: page.name, frames: []}))`
17333
+ );
17334
+ }
17335
+ async getPageFrames(pageId) {
17336
+ if (!this.page)
17337
+ throw new Error("Controller not started");
17338
+ const initialPageId = await this.getCurrentPage();
17339
+ if (pageId !== initialPageId) {
17340
+ await this.setCurrentPage(pageId);
17341
+ }
17342
+ const frames = await this.page.evaluate(
17239
17343
  `editor.getCurrentPageShapes().reduce((accumulator, shape) => {
17240
17344
  if (shape.type === 'frame') {
17241
- accumulator.push({ id: shape.id, name: shape.props.name })
17345
+ accumulator.push({ id: shape.id, name: shape.props.name === '' ? 'Frame' : shape.props.name })
17242
17346
  }
17243
17347
  return accumulator
17244
17348
  }, [])`
17245
17349
  );
17350
+ if (pageId !== initialPageId) {
17351
+ await this.setCurrentPage(initialPageId);
17352
+ }
17353
+ return frames;
17354
+ }
17355
+ async getCurrentPage() {
17356
+ if (!this.page)
17357
+ throw new Error("Controller not started");
17358
+ return await this.page.evaluate(`editor.getCurrentPageId()`);
17359
+ }
17360
+ async setCurrentPage(pageId) {
17361
+ if (!this.page)
17362
+ throw new Error("Controller not started");
17363
+ await this.page.evaluate(`editor.setCurrentPage("${pageId}")`);
17246
17364
  }
17247
17365
  // Helpers
17248
17366
  stripStyleElement(svg) {
@@ -17478,14 +17596,7 @@ async function tldrawToImage(tldrPathOrUrl, options = {}) {
17478
17596
  const tldrawUrl = isLocal ? tldrawServer.href : validatedPathOrUrl.href;
17479
17597
  const tldrawController = new TldrawController(tldrawUrl);
17480
17598
  await tldrawController.start();
17481
- let exportReport;
17482
- if (options.frames && typeof options.frames === "boolean") {
17483
- exportReport = await tldrawController.downloadAllFrames(options);
17484
- } else if (Array.isArray(options.frames) && options.frames.length > 0) {
17485
- exportReport = await tldrawController.downloadFrames(options.frames, options);
17486
- } else {
17487
- exportReport = await tldrawController.download(options);
17488
- }
17599
+ const exportReport = await tldrawController.download(options);
17489
17600
  await tldrawController.close();
17490
17601
  if (isLocal)
17491
17602
  tldrawServer.close();
@@ -1,5 +1,4 @@
1
1
  import type { TldrawToImageOptions } from './tldraw-to-image';
2
- type DownloadOptions = Omit<TldrawToImageOptions, 'frames'>;
3
2
  export default class TldrawController {
4
3
  private readonly href;
5
4
  private page?;
@@ -9,18 +8,19 @@ export default class TldrawController {
9
8
  private get isLocal();
10
9
  start(): Promise<void>;
11
10
  close(): Promise<void>;
12
- download(options: DownloadOptions): Promise<string[]>;
13
- downloadFrame(frameNameOrId: string, options: DownloadOptions): Promise<string[]>;
14
- downloadFrames(frameNamesOrIds: string[], options: DownloadOptions): Promise<string[]>;
15
- downloadAllFrames(options: DownloadOptions): Promise<string[]>;
11
+ download(options: TldrawToImageOptions): Promise<string[]>;
16
12
  loadFile(filePath: string): Promise<void>;
17
13
  getShareUrl(): Promise<string>;
18
- private _download;
19
14
  private closeMenus;
20
15
  private clickButtonTitles;
21
- private getPageFrameWithNameOrId;
16
+ private validatePages;
17
+ private validateFrames;
18
+ private getDownloadPlans;
19
+ private getSketchStructure;
20
+ private getPages;
22
21
  private getPageFrames;
22
+ private getCurrentPage;
23
+ private setCurrentPage;
23
24
  private stripStyleElement;
24
25
  private stripUndefined;
25
26
  }
26
- export {};
@@ -6,6 +6,7 @@ export type TldrawToImageOptions = {
6
6
  name?: string;
7
7
  output?: string;
8
8
  padding?: number;
9
+ pages?: boolean | number[] | string[];
9
10
  print?: boolean;
10
11
  scale?: number;
11
12
  stripStyle?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitschpatrol/tldraw-cli",
3
- "version": "4.3.2",
3
+ "version": "4.4.1",
4
4
  "type": "module",
5
5
  "description": "A CLI tool for exporting tldraw sketch URLs and local .tldr files to SVG or PNG images.",
6
6
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "url": "https://ericmika.com"
19
19
  },
20
20
  "license": "MIT",
21
- "packageManager": "pnpm@8.15.5",
21
+ "packageManager": "pnpm@8.15.6",
22
22
  "engines": {
23
23
  "node": ">=18.0.0",
24
24
  "pnpm": ">=8.0.0"
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@fontsource/inter": "^5.0.17",
48
48
  "express": "^4.19.2",
49
- "puppeteer": "^22.6.1",
49
+ "puppeteer": "^22.6.3",
50
50
  "uint8array-extras": "^1.1.0"
51
51
  },
52
52
  "devDependencies": {
@@ -54,8 +54,8 @@
54
54
  "@sindresorhus/slugify": "^2.2.1",
55
55
  "@tldraw/assets": "^2.0.2",
56
56
  "@types/express": "^4.17.21",
57
- "@types/react": "^18.2.73",
58
- "@types/react-dom": "^18.2.22",
57
+ "@types/react": "^18.2.74",
58
+ "@types/react-dom": "^18.2.24",
59
59
  "@types/yargs": "^17.0.32",
60
60
  "@vitejs/plugin-react-swc": "^3.6.0",
61
61
  "bumpp": "^9.4.0",
@@ -73,10 +73,10 @@
73
73
  "react": "^18.2.0",
74
74
  "react-dom": "^18.2.0",
75
75
  "tldraw": "^2.0.2",
76
- "tsx": "^4.7.1",
77
- "typescript": "^5.4.3",
76
+ "tsx": "^4.7.2",
77
+ "typescript": "^5.4.4",
78
78
  "untildify": "^5.0.0",
79
- "vite": "^5.1.7",
79
+ "vite": "^5.2.8",
80
80
  "vitest": "^1.4.0",
81
81
  "yargs": "^17.7.2"
82
82
  },
package/readme.md CHANGED
@@ -109,21 +109,22 @@ tldraw-cli export <files-or-urls..>
109
109
  | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
110
110
  | `files-or-urls` | The tldraw sketch to export. May be one or more paths to local `.tldr` files, or tldraw\.com sketch URLs. Accepts a mix of both file paths and URLs, and supports glob matching via your shell. Prints the absolute path(s) to the exported image(s) to `stdout`. _(Required.)_ | `array` |
111
111
 
112
- | Option | Alias | Argument | Description | Type | Default |
113
- | --------------- | ----- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------------------------- |
114
- | `--format` | `-f` | | Output image format. | `string` | `"svg"` |
115
- | `--output` | `-o` | | Output image directory. | `string` | `"./"` |
116
- | `--name` | `-n` | | Output image name (without extension). | `string` | The original file name or URL id is used. |
117
- | `--frames` | | | Export each sketch "frame" as a separate image. Pass one or more frame names or IDs to export specific frames, or skip the arguments to export all frames. | `array` | `false` |
118
- | `--transparent` | `-t` | | Export an image with a transparent background. | `boolean` | `false` |
119
- | `--dark` | `-d` | | Export a dark theme version of the image. | `boolean` | `false` |
120
- | `--padding` | | `[number]` | Set a specific padding amount around the exported image. | | `32` |
121
- | `--scale` | | `[number]` | Set a sampling amount for raster image exports. | | `1` |
122
- | `--strip-style` | | | Remove `<style>` elements from SVG output, useful to lighten the load of embedded fonts if you intend to provide your own stylesheets. Applies to SVG output only. | `boolean` | `false` |
123
- | `--print` | `-p` | | Print the exported image(s) to stdout instead of saving to a file. Incompatible with `--output`, and disregards `--name`. PNGs are printed as base64-encoded strings. | `boolean` | `false` |
124
- | `--verbose` | | | Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection. | `boolean` | `false` |
125
- | `--help` | `-h` | | Show help | `boolean` | |
126
- | `--version` | `-v` | | Show version number | `boolean` | |
112
+ | Option | Alias | Argument | Description | Type | Default |
113
+ | --------------- | ----- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------------------------- |
114
+ | `--format` | `-f` | | Output image format. | `string` | `"svg"` |
115
+ | `--output` | `-o` | | Output image directory. | `string` | `"./"` |
116
+ | `--name` | `-n` | | Output image name (without extension). | `string` | The original file name or URL id is used. |
117
+ | `--frames` | | | Export each sketch "frame" as a separate image. Pass one or more frame names or IDs to export specific frames, or pass the flag without the arguments to export all frames. By default, the entire first page is exported with all frames. | `array` | `false` |
118
+ | `--pages` | | | Export each sketch "page" as a separate image. Pass one or more page names or IDs to export specific page, or pass one or more page index numbers (from 0), or pass the flag without the arguments to export all pages. By default, only the first page is exported. | `array` | `false` |
119
+ | `--transparent` | `-t` | | Export an image with a transparent background. | `boolean` | `false` |
120
+ | `--dark` | `-d` | | Export a dark theme version of the image. | `boolean` | `false` |
121
+ | `--padding` | | `[number]` | Set a specific padding amount around the exported image. | | `32` |
122
+ | `--scale` | | `[number]` | Set a sampling factor for raster image exports. | | `1` |
123
+ | `--strip-style` | | | Remove `<style>` elements from SVG output, useful to lighten the load of embedded fonts if you intend to provide your own stylesheets. Applies to SVG output only. | `boolean` | `false` |
124
+ | `--print` | `-p` | | Print the exported image(s) to stdout instead of saving to a file. Incompatible with `--output`, and disregards `--name`. PNGs are printed as base64-encoded strings. | `boolean` | `false` |
125
+ | `--verbose` | | | Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection. | `boolean` | `false` |
126
+ | `--help` | `-h` | | Show help | `boolean` | |
127
+ | `--version` | `-v` | | Show version number | `boolean` | |
127
128
 
128
129
  #### Subcommand: `tldraw-cli open`
129
130
 
@@ -240,6 +241,24 @@ tldraw-cli export https://www.tldraw.com/s/v2_c_FI5RYWbdpAtjsy4OIKrKw --frames "
240
241
  tldraw-cli export https://www.tldraw.com/s/v2_c_FI5RYWbdpAtjsy4OIKrKw --frames "Frame 1" "Frame 3"
241
242
  ```
242
243
 
244
+ ##### Export a specific page by name from a tldraw\.com URL
245
+
246
+ ```sh
247
+ tldraw-cli export https://www.tldraw.com/s/v2_c_L_RFQ3mJA_BWHejdH2hlD --pages "Page 3"
248
+ ```
249
+
250
+ ##### Export a specific pages by index from a tldraw\.com URL
251
+
252
+ ```sh
253
+ tldraw-cli export https://www.tldraw.com/s/v2_c_L_RFQ3mJA_BWHejdH2hlD --pages 0 2
254
+ ```
255
+
256
+ ##### Export each pages as its own SVG from a tldraw\.com URL
257
+
258
+ ```sh
259
+ tldraw-cli export https://www.tldraw.com/s/v2_c_L_RFQ3mJA_BWHejdH2hlD --pages
260
+ ```
261
+
243
262
  ##### Export to JSON
244
263
 
245
264
  ```sh
@@ -280,6 +299,7 @@ The library exports a single async function, `tldrawToImage`, which takes an opt
280
299
  name?: string
281
300
  output?: string
282
301
  padding?: number
302
+ pages?: boolean | string[] | number[]
283
303
  print?: boolean
284
304
  scale?: number
285
305
  stripStyle?: boolean
@@ -362,9 +382,9 @@ On Discord:
362
382
 
363
383
  ## Implementation notes
364
384
 
365
- This tool is not a part of the official tldraw project, and it is currently only tested and known to be compatible with tldraw 2.0.0-beta.2.\_
385
+ This tool is not a part of the official tldraw project.
366
386
 
367
- Due to the current implementation of tldraw, export depends on functionality provided by a web browser. So, behind the scenes, this app serves a local instance of tldraw, then loads a `.tldr` and invokes the export download via the [Puppeteer](https://pptr.dev) headless browser automation tool.
387
+ Due to the architecture of tldraw, export depends on functionality provided by a web browser. So, behind the scenes, this app serves a local instance of tldraw, then loads a `.tldr` and invokes the export download via the [Puppeteer](https://pptr.dev) headless browser automation tool.
368
388
 
369
389
  This can be a bit slow, (exporting seems to take a second or two), but in the context of a statically-generated content pipeline it's not the end of the world.
370
390