chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as all_benchmarks from "./cross_platform_suite.mjs";
|
||||
import * as perspective_bench from "./src/js/benchmark.mjs";
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import * as url from "node:url";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* We use the `dependencies` of this package for the benchmark candidate
|
||||
* module list, so that we only need specify the dependencies and benchmark
|
||||
* candidates in one place.
|
||||
*/
|
||||
const VERSIONS = Object.keys(
|
||||
JSON.parse(fs.readFileSync(_require.resolve(`./package.json`)))
|
||||
.dependencies,
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
|
||||
perspective_bench.suite(
|
||||
// "ws://localhost:8082/websocket",
|
||||
["@perspective-dev/client", ...VERSIONS],
|
||||
path.join(__dirname, "dist/benchmark-js.arrow"),
|
||||
async function (path, version_idx) {
|
||||
let client, metadata;
|
||||
if (path.startsWith("ws://")) {
|
||||
console.log(path);
|
||||
const { default: perspective } = await import(
|
||||
"@perspective-dev/client"
|
||||
);
|
||||
client = await perspective.websocket(path);
|
||||
metadata = {
|
||||
version: "4.5.2",
|
||||
version_idx,
|
||||
};
|
||||
} else {
|
||||
const perspective = await import(path);
|
||||
const pkg_json = JSON.parse(
|
||||
fs.readFileSync(_require.resolve(`${path}/package.json`)),
|
||||
);
|
||||
|
||||
let version = pkg_json.version;
|
||||
console.log(`${path} (${pkg_json.name}@${version})`);
|
||||
if (version === "@perspective-dev/client") {
|
||||
version = `${version} (master)`;
|
||||
}
|
||||
|
||||
client = perspective.default || perspective;
|
||||
metadata = { version, version_idx };
|
||||
}
|
||||
|
||||
globalThis.__SEND__ = (x) => process.send(x);
|
||||
|
||||
await all_benchmarks.table_suite(client, metadata);
|
||||
await all_benchmarks.view_suite(client, metadata);
|
||||
await all_benchmarks.to_data_suite(client, metadata);
|
||||
await all_benchmarks.join_suite(client, metadata);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,111 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
export const CASES = [
|
||||
{
|
||||
name: "Y Line (split)",
|
||||
config: {
|
||||
plugin: "Y Line",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
split_by: ["Region"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Y Line",
|
||||
config: {
|
||||
plugin: "Y Line",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Y Bar",
|
||||
config: {
|
||||
plugin: "Y Bar",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "X Bar",
|
||||
config: {
|
||||
plugin: "X Bar",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Y Area",
|
||||
config: {
|
||||
plugin: "Y Area",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Y Scatter",
|
||||
config: {
|
||||
plugin: "Y Scatter",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "X/Y Scatter",
|
||||
config: {
|
||||
plugin: "X/Y Scatter",
|
||||
columns: ["Quantity", "Profit"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Heatmap",
|
||||
config: {
|
||||
plugin: "Heatmap",
|
||||
columns: ["Sales"],
|
||||
group_by: ["Category"],
|
||||
// split_by: ["Region"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Treemap",
|
||||
config: {
|
||||
plugin: "Treemap",
|
||||
columns: ["Sales"],
|
||||
// group_by: ["Category", "Sub-Category"],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: "Sunburst",
|
||||
// config: {
|
||||
// plugin: "Sunburst",
|
||||
// columns: ["Sales"],
|
||||
// // group_by: ["State", "City"],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: "Candlestick",
|
||||
config: {
|
||||
plugin: "Candlestick",
|
||||
columns: ["Open", "Close", "High", "Low"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Y OHLC",
|
||||
config: {
|
||||
plugin: "OHLC",
|
||||
columns: ["Open", "Close", "High", "Low"],
|
||||
// group_by: ["Category"],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,59 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
// Installed into the page once via `page.addScriptTag`. Exposes:
|
||||
// __BENCH_RESTORE__(config) - restore the viewer with `config`, drain RAFs,
|
||||
// cache the active plugin and a fresh View.
|
||||
// __BENCH_DRAW__() - drive one full plugin.draw(view) and return
|
||||
// its wall-clock duration (ms).
|
||||
//
|
||||
// Important: `viewer.getView()` returns the *session-owned* View, not a clone.
|
||||
// We never call `.delete()` on it — the viewer destroys it on the next
|
||||
// `restore()`. See viewer.rs::getView docs.
|
||||
|
||||
(() => {
|
||||
let _view = null;
|
||||
let _plugin = null;
|
||||
|
||||
async function waitFrames(n) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
}
|
||||
}
|
||||
|
||||
window.__BENCH_RESTORE__ = async function (config) {
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.reset();
|
||||
await viewer.restore(config);
|
||||
// Drain the viewer's own scheduled render so the GL context is sized
|
||||
// and warm before we start timing.
|
||||
await viewer.flush();
|
||||
await waitFrames(2);
|
||||
|
||||
_plugin = viewer.getPlugin();
|
||||
_view = await viewer.getView();
|
||||
};
|
||||
|
||||
window.__BENCH_DRAW__ = async function () {
|
||||
if (!_plugin || !_view) {
|
||||
throw new Error("__BENCH_DRAW__: call __BENCH_RESTORE__ first");
|
||||
}
|
||||
|
||||
// `plugin.draw(view)` performs the full upload + GL submit. The
|
||||
// returned promise resolves once `viewToColumnDataMap` has streamed
|
||||
// every chunk through `_renderChunkData`. That is the unit we want.
|
||||
const t0 = performance.now();
|
||||
await _plugin.draw(_view);
|
||||
const t1 = performance.now();
|
||||
return t1 - t0;
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,135 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
import * as process from "node:process";
|
||||
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
import * as perspective_bench from "./src/js/benchmark.mjs";
|
||||
import { CASES } from "./charts_cases.mjs";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
const PAGE_HARNESS = fs.readFileSync(
|
||||
path.join(__dirname, "charts_page_harness.js"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// Match the SwiftShader / DPR=1 launch flags from
|
||||
// `tools/test/playwright.config.ts` so frame timings come from the
|
||||
// deterministic software GL path, not whatever GPU the host has.
|
||||
const LAUNCH_ARGS = [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
'--proxy-server="direct://"',
|
||||
"--proxy-bypass-list=*",
|
||||
"--js-flags=--expose-gc",
|
||||
"--enable-precise-memory-info",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-renderer-backgrounding",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
// "--use-gl=swiftshader",
|
||||
// "--use-angle=swiftshader",
|
||||
];
|
||||
|
||||
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
|
||||
|
||||
// Single-version run — the workspace build. The `suite()` harness still
|
||||
// expects an array; we only need one entry.
|
||||
const VERSIONS = ["@perspective-dev/viewer-charts"];
|
||||
|
||||
perspective_bench.suite(
|
||||
VERSIONS,
|
||||
path.join(__dirname, "dist/benchmark-charts.arrow"),
|
||||
async function run_charts_bench(_path, version_idx) {
|
||||
const version = "workspace";
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
args: LAUNCH_ARGS,
|
||||
});
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
page.on("pageerror", (err) =>
|
||||
console.error("page error:", err.message),
|
||||
);
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
console.error("page console.error:", msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// The bench WebSocketServer (port 8081) mounts
|
||||
// `["src/html/", "../.."]` relative to cwd. Our cwd is
|
||||
// `tools/bench`, so `src/html/charts-bench.html` is served at
|
||||
// `/charts-bench.html` and `/node_modules/...` resolves through
|
||||
// the workspace root.
|
||||
await page.goto("http://localhost:8081/charts-bench.html");
|
||||
|
||||
await page.waitForFunction(
|
||||
() => window["__TEST_PERSPECTIVE_READY__"] === true,
|
||||
null,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
await page.addScriptTag({ content: PAGE_HARNESS });
|
||||
for (const c of CASES) {
|
||||
// Run one `benchmark()` per case in the page context. The
|
||||
// bench harness's browser branch uses `performance.now()`
|
||||
// around our `test()`, and `__SEND__` collects observations
|
||||
// we forward to the parent over IPC.
|
||||
const items = await page.evaluate(
|
||||
async ({ caseSpec, version, version_idx }) => {
|
||||
const { benchmark } = await import(
|
||||
"/tools/bench/src/js/benchmark.mjs"
|
||||
);
|
||||
|
||||
const total = [];
|
||||
window.__SEND__ = (x) => total.push(x);
|
||||
await window.__BENCH_RESTORE__(caseSpec.config);
|
||||
await benchmark({
|
||||
name: `${caseSpec.name}`,
|
||||
metadata: { version, version_idx },
|
||||
async after() {
|
||||
await new Promise((x) =>
|
||||
requestAnimationFrame(x),
|
||||
);
|
||||
},
|
||||
async test() {
|
||||
await window.__BENCH_DRAW__();
|
||||
},
|
||||
});
|
||||
|
||||
return total;
|
||||
},
|
||||
{ caseSpec: c, version, version_idx },
|
||||
);
|
||||
|
||||
for (const item of items) {
|
||||
process.send(item);
|
||||
}
|
||||
|
||||
// await page.pause();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,344 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { benchmark } from "./src/js/benchmark.mjs";
|
||||
import {
|
||||
check_version_gte,
|
||||
new_superstore_table,
|
||||
} from "./src/js/superstore.mjs";
|
||||
|
||||
export async function join_suite(perspective, metadata) {
|
||||
if (check_version_gte(metadata.version, "4.4.0")) {
|
||||
async function before_all() {
|
||||
const left = await perspective.table(
|
||||
new_superstore_table(metadata),
|
||||
);
|
||||
|
||||
const columns = await left.columns();
|
||||
const expressions = Object.fromEntries(
|
||||
columns
|
||||
.filter((x) => x !== "Row ID")
|
||||
.map((x) => [`${x}_2`, `"${x}"`]),
|
||||
);
|
||||
|
||||
const view = await left.view({
|
||||
columns: ["Row ID", ...Object.keys(expressions)],
|
||||
expressions,
|
||||
});
|
||||
|
||||
const right = await perspective.table(await view.to_arrow());
|
||||
await view.delete();
|
||||
return { left, right };
|
||||
}
|
||||
|
||||
async function after_all({ left, right }) {
|
||||
await left.delete();
|
||||
await right.delete();
|
||||
}
|
||||
|
||||
await benchmark({
|
||||
name: `.join()`,
|
||||
before_all,
|
||||
after_all,
|
||||
metadata,
|
||||
async after(_, joined) {
|
||||
await joined.delete();
|
||||
},
|
||||
async test({ left, right }) {
|
||||
return await perspective.join(left, right, "Row ID");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function to_data_suite(perspective, metadata) {
|
||||
async function before_all() {
|
||||
const table = await perspective.table(new_superstore_table(metadata));
|
||||
const view = await table.view();
|
||||
return { table, view };
|
||||
}
|
||||
|
||||
async function after_all({ table, view }) {
|
||||
if (check_version_gte(metadata.version, "2.10.9")) {
|
||||
await view.delete();
|
||||
}
|
||||
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
}
|
||||
|
||||
await benchmark({
|
||||
name: `.to_arrow()`,
|
||||
before_all,
|
||||
after_all,
|
||||
metadata,
|
||||
async test({ view }) {
|
||||
const _arrow = await view.to_arrow();
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.to_csv()`,
|
||||
before_all,
|
||||
after_all,
|
||||
metadata,
|
||||
async test({ view }) {
|
||||
const _csv = await view.to_csv();
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.to_columns()`,
|
||||
before_all,
|
||||
after_all,
|
||||
metadata,
|
||||
async test({ view }) {
|
||||
const _columns = await view.to_columns();
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.to_json()`,
|
||||
before_all,
|
||||
after_all,
|
||||
metadata,
|
||||
async test({ view }) {
|
||||
const _json = await view.to_json();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function view_suite(perspective, metadata) {
|
||||
async function before_all() {
|
||||
const table = await perspective.table(new_superstore_table(metadata));
|
||||
|
||||
const schema = await table.schema();
|
||||
return { table, schema };
|
||||
}
|
||||
|
||||
async function after_all({ table }) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
}
|
||||
|
||||
async function after({ table }, view) {
|
||||
if (check_version_gte(metadata.version, "2.10.9")) {
|
||||
await view.delete();
|
||||
}
|
||||
}
|
||||
|
||||
await benchmark({
|
||||
name: `.view()`,
|
||||
before_all,
|
||||
after_all,
|
||||
after,
|
||||
metadata,
|
||||
async test({ table }) {
|
||||
return await table.view();
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.view({group_by})`,
|
||||
before_all,
|
||||
after_all,
|
||||
after,
|
||||
metadata,
|
||||
async test({ table }) {
|
||||
if (check_version_gte(metadata.version, "1.2.0")) {
|
||||
return await table.view({ group_by: ["Product Name"] });
|
||||
} else {
|
||||
return await table.view({ row_pivots: ["Product Name"] });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.view({expressions})`,
|
||||
before_all,
|
||||
after_all,
|
||||
after,
|
||||
metadata,
|
||||
async test({ table }) {
|
||||
if (check_version_gte(metadata.version, "2.7.0")) {
|
||||
return await table.view({
|
||||
columns: ["AAA"],
|
||||
expressions: {
|
||||
AAA: `("Sales" + "Profit") / 2`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return await table.view({
|
||||
columns: ["AAA"],
|
||||
expressions: [`//AAA\n("Sales" + "Profit") / 2`],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.view({group_by, aggregates: "median"})`,
|
||||
before_all,
|
||||
after_all,
|
||||
after,
|
||||
metadata,
|
||||
async test({ table, schema }) {
|
||||
const columns = ["Sales", "Quantity", "City"];
|
||||
const aggregates = Object.fromEntries(
|
||||
Object.keys(schema).map((x) => [x, "median"]),
|
||||
);
|
||||
|
||||
if (check_version_gte(metadata.version, "1.2.0")) {
|
||||
return await table.view({
|
||||
group_by: ["State"],
|
||||
aggregates,
|
||||
columns,
|
||||
});
|
||||
} else {
|
||||
return await table.view({
|
||||
row_pivots: ["State"],
|
||||
aggregates,
|
||||
columns,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function table_suite(perspective, metadata) {
|
||||
async function before_all() {
|
||||
try {
|
||||
const table = await perspective.table(
|
||||
new_superstore_table(metadata),
|
||||
);
|
||||
|
||||
const view = await table.view();
|
||||
const csv = await view.to_csv();
|
||||
const arrow = await view.to_arrow();
|
||||
const json = await view.to_json();
|
||||
const columns = await view.to_columns();
|
||||
if (check_version_gte(metadata.version, "2.10.9")) {
|
||||
await view.delete();
|
||||
}
|
||||
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
|
||||
return { csv, arrow, table, json, columns };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (check_version_gte(metadata.version, "2.3.0")) {
|
||||
await benchmark({
|
||||
name: `.table(arrow, {limit: 1000})`,
|
||||
before_all,
|
||||
metadata,
|
||||
async after(_, table) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
},
|
||||
async test({ arrow }) {
|
||||
return await perspective.table(arrow.slice(), { limit: 1000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await benchmark({
|
||||
name: `.table(arrow)`,
|
||||
before_all,
|
||||
metadata,
|
||||
async after(_, table) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
},
|
||||
async test({ table, arrow }) {
|
||||
return await perspective.table(arrow.slice());
|
||||
},
|
||||
});
|
||||
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await benchmark({
|
||||
name: `table.update(arrow)`,
|
||||
before_all,
|
||||
metadata,
|
||||
async before({ arrow }) {
|
||||
let table2 = await perspective.table(arrow.slice(), {
|
||||
limit: 1000,
|
||||
});
|
||||
return table2;
|
||||
},
|
||||
async after(_, table) {
|
||||
if (!check_version_gte(metadata.version, "3.4.3")) {
|
||||
// Bug with old versions of perspective segfault when you delete
|
||||
// a table with pending updates.
|
||||
await table.size();
|
||||
}
|
||||
await table.delete();
|
||||
},
|
||||
async test({ arrow }, table2) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await table2.update(arrow.slice());
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await benchmark({
|
||||
name: `.table(csv)`,
|
||||
before_all,
|
||||
metadata,
|
||||
async after(_, table) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
},
|
||||
async test({ csv }) {
|
||||
return await perspective.table(csv);
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.table(json)`,
|
||||
before_all,
|
||||
metadata,
|
||||
async after(_, table) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
},
|
||||
|
||||
async test({ table, json }) {
|
||||
return await perspective.table(json);
|
||||
},
|
||||
});
|
||||
|
||||
await benchmark({
|
||||
name: `.table(columns)`,
|
||||
before_all,
|
||||
metadata,
|
||||
async after(_, table) {
|
||||
if (check_version_gte(metadata.version, "3.0.0")) {
|
||||
await table.delete();
|
||||
}
|
||||
},
|
||||
async test({ table, columns }) {
|
||||
return await perspective.table(columns);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@perspective-dev/bench",
|
||||
"version": "2.0.1",
|
||||
"description": "Benchmark utility based on perspective",
|
||||
"private": true,
|
||||
"main": "src/js/bench.mjs",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"perspective-bench"
|
||||
],
|
||||
"bin": "perspective-bench",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective/packages/bench"
|
||||
},
|
||||
"scripts": {
|
||||
"bench_js": "node --experimental-wasm-memory64 basic_suite.mjs",
|
||||
"bench_python": "node --experimental-wasm-memory64 python_suite.mjs",
|
||||
"bench_charts": "node --experimental-wasm-memory64 charts_suite.mjs"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@perspective-dev/client": "workspace:",
|
||||
"@perspective-dev/viewer-charts": "workspace:",
|
||||
"@playwright/test": "catalog:",
|
||||
"microtime": "catalog:",
|
||||
"superstore-arrow": "catalog:",
|
||||
"zx": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"perspective-4-5-1": "npm:@perspective-dev/client@4.5.1",
|
||||
"perspective-4-5-0": "npm:@perspective-dev/client@4.5.0",
|
||||
"perspective-4-4-0": "npm:@perspective-dev/client@4.4.0",
|
||||
"perspective-4-3-0": "npm:@perspective-dev/client@4.3.0",
|
||||
"perspective-4-2-0": "npm:@perspective-dev/client@4.2.0",
|
||||
"perspective-4-1-0": "npm:@perspective-dev/client@4.1.0",
|
||||
"perspective-4-0-0": "npm:@perspective-dev/client@4.0.0",
|
||||
"perspective-3-8-0": "npm:@finos/perspective@3.8.0",
|
||||
"perspective-3-7-0": "npm:@finos/perspective@3.7.0",
|
||||
"perspective-3-6-0": "npm:@finos/perspective@3.6.0",
|
||||
"perspective-3-5-0": "npm:@finos/perspective@3.5.0",
|
||||
"perspective-3-4-0": "npm:@finos/perspective@3.4.0",
|
||||
"perspective-3-3-0": "npm:@finos/perspective@3.3.0",
|
||||
"perspective-3-2-0": "npm:@finos/perspective@3.2.0",
|
||||
"perspective-3-1-0": "npm:@finos/perspective@3.1.0",
|
||||
"perspective-3-0-0": "npm:@finos/perspective@3.0.0",
|
||||
"perspective-2-10-0": "npm:@finos/perspective@2.10.0",
|
||||
"perspective-2-9-0": "npm:@finos/perspective@2.9.0",
|
||||
"perspective-2-8-0": "npm:@finos/perspective@2.8.0",
|
||||
"perspective-2-7-0": "npm:@finos/perspective@2.7.0",
|
||||
"perspective-2-6-0": "npm:@finos/perspective@2.6.0",
|
||||
"perspective-2-5-0": "npm:@finos/perspective@2.5.0",
|
||||
"perspective-2-4-0": "npm:@finos/perspective@2.4.0",
|
||||
"perspective-2-3-0": "npm:@finos/perspective@2.3.0",
|
||||
"perspective-2-2-0": "npm:@finos/perspective@2.2.0",
|
||||
"perspective-2-1-0": "npm:@finos/perspective@2.1.0",
|
||||
"perspective-1-1-0": "npm:@finos/perspective@1.1.0",
|
||||
"perspective-1-0-0": "npm:@finos/perspective@1.0.0",
|
||||
"perspective-0-10-0": "npm:@finos/perspective@0.10.0",
|
||||
"perspective-0-9-0": "npm:@finos/perspective@0.9.0"
|
||||
},
|
||||
"deprecatedDependencies": {
|
||||
"perspective-2-0-0": "npm:@finos/perspective@2.0.0",
|
||||
"perspective-1-9-0": "npm:@finos/perspective@1.9.0",
|
||||
"perspective-1-8-0": "npm:@finos/perspective@1.8.0",
|
||||
"perspective-1-7-0": "npm:@finos/perspective@1.7.0",
|
||||
"perspective-1-6-0": "npm:@finos/perspective@1.6.0",
|
||||
"perspective-1-5-0": "npm:@finos/perspective@1.5.0",
|
||||
"perspective-1-4-0": "npm:@finos/perspective@1.4.0",
|
||||
"perspective-1-3-0": "npm:@finos/perspective@1.3.0",
|
||||
"perspective-1-2-0": "npm:@finos/perspective@1.2.0"
|
||||
}
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { registerCmd } = require("@perspective-dev/bench");
|
||||
registerCmd();
|
||||
@@ -0,0 +1,85 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as all_benchmarks from "./cross_platform_suite.mjs";
|
||||
import * as perspective_bench from "./src/js/benchmark.mjs";
|
||||
import * as puppeteer from "puppeteer";
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
import * as process from "node:process";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
|
||||
/**
|
||||
* We use the `dependencies` of this package for the benchmark candidate
|
||||
* module list, so that we only need specify the dependencies and benchmark
|
||||
* candidates in one place.
|
||||
*/
|
||||
const VERSIONS = [
|
||||
"@perspective-dev/client",
|
||||
"perspective-3-0-0",
|
||||
"perspective-2-10-0",
|
||||
];
|
||||
|
||||
perspective_bench.suite(
|
||||
[...VERSIONS],
|
||||
path.join(__dirname, "dist/benchmark-js.arrow"),
|
||||
async function (path, version_idx) {
|
||||
let client, metadata;
|
||||
console.log(path);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
protocolTimeout: 100_000_000,
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto("http://localhost:8081/empty.html");
|
||||
|
||||
async function test_suite(suite) {
|
||||
const items = await page.evaluate(
|
||||
async ([version, suite, version_idx]) => {
|
||||
const { default: perspective } = await import(
|
||||
`/tools/bench/node_modules/${version}/dist/esm/perspective.inline.js`
|
||||
);
|
||||
const benchmarks = await import(
|
||||
"/tools/bench/cross_platform_suite.mjs"
|
||||
);
|
||||
|
||||
const metadata = {
|
||||
version,
|
||||
version_idx,
|
||||
};
|
||||
const total = [];
|
||||
window.__SEND__ = (x) => {
|
||||
total.push(x);
|
||||
};
|
||||
|
||||
const client = await perspective.worker();
|
||||
await benchmarks[suite](client, metadata);
|
||||
return total;
|
||||
},
|
||||
[path, suite, version_idx],
|
||||
);
|
||||
|
||||
for (const { obs_records, stats } of items) {
|
||||
process.send({ obs_records, stats });
|
||||
}
|
||||
}
|
||||
|
||||
await test_suite("table_suite");
|
||||
await test_suite("view_suite");
|
||||
await test_suite("to_data_suite");
|
||||
await test_suite("join_suite");
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/**
|
||||
* @module python_suite
|
||||
*
|
||||
* Run the Python benchmarks against hte Node.js client.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
|
||||
import * as python from "./src/js/servers/python.mjs";
|
||||
import * as all_benchmarks from "./cross_platform_suite.mjs";
|
||||
import * as perspective_bench from "./src/js/benchmark.mjs";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
|
||||
const CLIENT_VERSION = {
|
||||
master: "@perspective-dev/client",
|
||||
"4.5.1": "perspective-4-5-1",
|
||||
"4.5.0": "perspective-4-5-0",
|
||||
"4.4.0": "perspective-4-4-0",
|
||||
"4.3.0": "perspective-4-3-0",
|
||||
"4.2.0": "perspective-4-2-0",
|
||||
"4.1.0": "perspective-4-1-0",
|
||||
"4.0.0": "perspective-4-0-0",
|
||||
"3.8.0": "perspective-3-8-0",
|
||||
"3.6.0": "perspective-3-6-0",
|
||||
"3.5.0": "perspective-3-5-0",
|
||||
"3.4.0": "perspective-3-4-0",
|
||||
"3.3.0": "perspective-3-3-0",
|
||||
"3.2.0": "perspective-3-2-0",
|
||||
"3.1.0": "perspective-3-1-0",
|
||||
"3.0.3": "perspective-3-0-0",
|
||||
"2.10.1": "perspective-2-10-0",
|
||||
"2.9.0": "perspective-2-9-0",
|
||||
"2.8.0": "perspective-2-8-0",
|
||||
"2.7.0": "perspective-2-7-0",
|
||||
"2.6.0": "perspective-2-6-0",
|
||||
"2.5.0": "perspective-2-5-0",
|
||||
"2.4.0": "perspective-2-4-0",
|
||||
"2.3.2": "perspective-2-3-0",
|
||||
"2.3.1": "perspective-2-3-0",
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.join(__dirname, "./dist"), { recursive: true });
|
||||
|
||||
perspective_bench.suite(
|
||||
[...Object.keys(CLIENT_VERSION)],
|
||||
path.join(__dirname, "dist/benchmark-python.arrow"),
|
||||
async function (version, version_idx) {
|
||||
console.log(version);
|
||||
const { default: perspective } = await import(CLIENT_VERSION[version]);
|
||||
const client = await perspective.websocket(
|
||||
"ws://127.0.0.1:8082/websocket",
|
||||
{ maxPayload: 1024 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
const metadata = { version, version_idx };
|
||||
globalThis.__SEND__ = (x) => process.send(x);
|
||||
await all_benchmarks.table_suite(client, metadata);
|
||||
await all_benchmarks.view_suite(client, metadata);
|
||||
await all_benchmarks.to_data_suite(client, metadata);
|
||||
await all_benchmarks.join_suite(client, metadata);
|
||||
},
|
||||
python.start,
|
||||
python.stop,
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const fs = require("fs");
|
||||
import * as all_benchmarks from "./cross_platform_suite.mjs";
|
||||
import * as perspective_bench from "./src/js/benchmark.mjs";
|
||||
|
||||
/**
|
||||
* We use the `dependencies` of this package for the benchmark candidate
|
||||
* module list, so that we only need specify the dependencies and benchmark
|
||||
* candidates in one place.
|
||||
*/
|
||||
const VERSIONS = Object.keys(
|
||||
JSON.parse(fs.readFileSync(require.resolve(`./package.json`))).dependencies,
|
||||
);
|
||||
|
||||
perspective_bench.suite(
|
||||
// ["@perspective-dev/client", ...VERSIONS],
|
||||
[...VERSIONS],
|
||||
async function (path, version_idx) {
|
||||
let perspective = await import(path);
|
||||
perspective = perspective.default || perspective;
|
||||
const pkg_json = JSON.parse(
|
||||
fs.readFileSync(require.resolve(`${path}/package.json`)),
|
||||
);
|
||||
|
||||
let version = pkg_json.version;
|
||||
console.log(`${path} (${pkg_json.name}@${version})`);
|
||||
if (version_idx === 0) {
|
||||
version = `${version} (master)`;
|
||||
}
|
||||
|
||||
const metadata = { version, version_idx };
|
||||
await all_benchmarks.table_suite(perspective, metadata);
|
||||
// await all_benchmarks.view_suite(perspective, metadata);
|
||||
// await all_benchmarks.to_data_suite(perspective, metadata);
|
||||
},
|
||||
async function (path) {
|
||||
let psp = await import(path);
|
||||
psp = psp.default || psp;
|
||||
// const port = Math.floor(Math.random() * 1000) + 1000;
|
||||
const server = new psp.WebSocketServer({
|
||||
port: 8080,
|
||||
});
|
||||
|
||||
function buffer_to_arraybuffer(buffer) {
|
||||
return new Int8Array(
|
||||
buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const table = await psp.table(
|
||||
buffer_to_arraybuffer(
|
||||
fs.readFileSync(
|
||||
"../../node_modules/superstore-arrow/superstore.arrow",
|
||||
),
|
||||
).buffer,
|
||||
{ name: "superstore" },
|
||||
);
|
||||
|
||||
// // Legacy compat
|
||||
if (server.host_table) {
|
||||
server.host_table("superstore", table);
|
||||
}
|
||||
|
||||
return server;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// Synthetic dataset for chart benchmarking. The columns are
|
||||
// chosen to exercise the same chart shapes as the real data
|
||||
// (group_by / split_by / two-numeric-axes), without depending
|
||||
// on superstore.csv parsing time.
|
||||
const ROW_COUNT = 10_000;
|
||||
const CATEGORIES = ["Furniture", "Office Supplies", "Technology", "Apparel", "Books"];
|
||||
|
||||
const REGIONS = ["North", "South", "East", "West", "Central"];
|
||||
const SUBCATS = ["Phones", "Chairs", "Tables", "Storage", "Binders", "Paper", "Art", "Accessories", "Copiers", "Machines"];
|
||||
|
||||
function build_data(n) {
|
||||
const Category = new Array(n);
|
||||
const Region = new Array(n);
|
||||
const SubCategory = new Array(n);
|
||||
const Sales = new Array(n);
|
||||
const Profit = new Array(n);
|
||||
const Quantity = new Array(n);
|
||||
const Discount = new Array(n);
|
||||
const Open = new Array(n);
|
||||
const High = new Array(n);
|
||||
const Low = new Array(n);
|
||||
const Close = new Array(n);
|
||||
|
||||
// Deterministic pseudo-random so iterations are comparable.
|
||||
let seed = 0x9e3779b9;
|
||||
const rand = () => {
|
||||
seed |= 0;
|
||||
seed = (seed + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
Category[i] = CATEGORIES[i % CATEGORIES.length];
|
||||
Region[i] = REGIONS[(i * 7) % REGIONS.length];
|
||||
SubCategory[i] = SUBCATS[(i * 13) % SUBCATS.length];
|
||||
const base = 100 + rand() * 900;
|
||||
Sales[i] = base;
|
||||
Profit[i] = (rand() - 0.4) * base * 0.3;
|
||||
Quantity[i] = 1 + Math.floor(rand() * 20);
|
||||
Discount[i] = Math.round(rand() * 100) / 100;
|
||||
const o = base;
|
||||
const c = base + (rand() - 0.5) * base * 0.05;
|
||||
Open[i] = o;
|
||||
Close[i] = c;
|
||||
High[i] = Math.max(o, c) + rand() * base * 0.02;
|
||||
Low[i] = Math.min(o, c) - rand() * base * 0.02;
|
||||
}
|
||||
|
||||
return {
|
||||
Category,
|
||||
Region,
|
||||
"Sub-Category": SubCategory,
|
||||
Sales,
|
||||
Profit,
|
||||
Quantity,
|
||||
Discount,
|
||||
Open,
|
||||
High,
|
||||
Low,
|
||||
Close,
|
||||
};
|
||||
}
|
||||
|
||||
await customElements.whenDefined("perspective-viewer");
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(build_data(ROW_COUNT), {
|
||||
name: "charts-bench",
|
||||
});
|
||||
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
const el = document.getElementsByTagName("perspective-viewer")[0];
|
||||
el.addEventListener("perspective-config-update", async () => {
|
||||
const config = await el.save();
|
||||
delete config["table"];
|
||||
localStorage.setItem("layout", JSON.stringify(config));
|
||||
});
|
||||
|
||||
const websocket = await perspective.websocket("ws://localhost:8081/subscribe");
|
||||
const table = await websocket.open_table("benchmarks");
|
||||
const worker = await perspective.worker();
|
||||
const table2 = await worker.table(await table.view());
|
||||
el.load(table2);
|
||||
el.restore(JSON.parse(localStorage.getItem("layout")));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,359 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/**
|
||||
* @module perspective_bench
|
||||
* The main entrypoint of `perspective_bench` is a CLI node.js application which
|
||||
* may star
|
||||
*/
|
||||
|
||||
const MAX_ITERATIONS = 100;
|
||||
const MIN_ITERATIONS = 5;
|
||||
const WARM_UP_ITERATIONS = 10;
|
||||
|
||||
/**
|
||||
* Utility function to push/filter.
|
||||
* @param {*} a
|
||||
* @param {*} x
|
||||
*/
|
||||
function push_if(a, x) {
|
||||
if (x !== undefined) {
|
||||
a.push(x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate avg fora key.
|
||||
* @param {*} a
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
function avg(a, key) {
|
||||
return a.map((x) => x[key]).reduce((x, y) => x + y, 0) / a.length / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate stddev for a key.
|
||||
* @param {*} array
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
function stddev(array, key) {
|
||||
const n = array.length;
|
||||
const mean = array.reduce((a, b) => a + b[key] / 1000, 0) / n;
|
||||
return Math.sqrt(
|
||||
array
|
||||
.map((x) => Math.pow(x[key] / 1000 - mean, 2))
|
||||
.reduce((a, b) => a + b) / n,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate outliers
|
||||
* @param {*} someArray
|
||||
* @returns
|
||||
*/
|
||||
function markOutliers(someArray) {
|
||||
var values = someArray.concat();
|
||||
values.sort(function (a, b) {
|
||||
return a.real_time - b.real_time;
|
||||
});
|
||||
|
||||
var q1 = values[Math.floor(values.length / 4)].real_time;
|
||||
var q3 = values[Math.ceil(values.length * (3 / 4))].real_time;
|
||||
var iqr = q3 - q1;
|
||||
var maxValue = q3 + iqr * 1.5;
|
||||
var minValue = q1 - iqr * 1.5;
|
||||
return someArray.map(function (x) {
|
||||
x.outlier = !(x.real_time <= maxValue && x.real_time >= minValue);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list to arrow and write it to disk. `@perspective-dev/client` is
|
||||
* imported in this scope to prevent interpreter-wide side effects of the
|
||||
* library from impacting forked processes, based on an observation that some
|
||||
* runs inline had anomalies across many observations that couldn't be explained
|
||||
* by contemporary system load.
|
||||
* @param {Array} obs_records an array of records to persist
|
||||
*/
|
||||
async function persist_to_arrow(benchmarks_table, out_path = "") {
|
||||
const fs = await import("node:fs");
|
||||
const view = await benchmarks_table.view();
|
||||
const arrow = await view.to_arrow();
|
||||
fs.writeFileSync(out_path, Buffer.from(arrow), "binary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single benchmark case, reporting the runtime
|
||||
* @param {*} param0
|
||||
* @returns An observation record with timing and metadata fields for a single
|
||||
* iteration of this case.
|
||||
*/
|
||||
async function benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
}) {
|
||||
const args2 = args.slice();
|
||||
push_if(args2, await before?.(...args2));
|
||||
if (typeof process !== "undefined") {
|
||||
const { default: process } = await import("node:process");
|
||||
const { default: microtime } = await import("microtime");
|
||||
globalThis.gc(false);
|
||||
await new Promise((x) => setTimeout(x, 0));
|
||||
const start_time = microtime.now();
|
||||
const start_cpu = process.cpuUsage();
|
||||
const x = await test(...args2);
|
||||
const end = process.cpuUsage(start_cpu);
|
||||
const end_time = microtime.now() - start_time;
|
||||
push_if(args2, x);
|
||||
await after?.(...args2);
|
||||
return {
|
||||
cpu_time: end.user + end.system,
|
||||
real_time: end_time,
|
||||
user_time: end.user,
|
||||
system_time: end.system,
|
||||
benchmark,
|
||||
iteration: i,
|
||||
...structuredClone(metadata),
|
||||
};
|
||||
} else {
|
||||
const start_time = performance.now();
|
||||
const x = await test(...args2);
|
||||
const end_time = performance.now() - start_time;
|
||||
push_if(args2, x);
|
||||
await after?.(...args2);
|
||||
return {
|
||||
real_time: end_time * 1000,
|
||||
benchmark,
|
||||
iteration: i,
|
||||
...structuredClone(metadata),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the benchmarks in a forked process and colelct the observations.
|
||||
* @param {{version: string, i: number}} version the versions spec to send to
|
||||
* the child process.
|
||||
* @returns an array of observation records for this version.
|
||||
*/
|
||||
async function benchmark_node_version(version, benchmarks_table) {
|
||||
const cp = await import("node:child_process");
|
||||
const process = await import("node:process");
|
||||
const path = await import("node:path");
|
||||
const suite_path = path.join(process.argv[1]);
|
||||
let stats = [];
|
||||
const worker = cp.fork(suite_path, {
|
||||
execArgv: ["--expose-gc", "--experimental-wasm-memory64"],
|
||||
env: { BENCH_FLAG: "1" },
|
||||
});
|
||||
|
||||
let cont;
|
||||
worker.on("message", (details) => {
|
||||
if (details.finished) {
|
||||
cont();
|
||||
} else {
|
||||
let msg = " - ";
|
||||
if (!isNaN(details.stats.filtered_avg_cpu)) {
|
||||
msg += `${details.stats.filtered_avg_cpu}ms +/-${details.stats.stddev_percent}% (CPU), `;
|
||||
}
|
||||
msg += `${details.stats.filtered_avg_time}ms (Real) ${details.stats.non_outliers}/${details.stats.iterations} iterations - ${details.stats.benchmark}`;
|
||||
console.log(msg);
|
||||
benchmarks_table.update(details.obs_records);
|
||||
stats.push(details.stats);
|
||||
}
|
||||
});
|
||||
|
||||
worker.send({
|
||||
...version,
|
||||
stats,
|
||||
});
|
||||
|
||||
await new Promise((r) => {
|
||||
cont = r;
|
||||
});
|
||||
|
||||
worker.kill();
|
||||
return { stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a benchmark for a test case.
|
||||
* @param {*} param0
|
||||
*/
|
||||
export async function benchmark({
|
||||
name: benchmark,
|
||||
before,
|
||||
before_all,
|
||||
test,
|
||||
after,
|
||||
after_all,
|
||||
args = [],
|
||||
metadata = {},
|
||||
warm_up_iterations = WARM_UP_ITERATIONS,
|
||||
max_iterations = MAX_ITERATIONS,
|
||||
min_iterations = MIN_ITERATIONS,
|
||||
max_time = 3_000,
|
||||
} = {}) {
|
||||
let obs_records = [];
|
||||
push_if(args, await before_all?.(...args));
|
||||
const start_time = performance.now();
|
||||
for (let i = 0; i < warm_up_iterations; i++) {
|
||||
await benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
});
|
||||
}
|
||||
|
||||
let i;
|
||||
for (i = 0; i < max_iterations; i++) {
|
||||
if (performance.now() - start_time > max_time && i >= min_iterations) {
|
||||
break;
|
||||
}
|
||||
|
||||
obs_records.push(
|
||||
await benchmark_case({
|
||||
args,
|
||||
before,
|
||||
test,
|
||||
benchmark,
|
||||
metadata,
|
||||
after,
|
||||
i,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
obs_records = markOutliers(obs_records);
|
||||
const filtered = obs_records.filter((x) => !x.outlier);
|
||||
const n_outliers = obs_records.length - filtered.length;
|
||||
const filtered_avg_cpu = avg(filtered, "cpu_time");
|
||||
const filtered_avg_time = avg(filtered, "real_time").toFixed(3);
|
||||
const stddev_cpu = stddev(filtered, "cpu_time").toFixed(3);
|
||||
const stats = {
|
||||
benchmark,
|
||||
metadata,
|
||||
filtered_avg_cpu: filtered_avg_cpu.toFixed(3),
|
||||
stddev_percent: ((stddev_cpu / filtered_avg_cpu) * 100).toFixed(1),
|
||||
stddev_cpu,
|
||||
filtered_avg_time,
|
||||
hr_time: avg(filtered, "hr_time"),
|
||||
iterations: i,
|
||||
non_outliers: i - n_outliers,
|
||||
};
|
||||
|
||||
await after_all?.(...args);
|
||||
globalThis.__SEND__({ obs_records, stats });
|
||||
}
|
||||
|
||||
function buffer_to_arraybuffer(buffer) {
|
||||
return new Int8Array(
|
||||
buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Host a Perspective server to view the live benchmark data as it accumulates.
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
async function start_server({
|
||||
cwd_static_file_handler,
|
||||
make_session,
|
||||
WebSocketServer,
|
||||
}) {
|
||||
return new WebSocketServer({ assets: ["src/html/", "../.."], port: 8081 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a suite of benchmarks to run against a set of packages with
|
||||
* similar APIs.
|
||||
* @param {*} versions
|
||||
* @param {*} run_version_callback
|
||||
*/
|
||||
export async function suite(
|
||||
versions,
|
||||
out_path,
|
||||
run_version_callback,
|
||||
start_server_callback,
|
||||
stop_server_callback,
|
||||
) {
|
||||
if (!!process.env.BENCH_FLAG) {
|
||||
const { default: process } = await import("node:process");
|
||||
process.on("message", async function bench_all({ path, i }) {
|
||||
await run_version_callback(path, i);
|
||||
process.send({ finished: true });
|
||||
});
|
||||
} else {
|
||||
const psp = await import("@perspective-dev/client");
|
||||
const benchmarks_table = await psp.default.table(
|
||||
{
|
||||
version: "string",
|
||||
cpu_time: "float",
|
||||
system_time: "float",
|
||||
user_time: "float",
|
||||
real_time: "float",
|
||||
version_idx: "integer",
|
||||
benchmark: "string",
|
||||
outlier: "boolean",
|
||||
},
|
||||
{ name: "benchmarks" },
|
||||
);
|
||||
|
||||
const app = await start_server(psp);
|
||||
|
||||
for (let i = 0; i < versions.length; i++) {
|
||||
let s;
|
||||
if (start_server_callback) {
|
||||
s = await start_server_callback(versions[i], i);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
benchmark_node_version(
|
||||
{ path: versions[i], i },
|
||||
benchmarks_table,
|
||||
),
|
||||
// benchmark_puppeteer_version(
|
||||
// { path: versions[i], i },
|
||||
// benchmarks_table
|
||||
// ),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
// benchmark_version({ path: versions[i], i }, benchmarks_table),
|
||||
]);
|
||||
|
||||
await persist_to_arrow(benchmarks_table, out_path);
|
||||
|
||||
if (stop_server_callback) {
|
||||
await stop_server_callback(s, versions[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
await app.close();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
let SERVER;
|
||||
|
||||
/**
|
||||
* Create a Python `tornado` server, deleting and re-creating the virtual env
|
||||
* if necessary.
|
||||
*/
|
||||
export async function start(path) {
|
||||
const ac = new AbortController();
|
||||
let proc;
|
||||
await $`rm -rf benchmark_venv`;
|
||||
|
||||
if (path !== "master") {
|
||||
await $`python3 -m venv benchmark_venv`;
|
||||
const $$ = $({
|
||||
ac,
|
||||
prefix: "source benchmark_venv/bin/activate && ",
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
await $$`pip3 uninstall "perspective-python" -y`;
|
||||
await $$`pip3 install tornado "perspective-python==${path}"`;
|
||||
proc = $$`python3 src/python/server.py`;
|
||||
} else {
|
||||
proc = $`python3 src/python/server.py`;
|
||||
}
|
||||
|
||||
// Await server start
|
||||
let [resolve, sentinel] = (() => {
|
||||
let resolve;
|
||||
const sentinel = new Promise((x) => {
|
||||
resolve = x;
|
||||
});
|
||||
|
||||
return [resolve, sentinel];
|
||||
})();
|
||||
|
||||
async function await_log(pipe) {
|
||||
for await (const _ of proc[pipe]) {
|
||||
resolve?.();
|
||||
resolve = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
await_log("stdout");
|
||||
await_log("stderr");
|
||||
|
||||
await sentinel;
|
||||
SERVER = { ac, proc };
|
||||
return SERVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a Python server.
|
||||
* @param {*} server
|
||||
*/
|
||||
export async function stop(server) {
|
||||
server.proc.kill();
|
||||
try {
|
||||
await server.proc;
|
||||
} catch (e) {}
|
||||
|
||||
await server.ac.abort();
|
||||
SERVER = undefined;
|
||||
}
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
try {
|
||||
SERVER?.proc?.kill();
|
||||
await SERVER?.proc;
|
||||
await SERVER?.ac?.abort();
|
||||
} catch (e) {}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/**
|
||||
* Load a file as an `ArrayBuffer`, which is useful for loading Apache Arrow
|
||||
* Feather files.
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
async function get_buffer(path) {
|
||||
if (typeof window !== "undefined") {
|
||||
const resp = await fetch(
|
||||
"http://localhost:8080/node_modules/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
return await resp.arrayBuffer();
|
||||
} else {
|
||||
const fs = await import("node:fs");
|
||||
const { createRequire } = await import("node:module");
|
||||
const _require = createRequire(import.meta.url);
|
||||
return fs.readFileSync(_require.resolve(path)).buffer;
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERSTORE_ARROW = await get_buffer("superstore-arrow/superstore.arrow");
|
||||
const SUPERSTORE_FEATHER = await get_buffer(
|
||||
"superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
/**
|
||||
* Load the Superstore example data set as either a Feather (LZ4) or
|
||||
* uncompressed `Arrow`, depending on whether Perspective supports Feather.
|
||||
* @param {*} metadata
|
||||
* @returns
|
||||
*/
|
||||
export function new_superstore_table(metadata) {
|
||||
if (check_version_gte(metadata.version, "2.5.0")) {
|
||||
return SUPERSTORE_FEATHER.slice();
|
||||
} else {
|
||||
return SUPERSTORE_ARROW.slice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a version string e.g. "v1.2.3" is greater or equal to another
|
||||
* version string, which must be of the same length/have the same number of
|
||||
* minor version levels.
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @returns
|
||||
*/
|
||||
export function check_version_gte(a, b) {
|
||||
a = a.split(".").map((x) => parseInt(x));
|
||||
b = b.split(".").map((x) => parseInt(x));
|
||||
|
||||
if (a.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const i in a) {
|
||||
if (a[i] > b[i]) {
|
||||
return true;
|
||||
} else if (a[i] < b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
# This module handles historic versions of `perspective-python`'s API.
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
||||
# import concurrent.futures
|
||||
import threading
|
||||
import tornado
|
||||
import perspective
|
||||
import perspective.handlers.tornado
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
file_path = os.path.join(
|
||||
here, "..", "..", "node_modules", "superstore-arrow", "superstore.lz4.arrow"
|
||||
)
|
||||
|
||||
if perspective.__version__.startswith("2"):
|
||||
from perspective import PerspectiveManager, PerspectiveTornadoHandler
|
||||
|
||||
def perspective_thread(
|
||||
manager,
|
||||
):
|
||||
psp_loop = tornado.ioloop.IOLoop()
|
||||
# [x, y, z] = map(int, perspective.__version__.split("."))
|
||||
# if x > 2 or (x > 1 and y > 2):
|
||||
# # with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
# # manager.set_loop_callback(psp_loop.run_in_executor, executor)
|
||||
# psp_loop.start()
|
||||
# else:
|
||||
# manager.set_loop_callback(psp_loop.add_callback)
|
||||
psp_loop.start()
|
||||
|
||||
def make_app():
|
||||
manager = PerspectiveManager()
|
||||
thread = threading.Thread(target=perspective_thread, args=(manager,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/websocket",
|
||||
PerspectiveTornadoHandler,
|
||||
{"manager": manager, "check_origin": True},
|
||||
),
|
||||
(
|
||||
r"/node_modules/(.*)",
|
||||
tornado.web.StaticFileHandler,
|
||||
{"path": "../../node_modules/"},
|
||||
),
|
||||
],
|
||||
websocket_max_message_size=1000 * 1024 * 1024,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = make_app()
|
||||
app.listen(8082)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
print("Listening on 8082", flush=True)
|
||||
loop.start()
|
||||
|
||||
else:
|
||||
|
||||
def make_app(perspective_server):
|
||||
return tornado.web.Application(
|
||||
[
|
||||
(
|
||||
r"/websocket",
|
||||
perspective.handlers.tornado.PerspectiveTornadoHandler,
|
||||
{"perspective_server": perspective_server},
|
||||
),
|
||||
(
|
||||
r"/node_modules/(.*)",
|
||||
tornado.web.StaticFileHandler,
|
||||
{"path": "../../node_modules/"},
|
||||
),
|
||||
],
|
||||
websocket_max_message_size=1000 * 1024 * 1024,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
perspective_server = perspective.Server()
|
||||
app = make_app(perspective_server)
|
||||
app.listen(8082)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
client = perspective_server.new_local_client()
|
||||
print("Listening on 8082", flush=True)
|
||||
loop.start()
|
||||
@@ -0,0 +1,30 @@
|
||||
# `@perspective-dev/esbuild-plugin`
|
||||
|
||||
Applications bundled with `esbuild` can make use of the
|
||||
`@perspective-dev/esbuild-plugin` module. A full example can be found in the
|
||||
repo under
|
||||
[`examples/esbuild-example`](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-example).
|
||||
|
||||
```javascript
|
||||
const esbuild = require("esbuild");
|
||||
const { PerspectiveEsbuildPlugin } = require("@perspective-dev/esbuild-plugin");
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ["src/index.js"],
|
||||
plugins: [PerspectiveEsbuildPlugin()],
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
loader: {
|
||||
".ttf": "file",
|
||||
".wasm": "file",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When bundling via `esbuild`, you must also:
|
||||
|
||||
- Use the `type="module"` attribute in your app's `<script>` tag, as this build
|
||||
mode is only supported via ES modules.
|
||||
- Use the direct imports for the `esm` versions Perspective, specifically
|
||||
`@perspective-dev/client/dist/esm/perspective.js` and
|
||||
`@perspective-dev/viewer/dist/esm/perspective-viewer.js`
|
||||
@@ -0,0 +1,51 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const get_banner = (deps) => ({
|
||||
js: `
|
||||
define(
|
||||
${JSON.stringify(deps)},
|
||||
function (...args) {
|
||||
const exports = {};
|
||||
const module = {exports};
|
||||
const define = undefined;
|
||||
const require = (x) => args[${JSON.stringify(deps)}.indexOf(x)];
|
||||
`.trimStart(),
|
||||
});
|
||||
|
||||
const get_footer = () => ({
|
||||
js: `
|
||||
return exports;
|
||||
});`,
|
||||
});
|
||||
|
||||
exports.AMDLoader = function AMDLoader(deps) {
|
||||
function setup(build) {
|
||||
const options = build.initialOptions;
|
||||
if (deps === undefined) {
|
||||
console.warn("Setting `deps` to [`@jupyter-widgets/base`]");
|
||||
deps = [`@jupyter-widgets/base`];
|
||||
}
|
||||
|
||||
options.banner = get_banner(deps);
|
||||
options.footer = get_footer();
|
||||
if (options.format !== "cjs" && options.format !== undefined) {
|
||||
console.warn("Setting options to `cjs` for `AMDLoader`");
|
||||
options.format = "cjs";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "amd",
|
||||
setup,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const esbuild = require("esbuild");
|
||||
|
||||
const CUTOFF_PERCENT = 0.02;
|
||||
|
||||
const DEFAULT_BUILD = {
|
||||
target: ["es2022"],
|
||||
bundle: true,
|
||||
minify: !process.env.PSP_DEBUG,
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
entryNames: "[name]",
|
||||
chunkNames: "[name]",
|
||||
assetNames: "[name]",
|
||||
};
|
||||
|
||||
exports.build = async function build(config) {
|
||||
const result = await esbuild.build({
|
||||
...DEFAULT_BUILD,
|
||||
...config,
|
||||
});
|
||||
|
||||
if (result.metafile) {
|
||||
for (const output of Object.keys(result.metafile.outputs)) {
|
||||
const { inputs, bytes } = result.metafile.outputs[output];
|
||||
for (const input of Object.keys(inputs)) {
|
||||
if (inputs[input].bytesInOutput / bytes < CUTOFF_PERCENT) {
|
||||
delete inputs[input];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = await esbuild.analyzeMetafile(result.metafile, {
|
||||
color: true,
|
||||
});
|
||||
|
||||
console.log(text);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
exports.NodeModulesExternal = function NodeModulesExternal(whitelist) {
|
||||
function setup(build) {
|
||||
build.onResolve({ filter: /^[A-Za-z0-9\@]/ }, (args) => {
|
||||
if (!whitelist || !args.path.startsWith(whitelist)) {
|
||||
return {
|
||||
path: args.path,
|
||||
external: true,
|
||||
namespace: "skip-node-modules",
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "node_modules_external",
|
||||
setup,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const { WasmPlugin } = require("./wasm.js");
|
||||
const { WorkerPlugin } = require("./worker.js");
|
||||
|
||||
exports.PerspectiveEsbuildPlugin = function PerspectiveEsbuildPlugin(
|
||||
options = {},
|
||||
) {
|
||||
// const wasm_plugin = WasmPlugin(
|
||||
// !!options.wasm?.inline,
|
||||
// !options.wasm?.webpack_hack
|
||||
// );
|
||||
|
||||
// `inline` (default true) keeps the legacy single-bundle behavior
|
||||
// — worker source embedded as a string and wrapped in a Blob URL
|
||||
// at runtime. Pass `worker: { inline: false }` to emit the worker
|
||||
// as a sibling file (preserves source maps in DevTools).
|
||||
const worker_plugin = WorkerPlugin({
|
||||
inline: options.worker?.inline !== false,
|
||||
});
|
||||
|
||||
function setup(build) {
|
||||
// if (options.wasm !== false) {
|
||||
// wasm_plugin.setup(build);
|
||||
// }
|
||||
|
||||
if (options.worker !== false) {
|
||||
worker_plugin.setup(build);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "@perspective-dev/esbuild-plugin",
|
||||
setup,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@perspective-dev/esbuild-plugin",
|
||||
"version": "4.5.2",
|
||||
"description": "esbuild plugin for Perspective",
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"esbuild": "catalog:"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective/packages/build"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
exports.ResolvePlugin = function ResolvePlugin(options) {
|
||||
return {
|
||||
name: "resolve",
|
||||
setup: (build) => {
|
||||
for (const moduleName of Object.keys(options)) {
|
||||
const moduleTarget = options[moduleName];
|
||||
const filter = new RegExp("^" + moduleName + "$");
|
||||
|
||||
build.onResolve({ filter }, async (args) => {
|
||||
if (args.resolveDir === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "resolve",
|
||||
pluginData: {
|
||||
resolveDir: args.resolveDir,
|
||||
moduleName,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "resolve" }, async (args) => {
|
||||
const importerCode = `
|
||||
export * from '${args.path.replace(
|
||||
args.pluginData.moduleName,
|
||||
moduleTarget
|
||||
)}';
|
||||
export { default } from '${args.path.replace(
|
||||
args.pluginData.moduleName,
|
||||
moduleTarget
|
||||
)}';
|
||||
`;
|
||||
return {
|
||||
contents: importerCode,
|
||||
resolveDir: args.pluginData.resolveDir,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
exports.WasmPlugin = function WasmPlugin(inline, webpack_hack) {
|
||||
function setup(build) {
|
||||
const options = build.initialOptions;
|
||||
options.metafile = true;
|
||||
const KEYSET = [];
|
||||
|
||||
build.onResolve({ filter: /\.wasm$/ }, (args) => {
|
||||
if (
|
||||
args.namespace === "wasm-stub" ||
|
||||
args.namespace === "wasm-inline"
|
||||
) {
|
||||
let entryPoint = args.path;
|
||||
if (args.path.startsWith(".")) {
|
||||
entryPoint = path.join(
|
||||
args.pluginData.resolveDir,
|
||||
entryPoint
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
path: entryPoint,
|
||||
namespace: "wasm",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: inline ? "wasm-inline" : "wasm-stub",
|
||||
pluginData: {
|
||||
resolveDir: args.resolveDir,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad(
|
||||
{ filter: /.*/, namespace: "wasm-inline" },
|
||||
async (args) => ({
|
||||
pluginData: args.pluginData,
|
||||
contents: `
|
||||
import wasm from ${JSON.stringify(args.path)};
|
||||
export default function() {
|
||||
return Promise.resolve(wasm.buffer);
|
||||
};
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: "wasm-stub" }, async (args) => {
|
||||
const key = `__PSP_INLINE_WASM__${Math.random()
|
||||
.toString()
|
||||
.slice(2)}__`;
|
||||
KEYSET.push(key);
|
||||
const url = webpack_hack ? `${key}(wasm)` : `wasm`;
|
||||
|
||||
return {
|
||||
pluginData: args.pluginData,
|
||||
contents: `
|
||||
import wasm from ${JSON.stringify(args.path)};
|
||||
export default function() {
|
||||
return fetch(new URL(${url}, import.meta.url));
|
||||
};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: "wasm" }, async (args) => {
|
||||
const path = require.resolve(args.path);
|
||||
const contents = await fs.promises.readFile(path);
|
||||
return {
|
||||
pluginData: args.pluginData,
|
||||
contents,
|
||||
loader: inline ? "binary" : "file",
|
||||
};
|
||||
});
|
||||
|
||||
build.onEnd(({ metafile }) => {
|
||||
if (webpack_hack) {
|
||||
for (const file of Object.keys(metafile.outputs)) {
|
||||
if (file.endsWith(".js")) {
|
||||
let contents = fs.readFileSync(file).toString();
|
||||
let updated = false;
|
||||
for (const key of KEYSET) {
|
||||
const symbol = contents.match(
|
||||
new RegExp(`${key}\\(([a-zA-Z0-9_\$]+?)\\)`)
|
||||
);
|
||||
|
||||
if (symbol?.[1]) {
|
||||
updated = true;
|
||||
const escapedSymbol = symbol[1].replace(
|
||||
/\$/g,
|
||||
"\\$"
|
||||
);
|
||||
|
||||
const filename = contents.match(
|
||||
new RegExp(
|
||||
`(?<![a-zA-Z0-9_\$])${escapedSymbol}\\s*?=\\s*?\\"(.+?)\\"`
|
||||
)
|
||||
);
|
||||
|
||||
contents = contents.replace(
|
||||
new RegExp(
|
||||
`${key}\\(([a-zA-Z0-9_\$]+?)\\)`,
|
||||
"g"
|
||||
),
|
||||
`"${filename[1]}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
fs.writeFileSync(file, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "wasm",
|
||||
setup,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,314 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const esbuild = require("esbuild");
|
||||
|
||||
/**
|
||||
* Esbuild plugin that compiles a `.worker.js` import into a
|
||||
* URL-yielding module. The shim `getWorkerURL()` returns a string
|
||||
* that's usable by both `new Worker(url, { type: "module" })` *and*
|
||||
* `await import(url)` — the in-process renderer path uses the latter
|
||||
* so the chart code lives in the worker bundle exactly once.
|
||||
*
|
||||
* Two output modes:
|
||||
* - `inline: true` (default, prod). The subbuild's bytes are
|
||||
* embedded as a JS string in the parent bundle. `getWorkerURL()`
|
||||
* creates a Blob URL at runtime, cached so both consumers share
|
||||
* the same URL (and the same module instance via dynamic-import
|
||||
* dedup).
|
||||
* - `inline: false` (debug). The subbuild's output is written to
|
||||
* the parent's `outdir` next to the main bundle, with source
|
||||
* maps. `getWorkerURL()` resolves `import.meta.url` to that file,
|
||||
* so DevTools can show real source paths and breakpoints work.
|
||||
*/
|
||||
exports.WorkerPlugin = function WorkerPlugin(options = {}) {
|
||||
/**
|
||||
* Optional esbuild plugins to apply to the worker sub-build (e.g.
|
||||
* `GlslMinify`, `LightningCssMinify`). Use when the worker entry
|
||||
* imports the same custom-loader file types as the outer bundle.
|
||||
*/
|
||||
const subbuildPlugins = options.plugins || [];
|
||||
|
||||
/**
|
||||
* Optional `loader` map for the worker sub-build (e.g.
|
||||
* `{ ".glsl": "text", ".css": "text" }`).
|
||||
*/
|
||||
const subbuildLoader = options.loader || {};
|
||||
|
||||
/**
|
||||
* `false` to emit the worker bundle as a real file alongside the
|
||||
* parent bundle (debug builds — preserves source maps + real
|
||||
* paths in DevTools). Defaults to inline-Blob mode for prod.
|
||||
*/
|
||||
const inline = options.inline !== false;
|
||||
|
||||
const additionalOptions = options.additionalOptions || {};
|
||||
|
||||
function setup(build) {
|
||||
build.initialOptions.metafile = true;
|
||||
|
||||
build.onResolve({ filter: /\.worker(\.js)?$/ }, (args) => {
|
||||
if (args.namespace === "worker-stub") {
|
||||
const baseName = path
|
||||
.basename(args.path)
|
||||
.replace(".worker", "");
|
||||
|
||||
const entryPoint = path.join(
|
||||
args.pluginData.resolveDir,
|
||||
args.path,
|
||||
);
|
||||
|
||||
// `outdir` is set so that file-mode subbuilds produce
|
||||
// real, non-`<stdout>` paths in `outputFiles[].path`
|
||||
// — we use those paths to name the on-disk artifacts
|
||||
// when copying to the parent bundle's outdir. In
|
||||
// inline mode the path doesn't matter (we only read
|
||||
// the bytes), but setting `outdir` is harmless.
|
||||
const subbuild = esbuild.build({
|
||||
target: ["es2021"],
|
||||
entryPoints: [entryPoint],
|
||||
define: {
|
||||
global: "self",
|
||||
},
|
||||
outdir: ".",
|
||||
entryNames: "[name]",
|
||||
chunkNames: "[name]",
|
||||
assetNames: "[name]",
|
||||
minify: !process.env.PSP_DEBUG,
|
||||
bundle: true,
|
||||
sourcemap: !inline,
|
||||
write: false,
|
||||
plugins: subbuildPlugins,
|
||||
loader: subbuildLoader,
|
||||
format: "esm",
|
||||
...additionalOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
path: args.path.replace(".worker", ""),
|
||||
namespace: "worker",
|
||||
pluginData: {
|
||||
baseName,
|
||||
subbuild,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "worker-stub",
|
||||
pluginData: {
|
||||
resolveDir: args.resolveDir,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad(
|
||||
{ filter: /.*/, namespace: "worker-stub" },
|
||||
async (args) => {
|
||||
if (inline) {
|
||||
return {
|
||||
pluginData: args.pluginData,
|
||||
|
||||
// Inline mode: the parent bundle imports the
|
||||
// worker bytes as a text string and constructs
|
||||
// a Blob URL on first call to `getWorkerURL`.
|
||||
// The cached URL is reused for both
|
||||
// `new Worker(url)` and `await import(url)` so
|
||||
// module dedup keeps a single instance.
|
||||
//
|
||||
// `initialize()` adds a Worker-or-shim path:
|
||||
// attempts `new Worker(blobUrl)` first; falls
|
||||
// back to running the worker source text on the
|
||||
// main thread via `new Function(...)` when
|
||||
// Worker construction is unavailable (e.g.
|
||||
// `file://` origins where module-Worker support
|
||||
// is gated, or environments without the Worker
|
||||
// constructor at all). The shim returned by
|
||||
// \`make_host\` is MessagePort-shaped so
|
||||
// downstream consumers can treat it like a real
|
||||
// Worker.
|
||||
contents: `
|
||||
import workerSource from ${JSON.stringify(args.path)};
|
||||
let cached = null;
|
||||
export async function getWorkerURL() {
|
||||
if (cached) return cached;
|
||||
const blob = new Blob([workerSource], {
|
||||
type: "application/javascript",
|
||||
});
|
||||
|
||||
cached = URL.createObjectURL(blob);
|
||||
return cached;
|
||||
}
|
||||
|
||||
function make_host(a, b) {
|
||||
return {
|
||||
addEventListener(type, callback) {
|
||||
if (type === "message") {
|
||||
a.push(callback);
|
||||
}
|
||||
},
|
||||
removeEventListener(type, callback) {
|
||||
const idx = a.indexOf(callback);
|
||||
if (idx > -1) {
|
||||
a.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
postMessage(msg, ports) {
|
||||
for (const listener of b) {
|
||||
listener({
|
||||
data: msg,
|
||||
ports: ports,
|
||||
});
|
||||
}
|
||||
},
|
||||
terminate() {},
|
||||
location: { href: "" },
|
||||
};
|
||||
}
|
||||
|
||||
function run_single_threaded() {
|
||||
console.warn(
|
||||
"Running perspective in single-threaded mode"
|
||||
);
|
||||
const f = Function(
|
||||
"const self = arguments[0];" + workerSource
|
||||
);
|
||||
const workers = [];
|
||||
const mains = [];
|
||||
f(make_host(workers, mains));
|
||||
return make_host(mains, workers);
|
||||
}
|
||||
|
||||
export async function initialize(opts) {
|
||||
const workerOpts = opts || {
|
||||
type: "module",
|
||||
};
|
||||
try {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.location &&
|
||||
window.location.protocol &&
|
||||
window.location.protocol.startsWith(
|
||||
"file"
|
||||
)
|
||||
) {
|
||||
console.warn(
|
||||
"file:// protocol does not reliably support Web Workers"
|
||||
);
|
||||
return run_single_threaded();
|
||||
}
|
||||
|
||||
const url = await getWorkerURL();
|
||||
const worker = new Worker(url, workerOpts);
|
||||
return worker;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error instantiating worker; falling back to single-threaded mode",
|
||||
e
|
||||
);
|
||||
return run_single_threaded();
|
||||
}
|
||||
}
|
||||
|
||||
export default getWorkerURL;
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
// File mode: the subbuild writes its output to disk
|
||||
// next to the parent bundle, and `getWorkerURL`
|
||||
// resolves the worker file relative to the consuming
|
||||
// module's URL (so `<script type="module" src=...>`
|
||||
// and dynamic imports both work without a baked-in
|
||||
// absolute path). The basename comes from the
|
||||
// namespace import below.
|
||||
return {
|
||||
pluginData: args.pluginData,
|
||||
contents: `
|
||||
import basename from ${JSON.stringify(args.path)};
|
||||
const workerURL = new URL(
|
||||
"./" + basename,
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
export async function getWorkerURL() {
|
||||
return workerURL;
|
||||
}
|
||||
|
||||
export default getWorkerURL;
|
||||
`,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: "worker" }, async (args) => {
|
||||
const result = await args.pluginData.subbuild;
|
||||
|
||||
const outpath =
|
||||
build.initialOptions.outdir ||
|
||||
path.dirname(build.initialOptions.outfile);
|
||||
|
||||
if (!fs.existsSync(outpath)) {
|
||||
fs.mkdirSync(outpath, { recursive: true });
|
||||
}
|
||||
|
||||
if (inline) {
|
||||
// Embed the bundle bytes as a `text` string in the
|
||||
// parent bundle; the stub above wraps them in a Blob
|
||||
// URL at runtime. The `.js.map` (when present) is
|
||||
// discarded — Blob URLs strip the source-map comment
|
||||
// anyway since DevTools can't follow it back to disk.
|
||||
const jsOut = result.outputFiles.find((o) =>
|
||||
o.path.endsWith(".js"),
|
||||
);
|
||||
if (!jsOut) {
|
||||
throw new Error("worker subbuild produced no .js output");
|
||||
}
|
||||
|
||||
return { contents: jsOut.contents, loader: "text" };
|
||||
}
|
||||
|
||||
// File mode: write the worker bundle (and source map)
|
||||
// alongside the parent bundle. Multiple output files are
|
||||
// emitted by esbuild when `sourcemap: true`. Use the
|
||||
// actual emitted .js basename for the URL so the stub
|
||||
// and the on-disk artifact agree (esbuild-emitted
|
||||
// basename includes `.worker` from the entry filename).
|
||||
let jsBaseName = null;
|
||||
for (const out of result.outputFiles) {
|
||||
const base = path.basename(out.path);
|
||||
const dst = path.join(outpath, base);
|
||||
await fs.promises.writeFile(dst, out.contents);
|
||||
if (base.endsWith(".js")) {
|
||||
jsBaseName = base;
|
||||
}
|
||||
}
|
||||
|
||||
if (!jsBaseName) {
|
||||
throw new Error("worker subbuild produced no .js output");
|
||||
}
|
||||
|
||||
return {
|
||||
contents: `export default ${JSON.stringify(jsBaseName)};`,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: "webworker",
|
||||
setup,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import { get_scope } from "./sh_perspective.mjs";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
const scope = get_scope();
|
||||
if (scope.includes("viewer-charts")) {
|
||||
await $`pnpm run --recursive --filter bench bench_charts`.pipe(
|
||||
process.stdout,
|
||||
);
|
||||
} else if (scope.includes("client")) {
|
||||
await $`pnpm run --recursive --filter bench bench_js`.pipe(process.stdout);
|
||||
} else if (scope.includes("python")) {
|
||||
await $`pnpm run --recursive --filter bench bench_python`.pipe(
|
||||
process.stdout,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as dotenv from "dotenv";
|
||||
import { run_with_scope } from "./sh_perspective.mjs";
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
if (!fs.existsSync("./.perspectiverc") && !process.env.PACKAGE) {
|
||||
console.error("No .perspectiverc, running setup");
|
||||
process.env.PSP_BUILD_IMMEDIATELY = 1;
|
||||
await import("./setup.mjs");
|
||||
} else {
|
||||
await run_with_scope`build`;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { clean, get_scope } from "./sh_perspective.mjs";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const PACKAGES = get_scope();
|
||||
const JS_PKGS = [];
|
||||
const RUST_PKGS = [];
|
||||
|
||||
const CRATE_NAMES = fs.readdirSync("rust");
|
||||
|
||||
for (const pkg of PACKAGES) {
|
||||
if (pkg === "perspective-server") {
|
||||
console.log("-- Cleaning perspective-server");
|
||||
clean("rust/perspective-server/dist");
|
||||
} else if (CRATE_NAMES.indexOf(pkg) > -1) {
|
||||
RUST_PKGS.push(pkg);
|
||||
} else {
|
||||
JS_PKGS.push(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
if (JS_PKGS.length > 0 || RUST_PKGS.length > 0) {
|
||||
console.log(`-- Cleaning ${JS_PKGS.join(", ")} via pnpm`);
|
||||
const flags = JS_PKGS.concat(RUST_PKGS)
|
||||
.map(
|
||||
(x) =>
|
||||
`--filter @perspective-dev/${x} --if-present --filter ${x} --if-present`,
|
||||
)
|
||||
.join(" ");
|
||||
|
||||
execSync(`pnpm run ${flags} clean`, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
if (RUST_PKGS.length > 0) {
|
||||
if (process.env.PACKAGE?.length > 1) {
|
||||
console.log(`-- Cleaning ${RUST_PKGS.join(", ")} via cargo`);
|
||||
execSync(`cargo clean ${RUST_PKGS.map((x) => `-p ${x}`).join(" ")}`);
|
||||
} else {
|
||||
console.log(`-- Cleaning all crates via cargo`);
|
||||
execSync(`cargo clean`);
|
||||
}
|
||||
}
|
||||
|
||||
clean("docs/build", "docs/python", "docs/obj");
|
||||
@@ -0,0 +1,21 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import "zx/globals";
|
||||
import { run_with_scope } from "./sh_perspective.mjs";
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
const HOST = /host\: (.+?)$/gm.exec(await $`rustc -vV`)[1];
|
||||
await $`cargo doc --no-deps --target=${HOST}`;
|
||||
await run_with_scope`docs`;
|
||||
@@ -0,0 +1,26 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as url from "url";
|
||||
import * as dotenv from "dotenv";
|
||||
import { lint_js, lint_python } from "./lint.mjs";
|
||||
|
||||
if (import.meta.url.startsWith("file:")) {
|
||||
if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
|
||||
const { default: run } = await import("./lint_headers.mjs");
|
||||
await run(true);
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
await lint_js(true);
|
||||
lint_python(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
import * as dotenv from "dotenv";
|
||||
import { getWorkspaceRoot, getWorkspacePackageJson } from "./workspace.mjs";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
const pkg = getWorkspacePackageJson();
|
||||
|
||||
const emscripten = pkg.emscripten;
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
function base() {
|
||||
return path.resolve(getWorkspaceRoot(), ".emsdk").replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function emsdk_checkout() {
|
||||
$.sync`git clone https://github.com/emscripten-core/emsdk.git ${base()}`;
|
||||
}
|
||||
|
||||
function emsdk(...args) {
|
||||
const basedir = base();
|
||||
const suffix = os.type() == "Windows_NT" ? ".bat" : "";
|
||||
const emsdkBin = path.join(basedir, "emsdk" + suffix).replace(/\\/g, "/");
|
||||
$.sync`${emsdkBin} ${args}`;
|
||||
}
|
||||
|
||||
function toolchain_install() {
|
||||
console.log(`-- Installing Emscripten ${emscripten}`);
|
||||
$.sync({ cwd: ".emsdk" })`git pull`;
|
||||
emsdk("install", emscripten);
|
||||
emsdk("activate", emscripten);
|
||||
console.log(`-- Emscripten ${emscripten} installed`);
|
||||
}
|
||||
|
||||
function repo_check() {
|
||||
return fs.existsSync(path.join(base(), "emsdk_env.sh"));
|
||||
}
|
||||
|
||||
if (!process.env.PSP_SKIP_EMSDK_INSTALL) {
|
||||
// if a stale toolchain is still activated in the shell, these vars break
|
||||
// emsdk install in a confusing way. ensure they are unset
|
||||
for (let ev of ["EMSDK", "EMSDK_NODE", "EMSDK_PYTHON", "SSL_CERT_FILE"]) {
|
||||
delete process.env[ev];
|
||||
}
|
||||
|
||||
if (!repo_check()) {
|
||||
emsdk_checkout();
|
||||
}
|
||||
|
||||
toolchain_install();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import os from "os";
|
||||
import * as fs from "node:fs";
|
||||
import { mkdirSync } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import { getWorkspaceRoot, getWorkspacePackageJson } from "./workspace.mjs";
|
||||
|
||||
const pkg = getWorkspacePackageJson();
|
||||
|
||||
const LLVM_VERSION = pkg.llvm;
|
||||
const DOWNLOAD_DIR = path.join(getWorkspaceRoot(), ".llvm", "llvm-toolchain");
|
||||
|
||||
function getLLVMPackageName() {
|
||||
const system = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
if (arch === "arm64" && system === "linux") {
|
||||
return `clang+llvm-${LLVM_VERSION}-aarch64-linux-gnu.tar.xz`;
|
||||
} else if (arch === "x64" && system === "linux") {
|
||||
return `clang+llvm-${LLVM_VERSION}-x86_64-linux-gnu-ubuntu-22.04.tar.xz`;
|
||||
} else if (arch === "x64" && system === "darwin") {
|
||||
return `clang+llvm-${LLVM_VERSION}-x86_64-apple-darwin21.0.tar.xz`;
|
||||
} else if (arch === "arm64" && system === "darwin") {
|
||||
return `clang+llvm-${LLVM_VERSION}-arm64-apple-darwin22.0.tar.xz`;
|
||||
} else {
|
||||
throw new Error(`Unsupported system: ${arch} on ${system}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadLLVM(packageName) {
|
||||
const llvmUrl = `https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VERSION}/${packageName}`;
|
||||
const outputPath = path.join(DOWNLOAD_DIR, packageName);
|
||||
|
||||
console.log(`Downloading LLVM ${LLVM_VERSION} from ${llvmUrl}...`);
|
||||
|
||||
mkdirSync(DOWNLOAD_DIR, { recursive: true });
|
||||
fs.writeFileSync(`${DOWNLOAD_DIR}/.llvm-version`, LLVM_VERSION);
|
||||
execSync(`wget -O ${outputPath} ${llvmUrl}`, { stdio: "inherit" });
|
||||
execSync(`tar -xvf ${outputPath} -C ${DOWNLOAD_DIR} --strip-components=1`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
execSync(`rm ${outputPath}`, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
const llvmPackageName = getLLVMPackageName();
|
||||
|
||||
if (fs.existsSync(DOWNLOAD_DIR)) {
|
||||
console.log(`LLVM ${LLVM_VERSION} already downloaded`);
|
||||
if (
|
||||
!fs.existsSync(`${DOWNLOAD_DIR}/.llvm-version`) ||
|
||||
fs.readFileSync(`${DOWNLOAD_DIR}/.llvm-version`).toString().trim() !==
|
||||
LLVM_VERSION
|
||||
) {
|
||||
console.log(`LLVM version mismatch, re-downloading...`);
|
||||
fs.rmdirSync(DOWNLOAD_DIR, { recursive: true });
|
||||
downloadLLVM(llvmPackageName);
|
||||
}
|
||||
} else {
|
||||
downloadLLVM(llvmPackageName);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
// Download a full Pyodide distribution from github and extract it to rust/target dir for
|
||||
// use in integration tests
|
||||
|
||||
import { getPyodideVersion, getPyodideDownloadDir } from "./pyodide.mjs";
|
||||
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
const pyodideVersion = getPyodideVersion();
|
||||
|
||||
function downloadPyodide() {
|
||||
const pyodideUrl = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/pyodide-${pyodideVersion}.tar.bz2`;
|
||||
const downloadDir = getPyodideDownloadDir(); // the download dir is versioned
|
||||
const tarball = path.join(downloadDir, `pyodide-${pyodideVersion}.tar.bz2`);
|
||||
const buildStamp = path.join(downloadDir, "psp-build-stamp.txt");
|
||||
const pyodideLock = path.join(downloadDir, "pyodide", "pyodide-lock.json");
|
||||
if (fs.existsSync(buildStamp) && fs.existsSync(pyodideLock)) {
|
||||
console.log(
|
||||
`Pyodide ${pyodideVersion} already extracted to ${downloadDir}`,
|
||||
);
|
||||
} else {
|
||||
fs.rmSync(buildStamp, { force: true });
|
||||
console.log(
|
||||
`Downloading Pyodide ${pyodideVersion} from ${pyodideUrl}...`,
|
||||
);
|
||||
fs.mkdirSync(downloadDir, { recursive: true });
|
||||
execFileSync("wget", ["-O", tarball, pyodideUrl], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`Extracting ${tarball}...`);
|
||||
execFileSync("tar", ["-xvf", tarball, "-C", downloadDir], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`Removing ${tarball}...`);
|
||||
fs.rmSync(tarball);
|
||||
// assert presence of a known file
|
||||
assert(fs.existsSync(pyodideLock), `${pyodideLock} not found`);
|
||||
console.log(`Extracted to ${downloadDir}`);
|
||||
fs.writeFileSync(buildStamp, ""); // prevent re-download/extract
|
||||
}
|
||||
}
|
||||
|
||||
downloadPyodide();
|
||||
@@ -0,0 +1,76 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { get_scope } from "./sh_perspective.mjs";
|
||||
import * as url from "url";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
export async function lint_js(is_fix = false) {
|
||||
const eslint_flags = is_fix ? ["--fix"] : [];
|
||||
await $`eslint ${eslint_flags} .`.verbose();
|
||||
|
||||
const prettier_flags = is_fix ? "--write" : "--check";
|
||||
await $`prettier ${prettier_flags} \
|
||||
"examples/**/*.js" \
|
||||
"examples/**/*.tsx" \
|
||||
"tools/scripts/*.mjs" \
|
||||
"rust/**/*.ts" \
|
||||
"rust/**/*.js" \
|
||||
"packages/**/*.js" \
|
||||
"packages/**/*.ts" \
|
||||
"**/css/**/*.css" \
|
||||
"**/themes/**/*.css" \
|
||||
"**/html/*.html" \
|
||||
"packages/**/package.json" \
|
||||
"rust/**/package.json" \
|
||||
"examples/**/package.json" \
|
||||
"docs/package.json" \
|
||||
`.verbose();
|
||||
|
||||
await $`prettier --prose-wrap=always ${prettier_flags} "rust/*/docs/**/*.md"`.verbose();
|
||||
const check = is_fix ? [] : ["--check"];
|
||||
const dirty = is_fix ? ["--allow-dirty"] : [];
|
||||
const staged = is_fix ? ["--allow-staged"] : [];
|
||||
const fix = is_fix ? ["--fix"] : [];
|
||||
await $`cargo build -p perspective-lint --features=yew-fmt`.verbose();
|
||||
await $`cargo clippy ${fix} ${dirty} ${staged} -p perspective-viewer -- -Dwarnings`.verbose();
|
||||
await $`RUSTFMT="rust/target/debug/lint" cargo fmt ${check}`.verbose();
|
||||
}
|
||||
|
||||
export function lint_python(is_fix = false) {
|
||||
if (get_scope().indexOf("perspective-python") > -1) {
|
||||
if (is_fix) {
|
||||
$.sync`ruff check --fix`;
|
||||
} else {
|
||||
$.sync`ruff check`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url.startsWith("file:")) {
|
||||
if (process.argv[1] === url.fileURLToPath(import.meta.url)) {
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
const { default: run } = await import("./lint_headers.mjs");
|
||||
const exit_code = await run(false);
|
||||
// if (process.env.PSP_PROJECT === "python") {
|
||||
// await import("./lint_python.mjs");
|
||||
// } else {
|
||||
|
||||
await lint_js();
|
||||
lint_python();
|
||||
// }
|
||||
|
||||
process.exit(exit_code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { glob } from "glob-gitignore";
|
||||
import { promises as fs } from "fs";
|
||||
import * as fs_sync from "fs";
|
||||
|
||||
const IGNORE_PATHS = fs_sync
|
||||
.readFileSync(".gitignore")
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x.length > 0 && !x.startsWith("#"))
|
||||
.concat(["llvm/*", "cmake/*", "pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
||||
|
||||
const FIX_PATHS = [
|
||||
["**/*.rs", ["//", "/*", " *", " */"]],
|
||||
["**/*.js", ["//", "/*", " *", " */"]],
|
||||
["**/*.mjs", ["//", "/*", " *", " */"]],
|
||||
// ["**/*.css", ["//", "/*", " *", " */"]],
|
||||
["**/*.scss", ["//", "/*", " *", " */"]],
|
||||
["**/*.toml", ["# ", "#"]],
|
||||
["**/*.yaml", ["# ", "#"]],
|
||||
["**/*.py", ["# ", "#"]],
|
||||
["**/*.ts", ["//", "/*", " *", " */"]],
|
||||
["**/*.tsx", ["//", "/*", " *", " */"]],
|
||||
["**/*.cpp", ["//", "/*", " *", " */"]],
|
||||
["**/*.h", ["//", "/*", " *", " */"]],
|
||||
[".github/**/*.yaml", ["# ", "#"]],
|
||||
];
|
||||
|
||||
function header_text(c, ...args) {
|
||||
return (
|
||||
`
|
||||
${c} ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
${c} ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
${c} ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
${c} ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
${c} ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
${c} ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
${c} ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
${c} ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
${c} ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
${c} ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
${c} ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
`.trim() + args.join("")
|
||||
);
|
||||
}
|
||||
|
||||
async function check(is_write, pattern, comment) {
|
||||
const matches = await glob(pattern, {
|
||||
ignore: IGNORE_PATHS,
|
||||
});
|
||||
|
||||
let exit_code = 0;
|
||||
for (const match of matches) {
|
||||
if (fs_sync.lstatSync(match).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await fs.readFile(match);
|
||||
let contents = data.toString();
|
||||
let default_comment = comment;
|
||||
if (Array.isArray(comment)) {
|
||||
default_comment = comment[0];
|
||||
} else {
|
||||
comment = [comment];
|
||||
}
|
||||
|
||||
const expected_header = header_text(default_comment);
|
||||
let seen_whitespace = false;
|
||||
// Nightly builds modify the version of metadata scripts in place to
|
||||
// tag the nightly, so lint will fail if we don't make an exception
|
||||
// for these files.
|
||||
const is_nightly =
|
||||
process.argv.indexOf("--nightly") > -1 &&
|
||||
(match.indexOf("Cargo.toml") > -1 ||
|
||||
match.indexOf("pyproject.toml") > -1);
|
||||
|
||||
if (!contents.startsWith(expected_header) && !is_nightly) {
|
||||
console.error(`Missing header in file ${match}`);
|
||||
while (
|
||||
contents.length > 0 &&
|
||||
((!seen_whitespace &&
|
||||
comment.some((x) => contents.startsWith(x))) ||
|
||||
(seen_whitespace = contents.startsWith("\n")))
|
||||
) {
|
||||
contents = contents.substring(contents.indexOf("\n") + 1);
|
||||
}
|
||||
|
||||
if (is_write) {
|
||||
await fs.writeFile(match, `${expected_header}\n\n${contents}`);
|
||||
} else {
|
||||
exit_code = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
export default async function run(is_write) {
|
||||
let exit_code = 0;
|
||||
for (const [patt, comment] of FIX_PATHS) {
|
||||
const result = await check(is_write, patt, comment);
|
||||
exit_code = exit_code || result;
|
||||
}
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@perspective-dev/scripts",
|
||||
"version": "4.5.2",
|
||||
"description": "Build scripts based on perspective",
|
||||
"private": true,
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective/tools/scripts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"auto-changelog": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"inquirer": "catalog:",
|
||||
"glob-gitignore": "catalog:",
|
||||
"octokit": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"@iarna/toml": "catalog:",
|
||||
"@playwright/experimental-ct-react": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"zx": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
function lint_git(sha) {
|
||||
if (!sha || typeof sha !== "string") {
|
||||
throw new Error(`invalid sha: ${sha}`);
|
||||
}
|
||||
const result = $.sync`git log -1 ${sha} | grep -F "Signed-off-by: "`;
|
||||
if (result.exitCode !== 0) {
|
||||
console.error(
|
||||
"`git log -1 " +
|
||||
sha +
|
||||
"` is missing a Signed-off-by: and DCO check will surely fail.",
|
||||
);
|
||||
console.error("To sign off, run:\ngit commit --amend --edit --sign");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function readPrePushInput() {
|
||||
// Git supplies information about the push to the hook on stdin.
|
||||
// https://git-scm.com/docs/githooks#_pre_push
|
||||
const chunks = [];
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
// Makes developing the pre-push script more convenient. In particular
|
||||
// when you run `pnpm run prepush` from a shell terminal you don't have
|
||||
// to send EOF on stdin.
|
||||
return [];
|
||||
}
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const input = Buffer.concat(chunks).toString();
|
||||
const lines = input.split("\n").filter((l) => l.length > 0);
|
||||
return lines.map((line) => {
|
||||
const parts = line.trim().split(" ");
|
||||
|
||||
return {
|
||||
local_ref: parts[0],
|
||||
local_object_name: parts[1],
|
||||
remote_ref: parts[2],
|
||||
remote_object_name: parts[3],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
// Does not actually run all pre-push hook checks (it does not run the repo
|
||||
// lint script). These are lints which run only in pre-push. The
|
||||
// `prepush` script defined in package.json is responsible for running the
|
||||
// repo lint script.
|
||||
const pushes = await readPrePushInput();
|
||||
for (const push of pushes) {
|
||||
const { local_object_name } = push;
|
||||
lint_git(local_object_name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Octokit } from "octokit";
|
||||
import fs from "node:fs/promises";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
// GitHub API Wrapper
|
||||
const OCTOKIT = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
|
||||
const CURRENT_TAG = $.sync`git describe --exact-match --tags`.toString().trim();
|
||||
|
||||
const IS_DIRTY =
|
||||
(await $`git status --untracked-files=no --porcelain`).stdout.trim()
|
||||
.length > 0;
|
||||
|
||||
async function get_release_assets() {
|
||||
const resp = await OCTOKIT.request("GET /repos/{owner}/{repo}/releases", {
|
||||
owner: "perspective-dev",
|
||||
repo: "perspective",
|
||||
});
|
||||
|
||||
for (const release of resp.data) {
|
||||
if (release.tag_name === CURRENT_TAG) {
|
||||
return release.assets;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No release ${CURRENT_TAG} found`);
|
||||
}
|
||||
|
||||
async function download_release_assets(releases) {
|
||||
await Promise.all(
|
||||
releases.map(async (release) => {
|
||||
const resp = await OCTOKIT.request(
|
||||
"GET /repos/{owner}/{repo}/releases/assets/{asset_id}",
|
||||
{
|
||||
owner: "perspective-dev",
|
||||
repo: "perspective",
|
||||
asset_id: release.id,
|
||||
headers: {
|
||||
Accept: "application/octet-stream",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
console.log(`Writing ${release.name}`);
|
||||
await fs.writeFile(release.name, Buffer.from(resp.data));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const SH_ENV = {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
};
|
||||
|
||||
async function publish_release_assets(releases) {
|
||||
if (process.env.COMMIT) {
|
||||
for (const release of releases) {
|
||||
if (
|
||||
(release.name.endsWith("whl") ||
|
||||
release.name.endsWith("tar.gz")) &&
|
||||
release.name.indexOf("wasm") === -1
|
||||
) {
|
||||
execSync(`twine upload ${release.name}`, SH_ENV);
|
||||
} else if (release.name.endsWith(".tgz")) {
|
||||
execSync(`npm publish ${release.name}`, SH_ENV);
|
||||
} else {
|
||||
console.log(`Skipping "${release.name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
await $`mkdir -p rust/target/package && mv *.crate rust/target/package`;
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective-server --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective-client --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective-python --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective-js --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective-viewer --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
|
||||
execSync(
|
||||
`cargo publish -p perspective --allow-dirty --no-verify`,
|
||||
SH_ENV,
|
||||
);
|
||||
} else {
|
||||
console.warn(`COMMIT not specified, aborting`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
throw new Error("Missing Personal Access Token (GITHUB_TOKEN)");
|
||||
}
|
||||
|
||||
if (!process.env.COMMIT) {
|
||||
console.warn(
|
||||
"Running a dry run, this WILL NOT publish. Set the env var COMMIT to publish.",
|
||||
);
|
||||
}
|
||||
|
||||
if (IS_DIRTY) {
|
||||
throw new Error("Working tree dirty, aborting");
|
||||
}
|
||||
|
||||
const releases = await get_release_assets();
|
||||
console.log(`Found ${releases.length} artifacts for ${CURRENT_TAG}`);
|
||||
for (const release of releases) {
|
||||
console.log(` ${release.name}`);
|
||||
}
|
||||
|
||||
await download_release_assets(releases);
|
||||
await publish_release_assets(releases);
|
||||
@@ -0,0 +1,41 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
// Pyodide information:
|
||||
// - Pyodide version we build against
|
||||
// - Pyodide dist directory
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import { getWorkspacePackageJson, getRustTargetDir } from "./workspace.mjs";
|
||||
|
||||
export function getPyodideVersion() {
|
||||
const pyodideVersion = getWorkspacePackageJson().pyodide;
|
||||
if (!pyodideVersion) {
|
||||
throw new Error(`"pyodide" not set in package.json`);
|
||||
}
|
||||
return pyodideVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns pyodide download directory for the current version
|
||||
*/
|
||||
export function getPyodideDownloadDir() {
|
||||
return path.join(getRustTargetDir(), "pyodide", getPyodideVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns pyodide dist directory for the current version
|
||||
*/
|
||||
export function getPyodideDistDir() {
|
||||
return path.join(getPyodideDownloadDir(), "pyodide");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import { getWorkspacePackageJson } from "./workspace.mjs";
|
||||
|
||||
const pkg = getWorkspacePackageJson();
|
||||
|
||||
const wheel_file = fs.readdirSync(".").filter((x) => x.endsWith(".whl"))[0];
|
||||
execSync(`wheel unpack ${wheel_file}`);
|
||||
const pkg_name = wheel_file.split("-").slice(0, 2).join("-");
|
||||
const version = pkg.version.replace(/-(rc|alpha|beta)\.\d+/, (x) =>
|
||||
x.replace("-", "").replace(".", ""),
|
||||
);
|
||||
|
||||
const dest = `${pkg_name}/perspective_python-${version}.data`;
|
||||
const src = `rust/perspective-python/perspective_python-${version}.data`;
|
||||
fs.cpSync(src, dest, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
execSync(`wheel pack ${pkg_name}`);
|
||||
@@ -0,0 +1,253 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import inquirer from "inquirer";
|
||||
import fs from "fs";
|
||||
import * as dotenv from "dotenv";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const original = dotenv.config({
|
||||
path: "./.perspectiverc",
|
||||
quiet: true,
|
||||
}).parsed;
|
||||
|
||||
const CONFIG = new Proxy(
|
||||
new (class {
|
||||
constructor() {
|
||||
this.config = [];
|
||||
this._values = { ...original } || {};
|
||||
if (this._values.PACKAGE && this._values.PACKAGE.startsWith("@")) {
|
||||
this._values.PACKAGE = this._values.PACKAGE.slice(
|
||||
2,
|
||||
this._values.PACKAGE.length - 1,
|
||||
).replace(/\|/g, ",");
|
||||
}
|
||||
}
|
||||
remove(keyname) {
|
||||
const idx = this.config.find((x) => s.startsWith(keyname));
|
||||
if (idx >= 0) {
|
||||
this.config.splice(idx);
|
||||
}
|
||||
}
|
||||
add(new_config) {
|
||||
for (const key in new_config) {
|
||||
const val = new_config[key];
|
||||
if (val !== "" && !!val) {
|
||||
this._values[key] = val;
|
||||
this.config.push(`${key}=${val}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
write() {
|
||||
fs.writeFileSync("./.perspectiverc", this.config.join("\n"));
|
||||
if (process.env.PSP_BUILD_IMMEDIATELY || process.env.PSP_ONCE) {
|
||||
execSync("node tools/scripts/build.mjs", { stdio: "inherit" });
|
||||
if (process.env.PSP_ONCE) {
|
||||
while (this.config.length > 0) {
|
||||
this.config.pop();
|
||||
}
|
||||
|
||||
for (const key in this._values) {
|
||||
if (key in original) {
|
||||
this._values[key] = original[key];
|
||||
} else {
|
||||
delete this._values[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this._values.PACKAGE &&
|
||||
this._values.PACKAGE.startsWith("@")
|
||||
) {
|
||||
this._values.PACKAGE = this._values.PACKAGE.slice(
|
||||
2,
|
||||
this._values.PACKAGE.length - 1,
|
||||
).replace(/\|/g, ",");
|
||||
}
|
||||
|
||||
const config = [];
|
||||
for (const key in this._values) {
|
||||
config.push(`${key}=${this._values[key]}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync("./.perspectiverc", config.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
})(),
|
||||
{
|
||||
set: function (target, name, val) {
|
||||
target.add({
|
||||
[name]: val,
|
||||
});
|
||||
},
|
||||
get: function (target, name) {
|
||||
if (name in target._values) {
|
||||
return target._values[name];
|
||||
} else {
|
||||
return target[name];
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const PROMPT_DEBUG = {
|
||||
type: "confirm",
|
||||
name: "PSP_DEBUG",
|
||||
message: "Run debug build?",
|
||||
default: CONFIG["PSP_DEBUG"] || false,
|
||||
};
|
||||
|
||||
const PROMPT_DOCKER = {
|
||||
type: "confirm",
|
||||
name: "PSP_DOCKER",
|
||||
message: "Use docker for build env?",
|
||||
default: CONFIG["PSP_DOCKER"] || false,
|
||||
};
|
||||
|
||||
async function choose_docker() {
|
||||
const answers = await inquirer.prompt([PROMPT_DOCKER]);
|
||||
CONFIG.add(answers);
|
||||
CONFIG.write();
|
||||
}
|
||||
|
||||
async function focus_package() {
|
||||
const choices = [
|
||||
{
|
||||
key: "r",
|
||||
name: "docs",
|
||||
value: "docs",
|
||||
},
|
||||
|
||||
{
|
||||
key: "m",
|
||||
name: "metadata",
|
||||
value: "metadata",
|
||||
},
|
||||
{
|
||||
key: "y",
|
||||
name: "perspective-python",
|
||||
value: "python",
|
||||
},
|
||||
{
|
||||
key: "q",
|
||||
name: "perspective-python (pyodide)",
|
||||
value: "pyodide",
|
||||
},
|
||||
{
|
||||
key: "r",
|
||||
name: "perspective (rust)",
|
||||
value: "rust",
|
||||
},
|
||||
{
|
||||
key: "c",
|
||||
name: "@perspective-dev/server",
|
||||
value: "server",
|
||||
},
|
||||
{
|
||||
key: "p",
|
||||
name: "@perspective-dev/client",
|
||||
value: "client",
|
||||
},
|
||||
{
|
||||
key: "v",
|
||||
name: "@perspective-dev/viewer",
|
||||
value: "viewer",
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
name: "@perspective-dev/viewer-datagrid",
|
||||
value: "viewer-datagrid",
|
||||
},
|
||||
{
|
||||
key: "g",
|
||||
name: "@perspective-dev/viewer-charts",
|
||||
value: "viewer-charts",
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
name: "@perspective-dev/jupyterlab",
|
||||
value: "jupyterlab",
|
||||
},
|
||||
{
|
||||
key: "w",
|
||||
name: "@perspective-dev/workspace",
|
||||
value: "workspace",
|
||||
},
|
||||
{
|
||||
key: "a",
|
||||
name: "@perspective-dev/react",
|
||||
value: "react",
|
||||
},
|
||||
];
|
||||
const new_config = await inquirer.prompt([
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "PACKAGE",
|
||||
message: "Focus NPM package(s)?",
|
||||
default: () => {
|
||||
if (CONFIG["PACKAGE"]) {
|
||||
const packages = CONFIG["PACKAGE"].split(",");
|
||||
if (CONFIG["PSP_PYODIDE"] === "1") {
|
||||
const py = packages.indexOf("python");
|
||||
if (py >= 0) {
|
||||
packages[py] = "pyodide";
|
||||
}
|
||||
}
|
||||
return packages;
|
||||
} else {
|
||||
return [""];
|
||||
}
|
||||
},
|
||||
filter: (answer) => {
|
||||
if (!answer || answer.length === choices.length) {
|
||||
return "";
|
||||
} else {
|
||||
return answer;
|
||||
}
|
||||
},
|
||||
loop: false,
|
||||
pageSize: 20,
|
||||
choices,
|
||||
},
|
||||
]);
|
||||
|
||||
if (Array.isArray(new_config.PACKAGE)) {
|
||||
if (new_config.PACKAGE.length > 0) {
|
||||
let pyodide = new_config.PACKAGE.indexOf("pyodide");
|
||||
if (pyodide >= 0) {
|
||||
new_config.PACKAGE.splice(pyodide, 1);
|
||||
new_config.PSP_PYODIDE = 1;
|
||||
new_config.CI = 1;
|
||||
new_config.PACKAGE.push("python");
|
||||
} else {
|
||||
CONFIG.remove("PSP_PYODIDE");
|
||||
CONFIG.remove("CI");
|
||||
}
|
||||
|
||||
new_config.PACKAGE = `${new_config.PACKAGE.join(",")}`;
|
||||
} else {
|
||||
new_config.PACKAGE = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG.add(new_config);
|
||||
await javascript_options();
|
||||
}
|
||||
|
||||
async function javascript_options() {
|
||||
const new_config = await inquirer.prompt([PROMPT_DEBUG, PROMPT_DOCKER]);
|
||||
CONFIG.add(new_config);
|
||||
CONFIG.write();
|
||||
}
|
||||
|
||||
focus_package();
|
||||
@@ -0,0 +1,115 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
process.env.FORCE_COLOR = true;
|
||||
|
||||
/**
|
||||
* Calls `path.resolve` on each of the input path arguments, then removes the
|
||||
* path if it exists.
|
||||
*
|
||||
* @param {string} dirs paths to clean.
|
||||
* @example
|
||||
* clean("a/b/c"); // Cleans this dir
|
||||
* clean("a/b/c", "d/e/f"); // Cleans both dirs
|
||||
*/
|
||||
export function clean(...dirs) {
|
||||
for (const dir of dirs) {
|
||||
const resolved = path.resolve(dir);
|
||||
if (fs.existsSync(resolved)) {
|
||||
fs.rmSync(resolved, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value after this command-line flag, or `true` if it is the last
|
||||
* arg. This makes it easy to null-pun for boolean flags, and capture the
|
||||
* argument for argument-providing flags, and respect quotes and parens, in
|
||||
* one function. Can be used as a template literal - not sure why, 2 less
|
||||
* characters?
|
||||
*
|
||||
* @param {string} flag The command line flag name. Returns all arguments if
|
||||
* this param is `undefined`.
|
||||
* @returns {string} The next argument after this flag in the command args, or
|
||||
* `true.
|
||||
* @example
|
||||
* console.assert(getarg`--debug`);
|
||||
*/
|
||||
export function getarg(flag, ...args) {
|
||||
if (Array.isArray(flag)) {
|
||||
flag = flag.map((x, i) => x + (args[i] || "")).join("");
|
||||
}
|
||||
const argv = process.argv.slice(2);
|
||||
if (flag) {
|
||||
const index = argv.indexOf(flag);
|
||||
if (index > -1) {
|
||||
const next = argv[index + 1];
|
||||
if (next) {
|
||||
return next;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return argv
|
||||
.map(function (arg) {
|
||||
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
export function get_scope() {
|
||||
const package_venn = (process.env.PACKAGE || "").split(",").reduce(
|
||||
(acc, x) => {
|
||||
if (x.startsWith("!")) {
|
||||
acc.exclude.push(x);
|
||||
} else if (x != "") {
|
||||
acc.include.push(x);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ include: [], exclude: [] },
|
||||
);
|
||||
|
||||
let packages;
|
||||
if (package_venn.include.length === 0) {
|
||||
packages = JSON.parse($.sync`pnpm m ls --json --depth=-1`.toString())
|
||||
.filter((x) => x.name !== undefined)
|
||||
.map((x) => x.name.replace("@perspective-dev/", ""))
|
||||
.filter((x) => package_venn.exclude.indexOf(`!${x}`) === -1);
|
||||
} else {
|
||||
packages = package_venn.include.filter(
|
||||
(x) => package_venn.exclude.indexOf(`!${x}`) === -1,
|
||||
);
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
export const run_with_scope = async function run_recursive(strings, ...args) {
|
||||
let scope = get_scope();
|
||||
const cmd = strings[0].split(" ")[0];
|
||||
const filters = scope.map((x) => `--filter ${x} --if-present`).join(" ");
|
||||
execSync(`pnpm run --sequential --recursive ${filters} ${cmd}`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as fs from "fs";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
if (!fs.existsSync("./.perspectiverc") && !process.env.PACKAGE) {
|
||||
console.error("No .perspectiverc, running setup");
|
||||
process.env.PSP_BUILD_IMMEDIATELY = 1;
|
||||
await import("./setup");
|
||||
} else {
|
||||
await import("./test_js.mjs");
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { getarg, run_with_scope, get_scope } from "./sh_perspective.mjs";
|
||||
|
||||
const IS_NEEDS_BUILD = get_scope().some((x) => x === "jupyterlab");
|
||||
|
||||
// Unfortunately we have to handle parts of the Jupyter test case here,
|
||||
// as the Jupyter server needs to be run outside of the main Jest process.
|
||||
const IS_JUPYTER = !!getarg("--jupyter") && IS_NEEDS_BUILD;
|
||||
|
||||
if (getarg("--debug")) {
|
||||
console.log("-- Running tests in debug mode.");
|
||||
}
|
||||
|
||||
const IS_PLAYWRIGHT = get_scope().reduce(
|
||||
(is_playwright, pkg) =>
|
||||
is_playwright ||
|
||||
[
|
||||
"docs",
|
||||
"client",
|
||||
"react",
|
||||
"viewer",
|
||||
"viewer-datagrid",
|
||||
"viewer-charts",
|
||||
"viewer-workspace",
|
||||
"workspace",
|
||||
"jupyterlab",
|
||||
].includes(pkg),
|
||||
false,
|
||||
);
|
||||
|
||||
const IS_RUST = get_scope().reduce(
|
||||
(is_playwright, pkg) => is_playwright || ["rust"].includes(pkg),
|
||||
false,
|
||||
);
|
||||
|
||||
const IS_CI = process.env.CI || getarg("--ci") ? "CI=1" : "";
|
||||
if (IS_CI) {
|
||||
console.log("-- Running tests in CI mode.");
|
||||
}
|
||||
|
||||
function playwright(pkg, is_jlab) {
|
||||
const pkg_name = pkg ? `"${pkg}" ` : "";
|
||||
console.log(`-- Running ${pkg_name}Playwright test suite`);
|
||||
const args = process.argv
|
||||
.slice(2)
|
||||
.filter(
|
||||
(x) =>
|
||||
x !== "--ci" && x !== "--jupyter" && x !== "--fetch-snapshots",
|
||||
);
|
||||
|
||||
const env = { ...process.env, TZ: "UTC" };
|
||||
if (is_jlab) {
|
||||
env.PSP_JUPYTERLAB_TESTS = "1";
|
||||
env.__JUPYTERLAB_PORT__ = "6538";
|
||||
}
|
||||
|
||||
if (getarg("--fetch-snapshots")) {
|
||||
env.PSP_FETCH_SNAPSHOTS = "1";
|
||||
}
|
||||
|
||||
if (getarg("--update-snapshots")) {
|
||||
env.PSP_UPDATE_SNAPSHOTS = "1";
|
||||
}
|
||||
|
||||
if (IS_CI) {
|
||||
env.CI = "1";
|
||||
}
|
||||
|
||||
if (pkg) {
|
||||
env.PACKAGE = pkg;
|
||||
}
|
||||
|
||||
const cmd = [
|
||||
"npx",
|
||||
"playwright",
|
||||
"test",
|
||||
"--config=tools/test/playwright.config.ts",
|
||||
...args,
|
||||
].join(" ");
|
||||
execSync(cmd, { stdio: "inherit", env });
|
||||
}
|
||||
|
||||
if (!IS_JUPYTER) {
|
||||
// test:build irrelevant for jupyter tests
|
||||
await run_with_scope`test:build`;
|
||||
}
|
||||
|
||||
if (process.env.PACKAGE) {
|
||||
if (IS_NEEDS_BUILD) {
|
||||
await run_with_scope`test:jupyter:build`;
|
||||
}
|
||||
|
||||
if (IS_JUPYTER) {
|
||||
// Jupyterlab is guaranteed to have started at this point, so
|
||||
// copy the test files over and run the tests.
|
||||
playwright("jupyterlab", true);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (IS_PLAYWRIGHT) {
|
||||
playwright(process.env.PACKAGE);
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.PACKAGE.indexOf("python") >= 0 &&
|
||||
process.env.PACKAGE.indexOf("!python") === -1
|
||||
) {
|
||||
// Support `pnpm test -- --my_cool --test_arguments`
|
||||
const args = process.argv.slice(2);
|
||||
execSync(
|
||||
`pnpm run --recursive --filter @perspective-dev/python test ${args.join(" ")}`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
}
|
||||
|
||||
if (IS_RUST) {
|
||||
let target = "";
|
||||
let flags = "--release";
|
||||
if (!!process.env.PSP_DEBUG) {
|
||||
flags = "";
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.PSP_ARCH === "x86_64" &&
|
||||
process.platform === "darwin"
|
||||
) {
|
||||
target = "--target=x86_64-apple-darwin";
|
||||
} else if (
|
||||
process.env.PSP_ARCH === "aarch64" &&
|
||||
process.platform === "darwin"
|
||||
) {
|
||||
target = "--target=aarch64-apple-darwin";
|
||||
} else if (
|
||||
process.env.PSP_ARCH === "x86_64" &&
|
||||
process.platform === "linux"
|
||||
) {
|
||||
target =
|
||||
"--target=x86_64-unknown-linux-gnu --compatibility manylinux_2_28";
|
||||
} else if (
|
||||
process.env.PSP_ARCH === "aarch64" &&
|
||||
process.platform === "linux"
|
||||
) {
|
||||
target = "--target=aarch64-unknown-linux-gnu";
|
||||
}
|
||||
|
||||
execSync(
|
||||
`cargo test ${flags} ${target} -p perspective -p perspective-client`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("-- Running all tests");
|
||||
playwright();
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import fs from "fs";
|
||||
import { Octokit } from "octokit";
|
||||
import { parseReleases } from "auto-changelog/src/releases.js";
|
||||
import { fetchTags } from "auto-changelog/src/tags.js";
|
||||
import { fetchRemote } from "auto-changelog/src/remote.js";
|
||||
import * as toml from "@iarna/toml";
|
||||
|
||||
import "zx/globals";
|
||||
|
||||
// if (!process.env.GITHUB_TOKEN) {
|
||||
// throw new Error("Missing GITHUB_TOKEN");
|
||||
// }
|
||||
|
||||
const IS_NIGHTLY = process.argv.indexOf("--nightly") > -1;
|
||||
|
||||
const PKG_VERSION = JSON.parse(fs.readFileSync("./package.json")).version.match(
|
||||
/[0-9]+\.[0-9]+\.[0-9]+/,
|
||||
)[0];
|
||||
|
||||
const GIT_REV = await $`git show --no-patch --format=%ct HEAD`;
|
||||
|
||||
console.log();
|
||||
|
||||
const NEW_VERSION = IS_NIGHTLY
|
||||
? `${PKG_VERSION}-dev${+new Date(parseInt(GIT_REV.toString()))}`
|
||||
: PKG_VERSION;
|
||||
|
||||
/**
|
||||
* A Github data fetching cache designed to run in parallel with changelog formatting.
|
||||
* When a SHA is requested, it is looked up in a local cache, and if it isn't there,
|
||||
* the next Github API page is awaited. Requests are always triggered in advance.
|
||||
*/
|
||||
class GithubPRCache {
|
||||
constructor() {
|
||||
this.prs = new Map();
|
||||
this.page = 1;
|
||||
this.octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
this.next();
|
||||
}
|
||||
async next() {
|
||||
let ret;
|
||||
if (this._next_req) {
|
||||
ret = await this._next_req;
|
||||
}
|
||||
console.log(
|
||||
`Fetching Github commits ${(this.page - 1) * 100} - ${
|
||||
this.page * 100 - 1
|
||||
}`,
|
||||
);
|
||||
this._next_req = this.octokit.request(
|
||||
"GET /repos/{owner}/{repo}/pulls?state=closed&per_page={per_page}&page={page}",
|
||||
{
|
||||
owner: "perspective-dev",
|
||||
repo: "perspective",
|
||||
per_page: 100,
|
||||
page: this.page++,
|
||||
},
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
async get(sha) {
|
||||
while (!this.prs.has(sha) && !this.failed) {
|
||||
const resp = await this.next();
|
||||
if (resp.data.length == 0) {
|
||||
this.failed = true;
|
||||
}
|
||||
for (const pr of resp.data) {
|
||||
this.prs.set(pr.merge_commit_sha, pr);
|
||||
}
|
||||
}
|
||||
return this.prs.get(sha);
|
||||
}
|
||||
}
|
||||
function template_group(label, rows, prs) {
|
||||
if (rows.length > 0) {
|
||||
const group_prs = rows
|
||||
.map((merge) => `- ${merge.message} [#${merge.id}](${merge.href})`)
|
||||
.join("\n");
|
||||
return `${label}\n\n${group_prs}\n`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function template_release(args) {
|
||||
const prs = Object.keys(args.row)
|
||||
.map((label) => template_group(label, args.row[label]))
|
||||
.filter((x) => x.length > 0)
|
||||
.join("\n");
|
||||
return `# [${args.release.title}](https://github.com/perspective-dev/perspective/releases/tag/${args.release.title})
|
||||
|
||||
_${args.release.niceDate}_ ([Full changelog](${args.release.href}))
|
||||
|
||||
${prs}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the changelog text from the output of `auto-changelog`, iteratively calling
|
||||
* Github's API when necessary to get label information for grouping.
|
||||
*
|
||||
* @param {*} json the output data from `auto-changelog`
|
||||
* @returns
|
||||
*/
|
||||
async function template(json) {
|
||||
const cache = new GithubPRCache();
|
||||
let changelog = "";
|
||||
for (const release of json) {
|
||||
const row = {
|
||||
"**Breaking**": [],
|
||||
Features: [],
|
||||
Fixes: [],
|
||||
Misc: [],
|
||||
};
|
||||
for (const merge of release.merges) {
|
||||
const pr = await cache.get(merge.commit.hash);
|
||||
merge.pr = pr;
|
||||
if (pr) {
|
||||
const labels = pr.labels.map((x) => x.name);
|
||||
if (labels.indexOf("breaking") > -1) {
|
||||
row["**Breaking**"].push(merge);
|
||||
} else if (labels.indexOf("enhancement") > -1) {
|
||||
row.Features.push(merge);
|
||||
} else if (labels.indexOf("bug") > -1) {
|
||||
row.Fixes.push(merge);
|
||||
} else {
|
||||
row.Misc.push(merge);
|
||||
}
|
||||
} else {
|
||||
row.Misc.push(merge);
|
||||
}
|
||||
}
|
||||
changelog += template_release({
|
||||
row,
|
||||
release,
|
||||
});
|
||||
}
|
||||
return changelog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the `CHANGELOG.md` file from Github API.
|
||||
*/
|
||||
async function update_changelog() {
|
||||
let options = {
|
||||
remote: "origin",
|
||||
commitLimit: 10000,
|
||||
backfillLimit: 10000,
|
||||
tagPrefix: "",
|
||||
sortCommits: "relevance",
|
||||
appendGitLog: "",
|
||||
appendGitTag: "",
|
||||
latestVersion: NEW_VERSION,
|
||||
};
|
||||
const remote = await fetchRemote(options);
|
||||
options = {
|
||||
...options,
|
||||
...remote,
|
||||
};
|
||||
console.log("Fetching tags…");
|
||||
const tags = await fetchTags(options);
|
||||
console.log(`${tags.length} version tags found…`);
|
||||
const onParsed = ({ title }) => console.log(`Fetched ${title}…`);
|
||||
const json = await parseReleases(tags, options, onParsed);
|
||||
const changelog = await template(json);
|
||||
fs.writeFileSync("./CHANGELOG.md", changelog);
|
||||
$.sync`git add CHANGELOG.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all project `package.json` files.
|
||||
*/
|
||||
async function update_package_jsons() {
|
||||
const pkg = JSON.parse(fs.readFileSync("./package.json"));
|
||||
pkg.version = NEW_VERSION;
|
||||
const pkg_json = `${JSON.stringify(pkg, undefined, 4)}\n`;
|
||||
fs.writeFileSync("./package.json", pkg_json);
|
||||
const packages = {};
|
||||
for (const ws of pkg.workspaces) {
|
||||
if (ws === "packages/jupyterlab" && IS_NIGHTLY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const path of glob.sync(`${ws}/package.json`, {
|
||||
sync: true,
|
||||
})) {
|
||||
const pkg = JSON.parse(fs.readFileSync(path));
|
||||
pkg.version = NEW_VERSION;
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, undefined, 4) + "\n");
|
||||
packages[pkg.name] = {
|
||||
pkg,
|
||||
path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const cargo = toml.parse(fs.readFileSync("./Cargo.toml"));
|
||||
for (const pkg of cargo.workspace.members) {
|
||||
const crate = toml.parse(fs.readFileSync(`./${pkg}/Cargo.toml`));
|
||||
if (crate?.package.version) {
|
||||
crate.package.version = NEW_VERSION;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(crate?.dependencies)) {
|
||||
if (key.startsWith("perspective")) {
|
||||
crate.dependencies[key].version = NEW_VERSION;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(`./${pkg}/Cargo.toml`, toml.stringify(crate));
|
||||
}
|
||||
|
||||
// if (!IS_NIGHTLY) {
|
||||
const pyproject = toml.parse(
|
||||
fs.readFileSync(`./rust/perspective-python/pyproject.toml`),
|
||||
);
|
||||
|
||||
pyproject.tool.maturin.data = `perspective_python-${NEW_VERSION}.data`;
|
||||
fs.writeFileSync(
|
||||
`./rust/perspective-python/pyproject.toml`,
|
||||
toml.stringify(pyproject),
|
||||
);
|
||||
// }
|
||||
}
|
||||
|
||||
if (!IS_NIGHTLY) {
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
throw new Error("Missing GITHUB_TOKEN");
|
||||
}
|
||||
|
||||
await update_changelog();
|
||||
}
|
||||
|
||||
await update_package_jsons();
|
||||
@@ -0,0 +1,57 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import fs from "node:fs";
|
||||
import url from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
const workspaceRoot = path.normalize(path.join(__dirname, "..", ".."));
|
||||
const rustTargetDir = path.join(workspaceRoot, "rust", "target");
|
||||
const rustWheelsDir = path.join(rustTargetDir, "wheels");
|
||||
|
||||
const memoize = (f) => {
|
||||
let val = undefined;
|
||||
return () => {
|
||||
if (typeof val !== "undefined") return val;
|
||||
val = f();
|
||||
return val;
|
||||
};
|
||||
};
|
||||
|
||||
export function getWorkspaceRoot() {
|
||||
return workspaceRoot;
|
||||
}
|
||||
|
||||
export function getRustTargetDir() {
|
||||
return rustTargetDir;
|
||||
}
|
||||
|
||||
export function getRustWheelsDir() {
|
||||
return rustWheelsDir;
|
||||
}
|
||||
|
||||
export function getEmscriptenWheelPath() {
|
||||
const pspVersion = getWorkspacePackageJson().version.replace("-", ".");
|
||||
const wheeljunk = "cp311-abi3-pyemscripten_2025_0_wasm32";
|
||||
return path.join(
|
||||
rustWheelsDir,
|
||||
`perspective_python-${pspVersion}-${wheeljunk}.whl`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns memoized, deserialized contents of workspace package.json
|
||||
*/
|
||||
export const getWorkspacePackageJson = memoize(() =>
|
||||
JSON.parse(fs.readFileSync(path.join(workspaceRoot, "package.json"))),
|
||||
);
|
||||
Binary file not shown.
@@ -0,0 +1,100 @@
|
||||
Row ID,Order ID,Order Date,Ship Date,Ship Mode,Customer ID,Segment,Country,City,State,Postal Code,Region,Product ID,Category,Sub-Category,Sales,Quantity,Discount,Profit
|
||||
1,CA-2013-152156,11/9/2013,11/12/2013,Second Class,CG-12520,Consumer,United States,Henderson,Kentucky,42420,South,FUR-BO-10001798,Furniture,Bookcases,261.96,2,0.0,41.9136
|
||||
2,CA-2013-152156,11/9/2013,11/12/2013,Second Class,CG-12520,Consumer,United States,Henderson,Kentucky,42420,South,FUR-CH-10000454,Furniture,Chairs,731.94,3,0.0,219.582
|
||||
3,CA-2013-138688,6/13/2013,6/17/2013,Second Class,DV-13045,Corporate,United States,Los Angeles,California,90036,West,OFF-LA-10000240,Office Supplies,Labels,14.62,2,0.0,6.8714
|
||||
4,US-2012-108966,10/11/2012,10/18/2012,Standard Class,SO-20335,Consumer,United States,Fort Lauderdale,Florida,33311,South,FUR-TA-10000577,Furniture,Tables,957.5775,5,0.45,-383.031
|
||||
5,US-2012-108966,10/11/2012,10/18/2012,Standard Class,SO-20335,Consumer,United States,Fort Lauderdale,Florida,33311,South,OFF-ST-10000760,Office Supplies,Storage,22.368,2,0.2,2.5164
|
||||
6,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,FUR-FU-10001487,Furniture,Furnishings,48.86,7,0.0,14.1694
|
||||
7,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-AR-10002833,Office Supplies,Art,7.28,4,0.0,1.9656
|
||||
8,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,TEC-PH-10002275,Technology,Phones,907.152,6,0.2,90.7152
|
||||
9,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-BI-10003910,Office Supplies,Binders,18.504,3,0.2,5.7825
|
||||
10,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-AP-10002892,Office Supplies,Appliances,114.9,5,0.0,34.47
|
||||
11,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,FUR-TA-10001539,Furniture,Tables,1706.184,9,0.2,85.3092
|
||||
12,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,TEC-PH-10002033,Technology,Phones,911.424,4,0.2,68.3568
|
||||
13,CA-2014-114412,4/16/2014,4/21/2014,Standard Class,AA-10480,Consumer,United States,Concord,North Carolina,28027,South,OFF-PA-10002365,Office Supplies,Paper,15.552,3,0.2,5.4432
|
||||
14,CA-2013-161389,12/6/2013,12/11/2013,Standard Class,IM-15070,Consumer,United States,Seattle,Washington,98103,West,OFF-BI-10003656,Office Supplies,Binders,407.976,3,0.2,132.5922
|
||||
15,US-2012-118983,11/22/2012,11/26/2012,Standard Class,HP-14815,Home Office,United States,Fort Worth,Texas,76106,Central,OFF-AP-10002311,Office Supplies,Appliances,68.81,5,0.8,-123.858
|
||||
16,US-2012-118983,11/22/2012,11/26/2012,Standard Class,HP-14815,Home Office,United States,Fort Worth,Texas,76106,Central,OFF-BI-10000756,Office Supplies,Binders,2.544,3,0.8,-3.816
|
||||
17,CA-2011-105893,11/11/2011,11/18/2011,Standard Class,PK-19075,Consumer,United States,Madison,Wisconsin,53711,Central,OFF-ST-10004186,Office Supplies,Storage,665.88,6,0.0,13.3176
|
||||
18,CA-2011-167164,5/13/2011,5/15/2011,Second Class,AG-10270,Consumer,United States,West Jordan,Utah,84084,West,OFF-ST-10000107,Office Supplies,Storage,55.5,2,0.0,9.99
|
||||
19,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,OFF-AR-10003056,Office Supplies,Art,8.56,2,0.0,2.4824
|
||||
20,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,TEC-PH-10001949,Technology,Phones,213.48,3,0.2,16.011
|
||||
21,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,OFF-BI-10002215,Office Supplies,Binders,22.72,4,0.2,7.384
|
||||
22,CA-2013-137330,12/10/2013,12/14/2013,Standard Class,KB-16585,Corporate,United States,Fremont,Nebraska,68025,Central,OFF-AR-10000246,Office Supplies,Art,19.46,7,0.0,5.0596
|
||||
23,CA-2013-137330,12/10/2013,12/14/2013,Standard Class,KB-16585,Corporate,United States,Fremont,Nebraska,68025,Central,OFF-AP-10001492,Office Supplies,Appliances,60.34,7,0.0,15.6884
|
||||
24,US-2014-156909,7/17/2014,7/19/2014,Second Class,SF-20065,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-CH-10002774,Furniture,Chairs,71.372,2,0.3,-1.0196
|
||||
25,CA-2012-106320,9/25/2012,9/30/2012,Standard Class,EB-13870,Consumer,United States,Orem,Utah,84057,West,FUR-TA-10000577,Furniture,Tables,1044.63,3,0.0,240.2649
|
||||
26,CA-2013-121755,1/16/2013,1/20/2013,Second Class,EH-13945,Consumer,United States,Los Angeles,California,90049,West,OFF-BI-10001634,Office Supplies,Binders,11.648,2,0.2,4.2224
|
||||
27,CA-2013-121755,1/16/2013,1/20/2013,Second Class,EH-13945,Consumer,United States,Los Angeles,California,90049,West,TEC-AC-10003027,Technology,Accessories,90.57,3,0.0,11.7741
|
||||
28,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-BO-10004834,Furniture,Bookcases,3083.43,7,0.5,-1665.0522
|
||||
29,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-BI-10000474,Office Supplies,Binders,9.618,2,0.7,-7.0532
|
||||
30,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-FU-10004848,Furniture,Furnishings,124.2,3,0.2,15.525
|
||||
31,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-EN-10001509,Office Supplies,Envelopes,3.264,2,0.2,1.1016
|
||||
32,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-AR-10004042,Office Supplies,Art,86.304,6,0.2,9.7092
|
||||
33,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-BI-10001525,Office Supplies,Binders,6.858,6,0.7,-5.715
|
||||
34,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-AR-10001683,Office Supplies,Art,15.76,2,0.2,3.546
|
||||
35,CA-2014-107727,10/20/2014,10/24/2014,Second Class,MA-17560,Home Office,United States,Houston,Texas,77095,Central,OFF-PA-10000249,Office Supplies,Paper,29.472,3,0.2,9.9468
|
||||
36,CA-2013-117590,12/9/2013,12/11/2013,First Class,GH-14485,Corporate,United States,Richardson,Texas,75080,Central,TEC-PH-10004977,Technology,Phones,1097.544,7,0.2,123.4737
|
||||
37,CA-2013-117590,12/9/2013,12/11/2013,First Class,GH-14485,Corporate,United States,Richardson,Texas,75080,Central,FUR-FU-10003664,Furniture,Furnishings,190.92,5,0.6,-147.963
|
||||
38,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,OFF-EN-10002986,Office Supplies,Envelopes,113.328,9,0.2,35.415
|
||||
39,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,FUR-BO-10002545,Furniture,Bookcases,532.3992,3,0.32,-46.9764
|
||||
40,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,FUR-CH-10004218,Furniture,Chairs,212.058,3,0.3,-15.147
|
||||
41,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,TEC-PH-10000486,Technology,Phones,371.168,4,0.2,41.7564
|
||||
42,CA-2014-120999,9/11/2014,9/16/2014,Standard Class,LC-16930,Corporate,United States,Naperville,Illinois,60540,Central,TEC-PH-10004093,Technology,Phones,147.168,4,0.2,16.5564
|
||||
43,CA-2013-101343,7/18/2013,7/23/2013,Standard Class,RA-19885,Corporate,United States,Los Angeles,California,90049,West,OFF-ST-10003479,Office Supplies,Storage,77.88,2,0.0,3.894
|
||||
44,CA-2014-139619,9/20/2014,9/24/2014,Standard Class,ES-14080,Corporate,United States,Melbourne,Florida,32935,South,OFF-ST-10003282,Office Supplies,Storage,95.616,2,0.2,9.5616
|
||||
45,CA-2013-118255,3/12/2013,3/14/2013,First Class,ON-18715,Corporate,United States,Eagan,Minnesota,55122,Central,TEC-AC-10000171,Technology,Accessories,45.98,2,0.0,19.7714
|
||||
46,CA-2013-118255,3/12/2013,3/14/2013,First Class,ON-18715,Corporate,United States,Eagan,Minnesota,55122,Central,OFF-BI-10003291,Office Supplies,Binders,17.46,2,0.0,8.2062
|
||||
47,CA-2011-146703,10/20/2011,10/25/2011,Second Class,PO-18865,Consumer,United States,Westland,Michigan,48185,Central,OFF-ST-10001713,Office Supplies,Storage,211.96,4,0.0,8.4784
|
||||
48,CA-2013-169194,6/21/2013,6/26/2013,Standard Class,LH-16900,Consumer,United States,Dover,Delaware,19901,East,TEC-AC-10002167,Technology,Accessories,45.0,3,0.0,4.95
|
||||
49,CA-2013-169194,6/21/2013,6/26/2013,Standard Class,LH-16900,Consumer,United States,Dover,Delaware,19901,East,TEC-PH-10003988,Technology,Phones,21.8,2,0.0,6.104
|
||||
50,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,OFF-BI-10004410,Office Supplies,Binders,38.22,6,0.0,17.9634
|
||||
51,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,OFF-LA-10002762,Office Supplies,Labels,75.18,6,0.0,35.3346
|
||||
52,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,FUR-FU-10001706,Furniture,Furnishings,6.16,2,0.0,2.9568
|
||||
53,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,FUR-CH-10003061,Furniture,Chairs,89.99,1,0.0,17.0981
|
||||
54,CA-2013-105816,12/12/2013,12/18/2013,Standard Class,JM-15265,Corporate,United States,New York City,New York,10024,East,OFF-FA-10000304,Office Supplies,Fasteners,15.26,7,0.0,6.2566
|
||||
55,CA-2013-105816,12/12/2013,12/18/2013,Standard Class,JM-15265,Corporate,United States,New York City,New York,10024,East,TEC-PH-10002447,Technology,Phones,1029.95,5,0.0,298.6855
|
||||
56,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-ST-10000604,Office Supplies,Storage,208.56,6,0.0,52.14
|
||||
57,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-PA-10001569,Office Supplies,Paper,32.4,5,0.0,15.552
|
||||
58,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,FUR-CH-10003968,Furniture,Chairs,319.41,5,0.1,7.098
|
||||
59,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-PA-10000587,Office Supplies,Paper,14.56,2,0.0,6.9888
|
||||
60,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,TEC-AC-10002167,Technology,Accessories,30.0,2,0.0,3.3
|
||||
61,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-BI-10001460,Office Supplies,Binders,48.48,4,0.2,16.362
|
||||
62,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-AR-10001868,Office Supplies,Art,1.68,1,0.0,0.84
|
||||
63,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,TEC-AC-10004633,Technology,Accessories,13.98,2,0.0,6.1512
|
||||
64,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,OFF-BI-10001078,Office Supplies,Binders,25.824,6,0.2,9.3612
|
||||
65,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,OFF-PA-10003892,Office Supplies,Paper,146.73,3,0.0,68.9631
|
||||
66,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,FUR-FU-10000397,Furniture,Furnishings,79.76,4,0.0,22.3328
|
||||
67,US-2012-164175,4/30/2012,5/5/2012,Standard Class,PS-18970,Home Office,United States,Chicago,Illinois,60610,Central,FUR-CH-10001146,Furniture,Chairs,213.115,5,0.3,-15.2225
|
||||
68,CA-2011-106376,12/5/2011,12/10/2011,Standard Class,BS-11590,Corporate,United States,Gilbert,Arizona,85234,West,OFF-AR-10002671,Office Supplies,Art,1113.024,8,0.2,111.3024
|
||||
69,CA-2011-106376,12/5/2011,12/10/2011,Standard Class,BS-11590,Corporate,United States,Gilbert,Arizona,85234,West,TEC-PH-10002726,Technology,Phones,167.968,4,0.2,62.988
|
||||
70,CA-2013-119823,6/5/2013,6/7/2013,First Class,KD-16270,Consumer,United States,Springfield,Virginia,22153,South,OFF-PA-10000482,Office Supplies,Paper,75.88,2,0.0,35.6636
|
||||
71,CA-2013-106075,9/19/2013,9/24/2013,Standard Class,HM-14980,Consumer,United States,New York City,New York,10009,East,OFF-BI-10004654,Office Supplies,Binders,4.616,1,0.2,1.731
|
||||
72,CA-2014-114440,9/15/2014,9/18/2014,Second Class,TB-21520,Consumer,United States,Jackson,Michigan,49201,Central,OFF-PA-10004675,Office Supplies,Paper,19.05,3,0.0,8.763
|
||||
73,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,FUR-CH-10000513,Furniture,Chairs,831.936,8,0.2,-114.3912
|
||||
74,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,FUR-FU-10003708,Furniture,Furnishings,97.04,2,0.2,1.213
|
||||
75,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,OFF-ST-10004123,Office Supplies,Storage,72.784,1,0.2,-18.196
|
||||
76,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,OFF-BI-10004182,Office Supplies,Binders,1.248,3,0.8,-1.9344
|
||||
77,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,FUR-FU-10000260,Furniture,Furnishings,9.708,3,0.6,-5.8248
|
||||
78,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,OFF-ST-10000615,Office Supplies,Storage,27.24,3,0.2,2.724
|
||||
79,US-2011-147606,11/26/2011,12/1/2011,Second Class,JE-15745,Consumer,United States,Houston,Texas,77070,Central,FUR-FU-10003194,Furniture,Furnishings,19.3,5,0.6,-14.475
|
||||
80,CA-2013-127208,6/13/2013,6/16/2013,First Class,SC-20770,Corporate,United States,Decatur,Alabama,35601,South,OFF-AP-10002118,Office Supplies,Appliances,208.16,1,0.0,56.2032
|
||||
81,CA-2013-127208,6/13/2013,6/16/2013,First Class,SC-20770,Corporate,United States,Decatur,Alabama,35601,South,OFF-BI-10002309,Office Supplies,Binders,16.74,3,0.0,8.0352
|
||||
82,CA-2011-139451,10/12/2011,10/16/2011,Standard Class,DN-13690,Consumer,United States,San Francisco,California,94122,West,OFF-AR-10002053,Office Supplies,Art,14.9,5,0.0,4.172
|
||||
83,CA-2011-139451,10/12/2011,10/16/2011,Standard Class,DN-13690,Consumer,United States,San Francisco,California,94122,West,OFF-ST-10002370,Office Supplies,Storage,21.39,1,0.0,6.2031
|
||||
84,CA-2012-149734,9/3/2012,9/8/2012,Standard Class,JC-16105,Corporate,United States,Durham,North Carolina,27707,South,OFF-EN-10000927,Office Supplies,Envelopes,200.984,7,0.2,62.8075
|
||||
85,US-2014-119662,11/14/2014,11/17/2014,First Class,CS-12400,Home Office,United States,Chicago,Illinois,60623,Central,OFF-ST-10003656,Office Supplies,Storage,230.376,3,0.2,-48.9549
|
||||
86,CA-2014-140088,5/29/2014,5/31/2014,Second Class,PO-18865,Consumer,United States,Columbia,South Carolina,29203,South,FUR-CH-10000863,Furniture,Chairs,301.96,2,0.0,33.2156
|
||||
87,CA-2014-155558,10/27/2014,11/3/2014,Standard Class,PG-18895,Consumer,United States,Rochester,Minnesota,55901,Central,TEC-AC-10001998,Technology,Accessories,19.99,1,0.0,6.7966
|
||||
88,CA-2014-155558,10/27/2014,11/3/2014,Standard Class,PG-18895,Consumer,United States,Rochester,Minnesota,55901,Central,OFF-LA-10000134,Office Supplies,Labels,6.16,2,0.0,2.9568
|
||||
89,CA-2013-159695,4/6/2013,4/11/2013,Second Class,GM-14455,Home Office,United States,Houston,Texas,77095,Central,OFF-ST-10003442,Office Supplies,Storage,158.368,7,0.2,13.8572
|
||||
90,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,OFF-AR-10004930,Office Supplies,Art,20.1,3,0.0,6.633
|
||||
91,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,TEC-PH-10004093,Technology,Phones,73.584,2,0.2,8.2782
|
||||
92,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,OFF-PA-10000304,Office Supplies,Paper,6.48,1,0.0,3.1104
|
||||
93,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,OFF-PA-10003177,Office Supplies,Paper,12.96,2,0.0,6.2208
|
||||
94,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,FUR-FU-10003799,Furniture,Furnishings,53.34,3,0.0,16.5354
|
||||
95,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,OFF-BI-10002852,Office Supplies,Binders,32.96,2,0.0,16.1504
|
||||
96,US-2014-109484,11/7/2014,11/13/2014,Standard Class,RB-19705,Home Office,United States,Portland,Oregon,97206,West,OFF-BI-10004738,Office Supplies,Binders,5.682,1,0.7,-3.788
|
||||
97,CA-2014-161018,11/10/2014,11/12/2014,Second Class,PN-18775,Home Office,United States,New York City,New York,10009,East,FUR-FU-10000629,Furniture,Furnishings,96.53,7,0.0,40.5426
|
||||
98,CA-2014-157833,6/18/2014,6/21/2014,First Class,KD-16345,Consumer,United States,San Francisco,California,94122,West,OFF-BI-10001721,Office Supplies,Binders,51.312,3,0.2,17.9592
|
||||
99,CA-2013-149223,9/7/2013,9/12/2013,Standard Class,ER-13855,Corporate,United States,Saint Paul,Minnesota,55106,Central,OFF-AP-10000358,Office Supplies,Appliances,77.88,6,0.0,22.5852
|
||||
|
@@ -0,0 +1,33 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/all_types_small_multi.arrow",
|
||||
);
|
||||
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(buffer);
|
||||
await viewer.load(table);
|
||||
const config = {
|
||||
plugin: "datagrid",
|
||||
};
|
||||
await viewer.restore(config);
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,30 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "http://localhost:6598/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "http://localhost:6598/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"http://localhost:6598/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, { index: "Row ID" });
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,33 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, {
|
||||
index: "Row ID",
|
||||
name: "load-viewer-csv",
|
||||
});
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,41 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, { name: "load-viewer-superstore" });
|
||||
await viewer.load(table);
|
||||
const config = {
|
||||
plugin: "datagrid",
|
||||
group_by: ["Region", "State"],
|
||||
split_by: ["Category", "Sub-Category"],
|
||||
columns: ["Sales", "Profit"],
|
||||
master: false,
|
||||
name: "Sales Report",
|
||||
table: "load-viewer-superstore",
|
||||
linked: false,
|
||||
title: "Sales Report 2",
|
||||
};
|
||||
await viewer.restore(config);
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,42 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// Two-viewer fixture: both viewers bind to a single shared table so
|
||||
// Suite E can drive each chart independently and verify cross-chart
|
||||
// isolation (a pan on viewer A must not affect viewer B's plot
|
||||
// region; a streaming `table.update` on viewer A's table — which is
|
||||
// also viewer B's table — must surface as data updates on both
|
||||
// without blanking either).
|
||||
async function load() {
|
||||
const resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
const csv = await resp.text();
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(csv, {
|
||||
index: "Row ID",
|
||||
name: "load-viewer-two-csv",
|
||||
});
|
||||
|
||||
const viewers = document.querySelectorAll("perspective-viewer");
|
||||
for (const viewer of viewers) {
|
||||
await viewer.load(table);
|
||||
}
|
||||
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,22 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
const worker = await perspective.worker();
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,36 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/node_modules/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
let arrow = await resp.arrayBuffer();
|
||||
const worker = await perspective.worker();
|
||||
window.__WORKER__ = worker;
|
||||
const table = await worker.table(arrow, { name: "superstore" });
|
||||
let workspace = document.getElementById("workspace");
|
||||
window.__TABLE__ = table;
|
||||
if (workspace) {
|
||||
await workspace.load(worker);
|
||||
} else {
|
||||
window.__TABLE__ = table;
|
||||
}
|
||||
}
|
||||
|
||||
await load();
|
||||
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@perspective-dev/test",
|
||||
"version": "4.5.2",
|
||||
"description": "Test utility based on perspective",
|
||||
"private": true,
|
||||
"main": "src/js/index.ts",
|
||||
"module": "src/js/index.ts",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"src/**/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective/tools/test"
|
||||
},
|
||||
"scripts": {
|
||||
"prebench": "mkdirp build",
|
||||
"bench": "node bench/versions.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"superstore-arrow": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@playwright/experimental-ct-react": "catalog:",
|
||||
"zx": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { defineConfig, devices } from "@playwright/experimental-ct-react";
|
||||
import path from "path";
|
||||
import * as dotenv from "dotenv";
|
||||
import { createRequire } from "node:module";
|
||||
import url from "node:url";
|
||||
import { execSync } from "child_process";
|
||||
// @ts-ignore -- sh_perspective.mjs is plain JS without type declarations
|
||||
import { get_scope } from "../scripts/sh_perspective.mjs";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
// TODO Don't hardcode this. AFAICT this can only be accomplished by choosing
|
||||
// the port before calling the playwright CLI, via env var.
|
||||
const TEST_SERVER_PORT = 6598;
|
||||
|
||||
const RUN_JUPYTERLAB = !!process.env.PSP_JUPYTERLAB_TESTS;
|
||||
|
||||
// TODO use this from core
|
||||
const package_venn = (get_scope() as string[]).reduce(
|
||||
(acc: { include: string[]; exclude: string[] }, x: string) => {
|
||||
if (x.startsWith("!")) {
|
||||
acc.exclude.push(x);
|
||||
} else {
|
||||
acc.include.push(x);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ include: [] as string[], exclude: [] as string[] },
|
||||
);
|
||||
|
||||
let PACKAGE: string[] = [];
|
||||
if (package_venn.include.length === 0) {
|
||||
PACKAGE = (
|
||||
JSON.parse(execSync(`pnpm m ls --json --depth=-1`).toString()) as {
|
||||
name?: string;
|
||||
}[]
|
||||
)
|
||||
.filter((x): x is { name: string } => x.name !== undefined)
|
||||
.map((x) => x.name.replace("@perspective-dev/", ""))
|
||||
.filter((x: string) => package_venn.exclude.indexOf(`!${x}`) === -1);
|
||||
} else {
|
||||
PACKAGE = package_venn.include.filter(
|
||||
(x: string) => package_venn.exclude.indexOf(`!${x}`) === -1,
|
||||
);
|
||||
}
|
||||
|
||||
const DEVICE_OPTIONS = {
|
||||
"Desktop Chrome": {
|
||||
// Lock DPR to 1 so WebGL viewport / canvas backing sizes are
|
||||
// identical across host machines (matters for pixel snapshots).
|
||||
// This also overrides the `devices["Desktop Chrome"]` default of 2.
|
||||
deviceScaleFactor: 1,
|
||||
launchOptions: {
|
||||
args: [
|
||||
// "--disable-accelerated-2d-canvas",
|
||||
// "--disable-gpu",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--font-render-hinting=none",
|
||||
'--proxy-server="direct://"',
|
||||
"--proxy-bypass-list=*",
|
||||
"--js-flags=--expose-gc",
|
||||
"--enable-precise-memory-info",
|
||||
// Use the software WebGL backend so GPU/driver differences
|
||||
// between machines don't leak into snapshot pixels. The
|
||||
// 2× perf hit is under budget for the test suite.
|
||||
"--use-gl=swiftshader",
|
||||
"--use-angle=swiftshader",
|
||||
"--force-color-profile=srgb",
|
||||
"--disable-lcd-text",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const BROWSER_PACKAGES = [
|
||||
{
|
||||
packageName: "viewer",
|
||||
testDir: "rust/perspective-viewer/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "react",
|
||||
testDir: "packages/react/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "viewer-datagrid",
|
||||
testDir: "packages/viewer-datagrid/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "viewer-charts",
|
||||
testDir: "packages/viewer-charts/test/ts",
|
||||
},
|
||||
{
|
||||
packageName: "jupyterlab",
|
||||
testDir: "packages/jupyterlab/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "workspace",
|
||||
testDir: "packages/workspace/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "docs",
|
||||
testDir: "docs/test/js",
|
||||
},
|
||||
];
|
||||
|
||||
const NODE_PACKAGES = [
|
||||
{
|
||||
packageName: "client",
|
||||
testDir: "rust/perspective-js/test/js",
|
||||
},
|
||||
];
|
||||
|
||||
const BROWSER_AND_PYTHON_PACKAGES = [
|
||||
{
|
||||
packageName: "python-perspective-jupyterlab",
|
||||
testDir: "packages/jupyterlab/test/jupyter",
|
||||
},
|
||||
];
|
||||
|
||||
type ProjectConfig = NonNullable<
|
||||
Parameters<typeof defineConfig>[0]["projects"]
|
||||
>[number];
|
||||
|
||||
let PROJECTS = (() => {
|
||||
const acc: ProjectConfig[] = [];
|
||||
if (RUN_JUPYTERLAB) {
|
||||
for (const pkg of BROWSER_AND_PYTHON_PACKAGES) {
|
||||
for (const device of Object.keys(
|
||||
DEVICE_OPTIONS,
|
||||
) as (keyof typeof DEVICE_OPTIONS)[]) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-${device
|
||||
.toLowerCase()
|
||||
.replace(" ", "-")}`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
use: {
|
||||
...devices[device],
|
||||
// baseURL: `http://localhost:${TEST_SERVER_PORT}`,
|
||||
timezoneId: "UTC",
|
||||
launchOptions: {
|
||||
args: ["--js-flags=--expose-gc"],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const pkg of NODE_PACKAGES) {
|
||||
if (PACKAGE == undefined || PACKAGE.includes(pkg.packageName)) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-node`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const pkg of BROWSER_PACKAGES) {
|
||||
if (PACKAGE == undefined || PACKAGE.includes(pkg.packageName)) {
|
||||
for (const device of Object.keys(
|
||||
DEVICE_OPTIONS,
|
||||
) as (keyof typeof DEVICE_OPTIONS)[]) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-${device
|
||||
.toLowerCase()
|
||||
.replace(" ", "-")}`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
use: {
|
||||
...devices[device],
|
||||
...DEVICE_OPTIONS[device],
|
||||
baseURL: `http://localhost:${TEST_SERVER_PORT}`,
|
||||
timezoneId: "UTC",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
})();
|
||||
|
||||
const __require = createRequire(import.meta.url);
|
||||
|
||||
const GLOBAL_SETUP_PATH = __require.resolve(
|
||||
"@perspective-dev/jupyterlab/test/config/jupyter/globalSetup.ts",
|
||||
);
|
||||
|
||||
const GLOBAL_TEARDOWN_PATH = __require.resolve(
|
||||
"@perspective-dev/jupyterlab/test/config/jupyter/globalTeardown.ts",
|
||||
);
|
||||
|
||||
// See https://playwright.dev/docs/test-configuration.
|
||||
export default defineConfig({
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 30_000,
|
||||
},
|
||||
repeatEach: process.env.PSP_SATURATE
|
||||
? parseInt(process.env.PSP_SATURATE)
|
||||
: 0,
|
||||
forbidOnly: !!process.env.CI,
|
||||
workers: process.env.PSP_DEBUG ? 1 : "50%",
|
||||
retries: 0,
|
||||
quiet: !process.env.PSP_DEBUG,
|
||||
reporter: process.env.CI ? [["github"], ["html"]] : [["dot"]],
|
||||
projects: PROJECTS,
|
||||
outputDir: "dist/results",
|
||||
use: {
|
||||
launchOptions: {
|
||||
slowMo: 500, // 500 milliseconds delay
|
||||
},
|
||||
headless: !process.env.PSP_HEADED,
|
||||
ctPort: 3100,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
actionTimeout: 0,
|
||||
// trace: "retain-on-failure",
|
||||
// screenshot: "only-on-failure",
|
||||
// video: "retain-on-failure",
|
||||
},
|
||||
updateSnapshots: "none",
|
||||
globalSetup: RUN_JUPYTERLAB
|
||||
? GLOBAL_SETUP_PATH
|
||||
: path.join(__dirname, "src/js/global_startup.ts"),
|
||||
globalTeardown: RUN_JUPYTERLAB
|
||||
? GLOBAL_TEARDOWN_PATH
|
||||
: path.join(__dirname, "src/js/global_teardown.ts"),
|
||||
snapshotPathTemplate:
|
||||
"dist/snapshots/{projectName}/{testFilePath}/{arg}{ext}",
|
||||
webServer: {
|
||||
command: "tsx src/js/start_test_server.ts",
|
||||
port: TEST_SERVER_PORT,
|
||||
reuseExistingServer: true,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Testing Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-all-types-small-multi-arrow.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-csv.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-superstore.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-two-csv.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.panel.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.panel.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="panel left"><perspective-viewer></perspective-viewer></div>
|
||||
<div class="panel right"><perspective-viewer></perspective-viewer></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-workspace-arrow.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/workspace/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
perspective-workspace {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
perspective-workspace,
|
||||
perspective-viewer {
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-workspace id="workspace"></perspective-workspace>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
|
||||
type Logs = { [x: string]: string[] };
|
||||
type LogFilter = RegExp | [RegExp, { expected: boolean }];
|
||||
type ExpectedLogs = {
|
||||
[x: string]: LogFilter[];
|
||||
} & {
|
||||
push: (type: string, filter: LogFilter) => void;
|
||||
};
|
||||
export const test = base.extend<{
|
||||
consoleLogs: { logs: Logs; expectedLogs: ExpectedLogs };
|
||||
}>({
|
||||
consoleLogs: [
|
||||
async ({ page }, use) => {
|
||||
const logs: Logs = {};
|
||||
|
||||
page.setDefaultTimeout(5000);
|
||||
|
||||
page.on("console", (msg) => {
|
||||
let type = msg.type();
|
||||
let text = msg.text();
|
||||
logs[type] ? logs[type].push(text) : (logs[type] = [text]);
|
||||
});
|
||||
let expectedLogsConstructor: any = {
|
||||
warning: [[/Legacy `expressions` format/, { expected: false }]],
|
||||
error: [[/RENDERED STUB/, { expected: false }]],
|
||||
};
|
||||
|
||||
// this is crazy
|
||||
expectedLogsConstructor.push = (
|
||||
type: string,
|
||||
filter: LogFilter
|
||||
) => {
|
||||
((
|
||||
expectedLogs: ExpectedLogs,
|
||||
type: string,
|
||||
filter: LogFilter
|
||||
) => {
|
||||
expectedLogs[type]
|
||||
? expectedLogs[type].push(filter)
|
||||
: (expectedLogs[type] = [filter]);
|
||||
})(expectedLogsConstructor, type, filter);
|
||||
};
|
||||
|
||||
let expectedLogs = expectedLogsConstructor as ExpectedLogs;
|
||||
|
||||
await use({ logs, expectedLogs });
|
||||
|
||||
Object.entries(expectedLogs).forEach(([type, filters]) => {
|
||||
if (typeof filters == "function") {
|
||||
return;
|
||||
}
|
||||
for (let filter of filters) {
|
||||
let filterText: string | RegExp,
|
||||
expected = true;
|
||||
if (Array.isArray(filter)) {
|
||||
filterText = filter[0];
|
||||
expected = filter[1].expected;
|
||||
} else {
|
||||
filterText = filter;
|
||||
}
|
||||
let matched_expr = logs[type]?.find((msg) =>
|
||||
msg.match(filterText)
|
||||
);
|
||||
let msgLogs = Array.from(Object.entries(logs)).map(
|
||||
([k, v]) =>
|
||||
`"${k}": [\n\t` +
|
||||
v.map((v) => `"${v}"`).join(",\n\t") +
|
||||
"\n],\n"
|
||||
);
|
||||
let message = `Filter was "${filterText}"\nLogs were: ${msgLogs}`;
|
||||
expected
|
||||
? test
|
||||
.expect(matched_expr, {
|
||||
message,
|
||||
})
|
||||
.toBeDefined()
|
||||
: test
|
||||
.expect(matched_expr, {
|
||||
message,
|
||||
})
|
||||
.toBeUndefined();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { fetchSnapshots } from "./snapshot-sync.js";
|
||||
|
||||
export default async function run() {
|
||||
if (process.env.PSP_FETCH_SNAPSHOTS) {
|
||||
await fetchSnapshots();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { writebackSnapshots } from "./snapshot-sync.js";
|
||||
|
||||
export default async function run() {
|
||||
if (process.env.PSP_UPDATE_SNAPSHOTS) {
|
||||
await writebackSnapshots();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
export * from "./utils.ts";
|
||||
export * from "./simple_viewer_tests.ts";
|
||||
export * from "./models/page.ts";
|
||||
export { test, expect } from "./fixture.ts";
|
||||
@@ -0,0 +1,170 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, expect } from "@playwright/test";
|
||||
import { PageView } from "./page";
|
||||
import type { ColumnType } from "@perspective-dev/client";
|
||||
|
||||
export class ColumnSettingsSidebar {
|
||||
view: PageView;
|
||||
container: Locator;
|
||||
attributesTab: AttributesTab;
|
||||
styleTab: StyleTab;
|
||||
closeBtn: Locator;
|
||||
tabTitle: Locator;
|
||||
nameInputWrapper: Locator;
|
||||
nameInput: Locator;
|
||||
selectedTab: Locator;
|
||||
typeIcon: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.view = view;
|
||||
const viewer = view.container;
|
||||
this.container = viewer.locator("#column_settings_sidebar");
|
||||
this.attributesTab = new AttributesTab(this.container);
|
||||
this.styleTab = new StyleTab(this.container);
|
||||
this.closeBtn = viewer.locator("#column_settings_close_button");
|
||||
// Tab DOM now uses `.settings_tab` / `.settings_tab.selected_tab`
|
||||
// (see `containers/tab_list.rs`); the previous `.tab-padding`
|
||||
// filler span was removed, so no `:not(.tab-padding)` filter is
|
||||
// needed. These class names are shared with the viewer's query
|
||||
// tabbar (`#query_tabbar_tab`), so the locators are scoped to
|
||||
// `this.container` (the sidebar root) to avoid strict-mode
|
||||
// collisions across the two tab implementations.
|
||||
this.tabTitle = this.container.locator(".settings_tab .tab-title");
|
||||
this.nameInputWrapper = view.container.locator(
|
||||
".sidebar_header_contents",
|
||||
);
|
||||
this.nameInput = view.container.locator("input.sidebar_header_title");
|
||||
this.selectedTab = this.container.locator(
|
||||
".settings_tab.selected_tab",
|
||||
);
|
||||
this.typeIcon = this.container.locator(".type-icon");
|
||||
}
|
||||
|
||||
async openTab(name: string) {
|
||||
let locator = this.container.locator("#" + name);
|
||||
await locator.click({ timeout: 1000 });
|
||||
await this.container
|
||||
.locator(`.settings_tab.selected_tab #${name}`)
|
||||
.waitFor({ timeout: 1000 });
|
||||
}
|
||||
|
||||
async getTabs(): Promise<string[]> {
|
||||
return await this.tabTitle.allInnerTexts();
|
||||
}
|
||||
|
||||
async getSelectedTab(): Promise<string> {
|
||||
return await this.selectedTab.innerText();
|
||||
}
|
||||
|
||||
async getType(): Promise<ColumnType | "expression"> {
|
||||
const classList = await this.typeIcon.evaluate((icon) =>
|
||||
Array.from(icon.classList),
|
||||
);
|
||||
for (const ty of [
|
||||
"string",
|
||||
"boolean",
|
||||
"float",
|
||||
"integer",
|
||||
"date",
|
||||
"datetime",
|
||||
]) {
|
||||
if (classList.includes(ty)) {
|
||||
return <ColumnType>ty;
|
||||
}
|
||||
}
|
||||
if (classList.includes("expression")) {
|
||||
return "expression";
|
||||
}
|
||||
throw new Error("Unable to get a matching type for the column!");
|
||||
}
|
||||
}
|
||||
|
||||
export class AttributesTab {
|
||||
container: Locator;
|
||||
expressionEditor: ExpressionEditor;
|
||||
saveBtn: Locator;
|
||||
resetBtn: Locator;
|
||||
deleteBtn: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#attributes-tab");
|
||||
this.expressionEditor = new ExpressionEditor(this.container);
|
||||
this.saveBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-save",
|
||||
);
|
||||
this.resetBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-reset",
|
||||
);
|
||||
this.deleteBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-delete",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionEditor {
|
||||
container: Locator;
|
||||
content: Locator;
|
||||
textarea: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#editor-container");
|
||||
this.content = this.container.locator("#content");
|
||||
this.textarea = this.container.locator("textarea");
|
||||
}
|
||||
}
|
||||
|
||||
export class StyleTab {
|
||||
container: Locator;
|
||||
contents: Locator;
|
||||
symbolsEditor: SymbolsEditor;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#style-tab");
|
||||
this.contents = parent.locator(".style_contents");
|
||||
this.symbolsEditor = new SymbolsEditor(this.container);
|
||||
}
|
||||
}
|
||||
export class SymbolsEditor {
|
||||
container: Locator;
|
||||
pairsList: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#attributes-symbols");
|
||||
this.pairsList = this.container.locator("ul");
|
||||
}
|
||||
async getSymbolPairs() {
|
||||
const names = await this.pairsList
|
||||
.locator(".column_name")
|
||||
.allInnerTexts();
|
||||
const symbols = await this.pairsList
|
||||
.locator("option[selected]")
|
||||
.allInnerTexts();
|
||||
const zipped: SymbolPair[] = [];
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
zipped.push(new SymbolPair(names[i], symbols[i]));
|
||||
}
|
||||
return zipped;
|
||||
}
|
||||
async getPairsLength() {
|
||||
return (await this.pairsList.locator("li").all()).length;
|
||||
}
|
||||
}
|
||||
export class SymbolPair {
|
||||
key: String;
|
||||
value: String;
|
||||
constructor(key: String, value: String) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
import { ColumnSettingsSidebar } from "./column_settings.js";
|
||||
import { ColumnSelector, ColumnType, SettingsPanel } from "./settings_panel.js";
|
||||
import { DataGridPlugin } from "./plugins.js";
|
||||
import type {
|
||||
HTMLPerspectiveViewerElement,
|
||||
ViewerConfigUpdate,
|
||||
} from "@perspective-dev/viewer";
|
||||
|
||||
/**
|
||||
* This class is the primary interface between Playwright tests and the items on the Perspective Viewer.
|
||||
* It contains various subobjects such as the SettingsPanel and ColumnSettingsSidebar which contain their own
|
||||
* functionality and locators.
|
||||
*/
|
||||
export class PageView {
|
||||
readonly page: Page;
|
||||
container: Locator;
|
||||
settingsPanel: SettingsPanel;
|
||||
settingsCloseButton: Locator;
|
||||
/** Opens the settings panel. */
|
||||
settingsButton: Locator;
|
||||
columnSettingsSidebar: ColumnSettingsSidebar;
|
||||
|
||||
// plugins
|
||||
dataGrid: DataGridPlugin.DataGrid;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.container = page.locator("perspective-viewer");
|
||||
this.settingsCloseButton = this.container.locator(
|
||||
"#settings_close_button",
|
||||
);
|
||||
this.settingsButton = this.container.locator("#settings_button");
|
||||
this.columnSettingsSidebar = new ColumnSettingsSidebar(this);
|
||||
this.settingsPanel = new SettingsPanel(this);
|
||||
|
||||
this.dataGrid = new DataGridPlugin.DataGrid(page);
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.container.evaluate(async (viewer) => {
|
||||
let el = viewer as unknown as HTMLPerspectiveViewerElement;
|
||||
return await el.save();
|
||||
});
|
||||
}
|
||||
|
||||
async restore(config: ViewerConfigUpdate) {
|
||||
return this.container.evaluate(async function (viewer, config) {
|
||||
let el = viewer as unknown as HTMLPerspectiveViewerElement;
|
||||
return await el.restore(config);
|
||||
}, config as any);
|
||||
}
|
||||
|
||||
async openSettingsPanel() {
|
||||
if (await this.settingsPanel.container.isVisible()) {
|
||||
return this.settingsPanel;
|
||||
}
|
||||
await this.settingsButton.click();
|
||||
await this.settingsPanel.container.waitFor({ state: "visible" });
|
||||
return this.settingsPanel;
|
||||
}
|
||||
async closeSettingsPanel() {
|
||||
await this.settingsCloseButton.click();
|
||||
await this.settingsPanel.container.waitFor({ state: "hidden" });
|
||||
}
|
||||
|
||||
async getOrCreateColumnByType(type: ColumnType) {
|
||||
let settingsPanel = this.settingsPanel;
|
||||
await this.openSettingsPanel();
|
||||
let col = settingsPanel.activeColumns.getColumnByType(type);
|
||||
if (await col.container.isHidden()) {
|
||||
let expr: string = "";
|
||||
switch (type) {
|
||||
case "string":
|
||||
expr = "'foo'";
|
||||
break;
|
||||
case "integer":
|
||||
expr = "1";
|
||||
break;
|
||||
case "float":
|
||||
expr = "1.1";
|
||||
case "date":
|
||||
expr = "date(0,0,0)";
|
||||
case "datetime":
|
||||
expr = "now()";
|
||||
case "numeric":
|
||||
expr = "1";
|
||||
case "calendar":
|
||||
expr = "now()";
|
||||
}
|
||||
await this.settingsPanel.createNewExpression("expr", expr, true);
|
||||
await settingsPanel.activeColumns.activateColumn("expr");
|
||||
col = await settingsPanel.activeColumns.getColumnByName("expr");
|
||||
}
|
||||
await expect(col.container).toBeVisible();
|
||||
return col;
|
||||
}
|
||||
|
||||
async assureColumnSettingsOpen(column: ColumnSelector) {
|
||||
let isEditing = await column.editBtn.evaluate((btn) =>
|
||||
btn.className.includes("is-editing"),
|
||||
);
|
||||
if (!isEditing) {
|
||||
await this.container.evaluate((_) =>
|
||||
console.log("COLUMN SETTINGS CLOSED"),
|
||||
);
|
||||
await column.editBtn.click({ force: true });
|
||||
}
|
||||
expect(this.container).toBeVisible({ timeout: 1000 });
|
||||
}
|
||||
async assureColumnSettingsClosed() {
|
||||
if (await this.columnSettingsSidebar.container.isVisible()) {
|
||||
await this.columnSettingsSidebar.closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveElement() {
|
||||
return await this.container.evaluate((viewer) => {
|
||||
return viewer.shadowRoot?.activeElement;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the viewer.
|
||||
* Unlike page.
|
||||
* @param eventName
|
||||
* @param options
|
||||
* @returns A future
|
||||
*/
|
||||
async getEventListener(eventName: string, options?: { timeout?: number }) {
|
||||
let key = `__${eventName}_FIRED__`;
|
||||
await this.container.evaluate(
|
||||
(target, { eventName, key }) => {
|
||||
target.addEventListener(eventName, (event) => {
|
||||
window[key] = true;
|
||||
console.log(event);
|
||||
});
|
||||
},
|
||||
{ eventName, key },
|
||||
);
|
||||
return async () => {
|
||||
return await this.page.evaluate(
|
||||
async ({ options, key }) => {
|
||||
let i = 0;
|
||||
while (!window[key]) {
|
||||
await new Promise((x) => setTimeout(x, 10));
|
||||
let timeout = options?.timeout ?? 1000;
|
||||
if (i++ > timeout / 10) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ options, key },
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as DataGridPlugin from "./plugins/datagrid";
|
||||
|
||||
export { DataGridPlugin };
|
||||
@@ -0,0 +1,78 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
|
||||
export class DataGrid {
|
||||
element: Locator;
|
||||
regularTable: RegularTable;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.element = page.locator("perspective-viewer-datagrid");
|
||||
this.regularTable = new RegularTable(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
export class RegularTable {
|
||||
element: Locator;
|
||||
table: Locator;
|
||||
columnTitleRow: Locator;
|
||||
editBtnRow: Locator;
|
||||
openColumnEditBtn: Locator;
|
||||
realTitles: Locator;
|
||||
realEditBtns: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.element = parent.locator("regular-table");
|
||||
this.table = this.element.locator("table");
|
||||
this.columnTitleRow = this.element.locator("#psp-column-titles");
|
||||
this.editBtnRow = this.element.locator("#psp-column-edit-buttons");
|
||||
this.openColumnEditBtn = this.editBtnRow.locator(".psp-menu-open");
|
||||
this.realTitles = this.columnTitleRow.locator(
|
||||
"th:not(.psp-header-group-corner)"
|
||||
);
|
||||
this.realEditBtns = this.editBtnRow.locator(
|
||||
"th:not(.psp-header-group-corner)"
|
||||
);
|
||||
}
|
||||
|
||||
async getTitleIdx(name: string) {
|
||||
const ths = await this.realTitles.all();
|
||||
for (const [i, locator] of ths.entries()) {
|
||||
if ((await locator.innerText()) === name) {
|
||||
this.element.evaluate(
|
||||
(_, i) => console.log("getTitleIdx returned:", i),
|
||||
i
|
||||
);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
async getEditBtnByName(name: string) {
|
||||
const n = await this.getTitleIdx(name);
|
||||
expect(n).not.toBe(-1);
|
||||
return this.editBtnRow.locator("th").nth(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the name of a column and returns a locator for the first corresponding TD in the body.
|
||||
* @param name
|
||||
*/
|
||||
async getFirstCellByColumnName(name: string) {
|
||||
const n = await this.getTitleIdx(name);
|
||||
expect(n).not.toBe(-1);
|
||||
return this.table.locator("tbody tr").first().locator("td").nth(n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect, test } from "@playwright/test";
|
||||
import { PageView } from "./page";
|
||||
|
||||
type ViewParameter = "groupby" | "splitby" | "orderby" | "where";
|
||||
|
||||
export class SettingsPanel {
|
||||
pageView: PageView;
|
||||
container: Locator;
|
||||
closeButton: Locator;
|
||||
activeColumns: ActiveColumns;
|
||||
inactiveColumns: InactiveColumns;
|
||||
addExpressionButton: Locator;
|
||||
pluginSelector: Locator;
|
||||
groupbyInput: Locator;
|
||||
splitbyInput: Locator;
|
||||
orderbyInput: Locator;
|
||||
whereInput: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.pageView = view;
|
||||
const viewer = view.container;
|
||||
this.container = viewer.locator("#settings_panel");
|
||||
this.closeButton = viewer.locator("#settings_close_button");
|
||||
this.activeColumns = new ActiveColumns(this.pageView);
|
||||
this.inactiveColumns = new InactiveColumns(this.pageView);
|
||||
this.addExpressionButton = viewer.locator("#add-expression");
|
||||
this.pluginSelector = viewer.locator("#plugin_selector_container");
|
||||
this.groupbyInput = viewer.locator("#group_by input");
|
||||
this.splitbyInput = viewer.locator("#split_by input");
|
||||
this.orderbyInput = viewer.locator("#sort input");
|
||||
this.whereInput = viewer.locator("#filter input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and saves a new expression column.
|
||||
* @param expr
|
||||
*/
|
||||
async createNewExpression(
|
||||
name: string,
|
||||
expr: string,
|
||||
successAssertion: boolean = true
|
||||
) {
|
||||
await this.activeColumns.scrollToBottom();
|
||||
await this.addExpressionButton.click();
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
let exprEditor = sidebar.attributesTab.expressionEditor;
|
||||
await exprEditor.container.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
await sidebar.nameInput.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
expect(await exprEditor.textarea.isVisible()).toBe(true);
|
||||
expect(await sidebar.nameInput.isVisible()).toBe(true);
|
||||
|
||||
await sidebar.nameInput.focus();
|
||||
await sidebar.nameInput.clear();
|
||||
await sidebar.nameInput.type(name, { delay: 100 });
|
||||
await sidebar.nameInput.press("Enter");
|
||||
await sidebar.nameInput.blur();
|
||||
|
||||
await exprEditor.textarea.focus();
|
||||
await exprEditor.textarea.clear();
|
||||
await exprEditor.textarea.type(expr, { delay: 100 });
|
||||
await exprEditor.textarea.blur();
|
||||
|
||||
let saveBtn = this.pageView.page.locator(
|
||||
"#psp-expression-editor-button-save"
|
||||
);
|
||||
if (successAssertion) {
|
||||
expect(await saveBtn.isDisabled()).toBe(false);
|
||||
await saveBtn.click();
|
||||
await this.inactiveColumns.getColumnByName(name);
|
||||
} else {
|
||||
expect(await saveBtn.isDisabled()).toBe(true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Renames an inactive expression column.
|
||||
* @param name
|
||||
* @param expr
|
||||
*/
|
||||
async renameExpression(column: ColumnSelector, name: string) {
|
||||
await this.pageView.assureColumnSettingsOpen(column);
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
await sidebar.nameInput.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
expect(await sidebar.nameInput.isVisible()).toBe(true);
|
||||
|
||||
await sidebar.nameInput.focus();
|
||||
await sidebar.nameInput.clear();
|
||||
await sidebar.nameInput.type(name, { delay: 100 });
|
||||
|
||||
await expect(sidebar.attributesTab.saveBtn).toBeEnabled();
|
||||
await sidebar.attributesTab.saveBtn.click();
|
||||
}
|
||||
|
||||
async editExpression(column: ColumnSelector, newExpression: string) {
|
||||
await this.pageView.assureColumnSettingsOpen(column);
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
await sidebar.openTab("Attributes");
|
||||
let exprEditor = sidebar.attributesTab.expressionEditor;
|
||||
expect(await exprEditor.textarea.isVisible()).toBe(true);
|
||||
await exprEditor.textarea.focus();
|
||||
await exprEditor.textarea.clear();
|
||||
await exprEditor.textarea.type(newExpression, { delay: 100 });
|
||||
await exprEditor.textarea.blur();
|
||||
let saveBtn = this.pageView.page.locator(
|
||||
"#psp-expression-editor-button-save"
|
||||
);
|
||||
expect(await saveBtn.isDisabled()).toBe(false);
|
||||
await saveBtn.click();
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("groupby", name)
|
||||
*/
|
||||
async groupby(name: string) {
|
||||
return await this.setViewParameter("groupby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("splitby", name)
|
||||
*/
|
||||
async splitby(name: string) {
|
||||
return await this.setViewParameter("splitby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("orderby", name)
|
||||
*/
|
||||
async orderby(name: string) {
|
||||
return await this.setViewParameter("orderby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("where", name)
|
||||
*/
|
||||
async where(name: string) {
|
||||
return await this.setViewParameter("where", name);
|
||||
}
|
||||
/**
|
||||
* Sets a view parameter ("groupby", "splitby", "orderby", or "where") to the specified column name.
|
||||
* @param type
|
||||
* @param name
|
||||
*/
|
||||
async setViewParameter(type: ViewParameter, name: string) {
|
||||
let locator: Locator;
|
||||
switch (type) {
|
||||
case "groupby":
|
||||
locator = this.groupbyInput;
|
||||
break;
|
||||
case "orderby":
|
||||
locator = this.orderbyInput;
|
||||
break;
|
||||
case "splitby":
|
||||
locator = this.splitbyInput;
|
||||
break;
|
||||
case "where":
|
||||
locator = this.whereInput;
|
||||
break;
|
||||
default:
|
||||
throw "Invalid type passed!";
|
||||
}
|
||||
await locator.type(name);
|
||||
await this.pageView.page
|
||||
.locator("perspective-dropdown .selected")
|
||||
.first() // NOTE: There probably shouldn't actually be more than one.
|
||||
.waitFor();
|
||||
await locator.press("Enter");
|
||||
}
|
||||
|
||||
async removeViewParameter(type: ViewParameter, name: string) {
|
||||
let locator: Locator;
|
||||
switch (type) {
|
||||
case "groupby":
|
||||
locator = this.container.locator("#group_by .pivot-column");
|
||||
break;
|
||||
case "orderby":
|
||||
locator = this.container.locator("#sort .pivot-column");
|
||||
break;
|
||||
case "splitby":
|
||||
locator = this.container.locator("#split_by .pivot-column");
|
||||
break;
|
||||
case "where":
|
||||
locator = this.container.locator("#filter .pivot-column");
|
||||
break;
|
||||
default:
|
||||
throw "Invalid type passed!";
|
||||
}
|
||||
await locator
|
||||
.filter({ hasText: name })
|
||||
.first()
|
||||
.locator(".row_close")
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a plugin by it's display name, i.e. the innerText of the .plugin-select-item
|
||||
* @param name
|
||||
*/
|
||||
async selectPlugin(name: string) {
|
||||
await this.pluginSelector.click();
|
||||
await this.pluginSelector.locator(`[data-plugin="${name}"]`).click();
|
||||
}
|
||||
}
|
||||
|
||||
export class ColumnSelector {
|
||||
active: boolean;
|
||||
activeBtn: Locator;
|
||||
name: Locator;
|
||||
container: Locator;
|
||||
editBtn: Locator;
|
||||
aggSelector: Locator;
|
||||
|
||||
constructor(container: Locator, active: boolean) {
|
||||
this.container = container;
|
||||
this.active = active;
|
||||
this.name = container.locator("div .column_name");
|
||||
this.aggSelector = container.locator("select");
|
||||
this.editBtn = container.locator("div .expression-edit-button");
|
||||
this.activeBtn = container.locator(".is_column_active");
|
||||
}
|
||||
}
|
||||
|
||||
export type ColumnType =
|
||||
| "integer"
|
||||
| "float"
|
||||
| "string"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "numeric"
|
||||
| "calendar";
|
||||
|
||||
// TODO: Consolidate this and InactiveColumns into a super class
|
||||
export class ActiveColumns {
|
||||
view: PageView;
|
||||
page: Page;
|
||||
container: Locator;
|
||||
topPanel: Locator;
|
||||
columnSelector: Locator;
|
||||
newColumnInput: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.page = view.page;
|
||||
this.view = view;
|
||||
this.container = view.container.locator("#active-columns");
|
||||
this.topPanel = view.container.locator("#top_panel");
|
||||
this.columnSelector = view.container.locator(
|
||||
"#active-columns :not(.top-panel) .column-selector-column"
|
||||
);
|
||||
this.newColumnInput = view.container.locator(
|
||||
".column-selector-column .column-empty input"
|
||||
);
|
||||
}
|
||||
|
||||
getFirstVisibleColumn() {
|
||||
return new ColumnSelector(this.columnSelector.first(), true);
|
||||
}
|
||||
|
||||
async getColumnByName(name: string, exact: boolean = false) {
|
||||
let locators = this.columnSelector.filter({
|
||||
hasText: name,
|
||||
});
|
||||
|
||||
let locator: Locator;
|
||||
if (exact) {
|
||||
locator = (await locators.all()).filter(
|
||||
async (l) => (await l.innerText()) === name
|
||||
)[0];
|
||||
} else {
|
||||
locator = locators.first();
|
||||
}
|
||||
|
||||
return new ColumnSelector(locator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first visible column matching the passed in type.
|
||||
* @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime"
|
||||
*/
|
||||
getColumnByType(type: ColumnType) {
|
||||
let page = this.view.page;
|
||||
let has: Locator;
|
||||
switch (type) {
|
||||
case "numeric":
|
||||
has = page.locator(".float").or(page.locator(".integer"));
|
||||
break;
|
||||
case "calendar":
|
||||
has = page.locator(".date").or(page.locator(".datetime"));
|
||||
break;
|
||||
default:
|
||||
has = page.locator(`.${type}`);
|
||||
break;
|
||||
}
|
||||
return new ColumnSelector(
|
||||
this.columnSelector.filter({ has }).first(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async visibleColumns() {
|
||||
let all = await this.columnSelector.all();
|
||||
let mapped = all.map((locator) => {
|
||||
return new ColumnSelector(locator, true);
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
|
||||
async scrollToTop() {
|
||||
this.container.focus();
|
||||
await this.container.evaluate((node) => (node.scrollTop = 0));
|
||||
}
|
||||
async scrollToBottom() {
|
||||
this.container.focus();
|
||||
await this.container.evaluate(
|
||||
(node) => (node.scrollTop = node.scrollHeight)
|
||||
);
|
||||
}
|
||||
|
||||
async toggleColumn(name: string) {
|
||||
let has = this.view.page.getByText(name);
|
||||
this.columnSelector
|
||||
.filter({ has })
|
||||
.locator(".is_column_active")
|
||||
.click();
|
||||
}
|
||||
/**
|
||||
* This function will use the empty input column to activate a column.
|
||||
* During this process, it will scroll ActiveColumns to the bottom.
|
||||
* @param name
|
||||
*/
|
||||
async activateColumn(name: string) {
|
||||
await this.scrollToBottom();
|
||||
await this.newColumnInput.waitFor({ state: "visible" });
|
||||
await this.newColumnInput.type(name);
|
||||
await this.page
|
||||
.locator("perspective-dropdown .selected")
|
||||
.first() // NOTE: There probably shouldn't actually be more than one.
|
||||
.waitFor();
|
||||
await this.newColumnInput.press("Enter");
|
||||
let addedColumn = this.columnSelector.filter({ hasText: name }).first();
|
||||
return new ColumnSelector(addedColumn!, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class InactiveColumns {
|
||||
view: PageView;
|
||||
container: Locator;
|
||||
columnSelector: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.view = view;
|
||||
this.container = view.container.locator("#sub-columns");
|
||||
this.columnSelector = this.container.locator(".column-selector-column");
|
||||
}
|
||||
|
||||
async getColumnByName(name: string) {
|
||||
await this.container.waitFor({ state: "visible", timeout: 1000 });
|
||||
let locator = this.columnSelector.filter({ hasText: name });
|
||||
return new ColumnSelector(locator, true);
|
||||
}
|
||||
async visibleColumns() {
|
||||
let all = await this.columnSelector.all();
|
||||
let mapped = all.map((locator) => {
|
||||
return new ColumnSelector(locator, false);
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
/**
|
||||
* This function will click the toggle next to the inactive column,
|
||||
* making it the only active column.
|
||||
* @param name
|
||||
*/
|
||||
async toggleColumn(name: string) {
|
||||
this.columnSelector
|
||||
.filter({ has: this.container.getByText(name) })
|
||||
.locator(".is_column_active")
|
||||
.click();
|
||||
}
|
||||
/**
|
||||
* Gets the first visible column matching the passed in type.
|
||||
* @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime"
|
||||
*/
|
||||
getColumnByType(
|
||||
type:
|
||||
| "integer"
|
||||
| "float"
|
||||
| "string"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "numeric"
|
||||
| "calendar"
|
||||
) {
|
||||
let page = this.view.page;
|
||||
let has: Locator;
|
||||
switch (type) {
|
||||
case "numeric":
|
||||
has = page.locator(".float").or(page.locator(".integer"));
|
||||
break;
|
||||
case "calendar":
|
||||
has = page.locator(".date").or(page.locator(".datetime"));
|
||||
break;
|
||||
default:
|
||||
has = page.locator(`.${type}`);
|
||||
break;
|
||||
}
|
||||
return new ColumnSelector(
|
||||
this.columnSelector.filter({ has }).first(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const JSDOMEnvironment = require("jest-environment-jsdom");
|
||||
|
||||
module.exports = class TimezoneAwareJSDOMEnvironment extends JSDOMEnvironment {
|
||||
constructor(config, context) {
|
||||
if (context.testPath.endsWith("timezone.spec.js")) {
|
||||
process.env.TZ = "America/New_York";
|
||||
} else {
|
||||
process.env.TZ = "UTC";
|
||||
}
|
||||
|
||||
super(config);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import {
|
||||
compareContentsToSnapshot,
|
||||
getSvgContentString,
|
||||
shadow_click,
|
||||
} from "./utils.ts";
|
||||
import { test, expect } from "./index.js";
|
||||
import type { ViewerConfigUpdate } from "@perspective-dev/viewer";
|
||||
import { ViewConfigUpdate } from "@perspective-dev/client";
|
||||
|
||||
export type ContentExtractor = (page: any) => Promise<string>;
|
||||
|
||||
async function restoreViewer(page, viewerConfig: ViewerConfigUpdate) {
|
||||
return await page.evaluate(async (viewerConfig) => {
|
||||
const viewer = document.querySelector("perspective-viewer")!;
|
||||
await viewer.restore(viewerConfig);
|
||||
}, viewerConfig);
|
||||
}
|
||||
|
||||
function runSimpleCompareTest(
|
||||
viewerConfig: ViewerConfigUpdate,
|
||||
extractContent: ContentExtractor,
|
||||
snapshotPath: string[],
|
||||
) {
|
||||
return async ({ page }) => {
|
||||
await restoreViewer(page, viewerConfig);
|
||||
const content = await extractContent(page);
|
||||
await compareContentsToSnapshot(content, snapshotPath);
|
||||
};
|
||||
}
|
||||
|
||||
export function runPerspectiveEventClickTest() {
|
||||
return async ({ page }) => {
|
||||
const viewerConfig: ViewConfigUpdate = {
|
||||
filter: [["date", "<", "2025-01-01"]],
|
||||
};
|
||||
|
||||
await restoreViewer(page, viewerConfig);
|
||||
const viewer = await page.$("perspective-viewer")!;
|
||||
const perspectiveClick = viewer.evaluate(
|
||||
(element) =>
|
||||
new Promise((resolve) =>
|
||||
element.addEventListener("perspective-click", (event) => {
|
||||
// extract the detail
|
||||
resolve(event.detail);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await shadow_click(
|
||||
page,
|
||||
"perspective-viewer-datagrid",
|
||||
"regular-table > table > tbody > tr:nth-child(1) > td:nth-child(2)",
|
||||
);
|
||||
|
||||
const detail = await perspectiveClick;
|
||||
const expectedFilter = [["date", "<", "2025-01-01"]];
|
||||
const resultFilter = detail["config"]["filter"];
|
||||
expect(resultFilter).toEqual(expectedFilter);
|
||||
};
|
||||
}
|
||||
|
||||
export function run_standard_tests(
|
||||
context: string,
|
||||
extractContent: ContentExtractor,
|
||||
) {
|
||||
test("Show grid no settings", async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const viewer = document.querySelector("perspective-viewer")!;
|
||||
await viewer.getTable(); // Not sure why this is needed...
|
||||
await viewer.restore({ settings: true });
|
||||
});
|
||||
|
||||
const content = await getSvgContentString("perspective-viewer")(page);
|
||||
await compareContentsToSnapshot(content, [
|
||||
context,
|
||||
`show-grid-no-settings.txt`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("Displays visible columns", async ({ page }) => {
|
||||
await restoreViewer(page, {
|
||||
columns: ["Discount", "Profit", "Sales", "Quantity"],
|
||||
});
|
||||
|
||||
const visibleColumnContent = await extractContent(page);
|
||||
await compareContentsToSnapshot(visibleColumnContent, [
|
||||
context,
|
||||
`displays-visible-columns.txt`,
|
||||
]);
|
||||
});
|
||||
|
||||
test.describe("Pivot tests", () => {
|
||||
test(
|
||||
`Pivot by a row`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["State"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-row.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by two rows`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["Category", "Sub-Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-two-rows.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by a column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
split_by: ["Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by a row and a column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["State"],
|
||||
split_by: ["Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-row-and-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by two rows and two columns`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["Region", "State"],
|
||||
split_by: ["Category", "Sub-Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-two-rows-and-two-columns.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Sort tests", () => {
|
||||
test(
|
||||
`Sort by a hidden column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "Quantity"],
|
||||
sort: [["Sales", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-hidden-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Sort by a numeric column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "Sales"],
|
||||
sort: [["Quantity", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-numeric-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Sort by an alpha column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
sort: [["State", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-alpha-column.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Filter tests", () => {
|
||||
test(
|
||||
`Filter by a numeric column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["Sales", ">", 500]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-by-numeric-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Filter by an alpha column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["State", "==", "Texas"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-by-alpha-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Filter with 'in' comparator`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["State", "in", ["Texas", "California"]]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-with-in-comparator.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const TEST_ROOT = path.resolve(__dirname, "..", "..");
|
||||
const CACHE_DIR = path.join(TEST_ROOT, "dist", "git_snapshots");
|
||||
const DEST_DIR = path.join(TEST_ROOT, "dist", "snapshots");
|
||||
const DEFAULT_REF = "master";
|
||||
|
||||
function git(args: string[], cwd: string) {
|
||||
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||
}
|
||||
|
||||
function remoteHasRef(
|
||||
remoteUrl: string,
|
||||
ref: string,
|
||||
// token: string | undefined,
|
||||
): boolean {
|
||||
try {
|
||||
const args: string[] = [
|
||||
"-c",
|
||||
"http.https://github.com/.extraheader=",
|
||||
"ls-remote",
|
||||
"--heads",
|
||||
remoteUrl,
|
||||
ref,
|
||||
];
|
||||
const out = execFileSync("git", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
return out.length > 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRemoteUrl(repo: string, token: string | undefined): string {
|
||||
if (token) {
|
||||
return `https://${token}@github.com/${repo}.git`;
|
||||
}
|
||||
|
||||
return `git@github.com:${repo}.git`;
|
||||
}
|
||||
|
||||
function mirrorSnapshots(srcRoot: string, dest: string) {
|
||||
const srcSnapshots = path.join(srcRoot);
|
||||
if (!fs.existsSync(srcSnapshots)) {
|
||||
throw new Error(
|
||||
`Snapshot clone at ${srcRoot} does not contain dist/snapshots/`,
|
||||
);
|
||||
}
|
||||
|
||||
fs.rmSync(dest, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(srcSnapshots, dest, { recursive: true });
|
||||
fs.rmSync(path.join(dest, ".git"), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export async function fetchSnapshots(): Promise<void> {
|
||||
const repo = process.env.PSP_SNAPSHOT_REPO;
|
||||
if (!repo) {
|
||||
throw new Error(
|
||||
"PSP_SNAPSHOT_REPO is required when fetching snapshots (e.g. 'perspective-dev/perspective-snapshots').",
|
||||
);
|
||||
}
|
||||
|
||||
const token =
|
||||
process.env.PSP_SNAPSHOT_TOKEN || process.env.GITHUB_TOKEN || undefined;
|
||||
const requestedRef = process.env.PSP_SNAPSHOT_REF || DEFAULT_REF;
|
||||
const remoteUrl = buildRemoteUrl(repo, token);
|
||||
|
||||
let ref = requestedRef;
|
||||
if (
|
||||
requestedRef !== DEFAULT_REF &&
|
||||
!remoteHasRef(remoteUrl, requestedRef)
|
||||
) {
|
||||
console.log(
|
||||
`Snapshot branch '${requestedRef}' not found on ${repo}; falling back to '${DEFAULT_REF}'.`,
|
||||
);
|
||||
|
||||
ref = DEFAULT_REF;
|
||||
}
|
||||
|
||||
const cacheGitDir = path.join(CACHE_DIR, ".git");
|
||||
if (fs.existsSync(cacheGitDir)) {
|
||||
try {
|
||||
git(["remote", "set-url", "origin", remoteUrl], CACHE_DIR);
|
||||
git(["fetch", "--depth", "1", "origin", ref], CACHE_DIR);
|
||||
git(["checkout", "-B", ref, "FETCH_HEAD"], CACHE_DIR);
|
||||
git(["reset", "--hard", "FETCH_HEAD"], CACHE_DIR);
|
||||
git(["clean", "-fdx"], CACHE_DIR);
|
||||
} catch {
|
||||
fs.rmSync(CACHE_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cacheGitDir)) {
|
||||
fs.mkdirSync(path.dirname(CACHE_DIR), { recursive: true });
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"-c",
|
||||
"http.https://github.com/.extraheader=",
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--filter=blob:none",
|
||||
"--branch",
|
||||
ref,
|
||||
remoteUrl,
|
||||
CACHE_DIR,
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Fetched snapshots from ${repo}@${ref}`);
|
||||
mirrorSnapshots(CACHE_DIR, DEST_DIR);
|
||||
}
|
||||
|
||||
export async function writebackSnapshots(): Promise<void> {
|
||||
if (!fs.existsSync(path.join(CACHE_DIR, ".git"))) {
|
||||
console.log(
|
||||
`No snapshot clone at ${CACHE_DIR}; skipping writeback. Run with --fetch-snapshots first to populate the cache.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DEST_DIR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.cpSync(DEST_DIR, CACHE_DIR, { recursive: true, force: true });
|
||||
console.log(`\nCopied updated snapshots into ${CACHE_DIR}`);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import http from "node:http";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
function read_promise(filePath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, function (error, content) {
|
||||
if (error && error.code !== "ENOENT") {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(content);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".js": "text/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".arrow": "arraybuffer",
|
||||
".feather": "arraybuffer",
|
||||
".wasm": "application/wasm",
|
||||
};
|
||||
|
||||
async function cwd_static_file_handler(
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse<http.IncomingMessage>,
|
||||
assets = ["./"]
|
||||
) {
|
||||
let url =
|
||||
request.url
|
||||
?.split(/[\?\#]/)[0]
|
||||
.replace(/@[\^~]?\d+.[\d\*]*.[\d\*]*/, "") || "/";
|
||||
|
||||
if (url === "/") {
|
||||
url = "/index.html";
|
||||
}
|
||||
|
||||
let extname = path.extname(url);
|
||||
let contentType = CONTENT_TYPES[extname] || "text/html";
|
||||
try {
|
||||
for (const root of assets) {
|
||||
let filePath = root + url;
|
||||
let content = await read_promise(filePath);
|
||||
if (typeof content !== "undefined") {
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
|
||||
if (extname === ".arrow" || extname === ".feather") {
|
||||
response.end(content, "utf-8");
|
||||
} else {
|
||||
response.end(content);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`404 ${url}`);
|
||||
response.writeHead(404);
|
||||
response.end("", "utf-8");
|
||||
} catch (error) {
|
||||
console.error(`500 ${url}`);
|
||||
response.writeHead(500);
|
||||
response.end("", "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
const _server = http.createServer((x, y) =>
|
||||
cwd_static_file_handler(x, y, [
|
||||
path.join(__dirname, "..", "..", "..", ".."),
|
||||
])
|
||||
);
|
||||
|
||||
_server.listen({ port: 6598 });
|
||||
@@ -0,0 +1,398 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import type { ViewerConfigUpdate } from "@perspective-dev/viewer";
|
||||
import { test, expect, Locator, Page } from "@playwright/test";
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import * as prettier from "prettier";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const API_VERSION = JSON.parse(
|
||||
fs.readFileSync(__dirname + "/../../package.json").toString(),
|
||||
)["version"];
|
||||
|
||||
export const DEFAULT_CONFIG: ViewerConfigUpdate = {
|
||||
aggregates: {},
|
||||
columns_config: {},
|
||||
columns: [],
|
||||
expressions: {},
|
||||
filter: [],
|
||||
group_by: [],
|
||||
group_rollup_mode: "rollup",
|
||||
plugin: "",
|
||||
plugin_config: {},
|
||||
settings: false,
|
||||
sort: [],
|
||||
split_by: [],
|
||||
version: API_VERSION,
|
||||
table: "load-viewer-csv",
|
||||
title: null,
|
||||
theme: "Pro Light",
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean a `<svg>` for serialization/comparison.
|
||||
* @param selector
|
||||
* @param page
|
||||
* @returns
|
||||
*/
|
||||
export const getSvgContentString = (selector: string) => async (page: Page) => {
|
||||
const content = await page.evaluate(async (selector) => {
|
||||
let el = document.querySelector(selector) as Element;
|
||||
console.log(selector, el);
|
||||
|
||||
function removeAttrs(node: Element) {
|
||||
const svgAttrsToRemove = [
|
||||
"r",
|
||||
"d",
|
||||
"dx",
|
||||
"dy",
|
||||
"x",
|
||||
"y",
|
||||
"x1",
|
||||
"x2",
|
||||
"y1",
|
||||
"y2",
|
||||
"style",
|
||||
"d",
|
||||
"transform",
|
||||
"viewBox",
|
||||
"visibility",
|
||||
"style",
|
||||
];
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
for (const attr of svgAttrsToRemove) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function walkNode(node: Node) {
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ALL);
|
||||
|
||||
walk(walker, walker.currentNode as Element);
|
||||
}
|
||||
|
||||
function walk(walker: TreeWalker, node: Element) {
|
||||
if (!node) return;
|
||||
|
||||
if (node.shadowRoot) {
|
||||
walkNode(node.shadowRoot);
|
||||
}
|
||||
|
||||
removeAttrs(node);
|
||||
switch (node.nodeName) {
|
||||
case "style":
|
||||
node.textContent = "";
|
||||
break;
|
||||
case "svg":
|
||||
node.removeAttribute("viewBox");
|
||||
node.removeAttribute("height");
|
||||
break;
|
||||
case "g":
|
||||
case "path":
|
||||
case "line":
|
||||
case "circle":
|
||||
case "rect":
|
||||
case "text":
|
||||
if (
|
||||
["label", "segment"].some((c) =>
|
||||
node.classList.contains(c),
|
||||
)
|
||||
) {
|
||||
node.textContent = node.textContent?.slice(0, 2) as
|
||||
| string
|
||||
| null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
walk(walker, walker.nextNode() as Element);
|
||||
}
|
||||
|
||||
if (el?.shadowRoot) {
|
||||
el = el.shadowRoot as unknown as Element;
|
||||
}
|
||||
|
||||
let htmlString = "";
|
||||
for (const child of el.children) {
|
||||
if (child.tagName === "STYLE") {
|
||||
continue;
|
||||
}
|
||||
const clonedChild = child.cloneNode(true) as Element;
|
||||
|
||||
walkNode(clonedChild);
|
||||
|
||||
htmlString += clonedChild.outerHTML;
|
||||
}
|
||||
|
||||
return htmlString;
|
||||
}, selector);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const EXISTING_TITLES = new Map();
|
||||
|
||||
/**
|
||||
* Compares the content of an HTML element to a snapshot.
|
||||
* To generate new snapshots, run `pnpm run test --update-snapshots`.
|
||||
* This first runs the focused project(s) tests, which generates new
|
||||
* snapshots, and then updates the contents of results.tar.gz which
|
||||
* you can commit.
|
||||
* @param contents
|
||||
* @param snapshotPath
|
||||
*/
|
||||
export async function compareContentsToSnapshot(
|
||||
contents: string,
|
||||
): Promise<void> {
|
||||
// contents = contents
|
||||
// .replace(/(min-|max-)?(width|height): *\d+\.*\d+(px)?;? */g, "")
|
||||
// .replace(/style=""/g, "");
|
||||
|
||||
contents = contents.replace(/style=""/g, "");
|
||||
|
||||
const formatted = await prettier.format(contents, {
|
||||
parser: "html",
|
||||
});
|
||||
|
||||
const titlePath = test.info().titlePath;
|
||||
let snapshotPath = titlePath
|
||||
.slice(1)
|
||||
.map((s) =>
|
||||
s
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/gi, "-")
|
||||
.toLowerCase(),
|
||||
)
|
||||
.join("-");
|
||||
|
||||
const count = EXISTING_TITLES.get(snapshotPath) || 0;
|
||||
EXISTING_TITLES.set(snapshotPath, count + 1);
|
||||
if (count > 0) {
|
||||
snapshotPath = `${snapshotPath[0]}_${count}`;
|
||||
}
|
||||
|
||||
snapshotPath = `${snapshotPath}.txt`;
|
||||
await expect(formatted).toMatchSnapshot(snapshotPath);
|
||||
}
|
||||
|
||||
export async function compareSVGContentsToSnapshot(
|
||||
page: Page,
|
||||
selector: string,
|
||||
): Promise<void> {
|
||||
const svgContent = await getSvgContentString(selector)(page);
|
||||
await compareContentsToSnapshot(svgContent);
|
||||
}
|
||||
|
||||
export function getWorkspaceLightDOMContents(page: Page): Promise<string> {
|
||||
return page.evaluate(
|
||||
async () => document.querySelector("perspective-workspace")!.outerHTML,
|
||||
);
|
||||
}
|
||||
|
||||
export function getWorkspaceShadowDOMContents(page: Page): Promise<string> {
|
||||
return page.evaluate(async () => {
|
||||
return document
|
||||
.querySelector("perspective-workspace")!
|
||||
.shadowRoot!.querySelector("#container")!.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
export async function compareLightDOMContents(page: Page) {
|
||||
const contents = await getWorkspaceLightDOMContents(page);
|
||||
await compareContentsToSnapshot(contents);
|
||||
}
|
||||
|
||||
export async function compareShadowDOMContents(page: Page) {
|
||||
const contents = await getWorkspaceShadowDOMContents(page);
|
||||
await compareContentsToSnapshot(contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on an element, passing through shadow roots if necessary.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
* @param path
|
||||
*/
|
||||
export async function shadow_click(
|
||||
page: Page,
|
||||
...path: string[]
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ path }) => {
|
||||
let elem: ShadowRoot | Element | Document | null | undefined =
|
||||
document;
|
||||
while (path.length > 0) {
|
||||
if (
|
||||
elem instanceof Element &&
|
||||
(elem as Element).shadowRoot !== null
|
||||
) {
|
||||
elem = (elem as Element).shadowRoot;
|
||||
}
|
||||
|
||||
elem = elem?.querySelector(path.shift()!);
|
||||
}
|
||||
|
||||
function triggerMouseEvent(node: EventTarget, eventType: string) {
|
||||
var clickEvent = document.createEvent("MouseEvent");
|
||||
clickEvent.initEvent(eventType, true, true);
|
||||
node.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
triggerMouseEvent(elem as EventTarget, "mouseover");
|
||||
triggerMouseEvent(elem as EventTarget, "mousedown");
|
||||
triggerMouseEvent(elem as EventTarget, "mouseup");
|
||||
triggerMouseEvent(elem as EventTarget, "click");
|
||||
},
|
||||
{ path },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Types in an element, passing through shadow roots if necessary.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
* @param content
|
||||
* @param is_incremental
|
||||
* @param path
|
||||
*/
|
||||
export async function shadow_type(
|
||||
page: Page,
|
||||
content: string,
|
||||
is_incremental: boolean | string,
|
||||
...path: string[]
|
||||
): Promise<void> {
|
||||
if (typeof is_incremental !== "boolean") {
|
||||
path.unshift(is_incremental);
|
||||
is_incremental = false;
|
||||
}
|
||||
|
||||
await page.evaluate(
|
||||
async ({ content, path, is_incremental }) => {
|
||||
let elem: ShadowRoot | Element | Document | null | undefined =
|
||||
document;
|
||||
while (path.length > 0) {
|
||||
if (
|
||||
elem instanceof Element &&
|
||||
(elem as Element).shadowRoot !== null
|
||||
) {
|
||||
elem = (elem as Element).shadowRoot;
|
||||
}
|
||||
|
||||
elem = elem?.querySelector(path.shift()!);
|
||||
}
|
||||
|
||||
if (elem instanceof HTMLElement) {
|
||||
elem.focus();
|
||||
}
|
||||
|
||||
function triggerInputEvent(
|
||||
element: EventTarget | null | undefined,
|
||||
) {
|
||||
if (!element) return;
|
||||
const event = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (is_incremental) {
|
||||
while (content.length > 0) {
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
elem.value += content[0];
|
||||
} else if (
|
||||
elem instanceof HTMLElement &&
|
||||
elem.hasAttribute("contenteditable")
|
||||
) {
|
||||
elem.textContent = content;
|
||||
}
|
||||
|
||||
triggerInputEvent(elem);
|
||||
await new Promise(requestAnimationFrame);
|
||||
content = content.slice(1);
|
||||
}
|
||||
} else {
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
elem.value = content;
|
||||
} else if (
|
||||
elem instanceof HTMLElement &&
|
||||
elem.hasAttribute("contenteditable")
|
||||
) {
|
||||
elem.textContent = content;
|
||||
}
|
||||
|
||||
triggerInputEvent(elem);
|
||||
}
|
||||
|
||||
if (is_incremental) {
|
||||
if (elem instanceof HTMLElement) {
|
||||
const event = new Event("change", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
elem.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
document.activeElement.blur();
|
||||
},
|
||||
{ content, path, is_incremental },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurs the active element on the page, passing through shadow roots.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
*/
|
||||
export async function shadow_blur(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
let elem = document.activeElement;
|
||||
while (elem) {
|
||||
(elem as HTMLElement).blur();
|
||||
let shadowRoot = elem.shadowRoot;
|
||||
elem = shadowRoot ? shadowRoot.activeElement : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two locators to see if they match the same node in the DOM.
|
||||
* @param left
|
||||
* @param right
|
||||
* @param page
|
||||
*/
|
||||
export async function compareNodes(left: Locator, right: Locator, page: Page) {
|
||||
let leftEl = await left.elementHandle();
|
||||
let rightEl = await right.elementHandle();
|
||||
return await page.evaluate(
|
||||
async (compare) => {
|
||||
return compare.leftEl?.isEqualNode(compare.rightEl) || false;
|
||||
},
|
||||
{
|
||||
leftEl,
|
||||
rightEl,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"declaration": true,
|
||||
"diagnostics": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist/esm",
|
||||
"rootDir": ".",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user