@arcote.tech/platform 0.5.2 → 0.5.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/platform",
3
3
  "type": "module",
4
- "version": "0.5.2",
4
+ "version": "0.5.5",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc Platform — module system, router, layout, theme, i18n, platform app shell",
@@ -18,11 +18,11 @@
18
18
  "type-check": "tsc --noEmit"
19
19
  },
20
20
  "dependencies": {
21
- "@arcote.tech/arc-ds": "^0.5.2",
22
- "@arcote.tech/arc-react": "^0.5.2"
21
+ "@arcote.tech/arc-ds": "^0.5.5",
22
+ "@arcote.tech/arc-react": "^0.5.5"
23
23
  },
24
24
  "peerDependencies": {
25
- "@arcote.tech/arc": "^0.5.2",
25
+ "@arcote.tech/arc": "^0.5.5",
26
26
  "@lingui/core": "^5.0.0",
27
27
  "@lingui/react": "^5.0.0",
28
28
  "framer-motion": "^12.0.0",
package/src/index.ts CHANGED
@@ -109,6 +109,7 @@ export type {
109
109
  ArcFragment,
110
110
  ArcLayoutComponent,
111
111
  ArcModule,
112
+ BuildManifest,
112
113
  BuiltModule,
113
114
  ContextElementFragment,
114
115
  ModuleAccess,
@@ -3,11 +3,26 @@ import type { PageFragment } from "../types";
3
3
  import { useArcNavigate, useArcRoute } from "../router";
4
4
  import { SubNavShell } from "@arcote.tech/arc-ds";
5
5
 
6
+ /**
7
+ * Decide whether a child page should be shown as a tab in the sub-nav.
8
+ *
9
+ * Rules:
10
+ * - Explicit `nav: false` always hides the tab.
11
+ * - Parameterized paths (e.g. `/strategy/assistant/:stage`) are hidden by
12
+ * default — the literal path with `:param` isn't navigable.
13
+ * - Otherwise auto-show iff the page has both `icon` and `label`.
14
+ */
15
+ function shouldShowTab(page: PageFragment): boolean {
16
+ if (page.nav === false) return false;
17
+ if (page.path.includes("/:")) return false;
18
+ return Boolean(page.icon && page.label);
19
+ }
20
+
6
21
  export function PageSubNavShell({ children, tabs }: { children: ReactNode; tabs: readonly PageFragment[] }) {
7
22
  const navigate = useArcNavigate();
8
23
  const route = useArcRoute();
9
24
 
10
- const subNavTabs = tabs.map((t) => ({
25
+ const subNavTabs = tabs.filter(shouldShowTab).map((t) => ({
11
26
  path: t.path,
12
27
  icon: t.icon,
13
28
  label: t.label,
@@ -19,6 +19,7 @@ const slotDisplayModes: Record<string, DisplayMode> = {
19
19
  "main-content": "default",
20
20
  "sidebar-left": "default",
21
21
  "sidebar-right": "default",
22
+ "preview-pane": "default",
22
23
  };
23
24
 
24
25
  export function SlotRenderer({
@@ -1,17 +1,23 @@
1
1
  import { clearModules, getAllRegisteredModules, getContext, setActiveModules } from "./registry";
2
- import type { ModuleDescriptor } from "./types";
2
+ import type { BuildManifest, ModuleDescriptor } from "./types";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
 
5
- export interface ModuleManifest {
6
- modules: ModuleDescriptor[];
7
- buildTime: string;
8
- }
5
+ /** @deprecated Use BuildManifest from "./types" */
6
+ export type ModuleManifest = BuildManifest;
9
7
 
10
8
  export type ModuleLoaderState = "loading" | "ready" | "error";
11
9
 
10
+ /**
11
+ * URL for a module's JS file with cache-bust based on its content hash.
12
+ * When a module's bytes change, hash changes → URL changes → ES module cache invalidated.
13
+ * For explicit full reloads, pass `bust` (e.g. timestamp) to override.
14
+ */
12
15
  function moduleUrl(baseUrl: string, mod: ModuleDescriptor, bust?: string): string {
13
16
  const base = mod.url ?? `${baseUrl}/modules/${mod.file}`;
14
- return bust ? `${base}${base.includes("?") ? "&" : "?"}t=${bust}` : base;
17
+ const busterKey = bust ? "t" : "v";
18
+ const busterVal = bust ?? mod.hash;
19
+ if (!busterVal) return base;
20
+ return `${base}${base.includes("?") ? "&" : "?"}${busterKey}=${busterVal}`;
15
21
  }
16
22
 
17
23
  /** Read all persisted arc tokens from localStorage */
@@ -50,7 +56,7 @@ function buildHeaders(tokens?: Record<string, string>): HeadersInit {
50
56
  async function fetchManifest(
51
57
  baseUrl: string,
52
58
  tokens?: Record<string, string>,
53
- ): Promise<ModuleManifest> {
59
+ ): Promise<BuildManifest> {
54
60
  const headers = buildHeaders(tokens);
55
61
  const res = await fetch(`${baseUrl}/api/modules`, { headers });
56
62
  if (!res.ok)
@@ -58,45 +64,54 @@ async function fetchManifest(
58
64
  return res.json();
59
65
  }
60
66
 
67
+ /** Track hashes of modules we've already imported so hot-swaps re-import on change. */
68
+ const importedModuleHashes = new Map<string, string>();
69
+
61
70
  /**
62
71
  * Load all modules from manifest. Each module auto-registers via module().build().
63
72
  */
64
73
  export async function loadModules(
65
74
  baseUrl: string,
66
75
  tokens?: Record<string, string>,
67
- ): Promise<ModuleManifest> {
76
+ ): Promise<BuildManifest> {
68
77
  const manifest = await fetchManifest(baseUrl, tokens);
69
78
 
70
79
  await Promise.all(
71
- manifest.modules.map(
72
- (mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod)),
73
- ),
80
+ manifest.modules.map((mod) => {
81
+ importedModuleHashes.set(mod.name, mod.hash);
82
+ return import(/* @vite-ignore */ moduleUrl(baseUrl, mod));
83
+ }),
74
84
  );
75
85
 
76
86
  return manifest;
77
87
  }
78
88
 
79
89
  /**
80
- * Sync modules with server import any NEW modules, then set active filter.
81
- * Modules are never unregistered (ES module cache prevents re-execution).
82
- * Instead, visibility is controlled via an active set.
90
+ * Sync modules with server. Two triggers produce different behaviors:
91
+ * - new module (name unseen) dynamic import
92
+ * - hash changed (hot-swap after arc platform deploy) → dynamic import with new cache-bust
93
+ * Unchanged modules stay cached. Visibility is controlled via an active set.
83
94
  */
84
95
  export async function syncModules(
85
96
  baseUrl: string,
86
97
  tokens?: Record<string, string>,
87
- ): Promise<ModuleManifest> {
98
+ ): Promise<BuildManifest> {
88
99
  const manifest = await fetchManifest(baseUrl, tokens);
89
100
 
90
101
  const manifestNames = new Set(manifest.modules.map((m) => m.name));
91
- // Check against ALL registered modules (not just active ones)
92
- const registeredNames = new Set(getAllRegisteredModules().map((m) => m.name));
93
102
 
94
- // Import only truly new modules (never imported before)
95
- const newModules = manifest.modules.filter((m) => !registeredNames.has(m.name));
103
+ // A module needs reimporting if it's new OR its hash changed since last import
104
+ const toImport = manifest.modules.filter((m) => {
105
+ const prevHash = importedModuleHashes.get(m.name);
106
+ return prevHash !== m.hash;
107
+ });
96
108
 
97
- if (newModules.length > 0) {
109
+ if (toImport.length > 0) {
98
110
  await Promise.all(
99
- newModules.map((mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod))),
111
+ toImport.map((mod) => {
112
+ importedModuleHashes.set(mod.name, mod.hash);
113
+ return import(/* @vite-ignore */ moduleUrl(baseUrl, mod));
114
+ }),
100
115
  );
101
116
  }
102
117
 
package/src/types.ts CHANGED
@@ -102,9 +102,21 @@ export interface ModuleAccess {
102
102
  export interface ModuleDescriptor {
103
103
  readonly file: string;
104
104
  readonly name: string;
105
+ /** sha256 hex of the bundled .js content — used by deploy diff and client cache-bust. */
106
+ readonly hash: string;
105
107
  readonly url?: string;
106
108
  }
107
109
 
110
+ /** Build manifest written to .arc/platform/modules/manifest.json. */
111
+ export interface BuildManifest {
112
+ readonly modules: readonly ModuleDescriptor[];
113
+ /** sha256 hex over all shell bundle outputs concatenated. */
114
+ readonly shellHash: string;
115
+ /** sha256 hex over styles.css (+ theme.css if present). */
116
+ readonly stylesHash: string;
117
+ readonly buildTime: string;
118
+ }
119
+
108
120
  // ---------------------------------------------------------------------------
109
121
  // Moduł
110
122
  // ---------------------------------------------------------------------------