From 31dd8fb92844c444a0e79bda2be82958f09314d7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 5 Jun 2026 14:28:05 +0100 Subject: [PATCH] Prepare for move to element-web monorepo --- modules/.dockerignore | 1 + modules/.eslintrc.cjs | 79 +++++++++ modules/.gitignore | 158 ++++++++++++++++++ modules/.lintstagedrc | 3 + modules/.prettierignore | 1 + modules/.prettierrc.mjs | 3 + modules/Dockerfile | 24 +++ modules/docker-bake.hcl | 55 ++++++ modules/knip.ts | 30 ++++ modules/package.json | 61 +++++++ ...+vite-plugin-import-css-sheet+1.0.12.patch | 10 ++ ...+element-web-playwright-common+4.0.0.patch | 22 +++ modules/playwright.config.ts | 105 ++++++++++++ modules/playwright/element-web-test.ts | 35 ++++ modules/playwright/tsconfig.json | 13 ++ modules/tsconfig.json | 14 ++ 16 files changed, 614 insertions(+) create mode 100644 modules/.dockerignore create mode 100644 modules/.eslintrc.cjs create mode 100644 modules/.gitignore create mode 100644 modules/.lintstagedrc create mode 100644 modules/.prettierignore create mode 100644 modules/.prettierrc.mjs create mode 100644 modules/Dockerfile create mode 100644 modules/docker-bake.hcl create mode 100644 modules/knip.ts create mode 100644 modules/package.json create mode 100644 modules/patches/@arcmantle+vite-plugin-import-css-sheet+1.0.12.patch create mode 100644 modules/patches/@element-hq+element-web-playwright-common+4.0.0.patch create mode 100644 modules/playwright.config.ts create mode 100644 modules/playwright/element-web-test.ts create mode 100644 modules/playwright/tsconfig.json create mode 100644 modules/tsconfig.json diff --git a/modules/.dockerignore b/modules/.dockerignore new file mode 100644 index 0000000000..f83526d133 --- /dev/null +++ b/modules/.dockerignore @@ -0,0 +1 @@ +**/node_modules/ \ No newline at end of file diff --git a/modules/.eslintrc.cjs b/modules/.eslintrc.cjs new file mode 100644 index 0000000000..67a4bd314d --- /dev/null +++ b/modules/.eslintrc.cjs @@ -0,0 +1,79 @@ +module.exports = { + plugins: ["matrix-org", "eslint-plugin-react-compiler"], + extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], + parserOptions: { + project: ["./tsconfig.json"], + }, + env: { + browser: true, + node: true, + }, + rules: { + "react/jsx-key": ["error"], + + "no-restricted-properties": [ + "error", + ...buildRestrictedPropertiesOptions( + ["window.innerHeight", "window.innerWidth", "window.visualViewport"], + "Use UIStore to access window dimensions instead.", + ), + ...buildRestrictedPropertiesOptions( + ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], + "Use Media helper instead to centralise access for customisation.", + ), + ...buildRestrictedPropertiesOptions(["window.setImmediate"], "Use setTimeout instead."), + ], + "no-restricted-globals": [ + "error", + { + name: "setImmediate", + message: "Use setTimeout instead.", + }, + { + name: "Buffer", + message: "Buffer is not available in the web.", + }, + ], + + "import/no-duplicates": ["error"], + "matrix-org/require-copyright-header": "error", + + "react-compiler/react-compiler": "error", + }, + overrides: [ + { + files: [ + "packages/element-web-playwright-common/**/*.ts", + "playwright/**/*.ts", + "modules/*/element-web/e2e/**/*.{ts,tsx}", + ], + rules: { + // This is necessary for Playwright fixtures + "no-empty-pattern": "off", + // This is necessary for Playwright fixtures + "react-hooks/rules-of-hooks": "off", + // This just gets annoying in test code + "@typescript-eslint/explicit-function-return-type": "off", + }, + }, + ], + settings: { + react: { + version: "detect", + }, + }, +}; + +function buildRestrictedPropertiesOptions(properties, message) { + return properties.map((prop) => { + let [object, property] = prop.split("."); + if (object === "*") { + object = undefined; + } + return { + object, + property, + message, + }; + }); +} diff --git a/modules/.gitignore b/modules/.gitignore new file mode 100644 index 0000000000..bcaacaaac3 --- /dev/null +++ b/modules/.gitignore @@ -0,0 +1,158 @@ +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Python +.venv +__pycache__/ +.tox/ +.coverage* +coverage.* +*.egg-info/ +_trial_temp/ +.mypy_cache/ + +# Custom rules +# Ignore all built libraries +lib/ +temp/ +coverage/ + +/playwright-html-report/ +**/test-results +**/_results +# Only commit snapshots from Linux +**/e2e/snapshots/**/*.png +**/e2e/snapshots/**/*.yml +!**/e2e/snapshots/**/*-linux.png +!**/e2e/snapshots/**/*-linux.aria.yml +/playwright-report/ +.DS_Store diff --git a/modules/.lintstagedrc b/modules/.lintstagedrc new file mode 100644 index 0000000000..16e652a838 --- /dev/null +++ b/modules/.lintstagedrc @@ -0,0 +1,3 @@ +{ + "*": "prettier -u --write" +} diff --git a/modules/.prettierignore b/modules/.prettierignore new file mode 100644 index 0000000000..4ce36e1e79 --- /dev/null +++ b/modules/.prettierignore @@ -0,0 +1 @@ +*.api.md \ No newline at end of file diff --git a/modules/.prettierrc.mjs b/modules/.prettierrc.mjs new file mode 100644 index 0000000000..eac1f269d1 --- /dev/null +++ b/modules/.prettierrc.mjs @@ -0,0 +1,3 @@ +import config from "eslint-plugin-matrix-org/.prettierrc.js"; + +export default config; diff --git a/modules/Dockerfile b/modules/Dockerfile new file mode 100644 index 0000000000..16ba8480e1 --- /dev/null +++ b/modules/Dockerfile @@ -0,0 +1,24 @@ +ARG ELEMENT_VERSION=latest@sha256:a84f294ce46e4327ebacecb78bfc94cf6a45c7ffa5104a28f06b5ac69d0b2548 + +FROM --platform=$BUILDPLATFORM node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 AS builder + +ARG BUILD_CONTEXT + +RUN apk add --no-cache jq + +WORKDIR /app +COPY package.json yarn.lock ./ +# Copy the package.json files of all modules & packages to ensure the frozen workspace lockfile holds up +RUN --mount=type=bind,target=/docker-context \ + cd /docker-context/; \ + find . -path ./node_modules -prune -o -name "package.json" -mindepth 0 -maxdepth 4 -exec cp --parents "{}" /app/ \; +RUN yarn install --frozen-lockfile --ignore-scripts +COPY tsconfig.json ./ +COPY ./$BUILD_CONTEXT ./$BUILD_CONTEXT +RUN cd $BUILD_CONTEXT && yarn vite build +RUN mkdir /modules +RUN cp -r ./$BUILD_CONTEXT/lib/ /modules/$(jq -r '"\(.name)-v\(.version)"' ./$BUILD_CONTEXT/package.json) + +FROM ghcr.io/element-hq/element-web:${ELEMENT_VERSION} + +COPY --from=builder /modules /modules/ \ No newline at end of file diff --git a/modules/docker-bake.hcl b/modules/docker-bake.hcl new file mode 100644 index 0000000000..d8af5480cf --- /dev/null +++ b/modules/docker-bake.hcl @@ -0,0 +1,55 @@ +// https://github.com/docker/metadata-action#bake-definition +target "docker-metadata-action" {} + +variable "ELEMENT_VERSION" { + default = "latest" +} + +variable "DATE" { + validation { + condition = DATE != "" + error_message = "DATE must be set" + } + + validation { + condition = DATE == regex("\\d{6}", DATE) + error_message = "The variable 'DATE' must be of format YYMMNN where NN is the build number of the month." + } +} + +variable "BUILD_TAG" { + default = "${ELEMENT_VERSION}-${DATE}" +} + +group "default" { + targets = [ + "element-web-modules-opendesk-plugin" + ] +} + +target "_common" { + inherits = ["docker-metadata-action"] + platforms = [ + "linux/amd64", + "linux/arm64", + ] + context = "." +} + +target "_element_web_module_base" { + inherits = ["_common"] + dockerfile = "./Dockerfile" +} + +target "element-web-modules-opendesk-plugin" { + inherits = ["_element_web_module_base"] + tags = [ + "ghcr.io/element-hq/element-web-modules/opendesk-plugin:latest", + "ghcr.io/element-hq/element-web-modules/opendesk-plugin:${BUILD_TAG}", + "registry.opencode.de/bmi/opendesk/components/supplier/element/images/opendesk-element-web:${BUILD_TAG}" + ] + args = { + ELEMENT_VERSION = "${ELEMENT_VERSION}" + BUILD_CONTEXT = "modules/opendesk/element-web" + } +} \ No newline at end of file diff --git a/modules/knip.ts b/modules/knip.ts new file mode 100644 index 0000000000..2c332cd741 --- /dev/null +++ b/modules/knip.ts @@ -0,0 +1,30 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { KnipConfig } from "knip"; + +// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter` +process.env.GITHUB_ACTIONS = "1"; + +export default { + ignoreDependencies: [ + // Needed for lint:workflows + "@action-validator/cli", + "@action-validator/core", + // Unlisted peer dependency for @element-hq/element-web-module-api + "matrix-widget-api", + ], + ignoreBinaries: [ + // False positive for `yarn list` + "list", + ], + workspaces: { + "modules/*/element-web": { + entry: "src/index.ts{x,}", + }, + }, +} satisfies KnipConfig; diff --git a/modules/package.json b/modules/package.json new file mode 100644 index 0000000000..7353a6543d --- /dev/null +++ b/modules/package.json @@ -0,0 +1,61 @@ +{ + "private": true, + "type": "module", + "workspaces": [ + "packages/*", + "modules/*/element-web", + "modules/*/synapse" + ], + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", + "lint:prettier": "prettier --check .", + "lint:prettier:fix": "prettier --write .", + "lint:eslint": "eslint --max-warnings 0 modules/*/element-web/src", + "lint:types": "yarn workspaces run lint:types", + "lint:codestyle": "yarn workspaces run lint:codestyle", + "lint:knip": "knip", + "test": "yarn workspaces run test", + "test:playwright": "playwright test", + "test:playwright:open": "yarn test:playwright --ui", + "test:playwright:screenshots": "playwright-screenshots yarn test:playwright --update-snapshots --grep @screenshot", + "prepare": "patch-package && husky && yarn workspaces run prepare" + }, + "devDependencies": { + "@action-validator/cli": "^0.6.0", + "@action-validator/core": "^0.6.0", + "@element-hq/element-web-module-api": "1.14.0", + "@element-hq/element-web-playwright-common": "^6.0.0", + "@playwright/test": "^1.52.0", + "@stylistic/eslint-plugin": "^5.0.0", + "@types/node": "^22.12.0", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "@typescript-eslint/parser": "^8.22.0", + "eslint": "^8.26.0", + "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^10.0.0", + "eslint-plugin-deprecate": "^0.9.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-matrix-org": "^3.0.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-compiler": "^19.0.0-beta-27714ef-20250124", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-unicorn": "^56.0.0", + "husky": "^9.1.7", + "knip": "^6.0.0", + "lint-staged": "^17.0.0", + "patch-package": "^8.0.1", + "prettier": "^3.4.2", + "typescript": "^6.0.0" + }, + "resolutions": { + "vite": "8.0.15", + "@vitest/browser-playwright": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "vitest": "4.1.8" + }, + "packageManager": "yarn@1.22.22" +} diff --git a/modules/patches/@arcmantle+vite-plugin-import-css-sheet+1.0.12.patch b/modules/patches/@arcmantle+vite-plugin-import-css-sheet+1.0.12.patch new file mode 100644 index 0000000000..6606c16079 --- /dev/null +++ b/modules/patches/@arcmantle+vite-plugin-import-css-sheet+1.0.12.patch @@ -0,0 +1,10 @@ +diff --git a/node_modules/@arcmantle/vite-plugin-import-css-sheet/client.d.ts b/node_modules/@arcmantle/vite-plugin-import-css-sheet/client.d.ts +index d884a7e..533850e 100644 +--- a/node_modules/@arcmantle/vite-plugin-import-css-sheet/client.d.ts ++++ b/node_modules/@arcmantle/vite-plugin-import-css-sheet/client.d.ts +@@ -1,3 +1,4 @@ + declare module '*.css' { +- export default styles as CSSStyleSheet; ++ const styles: CSSStyleSheet; ++ export default styles; + } diff --git a/modules/patches/@element-hq+element-web-playwright-common+4.0.0.patch b/modules/patches/@element-hq+element-web-playwright-common+4.0.0.patch new file mode 100644 index 0000000000..048ffb753e --- /dev/null +++ b/modules/patches/@element-hq+element-web-playwright-common+4.0.0.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@element-hq/element-web-playwright-common/playwright-screenshots.sh b/node_modules/@element-hq/element-web-playwright-common/playwright-screenshots.sh +index d860e34..d252f3a 100755 +--- a/node_modules/@element-hq/element-web-playwright-common/playwright-screenshots.sh ++++ b/node_modules/@element-hq/element-web-playwright-common/playwright-screenshots.sh +@@ -16,7 +16,7 @@ function build_image() { + WS_PORT=3000 + + # Check the playwright version +-PW_VERSION=$(pnpm --silent -- playwright --version | awk '{print $2}') ++PW_VERSION=$(yarn --silent run playwright --version | awk '{print $2}') + IMAGE_NAME="ghcr.io/element-hq/element-web/playwright-server:$PW_VERSION" + + # If the image exists in the repository, pull it; otherwise, build it. +@@ -42,7 +42,7 @@ trap clean_up EXIT + + # Wait for playwright-server to be ready + echo "playwright-screenshots: Waiting for playwright-server" +-pnpm --dir "$SCRIPT_DIR" exec wait-on "tcp:$WS_PORT" ++npx -y wait-on "tcp:$WS_PORT" + + # Playwright seems to overwrite the last line from the console, so add an + # extra newline to make sure this doesn't get lost. diff --git a/modules/playwright.config.ts b/modules/playwright.config.ts new file mode 100644 index 0000000000..8dca7c75d8 --- /dev/null +++ b/modules/playwright.config.ts @@ -0,0 +1,105 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { defineConfig, devices, type Project } from "@playwright/test"; +import fs, { globSync } from "node:fs"; +import path from "node:path"; + +import type { Options } from "./playwright/element-web-test"; + +const chromeProject = { + ...devices["Desktop Chrome"], + channel: "chromium", + permissions: ["clipboard-write", "clipboard-read", "microphone"], + launchOptions: { + args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"], + }, + connectOptions: process.env.PW_TEST_CONNECT_WS_ENDPOINT + ? { + wsEndpoint: process.env.PW_TEST_CONNECT_WS_ENDPOINT, + exposeNetwork: "", + } + : undefined, +}; + +/** +We assume that all modules will have the following directory structure: + +└── modules/ + └── my-module/ + └── element-web/ + ├── e2e/ + │ ├── test-1.spec.ts + │ └── test-2.spec.ts + └── package.json + +The following code maps each such module (my-module in the example above) to a separate +playwright project. + */ +const projects: Project[] = []; + +// Get all the directories that hold playwright tests +const moduleTestDirectories = globSync("modules/*/element-web/e2e", {}); + +// Process each directory +for (const testDirectory of moduleTestDirectories) { + // Based on the directory structure, the parent directory of the test directory holds package.json. + const moduleDirectory = path.join(testDirectory, ".."); + + // Get module name from package.json + const packageJson = JSON.parse(fs.readFileSync(path.join(moduleDirectory, "package.json"), "utf-8")); + const MODULE_PREFIX = "@element-hq/element-web-module-"; + const name = packageJson.name.startsWith(MODULE_PREFIX) + ? packageJson.name.slice(MODULE_PREFIX.length) + : packageJson.name; + + // Create playwright project + projects.push({ + name, + use: { + ...chromeProject, + moduleDir: moduleDirectory, + }, + testDir: testDirectory, + snapshotDir: `${testDirectory}/snapshots`, + outputDir: `${testDirectory}/_results`, + }); +} + +const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; + +export default defineConfig({ + projects, + use: { + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + video: "retain-on-failure", + baseURL, + trace: "on-first-retry", + }, + webServer: { + command: process.env.WEBAPP_PATH + ? `npx serve -p 8080 -L ${process.env.WEBAPP_PATH}` + : "docker run --rm -p 8080:80 ghcr.io/element-hq/element-web:develop", + url: `${baseURL}/config.json`, + reuseExistingServer: true, + timeout: (process.env.CI ? 30 : 120) * 1000, + }, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI + ? [ + ["list"], + ["html"], + ["github"], + ["@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js"], + ] + : [["list"], ["html", { outputFolder: "playwright-html-report" }]], + // When running the browser in docker, set the platform to `linux` as that is the platform where the browser is running + snapshotPathTemplate: `{snapshotDir}/{testFilePath}/{arg}-${process.env.PW_TEST_CONNECT_WS_ENDPOINT ? "linux" : "{platform}"}{ext}`, + forbidOnly: !!process.env.CI, +}); diff --git a/modules/playwright/element-web-test.ts b/modules/playwright/element-web-test.ts new file mode 100644 index 0000000000..ca8ca83f7e --- /dev/null +++ b/modules/playwright/element-web-test.ts @@ -0,0 +1,35 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test as base, expect, type TestFixtures } from "@element-hq/element-web-playwright-common"; + +export interface Options { + moduleDir: string; + modules: string[]; +} + +export const test = base.extend({ + moduleDir: ["", { option: true }], + modules: async ({ moduleDir }, use) => { + await use([`${moduleDir}/lib/index.js`]); + }, + + page: async ({ page, modules, config }, use) => { + config.modules = []; + for (let i = 0; i < modules.length; i++) { + const module = `/modules/module-${i}/index.js`; + await page.route(module, async (route) => { + await route.fulfill({ path: modules[i] }); + }); + config.modules.push(module); + } + + await use(page); + }, +}); + +export { expect }; diff --git a/modules/playwright/tsconfig.json b/modules/playwright/tsconfig.json new file mode 100644 index 0000000000..e8183fcccf --- /dev/null +++ b/modules/playwright/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2022", + "jsx": "react", + "lib": ["ESNext", "es2022", "dom", "dom.iterable"], + "resolveJsonModule": true, + "esModuleInterop": true, + "moduleResolution": "node", + "module": "es2022", + "allowImportingTsExtensions": true + }, + "include": ["**/*.ts", "../modules/*/element-web/test/**/*.ts"] +} diff --git a/modules/tsconfig.json b/modules/tsconfig.json new file mode 100644 index 0000000000..00a3ce25e3 --- /dev/null +++ b/modules/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "lib": ["dom", "es2022", "esnext"], + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "declaration": true + } +}