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,127 @@
1
+ # Benchmarking optipack
2
+
3
+ Two complementary ways to measure what optipack actually buys you.
4
+
5
+ ## 1. Automated A/B (`optipack bench`) — engine-independent crossing counts
6
+
7
+ ```bash
8
+ optipack bench <packDir> [--ticks N] [--scenario file.json] [--level safe|balanced|risky] [--json out.json]
9
+ ```
10
+
11
+ Builds the optimized pack, then runs the **original** and **optimized** scripts
12
+ side by side under the harness mock for N simulated ticks with identical
13
+ scenario input, and reports:
14
+
15
+ - **Host-boundary crossings by kind** — the number of `player.location` reads,
16
+ `getComponent()` calls, `getDynamicProperty()` calls, `getEntities()` sweeps,
17
+ etc. This is the metric optipack exists to reduce, and it is
18
+ **engine-independent**: a crossing is a crossing in V8 (the test harness) or
19
+ QuickJS (the game).
20
+ - **Wall time per tick** — *indicative only*. The harness runs in Node (V8, with
21
+ a JIT); Minecraft runs QuickJS (interpreter, no JIT). Use the in-game
22
+ workflow below for authoritative numbers.
23
+ - **Effect-trace equality** — if the two builds diverge behaviorally, the bench
24
+ is flagged invalid (`exit code 1`).
25
+
26
+ Example (real output over 147 community scripts from JaylyDev/ScriptAPI):
27
+
28
+ ```
29
+ optipack bench — test/corpus/jayly-pack (entry scripts/main.js, 100 ticks, level risky)
30
+ boundary crossing original optimized delta
31
+ boundary.getBlock 29 29 0%
32
+ boundary.getComponent 400 202 -49%
33
+ boundary.getPlayers 206 206 0%
34
+ boundary.location 20 10 -50%
35
+ TOTAL 655 447 -32%
36
+ transforms applied: 97
37
+ wall (indicative, Node/V8): original 6.5ms (65µs/tick), optimized 6.4ms (64µs/tick)
38
+ ```
39
+
40
+ Read it as: at `--level risky`, R1's cross-tick component cache halves
41
+ `getComponent` crossings and S7/B1 halve location reads, with identical
42
+ behavior verified. At `balanced`, crossings stay flat on this corpus because
43
+ its hot paths are call-dominated (caching those is exactly what R1 opts into).
44
+
45
+ Scenario files drive events between ticks (`test/corpus/jayly-scenario.json`
46
+ is an example): `{"reads": {...}, "state": {players: [{id: "p1"}]}, "steps":
47
+ [{"op": "fire", "event": "world.afterEvents.worldLoad", "payload": {}},
48
+ {"op": "fireMany", "event": "world.afterEvents.playerBreakBlock", "count": 30,
49
+ "payload": {...}}, {"op": "tick", "n": 0}]}`. Payloads may reference mock
50
+ objects with `{"__entity": "p1"}`, `{"__dimension": "minecraft:overworld"}`,
51
+ or `{"__block": {"location": {...}, "dimension": "..."}}`.
52
+
53
+ A real-world corpus of MIT-licensed community scripts is fetched with:
54
+
55
+ ```bash
56
+ npm run fetch-corpus
57
+ node bin/optipack.js bench test/corpus/jayly-pack --scenario test/corpus/jayly-scenario.json
58
+ ```
59
+
60
+ ## 2. In-game A/B with the script profiler — authoritative numbers
61
+
62
+ The numbers that matter run on the real engine: **QuickJS inside Minecraft**,
63
+ where the script watchdog and the server tick budget live. Bedrock ships a
64
+ built-in script profiler.
65
+
66
+ 1. **Build both packs.** The original is just a copy (optipack always
67
+ optimizes):
68
+
69
+ ```powershell
70
+ # Windows
71
+ robocopy MyPack MyPack-original /E
72
+ optipack MyPack MyPack-optimized --level balanced
73
+ ```
74
+
75
+ ```bash
76
+ # Linux/macOS
77
+ cp -r MyPack MyPack-original
78
+ optipack MyPack MyPack-optimized --level balanced
79
+ ```
80
+
81
+ 2. **Install one pack** into your `development_behavior_packs` folder and apply
82
+ it to a test world (enable the required experiments if the pack needs them).
83
+
84
+ 3. **Profile a consistent play pattern.** In the world, run:
85
+
86
+ ```
87
+ /script profiler start
88
+ ```
89
+
90
+ exercise the same actions for a fixed period (e.g. 60 seconds of the same
91
+ movement/interactions, or a repeatable redstone/tick-driven scenario), then:
92
+
93
+ ```
94
+ /script profiler stop
95
+ ```
96
+
97
+ The profiler writes the execution profile to the content log (visible in
98
+ the in-game content log screen and in the world's log files under
99
+ `content_log/`).
100
+
101
+ 4. **Repeat with the other pack**, same actions, same duration.
102
+
103
+ 5. **Compare.** What to look for in the two profiles:
104
+ - total script time per tick / per frame;
105
+ - time in your hottest handlers (the profiler reports per-function costs);
106
+ - the count of host API calls attributed to your entry points.
107
+
108
+ optipack's transforms reduce *calls into the host* (location reads,
109
+ component queries, dynamic properties) and *allocations per tick*
110
+ (closures, form objects, intermediate strings) — those show up as smaller
111
+ per-function times and fewer API-call entries, without any behavioral
112
+ difference (which the harness already proved).
113
+
114
+ 6. **Send the logs back.** If you're reporting results, include both content
115
+ log profile sections (original + optimized), the optipack report JSON from
116
+ the optimized build, and the level you used. The structured
117
+ `optipack bench --json` output pairs well with this.
118
+
119
+ ### Notes and caveats
120
+
121
+ - Enable the script watchdog thresholds or keep profiling sessions short —
122
+ a pathological pack can trip the watchdog during profiling; that itself is
123
+ useful signal.
124
+ - Profile at least two sessions per variant and compare like with like (same
125
+ world, same player count, same actions). Entity counts dominate host costs.
126
+ - `optipack bench` crossing counts are the cheapest regression signal for
127
+ CI; the in-game profiler is the ground truth for player-visible wins.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 optipack contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,137 @@
1
- # Temporary Holding Version
1
+ # optipack
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A build-time optimizer for Minecraft Bedrock behavior-pack scripts.**
4
+
5
+ Bedrock runs your scripts on QuickJS: an interpreter with no JIT, no escape analysis, and an expensive crossing every time script code touches the host (`player.location` allocates a fresh Vector3 *every read*). All of it shares one tick budget. optipack rewrites your `scripts/` folder with 16 semantics-preserving transforms that cut those crossings and per-tick allocations — and proves behavior is unchanged while doing it.
6
+
7
+ ```
8
+ npm install -g optipack
9
+ optipack MyPack MyPack-optimized
10
+ ```
11
+
12
+ ## Measured impact
13
+
14
+ Host-boundary crossings before → after (engine-independent: identical in Node and in-game), with behavior verified identical by the test harness:
15
+
16
+ | Benchmark | Level | Crossings | Top reduction |
17
+ |---|---|---|---|
18
+ | [example pack](example/) (300 ticks) | balanced | 237 → **177** (−25%) | `location` reads −50% |
19
+ | [JaylyDev/ScriptAPI](https://github.com/JaylyDev/ScriptAPI) corpus, 147 real scripts | balanced | 655 → 655 (0%)¹ | — |
20
+ | same corpus | risky | 655 → **447** (−32%) | `getComponent` −49%, `location` −50% |
21
+
22
+ ¹ *Honest number: this corpus's hot paths are call-dominated (`getComponent` every tick) — caching those is exactly the opt-in risky tier. Safe transforms only ever remove reads.*
23
+
24
+ Reproduce with `optipack bench` (see [BENCHMARKING.md](BENCHMARKING.md)), which also covers the in-game `/script profiler` A/B workflow for authoritative QuickJS numbers.
25
+
26
+ ## How it works
27
+
28
+ ```
29
+ optipack <inputPack> <outputPack> [--level safe|balanced|risky] [--diff] [--dry-run] [--only ids] [--except ids] [--report path]
30
+ ```
31
+
32
+ - Only `scripts/**/*.js` is transformed; the manifest, JSON, functions, and textures are copied byte-for-byte. `import` declarations are never touched.
33
+ - Every output file is re-parsed (valid ESM, ES2022-grade syntax — nothing is down-leveled, no bundling, no minification).
34
+ - Same input + config → byte-identical output. Exit codes: `0` ok · `1` usage · `2` parse failure (file+line) · `3` internal error.
35
+
36
+ ```bash
37
+ optipack bench <packDir> --level risky # A/B benchmark: original vs optimized (see BENCHMARKING.md)
38
+ ```
39
+
40
+ ## The 16 transforms
41
+
42
+ Passes run in order; each is an independent pass with an ID, a level, and strict bail conditions — when a site can't be proven safe, the pass **bails and records the reason** (line + reason in the report) instead of guessing.
43
+
44
+ <details>
45
+ <summary><strong>SAFE — provably behavior-identical (enabled at every level)</strong></summary>
46
+
47
+ | ID | Name | What it does |
48
+ |---|---|---|
49
+ | S1 | `const-fold` | Folds literal arithmetic, string concat, `typeof`/`!` on literals, fully-literal templates, logical expressions with literal left side. Never across identifiers or calls. |
50
+ | S2 | `dead-code` | Removes statements after `return`/`throw`/`break`/`continue`, constant branches, unused locals with pure initializers. |
51
+ | S3 | `bool-simplify` | `x ? true : false` → `!!x`; `if (x) return true; else return false;` → `return !!x;`; `x === true` → `x` when statically boolean. |
52
+ | S4 | `for-of-to-indexed` | `for (const v of it)` → indexed loop when `it` is *provably* an array (literal, `.map/.filter/.split`, `Object.entries`, …). Never on host returns, Map/Set, strings, generators. |
53
+ | S5 | `array-methods-to-loops` | **Hot paths only.** Inline-arrow `.forEach/.map/.filter/.some/.every/.find` → indexed loops with exact per-method return semantics. |
54
+ | S6 | `destructure-to-direct` | **Hot paths only.** `const { x, y } = obj` → `obj.x` reads at use sites (≤ 3 uses); defaults hoisted to an `if`. |
55
+ | S7 | `hoist-invariant-members` | **Hot paths only.** Hoists loop-invariant host chains out of barrier-free loops — `total += chest.location.y` reads `chest.location` once instead of N times. |
56
+
57
+ </details>
58
+
59
+ <details>
60
+ <summary><strong>BALANCED — per-site-checked assumptions (default level)</strong></summary>
61
+
62
+ | ID | Name | What it does |
63
+ |---|---|---|
64
+ | B1 | `cache-getter-chains` | Repeated identical host chains in barrier-free regions → one hoisted `const` (`p.location.x` twice = 2 crossings + 2 allocations → 0). |
65
+ | B2 | `closure-lift` | Arrows created inside hot functions (re-allocated every tick) lifted to module scope when they capture nothing / module bindings / const primitives (inlined) / never-reassigned locals via a module-level cell. |
66
+ | B3 | `string-build` | `s += x` in a hot loop → `parts.push()` + one `join("")` (QuickJS strings are immutable flat buffers: `+=` is O(n²), parts+join is O(n)). |
67
+ | B4 | `dynprop-hoist` | `entity.getDynamicProperty("k")` with a constant key, read in a hot loop, hoisted above the loop. |
68
+ | B5 | `null-chain-flatten` | After a proven guard (`if (!a) return;`), `a?.b` → `a.b` within the barrier-free region. |
69
+
70
+ </details>
71
+
72
+ <details>
73
+ <summary><strong>RISKY — opt-in (<code>--level risky</code> or config); every application warns, each has a documented failure mode</strong></summary>
74
+
75
+ | ID | Name | What it does | Failure mode |
76
+ |---|---|---|---|
77
+ | R1 | `cross-tick-cache` | Module-level WeakMap caches `getComponent("…")` and `entity.id`/`typeId` across ticks. | **Stale component views after death/dimension change.** |
78
+ | R2 | `json-fold` | `const x = JSON.parse('…')` hoisted to a module const when provably never mutated or passed to unknown code. | Shared mutable state if mutated anyway. |
79
+ | R3 | `try-catch-strip` | Removes `try { } catch { }` with no calls in the try body, in hot functions. | A pathological engine-level throw now crashes the script instead of being swallowed. |
80
+ | R4 | `getter-cache-cross-barrier` | B1, allowed to cross call barriers within one synchronous callback. | Cached values stale if an intervening call teleports/damages the entity. |
81
+
82
+ </details>
83
+
84
+ ## Pragmas
85
+
86
+ ```js
87
+ // optipack: off ← first comment in the file: skip the whole file
88
+ // optipack-disable const-fold ← line before a statement/function: skip that node (or `all`, or comma lists)
89
+ // optipack-hot ← line before a function: force-mark it hot
90
+ ```
91
+
92
+ ## Config
93
+
94
+ `optipack.config.json` (CLI flags win):
95
+
96
+ ```json
97
+ {
98
+ "level": "balanced",
99
+ "transforms": { "json-fold": true },
100
+ "hotPath": { "transitiveDepth": 1 },
101
+ "lint": true
102
+ }
103
+ ```
104
+
105
+ `transforms` can force any pass on/off regardless of level — that's how you run RISKY passes without `--level risky`.
106
+
107
+ ## What the report tells you
108
+
109
+ Each build writes `optipack-report.json`: transforms applied per file, every bail (line + reason — the tool never silently skips), lint findings (e.g. `getAllPlayers()` in tick handlers, residual allocations), and byte sizes.
110
+
111
+ ## Correctness: effects, not vibes
112
+
113
+ The test suite (68 tests) proves **effects-equivalence**: scripts run against a mock `@minecraft/server` with an effect recorder that logs every *mutating* call and registration in order — reads return scripted sequences and are free to be eliminated. Original and optimized must produce **identical effect traces** across simulated ticks and events, for every fixture and for the example pack. Mocks exist for all ten `@minecraft/*` runtime modules (functional for `server`/`server-ui`, generated from real type definitions for the rest).
114
+
115
+ ```bash
116
+ npm test # 68 tests
117
+ npm run fetch-corpus # pull the real-world benchmark corpus (JaylyDev/ScriptAPI, MIT)
118
+ npm run update-snapshot # regenerate the example report snapshot
119
+ ```
120
+
121
+ ## Limitations
122
+
123
+ - Same-file analysis only; cross-file helpers aren't auto-hot (annotate with `// optipack-hot`).
124
+ - No bundling/minification — pair with esbuild etc. if you use npm libraries like `@minecraft/math`.
125
+ - S5 assumes dense arrays (sparse-array provenance bails); S7's before-loop hoist evaluates once per loop entry (safe for live host objects, documented edge for null roots + zero-iteration loops).
126
+
127
+ ## Development
128
+
129
+ ```
130
+ npm install
131
+ npm test
132
+ npm run update-snapshot # example report snapshot
133
+ npm run update-mock-stubs # regenerate @minecraft/* stubs from real type defs
134
+ npm run fetch-corpus # real-world benchmark corpus
135
+ ```
136
+
137
+ MIT — see [LICENSE](LICENSE). Benchmark corpus scripts belong to their original authors (see `test/corpus/SOURCES.json`).
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main(process.argv).then(
5
+ (code) => {
6
+ process.exitCode = code;
7
+ },
8
+ (err) => {
9
+ console.error(err && err.stack ? err.stack : String(err));
10
+ process.exitCode = 3;
11
+ }
12
+ );
@@ -0,0 +1,27 @@
1
+ {
2
+ "format_version": 2,
3
+ "header": {
4
+ "name": "optipack example pack",
5
+ "description": "Tiny behavior pack exercising tick loops, event handlers, a runJob generator and dynamic properties. Used by the optipack test suite.",
6
+ "uuid": "6f2a1c8e-3d44-4b0a-9d21-77aa01bb2f01",
7
+ "version": [1, 0, 0],
8
+ "min_engine_version": [1, 21, 0]
9
+ },
10
+ "modules": [
11
+ {
12
+ "type": "data",
13
+ "uuid": "9c1b3f60-8e2d-4c6a-b1e4-52dd03cc2f02",
14
+ "version": [1, 0, 0]
15
+ },
16
+ {
17
+ "type": "script",
18
+ "language": "javascript",
19
+ "uuid": "b7d4e2a0-5f31-4a88-8c70-63ee04dd2f03",
20
+ "entry": "scripts/main.js",
21
+ "version": [1, 0, 0]
22
+ }
23
+ ],
24
+ "dependencies": [
25
+ { "module_name": "@minecraft/server", "version": "2.0.0" }
26
+ ]
27
+ }
@@ -0,0 +1,24 @@
1
+ // optipack example — drop tables and dynamic property loops.
2
+ // Constant JSON folded at build time when R2 (json-fold) is enabled.
3
+
4
+ const DROP_TABLE = JSON.parse('{"common":["apple","bread"],"rare":["diamond"]}');
5
+
6
+ export function buildDropSummary(count) {
7
+ const items = DROP_TABLE.common;
8
+ let out = '';
9
+ for (let i = 0; i < items.length && i < count; i++) {
10
+ out += items[i] + ';';
11
+ }
12
+ return out;
13
+ }
14
+
15
+ // Hot loop re-reading dynamic properties: the B4 pattern when called per-tick.
16
+ export function pollDropState(entity) {
17
+ let picked = 0;
18
+ for (let i = 0; i < 3; i++) {
19
+ if (entity.getDynamicProperty('optipack:drops') !== undefined) {
20
+ picked = picked + 1;
21
+ }
22
+ }
23
+ return picked;
24
+ }
@@ -0,0 +1,67 @@
1
+ // optipack example — main entry.
2
+ // Registers a tick loop, an event handler, and a runJob generator.
3
+ import { world, system } from '@minecraft/server';
4
+ import { toZoneLabel, trackBaseline } from './util.js';
5
+ import { buildDropSummary, pollDropState } from './drops.js';
6
+
7
+ world.afterEvents.worldLoad.subscribe(() => {
8
+ world.sendMessage('optipack example loaded');
9
+ system.runInterval(onTick, 20);
10
+ world.afterEvents.playerSpawn.subscribe(onPlayerSpawn);
11
+ });
12
+
13
+ // Per-tick handler: re-reads host getters every run, builds a string with +=,
14
+ // and iterates provable arrays with for...of + array methods.
15
+ function onTick() {
16
+ const players = world.getAllPlayers();
17
+ let summary = '';
18
+ for (const player of players) {
19
+ const label = toZoneLabel(player);
20
+ const dist = player.location.x + player.location.y;
21
+ summary += label + ' dist=' + dist;
22
+ player.setDynamicProperty('optipack:lastDist', dist);
23
+ // Per-player baseline: re-reads the same host chain every iteration (S7).
24
+ let baseline = 0;
25
+ for (let i = 0; i < 4; i++) {
26
+ baseline += player.location.y;
27
+ }
28
+ player.setDynamicProperty('optipack:baseline', baseline);
29
+ }
30
+ if (summary.length > 0) {
31
+ world.sendMessage(summary);
32
+ }
33
+
34
+ const zones = ['overworld', 'nether', 'the_end'].filter((z) => z.length > 4);
35
+ for (const z of zones) {
36
+ world.setDynamicProperty('optipack:zone:' + z, true);
37
+ }
38
+
39
+ system.runJob(chunkScan());
40
+
41
+ if (players.length > 0) {
42
+ pollDropState(players[0]);
43
+ }
44
+ }
45
+
46
+ // runJob iterator: every yield is a barrier for host-object caching.
47
+ function* chunkScan() {
48
+ for (let cx = 0; cx < 4; cx++) {
49
+ const before = world.getDynamicProperty('optipack:scan:' + cx);
50
+ yield;
51
+ world.setDynamicProperty('optipack:scan:' + cx, (before ?? 0) + 1);
52
+ }
53
+ }
54
+
55
+ function onPlayerSpawn(ev) {
56
+ const p = ev.player;
57
+ if (!p) return;
58
+ world.sendMessage('welcome ' + p.name);
59
+ const health = p.getComponent('minecraft:health');
60
+ if (health) {
61
+ p.setDynamicProperty('optipack:spawnHp', health.currentValue);
62
+ }
63
+ const drops = buildDropSummary(2);
64
+ p.setDynamicProperty('optipack:drops', drops);
65
+ // Hot helper defined in util.js — exercises transitive hot-path analysis.
66
+ trackBaseline(p);
67
+ }
@@ -0,0 +1,30 @@
1
+ // optipack example — utilities. Hot helpers called from the tick loop.
2
+ import { world } from '@minecraft/server';
3
+
4
+ export function toZoneLabel(player) {
5
+ const x = player.location.x;
6
+ const z = player.location.z;
7
+ return 'pos:' + Math.floor(x) + ',' + Math.floor(z);
8
+ }
9
+
10
+ export function clamp(v, lo, hi) {
11
+ if (v < lo) return lo;
12
+ if (v > hi) return hi;
13
+ return v;
14
+ }
15
+
16
+ // Called from onPlayerSpawn which is called from a subscribed handler, and
17
+ // re-reads the same host chain in a loop: the S7 pattern.
18
+ export function trackBaseline(player) {
19
+ let sum = 0;
20
+ for (let i = 0; i < 8; i++) {
21
+ sum += player.location.y;
22
+ }
23
+ world.setDynamicProperty('optipack:baseline', sum);
24
+ return sum;
25
+ }
26
+
27
+ // Bool-shape helpers (S3).
28
+ export function isAboveWater(player) {
29
+ return player.location.y > 62 ? true : false;
30
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "optipack",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "QuickJS-targeted micro-optimizing pre-processor for Minecraft Bedrock behavior-pack scripts",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "optipack": "bin/optipack.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "main": "src/pipeline.js",
14
+ "scripts": {
15
+ "test": "node --test \"test/*.test.mjs\"",
16
+ "update-snapshot": "node test/update-example-snapshot.mjs",
17
+ "fetch-corpus": "node test/fetch-corpus.mjs",
18
+ "update-mock-stubs": "node test/update-mock-stubs.mjs"
19
+ },
20
+ "dependencies": {
21
+ "@babel/generator": "^7.26.0",
22
+ "@babel/parser": "^7.26.0",
23
+ "@babel/traverse": "^7.26.0",
24
+ "@babel/types": "^7.26.0",
25
+ "commander": "^12.1.0"
26
+ },
27
+ "author": "lettucebecaesar",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/lettucebecaesar/optipack.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/lettucebecaesar/optipack/issues"
34
+ },
35
+ "homepage": "https://github.com/lettucebecaesar/optipack#readme"
36
+ }