diff --git a/apps/petrinaut-website/LICENSE-APACHE.md b/apps/petrinaut-website/LICENSE-APACHE.md new file mode 100644 index 00000000000..4b43328a923 --- /dev/null +++ b/apps/petrinaut-website/LICENSE-APACHE.md @@ -0,0 +1,189 @@ +# Apache License + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +- **(a)** You must give any other recipients of the Work or Derivative Works a copy of + this License; and +- **(b)** You must cause any modified files to carry prominent notices stating that You + changed the files; and +- **(c)** You must retain, in the Source form of any Derivative Works that You distribute, + all copyright, patent, trademark, and attribution notices from the Source form + of the Work, excluding those notices that do not pertain to any part of the + Derivative Works; and +- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any + Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, in at least one of the + following places: within a NOTICE text file distributed as part of the + Derivative Works; within the Source form or documentation, if provided along + with the Derivative Works; or, within a display generated by the Derivative + Works, if and wherever such third-party notices normally appear. The contents of + the NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works that + You distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed as + modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: Apply the Apache License to a specific file + +To apply the Apache License to an individual file, attach the following notice. +The text should be enclosed in the appropriate comment syntax for the file +format. + + Copyright © 2025–, HASH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/petrinaut-website/LICENSE-MIT.md b/apps/petrinaut-website/LICENSE-MIT.md new file mode 100644 index 00000000000..d85585ee20d --- /dev/null +++ b/apps/petrinaut-website/LICENSE-MIT.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright © 2025–, HASH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/petrinaut-website/LICENSE.md b/apps/petrinaut-website/LICENSE.md new file mode 100644 index 00000000000..6dad94c0e5a --- /dev/null +++ b/apps/petrinaut-website/LICENSE.md @@ -0,0 +1,3 @@ +# License + +Licensed under either of the [Apache License, Version 2.0](LICENSE-APACHE.md) or [MIT license](LICENSE-MIT.md) at your option. diff --git a/apps/petrinaut-website/eslint.config.js b/apps/petrinaut-website/eslint.config.js new file mode 100644 index 00000000000..668199a212b --- /dev/null +++ b/apps/petrinaut-website/eslint.config.js @@ -0,0 +1,23 @@ +import { createBase } from "@local/eslint/deprecated"; + +export default [ + ...createBase(import.meta.dirname), + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ["vite.config.ts"], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + // Disabled because React Compiler handles optimization automatically + "react/jsx-no-bind": "off", + "react/jsx-no-constructed-context-values": "off", + "react-hooks/exhaustive-deps": "off", + }, + }, +]; diff --git a/libs/@hashintel/petrinaut/demo-site/favicon.png b/apps/petrinaut-website/favicon.png similarity index 100% rename from libs/@hashintel/petrinaut/demo-site/favicon.png rename to apps/petrinaut-website/favicon.png diff --git a/libs/@hashintel/petrinaut/demo-site/index.html b/apps/petrinaut-website/index.html similarity index 91% rename from libs/@hashintel/petrinaut/demo-site/index.html rename to apps/petrinaut-website/index.html index b8525e95898..86b7582ccc6 100644 --- a/libs/@hashintel/petrinaut/demo-site/index.html +++ b/apps/petrinaut-website/index.html @@ -32,7 +32,7 @@
- + diff --git a/apps/petrinaut-website/package.json b/apps/petrinaut-website/package.json new file mode 100644 index 00000000000..f8e24cbb8aa --- /dev/null +++ b/apps/petrinaut-website/package.json @@ -0,0 +1,31 @@ +{ + "name": "@apps/petrinaut-website", + "version": "0.0.0-private", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "fix:eslint": "eslint --fix .", + "lint:eslint": "eslint --report-unused-disable-directives .", + "lint:tsc": "tsgo --noEmit" + }, + "dependencies": { + "@hashintel/petrinaut": "workspace:*", + "@mantine/hooks": "8.3.5", + "@sentry/react": "10.22.0", + "immer": "10.1.3", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@local/eslint": "workspace:*", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260309.1", + "@vitejs/plugin-react": "5.1.4", + "babel-plugin-react-compiler": "1.0.0", + "eslint": "9.39.3", + "vite": "8.0.0-beta.18" + } +} diff --git a/libs/@hashintel/petrinaut/demo-site/main.tsx b/apps/petrinaut-website/src/main.tsx similarity index 95% rename from libs/@hashintel/petrinaut/demo-site/main.tsx rename to apps/petrinaut-website/src/main.tsx index 1fb8c72a28c..474e65cddfd 100644 --- a/libs/@hashintel/petrinaut/demo-site/main.tsx +++ b/apps/petrinaut-website/src/main.tsx @@ -1,3 +1,4 @@ +import "@hashintel/petrinaut/dist/main.css"; import "./sentry/instrument"; import * as Sentry from "@sentry/react"; diff --git a/libs/@hashintel/petrinaut/demo-site/main/app.tsx b/apps/petrinaut-website/src/main/app.tsx similarity index 96% rename from libs/@hashintel/petrinaut/demo-site/main/app.tsx rename to apps/petrinaut-website/src/main/app.tsx index 5ac24a5e94f..0a0f0fdd54a 100644 --- a/libs/@hashintel/petrinaut/demo-site/main/app.tsx +++ b/apps/petrinaut-website/src/main/app.tsx @@ -1,9 +1,8 @@ +import type { MinimalNetMetadata, SDCPN } from "@hashintel/petrinaut"; +import { convertOldFormatToSDCPN, Petrinaut } from "@hashintel/petrinaut"; import { produce } from "immer"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { MinimalNetMetadata, SDCPN } from "../../src/core/types/sdcpn"; -import { convertOldFormatToSDCPN } from "../../src/old-formats/convert-old-format"; -import { Petrinaut } from "../../src/petrinaut"; import { isOldFormatInLocalStorage, type SDCPNInLocalStorage, diff --git a/libs/@hashintel/petrinaut/demo-site/main/app/use-local-storage-sdcpns.ts b/apps/petrinaut-website/src/main/app/use-local-storage-sdcpns.ts similarity index 85% rename from libs/@hashintel/petrinaut/demo-site/main/app/use-local-storage-sdcpns.ts rename to apps/petrinaut-website/src/main/app/use-local-storage-sdcpns.ts index b472c44f290..42c2a4b1b9a 100644 --- a/libs/@hashintel/petrinaut/demo-site/main/app/use-local-storage-sdcpns.ts +++ b/apps/petrinaut-website/src/main/app/use-local-storage-sdcpns.ts @@ -1,11 +1,7 @@ +import type { OldFormat, SDCPN } from "@hashintel/petrinaut"; +import { isOldFormat } from "@hashintel/petrinaut"; import { useLocalStorage } from "@mantine/hooks"; -import type { SDCPN } from "../../../src/core/types/sdcpn"; -import { - isOldFormat, - type OldFormat, -} from "../../../src/old-formats/convert-old-format"; - const rootLocalStorageKey = "petrinaut-sdcpn"; export type SDCPNInLocalStorage = { diff --git a/libs/@hashintel/petrinaut/demo-site/main/app/use-undo-redo.ts b/apps/petrinaut-website/src/main/app/use-undo-redo.ts similarity index 97% rename from libs/@hashintel/petrinaut/demo-site/main/app/use-undo-redo.ts rename to apps/petrinaut-website/src/main/app/use-undo-redo.ts index 514f740989d..690bfefa270 100644 --- a/libs/@hashintel/petrinaut/demo-site/main/app/use-undo-redo.ts +++ b/apps/petrinaut-website/src/main/app/use-undo-redo.ts @@ -1,8 +1,7 @@ +import type { SDCPN } from "@hashintel/petrinaut"; +import { isSDCPNEqual } from "@hashintel/petrinaut"; import { useRef, useState } from "react"; -import type { SDCPN } from "../../../src/core/types/sdcpn"; -import { isSDCPNEqual } from "../../../src/petrinaut"; - export type HistoryEntry = { sdcpn: SDCPN; timestamp: string; diff --git a/libs/@hashintel/petrinaut/demo-site/sentry/instrument.ts b/apps/petrinaut-website/src/sentry/instrument.ts similarity index 76% rename from libs/@hashintel/petrinaut/demo-site/sentry/instrument.ts rename to apps/petrinaut-website/src/sentry/instrument.ts index 314a1451db4..7cf1372eea5 100644 --- a/libs/@hashintel/petrinaut/demo-site/sentry/instrument.ts +++ b/apps/petrinaut-website/src/sentry/instrument.ts @@ -1,6 +1,3 @@ -// Sentry is only used in the demo site, which is bundled by Vite, -// so we can keep @sentry/react a dev dependency. -// eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from "@sentry/react"; Sentry.init({ diff --git a/libs/@hashintel/petrinaut/demo-site/sentry/sentry-error-tracker-provider.tsx b/apps/petrinaut-website/src/sentry/sentry-error-tracker-provider.tsx similarity index 83% rename from libs/@hashintel/petrinaut/demo-site/sentry/sentry-error-tracker-provider.tsx rename to apps/petrinaut-website/src/sentry/sentry-error-tracker-provider.tsx index 2605ba6efd5..a123ed5d9c5 100644 --- a/libs/@hashintel/petrinaut/demo-site/sentry/sentry-error-tracker-provider.tsx +++ b/apps/petrinaut-website/src/sentry/sentry-error-tracker-provider.tsx @@ -1,7 +1,6 @@ +import { ErrorTrackerContext } from "@hashintel/petrinaut"; import * as Sentry from "@sentry/react"; -import { ErrorTrackerContext } from "../../src/error-tracker/error-tracker.context"; - /** * Provider that implements ErrorTrackerContext using Sentry */ diff --git a/libs/@hashintel/petrinaut/demo-site/vite-env.d.ts b/apps/petrinaut-website/src/vite-env.d.ts similarity index 100% rename from libs/@hashintel/petrinaut/demo-site/vite-env.d.ts rename to apps/petrinaut-website/src/vite-env.d.ts diff --git a/apps/petrinaut-website/tsconfig.json b/apps/petrinaut-website/tsconfig.json new file mode 100644 index 00000000000..a8ddecb6f7e --- /dev/null +++ b/apps/petrinaut-website/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "target": "es2024", + "lib": ["dom", "dom.iterable", "ESNext"], + "module": "preserve", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/libs/@hashintel/petrinaut/vercel-build.sh b/apps/petrinaut-website/vercel-build.sh similarity index 83% rename from libs/@hashintel/petrinaut/vercel-build.sh rename to apps/petrinaut-website/vercel-build.sh index 1f0766ac418..3b4d59e0e56 100755 --- a/libs/@hashintel/petrinaut/vercel-build.sh +++ b/apps/petrinaut-website/vercel-build.sh @@ -5,7 +5,7 @@ set -euo pipefail eval "$(mise activate bash --shims)" echo "Changing dir to root" -cd ../../.. +cd ../.. # TODO: Mise is picking up `.env` files. We need to overhaul our approach for # environment variables. To avoid this in the meantime, we'll remove the @@ -15,5 +15,5 @@ cd ../../.. # See: https://linear.app/hash/issue/H-3212/clean-up-env-files rm .env -echo "Building Petrinaut" -turbo build:site --filter='@hashintel/petrinaut' --env-mode=loose +echo "Building Petrinaut website" +turbo build --filter='@apps/petrinaut-website' --env-mode=loose diff --git a/libs/@hashintel/petrinaut/vercel-install.sh b/apps/petrinaut-website/vercel-install.sh similarity index 62% rename from libs/@hashintel/petrinaut/vercel-install.sh rename to apps/petrinaut-website/vercel-install.sh index b26528f6985..90c53544c46 100755 --- a/libs/@hashintel/petrinaut/vercel-install.sh +++ b/apps/petrinaut-website/vercel-install.sh @@ -3,7 +3,7 @@ set -euo pipefail echo "Changing dir to root" -cd ../../.. +cd ../.. echo "updating certificates" yum update ca-certificates -y @@ -27,22 +27,5 @@ mise list rust rustc --version cargo --version - -# TODO: investigate why producing a pruned repo results in a broken Vercel build -# update: Probably due to missing `patches/` folder, needs investigation - -#echo "Producing pruned repo" -#turbo prune --scope='@apps/hash-frontend' -# -#echo "Deleting contents of non-pruned dir to save space" -#git ls-files -z | xargs -0 rm -f -#git ls-tree --name-only -d -r -z HEAD | sort -rz | xargs -0 rm -rf -# -#echo "Moving pruned repo back to root" -#mv out/* . -#rm out -r - -# Install the pruned dependencies - echo "Installing yarn dependencies" LEFTHOOK=0 yarn install --immutable diff --git a/libs/@hashintel/petrinaut/vercel.json b/apps/petrinaut-website/vercel.json similarity index 79% rename from libs/@hashintel/petrinaut/vercel.json rename to apps/petrinaut-website/vercel.json index 2743c9ae994..b32d74b2e53 100644 --- a/libs/@hashintel/petrinaut/vercel.json +++ b/apps/petrinaut-website/vercel.json @@ -5,5 +5,5 @@ }, "buildCommand": "./vercel-build.sh", "installCommand": "./vercel-install.sh", - "outputDirectory": "./demo-site/dist" + "outputDirectory": "./dist" } diff --git a/libs/@hashintel/petrinaut/vite.site.config.ts b/apps/petrinaut-website/vite.config.ts similarity index 72% rename from libs/@hashintel/petrinaut/vite.site.config.ts rename to apps/petrinaut-website/vite.config.ts index 324ba33585e..e5814c9b46a 100644 --- a/libs/@hashintel/petrinaut/vite.site.config.ts +++ b/apps/petrinaut-website/vite.config.ts @@ -1,20 +1,15 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; -/** Demo site dev server and production build config. */ +/** Petrinaut website dev server and production build config. */ export default defineConfig(() => { const environment = process.env.VITE_VERCEL_ENV ?? "development"; const sentryDsn: string | undefined = process.env.SENTRY_DSN; return { - root: "demo-site", - define: { __ENVIRONMENT__: JSON.stringify(environment), __SENTRY_DSN__: JSON.stringify(sentryDsn), - - // This part could be in the library config - "process.versions": JSON.stringify({ pnp: undefined }), }, plugins: [ diff --git a/libs/@hashintel/petrinaut/demo-site/favicon.ico b/libs/@hashintel/petrinaut/demo-site/favicon.ico deleted file mode 100644 index 4965832f2c9..00000000000 Binary files a/libs/@hashintel/petrinaut/demo-site/favicon.ico and /dev/null differ diff --git a/libs/@hashintel/petrinaut/demo-site/tsconfig.json b/libs/@hashintel/petrinaut/demo-site/tsconfig.json deleted file mode 100644 index be04a920173..00000000000 --- a/libs/@hashintel/petrinaut/demo-site/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["./*.tsx", "./*.ts", "./*.d.ts", "main/app.tsx", "sentry/*.ts"] -} diff --git a/libs/@hashintel/petrinaut/eslint.config.js b/libs/@hashintel/petrinaut/eslint.config.js index d9e2693ad7c..b030e11bed3 100644 --- a/libs/@hashintel/petrinaut/eslint.config.js +++ b/libs/@hashintel/petrinaut/eslint.config.js @@ -10,7 +10,6 @@ export default [ "panda.config.ts", "postcss.config.cjs", "vite.config.ts", - "vite.site.config.ts", ".storybook/main.ts", ".storybook/manager.tsx", ".storybook/preview.tsx", @@ -20,23 +19,6 @@ export default [ }, }, }, - { - files: ["demo-site/**/*"], - languageOptions: { - parserOptions: { - projectService: { - defaultProject: "./demo-site/tsconfig.json", - }, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - files: ["demo-site/**/*.tsx"], - rules: { - "import/no-extraneous-dependencies": ["error", { devDependencies: true }], - }, - }, { rules: { // Disabled because React Compiler handles optimization automatically diff --git a/libs/@hashintel/petrinaut/package.json b/libs/@hashintel/petrinaut/package.json index 7a633752ba0..d605562371d 100644 --- a/libs/@hashintel/petrinaut/package.json +++ b/libs/@hashintel/petrinaut/package.json @@ -20,16 +20,13 @@ ], "type": "module", "sideEffects": [ - "demo-site/**/*.ts" + "*.css" ], "main": "dist/main.js", "types": "dist/main.d.ts", "scripts": { "build": "vite build", - "build:site": "vite build --config vite.site.config.ts", - "dev": "vite --config vite.site.config.ts", - "storybook:dev": "storybook dev", - "storybook:build": "storybook build --output-dir build/storybook", + "dev": "storybook dev", "fix:eslint": "eslint --fix .", "lint:eslint": "eslint --report-unused-disable-directives .", "lint:tsc": "tsgo --noEmit", @@ -66,7 +63,6 @@ "@hashintel/ds-helpers": "workspace:*", "@local/eslint": "workspace:*", "@pandacss/dev": "1.4.3", - "@sentry/react": "10.22.0", "@storybook/react-vite": "10.2.13", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", @@ -79,7 +75,6 @@ "@vitejs/plugin-react": "5.1.4", "babel-plugin-react-compiler": "1.0.0", "eslint": "9.39.3", - "immer": "10.1.3", "jsdom": "24.1.3", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/libs/@hashintel/petrinaut/src/lsp/worker/use-language-client.ts b/libs/@hashintel/petrinaut/src/lsp/worker/use-language-client.ts index b08b9aad72e..d9d17f4a02d 100644 --- a/libs/@hashintel/petrinaut/src/lsp/worker/use-language-client.ts +++ b/libs/@hashintel/petrinaut/src/lsp/worker/use-language-client.ts @@ -12,6 +12,15 @@ import type { SignatureHelp, } from "./protocol"; +/** Dynamically import and instantiate the language server worker (inlined as blob URL). */ +async function createLanguageServerWorker() { + const LanguageServerWorker = await import( + "./language-server.worker.ts?worker&inline" + ); + // eslint-disable-next-line new-cap + return new LanguageServerWorker.default(); +} + type Pending = { resolve: (result: never) => void; reject: (error: Error) => void; @@ -51,45 +60,59 @@ export function useLanguageClient(): LanguageClientApi { const workerRef = useRef(null); const pendingRef = useRef(new Map()); const nextId = useRef(0); + const queueRef = useRef([]); const diagnosticsCallbackRef = useRef< ((params: PublishDiagnosticsParams[]) => void) | null >(null); useEffect(() => { - const worker = new Worker( - new URL("./language-server.worker.ts", import.meta.url), - { - type: "module", - }, - ); - - worker.onmessage = (event: MessageEvent) => { - const msg = event.data; - - if ("id" in msg) { - // Response to a request - const pending = pendingRef.current.get(msg.id); - if (!pending) { - return; - } - pendingRef.current.delete(msg.id); - - if ("error" in msg) { - pending.reject(new Error(msg.error.message)); - } else { - pending.resolve(msg.result as never); - } - } else if ("method" in msg) { - // Server-pushed notification - diagnosticsCallbackRef.current?.(msg.params); + let terminated = false; + + void createLanguageServerWorker().then((worker) => { + if (terminated) { + worker.terminate(); + return; } - }; - workerRef.current = worker; + worker.addEventListener( + "message", + (event: MessageEvent) => { + const msg = event.data; + + if ("id" in msg) { + // Response to a request + const pending = pendingRef.current.get(msg.id); + if (!pending) { + return; + } + pendingRef.current.delete(msg.id); + + if ("error" in msg) { + pending.reject(new Error(msg.error.message)); + } else { + pending.resolve(msg.result as never); + } + } else if ("method" in msg) { + // Server-pushed notification + diagnosticsCallbackRef.current?.(msg.params); + } + }, + ); + + workerRef.current = worker; + + // Drain any messages queued before the worker was ready + for (const message of queueRef.current) { + worker.postMessage(message); + } + queueRef.current = []; + }); + const pending = pendingRef.current; return () => { - worker.terminate(); + terminated = true; + workerRef.current?.terminate(); workerRef.current = null; for (const entry of pending.values()) { entry.reject(new Error("Worker terminated")); @@ -101,7 +124,12 @@ export function useLanguageClient(): LanguageClientApi { // --- Notifications (fire-and-forget) --- const sendNotification = useCallback((message: Omit) => { - workerRef.current?.postMessage(message); + const worker = workerRef.current; + if (worker) { + worker.postMessage(message); + } else { + queueRef.current.push(message); + } }, []); const initialize = useCallback( @@ -140,17 +168,17 @@ export function useLanguageClient(): LanguageClientApi { // --- Requests (return Promise) --- const sendRequest = useCallback((message: ClientMessage): Promise => { - const worker = workerRef.current; - if (!worker) { - return Promise.reject(new Error("Worker not initialized")); - } - return new Promise((resolve, reject) => { pendingRef.current.set((message as { id: number }).id, { resolve: resolve as (result: never) => void, reject, }); - worker.postMessage(message); + const worker = workerRef.current; + if (worker) { + worker.postMessage(message); + } else { + queueRef.current.push(message); + } }); }, []); diff --git a/libs/@hashintel/petrinaut/src/main.ts b/libs/@hashintel/petrinaut/src/main.ts index fa5b406eb94..e23a3da809e 100644 --- a/libs/@hashintel/petrinaut/src/main.ts +++ b/libs/@hashintel/petrinaut/src/main.ts @@ -1 +1,8 @@ +export type { ErrorTracker } from "./error-tracker/error-tracker.context"; +export { ErrorTrackerContext } from "./error-tracker/error-tracker.context"; +export type { OldFormat } from "./old-formats/convert-old-format"; +export { + convertOldFormatToSDCPN, + isOldFormat, +} from "./old-formats/convert-old-format"; export * from "./petrinaut"; diff --git a/libs/@hashintel/petrinaut/src/monaco/completion-sync.tsx b/libs/@hashintel/petrinaut/src/monaco/completion-sync.tsx index b71da0d9deb..a520751db3a 100644 --- a/libs/@hashintel/petrinaut/src/monaco/completion-sync.tsx +++ b/libs/@hashintel/petrinaut/src/monaco/completion-sync.tsx @@ -1,4 +1,4 @@ -import type * as Monaco from "monaco-editor"; +import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { Suspense, use, useEffect } from "react"; import type { CompletionItem } from "vscode-languageserver-types"; import { CompletionItemKind, Position } from "vscode-languageserver-types"; diff --git a/libs/@hashintel/petrinaut/src/monaco/context.ts b/libs/@hashintel/petrinaut/src/monaco/context.ts index 3075ef10660..ab48ffe022c 100644 --- a/libs/@hashintel/petrinaut/src/monaco/context.ts +++ b/libs/@hashintel/petrinaut/src/monaco/context.ts @@ -1,5 +1,5 @@ import type { EditorProps } from "@monaco-editor/react"; -import type * as Monaco from "monaco-editor"; +import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { createContext } from "react"; export type MonacoContextValue = { diff --git a/libs/@hashintel/petrinaut/src/monaco/diagnostics-sync.tsx b/libs/@hashintel/petrinaut/src/monaco/diagnostics-sync.tsx index 027e4c1744f..c1dddfbbd22 100644 --- a/libs/@hashintel/petrinaut/src/monaco/diagnostics-sync.tsx +++ b/libs/@hashintel/petrinaut/src/monaco/diagnostics-sync.tsx @@ -1,4 +1,4 @@ -import type * as Monaco from "monaco-editor"; +import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { Suspense, use, useEffect, useRef } from "react"; import type { Diagnostic } from "vscode-languageserver-types"; import { DiagnosticSeverity } from "vscode-languageserver-types"; diff --git a/libs/@hashintel/petrinaut/src/monaco/hover-sync.tsx b/libs/@hashintel/petrinaut/src/monaco/hover-sync.tsx index 5fd454b65a1..aff2e3077bc 100644 --- a/libs/@hashintel/petrinaut/src/monaco/hover-sync.tsx +++ b/libs/@hashintel/petrinaut/src/monaco/hover-sync.tsx @@ -1,4 +1,4 @@ -import type * as Monaco from "monaco-editor"; +import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { Suspense, use, useEffect } from "react"; import type { Hover } from "vscode-languageserver-types"; import { MarkupKind, Position } from "vscode-languageserver-types"; diff --git a/libs/@hashintel/petrinaut/src/monaco/provider.tsx b/libs/@hashintel/petrinaut/src/monaco/provider.tsx index e36ac9f819b..0eff5ff7995 100644 --- a/libs/@hashintel/petrinaut/src/monaco/provider.tsx +++ b/libs/@hashintel/petrinaut/src/monaco/provider.tsx @@ -1,5 +1,3 @@ -import type * as Monaco from "monaco-editor"; - import { CompletionSync } from "./completion-sync"; import type { MonacoContextValue } from "./context"; import { MonacoContext } from "./context"; @@ -7,44 +5,6 @@ import { DiagnosticsSync } from "./diagnostics-sync"; import { HoverSync } from "./hover-sync"; import { SignatureHelpSync } from "./signature-help-sync"; -interface LanguageDefaults { - setModeConfiguration(config: Record): void; -} - -interface TypeScriptNamespace { - typescriptDefaults: LanguageDefaults; - javascriptDefaults: LanguageDefaults; -} - -/** - * Disable all built-in TypeScript language worker features. - * Syntax highlighting (Monarch tokenizer) is retained since it runs client-side. - */ -function disableBuiltInTypeScriptFeatures(monaco: typeof Monaco) { - // The `typescript` namespace is marked deprecated in newer type definitions - // but the runtime API still exists and is the only way to control the TS worker. - const ts = monaco.languages.typescript as unknown as TypeScriptNamespace; - - const modeConfiguration: Record = { - completionItems: false, - hovers: false, - documentSymbols: false, - definitions: false, - references: false, - documentHighlights: false, - rename: false, - diagnostics: false, - documentRangeFormattingEdits: false, - signatureHelp: false, - onTypeFormattingEdits: false, - codeActions: false, - inlayHints: false, - }; - - ts.typescriptDefaults.setModeConfiguration(modeConfiguration); - ts.javascriptDefaults.setModeConfiguration(modeConfiguration); -} - async function initMonaco(): Promise { // Disable all workers — no worker files will be shipped or loaded. (globalThis as Record).MonacoEnvironment = { @@ -52,14 +12,29 @@ async function initMonaco(): Promise { }; const [monaco, monacoReact] = await Promise.all([ - import("monaco-editor") as Promise, + import("monaco-editor/esm/vs/editor/editor.api.js"), import("@monaco-editor/react"), + // Import the TypeScript contribution to enable TypeScript language features. (side-effect) + // This does not import the TypeScript worker, unnecessary given our custom LSP provides the same functionality. + import( + "monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution.js" + ), ]); + window.MonacoEnvironment = { + getWorker() { + return new Worker( + new URL( + "monaco-editor/esm/vs/editor/editor.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + }, + }; + // Use local Monaco instance — no CDN fetch. monacoReact.loader.config({ monaco }); - - disableBuiltInTypeScriptFeatures(monaco); return { monaco, Editor: monacoReact.default }; } diff --git a/libs/@hashintel/petrinaut/src/monaco/signature-help-sync.tsx b/libs/@hashintel/petrinaut/src/monaco/signature-help-sync.tsx index ad151ed3abc..f7e1e292593 100644 --- a/libs/@hashintel/petrinaut/src/monaco/signature-help-sync.tsx +++ b/libs/@hashintel/petrinaut/src/monaco/signature-help-sync.tsx @@ -1,4 +1,4 @@ -import type * as Monaco from "monaco-editor"; +import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { Suspense, use, useEffect } from "react"; import { type MarkupContent, diff --git a/libs/@hashintel/petrinaut/src/simulation/worker/create-simulation-worker.ts b/libs/@hashintel/petrinaut/src/simulation/worker/create-simulation-worker.ts new file mode 100644 index 00000000000..df03ce84a0b --- /dev/null +++ b/libs/@hashintel/petrinaut/src/simulation/worker/create-simulation-worker.ts @@ -0,0 +1,6 @@ +/** Dynamically import and instantiate the simulation worker (inlined as blob URL). */ +export async function createSimulationWorker(): Promise { + const SimulationWorker = await import("./simulation.worker.ts?worker&inline"); + // eslint-disable-next-line new-cap + return new SimulationWorker.default(); +} diff --git a/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.test.ts b/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.test.ts index b09c6ffc94b..4eeec567166 100644 --- a/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.test.ts +++ b/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.test.ts @@ -7,18 +7,31 @@ */ import { act, renderHook } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SDCPN } from "../../core/types/sdcpn"; import type { ToMainMessage, ToWorkerMessage } from "./messages"; import { useSimulationWorker } from "./use-simulation-worker"; -// Mock Worker +// Mock Worker with addEventListener-based API class MockWorker { - onmessage: ((event: MessageEvent) => void) | null = null; - onerror: ((event: ErrorEvent) => void) | null = null; + private messageListeners: ((event: MessageEvent) => void)[] = + []; + + private errorListeners: ((event: ErrorEvent) => void)[] = []; + postedMessages: ToWorkerMessage[] = []; + addEventListener(type: string, listener: (event: never) => void): void { + if (type === "message") { + this.messageListeners.push( + listener as (event: MessageEvent) => void, + ); + } else if (type === "error") { + this.errorListeners.push(listener as (event: ErrorEvent) => void); + } + } + postMessage(message: ToWorkerMessage): void { this.postedMessages.push(message); } @@ -27,26 +40,24 @@ class MockWorker { // No-op } - // Helper to simulate worker sending a message back simulateMessage(message: ToMainMessage): void { - if (this.onmessage) { - this.onmessage({ data: message } as MessageEvent); + const event = { data: message } as MessageEvent; + for (const listener of this.messageListeners) { + listener(event); } } - // Helper to simulate worker error simulateError(message: string): void { - if (this.onerror) { - this.onerror({ message } as ErrorEvent); + const event = { message } as ErrorEvent; + for (const listener of this.errorListeners) { + listener(event); } } - // Helper to get last posted message getLastMessage(): ToWorkerMessage | undefined { return this.postedMessages[this.postedMessages.length - 1]; } - // Helper to get messages of a specific type getMessages( type: T, ): Extract[] { @@ -55,7 +66,6 @@ class MockWorker { ); } - // Helper to clear messages clearMessages(): void { this.postedMessages = []; } @@ -64,39 +74,22 @@ class MockWorker { // Store the mock worker instance for access in tests let mockWorkerInstance: MockWorker | null = null; -// Mock the Worker constructor -vi.stubGlobal( - "Worker", - class { - onmessage: ((event: MessageEvent) => void) | null = null; - onerror: ((event: ErrorEvent) => void) | null = null; - - constructor() { - mockWorkerInstance = new MockWorker(); - // Proxy onmessage/onerror to the mock instance - Object.defineProperty(this, "onmessage", { - get: () => mockWorkerInstance!.onmessage, - set: (fn: MockWorker["onmessage"]) => { - mockWorkerInstance!.onmessage = fn; - }, - }); - Object.defineProperty(this, "onerror", { - get: () => mockWorkerInstance!.onerror, - set: (fn: MockWorker["onerror"]) => { - mockWorkerInstance!.onerror = fn; - }, - }); - } - - postMessage(message: ToWorkerMessage): void { - mockWorkerInstance?.postMessage(message); - } - - terminate(): void { - mockWorkerInstance?.terminate(); - } +// Mock the extracted createSimulationWorker module so the dynamic import +// (with ?worker&inline) never runs. Returns a MockWorker synchronously +// via a resolved promise, eliminating all async timing issues. +vi.mock("./create-simulation-worker", () => ({ + createSimulationWorker: () => { + mockWorkerInstance = new MockWorker(); + return Promise.resolve(mockWorkerInstance); }, -); +})); + +// Flush the resolved Promise.then() callback from createSimulationWorker. +// The promise resolves synchronously (our mock), but the .then() in the +// useEffect still runs as a microtask — one await act() is sufficient. +async function flushMicrotasks() { + await act(async () => {}); +} // Helper to create a minimal valid SDCPN function createMinimalSDCPN(): SDCPN { @@ -129,33 +122,32 @@ function createMinimalSDCPN(): SDCPN { describe("useSimulationWorker", () => { beforeEach(() => { - vi.useFakeTimers(); mockWorkerInstance = null; }); - afterEach(() => { - vi.useRealTimers(); - }); - describe("initial state", () => { - it("starts with idle status", () => { + it("starts with idle status", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); expect(result.current.state.status).toBe("idle"); expect(result.current.state.frames).toEqual([]); expect(result.current.state.error).toBeNull(); }); - it("creates worker on mount", () => { + it("creates worker on mount", async () => { renderHook(() => useSimulationWorker()); + await flushMicrotasks(); expect(mockWorkerInstance).not.toBeNull(); }); }); describe("initialize action", () => { - it("sends init message to worker", () => { + it("sends init message to worker", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); + const sdcpn = createMinimalSDCPN(); const initialMarking = new Map([ ["p1", { values: new Float64Array([1.0]), count: 1 }], @@ -182,8 +174,10 @@ describe("useSimulationWorker", () => { expect(initMessages[0]?.maxTime).toBe(100); }); - it("serializes initialMarking Map to array", () => { + it("serializes initialMarking Map to array", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); + const sdcpn = createMinimalSDCPN(); const initialMarking = new Map([ ["p1", { values: new Float64Array([1.0, 2.0]), count: 2 }], @@ -206,8 +200,9 @@ describe("useSimulationWorker", () => { expect(initMessages[0]?.initialMarking[0]?.[0]).toBe("p1"); }); - it("clears frames on initialize", () => { + it("clears frames on initialize", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); // Simulate having some frames act(() => { @@ -241,8 +236,9 @@ describe("useSimulationWorker", () => { }); describe("start action", () => { - it("sends start message and updates status", () => { + it("sends start message and updates status", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.start(); @@ -254,8 +250,9 @@ describe("useSimulationWorker", () => { }); describe("pause action", () => { - it("sends pause message", () => { + it("sends pause message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.pause(); @@ -266,15 +263,14 @@ describe("useSimulationWorker", () => { }); describe("stop action", () => { - it("sends stop message and resets state", () => { + it("sends stop message and resets state", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); - // Start first act(() => { result.current.actions.start(); }); - // Stop act(() => { result.current.actions.stop(); }); @@ -286,8 +282,9 @@ describe("useSimulationWorker", () => { }); describe("setBackpressure action", () => { - it("sends setBackpressure message with maxFramesAhead", () => { + it("sends setBackpressure message with maxFramesAhead", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.setBackpressure({ maxFramesAhead: 50000 }); @@ -298,8 +295,9 @@ describe("useSimulationWorker", () => { expect(messages[0]?.maxFramesAhead).toBe(50000); }); - it("sends setBackpressure message with batchSize", () => { + it("sends setBackpressure message with batchSize", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.setBackpressure({ batchSize: 500 }); @@ -312,10 +310,10 @@ describe("useSimulationWorker", () => { }); describe("reset action", () => { - it("sends stop message and resets state", () => { + it("sends stop message and resets state", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); - // Add some state act(() => { result.current.actions.start(); mockWorkerInstance!.simulateMessage({ @@ -329,7 +327,6 @@ describe("useSimulationWorker", () => { }); }); - // Reset act(() => { result.current.actions.reset(); }); @@ -341,10 +338,10 @@ describe("useSimulationWorker", () => { }); describe("message handling", () => { - it("handles ready message", () => { + it("handles ready message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); - // Set status to initializing first act(() => { void result.current.actions.initialize({ sdcpn: createMinimalSDCPN(), @@ -358,7 +355,6 @@ describe("useSimulationWorker", () => { expect(result.current.state.status).toBe("initializing"); - // Simulate ready message act(() => { mockWorkerInstance!.simulateMessage({ type: "ready", @@ -369,8 +365,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.status).toBe("ready"); }); - it("handles frame message", () => { + it("handles frame message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); const frame = { time: 1.5, @@ -389,8 +386,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.frames[0]?.time).toBe(1.5); }); - it("handles frames (batch) message", () => { + it("handles frames (batch) message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); const frames = [ { time: 1, places: {}, transitions: {}, buffer: new Float64Array() }, @@ -407,8 +405,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.frames[2]?.time).toBe(3); }); - it("handles complete message", () => { + it("handles complete message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.start(); @@ -425,8 +424,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.status).toBe("complete"); }); - it("handles paused message", () => { + it("handles paused message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { result.current.actions.start(); @@ -442,8 +442,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.status).toBe("paused"); }); - it("handles error message", () => { + it("handles error message", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { mockWorkerInstance!.simulateMessage({ @@ -458,8 +459,9 @@ describe("useSimulationWorker", () => { expect(result.current.state.errorItemId).toBe("item123"); }); - it("handles worker onerror", () => { + it("handles worker onerror", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); act(() => { mockWorkerInstance!.simulateError("Worker crashed"); @@ -471,12 +473,12 @@ describe("useSimulationWorker", () => { }); describe("backpressure (ack)", () => { - it("sends ack message when ack action is called", () => { + it("sends ack message when ack action is called", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); mockWorkerInstance!.clearMessages(); - // Call ack action with frame number act(() => { result.current.actions.ack(42); }); @@ -486,8 +488,9 @@ describe("useSimulationWorker", () => { expect(ackMessages[0]?.frameNumber).toBe(42); }); - it("sends multiple ack messages with different frame numbers", () => { + it("sends multiple ack messages with different frame numbers", async () => { const { result } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); mockWorkerInstance!.clearMessages(); @@ -506,10 +509,11 @@ describe("useSimulationWorker", () => { }); describe("cleanup", () => { - it("terminates worker on unmount", () => { + it("terminates worker on unmount", async () => { const terminateSpy = vi.fn(); const { unmount } = renderHook(() => useSimulationWorker()); + await flushMicrotasks(); // Replace terminate with spy mockWorkerInstance!.terminate = terminateSpy; diff --git a/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.ts b/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.ts index 7bb4d86b847..85c83e6303d 100644 --- a/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.ts +++ b/libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.ts @@ -14,6 +14,7 @@ import { useEffect, useRef, useState } from "react"; import type { SDCPN } from "../../core/types/sdcpn"; import type { InitialMarking, SimulationFrame } from "../context"; +import { createSimulationWorker } from "./create-simulation-worker"; import type { ToMainMessage, ToWorkerMessage } from "./messages"; /** @@ -127,84 +128,97 @@ export function useSimulationWorker(): { // Initialize worker on mount useEffect(() => { - const worker = new Worker( - new URL("./simulation.worker.ts", import.meta.url), - { type: "module" }, - ); - - worker.onmessage = (event: MessageEvent) => { - const message = event.data; - - switch (message.type) { - case "ready": - setState((prev) => ({ - ...prev, - status: prev.status === "initializing" ? "ready" : prev.status, - })); - // Resolve pending initialization promise - if (pendingInitRef.current) { - pendingInitRef.current.resolve(); - pendingInitRef.current = null; - } - break; - - case "frame": - setState((prev) => ({ - ...prev, - frames: [...prev.frames, message.frame], - })); - break; - - case "frames": - setState((prev) => ({ - ...prev, - frames: [...prev.frames, ...message.frames], - })); - break; - - case "complete": - setState((prev) => ({ - ...prev, - status: "complete", - })); - break; - - case "paused": - setState((prev) => ({ - ...prev, - status: "paused", - })); - break; - - case "error": - setState((prev) => ({ - ...prev, - status: "error", - error: message.message, - errorItemId: message.itemId, - })); - // Reject pending initialization promise if this error occurred during init - if (pendingInitRef.current) { - pendingInitRef.current.reject(new Error(message.message)); - pendingInitRef.current = null; - } - break; - } - }; + let terminated = false; - worker.onerror = (error) => { - setState((prev) => ({ - ...prev, - status: "error", - error: error.message || "Worker error", - errorItemId: null, - })); - }; + void createSimulationWorker().then((worker) => { + if (terminated) { + worker.terminate(); + return; + } - workerRef.current = worker; + worker.addEventListener( + "message", + (event: MessageEvent) => { + const message = event.data; + + switch (message.type) { + case "ready": + setState((prev) => ({ + ...prev, + status: prev.status === "initializing" ? "ready" : prev.status, + })); + // Resolve pending initialization promise + if (pendingInitRef.current) { + pendingInitRef.current.resolve(); + pendingInitRef.current = null; + } + break; + + case "frame": + setState((prev) => ({ + ...prev, + frames: [...prev.frames, message.frame], + })); + break; + + case "frames": + setState((prev) => ({ + ...prev, + frames: [...prev.frames, ...message.frames], + })); + break; + + case "complete": + setState((prev) => ({ + ...prev, + status: "complete", + })); + break; + + case "paused": + setState((prev) => ({ + ...prev, + status: "paused", + })); + break; + + case "error": + setState((prev) => ({ + ...prev, + status: "error", + error: message.message, + errorItemId: message.itemId, + })); + // Reject pending initialization promise if this error occurred during init + if (pendingInitRef.current) { + pendingInitRef.current.reject(new Error(message.message)); + pendingInitRef.current = null; + } + break; + } + }, + ); + + worker.addEventListener("error", (error) => { + setState((prev) => ({ + ...prev, + status: "error", + error: error.message || "Worker error", + errorItemId: null, + })); + }); + + workerRef.current = worker; + }); return () => { - worker.terminate(); + terminated = true; + workerRef.current?.terminate(); + // Reject any pending initialization promise on teardown + if (pendingInitRef.current) { + pendingInitRef.current.reject(new Error("Worker terminated")); + pendingInitRef.current = null; + } }; }, []); diff --git a/libs/@hashintel/petrinaut/tsconfig.json b/libs/@hashintel/petrinaut/tsconfig.json index 10517ba660d..f391b98905b 100644 --- a/libs/@hashintel/petrinaut/tsconfig.json +++ b/libs/@hashintel/petrinaut/tsconfig.json @@ -15,6 +15,5 @@ "skipLibCheck": true, "isolatedModules": true }, - "include": ["src", "demo-site", ".storybook"], - "exclude": ["demo-site/dist/**/*"] + "include": ["src", ".storybook"] } diff --git a/libs/@hashintel/petrinaut/turbo.json b/libs/@hashintel/petrinaut/turbo.json index c634f43c34b..6d9a1d1f9e5 100644 --- a/libs/@hashintel/petrinaut/turbo.json +++ b/libs/@hashintel/petrinaut/turbo.json @@ -5,11 +5,6 @@ "dependsOn": ["^build"], "outputs": ["dist/**"], "cache": false - }, - "build:site": { - "dependsOn": ["^build"], - "outputs": ["dist/demo-site/**"], - "cache": false } } } diff --git a/libs/@hashintel/petrinaut/vite.config.ts b/libs/@hashintel/petrinaut/vite.config.ts index aa0bde0363a..aa305fc5e9c 100644 --- a/libs/@hashintel/petrinaut/vite.config.ts +++ b/libs/@hashintel/petrinaut/vite.config.ts @@ -20,7 +20,6 @@ export default defineConfig(({ command }) => ({ "react", "react-dom", "@xyflow/react", - "monaco-editor", "@babel/standalone", ], output: { @@ -39,6 +38,10 @@ export default defineConfig(({ command }) => ({ cssMinify: false, }, + define: { + "process.versions": JSON.stringify({ pnp: undefined }), + }, + worker: { plugins: () => [ replacePlugin({ diff --git a/libs/@hashintel/refractive/package.json b/libs/@hashintel/refractive/package.json index 9ebc2872c2f..b5683350a06 100644 --- a/libs/@hashintel/refractive/package.json +++ b/libs/@hashintel/refractive/package.json @@ -30,9 +30,6 @@ "lint:tsc": "tsc --noEmit", "prepublishOnly": "yarn build" }, - "dependencies": { - "canvas": "3.2.0" - }, "devDependencies": { "@local/eslint": "workspace:*", "@local/tsconfig": "workspace:*", diff --git a/libs/@hashintel/refractive/src/components/composite-parts.tsx b/libs/@hashintel/refractive/src/components/composite-parts.tsx index c1f53ff3bf9..d8c19ddb07a 100644 --- a/libs/@hashintel/refractive/src/components/composite-parts.tsx +++ b/libs/@hashintel/refractive/src/components/composite-parts.tsx @@ -1,5 +1,3 @@ -import type { ImageData } from "canvas"; - import { splitImageDataToParts } from "../helpers/split-imagedata-to-parts"; type CompositePartsProps = { diff --git a/libs/@hashintel/refractive/src/helpers/image-data-to-url.ts b/libs/@hashintel/refractive/src/helpers/image-data-to-url.ts index 51e175593b8..bcdb2de27b8 100644 --- a/libs/@hashintel/refractive/src/helpers/image-data-to-url.ts +++ b/libs/@hashintel/refractive/src/helpers/image-data-to-url.ts @@ -1,5 +1,3 @@ -import { createCanvas, type ImageData } from "canvas"; - export function imageDataToUrl( imageData: ImageData, width?: number, @@ -7,11 +5,10 @@ export function imageDataToUrl( x = 0, y = 0, ): string { - const canvas = createCanvas( - width ?? imageData.width, - height ?? imageData.height, - ); - const ctx = canvas.getContext("2d"); + const canvas = document.createElement("canvas"); + canvas.width = width ?? imageData.width; + canvas.height = height ?? imageData.height; + const ctx = canvas.getContext("2d")!; ctx.putImageData(imageData, -x, -y); return canvas.toDataURL(); } diff --git a/libs/@hashintel/refractive/src/helpers/split-imagedata-to-parts.ts b/libs/@hashintel/refractive/src/helpers/split-imagedata-to-parts.ts index d71671c82b8..ccec4bea253 100644 --- a/libs/@hashintel/refractive/src/helpers/split-imagedata-to-parts.ts +++ b/libs/@hashintel/refractive/src/helpers/split-imagedata-to-parts.ts @@ -1,5 +1,3 @@ -import type { ImageData } from "canvas"; - import { imageDataToUrl } from "./image-data-to-url"; // Each part is a Base64-encoded PNG image diff --git a/libs/@hashintel/refractive/src/maps/calculate-circle-map.ts b/libs/@hashintel/refractive/src/maps/calculate-circle-map.ts index 1859d221976..a81561b6b22 100644 --- a/libs/@hashintel/refractive/src/maps/calculate-circle-map.ts +++ b/libs/@hashintel/refractive/src/maps/calculate-circle-map.ts @@ -1,5 +1,3 @@ -import { createImageData } from "canvas"; - import type { ProcessPixelFunction } from "./process-pixel.type"; /** @@ -19,7 +17,7 @@ export function calculateCircleMap(props: { const width = Math.round(props.width); const height = Math.round(props.height); - const imageData = createImageData(width, height); + const imageData = new ImageData(width, height); // Fill buffer with base color new Uint32Array(imageData.data.buffer).fill(fillColor); diff --git a/libs/@hashintel/refractive/src/maps/calculate-rounded-square-map.ts b/libs/@hashintel/refractive/src/maps/calculate-rounded-square-map.ts index 14339efed18..9d900a5e1c9 100644 --- a/libs/@hashintel/refractive/src/maps/calculate-rounded-square-map.ts +++ b/libs/@hashintel/refractive/src/maps/calculate-rounded-square-map.ts @@ -1,5 +1,3 @@ -import { createImageData } from "canvas"; - import { calculateCircleMap } from "./calculate-circle-map"; import type { ProcessPixelFunction } from "./process-pixel.type"; @@ -73,7 +71,7 @@ export function calculateRoundedSquareMap(props: { const width = Math.round(props.width); const height = Math.round(props.height); - const imageData = createImageData(width, height); + const imageData = new ImageData(width, height); // Fill buffer with base color new Uint32Array(imageData.data.buffer).fill(fillColor); diff --git a/libs/@hashintel/refractive/vite.config.ts b/libs/@hashintel/refractive/vite.config.ts index de083db97d5..d021effce48 100644 --- a/libs/@hashintel/refractive/vite.config.ts +++ b/libs/@hashintel/refractive/vite.config.ts @@ -6,7 +6,6 @@ import dts from "vite-plugin-dts"; // Dependencies that should not be bundled into the library const external = [ - "canvas", "react", "react-dom", "react/jsx-runtime", diff --git a/yarn.lock b/yarn.lock index 50575ae997e..877e2412c6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -776,6 +776,27 @@ __metadata: languageName: unknown linkType: soft +"@apps/petrinaut-website@workspace:apps/petrinaut-website": + version: 0.0.0-use.local + resolution: "@apps/petrinaut-website@workspace:apps/petrinaut-website" + dependencies: + "@hashintel/petrinaut": "workspace:*" + "@local/eslint": "workspace:*" + "@mantine/hooks": "npm:8.3.5" + "@sentry/react": "npm:10.22.0" + "@types/react": "npm:19.2.7" + "@types/react-dom": "npm:19.2.3" + "@typescript/native-preview": "npm:7.0.0-dev.20260309.1" + "@vitejs/plugin-react": "npm:5.1.4" + babel-plugin-react-compiler: "npm:1.0.0" + eslint: "npm:9.39.3" + immer: "npm:10.1.3" + react: "npm:19.2.3" + react-dom: "npm:19.2.3" + vite: "npm:8.0.0-beta.18" + languageName: unknown + linkType: soft + "@apps/plugin-browser@workspace:*, @apps/plugin-browser@workspace:apps/plugin-browser": version: 0.0.0-use.local resolution: "@apps/plugin-browser@workspace:apps/plugin-browser" @@ -7995,7 +8016,6 @@ __metadata: "@mantine/hooks": "npm:8.3.5" "@monaco-editor/react": "npm:4.8.0-rc.3" "@pandacss/dev": "npm:1.4.3" - "@sentry/react": "npm:10.22.0" "@storybook/react-vite": "npm:10.2.13" "@testing-library/dom": "npm:10.4.1" "@testing-library/react": "npm:16.3.2" @@ -8012,7 +8032,6 @@ __metadata: d3-scale: "npm:4.0.2" elkjs: "npm:0.11.0" eslint: "npm:9.39.3" - immer: "npm:10.1.3" jsdom: "npm:24.1.3" monaco-editor: "npm:0.55.1" react: "npm:19.2.3" @@ -8076,7 +8095,6 @@ __metadata: "@types/react-dom": "npm:19.2.3" "@vitejs/plugin-react": "npm:5.0.4" babel-plugin-react-compiler: "npm:1.0.0" - canvas: "npm:3.2.0" eslint: "npm:9.39.3" typescript: "npm:5.9.3" vite: "npm:7.1.11"