mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
de-globalise styles
This commit is contained in:
@@ -230,6 +230,12 @@ Element Call behaves when it does not own the page — the size it is given,
|
||||
whether it stays inside its container, and what it says to its host, which is
|
||||
logged along the bottom.
|
||||
|
||||
The component's stylesheet is confined to the element it is mounted in: the
|
||||
build rewrites every selector so that it matches only Element Call's root or
|
||||
what is inside it, with `html`, `body` and `:root` standing for that root (see
|
||||
`component/build/scopeStylesToRoot.ts`). A host's own page keeps its styles,
|
||||
and Element Call brings its own fonts and design tokens along.
|
||||
|
||||
### Backend
|
||||
|
||||
A docker compose file `docker-compose-dev.yml` is provided to start the
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import postcss from "postcss";
|
||||
|
||||
import { ROOT_SELECTOR, scopeStylesToRoot } from "./scopeStylesToRoot";
|
||||
|
||||
const inRoot = `:where(${ROOT_SELECTOR}, ${ROOT_SELECTOR} *)`;
|
||||
const isRoot = `:where(${ROOT_SELECTOR})`;
|
||||
|
||||
async function scope(css: string, file = "base.css"): Promise<string> {
|
||||
const result = await postcss([scopeStylesToRoot()]).process(css, {
|
||||
from: file,
|
||||
});
|
||||
return result.css;
|
||||
}
|
||||
|
||||
describe("scopeStylesToRoot", () => {
|
||||
it("makes the root stand in for the document", async () => {
|
||||
expect(await scope("html { line-height: 1.15 }")).toBe(
|
||||
`${isRoot} { line-height: 1.15 }`,
|
||||
);
|
||||
expect(await scope("body { margin: 0 }")).toBe(`${isRoot} { margin: 0 }`);
|
||||
expect(await scope(":root { --a: 1 }")).toBe(`${isRoot} { --a: 1 }`);
|
||||
expect(await scope("body.no-scroll-body { position: fixed }")).toBe(
|
||||
`${isRoot}.no-scroll-body { position: fixed }`,
|
||||
);
|
||||
expect(await scope("body .x { color: red }")).toBe(
|
||||
`${isRoot} .x { color: red }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("collapses selectors that all became the root", async () => {
|
||||
expect(await scope("html, body, input { font: inherit }")).toBe(
|
||||
`${isRoot},input${inRoot} { font: inherit }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("confines everything else to the root and what is inside it", async () => {
|
||||
expect(await scope("h1 { margin: 0 }")).toBe(`h1${inRoot} { margin: 0 }`);
|
||||
expect(await scope(".cpd-theme-dark { --a: 1 }")).toBe(
|
||||
`.cpd-theme-dark${inRoot} { --a: 1 }`,
|
||||
);
|
||||
expect(await scope("* { box-sizing: border-box }")).toBe(
|
||||
`*${inRoot} { box-sizing: border-box }`,
|
||||
);
|
||||
expect(await scope(".a > .b + .c { color: red }")).toBe(
|
||||
`.a>.b+.c${inRoot} { color: red }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps pseudo-elements last", async () => {
|
||||
expect(await scope("button::-moz-focus-inner { border: 0 }")).toBe(
|
||||
`button${inRoot}::-moz-focus-inner { border: 0 }`,
|
||||
);
|
||||
expect(await scope(".a .b:hover::after { content: '' }")).toBe(
|
||||
`.a .b:hover${inRoot}::after { content: '' }`,
|
||||
);
|
||||
expect(await scope("p:first-letter { color: red }")).toBe(
|
||||
`p${inRoot}:first-letter { color: red }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves alone what already names the root", async () => {
|
||||
const css = `${ROOT_SELECTOR}[data-platform="ios"] { --a: 1 }`;
|
||||
expect(await scope(css)).toBe(css);
|
||||
});
|
||||
|
||||
it("reaches into layers and media queries", async () => {
|
||||
expect(
|
||||
await scope(
|
||||
"@layer normalize { h1 { margin: 0 } } @media (min-width: 1px) { p { margin: 0 } }",
|
||||
),
|
||||
).toBe(
|
||||
`@layer normalize { h1${inRoot} { margin: 0 } } @media (min-width: 1px) { p${inRoot} { margin: 0 } }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not touch keyframes or nested rules", async () => {
|
||||
expect(
|
||||
await scope("@keyframes spin { from { opacity: 0 } to { opacity: 1 } }"),
|
||||
).toBe("@keyframes spin { from { opacity: 0 } to { opacity: 1 } }");
|
||||
expect(
|
||||
await scope(
|
||||
".a { color: red; &:hover { color: blue } .b { color: green } }",
|
||||
),
|
||||
).toBe(
|
||||
`.a${inRoot} { color: red; &:hover { color: blue } .b { color: green } }`,
|
||||
);
|
||||
});
|
||||
|
||||
it("only touches the bare selectors of a CSS module", async () => {
|
||||
const file = "Settings.module.css";
|
||||
expect(await scope("pre { font-size: 1px }", file)).toBe(
|
||||
`pre${inRoot} { font-size: 1px }`,
|
||||
);
|
||||
expect(await scope(".modal pre { font-size: 1px }", file)).toBe(
|
||||
`.modal pre${inRoot} { font-size: 1px }`,
|
||||
);
|
||||
expect(await scope(".box_abc12 { border: 0 }", file)).toBe(
|
||||
".box_abc12 { border: 0 }",
|
||||
);
|
||||
expect(await scope(".a .b_abc12:hover { border: 0 }", file)).toBe(
|
||||
".a .b_abc12:hover { border: 0 }",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type AtRule,
|
||||
type Container,
|
||||
type Document,
|
||||
type Plugin,
|
||||
type Rule,
|
||||
} from "postcss";
|
||||
import selectorParser, {
|
||||
type Node,
|
||||
type Pseudo,
|
||||
type Selector,
|
||||
} from "postcss-selector-parser";
|
||||
|
||||
/**
|
||||
* How the stylesheets find Element Call's root element. The attribute is put
|
||||
* there by `useTheme`, on the container the host gives the component.
|
||||
*/
|
||||
export const ROOT_SELECTOR = "[data-element-call-root]";
|
||||
|
||||
// Both are `:where()`, which has no specificity of its own, so the rules keep
|
||||
// exactly the weight they had before being scoped and nothing in Element Call's
|
||||
// cascade changes — only where it applies.
|
||||
//
|
||||
// The root, or anything inside it. Appended to the element a rule is about,
|
||||
// rather than prepended to the whole selector, so that a rule about the root
|
||||
// itself (its theme class, say) still matches.
|
||||
const IN_ROOT = `:where(${ROOT_SELECTOR}, ${ROOT_SELECTOR} *)`;
|
||||
// The root itself, standing in for the document.
|
||||
const IS_ROOT = `:where(${ROOT_SELECTOR})`;
|
||||
|
||||
/**
|
||||
* Confines a stylesheet to Element Call's root element, for the build of
|
||||
* Element Call as a component.
|
||||
*
|
||||
* As a page of its own, Element Call can style the document: normalize.css and
|
||||
* Compound speak of `html`, `body` and bare elements, and the design tokens are
|
||||
* declared on `:root`. Embedded in a host, all of that would land on the host's
|
||||
* document too. This rewrites every selector so that it matches only the root
|
||||
* or its descendants:
|
||||
*
|
||||
* - `html`, `body` and `:root` become the root element, which is what stands in
|
||||
* for the document inside a host.
|
||||
* - Everything else keeps its selector and gains `:where([data-element-call-root],
|
||||
* [data-element-call-root] *)` on the element it styles.
|
||||
* - Selectors that already name the root are left alone, as are keyframe
|
||||
* selectors and rules nested inside another rule, which are relative to it.
|
||||
*
|
||||
* CSS modules are scoped by their class names already, so only their selectors
|
||||
* that would match by element alone — `pre` rather than `.pre` — are touched.
|
||||
*
|
||||
* The root's fonts and design tokens are still inherited by everything inside
|
||||
* it, the way they were from `body` and `:root`, and `@font-face` declarations
|
||||
* stay global, which they are by nature.
|
||||
*/
|
||||
export function scopeStylesToRoot(): Plugin {
|
||||
return {
|
||||
postcssPlugin: "element-call-scope-styles-to-root",
|
||||
Once(root) {
|
||||
const isModule =
|
||||
root.source?.input.file?.endsWith(".module.css") ?? false;
|
||||
root.walkRules((rule) => {
|
||||
if (isRelative(rule)) return;
|
||||
rule.selector = (isModule ? scopeBare : scopeAll).processSync(
|
||||
rule.selector,
|
||||
{ lossless: false },
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a rule's selectors are relative to something other than the document. */
|
||||
function isRelative(rule: Rule): boolean {
|
||||
let parent: Container | Document | undefined = rule.parent;
|
||||
while (parent !== undefined) {
|
||||
if (parent.type === "rule") return true;
|
||||
if (parent.type === "atrule") {
|
||||
const { name } = parent as AtRule;
|
||||
if (name.endsWith("keyframes") || name === "page") return true;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const processor = (isModule: boolean): ReturnType<typeof selectorParser> =>
|
||||
selectorParser((selectors) => {
|
||||
selectors.each((selector) => {
|
||||
scopeSelector(selector, isModule);
|
||||
});
|
||||
// Mapping `html, body` onto the root leaves the same selector twice
|
||||
const seen = new Set<string>();
|
||||
selectors.each((selector) => {
|
||||
const text = String(selector).trim();
|
||||
if (seen.has(text)) selector.remove();
|
||||
else seen.add(text);
|
||||
});
|
||||
});
|
||||
|
||||
// Everything, for stylesheets that speak of the document; only what a class
|
||||
// does not already confine, for CSS modules
|
||||
const scopeAll = processor(false);
|
||||
const scopeBare = processor(true);
|
||||
|
||||
function scopeSelector(selector: Selector, isModule: boolean): void {
|
||||
if (String(selector).includes(ROOT_SELECTOR)) return;
|
||||
|
||||
const compounds = splitCompounds(selector);
|
||||
if (compounds.length === 0) return;
|
||||
|
||||
// Something said of the document is said of the root instead
|
||||
const document = compounds[0].find(isDocumentSelector);
|
||||
if (document !== undefined) {
|
||||
document.replaceWith(pseudo(IS_ROOT));
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = compounds.at(-1)!;
|
||||
if (isModule && subject.some((node) => node.type === "class")) return;
|
||||
|
||||
// Pseudo-elements have to come last in a compound selector
|
||||
const pseudoElement = subject.find(isPseudoElement);
|
||||
if (pseudoElement === undefined) selector.append(pseudo(IN_ROOT));
|
||||
else selector.insertBefore(pseudoElement, pseudo(IN_ROOT));
|
||||
}
|
||||
|
||||
/** The compound selectors making up a complex selector, in order. */
|
||||
function splitCompounds(selector: Selector): Node[][] {
|
||||
const compounds: Node[][] = [[]];
|
||||
for (const node of selector.nodes) {
|
||||
if (node.type === "combinator") compounds.push([]);
|
||||
else if (node.type !== "comment") compounds.at(-1)!.push(node);
|
||||
}
|
||||
return compounds.filter((compound) => compound.length > 0);
|
||||
}
|
||||
|
||||
function isDocumentSelector(node: Node): boolean {
|
||||
return (
|
||||
(node.type === "tag" && (node.value === "html" || node.value === "body")) ||
|
||||
(node.type === "pseudo" && node.value === ":root")
|
||||
);
|
||||
}
|
||||
|
||||
function isPseudoElement(node: Node): node is Pseudo {
|
||||
if (node.type !== "pseudo") return false;
|
||||
return (
|
||||
node.value.startsWith("::") ||
|
||||
[":before", ":after", ":first-line", ":first-letter"].includes(node.value)
|
||||
);
|
||||
}
|
||||
|
||||
function pseudo(text: string): Pseudo {
|
||||
return selectorParser().astSync(text).nodes[0].nodes[0].clone() as Pseudo;
|
||||
}
|
||||
+4
-1
@@ -18,7 +18,10 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
// The design tokens, fonts and element defaults every Element Call stylesheet
|
||||
// builds on.
|
||||
// builds on. Written for a page, they speak of `html`, `body` and bare
|
||||
// elements; the component build confines them, and every other stylesheet in
|
||||
// this bundle, to the root element below (see build/scopeStylesToRoot.ts), so
|
||||
// that the host's document is left as it was.
|
||||
//
|
||||
// Where these land relative to the component stylesheets is the bundler's
|
||||
// choice — the standalone app puts them first, this build puts them in the
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"pako": "^2.0.4",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-selector-parser": "^7.1.1",
|
||||
"posthog-js": "1.408.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19",
|
||||
|
||||
@@ -77,6 +77,38 @@ test("keeps its modals inside the container it was given", async ({ page }) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves the host's own page unstyled", async ({ page }) => {
|
||||
const { username, roomId } = await createUserAndRoom("hoststyles");
|
||||
const panes = await startHarness(page, username, roomId);
|
||||
await expect(panes.first().getByTestId("lobby_joinCall")).toBeVisible({
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
// Element Call's stylesheet is written for a page of its own: normalize.css
|
||||
// gives `html` a line height, Compound gives `body` its font and feature
|
||||
// settings, and the design tokens live on `:root`. None of that may reach the
|
||||
// host's document — the harness sets none of these itself, so anything other
|
||||
// than the browser's defaults here came from us.
|
||||
const host = await page.evaluate(() => {
|
||||
const html = getComputedStyle(document.documentElement);
|
||||
const body = getComputedStyle(document.body);
|
||||
return {
|
||||
lineHeight: html.lineHeight,
|
||||
fontFeatureSettings: body.fontFeatureSettings,
|
||||
token: html.getPropertyValue("--cpd-color-text-primary"),
|
||||
};
|
||||
});
|
||||
expect(host).toEqual({
|
||||
lineHeight: "normal",
|
||||
fontFeatureSettings: "normal",
|
||||
token: "",
|
||||
});
|
||||
|
||||
// While inside the container, the same rules do apply
|
||||
const root = panes.first().locator("[data-element-call-root]");
|
||||
await expect(root).toHaveCSS("font-feature-settings", /"kern"/);
|
||||
});
|
||||
|
||||
test("tells its host what it is doing", async ({ page }) => {
|
||||
const { username, roomId } = await createUserAndRoom("hostbridge");
|
||||
const panes = await startHarness(page, username, roomId);
|
||||
|
||||
Generated
+3
@@ -223,6 +223,9 @@ importers:
|
||||
postcss-preset-env:
|
||||
specifier: ^10.0.0
|
||||
version: 10.6.1(postcss@8.5.25)
|
||||
postcss-selector-parser:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1
|
||||
posthog-js:
|
||||
specifier: 1.408.2
|
||||
version: 1.408.2
|
||||
|
||||
+8
-5
@@ -10,11 +10,14 @@ and element defaults its own stylesheets build on top of.
|
||||
|
||||
Split out from index.css so that Element Call embedded in a host application
|
||||
can have these without also being given the standalone page's layout, which
|
||||
would style the host's own document. What remains here does still reach outside
|
||||
Element Call's container — normalize.css and the typography below use bare
|
||||
element selectors, and the custom properties are declared on `:root` — so a
|
||||
host gets those too. Narrowing them needs a real host to check against, so it
|
||||
waits for the Element Web integration rather than being guessed at here.
|
||||
would style the host's own document. What remains here still speaks of the
|
||||
document — normalize.css and the typography below use bare element selectors,
|
||||
and the custom properties are declared on `:root` — which is right for the
|
||||
page, and is why the component build rewrites it: there every selector is
|
||||
confined to Element Call's root element (see component/build/scopeStylesToRoot.ts),
|
||||
with `html`, `body` and `:root` becoming that element. Nothing needs to be
|
||||
written differently here for that to work, but nothing here may rely on
|
||||
reaching the host's document either.
|
||||
|
||||
Nothing here should depend on where it lands relative to Element Call's
|
||||
component stylesheets: the bundler decides that, and it decides differently for
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
pre {
|
||||
.pre {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
|
||||
|
||||
@@ -680,9 +680,9 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
<AudioProcessingSettings />
|
||||
<Separator />
|
||||
<p>{t("developer_mode.environment_variables")}</p>
|
||||
<pre>{JSON.stringify(env, null, 2)}</pre>
|
||||
<pre className={styles.pre}>{JSON.stringify(env, null, 2)}</pre>
|
||||
<p>{t("developer_mode.url_params")}</p>
|
||||
<pre>{JSON.stringify(urlParams, null, 2)}</pre>
|
||||
<pre className={styles.pre}>{JSON.stringify(urlParams, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { realpathSync } from "node:fs";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { vitePluginsConfig } from "./vite.config";
|
||||
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
||||
|
||||
// Serves the harness under `component/dev`, which embeds Element Call as a
|
||||
// component the way a host application would. Development only: this is not
|
||||
@@ -35,6 +36,9 @@ export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
...vitePluginsConfig({ mode, html: false }),
|
||||
root: "component/dev",
|
||||
// The same scoping the library build applies, so the harness shows what a
|
||||
// host will get — including whether its own page is left alone
|
||||
css: { postcss: { plugins: [scopeStylesToRoot()] } },
|
||||
// So that the harness can read the same config.json the standalone app
|
||||
// does, if the developer has written one
|
||||
publicDir: "../../public",
|
||||
|
||||
+77
-56
@@ -8,67 +8,88 @@ Please see LICENSE in the repository root for full details.
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import { vitePluginsConfig } from "./vite.config";
|
||||
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
||||
|
||||
// Config for Element Call as a React component, to be imported by an
|
||||
// application embedding it rather than served as a page of its own.
|
||||
//
|
||||
// Deliberately not built on top of the full app's config, which exists to
|
||||
// produce a page and brings an HTML entry point along with it.
|
||||
export default defineConfig(({ mode }) => ({
|
||||
...vitePluginsConfig({ mode, html: false }),
|
||||
// A library has no public directory to serve. Without this the build copies
|
||||
// whatever is in `public` — including the developer's own config.json, which
|
||||
// is not in the repository — into the output we would publish.
|
||||
publicDir: false,
|
||||
build: {
|
||||
minify: mode === "production",
|
||||
sourcemap: true,
|
||||
// One stylesheet rather than one per chunk, so a host has a single file to
|
||||
// include
|
||||
cssCodeSplit: false,
|
||||
lib: {
|
||||
formats: ["es" as const],
|
||||
entry: "./component/index.tsx",
|
||||
fileName: "element-call",
|
||||
export default defineConfig(({ mode }) => {
|
||||
const base = vitePluginsConfig({ mode, html: false });
|
||||
return {
|
||||
...base,
|
||||
resolve: {
|
||||
...base.resolve,
|
||||
alias: {
|
||||
...base.resolve?.alias,
|
||||
// react-i18next depends on the CommonJS `use-sync-external-store/shim`, whose
|
||||
// `require("react")` cannot be bundled against an external React: rolldown leaves a
|
||||
// `require` shim that throws in the browser. React 18+ provides `useSyncExternalStore`
|
||||
// itself, so point the shim at React.
|
||||
"use-sync-external-store/shim": "react",
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
// The host already has these, and a second copy of any of them does not
|
||||
// merely bloat the bundle: React would hold two sets of hooks, and the
|
||||
// Matrix client would run two sync loops.
|
||||
//
|
||||
// Every subpath has to be named. Element Call reaches most of the Matrix
|
||||
// SDK as `matrix-js-sdk/lib/…`, and a bare "matrix-js-sdk" would not
|
||||
// catch those — while the pattern and callback forms of this option are
|
||||
// silently ignored by the bundler, so they cannot be used to cover them.
|
||||
// `pnpm lint:externals` reads this list and fails if the source imports
|
||||
// one of these packages by a path it does not name; a few entries below
|
||||
// are there only because the standalone app imports them, which costs
|
||||
// nothing.
|
||||
external: [
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"livekit-client",
|
||||
"matrix-js-sdk",
|
||||
"matrix-js-sdk/lib/browser-index",
|
||||
"matrix-js-sdk/lib/client",
|
||||
"matrix-js-sdk/lib/crypto-api",
|
||||
"matrix-js-sdk/lib/indexeddb-worker",
|
||||
"matrix-js-sdk/lib/logger",
|
||||
"matrix-js-sdk/lib/matrix",
|
||||
"matrix-js-sdk/lib/matrixrtc",
|
||||
"matrix-js-sdk/lib/matrixrtc/EncryptionManager",
|
||||
"matrix-js-sdk/lib/matrixrtc/IKeyTransport",
|
||||
"matrix-js-sdk/lib/matrixrtc/IMembershipManager",
|
||||
"matrix-js-sdk/lib/models/relations-container",
|
||||
"matrix-js-sdk/lib/models/room",
|
||||
"matrix-js-sdk/lib/models/typed-event-emitter",
|
||||
"matrix-js-sdk/lib/randomstring",
|
||||
"matrix-js-sdk/lib/sync",
|
||||
"matrix-js-sdk/lib/types",
|
||||
"matrix-js-sdk/lib/utils",
|
||||
],
|
||||
// A library has no public directory to serve. Without this the build copies
|
||||
// whatever is in `public` — including the developer's own config.json, which
|
||||
// is not in the repository — into the output we would publish.
|
||||
publicDir: false,
|
||||
// A host's document is not ours to style: everything in the stylesheet is
|
||||
// confined to the element Element Call is mounted in
|
||||
css: { postcss: { plugins: [scopeStylesToRoot()] } },
|
||||
build: {
|
||||
minify: mode === "production",
|
||||
sourcemap: true,
|
||||
// One stylesheet rather than one per chunk, so a host has a single file to
|
||||
// include
|
||||
cssCodeSplit: false,
|
||||
lib: {
|
||||
formats: ["es" as const],
|
||||
entry: "./component/index.tsx",
|
||||
fileName: "element-call",
|
||||
},
|
||||
rollupOptions: {
|
||||
// The host already has these, and a second copy of any of them does not
|
||||
// merely bloat the bundle: React would hold two sets of hooks, and the
|
||||
// Matrix client would run two sync loops.
|
||||
//
|
||||
// Every subpath has to be named. Element Call reaches most of the Matrix
|
||||
// SDK as `matrix-js-sdk/lib/…`, and a bare "matrix-js-sdk" would not
|
||||
// catch those — while the pattern and callback forms of this option are
|
||||
// silently ignored by the bundler, so they cannot be used to cover them.
|
||||
// `pnpm lint:externals` reads this list and fails if the source imports
|
||||
// one of these packages by a path it does not name; a few entries below
|
||||
// are there only because the standalone app imports them, which costs
|
||||
// nothing.
|
||||
external: [
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
// Emitted by the React Compiler for every compiled component; part of React, so it must be
|
||||
// the host's copy too (bundled, its CommonJS `require("react")` throws in the browser).
|
||||
"react/compiler-runtime",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"livekit-client",
|
||||
"matrix-js-sdk",
|
||||
"matrix-js-sdk/lib/browser-index",
|
||||
"matrix-js-sdk/lib/client",
|
||||
"matrix-js-sdk/lib/crypto-api",
|
||||
"matrix-js-sdk/lib/indexeddb-worker",
|
||||
"matrix-js-sdk/lib/logger",
|
||||
"matrix-js-sdk/lib/matrix",
|
||||
"matrix-js-sdk/lib/matrixrtc",
|
||||
"matrix-js-sdk/lib/matrixrtc/EncryptionManager",
|
||||
"matrix-js-sdk/lib/matrixrtc/IKeyTransport",
|
||||
"matrix-js-sdk/lib/matrixrtc/IMembershipManager",
|
||||
"matrix-js-sdk/lib/models/relations-container",
|
||||
"matrix-js-sdk/lib/models/room",
|
||||
"matrix-js-sdk/lib/models/typed-event-emitter",
|
||||
"matrix-js-sdk/lib/randomstring",
|
||||
"matrix-js-sdk/lib/sync",
|
||||
"matrix-js-sdk/lib/types",
|
||||
"matrix-js-sdk/lib/utils",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
+5
-1
@@ -21,7 +21,11 @@ export default defineConfig((configEnv) =>
|
||||
css: { include: /.+/ },
|
||||
setupFiles: ["src/vitest.setup.ts"],
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
include: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"component/**/*.test.ts",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user