optipack 0.0.0-stage → 1.0.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 (148) hide show
  1. package/BENCHMARKING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -2
  4. package/bin/optipack.js +12 -0
  5. package/example/manifest.json +27 -0
  6. package/example/scripts/drops.js +24 -0
  7. package/example/scripts/main.js +67 -0
  8. package/example/scripts/util.js +30 -0
  9. package/package.json +34 -4
  10. package/src/bench.js +231 -0
  11. package/src/cli.js +245 -0
  12. package/src/config.js +141 -0
  13. package/src/diff.js +108 -0
  14. package/src/harness/driver.js +216 -0
  15. package/src/harness/loader-hooks.mjs +63 -0
  16. package/src/harness/mock/@minecraft/common.mjs +17 -0
  17. package/src/harness/mock/@minecraft/debug-utilities.mjs +10 -0
  18. package/src/harness/mock/@minecraft/diagnostics.mjs +14 -0
  19. package/src/harness/mock/@minecraft/server-admin.mjs +14 -0
  20. package/src/harness/mock/@minecraft/server-editor.mjs +39 -0
  21. package/src/harness/mock/@minecraft/server-extras.mjs +538 -0
  22. package/src/harness/mock/@minecraft/server-gametest.mjs +22 -0
  23. package/src/harness/mock/@minecraft/server-graphics.mjs +21 -0
  24. package/src/harness/mock/@minecraft/server-net.mjs +15 -0
  25. package/src/harness/mock/@minecraft/server-ui.mjs +125 -0
  26. package/src/harness/mock/@minecraft/server.mjs +394 -0
  27. package/src/harness/mock/permissive.mjs +71 -0
  28. package/src/hotpath.js +158 -0
  29. package/src/lint.js +150 -0
  30. package/src/pack-walker.js +50 -0
  31. package/src/pass-utils.js +419 -0
  32. package/src/passes/b1-cache-getter-chains.js +406 -0
  33. package/src/passes/b2-closure-lift.js +316 -0
  34. package/src/passes/b3-string-build.js +167 -0
  35. package/src/passes/b4-dynprop-hoist.js +177 -0
  36. package/src/passes/b5-null-chain-flatten.js +137 -0
  37. package/src/passes/index.js +43 -0
  38. package/src/passes/r1-cross-tick-cache.js +192 -0
  39. package/src/passes/r2-json-fold.js +201 -0
  40. package/src/passes/r3-try-catch-strip.js +61 -0
  41. package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
  42. package/src/passes/s1-const-fold.js +130 -0
  43. package/src/passes/s2-dead-code.js +183 -0
  44. package/src/passes/s3-bool-simplify.js +82 -0
  45. package/src/passes/s4-for-of-to-indexed.js +140 -0
  46. package/src/passes/s5-array-methods-to-loops.js +357 -0
  47. package/src/passes/s6-destructure-to-direct.js +186 -0
  48. package/src/passes/s7-hoist-invariant-members.js +327 -0
  49. package/src/pipeline.js +186 -0
  50. package/src/pragma.js +115 -0
  51. package/src/report.js +98 -0
  52. package/test/array-methods-to-loops/expected.js +58 -0
  53. package/test/array-methods-to-loops/expects.json +6 -0
  54. package/test/array-methods-to-loops/input.js +23 -0
  55. package/test/array-methods-to-loops/scenario.json +22 -0
  56. package/test/array-methods-to-loops-bail/expected.js +10 -0
  57. package/test/array-methods-to-loops-bail/input.js +24 -0
  58. package/test/bool-simplify/expected.js +14 -0
  59. package/test/bool-simplify/expects.json +11 -0
  60. package/test/bool-simplify/input.js +18 -0
  61. package/test/cache-getter-chains/expected.js +20 -0
  62. package/test/cache-getter-chains/expects.json +11 -0
  63. package/test/cache-getter-chains/input.js +20 -0
  64. package/test/cache-getter-chains/scenario.json +35 -0
  65. package/test/closure-lift/expected.js +58 -0
  66. package/test/closure-lift/expects.json +1 -0
  67. package/test/closure-lift/input.js +50 -0
  68. package/test/closure-lift/scenario.json +9 -0
  69. package/test/const-fold/expected.js +12 -0
  70. package/test/const-fold/expects.json +1 -0
  71. package/test/const-fold/input.js +12 -0
  72. package/test/const-fold-bail/expected.js +7 -0
  73. package/test/const-fold-bail/expects.json +3 -0
  74. package/test/const-fold-bail/input.js +7 -0
  75. package/test/cross-tick-cache/config.json +1 -0
  76. package/test/cross-tick-cache/expected.js +38 -0
  77. package/test/cross-tick-cache/expects.json +6 -0
  78. package/test/cross-tick-cache/input.js +13 -0
  79. package/test/cross-tick-cache/scenario.json +33 -0
  80. package/test/dead-code/expected.js +14 -0
  81. package/test/dead-code/expects.json +5 -0
  82. package/test/dead-code/input.js +23 -0
  83. package/test/destructure-to-direct/expected.js +37 -0
  84. package/test/destructure-to-direct/expects.json +11 -0
  85. package/test/destructure-to-direct/input.js +28 -0
  86. package/test/destructure-to-direct/scenario.json +22 -0
  87. package/test/destructure-to-direct-bail/expected.js +36 -0
  88. package/test/destructure-to-direct-bail/expects.json +11 -0
  89. package/test/destructure-to-direct-bail/input.js +25 -0
  90. package/test/differential.test.mjs +267 -0
  91. package/test/dynprop-hoist/expected.js +24 -0
  92. package/test/dynprop-hoist/expects.json +11 -0
  93. package/test/dynprop-hoist/input.js +25 -0
  94. package/test/dynprop-hoist/scenario.json +24 -0
  95. package/test/example-report-snapshot.json +107 -0
  96. package/test/example.test.mjs +115 -0
  97. package/test/fetch-corpus.mjs +92 -0
  98. package/test/for-of-to-indexed/expected.js +59 -0
  99. package/test/for-of-to-indexed/expects.json +11 -0
  100. package/test/for-of-to-indexed/input.js +43 -0
  101. package/test/for-of-to-indexed-bail/expected.js +25 -0
  102. package/test/for-of-to-indexed-bail/expects.json +11 -0
  103. package/test/for-of-to-indexed-bail/input.js +29 -0
  104. package/test/gen-expected.mjs +33 -0
  105. package/test/getter-cache-cross-barrier/config.json +1 -0
  106. package/test/getter-cache-cross-barrier/expected.js +10 -0
  107. package/test/getter-cache-cross-barrier/expects.json +6 -0
  108. package/test/getter-cache-cross-barrier/input.js +11 -0
  109. package/test/getter-cache-cross-barrier/scenario.json +28 -0
  110. package/test/harness/fixture.js +117 -0
  111. package/test/hoist-invariant-members/expected.js +37 -0
  112. package/test/hoist-invariant-members/expects.json +11 -0
  113. package/test/hoist-invariant-members/input.js +34 -0
  114. package/test/hoist-invariant-members/scenario.json +35 -0
  115. package/test/hoist-invariant-members-bail/expected.js +19 -0
  116. package/test/hoist-invariant-members-bail/expects.json +5 -0
  117. package/test/hoist-invariant-members-bail/input.js +22 -0
  118. package/test/infra.test.mjs +270 -0
  119. package/test/json-fold/config.json +1 -0
  120. package/test/json-fold/expected.js +14 -0
  121. package/test/json-fold/expects.json +12 -0
  122. package/test/json-fold/input.js +17 -0
  123. package/test/json-fold-bail/config.json +1 -0
  124. package/test/json-fold-bail/expected.js +10 -0
  125. package/test/json-fold-bail/expects.json +5 -0
  126. package/test/json-fold-bail/input.js +14 -0
  127. package/test/null-chain-flatten/expected.js +22 -0
  128. package/test/null-chain-flatten/expects.json +5 -0
  129. package/test/null-chain-flatten/input.js +22 -0
  130. package/test/null-chain-flatten/scenario.json +24 -0
  131. package/test/server-ui-forms/expected.js +15 -0
  132. package/test/server-ui-forms/expects.json +1 -0
  133. package/test/server-ui-forms/input.js +25 -0
  134. package/test/server-ui-forms/scenario.json +1 -0
  135. package/test/string-build/expected.js +29 -0
  136. package/test/string-build/expects.json +5 -0
  137. package/test/string-build/input.js +29 -0
  138. package/test/string-build/scenario.json +27 -0
  139. package/test/string-build-bail/expected.js +10 -0
  140. package/test/string-build-bail/input.js +21 -0
  141. package/test/transforms.test.mjs +43 -0
  142. package/test/try-catch-strip/config.json +1 -0
  143. package/test/try-catch-strip/expected.js +57 -0
  144. package/test/try-catch-strip/expects.json +1 -0
  145. package/test/try-catch-strip/input.js +34 -0
  146. package/test/try-catch-strip/scenario.json +28 -0
  147. package/test/update-example-snapshot.mjs +19 -0
  148. package/test/update-mock-stubs.mjs +91 -0
@@ -0,0 +1,34 @@
1
+ // S7: loop-invariant member hoisting inside hot functions.
2
+ import { world, system } from '@minecraft/server';
3
+
4
+ // Before-loop hoist: player.location read once instead of 10 times.
5
+ function onTick(player) {
6
+ let total = 0;
7
+ for (let i = 0; i < 10; i++) {
8
+ total += player.location.y;
9
+ }
10
+ world.setDynamicProperty('total', total);
11
+ }
12
+
13
+ // Two chains, each used twice per iteration: both hoisted.
14
+ function onTick2(chest) {
15
+ let dx = 0;
16
+ for (let i = 0; i < 5; i++) {
17
+ dx += chest.location.x + chest.location.y;
18
+ }
19
+ world.setDynamicProperty('dx', dx);
20
+ }
21
+
22
+ // Loop body with a call: barrier, bails.
23
+ function onTick3(player) {
24
+ let total = 0;
25
+ for (let i = 0; i < 4; i++) {
26
+ total += player.location.y;
27
+ world.sendMessage('tick');
28
+ }
29
+ world.setDynamicProperty('total3', total);
30
+ }
31
+
32
+ system.runInterval(onTick, 20);
33
+ system.runInterval(onTick2, 20);
34
+ system.runInterval(onTick3, 20);
@@ -0,0 +1,35 @@
1
+ {
2
+ "reads": {
3
+ "location:p1": [
4
+ {
5
+ "y": 64,
6
+ "z": 2,
7
+ "x": 1
8
+ }
9
+ ],
10
+ "location:p2": [
11
+ {
12
+ "y": 70,
13
+ "z": 4,
14
+ "x": 3
15
+ }
16
+ ]
17
+ },
18
+ "entry": "scripts/input.js",
19
+ "steps": [
20
+ {
21
+ "n": 5,
22
+ "op": "tick"
23
+ }
24
+ ],
25
+ "state": {
26
+ "players": [
27
+ {
28
+ "id": "p1"
29
+ },
30
+ {
31
+ "id": "p2"
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,19 @@
1
+ // S7 bails: loop-variable roots, computed members, reassignment.
2
+ import { world, system } from '@minecraft/server';
3
+ function loopVar(chests) {
4
+ let total = 0;
5
+ for (let i = 0; i < chests.length; i++) {
6
+ total += chests[i].location.y;
7
+ }
8
+ world.setDynamicProperty('t', total);
9
+ }
10
+ function reassigned(player) {
11
+ let total = 0;
12
+ for (let i = 0; i < 4; i++) {
13
+ total += player.location.y;
14
+ }
15
+ player = null;
16
+ world.setDynamicProperty('t', total);
17
+ }
18
+ system.runInterval(() => loopVar([1, 2]), 20);
19
+ system.runInterval(() => reassigned(world.getAllPlayers()[0]), 20);
@@ -0,0 +1,5 @@
1
+ {
2
+ "transforms": {
3
+ "hoist-invariant-members": 0
4
+ }
5
+ }
@@ -0,0 +1,22 @@
1
+ // S7 bails: loop-variable roots, computed members, reassignment.
2
+ import { world, system } from '@minecraft/server';
3
+
4
+ function loopVar(chests) {
5
+ let total = 0;
6
+ for (let i = 0; i < chests.length; i++) {
7
+ total += chests[i].location.y;
8
+ }
9
+ world.setDynamicProperty('t', total);
10
+ }
11
+
12
+ function reassigned(player) {
13
+ let total = 0;
14
+ for (let i = 0; i < 4; i++) {
15
+ total += player.location.y;
16
+ }
17
+ player = null;
18
+ world.setDynamicProperty('t', total);
19
+ }
20
+
21
+ system.runInterval(() => loopVar([1, 2]), 20);
22
+ system.runInterval(() => reassigned(world.getAllPlayers()[0]), 20);
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Infrastructure tests: pragmas, config handling, CLI behaviors, determinism.
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { execFileSync, spawnSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ const root = path.join(here, '..');
14
+ const CLI = path.join(root, 'bin', 'optipack.js');
15
+
16
+ const { normalizeConfig, loadConfigFile, ConfigError } = await import('../src/config.js');
17
+ const { transformSource, runPack } = await import('../src/pipeline.js');
18
+
19
+ function mkPack(files) {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-infra-'));
21
+ for (const [rel, content] of Object.entries(files)) {
22
+ const abs = path.join(dir, rel);
23
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
24
+ fs.writeFileSync(abs, content);
25
+ }
26
+ return dir;
27
+ }
28
+
29
+ const cfg = (over = {}) => normalizeConfig({ level: 'balanced', ...over });
30
+
31
+ test('config: normalize + validation', () => {
32
+ assert.equal(cfg().level, 'balanced');
33
+ assert.equal(cfg({ level: 'risky' }).level, 'risky');
34
+ assert.equal(cfg({ hotPath: { transitiveDepth: 3 } }).hotPath.transitiveDepth, 3);
35
+ assert.throws(() => normalizeConfig({ level: 'nope' }), ConfigError);
36
+ assert.throws(() => normalizeConfig({ transforms: { 'x': 1 } }), ConfigError);
37
+ assert.throws(() => normalizeConfig({ hotPath: { transitiveDepth: -1 } }), ConfigError);
38
+ assert.throws(() => normalizeConfig({ unknownKey: true }), ConfigError);
39
+ });
40
+
41
+ test('config: file loading with BOM + explicit path', () => {
42
+ const dir = mkPack({ 'optipack.config.json': '\uFEFF{"level":"safe","transforms":{"json-fold":false}}' });
43
+ const { config } = loadConfigFile(path.join(dir, 'optipack.config.json'));
44
+ assert.equal(config.level, 'safe');
45
+ assert.equal(config.transforms['json-fold'], false);
46
+ assert.throws(() => loadConfigFile(path.join(dir, 'nope.json')), ConfigError);
47
+ });
48
+ test('pragma: file-level optipack: off disables everything', () => {
49
+ const src = [
50
+ '// optipack: off',
51
+ "export const a = 1 + 2;",
52
+ "export function f() {",
53
+ " let s = '';",
54
+ " for (const x of ['a']) { s += x; }",
55
+ " return s;",
56
+ '}',
57
+ ].join('\n');
58
+ const { code, fileReport } = transformSource('t.js', src, cfg());
59
+ assert.equal(code, src);
60
+ assert.deepEqual(fileReport.transforms, {});
61
+ });
62
+
63
+ test('pragma: optipack-disable on the line before a statement', () => {
64
+ const src = [
65
+ '// const-fold disabled for this declaration only',
66
+ '// optipack-disable const-fold',
67
+ 'export const a = 1 + 2;',
68
+ 'export const b = 3 + 4;',
69
+ ].join('\n');
70
+ const { code, fileReport } = transformSource('t.js', src, cfg());
71
+ assert.ok(code.includes('export const a = 1 + 2;'), 'disabled line unchanged');
72
+ assert.ok(code.includes('export const b = 7;'), 'enabled line folded');
73
+ assert.equal(fileReport.transforms['const-fold'], 1);
74
+ });
75
+
76
+ test('pragma: optipack-disable all', () => {
77
+ const src = [
78
+ 'export function f() {',
79
+ ' // optipack-disable all',
80
+ ' return 1 + 2;',
81
+ '}',
82
+ 'export const g = 2 + 3;',
83
+ ].join('\n');
84
+ const { code, fileReport } = transformSource('t.js', src, cfg());
85
+ assert.ok(code.includes('return 1 + 2;'), 'disabled return unchanged');
86
+ assert.ok(code.includes('export const g = 5;'), 'other code folded');
87
+ assert.equal(fileReport.transforms['const-fold'], 1);
88
+ });
89
+
90
+ test('pragma: optipack-hot force-marks a function', () => {
91
+ const src = [
92
+ "import { world } from '@minecraft/server';",
93
+ '// optipack-hot',
94
+ 'function notRegistered(player) {',
95
+ ' const a = player.location.x;',
96
+ ' const b = player.location.x;',
97
+ ' world.sendMessage(String(a + b));',
98
+ '}',
99
+ ].join('\n');
100
+ const { fileReport } = transformSource('t.js', src, cfg());
101
+ // Hot-marked -> B1 applies even without registration.
102
+ assert.ok(fileReport.transforms['cache-getter-chains'] >= 1, 'B1 applied to pragma-hot function');
103
+ });
104
+
105
+ test('pipeline: determinism — same input produces byte-identical output', () => {
106
+ const src = [
107
+ "import { world, system } from '@minecraft/server';",
108
+ 'function onTick() {',
109
+ " const tags = ['a', 'b'].filter((t) => t.length > 0);",
110
+ ' let s = 0;',
111
+ ' for (const t of tags) { s += t.length; }',
112
+ " world.sendMessage('' + s);",
113
+ '}',
114
+ 'system.runInterval(onTick, 20);',
115
+ ].join('\n');
116
+ const r1 = transformSource('t.js', src, cfg());
117
+ const r2 = transformSource('t.js', src, cfg());
118
+ assert.equal(r1.code, r2.code);
119
+ });
120
+
121
+ test('pipeline: output round-trips as ESM', () => {
122
+ const src = [
123
+ "import { world } from '@minecraft/server';",
124
+ 'export class Thing { #n = 1; get n() { return this.#n; } }',
125
+ 'export const arrow = async (x) => x ?? 0;',
126
+ ].join('\n');
127
+ const { code } = transformSource('t.js', src, cfg());
128
+ assert.ok(code.includes('class Thing'), 'modern syntax preserved');
129
+ });
130
+
131
+ test('pipeline: parse failure reports file and line', () => {
132
+ assert.throws(() => transformSource('scripts/bad.js', 'function {{{', cfg()), (e) => {
133
+ assert.equal(e.code, 2);
134
+ assert.ok(e.message.includes('scripts/bad.js'), 'file name in message');
135
+ assert.match(e.message, /:\d+:\d+/, 'line:column in message');
136
+ return true;
137
+ });
138
+ });
139
+
140
+ test('pack walker: non-script files copied verbatim, manifest untouched', () => {
141
+ const input = mkPack({
142
+ 'manifest.json': '{"format_version": 2, "header": {"name": "keep"}}',
143
+ 'scripts/main.js': 'export const a = 1 + 2;\n',
144
+ 'functions/tick.mcfunction': 'say hi\n',
145
+ 'texts/en_US.lang': 'item.name=Thing\n',
146
+ 'textures/foo.png': 'not-really-a-png',
147
+ 'scripts/nested/sub.js': 'const b = 2 + 2;\n',
148
+ });
149
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'pack');
150
+ const report = runPack(input, out, cfg());
151
+ assert.equal(fs.readFileSync(path.join(out, 'manifest.json'), 'utf8'), '{"format_version": 2, "header": {"name": "keep"}}');
152
+ assert.equal(fs.readFileSync(path.join(out, 'functions/tick.mcfunction'), 'utf8'), 'say hi\n');
153
+ assert.equal(fs.readFileSync(path.join(out, 'textures/foo.png'), 'utf8'), 'not-really-a-png');
154
+ assert.equal(fs.readFileSync(path.join(out, 'texts/en_US.lang'), 'utf8'), 'item.name=Thing\n');
155
+ const mainOut = fs.readFileSync(path.join(out, 'scripts/main.js'), 'utf8');
156
+ assert.ok(mainOut.includes('3'), 'script transformed');
157
+ assert.ok(fs.existsSync(path.join(out, 'scripts/nested/sub.js')));
158
+ assert.equal(report.files.length, 2);
159
+ });
160
+
161
+ test('CLI: exit 0 on the example pack, report written, manifest identical', () => {
162
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-cli-')), 'out');
163
+ const res = spawnSync(process.execPath, [CLI, path.join(root, 'example'), out, '--report', path.join(out, 'report.json')], { encoding: 'utf8' });
164
+ assert.equal(res.status, 0, res.stderr);
165
+ assert.ok(fs.existsSync(path.join(out, 'manifest.json')));
166
+ assert.ok(fs.existsSync(path.join(out, 'report.json')));
167
+ assert.ok(fs.existsSync(path.join(out, 'scripts/main.js')));
168
+ });
169
+
170
+ test('CLI: exit 1 on usage errors', () => {
171
+ const res1 = spawnSync(process.execPath, [CLI], { encoding: 'utf8' });
172
+ assert.equal(res1.status, 1);
173
+ const res2 = spawnSync(process.execPath, [CLI, 'Z:/definitely/missing/dir', 'out'], { encoding: 'utf8' });
174
+ assert.equal(res2.status, 1);
175
+ const res3 = spawnSync(process.execPath, [CLI, path.join(root, 'example'), 'out', '--level', 'bogus'], { encoding: 'utf8' });
176
+ assert.equal(res3.status, 1);
177
+ const res4 = spawnSync(process.execPath, [CLI, path.join(root, 'example'), 'out', '--only', 'no-such-transform'], { encoding: 'utf8' });
178
+ assert.equal(res4.status, 1);
179
+ });
180
+
181
+ test('CLI: exit 2 on parse failure with file+line in stderr', () => {
182
+ const input = mkPack({
183
+ 'manifest.json': '{}',
184
+ 'scripts/broken.js': 'function broken( {\n return 1;\n}\n',
185
+ });
186
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'out');
187
+ const res = spawnSync(process.execPath, [CLI, input, out], { encoding: 'utf8' });
188
+ assert.equal(res.status, 2);
189
+ assert.ok(res.stderr.includes('scripts/broken.js'), res.stderr);
190
+ assert.match(res.stderr, /:\d+:/, 'line number present');
191
+ });
192
+
193
+ test('CLI: --dry-run writes nothing', () => {
194
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'out');
195
+ const res = spawnSync(process.execPath, [CLI, path.join(root, 'example'), out, '--dry-run'], { encoding: 'utf8' });
196
+ assert.equal(res.status, 0, res.stderr);
197
+ assert.ok(res.stdout.includes('"level"'), 'report printed to stdout');
198
+ assert.equal(fs.existsSync(out), false, 'no output directory created');
199
+ });
200
+
201
+ test('CLI: --only restricts to the named transforms', () => {
202
+ const input = mkPack({ 'scripts/main.js': "export const a = 1 + 2;\nexport const b = 'x' + 'y';\n" });
203
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'out');
204
+ const res = spawnSync(process.execPath, [CLI, input, out, '--only', 'for-of-to-indexed', '--report', path.join(out, 'r.json')], { encoding: 'utf8' });
205
+ assert.equal(res.status, 0, res.stderr);
206
+ const outCode = fs.readFileSync(path.join(out, 'scripts/main.js'), 'utf8');
207
+ assert.ok(outCode.includes('1 + 2'), 'const-fold excluded');
208
+ });
209
+
210
+ test('CLI: --except disables a transform', () => {
211
+ const input = mkPack({ 'scripts/main.js': "export const a = 1 + 2;\n" });
212
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'out');
213
+ const res = spawnSync(process.execPath, [CLI, input, out, '--except', 'const-fold', '--report', path.join(out, 'r.json')], { encoding: 'utf8' });
214
+ assert.equal(res.status, 0, res.stderr);
215
+ assert.ok(fs.readFileSync(path.join(out, 'scripts/main.js'), 'utf8').includes('1 + 2'));
216
+ });
217
+
218
+ test('CLI: --diff prints unified diffs', () => {
219
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'out');
220
+ const res = spawnSync(process.execPath, [CLI, path.join(root, 'example'), out, '--diff', '--report', path.join(out, 'r.json')], { encoding: 'utf8' });
221
+ assert.equal(res.status, 0, res.stderr);
222
+ assert.ok(res.stdout.includes('--- a/scripts/main.js'), 'diff header present');
223
+ assert.ok(res.stdout.includes('@@'), 'hunk headers present');
224
+ });
225
+
226
+ test('CLI: --level safe skips balanced passes', () => {
227
+ const input = mkPack({
228
+ 'scripts/main.js': [
229
+ "import { world, system } from '@minecraft/server';",
230
+ 'function onTick(player) {',
231
+ ' const a = player.location.x;',
232
+ ' const b = player.location.x;',
233
+ " world.sendMessage('' + (a + b));",
234
+ '}',
235
+ 'system.runInterval(onTick, 20);',
236
+ ].join('\n'),
237
+ });
238
+ const outSafe = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-out-')), 'safe');
239
+ spawnSync(process.execPath, [CLI, input, outSafe, '--level', 'safe', '--report', path.join(outSafe, 'r.json')], { encoding: 'utf8' });
240
+ const safeCode = fs.readFileSync(path.join(outSafe, 'scripts/main.js'), 'utf8');
241
+ assert.ok(safeCode.includes('player.location.x'), 'B1 did not run at safe level');
242
+ });
243
+
244
+ test('CLI: bench reports boundary crossings and identical effects', () => {
245
+ // Use the example pack with the server-ui-forms scenario (players p1 ready).
246
+ const scenarioPath = path.join(root, 'test', 'server-ui-forms', 'scenario.json');
247
+ const res = spawnSync(process.execPath, [CLI, 'bench', path.join(root, 'example'), '--ticks', '40', '--scenario', scenarioPath], { encoding: 'utf8' });
248
+ assert.equal(res.status, 0, res.stderr);
249
+ assert.match(res.stdout, /boundary\.location/, 'crossing table present');
250
+ assert.match(res.stdout, /transforms applied: \d+/, 'transform count present');
251
+ assert.match(res.stdout, /original/, 'A/B columns present');
252
+ });
253
+
254
+ test('CLI: risky passes only run at risky level or explicit config', () => {
255
+ const src = [
256
+ "import { world, system } from '@minecraft/server';",
257
+ "const CFG = JSON.parse('{\"n\":1}');",
258
+ 'function onTick(player) {',
259
+ " world.sendMessage('' + CFG.n + player.id);",
260
+ '}',
261
+ 'system.runInterval(onTick, 20);',
262
+ ].join('\n');
263
+ const balanced = transformSource('t.js', src, cfg({ level: 'balanced' }));
264
+ assert.ok(balanced.code.includes('JSON.parse'), 'R2 did not run at balanced');
265
+ const risky = transformSource('t.js', src, cfg({ level: 'risky' }));
266
+ assert.ok(risky.code.includes('_opJson'), 'R2 ran at risky level');
267
+ assert.ok(risky.fileReport.warnings >= 1, 'R2 emitted a warning');
268
+ const explicit = transformSource('t.js', src, cfg({ level: 'safe', transforms: { 'json-fold': true } }));
269
+ assert.ok(explicit.code.includes('_opJson'), 'R2 ran via explicit config');
270
+ });
@@ -0,0 +1 @@
1
+ {"level": "risky"}
@@ -0,0 +1,14 @@
1
+ // R2: JSON.parse on a constant string folded to a module const (risky).
2
+ import { world } from '@minecraft/server';
3
+ const _opJson = JSON.parse('{"region":"eu","retries":3,"features":["a","b"]}');
4
+ export function describeConfig() {
5
+ return _opJson.region + ':' + _opJson.retries + ':' + _opJson.features.length;
6
+ }
7
+
8
+ // Reassignment of the parsed object: bails.
9
+ const MUT = JSON.parse('{"n":1}');
10
+ export function bump() {
11
+ MUT.n = 2;
12
+ return MUT.n;
13
+ }
14
+ world.sendMessage(describeConfig());
@@ -0,0 +1,12 @@
1
+ {
2
+ "bails": [
3
+ {
4
+ "transform": "R2",
5
+ "reasonContains": "property of the parsed object is written"
6
+ }
7
+ ],
8
+ "warnings": 1,
9
+ "transforms": {
10
+ "json-fold": 1
11
+ }
12
+ }
@@ -0,0 +1,17 @@
1
+ // R2: JSON.parse on a constant string folded to a module const (risky).
2
+ import { world } from '@minecraft/server';
3
+
4
+ const CONFIG = JSON.parse('{"region":"eu","retries":3,"features":["a","b"]}');
5
+
6
+ export function describeConfig() {
7
+ return CONFIG.region + ':' + CONFIG.retries + ':' + CONFIG.features.length;
8
+ }
9
+
10
+ // Reassignment of the parsed object: bails.
11
+ const MUT = JSON.parse('{"n":1}');
12
+ export function bump() {
13
+ MUT.n = 2;
14
+ return MUT.n;
15
+ }
16
+
17
+ world.sendMessage(describeConfig());
@@ -0,0 +1 @@
1
+ {"level": "risky"}
@@ -0,0 +1,10 @@
1
+ // R2 bail: JSON.parse result is mutated and passed around.
2
+ import { world } from '@minecraft/server';
3
+ const CFG = JSON.parse('{"n":1}');
4
+ export function passAround() {
5
+ return handle(CFG);
6
+ }
7
+ function handle(c) {
8
+ return c.n;
9
+ }
10
+ world.sendMessage(passAround());
@@ -0,0 +1,5 @@
1
+ {
2
+ "transforms": {
3
+ "json-fold": 0
4
+ }
5
+ }
@@ -0,0 +1,14 @@
1
+ // R2 bail: JSON.parse result is mutated and passed around.
2
+ import { world } from '@minecraft/server';
3
+
4
+ const CFG = JSON.parse('{"n":1}');
5
+
6
+ export function passAround() {
7
+ return handle(CFG);
8
+ }
9
+
10
+ function handle(c) {
11
+ return c.n;
12
+ }
13
+
14
+ world.sendMessage(passAround());
@@ -0,0 +1,22 @@
1
+ // B5: optional chains flattened after proven guards.
2
+ import { world, system } from '@minecraft/server';
3
+ function onTick(player) {
4
+ if (!player) return;
5
+ world.sendMessage('n=' + player.name);
6
+ const loc = player?.location;
7
+ world.sendMessage('y=' + (loc === undefined ? -1 : loc.y));
8
+
9
+ // Barrier (call) ends the guard window: `player?.name` below stays.
10
+ world.sendMessage('mid');
11
+ world.sendMessage('after=' + player?.name);
12
+ }
13
+
14
+ // Guard on an undefined-strictness check.
15
+ function withEqNull(ev) {
16
+ if (ev.player == null) return;
17
+ world.sendMessage('eq=' + ev.player?.name);
18
+ }
19
+ system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
20
+ system.runInterval(() => withEqNull({
21
+ player: null
22
+ }), 20);
@@ -0,0 +1,5 @@
1
+ {
2
+ "transforms": {
3
+ "null-chain-flatten": 1
4
+ }
5
+ }
@@ -0,0 +1,22 @@
1
+ // B5: optional chains flattened after proven guards.
2
+ import { world, system } from '@minecraft/server';
3
+
4
+ function onTick(player) {
5
+ if (!player) return;
6
+ world.sendMessage('n=' + player?.name);
7
+ const loc = player?.location;
8
+ world.sendMessage('y=' + (loc === undefined ? -1 : loc.y));
9
+
10
+ // Barrier (call) ends the guard window: `player?.name` below stays.
11
+ world.sendMessage('mid');
12
+ world.sendMessage('after=' + player?.name);
13
+ }
14
+
15
+ // Guard on an undefined-strictness check.
16
+ function withEqNull(ev) {
17
+ if (ev.player == null) return;
18
+ world.sendMessage('eq=' + ev.player?.name);
19
+ }
20
+
21
+ system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
22
+ system.runInterval(() => withEqNull({ player: null }), 20);
@@ -0,0 +1,24 @@
1
+ {
2
+ "reads": {
3
+ "name:p1": [
4
+ "Alice"
5
+ ]
6
+ },
7
+ "entry": "scripts/input.js",
8
+ "steps": [
9
+ {
10
+ "n": 5,
11
+ "op": "tick"
12
+ }
13
+ ],
14
+ "state": {
15
+ "players": [
16
+ {
17
+ "id": "p1"
18
+ },
19
+ {
20
+ "id": "p2"
21
+ }
22
+ ]
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ // Differential coverage for @minecraft/server-ui through the harness mock.
2
+ import { world, system } from '@minecraft/server';
3
+ import { ActionFormData, MessageFormData, uiManager } from '@minecraft/server-ui';
4
+ const _opLift = () => onTick(world.getAllPlayers()[0]);
5
+ function onTick(player) {
6
+ const menu = new ActionFormData().title('Main Menu').body('Choose an option').button('Play').button('Settings');
7
+ menu.show(player);
8
+ if (player.location.y > 64) {
9
+ const notice = new MessageFormData().title('Notice').body('You are high up').button1('OK').button2('Dismiss');
10
+ uiManager.showPlayer(player, notice);
11
+ }
12
+ }
13
+ world.afterEvents.worldLoad.subscribe(() => {
14
+ system.runInterval(_opLift, 1);
15
+ });
@@ -0,0 +1 @@
1
+ {"hotFunctions": 3, "transforms": {"closure-lift": 1}}
@@ -0,0 +1,25 @@
1
+ // Differential coverage for @minecraft/server-ui through the harness mock.
2
+ import { world, system } from '@minecraft/server';
3
+ import { ActionFormData, MessageFormData, uiManager } from '@minecraft/server-ui';
4
+
5
+ function onTick(player) {
6
+ const menu = new ActionFormData()
7
+ .title('Main Menu')
8
+ .body('Choose an option')
9
+ .button('Play')
10
+ .button('Settings');
11
+ menu.show(player);
12
+
13
+ if (player.location.y > 64) {
14
+ const notice = new MessageFormData()
15
+ .title('Notice')
16
+ .body('You are high up')
17
+ .button1('OK')
18
+ .button2('Dismiss');
19
+ uiManager.showPlayer(player, notice);
20
+ }
21
+ }
22
+
23
+ world.afterEvents.worldLoad.subscribe(() => {
24
+ system.runInterval(() => onTick(world.getAllPlayers()[0]), 1);
25
+ });
@@ -0,0 +1 @@
1
+ {"entry": "scripts/input.js", "reads": {"location:p1": [{"x": 1, "y": 70, "z": 2}, {"x": 2, "y": 60, "z": 3}]}, "state": {"players": [{"id": "p1"}]}, "steps": [{"op": "fire", "event": "world.afterEvents.worldLoad", "payload": {}}, {"op": "tick", "n": 10}]}
@@ -0,0 +1,29 @@
1
+ // B3: string accumulation in a hot loop -> parts + join.
2
+ import { world, system } from '@minecraft/server';
3
+ function onTick(players) {
4
+ let summary = '';
5
+ const _opParts = [];
6
+ for (let i = 0; i < players.length; i++) {
7
+ _opParts.push("" + (players[i].name + ';'));
8
+ }
9
+ summary = _opParts.join("");
10
+ world.sendMessage(summary);
11
+ }
12
+
13
+ // Written outside a loop: bails.
14
+ export function notALoop(players) {
15
+ let s = '';
16
+ s += 'x';
17
+ world.sendMessage(s);
18
+ }
19
+
20
+ // Read inside the loop: bails.
21
+ export function readInside(players) {
22
+ let s = '';
23
+ for (let i = 0; i < players.length; i++) {
24
+ s += players[i].name;
25
+ world.sendMessage(s);
26
+ }
27
+ world.sendMessage(s);
28
+ }
29
+ system.runInterval(() => onTick(world.getAllPlayers()), 20);
@@ -0,0 +1,5 @@
1
+ {
2
+ "transforms": {
3
+ "string-build": 1
4
+ }
5
+ }