Merge pull request #962 from hgiesel/editordirs

Make editor a rollup package within data/web/js
This commit is contained in:
Damien Elmes 2021-02-01 13:40:54 +10:00 committed by GitHub
commit 1f6ed0739f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 387 additions and 263 deletions

View file

@ -1,4 +1,5 @@
load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
load("compile_sass.bzl", "compile_sass") load("compile_sass.bzl", "compile_sass")
compile_sass( compile_sass(
@ -16,11 +17,21 @@ copy_file(
out = "core.css", out = "core.css",
) )
copy_page(
name = "editor",
srcs = [
"editor.css",
"editable.css",
],
package = "//ts/editor",
)
filegroup( filegroup(
name = "css", name = "css",
srcs = [ srcs = [
"core.css", "core.css",
"css_local", "css_local",
"editor",
], ],
visibility = ["//qt:__subpackages__"], visibility = ["//qt:__subpackages__"],
) )

View file

@ -1,9 +1,13 @@
load("@npm//@bazel/typescript:index.bzl", "ts_library") load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
load("//ts:prettier.bzl", "prettier_test") load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test")
ts_library( ts_library(
name = "pycmd", name = "pycmd",
srcs = ["pycmd.d.ts"], srcs = ["pycmd.d.ts"],
visibility = ["//qt/aqt/data/web/js:__subpackages__"],
) )
ts_library( ts_library(
@ -26,10 +30,19 @@ filegroup(
output_group = "es5_sources", output_group = "es5_sources",
) )
copy_page(
name = "editor",
srcs = [
"editor.js",
],
package = "//ts/editor",
)
filegroup( filegroup(
name = "js", name = "js",
srcs = [ srcs = [
"aqt_es5", "aqt_es5",
"editor",
"mathjax.js", "mathjax.js",
"//qt/aqt/data/web/js/vendor", "//qt/aqt/data/web/js/vendor",
], ],
@ -47,4 +60,7 @@ prettier_test(
# srcs = glob(["*.ts"]), # srcs = glob(["*.ts"]),
# ) # )
exports_files(["mathjax.js"]) exports_files([
"mathjax.js",
"tsconfig.json",
])

View file

@ -18,9 +18,9 @@ sql_format_setup()
exports_files([ exports_files([
"tsconfig.json", "tsconfig.json",
"d3_missing.d.ts",
".prettierrc", ".prettierrc",
"rollup.config.js", "rollup.config.js",
"rollup.aqt.config.js",
".eslintrc.js", ".eslintrc.js",
"licenses.json", "licenses.json",
"sql_format.ts", "sql_format.ts",

62
ts/editor/BUILD.bazel Normal file
View file

@ -0,0 +1,62 @@
load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
sass_binary(
name = "editor_css",
src = "editor.scss",
visibility = ["//visibility:public"],
)
sass_binary(
name = "editable_css",
src = "editable.scss",
visibility = ["//visibility:public"],
)
ts_library(
name = "editor_ts",
srcs = glob(["*.ts"]),
tsconfig = "//qt/aqt/data/web/js:tsconfig.json",
deps = [
"@npm//@types/jquery",
],
)
rollup_bundle(
name = "editor",
config_file = "//ts:rollup.aqt.config.js",
entry_point = "index.ts",
format = "iife",
link_workspace_root = True,
silent = True,
sourcemap = "false",
visibility = ["//visibility:public"],
deps = [
"editor_ts",
"@npm//@rollup/plugin-commonjs",
"@npm//@rollup/plugin-node-resolve",
"@npm//rollup-plugin-terser",
],
)
# Tests
################
prettier_test(
name = "format_check",
srcs = glob([
"*.ts",
]),
)
# eslint_test(
# name = "eslint",
# srcs = glob(
# [
# "*.ts",
# ],
# ),
# )

170
ts/editor/filterHtml.ts Normal file
View file

@ -0,0 +1,170 @@
import { nodeIsElement } from "./helpers";
export let filterHTML = function (
html: string,
internal: boolean,
extendedMode: boolean
): string {
// wrap it in <top> as we aren't allowed to change top level elements
const top = document.createElement("ankitop");
top.innerHTML = html;
if (internal) {
filterInternalNode(top);
} else {
filterNode(top, extendedMode);
}
let outHtml = top.innerHTML;
if (!extendedMode && !internal) {
// collapse whitespace
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
}
outHtml = outHtml.trim();
return outHtml;
};
let allowedTagsBasic = {};
let allowedTagsExtended = {};
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsBasic[tag] = { attrs: [] };
}
TAGS_WITHOUT_ATTRS = [
"B",
"BLOCKQUOTE",
"CODE",
"DD",
"DL",
"DT",
"EM",
"H1",
"H2",
"H3",
"I",
"LI",
"OL",
"PRE",
"RP",
"RT",
"RUBY",
"STRONG",
"TABLE",
"U",
"UL",
];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsExtended[tag] = { attrs: [] };
}
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
allowedTagsExtended["A"] = { attrs: ["HREF"] };
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
const allowedStyling = {
color: true,
"background-color": true,
"font-weight": true,
"font-style": true,
"text-decoration-line": true,
};
let isNightMode = function (): boolean {
return document.body.classList.contains("nightMode");
};
let filterExternalSpan = function (elem: HTMLElement) {
// filter out attributes
for (const attr of [...elem.attributes]) {
const attrName = attr.name.toUpperCase();
if (attrName !== "STYLE") {
elem.removeAttributeNode(attr);
}
}
// filter styling
for (const name of [...elem.style]) {
const value = elem.style.getPropertyValue(name);
if (
!allowedStyling.hasOwnProperty(name) ||
// google docs adds this unnecessarily
(name === "background-color" && value === "transparent") ||
// ignore coloured text in night mode for now
(isNightMode() && (name === "background-color" || name === "color"))
) {
elem.style.removeProperty(name);
}
}
};
allowedTagsExtended["SPAN"] = filterExternalSpan;
// add basic tags to extended
Object.assign(allowedTagsExtended, allowedTagsBasic);
function isHTMLElement(elem: Element): elem is HTMLElement {
return elem instanceof HTMLElement;
}
// filtering from another field
let filterInternalNode = function (elem: Element) {
if (isHTMLElement(elem)) {
elem.style.removeProperty("background-color");
elem.style.removeProperty("font-size");
elem.style.removeProperty("font-family");
}
// recurse
for (let i = 0; i < elem.children.length; i++) {
const child = elem.children[i];
filterInternalNode(child);
}
};
// filtering from external sources
let filterNode = function (node: Node, extendedMode: boolean): void {
if (!nodeIsElement(node)) {
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
for (const child of [...node.children]) {
filterNode(child, extendedMode);
}
if (node.tagName === "ANKITOP") {
return;
}
const tag = extendedMode
? allowedTagsExtended[node.tagName]
: allowedTagsBasic[node.tagName];
if (!tag) {
if (!node.innerHTML || node.tagName === "TITLE") {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else {
if (typeof tag === "function") {
// filtering function provided
tag(node);
} else {
// allowed, filter out attributes
for (const attr of [...node.attributes]) {
const attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) === -1) {
node.removeAttributeNode(attr);
}
}
}
}
};

65
ts/editor/helpers.ts Normal file
View file

@ -0,0 +1,65 @@
export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
const INLINE_TAGS = [
"A",
"ABBR",
"ACRONYM",
"AUDIO",
"B",
"BDI",
"BDO",
"BIG",
"BR",
"BUTTON",
"CANVAS",
"CITE",
"CODE",
"DATA",
"DATALIST",
"DEL",
"DFN",
"EM",
"EMBED",
"I",
"IFRAME",
"IMG",
"INPUT",
"INS",
"KBD",
"LABEL",
"MAP",
"MARK",
"METER",
"NOSCRIPT",
"OBJECT",
"OUTPUT",
"PICTURE",
"PROGRESS",
"Q",
"RUBY",
"S",
"SAMP",
"SCRIPT",
"SELECT",
"SLOT",
"SMALL",
"SPAN",
"STRONG",
"SUB",
"SUP",
"SVG",
"TEMPLATE",
"TEXTAREA",
"TIME",
"U",
"TT",
"VAR",
"VIDEO",
"WBR",
];
export function nodeIsInline(node: Node): boolean {
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
}

View file

@ -1,12 +1,25 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
import { filterHTML } from "./filterHtml";
import { nodeIsElement, nodeIsInline } from "./helpers";
import { bridgeCommand } from "./lib";
let currentField: EditingArea | null = null; let currentField: EditingArea | null = null;
let changeTimer: number | null = null; let changeTimer: number | null = null;
let currentNoteId: number | null = null; let currentNoteId: number | null = null;
declare interface String { declare global {
format(...args: string[]): string; interface String {
format(...args: string[]): string;
}
interface Selection {
modify(s: string, t: string, u: string): void;
addRange(r: Range): void;
removeAllRanges(): void;
getRangeAt(n: number): Range;
}
} }
/* kept for compatibility with add-ons */ /* kept for compatibility with add-ons */
@ -18,11 +31,11 @@ String.prototype.format = function (...args: string[]): string {
}); });
}; };
function setFGButton(col: string): void { export function setFGButton(col: string): void {
document.getElementById("forecolor").style.backgroundColor = col; document.getElementById("forecolor").style.backgroundColor = col;
} }
function saveNow(keepFocus: boolean): void { export function saveNow(keepFocus: boolean): void {
if (!currentField) { if (!currentField) {
return; return;
} }
@ -45,10 +58,6 @@ function triggerKeyTimer(): void {
}, 600); }, 600);
} }
interface Selection {
modify(s: string, t: string, u: string): void;
}
function onKey(evt: KeyboardEvent): void { function onKey(evt: KeyboardEvent): void {
// esc clears focus, allowing dialog to close // esc clears focus, allowing dialog to close
if (evt.code === "Escape") { if (evt.code === "Escape") {
@ -100,72 +109,6 @@ function onKeyUp(evt: KeyboardEvent): void {
} }
} }
function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
const INLINE_TAGS = [
"A",
"ABBR",
"ACRONYM",
"AUDIO",
"B",
"BDI",
"BDO",
"BIG",
"BR",
"BUTTON",
"CANVAS",
"CITE",
"CODE",
"DATA",
"DATALIST",
"DEL",
"DFN",
"EM",
"EMBED",
"I",
"IFRAME",
"IMG",
"INPUT",
"INS",
"KBD",
"LABEL",
"MAP",
"MARK",
"METER",
"NOSCRIPT",
"OBJECT",
"OUTPUT",
"PICTURE",
"PROGRESS",
"Q",
"RUBY",
"S",
"SAMP",
"SCRIPT",
"SELECT",
"SLOT",
"SMALL",
"SPAN",
"STRONG",
"SUB",
"SUP",
"SVG",
"TEMPLATE",
"TEXTAREA",
"TIME",
"U",
"TT",
"VAR",
"VIDEO",
"WBR",
];
function nodeIsInline(node: Node): boolean {
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
}
function inListItem(): boolean { function inListItem(): boolean {
const anchor = currentField.getSelection().anchorNode; const anchor = currentField.getSelection().anchorNode;
@ -179,7 +122,7 @@ function inListItem(): boolean {
return inList; return inList;
} }
function insertNewline(): void { export function insertNewline(): void {
if (!inPreEnvironment()) { if (!inPreEnvironment()) {
setFormat("insertText", "\n"); setFormat("insertText", "\n");
return; return;
@ -228,12 +171,12 @@ function updateButtonState(): void {
// 'col': document.queryCommandValue("forecolor") // 'col': document.queryCommandValue("forecolor")
} }
function toggleEditorButton(buttonid: string): void { export function toggleEditorButton(buttonid: string): void {
const button = $(buttonid)[0]; const button = $(buttonid)[0];
button.classList.toggle("highlighted"); button.classList.toggle("highlighted");
} }
function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
document.execCommand(cmd, false, arg); document.execCommand(cmd, false, arg);
if (!nosave) { if (!nosave) {
saveField("key"); saveField("key");
@ -256,7 +199,7 @@ function onFocus(evt: FocusEvent): void {
} }
elem.focusEditable(); elem.focusEditable();
currentField = elem; currentField = elem;
pycmd(`focus:${currentField.ord}`); bridgeCommand(`focus:${currentField.ord}`);
enableButtons(); enableButtons();
// do this twice so that there's no flicker on newer versions // do this twice so that there's no flicker on newer versions
caretToEnd(); caretToEnd();
@ -279,7 +222,7 @@ function onFocus(evt: FocusEvent): void {
} }
} }
function focusField(n: number): void { export function focusField(n: number): void {
const field = getEditorField(n); const field = getEditorField(n);
if (field) { if (field) {
@ -287,7 +230,7 @@ function focusField(n: number): void {
} }
} }
function focusIfField(x: number, y: number): boolean { export function focusIfField(x: number, y: number): boolean {
const elements = document.elementsFromPoint(x, y); const elements = document.elementsFromPoint(x, y);
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
let elem = elements[i] as EditingArea; let elem = elements[i] as EditingArea;
@ -303,7 +246,7 @@ function focusIfField(x: number, y: number): boolean {
} }
function onPaste(): void { function onPaste(): void {
pycmd("paste"); bridgeCommand("paste");
window.event.preventDefault(); window.event.preventDefault();
} }
@ -353,7 +296,9 @@ function saveField(type: "blur" | "key"): void {
return; return;
} }
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`); bridgeCommand(
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
);
} }
function wrappedExceptForWhitespace(text: string, front: string, back: string): string { function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
@ -361,7 +306,7 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string):
return match[1] + front + match[2] + back + match[3]; return match[1] + front + match[2] + back + match[3];
} }
function preventButtonFocus(): void { export function preventButtonFocus(): void {
for (const element of document.querySelectorAll("button.linkb")) { for (const element of document.querySelectorAll("button.linkb")) {
element.addEventListener("mousedown", (evt: Event) => { element.addEventListener("mousedown", (evt: Event) => {
evt.preventDefault(); evt.preventDefault();
@ -386,7 +331,7 @@ function maybeDisableButtons(): void {
} }
} }
function wrap(front: string, back: string): void { export function wrap(front: string, back: string): void {
wrapInternal(front, back, false); wrapInternal(front, back, false);
} }
@ -419,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
} }
function onCutOrCopy(): boolean { function onCutOrCopy(): boolean {
pycmd("cutOrCopy"); bridgeCommand("cutOrCopy");
return true; return true;
} }
@ -606,12 +551,12 @@ function adjustFieldAmount(amount: number): void {
} }
} }
function getEditorField(n: number): EditorField | null { export function getEditorField(n: number): EditorField | null {
const fields = document.getElementById("fields").children; const fields = document.getElementById("fields").children;
return (fields[n] as EditorField) ?? null; return (fields[n] as EditorField) ?? null;
} }
function forEditorField<T>( export function forEditorField<T>(
values: T[], values: T[],
func: (field: EditorField, value: T) => void func: (field: EditorField, value: T) => void
): void { ): void {
@ -622,7 +567,7 @@ function forEditorField<T>(
} }
} }
function setFields(fields: [string, string][]): void { export function setFields(fields: [string, string][]): void {
// webengine will include the variable after enter+backspace // webengine will include the variable after enter+backspace
// if we don't convert it to a literal colour // if we don't convert it to a literal colour
const color = window const color = window
@ -637,7 +582,7 @@ function setFields(fields: [string, string][]): void {
maybeDisableButtons(); maybeDisableButtons();
} }
function setBackgrounds(cols: ("dupe" | "")[]) { export function setBackgrounds(cols: ("dupe" | "")[]) {
forEditorField(cols, (field, value) => forEditorField(cols, (field, value) =>
field.editingArea.classList.toggle("dupe", value === "dupe") field.editingArea.classList.toggle("dupe", value === "dupe")
); );
@ -646,17 +591,17 @@ function setBackgrounds(cols: ("dupe" | "")[]) {
.classList.toggle("is-inactive", !cols.includes("dupe")); .classList.toggle("is-inactive", !cols.includes("dupe"));
} }
function setFonts(fonts: [string, number, boolean][]): void { export function setFonts(fonts: [string, number, boolean][]): void {
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => { forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr"); field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
}); });
} }
function setNoteId(id: number): void { export function setNoteId(id: number): void {
currentNoteId = id; currentNoteId = id;
} }
let pasteHTML = function ( export let pasteHTML = function (
html: string, html: string,
internal: boolean, internal: boolean,
extendedMode: boolean extendedMode: boolean
@ -667,172 +612,3 @@ let pasteHTML = function (
setFormat("inserthtml", html); setFormat("inserthtml", html);
} }
}; };
let filterHTML = function (
html: string,
internal: boolean,
extendedMode: boolean
): string {
// wrap it in <top> as we aren't allowed to change top level elements
const top = document.createElement("ankitop");
top.innerHTML = html;
if (internal) {
filterInternalNode(top);
} else {
filterNode(top, extendedMode);
}
let outHtml = top.innerHTML;
if (!extendedMode && !internal) {
// collapse whitespace
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
}
outHtml = outHtml.trim();
return outHtml;
};
let allowedTagsBasic = {};
let allowedTagsExtended = {};
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsBasic[tag] = { attrs: [] };
}
TAGS_WITHOUT_ATTRS = [
"B",
"BLOCKQUOTE",
"CODE",
"DD",
"DL",
"DT",
"EM",
"H1",
"H2",
"H3",
"I",
"LI",
"OL",
"PRE",
"RP",
"RT",
"RUBY",
"STRONG",
"TABLE",
"U",
"UL",
];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsExtended[tag] = { attrs: [] };
}
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
allowedTagsExtended["A"] = { attrs: ["HREF"] };
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
const allowedStyling = {
color: true,
"background-color": true,
"font-weight": true,
"font-style": true,
"text-decoration-line": true,
};
let isNightMode = function (): boolean {
return document.body.classList.contains("nightMode");
};
let filterExternalSpan = function (elem: HTMLElement) {
// filter out attributes
for (const attr of [...elem.attributes]) {
const attrName = attr.name.toUpperCase();
if (attrName !== "STYLE") {
elem.removeAttributeNode(attr);
}
}
// filter styling
for (const name of [...elem.style]) {
const value = elem.style.getPropertyValue(name);
if (
!allowedStyling.hasOwnProperty(name) ||
// google docs adds this unnecessarily
(name === "background-color" && value === "transparent") ||
// ignore coloured text in night mode for now
(isNightMode() && (name === "background-color" || name === "color"))
) {
elem.style.removeProperty(name);
}
}
};
allowedTagsExtended["SPAN"] = filterExternalSpan;
// add basic tags to extended
Object.assign(allowedTagsExtended, allowedTagsBasic);
function isHTMLElement(elem: Element): elem is HTMLElement {
return elem instanceof HTMLElement;
}
// filtering from another field
let filterInternalNode = function (elem: Element) {
if (isHTMLElement(elem)) {
elem.style.removeProperty("background-color");
elem.style.removeProperty("font-size");
elem.style.removeProperty("font-family");
}
// recurse
for (let i = 0; i < elem.children.length; i++) {
const child = elem.children[i];
filterInternalNode(child);
}
};
// filtering from external sources
let filterNode = function (node: Node, extendedMode: boolean): void {
if (!nodeIsElement(node)) {
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
for (const child of [...node.children]) {
filterNode(child, extendedMode);
}
if (node.tagName === "ANKITOP") {
return;
}
const tag = extendedMode
? allowedTagsExtended[node.tagName]
: allowedTagsBasic[node.tagName];
if (!tag) {
if (!node.innerHTML || node.tagName === "TITLE") {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else {
if (typeof tag === "function") {
// filtering function provided
tag(node);
} else {
// allowed, filter out attributes
for (const attr of [...node.attributes]) {
const attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) === -1) {
node.removeAttributeNode(attr);
}
}
}
}
};

9
ts/editor/lib.ts Normal file
View file

@ -0,0 +1,9 @@
declare global {
interface Window {
bridgeCommand<T>(command: string, callback?: (value: T) => void): void;
}
}
export function bridgeCommand<T>(command: string, callback?: (value: T) => void): void {
window.bridgeCommand<T>(command, callback);
}

15
ts/rollup.aqt.config.js Normal file
View file

@ -0,0 +1,15 @@
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { terser } from "rollup-plugin-terser";
import process from "process";
const production = process.env["COMPILATION_MODE"] === "opt";
export default {
output: {
name: "globalThis",
extend: true,
format: "iife",
},
plugins: [resolve({ browser: true }), commonjs(), production && terser()],
};