@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.
- package/README.md +21 -25
- package/README.ru.md +22 -24
- package/as/amxts.d.ts +63 -3
- package/as/constants.ts +684 -116
- package/as/entities.ts +1905 -1364
- package/as/events.ts +1696 -907
- package/as/facade.ts +1240 -220
- package/as/fetch.ts +1519 -0
- package/as/flags.ts +159 -145
- package/as/hlds.ts +1071 -0
- package/as/hooks.ts +5226 -5379
- package/as/kit.ts +15 -5
- package/as/lib/check.ts +1 -1
- package/as/natives.ts +2658 -2499
- package/as/remote.ts +39 -15
- package/as/tsconfig.json +7 -2
- package/as/vector.ts +12 -9
- package/includes/order.txt +22 -29
- package/includes/sources.json +0 -8
- package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
- package/package.json +29 -7
- package/runtime/deps/assemblyscript/dist/asc.js +86 -16
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
- package/runtime/licenses/MPL-2.0.txt +373 -0
- package/runtime/licenses/README.md +5 -1
- package/runtime/licenses/curl-COPYING +22 -0
- package/runtime/licenses/libssh2-COPYING +43 -0
- package/runtime/licenses/mbedTLS-LICENSE +553 -0
- package/runtime/natives.txt +108 -105
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
- package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
- package/scripts/apply-docs.ts +2 -2
- package/scripts/auto-imports.ts +2 -2
- package/scripts/build-identity.ts +115 -0
- package/scripts/build-wasm.ts +101 -50
- package/scripts/check.ts +17 -3
- package/scripts/client-messages.ts +246 -0
- package/scripts/compile-cache.ts +4 -2
- package/scripts/compile-pool.ts +128 -0
- package/scripts/compile-worker.ts +23 -0
- package/scripts/compile.ts +156 -59
- package/scripts/docker-server.ts +13 -3
- package/scripts/docs/as/amxts.ts +86 -0
- package/scripts/docs/as/facade.ts +688 -188
- package/scripts/docs/as/fetch.ts +805 -0
- package/scripts/docs/entities.ts +43 -144
- package/scripts/docs/events.ts +17 -28
- package/scripts/docs/game.ts +68 -22
- package/scripts/docs/messages.ts +330 -16
- package/scripts/editor-api.ts +101 -0
- package/scripts/hlds-events.ts +370 -0
- package/scripts/includes.ts +12 -10
- package/scripts/player-fields.ts +16 -15
- package/scripts/prebuilt.ts +65 -23
- package/scripts/prepare.ts +42 -28
- package/scripts/project.ts +120 -20
- package/scripts/reapi-events.ts +98 -0
- package/scripts/shared-modules.ts +85 -23
- package/scripts/source-map.ts +242 -0
- package/scripts/system.ts +1 -1
- package/scripts/typed-commands.ts +263 -0
- package/scripts/typed-configs.ts +23 -12
- package/scripts/ui.ts +3 -1
- package/scripts/upgrade-menus.ts +274 -0
- package/scripts/upgrade-names.ts +384 -0
- package/scripts/upgrade.ts +857 -0
- package/src/cli-api.mjs +37 -4
- package/src/system.mjs +74 -3
- package/src/testing/compile.ts +8 -5
- package/src/testing/index.ts +2 -1
- package/src/testing/natives.ts +199 -5
- package/src/testing/network.ts +531 -0
- package/src/testing/server.ts +572 -48
- package/src/testing/setup.ts +1 -1
- package/src/testing/tables.ts +25 -0
- package/as/modules/http.ts +0 -184
- package/scripts/docs/as/modules/http.ts +0 -66
package/scripts/typed-configs.ts
CHANGED
|
@@ -67,7 +67,7 @@ type ObjectShape = Extract<Shape, { kind: 'object' }>;
|
|
|
67
67
|
export type ImportReader = (from: string, spec: string) => { path: string; text: string } | null;
|
|
68
68
|
|
|
69
69
|
/** A shape that cannot be read, at a node of the file it is in. */
|
|
70
|
-
class ShapeError extends Error {
|
|
70
|
+
export class ShapeError extends Error {
|
|
71
71
|
constructor(readonly node: ts.Node, readonly file: ts.SourceFile, message: string) {
|
|
72
72
|
super(message);
|
|
73
73
|
}
|
|
@@ -93,7 +93,7 @@ interface Context {
|
|
|
93
93
|
file: ts.SourceFile;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
class Shapes {
|
|
96
|
+
export class Shapes {
|
|
97
97
|
private files = new Map<string, ts.SourceFile>();
|
|
98
98
|
|
|
99
99
|
constructor(private imports: ImportReader) {}
|
|
@@ -190,7 +190,7 @@ class Shapes {
|
|
|
190
190
|
return this.members(declaration.members, found.at, inside);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
declaration(name: ts.EntityName, at: Context): { node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration; at: Context } | null {
|
|
194
194
|
if (ts.isQualifiedName(name)) {
|
|
195
195
|
if (!ts.isIdentifier(name.left)) return null;
|
|
196
196
|
const namespace = this.importOf(name.left.text, at);
|
|
@@ -738,15 +738,26 @@ export function typedConfigs(path: string, display: string, text: string, import
|
|
|
738
738
|
let out = text;
|
|
739
739
|
for (const edit of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.with + out.slice(edit.end);
|
|
740
740
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
741
|
+
const code = ['// GENERATED by scripts/typed-configs.ts: the typed configs this file loads.', ...functions].join('\n');
|
|
742
|
+
return { text: withGenerated(path, out, code, `import * as ${NS} from "${HELPERS}";`), problems };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* A file's text with code the build wrote for it: after its last line of its
|
|
747
|
+
* own, so every line above keeps its number, but before the imports that
|
|
748
|
+
* follow it - the auto-imports' line - with the code's own import after
|
|
749
|
+
* them. The build moves a file's trailing imports to its top
|
|
750
|
+
* (HoistImports), and the code would stop them being the last.
|
|
751
|
+
*/
|
|
752
|
+
export function withGenerated(path: string, text: string, code: string, imports: string): string {
|
|
753
|
+
const file = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
754
|
+
const statements = file.statements;
|
|
755
|
+
let first = statements.length;
|
|
756
|
+
while (first > 0 && ts.isImportDeclaration(statements[first - 1])) first--;
|
|
757
|
+
const at = first < statements.length ? statements[first].getStart(file) : text.length;
|
|
758
|
+
const head = text.slice(0, at);
|
|
759
|
+
const tail = text.slice(at);
|
|
760
|
+
return `${head}${head.endsWith('\n') ? '' : '\n'}${code}\n${tail}${tail.length === 0 || tail.endsWith('\n') ? '' : '\n'}${imports}\n`;
|
|
750
761
|
}
|
|
751
762
|
|
|
752
763
|
/** A load's shape and the type its function takes: the type argument, the defaults' declared type, or one made from the literal. */
|
package/scripts/ui.ts
CHANGED
|
@@ -45,6 +45,9 @@ export function since(started: number): string {
|
|
|
45
45
|
return `${((performance.now() - started) / 1000).toFixed(1)}s`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Whether a line is rewritten in place: a terminal, not CI's log. */
|
|
49
|
+
export const live = Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* A step that takes a while, said as it starts - `◇ compiling hello` - and
|
|
50
53
|
* ended with the line that says it is done, or with none when an error says
|
|
@@ -52,7 +55,6 @@ export function since(started: number): string {
|
|
|
52
55
|
* (CI, a pipe) the start is a line of its own.
|
|
53
56
|
*/
|
|
54
57
|
export function progress(text: string) {
|
|
55
|
-
const live = Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
56
58
|
process.stdout.write(`${c.cyan('◇')} ${text}${live ? '' : '\n'}`);
|
|
57
59
|
return {
|
|
58
60
|
end(done?: string) {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// `amxts upgrade` for menu-core's code API: every function of a menu takes one
|
|
2
|
+
// object, the menu's context `{ player, target, row, menu }`, and an item is
|
|
3
|
+
// one object with its title in it.
|
|
4
|
+
//
|
|
5
|
+
// shop.addItem("Heal", { onSelect: heal }) shop.addItem({ title: "Heal", onSelect: ({ player }) => heal(player) })
|
|
6
|
+
// (player) => player.isAlive ({ player }) => player.isAlive
|
|
7
|
+
// (player, target) => new Player(target).name ({ target }) => target.name
|
|
8
|
+
// (player, target) => `${target}` ({ row: target }) => `${target}`
|
|
9
|
+
//
|
|
10
|
+
// A target was a number; in the context `target` is a Player and `row` the
|
|
11
|
+
// number it was. A parameter read only as `new Player(target)` becomes the
|
|
12
|
+
// context's `target`, one read as a number its `row` - so the code means what
|
|
13
|
+
// it meant. A function passed by its name and declared in the file is called
|
|
14
|
+
// from an arrow that hands it what it took; one declared elsewhere, a target
|
|
15
|
+
// given as a number and `menus.runActions` are listed. Conditions and
|
|
16
|
+
// condition filters keep their parameters. What is rewritten takes one object,
|
|
17
|
+
// so a second run changes nothing.
|
|
18
|
+
import type { Change, Edit, Left } from './upgrade';
|
|
19
|
+
import ts from 'typescript';
|
|
20
|
+
import { applyEdits } from './upgrade';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What each parameter of an old callback was, by its place: `number` is the
|
|
24
|
+
* target as a number (the context's `row`, or `target` where it was only made
|
|
25
|
+
* a Player), `menuName` the menu's name (the context's `menu.name`).
|
|
26
|
+
*/
|
|
27
|
+
type Role = 'player' | 'target' | 'number' | 'name' | 'menuName';
|
|
28
|
+
|
|
29
|
+
const ITEM: Role[] = ['player', 'number'];
|
|
30
|
+
const OPEN: Role[] = ['player'];
|
|
31
|
+
const NAMED: Role[] = ['player', 'number', 'name'];
|
|
32
|
+
const RESTRICTION: Role[] = ['player', 'name', 'number'];
|
|
33
|
+
const ACTION_CHECK: Role[] = ['player', 'menuName', 'name'];
|
|
34
|
+
const FILTER: Role[] = ['target', 'player'];
|
|
35
|
+
const SOURCE: Role[] = ['player', 'menuName'];
|
|
36
|
+
|
|
37
|
+
/** The module's functions that take a callback: where it is among the arguments, and what it took. */
|
|
38
|
+
const MODULE_CALLBACKS: Record<string, { at: number; roles: Role[] }> = {
|
|
39
|
+
addAction: { at: 1, roles: NAMED },
|
|
40
|
+
addPlaceholder: { at: 1, roles: NAMED },
|
|
41
|
+
addRestriction: { at: 1, roles: RESTRICTION },
|
|
42
|
+
addActionCheck: { at: 2, roles: ACTION_CHECK },
|
|
43
|
+
setListSource: { at: 1, roles: SOURCE },
|
|
44
|
+
};
|
|
45
|
+
/** A menu's methods whose last argument is a callback, and what it took. */
|
|
46
|
+
const MENU_CALLBACKS: Record<string, Role[]> = { addFilter: FILTER, addPlaceholder: NAMED, setListSource: SOURCE };
|
|
47
|
+
/** An item's fields that are text or a test of the menu's context. */
|
|
48
|
+
const ITEM_FIELDS = new Set(['title', 'onSelect', 'visible', 'enabled', 'message']);
|
|
49
|
+
/** The module's functions that give a menu. */
|
|
50
|
+
const GIVE_MENU = new Set(['create', 'find', 'register', 'menuAt', 'activeMenu']);
|
|
51
|
+
const MENU_CORE = new Set(['@amxts/menu-core', '~/modules/menu-core']);
|
|
52
|
+
|
|
53
|
+
/** The context's field a role is read from, and the value it hands a function that took that role. */
|
|
54
|
+
const ARGUMENT: Record<Role, { key: string; value: string }> = {
|
|
55
|
+
player: { key: 'player', value: 'player' },
|
|
56
|
+
target: { key: 'target', value: 'target' },
|
|
57
|
+
number: { key: 'row', value: 'row' },
|
|
58
|
+
name: { key: 'name', value: 'name' },
|
|
59
|
+
menuName: { key: 'menu', value: 'menu.name' },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isFunction = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression => ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
63
|
+
|
|
64
|
+
/** A file's menus brought to the context: the rewritten text, each change, and what is left to do by hand. */
|
|
65
|
+
export function upgradeMenus(file: string, text: string): { text: string; changes: Change[]; left: Left[] } {
|
|
66
|
+
if (!text.includes('menu') && !text.includes('addItem')) return { text, changes: [], left: [] };
|
|
67
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
68
|
+
const edits: Edit[] = [];
|
|
69
|
+
const left: Left[] = [];
|
|
70
|
+
const lineOf = (node: ts.Node) => source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
71
|
+
const slice = (node: ts.Node) => text.slice(node.getStart(source), node.getEnd());
|
|
72
|
+
|
|
73
|
+
// The names the module goes by here: `menus`, a namespace import, and its functions imported by name.
|
|
74
|
+
const namespaces = new Set(['menus']);
|
|
75
|
+
const imported = new Map<string, string>();
|
|
76
|
+
for (const statement of source.statements) {
|
|
77
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MENU_CORE.has(statement.moduleSpecifier.text)) continue;
|
|
78
|
+
const bindings = statement.importClause?.namedBindings;
|
|
79
|
+
if (bindings && ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
|
|
80
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
81
|
+
for (const each of bindings.elements) imported.set(each.name.text, (each.propertyName ?? each.name).text);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The module's function a call names - `menus.create`, or `create` imported by name - or null. */
|
|
85
|
+
const moduleCall = (call: ts.CallExpression): string | null => {
|
|
86
|
+
const callee = call.expression;
|
|
87
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && namespaces.has(callee.expression.text)) return callee.name.text;
|
|
88
|
+
return ts.isIdentifier(callee) ? imported.get(callee.text) ?? null : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// The variables, parameters and functions a name may be, by name: enough to
|
|
92
|
+
// tell a menu and a function of the file's own.
|
|
93
|
+
const declared = new Map<string, ts.Node>();
|
|
94
|
+
const collect = (node: ts.Node) => {
|
|
95
|
+
if ((ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) || ts.isParameter(node)) && node.name && ts.isIdentifier(node.name) && !declared.has(node.name.text)) declared.set(node.name.text, node);
|
|
96
|
+
ts.forEachChild(node, collect);
|
|
97
|
+
};
|
|
98
|
+
collect(source);
|
|
99
|
+
|
|
100
|
+
const unwrap = (node: ts.Expression): ts.Expression => (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node) || ts.isAsExpression(node) ? unwrap(node.expression) : node);
|
|
101
|
+
|
|
102
|
+
/** Whether an expression is a menu of menu-core: one the module gave, or a name of its `Menu` type. */
|
|
103
|
+
const isMenu = (node: ts.Expression): boolean => {
|
|
104
|
+
const bare = unwrap(node);
|
|
105
|
+
if (ts.isCallExpression(bare)) return GIVE_MENU.has(moduleCall(bare) ?? '');
|
|
106
|
+
if (!ts.isIdentifier(bare)) return false;
|
|
107
|
+
const declaration = declared.get(bare.text);
|
|
108
|
+
if (!declaration || !(ts.isVariableDeclaration(declaration) || ts.isParameter(declaration))) return false;
|
|
109
|
+
const type = declaration.type?.getText(source) ?? '';
|
|
110
|
+
if ([...namespaces].some(ns => type.startsWith(`${ns}.Menu`)) || imported.get(type.replace(/\s*\|.*$/, '')) === 'Menu') return true;
|
|
111
|
+
return !!declaration.initializer && isMenu(declaration.initializer);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** The function a name is in this file, with its parameters; null for one declared elsewhere, or a value. */
|
|
115
|
+
const functionNamed = (name: string): ts.SignatureDeclaration | null => {
|
|
116
|
+
const declaration = declared.get(name);
|
|
117
|
+
if (!declaration) return null;
|
|
118
|
+
if (ts.isFunctionDeclaration(declaration)) return declaration;
|
|
119
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer && isFunction(declaration.initializer)) return declaration.initializer;
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Whether a function already takes the context: one object, or a parameter of a context's type. */
|
|
124
|
+
const takesContext = (fn: ts.SignatureDeclaration) => {
|
|
125
|
+
const first = fn.parameters[0];
|
|
126
|
+
return !first || !ts.isIdentifier(first.name) || /Context\b/.test(first.type?.getText(source) ?? '');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** The reads of a parameter in a function's body, and those that only make it a Player: `new Player(target)`. */
|
|
130
|
+
const readsOf = (fn: ts.ArrowFunction | ts.FunctionExpression, name: string) => {
|
|
131
|
+
const reads: ts.Identifier[] = [];
|
|
132
|
+
const visit = (node: ts.Node) => {
|
|
133
|
+
if (ts.isIdentifier(node) && node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts.isPropertyAssignment(node.parent) && node.parent.name === node)) reads.push(node);
|
|
134
|
+
ts.forEachChild(node, visit);
|
|
135
|
+
};
|
|
136
|
+
visit(fn.body);
|
|
137
|
+
const asPlayer = reads.map(read => read.parent).filter((parent): parent is ts.NewExpression =>
|
|
138
|
+
ts.isNewExpression(parent) && ts.isIdentifier(parent.expression) && parent.expression.text === 'Player' && parent.arguments?.length === 1);
|
|
139
|
+
return { reads, asPlayer };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The edits that bring a callback - written in place, or passed by its
|
|
144
|
+
* name - to the context. A `text` may be a string: a name the file gives
|
|
145
|
+
* a value is one, and so is one it does not declare.
|
|
146
|
+
*/
|
|
147
|
+
const callbackEdits = (node: ts.Expression, roles: Role[], text = false): Edit[] => {
|
|
148
|
+
if (isFunction(node)) return inlineEdits(node, roles);
|
|
149
|
+
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return [];
|
|
150
|
+
const name = slice(node);
|
|
151
|
+
const fn = ts.isIdentifier(node) ? functionNamed(node.text) : null;
|
|
152
|
+
if (!fn && (text || (ts.isIdentifier(node) && declared.has(node.text)))) return [];
|
|
153
|
+
|
|
154
|
+
if (!fn) {
|
|
155
|
+
const first = ARGUMENT[roles[0]];
|
|
156
|
+
left.push({ file, line: lineOf(node), why: `${name} is declared elsewhere: a function of the menu takes one object now - if it takes (${roles.join(', ')}), pass ({ ${first.key} }) => ${name}(${first.value}) and so on` });
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (fn.parameters.length === 0 || takesContext(fn)) return [];
|
|
161
|
+
const taken = roles.slice(0, fn.parameters.length).map(role => ARGUMENT[role]);
|
|
162
|
+
const keys = [...new Set(taken.map(each => each.key))];
|
|
163
|
+
const wrapper = `({ ${keys.join(', ')} }) => ${name}(${taken.map(each => each.value).join(', ')})`;
|
|
164
|
+
return [{ start: node.getStart(source), end: node.getEnd(), with: wrapper, from: name }];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** A function written in place: its parameters become one object, the context's fields by the names it read them by. */
|
|
168
|
+
const inlineEdits = (fn: ts.ArrowFunction | ts.FunctionExpression, roles: Role[]): Edit[] => {
|
|
169
|
+
const params = fn.parameters;
|
|
170
|
+
if (params.length === 0 || !params.every(param => ts.isIdentifier(param.name))) return [];
|
|
171
|
+
const edits: Edit[] = [];
|
|
172
|
+
const fields: string[] = [];
|
|
173
|
+
|
|
174
|
+
params.slice(0, roles.length).forEach((param, at) => {
|
|
175
|
+
const name = (param.name as ts.Identifier).text;
|
|
176
|
+
const { reads, asPlayer } = readsOf(fn, name);
|
|
177
|
+
if (reads.length === 0) return;
|
|
178
|
+
let key = ARGUMENT[roles[at]].key;
|
|
179
|
+
|
|
180
|
+
// A target read only as `new Player(target)` is the context's Player.
|
|
181
|
+
if (roles[at] === 'number' && asPlayer.length === reads.length) {
|
|
182
|
+
key = 'target';
|
|
183
|
+
for (const made of asPlayer) edits.push({ start: made.getStart(source), end: made.getEnd(), with: name, from: slice(made) });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (roles[at] === 'menuName') left.push({ file, line: lineOf(param), why: `${name} was the menu's name: the context's menu is the menu itself - read menu.name` });
|
|
187
|
+
fields.push(key === name ? key : `${key}: ${name}`);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const first = params[0];
|
|
191
|
+
const end = params[params.length - 1].getEnd();
|
|
192
|
+
const parenthesized = text.slice(fn.getStart(source), first.getStart(source)).includes('(');
|
|
193
|
+
const binding = fields.length ? `{ ${fields.join(', ')} }` : '';
|
|
194
|
+
const replaced = parenthesized ? binding : `(${binding})`;
|
|
195
|
+
edits.push({ start: first.getStart(source), end, with: replaced, from: text.slice(first.getStart(source), end) });
|
|
196
|
+
return edits;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** The text a node becomes with edits inside it made. */
|
|
200
|
+
const rewritten = (node: ts.Node, inside: Edit[]) => {
|
|
201
|
+
const start = node.getStart(source);
|
|
202
|
+
let out = slice(node);
|
|
203
|
+
for (const edit of [...inside].sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start - start) + edit.with + out.slice(edit.end - start);
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** The fields of an item's object literal - text and tests of the context - brought to it. */
|
|
208
|
+
const itemEdits = (item: ts.ObjectLiteralExpression, fields: Set<string>, roles: (field: string) => Role[]): Edit[] => item.properties.flatMap((property) => {
|
|
209
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || !fields.has(property.name.text)) return [];
|
|
210
|
+
const value = property.initializer;
|
|
211
|
+
const field = property.name.text;
|
|
212
|
+
if (field !== 'enabled' || !ts.isArrayLiteralExpression(value)) return callbackEdits(value, roles(field), field === 'title' || field === 'message');
|
|
213
|
+
// A list of requirements: each `{ when, message }` is an item's test and text.
|
|
214
|
+
return value.elements.filter(ts.isObjectLiteralExpression).flatMap(requirement => itemEdits(requirement, new Set(['when', 'message']), () => ITEM));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/** `addItem(text, options)` - or `addFixedItem(slot, text, options)` - as one object with the title in it. */
|
|
218
|
+
const addItemEdits = (call: ts.CallExpression, at: number): Edit[] => {
|
|
219
|
+
const [label, options] = [call.arguments[at], call.arguments[at + 1]];
|
|
220
|
+
if (!label) return [];
|
|
221
|
+
if (ts.isObjectLiteralExpression(label) && !options) return itemEdits(label, ITEM_FIELDS, () => ITEM);
|
|
222
|
+
|
|
223
|
+
const title = rewritten(label, callbackEdits(label, ITEM, true));
|
|
224
|
+
if (!options) return [{ start: label.getStart(source), end: label.getEnd(), with: `{ title: ${title} }`, from: slice(label) }];
|
|
225
|
+
if (!ts.isObjectLiteralExpression(options)) return [{ start: label.getStart(source), end: options.getEnd(), with: `{ title: ${title}, ...${slice(options)} }`, from: `${slice(label)}, ${slice(options)}` }];
|
|
226
|
+
|
|
227
|
+
const brace = options.getStart(source) + 1;
|
|
228
|
+
const first = options.properties[0];
|
|
229
|
+
const multiline = first && text.slice(brace, first.getStart(source)).includes('\n');
|
|
230
|
+
const indent = multiline ? text.slice(brace, first.getStart(source)).replace(/^[^\n]*\n/, '') : '';
|
|
231
|
+
const opening = !first ? `{ title: ${title} ` : multiline ? `{\n${indent}title: ${title},` : `{ title: ${title},`;
|
|
232
|
+
return [
|
|
233
|
+
{ start: label.getStart(source), end: brace, with: opening, from: `${slice(label)}, {` },
|
|
234
|
+
...itemEdits(options, ITEM_FIELDS, () => ITEM),
|
|
235
|
+
];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/** `show(player, { target: player.id })`: the target is the player himself. */
|
|
239
|
+
const showEdits = (options: ts.Expression | undefined): Edit[] => {
|
|
240
|
+
if (!options || !ts.isObjectLiteralExpression(options)) return [];
|
|
241
|
+
return options.properties.flatMap((property) => {
|
|
242
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== 'target') return [];
|
|
243
|
+
const value = property.initializer;
|
|
244
|
+
if (ts.isPropertyAccessExpression(value) && value.name.text === 'id') return [{ start: value.getStart(source), end: value.getEnd(), with: slice(value.expression), from: slice(value) }];
|
|
245
|
+
if (ts.isNumericLiteral(value)) left.push({ file, line: lineOf(value), why: 'show\'s target is a player now: pass the Player, not its number' });
|
|
246
|
+
return [];
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const visit = (node: ts.Node) => {
|
|
251
|
+
ts.forEachChild(node, visit);
|
|
252
|
+
if (!ts.isCallExpression(node)) return;
|
|
253
|
+
const callee = node.expression;
|
|
254
|
+
const args = node.arguments;
|
|
255
|
+
const fn = moduleCall(node);
|
|
256
|
+
|
|
257
|
+
if (fn === 'create' && args[1] && ts.isObjectLiteralExpression(args[1])) edits.push(...itemEdits(args[1], new Set(['title', 'activeWhen']), field => (field === 'activeWhen' ? OPEN : ITEM)));
|
|
258
|
+
else if (fn === 'show') edits.push(...showEdits(args[2]));
|
|
259
|
+
else if (fn === 'runActions') left.push({ file, line: lineOf(node), why: 'runActions is a method of the menu now: menu.runActions(player, line, target), the target a Player' });
|
|
260
|
+
else if (fn && MODULE_CALLBACKS[fn] && args[MODULE_CALLBACKS[fn].at]) edits.push(...callbackEdits(args[MODULE_CALLBACKS[fn].at], MODULE_CALLBACKS[fn].roles));
|
|
261
|
+
if (fn || !ts.isPropertyAccessExpression(callee)) return;
|
|
262
|
+
|
|
263
|
+
const method = callee.name.text;
|
|
264
|
+
const menu = isMenu(callee.expression);
|
|
265
|
+
const oldItem = (at: number) => args[at] && (ts.isStringLiteralLike(args[at]) || ts.isTemplateExpression(args[at]) || isFunction(args[at]));
|
|
266
|
+
|
|
267
|
+
if (method === 'addItem' && (menu || oldItem(0))) edits.push(...addItemEdits(node, 0));
|
|
268
|
+
else if (method === 'addFixedItem' && (menu || oldItem(1))) edits.push(...addItemEdits(node, 1));
|
|
269
|
+
else if (menu && method === 'show') edits.push(...showEdits(args[1]));
|
|
270
|
+
else if (menu && MENU_CALLBACKS[method] && args.length >= 1) edits.push(...callbackEdits(args[method === 'addPlaceholder' ? 1 : 0], MENU_CALLBACKS[method]));
|
|
271
|
+
};
|
|
272
|
+
visit(source);
|
|
273
|
+
return { ...applyEdits(file, text, source, edits), left };
|
|
274
|
+
}
|