@trackunit/iris-app-sdk-vite 0.4.3 → 0.4.5

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,15 @@
1
+ ## 0.4.5 (2026-04-30)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated iris-app-build-utilities to 1.16.4
6
+ - Updated iris-app-api to 1.18.3
7
+ - Updated iris-app to 1.19.4
8
+
9
+ ## 0.4.4 (2026-04-30)
10
+
11
+ This was a version bump only for iris-app-sdk-vite to align it with other projects, there were no code changes.
12
+
1
13
  ## 0.4.3 (2026-04-30)
2
14
 
3
15
  ### 🧱 Updated Dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/iris-app-sdk-vite",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "executors": "./executors.json",
@@ -10,8 +10,8 @@
10
10
  "dependencies": {
11
11
  "rxjs": "7.8.1",
12
12
  "win-ca": "^3.5.1",
13
- "@trackunit/iris-app-build-utilities": "1.16.3",
14
- "@trackunit/iris-app-api": "1.18.2",
13
+ "@trackunit/iris-app-build-utilities": "1.16.4",
14
+ "@trackunit/iris-app-api": "1.18.3",
15
15
  "tslib": "^2.6.2",
16
16
  "vite": "7.3.1",
17
17
  "@module-federation/vite": "1.11.0",
@@ -22,7 +22,7 @@
22
22
  "@tailwindcss/postcss": "^4.1.18",
23
23
  "@vitejs/plugin-react-swc": "^4.2.3",
24
24
  "@nx/devkit": "22.6.5",
25
- "@trackunit/iris-app": "1.19.3",
25
+ "@trackunit/iris-app": "1.19.4",
26
26
  "cross-keychain": "^1.1.0",
27
27
  "jwt-decode": "^3.1.2"
28
28
  },
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapResponseTiming = exports.shortenPath = exports.timeAsync = exports.time = exports.logEvent = exports.logTiming = void 0;
4
+ // ---------------------------------------------------------------------------
5
+ // Dev-server timing instrumentation
6
+ //
7
+ // Lightweight, zero-dependency timing logs for the Iris-app dev server.
8
+ // Disabled by default; opt in with IRIS_TIMING=true. Logs to stdout with the
9
+ // "[iris-timing]" prefix so they're easy to grep / filter.
10
+ //
11
+ // Each log line looks like:
12
+ // [iris-timing] T+<elapsed_since_module_load> <duration> <label>
13
+ //
14
+ // Durations >1000ms are highlighted red, >200ms yellow, so the slow stuff
15
+ // jumps out in a typical terminal. Per-request lines below the
16
+ // TRANSFORM_LOG_THRESHOLD_MS are dropped to keep the output usable.
17
+ // ---------------------------------------------------------------------------
18
+ // Read lazily so tests / users can flip IRIS_TIMING at runtime.
19
+ // Disabled by default; set IRIS_TIMING=true to enable.
20
+ const isTimingEnabled = () => process.env.IRIS_TIMING === "true";
21
+ const TRANSFORM_LOG_THRESHOLD_MS = 50;
22
+ const TIMING_MODULE_START = performance.now();
23
+ const padMs = (ms) => `${ms.toFixed(0).padStart(6, " ")}ms`;
24
+ const colorize = (ms, text) => {
25
+ if (ms > 1000)
26
+ return `\x1b[31m${text}\x1b[0m`; // red
27
+ if (ms > 200)
28
+ return `\x1b[33m${text}\x1b[0m`; // yellow
29
+ return text;
30
+ };
31
+ /**
32
+ * Logs a single timing line: elapsed-since-module-load, duration, and label.
33
+ * Slow durations are colorized so they stand out in the terminal.
34
+ */
35
+ const logTiming = (durationMs, label) => {
36
+ if (!isTimingEnabled())
37
+ return;
38
+ const elapsed = padMs(performance.now() - TIMING_MODULE_START);
39
+ const dur = colorize(durationMs, padMs(durationMs));
40
+ // eslint-disable-next-line no-console
41
+ console.log(`\x1b[36m[iris-timing]\x1b[0m T+${elapsed} ${dur} ${label}`);
42
+ };
43
+ exports.logTiming = logTiming;
44
+ /**
45
+ * Logs a non-timed event (no duration column), e.g. "server: listening".
46
+ * Useful for marking moments in the dev-server lifecycle.
47
+ */
48
+ const logEvent = (label) => {
49
+ if (!isTimingEnabled())
50
+ return;
51
+ const elapsed = padMs(performance.now() - TIMING_MODULE_START);
52
+ // eslint-disable-next-line no-console
53
+ console.log(`\x1b[36m[iris-timing]\x1b[0m T+${elapsed} ${label}`);
54
+ };
55
+ exports.logEvent = logEvent;
56
+ /** Wraps a synchronous function and logs its duration. */
57
+ const time = (label, fn) => {
58
+ if (!isTimingEnabled())
59
+ return fn();
60
+ const start = performance.now();
61
+ try {
62
+ return fn();
63
+ }
64
+ finally {
65
+ (0, exports.logTiming)(performance.now() - start, label);
66
+ }
67
+ };
68
+ exports.time = time;
69
+ /** Wraps an async function and logs its duration. */
70
+ const timeAsync = async (label, fn) => {
71
+ if (!isTimingEnabled())
72
+ return fn();
73
+ const start = performance.now();
74
+ try {
75
+ return await fn();
76
+ }
77
+ finally {
78
+ (0, exports.logTiming)(performance.now() - start, label);
79
+ }
80
+ };
81
+ exports.timeAsync = timeAsync;
82
+ /**
83
+ * Shortens long absolute paths to a workspace-relative form for log lines.
84
+ * "/@fs/Users/mta/.../manager1/libs/css/core/src/lib/core.css"
85
+ * -> "libs/css/core/src/lib/core.css"
86
+ */
87
+ const shortenPath = (urlOrPath) => {
88
+ const idx = urlOrPath.indexOf("/manager1/");
89
+ if (idx >= 0)
90
+ return urlOrPath.substring(idx + "/manager1/".length);
91
+ const fsPrefix = "/@fs/";
92
+ if (urlOrPath.startsWith(fsPrefix))
93
+ return urlOrPath.substring(fsPrefix.length);
94
+ return urlOrPath;
95
+ };
96
+ exports.shortenPath = shortenPath;
97
+ /**
98
+ * Logs the total server-side response time per request via the `finish` event,
99
+ * which fires once the response has been fully written. This corresponds to
100
+ * the `wait` time you see in browser network panels.
101
+ *
102
+ * Combined with the per-handler timings inside our own middleware, it lets us
103
+ * see whether slowness is in our code or in downstream Vite work (transforms,
104
+ * optimizeDeps, etc).
105
+ */
106
+ const wrapResponseTiming = (req, res) => {
107
+ if (!isTimingEnabled())
108
+ return;
109
+ const start = performance.now();
110
+ res.on("finish", () => {
111
+ const durationMs = performance.now() - start;
112
+ if (durationMs >= TRANSFORM_LOG_THRESHOLD_MS) {
113
+ const url = req.url ?? "(no url)";
114
+ (0, exports.logTiming)(durationMs, `${req.method ?? "GET"} ${(0, exports.shortenPath)(url)}`);
115
+ }
116
+ });
117
+ };
118
+ exports.wrapResponseTiming = wrapResponseTiming;
119
+ //# sourceMappingURL=devServerTiming.js.map
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExtensionLoaderCache = void 0;
4
+ const devServerTiming_1 = require("./devServerTiming");
5
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
6
+ /**
7
+ * Creates an in-memory cache for the /extensionloader.js script.
8
+ *
9
+ * The /extensionloader.js handler proxies to https://iris.trackunit.app
10
+ * (or http://localhost:3000 when LOCAL=true). Without caching, every page
11
+ * load triggers a fresh network fetch -- and during a single cold load Vite
12
+ * typically reloads the page 2-3 times (because of optimizeDeps cascades),
13
+ * so the upstream is hit 3+ times. HAR profiling showed individual fetches
14
+ * costing 200ms-5s+ depending on CDN health, and one trace recorded a single
15
+ * ~5.2s fetch as the dominant gap in the dev waterfall.
16
+ *
17
+ * The script changes very rarely, so we cache it in memory with a TTL and
18
+ * dedupe concurrent fetches.
19
+ */
20
+ const createExtensionLoaderCache = ({ getUrl, ttlMs = DEFAULT_TTL_MS, shouldBypassCache, }) => {
21
+ let cached = null;
22
+ let inflight = null;
23
+ const getCachedExtensionLoaderBody = async () => {
24
+ const url = getUrl();
25
+ const now = Date.now();
26
+ const bypass = shouldBypassCache?.() === true;
27
+ if (!bypass && cached !== null && cached.url === url && now - cached.fetchedAt < ttlMs) {
28
+ return cached.body;
29
+ }
30
+ if (inflight !== null) {
31
+ const settled = await inflight;
32
+ return settled.body;
33
+ }
34
+ const fetchPromise = (async () => {
35
+ (0, devServerTiming_1.logEvent)(`extensionloader cache: cold load (${url})`);
36
+ const body = await (0, devServerTiming_1.timeAsync)(`fetch ${url}`, async () => {
37
+ const resp = await fetch(url);
38
+ if (!resp.ok) {
39
+ throw new Error(`upstream returned ${resp.status} ${resp.statusText}`);
40
+ }
41
+ return resp.text();
42
+ });
43
+ cached = { url, body, fetchedAt: Date.now() };
44
+ return { url, body };
45
+ })();
46
+ inflight = fetchPromise;
47
+ // Clear the inflight pointer once the fetch settles. Use then(cleanup, cleanup)
48
+ // instead of finally() so a rejection isn't re-thrown from this side-channel;
49
+ // the caller still observes the rejection via the awaited `fetchPromise`.
50
+ const clearInflightIfStillCurrent = () => {
51
+ if (inflight === fetchPromise) {
52
+ inflight = null;
53
+ }
54
+ };
55
+ fetchPromise.then(clearInflightIfStillCurrent, clearInflightIfStillCurrent);
56
+ const fetched = await fetchPromise;
57
+ return fetched.body;
58
+ };
59
+ return { getCachedExtensionLoaderBody };
60
+ };
61
+ exports.createExtensionLoaderCache = createExtensionLoaderCache;
62
+ //# sourceMappingURL=extensionLoaderCache.js.map
@@ -11,6 +11,9 @@ const path = tslib_1.__importStar(require("node:path"));
11
11
  const vite_2 = require("vite");
12
12
  const vite_plugin_checker_1 = tslib_1.__importDefault(require("vite-plugin-checker"));
13
13
  const vite_plugin_css_injected_by_js_1 = tslib_1.__importDefault(require("vite-plugin-css-injected-by-js"));
14
+ const devServerTiming_1 = require("./devServerTiming");
15
+ const extensionLoaderCache_1 = require("./extensionLoaderCache");
16
+ const manifestCache_1 = require("./manifestCache");
14
17
  // Only import enableTsConfigPath statically - other utilities are dynamically imported
15
18
  // AFTER tsconfig paths are registered to avoid oxc analysis warnings
16
19
  // MIME types for serving extension assets during development
@@ -57,7 +60,7 @@ const registerTsConfigPaths = (workspaceRoot, appDir) => {
57
60
  * Loads the Iris app manifest from the app directory.
58
61
  * Uses require() with cache clearing since Node.js doesn't support query strings in import paths.
59
62
  */
60
- const loadManifest = (workspaceRoot, appDir) => {
63
+ const loadManifest = (workspaceRoot, appDir) => (0, devServerTiming_1.time)("loadManifest (re-import iris-app-manifest.ts)", () => {
61
64
  // Register tsconfig paths before loading the manifest
62
65
  registerTsConfigPaths(workspaceRoot, appDir);
63
66
  const manifestPath = path.join(appDir, "iris-app-manifest.ts");
@@ -72,7 +75,7 @@ const loadManifest = (workspaceRoot, appDir) => {
72
75
  const manifestModule = require(manifestPath);
73
76
  manifestModule.default.moduleFormat = "esm";
74
77
  return manifestModule.default;
75
- };
78
+ });
76
79
  /**
77
80
  * Converts the shared dependencies from webpack format to Vite module federation format.
78
81
  * Handles both array and object variants of the Shared type.
@@ -138,7 +141,7 @@ const generateIndexHtml = ({ manifest, packageJson, options, }) => {
138
141
  </script>`
139
142
  : "";
140
143
  const devtools = options.config.mode === "development"
141
- ? `<script nonce="{{nonce}}" src="http://localhost:8097"></script>
144
+ ? `<script nonce="{{nonce}}" defer src="http://localhost:8097"></script>
142
145
  ${iframeRedirect}
143
146
  <script nonce="{{nonce}}">
144
147
  if (typeof global === 'undefined') { window.global = window; }
@@ -159,18 +162,20 @@ const generateIndexHtml = ({ manifest, packageJson, options, }) => {
159
162
  * Returns an array of plugins including Module Federation configuration.
160
163
  */
161
164
  async function getTrackunitIrisAppVitePlugins(options) {
165
+ const pluginConstructionStart = performance.now();
166
+ (0, devServerTiming_1.logEvent)("plugin construction: started");
162
167
  const resolvedAppDir = path.resolve(options.appDir);
163
168
  const workspaceRoot = options.workspaceRoot ?? path.resolve(options.appDir, "../..");
164
169
  // Load manifest upfront for federation config
165
170
  const initialManifest = loadManifest(workspaceRoot, resolvedAppDir);
166
171
  // Get build utilities (tsconfig paths now registered via loadManifest)
167
172
  // Get an available port in the Iris app port range
168
- const port = await (0, iris_app_build_utilities_1.getAvailablePort)(22220, 22229);
173
+ const port = await (0, devServerTiming_1.timeAsync)("getAvailablePort", () => (0, iris_app_build_utilities_1.getAvailablePort)(22220, 22229));
169
174
  // Get module federation configuration from manifest
170
- const webpackExposes = await (0, iris_app_build_utilities_1.getExposedExtensions)({
175
+ const webpackExposes = await (0, devServerTiming_1.timeAsync)(`getExposedExtensions (${initialManifest.extensions.length} extensions)`, () => (0, iris_app_build_utilities_1.getExposedExtensions)({
171
176
  nxRootDir: workspaceRoot,
172
177
  manifest: initialManifest,
173
- });
178
+ }));
174
179
  const shared = (0, iris_app_build_utilities_1.getSharedDependencies)({ manifest: initialManifest });
175
180
  const viteExposes = convertExposesToViteFormat(webpackExposes);
176
181
  const viteShared = convertSharedToViteFormat(shared);
@@ -246,7 +251,6 @@ async function getTrackunitIrisAppVitePlugins(options) {
246
251
  };
247
252
  // Create the iris app plugin
248
253
  let config;
249
- let manifest = initialManifest;
250
254
  const reloadManifest = () => {
251
255
  return loadManifest(workspaceRoot, resolvedAppDir);
252
256
  };
@@ -259,6 +263,23 @@ async function getTrackunitIrisAppVitePlugins(options) {
259
263
  manifestCopy.extensions = (0, iris_app_build_utilities_1.updateExtensions)([...manifestData.extensions]);
260
264
  return (0, iris_app_build_utilities_1.injectFullDescriptionImages)(manifestCopy, resolvedAppDir);
261
265
  };
266
+ const manifestCache = (0, manifestCache_1.createManifestCache)({
267
+ reloadManifest,
268
+ generateManifestJson,
269
+ initialManifest,
270
+ });
271
+ // The /extensionloader.js handler proxies to https://iris.trackunit.app
272
+ // (or http://localhost:3000 when LOCAL=true). The script changes very
273
+ // rarely, so we cache the body in memory with a TTL and dedupe concurrent
274
+ // fetches via createExtensionLoaderCache. Set IRIS_EXTENSIONLOADER_NO_CACHE=1
275
+ // to bypass the cache for debugging.
276
+ const getExtensionLoaderUrl = () => process.env.LOCAL === "true"
277
+ ? "http://localhost:3000/extensionloader.js"
278
+ : "https://iris.trackunit.app/extensionloader.js";
279
+ const extensionLoaderCache = (0, extensionLoaderCache_1.createExtensionLoaderCache)({
280
+ getUrl: getExtensionLoaderUrl,
281
+ shouldBypassCache: () => process.env.IRIS_EXTENSIONLOADER_NO_CACHE === "1",
282
+ });
262
283
  // No explicit type annotation to avoid excessive stack depth error with federation plugin's complex types
263
284
  const irisAppPlugin = {
264
285
  name: "trackunit-iris-app-vite-configuration",
@@ -285,7 +306,7 @@ async function getTrackunitIrisAppVitePlugins(options) {
285
306
  },
286
307
  },
287
308
  // PostCSS configuration for Tailwind CSS v4
288
- // Using @tailwindcss/postcss instead of @tailwindcss/vite for better monorepo support
309
+ // Using @tailwindcss/postcss instead of @tailwindcss/vite for better monorepo support.
289
310
  css: {
290
311
  postcss: {
291
312
  plugins: [require("@tailwindcss/postcss")({})],
@@ -358,15 +379,37 @@ async function getTrackunitIrisAppVitePlugins(options) {
358
379
  config = resolvedConfig;
359
380
  },
360
381
  buildStart() {
361
- manifest = reloadManifest();
382
+ manifestCache.reloadManifestSync();
362
383
  },
363
384
  configureServer(server) {
385
+ // Watch iris-app-manifest.ts and invalidate the manifest cache on change so
386
+ // HMR picks up edits (e.g. adding/removing extensions). All other paths
387
+ // serve from the cache populated by the first request.
388
+ const manifestFilePath = path.join(resolvedAppDir, "iris-app-manifest.ts");
389
+ server.watcher.add(manifestFilePath);
390
+ server.watcher.on("change", changedPath => {
391
+ if (path.normalize(changedPath) === path.normalize(manifestFilePath)) {
392
+ manifestCache.invalidateManifestCache();
393
+ (0, devServerTiming_1.logEvent)("manifest cache: invalidated (iris-app-manifest.ts changed)");
394
+ }
395
+ });
396
+ // Request timing middleware - MUST be registered before any other middleware
397
+ // so we observe the full server-side response time. Logs to stdout when
398
+ // the response takes longer than TRANSFORM_LOG_THRESHOLD_MS. Enable with
399
+ // IRIS_TIMING=true.
400
+ server.middlewares.use((req, res, next) => {
401
+ (0, devServerTiming_1.wrapResponseTiming)(req, res);
402
+ next();
403
+ });
364
404
  // Route /invoke requests to local serverside extensions when available.
365
405
  server.middlewares.use("/invoke", (0, iris_app_build_utilities_1.createInvokeProxyMiddleware)(options.serversidePortMap));
366
406
  // Serve extension assets from source during development (/{extensionId}/... → {sourceRoot}/...)
367
407
  server.middlewares.use((req, res, next) => {
368
408
  const url = req.url?.split("?")[0] ?? "/";
369
- const filePath = resolveExtensionAsset(url, manifest ?? reloadManifest(), workspaceRoot);
409
+ // Use the in-memory manifest if available; fall back to initialManifest
410
+ // (loaded once at plugin construction) so this synchronous middleware
411
+ // never blocks on a slow manifest reload.
412
+ const filePath = resolveExtensionAsset(url, manifestCache.getManifestSync() ?? initialManifest, workspaceRoot);
370
413
  if (filePath) {
371
414
  res.setHeader("Content-Type", getMimeType(filePath));
372
415
  res.end(fs.readFileSync(filePath));
@@ -415,8 +458,7 @@ async function getTrackunitIrisAppVitePlugins(options) {
415
458
  if (req.url === "/manifest.json") {
416
459
  void (async () => {
417
460
  try {
418
- manifest = reloadManifest();
419
- const manifestJson = generateManifestJson(manifest);
461
+ const manifestJson = await manifestCache.getCachedManifestJson();
420
462
  res.setHeader("Content-Type", "application/json");
421
463
  res.end(JSON.stringify(manifestJson, null, 2));
422
464
  }
@@ -444,14 +486,15 @@ async function getTrackunitIrisAppVitePlugins(options) {
444
486
  }
445
487
  }
446
488
  // Serve manifestAndToken during development (async route - handle response ourselves)
489
+ // Calls getCachedManifestJson directly instead of HTTP-fetching /manifest.json
490
+ // to avoid a redundant manifest reload on every request (the host calls this
491
+ // endpoint multiple times during initial app boot).
447
492
  if (req.url?.startsWith("/manifestAndToken")) {
448
493
  void (async () => {
449
494
  try {
450
- const host = req.headers.host || "localhost";
451
- const resp = await fetch(`http://${host}/manifest.json`);
452
- const body = await resp.json();
495
+ const manifestJson = await manifestCache.getCachedManifestJson();
453
496
  res.setHeader("Content-Type", "application/json");
454
- res.end(JSON.stringify({ manifest: body }));
497
+ res.end(JSON.stringify({ manifest: manifestJson }));
455
498
  }
456
499
  catch (e) {
457
500
  // eslint-disable-next-line no-console
@@ -462,17 +505,13 @@ async function getTrackunitIrisAppVitePlugins(options) {
462
505
  })();
463
506
  return; // Don't call next() - we're handling this route
464
507
  }
465
- // Serve extensionloader.js during development (async route - handle response ourselves)
508
+ // Serve extensionloader.js during development.
509
+ // To test extensionloader locally set LOCAL=true in front of the serve command.
510
+ // The fetched body is cached in memory; see createExtensionLoaderCache.
466
511
  if (req.url?.startsWith("/extensionloader.js")) {
467
512
  void (async () => {
468
513
  try {
469
- // To test extensionloader locally set LOCAL=true in front of the serve command
470
- let loaderUrl = "https://iris.trackunit.app/extensionloader.js";
471
- if (process.env.LOCAL === "true") {
472
- loaderUrl = "http://localhost:3000/extensionloader.js";
473
- }
474
- const resp = await fetch(loaderUrl);
475
- const body = await resp.text();
514
+ const body = await extensionLoaderCache.getCachedExtensionLoaderBody();
476
515
  res.setHeader("Content-Type", "application/javascript");
477
516
  res.end(body);
478
517
  }
@@ -494,11 +533,11 @@ async function getTrackunitIrisAppVitePlugins(options) {
494
533
  if (isRootOrSpaRoute) {
495
534
  void (async () => {
496
535
  try {
497
- manifest = reloadManifest();
536
+ const currentManifest = await manifestCache.getCachedManifest();
498
537
  const packageJsonPath = path.join(resolvedAppDir, "package.json");
499
538
  const packageJson = fs.readFileSync(packageJsonPath, "utf8");
500
- const html = generateIndexHtml({ manifest, packageJson, options });
501
- const transformedHtml = await server.transformIndexHtml(req.url ?? "/", html);
539
+ const html = generateIndexHtml({ manifest: currentManifest, packageJson, options });
540
+ const transformedHtml = await (0, devServerTiming_1.timeAsync)(`server.transformIndexHtml(${(0, devServerTiming_1.shortenPath)(url)})`, () => server.transformIndexHtml(req.url ?? "/", html));
502
541
  res.setHeader("Content-Type", "text/html");
503
542
  res.end(transformedHtml);
504
543
  }
@@ -516,15 +555,14 @@ async function getTrackunitIrisAppVitePlugins(options) {
516
555
  });
517
556
  // Log success message when server starts
518
557
  server.httpServer?.once("listening", () => {
558
+ (0, devServerTiming_1.logEvent)("server: listening");
519
559
  setTimeout(() => {
520
560
  (0, iris_app_build_utilities_1.logInfo)(`\n✨ Iris App is now started, check it out ✨\nhttps://new.manager.trackunit.com/goto/iris-app-dev\n`);
521
561
  }, 100);
522
562
  });
523
563
  },
524
564
  generateBundle() {
525
- if (!manifest) {
526
- manifest = reloadManifest();
527
- }
565
+ const manifest = manifestCache.getManifestSync() ?? manifestCache.reloadManifestSync();
528
566
  const packageJsonPath = path.join(resolvedAppDir, "package.json");
529
567
  const packageJson = fs.readFileSync(packageJsonPath, "utf8");
530
568
  // Generate and emit manifest.json
@@ -549,7 +587,7 @@ async function getTrackunitIrisAppVitePlugins(options) {
549
587
  },
550
588
  closeBundle() {
551
589
  if (config.command === "build") {
552
- const manifestForCopy = manifest ?? reloadManifest();
590
+ const manifestForCopy = manifestCache.getManifestSync() ?? manifestCache.reloadManifestSync();
553
591
  const copyPatterns = (0, iris_app_build_utilities_1.getCopyPatterns)({
554
592
  nxRootDir: workspaceRoot,
555
593
  appDir: resolvedAppDir,
@@ -604,7 +642,7 @@ async function getTrackunitIrisAppVitePlugins(options) {
604
642
  //
605
643
  // NOTE: Tailwind CSS is configured via PostCSS (in baseConfig.css.postcss above)
606
644
  // NOT using @tailwindcss/vite because it doesn't respect tailwind.config.js content paths
607
- // PostCSS approach matches Rspack and properly uses getTailwindContentForApp() from config files
645
+ // PostCSS approach matches Rspack and properly uses Tailwind v4 config files.
608
646
  const plugins = [
609
647
  (0, nx_tsconfig_paths_plugin_1.nxViteTsPaths)(),
610
648
  // React Fast Refresh — enables HMR for React components so file changes
@@ -626,6 +664,7 @@ async function getTrackunitIrisAppVitePlugins(options) {
626
664
  }),
627
665
  bundleAnalyzerPlugin,
628
666
  ];
667
+ (0, devServerTiming_1.logTiming)(performance.now() - pluginConstructionStart, "plugin construction: complete");
629
668
  return plugins.filter(Boolean);
630
669
  }
631
670
  //# sourceMappingURL=getTrackunitIrisAppVitePlugins.js.map
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createManifestCache = void 0;
4
+ const devServerTiming_1 = require("./devServerTiming");
5
+ /**
6
+ * Creates an in-memory cache for the Iris-app manifest.
7
+ *
8
+ * The cache deduplicates concurrent /manifest.json and /manifestAndToken
9
+ * requests so they don't re-resolve the manifest + all 25 extension entries
10
+ * on every call. Cold-load profiling showed each request taking ~3s; with the
11
+ * host firing several parallel calls during boot, that stacked into >6s of
12
+ * latency before any extension code could load.
13
+ *
14
+ * The cache is invalidated when iris-app-manifest.ts changes; callers wire
15
+ * that up through the dev server's file watcher.
16
+ */
17
+ const createManifestCache = ({ reloadManifest, generateManifestJson, initialManifest, }) => {
18
+ let manifest = initialManifest;
19
+ let manifestJsonCache = null;
20
+ // Shared promise for an in-flight reload, used to dedupe concurrent requests
21
+ // that arrive while the cache is empty.
22
+ let inflightManifestReload = null;
23
+ const ensureManifestLoaded = () => {
24
+ if (manifest !== undefined && manifestJsonCache !== null) {
25
+ return Promise.resolve(manifestJsonCache);
26
+ }
27
+ if (inflightManifestReload !== null) {
28
+ (0, devServerTiming_1.logEvent)("manifest cache: joining in-flight reload (deduped)");
29
+ return inflightManifestReload;
30
+ }
31
+ (0, devServerTiming_1.logEvent)("manifest cache: cold load (cache empty)");
32
+ const reloadPromise = (async () => {
33
+ const loadedManifest = manifest ?? reloadManifest();
34
+ manifest = loadedManifest;
35
+ manifestJsonCache = (0, devServerTiming_1.time)("generateManifestJson", () => generateManifestJson(loadedManifest));
36
+ return manifestJsonCache;
37
+ })();
38
+ inflightManifestReload = reloadPromise;
39
+ // Clear the inflight pointer once the reload settles so post-invalidate
40
+ // reads start a fresh load instead of joining this resolved/rejected
41
+ // promise. The identity check guards against `invalidateManifestCache`
42
+ // having already nulled it out (or replaced it with a newer reload). We
43
+ // attach the cleanup with then(cleanup, cleanup) instead of finally() so
44
+ // a rejection isn't re-thrown from this side-channel; the caller still
45
+ // observes the rejection via the returned `reloadPromise`.
46
+ const clearInflightIfStillCurrent = () => {
47
+ if (inflightManifestReload === reloadPromise) {
48
+ inflightManifestReload = null;
49
+ }
50
+ };
51
+ reloadPromise.then(clearInflightIfStillCurrent, clearInflightIfStillCurrent);
52
+ return reloadPromise;
53
+ };
54
+ const getCachedManifestJson = () => ensureManifestLoaded();
55
+ const getCachedManifest = async () => {
56
+ await ensureManifestLoaded();
57
+ if (manifest === undefined) {
58
+ throw new Error("Manifest unexpectedly undefined after reload");
59
+ }
60
+ return manifest;
61
+ };
62
+ const getManifestSync = () => manifest;
63
+ const reloadManifestSync = () => {
64
+ const fresh = reloadManifest();
65
+ manifest = fresh;
66
+ return fresh;
67
+ };
68
+ const invalidateManifestCache = () => {
69
+ manifestJsonCache = null;
70
+ manifest = undefined;
71
+ };
72
+ return {
73
+ getCachedManifestJson,
74
+ getCachedManifest,
75
+ getManifestSync,
76
+ reloadManifestSync,
77
+ invalidateManifestCache,
78
+ };
79
+ };
80
+ exports.createManifestCache = createManifestCache;
81
+ //# sourceMappingURL=manifestCache.js.map