@amxts/core 0.1.0

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/README.ru.md +162 -0
  4. package/amxmodx/base/include/amxconst.inc +594 -0
  5. package/amxmodx/base/include/amxmisc.inc +902 -0
  6. package/amxmodx/base/include/amxmodx.inc +3447 -0
  7. package/amxmodx/base/include/amxmodx_version.inc +20 -0
  8. package/amxmodx/base/include/cellarray.inc +525 -0
  9. package/amxmodx/base/include/cellstack.inc +166 -0
  10. package/amxmodx/base/include/celltrie.inc +412 -0
  11. package/amxmodx/base/include/core.inc +202 -0
  12. package/amxmodx/base/include/csstats.inc +298 -0
  13. package/amxmodx/base/include/csstats_const.inc +29 -0
  14. package/amxmodx/base/include/cstrike.inc +1287 -0
  15. package/amxmodx/base/include/cstrike_const.inc +521 -0
  16. package/amxmodx/base/include/csx.inc +259 -0
  17. package/amxmodx/base/include/cvars.inc +547 -0
  18. package/amxmodx/base/include/datapack.inc +161 -0
  19. package/amxmodx/base/include/dbi.inc +154 -0
  20. package/amxmodx/base/include/dodconst.inc +152 -0
  21. package/amxmodx/base/include/dodfun.inc +152 -0
  22. package/amxmodx/base/include/dodstats.inc +69 -0
  23. package/amxmodx/base/include/dodx.inc +161 -0
  24. package/amxmodx/base/include/engine.inc +1343 -0
  25. package/amxmodx/base/include/engine_const.inc +310 -0
  26. package/amxmodx/base/include/engine_stocks.inc +248 -0
  27. package/amxmodx/base/include/esf.inc +70 -0
  28. package/amxmodx/base/include/esf_const.inc +74 -0
  29. package/amxmodx/base/include/fakemeta.inc +1127 -0
  30. package/amxmodx/base/include/fakemeta_const.inc +793 -0
  31. package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
  32. package/amxmodx/base/include/fakemeta_util.inc +882 -0
  33. package/amxmodx/base/include/file.inc +666 -0
  34. package/amxmodx/base/include/float.inc +422 -0
  35. package/amxmodx/base/include/fun.inc +333 -0
  36. package/amxmodx/base/include/gameconfig.inc +93 -0
  37. package/amxmodx/base/include/geoip.inc +264 -0
  38. package/amxmodx/base/include/ham_const.inc +4352 -0
  39. package/amxmodx/base/include/hamsandwich.inc +439 -0
  40. package/amxmodx/base/include/hlsdk_const.inc +808 -0
  41. package/amxmodx/base/include/json.inc +777 -0
  42. package/amxmodx/base/include/lang.inc +119 -0
  43. package/amxmodx/base/include/message_const.inc +1130 -0
  44. package/amxmodx/base/include/message_stocks.inc +161 -0
  45. package/amxmodx/base/include/messages.inc +604 -0
  46. package/amxmodx/base/include/newmenus.inc +387 -0
  47. package/amxmodx/base/include/ns.inc +754 -0
  48. package/amxmodx/base/include/ns_const.inc +196 -0
  49. package/amxmodx/base/include/nvault.inc +154 -0
  50. package/amxmodx/base/include/regex.inc +348 -0
  51. package/amxmodx/base/include/sockets.inc +183 -0
  52. package/amxmodx/base/include/sorting.inc +107 -0
  53. package/amxmodx/base/include/sqlx.inc +582 -0
  54. package/amxmodx/base/include/string.inc +786 -0
  55. package/amxmodx/base/include/string_const.inc +156 -0
  56. package/amxmodx/base/include/string_stocks.inc +325 -0
  57. package/amxmodx/base/include/textparse_ini.inc +214 -0
  58. package/amxmodx/base/include/textparse_smc.inc +261 -0
  59. package/amxmodx/base/include/tfcconst.inc +84 -0
  60. package/amxmodx/base/include/tfcstats.inc +62 -0
  61. package/amxmodx/base/include/tfcx.inc +148 -0
  62. package/amxmodx/base/include/time.inc +118 -0
  63. package/amxmodx/base/include/tsconst.inc +154 -0
  64. package/amxmodx/base/include/tsfun.inc +198 -0
  65. package/amxmodx/base/include/tsstats.inc +65 -0
  66. package/amxmodx/base/include/tsx.inc +88 -0
  67. package/amxmodx/base/include/vault.inc +60 -0
  68. package/amxmodx/base/include/vector.inc +131 -0
  69. package/amxmodx/base/include/xs.inc +1570 -0
  70. package/as/amxts.d.ts +153 -0
  71. package/as/constants.ts +4730 -0
  72. package/as/effects.ts +1449 -0
  73. package/as/entities.ts +5482 -0
  74. package/as/events.ts +3054 -0
  75. package/as/facade.ts +4639 -0
  76. package/as/flags.ts +299 -0
  77. package/as/fs.ts +287 -0
  78. package/as/hooks.ts +18536 -0
  79. package/as/kit.ts +25 -0
  80. package/as/lib/check.ts +86 -0
  81. package/as/modules/http.ts +184 -0
  82. package/as/natives.ts +11845 -0
  83. package/as/os.ts +30 -0
  84. package/as/promise.ts +1392 -0
  85. package/as/promise.types.d.ts +82 -0
  86. package/as/remote.ts +288 -0
  87. package/as/tsconfig.json +22 -0
  88. package/as/vector.ts +65 -0
  89. package/bin/amxts.mjs +10 -0
  90. package/includes/menu_core.inc +355 -0
  91. package/includes/order.txt +54 -0
  92. package/includes/sources.json +27 -0
  93. package/includes/universal_config.inc +355 -0
  94. package/lint/oxlint-plugin.mjs +77 -0
  95. package/node_modules/assemblyscript/LICENSE +201 -0
  96. package/node_modules/assemblyscript/NOTICE +93 -0
  97. package/node_modules/assemblyscript/package.json +63 -0
  98. package/node_modules/assemblyscript/std/README.md +6 -0
  99. package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
  100. package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
  101. package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
  102. package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
  103. package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
  104. package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
  105. package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
  106. package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
  107. package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
  108. package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
  109. package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
  110. package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
  111. package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
  112. package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
  113. package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
  114. package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
  115. package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
  116. package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
  117. package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
  118. package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
  119. package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
  120. package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
  121. package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
  122. package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
  123. package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
  124. package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
  125. package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
  126. package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
  127. package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
  128. package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
  129. package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
  130. package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
  131. package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
  132. package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
  133. package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
  134. package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
  135. package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
  136. package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
  137. package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
  138. package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
  139. package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
  140. package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
  141. package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
  142. package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
  143. package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
  144. package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
  145. package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
  146. package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
  147. package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
  148. package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
  149. package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
  150. package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
  151. package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
  152. package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
  153. package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
  154. package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
  155. package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
  156. package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
  157. package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
  158. package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
  159. package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
  160. package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
  161. package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
  162. package/node_modules/assemblyscript/std/assembly.json +16 -0
  163. package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
  164. package/node_modules/assemblyscript/std/portable/index.js +416 -0
  165. package/node_modules/assemblyscript/std/portable.json +11 -0
  166. package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
  167. package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
  168. package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
  169. package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
  170. package/node_modules/assemblyscript/tsconfig-base.json +13 -0
  171. package/package.json +112 -0
  172. package/runtime/deps/assemblyscript/LICENSE +201 -0
  173. package/runtime/deps/assemblyscript/NOTICE +93 -0
  174. package/runtime/deps/assemblyscript/bin/asc.js +35 -0
  175. package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
  176. package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
  177. package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
  178. package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
  179. package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
  180. package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
  181. package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
  182. package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
  183. package/runtime/deps/assemblyscript/package.json +114 -0
  184. package/runtime/host/amxts.inc +85 -0
  185. package/runtime/licenses/AMXX-LICENSE.txt +34 -0
  186. package/runtime/licenses/AssemblyScript-LICENSE +201 -0
  187. package/runtime/licenses/AssemblyScript-NOTICE +93 -0
  188. package/runtime/licenses/Binaryen-LICENSE +201 -0
  189. package/runtime/licenses/Bun-LICENSE.md +84 -0
  190. package/runtime/licenses/GPL-3.0.txt +674 -0
  191. package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
  192. package/runtime/licenses/README.md +25 -0
  193. package/runtime/licenses/WAMR-LICENSE +219 -0
  194. package/runtime/natives.txt +1095 -0
  195. package/runtime/patches/README.md +59 -0
  196. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
  197. package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
  198. package/scripts/apply-docs.ts +508 -0
  199. package/scripts/asc.ts +27 -0
  200. package/scripts/auto-imports.ts +258 -0
  201. package/scripts/build-wasm.ts +595 -0
  202. package/scripts/check.ts +93 -0
  203. package/scripts/compile-cache.ts +179 -0
  204. package/scripts/compile.ts +401 -0
  205. package/scripts/docker-server.ts +73 -0
  206. package/scripts/docs/as/amxts.ts +210 -0
  207. package/scripts/docs/as/effects.ts +1684 -0
  208. package/scripts/docs/as/facade.ts +3302 -0
  209. package/scripts/docs/as/fs.ts +173 -0
  210. package/scripts/docs/as/lib/check.ts +68 -0
  211. package/scripts/docs/as/modules/http.ts +66 -0
  212. package/scripts/docs/as/os.ts +16 -0
  213. package/scripts/docs/as/promise.ts +132 -0
  214. package/scripts/docs/as/promise.types.ts +63 -0
  215. package/scripts/docs/as/vector.ts +48 -0
  216. package/scripts/docs/entities.ts +2338 -0
  217. package/scripts/docs/events.ts +245 -0
  218. package/scripts/docs/game.ts +497 -0
  219. package/scripts/docs/messages.ts +148 -0
  220. package/scripts/file-contents.ts +74 -0
  221. package/scripts/includes.ts +134 -0
  222. package/scripts/player-fields.ts +866 -0
  223. package/scripts/plugin-cache.ts +92 -0
  224. package/scripts/plugin-natives.ts +1157 -0
  225. package/scripts/prebuilt.ts +268 -0
  226. package/scripts/prepare.ts +134 -0
  227. package/scripts/project.ts +973 -0
  228. package/scripts/run.ts +28 -0
  229. package/scripts/shared-modules.ts +906 -0
  230. package/scripts/system.ts +33 -0
  231. package/scripts/tracked-fs.ts +115 -0
  232. package/scripts/typed-configs.ts +788 -0
  233. package/scripts/ui.ts +77 -0
  234. package/src/cli-api.mjs +175 -0
  235. package/src/parser/include-parser.ts +506 -0
  236. package/src/system.mjs +155 -0
  237. package/src/testing/compile-cache.ts +31 -0
  238. package/src/testing/compile.ts +222 -0
  239. package/src/testing/coroutines.ts +191 -0
  240. package/src/testing/index.ts +39 -0
  241. package/src/testing/kits.ts +55 -0
  242. package/src/testing/memory.ts +125 -0
  243. package/src/testing/natives.ts +925 -0
  244. package/src/testing/server.ts +1874 -0
  245. package/src/testing/setup.ts +63 -0
  246. package/src/testing/tables.ts +139 -0
  247. package/src/types.ts +51 -0
@@ -0,0 +1,906 @@
1
+ // Modules with one instance on the server.
2
+ //
3
+ // A module at ~/modules/<name> whose name a plugin at ~/<name>.ts has belongs
4
+ // to that plugin. A module package is put there by scripts/project.ts: its
5
+ // module file is ~/modules/menu-core, its natives (or a generated owner)
6
+ // ~/menu-core.ts. Every plugin reaches it the same way - `menus`, which the
7
+ // build imports for it (scripts/auto-imports.ts), or its own
8
+ //
9
+ // import * as menus from "@amxts/menu-core";
10
+ //
11
+ // In the owner it is the module. In any other plugin the build reads, in its
12
+ // place, a proxy generated here: the same exported functions and types, each
13
+ // function writing its arguments into a buffer (as/remote.ts) that the amxts
14
+ // module (runtime/src/module.cpp) hands to the owner, whose generated
15
+ // dispatcher calls the real function and writes the answer back. The plugin
16
+ // cannot tell the difference, and its editor reads the real module.
17
+ //
18
+ // What crosses, and how:
19
+ //
20
+ // - number, boolean, string (and a union of string literals, which is a
21
+ // string), arrays of them, `T | null`;
22
+ // - Player, by its id;
23
+ // - an object the module hands out and takes back - it is returned by one
24
+ // exported function and taken by another: a config, a section - or hands
25
+ // out with methods of its own - a menu - by a number in the owner's table.
26
+ // The proxy's type of the same name is a class whose fields and accessors
27
+ // read and write the owner's object and whose methods run on it there; its
28
+ // private fields stay the owner's, and a readonly one has no setter. An
29
+ // object the module exports - `export const semiclip = new Semiclip()` -
30
+ // is one too, with a number fixed when the owner starts: the proxy's
31
+ // `semiclip` is the owner's;
32
+ // - any other object (an interface, a class of fields - options, a row) by
33
+ // value, field by field; a class with methods (an event) comes back after
34
+ // the call with the fields the other side changed - preventDefault() works;
35
+ // - a function (a condition, an action, a list source): it stays in the
36
+ // plugin that wrote it, and the other side gets a stand-in with the same
37
+ // signature that calls it back.
38
+ //
39
+ // Anything else stops the build with the function and the parameter named.
40
+ //
41
+ // The types come from compiling the module on its own with asc: its exported
42
+ // functions are compiled as exports, so every result type the compiler
43
+ // inferred is known, and so is every field. That compile is done before the
44
+ // plugin's (asc is not reentrant) and remembered while the file is unchanged.
45
+ import { join, relative, resolve } from 'node:path';
46
+ // @ts-ignore - shipped as JavaScript, with types beside it we do not need here
47
+ import * as as from '../runtime/deps/assemblyscript/dist/assemblyscript.js';
48
+ import { ascMain } from './asc';
49
+ import { playerFieldsBuild } from './player-fields';
50
+ import { ascPath, sourcesFor } from './project';
51
+ import { existsSync, statSync } from './tracked-fs';
52
+
53
+ /** Where the owner's dispatcher is parsed: beside the plugin, like its natives' wrappers. */
54
+ const SERVE_FILE = '__amxts_serve.ts';
55
+ /** The entry that exports __amxts_rpc from ~/remote. */
56
+ const EXPORT_FILE = '__amxts_remote.ts';
57
+ const EXPORT_SOURCE = 'export { __amxts_rpc } from "~/remote";\n';
58
+
59
+ // ---------------------------------------------------------------- ownership
60
+
61
+ /** `menu-core` for as/modules/menu-core.ts; null for a file that is not a module of its own. */
62
+ function moduleName(root: string, path: string): string | null {
63
+ const rel = relative(resolve(root), resolve(path)).replace(/\\/g, '/');
64
+ const match = rel.match(/^modules\/([^/]+)\.ts$/);
65
+ return match ? match[1] : null;
66
+ }
67
+
68
+ /**
69
+ * Whether a plugin owns `~/modules/<name>`: as/<name>.ts beside it - or, on a
70
+ * server, where plugins are compiled from the plugins folder, <name>.aot.
71
+ */
72
+ export function hasOwner(root: string, name: string): boolean {
73
+ return sourcesFor(root).exists(join(root, `${name}.ts`)) || existsSync(join(root, `${name}.aot`));
74
+ }
75
+
76
+ /** The module a plugin owns: `menu-core` for as/menu-core.ts, when as/modules/menu-core.ts exists. */
77
+ function ownedModule(root: string, entry: string): string | null {
78
+ const rel = entry.replace(/\\/g, '/');
79
+ if (rel.includes('/')) return null;
80
+ const name = rel.replace(/\.ts$/, '');
81
+ return sourcesFor(root).exists(join(root, 'modules', `${name}.ts`)) ? name : null;
82
+ }
83
+
84
+ // ---------------------------------------------------------------- what crosses
85
+
86
+ type Wire
87
+ = | { kind: 'num'; as: string }
88
+ | { kind: 'bool' }
89
+ | { kind: 'str' }
90
+ | { kind: 'player' }
91
+ | { kind: 'null'; of: Wire }
92
+ | { kind: 'array'; of: Wire }
93
+ | { kind: 'fn'; params: Wire[]; result: Wire | null; sig: number }
94
+ | { kind: 'record'; cls: ClassInfo }
95
+ | { kind: 'handle'; cls: ClassInfo };
96
+
97
+ interface ClassInfo {
98
+ /** As written: `Menu`. */
99
+ name: string;
100
+ /** The compiler's name: `modules/menu-core/Menu`. */
101
+ internal: string;
102
+ /** Where to import it from: `~/facade`; null for the module's own. */
103
+ from: string | null;
104
+ /**
105
+ * `optional`: an optional boolean (`force?: boolean`) - whether it was
106
+ * given crosses too, so `options.force ?? true` reads the same on the other
107
+ * side. An optional number crosses as its undefined, an optional object as null.
108
+ */
109
+ fields: { name: string; wire: Wire; optional?: boolean; readonly?: boolean; declared?: string }[];
110
+ /** It has methods: a record goes back after a call, as the other side left it. */
111
+ methods: boolean;
112
+ /** A handle's public methods, which the proxy calls on the owner's object. */
113
+ calls: ExportedFunction[];
114
+ /** Import alias for a class the module does not declare. */
115
+ alias: string;
116
+ }
117
+
118
+ interface ExportedFunction {
119
+ name: string;
120
+ /** The parameters as the module writes them, defaults included. */
121
+ params: string[];
122
+ paramNames: string[];
123
+ wires: Wire[];
124
+ result: Wire | null;
125
+ }
126
+
127
+ export interface ModuleAnalysis {
128
+ name: string;
129
+ /** The module's source. */
130
+ text: string;
131
+ functions: ExportedFunction[];
132
+ /** Exported types other than handles, as the module writes them: copied into the proxy. */
133
+ declarations: string[];
134
+ /** The module's imports, as `import` lines, with the names they bring in. */
135
+ imports: { names: string[]; line: (used: Set<string>) => string | null }[];
136
+ handles: ClassInfo[];
137
+ classes: ClassInfo[];
138
+ /** The objects the module exports, `export const semiclip = new Semiclip()`: handles numbered 1, 2, ... in this order. */
139
+ objects: { name: string; cls: ClassInfo }[];
140
+ fns: Wire[];
141
+ hash: number;
142
+ }
143
+
144
+ const cache = new Map<string, { stamp: string; analysis: Promise<ModuleAnalysis> }>();
145
+
146
+ /** The module's exports, as the compiler sees them; remembered while the file is unchanged. */
147
+ export function analyzeModule(root: string, name: string): Promise<ModuleAnalysis> {
148
+ const path = join(root, 'modules', `${name}.ts`);
149
+ // The module and everything it imports: a field added to a facade class changes what crosses.
150
+ const sources = sourcesFor(root);
151
+ const stamp = [...sources.reach(path)].map((place) => {
152
+ const file = sources.real(place);
153
+ if (!file) return place;
154
+ const info = statSync(file);
155
+ return `${file}:${info.mtimeMs}:${info.size}`;
156
+ }).join('|');
157
+ const known = cache.get(path);
158
+ if (known && known.stamp === stamp) return known.analysis;
159
+ const analysis = analyze(root, name);
160
+ cache.set(path, { stamp, analysis });
161
+ analysis.catch(() => cache.delete(path));
162
+ return analysis;
163
+ }
164
+
165
+ async function analyze(root: string, name: string): Promise<ModuleAnalysis> {
166
+ let program: any = null;
167
+ const playerFields = playerFieldsBuild();
168
+ const sources = sourcesFor(root);
169
+ const entry = `modules/${name}.ts`;
170
+
171
+ const { error, stderr } = await ascMain([entry, '--noEmit'], {
172
+ readFile(filename: string, baseDir: string): string | null {
173
+ const path = ascPath(root, filename, baseDir);
174
+ const text = sources.read(path);
175
+ return text === null ? null : playerFields.read(relative('.', (sources.real(path) ?? path)), text);
176
+ },
177
+ writeFile() {},
178
+ listFiles: () => [],
179
+ transforms: [playerFields.transform, class { afterCompile() { program = (this as any).program; } }],
180
+ });
181
+ if (error || !program) throw new Error(`~/modules/${name} does not compile:\n${stderr.toString() || String(error?.message ?? error)}`);
182
+
183
+ return describe(program, name);
184
+ }
185
+
186
+ function describe(program: any, name: string): ModuleAnalysis {
187
+ const internalPath = `modules/${name}`;
188
+ const file = program.filesByName.get(internalPath);
189
+ const source = file.source;
190
+ const text: string = source.text;
191
+ const problems: string[] = [];
192
+ const where = (what: string) => `~/modules/${name}: ${what}`;
193
+
194
+ // A module package may keep part of its API in other files it re-exports -
195
+ // `export * from "./types"`. Those files are the module too: their exports
196
+ // are its exports, their types are copied into the proxy like its own.
197
+ // Reached from the module file as ~/modules/<name>/..., a library path.
198
+ const inPackage = (path: string) => path.startsWith(`${internalPath}/`) || path.startsWith(`~lib/~/${internalPath}/`);
199
+ const files: any[] = [];
200
+ const collectFiles = (each: any) => {
201
+ if (files.includes(each)) return;
202
+ files.push(each);
203
+ for (const star of each.exportsStar ?? []) {
204
+ if (inPackage(star.internalName)) collectFiles(star);
205
+ }
206
+ };
207
+ collectFiles(file);
208
+ /** Every export of the module and of the files it re-exports, with the file it is in. */
209
+ const allExports: [string, any, any][] = [];
210
+ for (const each of files) {
211
+ for (const [exportName, element] of each.exports ?? []) {
212
+ if (!allExports.some(([known]) => known === exportName)) allExports.push([exportName, element, each]);
213
+ }
214
+ }
215
+
216
+ const classes = new Map<string, ClassInfo>();
217
+ const fns: Wire[] = [];
218
+ const functions: ExportedFunction[] = [];
219
+ const declarations: string[] = [];
220
+ const handleNames = new Set<string>();
221
+
222
+ const slice = (node: any) => node.range.source.text.slice(node.range.start, node.range.end);
223
+ const isPackageFile = (path: string) => files.some(each => each.internalName === path);
224
+
225
+ // First the module's own classes that go both ways: handed out and taken back.
226
+ const returned = new Set<string>();
227
+ const taken = new Set<string>();
228
+ const own = (type: any, into: Set<string>) => {
229
+ const t = type.isNullableReference ? type.nonNullableType : type;
230
+ const cls = t.getClass();
231
+ if (!cls) return;
232
+ if (cls.prototype === program.arrayPrototype) return own(cls.typeArguments[0], into);
233
+ if (inPackage(cls.internalName)) into.add(cls.internalName);
234
+ };
235
+
236
+ const exported: { name: string; element: any; instance: any }[] = [];
237
+ /** `export const semiclip = new Semiclip()`: an object of a class of the module's own, one on the server. */
238
+ const objects: { name: string; cls: any }[] = [];
239
+ for (const [exportName, element] of allExports) {
240
+ if (element.kind === as.ElementKind.Global) {
241
+ const cls = element.is(as.CommonFlags.Const) ? element.type?.getClass?.() : null;
242
+ if (cls && inPackage(cls.internalName)) objects.push({ name: exportName, cls });
243
+ continue;
244
+ }
245
+ if (element.kind !== as.ElementKind.FunctionPrototype) continue;
246
+ const instance = program.instancesByName.get(element.internalName);
247
+ exported.push({ name: exportName, element, instance });
248
+ if (!instance?.signature) continue;
249
+ for (const type of instance.signature.parameterTypes) own(type, taken);
250
+ own(instance.signature.returnType, returned);
251
+ }
252
+ for (const each of returned) {
253
+ if (taken.has(each) || hasMethods(program.instancesByName.get(each))) handleNames.add(each);
254
+ }
255
+ for (const object of objects) handleNames.add(object.cls.internalName);
256
+
257
+ const numeric = /^(?:i8|i16|i32|u8|u16|u32|f32|f64)$/;
258
+
259
+ /** A class with public instance methods of its own - a menu - rather than a record of fields. */
260
+ function hasMethods(cls: any): boolean {
261
+ for (let c = cls; c; c = c.base) {
262
+ for (const [memberName, member] of c.members ?? []) {
263
+ if (memberName === 'constructor' || member.kind !== as.ElementKind.FunctionPrototype) continue;
264
+ if (!member.is(as.CommonFlags.Static) && !member.is(as.CommonFlags.Private) && !member.is(as.CommonFlags.Protected)) return true;
265
+ }
266
+ }
267
+ return false;
268
+ }
269
+
270
+ /** Handles' methods met while their classes were read: resolved once every handle is known. */
271
+ const methodsToRead: { info: ClassInfo; name: string; prototype: any; context: string }[] = [];
272
+
273
+ function classInfo(cls: any, context: string): ClassInfo | null {
274
+ const known = classes.get(cls.internalName);
275
+ if (known) return known;
276
+ const path = cls.internalName.replace(/<.*$/, '').replace(/\/[^/]*$/, '');
277
+ if (path.startsWith('~lib/') && !path.startsWith('~lib/~/')) {
278
+ problems.push(`${context}: ${cls.prototype.name} from the standard library cannot cross - pass its contents as an array or a record`);
279
+ return null;
280
+ }
281
+ if (cls.typeArguments?.length) {
282
+ problems.push(`${context}: ${cls.prototype.name} is generic - an object crosses when it is a plain interface or class`);
283
+ return null;
284
+ }
285
+ let from: string | null = null;
286
+ if (!isPackageFile(path)) {
287
+ from = path.startsWith('~lib/~/') ? `~/${path.slice(7)}` : `~/${path}`;
288
+ } else if (!file.lookupExport(cls.name)) {
289
+ problems.push(`${context}: ${cls.name} is not exported - a type that crosses has to be`);
290
+ return null;
291
+ }
292
+ const info: ClassInfo = { name: cls.name, internal: cls.internalName, from, fields: [], methods: false, calls: [], alias: `__F${classes.size}_${cls.name}` };
293
+ classes.set(cls.internalName, info);
294
+ const handle = handleNames.has(cls.internalName);
295
+
296
+ const chain: any[] = [];
297
+ for (let c = cls; c; c = c.base) chain.unshift(c);
298
+ for (const c of chain) {
299
+ if (!c.members) continue;
300
+ for (const [memberName, member] of c.members) {
301
+ if (memberName === 'constructor') continue;
302
+ const hidden = member.is(as.CommonFlags.Private) || member.is(as.CommonFlags.Protected);
303
+ if (member.kind === as.ElementKind.PropertyPrototype && member.isField) {
304
+ // A handle's private field stays in the owner's object; a record is copied whole.
305
+ if (hidden && handle) continue;
306
+ if (hidden) {
307
+ problems.push(`${context}: ${cls.name}.${memberName} is private - an object crosses with all its fields`);
308
+ continue;
309
+ }
310
+ if (info.fields.some(f => f.name === memberName)) continue;
311
+ const property = program.resolver.resolveProperty(member);
312
+ if (!property) continue;
313
+ const wire = toWire(property.type, `${context} - ${cls.name}.${memberName}`);
314
+ // A function field keeps its type as written - `string | (...) => string`
315
+ // takes a string in the proxy too (FunctionTypeNode.acceptsText), and
316
+ // `(...) => R | T[]` a function (NamedTypeNode.acceptsFunction).
317
+ const typeNode = member.fieldDeclaration?.type;
318
+ const bare = wire?.kind === 'null' ? wire.of : wire;
319
+ const declared = wire && typeNode && (bare?.kind === 'fn' || (bare?.kind === 'array' && typeNode.acceptsFunction)) ? slice(typeNode) : undefined;
320
+ if (wire) info.fields.push({ name: memberName, wire, optional: property.presenceOffset >= 0, readonly: member.is(as.CommonFlags.Readonly), declared });
321
+ } else if (member.kind === as.ElementKind.FunctionPrototype || member.kind === as.ElementKind.PropertyPrototype) {
322
+ if (!member.is(as.CommonFlags.Static)) info.methods = true;
323
+ // A handle's accessor - `get rule()`, `set rule(...)` - is read and
324
+ // written in the owner, as a field is: the setter runs there.
325
+ if (handle && member.kind === as.ElementKind.PropertyPrototype && !member.is(as.CommonFlags.Static) && !hidden) {
326
+ if (info.fields.some(f => f.name === memberName)) continue;
327
+ const getter = program.resolver.resolveProperty(member)?.getterInstance;
328
+ const wire = getter ? toWire(getter.signature.returnType, `${context} - ${cls.name}.${memberName}`) : null;
329
+ if (wire) info.fields.push({ name: memberName, wire, readonly: !member.setterPrototype });
330
+ continue;
331
+ }
332
+ const method = member.kind === as.ElementKind.FunctionPrototype && !member.is(as.CommonFlags.Static) && !hidden;
333
+ if (handle && method && !methodsToRead.some(m => m.info === info && m.name === memberName)) {
334
+ methodsToRead.push({ info, name: memberName, prototype: member, context: `${context} - ${cls.name}.${memberName}` });
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return info;
340
+ }
341
+
342
+ function toWire(type: any, context: string): Wire | null {
343
+ if (type.isNullableReference) {
344
+ const of = toWire(type.nonNullableType, context);
345
+ return of ? { kind: 'null', of } : null;
346
+ }
347
+ const text = String(type.toString());
348
+ if (text === 'bool') return { kind: 'bool' };
349
+ if (numeric.test(text)) return { kind: 'num', as: text };
350
+
351
+ const signature = type.getSignature?.();
352
+ if (signature) {
353
+ const params: Wire[] = [];
354
+ signature.parameterTypes.forEach((param: any, i: number) => {
355
+ const wire = toWire(param, `${context} - its parameter ${i + 1}`);
356
+ if (wire) params.push(wire);
357
+ });
358
+ const returns = signature.returnType;
359
+ const result = String(returns.toString()) === 'void' ? null : toWire(returns, `${context} - its result`);
360
+ const fn: Wire = { kind: 'fn', params, result, sig: 0 };
361
+ const same = fns.find(f => wireKey(f) === wireKey(fn));
362
+ if (same) return same;
363
+ (fn as any).sig = fns.length;
364
+ fns.push(fn);
365
+ return fn;
366
+ }
367
+
368
+ const cls = type.getClass?.();
369
+ if (cls) {
370
+ if (cls.internalName === '~lib/string/String') return { kind: 'str' };
371
+ if (cls.prototype === program.arrayPrototype) {
372
+ const of = toWire(cls.typeArguments[0], context);
373
+ return of ? { kind: 'array', of } : null;
374
+ }
375
+ if (cls.internalName === '~lib/~/facade/Player') return { kind: 'player' };
376
+ const info = classInfo(cls, context);
377
+ if (!info) return null;
378
+ if (handleNames.has(cls.internalName)) return { kind: 'handle', cls: info };
379
+ return { kind: 'record', cls: info };
380
+ }
381
+
382
+ problems.push(`${context}: ${text} cannot cross between plugins - number, boolean, string, arrays, Player, objects and functions of them can`);
383
+ return null;
384
+ }
385
+
386
+ /** A function as the other side calls it: an exported function, or a handle's method. Null when it cannot cross. */
387
+ function callOf(fnName: string, element: any, instance: any, context: string): ExportedFunction | null {
388
+ const declaration = element.declaration;
389
+ if (element.typeParameterNodes?.length || declaration.typeParameters?.length) {
390
+ problems.push(where(`${context} - a generic function cannot be called in another plugin`));
391
+ return null;
392
+ }
393
+ if (!instance?.signature) {
394
+ problems.push(where(`${context} - not compiled`));
395
+ return null;
396
+ }
397
+ const params: string[] = [];
398
+ const paramNames: string[] = [];
399
+ const wires: Wire[] = [];
400
+ declaration.signature.parameters.forEach((param: any, i: number) => {
401
+ const paramName = param.name.text;
402
+ const pcontext = where(`${context} - parameter "${paramName}"`);
403
+ if (param.parameterKind === as.ParameterKind.Rest) {
404
+ problems.push(`${pcontext}: a rest parameter cannot cross - take an array`);
405
+ return;
406
+ }
407
+ if (param.initializer && !plainDefault(param.initializer)) {
408
+ problems.push(`${pcontext}: its default is computed - the proxy evaluates it in the other plugin, so it has to be a literal`);
409
+ return;
410
+ }
411
+ const wire = toWire(instance.signature.parameterTypes[i], pcontext);
412
+ if (!wire) return;
413
+ params.push(slice(param));
414
+ paramNames.push(paramName);
415
+ wires.push(wire);
416
+ });
417
+ const returns = instance.signature.returnType;
418
+ const returned = String(returns.toString());
419
+ if (/Promise/.test(returned)) {
420
+ problems.push(where(`${context} - an async function cannot be called in another plugin`));
421
+ return null;
422
+ }
423
+ const result = returned === 'void' ? null : toWire(returns, where(`${context} - its result`));
424
+ if (returned !== 'void' && !result) return null;
425
+ return { name: fnName, params, paramNames, wires, result };
426
+ }
427
+
428
+ for (const { name: fnName, element, instance } of exported) {
429
+ const call = callOf(fnName, element, instance, `export function ${fnName}`);
430
+ if (call) functions.push(call);
431
+ }
432
+ for (const object of objects) classInfo(object.cls, where(`export const ${object.name}`));
433
+
434
+ // A method may take or give a class met only in another method: read until none is left.
435
+ for (let i = 0; i < methodsToRead.length; i++) {
436
+ const { info, name: methodName, prototype, context } = methodsToRead[i];
437
+ const instance = program.resolver.resolveFunction(prototype, null, new Map(), as.ReportMode.Swallow);
438
+ const call = callOf(methodName, prototype, instance, context);
439
+ if (call) info.calls.push(call);
440
+ }
441
+
442
+ for (const [exportName, element] of allExports) {
443
+ if (element.kind === as.ElementKind.FunctionPrototype) continue;
444
+ if (element.kind === as.ElementKind.Global) {
445
+ if (!objects.some(object => object.name === exportName)) problems.push(where(`export ${exportName}: a variable is not shared - export a function that reads or sets it, or an object of a class of the module's own`));
446
+ continue;
447
+ }
448
+ const declaration = element.declaration;
449
+ if (!declaration || !files.some(each => each.source === declaration.range.source)) continue;
450
+ if (handleNames.has(element.internalName)) continue;
451
+ const written = slice(declaration).replace(/^(?!export\b)/, 'export ');
452
+ declarations.push(/[;}]\s*$/.test(written) ? written : `${written};`);
453
+ }
454
+
455
+ // Every handle's fields became wires with its class: the proxy's getters and setters.
456
+ const handles = [...handleNames].map(n => classes.get(n)).filter((c): c is ClassInfo => !!c);
457
+
458
+ const imports: ModuleAnalysis['imports'] = [];
459
+ for (const statement of files.flatMap(each => each.source.statements)) {
460
+ if (statement.kind !== as.NodeKind.Import) continue;
461
+ // An import of another file of the package: its types are copied in.
462
+ if (isPackageFile(statement.internalPath)) continue;
463
+ const path = statement.path.value;
464
+ if (statement.namespaceName) {
465
+ const ns = statement.namespaceName.text;
466
+ imports.push({ names: [ns], line: used => (used.has(ns) ? `import * as ${ns} from "${path}";` : null) });
467
+ } else if (statement.declarations?.length) {
468
+ const specs: { local: string; foreign: string }[] = statement.declarations.map((d: any) => ({ local: d.name.text, foreign: d.foreignName.text }));
469
+ imports.push({
470
+ names: specs.map(s => s.local),
471
+ line: (used) => {
472
+ const kept = specs.filter(s => used.has(s.local));
473
+ if (kept.length === 0) return null;
474
+ return `import { ${kept.map(s => (s.local === s.foreign ? s.local : `${s.foreign} as ${s.local}`)).join(', ')} } from "${path}";`;
475
+ },
476
+ });
477
+ } else {
478
+ // `import "~/myplugin/player"`: for what it declares, kept.
479
+ imports.push({ names: [], line: () => `import "${path}";` });
480
+ }
481
+ }
482
+
483
+ if (problems.length) throw new Error(problems.join('\n'));
484
+
485
+ const schema = [
486
+ ...functions.map(f => `${f.name}(${f.wires.map(wireKey).join(',')}):${f.result ? wireKey(f.result) : 'void'}`),
487
+ ...[...classes.values()].map(c => `${c.internal}{${c.fields.map(f => `${f.name}${f.optional ? '?' : ''}:${wireKey(f.wire)}`).join(',')}}`),
488
+ ...handles.map(h => `handle ${h.internal}{${h.fields.filter(f => f.readonly).map(f => f.name).join(',')}}${h.calls.map(c => ` ${c.name}(${c.wires.map(wireKey).join(',')}):${c.result ? wireKey(c.result) : 'void'}`).join('')}`),
489
+ ...objects.map(object => `object ${object.name} ${object.cls.internalName}`),
490
+ ].join('\n');
491
+
492
+ const shared = objects.map(object => ({ name: object.name, cls: classes.get(object.cls.internalName)! }));
493
+ return { name, text, functions, declarations, imports, handles, classes: [...classes.values()], objects: shared, fns, hash: fnv(schema) };
494
+ }
495
+
496
+ /** A default the proxy can evaluate as well: a literal, `{}`, `[]`, `null`, `-1`. */
497
+ function plainDefault(node: any): boolean {
498
+ if (node.kind === as.NodeKind.Null || node.kind === as.NodeKind.True || node.kind === as.NodeKind.False) return true;
499
+ if (node.kind === as.NodeKind.UnaryPrefix) return plainDefault(node.operand);
500
+ if (node.kind !== as.NodeKind.Literal) return false;
501
+ if (node.literalKind === as.LiteralKind.Array) return node.elementExpressions.every((e: any) => e && plainDefault(e));
502
+ if (node.literalKind === as.LiteralKind.Object) return node.values.every((v: any) => plainDefault(v));
503
+ return node.literalKind !== as.LiteralKind.Template || node.expressions.length === 0;
504
+ }
505
+
506
+ function wireKey(wire: Wire): string {
507
+ switch (wire.kind) {
508
+ case 'num': return wire.as;
509
+ case 'bool': return 'bool';
510
+ case 'str': return 'str';
511
+ case 'player': return 'player';
512
+ case 'null': return `${wireKey(wire.of)}?`;
513
+ case 'array': return `${wireKey(wire.of)}[]`;
514
+ case 'fn': return `(${wire.params.map(wireKey).join(',')})=>${wire.result ? wireKey(wire.result) : 'void'}`;
515
+ case 'record': return `rec ${wire.cls.internal}`;
516
+ case 'handle': return `handle ${wire.cls.internal}`;
517
+ }
518
+ }
519
+
520
+ /** FNV-1a, as an i32: what both sides of a module check they agree on. */
521
+ function fnv(text: string): number {
522
+ let hash = 0x811C9DC5;
523
+ for (let i = 0; i < text.length; i++) {
524
+ hash ^= text.charCodeAt(i);
525
+ hash = Math.imul(hash, 0x01000193);
526
+ }
527
+ return hash | 0;
528
+ }
529
+
530
+ // ---------------------------------------------------------------- code
531
+
532
+ type Side = 'proxy' | 'serve';
533
+
534
+ /** The functions both sides write and read values with. */
535
+ class Codec {
536
+ private ids = new Map<string, number>();
537
+ private body: string[] = [];
538
+
539
+ constructor(private analysis: ModuleAnalysis, private side: Side) {}
540
+
541
+ /** The type as the generated file writes it. */
542
+ type(wire: Wire): string {
543
+ switch (wire.kind) {
544
+ case 'num': return wire.as;
545
+ case 'bool': return 'bool';
546
+ case 'str': return 'string';
547
+ case 'player': return '__Player';
548
+ case 'null': return wire.of.kind === 'fn' ? `(${this.type(wire.of)}) | null` : `${this.type(wire.of)} | null`;
549
+ case 'array': return `Array<${this.type(wire.of)}>`;
550
+ case 'fn': return `(${wire.params.map((p, i) => `a${i}: ${this.type(p)}`).join(', ')}) => ${wire.result ? this.type(wire.result) : 'void'}`;
551
+ case 'record':
552
+ case 'handle': return this.className(wire.cls);
553
+ }
554
+ }
555
+
556
+ className(cls: ClassInfo): string {
557
+ if (cls.from !== null) return cls.alias;
558
+ return this.side === 'proxy' ? cls.name : `__M_${cls.name}`;
559
+ }
560
+
561
+ /** The number of the write/read pair for this wire, generating it on first use. */
562
+ private id(wire: Wire): number {
563
+ const key = wireKey(wire);
564
+ const known = this.ids.get(key);
565
+ if (known !== undefined) return known;
566
+ const id = this.ids.size;
567
+ this.ids.set(key, id);
568
+ this.generate(wire, id);
569
+ return id;
570
+ }
571
+
572
+ write(wire: Wire, w: string, value: string): string {
573
+ return `__w${this.id(wire)}(${w}, ${value})`;
574
+ }
575
+
576
+ read(wire: Wire, r: string): string {
577
+ return `__r${this.id(wire)}(${r})`;
578
+ }
579
+
580
+ /** Puts the fields in `r` back into `value`: a class with methods, after a call. */
581
+ update(wire: Wire, r: string, value: string): string | null {
582
+ if (wire.kind !== 'record' || !wire.cls.methods) return null;
583
+ return `__u${this.id(wire)}(${r}, ${value})`;
584
+ }
585
+
586
+ private generate(wire: Wire, n: number) {
587
+ const T = this.type(wire);
588
+ const lines = this.body;
589
+ switch (wire.kind) {
590
+ case 'num':
591
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void { w.f64(<f64>v); }`);
592
+ lines.push(`function __r${n}(r: __Reader): ${T} { return <${T}>r.f64(); }`);
593
+ break;
594
+ case 'bool':
595
+ lines.push(`function __w${n}(w: __Writer, v: bool): void { w.bool(v); }`);
596
+ lines.push(`function __r${n}(r: __Reader): bool { return r.bool(); }`);
597
+ break;
598
+ case 'str':
599
+ lines.push(`function __w${n}(w: __Writer, v: string): void { w.str(v); }`);
600
+ lines.push(`function __r${n}(r: __Reader): string { return r.str(); }`);
601
+ break;
602
+ case 'player':
603
+ lines.push(`function __w${n}(w: __Writer, v: __Player): void { w.i32(<i32>v.id); }`);
604
+ lines.push(`function __r${n}(r: __Reader): __Player { return new __Player(r.i32()); }`);
605
+ break;
606
+ case 'null': {
607
+ const inner = this.id(wire.of);
608
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void {\n\tif (v == null) {\n\t\tw.bool(false);\n\t\treturn;\n\t}\n\tw.bool(true);\n\t__w${inner}(w, v!);\n}`);
609
+ lines.push(`function __r${n}(r: __Reader): ${T} {\n\tif (!r.bool()) return null;\n\treturn __r${inner}(r);\n}`);
610
+ break;
611
+ }
612
+ case 'array': {
613
+ const inner = this.id(wire.of);
614
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void {\n\tw.i32(v.length);\n\tfor (let i: i32 = 0; i < v.length; i++) __w${inner}(w, v[i]);\n}`);
615
+ lines.push(`function __r${n}(r: __Reader): ${T} {\n\tconst count = r.i32();\n\tconst list = new Array<${this.type(wire.of)}>();\n\tfor (let i: i32 = 0; i < count; i++) list.push(__r${inner}(r));\n\treturn list;\n}`);
616
+ break;
617
+ }
618
+ case 'fn':
619
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void { w.i32(__sendFunction(changetype<usize>(v), ${wire.sig}, __invoke)); }`);
620
+ lines.push(`function __r${n}(r: __Reader): ${T} { return changetype<${T}>(__receiveFunction(r, changetype<usize>(__stub${wire.sig}))); }`);
621
+ break;
622
+ case 'record': {
623
+ const fields = wire.cls.fields;
624
+ const writes = fields.map(f => (f.optional
625
+ ? `\tw.bool(v.${f.name} !== undefined);\n\tif (v.${f.name} !== undefined) ${this.write(f.wire, 'w', `v.${f.name}`)};`
626
+ : `\t${this.write(f.wire, 'w', `v.${f.name}`)};`));
627
+ const reads = fields.map(f => (f.optional
628
+ ? `\tif (r.bool()) o.${f.name} = ${this.read(f.wire, 'r')};\n\telse o.${f.name} = undefined;`
629
+ : `\to.${f.name} = ${this.read(f.wire, 'r')};`));
630
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void {\n${writes.join('\n')}\n}`);
631
+ lines.push(`function __u${n}(r: __Reader, o: ${T}): void {\n${reads.join('\n')}\n}`);
632
+ // The fields are all set here, so the object is made without its
633
+ // constructor - a class with parameters has one too.
634
+ lines.push(`function __r${n}(r: __Reader): ${T} {\n\tconst o = changetype<${T}>(__new(offsetof<${T}>(), idof<${T}>()));\n\t__u${n}(r, o);\n\treturn o;\n}`);
635
+ break;
636
+ }
637
+ case 'handle':
638
+ if (this.side === 'proxy') {
639
+ lines.push(`const __h${n} = new Map<i32, ${T}>();`);
640
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void { w.i32(v.__handle); }`);
641
+ lines.push(`function __r${n}(r: __Reader): ${T} {\n\tconst id = r.i32();\n\tif (__h${n}.has(id)) return __h${n}.get(id);\n\tconst o = new ${T}(id);\n\t__h${n}.set(id, o);\n\treturn o;\n}`);
642
+ } else {
643
+ lines.push(`function __w${n}(w: __Writer, v: ${T}): void { w.i32(__handles.id(changetype<usize>(v))); }`);
644
+ lines.push(`function __r${n}(r: __Reader): ${T} { return changetype<${T}>(__handles.at(r.i32())); }`);
645
+ }
646
+ break;
647
+ }
648
+ }
649
+
650
+ /** A stand-in for each function signature, and the switch that calls a real one. */
651
+ callbacks(): string {
652
+ const lines: string[] = [];
653
+ const cases: string[] = [];
654
+ for (const fn of this.analysis.fns) {
655
+ if (fn.kind !== 'fn') continue;
656
+ const T = this.type(fn);
657
+ const params = fn.params.map((p, i) => `a${i}: ${this.type(p)}`).join(', ');
658
+ const result = fn.result ? this.type(fn.result) : 'void';
659
+ const body = [
660
+ '\tconst target = __remoteTarget();',
661
+ '\tconst w = __beginCallback(target);',
662
+ ...fn.params.map((p, i) => `\t${this.write(p, 'w', `a${i}`)};`),
663
+ '\tconst r = __callBack(target, w);',
664
+ ...fn.params.map((p, i) => this.update(p, 'r', `a${i}`)).filter(Boolean).map(line => `\t${line};`),
665
+ ...(fn.result ? [`\treturn ${this.read(fn.result, 'r')};`] : []),
666
+ ];
667
+ lines.push(`function __stub${fn.sig}(${params}): ${result} {\n${body.join('\n')}\n}`);
668
+
669
+ const args = fn.params.map((_, i) => `a${i}`).join(', ');
670
+ const call = [
671
+ ...fn.params.map((p, i) => `\t\t\tconst a${i} = ${this.read(p, 'r')};`),
672
+ `\t\t\tconst f = changetype<${T}>(fn);`,
673
+ fn.result ? `\t\t\tconst result = f(${args});` : `\t\t\tf(${args});`,
674
+ ...fn.params.map((p, i) => (this.update(p, 'r', `a${i}`) ? `\t\t\t${this.write(p, 'w', `a${i}`)};` : null)).filter(Boolean),
675
+ ...(fn.result ? [`\t\t\t${this.write(fn.result, 'w', 'result')};`] : []),
676
+ '\t\t\tbreak;',
677
+ ];
678
+ cases.push(`\t\tcase ${fn.sig}: {\n${call.join('\n')}\n\t\t}`);
679
+ }
680
+ lines.push(`function __invoke(fn: usize, sig: i32, r: __Reader, w: __Writer): void {\n\tswitch (sig) {\n${cases.join('\n')}\n\t}\n}`);
681
+ return lines.join('\n\n');
682
+ }
683
+
684
+ /** Everything generated so far - call last, after every write/read/update. */
685
+ functions(): string {
686
+ return this.body.join('\n\n');
687
+ }
688
+
689
+ /** `import { Player as __Player } ...` and the rest of the classes from elsewhere. */
690
+ imports(): string {
691
+ const lines = ['import { Player as __Player } from "~/facade";'];
692
+ const bySource = new Map<string, string[]>();
693
+ for (const cls of this.analysis.classes) {
694
+ const from = cls.from ?? (this.side === 'serve' ? `~/modules/${this.analysis.name}` : null);
695
+ if (!from) continue;
696
+ const alias = this.className(cls);
697
+ if (!bySource.has(from)) bySource.set(from, []);
698
+ bySource.get(from)!.push(`${cls.name} as ${alias}`);
699
+ }
700
+ for (const [from, names] of bySource) lines.push(`import { ${names.join(', ')} } from "${from}";`);
701
+ return lines.join('\n');
702
+ }
703
+ }
704
+
705
+ const REMOTE_IMPORT = 'import { Reader as __Reader, Writer as __Writer, beginCallback as __beginCallback, callBack as __callBack, receiveFunction as __receiveFunction, remoteTarget as __remoteTarget, sendFunction as __sendFunction';
706
+
707
+ /**
708
+ * Operation numbers after the functions': for each handle, a getter and - a
709
+ * field that is not readonly - a setter for each field, then its methods.
710
+ * Both sides count them here.
711
+ */
712
+ function handleOps(analysis: ModuleAnalysis) {
713
+ let op = analysis.functions.length;
714
+ return analysis.handles.map(handle => ({
715
+ fields: handle.fields.map(field => ({ get: op++, set: field.readonly ? null : op++ })),
716
+ calls: handle.calls.map(() => op++),
717
+ }));
718
+ }
719
+
720
+ /**
721
+ * A function of the proxy that runs `fn` in the owner: an exported function,
722
+ * or - with `handle`, the object it is called on - a handle's method.
723
+ */
724
+ function callSource(codec: Codec, fn: ExportedFunction, op: number, indent: string, handle: string | null): string {
725
+ const result = fn.result ? codec.type(fn.result) : 'void';
726
+ const body = [
727
+ `const __w = __service.begin(${op});`,
728
+ ...(handle ? [`__w.i32(${handle});`] : []),
729
+ ...fn.paramNames.map((p, i) => `${codec.write(fn.wires[i], '__w', p)};`),
730
+ 'const __r = __service.call(__w);',
731
+ ...fn.paramNames.map((p, i) => codec.update(fn.wires[i], '__r', p)).filter(Boolean).map(line => `${line};`),
732
+ ...(fn.result ? [`return ${codec.read(fn.result, '__r')};`] : []),
733
+ ];
734
+ return `${indent}${fn.name}(${fn.params.join(', ')}): ${result} {\n${body.map(line => `${indent}\t${line}`).join('\n')}\n${indent}}`;
735
+ }
736
+
737
+ /** The module as a plugin that does not own it reads it. */
738
+ export function proxySource(analysis: ModuleAnalysis): string {
739
+ const codec = new Codec(analysis, 'proxy');
740
+ const name = analysis.name;
741
+ const parts: string[] = [];
742
+
743
+ const ops = handleOps(analysis);
744
+
745
+ const handleClasses = analysis.handles.map((handle, h) => {
746
+ const members = handle.fields.map((field, f) => {
747
+ const T = codec.type(field.wire);
748
+ const given = field.declared ?? T;
749
+ const { get, set } = ops[h].fields[f];
750
+ const getter = [
751
+ `\tget ${field.name}(): ${T} {`,
752
+ `\t\tconst w = __service.begin(${get});`,
753
+ '\t\tw.i32(this.__handle);',
754
+ `\t\treturn ${codec.read(field.wire, '__service.call(w)')};`,
755
+ '\t}',
756
+ ];
757
+ if (set === null) return getter.join('\n');
758
+ return [
759
+ ...getter,
760
+ `\tset ${field.name}(value: ${given}) {`,
761
+ `\t\tconst w = __service.begin(${set});`,
762
+ '\t\tw.i32(this.__handle);',
763
+ `\t\t${codec.write(field.wire, 'w', 'value')};`,
764
+ '\t\t__service.call(w);',
765
+ '\t}',
766
+ ].join('\n');
767
+ });
768
+ const methods = handle.calls.map((call, c) => callSource(codec, call, ops[h].calls[c], '\t', 'this.__handle'));
769
+ return `/** ${handle.name} in ${name}'s plugin: each field is read and written there, each method runs there. */\nexport class ${handle.name} {\n\tconstructor(public __handle: i32) {}\n\n${[...members, ...methods].join('\n\n')}\n}`;
770
+ });
771
+
772
+ const functions = analysis.functions.map((fn, index) => `export function ${callSource(codec, fn, index, '', null)}`);
773
+ // The owner numbers its exported objects first, in this order (serveSource).
774
+ const objects = analysis.objects.map((object, index) => `export const ${object.name} = new ${object.cls.name}(${index + 1});`);
775
+
776
+ const callbacks = codec.callbacks();
777
+ const body = [
778
+ analysis.declarations.join('\n\n'),
779
+ handleClasses.join('\n\n'),
780
+ objects.join('\n'),
781
+ `const __service = new __Service("${name}", ${analysis.hash});`,
782
+ codec.functions(),
783
+ callbacks,
784
+ functions.join('\n\n'),
785
+ ].join('\n\n');
786
+
787
+ const used = new Set(body.match(/[A-Z_$][\w$]*/gi) ?? []);
788
+ // A name two files of the package both import comes in once.
789
+ const moduleImports = analysis.imports.map((each) => {
790
+ const line = each.line(used);
791
+ for (const name of each.names) used.delete(name);
792
+ return line;
793
+ }).filter((line): line is string => line !== null);
794
+
795
+ parts.push(
796
+ `// GENERATED by scripts/shared-modules.ts: ~/modules/${name} in a plugin that does not own it.`,
797
+ `// Each function runs in ${name}.aot, the one instance on the server.`,
798
+ `${REMOTE_IMPORT}, Service as __Service } from "~/remote";`,
799
+ codec.imports(),
800
+ ...moduleImports,
801
+ '',
802
+ body,
803
+ '',
804
+ );
805
+ return parts.join('\n');
806
+ }
807
+
808
+ /** The owner's side: the dispatcher that runs each call on the real module. */
809
+ export function serveSource(analysis: ModuleAnalysis): string {
810
+ const codec = new Codec(analysis, 'serve');
811
+ const name = analysis.name;
812
+ const cases: string[] = [];
813
+
814
+ /** A call of `fn` on `target` - the module, or a handle's object read first. */
815
+ const callCase = (op: number, fn: ExportedFunction, target: string, before: string[]) => {
816
+ const args = fn.paramNames.map((_, i) => `a${i}`).join(', ');
817
+ const lines = [
818
+ ...before,
819
+ ...fn.wires.map((wire, i) => `\t\t\tconst a${i} = ${codec.read(wire, 'r')};`),
820
+ fn.result ? `\t\t\tconst result = ${target}.${fn.name}(${args});` : `\t\t\t${target}.${fn.name}(${args});`,
821
+ ...fn.wires.map((wire, i) => (codec.update(wire, 'r', `a${i}`) ? `\t\t\t${codec.write(wire, 'w', `a${i}`)};` : null)).filter(Boolean),
822
+ ...(fn.result ? [`\t\t\t${codec.write(fn.result, 'w', 'result')};`] : []),
823
+ '\t\t\tbreak;',
824
+ ];
825
+ cases.push(`\t\tcase ${op}: {\n${lines.join('\n')}\n\t\t}`);
826
+ };
827
+
828
+ analysis.functions.forEach((fn, index) => callCase(index, fn, '__m', []));
829
+
830
+ const ops = handleOps(analysis);
831
+ analysis.handles.forEach((handle, h) => {
832
+ const wire: Wire = { kind: 'handle', cls: handle };
833
+ handle.fields.forEach((field, f) => {
834
+ const { get, set } = ops[h].fields[f];
835
+ cases.push(`\t\tcase ${get}: {\n\t\t\tconst o = ${codec.read(wire, 'r')};\n\t\t\t${codec.write(field.wire, 'w', `o.${field.name}`)};\n\t\t\tbreak;\n\t\t}`);
836
+ if (set !== null) cases.push(`\t\tcase ${set}: {\n\t\t\tconst o = ${codec.read(wire, 'r')};\n\t\t\to.${field.name} = ${codec.read(field.wire, 'r')};\n\t\t\tbreak;\n\t\t}`);
837
+ });
838
+ handle.calls.forEach((call, c) => callCase(ops[h].calls[c], call, 'o', [`\t\t\tconst o = ${codec.read(wire, 'r')};`]));
839
+ });
840
+
841
+ const dispatch = `function __dispatch(op: i32, r: __Reader, w: __Writer): void {\n\tswitch (op) {\n${cases.join('\n')}\n\t}\n}`;
842
+ const callbacks = codec.callbacks();
843
+
844
+ return [
845
+ `// GENERATED by scripts/shared-modules.ts: ~/modules/${name}, run by this plugin for every other.`,
846
+ `${REMOTE_IMPORT}, Handles as __Handles, serve as __serve } from "~/remote";`,
847
+ `import * as __m from "~/modules/${name}";`,
848
+ codec.imports(),
849
+ '',
850
+ 'const __handles = new __Handles();',
851
+ '// The exported objects are handles 1, 2, ... - the numbers the proxies have.',
852
+ ...analysis.objects.map(object => `__handles.id(changetype<usize>(__m.${object.name}));`),
853
+ '',
854
+ dispatch,
855
+ '',
856
+ callbacks,
857
+ '',
858
+ codec.functions(),
859
+ '',
860
+ `__serve("${name}", ${analysis.hash}, __dispatch);`,
861
+ '',
862
+ ].join('\n');
863
+ }
864
+
865
+ // ---------------------------------------------------------------- one compile
866
+
867
+ /**
868
+ * One compile's shared modules: which of the modules it reaches it runs and
869
+ * which it calls, worked out - and the modules analysed - before asc starts.
870
+ * `read` goes into the compile's readFile, `transform` into its transforms.
871
+ */
872
+ export async function sharedModulesBuild(root: string, entry: string) {
873
+ const owned = ownedModule(root, entry);
874
+ const proxies = new Map<string, string>();
875
+ let serve: string | null = null;
876
+ let callbacks = false;
877
+
878
+ for (const path of sourcesFor(root).reach(join(root, entry))) {
879
+ const name = moduleName(root, path);
880
+ if (!name || !hasOwner(root, name)) continue;
881
+ const analysis = await analyzeModule(root, name);
882
+ if (analysis.fns.length) callbacks = true;
883
+ if (name === owned) serve = serveSource(analysis);
884
+ else proxies.set(resolve(path), proxySource(analysis));
885
+ }
886
+
887
+ return {
888
+ /** The proxy in place of a module this plugin does not own. */
889
+ read(path: string, text: string): string {
890
+ return proxies.get(resolve(path)) ?? text;
891
+ },
892
+
893
+ transform: class {
894
+ afterParse(parser: any): void {
895
+ if (!serve && proxies.size === 0) return;
896
+ // A function that crosses is a stand-in with its place in `_env`,
897
+ // which the compiler passes on every indirect call only when asked.
898
+ if (callbacks) parser.needsCallEnv = true;
899
+ const path = entry.replace(/\\/g, '/');
900
+ const dir = path.includes('/') ? path.replace(/\/[^/]*$/, '/') : '';
901
+ if (serve) parser.parseFile(serve, `${dir}${SERVE_FILE}`, true);
902
+ parser.parseFile(EXPORT_SOURCE, `${dir}${EXPORT_FILE}`, true);
903
+ }
904
+ },
905
+ };
906
+ }