@arcote.tech/platform 0.5.2 → 0.5.6
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 +4 -4
- package/src/index.ts +1 -0
- package/src/layout/page-sub-nav-shell.tsx +16 -1
- package/src/layout/slot-renderer.tsx +1 -0
- package/src/module-loader.ts +36 -21
- package/src/types.ts +12 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/platform",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.6",
|
|
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.
|
|
22
|
-
"@arcote.tech/arc-react": "^0.5.
|
|
21
|
+
"@arcote.tech/arc-ds": "^0.5.6",
|
|
22
|
+
"@arcote.tech/arc-react": "^0.5.6"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@arcote.tech/arc": "^0.5.
|
|
25
|
+
"@arcote.tech/arc": "^0.5.6",
|
|
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
|
@@ -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,
|
package/src/module-loader.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
76
|
+
): Promise<BuildManifest> {
|
|
68
77
|
const manifest = await fetchManifest(baseUrl, tokens);
|
|
69
78
|
|
|
70
79
|
await Promise.all(
|
|
71
|
-
manifest.modules.map(
|
|
72
|
-
(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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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<
|
|
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
|
-
//
|
|
95
|
-
const
|
|
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 (
|
|
109
|
+
if (toImport.length > 0) {
|
|
98
110
|
await Promise.all(
|
|
99
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|