@trackunit/iris-app-sdk-vite 1.6.3 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 1.7.0 (2026-10-02)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **iris-app-sdk-vite:** add productionReact option to serve [GLU-2569] ([#26121](https://github.com/Trackunit/manager/pull/26121), [#26118](https://github.com/Trackunit/manager/issues/26118))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Cursor @cursoragent
10
+ - Mikkel Thorbjørn Andersen @mta-trackunit
11
+
1
12
  ## 1.6.3 (2026-10-01)
2
13
 
3
14
  ### 🧱 Updated Dependencies
package/README.md CHANGED
@@ -61,11 +61,12 @@ Add to your `project.json`:
61
61
 
62
62
  ### Serve Options
63
63
 
64
- | Option | Type | Required | Description |
65
- | ------------ | ------ | -------- | ------------------------------------------------------- |
66
- | `port` | number | No | Dev server port (default: auto-select from 22220-22229) |
67
- | `host` | string | No | Dev server host (default: "localhost") |
68
- | `viteConfig` | string | No | Path to optional custom vite config file for overrides |
64
+ | Option | Type | Required | Description |
65
+ | ----------------- | ------- | -------- | --------------------------------------------------------------------------------------------- |
66
+ | `port` | number | No | Dev server port (default: auto-select from 22220-22229) |
67
+ | `host` | string | No | Dev server host (default: "localhost") |
68
+ | `viteConfig` | string | No | Path to optional custom vite config file for overrides |
69
+ | `productionReact` | boolean | No | Serve React's production runtime instead of the development one (default: `false`). See below |
69
70
 
70
71
  ### Build Options
71
72
 
@@ -199,6 +200,28 @@ react-devtools
199
200
 
200
201
  React-devtools will automatically connect to whichever Iris App is running in your browser.
201
202
 
203
+ ### Reproducing production-only React behaviour
204
+
205
+ Deployed Iris apps are meant to run React's **production** runtime, while `serve` uses React's development runtime by default (warnings, `jsxDEV`, React Fast Refresh). To reproduce an issue that only shows up in a deployed app, serve with React's production runtime:
206
+
207
+ ```bash
208
+ nx run your-app:serve --productionReact
209
+ ```
210
+
211
+ What changes with `--productionReact`:
212
+
213
+ - React and your JSX are compiled for production: no `jsxDEV`, no development warnings or prop logging, and React errors are reported as coded messages (see https://react.dev/errors).
214
+ - React Fast Refresh is turned off, so changes reload the page instead of hot-swapping components.
215
+ - Pre-bundled dependencies are re-optimized when you switch the option on or off, so you never get the other mode's React from Vite's cache.
216
+
217
+ What stays the same: the dev server routes, the manifest and the iframe redirect still work, and `process.env.NODE_ENV` of the Nx process is not changed. React DevTools still connects, with the reduced feature set of any production React build.
218
+
219
+ Things to know:
220
+
221
+ - The compile-time `define` for `process.env.NODE_ENV` (which affects your app code and pre-bundled dependencies) switches to `"production"`. Vite's `mode` stays `development`, so `import.meta.env.DEV`, `import.meta.env.PROD` and `import.meta.env.MODE` keep their development values, and the Nx process environment is unchanged.
222
+ - If your `vite.config.ts` extends the default config, keep its `define`, `oxc` and `optimizeDeps` settings and do not add a React plugin. Otherwise the option does not take effect (or the app fails with `jsxDEV is not a function`); `serve` prints a warning when it detects this.
223
+ - The option only affects `serve`. It never changes the output of `build`.
224
+
202
225
  ### Common Issues
203
226
 
204
227
  **"Failed to resolve dependency" warnings**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/iris-app-sdk-vite",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "executors": "./executors.json",
@@ -12,6 +12,7 @@ const op = tslib_1.__importStar(require("rxjs/operators"));
12
12
  require("win-ca");
13
13
  const checkCustomFields_1 = require("../utils/checkCustomFields");
14
14
  const defaultViteConfig_1 = require("../utils/defaultViteConfig");
15
+ const findProductionReactConflicts_1 = require("../utils/findProductionReactConflicts");
15
16
  const manifestLoader_1 = require("../utils/manifestLoader");
16
17
  const resolveSdkVersion_1 = require("../utils/resolveSdkVersion");
17
18
  /**
@@ -25,6 +26,7 @@ async function* serveExecutor(options, context) {
25
26
  await (0, iris_app_build_utilities_1.checkPackageVersion)(false);
26
27
  const settings = (0, iris_app_1.getSettings)();
27
28
  const skipTypeChecking = options.skipTypeChecking ?? false;
29
+ const productionReact = options.productionReact ?? false;
28
30
  // eslint-disable-next-line @trackunit/no-typescript-assertion, @typescript-eslint/no-non-null-assertion
29
31
  const projectRoot = context.projectsConfigurations.projects[context.projectName].root;
30
32
  const projectRootDir = (0, path_1.join)(context.root, projectRoot);
@@ -69,6 +71,7 @@ async function* serveExecutor(options, context) {
69
71
  const defaultConfig = await (0, defaultViteConfig_1.getDefaultViteConfig)("development", context.root, projectRootDir, irisAppManifest, context, {
70
72
  serversidePortMap: serversideResult.portMap,
71
73
  skipTypeChecking,
74
+ productionReact,
72
75
  // Surface a persistent rebuild reminder when the in-sync check was skipped.
73
76
  degraded: checkCustomFieldsResult === "unverified",
74
77
  });
@@ -83,6 +86,15 @@ async function* serveExecutor(options, context) {
83
86
  }
84
87
  finalConfig = customConfig;
85
88
  }
89
+ if (productionReact) {
90
+ const conflicts = (0, findProductionReactConflicts_1.findProductionReactConflicts)(finalConfig);
91
+ if (conflicts.length > 0) {
92
+ const conflictLines = conflicts.map(conflict => ` - ${conflict}`).join("\n");
93
+ // eslint-disable-next-line no-console
94
+ console.warn(`\n ⚠️ --productionReact may not take effect, because the final Vite config (including your vite.config.ts) is inconsistent with it:\n${conflictLines}\n` +
95
+ " Keep the default config's `define`, `oxc` and `optimizeDeps` settings when you extend it, and do not add a React plugin.\n");
96
+ }
97
+ }
86
98
  // Apply port override if specified
87
99
  if (options.port !== undefined) {
88
100
  finalConfig.server = { ...finalConfig.server, port: options.port };
@@ -28,6 +28,11 @@
28
28
  "type": "boolean",
29
29
  "description": "Skip TypeScript type checking during dev server",
30
30
  "default": false
31
+ },
32
+ "productionReact": {
33
+ "type": "boolean",
34
+ "description": "Serve React's production runtime (no jsxDEV, no React dev warnings, no Fast Refresh) to reproduce issues that only show up in deployed apps. Only affects local serving; it never changes the output of build.",
35
+ "default": false
31
36
  }
32
37
  },
33
38
  "required": []
@@ -28,6 +28,7 @@ const virtualEntryPlugin = {
28
28
  return null;
29
29
  },
30
30
  };
31
+ const PRODUCTION_NODE_ENV = JSON.stringify("production");
31
32
  /**
32
33
  * Gets the default Vite config for Iris Apps.
33
34
  *
@@ -39,6 +40,7 @@ const virtualEntryPlugin = {
39
40
  * @returns {Promise<UserConfig>} the default Vite config
40
41
  */
41
42
  const getDefaultViteConfig = async (mode, nxRootDir, appDir, irisAppManifest, context, options = {}) => {
43
+ const productionReact = options.productionReact ?? false;
42
44
  // Dynamic import - safe after enableTsConfigPath() is called
43
45
  const irisAppPlugins = await (0, getTrackunitIrisAppVitePlugins_1.getTrackunitIrisAppVitePlugins)({
44
46
  appDir,
@@ -51,6 +53,7 @@ const getDefaultViteConfig = async (mode, nxRootDir, appDir, irisAppManifest, co
51
53
  serversidePortMap: options.serversidePortMap,
52
54
  skipTypeChecking: options.skipTypeChecking,
53
55
  degraded: options.degraded,
56
+ productionReact,
54
57
  });
55
58
  return {
56
59
  root: appDir,
@@ -63,9 +66,22 @@ const getDefaultViteConfig = async (mode, nxRootDir, appDir, irisAppManifest, co
63
66
  rolldownOptions: {
64
67
  transform: {
65
68
  target: "esnext",
69
+ // Pre-bundled React picks its runtime from this define. Vite also hashes it into the
70
+ // dependency cache key, so toggling the option re-optimizes instead of reusing the
71
+ // other mode's React.
72
+ ...(productionReact ? { define: { "process.env.NODE_ENV": PRODUCTION_NODE_ENV } } : {}),
66
73
  },
67
74
  },
68
75
  },
76
+ ...(productionReact
77
+ ? {
78
+ // `NODE_ENV` for the app's own code, without touching `process.env` (the custom-field
79
+ // escape-hatch guard reads the real environment).
80
+ define: { "process.env.NODE_ENV": PRODUCTION_NODE_ENV },
81
+ // Replaces the React plugin's serve transform, which always emits `jsxDEV`.
82
+ oxc: { jsx: { runtime: "automatic", development: false } },
83
+ }
84
+ : {}),
69
85
  build: {
70
86
  outDir: `dist/apps/${context.projectName}`,
71
87
  emptyOutDir: true,
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findProductionReactConflicts = void 0;
4
+ const NODE_ENV_DEFINE_KEY = "process.env.NODE_ENV";
5
+ const PRODUCTION_DEFINE_VALUE = '"production"';
6
+ const flattenPluginNames = (plugins) => (plugins ?? []).flatMap((plugin) => {
7
+ if (Array.isArray(plugin)) {
8
+ return flattenPluginNames(plugin);
9
+ }
10
+ if (typeof plugin === "object" && plugin !== null && "name" in plugin && typeof plugin.name === "string") {
11
+ return [plugin.name];
12
+ }
13
+ return [];
14
+ });
15
+ /**
16
+ * Finds what in the final Vite config (default config merged with the app's own `vite.config.ts`)
17
+ * defeats `serve --productionReact`. The option relies on three settings from the default config
18
+ * and on the React plugin being absent; an app config that replaces or adds them silently serves
19
+ * React's development runtime again, or crashes because `jsxDEV` is undefined in production React.
20
+ *
21
+ * @param config - the final Vite config that is handed to the dev server
22
+ * @returns {Array<string>} one human readable problem per conflict, empty when the config is consistent
23
+ */
24
+ const findProductionReactConflicts = (config) => {
25
+ const conflicts = [];
26
+ const defineValue = config.define?.[NODE_ENV_DEFINE_KEY];
27
+ if (defineValue === undefined) {
28
+ conflicts.push(`\`define["${NODE_ENV_DEFINE_KEY}"]\` is missing, so the app's own code keeps React's development runtime`);
29
+ }
30
+ else if (defineValue !== PRODUCTION_DEFINE_VALUE) {
31
+ conflicts.push(`\`define["${NODE_ENV_DEFINE_KEY}"]\` is ${JSON.stringify(defineValue)} instead of ${JSON.stringify(PRODUCTION_DEFINE_VALUE)}, so the app's own code keeps React's development runtime`);
32
+ }
33
+ const optimizerDefineValue = config.optimizeDeps?.rolldownOptions?.transform?.define?.[NODE_ENV_DEFINE_KEY];
34
+ if (optimizerDefineValue === undefined) {
35
+ conflicts.push(`\`optimizeDeps.rolldownOptions.transform.define["${NODE_ENV_DEFINE_KEY}"]\` is missing, so pre-bundled dependencies (React itself) keep the development runtime`);
36
+ }
37
+ else if (optimizerDefineValue !== PRODUCTION_DEFINE_VALUE) {
38
+ conflicts.push(`\`optimizeDeps.rolldownOptions.transform.define["${NODE_ENV_DEFINE_KEY}"]\` is ${JSON.stringify(optimizerDefineValue)} instead of ${JSON.stringify(PRODUCTION_DEFINE_VALUE)}, so pre-bundled dependencies (React itself) keep the development runtime`);
39
+ }
40
+ if (config.oxc === undefined || config.oxc === false) {
41
+ conflicts.push("`oxc.jsx` is missing, so JSX is not compiled with the production runtime");
42
+ }
43
+ else {
44
+ const jsxConfig = typeof config.oxc === "object" ? config.oxc.jsx : undefined;
45
+ if (jsxConfig === undefined) {
46
+ conflicts.push("`oxc.jsx` is missing, so JSX is not compiled with the production runtime");
47
+ }
48
+ else if (typeof jsxConfig === "string") {
49
+ conflicts.push(`\`oxc.jsx\` is set to ${JSON.stringify(jsxConfig)} instead of an object with \`runtime: "automatic"\` and \`development: false\``);
50
+ }
51
+ else {
52
+ if (jsxConfig.runtime !== "automatic") {
53
+ conflicts.push(`\`oxc.jsx.runtime\` must be \`"automatic"\` for production React, got ${JSON.stringify(jsxConfig.runtime ?? "undefined")}`);
54
+ }
55
+ if (jsxConfig.development !== false) {
56
+ conflicts.push(`\`oxc.jsx.development\` must be \`false\` for production React (got ${JSON.stringify(jsxConfig.development ?? "undefined")})`);
57
+ }
58
+ }
59
+ }
60
+ const reactPlugins = flattenPluginNames(config.plugins).filter(name => name.startsWith("vite:react"));
61
+ if (reactPlugins.length > 0) {
62
+ conflicts.push(`a React plugin is registered (${reactPlugins.join(", ")}); its serve transform emits \`jsxDEV\`, which is undefined in React's production runtime`);
63
+ }
64
+ return conflicts;
65
+ };
66
+ exports.findProductionReactConflicts = findProductionReactConflicts;
67
+ //# sourceMappingURL=findProductionReactConflicts.js.map
@@ -677,7 +677,9 @@ async function getTrackunitIrisAppVitePlugins(options) {
677
677
  // React Fast Refresh — enables HMR for React components so file changes
678
678
  // are hot-swapped without a full page reload. Safe with Module Federation
679
679
  // because Iris apps bundle their own React (iframe isolation).
680
- (0, plugin_react_swc_1.default)({}),
680
+ // Skipped when serving with React's production runtime: its serve transform is
681
+ // development-only (`jsxDEV`, which is `undefined` in React's production build).
682
+ !options.productionReact && (0, plugin_react_swc_1.default)({}),
681
683
  (0, tailwindSourcesPlugin_1.createTailwindSourcesPlugin)({ rootDir: workspaceRoot }),
682
684
  hasExposes && disableHostAutoInitPlugin,
683
685
  federationPlugin,