From 26c5141c69204ca5900404a6a9d316b6ed81efe2 Mon Sep 17 00:00:00 2001 From: Michael Beckemeyer Date: Sat, 29 Jun 2024 15:44:42 +0200 Subject: [PATCH] Start working on developer tooling --- .changeset/ninety-glasses-fix.md | 6 + .changeset/wicked-cats-vanish.md | 27 ++ package.json | 9 +- patches/@chakra-ui__hooks@2.2.1.patch | 13 + patches/@chakra-ui__menu@2.2.1.patch | 13 + ...ra-ui__react-use-outside-click@2.2.0.patch | 13 + pnpm-lock.yaml | 59 +++- src/packages/chakra-integration/Provider.tsx | 60 ++-- src/packages/runtime/CustomElement.ts | 4 +- .../runtime/builtin-services/index.ts | 12 +- src/packages/runtime/dev-tools/DevTools.tsx | 49 ++++ .../runtime/dev-tools/DevToolsBadge.tsx | 35 +++ .../runtime/dev-tools/DevToolsContent.tsx | 275 ++++++++++++++++++ src/packages/runtime/dev-tools/index.tsx | 3 + src/packages/runtime/package.json | 6 +- .../ReactIntegration.test.ts | 2 +- .../react-integration/ReactIntegration.tsx | 33 ++- .../runtime/service-layer/PackageRepr.ts | 30 +- .../runtime/service-layer/ServiceLayer.ts | 10 +- .../runtime/service-layer/ServiceRepr.ts | 48 +-- src/packages/test-utils/react.tsx | 2 +- vite.config.ts | 2 +- 22 files changed, 619 insertions(+), 92 deletions(-) create mode 100644 .changeset/ninety-glasses-fix.md create mode 100644 .changeset/wicked-cats-vanish.md create mode 100644 patches/@chakra-ui__hooks@2.2.1.patch create mode 100644 patches/@chakra-ui__menu@2.2.1.patch create mode 100644 patches/@chakra-ui__react-use-outside-click@2.2.0.patch create mode 100644 src/packages/runtime/dev-tools/DevTools.tsx create mode 100644 src/packages/runtime/dev-tools/DevToolsBadge.tsx create mode 100644 src/packages/runtime/dev-tools/DevToolsContent.tsx create mode 100644 src/packages/runtime/dev-tools/index.tsx diff --git a/.changeset/ninety-glasses-fix.md b/.changeset/ninety-glasses-fix.md new file mode 100644 index 00000000..b943b879 --- /dev/null +++ b/.changeset/ninety-glasses-fix.md @@ -0,0 +1,6 @@ +--- +"@open-pioneer/chakra-integration": major +--- + +Rename `container` prop to `rootNode`. This property refers to the application's shadow root and was misnamed. +Introduce `container` prop that refers to the application's container element (the root html element inside the shadow root). diff --git a/.changeset/wicked-cats-vanish.md b/.changeset/wicked-cats-vanish.md new file mode 100644 index 00000000..9e22d675 --- /dev/null +++ b/.changeset/wicked-cats-vanish.md @@ -0,0 +1,27 @@ +--- +"@open-pioneer/runtime": minor +--- + +Merge the dom nodes `.pioneer-root` and `.chakra-host`. + +Previously, the DOM hierarchy for a trails application looked like this: + +```text + +└── shadow root + └──
+ └──
+``` + +It now looks like this: + +```text + +└── shadow root + └──
+``` + +Since all UI elements where already children of the `.chakra-host` element, this should not affect most applications. + +The presence of two node made it possible to accidentally create a node where Chakra's style rules didn't apply. +This change prevents that error. diff --git a/package.json b/package.json index fb20900e..232ec08a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "semver: https://github.com/advisories/GHSA-c2qf-rxjj-qqgw", "tough-cookie: https://github.com/advisories/GHSA-72xf-g2v4-qvf3", "braces: https://github.com/advisories/GHSA-grv7-fg5c-xmjg", + "react-remove-scroll: new version contains fix for shadow doms", "", "ignored cves", "============", @@ -45,13 +46,17 @@ "ol-mapbox-style": "workspace:disabled-package@*", "semver@<7.5.2": ">=7.5.2", "tough-cookie@<4.1.3": ">=4.1.3", - "braces@<3.0.3": ">=3.0.3" + "braces@<3.0.3": ">=3.0.3", + "react-remove-scroll": ">=2.5.10" }, "auditConfig": { "ignoreCves": [] }, "patchedDependencies": { - "@changesets/assemble-release-plan@6.0.0": "patches/@changesets__assemble-release-plan@6.0.0.patch" + "@changesets/assemble-release-plan@6.0.0": "patches/@changesets__assemble-release-plan@6.0.0.patch", + "@chakra-ui/menu@2.2.1": "patches/@chakra-ui__menu@2.2.1.patch", + "@chakra-ui/react-use-outside-click@2.2.0": "patches/@chakra-ui__react-use-outside-click@2.2.0.patch", + "@chakra-ui/hooks@2.2.1": "patches/@chakra-ui__hooks@2.2.1.patch" }, "peerDependencyRules": { "allowedVersions": { diff --git a/patches/@chakra-ui__hooks@2.2.1.patch b/patches/@chakra-ui__hooks@2.2.1.patch new file mode 100644 index 00000000..395a1c79 --- /dev/null +++ b/patches/@chakra-ui__hooks@2.2.1.patch @@ -0,0 +1,13 @@ +diff --git a/dist/chunk-R5W6LHQW.mjs b/dist/chunk-R5W6LHQW.mjs +index 75c349637d59f487c75f477f0cea9ad7b5e45fff..8a41cf24fc40770e30553fe572cb722edda6b4c8 100644 +--- a/dist/chunk-R5W6LHQW.mjs ++++ b/dist/chunk-R5W6LHQW.mjs +@@ -60,7 +60,7 @@ function isValidEvent(event, ref) { + if (!doc.contains(target)) + return false; + } +- return !((_a = ref.current) == null ? void 0 : _a.contains(target)); ++ return !((_a = ref.current) == null ? void 0 : event.composedPath().includes(_a)); + } + + export { diff --git a/patches/@chakra-ui__menu@2.2.1.patch b/patches/@chakra-ui__menu@2.2.1.patch new file mode 100644 index 00000000..b21e6b78 --- /dev/null +++ b/patches/@chakra-ui__menu@2.2.1.patch @@ -0,0 +1,13 @@ +diff --git a/dist/chunk-SANI5SUM.mjs b/dist/chunk-SANI5SUM.mjs +index 4cd8ab09dbb6a5c45fbf5d5630a32c8f6147805e..211fc6ae0907e9befa23cca9c9d6f3844ca69853 100644 +--- a/dist/chunk-SANI5SUM.mjs ++++ b/dist/chunk-SANI5SUM.mjs +@@ -124,7 +124,7 @@ function useMenu(props = {}) { + ref: menuRef, + handler: (event) => { + var _a; +- if (!((_a = buttonRef.current) == null ? void 0 : _a.contains(event.target))) { ++ if (!((_a = buttonRef.current) == null ? void 0 : event.composedPath().includes(_a))) { + onClose(); + } + } diff --git a/patches/@chakra-ui__react-use-outside-click@2.2.0.patch b/patches/@chakra-ui__react-use-outside-click@2.2.0.patch new file mode 100644 index 00000000..ad038caa --- /dev/null +++ b/patches/@chakra-ui__react-use-outside-click@2.2.0.patch @@ -0,0 +1,13 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index 2d6e217a22b70ae9e87d0e87dd57c99a1fea48fa..831d92871f165986747f69a7738447501ca7113c 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -57,7 +57,7 @@ function isValidEvent(event, ref) { + if (!doc.contains(target)) + return false; + } +- return !((_a = ref.current) == null ? void 0 : _a.contains(target)); ++ return !((_a = ref.current) == null ? void 0 : event.composedPath().includes(_a)); + } + function getOwnerDocument(node) { + var _a; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4df83f46..4c7575f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,18 @@ overrides: semver@<7.5.2: '>=7.5.2' tough-cookie@<4.1.3: '>=4.1.3' braces@<3.0.3: '>=3.0.3' + react-remove-scroll: '>=2.5.10' patchedDependencies: + '@chakra-ui/hooks@2.2.1': + hash: zntq7izvicj3ptw5qmsyjygbmu + path: patches/@chakra-ui__hooks@2.2.1.patch + '@chakra-ui/menu@2.2.1': + hash: x7y3u4pvzv3wpkejsnxo3rp5vq + path: patches/@chakra-ui__menu@2.2.1.patch + '@chakra-ui/react-use-outside-click@2.2.0': + hash: uw4qsrve2rd7lemccfb44ornky + path: patches/@chakra-ui__react-use-outside-click@2.2.0.patch '@changesets/assemble-release-plan@6.0.0': hash: h32n3ge4nf46sxbiqfnayewo7u path: patches/@changesets__assemble-release-plan@6.0.0.patch @@ -366,6 +376,9 @@ importers: src/packages/runtime: dependencies: + '@conterra/reactivity-core': + specifier: ^0.4.0 + version: 0.4.0 '@formatjs/intl': specifier: ^2.9.9 version: 2.9.9(typescript@5.3.2) @@ -378,6 +391,9 @@ importers: '@open-pioneer/core': specifier: workspace:^ version: link:../core + '@open-pioneer/reactivity': + specifier: workspace:^ + version: link:../reactivity '@open-pioneer/runtime-react-support': specifier: workspace:^ version: link:../runtime-react-support @@ -394,6 +410,9 @@ importers: core-packages: specifier: workspace:^ version: link:../../.. + ts-essentials: + specifier: ^10.0.1 + version: 10.0.1(typescript@5.3.2) publishDirectory: dist src/packages/runtime-react-support: @@ -3658,8 +3677,8 @@ packages: react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - react-remove-scroll-bar@2.3.4: - resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -3668,8 +3687,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.5.6: - resolution: {integrity: sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==} + react-remove-scroll@2.5.10: + resolution: {integrity: sha512-m3zvBRANPBw3qxVVjEIPEQinkcwlFZ4qyomuWVpNJdv4c6MvHfXV0C3L9Jx5rr3HeBHKNRX+1jreB5QloDIJjA==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4112,6 +4131,14 @@ packages: ts-easing@0.2.0: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + ts-essentials@10.0.1: + resolution: {integrity: sha512-HPH+H2bkkO8FkMDau+hFvv7KYozzned9Zr1Urn7rRPXMF4mZmCKOq+u4AI1AAW+2bofIOXTuSdKo9drQuni2dQ==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + ts-node@10.9.1: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -4684,7 +4711,7 @@ snapshots: '@chakra-ui/system': 2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/hooks@2.2.1(react@18.3.1)': + '@chakra-ui/hooks@2.2.1(patch_hash=zntq7izvicj3ptw5qmsyjygbmu)(react@18.3.1)': dependencies: '@chakra-ui/react-utils': 2.0.12(react@18.3.1) '@chakra-ui/utils': 2.0.15 @@ -4746,7 +4773,7 @@ snapshots: '@chakra-ui/system': 2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(framer-motion@10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@chakra-ui/menu@2.2.1(patch_hash=x7y3u4pvzv3wpkejsnxo3rp5vq)(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(framer-motion@10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@chakra-ui/clickable': 2.1.0(react@18.3.1) '@chakra-ui/descendant': 3.1.0(react@18.3.1) @@ -4759,7 +4786,7 @@ snapshots: '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) + '@chakra-ui/react-use-outside-click': 2.2.0(patch_hash=uw4qsrve2rd7lemccfb44ornky)(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/system': 2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1) @@ -4782,7 +4809,7 @@ snapshots: framer-motion: 10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) - react-remove-scroll: 2.5.6(@types/react@18.2.39)(react@18.3.1) + react-remove-scroll: 2.5.10(@types/react@18.2.39)(react@18.3.1) transitivePeerDependencies: - '@types/react' @@ -4946,7 +4973,7 @@ snapshots: dependencies: react: 18.3.1 - '@chakra-ui/react-use-outside-click@2.2.0(react@18.3.1)': + '@chakra-ui/react-use-outside-click@2.2.0(patch_hash=uw4qsrve2rd7lemccfb44ornky)(react@18.3.1)': dependencies: '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) react: 18.3.1 @@ -5001,14 +5028,14 @@ snapshots: '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.39)(react@18.3.1) '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@chakra-ui/hooks': 2.2.1(react@18.3.1) + '@chakra-ui/hooks': 2.2.1(patch_hash=zntq7izvicj3ptw5qmsyjygbmu)(react@18.3.1) '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/live-region': 2.1.0(react@18.3.1) '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(framer-motion@10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@chakra-ui/menu': 2.2.1(patch_hash=x7y3u4pvzv3wpkejsnxo3rp5vq)(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(framer-motion@10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(@types/react@18.2.39)(framer-motion@10.16.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1) '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.39)(react@18.3.1))(@types/react@18.2.39)(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -7998,7 +8025,7 @@ snapshots: react-lifecycles-compat@3.0.4: {} - react-remove-scroll-bar@2.3.4(@types/react@18.2.39)(react@18.3.1): + react-remove-scroll-bar@2.3.6(@types/react@18.2.39)(react@18.3.1): dependencies: react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.2.39)(react@18.3.1) @@ -8006,10 +8033,10 @@ snapshots: optionalDependencies: '@types/react': 18.2.39 - react-remove-scroll@2.5.6(@types/react@18.2.39)(react@18.3.1): + react-remove-scroll@2.5.10(@types/react@18.2.39)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.4(@types/react@18.2.39)(react@18.3.1) + react-remove-scroll-bar: 2.3.6(@types/react@18.2.39)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.2.39)(react@18.3.1) tslib: 2.6.0 use-callback-ref: 1.3.0(@types/react@18.2.39)(react@18.3.1) @@ -8489,6 +8516,10 @@ snapshots: ts-easing@0.2.0: {} + ts-essentials@10.0.1(typescript@5.3.2): + optionalDependencies: + typescript: 5.3.2 + ts-node@10.9.1(@swc/core@1.4.2)(@types/node@18.19.0)(typescript@5.3.2): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/src/packages/chakra-integration/Provider.tsx b/src/packages/chakra-integration/Provider.tsx index 5e6f7444..de36745c 100644 --- a/src/packages/chakra-integration/Provider.tsx +++ b/src/packages/chakra-integration/Provider.tsx @@ -20,12 +20,20 @@ import { PortalRootProvider } from "./PortalFix"; export type CustomChakraProviderProps = PropsWithChildren<{ /** - * Container node where styles will be mounted. + * Node where styles will be mounted. + * This is typically the shadow root, but it may be any Node. + * * Note that updates of this property are not supported. + */ + rootNode: Node; + + /** + * Container element where the application will be mounted. + * This is used to render portal contents. * - * This is typically the shadow root. + * Note that updates of this property are not supported. */ - container: Node; + container: HTMLElement; /** * Configures the color mode of the application. @@ -62,6 +70,7 @@ const colorModeClassnames = { // https://github.com/chakra-ui/chakra-ui/issues/2439 export const CustomChakraProvider: FC = ({ + rootNode, container, colorMode, children, @@ -102,12 +111,15 @@ export const CustomChakraProvider: FC = ({ 4. Set color mode on the root container instead of html and body. */ + useEffect(() => { + container.classList.add("chakra-host"); + return () => container.classList.remove("chakra-host"); + }, [container]); - const cache = useEmotionCache(container); - + const cache = useEmotionCache(rootNode); const theme = useMemo(() => wrapTheme(themeProp), [themeProp]); - const chakraHost = useRef(null); + const chakraHost = useRef(container); const toastOptions: ToastProviderProps = { portalProps: { containerRef: chakraHost @@ -117,25 +129,21 @@ export const CustomChakraProvider: FC = ({ const ColorMode = useSyncedColorMode(chakraHost, colorMode); return ( -
- - - - - - - - - - {children} - - - - - - - -
+ + + + + + + + + {children} + + + + + + ); }; @@ -145,7 +153,7 @@ export const CustomChakraProvider: FC = ({ * The current color mode is automatically propagates as a css class on the chakra host element. */ function useSyncedColorMode( - chakraHost: RefObject, + chakraHost: RefObject, colorMode: "light" | "dark" | undefined ) { const mode = colorMode ?? "light"; diff --git a/src/packages/runtime/CustomElement.ts b/src/packages/runtime/CustomElement.ts index 47aa9767..20036062 100644 --- a/src/packages/runtime/CustomElement.ts +++ b/src/packages/runtime/CustomElement.ts @@ -364,8 +364,8 @@ class ApplicationInstance { // Launch react this.reactIntegration = new ReactIntegration({ - rootNode: container, - container: shadowRoot, + container: container, + shadowRoot: shadowRoot, theme: elementOptions.theme, serviceLayer, packages diff --git a/src/packages/runtime/builtin-services/index.ts b/src/packages/runtime/builtin-services/index.ts index 24101dc0..58e7c1e6 100644 --- a/src/packages/runtime/builtin-services/index.ts +++ b/src/packages/runtime/builtin-services/index.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 import { createEmptyI18n } from "../i18n"; +import { ReferenceSpec } from "../service-layer/InterfaceSpec"; import { PackageRepr } from "../service-layer/PackageRepr"; import { createConstructorFactory, @@ -86,9 +87,18 @@ export function createBuiltinPackage(properties: BuiltinPackageProperties): Pack ] }); + const uiReferences: ReferenceSpec[] = []; + if (import.meta.env.DEV) { + // Needed by dev tools + uiReferences.push({ + interfaceName: RUNTIME_APPLICATION_CONTEXT + }); + } + return new PackageRepr({ name: RUNTIME_PACKAGE_NAME, services: [apiService, appContext, lifecycleEventService], - intl: i18n + intl: i18n, + uiReferences: uiReferences }); } diff --git a/src/packages/runtime/dev-tools/DevTools.tsx b/src/packages/runtime/dev-tools/DevTools.tsx new file mode 100644 index 00000000..6cbd5690 --- /dev/null +++ b/src/packages/runtime/dev-tools/DevTools.tsx @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +import { ReactNode, useEffect, useState } from "react"; +import { DevToolsBadge } from "./DevToolsBadge"; +import { createLogger } from "@open-pioneer/core"; +import { ServiceLayer } from "../service-layer/ServiceLayer"; +const LOG = createLogger("runtime:dev-tools"); + +export interface DevToolsProps { + /** + * Reference to the service layer, for introspection. + * This is kept via internal props instead of generic hooks because + * it is not exposed via a public API. + */ + serviceLayer: ServiceLayer; +} + +export function DevTools(props: DevToolsProps) { + const [isOpen, setIsOpen] = useState(false); + const [devToolsContent, setDevToolsContent] = useState(undefined); + + useEffect(() => { + if (!isOpen) { + return; + } + + // Lazy loading without suspense. + // Note that the content is a modal (not part of the normal document flow). + import("./DevToolsContent") + .then(({ DevToolsContent }) => { + setDevToolsContent( + setIsOpen(false)} + serviceLayer={props.serviceLayer} + /> + ); + }) + .catch((error) => { + LOG.error("Failed to load dev tools content", error); + }); + }, [isOpen, props.serviceLayer]); + + return ( + <> + {!isOpen && setIsOpen(true)} />}{" "} + {isOpen && devToolsContent} + + ); +} diff --git a/src/packages/runtime/dev-tools/DevToolsBadge.tsx b/src/packages/runtime/dev-tools/DevToolsBadge.tsx new file mode 100644 index 00000000..835f664b --- /dev/null +++ b/src/packages/runtime/dev-tools/DevToolsBadge.tsx @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +import { Box, Button } from "@open-pioneer/chakra-integration"; + +export interface DevToolsBadgeProps { + onOpen: () => void; +} + +export function DevToolsBadge(props: DevToolsBadgeProps) { + return ( + + + + ); +} diff --git a/src/packages/runtime/dev-tools/DevToolsContent.tsx b/src/packages/runtime/dev-tools/DevToolsContent.tsx new file mode 100644 index 00000000..a547f4f1 --- /dev/null +++ b/src/packages/runtime/dev-tools/DevToolsContent.tsx @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Button, + Code, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + HStack, + Heading, + ListItem, + Tab, + TabList, + TabPanel, + TabPanels, + Table, + Tabs, + Tbody, + Td, + Tooltip, + Tr, + Text, + UnorderedList, + useDisclosure +} from "@open-pioneer/chakra-integration"; +import { useService } from "open-pioneer:react-hooks"; +import { ReactNode, useEffect, useMemo, useState } from "react"; +import { ApplicationContext } from "../api"; +import { ServiceLayer } from "../service-layer/ServiceLayer"; +import { ReadonlyPackageRepr } from "../service-layer/PackageRepr"; + +export interface DevToolsContentProps { + serviceLayer: ServiceLayer; + + onClose: () => void; +} + +export function DevToolsContent(props: DevToolsContentProps) { + const { onClose } = props; + + return ( + + + + App State + Packages + Service Graph + + + + + + + + + + + + + ); +} + +function AppState() { + const appContext = useService("runtime.ApplicationContext"); + const hostElement = appContext.getHostElement(); + const shadowRoot = appContext.getShadowRoot(); + const appContainer = appContext.getApplicationContainer(); + const supportedMessageLocales = appContext.getSupportedLocales(); + const currentLocale = appContext.getLocale(); + return ( + + + + + + + + + + + + + + + + + + + + + + + +
Host element + +
Shadow root + +
Application container + +
Supported message locales + +
Current locale + +
+ ); +} + +function PackagesOverview(props: { serviceLayer: ServiceLayer }) { + const { serviceLayer } = props; + const packages = serviceLayer.packages; + + const [activeIndices, setActiveIndices] = useState([]); + const onAccordionChange = (indices: number | number[]) => { + setActiveIndices(indices); + }; + + const packageItems = useMemo(() => { + const sortedPackages = Array.from(packages).sort((a, b) => { + return a.name.localeCompare(b.name); + }); + return sortedPackages.map((pkg) => { + return ( + + + + + {pkg.name} + + + + + + + + + ); + }); + }, [packages]); + + return ( + <> + + Note: only lists trails packages. + + + + + + + {packageItems} + + + ); +} + +function PackageDetails(props: { pkg: ReadonlyPackageRepr }) { + const { name, services, properties } = props.pkg; + + const serviceList = useMemo(() => { + return ( + + {services.map((service) => { + return {service.id}; + })} + + ); + }, [services]); + + return ( + + + + + + + + + + + + + + + +
Name{name}
Properties + {JSON.stringify(properties, undefined, 4)} +
Services{serviceList}
+ ); +} + +function ObjectInspector(props: { object: unknown; displayText?: string }) { + const { object, displayText } = props; + const renderedObject = displayText ?? String(object); + + return ( + + {renderedObject} + + + + + ); +} + +function DevToolsDrawer(props: { onClose: () => void; children: ReactNode }) { + const { isOpen, onClose, onOpen } = useDisclosure(); + useEffect(() => { + // Defer opening the drawer.. overlay does not work correctly otherwise for some reason + onOpen(); + }, [onOpen]); + + return ( + + + + + + + Trails Dev Tools + + + {props.children} + + + ); +} + +function CodeBlock(props: { children: string }) { + return ( + + {props.children} + + ); +} diff --git a/src/packages/runtime/dev-tools/index.tsx b/src/packages/runtime/dev-tools/index.tsx new file mode 100644 index 00000000..242b9d55 --- /dev/null +++ b/src/packages/runtime/dev-tools/index.tsx @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +export { DevTools, type DevToolsProps } from "./DevTools"; diff --git a/src/packages/runtime/package.json b/src/packages/runtime/package.json index 03d1e949..ee490127 100644 --- a/src/packages/runtime/package.json +++ b/src/packages/runtime/package.json @@ -22,15 +22,19 @@ "@open-pioneer/chakra-integration": "workspace:^", "@open-pioneer/core": "workspace:^", "@open-pioneer/runtime-react-support": "workspace:^", + "@open-pioneer/reactivity": "workspace:^", + "@conterra/reactivity-core": "^0.4.0", "react": "^18.3.1", "react-dom": "^18.2.0" }, "devDependencies": { + "ts-essentials": "^10.0.1", "@open-pioneer/test-utils": "workspace:^", "core-packages": "workspace:^" }, "publishConfig": { "directory": "dist", "linkDirectory": false - } + }, + "dependencies": {} } diff --git a/src/packages/runtime/react-integration/ReactIntegration.test.ts b/src/packages/runtime/react-integration/ReactIntegration.test.ts index 1cef494d..589bf309 100644 --- a/src/packages/runtime/react-integration/ReactIntegration.test.ts +++ b/src/packages/runtime/react-integration/ReactIntegration.test.ts @@ -428,8 +428,8 @@ function createIntegration(options?: { serviceLayer.start(); const integration = new ReactIntegration({ + shadowRoot: wrapper, container: wrapper, - rootNode: wrapper, theme: options?.theme, packages, serviceLayer diff --git a/src/packages/runtime/react-integration/ReactIntegration.tsx b/src/packages/runtime/react-integration/ReactIntegration.tsx index 215378f1..fab9cc8d 100644 --- a/src/packages/runtime/react-integration/ReactIntegration.tsx +++ b/src/packages/runtime/react-integration/ReactIntegration.tsx @@ -1,27 +1,29 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 -import { ComponentType, StrictMode } from "react"; -import { createRoot, Root } from "react-dom/client"; +import { theme as defaultTrailsTheme } from "@open-pioneer/base-theme"; +import { CustomChakraProvider } from "@open-pioneer/chakra-integration"; import { Error } from "@open-pioneer/core"; -import { ErrorId } from "../errors"; -import { ServiceLayer } from "../service-layer/ServiceLayer"; import { PackageContext, PackageContextMethods } from "@open-pioneer/runtime-react-support"; -import { PackageRepr } from "../service-layer/PackageRepr"; +import { ComponentType, StrictMode } from "react"; +import { Root, createRoot } from "react-dom/client"; +import { ErrorId } from "../errors"; import { InterfaceSpec, renderInterfaceSpec } from "../service-layer/InterfaceSpec"; +import { PackageRepr } from "../service-layer/PackageRepr"; +import { ServiceLayer } from "../service-layer/ServiceLayer"; import { renderAmbiguousServiceChoices } from "../service-layer/ServiceLookup"; -import { CustomChakraProvider } from "@open-pioneer/chakra-integration"; -import { theme as defaultTrailsTheme } from "@open-pioneer/base-theme"; +import { DevTools } from "../dev-tools"; export interface ReactIntegrationOptions { packages: Map; serviceLayer: ServiceLayer; - rootNode: HTMLDivElement; - container: Node; + container: HTMLDivElement; + shadowRoot: Node; theme: Record | undefined; } export class ReactIntegration { - private containerNode: Node; + private shadowRoot: Node; + private container: HTMLDivElement; private theme: Record | undefined; private packages: Map; private serviceLayer: ServiceLayer; @@ -29,11 +31,12 @@ export class ReactIntegration { private packageContext: PackageContextMethods; constructor(options: ReactIntegrationOptions) { - this.containerNode = options.container; + this.shadowRoot = options.shadowRoot; + this.container = options.container; this.theme = options.theme; this.packages = options.packages; this.serviceLayer = options.serviceLayer; - this.root = createRoot(options.rootNode); + this.root = createRoot(options.container); this.packageContext = { getService: (packageName, interfaceName, options) => { const spec: InterfaceSpec = { interfaceName, ...options }; @@ -103,11 +106,15 @@ export class ReactIntegration { this.root.render( + {import.meta.env.DEV && !import.meta.env.VITEST && ( + + )} diff --git a/src/packages/runtime/service-layer/PackageRepr.ts b/src/packages/runtime/service-layer/PackageRepr.ts index c6b1f0f8..d947a902 100644 --- a/src/packages/runtime/service-layer/PackageRepr.ts +++ b/src/packages/runtime/service-layer/PackageRepr.ts @@ -6,7 +6,7 @@ import { ErrorId } from "../errors"; import { PackageIntl, AppI18n } from "../i18n"; import { PackageMetadata, PropertyMetadata } from "../metadata"; import { parseReferenceSpec, ReferenceSpec } from "./InterfaceSpec"; -import { ServiceRepr } from "./ServiceRepr"; +import { ReadonlyServiceRepr, ServiceRepr } from "./ServiceRepr"; export interface PackageReprOptions { name: string; @@ -16,7 +16,24 @@ export interface PackageReprOptions { properties?: Record; } -export class PackageRepr { +export interface ReadonlyPackageRepr { + /** Package name */ + readonly name: string; + + /** Services defined by the package */ + readonly services: readonly ReadonlyServiceRepr[]; + + /** Interfaces required by UI components. */ + readonly uiReferences: readonly Readonly[]; + + /** Resolved (perhaps customized) package properties. */ + readonly properties: Readonly>; + + /** Locale-dependant i18n messages. */ + readonly intl: PackageIntl; +} + +export class PackageRepr implements ReadonlyPackageRepr { static create( data: PackageMetadata, i18n: PackageIntl, @@ -46,19 +63,10 @@ export class PackageRepr { }); } - /** Package name */ readonly name: string; - - /** Services defined by the package */ readonly services: readonly ServiceRepr[]; - - /** Interfaces required by UI components. */ readonly uiReferences: readonly Readonly[]; - - /** Resolved (perhaps customized) package properties. */ readonly properties: Readonly>; - - /** Locale-dependant i18n messages. */ readonly intl: PackageIntl; constructor(options: PackageReprOptions) { diff --git a/src/packages/runtime/service-layer/ServiceLayer.ts b/src/packages/runtime/service-layer/ServiceLayer.ts index b764ac0f..9faa6986 100644 --- a/src/packages/runtime/service-layer/ServiceLayer.ts +++ b/src/packages/runtime/service-layer/ServiceLayer.ts @@ -9,7 +9,7 @@ import { UIDependency, verifyDependencies } from "./verifyDependencies"; -import { PackageRepr } from "./PackageRepr"; +import { PackageRepr, ReadonlyPackageRepr } from "./PackageRepr"; import { ReadonlyServiceLookup, ServiceLookupResult, ServicesLookupResult } from "./ServiceLookup"; import { InterfaceSpec, @@ -49,6 +49,8 @@ interface DependencyDeclarations { } export class ServiceLayer { + private _packages: readonly PackageRepr[]; + // All services in the application. private allServices: ServiceRepr[]; @@ -72,6 +74,8 @@ export class ServiceLayer { * In its current form, the service layer will start only forced references and references needed by the UI (and their dependencies). */ constructor(packages: readonly PackageRepr[], forcedReferences: ReferenceSpec[] = []) { + this._packages = packages; + const allServices = packages.map((pkg) => pkg.services).flat(); const uiDependencies = packages .map((pkg) => @@ -110,6 +114,10 @@ export class ServiceLayer { this.state = "destroyed"; } + get packages(): readonly ReadonlyPackageRepr[] { + return this._packages; + } + start() { if (this.state !== "not-started") { throw new Error(ErrorId.INTERNAL, "Service layer was already started."); diff --git a/src/packages/runtime/service-layer/ServiceRepr.ts b/src/packages/runtime/service-layer/ServiceRepr.ts index a7c68348..12b069e1 100644 --- a/src/packages/runtime/service-layer/ServiceRepr.ts +++ b/src/packages/runtime/service-layer/ServiceRepr.ts @@ -6,6 +6,7 @@ import { Service, ServiceConstructor, ServiceOptions } from "../Service"; import { Error } from "@open-pioneer/core"; import { InterfaceSpec, parseReferenceSpec, ReferenceSpec } from "./InterfaceSpec"; import { PackageIntl } from "../i18n"; +import { DeepReadonly } from "ts-essentials"; export type ServiceState = "not-constructed" | "constructing" | "constructed" | "destroyed"; @@ -34,11 +35,37 @@ export interface ServiceReprOptions { properties?: Record; } +export interface ReadonlyServiceRepr { + /** Unique id of this service. Contains the package name and the service name. */ + readonly id: string; + + /** Name of this service in it's package. */ + readonly name: string; + + /** Name of the parent package. */ + readonly packageName: string; + + /** Locale-dependant i18n messages. */ + readonly intl: PackageIntl; + + /** Service properties made available via the service's constructor. */ + readonly properties: Readonly>; + + /** Dependencies required by the service constructor. */ + readonly dependencies: DeepReadonly; + + /** Interfaces provided by the service. */ + readonly interfaces: DeepReadonly; + + /** The factory that creates instances of this service. */ + readonly factory: DeepReadonly; +} + /** * Represents metadata and state of a service in the runtime. * `this.instance` is the actual service instance (when constructed). */ -export class ServiceRepr { +export class ServiceRepr implements ReadonlyServiceRepr { static create( packageName: string, data: ServiceMetadata, @@ -76,33 +103,18 @@ export class ServiceRepr { }); } - /** Unique id of this service. Contains the package name and the service name. */ readonly id: string; - - /** Name of this service in it's package. */ readonly name: string; - - /** Name of the parent package. */ readonly packageName: string; - - /** Locale-dependant i18n messages. */ readonly intl: PackageIntl; - - /** Service properties made available via the service's constructor. */ readonly properties: Readonly>; - - /** Dependencies required by the service constructor. */ readonly dependencies: readonly ServiceDependency[]; - - /** Interfaces provided by the service. */ - readonly interfaces: readonly Readonly[]; + readonly interfaces: readonly InterfaceSpec[]; + readonly factory: ServiceFactory; /** Number of references to this service. */ private _useCount = 0; - /** Service factory to construct an instance. */ - private factory: ServiceFactory; - /** Current state of this service. "constructed" -> instance is available. */ private _state: ServiceState = "not-constructed"; diff --git a/src/packages/test-utils/react.tsx b/src/packages/test-utils/react.tsx index 4b31a3d6..cfd30d2c 100644 --- a/src/packages/test-utils/react.tsx +++ b/src/packages/test-utils/react.tsx @@ -63,7 +63,7 @@ export const PackageContextProvider: FC = (props) = const { children, ...rest } = props; const contextMethods = useMemo(() => createPackageContextMethods(rest), [rest]); return ( - + {children} diff --git a/vite.config.ts b/vite.config.ts index 3f2756ef..7a1f9932 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,7 +40,7 @@ export default defineConfig(({ mode }) => { const isVitest = mode === "test"; // Allowed values are "DEBUG", "INFO", "WARN", "ERROR" - const logLevel = devMode ? "INFO": "WARN"; + const logLevel = devMode ? "INFO" : "WARN"; return { root: resolve(__dirname, "src"),