Files
element-call-Github/eslint/SdkImportBoundary.js
T
Timo K. 5b13e1da81 renames and restructure
- session -> matrixrtc (folder)
 - move MatrixRtcClient to top level of sdk
 - RtcTransport -> Transport
 - backend -> media-backend
2026-10-08 13:53:57 +02:00

129 lines
4.0 KiB
JavaScript

/*
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 { dirname, resolve, sep } from "node:path";
import { ESLintUtils } from "@typescript-eslint/utils";
/**
* Packages a host supplies and the SDK must not know about: UI, i18n and
* Element Call's design system.
*/
const BANNED_PACKAGES = [
"react",
"react-dom",
"react-i18next",
"i18next",
"@vector-im/",
];
/**
* Packages only a media backend may import. The session speaks MatrixRTC and
* sees media through `sdk/src/media-backend/api.ts`; a backend under
* `sdk/src/media-backend/<name>/` is the one place its protocol's library appears.
*/
const BACKEND_PACKAGES = ["livekit-client", "@livekit/"];
/**
* Keeps the SDK free of Element Call: nothing under `sdk/` may import a file
* outside `sdk/` by relative path (that is `src/`, `component/`,
* `playwright/`) or one of the packages above. The development harness under
* `sdk/dev/` is a host, not part of the library, and is exempt. Keeps the
* session free of any one media backend, too: outside `sdk/src/media-backend/`,
* a backend's packages may only be imported for their types, and only in
* the public API, the errors and the test helpers that have to name them.
*/
const rule = ESLintUtils.RuleCreator(
() => "https://github.com/element-hq/element-call",
)({
name: "sdk-import-boundary",
meta: {
type: "problem",
docs: {
description:
"Disallow the SDK importing Element Call (anything outside sdk/) or host-supplied packages.",
},
messages: {
outsideSdk:
"The SDK must not import '{{specifier}}': it resolves outside sdk/. Copy or move what it needs into sdk/src.",
bannedPackage:
"The SDK must not import '{{specifier}}': a host supplies that, the SDK has no UI.",
backendPackage:
"Only a media backend under sdk/src/media-backend/ may import '{{specifier}}'; the session sees media through media-backend/api.ts.",
},
schema: [],
},
defaultOptions: [],
create(context) {
const filename = context.filename;
const sdkRoot = resolve(context.cwd, "sdk") + sep;
if (!filename.startsWith(sdkRoot)) return {};
if (filename.startsWith(resolve(sdkRoot, "dev") + sep)) return {};
const inBackend = filename.startsWith(
resolve(sdkRoot, "src", "media-backend") + sep,
);
const matchesPackage = (specifier, pkg) =>
specifier === pkg ||
specifier.startsWith(pkg.endsWith("/") ? pkg : `${pkg}/`);
const check = (node, specifier, typeOnly = false) => {
if (typeof specifier !== "string") return;
if (
!inBackend &&
!typeOnly &&
BACKEND_PACKAGES.some((pkg) => matchesPackage(specifier, pkg))
) {
context.report({
node,
messageId: "backendPackage",
data: { specifier },
});
return;
}
if (specifier.startsWith(".")) {
const target = resolve(dirname(filename), specifier);
if (!target.startsWith(sdkRoot))
context.report({
node,
messageId: "outsideSdk",
data: { specifier },
});
} else if (
BANNED_PACKAGES.some((pkg) => matchesPackage(specifier, pkg))
) {
context.report({
node,
messageId: "bannedPackage",
data: { specifier },
});
}
};
return {
ImportDeclaration(node) {
check(
node,
node.source.value,
node.importKind === "type" ||
node.specifiers.every((s) => s.importKind === "type"),
);
},
ExportNamedDeclaration(node) {
if (node.source) check(node, node.source.value);
},
ExportAllDeclaration(node) {
check(node, node.source.value);
},
ImportExpression(node) {
if (node.source.type === "Literal") check(node, node.source.value);
},
};
},
});
export default rule;