@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,973 @@
1
+ // A project: its amxts.config.ts, the module packages it lists, and the one
2
+ // tree of sources a plugin compiles from.
3
+ //
4
+ // // amxts.config.ts
5
+ // export default defineConfig({
6
+ // modules: ["@amxts/config-core", "@amxts/menu-core"],
7
+ // menus: { file: "myplugin/menu" },
8
+ // });
9
+ //
10
+ // A module package says what it is in package.json:
11
+ //
12
+ // "amxts": { "module": "src/index.ts", "natives": "src/natives.ts", "include": "include/menu_core.inc" }
13
+ //
14
+ // and in its module file, with the global defineModule:
15
+ //
16
+ // export default defineModule<MenuCoreOptions>({ // src/index.ts
17
+ // meta: { name: "menu-core", configKey: "menus" },
18
+ // requires: ["@amxts/config-core"],
19
+ // defaults: { folder: "" },
20
+ // imports: [{ from: "@amxts/menu-core", as: "menus" }],
21
+ // setup(options) { ... },
22
+ // });
23
+ //
24
+ // The config is read here, at build time, with the build's own TypeScript
25
+ // runtime - it is configuration, not plugin code. A module's definition is
26
+ // read from its source with the TypeScript parser: meta, requires, defaults
27
+ // and imports are literals, and `setup` becomes a plain function of the module
28
+ // that its top level calls with the merged options (module defaults, then
29
+ // the config under configKey) - so it runs where the module runs: in the
30
+ // owner plugin, once, when the server loads it.
31
+ //
32
+ // The tree asc reads is virtual. `~/` is the core's as/ folder, and over it:
33
+ //
34
+ // ~/<file> the project's plugins folder first, then the core's
35
+ // ~/modules/<name>.ts a module package's module file
36
+ // ~/modules/<name>/<path> any other file of that package
37
+ // ~/<name>.ts the module's owner: its natives file, or one
38
+ // generated for a module without natives
39
+ //
40
+ // Imports are rewritten on the way in: "@amxts/core" is the facade,
41
+ // "@amxts/core/<x>" is ~/<x>, a module package's name is ~/modules/<name>,
42
+ // and a relative import inside a package is the same file by its place in
43
+ // the tree. So everything downstream - the shared-modules proxy, the natives,
44
+ // the fields on Player - sees modules where it always has. Before that a
45
+ // plugin's file gets the imports of what it uses without importing it
46
+ // (scripts/auto-imports.ts), so they are rewritten like its own.
47
+ import type { AutoImport, ModuleImport } from './auto-imports';
48
+ import { createRequire } from 'node:module';
49
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
50
+ import { fileURLToPath } from 'node:url';
51
+ import ts from 'typescript';
52
+ import { coreImports, importedName, importTable, withAutoImports } from './auto-imports';
53
+ import { includeDirs } from './includes';
54
+ import { existsSync, readdirSync, readFileSync, statSync } from './tracked-fs';
55
+ import { typedConfigs } from './typed-configs';
56
+
57
+ /** The core's folder: the package this file ships in. */
58
+ export const CORE_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
59
+ /** What `~/` is: the core's facade, natives and libraries. */
60
+ export const CORE_PLUGINS = join(CORE_DIR, 'as');
61
+
62
+ export const CONFIG_FILE = 'amxts.config.ts';
63
+
64
+ /**
65
+ * The facade a compile reads, whose exports plugins use without an import:
66
+ * the core's - or, on a server, where the plugins folder holds the core's
67
+ * as/, the one there.
68
+ */
69
+ function facadeFile(dir: string): string {
70
+ const core = join(CORE_PLUGINS, 'facade.ts');
71
+ return existsSync(core) ? core : join(dir, 'facade.ts');
72
+ }
73
+
74
+ /**
75
+ * A project's folders of game files and where each goes in the game folder,
76
+ * as the amxts-server image lays a project out (docker/server/start.sh): its
77
+ * modules (.so) and Pawn plugins in addons, its dictionaries in data, the
78
+ * files its plugins precache in the game's own folders.
79
+ */
80
+ export const PROJECT_GAME_FOLDERS: [string, string][] = [
81
+ ['addons', 'addons'],
82
+ ['data', 'addons/amxmodx/data'],
83
+ ...['maps', 'models', 'sound', 'sprites', 'gfx', 'overviews', 'resource'].map((dir): [string, string] => [dir, dir]),
84
+ ];
85
+
86
+ /** What amxts.config.ts exports, before any module's own key. */
87
+ export interface AmxtsConfig {
88
+ modules?: string[];
89
+ /** Where the project's plugins are, from the project's folder: "plugins". */
90
+ pluginsDir?: string;
91
+ /** Where the build writes the .aot files and plugins.ini: "dist". */
92
+ outDir?: string;
93
+ /** The server the project is for, "rehlds" or "hlds": which includes the amxts command fetches without a server. */
94
+ target?: 'rehlds' | 'hlds';
95
+ /** `{ autoImport: false }`: plugins import what they use themselves. */
96
+ imports?: { autoImport?: boolean };
97
+ /** The modules Pawn plugins call the natives of: built even when no plugin of the project uses them. */
98
+ pawn?: string[];
99
+ [configKey: string]: unknown;
100
+ }
101
+
102
+ export type OptionValue = string | number | boolean | null | OptionValue[] | { [key: string]: OptionValue };
103
+ export type Options = Record<string, OptionValue>;
104
+
105
+ /** What a module file's defineModule({...}) says, read from its source. */
106
+ export interface ModuleDefinition {
107
+ name: string | null;
108
+ configKey: string | null;
109
+ requires: string[];
110
+ defaults: Options;
111
+ /** What plugins use without an import: `[{ from: "@amxts/menu-core", as: "menus" }]`, `[{ from: "@amxts/resemiclip", name: "semiclip" }]`. */
112
+ imports: ModuleImport[];
113
+ /** `defineModule<MenuCoreOptions>`: the type setup's parameter is. */
114
+ optionsType: string | null;
115
+ hasSetup: boolean;
116
+ }
117
+
118
+ export interface ModulePackage {
119
+ /** The package name: "@amxts/menu-core". */
120
+ name: string;
121
+ /** Its name without the scope - the module's name in the tree and on the server: "menu-core". */
122
+ short: string;
123
+ dir: string;
124
+ /** The module file: the API plugins import. */
125
+ module: string;
126
+ /**
127
+ * Its Pawn natives: the plugin that owns the module - its one instance on
128
+ * the server. Without one, the build generates an owner that only runs it.
129
+ */
130
+ natives: string | null;
131
+ /** The Pawn include the natives implement. */
132
+ include: string | null;
133
+ /**
134
+ * `"contract": true`: the include is the original's, kept as it is - the
135
+ * build checks the natives against it instead of writing it.
136
+ */
137
+ contract: boolean;
138
+ /**
139
+ * Its test kit - `"amxts": { "testing": "testing/index.ts" }` - a file for
140
+ * the test runner whose default export extends the fake server with what
141
+ * the module needs there (defineTestKit in @amxts/core/test-utils).
142
+ */
143
+ testing: string | null;
144
+ version: string;
145
+ description: string;
146
+ definition: ModuleDefinition;
147
+ }
148
+
149
+ export interface Project {
150
+ dir: string;
151
+ config: AmxtsConfig | null;
152
+ pluginsDir: string;
153
+ outDir: string;
154
+ /** Every module package found: the project itself, modules/*, node_modules. */
155
+ packages: ModulePackage[];
156
+ /** The ones this project uses, in load order: every module after what it requires. */
157
+ modules: ModulePackage[];
158
+ /** What plugins use without an import - the facade's API, the modules' namespaces - unless the config turns it off. */
159
+ autoImports: AutoImport[];
160
+ /** What is wrong with the project: a missing module, a requirement not listed. */
161
+ problems: string[];
162
+ }
163
+
164
+ // ---------------------------------------------------------------- definitions
165
+
166
+ /** The literal value of an expression in a module definition, or a problem. */
167
+ function literal(node: ts.Expression, where: string, problems: string[]): OptionValue {
168
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) return literal(node.expression, where, problems);
169
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
170
+ if (ts.isNumericLiteral(node)) return Number(node.text);
171
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) return -Number(node.operand.text);
172
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
173
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
174
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
175
+ if (ts.isArrayLiteralExpression(node)) return node.elements.map((element, i) => literal(element, `${where}[${i}]`, problems));
176
+ if (ts.isObjectLiteralExpression(node)) {
177
+ const object: Options = {};
178
+ for (const property of node.properties) {
179
+ if (!ts.isPropertyAssignment(property) || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
180
+ problems.push(`${where}: only \`key: value\` goes here`);
181
+ continue;
182
+ }
183
+ object[property.name.text] = literal(property.initializer, `${where}.${property.name.text}`, problems);
184
+ }
185
+ return object;
186
+ }
187
+ problems.push(`${where}: a literal goes here - a string, a number, a boolean, an array or an object of them - not "${node.getText()}"`);
188
+ return null;
189
+ }
190
+
191
+ /** The `export default defineModule(...)` statement of a module file, if it has one. */
192
+ function definitionCall(file: ts.SourceFile): { statement: ts.ExportAssignment; call: ts.CallExpression } | null {
193
+ for (const statement of file.statements) {
194
+ if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
195
+ const call = statement.expression;
196
+ if (ts.isCallExpression(call) && ts.isIdentifier(call.expression) && call.expression.text === 'defineModule') return { statement, call };
197
+ }
198
+ return null;
199
+ }
200
+
201
+ function parse(path: string, text: string) {
202
+ return ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
203
+ }
204
+
205
+ /** A module file's definition; null when it has none. Throws on one it cannot read. */
206
+ export function readDefinition(path: string, text = readFileSync(path, 'utf8')): ModuleDefinition | null {
207
+ if (!text.includes('defineModule')) return null;
208
+ const found = definitionCall(parse(path, text));
209
+ if (!found) return null;
210
+
211
+ const where = `${path.replace(/\\/g, '/')}: defineModule`;
212
+ const problems: string[] = [];
213
+ const [argument] = found.call.arguments;
214
+ if (!argument || !ts.isObjectLiteralExpression(argument)) throw new Error(`${where} takes an object literal: { meta, requires, defaults, imports, setup }`);
215
+
216
+ const definition: ModuleDefinition = {
217
+ name: null,
218
+ configKey: null,
219
+ requires: [],
220
+ defaults: {},
221
+ imports: [],
222
+ optionsType: found.call.typeArguments?.[0]?.getText() ?? null,
223
+ hasSetup: false,
224
+ };
225
+
226
+ for (const property of argument.properties) {
227
+ const key = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : '';
228
+ if (key === 'setup') {
229
+ definition.hasSetup = true;
230
+ continue;
231
+ }
232
+ if (!ts.isPropertyAssignment(property)) {
233
+ problems.push(`${where}: ${key || 'this member'} - only setup is a function`);
234
+ continue;
235
+ }
236
+ const value = literal(property.initializer, `${where} ${key}`, problems);
237
+ if (key === 'meta') {
238
+ const meta = (value ?? {}) as Options;
239
+ if (typeof meta.name === 'string') definition.name = meta.name;
240
+ if (typeof meta.configKey === 'string') definition.configKey = meta.configKey;
241
+ } else if (key === 'requires') {
242
+ if (!Array.isArray(value) || value.some(each => typeof each !== 'string')) problems.push(`${where} requires: a list of package names`);
243
+ else definition.requires = value as string[];
244
+ } else if (key === 'defaults') {
245
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) problems.push(`${where} defaults: an object`);
246
+ else definition.defaults = value as Options;
247
+ } else if (key === 'imports') {
248
+ const named = (each: Options) => [each.as, each.name].filter(one => one !== undefined);
249
+ const valid = Array.isArray(value) && value.every(each => isObject(each) && typeof each.from === 'string' && Object.keys(each).length === 2
250
+ && named(each).length === 1 && named(each).every(one => typeof one === 'string' && /^[A-Z_$][\w$]*$/i.test(one)));
251
+ if (!valid) problems.push(`${where} imports: a list of { from, as } or { from, name } - { from: "@you/greeter", as: "greeter" } is the module as a namespace, { from: "@you/greeter", name: "greeter" } its export greeter; plugins use either without an import`);
252
+ else definition.imports = value as unknown as ModuleImport[];
253
+ } else {
254
+ problems.push(`${where}: "${key}" is not a part of a module - meta, requires, defaults, imports and setup are`);
255
+ }
256
+ }
257
+
258
+ if (definition.hasSetup && !definition.optionsType && Object.keys(definition.defaults).length > 0) {
259
+ problems.push(`${where}: name the options' type - defineModule<MyOptions>({ ... }) - setup's parameter is that type`);
260
+ }
261
+ if (problems.length) throw new Error(problems.join('\n'));
262
+ return definition;
263
+ }
264
+
265
+ /** A string of spaces as long as `text`, its line breaks kept: offsets after it stay put. */
266
+ function blank(text: string): string {
267
+ return text.replace(/[^\r\n]/g, ' ');
268
+ }
269
+
270
+ /** An options value as AssemblyScript source: an object literal the options' interface takes. */
271
+ export function optionsSource(value: OptionValue): string {
272
+ if (value === null) return 'null';
273
+ if (typeof value === 'string') return JSON.stringify(value);
274
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
275
+ if (Array.isArray(value)) return `[${value.map(optionsSource).join(', ')}]`;
276
+ return `{ ${Object.entries(value).map(([key, each]) => `${/^[A-Z_$][\w$]*$/i.test(key) ? key : JSON.stringify(key)}: ${optionsSource(each)}`).join(', ')} }`;
277
+ }
278
+
279
+ /**
280
+ * The module file as asc reads it: `export default defineModule({ ... })`
281
+ * becomes `function __amxts_setup(options: T) { ... }`, in the same place and
282
+ * on the same lines, and the file's last line calls it with `options`. What
283
+ * the editor needs and asc cannot read - `declare module "@amxts/core"` with
284
+ * the module's ModuleOptions - is blanked.
285
+ */
286
+ export function moduleSource(path: string, text: string, options: Options): string {
287
+ let out = withoutOptionsAugmentation(text);
288
+ if (!out.includes('defineModule')) return out;
289
+ const file = parse(path, out);
290
+ const found = definitionCall(file);
291
+ if (!found) return out;
292
+
293
+ const definition = found.call.arguments[0] as ts.ObjectLiteralExpression;
294
+ const setup = definition.properties.find(p => p.name && ts.isIdentifier(p.name) && p.name.text === 'setup');
295
+ let fn: ts.FunctionLikeDeclaration | null = null;
296
+ if (setup && ts.isMethodDeclaration(setup)) fn = setup;
297
+ else if (setup && ts.isPropertyAssignment(setup) && (ts.isArrowFunction(setup.initializer) || ts.isFunctionExpression(setup.initializer))) fn = setup.initializer;
298
+
299
+ const start = found.statement.getStart(file);
300
+ const end = found.statement.end;
301
+ if (!fn || !fn.body || !ts.isBlock(fn.body)) return out.slice(0, start) + blank(out.slice(start, end)) + out.slice(end);
302
+
303
+ // A setup that takes no options - a module without any - is called without them.
304
+ const type = found.call.typeArguments?.[0]?.getText(file) ?? 'Record<string, string>';
305
+ const param = fn.parameters[0]?.name.getText(file);
306
+ const head = fn.getStart(file);
307
+ const body = fn.body.getStart(file);
308
+ out = [
309
+ out.slice(0, start),
310
+ blank(out.slice(start, head)),
311
+ `function __amxts_setup(${param ? `${param}: ${type}` : ''}): void `,
312
+ out.slice(body, fn.body.end),
313
+ blank(out.slice(fn.body.end, end)),
314
+ out.slice(end),
315
+ ].join('');
316
+ return `${out.replace(/\s*$/, '')}\n__amxts_setup(${param ? optionsSource(options) : ''});\n`;
317
+ }
318
+
319
+ /**
320
+ * `declare module "@amxts/core" { interface ModuleOptions { ... } }` blanked:
321
+ * it types amxts.config.ts in the editor and means nothing to asc. Anything
322
+ * else in the block (Player's fields) is left for scripts/player-fields.ts.
323
+ */
324
+ function withoutOptionsAugmentation(text: string): string {
325
+ if (!text.includes('ModuleOptions')) return text;
326
+ let out = text;
327
+ for (const match of text.matchAll(/declare\s+module\s+["'](?:@amxts\/core|~\/facade)["']\s*\{/g)) {
328
+ const open = match.index! + match[0].length - 1;
329
+ const close = closing(text, open);
330
+ if (close < 0) continue;
331
+ let block = out.slice(open + 1, close);
332
+ for (const inner of block.matchAll(/(?:export\s+)?interface\s+ModuleOptions\s*\{/g)) {
333
+ const innerOpen = inner.index! + inner[0].length - 1;
334
+ const innerClose = closing(block, innerOpen);
335
+ if (innerClose < 0) continue;
336
+ block = block.slice(0, inner.index!) + blank(block.slice(inner.index!, innerClose + 1)) + block.slice(innerClose + 1);
337
+ }
338
+ const emptied = !/\S/.test(block.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''));
339
+ out = emptied
340
+ ? out.slice(0, match.index!) + blank(out.slice(match.index!, close + 1)) + out.slice(close + 1)
341
+ : out.slice(0, open + 1) + block + out.slice(close);
342
+ }
343
+ return out;
344
+ }
345
+
346
+ function closing(text: string, open: number): number {
347
+ let depth = 0;
348
+ for (let i = open; i < text.length; i++) {
349
+ if (text[i] === '{') depth++;
350
+ else if (text[i] === '}' && --depth === 0) return i;
351
+ }
352
+ return -1;
353
+ }
354
+
355
+ // ---------------------------------------------------------------- packages
356
+
357
+ function readJson(path: string): any {
358
+ try {
359
+ return JSON.parse(readFileSync(path, 'utf8'));
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
365
+ /** The module package in `dir`, when its package.json has an "amxts" field with a module. */
366
+ export function readPackage(dir: string): ModulePackage | null {
367
+ const json = readJson(join(dir, 'package.json'));
368
+ const amxts = json?.amxts;
369
+ if (!json?.name || !amxts || typeof amxts.module !== 'string') return null;
370
+
371
+ const module = resolve(dir, amxts.module);
372
+ if (!existsSync(module)) throw new Error(`${json.name}: its module ${amxts.module} is not there (package.json, "amxts".module)`);
373
+ const natives = typeof amxts.natives === 'string' ? resolve(dir, amxts.natives) : null;
374
+ if (natives && !existsSync(natives)) throw new Error(`${json.name}: its natives ${amxts.natives} are not there (package.json, "amxts".natives)`);
375
+ const include = typeof amxts.include === 'string' ? resolve(dir, amxts.include) : null;
376
+
377
+ const short = String(json.name).replace(/^@[^/]+\//, '');
378
+ const definition = readDefinition(module) ?? { name: null, configKey: null, requires: [], defaults: {}, imports: [], optionsType: null, hasSetup: false };
379
+ if (definition.name && definition.name !== short) {
380
+ throw new Error(`${json.name}: defineModule's meta.name is "${definition.name}" - it is the package's name without the scope, "${short}"`);
381
+ }
382
+ // A module gives its own API, whose one instance runs on the server; what
383
+ // another package exports is that package's to give.
384
+ const foreign = definition.imports.find(each => each.from !== json.name);
385
+ if (foreign) throw new Error(`${json.name}: defineModule's imports give ${importedName(foreign)} from "${foreign.from}" - a module gives its own API, from "${json.name}"`);
386
+ if (amxts.contract === true && !include) throw new Error(`${json.name}: "contract": true needs the include it keeps ("amxts".include)`);
387
+ if (include && amxts.contract === true && !existsSync(include)) throw new Error(`${json.name}: its contract ${amxts.include} is not there (package.json, "amxts".include)`);
388
+ const testing = typeof amxts.testing === 'string' ? resolve(dir, amxts.testing) : null;
389
+ if (testing && !existsSync(testing)) throw new Error(`${json.name}: its test kit ${amxts.testing} is not there (package.json, "amxts".testing)`);
390
+ return { name: json.name, short, dir: resolve(dir), module, natives, include, contract: amxts.contract === true, testing, version: String(json.version ?? '0.0.0'), description: String(json.description ?? ''), definition };
391
+ }
392
+
393
+ /**
394
+ * Every module package the project can see: itself, its modules/ folder, then
395
+ * node_modules - its own, then each folder's above it, as Node looks for a
396
+ * package: a module's playground finds what the module installed.
397
+ */
398
+ function discover(dir: string): ModulePackage[] {
399
+ const found = new Map<string, ModulePackage>();
400
+ const add = (packageDir: string) => {
401
+ if (!existsSync(join(packageDir, 'package.json'))) return;
402
+ const found_ = readPackage(packageDir);
403
+ if (found_ && !found.has(found_.name)) found.set(found_.name, found_);
404
+ };
405
+ const each = (folder: string, visit: (path: string, name: string) => void) => {
406
+ if (!existsSync(folder)) return;
407
+ for (const name of readdirSync(folder)) {
408
+ const path = join(folder, name);
409
+ // A package that cannot be read says why; only a path that cannot be stat'ed is skipped.
410
+ let directory = false;
411
+ try {
412
+ directory = statSync(path).isDirectory();
413
+ } catch {}
414
+ if (directory) visit(path, name);
415
+ }
416
+ };
417
+
418
+ add(dir);
419
+ // A `file:` dependency is read where it is: a module being written beside
420
+ // the project, installed or not (Bun copies a file: folder, npm links it).
421
+ const json = readJson(join(dir, 'package.json'));
422
+ for (const deps of [json?.dependencies, json?.devDependencies, json?.peerDependencies]) {
423
+ for (const spec of Object.values(deps ?? {})) {
424
+ if (typeof spec === 'string' && spec.startsWith('file:')) add(resolve(dir, spec.slice(5)));
425
+ }
426
+ }
427
+ each(join(dir, 'modules'), path => add(path));
428
+ for (let at = resolve(dir); ; at = dirname(at)) {
429
+ each(join(at, 'node_modules'), (path, name) => {
430
+ if (name.startsWith('.')) return;
431
+ if (name.startsWith('@')) each(path, inner => add(inner));
432
+ else add(path);
433
+ });
434
+ if (dirname(at) === at) break;
435
+ }
436
+ return [...found.values()];
437
+ }
438
+
439
+ // ---------------------------------------------------------------- config
440
+
441
+ /** amxts.config.ts's default export, read with a global defineConfig; null without the file. */
442
+ export function readConfig(dir: string): AmxtsConfig | null {
443
+ const path = join(dir, CONFIG_FILE);
444
+ if (!existsSync(path)) return null;
445
+
446
+ const scope = globalThis as any;
447
+ scope.defineConfig ??= (config: AmxtsConfig) => config;
448
+ const require = createRequire(import.meta.url);
449
+ delete require.cache[path];
450
+ let loaded;
451
+ try {
452
+ loaded = require(path);
453
+ } catch (error) {
454
+ // Bun's parse error carries where it is, not a stack.
455
+ const at = (error as { position?: { line: number; column: number; lineText: string } }).position;
456
+ const where = at ? `:${at.line}:${at.column + 1}` : '';
457
+ const line = at ? `\n ${at.lineText.trim()}` : '';
458
+ throw new Error(`${CONFIG_FILE}${where} - ${(error as Error).message}${line}`);
459
+ }
460
+ const config = loaded?.default ?? loaded;
461
+ if (!config || typeof config !== 'object') throw new Error(`${CONFIG_FILE}: export default defineConfig({ modules: [...] })`);
462
+ if (config.modules !== undefined && (!Array.isArray(config.modules) || config.modules.some((m: unknown) => typeof m !== 'string'))) {
463
+ throw new Error(`${CONFIG_FILE}: modules is a list of package names - ["@amxts/menu-core"]`);
464
+ }
465
+ if (config.target !== undefined && config.target !== 'rehlds' && config.target !== 'hlds') {
466
+ throw new Error(`${CONFIG_FILE}: target is "rehlds" (ReHLDS, ReGameDLL and ReAPI) or "hlds" (plain HLDS), not ${JSON.stringify(config.target)}`);
467
+ }
468
+ if (config.imports !== undefined && (!isObject(config.imports) || Object.entries(config.imports).some(([key, value]) => key !== 'autoImport' || typeof value !== 'boolean'))) {
469
+ throw new Error(`${CONFIG_FILE}: imports is { autoImport: false } - plugins import what they use themselves - or left out`);
470
+ }
471
+ if (config.pawn !== undefined && (!Array.isArray(config.pawn) || config.pawn.some((m: unknown) => typeof m !== 'string'))) {
472
+ throw new Error(`${CONFIG_FILE}: pawn is a list of package names, the modules whose natives Pawn plugins call - ["@amxts/menu-core"]`);
473
+ }
474
+ return config;
475
+ }
476
+
477
+ /** Every listed module after what it requires, in the order the config lists them otherwise. */
478
+ function ordered(listed: ModulePackage[], problems: string[]): ModulePackage[] {
479
+ const byName = new Map(listed.map(pkg => [pkg.name, pkg]));
480
+ const out: ModulePackage[] = [];
481
+ const state = new Map<string, 'visiting' | 'done'>();
482
+ const visit = (pkg: ModulePackage, chain: string[]) => {
483
+ if (state.get(pkg.name) === 'done') return;
484
+ if (state.get(pkg.name) === 'visiting') {
485
+ problems.push(`${CONFIG_FILE}: modules require each other in a circle - ${[...chain, pkg.name].join(' -> ')}`);
486
+ return;
487
+ }
488
+ state.set(pkg.name, 'visiting');
489
+ for (const required of pkg.definition.requires) {
490
+ const dependency = byName.get(required);
491
+ if (dependency) visit(dependency, [...chain, pkg.name]);
492
+ }
493
+ state.set(pkg.name, 'done');
494
+ out.push(pkg);
495
+ };
496
+ for (const pkg of listed) visit(pkg, []);
497
+ return out;
498
+ }
499
+
500
+ const projects = new Map<string, { stamp: string; project: Project }>();
501
+
502
+ function stampOf(dir: string): string {
503
+ const path = join(dir, CONFIG_FILE);
504
+ return existsSync(path) ? String(statSync(path).mtimeMs) : 'none';
505
+ }
506
+
507
+ /**
508
+ * The project in `dir`: its config, its modules in load order and what is
509
+ * wrong with them. Without amxts.config.ts every module package it can see
510
+ * is in use, in dependency order - a module's own repository, testing itself.
511
+ */
512
+ export function loadProject(dir = process.cwd()): Project {
513
+ const at = resolve(dir);
514
+ const stamp = stampOf(at);
515
+ const known = projects.get(at);
516
+ if (known && known.stamp === stamp) return known.project;
517
+
518
+ const config = readConfig(at);
519
+ const packages = discover(at);
520
+ const problems: string[] = [];
521
+ const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
522
+
523
+ let listed: ModulePackage[];
524
+ if (config) {
525
+ listed = [];
526
+ for (const name of config.modules ?? []) {
527
+ const pkg = byName.get(name);
528
+ if (!pkg) problems.push(`${CONFIG_FILE}: module "${name}" is not installed - no package of that name with an "amxts" field in modules/ or node_modules`);
529
+ else if (!listed.includes(pkg)) listed.push(pkg);
530
+ }
531
+ // What a listed module requires comes along: `modules:
532
+ // ["@amxts/menu-core"]` is enough. The package manager has put it in
533
+ // node_modules already (a peer dependency).
534
+ for (let i = 0; i < listed.length; i++) {
535
+ for (const required of listed[i].definition.requires) {
536
+ if (listed.some(each => each.name === required)) continue;
537
+ const pkg = byName.get(required);
538
+ if (pkg) listed.push(pkg);
539
+ else problems.push(`${CONFIG_FILE}: ${listed[i].name} requires ${required}, which is not installed - npm install ${required}`);
540
+ }
541
+ }
542
+ } else {
543
+ listed = packages;
544
+ }
545
+
546
+ const pluginsDir = resolve(at, config?.pluginsDir ?? 'plugins');
547
+ const modules = ordered(listed, problems);
548
+ const project: Project = {
549
+ dir: at,
550
+ config,
551
+ pluginsDir: existsSync(pluginsDir) ? pluginsDir : CORE_PLUGINS,
552
+ outDir: resolve(at, config?.outDir ?? 'dist'),
553
+ packages,
554
+ modules,
555
+ autoImports: config?.imports?.autoImport === false
556
+ ? []
557
+ : [...importTable(coreImports(facadeFile(at)), modules.map(pkg => ({ name: pkg.name, imports: pkg.definition.imports })), problems).values()],
558
+ problems,
559
+ };
560
+ if (config) checkOptionKeys(project, problems);
561
+ projects.set(at, { stamp, project });
562
+ return project;
563
+ }
564
+
565
+ /** Keys under a module's configKey that it has no default for: a typo, or an option it does not have. */
566
+ function checkOptionKeys(project: Project, problems: string[]) {
567
+ // A key no listed module takes would be dropped without a word.
568
+ const keys = new Set(['modules', 'pluginsDir', 'outDir', 'target', 'imports', 'pawn', ...project.modules.map(pkg => pkg.definition.configKey).filter(Boolean)]);
569
+ for (const key of Object.keys(project.config!)) {
570
+ if (!keys.has(key)) problems.push(`${CONFIG_FILE}: ${key} - no module in modules takes it (its defineModule has no configKey "${key}")`);
571
+ }
572
+ for (const name of project.config!.pawn ?? []) {
573
+ if (!project.modules.some(pkg => pkg.name === name)) problems.push(`${CONFIG_FILE}: pawn names ${name}, which modules does not list`);
574
+ }
575
+
576
+ for (const pkg of project.modules) {
577
+ const key = pkg.definition.configKey;
578
+ if (!key || project.config![key] === undefined) continue;
579
+ const given = project.config![key];
580
+ if (given === null || typeof given !== 'object' || Array.isArray(given)) {
581
+ problems.push(`${CONFIG_FILE}: ${key} is ${pkg.name}'s options - an object`);
582
+ continue;
583
+ }
584
+ for (const option of Object.keys(given)) {
585
+ if (!(option in pkg.definition.defaults)) {
586
+ problems.push(`${CONFIG_FILE}: ${key}.${option} - ${pkg.name} has no such option (${Object.keys(pkg.definition.defaults).join(', ') || 'it has none'})`);
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ function isObject(value: unknown): value is Record<string, OptionValue> {
593
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
594
+ }
595
+
596
+ /** Defaults, then the config's values over them: objects merged key by key, anything else replaced. */
597
+ export function mergeOptions(defaults: Options, given: unknown): Options {
598
+ if (!isObject(given)) return { ...defaults };
599
+ const out: Options = { ...defaults };
600
+ for (const [key, value] of Object.entries(given)) {
601
+ if (value === undefined) continue;
602
+ out[key] = isObject(out[key]) && isObject(value) ? mergeOptions(out[key] as Options, value) : value as OptionValue;
603
+ }
604
+ return out;
605
+ }
606
+
607
+ /** The options a module's setup gets in this project. */
608
+ export function optionsOf(project: Project, definition: ModuleDefinition): Options {
609
+ const key = definition.configKey;
610
+ return mergeOptions(definition.defaults, key && project.config ? project.config[key] : undefined);
611
+ }
612
+
613
+ /**
614
+ * Everything in the core's as/ that is not a plugin: the facade a plugin
615
+ * imports, the kit a module imports and the generated native layer under them.
616
+ */
617
+ export const NOT_PLUGINS = new Set(['facade.ts', 'kit.ts', 'promise.ts', 'vector.ts', 'effects.ts', 'fs.ts', 'os.ts', 'natives.ts', 'remote.ts', 'constants.ts', 'events.ts', 'entities.ts', 'flags.ts', 'hooks.ts']);
618
+
619
+ /**
620
+ * The project's own plugins: the .ts files at the top of its plugins folder.
621
+ * A module's own folder, without a plugins folder, has none - only the core's
622
+ * repository builds the core's as/.
623
+ */
624
+ export function projectPlugins(project: Project): string[] {
625
+ const core = resolve(project.pluginsDir) === resolve(CORE_PLUGINS);
626
+ if (core && resolve(project.dir) !== resolve(CORE_DIR)) return [];
627
+ return readdirSync(project.pluginsDir)
628
+ .filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts') && !(core && NOT_PLUGINS.has(f)))
629
+ .map(f => join(project.pluginsDir, f));
630
+ }
631
+
632
+ /** plugins.ini: the modules' owners in load order, then the project's plugins. */
633
+ export function pluginList(project: Project, plugins: string[], modules = project.modules): string[] {
634
+ const owners = modules.map(pkg => `${pkg.short}.aot`);
635
+ return [...owners, ...plugins.filter(plugin => !owners.includes(plugin))];
636
+ }
637
+
638
+ /**
639
+ * The modules the project needs on the server, in load order: the ones its
640
+ * plugins import - by hand or through an auto-import, themselves or through
641
+ * the files they import - with what those modules import and require, and
642
+ * the ones `pawn` keeps for Pawn plugins, whose use no build can see.
643
+ * Without amxts.config.ts every module is in use: a module's own
644
+ * repository, testing itself.
645
+ */
646
+ export function modulesInUse(sources: Sources, plugins: string[]): ModulePackage[] {
647
+ const { project } = sources;
648
+ if (!project.config) return project.modules;
649
+ const used = new Set(project.config.pawn ?? []);
650
+ const reached = new Set<string>();
651
+ const reach = (source: string) => sources.reach(join(sources.root, sources.entry(source)), reached);
652
+ plugins.forEach(reach);
653
+ // A module in use brings what its owner imports and what it requires.
654
+ for (let before = -1; used.size !== before;) {
655
+ before = used.size;
656
+ for (const pkg of project.modules) {
657
+ const imported = reached.has(join(sources.root, 'modules', `${pkg.short}.ts`));
658
+ if (!imported && !used.has(pkg.name)) continue;
659
+ reach(sources.ownerSource(pkg));
660
+ used.add(pkg.name);
661
+ for (const required of pkg.definition.requires) used.add(required);
662
+ }
663
+ }
664
+ return project.modules.filter(pkg => used.has(pkg.name));
665
+ }
666
+
667
+ // ---------------------------------------------------------------- the tree
668
+
669
+ const IMPORT = /(\bfrom\s*|\bimport(?:\s*\()?\s*|\bdeclare\s+module\s+)(["'])([^"'\n]+)\2/g;
670
+
671
+ function posix(path: string): string {
672
+ return path.replace(/\\/g, '/');
673
+ }
674
+
675
+ /** Whether `path` is `dir` or under it - on the same drive. */
676
+ function inside(dir: string, path: string): boolean {
677
+ const rel = relative(dir, path);
678
+ return !rel.startsWith('..') && !isAbsolute(rel);
679
+ }
680
+
681
+ /**
682
+ * Whether a file on disk is the package's own: in its folder, but not what it
683
+ * installed - its node_modules, the core among them when it is a copy there.
684
+ */
685
+ function ofPackage(pkg: ModulePackage, path: string): boolean {
686
+ return inside(pkg.dir, path) && !inside(join(pkg.dir, 'node_modules'), path) && !inside(CORE_DIR, path);
687
+ }
688
+
689
+ /** Plugins with their auto-imports added, by the project's table: a compile's Sources are made anew, the table is the project's. */
690
+ const importedFiles = new WeakMap<AutoImport[], { table: Map<string, AutoImport>; texts: Map<string, { text: string; out: string }> }>();
691
+
692
+ /**
693
+ * The sources one compile reads: the core's as/ as `~/`, the project's plugins
694
+ * over it, and the module packages mapped into it. Every path in and out is an
695
+ * absolute path in that tree; `real` says which file on disk it is.
696
+ */
697
+ export class Sources {
698
+ /** A plugin that imports a module package the config does not list. */
699
+ readonly problems: string[] = [];
700
+
701
+ constructor(readonly root: string, readonly project: Project) {}
702
+
703
+ /**
704
+ * Whether a file is a plugin's code, which auto-imports reach: the
705
+ * project's plugins folder but its modules/, or a plugin outside every
706
+ * package (a test's). The core's as/ and a module - a package, or a file
707
+ * under modules/ - import what they use: a module is compiled in projects
708
+ * and on servers whose settings it does not know.
709
+ */
710
+ private isPlugin(real: string): boolean {
711
+ const own = resolve(this.project.pluginsDir) !== resolve(CORE_PLUGINS);
712
+ if (own && inside(this.project.pluginsDir, real)) return !posix(relative(this.project.pluginsDir, real)).startsWith('modules/');
713
+ if (inside(CORE_PLUGINS, real) || this.packageOf(real)) return false;
714
+ // On a server the plugins folder holds the core's as/ and the modules too.
715
+ const place = this.rel(this.place(real));
716
+ return !place.startsWith('modules/') && !NOT_PLUGINS.has(place);
717
+ }
718
+
719
+ /** A plugin's text with the imports of what it uses without importing (scripts/auto-imports.ts). */
720
+ private withImports(real: string, text: string): string {
721
+ const imports = this.project.autoImports;
722
+ if (imports.length === 0 || !this.isPlugin(real)) return text;
723
+ let files = importedFiles.get(imports);
724
+ if (!files) importedFiles.set(imports, files = { table: new Map(imports.map(entry => [entry.name, entry])), texts: new Map() });
725
+ const known = files.texts.get(real);
726
+ if (known?.text === text) return known.out;
727
+ const out = withAutoImports(real, text, files.table);
728
+ files.texts.set(real, { text, out });
729
+ return out;
730
+ }
731
+
732
+ private rel(path: string): string {
733
+ return posix(relative(this.root, resolve(path)));
734
+ }
735
+
736
+ private moduleNamed(short: string): ModulePackage | null {
737
+ return this.project.modules.find(pkg => pkg.short === short) ?? null;
738
+ }
739
+
740
+ /** The package a file on disk belongs to. */
741
+ packageOf(real: string): ModulePackage | null {
742
+ const path = resolve(real);
743
+ return this.project.modules.find(pkg => path === pkg.module || path === pkg.natives || ofPackage(pkg, path)) ?? null;
744
+ }
745
+
746
+ /** The file on disk at this place in the tree, or null. */
747
+ real(path: string): string | null {
748
+ const rel = this.rel(path);
749
+ if (!inside(this.root, resolve(path))) return existsSync(path) ? resolve(path) : null;
750
+
751
+ if (this.project.pluginsDir !== resolve(this.root)) {
752
+ const own = join(this.project.pluginsDir, rel);
753
+ if (existsSync(own)) return own;
754
+ }
755
+ const core = resolve(this.root, rel);
756
+ if (existsSync(core)) return core;
757
+
758
+ const moduleFile = rel.match(/^modules\/([^/]+)\.ts$/);
759
+ if (moduleFile) return this.moduleNamed(moduleFile[1])?.module ?? null;
760
+ const packageFile = rel.match(/^modules\/([^/]+)\/(.+)$/);
761
+ if (packageFile) {
762
+ const pkg = this.moduleNamed(packageFile[1]);
763
+ const file = pkg ? join(pkg.dir, packageFile[2]) : null;
764
+ return file && existsSync(file) ? file : null;
765
+ }
766
+ const owner = rel.match(/^([^/]+)\.ts$/);
767
+ if (owner) return this.moduleNamed(owner[1])?.natives ?? null;
768
+ return null;
769
+ }
770
+
771
+ /** The owner the build writes for a module without natives, at ~/<name>.ts. */
772
+ generated(path: string): string | null {
773
+ const owner = this.rel(path).match(/^([^/]+)\.ts$/);
774
+ const pkg = owner ? this.moduleNamed(owner[1]) : null;
775
+ if (!pkg || pkg.natives || this.real(path)) return null;
776
+ return [
777
+ `// GENERATED by scripts/project.ts: the plugin that runs ${pkg.name} - its one instance on the server.`,
778
+ 'import { plugin } from "~/facade";',
779
+ `import "~/modules/${pkg.short}";`,
780
+ '',
781
+ `plugin({ name: ${JSON.stringify(pkg.short)}, version: ${JSON.stringify(pkg.version)}, author: "", description: ${JSON.stringify(pkg.description)} });`,
782
+ '',
783
+ ].join('\n');
784
+ }
785
+
786
+ exists(path: string): boolean {
787
+ return this.real(path) !== null || this.generated(path) !== null;
788
+ }
789
+
790
+ /** What the build compiles as a module's owner: its natives file, or the generated one's place. */
791
+ ownerSource(pkg: ModulePackage): string {
792
+ return pkg.natives ?? join(this.root, `${pkg.short}.ts`);
793
+ }
794
+
795
+ /** Where a file on disk is in the tree: a package's plugin is ~/<name>.ts. */
796
+ place(real: string): string {
797
+ const path = resolve(real);
798
+ for (const pkg of this.project.modules) {
799
+ if (path === pkg.natives) return join(this.root, `${pkg.short}.ts`);
800
+ if (path === pkg.module) return join(this.root, 'modules', `${pkg.short}.ts`);
801
+ if (ofPackage(pkg, path)) return join(this.root, 'modules', pkg.short, relative(pkg.dir, path));
802
+ }
803
+ if (this.project.pluginsDir !== resolve(this.root) && inside(this.project.pluginsDir, path)) {
804
+ return join(this.root, relative(this.project.pluginsDir, path));
805
+ }
806
+ return path;
807
+ }
808
+
809
+ /** An entry for asc: the plugin's place in the tree, relative to it. */
810
+ entry(real: string): string {
811
+ return this.rel(this.place(real));
812
+ }
813
+
814
+ /** Each file's typed configs, made once a compile (scripts/typed-configs.ts). */
815
+ private typed = new Map<string, { text: string; out: string }>();
816
+
817
+ /**
818
+ * The file's text as asc reads it: auto-imports added, imports rewritten,
819
+ * a module's definition turned into its setup, its typed configs' code
820
+ * generated.
821
+ */
822
+ read(path: string): string | null {
823
+ const text = this.readPlain(path);
824
+ if (text === null) return null;
825
+ const at = resolve(path);
826
+ const known = this.typed.get(at);
827
+ if (known && known.text === text) return known.out;
828
+
829
+ const display = posix(relative(this.project.dir, this.real(at) ?? at));
830
+ const made = typedConfigs(at, display, text, (from, spec) => {
831
+ const file = this.resolveImport(from, spec);
832
+ const found = file ? this.readPlain(file) : null;
833
+ return file && found !== null ? { path: file, text: found } : null;
834
+ });
835
+ for (const problem of made.problems) {
836
+ if (!this.problems.includes(problem)) this.problems.push(problem);
837
+ }
838
+ this.typed.set(at, { text, out: made.text });
839
+ return made.text;
840
+ }
841
+
842
+ /** The file's text with its auto-imports added, its imports rewritten and a module's definition turned into its setup. */
843
+ private readPlain(path: string): string | null {
844
+ const real = this.real(path);
845
+ if (!real) return this.generated(path);
846
+ // A module's ModuleOptions augmentation may be in any of its files (types.ts).
847
+ const text = withoutOptionsAugmentation(this.rewrite(real, this.withImports(real, readFileSync(real, 'utf8'))));
848
+ const pkg = this.project.modules.find(each => each.module === real);
849
+ if (pkg) return moduleSource(real, text, optionsOf(this.project, pkg.definition));
850
+ if (/^modules\/[^/]+\.ts$/.test(this.rel(path)) && text.includes('defineModule')) {
851
+ const definition = readDefinition(real, text);
852
+ return moduleSource(real, text, definition ? optionsOf(this.project, definition) : {});
853
+ }
854
+ return text;
855
+ }
856
+
857
+ /** A specifier as the tree has it: a package by its place, a relative import inside a package likewise. */
858
+ private specifier(real: string, spec: string): string {
859
+ if (spec === '@amxts/core') return '~/facade';
860
+ if (spec.startsWith('@amxts/core/')) return `~/${spec.slice('@amxts/core/'.length)}`;
861
+
862
+ if (spec.startsWith('.')) {
863
+ const pkg = this.packageOf(real);
864
+ if (!pkg) return spec;
865
+ // Always by its `~/` place: asc tells files apart by the path they
866
+ // were reached by, so the plugin's `./module` and another plugin's
867
+ // "@amxts/menu-core" have to be one path, or the owner would carry
868
+ // two copies of the module.
869
+ const target = resolve(dirname(real), spec);
870
+ const file = [target, `${target}.ts`, join(target, 'index.ts')].find(each => existsSync(each) && statSync(each).isFile());
871
+ if (file === pkg.module) return `~/modules/${pkg.short}`;
872
+ if (file === pkg.natives) return `~/${pkg.short}`;
873
+ return `~/modules/${pkg.short}/${posix(relative(pkg.dir, target))}`;
874
+ }
875
+
876
+ if (spec.startsWith('~/')) return spec;
877
+ const name = spec.match(/^(@[^/]+\/[^/]+|[^@/][^/]*)/)?.[1];
878
+ const active = this.project.modules.find(pkg => pkg.name === name);
879
+ if (active) return spec === active.name ? `~/modules/${active.short}` : `~/modules/${active.short}/${spec.slice(name!.length + 1)}`;
880
+ if (this.project.packages.some(pkg => pkg.name === name)) {
881
+ const problem = `${posix(relative(this.project.dir, real))}: imports ${name}, which ${CONFIG_FILE} does not list - add it to modules`;
882
+ if (!this.problems.includes(problem)) this.problems.push(problem);
883
+ }
884
+ return spec;
885
+ }
886
+
887
+ rewrite(real: string, text: string): string {
888
+ if (!text.includes('@') && !(this.packageOf(real) && /["']\.\.?\//.test(text))) return text;
889
+ return text.replace(IMPORT, (whole, head: string, quote: string, spec: string) => {
890
+ const next = this.specifier(real, spec);
891
+ return next === spec ? whole : `${head}${quote}${next}${quote}`;
892
+ });
893
+ }
894
+
895
+ /** The tree place a specifier in `from` points at, before the .ts is tried. */
896
+ private target(from: string, spec: string): string | null {
897
+ if (spec.startsWith('~/')) return join(this.root, spec.slice(2));
898
+ if (spec.startsWith('.')) return join(dirname(from), spec);
899
+ return null;
900
+ }
901
+
902
+ /** The file in the tree an import in `from` leads to, or null. */
903
+ resolveImport(from: string, spec: string): string | null {
904
+ const next = this.target(resolve(from), spec);
905
+ if (!next) return null;
906
+ return [next.endsWith('.ts') ? next : `${next}.ts`, join(next, 'index.ts')].find(each => this.exists(each)) ?? null;
907
+ }
908
+
909
+ /** Every place in the tree a source reaches through its imports, itself included. */
910
+ reach(path: string, seen = new Set<string>()): Set<string> {
911
+ const at = resolve(path);
912
+ if (seen.has(at)) return seen;
913
+ const text = this.read(at);
914
+ if (text === null) return seen;
915
+ seen.add(at);
916
+ for (const [, , , spec] of text.matchAll(IMPORT)) {
917
+ const file = this.resolveImport(at, spec);
918
+ if (file) this.reach(file, seen);
919
+ }
920
+ return seen;
921
+ }
922
+
923
+ /** The include a plugin's natives must match: its package's, when the package says `"contract": true`. */
924
+ contractOf(entry: string): string | null {
925
+ const place = join(this.root, entry);
926
+ const real = this.real(place);
927
+ const pkg = real ? this.packageOf(real) : null;
928
+ return pkg && pkg.contract && pkg.natives === real ? pkg.include : null;
929
+ }
930
+
931
+ /** Where a plugin's `include: "x.inc"` may be: beside it, its package's include, the plugins folders, then the include folders (`includeDirs`). */
932
+ includeCandidates(entry: string, file: string): string[] {
933
+ const place = join(this.root, entry);
934
+ const real = this.real(place);
935
+ const pkg = real ? this.packageOf(real) : null;
936
+ return [
937
+ ...(real ? [join(dirname(real), file)] : []),
938
+ ...(pkg?.include && basename(pkg.include) === file ? [pkg.include] : []),
939
+ ...(pkg ? [join(pkg.dir, 'include', file), join(pkg.dir, file)] : []),
940
+ join(dirname(place), file),
941
+ join(this.project.pluginsDir, file),
942
+ join(this.root, file),
943
+ ...includeDirs(this.project.dir).map(dir => join(dir, file)),
944
+ ];
945
+ }
946
+ }
947
+
948
+ /** The tree for `root` in the current project (process.cwd(), or setProjectDir); one per compile. */
949
+ export function sourcesFor(root: string): Sources {
950
+ return new Sources(resolve(root), loadProject(projectDir));
951
+ }
952
+
953
+ /**
954
+ * The file asc asks for, as a path. asc reads any non-relative import as a
955
+ * library, so `~/facade` arrives as node_modules/~/facade.ts: `~/` is the
956
+ * plugins folder `root`, which spares a plugin in a subfolder from counting
957
+ * dots back to it. Anything else is where asc looked, from `baseDir`.
958
+ */
959
+ export function ascPath(root: string, filename: string, baseDir: string): string {
960
+ const alias = filename.replace(/\\/g, '/').match(/(?:^|\/)node_modules\/~\/(.*)$/);
961
+ return alias ? resolve(root, alias[1]) : resolve(baseDir && baseDir !== '.' ? baseDir : root, filename);
962
+ }
963
+
964
+ let projectDir = process.cwd();
965
+
966
+ /** The project builds and tests read from; process.cwd() until set. */
967
+ export function setProjectDir(dir: string) {
968
+ projectDir = resolve(dir);
969
+ }
970
+
971
+ export function currentProjectDir(): string {
972
+ return projectDir;
973
+ }