├── .prettierrc ├── cli-examples ├── .gitignore ├── air-quality.sh ├── wealth-nations.sh ├── suntimes.sh ├── us-covid19.sh ├── bar-charts.sh ├── calendar-view.sh └── benchmark.sh ├── examples ├── package.json ├── apikey.js ├── pdf.js ├── barchart.js ├── italy-covid.js ├── suntime.js ├── ca-counties.js ├── cluster.js └── yarn.lock ├── tests ├── basic.js ├── render.js ├── benchmark.js └── errors.js ├── bin ├── observable-prerender-open ├── observable-prerender-benchmark ├── observable-prerender ├── observable-prerender-animate └── utils.js ├── package.json ├── CHANGELOG.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── src ├── content │ └── index.html └── index.js ├── README.md └── yarn.lock /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth":2 3 | } -------------------------------------------------------------------------------- /cli-examples/.gitignore: -------------------------------------------------------------------------------- 1 | out-* 2 | *.csv 3 | *.json -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "apikey.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "fs-extra": "^9.0.1", 13 | "puppeteer-cluster": "^0.22.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cli-examples/air-quality.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-airquality" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | rm -rf $OUTDIR || true 7 | 8 | mkdir -p $OUTDIR 9 | mkdir -p $OUTDIR/frames 10 | 11 | observable-prerender-animate \ 12 | d/3e57ec399b66f83d mapSVG \ 13 | --iter-waitfor update \ 14 | --iter currentTime:times \ 15 | --out-dir $OUTDIR/frames \ 16 | --format png 17 | 18 | ffmpeg -framerate 30 \ 19 | -i $OUTDIR/frames/%03d_mapSVG.png \ 20 | -c:v libx264 -pix_fmt yuv420p \ 21 | $OUTDIR/air-quality.mp4 -------------------------------------------------------------------------------- /cli-examples/wealth-nations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-nations" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | rm -rf $OUTDIR || true 7 | 8 | mkdir -p $OUTDIR 9 | mkdir -p $OUTDIR/frames 10 | 11 | observable-prerender-animate \ 12 | d/9edd920538384e6b chart \ 13 | --iter-waitfor update \ 14 | --iter year:years \ 15 | --out-dir $OUTDIR/frames \ 16 | --format png 17 | 18 | 19 | ffmpeg -framerate 30 \ 20 | -i $OUTDIR/frames/%04d_chart.png \ 21 | -c:v libx264 -pix_fmt yuv420p \ 22 | $OUTDIR/health-wealth-nations.mp4 -------------------------------------------------------------------------------- /cli-examples/suntimes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-suntimes" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | rm -rf $OUTDIR || true 7 | 8 | mkdir -p $OUTDIR 9 | mkdir -p $OUTDIR/frames 10 | 11 | observable-prerender-animate \ 12 | @asg017/sunrise-and-sunset-worldwide graphic \ 13 | --iter-waitfor controller \ 14 | --iter-index \ 15 | --iter timeI:times \ 16 | --out-dir $OUTDIR/frames 17 | 18 | ffmpeg -framerate 30 \ 19 | -i $OUTDIR/frames/%03d_graphic.png \ 20 | -c:v libx264 -pix_fmt yuv420p \ 21 | $OUTDIR/suntimes.mp4 -------------------------------------------------------------------------------- /cli-examples/us-covid19.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-uscovid" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | rm -rf $OUTDIR || true 7 | 8 | mkdir -p $OUTDIR 9 | mkdir -p $OUTDIR/frames 10 | 11 | observable-prerender-animate \ 12 | @mbostock/covid-19-daily-new-cases chart \ 13 | --iter-waitfor update \ 14 | --iter date:dates \ 15 | --out-dir $OUTDIR/frames \ 16 | --format png 17 | 18 | ffmpeg -framerate 15 \ 19 | -i $OUTDIR/frames/%03d_chart.png \ 20 | -c:v libx264 -pix_fmt yuv420p \ 21 | -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \ 22 | $OUTDIR/us-covid19-daily.mp4 -------------------------------------------------------------------------------- /cli-examples/bar-charts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-barcharts" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | rm -rf $OUTDIR || true 7 | 8 | mkdir -p $OUTDIR 9 | 10 | echo '[{ "name": "alex", "value": 20 },{ "name": "brian", "value": 30 },{ "name": "craig", "value": 10 }]' > $BASEDIR/barchart-data.json 11 | 12 | observable-prerender @d3/bar-chart chart \ 13 | -o "$OUTDIR/bar-chart.png" -q 14 | 15 | observable-prerender @d3/bar-chart chart \ 16 | -o "$OUTDIR/green-bar-chart.png" \ 17 | --redefine color:lightgreen \ 18 | --redefine-file data:json:$BASEDIR/barchart-data.json --quiet -------------------------------------------------------------------------------- /examples/apikey.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-apikey"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load("d/c8a268537d9561d7", ["password"], { 16 | OBSERVABLEHQ_API_KEY: process.env.OBSERVABLEHQ_API_KEY, 17 | }); 18 | console.log(`SECRET: "${await notebook.value("password")}"`); 19 | await notebook.browser.close(); 20 | })(); 21 | -------------------------------------------------------------------------------- /examples/pdf.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-pdf"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load("@jrus/scpie"); 16 | const notebookStyling = await load("d/d2faf59bd6493a6d", [], { 17 | browser: notebook.browser, 18 | }); 19 | 20 | await notebook.pdf(join(OUT_DIR, "notebook.pdf")); 21 | await notebookStyling.pdf(join(OUT_DIR, "notebook-styling.pdf")); 22 | 23 | await notebook.browser.close(); 24 | })(); 25 | -------------------------------------------------------------------------------- /cli-examples/calendar-view.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASEDIR=$(dirname "$0") 3 | OUTDIR="$BASEDIR/out-calendar" 4 | PATH="$PATH:$BASEDIR/../bin" 5 | 6 | if [ -e $BASEDIR/dji.csv ] 7 | then 8 | : 9 | else 10 | echo "Download the CSV at https://finance.yahoo.com/quote/%5EDJI/history/ and save it as 'dji.csv' in the cli-examples directory." 11 | exit 12 | fi 13 | 14 | rm -rf $OUTDIR || true 15 | 16 | mkdir -p $OUTDIR 17 | 18 | observable-prerender @d3/calendar-view chart \ 19 | -o "$OUTDIR/calendar-2000s.png" \ 20 | --file-attachments ^DJI@2.csv:$BASEDIR/dji.csv 21 | 22 | observable-prerender @d3/calendar-view chart \ 23 | -o "$OUTDIR/calendar-2020.png" \ 24 | --file-attachments ^DJI@2.csv:<(csvgrep -c Date -r "2020*" $BASEDIR/dji.csv) -------------------------------------------------------------------------------- /examples/barchart.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-barchart"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load("@d3/bar-chart", ["chart", "data"]); 16 | const data = [ 17 | { name: "alex", value: 20 }, 18 | { name: "brian", value: 30 }, 19 | { name: "craig", value: 10 }, 20 | ]; 21 | await notebook.redefine("data", data); 22 | await notebook.screenshot("chart", join(OUT_DIR, "bar-chart.png")); 23 | await notebook.browser.close(); 24 | })(); 25 | -------------------------------------------------------------------------------- /tests/basic.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | 3 | const test = require("tape"); 4 | 5 | test("basic tests", async (t) => { 6 | let a, b, c, d; 7 | const cells = ["a", "b", "c", "d"]; 8 | const notebook = await load("d/b6147a7172ef9c60", cells); 9 | [a, b, c, d] = await Promise.all(cells.map((d) => notebook.value(d))); 10 | 11 | t.equal(a, 1); 12 | t.equals(b, 2); 13 | t.equals(c, 3); 14 | t.equals(d, 100); 15 | 16 | notebook.redefine("a", 4); 17 | await notebook.waitFor("c"); 18 | t.equals(await notebook.value("a"), 4); 19 | t.equals(await notebook.value("c"), 6); 20 | 21 | notebook.redefine({ a: 100, d: "hello" }); 22 | await notebook.waitFor("a"); 23 | await notebook.waitFor("d"); 24 | t.equals(await notebook.value("a"), 100); 25 | t.equals(await notebook.value("d"), "hello"); 26 | 27 | await notebook.browser.close(); 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/italy-covid.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-italy-covid"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load("d/faa5f8296c8ee793", [ 16 | "map", 17 | "style", 18 | "control1", 19 | "control2", 20 | ]); 21 | const dates = await notebook.value("dates"); 22 | let i = 0; 23 | for (let i = 0; i < dates.length; i++) { 24 | console.log(i); 25 | await notebook.redefine("index", i); 26 | await notebook.screenshot( 27 | "map", 28 | join(OUT_DIR, `${("000" + i).slice(-3)}.png`) 29 | ); 30 | } 31 | await notebook.browser.close(); 32 | })(); 33 | -------------------------------------------------------------------------------- /tests/render.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | 3 | const test = require("tape"); 4 | 5 | test("render tests", async (t) => { 6 | const notebook = await load("d/55ca6d4775103132", ["chart", "target"]); 7 | t.equals( 8 | await notebook.svg("chart"), 9 | `Alex` 10 | ); 11 | t.equals(await notebook.html("target"), "
Whats up, Alex!
"); 12 | 13 | await notebook.redefine({ name: "norton" }); 14 | 15 | t.equals( 16 | await notebook.svg("chart"), 17 | `norton` 18 | ); 19 | t.equals( 20 | await notebook.html("target"), 21 | "
Whats up, norton!
" 22 | ); 23 | 24 | await notebook.browser.close(); 25 | t.end(); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/suntime.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-suntime"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load("@asg017/sunrise-and-sunset-worldwide", [ 16 | "graphic", 17 | "controller", 18 | ]); 19 | notebook.redefine({ coordinates: [-51.42, -57.51] }); 20 | const times = await notebook.value("times"); 21 | for (let i = 0; i < times.length; i++) { 22 | console.log(`${i}/${times.length}`); 23 | await notebook.redefine("timeI", i); 24 | await notebook.waitFor("controller"); 25 | await notebook.screenshot( 26 | "graphic", 27 | join(OUT_DIR, `sun${("000" + i).slice(-3)}.png`) 28 | ); 29 | } 30 | await notebook.browser.close(); 31 | })(); 32 | -------------------------------------------------------------------------------- /examples/ca-counties.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 3 | const { join } = require("path"); 4 | 5 | const OUT_DIR = join(__dirname, "out-ca-counties"); 6 | 7 | if (!existsSync(OUT_DIR)) { 8 | mkdirSync(OUT_DIR); 9 | } else { 10 | removeSync(OUT_DIR); 11 | mkdirSync(OUT_DIR); 12 | } 13 | 14 | (async function () { 15 | const notebook = await load( 16 | "@datadesk/base-maps-for-all-58-california-counties", 17 | ["chart", "viewof county"] 18 | ); 19 | notebook.waitFor("chart"); 20 | const counties = await notebook.value("counties"); 21 | for await (let county of counties) { 22 | console.log(`Doing ${county.name}`); 23 | await notebook.redefine("county", county.fips); 24 | await notebook.waitFor("chart"); 25 | await notebook.screenshot("chart", join(OUT_DIR, `${county.name}.png`)); 26 | await notebook.svg("chart", join(OUT_DIR, `${county.name}.svg`)); 27 | } 28 | await notebook.browser.close(); 29 | })(); 30 | -------------------------------------------------------------------------------- /examples/cluster.js: -------------------------------------------------------------------------------- 1 | const { Cluster } = require("puppeteer-cluster"); 2 | 3 | const { load } = require("../src"); 4 | const { existsSync, mkdirSync, removeSync } = require("fs-extra"); 5 | const { join } = require("path"); 6 | 7 | const OUT_DIR = join(__dirname, "out-cluster"); 8 | 9 | if (!existsSync(OUT_DIR)) { 10 | mkdirSync(OUT_DIR); 11 | } else { 12 | removeSync(OUT_DIR); 13 | mkdirSync(OUT_DIR); 14 | } 15 | 16 | (async () => { 17 | const cluster = await Cluster.launch({ 18 | concurrency: Cluster.CONCURRENCY_CONTEXT, 19 | maxConcurrency: 2, 20 | }); 21 | 22 | await cluster.task(async ({ page, data: notebookId }) => { 23 | const notebook = await load(notebookId, ["chart"], { page }); 24 | await notebook.screenshot( 25 | "chart", 26 | join(OUT_DIR, `${notebookId}.png`.replace("/", "_")) 27 | ); 28 | }); 29 | 30 | cluster.queue("@d3/bar-chart"); 31 | cluster.queue("@d3/line-chart"); 32 | cluster.queue("@d3/directed-chord-diagram"); 33 | cluster.queue("@d3/spike-map"); 34 | cluster.queue("@d3/fan-chart"); 35 | 36 | await cluster.idle(); 37 | await cluster.close(); 38 | })(); 39 | -------------------------------------------------------------------------------- /bin/observable-prerender-open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { program } = require("commander"); 3 | const { load } = require("../src"); 4 | 5 | const { 6 | applyBrowserOptions, 7 | applyRedefineOptions, 8 | getNotebookConfig, 9 | runRedefines, 10 | } = require("./utils.js"); 11 | 12 | let p = program 13 | .version(require("../package.json").version) 14 | .arguments(" [cells...]") 15 | .description("Open an Observable notebook."); 16 | p = applyBrowserOptions(p); 17 | p = applyRedefineOptions(p); 18 | p.action(async function (argNotebook, argCells) { 19 | const opts = program.opts(); 20 | const config = getNotebookConfig(opts); 21 | console.log(config.headless); 22 | config.headless = false; 23 | load(argNotebook, argCells, config) 24 | .then(async (notebook) => { 25 | await runRedefines(notebook, opts, true); 26 | console.log(`Notebook ${argNotebook} loaded.`); 27 | console.log(notebook.browser.wsEndpoint()); 28 | process.on("exit", () => notebook.close()); 29 | }) 30 | .catch((error) => { 31 | console.error(`Error caught when loading ${argNotebook} with`, argCells); 32 | console.error(error); 33 | }); 34 | }); 35 | 36 | program.parse(process.argv); 37 | -------------------------------------------------------------------------------- /cli-examples/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | BASEDIR=$(dirname "$0") 5 | OUTDIR="$BASEDIR/out-benchmark" 6 | PATH="$PATH:$BASEDIR/../bin" 7 | 8 | rm -rf $OUTDIR || true 9 | mkdir -p $OUTDIR 10 | 11 | RESULTS="$OUTDIR/results.ndjson" 12 | 13 | rm $RESULTS || true 14 | touch $RESULTS 15 | 16 | observable-prerender-benchmark @d3/world-airports map >> $RESULTS 17 | observable-prerender-benchmark @d3/u-s-state-capitals map >> $RESULTS 18 | observable-prerender-benchmark @mbostock/u-s-airports-voronoi chart >> $RESULTS 19 | observable-prerender-benchmark @jashkenas/world-cities-delaunay chart >> $RESULTS 20 | observable-prerender-benchmark @mbostock/lets-try-t-sne chart >> $RESULTS 21 | observable-prerender-benchmark @codingwithfire/hhs-hospital-data-visualizations-iv hospitalDots >> $RESULTS 22 | observable-prerender-benchmark @codingwithfire/cmu-covidcast-api-bubbles-export map >> $RESULTS 23 | observable-prerender-benchmark @codingwithfire/voronoi-purpleair map >> $RESULTS 24 | observable-prerender-benchmark @asg017/covid19-cases-in-whittier-california chart >> $RESULTS 25 | observable-prerender-benchmark @karimdouieb/2016-u-s-presidential-election vote_map_population_split_bubble >> $RESULTS 26 | observable-prerender-benchmark @d3/choropleth chart >> $RESULTS -------------------------------------------------------------------------------- /tests/benchmark.js: -------------------------------------------------------------------------------- 1 | const { load } = require("../src"); 2 | const { group } = require("d3-array"); 3 | const test = require("tape"); 4 | 5 | test("benchmarks tests", async (t) => { 6 | const notebook = await load("d/32b4a089fbb4be18", ["c", "z"], { 7 | benchmark: true, 8 | }); 9 | await notebook.value("c"); 10 | await notebook.value("z"); 11 | const benchmarkEvents = notebook.events 12 | .filter((d) => d.type === "benchmark") 13 | .map((d) => d.data); 14 | const g = group( 15 | benchmarkEvents, 16 | (d) => d.name, 17 | (d) => d.status 18 | ); 19 | console.log(benchmarkEvents, g, g.get("a")); 20 | t.true( 21 | g.get("a").get("fulfilled")[0].time <= g.get("c").get("fulfilled")[0].time 22 | ); 23 | t.true( 24 | g.get("b").get("fulfilled")[0].time <= g.get("c").get("fulfilled")[0].time 25 | ); 26 | 27 | const xTime = 28 | g.get("x").get("fulfilled")[0].time - g.get("x").get("pending")[0].time; 29 | const yTime = 30 | g.get("y").get("fulfilled")[0].time - g.get("y").get("pending")[0].time; 31 | const zTime = 32 | g.get("z").get("fulfilled")[0].time - g.get("z").get("pending")[0].time; 33 | 34 | t.true(!Number.isNaN(xTime), 100 < xTime < 110); 35 | t.true(!Number.isNaN(yTime), 50 < yTime < 60); 36 | t.true(!Number.isNaN(yTime), 0 < zTime < 10); 37 | 38 | await notebook.browser.close(); 39 | t.end(); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alex.garcia/observable-prerender", 3 | "version": "0.4.3", 4 | "description": "Pre-render and programmatically control Observable notebooks with Puppeteer!", 5 | "main": "src/index.js", 6 | "author": "Alex Garcia ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/asg017/observable-prerender.git" 10 | }, 11 | "license": "ISC", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "bin": { 16 | "observable-prerender": "bin/observable-prerender", 17 | "observable-prerender-animate": "bin/observable-prerender-animate", 18 | "observable-prerender-benchmark": "bin/observable-prerender-benchmark", 19 | "observable-prerender-open": "bin/observable-prerender-open", 20 | "op": "bin/observable-prerender", 21 | "op-animate": "bin/observable-prerender-animate", 22 | "op-benchmark": "bin/observable-prerender-benchmark", 23 | "op-open": "bin/observable-prerender-open" 24 | }, 25 | "files": [ 26 | "src/*", 27 | "bin/*" 28 | ], 29 | "scripts": { 30 | "test": "tape ./tests/**/*.js", 31 | "t": "tape" 32 | }, 33 | "dependencies": { 34 | "commander": "^6.1.0", 35 | "d3-dsv": "^2.0.0", 36 | "puppeteer": "^5.0.0", 37 | "rw": "^1.3.3" 38 | }, 39 | "devDependencies": { 40 | "d3-array": "^2.9.1", 41 | "fs-extra": "^9.0.1", 42 | "prettier": "2.1.1", 43 | "tape": "^5.0.1" 44 | }, 45 | "keywords": [ 46 | "observable", 47 | "observablehq", 48 | "observable-notebooks", 49 | "prerender", 50 | "puppeteer" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tests/errors.js: -------------------------------------------------------------------------------- 1 | const { load, ObservablePrerenderError } = require("../src"); 2 | const puppeteer = require("puppeteer"); 3 | const test = require("tape"); 4 | 5 | test("ObservablePrerenderError throws stuff", async (t) => { 6 | const browser = await puppeteer.launch(); 7 | await load("@asg017/i-do-not-exist", [], { browser }) 8 | .then(t.fail) 9 | .catch((error) => { 10 | t.true(error instanceof ObservablePrerenderError); 11 | t.true( 12 | error.message.startsWith( 13 | "Error fetching the notebook @asg017/i-do-not-exist" 14 | ) 15 | ); 16 | }); 17 | 18 | const notebook = await load("@asg017/bar-chart-errors", [], { browser }); 19 | 20 | t.true((await notebook.value("color")) === "steelblue"); 21 | 22 | await notebook 23 | .value("notExist") 24 | .then(t.fail) 25 | .catch((error) => { 26 | t.true(error instanceof ObservablePrerenderError); 27 | t.equals( 28 | error.message, 29 | `There is no cell with name "notExist" in the embeded notebook.` 30 | ); 31 | }); 32 | 33 | await notebook 34 | .value("yodaError") 35 | .then(t.fail) 36 | .catch((error) => { 37 | t.true(error instanceof ObservablePrerenderError); 38 | t.equals(error.message, `The cell "yodaError" resolved to an error.`); 39 | }); 40 | 41 | await notebook 42 | .value("circ") 43 | .then(t.fail) 44 | .catch((error) => { 45 | t.true(error instanceof ObservablePrerenderError); 46 | t.equals( 47 | error.message, 48 | `An Observable Runtime error occured when getting the value for "circ": "circ is defined more than once"` 49 | ); 50 | }); 51 | 52 | await browser.close(); 53 | t.end(); 54 | }); 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.4.0] - 2021-02-18 4 | 5 | ### Added 6 | 7 | - `notebook.$(cellName)` method to get the ElementHandle of a cell's container. 8 | - New `--browser-wsendpoint` on `op` CLI programs for connecting to remote browser instances. 9 | - Better error handling when loading non-existant notebook and cells. 10 | - `notebook.close()` to close the notebook's browser when done, or only the notebook's page if connected to remote browser. 11 | 12 | ### Fixed 13 | 14 | - Bugs on some CLI programs that didn't allow reading from stdin in `--redefine-file` and `--file-attachments` 15 | 16 | ## [v0.3.0] - 2021-02-16 17 | 18 | ### Added 19 | 20 | - CLI `observable-prerender-open`, to open a given notebook in a Puppeteer browser for easier debugging. 21 | - CLI an alpha version of `observable-prerender-benchmark`, to benchmark a given notebook's cell execution time. 22 | - CLI programs now have `op-*` aliases cooresponding to `observable-prerender-*` programs. 23 | - `--format=html`, `--format=text`, `--format=json` options for `observable-prerender`. 24 | 25 | ## Changed 26 | 27 | - CLI and Node library use `rw` instead of `fs`, allowing for easier `stdin`/`stdout` usage with `path="-"`. 28 | - Added `notebook.html(cell)` utility that returns the `.outerHTML` value of a given cell. 29 | - In `observable-prerender`, `--quiet` has been depracated in favor of `--verbose`, meaning progress logs are now opt-in. 30 | - `notebook.waitFor(cell)`'s 2nd parameter, `status`, has been deprecated. Now only waiting for a cell to become `fullfilled` is supported. 31 | 32 | ## [v0.1.0] - 2020-08-07 33 | 34 | ### Added 35 | 36 | - New CLI programs `observable-prerender` and `observable-prerender-animate`. 37 | - `width`, `height`, and `headless` options to `load()`'s config parameter, for setting the width/height of a new Puppeteer browser, and for determining of that browser should be headless. 38 | - `notebook.fileAttachments`, for replacing Observable notebook FileAttachments with local files. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | examples/out-* -------------------------------------------------------------------------------- /examples/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | at-least-node@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 8 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 9 | 10 | debug@^4.1.1: 11 | version "4.1.1" 12 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 13 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 14 | dependencies: 15 | ms "^2.1.1" 16 | 17 | fs-extra@^9.0.1: 18 | version "9.0.1" 19 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" 20 | integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== 21 | dependencies: 22 | at-least-node "^1.0.0" 23 | graceful-fs "^4.2.0" 24 | jsonfile "^6.0.1" 25 | universalify "^1.0.0" 26 | 27 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 28 | version "4.2.4" 29 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" 30 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== 31 | 32 | jsonfile@^6.0.1: 33 | version "6.0.1" 34 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" 35 | integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== 36 | dependencies: 37 | universalify "^1.0.0" 38 | optionalDependencies: 39 | graceful-fs "^4.1.6" 40 | 41 | ms@^2.1.1: 42 | version "2.1.2" 43 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 44 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 45 | 46 | puppeteer-cluster@^0.22.0: 47 | version "0.22.0" 48 | resolved "https://registry.yarnpkg.com/puppeteer-cluster/-/puppeteer-cluster-0.22.0.tgz#4ab214671f414f15ad6a94a4b61ed0b4172e86e6" 49 | integrity sha512-hmydtMwfVM+idFIDzS8OXetnujHGre7RY3BGL+3njy9+r8Dcu3VALkZHfuBEPf6byKssTCgzxU1BvLczifXd5w== 50 | dependencies: 51 | debug "^4.1.1" 52 | 53 | universalify@^1.0.0: 54 | version "1.0.0" 55 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" 56 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 57 | -------------------------------------------------------------------------------- /bin/observable-prerender-benchmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { program } = require("commander"); 3 | const { writeFileSync } = require("rw/lib/rw/dash"); 4 | const { load } = require("../src"); 5 | const { 6 | consolidateFormat, 7 | getNotebookConfig, 8 | applyBrowserOptions, 9 | applyRedefineOptions, 10 | } = require("./utils"); 11 | const { join } = require("path"); 12 | 13 | const validFormats = new Set(["json", "html"]); 14 | 15 | let p = program 16 | .version(require("../package.json").version) 17 | .arguments(" ") 18 | .description("Benchmark an Observable notebook.") 19 | .option("-o, --out [file]", "Output file path for the generated report.") 20 | .option("-f, --format [format]", "Type of output, json or html.") 21 | .option("-v, --verbose", "Print logs to follow progress."); 22 | 23 | p = applyBrowserOptions(p); 24 | p = applyRedefineOptions(p); 25 | 26 | p.action(function (argNotebook, argCells) { 27 | const opts = program.opts(); 28 | const { out, verbose } = opts; 29 | let { format } = opts; 30 | format = consolidateFormat(out, format, validFormats, "html"); 31 | 32 | const config = Object.assign(getNotebookConfig(opts), { benchmark: true }); 33 | 34 | if (verbose) console.log(`Loading ${argNotebook} with `, argCells); 35 | 36 | load(argNotebook, argCells, config).then(async (notebook) => { 37 | if (verbose) 38 | console.log(`Loaded notebook, waiting for cells to resolve...`); 39 | 40 | await Promise.all(argCells.map((cell) => notebook.value(cell))); 41 | 42 | await notebook.page.waitForFunction(() => window.notebookModule); 43 | 44 | if (verbose) console.log(`Cells resolved, retrieving metrics information.`); 45 | 46 | const graph = await notebook.page.evaluate(() => { 47 | const ignore = new Set( 48 | window.notebookModule._runtime._builtin._scope.keys() 49 | ); 50 | return Array.from(window.notebookModule._runtime._variables) 51 | .filter( 52 | (v) => 53 | !( 54 | (v._inputs.length === 1 && v._module !== v._inputs[0]._module) // isimport 55 | ) && v._reachable 56 | ) 57 | .filter((v) => v._module !== window.notebookModule._runtime._builtin) // is builtin 58 | .filter((v) => !ignore.has(v._name)) 59 | .map((v) => ({ 60 | name: v._name, 61 | inputs: v._inputs.map((d) => d._name).filter((d) => !ignore.has(d)), 62 | })); 63 | }); 64 | const metrics = await notebook.page.metrics(); 65 | const benchmarkResults = { 66 | notebook: argNotebook, 67 | cells: argCells, 68 | graph, 69 | events: notebook.events, 70 | metrics, 71 | }; 72 | 73 | let path; 74 | if (format === "json") { 75 | path = out || join(process.cwd(), `report.json`); 76 | writeFileSync(path, JSON.stringify(benchmarkResults)); 77 | } else { 78 | path = out || join(process.cwd(), "report.html"); 79 | if (verbose) console.log("Generating report..."); 80 | const resultsNotebook = await load("d/7dc9b816179869cc", ["all"], { 81 | browser: notebook.browser, 82 | }); 83 | await resultsNotebook.redefine("src", JSON.stringify(benchmarkResults)); 84 | await resultsNotebook.html("all", path); 85 | } 86 | if (verbose) 87 | console.log( 88 | `Benchmarking complete! Results ${ 89 | out === "-" ? "printed to stdout" : `saved to ${out}` 90 | }` 91 | ); 92 | 93 | await notebook.close(); 94 | }); 95 | }); 96 | 97 | program.parse(process.argv); 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexsebastian.garcia@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /bin/observable-prerender: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { program } = require("commander"); 3 | const { load } = require("../src"); 4 | const { 5 | applyBrowserOptions, 6 | applyRedefineOptions, 7 | getNotebookConfig, 8 | runRedefines, 9 | consolidateFormat, 10 | } = require("./utils.js"); 11 | const { join } = require("path"); 12 | const { writeFileSync } = require("rw").dash; 13 | 14 | const formatOptions = new Set([ 15 | "png", 16 | "svg", 17 | "jpeg", 18 | "html", 19 | "svg", 20 | "text", 21 | "txt", 22 | "json", 23 | ]); 24 | 25 | let p = program 26 | .version(require("../package.json").version) 27 | .arguments(" [cells...]") 28 | .description("Pre-render an Observable notebook.") 29 | .option( 30 | "-f, --format [format]", 31 | `Type of output: One of ${Array.from(formatOptions).join(", ")}.`, 32 | null 33 | ) 34 | .option( 35 | "-o, --out [file]", 36 | "Output file path. Can only be used when one cell name is passed into 'cells'." 37 | ) 38 | .option( 39 | "--waitfor [cells...]", 40 | "Extra cells to include in the document, but not save as a file (e.g. style cells, animation loops, etc.)." 41 | ) 42 | .option( 43 | "--out-dir ", 44 | "Specify a directory to save all the frames to." 45 | ) 46 | .option( 47 | "--token ", 48 | "An observablehq.com API token to access the notebook." 49 | ) 50 | .option("-v, --verbose", "Print logs to follow progress."); 51 | 52 | p = applyBrowserOptions(p); 53 | p = applyRedefineOptions(p); 54 | 55 | p.action(function (argNotebook, argCells) { 56 | const opts = program.opts(); 57 | const { out, outDir, verbose, waitfor = [] } = opts; 58 | let { format } = opts; 59 | 60 | const notebookConfig = getNotebookConfig(opts); 61 | 62 | if (out && argCells.length > 1) { 63 | console.error( 64 | `Only 1 cell could be passed into 'cells' when '--out' is specified. ${argCells.length} were passed in.` 65 | ); 66 | process.exit(1); 67 | } 68 | 69 | if (out && outDir) { 70 | console.error(`Only 1 of --out and --out-dir can be specified.`); 71 | process.exit(1); 72 | } 73 | 74 | format = consolidateFormat(out, format, formatOptions, "svg"); 75 | const cells = [...argCells, ...waitfor]; 76 | if (verbose) 77 | console.log(`Loading notebook "${argNotebook}" with cells `, cells); 78 | 79 | load(argNotebook, cells, notebookConfig).then(async (notebook) => { 80 | await runRedefines(notebook, opts, verbose); 81 | await Promise.all(waitfor.map((cell) => notebook.waitFor(cell))); 82 | await Promise.all( 83 | argCells.map(async (cell) => { 84 | const path = out 85 | ? out 86 | : join(outDir || process.cwd(), `${cell}.${format}`); 87 | 88 | if (verbose) 89 | console.log(`Saving cell ${cell} as a ${format} file to ${path}`); 90 | 91 | if (format === "svg") return notebook.svg(cell, path); 92 | if (format === "html") return notebook.html(cell, path); 93 | if (format === "text" || format === "txt") 94 | return writeFileSync(path, await notebook.value(cell)); 95 | if (format === "json") 96 | return writeFileSync( 97 | path, 98 | JSON.stringify(await notebook.value(cell)) 99 | ); 100 | return notebook.screenshot(cell, path, { type: format }); 101 | }) 102 | ).catch(async (error) => { 103 | console.error(`Error encountered when rendering cells`); 104 | console.error(error); 105 | notebook.close(); 106 | process.exit(1); 107 | }); 108 | notebook.close(); 109 | process.exit(0); 110 | }); 111 | }); 112 | 113 | program.parse(process.argv); 114 | -------------------------------------------------------------------------------- /bin/observable-prerender-animate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { program } = require("commander"); 3 | const { load } = require("../src"); 4 | const { 5 | applyBrowserOptions, 6 | applyRedefineOptions, 7 | getNotebookConfig, 8 | runRedefines, 9 | } = require("./utils.js"); 10 | const { join } = require("path"); 11 | 12 | function parseArgIter(iter) { 13 | const sep = iter.indexOf(":"); 14 | if (sep < 0) { 15 | console.error( 16 | `SyntaxError: --iter format must be in "abc:xyz" format, no ":" found.` 17 | ); 18 | process.exit(1); 19 | } 20 | return { 21 | counter: iter.substring(0, sep), 22 | iterable: iter.substring(sep + 1), 23 | }; 24 | } 25 | 26 | let p = program 27 | .version(require("../package.json").version) 28 | .arguments(" [cells...]") 29 | .description("Pre-render an Observable notebook.") 30 | .requiredOption( 31 | "--iter ", 32 | "The cell you want to iterate on, followed by the cell that contains the values that will be iterated through." 33 | ) 34 | .option( 35 | "--iter-waitfor ", 36 | 'What cell to "wait for" to be fulfilled when iterating through the iterator.' 37 | ) 38 | .option( 39 | "--iter-index", 40 | "Whether to use the actual values of the iteratorCell or the indicies." 41 | ) 42 | .option("-f, --format ", "Type of output, png, jpeg, or svg", "png") 43 | .option( 44 | "--out-dir ", 45 | "Specify a directory to save all the frames to.", 46 | process.cwd() 47 | ) 48 | .option("-v, --verbose", "Print logs to follow progress."); 49 | 50 | p = applyBrowserOptions(p); 51 | p = applyRedefineOptions(p); 52 | 53 | p.action(function (argNotebook, argCells) { 54 | const opts = program.opts(); 55 | const { iter, iterWaitfor = [], outDir, verbose } = opts; 56 | let { format } = opts; 57 | 58 | const iterIndex = Boolean(opts.iterIndex); 59 | const argIter = parseArgIter(iter); 60 | 61 | const embedCells = [...argCells, ...iterWaitfor]; 62 | 63 | const notebookConfig = getNotebookConfig(opts); 64 | 65 | if (verbose) 66 | console.log(`Loading notebook "${argNotebook}" with cells `, argCells); 67 | 68 | load(argNotebook, embedCells, notebookConfig).then(async (notebook) => { 69 | await runRedefines(notebook, opts, verbose); 70 | 71 | // iterate through the cell 72 | const iterableValues = await notebook.value(argIter.iterable); 73 | 74 | const zeroPad = Array.from({ 75 | length: Math.ceil(Math.log(iterableValues.length) / Math.log(10)), 76 | }) 77 | .map(() => "0") 78 | .join(""); 79 | 80 | for await (const [i, iterableValue] of iterableValues.entries()) { 81 | if (verbose) 82 | console.log(`[${i + 1}/${iterableValues.length}] Redefining`); 83 | 84 | if (iterIndex) await notebook.redefine(argIter.counter, i); 85 | else await notebook.redefine(argIter.counter, iterableValue); 86 | 87 | if (verbose) 88 | console.log( 89 | `[${i + 1}/${iterableValues.length}] Waiting for`, 90 | iterWaitfor 91 | ); 92 | await Promise.all(iterWaitfor.map((cell) => notebook.waitFor(cell))); 93 | 94 | await Promise.all( 95 | argCells.map((cell) => { 96 | if (verbose) 97 | console.log( 98 | `[${i + 1}/${iterableValues.length}] Screenshotting`, 99 | cell 100 | ); 101 | const path = join( 102 | outDir, 103 | `${(zeroPad + i).slice(-zeroPad.length)}_${cell}.${format}` 104 | ); 105 | if (format === "svg") return notebook.svg(cell, path); 106 | return notebook.screenshot(cell, path, { type: format }); 107 | }) 108 | ); 109 | } 110 | 111 | notebook.close(); 112 | }); 113 | }); 114 | 115 | program.parse(process.argv); 116 | -------------------------------------------------------------------------------- /src/content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | observable-prerender 13 | 14 | 15 | 16 | 23 | 138 | 139 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | const readline = require("readline"); 2 | const { csvParse } = require("d3-dsv"); 3 | const rw = require("rw").dash; 4 | const { DEFAULT_WIDTH, DEFAULT_HEIGHT } = require("../src"); 5 | const { isAbsolute, extname } = require("path"); 6 | 7 | // given a --out /path/to/file.txt and a maybe-provided format, 8 | // ensure there are no conflicts and return proper format. 9 | function consolidateFormat(outPath, format, validFormats, defaultFormat) { 10 | const outFileExtension = outPath && extname(outPath).slice(1); 11 | 12 | if (outFileExtension) { 13 | if (format && format !== outFileExtension) 14 | throw Error( 15 | `The provided format "${format}" Doesn't match extension "${outFileExtension}" on the given path ${outPath}` 16 | ); 17 | if (validFormats.has(outFileExtension)) return outFileExtension; 18 | throw Error( 19 | `Provided format "${outFileExtension}" is not valid. Not in (${Array.from( 20 | validFormats 21 | ).join(", ")}) ` 22 | ); 23 | } 24 | 25 | if (!format) return defaultFormat; 26 | if (validFormats.has(format)) return format; 27 | throw Error( 28 | `Provided format "${format}" is not valid. Not in (${Array.from( 29 | validFormats 30 | ).join(", ")}) ` 31 | ); 32 | } 33 | 34 | // given a commander program, add --redefine, --redefine-file, and --file-attachments options 35 | function applyRedefineOptions(program) { 36 | return program 37 | .option("--redefine ", "Redefine a cell (string only)") 38 | .option( 39 | "--redefine-file :value...>", 40 | "Redefine a cell with a file" 41 | ) 42 | .option( 43 | "--file-attachments ", 44 | "Redefine a file attachment with a local file" 45 | ); 46 | } 47 | 48 | // given commander program, add --width, --height, --toke, --no-headless, and --browser-wsendpoint options 49 | function applyBrowserOptions(program) { 50 | return program 51 | .option( 52 | "-w, --width ", 53 | `Width of the Puppeteer browser. Default ${DEFAULT_WIDTH}` 54 | ) 55 | .option( 56 | "-h, --height ", 57 | `Height of the Puppeteer browser. Default ${DEFAULT_HEIGHT}` 58 | ) 59 | .option( 60 | "--token ", 61 | "An observablehq.com API token to access the notebook." 62 | ) 63 | .option( 64 | "--no-headless", 65 | "Turn off headless mode on the Puppeteer browser, meaning open the browser to the user." 66 | ) 67 | .option("--browser-wsendpoint ", `WS Endpoint of browser to us.`); 68 | } 69 | 70 | // run the --redefine, --redefine-file, and --file-attachments 71 | // options found in opts on the given notebook 72 | async function runRedefines(notebook, opts, verbose) { 73 | const { redefine = [], redefineFile = [], fileAttachments = [] } = opts; 74 | 75 | const redefines = parseArgRedefines(redefine); 76 | const redefineFiles = parseArgRedefineFiles(redefineFile); 77 | const redefineFileAttachments = parseArgRedefines(fileAttachments); 78 | 79 | for (const redefine of redefines) { 80 | const { cell, value, format } = redefine; 81 | if (verbose) console.log(`Redefining ${cell} with format ${format}`); 82 | const val = format === "number" ? +value : value; 83 | notebook.redefine(cell, val); 84 | } 85 | 86 | for (const redefineFile of redefineFiles) { 87 | const { cell, value, format: redefineFileFormat } = redefineFile; 88 | if (verbose) 89 | console.log( 90 | `Redefining ${cell} with file ${value} with format ${redefineFileFormat}` 91 | ); 92 | const data = await valueOfFile(value, redefineFileFormat); 93 | notebook.redefine(cell, data); 94 | } 95 | 96 | if (redefineFileAttachments.length > 0) { 97 | const files = {}; 98 | for (const { cell, value } of redefineFileAttachments) { 99 | if (verbose) 100 | console.log(`Replacing FileAttachment ${cell} with file ${value}`); 101 | files[cell] = isAbsolute(value) ? value : join(process.cwd(), value); 102 | } 103 | await notebook.fileAttachments(files); 104 | } 105 | } 106 | 107 | function getNotebookConfig(opts) { 108 | let { width, height, headless, browserWsendpoint, token } = opts; 109 | 110 | if (width) width = +width; 111 | if (height) height = +height; 112 | 113 | const config = { 114 | width, 115 | height, 116 | headless, 117 | browserWSEndpoint: browserWsendpoint, 118 | }; 119 | 120 | if (token) config["OBSERVABLEHQ_API_KEY"] = token; 121 | 122 | return config; 123 | } 124 | 125 | // read the file found at path with the given format 126 | async function valueOfFile(path, format) { 127 | switch (format) { 128 | case "csv": 129 | return csvParse(rw.readFileSync(path)); 130 | case "ndjson": 131 | let data = []; 132 | return await new Promise((resolve, reject) => { 133 | readline 134 | .createInterface({ 135 | input: rw.createReadStream(path), 136 | output: null, 137 | }) 138 | .on("line", (line) => { 139 | try { 140 | data.push(JSON.parse(line)); 141 | } catch (error) { 142 | reject(error); 143 | } 144 | }) 145 | .on("close", () => { 146 | resolve(data); 147 | }); 148 | }).catch((error) => { 149 | console.error(`SyntaxError when reading ${path} as ndjson: `); 150 | console.error(error.message); 151 | process.exit(1); 152 | }); 153 | case "json": 154 | return JSON.parse(rw.readFileSync(path, "utf8")); 155 | case "string": 156 | return rw.readFileSync(path, "utf8"); 157 | default: 158 | console.error(`Unknown format passed in: ${format}`); 159 | process.exit(1); 160 | } 161 | } 162 | 163 | // parse list of --redefine flags 164 | function parseArgRedefines(argRedefines) { 165 | const redefines = []; 166 | for (const redefine of argRedefines) { 167 | redefines.push(parseRedefine(redefine)); 168 | } 169 | return redefines; 170 | } 171 | 172 | // parse list of --redefine-file flags 173 | function parseArgRedefineFiles(argRedefineFiles) { 174 | const redefineFiles = []; 175 | for (const redefineFile of argRedefineFiles) { 176 | redefineFiles.push(parseRedefineFile(redefineFile)); 177 | } 178 | return redefineFiles; 179 | } 180 | 181 | // parse "cell:format:value" for --redefine flags 182 | function parseRedefine(redefine) { 183 | const firstSep = redefine.indexOf(":"); 184 | if (firstSep < 0) { 185 | console.error( 186 | `Redefine syntax for "${redefine}" is incorrect. A ':' must be included.` 187 | ); 188 | process.exit(1); 189 | } 190 | const secondSep = redefine.indexOf(":", firstSep + 1); 191 | 192 | const cell = redefine.substring(0, firstSep); 193 | const value = redefine.substring( 194 | secondSep > -1 ? secondSep + 1 : firstSep + 1 195 | ); 196 | const format = 197 | secondSep > -1 ? redefine.substring(firstSep + 1, secondSep) : "string"; 198 | return { cell, value, format }; 199 | } 200 | 201 | // Parse "cell:format:path" flag for --redefine-file 202 | function parseRedefineFile(redefine) { 203 | const firstSep = redefine.indexOf(":"); 204 | let secondSep, format; 205 | if (firstSep < 0) { 206 | console.error( 207 | `Redefine syntax for "${redefine}" is incorrect. A ':' must be included.` 208 | ); 209 | process.exit(1); 210 | } 211 | secondSep = redefine.indexOf(":", firstSep + 1); 212 | if (secondSep < 0) { 213 | secondSep = firstSep; 214 | format = "string"; 215 | } else { 216 | format = redefine.substring(firstSep + 1, secondSep); 217 | } 218 | 219 | const cell = redefine.substring(0, firstSep); 220 | const value = redefine.substring(secondSep + 1); 221 | return { cell, value, format }; 222 | } 223 | 224 | module.exports = { 225 | parseArgRedefines, 226 | parseArgRedefineFiles, 227 | valueOfFile, 228 | applyBrowserOptions, 229 | applyRedefineOptions, 230 | runRedefines, 231 | getNotebookConfig, 232 | consolidateFormat, 233 | }; 234 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const path = require("path"); 3 | const rw = require("rw").dash; 4 | 5 | const DEFAULT_WIDTH = 1200; 6 | const DEFAULT_HEIGHT = Math.floor((DEFAULT_WIDTH * 9) / 16); 7 | 8 | function serializeCellName(cell) { 9 | return cell.replace(/ /g, "_"); 10 | } 11 | 12 | const htmlPage = rw.readFileSync( 13 | path.join(__dirname, "content", "index.html"), 14 | "utf8" 15 | ); 16 | 17 | class ObservablePrerenderError extends Error { 18 | constructor(message, data, ...params) { 19 | super(...params); 20 | 21 | if (Error.captureStackTrace) { 22 | Error.captureStackTrace(this, ObservablePrerenderError); 23 | } 24 | 25 | this.message = message; 26 | this.data = data; 27 | } 28 | } 29 | 30 | class Notebook { 31 | constructor(browser, page, launchedBrowser) { 32 | this.browser = browser; 33 | this.page = page; 34 | this.launchedBrowser = launchedBrowser; 35 | this.events = []; 36 | } 37 | async close() { 38 | return this.launchedBrowser ? this.browser.close() : this.page.close(); 39 | } 40 | async $(cellName) { 41 | return this.page.$(`#notebook-${serializeCellName(cellName)}`); 42 | } 43 | async _value(cell) { 44 | await this.page.waitForFunction(() => window.notebookModule); 45 | return await this.page.evaluate(async (cell) => { 46 | return await window.notebookModule 47 | .value(cell) 48 | .then((value) => ({ value })) 49 | .catch((error) => { 50 | if (error instanceof window.RuntimeError) 51 | return { errorType: "runtime", error }; 52 | return { errorType: "other", error }; 53 | }); 54 | }, cell); 55 | } 56 | async value(cell) { 57 | await this.page.waitForFunction(() => window.notebookModule); 58 | const { value, error, errorType } = await this._value(cell); 59 | if (!errorType) return value; 60 | if (errorType === "runtime") { 61 | if (error.message === `${cell} is not defined`) 62 | throw new ObservablePrerenderError( 63 | `There is no cell with name "${cell}" in the embeded notebook.`, 64 | { cell, error } 65 | ); 66 | throw new ObservablePrerenderError( 67 | `An Observable Runtime error occured when getting the value for "${cell}": "${error.message}"`, 68 | { cell, error } 69 | ); 70 | } 71 | 72 | throw new ObservablePrerenderError( 73 | `The cell "${cell}" resolved to an error.`, 74 | { 75 | cell, 76 | error, 77 | } 78 | ); 79 | } 80 | async html(cell, path) { 81 | await this.waitFor(cell); 82 | const html = await this.$(cell).then((container) => 83 | container.evaluate((e) => e.innerHTML) 84 | ); 85 | if (path) return rw.writeFileSync(path, html); 86 | return html; 87 | } 88 | // inspired by https://observablehq.com/@mbostock/saving-svg 89 | async svg(cell, path) { 90 | await this.waitFor(cell); 91 | const html = await this.$(cell).then((container) => 92 | container.$eval(`svg`, (e) => { 93 | const xmlns = "http://www.w3.org/2000/xmlns/"; 94 | const xlinkns = "http://www.w3.org/1999/xlink"; 95 | const svgns = "http://www.w3.org/2000/svg"; 96 | 97 | const svg = e.cloneNode(true); 98 | const fragment = window.location.href + "#"; 99 | const walker = document.createTreeWalker( 100 | svg, 101 | NodeFilter.SHOW_ELEMENT, 102 | null, 103 | false 104 | ); 105 | while (walker.nextNode()) { 106 | for (const attr of walker.currentNode.attributes) { 107 | if (attr.value.includes(fragment)) { 108 | attr.value = attr.value.replace(fragment, "#"); 109 | } 110 | } 111 | } 112 | svg.setAttributeNS(xmlns, "xmlns", svgns); 113 | svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns); 114 | const serializer = new window.XMLSerializer(); 115 | const string = serializer.serializeToString(svg); 116 | return string; 117 | }) 118 | ); 119 | if (path) 120 | return new Promise((resolve, reject) => 121 | rw.writeFile(path, html, "utf8", (err) => 122 | err ? reject(err) : resolve() 123 | ) 124 | ); 125 | return html; 126 | } 127 | async screenshot(cell, path, options = {}) { 128 | await this.waitFor(cell); 129 | const container = await this.$(cell); 130 | return await container.screenshot({ path, ...options }); 131 | } 132 | 133 | async pdf(path, options = {}) { 134 | await this.waitFor(); 135 | options = Object.assign(options, { path: path }); 136 | return await this.page.pdf(options); 137 | } 138 | 139 | async waitFor(cell) { 140 | await this.page.waitForFunction(() => window.notebookModule); 141 | if (!cell) { 142 | return await this.page.evaluate(async () => { 143 | // with <3 from https://observablehq.com/@observablehq/notebook-visualizer 144 | await Promise.all( 145 | Array.from(window.notebookModule._runtime._variables) 146 | .filter( 147 | (v) => 148 | !( 149 | (v._inputs.length === 1 && v._module !== v._inputs[0]._module) // isimport 150 | ) && v._reachable 151 | ) 152 | .filter( 153 | (v) => v._module !== window.notebookModule._runtime._builtin 154 | ) 155 | // this is basically .value 156 | // https://github.com/observablehq/runtime/blob/master/src/module.js#L55-L64 157 | .map(async (v) => { 158 | if (v._observer === {}) { 159 | v._observer = true; 160 | window.notebookModule._runtime._dirty.add(v); 161 | } 162 | await window.notebookModule._runtime._compute(); 163 | await v._promise; 164 | return true; 165 | }) 166 | ); 167 | return Promise.resolve(true); 168 | }); 169 | } 170 | return await this.page.evaluate(async (cell) => { 171 | return window.notebookModule.value(cell); 172 | }, cell); 173 | } 174 | 175 | // arg files is an object where keys are the file attachment names 176 | // to override, and values are the (hopefully absolute) path to the 177 | // local file to replace with. 178 | async fileAttachments(files = {}) { 179 | const filesArr = []; 180 | for (const key in files) { 181 | filesArr.push([key, files[key]]); 182 | } 183 | const filePaths = Object.values(files); 184 | 185 | await this.page.exposeFunction("readfile", async (filePath) => { 186 | return new Promise((resolve, reject) => { 187 | if (!filePaths.includes(filePath)) { 188 | return reject( 189 | `Only files exposed in the .fileAttachments argument can be exposed.` 190 | ); 191 | } 192 | rw.readFile(filePath, "utf8", (err, text) => { 193 | if (err) reject(err); 194 | else resolve(text); 195 | }); 196 | }); 197 | }); 198 | 199 | await this.page.evaluate(async (files) => { 200 | const fa = new Map( 201 | await Promise.all( 202 | Object.keys(files).map(async (name) => { 203 | const file = files[name]; 204 | const content = await window.readfile(file); 205 | const url = window.URL.createObjectURL(new Blob([content])); 206 | return [name, url]; 207 | }) 208 | ) 209 | ); 210 | 211 | window.notebookModule.redefine("FileAttachment", [], () => 212 | window.rt.fileAttachments((name) => fa.get(name)) 213 | ); 214 | }, files); 215 | } 216 | async redefine(cell, value) { 217 | await this.page.waitForFunction(() => window.redefine); 218 | if (typeof cell === "string") { 219 | await this.page.evaluate( 220 | (cell, value) => { 221 | window.redefine({ [cell]: value }); 222 | }, 223 | cell, 224 | value 225 | ); 226 | } else if (typeof cell === "object") { 227 | await this.page.evaluate((cells) => { 228 | window.redefine(cells); 229 | }, cell); 230 | } 231 | } 232 | } 233 | async function load(notebook, targets = [], config = {}) { 234 | // width, height, headless 235 | let { 236 | browser, 237 | page, 238 | OBSERVABLEHQ_API_KEY, 239 | headless = true, 240 | width = DEFAULT_WIDTH, 241 | height = DEFAULT_HEIGHT, 242 | benchmark = false, 243 | browserWSEndpoint, 244 | } = config; 245 | let launchedBrowser = false; 246 | if (!browser) { 247 | if (page) browser = page.browser(); 248 | else if (browserWSEndpoint) { 249 | browser = await puppeteer.connect({ 250 | browserWSEndpoint, 251 | }); 252 | } else { 253 | browser = await puppeteer.launch({ 254 | defaultViewport: { width, height }, 255 | args: [`--window-size=${width},${height}`], 256 | headless, 257 | }); 258 | launchedBrowser = true; 259 | } 260 | } 261 | if (!page) { 262 | page = await browser.newPage(); 263 | } 264 | 265 | const nb = new Notebook(browser, page, launchedBrowser); 266 | page.exposeFunction( 267 | "__OBSERVABLE_PRERENDER_BENCHMARK", 268 | (name, status, time) => { 269 | nb.events.push({ 270 | type: "benchmark", 271 | data: { name, status, time }, 272 | }); 273 | } 274 | ); 275 | 276 | await page.setContent(htmlPage, { waitUntil: "load" }); 277 | 278 | await page.waitForFunction(() => window.run); 279 | 280 | const result = await page.evaluate( 281 | async (notebook, targets, OBSERVABLEHQ_API_KEY, benchmark) => 282 | window.run({ 283 | notebook, 284 | targets, 285 | OBSERVABLEHQ_API_KEY, 286 | benchmark, 287 | }), 288 | notebook, 289 | targets, 290 | OBSERVABLEHQ_API_KEY, 291 | benchmark 292 | ); 293 | 294 | if (result) 295 | throw new ObservablePrerenderError( 296 | `Error fetching the notebook ${notebook}. Ensure that the notebook is public or link shared, or pass in an API key with OBSERVABLEHQ_API_KEY.`, 297 | { error: result } 298 | ); 299 | return nb; 300 | } 301 | 302 | module.exports = { 303 | load, 304 | DEFAULT_WIDTH, 305 | DEFAULT_HEIGHT, 306 | ObservablePrerenderError, 307 | }; 308 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # observable-prerender 2 | 3 | Pre-render Observable notebooks with Puppeteer! Inspired by [d3-pre](https://github.com/fivethirtyeight/d3-pre) 4 | 5 | ## Why tho 6 | 7 | Observable notebooks run in the browser and use browser APIs, like SVG, canvas, webgl, and so much more. Sometimes, you may want to script or automate Observable notebooks in some way. For example, you may want to: 8 | 9 | - Create a bar chart with custom data 10 | - Generate a SVG map for every county in California 11 | - Render frames for a MP4 screencast of a custom animation 12 | 13 | If you wanted to do this before, you'd have to manually open a browser, re-write code, upload different file attachments, download cells, and repeat it all many times. Now you can script it all! 14 | 15 | ## Examples 16 | 17 | Check out `examples/` for workable code. 18 | 19 | ### Create a bar chart with your own data 20 | 21 | ```javascript 22 | const { load } = require("@alex.garcia/observable-prerender"); 23 | (async () => { 24 | const notebook = await load("@d3/bar-chart", ["chart", "data"]); 25 | const data = [ 26 | { name: "alex", value: 20 }, 27 | { name: "brian", value: 30 }, 28 | { name: "craig", value: 10 }, 29 | ]; 30 | await notebook.redefine("data", data); 31 | await notebook.screenshot("chart", "bar-chart.png"); 32 | await notebook.browser.close(); 33 | })(); 34 | ``` 35 | 36 | Result: 37 | Screenshot of a bar chart with 3 bars, with labels alex, brian and craig, with values 20, 30, and 10, respectively. 38 | 39 | ### Create a map of every county in California 40 | 41 | ```javascript 42 | const { load } = require("@alex.garcia/observable-prerender"); 43 | (async () => { 44 | const notebook = await load( 45 | "@datadesk/base-maps-for-all-58-california-counties", 46 | ["chart"] 47 | ); 48 | const counties = await notebook.value("counties"); 49 | for await (let county of counties) { 50 | await notebook.redefine("county", county.fips); 51 | await notebook.screenshot("chart", `${county.name}.png`); 52 | await notebook.svg("chart", `${county.name}.svg`); 53 | } 54 | await notebook.browser.close(); 55 | })(); 56 | ``` 57 | 58 | Some of the resulting PNGs: 59 | 60 | | - | - | 61 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 62 | | Picture of a simple map of Los Angeles county. | Picture of a simple map of Merced county. | 63 | | Picture of a simple map of Sacramento county. | Picture of a simple map of San Diegoo county. | 64 | 65 | ### Create frames for an animated GIF 66 | 67 | Create PNG frames with `observable-prerender`: 68 | 69 | ```javascript 70 | const { load } = require("@alex.garcia/observable-prerender"); 71 | (async () => { 72 | const notebook = await load("@asg017/sunrise-and-sunset-worldwide", [ 73 | "graphic", 74 | "controller", 75 | ]); 76 | const times = await notebook.value("times"); 77 | for (let i = 0; i < times.length; i++) { 78 | await notebook.redefine("timeI", i); 79 | await notebook.waitFor("controller"); 80 | await notebook.screenshot("graphic", `sun${i}.png`); 81 | } 82 | await notebook.browser.close(); 83 | })(); 84 | ``` 85 | 86 | Then use something like ffmpeg to create a MP4 video with those frames! 87 | 88 | ```bash 89 | ffmpeg.exe -framerate 30 -i sun%03d.png -c:v libx264 -pix_fmt yuv420p out.mp4 90 | ``` 91 | 92 | Result (as a GIF, since GitHub only supports gifs): 93 | 94 | Screencast of a animation of sunlight time in Los Angeles during the year. 95 | 96 | ### Working with `puppeteer-cluster` 97 | 98 | You can pass in raw Puppeteer `browser`/`page` objects into `load()`, which works really well with 3rd party Puppeteer tools like `puppeteer-cluster`. Here's an example where we have a cluster of Puppeteer workers that take screenshots of the `chart` cells of various D3 examples: 99 | 100 | ```js 101 | const { Cluster } = require("puppeteer-cluster"); 102 | const { load } = require("@alex.garcia/observable-prerender"); 103 | 104 | (async () => { 105 | const cluster = await Cluster.launch({ 106 | concurrency: Cluster.CONCURRENCY_CONTEXT, 107 | maxConcurrency: 2, 108 | }); 109 | 110 | await cluster.task(async ({ page, data: notebookId }) => { 111 | const notebook = await load(notebookId, ["chart"], { page }); 112 | await notebook.screenshot("chart", `${notebookId}.png`.replace("/", "_")); 113 | }); 114 | 115 | cluster.queue("@d3/bar-chart"); 116 | cluster.queue("@d3/line-chart"); 117 | cluster.queue("@d3/directed-chord-diagram"); 118 | cluster.queue("@d3/spike-map"); 119 | cluster.queue("@d3/fan-chart"); 120 | 121 | await cluster.idle(); 122 | await cluster.close(); 123 | })(); 124 | ``` 125 | 126 | ### The `observable-prerender` CLI 127 | 128 | Check out the `/cli-examples` directory for bash scripts that show off the different arguments of the bundled CLI programs. 129 | 130 | ## Install 131 | 132 | ```bash 133 | npm install @alex.garcia/observable-prerender 134 | ``` 135 | 136 | ## API Reference 137 | 138 | Although not required, a solid understanding of the Observable notebook runtime and the embedding process could help greatly when building with this tool. Here's some resources you could use to learn more: 139 | 140 | - [How Observable Runs from Observable](https://observablehq.com/@observablehq/how-observable-runs) 141 | - [Downloading and Embedding Notebooks from Observable](https://observablehq.com/@observablehq/downloading-and-embedding-notebooks) 142 | 143 | ### prerender.**load**(notebook, _targets_, _config_) 144 | 145 | Load the given notebook into a page in a browser. 146 | 147 | - `notebook` <[string]> ID of the notebook on observablehq.com, like `@d3/bar-chart` or `@asg017/bitmoji`. For unlisted notebooks, be sure to include the `d/` prefix (e.g. `d/27a0b05d777304bd`). 148 | - `targets` <[Array]<[string]>> array of cell names that will be evaluated. Every cell in `targets` (and the cells they depend on) will be evaluated and render to the page's DOM. If not supplied, then all cells (including anonymous ones) will be evaluated by default. 149 | 150 | - `config` is an object with key/values for more control over how to load the notebook. 151 | 152 | | Key | Value | 153 | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 154 | | `browser` | Supply a Puppeteer Browser object instead of creating a new one. Good for `headless:false` debugging. | 155 | | `page` | Supply a Puppeteer Page object instead of creating a new browser or page. Good for use in something like [`puppeteer-cluster`](https://github.com/thomasdondorf/puppeteer-cluster) | 156 | | `OBSERVABLEHQ_API_KEY` | Supply an [ObservableHQ API Key](https://observablehq.com/@observablehq/api-keys) to load in private notebooks. NOTE: This library uses the api_key URL query parameter to supply the key to Observable, which according to their guide, is meant for testing and development. | 157 | | `height` | Number, height of the Puppeteer browser that will be created. If `browser` is also passed, this will be ignored. Default `675`. | 158 | | `width` | Number, idth of the Puppeteer browser that will be created. If `browser` is also passed, this will be ignored. Default `1200`. | 159 | | `headless` | Boolean, whether the Puppeteer browser should be "headless" or not. great for debugging. Default `true`. | 160 | 161 | `.load()` returns a Notebook object. A Notebook has `page` and `browser` properties, which are the Puppeteer page and browser objects that the notebook is loaded with. This gives a lower-level API to the underlying Puppeteer objects that render the notebook, in case you want more fine-grain API access for more control. 162 | 163 | ### notebook.**value**(cell) 164 | 165 | Returns a Promise that resolves value of the given cell for the book. For example, if the `@d3/bar-chart` notebook is loaded, then `.value("color")` would return `"steelblue"`, `.value("height")` would return `500`, and `.value("data)` would return the 26-length JS array containing the data. 166 | 167 | Keep in mind that the value return is serialized from the browser to Node, see below for details. 168 | 169 | ### notebook.**redefine**(cell, value) 170 | 171 | Redefine a specific cell in the Notebook runtime to a new value. `cell` is the name of the cell that will be redefined, and `value` is the value that cell will be redefined as. If `cell` is an object, then all of the object's keys/values will be redefined on the notebook (e.g. `cell={a:1, b:2}` would redefine cell `a` to `1` and `b` to `2`). 172 | 173 | Keep in mind that the value return is serialized from the browser to Node, see below for details. 174 | 175 | ### notebook.**screenshot**(cell, path, _options_) 176 | 177 | Take a screenshot of the container of the element that contains the rendered value of `cell`. `path` is the path of the saved screenshot (PNG), and `options` is any extra options that get added to the underlying Puppeteer `.screenshot()` function ([list of options here](https://pptr.dev/#?product=Puppeteer&version=v5.0.0&show=api-pagescreenshotoptions)). For example, if the `@d3/bar-chart` notebook is loaded, `notebook.screenshot('chart')` 178 | 179 | ### notebook.**svg**(cell, path) 180 | 181 | If `cell` is a SVG cell, this will save that cell's SVG into `path`, like `.screenshot()`. Keep in mind, the browser's CSS won't be exported into the SVG, so beware of styling with `class`. 182 | 183 | ### notebook.**pdf**(path, _options_) 184 | 185 | Use Puppeteer's [`.pdf()`](https://pptr.dev/#?product=Puppeteer&version=v5.3.1&show=api-pagepdfoptions) function to render the entire page as a PDF. `path` is the path of the PDF to save to, `options` will be passed into Puppeteer's `.pdf()` function. This will wait for all the cells in the notebook to be fulfilled. Note, this can't be used on a non-headless browser. 186 | 187 | ### notebook.**waitFor**(cell, _status_) 188 | 189 | Returns a Promise that resolves when the cell named `cell` is `"fulfilled"` (see the Observable inspector documentation for more details). The default is fulfilled, but `status` could also be `"pending"` or `"rejected"`. Use this function to ensure that youre redefined changes propagate to dependent cells. If no parameters are passed in, then the Promise will wait all the cells, including un-named ones, to finish executing. 190 | 191 | ### notebook.**fileAttachments**(files) 192 | 193 | Replace the [FileAttachments](https://observablehq.com/@observablehq/file-attachments) of the notebook with those defined in `files`. `files` is an object where the keys are the names of the FileAttachment, and the values are the absolute paths to the files that will replace the FileAttachments. 194 | 195 | ### notebook.**\$**(cell) 196 | 197 | Returns the [`ElementHandle`](https://pptr.dev/#?product=Puppeteer&version=v7.1.0&show=api-class-elementhandle) of the container HTML element for the given observed cell. Can be used to call `.click()`, `.screenshot()`, `.evaluate()`, or any other method to have more control of a specfic rendered cell. 198 | 199 | ## CLI Reference 200 | 201 | `observable-prerender` also comes bundled with 2 CLI programs, `observable-prerender` and `observable-prerender-animate`, that allow you to more quickly pre-render notebooks and integrate with local files and other CLI tools. 202 | 203 | ### `observable-prerender [options] [cells...]` 204 | 205 | Pre-render the given notebook and take screenshots of the given cells. `` is the observablehq.com ID of the notebook to load, same argument as the 1st argument in `.load()`. `[cells...]` is the list of cells that will be screenshotted from the notebook. By default, the screenshots will be saved as `.` in the current directory. 206 | 207 | Run `observable-prerender --help` to get a full list of options. 208 | 209 | ### `observable-prerender-animate [options] [cells...] --iter cell:cellIterator` 210 | 211 | Pre-render the given notebook, iterate through the values of the `cellIterator` cell on the `cell` cell, and take screenshots of the argument cells. `` is the observablehq.com ID of the notebook to load, same argument as the 1st argument in `.load()`. `[cells...]` is the list of cells that will be screenshotted from the notebook. `--iter` is the only required option, in the format of `cell:cellIterator`, where `cell` is the cell that will change on every loop, and `cellIterator` will be the cell that contains all the values. 212 | 213 | Run `observable-prerender-animate --help` to get a full list of options. 214 | 215 | ## Caveats 216 | 217 | ### Beta 218 | 219 | This library is mostly a proof of concept, and probably will change in the future. Follow Issue #2 to know when the stable v1 library will be complete. As always, feedback, bug reports, and ideas will make v1 even better! 220 | 221 | ### Serialization 222 | 223 | There is a Puppeteer serialization process when switching from browser JS data to Node. Returning primitives like arrays, plain JS objects, numbers, and strings will work fine, but custom objects, HTML elements, Date objects, and some typed arrays may not. Which means that some methods like `.value()` or `.redefine()` may be limited or may not work as expected, causing subtle bugs. Check out the [Puppeteer docs](https://pptr.dev/#?product=Puppeteer&version=v3.1.0&show=api-pageevaluatepagefunction-args) for more info about this. 224 | 225 | ### Animation is hard 226 | 227 | You won't be able to make neat screencasts from all Observable notebooks. Puppeteer doesn't support taking a video recording of a browser, so instead, the suggested method is to take several PNG screenshots, then stitch them all together into a gif/mp4 using ffmpeg or some other service. 228 | 229 | So what should you screenshot, exactly? It depends on your notebook. You probably need to have some counter/index/pointer that changes the graph when updated (see [scrubber](https://observablehq.com/@mbostock/scrubber)). You can programmatically redefine that cell using `notebook.redefine` in some loop, then screenshot the graph once the changes propagate (`notebook.waitFor`). But keep in mind, this may work for JS transitions, but CSS animations may not render properly or in time, so it really depends on how you built your notebook. it's super hard to get it right without some real digging. 230 | 231 | If you run into any issues getting frames for a animation, feel free to open an issue! 232 | 233 | ## "Benchmarking" 234 | 235 | In this project, "Benchmarking" can refer to three different things: the `op-benchmark` CLI tool, internal benchmarks for the package, and external benchmarks for comparing against other embedding options. 236 | 237 | ### `op-benchmark` for Benchmarking Notebooks 238 | 239 | `op-benchmark` is a CLI tool bundled with `observable-prerender` that measures how long every cell's execution time for a given notebook. It's meant to be used by anyone to test their own notebooks, and is part of the `observable-prerender` suite of tools. 240 | 241 | ### Internal Benchmarking 242 | 243 | `/benchmark-internal` is a series of tests performed against `observable-prerender` to ensure `observable-prerender` runs as fast as possible, and that new changes to drastically effect the performace of the tool. This is meant to be used by `observable-prerender` developers, not by users of the `observable-prerender` tool. 244 | 245 | #### External Benchmarking 246 | 247 | `/benchmark-external` contains serveral tests to compare `observable-prerender` with other Observable notebook embeding options. A common use-case for `observable-prerender` is to pre-render Observable notebooks for faster performance for end users, so these tests are to ensure and measure how much faster `observable-prerender` actually is. This is meant for `observable-prerender` developers, not for general users. 248 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@*": 6 | version "14.0.14" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" 8 | integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== 9 | 10 | "@types/yauzl@^2.9.1": 11 | version "2.9.1" 12 | resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" 13 | integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== 14 | dependencies: 15 | "@types/node" "*" 16 | 17 | agent-base@5: 18 | version "5.1.1" 19 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" 20 | integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== 21 | 22 | array-filter@^1.0.0: 23 | version "1.0.0" 24 | resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" 25 | integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= 26 | 27 | at-least-node@^1.0.0: 28 | version "1.0.0" 29 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 30 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 31 | 32 | available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: 33 | version "1.0.2" 34 | resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" 35 | integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== 36 | dependencies: 37 | array-filter "^1.0.0" 38 | 39 | balanced-match@^1.0.0: 40 | version "1.0.0" 41 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 42 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 43 | 44 | base64-js@^1.0.2: 45 | version "1.3.1" 46 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" 47 | integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== 48 | 49 | bl@^4.0.1: 50 | version "4.0.2" 51 | resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" 52 | integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== 53 | dependencies: 54 | buffer "^5.5.0" 55 | inherits "^2.0.4" 56 | readable-stream "^3.4.0" 57 | 58 | brace-expansion@^1.1.7: 59 | version "1.1.11" 60 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 61 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 62 | dependencies: 63 | balanced-match "^1.0.0" 64 | concat-map "0.0.1" 65 | 66 | buffer-crc32@~0.2.3: 67 | version "0.2.13" 68 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 69 | integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= 70 | 71 | buffer@^5.2.1, buffer@^5.5.0: 72 | version "5.6.0" 73 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" 74 | integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== 75 | dependencies: 76 | base64-js "^1.0.2" 77 | ieee754 "^1.1.4" 78 | 79 | chownr@^1.1.1: 80 | version "1.1.4" 81 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" 82 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== 83 | 84 | commander@2: 85 | version "2.20.3" 86 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 87 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 88 | 89 | commander@^6.1.0: 90 | version "6.1.0" 91 | resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" 92 | integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== 93 | 94 | concat-map@0.0.1: 95 | version "0.0.1" 96 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 97 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 98 | 99 | d3-array@^2.9.1: 100 | version "2.9.1" 101 | resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.9.1.tgz#f355cc72b46c8649b3f9212029e2d681cb5b9643" 102 | integrity sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg== 103 | 104 | d3-dsv@^2.0.0: 105 | version "2.0.0" 106 | resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-2.0.0.tgz#b37b194b6df42da513a120d913ad1be22b5fe7c5" 107 | integrity sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w== 108 | dependencies: 109 | commander "2" 110 | iconv-lite "0.4" 111 | rw "1" 112 | 113 | debug@4, debug@^4.1.0, debug@^4.1.1: 114 | version "4.1.1" 115 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 116 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 117 | dependencies: 118 | ms "^2.1.1" 119 | 120 | deep-equal@^2.0.3: 121 | version "2.0.3" 122 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" 123 | integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== 124 | dependencies: 125 | es-abstract "^1.17.5" 126 | es-get-iterator "^1.1.0" 127 | is-arguments "^1.0.4" 128 | is-date-object "^1.0.2" 129 | is-regex "^1.0.5" 130 | isarray "^2.0.5" 131 | object-is "^1.1.2" 132 | object-keys "^1.1.1" 133 | object.assign "^4.1.0" 134 | regexp.prototype.flags "^1.3.0" 135 | side-channel "^1.0.2" 136 | which-boxed-primitive "^1.0.1" 137 | which-collection "^1.0.1" 138 | which-typed-array "^1.1.2" 139 | 140 | define-properties@^1.1.2, define-properties@^1.1.3: 141 | version "1.1.3" 142 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" 143 | integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== 144 | dependencies: 145 | object-keys "^1.0.12" 146 | 147 | defined@^1.0.0: 148 | version "1.0.0" 149 | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" 150 | integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= 151 | 152 | dotignore@^0.1.2: 153 | version "0.1.2" 154 | resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" 155 | integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== 156 | dependencies: 157 | minimatch "^3.0.4" 158 | 159 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: 160 | version "1.4.4" 161 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 162 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 163 | dependencies: 164 | once "^1.4.0" 165 | 166 | es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: 167 | version "1.17.6" 168 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" 169 | integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== 170 | dependencies: 171 | es-to-primitive "^1.2.1" 172 | function-bind "^1.1.1" 173 | has "^1.0.3" 174 | has-symbols "^1.0.1" 175 | is-callable "^1.2.0" 176 | is-regex "^1.1.0" 177 | object-inspect "^1.7.0" 178 | object-keys "^1.1.1" 179 | object.assign "^4.1.0" 180 | string.prototype.trimend "^1.0.1" 181 | string.prototype.trimstart "^1.0.1" 182 | 183 | es-get-iterator@^1.1.0: 184 | version "1.1.0" 185 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" 186 | integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== 187 | dependencies: 188 | es-abstract "^1.17.4" 189 | has-symbols "^1.0.1" 190 | is-arguments "^1.0.4" 191 | is-map "^2.0.1" 192 | is-set "^2.0.1" 193 | is-string "^1.0.5" 194 | isarray "^2.0.5" 195 | 196 | es-to-primitive@^1.2.1: 197 | version "1.2.1" 198 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" 199 | integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== 200 | dependencies: 201 | is-callable "^1.1.4" 202 | is-date-object "^1.0.1" 203 | is-symbol "^1.0.2" 204 | 205 | extract-zip@^2.0.0: 206 | version "2.0.1" 207 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" 208 | integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== 209 | dependencies: 210 | debug "^4.1.1" 211 | get-stream "^5.1.0" 212 | yauzl "^2.10.0" 213 | optionalDependencies: 214 | "@types/yauzl" "^2.9.1" 215 | 216 | fd-slicer@~1.1.0: 217 | version "1.1.0" 218 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" 219 | integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= 220 | dependencies: 221 | pend "~1.2.0" 222 | 223 | find-up@^4.0.0: 224 | version "4.1.0" 225 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 226 | integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== 227 | dependencies: 228 | locate-path "^5.0.0" 229 | path-exists "^4.0.0" 230 | 231 | for-each@^0.3.3: 232 | version "0.3.3" 233 | resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" 234 | integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== 235 | dependencies: 236 | is-callable "^1.1.3" 237 | 238 | foreach@^2.0.5: 239 | version "2.0.5" 240 | resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 241 | integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= 242 | 243 | fs-constants@^1.0.0: 244 | version "1.0.0" 245 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" 246 | integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== 247 | 248 | fs-extra@^9.0.1: 249 | version "9.0.1" 250 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" 251 | integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== 252 | dependencies: 253 | at-least-node "^1.0.0" 254 | graceful-fs "^4.2.0" 255 | jsonfile "^6.0.1" 256 | universalify "^1.0.0" 257 | 258 | fs.realpath@^1.0.0: 259 | version "1.0.0" 260 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 261 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 262 | 263 | function-bind@^1.1.1: 264 | version "1.1.1" 265 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 266 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 267 | 268 | get-stream@^5.1.0: 269 | version "5.1.0" 270 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" 271 | integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== 272 | dependencies: 273 | pump "^3.0.0" 274 | 275 | glob@^7.1.3, glob@^7.1.6: 276 | version "7.1.6" 277 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 278 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 279 | dependencies: 280 | fs.realpath "^1.0.0" 281 | inflight "^1.0.4" 282 | inherits "2" 283 | minimatch "^3.0.4" 284 | once "^1.3.0" 285 | path-is-absolute "^1.0.0" 286 | 287 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 288 | version "4.2.4" 289 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" 290 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== 291 | 292 | has-symbols@^1.0.0, has-symbols@^1.0.1: 293 | version "1.0.1" 294 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" 295 | integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== 296 | 297 | has@^1.0.3: 298 | version "1.0.3" 299 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 300 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 301 | dependencies: 302 | function-bind "^1.1.1" 303 | 304 | https-proxy-agent@^4.0.0: 305 | version "4.0.0" 306 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" 307 | integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== 308 | dependencies: 309 | agent-base "5" 310 | debug "4" 311 | 312 | iconv-lite@0.4: 313 | version "0.4.24" 314 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 315 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 316 | dependencies: 317 | safer-buffer ">= 2.1.2 < 3" 318 | 319 | ieee754@^1.1.4: 320 | version "1.1.13" 321 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 322 | integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== 323 | 324 | inflight@^1.0.4: 325 | version "1.0.6" 326 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 327 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 328 | dependencies: 329 | once "^1.3.0" 330 | wrappy "1" 331 | 332 | inherits@2, inherits@^2.0.3, inherits@^2.0.4: 333 | version "2.0.4" 334 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 335 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 336 | 337 | is-arguments@^1.0.4: 338 | version "1.0.4" 339 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" 340 | integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== 341 | 342 | is-bigint@^1.0.0: 343 | version "1.0.0" 344 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" 345 | integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== 346 | 347 | is-boolean-object@^1.0.0: 348 | version "1.0.1" 349 | resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" 350 | integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== 351 | 352 | is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.0: 353 | version "1.2.0" 354 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" 355 | integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== 356 | 357 | is-date-object@^1.0.1, is-date-object@^1.0.2: 358 | version "1.0.2" 359 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" 360 | integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== 361 | 362 | is-map@^2.0.1: 363 | version "2.0.1" 364 | resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" 365 | integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== 366 | 367 | is-number-object@^1.0.3: 368 | version "1.0.4" 369 | resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" 370 | integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== 371 | 372 | is-regex@^1.0.5, is-regex@^1.1.0: 373 | version "1.1.0" 374 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" 375 | integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== 376 | dependencies: 377 | has-symbols "^1.0.1" 378 | 379 | is-set@^2.0.1: 380 | version "2.0.1" 381 | resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" 382 | integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== 383 | 384 | is-string@^1.0.4, is-string@^1.0.5: 385 | version "1.0.5" 386 | resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" 387 | integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== 388 | 389 | is-symbol@^1.0.2: 390 | version "1.0.3" 391 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" 392 | integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== 393 | dependencies: 394 | has-symbols "^1.0.1" 395 | 396 | is-typed-array@^1.1.3: 397 | version "1.1.3" 398 | resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" 399 | integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== 400 | dependencies: 401 | available-typed-arrays "^1.0.0" 402 | es-abstract "^1.17.4" 403 | foreach "^2.0.5" 404 | has-symbols "^1.0.1" 405 | 406 | is-weakmap@^2.0.1: 407 | version "2.0.1" 408 | resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" 409 | integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== 410 | 411 | is-weakset@^2.0.1: 412 | version "2.0.1" 413 | resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" 414 | integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== 415 | 416 | isarray@^2.0.5: 417 | version "2.0.5" 418 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" 419 | integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== 420 | 421 | jsonfile@^6.0.1: 422 | version "6.0.1" 423 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" 424 | integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== 425 | dependencies: 426 | universalify "^1.0.0" 427 | optionalDependencies: 428 | graceful-fs "^4.1.6" 429 | 430 | locate-path@^5.0.0: 431 | version "5.0.0" 432 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" 433 | integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== 434 | dependencies: 435 | p-locate "^4.1.0" 436 | 437 | mime@^2.0.3: 438 | version "2.4.6" 439 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" 440 | integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== 441 | 442 | minimatch@^3.0.4: 443 | version "3.0.4" 444 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 445 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 446 | dependencies: 447 | brace-expansion "^1.1.7" 448 | 449 | minimist@^1.2.5: 450 | version "1.2.5" 451 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 452 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 453 | 454 | mitt@^2.0.1: 455 | version "2.0.1" 456 | resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.0.1.tgz#9e8a075b4daae82dd91aac155a0ece40ca7cb393" 457 | integrity sha512-FhuJY+tYHLnPcBHQhbUFzscD5512HumCPE4URXZUgPi3IvOJi4Xva5IIgy3xX56GqCmw++MAm5UURG6kDBYTdg== 458 | 459 | mkdirp-classic@^0.5.2: 460 | version "0.5.3" 461 | resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" 462 | integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== 463 | 464 | ms@^2.1.1: 465 | version "2.1.2" 466 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 467 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 468 | 469 | object-inspect@^1.7.0: 470 | version "1.8.0" 471 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" 472 | integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== 473 | 474 | object-is@^1.1.2: 475 | version "1.1.2" 476 | resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" 477 | integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== 478 | dependencies: 479 | define-properties "^1.1.3" 480 | es-abstract "^1.17.5" 481 | 482 | object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: 483 | version "1.1.1" 484 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 485 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 486 | 487 | object.assign@^4.1.0: 488 | version "4.1.0" 489 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" 490 | integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== 491 | dependencies: 492 | define-properties "^1.1.2" 493 | function-bind "^1.1.1" 494 | has-symbols "^1.0.0" 495 | object-keys "^1.0.11" 496 | 497 | once@^1.3.0, once@^1.3.1, once@^1.4.0: 498 | version "1.4.0" 499 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 500 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 501 | dependencies: 502 | wrappy "1" 503 | 504 | p-limit@^2.2.0: 505 | version "2.3.0" 506 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 507 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 508 | dependencies: 509 | p-try "^2.0.0" 510 | 511 | p-locate@^4.1.0: 512 | version "4.1.0" 513 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" 514 | integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== 515 | dependencies: 516 | p-limit "^2.2.0" 517 | 518 | p-try@^2.0.0: 519 | version "2.2.0" 520 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 521 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 522 | 523 | path-exists@^4.0.0: 524 | version "4.0.0" 525 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 526 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 527 | 528 | path-is-absolute@^1.0.0: 529 | version "1.0.1" 530 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 531 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 532 | 533 | path-parse@^1.0.6: 534 | version "1.0.6" 535 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 536 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 537 | 538 | pend@~1.2.0: 539 | version "1.2.0" 540 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 541 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= 542 | 543 | pkg-dir@^4.2.0: 544 | version "4.2.0" 545 | resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" 546 | integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== 547 | dependencies: 548 | find-up "^4.0.0" 549 | 550 | prettier@2.1.1: 551 | version "2.1.1" 552 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" 553 | integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== 554 | 555 | progress@^2.0.1: 556 | version "2.0.3" 557 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 558 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 559 | 560 | proxy-from-env@^1.0.0: 561 | version "1.1.0" 562 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 563 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 564 | 565 | pump@^3.0.0: 566 | version "3.0.0" 567 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 568 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 569 | dependencies: 570 | end-of-stream "^1.1.0" 571 | once "^1.3.1" 572 | 573 | puppeteer@^5.0.0: 574 | version "5.0.0" 575 | resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.0.0.tgz#7cf1b1a5c5b6ce5d7abe4d9c9f206d4c52e214ff" 576 | integrity sha512-JnZcgRQnfowRSJoSHteKU7G9fP/YYGB/juPn8m4jNqtzvR0h8GOoFmdjTBesJFfzhYkPU1FosHXnBVUB++xgaA== 577 | dependencies: 578 | debug "^4.1.0" 579 | extract-zip "^2.0.0" 580 | https-proxy-agent "^4.0.0" 581 | mime "^2.0.3" 582 | mitt "^2.0.1" 583 | pkg-dir "^4.2.0" 584 | progress "^2.0.1" 585 | proxy-from-env "^1.0.0" 586 | rimraf "^3.0.2" 587 | tar-fs "^2.0.0" 588 | unbzip2-stream "^1.3.3" 589 | ws "^7.2.3" 590 | 591 | readable-stream@^3.1.1, readable-stream@^3.4.0: 592 | version "3.6.0" 593 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 594 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 595 | dependencies: 596 | inherits "^2.0.3" 597 | string_decoder "^1.1.1" 598 | util-deprecate "^1.0.1" 599 | 600 | regexp.prototype.flags@^1.3.0: 601 | version "1.3.0" 602 | resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" 603 | integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== 604 | dependencies: 605 | define-properties "^1.1.3" 606 | es-abstract "^1.17.0-next.1" 607 | 608 | resolve@^1.17.0: 609 | version "1.17.0" 610 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" 611 | integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== 612 | dependencies: 613 | path-parse "^1.0.6" 614 | 615 | resumer@^0.0.0: 616 | version "0.0.0" 617 | resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" 618 | integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= 619 | dependencies: 620 | through "~2.3.4" 621 | 622 | rimraf@^3.0.2: 623 | version "3.0.2" 624 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 625 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 626 | dependencies: 627 | glob "^7.1.3" 628 | 629 | rw@1, rw@^1.3.3: 630 | version "1.3.3" 631 | resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" 632 | integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= 633 | 634 | safe-buffer@~5.2.0: 635 | version "5.2.1" 636 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 637 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 638 | 639 | "safer-buffer@>= 2.1.2 < 3": 640 | version "2.1.2" 641 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 642 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 643 | 644 | side-channel@^1.0.2: 645 | version "1.0.2" 646 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" 647 | integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== 648 | dependencies: 649 | es-abstract "^1.17.0-next.1" 650 | object-inspect "^1.7.0" 651 | 652 | string.prototype.trim@^1.2.1: 653 | version "1.2.1" 654 | resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" 655 | integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== 656 | dependencies: 657 | define-properties "^1.1.3" 658 | es-abstract "^1.17.0-next.1" 659 | function-bind "^1.1.1" 660 | 661 | string.prototype.trimend@^1.0.1: 662 | version "1.0.1" 663 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" 664 | integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== 665 | dependencies: 666 | define-properties "^1.1.3" 667 | es-abstract "^1.17.5" 668 | 669 | string.prototype.trimstart@^1.0.1: 670 | version "1.0.1" 671 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" 672 | integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== 673 | dependencies: 674 | define-properties "^1.1.3" 675 | es-abstract "^1.17.5" 676 | 677 | string_decoder@^1.1.1: 678 | version "1.3.0" 679 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 680 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 681 | dependencies: 682 | safe-buffer "~5.2.0" 683 | 684 | tape@^5.0.1: 685 | version "5.0.1" 686 | resolved "https://registry.yarnpkg.com/tape/-/tape-5.0.1.tgz#0d70ce90a586387c4efda4393e72872672a416a3" 687 | integrity sha512-wVsOl2shKPcjdJdc8a+PwacvrOdJZJ57cLUXlxW4TQ2R6aihXwG0m0bKm4mA4wjtQNTaLMCrYNEb4f9fjHKUYQ== 688 | dependencies: 689 | deep-equal "^2.0.3" 690 | defined "^1.0.0" 691 | dotignore "^0.1.2" 692 | for-each "^0.3.3" 693 | function-bind "^1.1.1" 694 | glob "^7.1.6" 695 | has "^1.0.3" 696 | inherits "^2.0.4" 697 | is-regex "^1.0.5" 698 | minimist "^1.2.5" 699 | object-inspect "^1.7.0" 700 | object-is "^1.1.2" 701 | object.assign "^4.1.0" 702 | resolve "^1.17.0" 703 | resumer "^0.0.0" 704 | string.prototype.trim "^1.2.1" 705 | through "^2.3.8" 706 | 707 | tar-fs@^2.0.0: 708 | version "2.1.0" 709 | resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" 710 | integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== 711 | dependencies: 712 | chownr "^1.1.1" 713 | mkdirp-classic "^0.5.2" 714 | pump "^3.0.0" 715 | tar-stream "^2.0.0" 716 | 717 | tar-stream@^2.0.0: 718 | version "2.1.2" 719 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" 720 | integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== 721 | dependencies: 722 | bl "^4.0.1" 723 | end-of-stream "^1.4.1" 724 | fs-constants "^1.0.0" 725 | inherits "^2.0.3" 726 | readable-stream "^3.1.1" 727 | 728 | through@^2.3.8, through@~2.3.4: 729 | version "2.3.8" 730 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 731 | integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 732 | 733 | unbzip2-stream@^1.3.3: 734 | version "1.4.3" 735 | resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" 736 | integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== 737 | dependencies: 738 | buffer "^5.2.1" 739 | through "^2.3.8" 740 | 741 | universalify@^1.0.0: 742 | version "1.0.0" 743 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" 744 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 745 | 746 | util-deprecate@^1.0.1: 747 | version "1.0.2" 748 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 749 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 750 | 751 | which-boxed-primitive@^1.0.1: 752 | version "1.0.1" 753 | resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" 754 | integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== 755 | dependencies: 756 | is-bigint "^1.0.0" 757 | is-boolean-object "^1.0.0" 758 | is-number-object "^1.0.3" 759 | is-string "^1.0.4" 760 | is-symbol "^1.0.2" 761 | 762 | which-collection@^1.0.1: 763 | version "1.0.1" 764 | resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" 765 | integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== 766 | dependencies: 767 | is-map "^2.0.1" 768 | is-set "^2.0.1" 769 | is-weakmap "^2.0.1" 770 | is-weakset "^2.0.1" 771 | 772 | which-typed-array@^1.1.2: 773 | version "1.1.2" 774 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" 775 | integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== 776 | dependencies: 777 | available-typed-arrays "^1.0.2" 778 | es-abstract "^1.17.5" 779 | foreach "^2.0.5" 780 | function-bind "^1.1.1" 781 | has-symbols "^1.0.1" 782 | is-typed-array "^1.1.3" 783 | 784 | wrappy@1: 785 | version "1.0.2" 786 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 787 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 788 | 789 | ws@^7.2.3: 790 | version "7.3.1" 791 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" 792 | integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== 793 | 794 | yauzl@^2.10.0: 795 | version "2.10.0" 796 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" 797 | integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= 798 | dependencies: 799 | buffer-crc32 "~0.2.3" 800 | fd-slicer "~1.1.0" 801 | --------------------------------------------------------------------------------