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
|
whether it stays inside its container, and what it says to its host, which is
|
||||||
logged along the bottom.
|
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
|
### Backend
|
||||||
|
|
||||||
A docker compose file `docker-compose-dev.yml` is provided to start the
|
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
|
// 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
|
// 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
|
// choice — the standalone app puts them first, this build puts them in the
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
|
"postcss-selector-parser": "^7.1.1",
|
||||||
"posthog-js": "1.408.2",
|
"posthog-js": "1.408.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "19",
|
"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 }) => {
|
test("tells its host what it is doing", async ({ page }) => {
|
||||||
const { username, roomId } = await createUserAndRoom("hostbridge");
|
const { username, roomId } = await createUserAndRoom("hostbridge");
|
||||||
const panes = await startHarness(page, username, roomId);
|
const panes = await startHarness(page, username, roomId);
|
||||||
|
|||||||
Generated
+3
@@ -223,6 +223,9 @@ importers:
|
|||||||
postcss-preset-env:
|
postcss-preset-env:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.6.1(postcss@8.5.25)
|
version: 10.6.1(postcss@8.5.25)
|
||||||
|
postcss-selector-parser:
|
||||||
|
specifier: ^7.1.1
|
||||||
|
version: 7.1.1
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: 1.408.2
|
specifier: 1.408.2
|
||||||
version: 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
|
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
|
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
|
would style the host's own document. What remains here still speaks of the
|
||||||
Element Call's container — normalize.css and the typography below use bare
|
document — normalize.css and the typography below use bare element selectors,
|
||||||
element selectors, and the custom properties are declared on `:root` — so a
|
and the custom properties are declared on `:root` — which is right for the
|
||||||
host gets those too. Narrowing them needs a real host to check against, so it
|
page, and is why the component build rewrites it: there every selector is
|
||||||
waits for the Element Web integration rather than being guessed at here.
|
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
|
Nothing here should depend on where it lands relative to Element Call's
|
||||||
component stylesheets: the bundler decides that, and it decides differently for
|
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.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pre {
|
.pre {
|
||||||
font-size: var(--font-size-micro);
|
font-size: var(--font-size-micro);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -680,9 +680,9 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
<AudioProcessingSettings />
|
<AudioProcessingSettings />
|
||||||
<Separator />
|
<Separator />
|
||||||
<p>{t("developer_mode.environment_variables")}</p>
|
<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>
|
<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 * as fs from "node:fs";
|
||||||
|
|
||||||
import { vitePluginsConfig } from "./vite.config";
|
import { vitePluginsConfig } from "./vite.config";
|
||||||
|
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
||||||
|
|
||||||
// Serves the harness under `component/dev`, which embeds Element Call as a
|
// Serves the harness under `component/dev`, which embeds Element Call as a
|
||||||
// component the way a host application would. Development only: this is not
|
// component the way a host application would. Development only: this is not
|
||||||
@@ -35,6 +36,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
return {
|
return {
|
||||||
...vitePluginsConfig({ mode, html: false }),
|
...vitePluginsConfig({ mode, html: false }),
|
||||||
root: "component/dev",
|
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
|
// So that the harness can read the same config.json the standalone app
|
||||||
// does, if the developer has written one
|
// does, if the developer has written one
|
||||||
publicDir: "../../public",
|
publicDir: "../../public",
|
||||||
|
|||||||
@@ -8,18 +8,35 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
import { vitePluginsConfig } from "./vite.config";
|
import { vitePluginsConfig } from "./vite.config";
|
||||||
|
import { scopeStylesToRoot } from "./component/build/scopeStylesToRoot";
|
||||||
|
|
||||||
// Config for Element Call as a React component, to be imported by an
|
// 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.
|
// 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
|
// 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.
|
// produce a page and brings an HTML entry point along with it.
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => {
|
||||||
...vitePluginsConfig({ mode, html: false }),
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
// A library has no public directory to serve. Without this the build copies
|
// 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
|
// whatever is in `public` — including the developer's own config.json, which
|
||||||
// is not in the repository — into the output we would publish.
|
// is not in the repository — into the output we would publish.
|
||||||
publicDir: false,
|
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: {
|
build: {
|
||||||
minify: mode === "production",
|
minify: mode === "production",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
@@ -47,6 +64,9 @@ export default defineConfig(({ mode }) => ({
|
|||||||
external: [
|
external: [
|
||||||
"react",
|
"react",
|
||||||
"react/jsx-runtime",
|
"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",
|
||||||
"react-dom/client",
|
"react-dom/client",
|
||||||
"livekit-client",
|
"livekit-client",
|
||||||
@@ -71,4 +91,5 @@ export default defineConfig(({ mode }) => ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|||||||
+5
-1
@@ -21,7 +21,11 @@ export default defineConfig((configEnv) =>
|
|||||||
css: { include: /.+/ },
|
css: { include: /.+/ },
|
||||||
setupFiles: ["src/vitest.setup.ts"],
|
setupFiles: ["src/vitest.setup.ts"],
|
||||||
environment: "jsdom",
|
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