akanjs 3.0.0-beta.22 → 3.0.0-beta.24
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/client/csrTypes.ts +27 -1
- package/client/native.ts +3 -0
- package/client/route/RouteDefinition.ts +41 -1
- package/fetch/client/wsClient.ts +6 -1
- package/package.json +16 -6
- package/server/akanApp.ts +2 -3
- package/server/artifact/builderRpc.ts +5 -0
- package/server/artifact/ipcTypes.ts +10 -0
- package/server/hmr/clientScript.ts +31 -2
- package/server/hmr/csrDevManifest.ts +25 -0
- package/server/hmr/csrDevRuntime.ts +474 -0
- package/server/hmr/csrDevShell.ts +111 -0
- package/server/hmr/csrDevSourceMap.ts +142 -0
- package/server/hmr/devHmrController.ts +33 -16
- package/server/hmr/runtimeMetadataFile.ts +17 -0
- package/server/hmr/wsHub.ts +30 -4
- package/server/resolver/signal.resolver.ts +11 -5
- package/server/routing/apiRouter.ts +2 -0
- package/server/types.tsx +2 -0
- package/server/vendor/react-dom.ts +1 -0
- package/server/vendor/react-refresh-runtime.ts +13 -1
- package/server/vendor/react.ts +2 -0
- package/server/webRouter.ts +32 -2
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +3 -3
- package/store/agent/ScreenTarget.ts +1 -2
- package/store/agent/StoreSurfaceSource.ts +3 -1
- package/store/liveInsert.ts +35 -13
- package/store/storeInstance.ts +28 -21
- package/types/client/csrTypes.d.ts +22 -1
- package/types/client/native.d.ts +1 -0
- package/types/client/route/RouteDefinition.d.ts +14 -0
- package/types/server/artifact/builderRpc.d.ts +2 -1
- package/types/server/artifact/ipcTypes.d.ts +11 -0
- package/types/server/hmr/csrDevManifest.d.ts +18 -0
- package/types/server/hmr/csrDevRuntime.d.ts +71 -0
- package/types/server/hmr/csrDevShell.d.ts +14 -0
- package/types/server/hmr/csrDevSourceMap.d.ts +17 -0
- package/types/server/hmr/devHmrController.d.ts +2 -1
- package/types/server/hmr/runtimeMetadataFile.d.ts +1 -0
- package/types/server/hmr/wsHub.d.ts +11 -0
- package/types/server/routing/apiRouter.d.ts +1 -0
- package/types/server/types.d.ts +2 -0
- package/types/server/vendor/react-dom.d.ts +1 -1
- package/types/server/vendor/react-refresh-runtime.d.ts +1 -1
- package/types/server/vendor/react.d.ts +1 -1
- package/types/store/storeInstance.d.ts +4 -3
- package/types/ui/System/Client.d.ts +5 -3
- package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +17 -0
- package/types/vendor/@akanjs/native/plugins/app-state/src/index.d.ts +6 -0
- package/types/vendor/use-agentic/AgentActivity.d.ts +11 -0
- package/types/vendor/use-agentic/AgentGate.d.ts +10 -0
- package/types/vendor/use-agentic/AgenticSurface.d.ts +15 -5
- package/types/vendor/use-agentic/index.d.ts +2 -0
- package/types/vendor/use-agentic/surfaceContext.d.ts +3 -0
- package/types/webkit/CsrRouteTable.d.ts +32 -0
- package/types/webkit/CsrStack.d.ts +45 -0
- package/types/webkit/RenderLayer.d.ts +2 -2
- package/types/webkit/bootCsr.d.ts +7 -8
- package/types/webkit/index.d.ts +4 -1
- package/types/webkit/nativeNavigation.d.ts +13 -0
- package/types/webkit/useCsrValues.d.ts +6 -4
- package/types/webkit/useFetch.d.ts +3 -1
- package/types/webkit/useHistory.d.ts +5 -1
- package/types/webkit/usePageActivity.d.ts +2 -0
- package/types/webkit/usePageFocusEffect.d.ts +7 -0
- package/types/webkit/usePageLocation.d.ts +13 -0
- package/ui/Data/Dashboard.tsx +2 -2
- package/ui/Data/ListContainer.tsx +2 -2
- package/ui/Layout/BottomInset.tsx +1 -1
- package/ui/Layout/Navbar.tsx +1 -1
- package/ui/Layout/TopInset.tsx +4 -3
- package/ui/System/CSR.tsx +40 -79
- package/ui/System/Client.tsx +28 -13
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +53 -9
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativePlugin.kt +16 -0
- package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +20 -0
- package/vendor/@akanjs/native/plugins/app/native-plugin.json +4 -4
- package/vendor/@akanjs/native/plugins/app/src/desktop.ts +3 -0
- package/vendor/@akanjs/native/plugins/app/src/index.ts +14 -2
- package/vendor/@akanjs/native/plugins/app-state/android/AppStatePlugin.kt +38 -2
- package/vendor/@akanjs/native/plugins/app-state/ios/AppStatePlugin.swift +19 -0
- package/vendor/@akanjs/native/plugins/app-state/native-plugin.json +1 -1
- package/vendor/@akanjs/native/plugins/app-state/src/index.ts +6 -1
- package/vendor/use-agentic/AgentActivity.tsx +26 -0
- package/vendor/use-agentic/AgentGate.ts +25 -0
- package/vendor/use-agentic/AgenticSurface.ts +92 -34
- package/vendor/use-agentic/index.ts +2 -0
- package/vendor/use-agentic/surfaceContext.ts +3 -0
- package/webkit/CsrRouteTable.ts +298 -0
- package/webkit/CsrStack.ts +98 -0
- package/webkit/RenderLayer.tsx +30 -8
- package/webkit/bootCsr.tsx +13 -217
- package/webkit/index.ts +4 -1
- package/webkit/nativeNavigation.ts +78 -5
- package/webkit/useCsrValues.ts +118 -22
- package/webkit/useFetch.ts +9 -2
- package/webkit/useHistory.ts +23 -6
- package/webkit/useLocation.ts +6 -3
- package/webkit/usePageActivity.ts +6 -0
- package/webkit/usePageFocusEffect.ts +13 -0
- package/webkit/usePageLocation.ts +17 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
interface RefreshRuntime {
|
|
2
|
+
injectIntoGlobalHook(host: object): void;
|
|
3
|
+
register(type: unknown, id: string): void;
|
|
4
|
+
performReactRefresh(): unknown;
|
|
5
|
+
createSignatureFunctionForTransform(): (type: unknown, ...rest: unknown[]) => unknown;
|
|
6
|
+
isLikelyComponentType(value: unknown): boolean;
|
|
7
|
+
getFamilyByType(value: unknown): unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CsrAcceptCallback = (updated: string[]) => void;
|
|
11
|
+
|
|
12
|
+
export interface CsrHotContext {
|
|
13
|
+
readonly data: Record<string, unknown> | undefined;
|
|
14
|
+
accept(deps?: string | string[], callback?: CsrAcceptCallback): void;
|
|
15
|
+
dispose(callback: (data: Record<string, unknown>) => void): void;
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CsrModuleRecord {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
exports: unknown;
|
|
22
|
+
readonly hot: CsrHotContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CsrModuleFactory = (
|
|
26
|
+
this: unknown,
|
|
27
|
+
require: (specifier: string) => unknown,
|
|
28
|
+
module: CsrModuleRecord,
|
|
29
|
+
exports: unknown,
|
|
30
|
+
$RefreshReg$: (type: unknown, localId: string) => void,
|
|
31
|
+
$RefreshSig$: () => (type: unknown, ...rest: unknown[]) => unknown,
|
|
32
|
+
__akanImport: (specifier: string) => Promise<unknown>,
|
|
33
|
+
) => void;
|
|
34
|
+
|
|
35
|
+
export interface CsrUpdateMessage {
|
|
36
|
+
generation: number;
|
|
37
|
+
url?: string | null;
|
|
38
|
+
reload?: boolean;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CsrDevRuntimeApi {
|
|
43
|
+
readonly generation: number;
|
|
44
|
+
define(id: string, factory: CsrModuleFactory): void;
|
|
45
|
+
defineHelpers(hash: string, factory: () => Record<string, unknown>): void;
|
|
46
|
+
helpers(hash: string): Record<string, unknown>;
|
|
47
|
+
start(options: { generation: number; refresh: string }): void;
|
|
48
|
+
update(generation: number, factories: Record<string, CsrModuleFactory>): void;
|
|
49
|
+
accept(ownerId: string, deps: string[], callback: CsrAcceptCallback): void;
|
|
50
|
+
hot(message: CsrUpdateMessage): void;
|
|
51
|
+
toESM(mod: unknown, isNodeMode?: number): unknown;
|
|
52
|
+
reExport(target: object, mod: unknown, secondTarget?: object): object | undefined;
|
|
53
|
+
inspect(): { generation: number; executed: string[]; modules: number };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface CsrScriptElement {
|
|
57
|
+
src: string;
|
|
58
|
+
dataset: Record<string, string | undefined>;
|
|
59
|
+
onload: (() => void) | null;
|
|
60
|
+
onerror: (() => void) | null;
|
|
61
|
+
remove(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CsrDevRuntimeHost {
|
|
65
|
+
__akan?: CsrDevRuntimeApi;
|
|
66
|
+
__AKAN_CSR_LAST_UPDATE__?: { generation: number; executed: string[] };
|
|
67
|
+
document: {
|
|
68
|
+
currentScript: unknown;
|
|
69
|
+
head: { appendChild(node: unknown): unknown };
|
|
70
|
+
createElement(tagName: "script"): CsrScriptElement;
|
|
71
|
+
};
|
|
72
|
+
location: { reload(): void };
|
|
73
|
+
console: { warn(...args: unknown[]): void };
|
|
74
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
75
|
+
clearTimeout(handle: unknown): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const installCsrDevRuntime = (host: CsrDevRuntimeHost): void => {
|
|
79
|
+
if (host.__akan) return;
|
|
80
|
+
const modulePrefix = "akan-module:";
|
|
81
|
+
const maxInvalidateRounds = 8;
|
|
82
|
+
|
|
83
|
+
class HotContext implements CsrHotContext {
|
|
84
|
+
selfAccepted = false;
|
|
85
|
+
invalidated = false;
|
|
86
|
+
readonly disposers: ((data: Record<string, unknown>) => void)[] = [];
|
|
87
|
+
readonly acceptedDeps = new Map<string, CsrAcceptCallback>();
|
|
88
|
+
constructor(readonly data: Record<string, unknown> | undefined) {}
|
|
89
|
+
accept(deps?: string | string[], callback: CsrAcceptCallback = () => undefined) {
|
|
90
|
+
if (deps === undefined) {
|
|
91
|
+
this.selfAccepted = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const dep of Array.isArray(deps) ? deps : [deps])
|
|
95
|
+
this.acceptedDeps.set(dep.startsWith(modulePrefix) ? dep.slice(modulePrefix.length) : dep, callback);
|
|
96
|
+
}
|
|
97
|
+
dispose(callback: (data: Record<string, unknown>) => void) {
|
|
98
|
+
this.disposers.push(callback);
|
|
99
|
+
}
|
|
100
|
+
invalidate() {
|
|
101
|
+
this.invalidated = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class ModuleRecord implements CsrModuleRecord {
|
|
106
|
+
exports: unknown = {};
|
|
107
|
+
readonly parents = new Set<string>();
|
|
108
|
+
readonly children = new Set<string>();
|
|
109
|
+
readonly hot: HotContext;
|
|
110
|
+
constructor(
|
|
111
|
+
readonly id: string,
|
|
112
|
+
data: Record<string, unknown> | undefined,
|
|
113
|
+
) {
|
|
114
|
+
this.hot = new HotContext(data);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class CsrDevRegistry implements CsrDevRuntimeApi {
|
|
119
|
+
#factories = new Map<string, CsrModuleFactory>();
|
|
120
|
+
#helperFactories = new Map<string, () => Record<string, unknown>>();
|
|
121
|
+
#helperValues = new Map<string, Record<string, unknown>>();
|
|
122
|
+
#cache = new Map<string, ModuleRecord>();
|
|
123
|
+
#hotData = new Map<string, Record<string, unknown>>();
|
|
124
|
+
#refresh: RefreshRuntime | null = null;
|
|
125
|
+
#refreshTimer: unknown = null;
|
|
126
|
+
#generation = 0;
|
|
127
|
+
#executed: string[] = [];
|
|
128
|
+
#patchQueue: Promise<void> = Promise.resolve();
|
|
129
|
+
#settling: Promise<void> = Promise.resolve();
|
|
130
|
+
#reloading = false;
|
|
131
|
+
|
|
132
|
+
get generation() {
|
|
133
|
+
return this.#generation;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
define(id: string, factory: CsrModuleFactory) {
|
|
137
|
+
this.#factories.set(id, factory);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
defineHelpers(hash: string, factory: () => Record<string, unknown>) {
|
|
141
|
+
if (!this.#helperFactories.has(hash)) this.#helperFactories.set(hash, factory);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
helpers(hash: string) {
|
|
145
|
+
const cached = this.#helperValues.get(hash);
|
|
146
|
+
if (cached) return cached;
|
|
147
|
+
const factory = this.#helperFactories.get(hash);
|
|
148
|
+
if (!factory) throw new Error(`[akan-csr] no helpers registered as ${hash}`);
|
|
149
|
+
const value = factory();
|
|
150
|
+
this.#helperValues.set(hash, value);
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
start({ generation, refresh }: { generation: number; refresh: string }) {
|
|
155
|
+
const script = host.document.currentScript as CsrScriptElement | null;
|
|
156
|
+
const entry = script?.dataset.akanCsrEntry;
|
|
157
|
+
if (!entry) throw new Error("[akan-csr] the app script carries no data-akan-csr-entry");
|
|
158
|
+
this.#generation = generation;
|
|
159
|
+
this.#refresh = this.#load(refresh).exports as RefreshRuntime;
|
|
160
|
+
|
|
161
|
+
this.#refresh.injectIntoGlobalHook(host);
|
|
162
|
+
this.#load(entry);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
update(generation: number, factories: Record<string, CsrModuleFactory>) {
|
|
166
|
+
if (generation <= this.#generation) return;
|
|
167
|
+
if (generation !== this.#generation + 1) {
|
|
168
|
+
this.#reload(`missed generation ${this.#generation + 1} before ${generation}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const ids = Object.keys(factories);
|
|
172
|
+
for (const id of ids) this.#factories.set(id, factories[id] as CsrModuleFactory);
|
|
173
|
+
this.#generation = generation;
|
|
174
|
+
this.#executed = [];
|
|
175
|
+
const threw = (error: unknown) =>
|
|
176
|
+
this.#reload(
|
|
177
|
+
`generation ${generation} threw while applying: ${error instanceof Error ? error.message : error}`,
|
|
178
|
+
);
|
|
179
|
+
let pending: Promise<void>[] = [];
|
|
180
|
+
try {
|
|
181
|
+
pending = this.#apply(ids.filter((id) => this.#cache.has(id)));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
threw(error);
|
|
184
|
+
}
|
|
185
|
+
host.__AKAN_CSR_LAST_UPDATE__ = { generation, executed: this.#executed.slice() };
|
|
186
|
+
if (pending.length > 0) this.#settling = Promise.all(pending).then(() => this.#scheduleRefresh(), threw);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
accept(ownerId: string, deps: string[], callback: CsrAcceptCallback) {
|
|
190
|
+
const owner = this.#cache.get(ownerId);
|
|
191
|
+
if (!owner) throw new Error(`[akan-csr] ${ownerId} accepts updates before it is loaded`);
|
|
192
|
+
owner.hot.accept(deps, callback);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
hot(message: CsrUpdateMessage) {
|
|
196
|
+
if (message.reload) {
|
|
197
|
+
this.#reload(message.reason ?? "the dev server asked for a reload");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (message.generation <= this.#generation) return;
|
|
201
|
+
const url = message.url;
|
|
202
|
+
if (!url) {
|
|
203
|
+
this.#reload(`generation ${message.generation} arrived without a patch`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.#patchQueue = this.#patchQueue.then(() => this.#loadPatch(url)).then(() => this.#settling);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
inspect() {
|
|
211
|
+
return { generation: this.#generation, executed: this.#executed.slice(), modules: this.#cache.size };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
readonly toESM = (mod: unknown, _isNodeMode?: number): unknown => {
|
|
215
|
+
|
|
216
|
+
if (mod != null && (mod as { __esModule?: boolean }).__esModule) return mod;
|
|
217
|
+
const namespace: Record<string, unknown> = {};
|
|
218
|
+
Object.defineProperty(namespace, "default", { value: mod, enumerable: true });
|
|
219
|
+
if (mod == null || (typeof mod !== "object" && typeof mod !== "function")) return namespace;
|
|
220
|
+
for (const key of Object.getOwnPropertyNames(mod)) {
|
|
221
|
+
if (key === "default" || key === "__esModule") continue;
|
|
222
|
+
Object.defineProperty(namespace, key, { get: () => (mod as Record<string, unknown>)[key], enumerable: true });
|
|
223
|
+
}
|
|
224
|
+
return namespace;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
readonly reExport = (target: object, mod: unknown, secondTarget?: object): object | undefined => {
|
|
228
|
+
const keys =
|
|
229
|
+
mod != null && (typeof mod === "object" || typeof mod === "function") ? Object.getOwnPropertyNames(mod) : [];
|
|
230
|
+
for (const to of secondTarget ? [target, secondTarget] : [target]) {
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
|
|
233
|
+
if (key === "default" || key === "__esModule" || Object.prototype.hasOwnProperty.call(to, key)) continue;
|
|
234
|
+
Object.defineProperty(to, key, { get: () => (mod as Record<string, unknown>)[key], enumerable: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return secondTarget;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
#loadPatch(url: string) {
|
|
241
|
+
return new Promise<void>((resolve) => {
|
|
242
|
+
const script = host.document.createElement("script");
|
|
243
|
+
script.src = url;
|
|
244
|
+
script.onload = () => {
|
|
245
|
+
script.remove();
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
script.onerror = () => {
|
|
249
|
+
this.#reload(`patch ${url} failed to load`);
|
|
250
|
+
resolve();
|
|
251
|
+
};
|
|
252
|
+
host.document.head.appendChild(script);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#requireFrom(parentId: string) {
|
|
257
|
+
return (specifier: string) => {
|
|
258
|
+
const record = this.#load(
|
|
259
|
+
specifier.startsWith(modulePrefix) ? specifier.slice(modulePrefix.length) : specifier,
|
|
260
|
+
);
|
|
261
|
+
this.#link(parentId, record);
|
|
262
|
+
return record.exports;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#link(parentId: string, record: ModuleRecord) {
|
|
267
|
+
record.parents.add(parentId);
|
|
268
|
+
this.#cache.get(parentId)?.children.add(record.id);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#load(id: string): ModuleRecord {
|
|
272
|
+
const cached = this.#cache.get(id);
|
|
273
|
+
if (cached) return cached;
|
|
274
|
+
const factory = this.#factories.get(id);
|
|
275
|
+
if (!factory) throw new Error(`[akan-csr] no module registered as ${id}`);
|
|
276
|
+
const record = new ModuleRecord(id, this.#hotData.get(id));
|
|
277
|
+
this.#cache.set(id, record);
|
|
278
|
+
const requireFromHere = this.#requireFrom(id);
|
|
279
|
+
const register = (type: unknown, localId: string) => this.#refresh?.register(type, `${id} ${localId}`);
|
|
280
|
+
const signature = () => this.#refresh?.createSignatureFunctionForTransform() ?? ((type: unknown) => type);
|
|
281
|
+
const importFromHere = (specifier: string) => new Promise((resolve) => resolve(requireFromHere(specifier)));
|
|
282
|
+
try {
|
|
283
|
+
factory.call(record.exports, requireFromHere, record, record.exports, register, signature, importFromHere);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
this.#cache.delete(id);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
this.#executed.push(id);
|
|
289
|
+
if (!id.startsWith("stub:") && !id.includes("node_modules/")) this.#afterExecute(record);
|
|
290
|
+
return record;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#isComponent(value: unknown) {
|
|
294
|
+
const refresh = this.#refresh;
|
|
295
|
+
if (!refresh) return false;
|
|
296
|
+
try {
|
|
297
|
+
return refresh.getFamilyByType(value) !== undefined || refresh.isLikelyComponentType(value);
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#exportEntries(exports: unknown): [string, unknown][] {
|
|
304
|
+
if (exports == null || (typeof exports !== "object" && typeof exports !== "function")) return [];
|
|
305
|
+
return Object.keys(exports)
|
|
306
|
+
.filter((key) => key !== "__esModule")
|
|
307
|
+
.map((key) => {
|
|
308
|
+
try {
|
|
309
|
+
return [key, (exports as Record<string, unknown>)[key]];
|
|
310
|
+
} catch {
|
|
311
|
+
return [key, undefined];
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#afterExecute(record: ModuleRecord) {
|
|
317
|
+
const refresh = this.#refresh;
|
|
318
|
+
if (!refresh) return;
|
|
319
|
+
const previous = record.hot.data?.refresh as { boundary: boolean; signature: unknown[] } | undefined;
|
|
320
|
+
const entries = this.#exportEntries(record.exports);
|
|
321
|
+
for (const [key, value] of entries)
|
|
322
|
+
if (this.#isComponent(value)) refresh.register(value, `${record.id} %exports% ${key}`);
|
|
323
|
+
const boundary =
|
|
324
|
+
this.#isComponent(record.exports) ||
|
|
325
|
+
(entries.length > 0 && entries.every(([, value]) => this.#isComponent(value)));
|
|
326
|
+
if (!boundary) {
|
|
327
|
+
if (previous?.boundary) record.hot.invalidate();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
record.hot.accept();
|
|
331
|
+
const signature = [
|
|
332
|
+
this.#safeFamily(record.exports),
|
|
333
|
+
...entries.flatMap(([key, value]) => [key, this.#safeFamily(value)]),
|
|
334
|
+
];
|
|
335
|
+
record.hot.dispose((data) => {
|
|
336
|
+
data.refresh = { boundary: true, signature };
|
|
337
|
+
});
|
|
338
|
+
if (!previous) return;
|
|
339
|
+
const changed =
|
|
340
|
+
!previous.boundary ||
|
|
341
|
+
previous.signature.length !== signature.length ||
|
|
342
|
+
previous.signature.some((part, index) => part !== signature[index]);
|
|
343
|
+
if (changed) record.hot.invalidate();
|
|
344
|
+
else this.#scheduleRefresh();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#safeFamily(value: unknown) {
|
|
348
|
+
try {
|
|
349
|
+
return this.#refresh?.getFamilyByType(value);
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#scheduleRefresh() {
|
|
356
|
+
if (this.#refreshTimer !== null) host.clearTimeout(this.#refreshTimer);
|
|
357
|
+
this.#refreshTimer = host.setTimeout(() => {
|
|
358
|
+
this.#refreshTimer = null;
|
|
359
|
+
this.#refresh?.performReactRefresh();
|
|
360
|
+
}, 16);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#apply(changed: string[]): Promise<void>[] {
|
|
364
|
+
const settling: Promise<void>[] = [];
|
|
365
|
+
let pending = changed;
|
|
366
|
+
for (let round = 0; pending.length > 0; round += 1) {
|
|
367
|
+
if (round === maxInvalidateRounds) {
|
|
368
|
+
this.#reload("updates kept invalidating their boundaries");
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
const plan = this.#collectOutdated(pending);
|
|
372
|
+
if ("reload" in plan) {
|
|
373
|
+
this.#reload(plan.reload);
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
const parentsOf = new Map(
|
|
377
|
+
[...plan.boundaries].map((id) => [id, [...(this.#cache.get(id)?.parents ?? [])]] as const),
|
|
378
|
+
);
|
|
379
|
+
this.#dispose(plan.outdated);
|
|
380
|
+
const invalidated: string[] = [];
|
|
381
|
+
for (const id of plan.boundaries) {
|
|
382
|
+
const record = this.#load(id);
|
|
383
|
+
for (const parent of parentsOf.get(id) ?? []) this.#link(parent, record);
|
|
384
|
+
if (record.hot.invalidated) invalidated.push(id);
|
|
385
|
+
}
|
|
386
|
+
settling.push(...this.#runAcceptedDeps(plan.accepted));
|
|
387
|
+
pending = [];
|
|
388
|
+
for (const id of invalidated) {
|
|
389
|
+
const parents = [...(this.#cache.get(id)?.parents ?? [])];
|
|
390
|
+
if (parents.length === 0) {
|
|
391
|
+
this.#reload(`${id} changed its exports and nothing above it can take the update`);
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
pending.push(...parents);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return settling;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#runAcceptedDeps(accepted: Map<string, Set<string>>): Promise<void>[] {
|
|
401
|
+
const settling: Promise<void>[] = [];
|
|
402
|
+
for (const [parentId, deps] of accepted) {
|
|
403
|
+
const parent = this.#cache.get(parentId);
|
|
404
|
+
if (!parent) continue;
|
|
405
|
+
const updatedBy = new Map<CsrAcceptCallback, string[]>();
|
|
406
|
+
for (const dep of deps) {
|
|
407
|
+
this.#link(parentId, this.#load(dep));
|
|
408
|
+
const callback = parent.hot.acceptedDeps.get(dep);
|
|
409
|
+
if (callback) updatedBy.set(callback, [...(updatedBy.get(callback) ?? []), dep]);
|
|
410
|
+
}
|
|
411
|
+
for (const [callback, updated] of updatedBy) {
|
|
412
|
+
const result: unknown = callback(updated);
|
|
413
|
+
if (result instanceof Promise) settling.push(result);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (accepted.size > 0) this.#scheduleRefresh();
|
|
417
|
+
return settling;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#collectOutdated(
|
|
421
|
+
start: string[],
|
|
422
|
+
): { outdated: Set<string>; boundaries: Set<string>; accepted: Map<string, Set<string>> } | { reload: string } {
|
|
423
|
+
const outdated = new Set<string>();
|
|
424
|
+
const boundaries = new Set<string>();
|
|
425
|
+
const accepted = new Map<string, Set<string>>();
|
|
426
|
+
const queue = [...start];
|
|
427
|
+
while (queue.length > 0) {
|
|
428
|
+
const id = queue.shift() as string;
|
|
429
|
+
if (outdated.has(id)) continue;
|
|
430
|
+
const record = this.#cache.get(id);
|
|
431
|
+
if (!record) continue;
|
|
432
|
+
outdated.add(id);
|
|
433
|
+
if (record.hot.selfAccepted && !record.hot.invalidated) {
|
|
434
|
+
boundaries.add(id);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (record.parents.size === 0) return { reload: `no component boundary above ${id}` };
|
|
438
|
+
for (const parentId of record.parents) {
|
|
439
|
+
if (!this.#cache.get(parentId)?.hot.acceptedDeps.has(id)) {
|
|
440
|
+
queue.push(parentId);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const deps = accepted.get(parentId) ?? new Set<string>();
|
|
444
|
+
accepted.set(parentId, deps.add(id));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const parentId of accepted.keys()) if (outdated.has(parentId)) accepted.delete(parentId);
|
|
448
|
+
return { outdated, boundaries, accepted };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#dispose(ids: Set<string>) {
|
|
452
|
+
for (const id of ids) {
|
|
453
|
+
const record = this.#cache.get(id);
|
|
454
|
+
if (!record) continue;
|
|
455
|
+
const data: Record<string, unknown> = {};
|
|
456
|
+
for (const dispose of record.hot.disposers) dispose(data);
|
|
457
|
+
this.#hotData.set(id, data);
|
|
458
|
+
this.#cache.delete(id);
|
|
459
|
+
for (const child of record.children) this.#cache.get(child)?.parents.delete(id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#reload(reason: string) {
|
|
464
|
+
if (this.#reloading) return;
|
|
465
|
+
this.#reloading = true;
|
|
466
|
+
host.console.warn(`[akan-csr] full reload: ${reason}`);
|
|
467
|
+
host.location.reload();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
host.__akan = new CsrDevRegistry();
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export const CSR_DEV_RUNTIME_SCRIPT = `(${installCsrDevRuntime.toString()})(self);\n`;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveStaticPath } from "../staticPath";
|
|
3
|
+
import {
|
|
4
|
+
CSR_DEV_APP_FILE,
|
|
5
|
+
CSR_DEV_DIRNAME,
|
|
6
|
+
CSR_DEV_MANIFEST_FILE,
|
|
7
|
+
CSR_DEV_ROUTE_PREFIX,
|
|
8
|
+
type CsrDevLayout,
|
|
9
|
+
type CsrDevManifest,
|
|
10
|
+
csrDevModuleFile,
|
|
11
|
+
} from "./csrDevManifest";
|
|
12
|
+
import { CSR_DEV_RUNTIME_SCRIPT } from "./csrDevRuntime";
|
|
13
|
+
import { CsrDevSourceMap, type RawSourceMap } from "./csrDevSourceMap";
|
|
14
|
+
|
|
15
|
+
export interface CsrDevShellRenderOptions {
|
|
16
|
+
basePath: string;
|
|
17
|
+
lang: string;
|
|
18
|
+
title: string;
|
|
19
|
+
cssHref: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CsrDevShell {
|
|
23
|
+
static readonly #runtimeHash = Bun.hash(CSR_DEV_RUNTIME_SCRIPT).toString(36);
|
|
24
|
+
static readonly #servedFile = /^(app\.js|vendor-[\w-]+\.js|patch-\d+\.js|assets\/[\w.-]+)$/;
|
|
25
|
+
static readonly #sourceMapFile = /^(app\.js|patch-\d+\.js)\.map$/;
|
|
26
|
+
readonly #dir: string;
|
|
27
|
+
|
|
28
|
+
constructor(artifactDir: string) {
|
|
29
|
+
this.#dir = path.join(artifactDir, CSR_DEV_DIRNAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async readManifest(): Promise<CsrDevManifest | null> {
|
|
33
|
+
const file = Bun.file(path.join(this.#dir, CSR_DEV_MANIFEST_FILE));
|
|
34
|
+
if (!(await file.exists())) return null;
|
|
35
|
+
return (await file.json().catch(() => null)) as CsrDevManifest | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async render({ basePath, lang, title, cssHref }: CsrDevShellRenderOptions): Promise<string | null> {
|
|
39
|
+
const manifest = await this.readManifest();
|
|
40
|
+
const entry = manifest?.entries[basePath];
|
|
41
|
+
if (!manifest || !entry) return null;
|
|
42
|
+
const stylesheet = cssHref
|
|
43
|
+
? ` <link rel="stylesheet" href="${CsrDevShell.#attr(cssHref)}" data-akan-css="active" />\n`
|
|
44
|
+
: "";
|
|
45
|
+
return `<!doctype html>
|
|
46
|
+
<html lang="${CsrDevShell.#attr(lang)}">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="utf-8" />
|
|
49
|
+
<title>${CsrDevShell.#attr(title)}</title>
|
|
50
|
+
<base href="/" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
52
|
+
${stylesheet} </head>
|
|
53
|
+
<body>
|
|
54
|
+
<div id="root"></div>
|
|
55
|
+
<script src="${CSR_DEV_ROUTE_PREFIX}runtime.js?v=${CsrDevShell.#runtimeHash}"></script>
|
|
56
|
+
<script src="${CSR_DEV_ROUTE_PREFIX}${CsrDevShell.#attr(manifest.vendorFile)}"></script>
|
|
57
|
+
<script src="${CSR_DEV_ROUTE_PREFIX}${CSR_DEV_APP_FILE}?g=${manifest.generation}" data-akan-csr-entry="${CsrDevShell.#attr(entry)}"></script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async serve(req: Request): Promise<Response> {
|
|
64
|
+
const name = new URL(req.url).pathname.slice(CSR_DEV_ROUTE_PREFIX.length);
|
|
65
|
+
if (name === "runtime.js") return CsrDevShell.#js(CSR_DEV_RUNTIME_SCRIPT, "public, max-age=31536000, immutable");
|
|
66
|
+
if (CsrDevShell.#sourceMapFile.test(name)) return await this.#serveSourceMap(name);
|
|
67
|
+
if (!CsrDevShell.#servedFile.test(name)) return new Response("Not Found", { status: 404 });
|
|
68
|
+
const filePath = resolveStaticPath(this.#dir, name);
|
|
69
|
+
const file = filePath ? Bun.file(filePath) : null;
|
|
70
|
+
if (!file || !(await file.exists())) return new Response("Not Found", { status: 404 });
|
|
71
|
+
const cacheControl = name.startsWith("vendor-") ? "public, max-age=31536000, immutable" : "no-store";
|
|
72
|
+
if (name.startsWith("assets/"))
|
|
73
|
+
return new Response(file, { headers: CsrDevShell.#headers(file.type, cacheControl) });
|
|
74
|
+
return CsrDevShell.#js(file, cacheControl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #serveSourceMap(name: string): Promise<Response> {
|
|
78
|
+
const generatedFile = name.slice(0, -".map".length);
|
|
79
|
+
const layoutFile = Bun.file(path.join(this.#dir, `${generatedFile}.layout.json`));
|
|
80
|
+
if (!(await layoutFile.exists())) return new Response("Not Found", { status: 404 });
|
|
81
|
+
const layout = (await layoutFile.json()) as CsrDevLayout;
|
|
82
|
+
const sections = await Promise.all(
|
|
83
|
+
layout.modules.map(async ([id, line]) => {
|
|
84
|
+
const file = Bun.file(path.join(this.#dir, csrDevModuleFile(id, ".js.map")));
|
|
85
|
+
return (await file.exists()) ? { line, map: (await file.json()) as RawSourceMap } : null;
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
const map = CsrDevSourceMap.merge(
|
|
89
|
+
generatedFile,
|
|
90
|
+
sections.filter((section) => section !== null),
|
|
91
|
+
layout.lineCount,
|
|
92
|
+
);
|
|
93
|
+
return new Response(JSON.stringify(map), { headers: CsrDevShell.#headers("application/json", "no-store") });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static #js(body: string | Blob, cacheControl: string): Response {
|
|
97
|
+
return new Response(body, { headers: CsrDevShell.#headers("application/javascript; charset=utf-8", cacheControl) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static #headers(contentType: string, cacheControl: string): Headers {
|
|
101
|
+
return new Headers({
|
|
102
|
+
"Content-Type": contentType || "application/octet-stream",
|
|
103
|
+
"Cache-Control": cacheControl,
|
|
104
|
+
"X-Content-Type-Options": "nosniff",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static #attr(value: string): string {
|
|
109
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
110
|
+
}
|
|
111
|
+
}
|