mirror of
https://github.com/element-hq/element-call.git
synced 2026-10-06 16:47:18 +00:00
Add the MatrixRTC SDK package, its harness and a failing smoke test
Set up `sdk/` as a package shaped exactly like `component/`: its own package.json with peer dependencies, the pnpm workspace marker so a host can install it as a git dependency, a build tsconfig for type emission, and a vite lib config with an externals list that `pnpm lint:externals` now checks for both library builds. The package holds only the interface for now. `createRtcSession` returns an empty object; `RtcSession`, `RtcMember`, `MemberMedia` and `TransportMetadata` are the contract Element Call's `CallViewModel` will be rebuilt on (see sdk-plan.md). The shared primitives are re-exported from `src` until the first implementation slice moves them. `sdk/dev` is the smallest consumer of the SDK: a plain page that logs in, joins a room and renders one tile per member, served by `pnpm dev:sdk` on port 3002. Playwright starts it as a third web server, and `playwright/sdk/smoke.spec.ts` has two browsers join one session. It fails on the status line with the SDK's own error until there is an implementation behind the interface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b0dd1c742f
commit
12bb87fa4c
@@ -6,58 +6,73 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks that the component build leaves the packages a host must supply to
|
||||
* the host.
|
||||
* Checks that the library builds leave the packages a host must supply to the
|
||||
* host.
|
||||
*
|
||||
* A host application already has React, the Matrix SDK, LiveKit and Compound,
|
||||
* and a second copy of any of them is worse than dead weight: React would hold
|
||||
* two sets of hooks, the Matrix client would run two sync loops, and a second
|
||||
* Compound would style the tooltips it floats into the host's body with class
|
||||
* names the host's stylesheet does not know. So the
|
||||
* component build lists them as external — but that list has to name every
|
||||
* subpath, since the bundler silently ignores the pattern and callback forms
|
||||
* of the option, and an import it does not cover is bundled with no warning at
|
||||
* all. That is the failure this guards against.
|
||||
* names the host's stylesheet does not know. So each library build lists them
|
||||
* as external — but that list has to name every subpath, since the bundler
|
||||
* silently ignores the pattern and callback forms of the option, and an import
|
||||
* it does not cover is bundled with no warning at all. That is the failure
|
||||
* this guards against.
|
||||
*
|
||||
* It reads the list from the build config itself, so there is one copy of it,
|
||||
* and compares it against every import of those packages in the source.
|
||||
* It reads each list from the build config itself, so there is one copy of it
|
||||
* per build, and compares it against every import of those packages in the
|
||||
* source.
|
||||
*
|
||||
* The comparison is deliberately over-approximate: it looks at all of `src`
|
||||
* rather than only the modules the component actually pulls in, so it will
|
||||
* sometimes ask for a subpath that only the standalone app imports. Listing
|
||||
* one the component never imports costs nothing — the bundler ignores it —
|
||||
* whereas missing one costs a duplicate package.
|
||||
* rather than only the modules a build actually pulls in, so it will sometimes
|
||||
* ask for a subpath that only the standalone app imports. Listing one a build
|
||||
* never imports costs nothing — the bundler ignores it — whereas missing one
|
||||
* costs a duplicate package.
|
||||
*/
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { loadConfigFromFile } from "vite";
|
||||
|
||||
const CONFIG = "vite-component.config.ts";
|
||||
const SOURCES = ["src", "component"];
|
||||
// The development harness is a host, not part of the component: it is the
|
||||
// one that imports what a host supplies (Compound's stylesheets, say).
|
||||
const EXCLUDED = ["component/dev"];
|
||||
|
||||
/** The packages whose duplication would break a host, rather than merely enlarge it. */
|
||||
const MUST_BE_EXTERNAL = [
|
||||
"react",
|
||||
"react-dom",
|
||||
"matrix-js-sdk",
|
||||
"livekit-client",
|
||||
"@vector-im/compound-web",
|
||||
"@vector-im/compound-design-tokens",
|
||||
/**
|
||||
* The library builds, each with the directories it is built from and the
|
||||
* packages whose duplication would break a host rather than merely enlarge it.
|
||||
* A development harness is a host, not part of the library: it is the one that
|
||||
* imports what a host supplies (Compound's stylesheets, say).
|
||||
*/
|
||||
const TARGETS = [
|
||||
{
|
||||
config: "vite-component.config.ts",
|
||||
what: "the component build",
|
||||
sources: ["src", "component"],
|
||||
excluded: ["component/dev"],
|
||||
mustBeExternal: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"matrix-js-sdk",
|
||||
"livekit-client",
|
||||
"@vector-im/compound-web",
|
||||
"@vector-im/compound-design-tokens",
|
||||
],
|
||||
},
|
||||
{
|
||||
config: "vite-sdk.config.ts",
|
||||
what: "the SDK build",
|
||||
sources: ["src", "sdk"],
|
||||
excluded: ["sdk/dev"],
|
||||
mustBeExternal: ["matrix-js-sdk", "livekit-client", "rxjs"],
|
||||
},
|
||||
];
|
||||
|
||||
const isTestFile = (name) =>
|
||||
name.includes(".test.") || name.includes(".stories.");
|
||||
|
||||
/** Every source file under the given directories, recursively. */
|
||||
async function* sourceFiles(dir) {
|
||||
async function* sourceFiles(dir, excluded) {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (EXCLUDED.includes(path)) continue;
|
||||
if (entry.isDirectory()) yield* sourceFiles(path);
|
||||
if (excluded.includes(path)) continue;
|
||||
if (entry.isDirectory()) yield* sourceFiles(path, excluded);
|
||||
else if (/\.(ts|tsx)$/.test(entry.name) && !isTestFile(entry.name))
|
||||
yield path;
|
||||
}
|
||||
@@ -89,60 +104,60 @@ function imports(source) {
|
||||
* with the host's copy of anything. Worker sub-builds do not inherit this
|
||||
* option anyway.
|
||||
*/
|
||||
const mustBeExternal = (specifier) =>
|
||||
const mustBeExternal = (specifier, packages) =>
|
||||
!specifier.includes("?") &&
|
||||
MUST_BE_EXTERNAL.some(
|
||||
(pkg) => specifier === pkg || specifier.startsWith(`${pkg}/`),
|
||||
);
|
||||
packages.some((pkg) => specifier === pkg || specifier.startsWith(`${pkg}/`));
|
||||
|
||||
const loaded = await loadConfigFromFile(
|
||||
{ command: "build", mode: "production" },
|
||||
CONFIG,
|
||||
);
|
||||
if (loaded === null) {
|
||||
console.error(`Could not load ${CONFIG}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const declared = new Set(loaded.config.build?.rollupOptions?.external ?? []);
|
||||
if (declared.size === 0) {
|
||||
console.error(
|
||||
`${CONFIG} declares nothing external. Either the option moved, or the ` +
|
||||
`list is empty; either way this check is not looking at what it thinks.`,
|
||||
/** @returns Whether the target's externals list covers its imports. */
|
||||
async function check({
|
||||
config,
|
||||
what,
|
||||
sources,
|
||||
excluded,
|
||||
mustBeExternal: packages,
|
||||
}) {
|
||||
const loaded = await loadConfigFromFile(
|
||||
{ command: "build", mode: "production" },
|
||||
config,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Where each missing specifier is imported, so the message can point at it
|
||||
const missing = new Map();
|
||||
const seen = new Set();
|
||||
for (const dir of SOURCES)
|
||||
for await (const file of sourceFiles(dir)) {
|
||||
const source = await readFile(file, "utf8");
|
||||
for (const specifier of imports(source)) {
|
||||
if (!mustBeExternal(specifier)) continue;
|
||||
seen.add(specifier);
|
||||
if (declared.has(specifier)) continue;
|
||||
const files = missing.get(specifier) ?? [];
|
||||
files.push(file);
|
||||
missing.set(specifier, files);
|
||||
}
|
||||
if (loaded === null) {
|
||||
console.error(`Could not load ${config}`);
|
||||
return false;
|
||||
}
|
||||
const declared = new Set(loaded.config.build?.rollupOptions?.external ?? []);
|
||||
if (declared.size === 0) {
|
||||
console.error(
|
||||
`${config} declares nothing external. Either the option moved, or the ` +
|
||||
`list is empty; either way this check is not looking at what it thinks.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (missing.size > 0) {
|
||||
// Where each missing specifier is imported, so the message can point at it
|
||||
const missing = new Map();
|
||||
for (const dir of sources)
|
||||
for await (const file of sourceFiles(dir, excluded)) {
|
||||
const source = await readFile(file, "utf8");
|
||||
for (const specifier of imports(source)) {
|
||||
if (!mustBeExternal(specifier, packages)) continue;
|
||||
if (declared.has(specifier)) continue;
|
||||
const files = missing.get(specifier) ?? [];
|
||||
files.push(file);
|
||||
missing.set(specifier, files);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.size === 0) return true;
|
||||
console.error(
|
||||
`${CONFIG} does not declare these imports external, so the component ` +
|
||||
`build would bundle its own copy of them:\n`,
|
||||
`${config} does not declare these imports external, so ${what} ` +
|
||||
`would bundle its own copy of them:\n`,
|
||||
);
|
||||
for (const [specifier, files] of [...missing].sort())
|
||||
console.error(` ${specifier}\n imported by ${files.join(", ")}`);
|
||||
console.error(`\nAdd each one to the \`external\` list in ${CONFIG}.`);
|
||||
process.exit(1);
|
||||
console.error(`\nAdd each one to the \`external\` list in ${config}.\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deliberately no complaint about declarations nothing imports. Some of them
|
||||
// cannot be seen from the source at all — `react/jsx-runtime` is injected by
|
||||
// the JSX transform — and an extra declaration is inert, so there is nothing
|
||||
// to warn about.
|
||||
console.log(
|
||||
`${declared.size} external declarations cover all ${seen.size} imports of ${MUST_BE_EXTERNAL.join(", ")}.`,
|
||||
);
|
||||
let ok = true;
|
||||
for (const target of TARGETS) ok = (await check(target)) && ok;
|
||||
if (!ok) process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user