@amxts/core 0.1.0 → 0.2.1

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 (77) hide show
  1. package/README.md +21 -25
  2. package/README.ru.md +22 -24
  3. package/as/amxts.d.ts +63 -3
  4. package/as/constants.ts +684 -116
  5. package/as/entities.ts +1905 -1364
  6. package/as/events.ts +1696 -907
  7. package/as/facade.ts +1240 -220
  8. package/as/fetch.ts +1519 -0
  9. package/as/flags.ts +159 -145
  10. package/as/hlds.ts +1071 -0
  11. package/as/hooks.ts +5226 -5379
  12. package/as/kit.ts +15 -5
  13. package/as/lib/check.ts +1 -1
  14. package/as/natives.ts +2658 -2499
  15. package/as/remote.ts +39 -15
  16. package/as/tsconfig.json +7 -2
  17. package/as/vector.ts +12 -9
  18. package/includes/order.txt +22 -29
  19. package/includes/sources.json +0 -8
  20. package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
  21. package/package.json +29 -7
  22. package/runtime/deps/assemblyscript/dist/asc.js +86 -16
  23. package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
  24. package/runtime/licenses/MPL-2.0.txt +373 -0
  25. package/runtime/licenses/README.md +5 -1
  26. package/runtime/licenses/curl-COPYING +22 -0
  27. package/runtime/licenses/libssh2-COPYING +43 -0
  28. package/runtime/licenses/mbedTLS-LICENSE +553 -0
  29. package/runtime/natives.txt +108 -105
  30. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
  31. package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
  32. package/scripts/apply-docs.ts +2 -2
  33. package/scripts/auto-imports.ts +2 -2
  34. package/scripts/build-identity.ts +115 -0
  35. package/scripts/build-wasm.ts +101 -50
  36. package/scripts/check.ts +17 -3
  37. package/scripts/client-messages.ts +246 -0
  38. package/scripts/compile-cache.ts +4 -2
  39. package/scripts/compile-pool.ts +128 -0
  40. package/scripts/compile-worker.ts +23 -0
  41. package/scripts/compile.ts +156 -59
  42. package/scripts/docker-server.ts +13 -3
  43. package/scripts/docs/as/amxts.ts +86 -0
  44. package/scripts/docs/as/facade.ts +688 -188
  45. package/scripts/docs/as/fetch.ts +805 -0
  46. package/scripts/docs/entities.ts +43 -144
  47. package/scripts/docs/events.ts +17 -28
  48. package/scripts/docs/game.ts +68 -22
  49. package/scripts/docs/messages.ts +330 -16
  50. package/scripts/editor-api.ts +101 -0
  51. package/scripts/hlds-events.ts +370 -0
  52. package/scripts/includes.ts +12 -10
  53. package/scripts/player-fields.ts +16 -15
  54. package/scripts/prebuilt.ts +65 -23
  55. package/scripts/prepare.ts +42 -28
  56. package/scripts/project.ts +120 -20
  57. package/scripts/reapi-events.ts +98 -0
  58. package/scripts/shared-modules.ts +85 -23
  59. package/scripts/source-map.ts +242 -0
  60. package/scripts/system.ts +1 -1
  61. package/scripts/typed-commands.ts +263 -0
  62. package/scripts/typed-configs.ts +23 -12
  63. package/scripts/ui.ts +3 -1
  64. package/scripts/upgrade-menus.ts +274 -0
  65. package/scripts/upgrade-names.ts +384 -0
  66. package/scripts/upgrade.ts +857 -0
  67. package/src/cli-api.mjs +37 -4
  68. package/src/system.mjs +74 -3
  69. package/src/testing/compile.ts +8 -5
  70. package/src/testing/index.ts +2 -1
  71. package/src/testing/natives.ts +199 -5
  72. package/src/testing/network.ts +531 -0
  73. package/src/testing/server.ts +572 -48
  74. package/src/testing/setup.ts +1 -1
  75. package/src/testing/tables.ts +25 -0
  76. package/as/modules/http.ts +0 -184
  77. package/scripts/docs/as/modules/http.ts +0 -66
@@ -5,6 +5,13 @@
5
5
  // would compile the same thing; when it would not, the build says why and
6
6
  // compiles it.
7
7
  //
8
+ // The manifest also carries the module's surface - the proxy a plugin that
9
+ // uses the module reads in its place, and the owner's dispatcher
10
+ // (scripts/shared-modules.ts) - which every compile of a plugin that uses
11
+ // it would otherwise make by analysing the module with asc. It is taken on
12
+ // the same terms as the .aot, but for what only the machine code depends on:
13
+ // the system and the forwards' declarations.
14
+ //
8
15
  // bun scripts/prebuilt.ts in a project: compile every module it lists
9
16
  // into the module's prebuilt/, for every
10
17
  // system (scripts/publish.ts runs it on the
@@ -34,15 +41,18 @@
34
41
  // no change to the code.
35
42
  import type { NativesBeside, PluginNative } from './plugin-natives';
36
43
  import type { ModulePackage, Options, Sources } from './project';
44
+ import type { ModuleSurface } from './shared-modules';
37
45
  import type { System } from './system';
38
46
  import { createHash } from 'node:crypto';
39
47
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
40
48
  import { join, relative, resolve, sep } from 'node:path';
41
49
  import ts from 'typescript';
50
+ import { coreVersion } from './build-identity';
42
51
  import { compileToMachineCode, compileToWasm } from './compile';
43
52
  import { codeFiles } from './compile-cache';
44
53
  import { includeForward, nativesBeside } from './plugin-natives';
45
- import { CORE_DIR, CORE_PLUGINS, loadProject, optionsOf, sourcesFor } from './project';
54
+ import { CORE_DIR, CORE_PLUGINS, loadProject, optionsOf, shared, sourcesFor } from './project';
55
+ import { moduleSurface } from './shared-modules';
46
56
  import { SYSTEM_NAME, SYSTEMS, wamrcPath } from './system';
47
57
  import { c, log, since } from './ui';
48
58
 
@@ -76,6 +86,8 @@ export interface PrebuiltManifest {
76
86
  natives: PluginNative[];
77
87
  beside: NativesBeside;
78
88
  systems: Partial<Record<System, { file: string; size: number; sha256: string }>>;
89
+ /** What a plugin that uses the module compiles against (scripts/shared-modules.ts); missing in a manifest made without it. */
90
+ surface?: ModuleSurface;
79
91
  }
80
92
 
81
93
  /** What a build does with a module's prebuilt .aot: takes it, or compiles the module and says why. */
@@ -142,7 +154,7 @@ function coreHash(): string {
142
154
  /** The core or a module package the project has, by name, as a prebuilt module is checked against it. */
143
155
  function installed(name: string, sources: Sources): { version: string; dir: string; hash: () => string } | null {
144
156
  if (name === CORE_NAME) {
145
- return { version: String(JSON.parse(readFileSync(join(CORE_DIR, 'package.json'), 'utf8')).version), dir: CORE_DIR, hash: coreHash };
157
+ return { version: coreVersion(), dir: CORE_DIR, hash: coreHash };
146
158
  }
147
159
  const pkg = sources.project.packages.find(each => each.name === name);
148
160
  return pkg ? { version: pkg.version, dir: pkg.dir, hash: () => packageHash(pkg) } : null;
@@ -155,49 +167,77 @@ function declarationOf(name: string): string | null {
155
167
  }
156
168
 
157
169
  /**
158
- * The prebuilt .aot of a module from npm for `system`, when it is what the
159
- * project would compile; else why not. Null for a module that has none - a
160
- * folder on this machine, or a package without prebuilt/ - which is compiled
161
- * as any plugin is.
170
+ * A module package's manifest, or why it cannot be used. Null for a module
171
+ * that has none - a folder on this machine, or a package without prebuilt/ -
172
+ * which is compiled as any plugin is.
162
173
  */
163
- export function prebuiltOf(pkg: ModulePackage, system: System, sources: Sources): PrebuiltUse | null {
174
+ function manifestOf(pkg: ModulePackage): PrebuiltManifest | { why: string } | null {
164
175
  const file = join(pkg.dir, PREBUILT_DIR, MANIFEST);
165
176
  if (!fromRegistry(pkg.dir) || !existsSync(file)) return null;
166
177
  const named = `${pkg.short} ${pkg.version}`;
167
- let manifest: PrebuiltManifest;
168
178
  try {
169
- manifest = JSON.parse(readFileSync(file, 'utf8'));
179
+ const manifest = JSON.parse(readFileSync(file, 'utf8')) as PrebuiltManifest;
180
+ return manifest.format === FORMAT ? manifest : { why: `${named} comes compiled for another build of amxts` };
170
181
  } catch {
171
182
  return { why: `${named}: its ${PREBUILT_DIR}/${MANIFEST} cannot be read` };
172
183
  }
173
- if (manifest.format !== FORMAT) return { why: `${named} comes compiled for another build of amxts` };
184
+ }
185
+
186
+ /**
187
+ * The prebuilt .aot of a module from npm for `system`, when it is what the
188
+ * project would compile; else why not. Null for a module that has none.
189
+ */
190
+ export function prebuiltOf(pkg: ModulePackage, system: System, sources: Sources): PrebuiltUse | null {
191
+ const manifest = manifestOf(pkg);
192
+ if (!manifest || 'why' in manifest) return manifest;
193
+ const named = `${pkg.short} ${pkg.version}`;
174
194
  const aot = manifest.systems[system];
175
195
  if (!aot) return { why: `${named} comes compiled for ${Object.keys(manifest.systems).map(each => SYSTEM_NAME[each as System]).join(' and ')}, not ${SYSTEM_NAME[system]}` };
196
+ const why = mismatch(manifest, pkg, sources);
197
+ if (why) return { why };
198
+ for (const [name, declared] of Object.entries(manifest.forwards)) {
199
+ if (declarationOf(name) !== declared) return { why: `the includes declare the forward ${name} otherwise than when ${named} was built` };
200
+ }
201
+
202
+ const bytes = new Uint8Array(readFileSync(join(pkg.dir, aot.file)));
203
+ if (sha256(bytes) !== aot.sha256) return { why: `${named}: ${aot.file} is not the file its manifest lists` };
204
+ return { aot: bytes, natives: manifest.natives, beside: manifest.beside };
205
+ }
206
+
207
+ /**
208
+ * The surface a module from npm came with (scripts/shared-modules.ts), when
209
+ * it is what the project would make; else why not. Null for a module that
210
+ * has none.
211
+ */
212
+ export function prebuiltSurface(pkg: ModulePackage, sources: Sources): { surface: ModuleSurface } | { why: string } | null {
213
+ const manifest = manifestOf(pkg);
214
+ if (!manifest || 'why' in manifest) return manifest;
215
+ if (!manifest.surface) return { why: `${pkg.short} ${pkg.version} comes without its analysis` };
216
+ const why = mismatch(manifest, pkg, sources);
217
+ return why ? { why } : { surface: manifest.surface };
218
+ }
176
219
 
220
+ /** Why the project would not compile a module as its manifest says it was: another core or package, other options, a file in the way; null when it would. */
221
+ function mismatch(manifest: PrebuiltManifest, pkg: ModulePackage, sources: Sources): string | null {
222
+ const named = `${pkg.short} ${pkg.version}`;
177
223
  for (const [name, built] of Object.entries(manifest.from)) {
178
224
  const have = installed(name, sources);
179
- if (!have) return { why: `${named} was built with ${name}, which the project does not have` };
180
- if (have.version !== built.version) return { why: `${named} was built for ${name} ${built.version}, the project has ${have.version}` };
225
+ if (!have) return `${named} was built with ${name}, which the project does not have`;
226
+ if (have.version !== built.version) return `${named} was built for ${name} ${built.version}, the project has ${have.version}`;
181
227
  if (!fromRegistry(have.dir) && have.hash() !== built.hash) {
182
- return { why: `${named} was built from another ${name} ${built.version} than the one in ${posix(relative(sources.project.dir, have.dir)) || '.'}` };
228
+ return `${named} was built from another ${name} ${built.version} than the one in ${posix(relative(sources.project.dir, have.dir)) || '.'}`;
183
229
  }
184
230
  }
185
231
  for (const [name, options] of Object.entries(manifest.options)) {
186
232
  const module = sources.project.packages.find(each => each.name === name)!;
187
233
  if (JSON.stringify(optionsOf(sources.project, module.definition)) !== JSON.stringify(options)) {
188
- return { why: `${named} was built with ${name === pkg.name ? 'its' : `${module.short}'s`} default options, and amxts.config.ts sets ${module.definition.configKey}` };
234
+ return `${named} was built with ${name === pkg.name ? 'its' : `${module.short}'s`} default options, and amxts.config.ts sets ${module.definition.configKey}`;
189
235
  }
190
236
  }
191
- for (const [name, declared] of Object.entries(manifest.forwards)) {
192
- if (declarationOf(name) !== declared) return { why: `the includes declare the forward ${name} otherwise than when ${named} was built` };
193
- }
194
237
  const own = resolve(sources.project.pluginsDir) !== resolve(CORE_PLUGINS);
195
238
  const shadow = own ? manifest.places.find(place => existsSync(join(sources.project.pluginsDir, place))) : undefined;
196
- if (shadow) return { why: `${posix(relative(sources.project.dir, join(sources.project.pluginsDir, shadow)))} takes the place of a file ${named} was built from` };
197
-
198
- const bytes = new Uint8Array(readFileSync(join(pkg.dir, aot.file)));
199
- if (sha256(bytes) !== aot.sha256) return { why: `${named}: ${aot.file} is not the file its manifest lists` };
200
- return { aot: bytes, natives: manifest.natives, beside: manifest.beside };
239
+ if (shadow) return `${posix(relative(sources.project.dir, join(sources.project.pluginsDir, shadow)))} takes the place of a file ${named} was built from`;
240
+ return null;
201
241
  }
202
242
 
203
243
  /** The `new Forward<...>("name")` calls of a package whose arguments are not all text: their names. */
@@ -255,6 +295,7 @@ async function prebuild(pkg: ModulePackage, sources: Sources): Promise<string> {
255
295
  natives,
256
296
  beside: nativesBeside(natives),
257
297
  systems,
298
+ surface: await moduleSurface(CORE_PLUGINS, pkg.short),
258
299
  };
259
300
  writeFileSync(join(dir, MANIFEST), `${JSON.stringify(manifest, null, '\t')}\n`);
260
301
  const sizes = SYSTEMS.map(system => `${SYSTEM_NAME[system]} ${(systems[system]!.size / 1024).toFixed(0)} KB`).join(', ');
@@ -264,5 +305,6 @@ async function prebuild(pkg: ModulePackage, sources: Sources): Promise<string> {
264
305
  if (import.meta.main) {
265
306
  const project = loadProject(process.cwd());
266
307
  if (project.problems.length) throw new Error(project.problems.join('\n'));
267
- for (const pkg of project.modules) log.success(await prebuild(pkg, sourcesFor(CORE_PLUGINS)));
308
+ // A library has no owner to compile: it is compiled into the plugins that import it.
309
+ for (const pkg of shared(project.modules)) log.success(await prebuild(pkg, sourcesFor(CORE_PLUGINS)));
268
310
  }
@@ -5,19 +5,22 @@
5
5
  // the line it prints only under --debug)
6
6
  //
7
7
  // The project's own tsconfig.json extends it: { "extends": "./.amxts/tsconfig.json" }.
8
- // It says what the build says - `~/` is the project's plugins over the
9
- // core's as/, "@amxts/core" is the facade, a module package is its module
10
- // file - and it takes in amxts.config.ts and every module the config lists, so
11
- // that the options each module declares (ModuleOptions) type the config.
12
- // Beside it, imports.d.ts makes what plugins use without an import - the
13
- // facade's API, the modules' namespaces - globals for the editor
14
- // (scripts/auto-imports.ts).
8
+ // It says what the build says - `~/` is the project's plugins folder,
9
+ // "@amxts/core" and its entries are the core's API, a module package is its
10
+ // module file - and it takes in amxts.config.ts and every module the config
11
+ // lists, so that the options each module declares (ModuleOptions) type the
12
+ // config. Beside it, imports.d.ts makes what plugins use without an import -
13
+ // the facade's API, the modules' namespaces - globals for the editor
14
+ // (scripts/auto-imports.ts), and api/ holds the API with its tooltips in the
15
+ // language AMXTS_DOCS_LANG picks, when that is not English
16
+ // (scripts/editor-api.ts): the installed packages are never written to.
15
17
  import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
16
18
  import { createRequire } from 'node:module';
17
19
  import { dirname, isAbsolute, join, relative } from 'node:path';
18
- import { applyProject, docsLang } from './apply-docs';
20
+ import { applySources, docsLang, moduleSources, registerFilter } from './apply-docs';
19
21
  import { importsDeclaration } from './auto-imports';
20
- import { CONFIG_FILE, CORE_DIR, CORE_PLUGINS, loadProject } from './project';
22
+ import { editorApi } from './editor-api';
23
+ import { CONFIG_FILE, CORE_DIR, CORE_ENTRIES, CORE_PLUGINS, loadProject, shared } from './project';
21
24
  import { c, debug, log } from './ui';
22
25
 
23
26
  function fail(message: string): void {
@@ -32,19 +35,18 @@ if (project.problems.length) {
32
35
  process.exit(1);
33
36
  }
34
37
 
35
- // The hood's tooltips - the core's and the modules' - in the language .env
36
- // picks (AMXTS_DOCS_LANG), as the generated API has them. A core it cannot
37
- // write to keeps English: the editor still has words, only not these.
38
- let docs = '';
39
- try {
40
- docs = await applyProject(project.dir, docsLang());
41
- } catch (error) {
42
- fail(`docs: ${(error as Error).message}`);
43
- }
38
+ const lang = docsLang();
44
39
 
45
40
  // A module's own folder, without a project config, has its own tsconfig.json
46
- // (extending the core's as/tsconfig.json): nothing to write there.
47
- if (!project.config && project.packages.some(pkg => pkg.dir === project.dir)) process.exit(0);
41
+ // (extending the core's as/tsconfig.json): nothing to write there but its
42
+ // own words, in place - its repository's filter stores them in English.
43
+ const own = project.config ? null : project.packages.find(pkg => pkg.dir === project.dir);
44
+ if (own) {
45
+ registerFilter(own.dir);
46
+ const changed = await applySources(moduleSources(own), lang);
47
+ if (!process.argv.includes('--quiet') || debug()) log.step(c.dim(`docs: ${lang}, ${changed.length ? `${changed.length} file(s) rewritten` : 'up to date'}`));
48
+ process.exit(0);
49
+ }
48
50
 
49
51
  const out = join(project.dir, '.amxts');
50
52
  /** A path as the generated config writes it: from .amxts/, with forward slashes. */
@@ -74,19 +76,28 @@ function installed(name: string, dir: string) {
74
76
  }
75
77
  }
76
78
 
77
- // `~/` is not a package. The core and the modules are, once installed; one
78
- // that is not (a module in modules/, a core run from elsewhere) is mapped to
79
- // its file, as the build finds it.
79
+ // `~/` is the project's plugins folder, not a package. The core and the
80
+ // modules are, once installed; one that is not (a module in modules/, a core
81
+ // run from elsewhere) is mapped to its file, as the build finds it. In a
82
+ // language other than English they are all mapped to the copy with its
83
+ // words, which the build never reads.
80
84
  const paths: Record<string, string[]> = {
81
- '~/*': [...new Set([`${from(project.pluginsDir)}/*`, `${from(CORE_PLUGINS)}/*`])],
85
+ '~/*': [`${from(project.pluginsDir)}/*`],
82
86
  };
83
87
  if (!installed('@amxts/core', CORE_DIR)) {
84
- paths['@amxts/core'] = [from(join(CORE_PLUGINS, 'facade.ts'))];
85
- paths['@amxts/core/*'] = [`${from(CORE_PLUGINS)}/*`];
88
+ for (const [name, file] of Object.entries(CORE_ENTRIES)) paths[name] = [from(join(CORE_PLUGINS, file))];
86
89
  }
87
90
  for (const pkg of project.modules) {
88
91
  if (!installed(pkg.name, pkg.dir)) paths[pkg.name] = [from(pkg.module)];
89
92
  }
93
+ let api: Awaited<ReturnType<typeof editorApi>> = null;
94
+ try {
95
+ api = await editorApi(project, lang, join(out, 'api'));
96
+ } catch (error) {
97
+ fail(`docs: ${(error as Error).message}`);
98
+ }
99
+ for (const [name, file] of Object.entries(api?.paths ?? {})) paths[name] = [from(file)];
100
+ const docs = lang === 'en' ? '' : `docs: ${lang}${api?.written ? ', .amxts/api written' : ''}`;
90
101
 
91
102
  const tsconfig = {
92
103
  extends: from(assembly),
@@ -104,12 +115,15 @@ const tsconfig = {
104
115
  // plugin declares is its own, never a global another plugin's name
105
116
  // collides with.
106
117
  moduleDetection: 'force',
118
+ // `catch (error)` reads `error.message` as the compiler takes it: the
119
+ // base config's strict mode would make the caught value `unknown`.
120
+ useUnknownInCatchVariables: false,
107
121
  paths,
108
122
  },
109
123
  include: [
110
124
  `${from(project.pluginsDir)}/**/*.ts`,
111
125
  from(join(project.dir, CONFIG_FILE)),
112
- `${from(CORE_PLUGINS)}/*.d.ts`,
126
+ `${from(api?.core ?? CORE_PLUGINS)}/*.d.ts`,
113
127
  './modules.d.ts',
114
128
  './imports.d.ts',
115
129
  ],
@@ -120,7 +134,7 @@ writeFileSync(join(out, 'tsconfig.json'), `${JSON.stringify(tsconfig, null, '\t'
120
134
  writeFileSync(join(out, 'modules.d.ts'), [
121
135
  '// GENERATED by amxts prepare: the modules amxts.config.ts lists, for the',
122
136
  '// options each one adds to ModuleOptions.',
123
- ...project.modules.map(pkg => `import "${pkg.name}";`),
137
+ ...shared(project.modules).map(pkg => `import "${pkg.name}";`),
124
138
  '',
125
139
  'export {};',
126
140
  '',
@@ -11,6 +11,10 @@
11
11
  //
12
12
  // "amxts": { "module": "src/index.ts", "natives": "src/natives.ts", "include": "include/menu_core.inc" }
13
13
  //
14
+ // - or, for a library, `"amxts": { "module": "src/index.ts", "library": true }`:
15
+ // code compiled into each plugin that imports it, as an npm library is, with
16
+ // no instance on the server, no owner, no proxy and no defineModule.
17
+ //
14
18
  // and in its module file, with the global defineModule:
15
19
  //
16
20
  // export default defineModule<MenuCoreOptions>({ // src/index.ts
@@ -52,6 +56,7 @@ import ts from 'typescript';
52
56
  import { coreImports, importedName, importTable, withAutoImports } from './auto-imports';
53
57
  import { includeDirs } from './includes';
54
58
  import { existsSync, readdirSync, readFileSync, statSync } from './tracked-fs';
59
+ import { typedCommands } from './typed-commands';
55
60
  import { typedConfigs } from './typed-configs';
56
61
 
57
62
  /** The core's folder: the package this file ships in. */
@@ -123,6 +128,12 @@ export interface ModulePackage {
123
128
  dir: string;
124
129
  /** The module file: the API plugins import. */
125
130
  module: string;
131
+ /**
132
+ * `"library": true`: compiled into each plugin that imports it, like an
133
+ * npm library - no instance on the server, so no owner, no proxy, no
134
+ * defineModule, natives, include or test kit.
135
+ */
136
+ library: boolean;
126
137
  /**
127
138
  * Its Pawn natives: the plugin that owns the module - its one instance on
128
139
  * the server. Without one, the build generates an owner that only runs it.
@@ -375,7 +386,16 @@ export function readPackage(dir: string): ModulePackage | null {
375
386
  const include = typeof amxts.include === 'string' ? resolve(dir, amxts.include) : null;
376
387
 
377
388
  const short = String(json.name).replace(/^@[^/]+\//, '');
378
- const definition = readDefinition(module) ?? { name: null, configKey: null, requires: [], defaults: {}, imports: [], optionsType: null, hasSetup: false };
389
+ const library = amxts.library === true;
390
+ const owned = ['natives', 'include', 'contract', 'testing'].filter(key => amxts[key] !== undefined);
391
+ if (library && owned.length) {
392
+ throw new Error(`${json.name}: a library is compiled into each plugin that imports it and runs no plugin of its own - it has no ${owned.map(key => `"${key}"`).join(', ')} (package.json, "amxts")`);
393
+ }
394
+ const read = readDefinition(module);
395
+ if (library && read) {
396
+ throw new Error(`${json.name}: a library has no defineModule - no options, no instance on the server; plugins import what it exports. Leave "library": true out to make it a module`);
397
+ }
398
+ const definition = read ?? { name: null, configKey: null, requires: [], defaults: {}, imports: [], optionsType: null, hasSetup: false };
379
399
  if (definition.name && definition.name !== short) {
380
400
  throw new Error(`${json.name}: defineModule's meta.name is "${definition.name}" - it is the package's name without the scope, "${short}"`);
381
401
  }
@@ -387,7 +407,7 @@ export function readPackage(dir: string): ModulePackage | null {
387
407
  if (include && amxts.contract === true && !existsSync(include)) throw new Error(`${json.name}: its contract ${amxts.include} is not there (package.json, "amxts".include)`);
388
408
  const testing = typeof amxts.testing === 'string' ? resolve(dir, amxts.testing) : null;
389
409
  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 };
410
+ return { name: json.name, short, dir: resolve(dir), module, library, natives, include, contract: amxts.contract === true, testing, version: String(json.version ?? '0.0.0'), description: String(json.description ?? ''), definition };
391
411
  }
392
412
 
393
413
  /**
@@ -614,7 +634,30 @@ export function optionsOf(project: Project, definition: ModuleDefinition): Optio
614
634
  * Everything in the core's as/ that is not a plugin: the facade a plugin
615
635
  * imports, the kit a module imports and the generated native layer under them.
616
636
  */
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']);
637
+ export const NOT_PLUGINS = new Set(['facade.ts', 'kit.ts', 'promise.ts', 'vector.ts', 'effects.ts', 'fetch.ts', 'fs.ts', 'os.ts', 'natives.ts', 'remote.ts', 'constants.ts', 'events.ts', 'entities.ts', 'flags.ts', 'hooks.ts', 'hlds.ts']);
638
+
639
+ /**
640
+ * The core's API a plugin imports, by the package's name, and its file in
641
+ * the core's as/ - its place in the tree. package.json's "exports" gives
642
+ * the same files to the editor.
643
+ */
644
+ export const CORE_ENTRIES: Record<string, string> = {
645
+ '@amxts/core': 'facade.ts',
646
+ '@amxts/core/natives': 'natives.ts',
647
+ '@amxts/core/constants': 'constants.ts',
648
+ '@amxts/core/fs': 'fs.ts',
649
+ '@amxts/core/os': 'os.ts',
650
+ '@amxts/core/kit': 'kit.ts',
651
+ '@amxts/core/check': 'lib/check.ts',
652
+ };
653
+
654
+ /** Every place of the core's as/ in the tree: what `~/` must not reach from a plugin. */
655
+ const CORE_PLACES = new Set([...NOT_PLUGINS, ...Object.values(CORE_ENTRIES), 'amxts.d.ts']);
656
+
657
+ /** How a plugin names the core's file at a place: its entry, or the facade, which exports the rest. */
658
+ export function coreEntryOf(place: string): string {
659
+ return Object.entries(CORE_ENTRIES).find(([, file]) => file === place)?.[0] ?? '@amxts/core';
660
+ }
618
661
 
619
662
  /**
620
663
  * The project's own plugins: the .ts files at the top of its plugins folder.
@@ -629,8 +672,13 @@ export function projectPlugins(project: Project): string[] {
629
672
  .map(f => join(project.pluginsDir, f));
630
673
  }
631
674
 
675
+ /** The modules that run on the server - each in its owner plugin: every one but a library. */
676
+ export function shared(modules: ModulePackage[]): ModulePackage[] {
677
+ return modules.filter(pkg => !pkg.library);
678
+ }
679
+
632
680
  /** 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[] {
681
+ export function pluginList(project: Project, plugins: string[], modules = shared(project.modules)): string[] {
634
682
  const owners = modules.map(pkg => `${pkg.short}.aot`);
635
683
  return [...owners, ...plugins.filter(plugin => !owners.includes(plugin))];
636
684
  }
@@ -641,11 +689,12 @@ export function pluginList(project: Project, plugins: string[], modules = projec
641
689
  * the files they import - with what those modules import and require, and
642
690
  * the ones `pawn` keeps for Pawn plugins, whose use no build can see.
643
691
  * Without amxts.config.ts every module is in use: a module's own
644
- * repository, testing itself.
692
+ * repository, testing itself. A library is never one: it is compiled into
693
+ * the plugins, and what it imports is reached through them.
645
694
  */
646
695
  export function modulesInUse(sources: Sources, plugins: string[]): ModulePackage[] {
647
696
  const { project } = sources;
648
- if (!project.config) return project.modules;
697
+ if (!project.config) return shared(project.modules);
649
698
  const used = new Set(project.config.pawn ?? []);
650
699
  const reached = new Set<string>();
651
700
  const reach = (source: string) => sources.reach(join(sources.root, sources.entry(source)), reached);
@@ -653,7 +702,7 @@ export function modulesInUse(sources: Sources, plugins: string[]): ModulePackage
653
702
  // A module in use brings what its owner imports and what it requires.
654
703
  for (let before = -1; used.size !== before;) {
655
704
  before = used.size;
656
- for (const pkg of project.modules) {
705
+ for (const pkg of shared(project.modules)) {
657
706
  const imported = reached.has(join(sources.root, 'modules', `${pkg.short}.ts`));
658
707
  if (!imported && !used.has(pkg.name)) continue;
659
708
  reach(sources.ownerSource(pkg));
@@ -661,7 +710,7 @@ export function modulesInUse(sources: Sources, plugins: string[]): ModulePackage
661
710
  for (const required of pkg.definition.requires) used.add(required);
662
711
  }
663
712
  }
664
- return project.modules.filter(pkg => used.has(pkg.name));
713
+ return shared(project.modules).filter(pkg => used.has(pkg.name));
665
714
  }
666
715
 
667
716
  // ---------------------------------------------------------------- the tree
@@ -772,7 +821,7 @@ export class Sources {
772
821
  generated(path: string): string | null {
773
822
  const owner = this.rel(path).match(/^([^/]+)\.ts$/);
774
823
  const pkg = owner ? this.moduleNamed(owner[1]) : null;
775
- if (!pkg || pkg.natives || this.real(path)) return null;
824
+ if (!pkg || pkg.natives || pkg.library || this.real(path)) return null;
776
825
  return [
777
826
  `// GENERATED by scripts/project.ts: the plugin that runs ${pkg.name} - its one instance on the server.`,
778
827
  'import { plugin } from "~/facade";',
@@ -811,7 +860,7 @@ export class Sources {
811
860
  return this.rel(this.place(real));
812
861
  }
813
862
 
814
- /** Each file's typed configs, made once a compile (scripts/typed-configs.ts). */
863
+ /** Each file's typed configs and commands, made once a compile (scripts/typed-configs.ts, scripts/typed-commands.ts). */
815
864
  private typed = new Map<string, { text: string; out: string }>();
816
865
 
817
866
  /**
@@ -827,14 +876,14 @@ export class Sources {
827
876
  if (known && known.text === text) return known.out;
828
877
 
829
878
  const display = posix(relative(this.project.dir, this.real(at) ?? at));
830
- const made = typedConfigs(at, display, text, (from, spec) => {
879
+ const reader = (from: string, spec: string) => {
831
880
  const file = this.resolveImport(from, spec);
832
881
  const found = file ? this.readPlain(file) : null;
833
882
  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
- }
883
+ };
884
+ const configs = typedConfigs(at, display, text, reader);
885
+ const made = typedCommands(at, display, configs.text, reader);
886
+ for (const problem of [...configs.problems, ...made.problems]) this.problem(problem);
838
887
  this.typed.set(at, { text, out: made.text });
839
888
  return made.text;
840
889
  }
@@ -843,8 +892,21 @@ export class Sources {
843
892
  private readPlain(path: string): string | null {
844
893
  const real = this.real(path);
845
894
  if (!real) return this.generated(path);
895
+ const raw = readFileSync(real, 'utf8');
896
+ const at = resolve(path);
897
+ const known = this.plain.get(at);
898
+ if (known?.raw === raw) return known.out;
899
+ const out = this.plainOf(path, real, raw);
900
+ this.plain.set(at, { raw, out });
901
+ return out;
902
+ }
903
+
904
+ /** Each file's plain text, made once a compile: a build reaches the facade from every plugin. */
905
+ private plain = new Map<string, { raw: string; out: string }>();
906
+
907
+ private plainOf(path: string, real: string, raw: string): string {
846
908
  // 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'))));
909
+ const text = withoutOptionsAugmentation(this.rewrite(real, this.withImports(real, raw)));
848
910
  const pkg = this.project.modules.find(each => each.module === real);
849
911
  if (pkg) return moduleSource(real, text, optionsOf(this.project, pkg.definition));
850
912
  if (/^modules\/[^/]+\.ts$/.test(this.rel(path)) && text.includes('defineModule')) {
@@ -854,10 +916,49 @@ export class Sources {
854
916
  return text;
855
917
  }
856
918
 
919
+ private problem(text: string) {
920
+ if (!this.problems.includes(text)) this.problems.push(text);
921
+ }
922
+
923
+ /**
924
+ * Whether a file on disk is the core's as/: the hood, which names its
925
+ * places by `~/`. On a server the plugins folder holds them, by place.
926
+ */
927
+ private isCore(real: string): boolean {
928
+ if (inside(CORE_PLUGINS, real)) return true;
929
+ const own = resolve(this.project.pluginsDir) !== resolve(CORE_PLUGINS) && inside(this.project.pluginsDir, real);
930
+ return !own && !this.packageOf(real) && CORE_PLACES.has(this.rel(this.place(real)));
931
+ }
932
+
933
+ /**
934
+ * `~/` in a plugin or a module is the project's own files: the core's API
935
+ * goes by `@amxts/core/<entry>` and a module package by its name. What
936
+ * to write instead, or null for a file of the project's own.
937
+ */
938
+ private notOwn(real: string, spec: string): string | null {
939
+ const file = this.resolveImport(this.place(real), spec);
940
+ const target = file ? this.real(file) : null;
941
+ if (!target) return file && this.generated(file) ? '' : null;
942
+ const pkg = this.packageOf(target);
943
+ if (pkg) return pkg.module === target ? pkg.name : `${pkg.name}/${posix(relative(pkg.dir, target)).replace(/\.ts$/, '')}`;
944
+ return this.isCore(target) ? coreEntryOf(this.rel(file!)) : null;
945
+ }
946
+
857
947
  /** A specifier as the tree has it: a package by its place, a relative import inside a package likewise. */
858
948
  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)}`;
949
+ const display = () => posix(relative(this.project.dir, real));
950
+ if (spec === '@amxts/core' || spec.startsWith('@amxts/core/')) {
951
+ const place = CORE_ENTRIES[spec];
952
+ if (place) return `~/${place.replace(/\.ts$/, '')}`;
953
+ this.problem(`${display()}: imports ${spec}, which @amxts/core does not export - its API is ${Object.keys(CORE_ENTRIES).join(', ')}`);
954
+ return spec;
955
+ }
956
+
957
+ if (spec.startsWith('~/')) {
958
+ const instead = this.isCore(real) ? null : this.notOwn(real, spec);
959
+ if (instead !== null) this.problem(`${display()}: ${spec} is not a file of the project - \`~/\` is the plugins folder; import ${instead || 'the module by its package name'} (npx amxts upgrade rewrites these imports)`);
960
+ return spec;
961
+ }
861
962
 
862
963
  if (spec.startsWith('.')) {
863
964
  const pkg = this.packageOf(real);
@@ -873,7 +974,6 @@ export class Sources {
873
974
  return `~/modules/${pkg.short}/${posix(relative(pkg.dir, target))}`;
874
975
  }
875
976
 
876
- if (spec.startsWith('~/')) return spec;
877
977
  const name = spec.match(/^(@[^/]+\/[^/]+|[^@/][^/]*)/)?.[1];
878
978
  const active = this.project.modules.find(pkg => pkg.name === name);
879
979
  if (active) return spec === active.name ? `~/modules/${active.short}` : `~/modules/${active.short}/${spec.slice(name!.length + 1)}`;
@@ -885,7 +985,7 @@ export class Sources {
885
985
  }
886
986
 
887
987
  rewrite(real: string, text: string): string {
888
- if (!text.includes('@') && !(this.packageOf(real) && /["']\.\.?\//.test(text))) return text;
988
+ if (!text.includes('@') && !text.includes('~/') && !(this.packageOf(real) && /["']\.\.?\//.test(text))) return text;
889
989
  return text.replace(IMPORT, (whole, head: string, quote: string, spec: string) => {
890
990
  const next = this.specifier(real, spec);
891
991
  return next === spec ? whole : `${head}${quote}${next}${quote}`;
@@ -0,0 +1,98 @@
1
+ // The game events reapi alone delivers - ReGameDLL's and ReHLDS's own
2
+ // functions - and the build's refusal of a listener for one nothing else
3
+ // hears, in a project for plain HLDS.
4
+ //
5
+ // A server without reapi hears every other game event through Ham Sandwich
6
+ // (as/hooks.ts picks it once, by hasModule), and many of these through
7
+ // another stock hook (scripts/hlds-events.ts); a listener for one of the rest
8
+ // is only a line in its console. A project whose amxts.config.ts says
9
+ // `target: "hlds"` has said there is no reapi, so the build stops at such a
10
+ // listener instead, with the file and the line.
11
+ import { join } from 'node:path';
12
+ // @ts-ignore - shipped as JavaScript, with types beside it we do not need here
13
+ import * as asc from '../runtime/deps/assemblyscript/dist/assemblyscript.js';
14
+ import { HEARD } from './hlds-events';
15
+ import { CORE_PLUGINS } from './project';
16
+ import { readFileSync } from './tracked-fs';
17
+
18
+ let cached: { text: string; events: Set<string> } | null = null;
19
+
20
+ /**
21
+ * The events of as/hooks.ts's GameEventMap reapi alone delivers: an event
22
+ * class with no Ham Sandwich function behind it (`private static readonly
23
+ * ham`) is a hookchain's alone.
24
+ */
25
+ export function reapiEvents(hooksText = readFileSync(join(CORE_PLUGINS, 'hooks.ts'), 'utf8')): Set<string> {
26
+ if (cached?.text === hooksText) return cached.events;
27
+
28
+ const reapiClasses = new Set<string>();
29
+ for (const [, name, body] of hooksText.matchAll(/^export class (\w+) extends HookEvent \{\n([\s\S]*?)\n\}\n/gm)) {
30
+ if (!body.includes('private static readonly ham:')) reapiClasses.add(name);
31
+ }
32
+ const map = hooksText.slice(hooksText.indexOf('export interface GameEventMap'));
33
+ const events = new Set<string>();
34
+ for (const [, event, name] of map.slice(0, map.indexOf('\n}\n')).matchAll(/^\t(\w+): (\w+);/gm)) {
35
+ if (reapiClasses.has(name)) events.add(event);
36
+ }
37
+
38
+ cached = { text: hooksText, events };
39
+ return events;
40
+ }
41
+
42
+ /** The events reapi alone delivers that plain HLDS does not hear at all. */
43
+ export function unheardEvents(hooksText?: string): Set<string> {
44
+ return new Set([...reapiEvents(hooksText)].filter(event => !Object.hasOwn(HEARD, event)));
45
+ }
46
+
47
+ /** The event a `game.addEventListener("name", ...)` call listens for; undefined for any other call. */
48
+ function gameEventOf(call: any): string | undefined {
49
+ const callee = call.expression;
50
+ if (callee.kind !== asc.NodeKind.PropertyAccess || callee.property.text !== 'addEventListener') return undefined;
51
+ if (callee.expression.kind !== asc.NodeKind.Identifier || callee.expression.text !== 'game') return undefined;
52
+ const type = call.args[0];
53
+ return type?.kind === asc.NodeKind.Literal && type.literalKind === asc.LiteralKind.String ? type.value : undefined;
54
+ }
55
+
56
+ /** Calls `visit` on every node under `node`. */
57
+ function walk(node: any, visit: (node: any) => void) {
58
+ if (Array.isArray(node)) {
59
+ for (const each of node) walk(each, visit);
60
+ return;
61
+ }
62
+ if (!node || typeof node !== 'object' || typeof node.kind !== 'number') return;
63
+ visit(node);
64
+ for (const key in node) {
65
+ if (key !== 'range') walk(node[key], visit);
66
+ }
67
+ }
68
+
69
+ /** The listeners in a compile's sources for an event plain HLDS does not hear, each where it is written. */
70
+ export function reapiListeners(sources: any[], events = unheardEvents()): string[] {
71
+ const found: string[] = [];
72
+ for (const source of sources) {
73
+ if (source.normalizedPath.startsWith('~lib/') || !source.text.includes('addEventListener')) continue;
74
+ walk(source.statements, (node) => {
75
+ if (node.kind !== asc.NodeKind.Call) return;
76
+ const event = gameEventOf(node);
77
+ if (event === undefined || !events.has(event)) return;
78
+ const line = source.text.slice(0, node.range.start).split('\n').length;
79
+ found.push(`${source.normalizedPath}:${line}: "${event}" needs ReAPI, and amxts.config.ts's target is "hlds" - nothing on plain HLDS hears it. Listen for another event, or set target: "rehlds"`);
80
+ });
81
+ }
82
+ return found;
83
+ }
84
+
85
+ /** The transform that stops a plugin for plain HLDS at a listener nothing there could call. */
86
+ function hldsTransform() {
87
+ return class {
88
+ afterParse(parser: any): void {
89
+ const problems = reapiListeners(parser.sources);
90
+ if (problems.length) throw new Error(problems.join('\n'));
91
+ }
92
+ };
93
+ }
94
+
95
+ /** What a project's target adds to a compile: for plain HLDS, the refusal of the events it does not hear. */
96
+ export function targetTransforms(target: string | undefined) {
97
+ return target === 'hlds' ? [hldsTransform()] : [];
98
+ }