feat: Obfuscate variables in plugin - #2755
Conversation
|
pkg.pr.new packages benchmark commit |
Bundle size comparison (
|
| 🟢 Decreased | ➖ Unchanged | 🔴 Increased (max 0.01%) | ❔ Unknown |
|---|---|---|---|
| 0 | 301 | 21 | 0 |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 260.06 kB ( |
| tgpu_initFromDevice.ts | 259.53 kB ( |
| tgpu_resolve.ts | 165.43 kB ( |
| tgpu_resolveWithContext.ts | 165.37 kB ( |
| tgpu_bindGroupLayout.ts | 69.27 kB ( |
| tgpu_mutableAccessor.ts | 66.27 kB ( |
| tgpu_accessor.ts | 66.26 kB ( |
| tgpu_privateVar.ts | 65.61 kB ( |
| tgpu_workgroupVar.ts | 65.60 kB ( |
| tgpu_const.ts | 64.85 kB ( |
| tgpu_fn.ts | 38.45 kB ( |
| tgpu_fragmentFn.ts | 38.45 kB ( |
| tgpu_vertexFn.ts | 38.27 kB ( |
| tgpu_computeFn.ts | 37.97 kB ( |
| tgpu_vertexLayout.ts | 27.08 kB ( |
| tgpu_comptime.ts | 14.77 kB ( |
| tgpu_unroll.ts | 1.66 kB ( |
| tgpu_slot.ts | 1.54 kB ( |
| tgpu_lazy.ts | 1.19 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.84, 1.66, 3.72, 5.01, 6.05, 10.37, 18.50, 20.48]
line [0.85, 1.60, 3.59, 5.63, 6.21, 10.12, 18.47, 22.17]
line [0.86, 1.70, 3.92, 5.16, 6.41, 9.95, 20.66, 22.33]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.32, 0.48, 0.66, 0.78, 1.05, 1.05, 1.28, 1.43]
line [0.31, 0.50, 0.65, 0.74, 1.07, 1.08, 1.25, 1.36]
line [0.29, 0.92, 0.67, 0.79, 1.03, 1.15, 1.35, 1.48]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.90, 1.88, 3.55, 5.57, 10.33, 22.59, 46.95, 95.18]
line [0.92, 1.90, 3.06, 5.73, 10.34, 22.43, 46.59, 95.60]
line [0.85, 1.93, 3.85, 5.69, 10.87, 23.51, 49.53, 100.90]
|
| declaredNames: string[]; | ||
| }; | ||
|
|
||
| export type Externals = Set<string>; |
There was a problem hiding this comment.
Less work swapping it for a map here than in unplugin, and it needs to become a map eventually
|
@pullfrog review |
There was a problem hiding this comment.
Important
The externalNames iteration in the obfuscator swaps the map's key and value. This works today only because tinyest-for-wgsl stores identical strings for both; it will silently produce incorrect WGSL if that invariant ever changes.
Reviewed changes — PR #2755 adds an experimental plugin-time obfuscation pass that renames parameters, local variables, and external bindings in the transpiled tinyest AST, plus the EXPERIMENTAL_obfuscate option across the Babel/Rollup/Bun plugin entry points, a new docs page, and supporting test updates.
- Added
packages/unplugin-typegpu/src/core/obfuscate.ts— AST-to-AST obfuscation ofparams,body, andexternalNames. - Changed
ExternalsfromSet<string>toMap<string, string>inpackages/tinyest-for-wgsl/src/types.tsand updated all consumers/test snapshots. - Wired
EXPERIMENTAL_obfuscateintounplugin-typegpuplugin options, with a guard that rejects enabling it alongsideautoNamingEnabled. - Added docs entry (
apps/typegpu-docs/.../minifying-shaders.mdx) and sidebar link. - Updated/added tests in
packages/tinyest-for-wgsl/tests/parsers.test.ts,packages/unplugin-typegpu/test/obfuscation.test.ts, and severaltypegpusnapshot tests.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
| const body = obf(ctx, fn.body); | ||
|
|
||
| const externalNames = new Map(); | ||
| fn.externalNames.forEach((key, value) => externalNames.set(ctx.obfuscator.obfuscate(key), value)); |
There was a problem hiding this comment.
Map.prototype.forEach passes (value, key), so the parameter names here are backwards: key receives the original external chain and value receives the AST identifier. Today tinyest-for-wgsl stores the same string for both (externalNames.set(externalChain, externalChain)), so the result is correct by accident. If the map is ever used to store distinct aliases (which the new Map<string, string> type now allows), the obfuscated external key will no longer match the identifiers in the AST body, and the resolved WGSL will reference the wrong value or fail to resolve.
Technical details
# externalNames key/value swap
## Affected sites
- packages/unplugin-typegpu/src/core/obfuscate.ts:78-79 — iterates `externalNames` with swapped key/value parameters
## Required outcome
- The obfuscated map must use `obfuscate(astIdentifier)` as the key and the original JS chain as the value.
## Suggested approach
Replace the `forEach` with a `for...of` to avoid the argument-order trap:
```ts
for (const [key, value] of fn.externalNames) {
externalNames.set(ctx.obfuscator.obfuscate(key), value);
}
</details>
```suggestion
const externalNames = new Map();
for (const [key, value] of fn.externalNames) {
externalNames.set(ctx.obfuscator.obfuscate(key), value);
}
return { params, body, externalNames };

Blocked by #2788
We obfuscate variables, externals and parameters. Struct props are left as-is (at least for now).
Testing: (!! currently examples fail, see #2788)
Further work: #2783, #2786.
Also, helpers like
fullScreenTrianglewill remain as is, they are implemented in raw wgsl.