catalyst-core 0.3.0-beta.3 → 0.3.0-beta.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/dist/index.jsx CHANGED
@@ -1,4 +1,4 @@
1
- export * from "react-router-dom"
1
+ export * from "react-router"
2
2
  export * from "./web-router/components/RouterDataProvider.jsx"
3
3
  export * from "./web-router/components/MetaTag.jsx"
4
4
  export { split, hydrationReady } from "./web-router/components/Split.jsx"
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useNativeTransition = void 0;
7
7
  var _react = require("react");
8
- var _reactRouterDom = require("react-router-dom");
8
+ var _reactRouter = require("react-router");
9
9
  var _NativeBridge = _interopRequireDefault(require("../utils/NativeBridge.js"));
10
10
  var _NativeInterfaces = require("../constants/NativeInterfaces.js");
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -34,7 +34,7 @@ const MIN_TIMEOUT = 800;
34
34
  * timeout: ms safety timer (default: max(duration*3, 800))
35
35
  */
36
36
  const useNativeTransition = (defaults = {}) => {
37
- const router = (0, _reactRouterDom.useNavigate)();
37
+ const router = (0, _reactRouter.useNavigate)();
38
38
  const isNative = typeof window !== "undefined" && _NativeBridge.default.isAvailable();
39
39
  const [transitioning, setTransitioning] = (0, _react.useState)(false);
40
40
  const overlayRef = (0, _react.useRef)(null);
@@ -10,6 +10,7 @@ import pc from "picocolors"
10
10
  import fs from "fs"
11
11
  const { cyan, yellow, green } = pc
12
12
 
13
+ import { toMountPathPrefix } from "../vite/resolveDevServerConfig.js"
13
14
  import { validateMiddleware, safeCall } from "./utils/validator.js"
14
15
  import { botDetectionMiddleware } from "./utils/botDetectionMiddleware.js"
15
16
  import { cjsRequire } from "./utils/cjsRequire.js"
@@ -208,6 +209,12 @@ async function createServer() {
208
209
  appType: "custom",
209
210
  root: process.env.src_path,
210
211
  })
212
+
213
+ // Expose Vite's resolved base to the SSR renderer so the URLs it injects
214
+ // (client entry, react-refresh preamble) match the path Vite serves under.
215
+ // Vite normalizes base with a trailing slash; trim it for clean URL joins.
216
+ process.env.APP_MOUNT_PATH = toMountPathPrefix(vite.config.base)
217
+
211
218
  app.use(vite.middlewares)
212
219
  }
213
220
 
@@ -30,7 +30,10 @@ export function Body(props) {
30
30
  __html: `window.__SAFE_AREA_INITIAL__ = ${JSON.stringify(safeArea)}; window.__CATALYST_NATIVE_WEBVIEW__ = ${nativeWebView ? "true" : "false"}`,
31
31
  }}
32
32
  />
33
- {process.env.NODE_ENV === "development" && <script type="module" src="/client/index.js"></script>}
33
+ {process.env.NODE_ENV === "development" && (
34
+ // Base pinned to Vite's resolved base by server/expressServer.js.
35
+ <script type="module" src={`${process.env.APP_MOUNT_PATH || ""}/client/index.js`}></script>
36
+ )}
34
37
  {jsx}
35
38
  <script
36
39
  /* eslint-disable */
@@ -4,7 +4,7 @@ import { Provider } from "react-redux"
4
4
  import { Body } from "./document/Body.jsx"
5
5
  import { Head } from "./document/Head.jsx"
6
6
 
7
- import { StaticRouter } from "react-router-dom/server"
7
+ import { StaticRouter } from "react-router"
8
8
  import ServerRouter from "../../router/ServerRouter.js"
9
9
  import { renderToPipeableStream } from "react-dom/server"
10
10
  import { getUserAgentDetails } from "../utils/userAgentUtil.js"
@@ -6,8 +6,10 @@ const FastRefresh = () => {
6
6
  type="module"
7
7
  // eslint-disable-next-line risxss/catch-potential-xss-react
8
8
  dangerouslySetInnerHTML={{
9
+ // Base-prefixed so the preamble resolves under a sub-path; otherwise
10
+ // plugin-react modules throw "$RefreshSig$ is not defined".
9
11
  __html: `
10
- import { injectIntoGlobalHook } from "/@react-refresh";
12
+ import { injectIntoGlobalHook } from "${process.env.APP_MOUNT_PATH || ""}/@react-refresh";
11
13
  injectIntoGlobalHook(window);
12
14
  window.$RefreshReg$ = () => {};
13
15
  window.$RefreshSig$ = () => (type) => type;
@@ -5,6 +5,34 @@ import { pathToFileURL } from "url"
5
5
  const emptyConfig = {
6
6
  ssrPlugins: [],
7
7
  clientPlugins: [],
8
+ /**
9
+ * devServer lets apps tune the Vite dev server without editing framework
10
+ * internals. All fields are optional and map 1-to-1 onto Vite's `server`
11
+ * config (https://vite.dev/config/server-options).
12
+ *
13
+ * The most common use-case is an app served under a sub-path behind an SSL
14
+ * reverse proxy (nginx/caddy):
15
+ *
16
+ * // buildConfig.js
17
+ * export default {
18
+ * devServer: {
19
+ * // Vite's base path — the single source of truth for the dev mount
20
+ * // path; must match the reverse-proxy location prefix. Defaults to "/".
21
+ * base: "/o-mweb",
22
+ *
23
+ * // HMR WebSocket tuning when the browser connects via the proxy
24
+ * hmr: {
25
+ * clientPort: 443, // port the browser uses (proxy's SSL port)
26
+ * path: "__vite_hmr" // WebSocket pathname (relative to base)
27
+ * },
28
+ *
29
+ * // Vite 6 rejects requests whose Host header isn't localhost/127.0.0.1.
30
+ * // Add your proxy hostname here so the dev server accepts them.
31
+ * allowedHosts: ["local.odinweb.1mg.com"],
32
+ * },
33
+ * }
34
+ */
35
+ devServer: {},
8
36
  }
9
37
 
10
38
  export async function loadCustomViteConfig() {
@@ -308,9 +308,37 @@ function isValidExportName(name) {
308
308
  // Loader-scoped require for discovering CJS exports
309
309
  const loaderRequire = createRequire(import.meta.url)
310
310
 
311
- const CJS_PATTERN = /\b(module\.exports\b|exports\.\w+\s*=)/
312
311
  const REQUIRE_CALL_PATTERN = /\brequire\s*\(/
313
312
 
313
+ /**
314
+ * Decide whether a source file is really CommonJS by parsing it, not by
315
+ * regex-matching the raw text.
316
+ *
317
+ * A regex like /module\.exports\b/ cannot tell code from text, so any file that
318
+ * merely *mentions* module.exports inside a string, comment or template literal
319
+ * was misclassified as CJS. When such a file was real ESM with its own
320
+ * `export default`, the CJS shim appended a second one and Node failed the
321
+ * module with "Duplicate export of 'default'". Docs sites hit this constantly:
322
+ * a Markdown page documenting a CJS config compiles into a string literal in
323
+ * the bundled ESM output. Do not reintroduce source-regex detection.
324
+ *
325
+ * esbuild parses properly and lowers CJS itself, so a file it wraps in its
326
+ * __commonJS helper had no ESM syntax of its own — that is the CJS signal.
327
+ */
328
+ function isCjsSource(source, filePath) {
329
+ try {
330
+ const { code } = transformSync(source, {
331
+ loader: "jsx",
332
+ format: "esm",
333
+ sourcefile: filePath,
334
+ target: "node20",
335
+ })
336
+ return /\bvar __commonJS =/.test(code)
337
+ } catch {
338
+ return false
339
+ }
340
+ }
341
+
314
342
  // Shim __dirname and __filename for ESM app files (these globals only exist in CJS)
315
343
  const createDirnameShim = (source) => {
316
344
  const hasFilename = /\b(?:const|let|var)\s+__filename\b/.test(source)
@@ -396,7 +424,7 @@ export async function load(url, context, defaultLoad) {
396
424
  // We can't use createRequire here because the file lives in a "type":"module"
397
425
  // package — Node's CJS require refuses to load it. Instead, we wrap the
398
426
  // source inline so that `module.exports` becomes the default export.
399
- if (CJS_PATTERN.test(source)) {
427
+ if (isCjsSource(source, filePath)) {
400
428
  const shimmed = [
401
429
  createRequireShim(source),
402
430
  createDirnameShim(source),
@@ -0,0 +1,45 @@
1
+ // Pure resolvers for the Vite dev-server config derived from an app's
2
+ // `buildConfig.devServer`. Side-effect free so the base precedence and the fs
3
+ // allowlist merge can be unit-tested without spinning up Vite.
4
+
5
+ /**
6
+ * Vite `base` for the dev server. `buildConfig.devServer.base` is the single
7
+ * source of truth (default "/"). Normalized to a leading slash and no trailing
8
+ * slash so it doubles as a URL prefix for the SSR renderer.
9
+ */
10
+ export function resolveDevBase(devServer = {}) {
11
+ const base = devServer?.base
12
+ if (!base || base === "/") return "/"
13
+ return `/${String(base).trim().replace(/^\/+|\/+$/g, "")}`
14
+ }
15
+
16
+ /**
17
+ * A resolved base as a URL prefix. Root ("/") becomes "" so
18
+ * `${prefix}/client/index.js` stays single-slashed.
19
+ */
20
+ export function toMountPathPrefix(base) {
21
+ return !base || base === "/" ? "" : String(base).replace(/\/+$/, "")
22
+ }
23
+
24
+ /**
25
+ * fs.allow for the dev server. Framework paths are always present; apps may
26
+ * extend the list via `devServer.fs.allow` but cannot replace it or relax other
27
+ * fs guards (e.g. `strict`), so buildConfig can't widen filesystem exposure.
28
+ */
29
+ export function resolveDevFsAllow(devFs, frameworkPaths) {
30
+ const extra = Array.isArray(devFs?.allow) ? devFs.allow : []
31
+ return [...frameworkPaths, ...extra]
32
+ }
33
+
34
+ /**
35
+ * Vite `server` block from `buildConfig.devServer`. Passthrough keys are spread
36
+ * first so the framework-owned `hmr` and `fs` always win.
37
+ */
38
+ export function buildDevServer(devServer = {}, { frameworkPaths, isProduction = false } = {}) {
39
+ const { base, hmr, fs, ...serverOverrides } = devServer
40
+ return {
41
+ ...serverOverrides,
42
+ hmr: isProduction ? false : (hmr ?? true),
43
+ fs: { allow: resolveDevFsAllow(fs, frameworkPaths) },
44
+ }
45
+ }
@@ -1,7 +1,7 @@
1
1
  import loadEnvironmentVariables from "../scripts/loadEnvironmentVariables.js"
2
2
  loadEnvironmentVariables()
3
3
  import { defineConfig } from "vite"
4
- import baseConfig, { getClientEnvVariables } from "./vite.config.js"
4
+ import { sharedViteConfig as baseConfig, getClientEnvVariables } from "./vite.config.js"
5
5
  import path from "path"
6
6
  import { manifestCategorizationPlugin } from "./manifest-categorization-plugin.js"
7
7
  import { injectCacheKeyPlugin } from "./inject-cache-key-plugin.js"
@@ -2,6 +2,8 @@ import { defineConfig, transformWithEsbuild } from "vite"
2
2
  import react from "@vitejs/plugin-react"
3
3
  import svgr from "vite-plugin-svgr"
4
4
  import { builtinModules, createRequire } from "node:module"
5
+ import { loadCustomViteConfig } from "./loadCustomViteConfig.js"
6
+ import { resolveDevBase, buildDevServer } from "./resolveDevServerConfig.js"
5
7
 
6
8
  /**
7
9
  * Treat all .scss imports as CSS modules (webpack compat).
@@ -286,6 +288,7 @@ const browserOptimizeDeps = [
286
288
  "react-dom",
287
289
  "react-dom/client",
288
290
  "react-redux",
291
+ "react-router",
289
292
  "react-router-dom",
290
293
  "redux",
291
294
  "redux-thunk",
@@ -362,7 +365,10 @@ export const isNodeOnlyExternal = (id) =>
362
365
  id === "@grpc/grpc-js" ||
363
366
  id.startsWith("@opentelemetry/")
364
367
 
365
- export default defineConfig({
368
+ // ─── Shared config ────────────────────────────────────────────────────────────
369
+ // Exported for the production build (vite.config.client.js spreads this in).
370
+ // Does NOT include `base` or `server` — those differ between dev and prod.
371
+ export const sharedViteConfig = {
366
372
  // Parallel `vite build` (SSR + client) must use separate dirs or Vite will block on shared `node_modules/.vite`.
367
373
  cacheDir: path.join(
368
374
  process.env.src_path,
@@ -391,6 +397,7 @@ export default defineConfig({
391
397
  exclude: [
392
398
  "react",
393
399
  "react-dom",
400
+ "react-router",
394
401
  "react-router-dom",
395
402
  "@tata1mg/router",
396
403
  // "@tata1mg/slowoi-react",
@@ -422,8 +429,15 @@ export default defineConfig({
422
429
  resolve: {
423
430
  alias: alias(),
424
431
  // Ensure only one copy of React-related packages is used (prevents
425
- // duplicate instances from hoisted/linked packages in monorepos)
426
- dedupe: ["react", "react-dom", "react-router-dom", "@tata1mg/slowboi-react"],
432
+ // duplicate instances from hoisted/linked packages in monorepos).
433
+ // NOTE: react-router deliberately excluded from dedupe. Consumer apps
434
+ // that still have an older react-router (e.g. v6, hoisted transitively
435
+ // through another dependency) at their own node_modules root would
436
+ // have that copy forced onto catalyst-core's v7 import too, silently
437
+ // resolving to the wrong package version (wrong export list, e.g.
438
+ // missing StaticRouter/Link) instead of an error. See tata1mg/1mg_web
439
+ // fix/react-router-v7-upgrade-staging for the confirmed repro.
440
+ dedupe: ["react", "react-dom", "react-router-dom"],
427
441
  },
428
442
  define: {
429
443
  ...getClientEnvVariables(),
@@ -488,17 +502,34 @@ export default defineConfig({
488
502
  "**/*.woff2",
489
503
  ],
490
504
 
491
- // Server configuration
492
- server: {
493
- hmr: !isProduction,
494
- fs: {
495
- allow: [process.env.src_path, __dirname],
496
- },
497
- },
498
-
499
505
  // Preview configuration for production preview
500
506
  preview: {
501
507
  port: process.env.NODE_SERVER_PORT ? parseInt(process.env.NODE_SERVER_PORT) : 3005,
502
508
  host: process.env.NODE_SERVER_HOSTNAME || "localhost",
503
509
  },
510
+ }
511
+
512
+ // ─── Dev server config ────────────────────────────────────────────────────────
513
+ // Async factory so it can read the app's buildConfig.devServer for `base`, HMR
514
+ // and other Vite server options. `base` comes solely from devServer.base; the
515
+ // resolved value is propagated to the SSR renderer by server/expressServer.js.
516
+ // The production build (vite.config.client.js) ignores this and spreads
517
+ // sharedViteConfig directly.
518
+ export default defineConfig(async () => {
519
+ const { devServer = {}, ssrPlugins = [] } = await loadCustomViteConfig()
520
+
521
+ return {
522
+ ...sharedViteConfig,
523
+ base: resolveDevBase(devServer),
524
+ server: buildDevServer(devServer, {
525
+ frameworkPaths: [process.env.src_path, __dirname],
526
+ isProduction,
527
+ }),
528
+ // Apply the app's SSR plugins in dev too — the production build already
529
+ // does this in vite.config.server.js. This lets buildConfig.ssrPlugins
530
+ // inject SSR-only config (e.g. a plugin whose `config()` hook adds
531
+ // ssr.noExternal to bundle React-consuming deps so dev SSR shares the
532
+ // app's single React instance) without hardcoding anything in the framework.
533
+ plugins: [...(sharedViteConfig.plugins || []), ...ssrPlugins],
534
+ }
504
535
  })
@@ -1,7 +1,11 @@
1
1
  import loadEnvironmentVariables from "../scripts/loadEnvironmentVariables.js"
2
2
  loadEnvironmentVariables()
3
3
  import { defineConfig } from "vite"
4
- import baseConfig, { getClientEnvVariables, isNodeOnlyExternal } from "./vite.config.js"
4
+ // Spread the static shared config (a plain object), NOT the default export —
5
+ // the default export is now an async dev-server factory function, and spreading
6
+ // a function would silently drop cacheDir, resolve aliases, css, plugins and
7
+ // build from the production SSR bundle. Mirrors vite.config.client.js.
8
+ import { sharedViteConfig as baseConfig, getClientEnvVariables, isNodeOnlyExternal } from "./vite.config.js"
5
9
  import path from "path"
6
10
  import { fileURLToPath } from "url"
7
11
  import { dirname } from "path"
@@ -3,7 +3,7 @@ import { HelmetProvider, Helmet } from "react-helmet-async"
3
3
  import { OneMgRouterContext } from "../context.jsx"
4
4
  import { useRouterData } from "./RouterDataProvider.jsx"
5
5
  import { deleteHeadTagsByDataAttribute, getMetaData } from "../utils/metaDataUtils.jsx"
6
- import { useLocation } from "react-router-dom"
6
+ import { useLocation } from "react-router"
7
7
 
8
8
  /**
9
9
  * @description renders meta tags component assigned to setMetaData
@@ -7,7 +7,7 @@ import {
7
7
  useSearchParams,
8
8
  UNSAFE_RouteContext,
9
9
  matchRoutes,
10
- } from "react-router-dom"
10
+ } from "react-router"
11
11
  import { OneMgRouterContext } from "../context.jsx"
12
12
  // import sanitizeHtml from "sanitize-html"
13
13
 
@@ -41,15 +41,15 @@ const INITIAL_DATA_STATE = {
41
41
  /**
42
42
  * @typedef RouterFetcherProps
43
43
  * @property {any} route route object
44
- * @property {import("react-router-dom").Location} location the current location object, which represents the current URL in web browsers.
45
- * @property {import("react-router-dom").Params} params object of key/value pairs of the dynamic params from the current URL that were matched by the route path.
44
+ * @property {import("react-router").Location} location the current location object, which represents the current URL in web browsers.
45
+ * @property {import("react-router").Params} params object of key/value pairs of the dynamic params from the current URL that were matched by the route path.
46
46
  * @property {URLSearchParams} searchParams search parameters via URLSearchParams interface.
47
- * @property {import("react-router-dom").NavigateFunction} navigate function to navigate to other pages based on response.
47
+ * @property {import("react-router").NavigateFunction} navigate function to navigate to other pages based on response.
48
48
  */
49
49
 
50
50
  /**
51
51
  * @typedef ServerFetchDataProps
52
- * @property {import("react-router-dom").RouteObject[]} routes routes Array
52
+ * @property {import("react-router").RouteObject[]} routes routes Array
53
53
  * @property {string} url current url
54
54
  * @property {import("express").Request} req Express request object
55
55
  */
@@ -139,7 +139,7 @@ const fetchRouteData = async (routerProps, fetcherArgs, refetchArgs) => {
139
139
  /**
140
140
  *
141
141
  * @param {RouteContextObject} routeContext route context object
142
- * @returns {import('react-router-dom').RouteMatch[]} Array of matched routes
142
+ * @returns {import('react-router').RouteMatch[]} Array of matched routes
143
143
  */
144
144
  const getMatchedRoutes = ({ matches, outlet }) => {
145
145
  if (outlet) {
@@ -150,7 +150,7 @@ const getMatchedRoutes = ({ matches, outlet }) => {
150
150
 
151
151
  /**
152
152
  * Generates route key for given route using pathname and query params
153
- * @param {import('react-router-dom').RouteMatch} match Router Match Object
153
+ * @param {import('react-router').RouteMatch} match Router Match Object
154
154
  * @param {string} searchParamsString Query params string
155
155
  * @returns {string} routerKey
156
156
  */
@@ -190,7 +190,7 @@ export const RouterDataProvider = ({ children, initialState, fetcherArgs = {}, c
190
190
 
191
191
  /**
192
192
  * @description HOF which returns a function to refetch the route data
193
- * @param {import("react-router-dom").RouteObject} route
193
+ * @param {import("react-router").RouteObject} route
194
194
  * @param {string} routeKey
195
195
  * @returns
196
196
  */
@@ -214,7 +214,7 @@ export const RouterDataProvider = ({ children, initialState, fetcherArgs = {}, c
214
214
 
215
215
  /**
216
216
  * @description HOF which returns a function to clear the route data immediately or after given time in ms
217
- * @param {import("react-router-dom").RouteObject} route
217
+ * @param {import("react-router").RouteObject} route
218
218
  * @param {string} routeKey
219
219
  * @returns
220
220
  */
@@ -235,7 +235,7 @@ export const RouterDataProvider = ({ children, initialState, fetcherArgs = {}, c
235
235
 
236
236
  /**
237
237
  * @description Check the config for refetching the data
238
- * @param {import('react-router-dom').RouteObject} route
238
+ * @param {import('react-router').RouteObject} route
239
239
  * @returns {boolean}
240
240
  */
241
241
  const shouldFetch = (route) => {
@@ -2,7 +2,7 @@ import { createContext } from "react"
2
2
 
3
3
  /**
4
4
  * @typedef oneMgRouterContext
5
- * @property {import("react-router-dom").RouteMatch<string, import("react-router-dom").RouteObject>[]=} matchedRoutes
5
+ * @property {import("react-router").RouteMatch<string, import("react-router").RouteObject>[]=} matchedRoutes
6
6
  */
7
7
 
8
8
  /**
@@ -1,4 +1,4 @@
1
- import { useNavigate, useLocation } from "react-router-dom"
1
+ import { useNavigate, useLocation } from "react-router"
2
2
 
3
3
  /**
4
4
  * @param {number} delay
@@ -47,11 +47,11 @@ export type RouterFetcherProps = {
47
47
  /**
48
48
  * the current location object, which represents the current URL in web browsers.
49
49
  */
50
- location: import("react-router-dom").Location
50
+ location: import("react-router").Location
51
51
  /**
52
52
  * object of key/value pairs of the dynamic params from the current URL that were matched by the route path.
53
53
  */
54
- params: import("react-router-dom").Params
54
+ params: import("react-router").Params
55
55
  /**
56
56
  * search parameters via URLSearchParams interface.
57
57
  */
@@ -59,13 +59,13 @@ export type RouterFetcherProps = {
59
59
  /**
60
60
  * function to navigate to other pages based on response.
61
61
  */
62
- navigate: import("react-router-dom").NavigateFunction
62
+ navigate: import("react-router").NavigateFunction
63
63
  }
64
64
  export type ServerFetchDataProps = {
65
65
  /**
66
66
  * routes Array
67
67
  */
68
- routes: import("react-router-dom").RouteObject[]
68
+ routes: import("react-router").RouteObject[]
69
69
  /**
70
70
  * current url
71
71
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef oneMgRouterContext
3
- * @property {import("react-router-dom").RouteMatch<string, import("react-router-dom").RouteObject>[]=} matchedRoutes
3
+ * @property {import("react-router").RouteMatch<string, import("react-router").RouteObject>[]=} matchedRoutes
4
4
  */
5
5
  /**
6
6
  * @description This is internal context made for sharing data among router components
@@ -9,6 +9,6 @@
9
9
  export const OneMgRouterContext: import("react").Context<oneMgRouterContext>
10
10
  export type oneMgRouterContext = {
11
11
  matchedRoutes?:
12
- | import("react-router-dom").RouteMatch<string, import("react-router-dom").RouteObject>[]
12
+ | import("react-router").RouteMatch<string, import("react-router").RouteObject>[]
13
13
  | undefined
14
14
  }
@@ -1 +1 @@
1
- export function useNavigateWithTransition(): import("react-router-dom").NavigateFunction
1
+ export function useNavigateWithTransition(): import("react-router").NavigateFunction
@@ -1,3 +1,3 @@
1
- export * from "react-router-dom"
1
+ export * from "react-router"
2
2
  declare const _default: any
3
3
  export default _default
@@ -22,3 +22,19 @@ export function deleteHeadTagsByDataAttribute(attributeName: string, attributeVa
22
22
  * @returns {Array<JSX.Element>} - List of all meta tags for the matched location.
23
23
  */
24
24
  export function getMetaData(matchedRoutes: Array<Match>, routeData: Object): Array<JSX.Element>
25
+ /**
26
+ * Static function defined on a route component that returns metadata elements
27
+ * to be rendered in the document head for that route.
28
+ * Must return an array of JSX elements (e.g. <title>, <meta>), NOT a plain
29
+ * object. Returning an object fails silently: no error is thrown, but the
30
+ * tags will not appear in the rendered <head>.
31
+ *
32
+ * @param {any} fetcherData - Data returned from the route's fetcher (serverFetcher/clientFetcher).
33
+ * @returns {Array<JSX.Element>} - Array of JSX head elements (title, meta, link, etc).
34
+ *
35
+ * @example
36
+ * HomePage.setMetaData = (apiResponse) => [
37
+ * <title key="title">{apiResponse?.title || "Home"}</title>,
38
+ * ];
39
+ */
40
+ export type SetMetaData = (fetcherData: any) => Array<JSX.Element>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catalyst-core",
3
- "version": "0.3.0-beta.3",
3
+ "version": "0.3.0-beta.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.jsx",
6
6
  "description": "Web framework that provides great performance out of the box",
@@ -30,6 +30,7 @@
30
30
  "scripts": {
31
31
  "lint": "eslint .",
32
32
  "lint-staged": "lint-staged",
33
+ "test:unit": "node --test \"test/**/*.test.js\"",
33
34
  "prettify": "prettier . --write",
34
35
  "prepare": "node ../../scripts/safe-rimraf.js ./dist && cp -r ./src ./dist && babel src/native --out-dir dist/native --config-file ./babel.native.config.cjs --ignore '**/.build/**' && babel src/sentry.js --out-file dist/sentry.cjs --config-file ./babel.native.config.cjs",
35
36
  "prepublishOnly": "npm run prepare",
@@ -49,7 +50,7 @@
49
50
  "react-helmet-async": "^1.3.0",
50
51
  "react-redux": "^9.2.0",
51
52
  "react-refresh": "^0.14.0",
52
- "react-router-dom": "^6.16.0",
53
+ "react-router": "^7.18.2",
53
54
  "redux": "^4.2.1",
54
55
  "sanitize-html": "^2.17.6",
55
56
  "sass": "^1.89.2",
@@ -0,0 +1,113 @@
1
+ import { test } from "node:test"
2
+ import assert from "node:assert/strict"
3
+
4
+ import {
5
+ resolveDevBase,
6
+ toMountPathPrefix,
7
+ resolveDevFsAllow,
8
+ buildDevServer,
9
+ } from "../../src/vite/resolveDevServerConfig.js"
10
+
11
+ // ─── resolveDevBase ───────────────────────────────────────────────────────────
12
+
13
+ test("resolveDevBase: resolves from devServer.base", () => {
14
+ assert.equal(resolveDevBase({ base: "/o-mweb" }), "/o-mweb")
15
+ })
16
+
17
+ test("resolveDevBase: defaults to '/' when base is unset", () => {
18
+ assert.equal(resolveDevBase({}), "/")
19
+ assert.equal(resolveDevBase(undefined), "/")
20
+ assert.equal(resolveDevBase({ base: "/" }), "/")
21
+ })
22
+
23
+ test("resolveDevBase: normalizes to a leading slash, no trailing slash", () => {
24
+ assert.equal(resolveDevBase({ base: "o-mweb" }), "/o-mweb")
25
+ assert.equal(resolveDevBase({ base: "/o-mweb/" }), "/o-mweb")
26
+ assert.equal(resolveDevBase({ base: " /o-mweb// " }), "/o-mweb")
27
+ })
28
+
29
+ // ─── toMountPathPrefix ────────────────────────────────────────────────────────
30
+
31
+ test("toMountPathPrefix: root maps to empty string for clean URL joins", () => {
32
+ assert.equal(toMountPathPrefix("/"), "")
33
+ assert.equal(toMountPathPrefix(""), "")
34
+ assert.equal(toMountPathPrefix(undefined), "")
35
+ })
36
+
37
+ test("toMountPathPrefix: trims Vite's trailing slash", () => {
38
+ assert.equal(toMountPathPrefix("/o-mweb/"), "/o-mweb")
39
+ assert.equal(toMountPathPrefix("/o-mweb"), "/o-mweb")
40
+ // The joined result stays single-slashed.
41
+ assert.equal(`${toMountPathPrefix("/o-mweb/")}/client/index.js`, "/o-mweb/client/index.js")
42
+ assert.equal(`${toMountPathPrefix("/")}/client/index.js`, "/client/index.js")
43
+ })
44
+
45
+ // ─── resolveDevFsAllow: security-sensitive allowlist merge ─────────────────────
46
+
47
+ const FRAMEWORK = ["/app/src", "/framework/dist/vite"]
48
+
49
+ test("resolveDevFsAllow: framework paths always present, none provided", () => {
50
+ assert.deepEqual(resolveDevFsAllow(undefined, FRAMEWORK), FRAMEWORK)
51
+ assert.deepEqual(resolveDevFsAllow({}, FRAMEWORK), FRAMEWORK)
52
+ })
53
+
54
+ test("resolveDevFsAllow: apps may EXTEND but never drop framework paths", () => {
55
+ const result = resolveDevFsAllow({ allow: ["/extra/pkg"] }, FRAMEWORK)
56
+ assert.deepEqual(result, ["/app/src", "/framework/dist/vite", "/extra/pkg"])
57
+ })
58
+
59
+ test("resolveDevFsAllow: non-array allow is ignored (no widening via bad input)", () => {
60
+ assert.deepEqual(resolveDevFsAllow({ allow: "/" }, FRAMEWORK), FRAMEWORK)
61
+ assert.deepEqual(resolveDevFsAllow({ allow: null }, FRAMEWORK), FRAMEWORK)
62
+ })
63
+
64
+ // ─── buildDevServer: precedence + security ────────────────────────────────────
65
+
66
+ test("buildDevServer: hmr defaults to true in dev", () => {
67
+ const server = buildDevServer({}, { frameworkPaths: FRAMEWORK, isProduction: false })
68
+ assert.equal(server.hmr, true)
69
+ })
70
+
71
+ test("buildDevServer: hmr is false in production", () => {
72
+ const server = buildDevServer({ hmr: { clientPort: 443 } }, { frameworkPaths: FRAMEWORK, isProduction: true })
73
+ assert.equal(server.hmr, false)
74
+ })
75
+
76
+ test("buildDevServer: app hmr overrides are honored in dev", () => {
77
+ const hmr = { clientPort: 443, path: "__vite_hmr" }
78
+ const server = buildDevServer({ hmr }, { frameworkPaths: FRAMEWORK, isProduction: false })
79
+ assert.deepEqual(server.hmr, hmr)
80
+ })
81
+
82
+ test("buildDevServer: passthrough keys (proxy/allowedHosts) are forwarded", () => {
83
+ const server = buildDevServer(
84
+ { allowedHosts: ["local.example.com"], proxy: { "/api": "http://localhost:9000" } },
85
+ { frameworkPaths: FRAMEWORK, isProduction: false }
86
+ )
87
+ assert.deepEqual(server.allowedHosts, ["local.example.com"])
88
+ assert.deepEqual(server.proxy, { "/api": "http://localhost:9000" })
89
+ })
90
+
91
+ test("buildDevServer: app fs cannot replace the allowlist, only extend it", () => {
92
+ const server = buildDevServer(
93
+ { fs: { allow: ["/extra"] } },
94
+ { frameworkPaths: FRAMEWORK, isProduction: false }
95
+ )
96
+ assert.deepEqual(server.fs.allow, ["/app/src", "/framework/dist/vite", "/extra"])
97
+ })
98
+
99
+ test("buildDevServer: app cannot relax fs.strict or clobber fs via overrides", () => {
100
+ const server = buildDevServer(
101
+ { fs: { allow: ["/"], strict: false } },
102
+ { frameworkPaths: FRAMEWORK, isProduction: false }
103
+ )
104
+ // strict is dropped entirely (framework keeps Vite's secure default), and
105
+ // the framework paths remain — "/" is merely appended, not a replacement.
106
+ assert.equal(server.fs.strict, undefined)
107
+ assert.ok(server.fs.allow.includes("/app/src"))
108
+ })
109
+
110
+ test("buildDevServer: 'base' is not leaked into the server block", () => {
111
+ const server = buildDevServer({ base: "/o-mweb" }, { frameworkPaths: FRAMEWORK, isProduction: false })
112
+ assert.equal(server.base, undefined)
113
+ })