= ({
return;
}
+ const { calculateGraphLayout } = await import(
+ "../lib/calculate-graph-layout"
+ );
const positions = await calculateGraphLayout(sdcpn, dimensions);
guardedMutate((sdcpnToMutate) => {
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
index e862a215fd4..c927d0fbb12 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
@@ -3,15 +3,8 @@ import { use, useRef, useState } from "react";
import { Box } from "../../components/box";
import { Stack } from "../../components/stack";
-import { productionMachines } from "../../examples/broken-machines";
-import { deploymentPipelineSDCPN } from "../../examples/deployment-pipeline";
-import { satellitesSDCPN } from "../../examples/satellites";
-import { probabilisticSatellitesSDCPN } from "../../examples/satellites-launcher";
-import { sirModel } from "../../examples/sir-model";
-import { supplyChainStochasticSDCPN } from "../../examples/supply-chain-stochastic";
import { exportSDCPN } from "../../file-format/export-sdcpn";
import { importSDCPN } from "../../file-format/import-sdcpn";
-import { calculateGraphLayout } from "../../lib/calculate-graph-layout";
import { EditorContext } from "../../state/editor-context";
import { MutationContext } from "../../state/mutation-context";
import { PortalContainerContext } from "../../state/portal-container-context";
@@ -158,6 +151,16 @@ export const EditorView = ({
exportTikZ({ petriNetDefinition, title });
}
+ async function handleLoadExample(
+ loadExample: () => Promise<{
+ title: string;
+ petriNetDefinition: typeof petriNetDefinition;
+ }>,
+ ) {
+ createNewNet(await loadExample());
+ clearSelection();
+ }
+
async function handleImport() {
const result = await importSDCPN();
if (!result) {
@@ -176,6 +179,9 @@ export const EditorView = ({
// We must do this before createNewNet because after createNewNet triggers a
// re-render, the mutatePetriNetDefinition closure would be stale.
if (hadMissingPositions) {
+ const { calculateGraphLayout } = await import(
+ "../../lib/calculate-graph-layout"
+ );
const positions = await calculateGraphLayout(sdcpnToLoad, dims);
if (Object.keys(positions).length > 0) {
@@ -275,50 +281,61 @@ export const EditorView = ({
{
id: "load-example-supply-chain-stochastic",
label: "Probabilistic Supply Chain",
- onClick: () => {
- createNewNet(supplyChainStochasticSDCPN);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/supply-chain-stochastic"))
+ .supplyChainStochasticSDCPN,
+ ),
},
{
id: "load-example-satellites",
label: "Satellites",
- onClick: () => {
- createNewNet(satellitesSDCPN);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/satellites"))
+ .satellitesSDCPN,
+ ),
},
{
id: "load-example-probabilistic-satellites",
label: "Probabilistic Satellites Launcher",
- onClick: () => {
- createNewNet(probabilisticSatellitesSDCPN);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/satellites-launcher"))
+ .probabilisticSatellitesSDCPN,
+ ),
},
{
id: "load-example-production-machines",
label: "Production Machines",
- onClick: () => {
- createNewNet(productionMachines);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/broken-machines"))
+ .productionMachines,
+ ),
},
{
id: "load-example-sir-model",
label: "SIR Model",
- onClick: () => {
- createNewNet(sirModel);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/sir-model")).sirModel,
+ ),
},
{
id: "load-example-deployment-pipeline",
label: "Deployment Pipeline",
- onClick: () => {
- createNewNet(deploymentPipelineSDCPN);
- clearSelection();
- },
+ onClick: () =>
+ handleLoadExample(
+ async () =>
+ (await import("../../examples/deployment-pipeline"))
+ .deploymentPipelineSDCPN,
+ ),
},
],
},
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx
index bce67d0ee91..7d5c5c5fe63 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx
@@ -282,7 +282,7 @@ const TimelineViewPicker: React.FC = () => {
);
};
-const TimelineHeaderActions: React.FC = () => (
+export const TimelineHeaderActions: React.FC = () => (
@@ -1342,7 +1342,7 @@ const TimelineLegend: React.FC<{
// -- Main component -----------------------------------------------------------
-const SimulationTimelineContent: React.FC = () => {
+export const SimulationTimelineContent: React.FC = () => {
const { timelineChartType: chartType } = use(EditorContext);
const { totalFrames } = use(SimulationContext);
const { currentFrameIndex } = use(PlaybackContext);
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx
index a39409806db..640fbc38db3 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx
@@ -1,5 +1,5 @@
import { css } from "@hashintel/ds-helpers/css";
-import { use, useEffect, useMemo, useState } from "react";
+import { use, useEffect, useState } from "react";
import { TbDotsVertical, TbSparkles } from "react-icons/tb";
import { IconButton } from "../../../../../../../components/icon-button";
@@ -20,7 +20,7 @@ import {
import { CodeEditor } from "../../../../../../../monaco/code-editor";
import { PlaybackContext } from "../../../../../../../playback/context";
import { SimulationContext } from "../../../../../../../simulation/context";
-import { compileVisualizer } from "../../../../../../../simulation/simulator/compile-visualizer";
+import type { VisualizerComponent } from "../../../../../../../simulation/simulator/compile-visualizer";
import { EditorContext } from "../../../../../../../state/editor-context";
import { usePlacePropertiesContext } from "../../context";
import { VisualizerErrorBoundary } from "./visualizer-error-boundary";
@@ -86,18 +86,49 @@ const VisualizerPreview: React.FC = () => {
const { currentFrame, totalFrames } = use(PlaybackContext);
const defaultParameterValues = useDefaultParameterValues();
+ const [VisualizerComponent, setVisualizerComponent] =
+ useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
- const VisualizerComponent = useMemo(() => {
if (!place.visualizerCode) {
- return null;
- }
- try {
- return compileVisualizer(place.visualizerCode);
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error("Failed to compile visualizer code:", error);
- return null;
+ setVisualizerComponent(null);
+ return;
}
+
+ void import(
+ "../../../../../../../simulation/simulator/compile-visualizer"
+ ).then(
+ ({ compileVisualizer }) => {
+ if (cancelled) {
+ return;
+ }
+
+ try {
+ setVisualizerComponent(() =>
+ compileVisualizer(place.visualizerCode!),
+ );
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Failed to compile visualizer code:", error);
+ setVisualizerComponent(null);
+ }
+ },
+ (error: unknown) => {
+ if (cancelled) {
+ return;
+ }
+
+ // eslint-disable-next-line no-console
+ console.error("Failed to load visualizer compiler:", error);
+ setVisualizerComponent(null);
+ },
+ );
+
+ return () => {
+ cancelled = true;
+ };
}, [place.visualizerCode]);
if (!place.visualizerCode) {
diff --git a/libs/@hashintel/petrinaut/vite.config.test.ts b/libs/@hashintel/petrinaut/vite.config.test.ts
new file mode 100644
index 00000000000..dcf6694231f
--- /dev/null
+++ b/libs/@hashintel/petrinaut/vite.config.test.ts
@@ -0,0 +1,88 @@
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+
+import { describe, expect, it } from "vitest";
+
+import { isLibraryExternal, shouldApplyReactCompiler } from "./vite.config";
+
+describe("petrinaut package boundary", () => {
+ it("externalizes peer dependency subpaths as part of the library boundary", () => {
+ expect(isLibraryExternal("@hashintel/ds-components")).toBe(true);
+ expect(isLibraryExternal("@hashintel/ds-components/preset")).toBe(true);
+ expect(isLibraryExternal("@hashintel/ds-helpers")).toBe(true);
+ expect(isLibraryExternal("@hashintel/ds-helpers/css")).toBe(true);
+ expect(isLibraryExternal("@xyflow/react")).toBe(true);
+ expect(isLibraryExternal("@xyflow/react/dist/style.css")).toBe(true);
+ expect(isLibraryExternal("react")).toBe(true);
+ expect(isLibraryExternal("react/jsx-runtime")).toBe(true);
+ expect(isLibraryExternal("react-dom")).toBe(true);
+ expect(isLibraryExternal("react-dom/client")).toBe(true);
+ expect(isLibraryExternal("use-sync-external-store/shim/with-selector")).toBe(
+ true,
+ );
+ });
+
+ it("keeps local and ordinary dependency imports inside the library build", () => {
+ expect(isLibraryExternal("./src/main")).toBe(false);
+ expect(isLibraryExternal("fuzzysort")).toBe(false);
+ expect(isLibraryExternal("@ark-ui/react/select")).toBe(false);
+ });
+
+ it("declares explicit public exports for the entrypoint and stylesheet", async () => {
+ const packageJson = JSON.parse(
+ await readFile(path.join(import.meta.dirname, "package.json"), "utf8"),
+ ) as {
+ exports?: {
+ "."?: { types?: string; import?: string };
+ "./styles.css"?: string;
+ "./package.json"?: string;
+ };
+ main?: string;
+ style?: string;
+ types?: string;
+ };
+
+ expect(packageJson.main).toBe("dist/main.js");
+ expect(packageJson.types).toBe("dist/main.d.ts");
+ expect(packageJson.style).toBe("dist/main.css");
+ expect(packageJson.exports?.["."]).toEqual({
+ types: "./dist/main.d.ts",
+ import: "./dist/main.js",
+ });
+ expect(packageJson.exports?.["./styles.css"]).toBe("./dist/main.css");
+ expect(packageJson.exports?.["./package.json"]).toBe("./package.json");
+ });
+
+ it("limits React Compiler to Petrinaut source modules that import React", () => {
+ expect(
+ shouldApplyReactCompiler(
+ "/repo/libs/@hashintel/petrinaut/src/components/input.tsx",
+ 'import { useId } from "react";',
+ ),
+ ).toBe(true);
+ expect(
+ shouldApplyReactCompiler(
+ "/repo/libs/@hashintel/petrinaut/src/hooks/use-latest.ts",
+ 'import { useRef } from "react";',
+ ),
+ ).toBe(true);
+ expect(
+ shouldApplyReactCompiler(
+ "/repo/libs/@hashintel/petrinaut/src/simulation/worker/simulation.worker.ts",
+ 'import { compileSimulation } from "../simulator";',
+ ),
+ ).toBe(false);
+ expect(
+ shouldApplyReactCompiler(
+ "/repo/libs/@hashintel/petrinaut/src/simulation/simulator/build-simulation.ts",
+ 'import { compileUserCode } from "./compile-user-code";',
+ ),
+ ).toBe(false);
+ expect(
+ shouldApplyReactCompiler(
+ "/repo/libs/@hashintel/petrinaut/src/components/input.test.tsx",
+ 'import { render } from "@testing-library/react";',
+ ),
+ ).toBe(false);
+ });
+});
diff --git a/libs/@hashintel/petrinaut/vite.config.ts b/libs/@hashintel/petrinaut/vite.config.ts
index e79a660f250..f86ea0a7116 100644
--- a/libs/@hashintel/petrinaut/vite.config.ts
+++ b/libs/@hashintel/petrinaut/vite.config.ts
@@ -1,9 +1,63 @@
-import babel from "@rolldown/plugin-babel";
+import babel, { defineRolldownBabelPreset } from "@rolldown/plugin-babel";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import { replacePlugin } from "rolldown/plugins";
import { dts } from "rolldown-plugin-dts";
import { defineConfig, esmExternalRequirePlugin } from "vite";
+export const libraryExternalPatterns = [
+ /^@babel\/standalone$/,
+ /^@hashintel\/ds-components(\/.*)?$/,
+ /^@hashintel\/ds-helpers(\/.*)?$/,
+ /^@xyflow\/react(\/.*)?$/,
+ /^react(\/.*)?$/,
+ /^react-dom(\/.*)?$/,
+ /^use-sync-external-store(\/.*)?$/,
+];
+
+export function isLibraryExternal(id: string) {
+ return libraryExternalPatterns.some((pattern) => pattern.test(id));
+}
+
+const reactCompilerIdInclude = /[\\/]src[\\/].+\.[jt]sx?$/;
+const reactCompilerIdExclude = [
+ /[\\/]src[\\/].+\.stories\.[jt]sx?$/,
+ /[\\/]src[\\/].+\.test\.[jt]sx?$/,
+ /[\\/]src[\\/].+\.worker\.[jt]s$/,
+ /[\\/]src[\\/]simulation[\\/]worker[\\/]/,
+ /[\\/]src[\\/]lsp[\\/]worker[\\/]/,
+];
+const reactCompilerCodeInclude =
+ /(?=[\s\S]*(?:from\s+["']react(?:\/[^"']*)?["']|import\s+["']react(?:\/[^"']*)?["']))(?=[\s\S]*(?:\b[A-Z]|\buse))/;
+
+export function shouldApplyReactCompiler(id: string, code: string) {
+ return (
+ reactCompilerIdInclude.test(id) &&
+ !reactCompilerIdExclude.some((pattern) => pattern.test(id)) &&
+ reactCompilerCodeInclude.test(code)
+ );
+}
+
+const baseReactCompilerBabelPreset = reactCompilerPreset({
+ target: "19",
+ compilationMode: "infer",
+ // @ts-expect-error - panicThreshold is accepted at runtime
+ panicThreshold: "critical_errors",
+});
+
+const reactCompilerBabelPreset = defineRolldownBabelPreset({
+ ...baseReactCompilerBabelPreset,
+ rolldown: {
+ ...baseReactCompilerBabelPreset.rolldown,
+ filter: {
+ id: {
+ include: reactCompilerIdInclude,
+ exclude: reactCompilerIdExclude,
+ },
+ code: reactCompilerCodeInclude,
+ },
+ },
+});
+
/**
* Library build config
*/
@@ -15,20 +69,10 @@ export default defineConfig(({ command }) => ({
formats: ["es"],
},
rolldownOptions: {
- external: [
- "@hashintel/ds-components",
- "@hashintel/ds-helpers",
- "react",
- "react-dom",
- "@xyflow/react",
- "@babel/standalone",
- // Pure-CJS dep pulled in transitively by @tanstack/react-form →
- // @tanstack/react-store. Rolldown can't safely transform its
- // `require("react")` when react is external, so it falls back to a
- // runtime require helper that throws in the browser. Externalising it
- // pushes CJS→ESM interop to the consumer's bundler.
- /^use-sync-external-store(\/.*)?$/,
- ],
+ // Keep peer packages external by subpath too. Source imports helper
+ // subpaths such as `@hashintel/ds-helpers/css`; externalizing only the
+ // package root lets those internals leak into Petrinaut's emitted graph.
+ external: isLibraryExternal,
output: {
globals: {
react: "React",
@@ -80,14 +124,7 @@ export default defineConfig(({ command }) => ({
react(),
babel({
- presets: [
- reactCompilerPreset({
- target: "19",
- compilationMode: "infer",
- // @ts-expect-error - panicThreshold is accepted at runtime
- panicThreshold: "critical_errors",
- }),
- ],
+ presets: [reactCompilerBabelPreset],
}),
command === "build" &&
diff --git a/yarn.lock b/yarn.lock
index 0e0a96f1b80..75dc63dc994 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6642,13 +6642,6 @@ __metadata:
languageName: node
linkType: hard
-"@fontsource-variable/jetbrains-mono@npm:5.2.8":
- version: 5.2.8
- resolution: "@fontsource-variable/jetbrains-mono@npm:5.2.8"
- checksum: 10c0/574e5463b802cfdd6ec8dd16724d2fd5ee38204815729c9dca0f457a417f0a4d32e6ec4ed2dfa0e5a5de5a9b0deaeb9f3c0b49b332763ed40172de43d6b1502f
- languageName: node
- linkType: hard
-
"@fortawesome/fontawesome-common-types@npm:6.7.2":
version: 6.7.2
resolution: "@fortawesome/fontawesome-common-types@npm:6.7.2"
@@ -7677,9 +7670,6 @@ __metadata:
dependencies:
"@ark-ui/react": "npm:5.26.2"
"@babel/standalone": "npm:7.28.5"
- "@fontsource-variable/inter": "npm:5.2.8"
- "@fontsource-variable/inter-tight": "npm:5.2.7"
- "@fontsource-variable/jetbrains-mono": "npm:5.2.8"
"@hashintel/ds-components": "workspace:^"
"@hashintel/ds-helpers": "workspace:*"
"@hashintel/refractive": "workspace:^"