optipack 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -2
- package/bin/optipack.js +12 -0
- package/example/manifest.json +27 -0
- package/example/scripts/drops.js +24 -0
- package/example/scripts/main.js +67 -0
- package/example/scripts/util.js +30 -0
- package/package.json +34 -6
- package/src/cli.js +180 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -0
- package/src/hotpath.js +158 -0
- package/src/lint.js +150 -0
- package/src/pack-walker.js +50 -0
- package/src/pass-utils.js +419 -0
- package/src/passes/b1-cache-getter-chains.js +406 -0
- package/src/passes/b2-closure-lift.js +316 -0
- package/src/passes/b3-string-build.js +167 -0
- package/src/passes/b4-dynprop-hoist.js +177 -0
- package/src/passes/b5-null-chain-flatten.js +137 -0
- package/src/passes/index.js +43 -0
- package/src/passes/r1-cross-tick-cache.js +192 -0
- package/src/passes/r2-json-fold.js +201 -0
- package/src/passes/r3-try-catch-strip.js +61 -0
- package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
- package/src/passes/s1-const-fold.js +130 -0
- package/src/passes/s2-dead-code.js +183 -0
- package/src/passes/s3-bool-simplify.js +82 -0
- package/src/passes/s4-for-of-to-indexed.js +140 -0
- package/src/passes/s5-array-methods-to-loops.js +357 -0
- package/src/passes/s6-destructure-to-direct.js +186 -0
- package/src/passes/s7-hoist-invariant-members.js +327 -0
- package/src/pipeline.js +186 -0
- package/src/pragma.js +115 -0
- package/src/report.js +98 -0
- package/test/array-methods-to-loops/expected.js +58 -0
- package/test/array-methods-to-loops/expects.json +6 -0
- package/test/array-methods-to-loops/input.js +23 -0
- package/test/array-methods-to-loops/scenario.json +22 -0
- package/test/array-methods-to-loops-bail/expected.js +10 -0
- package/test/array-methods-to-loops-bail/input.js +24 -0
- package/test/bool-simplify/expected.js +14 -0
- package/test/bool-simplify/expects.json +11 -0
- package/test/bool-simplify/input.js +18 -0
- package/test/cache-getter-chains/expected.js +20 -0
- package/test/cache-getter-chains/expects.json +11 -0
- package/test/cache-getter-chains/input.js +20 -0
- package/test/cache-getter-chains/scenario.json +35 -0
- package/test/closure-lift/expected.js +58 -0
- package/test/closure-lift/expects.json +1 -0
- package/test/closure-lift/input.js +50 -0
- package/test/closure-lift/scenario.json +9 -0
- package/test/const-fold/expected.js +12 -0
- package/test/const-fold/expects.json +1 -0
- package/test/const-fold/input.js +12 -0
- package/test/const-fold-bail/expected.js +7 -0
- package/test/const-fold-bail/expects.json +3 -0
- package/test/const-fold-bail/input.js +7 -0
- package/test/cross-tick-cache/config.json +1 -0
- package/test/cross-tick-cache/expected.js +38 -0
- package/test/cross-tick-cache/expects.json +6 -0
- package/test/cross-tick-cache/input.js +13 -0
- package/test/cross-tick-cache/scenario.json +33 -0
- package/test/dead-code/expected.js +14 -0
- package/test/dead-code/expects.json +5 -0
- package/test/dead-code/input.js +23 -0
- package/test/destructure-to-direct/expected.js +37 -0
- package/test/destructure-to-direct/expects.json +11 -0
- package/test/destructure-to-direct/input.js +28 -0
- package/test/destructure-to-direct/scenario.json +22 -0
- package/test/destructure-to-direct-bail/expected.js +36 -0
- package/test/destructure-to-direct-bail/expects.json +11 -0
- package/test/destructure-to-direct-bail/input.js +25 -0
- package/test/differential.test.mjs +267 -0
- package/test/dynprop-hoist/expected.js +24 -0
- package/test/dynprop-hoist/expects.json +11 -0
- package/test/dynprop-hoist/input.js +25 -0
- package/test/dynprop-hoist/scenario.json +24 -0
- package/test/example-report-snapshot.json +107 -0
- package/test/example.test.mjs +115 -0
- package/test/for-of-to-indexed/expected.js +59 -0
- package/test/for-of-to-indexed/expects.json +11 -0
- package/test/for-of-to-indexed/input.js +43 -0
- package/test/for-of-to-indexed-bail/expected.js +25 -0
- package/test/for-of-to-indexed-bail/expects.json +11 -0
- package/test/for-of-to-indexed-bail/input.js +29 -0
- package/test/gen-expected.mjs +33 -0
- package/test/getter-cache-cross-barrier/config.json +1 -0
- package/test/getter-cache-cross-barrier/expected.js +10 -0
- package/test/getter-cache-cross-barrier/expects.json +6 -0
- package/test/getter-cache-cross-barrier/input.js +11 -0
- package/test/getter-cache-cross-barrier/scenario.json +28 -0
- package/test/harness/fixture.js +117 -0
- package/test/harness/setup.js +198 -0
- package/test/hoist-invariant-members/expected.js +37 -0
- package/test/hoist-invariant-members/expects.json +11 -0
- package/test/hoist-invariant-members/input.js +34 -0
- package/test/hoist-invariant-members/scenario.json +35 -0
- package/test/hoist-invariant-members-bail/expected.js +19 -0
- package/test/hoist-invariant-members-bail/expects.json +5 -0
- package/test/hoist-invariant-members-bail/input.js +22 -0
- package/test/infra.test.mjs +260 -0
- package/test/json-fold/config.json +1 -0
- package/test/json-fold/expected.js +14 -0
- package/test/json-fold/expects.json +12 -0
- package/test/json-fold/input.js +17 -0
- package/test/json-fold-bail/config.json +1 -0
- package/test/json-fold-bail/expected.js +10 -0
- package/test/json-fold-bail/expects.json +5 -0
- package/test/json-fold-bail/input.js +14 -0
- package/test/loader-hooks.mjs +38 -0
- package/test/mock/@minecraft/server.mjs +274 -0
- package/test/null-chain-flatten/expected.js +22 -0
- package/test/null-chain-flatten/expects.json +5 -0
- package/test/null-chain-flatten/input.js +22 -0
- package/test/null-chain-flatten/scenario.json +24 -0
- package/test/string-build/expected.js +29 -0
- package/test/string-build/expects.json +5 -0
- package/test/string-build/input.js +29 -0
- package/test/string-build/scenario.json +27 -0
- package/test/string-build-bail/expected.js +10 -0
- package/test/string-build-bail/input.js +21 -0
- package/test/transforms.test.mjs +43 -0
- package/test/try-catch-strip/config.json +1 -0
- package/test/try-catch-strip/expected.js +57 -0
- package/test/try-catch-strip/expects.json +1 -0
- package/test/try-catch-strip/input.js +34 -0
- package/test/try-catch-strip/scenario.json +28 -0
- package/test/update-example-snapshot.mjs +19 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example pack end-to-end: run the CLI over example/, verify parse validity,
|
|
3
|
+
* manifest fidelity, effects-equivalence across the whole script graph, report
|
|
4
|
+
* snapshot, and determinism.
|
|
5
|
+
*/
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const root = path.join(here, '..');
|
|
16
|
+
const CLI = path.join(root, 'bin', 'optipack.js');
|
|
17
|
+
const EXAMPLE = path.join(root, 'example');
|
|
18
|
+
|
|
19
|
+
function mkOut(prefix) {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('example: CLI produces a valid optimized pack + report', async () => {
|
|
24
|
+
const out = mkOut('optipack-example-');
|
|
25
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out], { encoding: 'utf8' });
|
|
26
|
+
assert.equal(res.status, 0, res.stderr);
|
|
27
|
+
|
|
28
|
+
// Manifest and non-scripts copied verbatim.
|
|
29
|
+
assert.equal(
|
|
30
|
+
fs.readFileSync(path.join(out, 'manifest.json'), 'utf8'),
|
|
31
|
+
fs.readFileSync(path.join(EXAMPLE, 'manifest.json'), 'utf8')
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Report exists with the expected schema shape.
|
|
35
|
+
const report = JSON.parse(fs.readFileSync(path.join(out, 'optipack-report.json'), 'utf8'));
|
|
36
|
+
assert.equal(report.level, 'balanced');
|
|
37
|
+
assert.ok(Array.isArray(report.files) && report.files.length === 3);
|
|
38
|
+
assert.ok(report.files.every((f) => f.path.startsWith('scripts/')));
|
|
39
|
+
assert.ok(report.totals.transforms > 0, 'transforms applied');
|
|
40
|
+
for (const f of report.files) {
|
|
41
|
+
for (const key of ['path', 'hotFunctions', 'transforms', 'bails', 'lint', 'residualAllocs', 'bytes']) {
|
|
42
|
+
assert.ok(key in f, `report file has ${key}`);
|
|
43
|
+
}
|
|
44
|
+
assert.ok(f.bytes.after > 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Every output script re-parses as ESM.
|
|
48
|
+
const { parseSource } = await import('../src/pipeline.js');
|
|
49
|
+
for (const f of ['scripts/main.js', 'scripts/util.js', 'scripts/drops.js']) {
|
|
50
|
+
const src = fs.readFileSync(path.join(out, f), 'utf8');
|
|
51
|
+
assert.doesNotThrow(() => parseSource(f, src), `${f} round-trips`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('example: effects-equivalence across the whole script graph', async () => {
|
|
56
|
+
const out = mkOut('optipack-example-');
|
|
57
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out], { encoding: 'utf8' });
|
|
58
|
+
assert.equal(res.status, 0, res.stderr);
|
|
59
|
+
|
|
60
|
+
const { runVariant, assertSameEffects, pathToUrl } = await import('./harness/setup.js');
|
|
61
|
+
const scenario = {
|
|
62
|
+
reads: {
|
|
63
|
+
'location:p1': [{ x: 10, y: 64, z: 20 }, { x: 12, y: 65, z: 22 }],
|
|
64
|
+
'name:p1': ['Alex'],
|
|
65
|
+
'dp:p1:optipack:lastDist': [undefined],
|
|
66
|
+
'dp:p1:optipack:baseline': [undefined],
|
|
67
|
+
'dp:p1:optipack:drops': [undefined],
|
|
68
|
+
'world.dp:scan:0': [0],
|
|
69
|
+
'world.dp:scan:1': [0],
|
|
70
|
+
'world.dp:scan:2': [0],
|
|
71
|
+
'world.dp:scan:3': [0],
|
|
72
|
+
},
|
|
73
|
+
state: { players: [{ id: 'p1' }] },
|
|
74
|
+
steps: [
|
|
75
|
+
{ op: 'fire', event: 'world.afterEvents.worldLoad', payload: {} },
|
|
76
|
+
{ op: 'tick', n: 60 },
|
|
77
|
+
{ op: 'fire', event: 'world.afterEvents.playerSpawn', payload: { player: { __entity: 'p1' } } },
|
|
78
|
+
{ op: 'tick', n: 40 },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const a = await runVariant(pathToUrl(path.join(EXAMPLE, 'scripts', 'main.js')), 'orig', scenario);
|
|
83
|
+
const b = await runVariant(pathToUrl(path.join(out, 'scripts', 'main.js')), 'opt', scenario);
|
|
84
|
+
assertSameEffects(a, b, 'example pack');
|
|
85
|
+
assert.ok(a.effects.length > 10, 'scenario exercised real effects');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('example: report snapshot is stable', () => {
|
|
89
|
+
const out = mkOut('optipack-example-');
|
|
90
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out, '--report', path.join(out, 'r.json')], { encoding: 'utf8' });
|
|
91
|
+
assert.equal(res.status, 0, res.stderr);
|
|
92
|
+
const actual = fs.readFileSync(path.join(out, 'r.json'), 'utf8');
|
|
93
|
+
const snapshotPath = path.join(here, 'example-report-snapshot.json');
|
|
94
|
+
if (process.env.UPDATE_SNAPSHOT) {
|
|
95
|
+
fs.writeFileSync(snapshotPath, actual);
|
|
96
|
+
}
|
|
97
|
+
assert.ok(fs.existsSync(snapshotPath), 'snapshot exists (run npm run update-snapshot)');
|
|
98
|
+
const expected = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
|
|
99
|
+
const got = JSON.parse(actual);
|
|
100
|
+
assert.deepEqual(got, expected, 'report matches snapshot');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('example: deterministic output — two runs are byte-identical', () => {
|
|
104
|
+
const out1 = mkOut('optipack-det1-');
|
|
105
|
+
const out2 = mkOut('optipack-det2-');
|
|
106
|
+
spawnSync(process.execPath, [CLI, EXAMPLE, out1], { encoding: 'utf8' });
|
|
107
|
+
spawnSync(process.execPath, [CLI, EXAMPLE, out2], { encoding: 'utf8' });
|
|
108
|
+
for (const f of ['scripts/main.js', 'scripts/util.js', 'scripts/drops.js', 'optipack-report.json']) {
|
|
109
|
+
assert.equal(
|
|
110
|
+
fs.readFileSync(path.join(out1, f), 'utf8'),
|
|
111
|
+
fs.readFileSync(path.join(out2, f), 'utf8'),
|
|
112
|
+
`${f} identical across runs`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// S4: for-of conversion over provably-array iterables.
|
|
2
|
+
const names = ['a', 'b', 'c'];
|
|
3
|
+
export function sumLengths() {
|
|
4
|
+
let n = 0;
|
|
5
|
+
for (let _opI = 0; _opI < names.length; _opI++) {
|
|
6
|
+
const v = names[_opI];
|
|
7
|
+
{
|
|
8
|
+
n += v.length;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
export function letters() {
|
|
14
|
+
let s = 0;
|
|
15
|
+
{
|
|
16
|
+
const _opArr = 'hello'.split('');
|
|
17
|
+
for (let _opI2 = 0; _opI2 < _opArr.length; _opI2++) {
|
|
18
|
+
const ch = _opArr[_opI2];
|
|
19
|
+
{
|
|
20
|
+
if (ch === 'l') s += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
export function mapped() {
|
|
27
|
+
const vals = ['a', 'bb'].map(v => v + '!');
|
|
28
|
+
let out = 0;
|
|
29
|
+
for (let _opI3 = 0; _opI3 < vals.length; _opI3++) {
|
|
30
|
+
const v = vals[_opI3];
|
|
31
|
+
{
|
|
32
|
+
out += v.length;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function withDestructure(pairs) {
|
|
38
|
+
const entries = Object.entries({
|
|
39
|
+
a: 1,
|
|
40
|
+
b: 2
|
|
41
|
+
});
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (let _opI4 = 0; _opI4 < entries.length; _opI4++) {
|
|
44
|
+
const _opEl = entries[_opI4];
|
|
45
|
+
const k = _opEl[0];
|
|
46
|
+
const v = _opEl[1];
|
|
47
|
+
{
|
|
48
|
+
total += v;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return total;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Host returns are never converted (bail).
|
|
55
|
+
export function hostIter(players) {
|
|
56
|
+
for (const p of players) {
|
|
57
|
+
p.x = 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// S4: for-of conversion over provably-array iterables.
|
|
2
|
+
const names = ['a', 'b', 'c'];
|
|
3
|
+
|
|
4
|
+
export function sumLengths() {
|
|
5
|
+
let n = 0;
|
|
6
|
+
for (const v of names) {
|
|
7
|
+
n += v.length;
|
|
8
|
+
}
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function letters() {
|
|
13
|
+
let s = 0;
|
|
14
|
+
for (const ch of 'hello'.split('')) {
|
|
15
|
+
if (ch === 'l') s += 1;
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mapped() {
|
|
21
|
+
const vals = ['a', 'bb'].map((v) => v + '!');
|
|
22
|
+
let out = 0;
|
|
23
|
+
for (const v of vals) {
|
|
24
|
+
out += v.length;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function withDestructure(pairs) {
|
|
30
|
+
const entries = Object.entries({ a: 1, b: 2 });
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const [k, v] of entries) {
|
|
33
|
+
total += v;
|
|
34
|
+
}
|
|
35
|
+
return total;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Host returns are never converted (bail).
|
|
39
|
+
export function hostIter(players) {
|
|
40
|
+
for (const p of players) {
|
|
41
|
+
p.x = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// S4 bails: Map/Set/strings/host returns are never converted.
|
|
2
|
+
const m = new Map();
|
|
3
|
+
export function mapIter(m2) {
|
|
4
|
+
for (const [k, v] of m2) {
|
|
5
|
+
m.set(k, v);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function stringIter(s) {
|
|
9
|
+
let n = 0;
|
|
10
|
+
for (const ch of s) {
|
|
11
|
+
n += 1;
|
|
12
|
+
}
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
export function hostReturns(players) {
|
|
16
|
+
for (const p of worldPlayers()) {
|
|
17
|
+
p.ping = 1;
|
|
18
|
+
}
|
|
19
|
+
for (const p of players) {
|
|
20
|
+
p.pong = 2;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function worldPlayers() {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// S4 bails: Map/Set/strings/host returns are never converted.
|
|
2
|
+
const m = new Map();
|
|
3
|
+
|
|
4
|
+
export function mapIter(m2) {
|
|
5
|
+
for (const [k, v] of m2) {
|
|
6
|
+
m.set(k, v);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stringIter(s) {
|
|
11
|
+
let n = 0;
|
|
12
|
+
for (const ch of s) {
|
|
13
|
+
n += 1;
|
|
14
|
+
}
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hostReturns(players) {
|
|
19
|
+
for (const p of worldPlayers()) {
|
|
20
|
+
p.ping = 1;
|
|
21
|
+
}
|
|
22
|
+
for (const p of players) {
|
|
23
|
+
p.pong = 2;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function worldPlayers() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// One-off helper: generate expected.js for every fixture that lacks one.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const root = here; // fixtures live directly under test/
|
|
8
|
+
|
|
9
|
+
const { normalizeConfig } = await import('../src/config.js');
|
|
10
|
+
const { transformSource } = await import('../src/pipeline.js');
|
|
11
|
+
|
|
12
|
+
const dirs = fs.readdirSync(root, { withFileTypes: true })
|
|
13
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(root, e.name, 'input.js')))
|
|
14
|
+
.map((e) => e.name)
|
|
15
|
+
.sort();
|
|
16
|
+
|
|
17
|
+
const only = process.argv[2] || null;
|
|
18
|
+
for (const dir of dirs) {
|
|
19
|
+
if (only && dir !== only) continue;
|
|
20
|
+
const fdir = path.join(root, dir);
|
|
21
|
+
if (fs.existsSync(path.join(fdir, 'expected.js'))) continue;
|
|
22
|
+
const input = fs.readFileSync(path.join(fdir, 'input.js'), 'utf8');
|
|
23
|
+
const configPath = path.join(fdir, 'config.json');
|
|
24
|
+
const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : { level: 'balanced' };
|
|
25
|
+
const cfg = normalizeConfig(raw);
|
|
26
|
+
try {
|
|
27
|
+
const { code, fileReport } = transformSource('input.js', input, cfg);
|
|
28
|
+
fs.writeFileSync(path.join(fdir, 'expected.js'), code);
|
|
29
|
+
console.log(`${dir}: generated (${Object.keys(fileReport.transforms).join(', ') || 'no transforms'})`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(`${dir}: FAILED: ${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"level": "risky"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// R4: getter caching across call barriers (risky).
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(player) {
|
|
4
|
+
const _opC = player.location.x;
|
|
5
|
+
const before = _opC;
|
|
6
|
+
world.sendMessage('moving'); // call barrier — B1 would bail, R4 crosses it
|
|
7
|
+
const after = _opC;
|
|
8
|
+
world.sendMessage('moved=' + (after - before));
|
|
9
|
+
}
|
|
10
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// R4: getter caching across call barriers (risky).
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(player) {
|
|
5
|
+
const before = player.location.x;
|
|
6
|
+
world.sendMessage('moving'); // call barrier — B1 would bail, R4 crosses it
|
|
7
|
+
const after = player.location.x;
|
|
8
|
+
world.sendMessage('moved=' + (after - before));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reads": {
|
|
3
|
+
"location:p1": [
|
|
4
|
+
{
|
|
5
|
+
"y": 64,
|
|
6
|
+
"z": 2,
|
|
7
|
+
"x": 1
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"entry": "scripts/input.js",
|
|
12
|
+
"steps": [
|
|
13
|
+
{
|
|
14
|
+
"n": 5,
|
|
15
|
+
"op": "tick"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"state": {
|
|
19
|
+
"players": [
|
|
20
|
+
{
|
|
21
|
+
"id": "p1"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "p2"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture runner for per-transform unit tests.
|
|
3
|
+
*
|
|
4
|
+
* A fixture directory contains:
|
|
5
|
+
* input.js — source fed through the full pipeline
|
|
6
|
+
* expected.js — (optional) exact expected output; when absent the input
|
|
7
|
+
* must come out unchanged (bail fixtures)
|
|
8
|
+
* config.json — (optional) { level, transforms, hotPath, lint } user config
|
|
9
|
+
* expects.json — (optional) assertions on the report:
|
|
10
|
+
* { transforms: {id: count}, bails: [{transform, reasonContains}],
|
|
11
|
+
* lint: [{rule}], warnings: n }
|
|
12
|
+
* scenario.json— (optional) differential scenario { entry, reads, state, steps }
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
|
|
18
|
+
export async function runFixtureDir(dir) {
|
|
19
|
+
const inputPath = path.join(dir, 'input.js');
|
|
20
|
+
const input = fs.readFileSync(inputPath, 'utf8');
|
|
21
|
+
const expectedPath = path.join(dir, 'expected.js');
|
|
22
|
+
const hasExpected = fs.existsSync(expectedPath);
|
|
23
|
+
const expected = hasExpected ? fs.readFileSync(expectedPath, 'utf8') : null;
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(dir, 'config.json');
|
|
26
|
+
const config = fs.existsSync(configPath)
|
|
27
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, ''))
|
|
28
|
+
: { level: 'balanced' };
|
|
29
|
+
|
|
30
|
+
const expectsPath = path.join(dir, 'expects.json');
|
|
31
|
+
const expects = fs.existsSync(expectsPath) ? JSON.parse(fs.readFileSync(expectsPath, 'utf8')) : null;
|
|
32
|
+
|
|
33
|
+
const scenarioPath = path.join(dir, 'scenario.json');
|
|
34
|
+
const scenario = fs.existsSync(scenarioPath) ? JSON.parse(fs.readFileSync(scenarioPath, 'utf8')) : null;
|
|
35
|
+
|
|
36
|
+
const { normalizeConfig } = await import('../../src/config.js');
|
|
37
|
+
const { runPack } = await import('../../src/pipeline.js');
|
|
38
|
+
|
|
39
|
+
const inputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-fx-in-'));
|
|
40
|
+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-fx-out-'));
|
|
41
|
+
fs.mkdirSync(path.join(inputDir, 'scripts'), { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(inputDir, 'scripts', 'input.js'), input);
|
|
43
|
+
const cfg = normalizeConfig(config);
|
|
44
|
+
const report = runPack(inputDir, outputDir, cfg);
|
|
45
|
+
const output = fs.readFileSync(path.join(outputDir, 'scripts', 'input.js'), 'utf8');
|
|
46
|
+
|
|
47
|
+
const result = { input, output, expected, report, expects, scenario, outputDir, config: cfg };
|
|
48
|
+
|
|
49
|
+
// Exact output comparison.
|
|
50
|
+
const want = hasExpected ? expected : input;
|
|
51
|
+
const got = output.replace(/\r\n/g, '\n');
|
|
52
|
+
const wantNorm = want.replace(/\r\n/g, '\n');
|
|
53
|
+
if (got !== wantNorm) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`fixture output mismatch for ${path.basename(dir)}:\n` +
|
|
56
|
+
`--- expected ---\n${wantNorm}\n--- actual ---\n${got}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Report expectations.
|
|
61
|
+
if (expects) {
|
|
62
|
+
const file = report.files[0];
|
|
63
|
+
if (expects.transforms) {
|
|
64
|
+
for (const [id, count] of Object.entries(expects.transforms)) {
|
|
65
|
+
const gotCount = (file.transforms[id] || 0);
|
|
66
|
+
if (gotCount !== count) {
|
|
67
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${count} of ${id}, got ${gotCount}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (expects.noTransforms) {
|
|
72
|
+
const total = Object.values(file.transforms).reduce((a, b) => a + b, 0);
|
|
73
|
+
if (total !== 0) {
|
|
74
|
+
throw new Error(`fixture ${path.basename(dir)}: expected no transforms, got ${JSON.stringify(file.transforms)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (expects.bails) {
|
|
78
|
+
for (const wantBail of expects.bails) {
|
|
79
|
+
const found = file.bails.find((b) =>
|
|
80
|
+
b.transform === wantBail.transform &&
|
|
81
|
+
(wantBail.reasonContains === undefined || b.reason.includes(wantBail.reasonContains)) &&
|
|
82
|
+
(wantBail.line === undefined || b.line === wantBail.line));
|
|
83
|
+
if (!found) {
|
|
84
|
+
throw new Error(`fixture ${path.basename(dir)}: expected bail ${JSON.stringify(wantBail)}, have ${JSON.stringify(file.bails)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (expects.lint) {
|
|
89
|
+
for (const wantLint of expects.lint) {
|
|
90
|
+
const found = file.lint.find((l) => l.rule === wantLint.rule &&
|
|
91
|
+
(wantLint.line === undefined || l.line === wantLint.line));
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`fixture ${path.basename(dir)}: expected lint ${JSON.stringify(wantLint)}, have ${JSON.stringify(file.lint)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (expects.warnings !== undefined) {
|
|
98
|
+
if (report.warnings.length !== expects.warnings) {
|
|
99
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${expects.warnings} warnings, got ${report.warnings.length}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (expects.hotFunctions !== undefined) {
|
|
103
|
+
if (file.hotFunctions !== expects.hotFunctions) {
|
|
104
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${expects.hotFunctions} hot functions, got ${file.hotFunctions}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function fixtureDirs(baseDir) {
|
|
113
|
+
return fs.readdirSync(baseDir, { withFileTypes: true })
|
|
114
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(baseDir, e.name, 'input.js')))
|
|
115
|
+
.map((e) => path.join(baseDir, e.name))
|
|
116
|
+
.sort();
|
|
117
|
+
}
|