catalyst-core 1.0.1 → 1.0.2-canary.2

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.
Files changed (114) hide show
  1. package/bin/catalyst.js +105 -1
  2. package/dist/cli/README.md +119 -0
  3. package/dist/cli/appConfig.js +50 -0
  4. package/dist/cli/buildErrors.js +170 -0
  5. package/dist/cli/buildWarnings.js +28 -0
  6. package/dist/cli/devOutput.js +524 -0
  7. package/dist/cli/diagnostic.js +388 -0
  8. package/dist/cli/hints.js +69 -0
  9. package/dist/cli/package.json +3 -0
  10. package/dist/cli/spinner.js +141 -0
  11. package/dist/cli/theme.js +155 -0
  12. package/dist/errors/index.js +7 -3
  13. package/dist/errors/registry.js +8 -0
  14. package/dist/errors-index.json +7 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.jsx +1 -1
  17. package/dist/logger.js +61 -6
  18. package/dist/native/androidProject/app/build.gradle.kts +2 -1
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
  23. package/dist/native/androidSetup.js +46 -57
  24. package/dist/native/buildAndroid/assets.js +22 -13
  25. package/dist/native/buildAndroid/build.js +98 -26
  26. package/dist/native/buildAndroid/config.js +43 -12
  27. package/dist/native/buildAndroid/index.js +49 -191
  28. package/dist/native/buildAppAndroid.js +19 -8
  29. package/dist/native/buildAppIos.js +35 -16
  30. package/dist/native/buildIos/assets.js +1 -6
  31. package/dist/native/buildIos/build.js +61 -94
  32. package/dist/native/buildIos/config.js +26 -8
  33. package/dist/native/buildIos/index.js +143 -64
  34. package/dist/native/buildIos/plugins.js +12 -15
  35. package/dist/native/internalPluginUtils.js +0 -1
  36. package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
  39. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
  40. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
  41. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
  42. package/dist/native/pluginComposerAndroid.js +2 -3
  43. package/dist/native/renameAndroidProject.js +42 -20
  44. package/dist/native/setupEmulatorIos.js +28 -49
  45. package/dist/native/setupServer.js +12 -15
  46. package/dist/native/terminalProgress.js +186 -147
  47. package/dist/native/utils.js +335 -54
  48. package/dist/otel.js +15 -9
  49. package/dist/scripts/build.js +314 -42
  50. package/dist/scripts/generateOfflineManifest.js +11 -1
  51. package/dist/scripts/loadEnvironmentVariables.js +24 -2
  52. package/dist/scripts/preServerInit.js +32 -10
  53. package/dist/scripts/preflight.js +115 -11
  54. package/dist/scripts/scriptUtils.js +34 -0
  55. package/dist/scripts/serve.js +65 -9
  56. package/dist/scripts/start.js +58 -21
  57. package/dist/server/expressServer.js +167 -32
  58. package/dist/server/renderer/document/Body.d.ts +0 -11
  59. package/dist/server/renderer/document/Body.jsx +0 -10
  60. package/dist/server/renderer/document/Head.d.ts +0 -13
  61. package/dist/server/renderer/document/Head.jsx +0 -12
  62. package/dist/server/renderer/handler.jsx +87 -11
  63. package/dist/server/utils/validator.js +14 -11
  64. package/dist/vite/manifest-categorization-plugin.js +7 -5
  65. package/dist/vite/resolveDevServerConfig.js +9 -2
  66. package/dist/vite/vite.config.client.js +19 -1
  67. package/dist/vite/vite.config.js +96 -19
  68. package/dist/vite/vite.config.server.js +21 -0
  69. package/dist/web-router/components/MetaTag.jsx +20 -14
  70. package/dist/web-router/components/MetaTag.test.jsx +108 -32
  71. package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
  72. package/dist/web-router/components/RouterDataProvider.jsx +35 -1
  73. package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
  74. package/dist/web-router/context.d.ts +15 -1
  75. package/dist/web-router/context.jsx +11 -0
  76. package/dist/webmcp/WebMcpProvider.jsx +234 -0
  77. package/dist/webmcp/declarative.js +207 -0
  78. package/dist/webmcp/declarative.test.js +122 -0
  79. package/dist/webmcp/errors.js +59 -0
  80. package/dist/webmcp/errors.test.js +47 -0
  81. package/dist/webmcp/index.js +40 -0
  82. package/dist/webmcp/native.js +37 -0
  83. package/dist/webmcp/native.test.js +41 -0
  84. package/dist/webmcp/pageInfo.js +119 -0
  85. package/dist/webmcp/pageInfo.test.js +85 -0
  86. package/dist/webmcp/registry.dom.test.js +175 -0
  87. package/dist/webmcp/registry.js +540 -0
  88. package/dist/webmcp/registry.test.js +228 -0
  89. package/dist/webmcp/routeContext.js +11 -0
  90. package/dist/webmcp/shim-entry.js +8 -0
  91. package/dist/webmcp/shim.js +149 -0
  92. package/dist/webmcp/useTool.js +124 -0
  93. package/package.json +4 -7
  94. package/test/cli/buildErrors.test.ts +173 -0
  95. package/test/cli/buildWarnings.test.cjs +62 -0
  96. package/test/cli/devOutput.test.ts +549 -0
  97. package/test/cli/diagnostic.test.ts +434 -0
  98. package/test/cli/output.test.cjs +90 -0
  99. package/test/cli/spinner.test.ts +177 -0
  100. package/test/cli/theme.test.ts +163 -0
  101. package/test/native/redactSecrets.test.ts +72 -0
  102. package/test/native/utils.test.ts +532 -0
  103. package/test/scripts/build.test.ts +275 -0
  104. package/test/scripts/preflight.test.ts +100 -2
  105. package/test/scripts/serve.test.ts +102 -0
  106. package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
  107. package/test/server/handler.test.ts +17 -0
  108. package/test/server/logger.test.ts +185 -0
  109. package/test/server/manifestCache.test.ts +13 -0
  110. package/test/server/otel.test.ts +556 -0
  111. package/test/server/validator.test.ts +19 -8
  112. package/vitest.config.ts +4 -0
  113. package/dist/scripts/checkVersion.js +0 -3
  114. package/dist/scripts/cleanCache.js +0 -30
@@ -8,14 +8,3 @@ import React from "react";
8
8
  * @param {object} children - contains any child elements defined within the component
9
9
  */
10
10
  export declare function Body(props: any): React.JSX.Element;
11
- export declare namespace Body {
12
- var propTypes: {
13
- initialState: any;
14
- jsx: any;
15
- statusCode: any;
16
- fetcherData: any;
17
- children: any;
18
- safeArea: any;
19
- nativeWebView: any;
20
- };
21
- }
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import PropTypes from "prop-types";
3
2
  const DEFAULT_SAFE_AREA = {
4
3
  top: 0,
5
4
  right: 0,
@@ -51,12 +50,3 @@ export function Body(props) {
51
50
  {children}
52
51
  </body>;
53
52
  }
54
- Body.propTypes = {
55
- initialState: PropTypes.object,
56
- jsx: PropTypes.any,
57
- statusCode: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
58
- fetcherData: PropTypes.object,
59
- children: PropTypes.node,
60
- safeArea: PropTypes.object,
61
- nativeWebView: PropTypes.bool
62
- };
@@ -5,16 +5,3 @@ import React from "react";
5
5
  * Route-cached deferred CSS is inlined here on repeat visits; new deferred CSS still appends after body.
6
6
  */
7
7
  export declare function Head(props: any): React.JSX.Element;
8
- export declare namespace Head {
9
- var propTypes: {
10
- isBot: any;
11
- inlineCss: any;
12
- deferredRouteInlineCss: any;
13
- jsScripts: any;
14
- criticalPreloadLinks: any;
15
- deferredPreloadLinks: any;
16
- metaTags: any;
17
- publicAssetPath: any;
18
- children: any;
19
- };
20
- }
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import PropTypes from "prop-types";
3
2
  import FastRefresh from "../../../vite/FastRefresh.jsx";
4
3
 
5
4
  /**
@@ -49,14 +48,3 @@ export function Head(props) {
49
48
  {children}
50
49
  </head>;
51
50
  }
52
- Head.propTypes = {
53
- isBot: PropTypes.bool,
54
- inlineCss: PropTypes.string,
55
- deferredRouteInlineCss: PropTypes.string,
56
- jsScripts: PropTypes.array,
57
- criticalPreloadLinks: PropTypes.array,
58
- deferredPreloadLinks: PropTypes.array,
59
- metaTags: PropTypes.array,
60
- publicAssetPath: PropTypes.string,
61
- children: PropTypes.node
62
- };
@@ -29,15 +29,84 @@ import { resolveOutputMode, getDebugEnvInfo } from "../../scripts/scriptUtils.js
29
29
  // this process never sees the parent's CLI flags, so process.argv here would
30
30
  // never contain --debug/--verbose — only CATALYST_OUTPUT_MODE is real input.
31
31
  const outputMode = resolveOutputMode([], process.env);
32
- // Debug mode's boxed output already embeds the full stack trace (see
33
- // errors/index.js#formatDebug), so only default/verbose need the original
34
- // error printed separately to avoid losing the stack.
35
- const logSSRError = (stage, error) => {
32
+ const STAGE_LABEL = {
33
+ RENDER: "render",
34
+ FETCHER: "serverFetcher",
35
+ SERVER_SIDE_FUNCTION: "serverSideFunction",
36
+ REQUEST_HANDLING: "request"
37
+ };
38
+
39
+ // One failure is reported once. A render error used to surface at every catch
40
+ // site it bubbled through -- render, fetcher, serverSideFunction, request --
41
+ // each re-rendering the same broken tree, so one throw printed four coded
42
+ // reports and four stacks per request.
43
+ const reported = new WeakSet();
44
+
45
+ // Marks an error that came out of rendering. The fetcher and
46
+ // serverSideFunction fallbacks re-render the page; re-rendering after the
47
+ // render itself failed only throws the same error again.
48
+ const RENDER_FAILED = Symbol.for("catalyst.renderFailed");
49
+ const asError = error => error && typeof error === "object" ? error : new Error(String(error));
50
+ const markRenderFailure = error => {
51
+ const err = asError(error);
52
+ err[RENDER_FAILED] = true;
53
+ // Already reported by the render path; a thrown string arrives here as a
54
+ // fresh Error the outer catch would otherwise report a second time.
55
+ reported.add(err);
56
+ return err;
57
+ };
58
+ const isRenderFailure = error => Boolean(error && typeof error === "object" && error[RENDER_FAILED]);
59
+ const logSSRError = (stage, error, req) => {
60
+ if (error && typeof error === "object") {
61
+ if (reported.has(error)) return;
62
+ reported.add(error);
63
+ }
36
64
  const wrapped = wrapSSRError(stage, error);
65
+
66
+ // This module is bundled into the app's build, so it cannot reach
67
+ // catalyst-core's terminal renderer by path. The server that loads it
68
+ // provides one (see expressServer.ts); a custom server without it gets the
69
+ // registry's plain formatting.
70
+ const report = globalThis.__catalystReportRuntimeError;
71
+ if (typeof report === "function") {
72
+ const where = req ? `${req.method} ${req.originalUrl}` : null;
73
+ report({
74
+ error,
75
+ code: wrapped.code,
76
+ docs: wrapped.docUrl,
77
+ scope: [where, STAGE_LABEL[stage]].filter(Boolean).join(" · ")
78
+ });
79
+ return;
80
+ }
81
+ // Debug mode's boxed output already embeds the full stack trace (see
82
+ // errors/index.js#formatDebug), so only default/verbose need the original
83
+ // error printed separately to avoid losing the stack.
37
84
  const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined;
38
85
  console.error(formatError(wrapped, outputMode, debugEnv));
39
86
  if (outputMode !== "debug") console.error(error);
40
87
  };
88
+
89
+ /**
90
+ * Finish a request whose handling failed. Without this a render error left the
91
+ * response open: the browser spun until it timed out and the terminal was the
92
+ * only place the failure showed.
93
+ */
94
+ const sendFailure = (res, error) => {
95
+ // Plain Node response calls, and never a throw: failing while reporting a
96
+ // failure would turn one error into an unhandled rejection.
97
+ try {
98
+ if (res.headersSent) {
99
+ if (!res.writableEnded) res.end();
100
+ return;
101
+ }
102
+ const detail = process.env.NODE_ENV !== "production" && error ? `\n\n${error?.message || String(error)}\n\nThe full error is in the terminal running catalyst start.` : "";
103
+ res.statusCode = 500;
104
+ res.setHeader?.("content-type", "text/plain; charset=utf-8");
105
+ res.end(`Internal Server Error${detail}`);
106
+ } catch {
107
+ // The connection is already gone; nothing left to tell it.
108
+ }
109
+ };
41
110
  const DEFAULT_SAFE_AREA_INSETS = {
42
111
  top: 0,
43
112
  right: 0,
@@ -286,6 +355,10 @@ const _renderMarkUp = async (errorCode, req, res, metaTags, fetcherData, store,
286
355
  cb();
287
356
  }
288
357
  });
358
+ // destroy(error) below emits "error" on this stream. Unhandled, that
359
+ // became an uncaughtException per render on top of the report
360
+ // already printed; the failure is handled through reject().
361
+ tail.on("error", () => {});
289
362
  tail.pipe(res);
290
363
  const {
291
364
  pipe
@@ -299,7 +372,7 @@ const _renderMarkUp = async (errorCode, req, res, metaTags, fetcherData, store,
299
372
  resolve();
300
373
  },
301
374
  onError(error) {
302
- logSSRError("RENDER", error);
375
+ logSSRError("RENDER", error, req);
303
376
  safeCall(onRenderError, {
304
377
  req,
305
378
  res,
@@ -308,20 +381,20 @@ const _renderMarkUp = async (errorCode, req, res, metaTags, fetcherData, store,
308
381
  });
309
382
  cleanupSafeArea();
310
383
  tail.destroy(error);
311
- reject(error);
384
+ reject(markRenderFailure(error));
312
385
  }
313
386
  });
314
387
  });
315
388
  } catch (error) {
316
389
  cleanupSafeArea();
317
- logSSRError("RENDER", error);
390
+ logSSRError("RENDER", error, req);
318
391
  safeCall(onRenderError, {
319
392
  req,
320
393
  res,
321
394
  store,
322
395
  error
323
396
  });
324
- return Promise.reject(error);
397
+ return Promise.reject(markRenderFailure(error));
325
398
  }
326
399
  };
327
400
  const tracedRenderMarkUp = withObservability(SSR_SERVICE, _renderMarkUp, "renderMarkUp");
@@ -412,7 +485,8 @@ async function _handler(req, res) {
412
485
  await tracedRenderMarkUp(null, req, res, allTags, fetcherData, store, allMatches, context, chunkExtractor);
413
486
  }
414
487
  } catch (error) {
415
- logSSRError("FETCHER", error);
488
+ if (isRenderFailure(error)) throw error;
489
+ logSSRError("FETCHER", error, req);
416
490
  safeCall(onFetcherError, {
417
491
  req,
418
492
  res,
@@ -424,7 +498,8 @@ async function _handler(req, res) {
424
498
  await tracedRenderMarkUp(404, req, res, allTags, fetcherData, store, allMatches, context, chunkExtractor);
425
499
  }
426
500
  } catch (error) {
427
- logSSRError("SERVER_SIDE_FUNCTION", error);
501
+ if (isRenderFailure(error)) throw error;
502
+ logSSRError("SERVER_SIDE_FUNCTION", error, req);
428
503
  safeCall(onAppServerSideError, {
429
504
  req,
430
505
  res,
@@ -436,12 +511,13 @@ async function _handler(req, res) {
436
511
  await tracedRenderMarkUp(error.status_code, req, res, allTags, fetcherData, store, allMatches, context, chunkExtractor);
437
512
  }
438
513
  } catch (error) {
439
- logSSRError("REQUEST_HANDLING", error);
514
+ logSSRError("REQUEST_HANDLING", error, req);
440
515
  safeCall(onRequestError, {
441
516
  req,
442
517
  res,
443
518
  error
444
519
  });
520
+ sendFailure(res, error);
445
521
  }
446
522
  }
447
523
  const handler = withObservability(SSR_SERVICE, _handler, "handler");
@@ -1,4 +1,3 @@
1
- import pc from "ansis";
2
1
  import { createError, formatError, ERROR_CODES } from "../../errors/index.js";
3
2
  import { resolveOutputMode, getDebugEnvInfo } from "../../scripts/scriptUtils.js";
4
3
 
@@ -17,13 +16,19 @@ const outputMode = resolveOutputMode([], process.env);
17
16
  // single error and fall back, keeping the request alive.
18
17
  // `handleError` below is the shared "log it, in whatever output mode" helper
19
18
  // those in-server sites use.
20
- const handleError = e => {
19
+ // The default view is the CLI's diagnostic block. This module is also bundled
20
+ // into the app's SSR build, where the CLI is not reachable by a relative path,
21
+ // so the server process installs the renderer as a global (preServerInit.js,
22
+ // the first module it loads). Without it, or under --verbose/--debug, the
23
+ // registry's own format is used.
24
+ const renderError = e => {
25
+ const render = globalThis.__catalystRenderRegistryError;
26
+ if (outputMode === "default" && typeof render === "function") return render(e, "server");
21
27
  const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined;
22
- if (outputMode === "default") {
23
- console.log(pc.red("Failed to start server: "), formatError(e, outputMode));
24
- } else {
25
- console.log(formatError(e, outputMode, debugEnv));
26
- }
28
+ return formatError(e, outputMode, debugEnv);
29
+ };
30
+ const handleError = e => {
31
+ console.log(renderError(e));
27
32
  };
28
33
  const validatePreInitServer = fn => {
29
34
  if (!fn) return createError(ERROR_CODES.PREFLIGHT_PRE_SERVER_INIT_MISSING);
@@ -112,8 +117,7 @@ const safeCall = async (fn, ...args) => {
112
117
  const wrapped = createError(ERROR_CODES.PROCESS_USER_HOOK_FAILED, {
113
118
  cause: e
114
119
  });
115
- const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined;
116
- console.error(formatError(wrapped, outputMode, debugEnv));
120
+ console.error(renderError(wrapped));
117
121
  }
118
122
  };
119
123
 
@@ -132,8 +136,7 @@ const safeCallNamed = async (hookName, fn, ...args) => {
132
136
  details: `The "${hookName}" hook threw. See the cause below.`,
133
137
  cause: e
134
138
  });
135
- const debugEnv = outputMode === "debug" ? getDebugEnvInfo() : undefined;
136
- console.error(formatError(wrapped, outputMode, debugEnv));
139
+ console.error(renderError(wrapped));
137
140
  }
138
141
  };
139
142
  export { handleError, validateConfigFile, validateConfigureStore, validateCustomDocument, validateGetRoutes, validatePackageJson, validateReducerFunction, validateModuleAlias, validatePreInitServer, validateMiddleware, safeCall, safeCallNamed };
@@ -199,7 +199,7 @@ export function manifestCategorizationPlugin(options = {}) {
199
199
  if (matchedViteEntry && matchedViteKey) {
200
200
  newCategorizedChunks[category][matchedViteKey] = { ...matchedViteEntry }
201
201
  } else {
202
- console.log(`❌ No manifest match found for ${chunkName}, keeping original structure`)
202
+ console.debug(`No manifest match for ${chunkName}; keeping original structure`)
203
203
  newCategorizedChunks[category][chunkName] = chunkData
204
204
  }
205
205
  }
@@ -405,7 +405,7 @@ export function manifestCategorizationPlugin(options = {}) {
405
405
  }
406
406
  } catch (err) {
407
407
  // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.log here does no printf-style substitution.
408
- console.log(`❌ Error resolving ${importPath}:`, err.message)
408
+ console.debug(`Could not resolve ${importPath}: ${err.message}`)
409
409
  }
410
410
  })
411
411
  )
@@ -430,16 +430,18 @@ export function manifestCategorizationPlugin(options = {}) {
430
430
  const manifestContent = fs.readFileSync(manifestPath, "utf8")
431
431
  viteManifest = JSON.parse(manifestContent)
432
432
  } else {
433
- console.log("❌ Could not find manifest.json at expected path:", manifestPath)
433
+ // Silent: this fires as a *consequence* of the bundle
434
+ // failing, so shouting about a missing manifest adds a
435
+ // second, misleading error on top of the real one.
434
436
  return
435
437
  }
436
438
  } catch (e) {
437
- console.log("❌ Could not read manifest from file system:", e.message)
439
+ // Same reasoning as above: a consequence, not the cause.
438
440
  return
439
441
  }
440
442
 
441
443
  if (!processedManifest) {
442
- console.log("❌ No processed manifest available")
444
+ console.debug("No processed manifest available")
443
445
  return
444
446
  }
445
447
 
@@ -38,11 +38,18 @@ export function resolveDevFsAllow(devFs, frameworkPaths) {
38
38
  * first so the framework-owned `hmr` and `fs` always win.
39
39
  */
40
40
  export function buildDevServer(devServer = {}, { frameworkPaths, isProduction = false } = {}) {
41
- // eslint-disable-next-line no-unused-vars -- base is destructured only to omit it from serverOverrides
41
+ // `base` is pulled out to keep it out of serverOverrides -- it is applied
42
+ // at the config root, not under `server` -- so it is unused by design.
43
+ // eslint-disable-next-line no-unused-vars
42
44
  const { base, hmr, fs, ...serverOverrides } = devServer
45
+ // CATALYST_NO_HMR lets a one-shot consumer of this config (the offline
46
+ // manifest step, which spins up a throwaway server purely to load a
47
+ // module) opt out. isProduction is captured at module load, so it cannot
48
+ // be flipped by setting NODE_ENV just before createServer().
49
+ const hmrDisabled = isProduction || process.env.CATALYST_NO_HMR === "true"
43
50
  return {
44
51
  ...serverOverrides,
45
- hmr: isProduction ? false : (hmr ?? true),
52
+ hmr: hmrDisabled ? false : (hmr ?? true),
46
53
  fs: { allow: resolveDevFsAllow(fs, frameworkPaths) },
47
54
  }
48
55
  }
@@ -31,6 +31,10 @@ const createClientConfig = async () => {
31
31
  sourcemap: false,
32
32
  manifest: true,
33
33
  ssrManifest: true,
34
+ // Parallel client and server builds share this directory -- emptying it
35
+ // would delete the other bundle. Declared explicitly so Vite does not
36
+ // warn about an outDir outside the project root.
37
+ emptyOutDir: false,
34
38
  outDir: path.join(process.env.src_path, process.env.BUILD_OUTPUT_PATH || "build"),
35
39
 
36
40
  // cssCodeSplit: true (default) — each chunk gets its own CSS file.
@@ -38,6 +42,20 @@ const createClientConfig = async () => {
38
42
  // per-chunk CSS is fine — more files but each is small and cacheable.
39
43
 
40
44
  rollupOptions: {
45
+ // Advisory warnings about third-party code the app author
46
+ // cannot act on: a dependency's misplaced /*#__PURE__*/ comment
47
+ // and unused re-exports from our own entry. Genuine problems
48
+ // (unresolved imports, circular deps, missing exports) still
49
+ // surface.
50
+ onwarn(warning, defaultHandler) {
51
+ const ignored = new Set([
52
+ "INVALID_ANNOTATION",
53
+ "UNUSED_EXTERNAL_IMPORT",
54
+ "MODULE_LEVEL_DIRECTIVE",
55
+ ])
56
+ if (ignored.has(warning.code)) return
57
+ defaultHandler(warning)
58
+ },
41
59
  input: {
42
60
  main: path.join(process.env.src_path, "client/index.js"),
43
61
  },
@@ -74,7 +92,7 @@ const createClientConfig = async () => {
74
92
  return undefined
75
93
  }
76
94
  if (
77
- /[\\/]node_modules[\\/](react|react-dom|scheduler|react-fast-compare|react-side-effect|react-helmet-async)[\\/]/.test(
95
+ /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/.test(
78
96
  id
79
97
  )
80
98
  ) {
@@ -51,15 +51,27 @@ function scssModulesPlugin() {
51
51
  }
52
52
  }
53
53
 
54
+ // Automatic runtime so the output imports react/jsx-(dev-)runtime: plugin-react
55
+ // only adds Fast Refresh to .js files that import it. Classic createElement
56
+ // output leaves the module without an HMR boundary, forcing full page reloads.
54
57
  function jsxInJsPlugin() {
58
+ let isProduction = process.env.NODE_ENV === "production"
59
+
55
60
  return {
56
61
  name: "vite-plugin-jsx-in-js",
57
62
  enforce: "pre",
63
+ configResolved(config) {
64
+ isProduction = config.isProduction
65
+ },
58
66
  async transform(code, id) {
59
67
  if (!id.endsWith(".js")) return null
60
68
  if (id.includes("node_modules")) return null
61
69
  if (!/</.test(code)) return null
62
- return transformWithEsbuild(code, id, { loader: "jsx" })
70
+ return transformWithEsbuild(code, id, {
71
+ loader: "jsx",
72
+ jsx: "automatic",
73
+ jsxDev: !isProduction,
74
+ })
63
75
  },
64
76
  }
65
77
  }
@@ -123,14 +135,20 @@ import { imageUrl, fontUrl } from "./scssParams.js"
123
135
  // options — so anything else is left alone and Vite falls back to its normal
124
136
  // auto-loaded postcss.config.js resolution.
125
137
  const resolvePostcssPlugins = () => {
126
- // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
127
- const configPath = path.join(process.env.src_path, "postcss.config.js")
128
- if (!fs.existsSync(configPath)) return null
138
+ // .cjs first: an app with "type": "module" (the scaffold default) must
139
+ // name a CommonJS config .cjs, and a .js one is ESM there.
140
+ const configPath = ["postcss.config.cjs", "postcss.config.js"]
141
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
142
+ .map((name) => path.join(process.env.src_path, name))
143
+ .find((candidate) => fs.existsSync(candidate))
144
+ if (!configPath) return null
129
145
 
130
146
  const appRequire = createRequire(configPath)
131
147
 
132
148
  try {
133
- const userConfig = appRequire(configPath)
149
+ const loaded = appRequire(configPath)
150
+ // require() of an ESM config returns its namespace.
151
+ const userConfig = loaded?.__esModule || loaded?.default ? loaded.default : loaded
134
152
  const pluginsConfig = userConfig?.plugins
135
153
 
136
154
  if (!pluginsConfig || Array.isArray(pluginsConfig) || typeof pluginsConfig !== "object") {
@@ -256,7 +274,27 @@ const getScssResourcesImport = () => {
256
274
 
257
275
  // Pre-bundling upfront avoids Vite discovering deps at runtime which causes
258
276
  // cascading full-page reloads and "stuck in loading" on dev server.
259
- const browserOptimizeDeps = [
277
+ /**
278
+ * Keep only the packages this app can actually resolve.
279
+ *
280
+ * The list below is a superset covering every dependency a Catalyst app might
281
+ * use. Vite logs "Failed to resolve dependency: X, present in
282
+ * 'optimizeDeps.include'" for each entry it cannot find, so an app that uses
283
+ * half of them opened with 23 warning lines it could do nothing about.
284
+ */
285
+ function resolvableFrom(candidates, appRoot) {
286
+ const resolver = createRequire(path.join(appRoot, "package.json"))
287
+ return candidates.filter((name) => {
288
+ try {
289
+ resolver.resolve(name)
290
+ return true
291
+ } catch (error) {
292
+ return false
293
+ }
294
+ })
295
+ }
296
+
297
+ const candidateBrowserDeps = [
260
298
  "react",
261
299
  "react-dom",
262
300
  "react-dom/client",
@@ -266,9 +304,7 @@ const browserOptimizeDeps = [
266
304
  "redux",
267
305
  "redux-thunk",
268
306
  "axios",
269
- "react-helmet-async",
270
307
  "react-google-recaptcha",
271
- "react-fast-compare",
272
308
  "history",
273
309
  "lottie-web",
274
310
  // Pre-bundle alongside React so esbuild marks react/react-router as external
@@ -295,7 +331,6 @@ const browserOptimizeDeps = [
295
331
  // first navigation that pulls them in doesn't trigger a full-page reload.
296
332
  "react-loadable-visibility",
297
333
  "react-detect-offline",
298
- "react-side-effect",
299
334
  "react-async-script",
300
335
  "normalize.css",
301
336
  ]
@@ -355,14 +390,12 @@ export const sharedViteConfig = {
355
390
  // the bundler never tries to resolve the optional (opt-in) OTEL packages.
356
391
  external: nodeOnlyExternalDeps,
357
392
  optimizeDeps: {
358
- include: [
359
- "invariant",
360
- "react-fast-compare",
361
- "shallowequal",
362
- "prop-types",
363
- "redux-thunk",
364
- "redux-logger",
365
- ],
393
+ // Same treatment as the browser list: only include what this app
394
+ // can resolve, or Vite warns about every entry it cannot find.
395
+ include: resolvableFrom(
396
+ ["invariant", "shallowequal", "prop-types", "redux-thunk", "redux-logger"],
397
+ process.env.src_path
398
+ ),
366
399
  // Prevent pre-bundling React ecosystem packages for SSR to avoid duplicate
367
400
  // React instances. Pre-bundled copies in .vite/deps_ssr/ create a separate
368
401
  // React instance from node_modules/react used by react-dom/server, causing
@@ -429,8 +462,18 @@ export const sharedViteConfig = {
429
462
  path.join(process.env.src_path, "client/index.js"),
430
463
  path.join(process.env.src_path, "src/**/*.{js,jsx,ts,tsx}"),
431
464
  ],
432
- include: browserOptimizeDeps,
433
- exclude: ["catalyst-core/router/ClientRouter"],
465
+ include: resolvableFrom(candidateBrowserDeps, process.env.src_path),
466
+ // catalyst-core/webmcp (and its shim subpath) must share the SAME
467
+ // pre-bundled module instance as catalyst-core's main entry:
468
+ // WebMcpProvider reads router state through a React context
469
+ // (OneMgRouterContext, via useRouterState()) that RouterDataProvider
470
+ // populates. If esbuild's dep scanner pre-bundles the subpath as its
471
+ // own separate optimized chunk, that chunk gets its own
472
+ // `createContext()` call — a second, unlinked context instance that
473
+ // WebMcpProvider reads from empty, throwing "must be used within a
474
+ // RouterDataProvider" even though one genuinely is an ancestor.
475
+ // Same rationale as the existing ClientRouter exclusion below.
476
+ exclude: ["catalyst-core/router/ClientRouter", "catalyst-core/webmcp", "catalyst-core/webmcp/shim"],
434
477
  esbuildOptions: {
435
478
  format: "esm",
436
479
  target: "node2022",
@@ -482,6 +525,16 @@ export const sharedViteConfig = {
482
525
  },
483
526
  }
484
527
 
528
+ // Every catalyst-core subpath export except the main entry and webmcp, as bare
529
+ // specifiers ("catalyst-core/hooks"). Read from package.json so a new subpath
530
+ // stays externalized in dev SSR without touching this list.
531
+ function catalystCoreExternalSubpaths() {
532
+ const { exports = {} } = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"))
533
+ return Object.keys(exports)
534
+ .filter((key) => key.startsWith("./") && key !== "./package.json" && !key.startsWith("./webmcp"))
535
+ .map((key) => `catalyst-core/${key.slice(2)}`)
536
+ }
537
+
485
538
  // ─── Dev server config ────────────────────────────────────────────────────────
486
539
  // Async factory so it can read the app's buildConfig.devServer for `base`, HMR
487
540
  // and other Vite server options. `base` comes solely from devServer.base; the
@@ -494,6 +547,30 @@ export default defineConfig(async () => {
494
547
  return {
495
548
  ...sharedViteConfig,
496
549
  base: resolveDevBase(devServer),
550
+ // Dev SSR only: transform catalyst-core/webmcp through Vite instead of
551
+ // externalizing it. Node cannot load its .jsx files, and its provider
552
+ // must share one module record (one createContext()) with the main
553
+ // entry, which Vite always transforms (.jsx is never externalized).
554
+ // ssr.noExternal only matches package names, so the package goes in
555
+ // noExternal and every other subpath is pinned external: CJS subpaths
556
+ // (hooks, WebBridge, sentry) would fail to inline ("exports is not
557
+ // defined"), and apps that do not import webmcp resolve exactly as
558
+ // before. Keyed on NODE_ENV, not Vite's `command`: `catalyst build`
559
+ // also spins up this config for the offline manifest, and built
560
+ // output runs under plain Node ESM, one module record per file.
561
+ ssr: {
562
+ ...sharedViteConfig.ssr,
563
+ noExternal: [
564
+ ...new Set([
565
+ ...(sharedViteConfig.ssr?.noExternal || []),
566
+ ...(isProduction ? [] : ["catalyst-core"]),
567
+ ]),
568
+ ],
569
+ external: [
570
+ ...(sharedViteConfig.ssr?.external || []),
571
+ ...(isProduction ? [] : catalystCoreExternalSubpaths()),
572
+ ],
573
+ },
497
574
  server: buildDevServer(devServer, {
498
575
  frameworkPaths: [process.env.src_path, __dirname],
499
576
  isProduction,
@@ -28,8 +28,15 @@ export default defineConfig(async () => {
28
28
  ...(customViteConfig?.ssrPlugins || []),
29
29
  ],
30
30
 
31
+ // The CLI prints its own build summary and asset table, so Vite's
32
+ // duplicate table and rollup's advisory warnings are pure noise.
33
+
31
34
  build: {
32
35
  ...baseConfig.build,
36
+ // Parallel client and server builds share this directory -- emptying it
37
+ // would delete the other bundle. Declared explicitly so Vite does not
38
+ // warn about an outDir outside the project root.
39
+ emptyOutDir: false,
33
40
  outDir: path.join(process.env.src_path, process.env.BUILD_OUTPUT_PATH || "build"),
34
41
  target: "es2022",
35
42
  minify: "esbuild",
@@ -38,6 +45,20 @@ export default defineConfig(async () => {
38
45
  ssrManifest: false,
39
46
 
40
47
  rollupOptions: {
48
+ // Advisory warnings about third-party code the app author
49
+ // cannot act on: a dependency's misplaced /*#__PURE__*/ comment
50
+ // and unused re-exports from our own entry. Genuine problems
51
+ // (unresolved imports, circular deps, missing exports) still
52
+ // surface.
53
+ onwarn(warning, defaultHandler) {
54
+ const ignored = new Set([
55
+ "INVALID_ANNOTATION",
56
+ "UNUSED_EXTERNAL_IMPORT",
57
+ "MODULE_LEVEL_DIRECTIVE",
58
+ ])
59
+ if (ignored.has(warning.code)) return
60
+ defaultHandler(warning)
61
+ },
41
62
  // Belt-and-suspenders with ssr.external: ensures the opt-in OTEL /
42
63
  // node-only packages (and their transitive @opentelemetry/* deps) are
43
64
  // never resolved/bundled, even though they may not be installed.