@docouno/devkit 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -2
- package/devkit +0 -0
- package/notices/NOTICE.txt +395 -0
- package/notices/cargo/adler2-2.0.1-238207d3/notice-001.txt +12 -0
- package/notices/cargo/adler2-2.0.1-238207d3/notice-002.txt +201 -0
- package/notices/cargo/adler2-2.0.1-238207d3/notice-003.txt +23 -0
- package/notices/cargo/aho-corasick-1.1.5-90cd2b64/notice-001.txt +3 -0
- package/notices/cargo/aho-corasick-1.1.5-90cd2b64/notice-002.txt +21 -0
- package/notices/cargo/aho-corasick-1.1.5-90cd2b64/notice-003.txt +24 -0
- package/notices/cargo/annotate-snippets-0.12.17-e17d8c74/notice-001.txt +202 -0
- package/notices/cargo/annotate-snippets-0.12.17-e17d8c74/notice-002.txt +19 -0
- package/notices/cargo/anstyle-1.0.14-f65811ba/notice-001.txt +202 -0
- package/notices/cargo/anstyle-1.0.14-f65811ba/notice-002.txt +19 -0
- package/notices/cargo/anyhow-1.0.104-dc6381fb/notice-001.txt +176 -0
- package/notices/cargo/anyhow-1.0.104-dc6381fb/notice-002.txt +23 -0
- package/notices/cargo/arraydeque-0.5.1-42a9cc56/notice-001.txt +21 -0
- package/notices/cargo/autocfg-1.5.1-41f0cbe6/notice-001.txt +201 -0
- package/notices/cargo/autocfg-1.5.1-41f0cbe6/notice-002.txt +25 -0
- package/notices/cargo/base64-0.22.1-26575a7c/notice-001.txt +201 -0
- package/notices/cargo/base64-0.22.1-26575a7c/notice-002.txt +21 -0
- package/notices/cargo/base64-0.23.1-9f42fcbf/notice-001.txt +201 -0
- package/notices/cargo/base64-0.23.1-9f42fcbf/notice-002.txt +21 -0
- package/notices/cargo/bitflags-2.13.2-a49cf0c5/notice-001.txt +201 -0
- package/notices/cargo/bitflags-2.13.2-a49cf0c5/notice-002.txt +25 -0
- package/notices/cargo/block-buffer-0.10.4-0a748df3/notice-001.txt +201 -0
- package/notices/cargo/block-buffer-0.10.4-0a748df3/notice-002.txt +25 -0
- package/notices/cargo/bytes-1.12.1-e3524fe6/notice-001.txt +25 -0
- package/notices/cargo/cc-1.10.0-cda055ec/notice-001.txt +201 -0
- package/notices/cargo/cc-1.10.0-cda055ec/notice-002.txt +25 -0
- package/notices/cargo/cfg-if-1.0.5-cd0760fa/notice-001.txt +201 -0
- package/notices/cargo/cfg-if-1.0.5-cd0760fa/notice-002.txt +25 -0
- package/notices/cargo/core_detect-1.0.0-7ba90a69/notice-001.txt +201 -0
- package/notices/cargo/core_detect-1.0.0-7ba90a69/notice-002.txt +25 -0
- package/notices/cargo/cpufeatures-0.2.17-bcd8b7c7/notice-001.txt +201 -0
- package/notices/cargo/cpufeatures-0.2.17-bcd8b7c7/notice-002.txt +25 -0
- package/notices/cargo/crc32fast-1.5.3-6de55ffd/notice-001.txt +202 -0
- package/notices/cargo/crc32fast-1.5.3-6de55ffd/notice-002.txt +21 -0
- package/notices/cargo/crypto-common-0.1.7-d182dd5c/notice-001.txt +201 -0
- package/notices/cargo/crypto-common-0.1.7-d182dd5c/notice-002.txt +25 -0
- package/notices/cargo/digest-0.10.7-c92dfead/notice-001.txt +201 -0
- package/notices/cargo/digest-0.10.7-c92dfead/notice-002.txt +25 -0
- package/notices/cargo/encoding_rs-0.8.42-46195b9c/notice-001.txt +17 -0
- package/notices/cargo/encoding_rs-0.8.42-46195b9c/notice-002.txt +202 -0
- package/notices/cargo/encoding_rs-0.8.42-46195b9c/notice-003.txt +25 -0
- package/notices/cargo/encoding_rs-0.8.42-46195b9c/notice-004.txt +26 -0
- package/notices/cargo/encoding_rs_io-0.1.8-60e8f178/notice-001.txt +8 -0
- package/notices/cargo/encoding_rs_io-0.1.8-60e8f178/notice-002.txt +201 -0
- package/notices/cargo/encoding_rs_io-0.1.8-60e8f178/notice-003.txt +21 -0
- package/notices/cargo/equivalent-1.0.3-2193bbe7/notice-001.txt +201 -0
- package/notices/cargo/equivalent-1.0.3-2193bbe7/notice-002.txt +25 -0
- package/notices/cargo/errno-0.3.14-3ca95530/notice-001.txt +201 -0
- package/notices/cargo/errno-0.3.14-3ca95530/notice-002.txt +25 -0
- package/notices/cargo/fastrand-2.5.0-f2fd8a52/notice-001.txt +201 -0
- package/notices/cargo/fastrand-2.5.0-f2fd8a52/notice-002.txt +23 -0
- package/notices/cargo/filetime-0.2.29-d64063cd/notice-001.txt +201 -0
- package/notices/cargo/filetime-0.2.29-d64063cd/notice-002.txt +25 -0
- package/notices/cargo/find-msvc-tools-0.1.15-7bf23a75/notice-001.txt +201 -0
- package/notices/cargo/find-msvc-tools-0.1.15-7bf23a75/notice-002.txt +25 -0
- package/notices/cargo/flate2-1.1.10-8985ab4e/notice-001.txt +201 -0
- package/notices/cargo/flate2-1.1.10-8985ab4e/notice-002.txt +25 -0
- package/notices/cargo/foldhash-0.2.0-3ee6d7fb/notice-001.txt +19 -0
- package/notices/cargo/generic-array-0.14.7-31bd8478/notice-001.txt +21 -0
- package/notices/cargo/getrandom-0.2.17-766fa84f/notice-001.txt +201 -0
- package/notices/cargo/getrandom-0.2.17-766fa84f/notice-002.txt +26 -0
- package/notices/cargo/getrandom-0.4.3-4f1c9ebb/notice-001.txt +201 -0
- package/notices/cargo/getrandom-0.4.3-4f1c9ebb/notice-002.txt +26 -0
- package/notices/cargo/granit-parser-2.0.0-65bc2836/notice-001.txt +15 -0
- package/notices/cargo/hashbrown-0.17.1-97fc3cc1/notice-001.txt +201 -0
- package/notices/cargo/hashbrown-0.17.1-97fc3cc1/notice-002.txt +25 -0
- package/notices/cargo/http-1.5.0-6a40d4c4/notice-001.txt +201 -0
- package/notices/cargo/http-1.5.0-6a40d4c4/notice-002.txt +25 -0
- package/notices/cargo/httparse-1.10.1-cfa5aaf5/notice-001.txt +201 -0
- package/notices/cargo/httparse-1.10.1-cfa5aaf5/notice-002.txt +20 -0
- package/notices/cargo/indexmap-2.14.2-a08229e1/notice-001.txt +201 -0
- package/notices/cargo/indexmap-2.14.2-a08229e1/notice-002.txt +25 -0
- package/notices/cargo/itoa-1.0.18-b22a1f7a/notice-001.txt +176 -0
- package/notices/cargo/itoa-1.0.18-b22a1f7a/notice-002.txt +23 -0
- package/notices/cargo/libc-0.2.191-18856c2b/notice-001.txt +176 -0
- package/notices/cargo/libc-0.2.191-18856c2b/notice-002.txt +25 -0
- package/notices/cargo/linux-raw-sys-0.12.1-270748a5/notice-001.txt +29 -0
- package/notices/cargo/linux-raw-sys-0.12.1-270748a5/notice-002.txt +201 -0
- package/notices/cargo/linux-raw-sys-0.12.1-270748a5/notice-003.txt +220 -0
- package/notices/cargo/linux-raw-sys-0.12.1-270748a5/notice-004.txt +23 -0
- package/notices/cargo/log-0.4.34-23464387/notice-001.txt +201 -0
- package/notices/cargo/log-0.4.34-23464387/notice-002.txt +25 -0
- package/notices/cargo/memchr-2.8.3-9c6980a5/notice-001.txt +3 -0
- package/notices/cargo/memchr-2.8.3-9c6980a5/notice-002.txt +21 -0
- package/notices/cargo/memchr-2.8.3-9c6980a5/notice-003.txt +24 -0
- package/notices/cargo/miniz_oxide-0.9.1-d0d7eacf/notice-001.txt +25 -0
- package/notices/cargo/miniz_oxide-0.9.1-d0d7eacf/notice-002.txt +177 -0
- package/notices/cargo/miniz_oxide-0.9.1-d0d7eacf/notice-003.txt +24 -0
- package/notices/cargo/miniz_oxide-0.9.1-d0d7eacf/notice-004.txt +14 -0
- package/notices/cargo/multiversion_no_op-1.0.0-1ef14dda/notice-001.txt +15 -0
- package/notices/cargo/multiversion_no_op-1.0.0-1ef14dda/notice-002.txt +202 -0
- package/notices/cargo/multiversion_no_op-1.0.0-1ef14dda/notice-003.txt +25 -0
- package/notices/cargo/nohash-hasher-0.2.0-8ab2027b/notice-001.txt +202 -0
- package/notices/cargo/nohash-hasher-0.2.0-8ab2027b/notice-002.txt +18 -0
- package/notices/cargo/num-traits-0.2.19-d58f8d42/notice-001.txt +201 -0
- package/notices/cargo/num-traits-0.2.19-d58f8d42/notice-002.txt +25 -0
- package/notices/cargo/once_cell-1.21.4-4a678fe2/notice-001.txt +201 -0
- package/notices/cargo/once_cell-1.21.4-4a678fe2/notice-002.txt +23 -0
- package/notices/cargo/percent-encoding-2.3.2-9472d0b5/notice-001.txt +201 -0
- package/notices/cargo/percent-encoding-2.3.2-9472d0b5/notice-002.txt +25 -0
- package/notices/cargo/proc-macro2-1.0.107-ccabd92a/notice-001.txt +176 -0
- package/notices/cargo/proc-macro2-1.0.107-ccabd92a/notice-002.txt +23 -0
- package/notices/cargo/quote-1.0.47-0f6442d5/notice-001.txt +176 -0
- package/notices/cargo/quote-1.0.47-0f6442d5/notice-002.txt +23 -0
- package/notices/cargo/regex-1.13.1-3a9e4453/notice-001.txt +201 -0
- package/notices/cargo/regex-1.13.1-3a9e4453/notice-002.txt +25 -0
- package/notices/cargo/regex-automata-0.4.18-8f2fdfe1/notice-001.txt +201 -0
- package/notices/cargo/regex-automata-0.4.18-8f2fdfe1/notice-002.txt +25 -0
- package/notices/cargo/regex-syntax-0.8.11-22c58573/notice-001.txt +201 -0
- package/notices/cargo/regex-syntax-0.8.11-22c58573/notice-002.txt +25 -0
- package/notices/cargo/regex-syntax-0.8.11-22c58573/notice-003.txt +57 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-001.txt +9 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-002.txt +272 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-003.txt +13 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-004.txt +201 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-005.txt +23 -0
- package/notices/cargo/ring-0.17.14-a2ecd079/notice-006.txt +15 -0
- package/notices/cargo/rustix-1.1.5-75031891/notice-001.txt +29 -0
- package/notices/cargo/rustix-1.1.5-75031891/notice-002.txt +201 -0
- package/notices/cargo/rustix-1.1.5-75031891/notice-003.txt +220 -0
- package/notices/cargo/rustix-1.1.5-75031891/notice-004.txt +23 -0
- package/notices/cargo/rustls-0.23.46-d439923c/notice-001.txt +201 -0
- package/notices/cargo/rustls-0.23.46-d439923c/notice-002.txt +15 -0
- package/notices/cargo/rustls-0.23.46-d439923c/notice-003.txt +25 -0
- package/notices/cargo/rustls-pki-types-1.15.1-0cee9399/notice-001.txt +201 -0
- package/notices/cargo/rustls-pki-types-1.15.1-0cee9399/notice-002.txt +25 -0
- package/notices/cargo/rustls-webpki-0.103.16-8f5bb5a5/notice-001.txt +19 -0
- package/notices/cargo/rustversion-1.0.23-84eb0e4f/notice-001.txt +176 -0
- package/notices/cargo/rustversion-1.0.23-84eb0e4f/notice-002.txt +23 -0
- package/notices/cargo/scopeguard-1.2.0-ccdf1abe/notice-001.txt +201 -0
- package/notices/cargo/scopeguard-1.2.0-ccdf1abe/notice-002.txt +25 -0
- package/notices/cargo/serde-1.0.229-f2b8eb6e/notice-001.txt +176 -0
- package/notices/cargo/serde-1.0.229-f2b8eb6e/notice-002.txt +23 -0
- package/notices/cargo/serde-saphyr-2.0.0-6dbd70ad/notice-001.txt +176 -0
- package/notices/cargo/serde-saphyr-2.0.0-6dbd70ad/notice-002.txt +23 -0
- package/notices/cargo/serde_core-1.0.229-08cccc74/notice-001.txt +176 -0
- package/notices/cargo/serde_core-1.0.229-08cccc74/notice-002.txt +23 -0
- package/notices/cargo/serde_derive-1.0.229-db09cb76/notice-001.txt +176 -0
- package/notices/cargo/serde_derive-1.0.229-db09cb76/notice-002.txt +23 -0
- package/notices/cargo/serde_json-1.0.154-084efbf1/notice-001.txt +176 -0
- package/notices/cargo/serde_json-1.0.154-084efbf1/notice-002.txt +23 -0
- package/notices/cargo/serde_spanned-1.1.2-13274efe/notice-001.txt +202 -0
- package/notices/cargo/serde_spanned-1.1.2-13274efe/notice-002.txt +19 -0
- package/notices/cargo/sha1-0.10.7-be2212b4/notice-001.txt +201 -0
- package/notices/cargo/sha1-0.10.7-be2212b4/notice-002.txt +27 -0
- package/notices/cargo/sha2-0.10.9-be6ca6b4/notice-001.txt +201 -0
- package/notices/cargo/sha2-0.10.9-be6ca6b4/notice-002.txt +27 -0
- package/notices/cargo/shell-words-1.1.1-0d55474b/notice-001.txt +201 -0
- package/notices/cargo/shell-words-1.1.1-0d55474b/notice-002.txt +25 -0
- package/notices/cargo/shlex-2.0.1-39c59096/notice-001.txt +13 -0
- package/notices/cargo/shlex-2.0.1-39c59096/notice-002.txt +21 -0
- package/notices/cargo/signal-hook-0.3.18-a332e163/notice-001.txt +201 -0
- package/notices/cargo/signal-hook-0.3.18-a332e163/notice-002.txt +25 -0
- package/notices/cargo/signal-hook-registry-1.4.8-497004d4/notice-001.txt +201 -0
- package/notices/cargo/signal-hook-registry-1.4.8-497004d4/notice-002.txt +25 -0
- package/notices/cargo/simd-adler32-0.3.10-938489e4/notice-001.txt +21 -0
- package/notices/cargo/simdutf8-0.1.5-2b987c0c/notice-001.txt +177 -0
- package/notices/cargo/simdutf8-0.1.5-2b987c0c/notice-002.txt +19 -0
- package/notices/cargo/smallvec-1.16.4-cc422042/notice-001.txt +201 -0
- package/notices/cargo/smallvec-1.16.4-cc422042/notice-002.txt +25 -0
- package/notices/cargo/subtle-2.6.1-380cc33b/notice-001.txt +29 -0
- package/notices/cargo/syn-3.0.9-7a98808e/notice-001.txt +176 -0
- package/notices/cargo/syn-3.0.9-7a98808e/notice-002.txt +23 -0
- package/notices/cargo/tar-0.4.46-d72ec350/notice-001.txt +201 -0
- package/notices/cargo/tar-0.4.46-d72ec350/notice-002.txt +25 -0
- package/notices/cargo/tempfile-3.27.0-1f0bd317/notice-001.txt +201 -0
- package/notices/cargo/tempfile-3.27.0-1f0bd317/notice-002.txt +25 -0
- package/notices/cargo/toml-1.1.8-spec-1.1.0-ee8a4c7b/notice-001.txt +202 -0
- package/notices/cargo/toml-1.1.8-spec-1.1.0-ee8a4c7b/notice-002.txt +19 -0
- package/notices/cargo/toml_datetime-1.1.2-spec-1.1.0-f08fbf1e/notice-001.txt +202 -0
- package/notices/cargo/toml_datetime-1.1.2-spec-1.1.0-f08fbf1e/notice-002.txt +19 -0
- package/notices/cargo/toml_parser-1.1.5-spec-1.1.0-a11a62b2/notice-001.txt +202 -0
- package/notices/cargo/toml_parser-1.1.5-spec-1.1.0-a11a62b2/notice-002.txt +19 -0
- package/notices/cargo/toml_writer-1.1.3-spec-1.1.0-62b81a9a/notice-001.txt +202 -0
- package/notices/cargo/toml_writer-1.1.3-spec-1.1.0-62b81a9a/notice-002.txt +19 -0
- package/notices/cargo/typenum-1.20.1-6cdf3ca9/notice-001.txt +1 -0
- package/notices/cargo/typenum-1.20.1-6cdf3ca9/notice-002.txt +201 -0
- package/notices/cargo/typenum-1.20.1-6cdf3ca9/notice-003.txt +21 -0
- package/notices/cargo/unicode-ident-1.0.27-0f0691b5/notice-001.txt +176 -0
- package/notices/cargo/unicode-ident-1.0.27-0f0691b5/notice-002.txt +23 -0
- package/notices/cargo/unicode-ident-1.0.27-0f0691b5/notice-003.txt +39 -0
- package/notices/cargo/unicode-width-0.2.2-39fb3891/notice-001.txt +7 -0
- package/notices/cargo/unicode-width-0.2.2-39fb3891/notice-002.txt +201 -0
- package/notices/cargo/unicode-width-0.2.2-39fb3891/notice-003.txt +25 -0
- package/notices/cargo/untrusted-0.9.0-67ed8671/notice-001.txt +13 -0
- package/notices/cargo/ureq-3.5.0-13110b1a/notice-001.txt +201 -0
- package/notices/cargo/ureq-3.5.0-13110b1a/notice-002.txt +21 -0
- package/notices/cargo/ureq-proto-0.6.5-5c45f89a/notice-001.txt +202 -0
- package/notices/cargo/ureq-proto-0.6.5-5c45f89a/notice-002.txt +7 -0
- package/notices/cargo/utf8-zero-0.8.1-cf839cb2/notice-001.txt +176 -0
- package/notices/cargo/utf8-zero-0.8.1-cf839cb2/notice-002.txt +23 -0
- package/notices/cargo/uuid-1.29.0-9339a6aa/notice-001.txt +201 -0
- package/notices/cargo/uuid-1.29.0-9339a6aa/notice-002.txt +26 -0
- package/notices/cargo/version_check-0.9.5-13f5a7bf/notice-001.txt +201 -0
- package/notices/cargo/version_check-0.9.5-13f5a7bf/notice-002.txt +19 -0
- package/notices/cargo/webpki-roots-1.0.11-98a8d969/notice-001.txt +61 -0
- package/notices/cargo/winnow-1.0.4-06523b18/notice-001.txt +18 -0
- package/notices/cargo/zeroize-1.9.1-23ddc90c/notice-001.txt +202 -0
- package/notices/cargo/zeroize-1.9.1-23ddc90c/notice-002.txt +25 -0
- package/notices/cargo/zmij-1.0.23-bfa80998/notice-001.txt +23 -0
- package/notices/notice-set.json +2779 -0
- package/notices/rust/COPYRIGHT-library.html +29357 -0
- package/notices/rust/licenses/Apache-2.0.txt +73 -0
- package/notices/rust/licenses/BSD-2-Clause.txt +9 -0
- package/notices/rust/licenses/CC-BY-SA-4.0.txt +427 -0
- package/notices/rust/licenses/GCC-exception-3.1.txt +30 -0
- package/notices/rust/licenses/GPL-2.0-only.txt +133 -0
- package/notices/rust/licenses/GPL-3.0-or-later.txt +202 -0
- package/notices/rust/licenses/ISC.txt +7 -0
- package/notices/rust/licenses/LLVM-exception.txt +15 -0
- package/notices/rust/licenses/MIT.txt +9 -0
- package/notices/rust/licenses/NCSA.txt +29 -0
- package/notices/rust/licenses/OFL-1.1.txt +43 -0
- package/notices/rust/licenses/Unicode-3.0.txt +39 -0
- package/npm/docouno-env-0.3.0.tgz +0 -0
- package/package.json +27 -4
- package/release.json +1249 -0
- package/runtime/LICENSE +21 -0
- package/runtime/compose.tools.yaml +102 -0
- package/runtime/desktop-seccomp.json +1262 -0
- package/runtime/desktop.Dockerfile +29 -0
- package/runtime/devkit.mk +81 -0
- package/runtime/devkit.sh +87 -0
- package/runtime/licenses/Apache-2.0.txt +202 -0
- package/runtime/licenses/moby-profiles.NOTICE +4 -0
- package/runtime/rust.Dockerfile +8 -0
- package/runtime/toolchains.env +8 -0
- package/templates/electron-react/LICENSE +21 -0
- package/templates/electron-react/electron-builder.config.cjs +11 -0
- package/templates/electron-react/electron.vite.config.cjs +20 -0
- package/templates/electron-react/eslint.config.mjs +2 -0
- package/templates/electron-react/package-lock.json +7106 -0
- package/templates/electron-react/package.json +57 -0
- package/templates/electron-react/prettier.config.mjs +1 -0
- package/templates/electron-react/src/bridge.ts +10 -0
- package/templates/electron-react/src/main/api.test.ts +26 -0
- package/templates/electron-react/src/main/api.ts +38 -0
- package/templates/electron-react/src/main/index.ts +118 -0
- package/templates/electron-react/src/main/renderer-url.test.ts +18 -0
- package/templates/electron-react/src/main/renderer-url.ts +21 -0
- package/templates/electron-react/src/preload/index.ts +7 -0
- package/templates/electron-react/src/renderer/index.html +16 -0
- package/templates/electron-react/src/renderer/src/ClientApp.test.tsx +41 -0
- package/templates/electron-react/src/renderer/src/ClientApp.tsx +75 -0
- package/templates/electron-react/src/renderer/src/api.test.ts +15 -0
- package/templates/electron-react/src/renderer/src/api.ts +17 -0
- package/templates/electron-react/src/renderer/src/env.d.ts +6 -0
- package/templates/electron-react/src/renderer/src/main.tsx +24 -0
- package/templates/electron-react/src/renderer/src/style.css +71 -0
- package/templates/electron-react/tsconfig.json +8 -0
- package/templates/electron-react/tsconfig.node.json +13 -0
- package/templates/electron-react/tsconfig.test.json +7 -0
- package/templates/electron-react/variants/shared/files/src/renderer/src/main.tsx +24 -0
- package/templates/electron-react/variants/shared/variant.json +7 -0
- package/templates/electron-react/vitest.config.mjs +19 -0
- package/templates/node-api/LICENSE +21 -0
- package/templates/node-api/eslint.config.mjs +2 -0
- package/templates/node-api/package-lock.json +1989 -0
- package/templates/node-api/package.json +41 -0
- package/templates/node-api/prettier.config.mjs +1 -0
- package/templates/node-api/src/greeting.test.ts +14 -0
- package/templates/node-api/src/greeting.ts +8 -0
- package/templates/node-api/src/http.test.ts +64 -0
- package/templates/node-api/src/http.ts +46 -0
- package/templates/node-api/src/index.ts +26 -0
- package/templates/node-api/tsconfig.build.json +5 -0
- package/templates/node-api/tsconfig.json +9 -0
- package/templates/react-client/LICENSE +21 -0
- package/templates/react-client/eslint.config.mjs +2 -0
- package/templates/react-client/package-lock.json +3758 -0
- package/templates/react-client/package.json +68 -0
- package/templates/react-client/prettier.config.mjs +1 -0
- package/templates/react-client/src/ClientApp.test.tsx +41 -0
- package/templates/react-client/src/ClientApp.tsx +75 -0
- package/templates/react-client/src/index.ts +2 -0
- package/templates/react-client/src/style.css +71 -0
- package/templates/react-client/tsconfig.build.json +10 -0
- package/templates/react-client/tsconfig.json +8 -0
- package/templates/react-client/tsconfig.test.json +7 -0
- package/templates/react-client/vitest.config.mjs +19 -0
- package/templates/react-web/LICENSE +21 -0
- package/templates/react-web/eslint.config.mjs +2 -0
- package/templates/react-web/index.html +12 -0
- package/templates/react-web/package-lock.json +3754 -0
- package/templates/react-web/package.json +50 -0
- package/templates/react-web/prettier.config.mjs +1 -0
- package/templates/react-web/src/ClientApp.test.tsx +41 -0
- package/templates/react-web/src/ClientApp.tsx +75 -0
- package/templates/react-web/src/api.test.ts +24 -0
- package/templates/react-web/src/api.ts +29 -0
- package/templates/react-web/src/main.tsx +15 -0
- package/templates/react-web/src/style.css +71 -0
- package/templates/react-web/tsconfig.json +8 -0
- package/templates/react-web/tsconfig.test.json +7 -0
- package/templates/react-web/variants/shared/files/src/main.tsx +15 -0
- package/templates/react-web/variants/shared/variant.json +3 -0
- package/templates/react-web/vite.config.mjs +14 -0
- package/templates/react-web/vitest.config.mjs +19 -0
- package/templates/rust-api/Cargo.lock +476 -0
- package/templates/rust-api/Cargo.toml +22 -0
- package/templates/rust-api/LICENSE +21 -0
- package/templates/rust-api/rust-toolchain.toml +3 -0
- package/templates/rust-api/rustfmt.toml +2 -0
- package/templates/rust-api/src/lib.rs +68 -0
- package/templates/rust-api/src/main.rs +23 -0
- package/templates/rust-api/tests/api.rs +85 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devkit-electron-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^24.22.1"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"check": "devkit-verify && npm run format:check && npm run lint && npm run typecheck && npm test",
|
|
11
|
+
"format:check": "prettier --check src tsconfig.json tsconfig.node.json tsconfig.test.json eslint.config.mjs prettier.config.mjs vitest.config.mjs electron.vite.config.cjs electron-builder.config.cjs",
|
|
12
|
+
"lint": "eslint --no-ignore --max-warnings 0 src eslint.config.mjs prettier.config.mjs vitest.config.mjs electron.vite.config.cjs electron-builder.config.cjs",
|
|
13
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.test.json",
|
|
14
|
+
"test": "vitest run --config vitest.config.mjs",
|
|
15
|
+
"fix": "prettier --write src tsconfig.json tsconfig.node.json tsconfig.test.json eslint.config.mjs prettier.config.mjs vitest.config.mjs electron.vite.config.cjs electron-builder.config.cjs && eslint --no-ignore --max-warnings 0 --fix src eslint.config.mjs prettier.config.mjs vitest.config.mjs electron.vite.config.cjs electron-builder.config.cjs && prettier --write src tsconfig.json tsconfig.node.json tsconfig.test.json eslint.config.mjs prettier.config.mjs vitest.config.mjs electron.vite.config.cjs electron-builder.config.cjs",
|
|
16
|
+
"build": "npm run typecheck && electron-vite build",
|
|
17
|
+
"dev": "electron-vite dev",
|
|
18
|
+
"package": "electron-builder --config electron-builder.config.cjs --linux dir --publish never && tar -czf release/app-linux.tar.gz -C release/linux-unpacked ."
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@docouno/env": "0.3.0",
|
|
22
|
+
"@testing-library/dom": "10.4.2",
|
|
23
|
+
"@testing-library/react": "16.3.3",
|
|
24
|
+
"@types/node": "24.19.2",
|
|
25
|
+
"@types/react": "19.3.0",
|
|
26
|
+
"@types/react-dom": "19.3.0",
|
|
27
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
28
|
+
"eslint": "10.13.0",
|
|
29
|
+
"jsdom": "30.1.3",
|
|
30
|
+
"prettier": "3.9.10",
|
|
31
|
+
"typescript": "6.0.3",
|
|
32
|
+
"vite": "8.3.5",
|
|
33
|
+
"vitest": "5.0.5",
|
|
34
|
+
"react": "19.3.1",
|
|
35
|
+
"react-dom": "19.3.1",
|
|
36
|
+
"electron": "45.0.0",
|
|
37
|
+
"electron-vite": "6.0.0",
|
|
38
|
+
"electron-builder": "26.15.3"
|
|
39
|
+
},
|
|
40
|
+
"devkit": {
|
|
41
|
+
"sourceDirectories": [
|
|
42
|
+
"src"
|
|
43
|
+
],
|
|
44
|
+
"typecheckConfigs": [
|
|
45
|
+
"tsconfig.json",
|
|
46
|
+
"tsconfig.node.json",
|
|
47
|
+
"tsconfig.test.json"
|
|
48
|
+
],
|
|
49
|
+
"boundaries": {
|
|
50
|
+
"forbidCrossAppImports": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"main": "dist/main/index.js",
|
|
54
|
+
"description": "A React desktop application",
|
|
55
|
+
"author": "Application authors",
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@docouno/env/prettier';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { requestGreeting } from './api.js';
|
|
3
|
+
it('uses the configured backend and encodes the name', async () => {
|
|
4
|
+
let requested = '';
|
|
5
|
+
const fetcher: typeof fetch = (input) => {
|
|
6
|
+
requested = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
7
|
+
return Promise.resolve(new Response('{"message":"Hello!"}'));
|
|
8
|
+
};
|
|
9
|
+
await expect(requestGreeting('http://api:8080', 'Ada & Bob', fetcher)).resolves.toEqual({
|
|
10
|
+
message: 'Hello!',
|
|
11
|
+
});
|
|
12
|
+
expect(requested).toBe('http://api:8080/api/greeting?name=Ada+%26+Bob');
|
|
13
|
+
});
|
|
14
|
+
it('rejects invalid names and backend failures', async () => {
|
|
15
|
+
await expect(requestGreeting('http://api:8080', 42)).rejects.toThrow('name');
|
|
16
|
+
await expect(
|
|
17
|
+
requestGreeting('http://api:8080', 'Ada', () =>
|
|
18
|
+
Promise.resolve(new Response('down', { status: 503 })),
|
|
19
|
+
),
|
|
20
|
+
).rejects.toThrow('503');
|
|
21
|
+
await expect(
|
|
22
|
+
requestGreeting('http://api:8080', 'Ada', () =>
|
|
23
|
+
Promise.resolve(new Response('{"message":42}')),
|
|
24
|
+
),
|
|
25
|
+
).rejects.toThrow('invalid greeting');
|
|
26
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Greeting } from '../bridge.js';
|
|
2
|
+
export async function requestGreeting(
|
|
3
|
+
target: string,
|
|
4
|
+
name: unknown,
|
|
5
|
+
fetcher: typeof fetch = fetch,
|
|
6
|
+
): Promise<Greeting> {
|
|
7
|
+
if (typeof name !== 'string' || name.length > 80) {
|
|
8
|
+
throw new Error('A name of at most 80 characters is required.');
|
|
9
|
+
}
|
|
10
|
+
const url = new URL('/api/greeting', target);
|
|
11
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
12
|
+
throw new Error('Invalid API target.');
|
|
13
|
+
}
|
|
14
|
+
url.searchParams.set('name', name);
|
|
15
|
+
const response = await fetcher(url, {
|
|
16
|
+
headers: { accept: 'application/json' },
|
|
17
|
+
signal: AbortSignal.timeout(5000),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`API request failed (${response.status}).`);
|
|
21
|
+
}
|
|
22
|
+
let reply: unknown;
|
|
23
|
+
try {
|
|
24
|
+
reply = await response.json();
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error('API returned invalid JSON.');
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
!reply ||
|
|
30
|
+
typeof reply !== 'object' ||
|
|
31
|
+
!('message' in reply) ||
|
|
32
|
+
typeof reply.message !== 'string' ||
|
|
33
|
+
!reply.message
|
|
34
|
+
) {
|
|
35
|
+
throw new Error('API returned an invalid greeting.');
|
|
36
|
+
}
|
|
37
|
+
return { message: reply.message };
|
|
38
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { app, BrowserWindow, ipcMain, net, protocol } from 'electron';
|
|
2
|
+
import type { IpcMainInvokeEvent } from 'electron';
|
|
3
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve, sep } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { requestGreeting } from './api.js';
|
|
7
|
+
import { isTrustedRendererURL } from './renderer-url.js';
|
|
8
|
+
|
|
9
|
+
protocol.registerSchemesAsPrivileged([
|
|
10
|
+
{
|
|
11
|
+
scheme: 'app',
|
|
12
|
+
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true },
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
const rendererRoot = resolve(import.meta.dirname, '../renderer');
|
|
16
|
+
const development =
|
|
17
|
+
!app.isPackaged && process.env.ELECTRON_RENDERER_URL
|
|
18
|
+
? new URL(process.env.ELECTRON_RENDERER_URL)
|
|
19
|
+
: undefined;
|
|
20
|
+
if (
|
|
21
|
+
development &&
|
|
22
|
+
(development.protocol !== 'http:' ||
|
|
23
|
+
!['localhost', '127.0.0.1', '[::1]'].includes(development.hostname) ||
|
|
24
|
+
development.username ||
|
|
25
|
+
development.password)
|
|
26
|
+
) {
|
|
27
|
+
throw new Error('Development requires a local HTTP renderer origin.');
|
|
28
|
+
}
|
|
29
|
+
const rendererURL = development?.href ?? 'app://local/index.html';
|
|
30
|
+
const apiTarget = process.env.DEVKIT_API_TARGET;
|
|
31
|
+
|
|
32
|
+
function inside(root: string, file: string): boolean {
|
|
33
|
+
return file === root || file.startsWith(root + sep);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function createWindow(): Promise<void> {
|
|
37
|
+
const root = await realpath(rendererRoot);
|
|
38
|
+
protocol.handle('app', async (request) => {
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
if (
|
|
41
|
+
url.protocol !== 'app:' ||
|
|
42
|
+
url.hostname !== 'local' ||
|
|
43
|
+
url.port ||
|
|
44
|
+
url.username ||
|
|
45
|
+
url.password
|
|
46
|
+
) {
|
|
47
|
+
return new Response('Forbidden', { status: 403 });
|
|
48
|
+
}
|
|
49
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
50
|
+
return new Response('Method not allowed', { status: 405 });
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const relative = decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname);
|
|
54
|
+
const file = resolve(root, '.' + relative);
|
|
55
|
+
if (!inside(root, file)) {
|
|
56
|
+
return new Response('Forbidden', { status: 403 });
|
|
57
|
+
}
|
|
58
|
+
const actual = await realpath(file);
|
|
59
|
+
if (!inside(root, actual) || !(await stat(actual)).isFile()) {
|
|
60
|
+
return new Response('Forbidden', { status: 403 });
|
|
61
|
+
}
|
|
62
|
+
return net.fetch(pathToFileURL(actual).href);
|
|
63
|
+
} catch {
|
|
64
|
+
return new Response('Not found', { status: 404 });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const window = new BrowserWindow({
|
|
68
|
+
width: 1000,
|
|
69
|
+
height: 760,
|
|
70
|
+
webPreferences: {
|
|
71
|
+
preload: join(import.meta.dirname, '../preload/index.cjs'),
|
|
72
|
+
sandbox: true,
|
|
73
|
+
contextIsolation: true,
|
|
74
|
+
nodeIntegration: false,
|
|
75
|
+
webSecurity: true,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
function assertSender(event: IpcMainInvokeEvent): void {
|
|
79
|
+
if (
|
|
80
|
+
event.sender !== window.webContents ||
|
|
81
|
+
event.senderFrame !== window.webContents.mainFrame ||
|
|
82
|
+
!isTrustedRendererURL(event.senderFrame.url, rendererURL)
|
|
83
|
+
) {
|
|
84
|
+
throw new Error('Untrusted renderer.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
ipcMain.handle('desktop:capabilities', (event) => {
|
|
88
|
+
assertSender(event);
|
|
89
|
+
return { apiEnabled: Boolean(apiTarget) };
|
|
90
|
+
});
|
|
91
|
+
ipcMain.handle('desktop:greet', (event, name: unknown) => {
|
|
92
|
+
assertSender(event);
|
|
93
|
+
if (!apiTarget) {
|
|
94
|
+
throw new Error('No API is configured.');
|
|
95
|
+
}
|
|
96
|
+
return requestGreeting(apiTarget, name);
|
|
97
|
+
});
|
|
98
|
+
window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
99
|
+
window.webContents.on('will-navigate', (event, url) => {
|
|
100
|
+
if (!isTrustedRendererURL(url, rendererURL)) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
window.webContents.session.setPermissionRequestHandler((_contents, _permission, callback) =>
|
|
105
|
+
callback(false),
|
|
106
|
+
);
|
|
107
|
+
window.webContents.session.setPermissionCheckHandler(() => false);
|
|
108
|
+
await window.loadURL(rendererURL);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
app
|
|
112
|
+
.whenReady()
|
|
113
|
+
.then(createWindow)
|
|
114
|
+
.catch((error: unknown) => {
|
|
115
|
+
console.error(error);
|
|
116
|
+
app.exit(1);
|
|
117
|
+
});
|
|
118
|
+
app.on('window-all-closed', () => app.quit());
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { isTrustedRendererURL } from './renderer-url.js';
|
|
3
|
+
it('matches the exact development origin rather than a port prefix', () => {
|
|
4
|
+
expect(isTrustedRendererURL('http://localhost:5173/page', 'http://localhost:5173')).toBe(true);
|
|
5
|
+
expect(isTrustedRendererURL('http://localhost:51730/page', 'http://localhost:5173')).toBe(false);
|
|
6
|
+
expect(isTrustedRendererURL('http://localhost.example:5173/page', 'http://localhost:5173')).toBe(
|
|
7
|
+
false,
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
it('matches only the local application scheme and host', () => {
|
|
11
|
+
expect(isTrustedRendererURL('app://local/index.html', 'app://local/index.html')).toBe(true);
|
|
12
|
+
expect(isTrustedRendererURL('app://local.example/index.html', 'app://local/index.html')).toBe(
|
|
13
|
+
false,
|
|
14
|
+
);
|
|
15
|
+
expect(isTrustedRendererURL('app://local:9999/index.html', 'app://local/index.html')).toBe(false);
|
|
16
|
+
expect(isTrustedRendererURL('app://user@local/index.html', 'app://local/index.html')).toBe(false);
|
|
17
|
+
expect(isTrustedRendererURL('not a URL', 'app://local/index.html')).toBe(false);
|
|
18
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function isTrustedRendererURL(candidate: string, expected: string): boolean {
|
|
2
|
+
try {
|
|
3
|
+
const target = new URL(candidate);
|
|
4
|
+
const renderer = new URL(expected);
|
|
5
|
+
if (target.username || target.password) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (renderer.protocol === 'http:') {
|
|
9
|
+
return target.protocol === 'http:' && target.origin === renderer.origin;
|
|
10
|
+
}
|
|
11
|
+
return (
|
|
12
|
+
renderer.protocol === 'app:' &&
|
|
13
|
+
renderer.hostname === 'local' &&
|
|
14
|
+
target.protocol === 'app:' &&
|
|
15
|
+
target.hostname === 'local' &&
|
|
16
|
+
target.port === ''
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from 'electron';
|
|
2
|
+
import type { DesktopBridge, DesktopCapabilities, Greeting } from '../bridge.js';
|
|
3
|
+
const bridge: DesktopBridge = {
|
|
4
|
+
getCapabilities: () => ipcRenderer.invoke('desktop:capabilities') as Promise<DesktopCapabilities>,
|
|
5
|
+
greet: (name) => ipcRenderer.invoke('desktop:greet', name) as Promise<Greeting>,
|
|
6
|
+
};
|
|
7
|
+
contextBridge.exposeInMainWorld('desktop', Object.freeze(bridge));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:5173"
|
|
9
|
+
/>
|
|
10
|
+
<title>Devkit desktop</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, expect, it } from 'vitest';
|
|
3
|
+
import { ClientApp } from './ClientApp.js';
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
it('works locally without requiring a backend', () => {
|
|
8
|
+
render(<ClientApp platform="web" />);
|
|
9
|
+
fireEvent.change(screen.getByLabelText('Name'), { target: { value: ' Ada ' } });
|
|
10
|
+
expect(screen.getByTestId('local-greeting').textContent).toBe('Hello, Ada!');
|
|
11
|
+
fireEvent.click(screen.getByRole('button', { name: 'Increment' }));
|
|
12
|
+
expect(screen.getByTestId('count').textContent).toBe('Clicks: 1');
|
|
13
|
+
expect(screen.queryByRole('button', { name: 'Ask API' })).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('uses the supplied API and renders its response', async () => {
|
|
17
|
+
let requested = '';
|
|
18
|
+
render(
|
|
19
|
+
<ClientApp
|
|
20
|
+
platform="desktop"
|
|
21
|
+
api={{
|
|
22
|
+
greet: (name) => {
|
|
23
|
+
requested = name;
|
|
24
|
+
return Promise.resolve({ message: 'Hello from API!' });
|
|
25
|
+
},
|
|
26
|
+
}}
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Ada' } });
|
|
30
|
+
fireEvent.click(screen.getByRole('button', { name: 'Ask API' }));
|
|
31
|
+
expect(await screen.findByText('Hello from API!')).toBeDefined();
|
|
32
|
+
expect(requested).toBe('Ada');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows an API failure without substituting a local response', async () => {
|
|
36
|
+
render(
|
|
37
|
+
<ClientApp platform="web" api={{ greet: () => Promise.reject(new Error('Unavailable')) }} />,
|
|
38
|
+
);
|
|
39
|
+
fireEvent.click(screen.getByRole('button', { name: 'Ask API' }));
|
|
40
|
+
expect((await screen.findByRole('alert')).textContent).toBe('Unavailable');
|
|
41
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ClientApi {
|
|
4
|
+
greet(name: string): Promise<{ message: string }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type RemoteState = { phase: 'idle' | 'loading' } | { phase: 'ready' | 'error'; message: string };
|
|
8
|
+
|
|
9
|
+
export function ClientApp({ api, platform }: { api?: ClientApi; platform: string }) {
|
|
10
|
+
const [name, setName] = useState('world');
|
|
11
|
+
const [count, setCount] = useState(0);
|
|
12
|
+
const [remote, setRemote] = useState<RemoteState>({ phase: 'idle' });
|
|
13
|
+
const request = useRef(0);
|
|
14
|
+
useEffect(
|
|
15
|
+
() => () => {
|
|
16
|
+
request.current += 1;
|
|
17
|
+
},
|
|
18
|
+
[],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function requestGreeting() {
|
|
22
|
+
if (!api) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const current = ++request.current;
|
|
26
|
+
setRemote({ phase: 'loading' });
|
|
27
|
+
api.greet(name).then(
|
|
28
|
+
(reply) => {
|
|
29
|
+
if (request.current === current) {
|
|
30
|
+
setRemote({ phase: 'ready', message: reply.message });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
(error: unknown) => {
|
|
34
|
+
if (request.current === current) {
|
|
35
|
+
setRemote({
|
|
36
|
+
phase: 'error',
|
|
37
|
+
message: error instanceof Error ? error.message : 'API request failed.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<main>
|
|
46
|
+
<p className="eyebrow">Devkit · {platform}</p>
|
|
47
|
+
<h1>A small app, ready to change.</h1>
|
|
48
|
+
<p>One interface for local interaction and an optional API connection.</p>
|
|
49
|
+
<section className="card" aria-label="Local interaction">
|
|
50
|
+
<label htmlFor="name">Name</label>
|
|
51
|
+
<input
|
|
52
|
+
id="name"
|
|
53
|
+
value={name}
|
|
54
|
+
maxLength={80}
|
|
55
|
+
onChange={(event) => setName(event.target.value)}
|
|
56
|
+
/>
|
|
57
|
+
<output data-testid="local-greeting">Hello, {name.trim() || 'world'}!</output>
|
|
58
|
+
<button type="button" onClick={() => setCount((value) => value + 1)}>
|
|
59
|
+
Increment
|
|
60
|
+
</button>
|
|
61
|
+
<span data-testid="count">Clicks: {count}</span>
|
|
62
|
+
</section>
|
|
63
|
+
{api && (
|
|
64
|
+
<section className="api-panel" aria-label="API greeting">
|
|
65
|
+
<h2>API connection</h2>
|
|
66
|
+
<button type="button" onClick={requestGreeting} disabled={remote.phase === 'loading'}>
|
|
67
|
+
{remote.phase === 'loading' ? 'Requesting…' : 'Ask API'}
|
|
68
|
+
</button>
|
|
69
|
+
{remote.phase === 'ready' && <p role="status">{remote.message}</p>}
|
|
70
|
+
{remote.phase === 'error' && <p role="alert">{remote.message}</p>}
|
|
71
|
+
</section>
|
|
72
|
+
)}
|
|
73
|
+
</main>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { greet } from './api.js';
|
|
3
|
+
it('uses the specific desktop greeting bridge', async () => {
|
|
4
|
+
let requested = '';
|
|
5
|
+
await expect(
|
|
6
|
+
greet('Ada', {
|
|
7
|
+
getCapabilities: () => Promise.resolve({ apiEnabled: true }),
|
|
8
|
+
greet: (name) => {
|
|
9
|
+
requested = name;
|
|
10
|
+
return Promise.resolve({ message: 'Hello, Ada!' });
|
|
11
|
+
},
|
|
12
|
+
}),
|
|
13
|
+
).resolves.toEqual({ message: 'Hello, Ada!' });
|
|
14
|
+
expect(requested).toBe('Ada');
|
|
15
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DesktopBridge, Greeting } from '../../bridge.js';
|
|
2
|
+
export async function greet(
|
|
3
|
+
name: string,
|
|
4
|
+
bridge: DesktopBridge = window.desktop,
|
|
5
|
+
): Promise<Greeting> {
|
|
6
|
+
const value: unknown = await bridge.greet(name);
|
|
7
|
+
if (
|
|
8
|
+
!value ||
|
|
9
|
+
typeof value !== 'object' ||
|
|
10
|
+
!('message' in value) ||
|
|
11
|
+
typeof value.message !== 'string' ||
|
|
12
|
+
!value.message
|
|
13
|
+
) {
|
|
14
|
+
throw new Error('Desktop returned an invalid greeting.');
|
|
15
|
+
}
|
|
16
|
+
return { message: value.message };
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { ClientApp } from './ClientApp.js';
|
|
4
|
+
import { greet } from './api.js';
|
|
5
|
+
import './style.css';
|
|
6
|
+
const element = document.getElementById('root');
|
|
7
|
+
if (!element) {
|
|
8
|
+
throw new Error('Missing application root.');
|
|
9
|
+
}
|
|
10
|
+
const root = createRoot(element);
|
|
11
|
+
window.desktop.getCapabilities().then(
|
|
12
|
+
({ apiEnabled }) =>
|
|
13
|
+
root.render(
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<ClientApp platform="desktop" {...(apiEnabled ? { api: { greet } } : {})} />
|
|
16
|
+
</StrictMode>,
|
|
17
|
+
),
|
|
18
|
+
(error: unknown) =>
|
|
19
|
+
root.render(
|
|
20
|
+
<p role="alert">
|
|
21
|
+
{error instanceof Error ? error.message : 'Desktop initialization failed.'}
|
|
22
|
+
</p>,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, sans-serif;
|
|
3
|
+
color: #17233a;
|
|
4
|
+
background: #f4f7fb;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
}
|
|
11
|
+
main {
|
|
12
|
+
max-width: 44rem;
|
|
13
|
+
margin: 4rem auto;
|
|
14
|
+
padding: 0 1.5rem;
|
|
15
|
+
}
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: clamp(2rem, 5vw, 3rem);
|
|
18
|
+
line-height: 1.1;
|
|
19
|
+
}
|
|
20
|
+
h2 {
|
|
21
|
+
font-size: 1.25rem;
|
|
22
|
+
}
|
|
23
|
+
p {
|
|
24
|
+
line-height: 1.6;
|
|
25
|
+
}
|
|
26
|
+
.eyebrow {
|
|
27
|
+
color: #3159a6;
|
|
28
|
+
font-weight: 650;
|
|
29
|
+
}
|
|
30
|
+
.card,
|
|
31
|
+
.api-panel {
|
|
32
|
+
display: grid;
|
|
33
|
+
gap: 1rem;
|
|
34
|
+
margin-top: 2rem;
|
|
35
|
+
padding: 1.5rem;
|
|
36
|
+
border: 1px solid #dce3ef;
|
|
37
|
+
border-radius: 1rem;
|
|
38
|
+
background: white;
|
|
39
|
+
}
|
|
40
|
+
input,
|
|
41
|
+
button {
|
|
42
|
+
font: inherit;
|
|
43
|
+
padding: 0.75rem 1rem;
|
|
44
|
+
border-radius: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
input {
|
|
47
|
+
border: 1px solid #8391a9;
|
|
48
|
+
}
|
|
49
|
+
button {
|
|
50
|
+
justify-self: start;
|
|
51
|
+
border: none;
|
|
52
|
+
background: #3159a6;
|
|
53
|
+
color: white;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
button:disabled {
|
|
57
|
+
opacity: 0.65;
|
|
58
|
+
cursor: wait;
|
|
59
|
+
}
|
|
60
|
+
input:focus-visible,
|
|
61
|
+
button:focus-visible {
|
|
62
|
+
outline: 3px solid #8eafe8;
|
|
63
|
+
outline-offset: 3px;
|
|
64
|
+
}
|
|
65
|
+
output {
|
|
66
|
+
font-size: 1.4rem;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
}
|
|
69
|
+
[role='alert'] {
|
|
70
|
+
color: #a5222a;
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@docouno/env/tsconfig/node.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"lib": ["ES2023", "DOM"],
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"checkJs": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/main/**/*.ts", "src/preload/**/*.ts", "src/bridge.ts", "*.cjs", "*.mjs"],
|
|
12
|
+
"exclude": ["src/**/*.test.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { ClientApp } from '__DEVKIT_CLIENT_PACKAGE__';
|
|
4
|
+
import { greet } from './api.js';
|
|
5
|
+
import '__DEVKIT_CLIENT_PACKAGE__/style.css';
|
|
6
|
+
const element = document.getElementById('root');
|
|
7
|
+
if (!element) {
|
|
8
|
+
throw new Error('Missing application root.');
|
|
9
|
+
}
|
|
10
|
+
const root = createRoot(element);
|
|
11
|
+
window.desktop.getCapabilities().then(
|
|
12
|
+
({ apiEnabled }) =>
|
|
13
|
+
root.render(
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<ClientApp platform="desktop" {...(apiEnabled ? { api: { greet } } : {})} />
|
|
16
|
+
</StrictMode>,
|
|
17
|
+
),
|
|
18
|
+
(error: unknown) =>
|
|
19
|
+
root.render(
|
|
20
|
+
<p role="alert">
|
|
21
|
+
{error instanceof Error ? error.message : 'Desktop initialization failed.'}
|
|
22
|
+
</p>,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
import RunnableTestsReporter from '@docouno/env/vitest-reporter';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
cacheDir: 'node_modules/.cache/devkit-vitest',
|
|
8
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
9
|
+
test: {
|
|
10
|
+
environment: 'jsdom',
|
|
11
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
12
|
+
globals: false,
|
|
13
|
+
passWithNoTests: false,
|
|
14
|
+
reporters: ['default', new RunnableTestsReporter()],
|
|
15
|
+
restoreMocks: true,
|
|
16
|
+
clearMocks: true,
|
|
17
|
+
maxWorkers: 2,
|
|
18
|
+
},
|
|
19
|
+
});
|