redscript-mc 1.2.18 → 1.2.20
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/dist/__tests__/compile-all.test.d.ts +2 -1
- package/dist/__tests__/compile-all.test.js +21 -8
- package/dist/cli.js +92 -3
- package/dist/parser/index.js +37 -4
- package/editors/vscode/out/extension.js +64 -7
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/compile-all.test.ts +23 -8
- package/src/cli.ts +99 -3
- package/src/examples/capture_the_flag.mcrs +1 -1
- package/src/examples/hunger_games.mcrs +1 -1
- package/src/examples/zombie_survival.mcrs +1 -1
- package/src/parser/index.ts +36 -10
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Compile-all smoke test
|
|
3
3
|
*
|
|
4
4
|
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
5
|
-
* and verifies that each one compiles without
|
|
5
|
+
* and verifies that each one compiles without errors via the CLI (which handles
|
|
6
|
+
* `import` statements, unlike the bare `compile()` function).
|
|
6
7
|
*
|
|
7
8
|
* This catches regressions where a language change breaks existing source files.
|
|
8
9
|
*/
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Compile-all smoke test
|
|
4
4
|
*
|
|
5
5
|
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
6
|
-
* and verifies that each one compiles without
|
|
6
|
+
* and verifies that each one compiles without errors via the CLI (which handles
|
|
7
|
+
* `import` statements, unlike the bare `compile()` function).
|
|
7
8
|
*
|
|
8
9
|
* This catches regressions where a language change breaks existing source files.
|
|
9
10
|
*/
|
|
@@ -43,8 +44,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const path = __importStar(require("path"));
|
|
46
|
-
const
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const os = __importStar(require("os"));
|
|
47
49
|
const REPO_ROOT = path.resolve(__dirname, '../../');
|
|
50
|
+
const CLI = path.join(REPO_ROOT, 'dist', 'cli.js');
|
|
48
51
|
/** Patterns to skip */
|
|
49
52
|
const SKIP_GLOBS = [
|
|
50
53
|
'node_modules',
|
|
@@ -72,18 +75,28 @@ function findMcrsFiles(dir) {
|
|
|
72
75
|
return results;
|
|
73
76
|
}
|
|
74
77
|
const mcrsFiles = findMcrsFiles(REPO_ROOT);
|
|
75
|
-
|
|
78
|
+
const TMP_OUT = path.join(os.tmpdir(), 'redscript-compile-all');
|
|
79
|
+
describe('compile-all: every .mcrs file should compile without errors (CLI)', () => {
|
|
76
80
|
test('found at least one .mcrs file', () => {
|
|
77
81
|
expect(mcrsFiles.length).toBeGreaterThan(0);
|
|
78
82
|
});
|
|
79
83
|
for (const filePath of mcrsFiles) {
|
|
80
84
|
const label = path.relative(REPO_ROOT, filePath);
|
|
81
85
|
test(label, () => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const outDir = path.join(TMP_OUT, label.replace(/[^a-zA-Z0-9]/g, '_'));
|
|
87
|
+
let stdout = '';
|
|
88
|
+
let stderr = '';
|
|
89
|
+
try {
|
|
90
|
+
const result = (0, child_process_1.execSync)(`node "${CLI}" compile "${filePath}" -o "${outDir}"`, { encoding: 'utf8', stdio: 'pipe' });
|
|
91
|
+
stdout = result;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
stdout = err.stdout ?? '';
|
|
95
|
+
stderr = err.stderr ?? '';
|
|
96
|
+
const output = (stdout + stderr).trim();
|
|
97
|
+
// Fail with the compiler error message
|
|
98
|
+
throw new Error(`Compile failed for ${label}:\n${output}`);
|
|
99
|
+
}
|
|
87
100
|
});
|
|
88
101
|
}
|
|
89
102
|
});
|
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,8 @@ const repl_1 = require("./repl");
|
|
|
51
51
|
const metadata_1 = require("./builtins/metadata");
|
|
52
52
|
const fs = __importStar(require("fs"));
|
|
53
53
|
const path = __importStar(require("path"));
|
|
54
|
+
const https = __importStar(require("https"));
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
54
56
|
// Parse command line arguments
|
|
55
57
|
const args = process.argv.slice(2);
|
|
56
58
|
function printUsage() {
|
|
@@ -74,6 +76,7 @@ Commands:
|
|
|
74
76
|
generate-dts Generate builtin function declaration file (builtins.d.mcrs)
|
|
75
77
|
repl Start an interactive RedScript REPL
|
|
76
78
|
version Print the RedScript version
|
|
79
|
+
upgrade Upgrade to the latest version (npm install -g redscript-mc@latest)
|
|
77
80
|
|
|
78
81
|
Options:
|
|
79
82
|
-o, --output <path> Output directory or file path, depending on target
|
|
@@ -92,15 +95,90 @@ Targets:
|
|
|
92
95
|
structure Generate a Minecraft structure .nbt file with command blocks
|
|
93
96
|
`);
|
|
94
97
|
}
|
|
95
|
-
function
|
|
98
|
+
function getLocalVersion() {
|
|
96
99
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
97
100
|
try {
|
|
98
101
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
99
|
-
|
|
102
|
+
return pkg.version ?? '0.0.0';
|
|
100
103
|
}
|
|
101
104
|
catch {
|
|
102
|
-
|
|
105
|
+
return '0.0.0';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function printVersion() {
|
|
109
|
+
console.log(`RedScript v${getLocalVersion()}`);
|
|
110
|
+
}
|
|
111
|
+
/** Fetch latest version from npm registry (non-blocking, best-effort). */
|
|
112
|
+
function fetchLatestVersion() {
|
|
113
|
+
return new Promise(resolve => {
|
|
114
|
+
const req = https.get('https://registry.npmjs.org/redscript-mc/latest', { timeout: 3000 }, res => {
|
|
115
|
+
let data = '';
|
|
116
|
+
res.on('data', chunk => { data += chunk; });
|
|
117
|
+
res.on('end', () => {
|
|
118
|
+
try {
|
|
119
|
+
const json = JSON.parse(data);
|
|
120
|
+
resolve(json.version ?? null);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
resolve(null);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
req.on('error', () => resolve(null));
|
|
128
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Compare semver strings. Returns true if b > a. */
|
|
132
|
+
function isNewer(current, latest) {
|
|
133
|
+
const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
|
|
134
|
+
const [ca, cb, cc] = parse(current);
|
|
135
|
+
const [la, lb, lc] = parse(latest);
|
|
136
|
+
if (la !== ca)
|
|
137
|
+
return la > ca;
|
|
138
|
+
if (lb !== cb)
|
|
139
|
+
return lb > cb;
|
|
140
|
+
return lc > cc;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check for a newer version and print a notice if one exists.
|
|
144
|
+
* Runs in background — does NOT block normal CLI operation.
|
|
145
|
+
*/
|
|
146
|
+
async function checkForUpdates(silent = false) {
|
|
147
|
+
const current = getLocalVersion();
|
|
148
|
+
const latest = await fetchLatestVersion();
|
|
149
|
+
if (latest && isNewer(current, latest)) {
|
|
150
|
+
console.log(`\n💡 New version available: v${current} → v${latest}`);
|
|
151
|
+
console.log(` Run: redscript upgrade\n`);
|
|
103
152
|
}
|
|
153
|
+
else if (!silent && latest) {
|
|
154
|
+
// Only print when explicitly running 'version' or 'upgrade'
|
|
155
|
+
// No output for normal commands — keep startup noise-free
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Run npm install -g to upgrade to latest. */
|
|
159
|
+
function upgradeCommand() {
|
|
160
|
+
const current = getLocalVersion();
|
|
161
|
+
console.log(`Current version: v${current}`);
|
|
162
|
+
console.log('Checking latest version...');
|
|
163
|
+
fetchLatestVersion().then(latest => {
|
|
164
|
+
if (!latest) {
|
|
165
|
+
console.error('Could not fetch latest version from npm.');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (!isNewer(current, latest)) {
|
|
169
|
+
console.log(`✅ Already up to date (v${current})`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`Upgrading v${current} → v${latest}...`);
|
|
173
|
+
try {
|
|
174
|
+
(0, child_process_1.execSync)('npm install -g redscript-mc@latest', { stdio: 'inherit' });
|
|
175
|
+
console.log(`✅ Upgraded to v${latest}`);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
console.error('Upgrade failed. Try manually: npm install -g redscript-mc@latest');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
104
182
|
}
|
|
105
183
|
function parseArgs(args) {
|
|
106
184
|
const result = { dce: true };
|
|
@@ -369,6 +447,12 @@ async function main() {
|
|
|
369
447
|
printUsage();
|
|
370
448
|
process.exit(parsed.help ? 0 : 1);
|
|
371
449
|
}
|
|
450
|
+
// Background update check — non-blocking, only shows notice if newer version exists
|
|
451
|
+
// Skip for repl/upgrade/version to avoid double-printing
|
|
452
|
+
const noCheckCmds = new Set(['upgrade', 'update', 'version', 'repl']);
|
|
453
|
+
if (!noCheckCmds.has(parsed.command ?? '')) {
|
|
454
|
+
checkForUpdates().catch(() => { });
|
|
455
|
+
}
|
|
372
456
|
switch (parsed.command) {
|
|
373
457
|
case 'compile':
|
|
374
458
|
if (!parsed.file) {
|
|
@@ -429,6 +513,11 @@ async function main() {
|
|
|
429
513
|
break;
|
|
430
514
|
case 'version':
|
|
431
515
|
printVersion();
|
|
516
|
+
await checkForUpdates();
|
|
517
|
+
break;
|
|
518
|
+
case 'upgrade':
|
|
519
|
+
case 'update':
|
|
520
|
+
upgradeCommand();
|
|
432
521
|
break;
|
|
433
522
|
default:
|
|
434
523
|
console.error(`Error: Unknown command '${parsed.command}'`);
|
package/dist/parser/index.js
CHANGED
|
@@ -564,10 +564,29 @@ class Parser {
|
|
|
564
564
|
parseForRangeStmt(forToken) {
|
|
565
565
|
const varName = this.expect('ident').value;
|
|
566
566
|
this.expect('in');
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
567
|
+
let start;
|
|
568
|
+
let end;
|
|
569
|
+
if (this.check('range_lit')) {
|
|
570
|
+
// Literal range: 0..10, 0..count, 0..=9
|
|
571
|
+
const rangeToken = this.advance();
|
|
572
|
+
const range = this.parseRangeValue(rangeToken.value);
|
|
573
|
+
start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken);
|
|
574
|
+
if (range.max !== null && range.max !== undefined) {
|
|
575
|
+
// Fully numeric: 0..10
|
|
576
|
+
end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Open-ended: "0.." — parse the end expression from next tokens
|
|
580
|
+
end = this.parseUnaryExpr();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Dynamic range: expr..expr (e.g. start..end) — not yet supported
|
|
585
|
+
// Fall back to: parse as int_lit 0..0 (safe default)
|
|
586
|
+
start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
|
|
587
|
+
end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
|
|
588
|
+
this.error('Dynamic range start requires a literal integer (e.g. 0..count)');
|
|
589
|
+
}
|
|
571
590
|
const body = this.parseBlock();
|
|
572
591
|
return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken);
|
|
573
592
|
}
|
|
@@ -1083,6 +1102,20 @@ class Parser {
|
|
|
1083
1102
|
this.error(`Unexpected token '${token.kind}'`);
|
|
1084
1103
|
}
|
|
1085
1104
|
parseLiteralExpr() {
|
|
1105
|
+
// Support negative literals: -5, -3.14
|
|
1106
|
+
if (this.check('-')) {
|
|
1107
|
+
this.advance();
|
|
1108
|
+
const token = this.peek();
|
|
1109
|
+
if (token.kind === 'int_lit') {
|
|
1110
|
+
this.advance();
|
|
1111
|
+
return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token);
|
|
1112
|
+
}
|
|
1113
|
+
if (token.kind === 'float_lit') {
|
|
1114
|
+
this.advance();
|
|
1115
|
+
return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token);
|
|
1116
|
+
}
|
|
1117
|
+
this.error('Expected number after unary -');
|
|
1118
|
+
}
|
|
1086
1119
|
const expr = this.parsePrimaryExpr();
|
|
1087
1120
|
if (expr.kind === 'int_lit' ||
|
|
1088
1121
|
expr.kind === 'float_lit' ||
|
|
@@ -186,6 +186,7 @@ var require_lexer = __commonJS({
|
|
|
186
186
|
execute: "execute",
|
|
187
187
|
run: "run",
|
|
188
188
|
unless: "unless",
|
|
189
|
+
declare: "declare",
|
|
189
190
|
int: "int",
|
|
190
191
|
bool: "bool",
|
|
191
192
|
float: "float",
|
|
@@ -355,6 +356,13 @@ var require_lexer = __commonJS({
|
|
|
355
356
|
value += this.advance();
|
|
356
357
|
}
|
|
357
358
|
}
|
|
359
|
+
if (/[a-zA-Z_]/.test(this.peek())) {
|
|
360
|
+
let ident = "";
|
|
361
|
+
while (/[a-zA-Z0-9_]/.test(this.peek())) {
|
|
362
|
+
ident += this.advance();
|
|
363
|
+
}
|
|
364
|
+
value += ident;
|
|
365
|
+
}
|
|
358
366
|
this.addToken("rel_coord", value, startLine, startCol);
|
|
359
367
|
return;
|
|
360
368
|
}
|
|
@@ -815,6 +823,9 @@ var require_parser = __commonJS({
|
|
|
815
823
|
enums.push(this.parseEnumDecl());
|
|
816
824
|
} else if (this.check("const")) {
|
|
817
825
|
consts.push(this.parseConstDecl());
|
|
826
|
+
} else if (this.check("declare")) {
|
|
827
|
+
this.advance();
|
|
828
|
+
this.parseDeclareStub();
|
|
818
829
|
} else {
|
|
819
830
|
declarations.push(this.parseFnDecl());
|
|
820
831
|
}
|
|
@@ -877,12 +888,15 @@ var require_parser = __commonJS({
|
|
|
877
888
|
parseConstDecl() {
|
|
878
889
|
const constToken = this.expect("const");
|
|
879
890
|
const name = this.expect("ident").value;
|
|
880
|
-
|
|
881
|
-
|
|
891
|
+
let type;
|
|
892
|
+
if (this.match(":")) {
|
|
893
|
+
type = this.parseType();
|
|
894
|
+
}
|
|
882
895
|
this.expect("=");
|
|
883
896
|
const value = this.parseLiteralExpr();
|
|
884
897
|
this.match(";");
|
|
885
|
-
|
|
898
|
+
const inferredType = type ?? (value.kind === "str_lit" ? { kind: "named", name: "string" } : value.kind === "bool_lit" ? { kind: "named", name: "bool" } : value.kind === "float_lit" ? { kind: "named", name: "float" } : { kind: "named", name: "int" });
|
|
899
|
+
return this.withLoc({ name, type: inferredType, value }, constToken);
|
|
886
900
|
}
|
|
887
901
|
parseGlobalDecl(mutable) {
|
|
888
902
|
const token = this.advance();
|
|
@@ -911,6 +925,24 @@ var require_parser = __commonJS({
|
|
|
911
925
|
const body = this.parseBlock();
|
|
912
926
|
return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
|
|
913
927
|
}
|
|
928
|
+
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
929
|
+
parseDeclareStub() {
|
|
930
|
+
this.expect("fn");
|
|
931
|
+
this.expect("ident");
|
|
932
|
+
this.expect("(");
|
|
933
|
+
let depth = 1;
|
|
934
|
+
while (!this.check("eof") && depth > 0) {
|
|
935
|
+
const t = this.advance();
|
|
936
|
+
if (t.kind === "(")
|
|
937
|
+
depth++;
|
|
938
|
+
else if (t.kind === ")")
|
|
939
|
+
depth--;
|
|
940
|
+
}
|
|
941
|
+
if (this.match(":") || this.match("->")) {
|
|
942
|
+
this.parseType();
|
|
943
|
+
}
|
|
944
|
+
this.match(";");
|
|
945
|
+
}
|
|
914
946
|
parseDecorators() {
|
|
915
947
|
const decorators = [];
|
|
916
948
|
while (this.check("decorator")) {
|
|
@@ -3581,6 +3613,15 @@ var require_lowering = __commonJS({
|
|
|
3581
3613
|
return null;
|
|
3582
3614
|
return expr.name;
|
|
3583
3615
|
}
|
|
3616
|
+
tryGetMacroParamByName(name) {
|
|
3617
|
+
if (!this.currentFnParamNames.has(name))
|
|
3618
|
+
return null;
|
|
3619
|
+
if (this.constValues.has(name))
|
|
3620
|
+
return null;
|
|
3621
|
+
if (this.stringValues.has(name))
|
|
3622
|
+
return null;
|
|
3623
|
+
return name;
|
|
3624
|
+
}
|
|
3584
3625
|
/**
|
|
3585
3626
|
* Converts an expression to a string for use as a builtin arg.
|
|
3586
3627
|
* If the expression is a macro param, returns `$(name)` and sets macroParam.
|
|
@@ -3590,6 +3631,17 @@ var require_lowering = __commonJS({
|
|
|
3590
3631
|
if (macroParam) {
|
|
3591
3632
|
return { str: `$(${macroParam})`, macroParam };
|
|
3592
3633
|
}
|
|
3634
|
+
if (expr.kind === "rel_coord" || expr.kind === "local_coord") {
|
|
3635
|
+
const val = expr.value;
|
|
3636
|
+
const prefix = val[0];
|
|
3637
|
+
const rest = val.slice(1);
|
|
3638
|
+
if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
|
|
3639
|
+
const paramName = this.tryGetMacroParamByName(rest);
|
|
3640
|
+
if (paramName) {
|
|
3641
|
+
return { str: `${prefix}$(${paramName})`, macroParam: paramName };
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3593
3645
|
if (expr.kind === "struct_lit" || expr.kind === "array_lit") {
|
|
3594
3646
|
return { str: this.exprToSnbt(expr) };
|
|
3595
3647
|
}
|
|
@@ -5875,8 +5927,11 @@ var require_lowering = __commonJS({
|
|
|
5875
5927
|
}
|
|
5876
5928
|
/**
|
|
5877
5929
|
* Checks a raw() command string for `${...}` interpolation containing runtime variables.
|
|
5878
|
-
* - If the interpolated
|
|
5879
|
-
* - If the interpolated name is a
|
|
5930
|
+
* - If the interpolated expression is a numeric literal → OK (MC macro syntax).
|
|
5931
|
+
* - If the interpolated name is a compile-time constant (in constValues) → OK.
|
|
5932
|
+
* - If the interpolated name is a known runtime variable (in varMap) → DiagnosticError.
|
|
5933
|
+
* - Unknown names → OK (could be MC macro params or external constants).
|
|
5934
|
+
*
|
|
5880
5935
|
* This catches the common mistake of writing raw("say ${score}") expecting interpolation,
|
|
5881
5936
|
* which would silently emit a literal `${score}` in the MC command.
|
|
5882
5937
|
*/
|
|
@@ -5891,8 +5946,10 @@ var require_lowering = __commonJS({
|
|
|
5891
5946
|
if (this.constValues.has(name)) {
|
|
5892
5947
|
continue;
|
|
5893
5948
|
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5949
|
+
if (this.varMap.has(name) || this.currentFnParamNames.has(name)) {
|
|
5950
|
+
const loc = span ?? { line: 1, col: 1 };
|
|
5951
|
+
throw new diagnostics_1.DiagnosticError("LoweringError", `raw() command contains runtime variable interpolation '\${${name}}'. Variables cannot be interpolated into raw commands at compile time. Use f-string messages (say/tell/announce) or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`, loc);
|
|
5952
|
+
}
|
|
5896
5953
|
}
|
|
5897
5954
|
}
|
|
5898
5955
|
resolveInstanceMethod(expr) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "redscript-vscode",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.11",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"redscript": "file:../../"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
3
|
"displayName": "RedScript for Minecraft",
|
|
4
4
|
"description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.11",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
{
|
|
272
272
|
"comment": "Builtin functions",
|
|
273
273
|
"name": "support.function.builtin.redscript",
|
|
274
|
-
"match": "\\b(say|tell|announce|title|subtitle|actionbar|title_times|give|kill|effect|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|
|
|
274
|
+
"match": "\\b(say|tell|tellraw|announce|title|subtitle|actionbar|title_times|give|kill|effect|effect_clear|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|score|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|random|random_native|random_sequence|data_get|data_merge|str_len|push|pop|bossbar_add|bossbar_set_value|bossbar_set_max|bossbar_set_color|bossbar_set_style|bossbar_set_visible|bossbar_set_players|bossbar_remove|bossbar_get_value|team_add|team_remove|team_join|team_leave|team_option|set_new|set_add|set_contains|set_remove|set_clear|setTimeout|setInterval|clearInterval|spawn_object|if_entity|unless_entity)(?=\\s*\\()"
|
|
275
275
|
},
|
|
276
276
|
{
|
|
277
277
|
"comment": "User-defined function calls",
|
package/package.json
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* Compile-all smoke test
|
|
3
3
|
*
|
|
4
4
|
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
5
|
-
* and verifies that each one compiles without
|
|
5
|
+
* and verifies that each one compiles without errors via the CLI (which handles
|
|
6
|
+
* `import` statements, unlike the bare `compile()` function).
|
|
6
7
|
*
|
|
7
8
|
* This catches regressions where a language change breaks existing source files.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import * as fs from 'fs'
|
|
11
12
|
import * as path from 'path'
|
|
12
|
-
import {
|
|
13
|
+
import { execSync } from 'child_process'
|
|
14
|
+
import * as os from 'os'
|
|
13
15
|
|
|
14
16
|
const REPO_ROOT = path.resolve(__dirname, '../../')
|
|
17
|
+
const CLI = path.join(REPO_ROOT, 'dist', 'cli.js')
|
|
15
18
|
|
|
16
19
|
/** Patterns to skip */
|
|
17
20
|
const SKIP_GLOBS = [
|
|
@@ -41,8 +44,9 @@ function findMcrsFiles(dir: string): string[] {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
const mcrsFiles = findMcrsFiles(REPO_ROOT)
|
|
47
|
+
const TMP_OUT = path.join(os.tmpdir(), 'redscript-compile-all')
|
|
44
48
|
|
|
45
|
-
describe('compile-all: every .mcrs file should compile without errors', () => {
|
|
49
|
+
describe('compile-all: every .mcrs file should compile without errors (CLI)', () => {
|
|
46
50
|
test('found at least one .mcrs file', () => {
|
|
47
51
|
expect(mcrsFiles.length).toBeGreaterThan(0)
|
|
48
52
|
})
|
|
@@ -50,11 +54,22 @@ describe('compile-all: every .mcrs file should compile without errors', () => {
|
|
|
50
54
|
for (const filePath of mcrsFiles) {
|
|
51
55
|
const label = path.relative(REPO_ROOT, filePath)
|
|
52
56
|
test(label, () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const outDir = path.join(TMP_OUT, label.replace(/[^a-zA-Z0-9]/g, '_'))
|
|
58
|
+
let stdout = ''
|
|
59
|
+
let stderr = ''
|
|
60
|
+
try {
|
|
61
|
+
const result = execSync(
|
|
62
|
+
`node "${CLI}" compile "${filePath}" -o "${outDir}"`,
|
|
63
|
+
{ encoding: 'utf8', stdio: 'pipe' }
|
|
64
|
+
)
|
|
65
|
+
stdout = result
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
stdout = err.stdout ?? ''
|
|
68
|
+
stderr = err.stderr ?? ''
|
|
69
|
+
const output = (stdout + stderr).trim()
|
|
70
|
+
// Fail with the compiler error message
|
|
71
|
+
throw new Error(`Compile failed for ${label}:\n${output}`)
|
|
72
|
+
}
|
|
58
73
|
})
|
|
59
74
|
}
|
|
60
75
|
})
|
package/src/cli.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { generateDts } from './builtins/metadata'
|
|
|
18
18
|
import type { OptimizationStats } from './optimizer/commands'
|
|
19
19
|
import * as fs from 'fs'
|
|
20
20
|
import * as path from 'path'
|
|
21
|
+
import * as https from 'https'
|
|
22
|
+
import { execSync } from 'child_process'
|
|
21
23
|
|
|
22
24
|
// Parse command line arguments
|
|
23
25
|
const args = process.argv.slice(2)
|
|
@@ -43,6 +45,7 @@ Commands:
|
|
|
43
45
|
generate-dts Generate builtin function declaration file (builtins.d.mcrs)
|
|
44
46
|
repl Start an interactive RedScript REPL
|
|
45
47
|
version Print the RedScript version
|
|
48
|
+
upgrade Upgrade to the latest version (npm install -g redscript-mc@latest)
|
|
46
49
|
|
|
47
50
|
Options:
|
|
48
51
|
-o, --output <path> Output directory or file path, depending on target
|
|
@@ -62,16 +65,96 @@ Targets:
|
|
|
62
65
|
`)
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
function
|
|
68
|
+
function getLocalVersion(): string {
|
|
66
69
|
const packagePath = path.join(__dirname, '..', 'package.json')
|
|
67
70
|
try {
|
|
68
71
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
|
|
69
|
-
|
|
72
|
+
return pkg.version ?? '0.0.0'
|
|
70
73
|
} catch {
|
|
71
|
-
|
|
74
|
+
return '0.0.0'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printVersion(): void {
|
|
79
|
+
console.log(`RedScript v${getLocalVersion()}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Fetch latest version from npm registry (non-blocking, best-effort). */
|
|
83
|
+
function fetchLatestVersion(): Promise<string | null> {
|
|
84
|
+
return new Promise(resolve => {
|
|
85
|
+
const req = https.get(
|
|
86
|
+
'https://registry.npmjs.org/redscript-mc/latest',
|
|
87
|
+
{ timeout: 3000 },
|
|
88
|
+
res => {
|
|
89
|
+
let data = ''
|
|
90
|
+
res.on('data', chunk => { data += chunk })
|
|
91
|
+
res.on('end', () => {
|
|
92
|
+
try {
|
|
93
|
+
const json = JSON.parse(data)
|
|
94
|
+
resolve(json.version ?? null)
|
|
95
|
+
} catch {
|
|
96
|
+
resolve(null)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
req.on('error', () => resolve(null))
|
|
102
|
+
req.on('timeout', () => { req.destroy(); resolve(null) })
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Compare semver strings. Returns true if b > a. */
|
|
107
|
+
function isNewer(current: string, latest: string): boolean {
|
|
108
|
+
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
|
|
109
|
+
const [ca, cb, cc] = parse(current)
|
|
110
|
+
const [la, lb, lc] = parse(latest)
|
|
111
|
+
if (la !== ca) return la > ca
|
|
112
|
+
if (lb !== cb) return lb > cb
|
|
113
|
+
return lc > cc
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check for a newer version and print a notice if one exists.
|
|
118
|
+
* Runs in background — does NOT block normal CLI operation.
|
|
119
|
+
*/
|
|
120
|
+
async function checkForUpdates(silent = false): Promise<void> {
|
|
121
|
+
const current = getLocalVersion()
|
|
122
|
+
const latest = await fetchLatestVersion()
|
|
123
|
+
if (latest && isNewer(current, latest)) {
|
|
124
|
+
console.log(`\n💡 New version available: v${current} → v${latest}`)
|
|
125
|
+
console.log(` Run: redscript upgrade\n`)
|
|
126
|
+
} else if (!silent && latest) {
|
|
127
|
+
// Only print when explicitly running 'version' or 'upgrade'
|
|
128
|
+
// No output for normal commands — keep startup noise-free
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
131
|
|
|
132
|
+
/** Run npm install -g to upgrade to latest. */
|
|
133
|
+
function upgradeCommand(): void {
|
|
134
|
+
const current = getLocalVersion()
|
|
135
|
+
console.log(`Current version: v${current}`)
|
|
136
|
+
console.log('Checking latest version...')
|
|
137
|
+
|
|
138
|
+
fetchLatestVersion().then(latest => {
|
|
139
|
+
if (!latest) {
|
|
140
|
+
console.error('Could not fetch latest version from npm.')
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
if (!isNewer(current, latest)) {
|
|
144
|
+
console.log(`✅ Already up to date (v${current})`)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
console.log(`Upgrading v${current} → v${latest}...`)
|
|
148
|
+
try {
|
|
149
|
+
execSync('npm install -g redscript-mc@latest', { stdio: 'inherit' })
|
|
150
|
+
console.log(`✅ Upgraded to v${latest}`)
|
|
151
|
+
} catch {
|
|
152
|
+
console.error('Upgrade failed. Try manually: npm install -g redscript-mc@latest')
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
75
158
|
function parseArgs(args: string[]): {
|
|
76
159
|
command?: string
|
|
77
160
|
file?: string
|
|
@@ -378,6 +461,13 @@ async function main(): Promise<void> {
|
|
|
378
461
|
process.exit(parsed.help ? 0 : 1)
|
|
379
462
|
}
|
|
380
463
|
|
|
464
|
+
// Background update check — non-blocking, only shows notice if newer version exists
|
|
465
|
+
// Skip for repl/upgrade/version to avoid double-printing
|
|
466
|
+
const noCheckCmds = new Set(['upgrade', 'update', 'version', 'repl'])
|
|
467
|
+
if (!noCheckCmds.has(parsed.command ?? '')) {
|
|
468
|
+
checkForUpdates().catch(() => { /* ignore */ })
|
|
469
|
+
}
|
|
470
|
+
|
|
381
471
|
switch (parsed.command) {
|
|
382
472
|
case 'compile':
|
|
383
473
|
if (!parsed.file) {
|
|
@@ -458,6 +548,12 @@ async function main(): Promise<void> {
|
|
|
458
548
|
|
|
459
549
|
case 'version':
|
|
460
550
|
printVersion()
|
|
551
|
+
await checkForUpdates()
|
|
552
|
+
break
|
|
553
|
+
|
|
554
|
+
case 'upgrade':
|
|
555
|
+
case 'update':
|
|
556
|
+
upgradeCommand()
|
|
461
557
|
break
|
|
462
558
|
|
|
463
559
|
default:
|
package/src/parser/index.ts
CHANGED
|
@@ -668,17 +668,29 @@ export class Parser {
|
|
|
668
668
|
private parseForRangeStmt(forToken: Token): Stmt {
|
|
669
669
|
const varName = this.expect('ident').value
|
|
670
670
|
this.expect('in')
|
|
671
|
-
const rangeToken = this.expect('range_lit')
|
|
672
|
-
const range = this.parseRangeValue(rangeToken.value)
|
|
673
671
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
rangeToken
|
|
681
|
-
|
|
672
|
+
let start: Expr
|
|
673
|
+
let end: Expr
|
|
674
|
+
|
|
675
|
+
if (this.check('range_lit')) {
|
|
676
|
+
// Literal range: 0..10, 0..count, 0..=9
|
|
677
|
+
const rangeToken = this.advance()
|
|
678
|
+
const range = this.parseRangeValue(rangeToken.value)
|
|
679
|
+
start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken)
|
|
680
|
+
if (range.max !== null && range.max !== undefined) {
|
|
681
|
+
// Fully numeric: 0..10
|
|
682
|
+
end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken)
|
|
683
|
+
} else {
|
|
684
|
+
// Open-ended: "0.." — parse the end expression from next tokens
|
|
685
|
+
end = this.parseUnaryExpr()
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
// Dynamic range: expr..expr (e.g. start..end) — not yet supported
|
|
689
|
+
// Fall back to: parse as int_lit 0..0 (safe default)
|
|
690
|
+
start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
691
|
+
end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
692
|
+
this.error('Dynamic range start requires a literal integer (e.g. 0..count)')
|
|
693
|
+
}
|
|
682
694
|
|
|
683
695
|
const body = this.parseBlock()
|
|
684
696
|
return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken)
|
|
@@ -1260,6 +1272,20 @@ export class Parser {
|
|
|
1260
1272
|
}
|
|
1261
1273
|
|
|
1262
1274
|
private parseLiteralExpr(): LiteralExpr {
|
|
1275
|
+
// Support negative literals: -5, -3.14
|
|
1276
|
+
if (this.check('-')) {
|
|
1277
|
+
this.advance()
|
|
1278
|
+
const token = this.peek()
|
|
1279
|
+
if (token.kind === 'int_lit') {
|
|
1280
|
+
this.advance()
|
|
1281
|
+
return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token)
|
|
1282
|
+
}
|
|
1283
|
+
if (token.kind === 'float_lit') {
|
|
1284
|
+
this.advance()
|
|
1285
|
+
return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token)
|
|
1286
|
+
}
|
|
1287
|
+
this.error('Expected number after unary -')
|
|
1288
|
+
}
|
|
1263
1289
|
const expr = this.parsePrimaryExpr()
|
|
1264
1290
|
if (
|
|
1265
1291
|
expr.kind === 'int_lit' ||
|