chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
+75
View File
@@ -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);
},
);
+111
View File
@@ -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"],
},
},
];
+59
View File
@@ -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;
};
})();
+135
View File
@@ -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();
}
},
);
+344
View File
@@ -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);
},
});
}
+74
View File
@@ -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"
}
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
const { registerCmd } = require("@perspective-dev/bench");
registerCmd();
+85
View File
@@ -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");
},
);
+79
View File
@@ -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,
);
+80
View File
@@ -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;
},
);
+113
View File
@@ -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>
View File
+38
View File
@@ -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>
+359
View File
@@ -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);
}
}
+86
View File
@@ -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) {}
});
+78
View File
@@ -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;
}
+101
View File
@@ -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()
+30
View File
@@ -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`
+51
View File
@@ -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,
};
};
+50
View File
@@ -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);
}
};
+30
View File
@@ -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,
};
};
+46
View File
@@ -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,
};
};
+22
View File
@@ -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"
}
}
+55
View File
@@ -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,
};
});
}
},
};
};
+138
View File
@@ -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,
};
};
+314
View File
@@ -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,
};
};
+31
View File
@@ -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,
);
}
+25
View File
@@ -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`;
}
+56
View File
@@ -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");
+21
View File
@@ -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`;
+26
View File
@@ -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);
}
}
+66
View File
@@ -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();
}
+72
View File
@@ -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);
}
+57
View File
@@ -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();
+76
View File
@@ -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);
}
}
+119
View File
@@ -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;
}
+28
View File
@@ -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:"
}
}
+70
View File
@@ -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);
}
}
+144
View File
@@ -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);
+41
View File
@@ -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");
}
+32
View File
@@ -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}`);
+253
View File
@@ -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();
+115
View File
@@ -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",
});
};
+24
View File
@@ -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");
}
+166
View File
@@ -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();
}
+246
View File
@@ -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();
+57
View File
@@ -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.
+100
View File
@@ -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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
46 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
47 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
48 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
49 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
50 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
51 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
52 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
53 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
54 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
55 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
56 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
57 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
58 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
59 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
60 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
61 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
62 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
63 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
64 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
65 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
66 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
67 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
68 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
69 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
70 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
71 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
72 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
73 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
74 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
75 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
76 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
77 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
78 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
79 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
80 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
81 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
82 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
83 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
84 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
85 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
86 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
87 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
88 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
89 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
90 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
91 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
92 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
93 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
94 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
95 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
96 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
97 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
98 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
99 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
100 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;
+33
View File
@@ -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;
+41
View File
@@ -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;
+42
View File
@@ -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;
+22
View File
@@ -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;
+36
View File
@@ -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;
+32
View File
@@ -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:"
}
}
+256
View File
@@ -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",
},
});
+12
View File
@@ -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>
+11
View File
@@ -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>
+31
View File
@@ -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>
+32
View File
@@ -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>
+56
View File
@@ -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>
+48
View File
@@ -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>
+102
View File
@@ -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 },
],
});
+19
View File
@@ -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();
}
}
+19
View File
@@ -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();
}
}
+16
View File
@@ -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";
+170
View File
@@ -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;
}
}
+168
View File
@@ -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 },
);
};
}
}
+15
View File
@@ -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);
}
}
+422
View File
@@ -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
);
}
}
+25
View File
@@ -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);
}
};
+249
View File
@@ -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`],
),
);
});
}
+155
View File
@@ -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}`);
}
+94
View File
@@ -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 });
+398
View File
@@ -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,
},
);
}
+17
View File
@@ -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"
}
}